Anika Vuurzoon
Created December 29, 2019 © GPL3+

Lego Mindstorms Puppy Training: Using Alexa Voice Control

Get the ultimate puppy owner experience: build a Lego Mindstorms puppy and teach the puppy the basic commands using Alexa.

IntermediateFull instructions provided6 hours933

Things used in this project

Hardware components

Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
With the Lego Education 45544 a robot with all the functionalities of the robot used in this project can be build.
×1
Amazon Echo
Amazon Alexa Amazon Echo
×1

Story

Read more

Code

puppy_training.py

Python
Python code to run on EV3
#!/usr/bin/env python3
# Copyright 2019 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
#
# You may not use this file except in compliance with the terms and conditions
# set forth in the accompanying LICENSE.TXT file.
#
# THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH
# RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
# THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.

import os
import sys
import logging
import json
import random
import threading
from enum import Enum

from PIL import Image
from agt import AlexaGadget
from time import sleep

import ev3dev.ev3 as ev3

# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(message)s')
logging.getLogger().addHandler(logging.StreamHandler(sys.stderr))
logger = logging.getLogger(__name__)


class Command(Enum):
    """
    The list of preset commands and their invocation variation.
    These variations correspond to the skill slot values.
    """
    SIT = ['sit', 'set']
    DOWN = ['down']
    STAND = ['stand']
    NO = ['no']


class EventName(Enum):
    """
    The list of custom event name sent from this gadget
    """
    POSE_FINISHED = "Pose_Finished"
    FEEDBACK = "Feedback"

class POSE:
    SIT = "sit"
    DOWN = "down"
    STAND = "stand"


class MindstormsGadget(AlexaGadget):
    """
    A Mindstorms gadget that can perform bi-directional interaction with an Alexa skill.
    """

    def __init__(self):
        """
        Performs Alexa Gadget initialization routines and ev3dev resource allocation.
        """
        super().__init__()

        # brick
        self.btn = ev3.Button()
        self.shut_down = False        
        self.lcd = ev3.Screen()
        self.sound = ev3.Sound()
        
        # sensors
        self.sensor_back = ev3.TouchSensor('in1'); assert self.sensor_back.connected #feels pet on back
        self.color = ev3.ColorSensor('in4');      assert self.color.connected  # measures color
    
        # motors
        self.lleg = ev3.LargeMotor('outD');  assert self.lleg.connected  # left leg
        self.rleg = ev3.LargeMotor('outA');  assert self.rleg.connected  # right leg
        self.head = ev3.MediumMotor('outC'); assert self.head.connected  # head

        # initialize current pose and command        
        self.pose = POSE.SIT
        self.command = ''    

        # calibrate all rotation sensors
        self.Calibrate()

        # arrays of poses connected to commands: initially puppy connects all poses to each command
        self.Command_Sit = ['sit', 'stand', 'down']
        self.Command_Stand = ['sit', 'stand', 'down']
        self.Command_Down = ['sit', 'stand', 'down']
        
        # start threads to take care of eyes and petting
        threading.Thread(target=self._eyes_thread, daemon=True).start()
        threading.Thread(target=self._pet_thread, daemon=True).start()

        


    def on_connected(self, device_addr):
        """
        Gadget connected to the paired Echo device.
        :param device_addr: the address of the device we connected to
        """
        logger.info("{} connected to Echo device".format(self.friendly_name))

    def on_disconnected(self, device_addr):
        """
        Gadget disconnected from the paired Echo device.
        :param device_addr: the address of the device we disconnected from
        """
        logger.info("{} disconnected from Echo device".format(self.friendly_name))

    def on_custom_mindstorms_gadget_control(self, directive):
        """
        Handles the Custom.Mindstorms.Gadget control directive.
        :param directive: the custom directive with the matching namespace and name
        """
        try:
            payload = json.loads(directive.payload.decode("utf-8"))
            print("Control payload: {}".format(payload), file=sys.stderr)
            control_type = payload["type"]

            if control_type == "command":
                # Expected params: [command]
                self._activate(payload["command"])
            if control_type == "restart":
                self.Command_Sit = ['sit', 'stand', 'down']
                self.Command_Stand = ['sit', 'stand', 'down']
                self.Command_Down = ['sit', 'stand', 'down']
                print("learned behaviour is resetted")


        except KeyError:
            print("Missing expected parameters: {}".format(directive), file=sys.stderr)

    def Calibrate(self):
        # calibrate left leg      
        self.lleg.run_direct(duty_cycle_sp=60)
        while self.lleg.is_stalled == False:
            sleep(0.1)

        self.lleg.reset(position_sp=0)
        self.lleg.run_direct(duty_cycle_sp=0)

        # calibrate right leg
        self.rleg.run_direct(duty_cycle_sp=60)
        while self.rleg.is_stalled == False:
            sleep(0.1)

        self.rleg.reset(position_sp=0)
        self.rleg.run_direct(duty_cycle_sp=0)

        # to stand pose
        self.Action_Sit2Stand()

        #calibrate head (manually)

        #wait for pet on back
        sleep(0.1)
        while self.sensor_back.is_pressed == 0:
            print(self.sensor_back.is_pressed)
            sleep(0.1)

        # wait untill sensor is released
        while self.sensor_back.is_pressed == 1:
            sleep(0.1)

        # until a pet on back check color sensor and change head position
        while self.sensor_back.is_pressed == 0:
            if self.color.color in [5]: # red move head up
                self.head.run_direct(duty_cycle_sp=-100)
                print("Moving Up, pet back if position OK")
            elif self.color.color in [3]: # green move head down
                self.head.run_direct(duty_cycle_sp=100)
                print("Moving Down, pet back if position OK")
            print(self.sensor_back.is_pressed)
            sleep(0.2)
            self.head.run_direct(duty_cycle_sp=0)

        #reset position head
        self.head.reset(position_sp=0)
        sleep(0.5)

        #go to sit positon
        self.Action_Stand2Sit()

        sleep(2)
        print("Calibration finished")

    def _activate(self, command):
        """
        Handles preset commands.
        :param command: the preset command
        :param speed: the speed if applicable
        """
        print("Command: ({})".format(command), file=sys.stderr)

        if command not in Command.NO.value:
            # perform command
            if command in Command.SIT.value:
                self.Command(POSE.SIT)

            if command in Command.DOWN.value:
                self.Command(POSE.DOWN)

            if command in Command.STAND.value:
                self.Command(POSE.STAND)
                
            self._send_event(EventName.POSE_FINISHED, {"NewPose": self.pose, "Command": self.command})
        else:
            # if command is no: give negative feedback
            self.Negative_Feedback()


    def _send_event(self, name: EventName, payload):
        """
        Sends a custom event to trigger a sentry action.
        :param name: the name of the custom event
        :param payload: the sentry JSON payload
        """
        self.send_custom_event('Custom.Mindstorms.Gadget', name.value, payload)

    def Command(self,command):
        self.command = command
        new_pose = self.pose

        if command in POSE.STAND:
            new_pose = random.choice(self.Command_Stand) # Pick random pose out of list stand
        elif command in POSE.SIT:
            new_pose = random.choice(self.Command_Sit)  # Pick random pose out of list sit
        elif command in POSE.DOWN:
            new_pose = random.choice(self.Command_Down) # Pick random pose out of list down

        # Perform action to new pose
        self.Action_to_Pose(new_pose)

        print("command: " + command + "new pose: " + new_pose)



    def Action_to_Pose(self,command):
        #Choose actions according to current pose and new pose
        if self.pose in POSE.SIT and command in POSE.STAND:
            self.Action_Sit2Stand()
        elif self.pose in POSE.STAND and command in POSE.DOWN:
            self.Action_Stand2Down()
        elif self.pose in POSE.DOWN and command in POSE.STAND:
            self.Action_Down2Stand()
        elif self.pose in POSE.STAND and command in POSE.SIT:
            self.Action_Stand2Sit()
        elif self.pose in POSE.SIT and command in POSE.DOWN:
            self.Action_Sit2Stand()
            self.Action_Stand2Down()
        elif self.pose in POSE.DOWN and command in POSE.SIT:
            self.Action_Down2Stand()
            self.Action_Stand2Sit()
        
    def Action_Sit2Stand(self):
        
        #To go to stand position from sit first a little push of power is necessary
        self.lleg.run_direct(duty_cycle_sp=-80)
        self.rleg.run_direct(duty_cycle_sp=-80)

        # wait until almost is stand position
        while self.lleg.position > -35:
            sleep(0.05)
            print(self.lleg.position_sp)
        
        #turn of power
        self.lleg.run_direct(duty_cycle_sp=0)
        self.rleg.run_direct(duty_cycle_sp=0)

        #goto stand position        
        self.lleg.run_to_abs_pos(position_sp=-65,speed_sp=-70,stop_action="brake")
        self.rleg.run_to_abs_pos(position_sp=-65,speed_sp=-70,stop_action="brake")

        # change current pose to stand
        self.pose = POSE.STAND

       
    def Action_Stand2Down(self):
        #goto down position
        self.lleg.run_to_abs_pos(position_sp=-140,speed_sp=-70,stop_action="brake")
        self.rleg.run_to_abs_pos(position_sp=-140,speed_sp=-70,stop_action="brake")

        # change current pose to down
        self.pose = POSE.DOWN

    def Action_Down2Stand(self):
        #goto stand position
        self.lleg.run_to_abs_pos(position_sp=-60,speed_sp=70,stop_action="brake")
        self.rleg.run_to_abs_pos(position_sp=-60,speed_sp=70,stop_action="brake")

        # change current pose to stand
        self.pose = POSE.STAND
            
    def Action_Stand2Sit(self):
        #goto sit position
        self.lleg.run_to_abs_pos(position_sp=0,speed_sp=70,stop_action="brake")
        self.rleg.run_to_abs_pos(position_sp=0,speed_sp=70,stop_action="brake")

        # wait untill position close enough
        while self.lleg.position < -20:
            sleep(0.05)

        # turn of motors
        self.lleg.run_direct(duty_cycle_sp=0)
        self.rleg.run_direct(duty_cycle_sp=0)

        # change current pose to sit
        self.pose = POSE.SIT

    def Move_Head(self):
        #turning head needs 100% power
        #turn head up
        self.head.run_direct(duty_cycle_sp=-100)
        sleep(2)
        self.head.run_direct(duty_cycle_sp=100)
        # turn head down to original position
        while self.head.position < -5:
            sleep(0.1)
        self.head.run_direct(duty_cycle_sp=0)
    
    def Negative_Feedback(self):
        # If negative feedback is received ('Alexa, no')
        # Remove the current pose from the list corresponding to the command
        # This way this pose will not be performed when the command is given
        if self.command == POSE.SIT:
            self.Command_Sit.remove(self.pose)
            print(self.Command_Sit)
        elif self.command == POSE.STAND:
            self.Command_Stand.remove(self.pose)
            print(self.Command_Stand)
        elif self.command == POSE.DOWN:
            self.Command_Down.remove(self.pose)
            print(self.Command_Down)

        #Send message to Alexa to indicate negative feedback received
        self._send_event(EventName.FEEDBACK, {'Feedback': "negative"})
    
    def Positive_Feedback(self):
        # If positive feedback is given (Pet on back)
        # Make the list corresponding to the current command equivalent to current pose
        # This way next time the command is given the current pose is performed
        if self.command == POSE.SIT:
            self.Command_Sit=[self.pose]
            print(self.Command_Sit)
        elif self.command == POSE.STAND:
            self.Command_Stand=[self.pose]
            print(self.Command_Stand)
        elif self.command == POSE.DOWN:
            self.Command_Down=[self.pose]
            print(self.Command_Down)

        # Send message to alexa to indicate positive feedback
        self._send_event(EventName.FEEDBACK, {'Feedback': "positive"})

    def _eyes_thread(self):
        #Load eyes looking left and right
        eyes_left = Image.open('/home/robot/eyes/Middle left.bmp')
        eyes_right = Image.open('/home/robot/eyes/Middle right.bmp')
        
        #while active change between looking left and right
        while True:
            self.lcd.image.paste(eyes_left, (0,0))
            self.lcd.update()
            sleep(1.5)
            self.lcd.image.paste(eyes_right, (0,0))
            self.lcd.update()
            sleep(1.5)

    def _pet_thread(self):
        # Wait until sensor on back is pressed
        while True:
            if self.sensor_back.is_pressed==1:
                self.Move_Head()
                self.Positive_Feedback()



if __name__ == '__main__':

    gadget = MindstormsGadget()

    # Set LCD font and turn off blinking LEDs
    os.system('setfont Lat7-Terminus12x6')

    # Startup sequence
    gadget.sound.play_song((('C4', 'e'), ('D4', 'e'), ('E5', 'q')))

    # Gadget main entry point
    gadget.main()

    # Shutdown sequence
    gadget.sound.play_song((('E5', 'e'), ('C4', 'e')))

index.js

JavaScript
/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * You may not use this file except in compliance with the terms and conditions 
 * set forth in the accompanying LICENSE.TXT file.
 *
 * THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH 
 * RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
*/

const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');

// The namespace of the custom directive to be sent by this skill
const NAMESPACE = 'Custom.Mindstorms.Gadget';

// The name of the custom directive to be sent this skill
const NAME_CONTROL = 'control';

const Positive_Behavior = ['Well done, praise your puppy by petting her back', 'Very good, she deserves a pet on her back', 'Good girl, that is worth a big cuddle']
const Negative_Behavior = ['Hm, still a lot to learn, correct your puppy by saying: no', 'That is not correct, tell her by saying: no', 'Almost there, correct her by saying: no']
const After_Positive_Feedback = ['Try a new command, you can teach the commands: sit, down, and stand', 'Try this command again to check if she really understands the command', 'Good job, try another command']
const After_Negative_Feedback = ['Try the same command again, maybe she understands it this time', 'Maybe another command will help her understand', 'Try again, she will get there'];

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchReq  uest';
    },
    handle: async function(handlerInput) {

        const request = handlerInput.requestEnvelope;
        const { apiEndpoint, apiAccessToken } = request.context.System;
        const apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
        if ((apiResponse.endpoints || []).length === 0) {
            return handlerInput.responseBuilder
            .speak("I couldn't find an EV3 Brick connected to this Echo device. Please check to make sure your EV3 Brick is connected, and try again.")
            .getResponse();
        }

        // Store the gadget endpointId to be used in this skill session
        const endpointId = apiResponse.endpoints[0].endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Set skill duration to 5 minutes (ten 30-seconds interval)
        Util.putSessionAttribute(handlerInput, 'duration',  10);

        // Set the token to track the event handler
        const token = handlerInput.requestEnvelope.request.requestId;
        Util.putSessionAttribute(handlerInput, 'token', token);

        let speechOutput = "Welcome to the puppy training, you can teach your dog the commands: sit, down and stand. Start by saying a command!";
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .addDirective(Util.buildStartEventHandler(token,25000, {}))
            .getResponse();
    }
};


// Construct and send a custom directive to the connected gadget with data from
// the SetCommandIntent.
const SetCommandIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetCommandIntent';
    },
    handle: function (handlerInput) {

        let command = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Command');
        if (!command) {
            return handlerInput.responseBuilder
                .speak("Your puppy does not understand this command. You can teach the commands: sit, down and stand")
                .withShouldEndSession(false)
                .getResponse();
        }

        const attributesManager = handlerInput.attributesManager;
        let endpointId = attributesManager.getSessionAttributes().endpointId || [];

        // Construct the directive with the payload containing the move parameters
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'command',
                command: command
            });


        return handlerInput.responseBuilder
            .addDirective(directive)
            .getResponse();
    }
};

// Construct and send a custom directive to the connected gadget with data from
// the SetCommandIntent.
const RestartIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RestartIntent';
    },
    handle: function (handlerInput) {

        const attributesManager = handlerInput.attributesManager;
        let endpointId = attributesManager.getSessionAttributes().endpointId || [];

        // Construct the directive with the payload containing the move parameters
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'restart'
            });

        let speechOutput = 'A fresh training has started, you have to teach all commands again';
        return handlerInput.responseBuilder
            .addDirective(directive)
            .speak(speechOutput)
            .getResponse();
    }
};

const EventsReceivedRequestHandler = {
    // Checks for a valid token and endpoint.
    canHandle(handlerInput) {
        let { request } = handlerInput.requestEnvelope;
        console.log('Request type: ' + Alexa.getRequestType(handlerInput.requestEnvelope));
        if (request.type !== 'CustomInterfaceController.EventsReceived') return false;

        const attributesManager = handlerInput.attributesManager;
        let sessionAttributes = attributesManager.getSessionAttributes();
        let customEvent = request.events[0];

        // Validate event token
        if (sessionAttributes.token !== request.token) {
            console.log("Event token doesn't match. Ignoring this event");
            return false;
        }

        // Validate endpoint
        let requestEndpoint = customEvent.endpoint.endpointId;
        if (requestEndpoint !== sessionAttributes.endpointId) {
            console.log("Event endpoint id doesn't match. Ignoring this event");
            return false;
        }
        return true;
    },
    handle(handlerInput) {

        console.log("== Received Custom Event ==");
        let customEvent = handlerInput.requestEnvelope.request.events[0];
        let payload = customEvent.payload;
        let name = customEvent.header.name;
        
        let speechOutput = ''
        
        if (name === 'Pose_Finished'){
            let NewPose = payload.NewPose;
            let Command = payload.Command;
            if (NewPose === Command){
                speechOutput = Positive_Behavior[Math.floor((Math.random() * 3))];
            }
            else{
                speechOutput = Negative_Behavior[Math.floor((Math.random() * 3))];
            }
        } else if (name === 'Feedback'){
            let feedback = payload.Feedback;
            if (feedback === 'negative'){
                speechOutput = After_Negative_Feedback[Math.floor((Math.random() * 3))];
            } else{
                speechOutput = After_Positive_Feedback[Math.floor((Math.random() * 3))];
            }
        }
        
        
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .getResponse();
    }
};
const ExpiredRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'CustomInterfaceController.Expired'
    },
    handle(handlerInput) {
        console.log("== Custom Event Expiration Input ==");

        // Set the token to track the event handler
        const token = handlerInput.requestEnvelope.request.requestId;
        Util.putSessionAttribute(handlerInput, 'token', token);

        const attributesManager = handlerInput.attributesManager;
        let duration = attributesManager.getSessionAttributes().duration || 0;
        if (duration > 0) {
            Util.putSessionAttribute(handlerInput, 'duration', --duration);

            // Extends skill session
            return handlerInput.responseBuilder
                .addDirective(Util.buildStartEventHandler(token, 25000, {}))
                .getResponse();
        }
        else {
            // End skill session
            return handlerInput.responseBuilder
                .speak("The puppy training ended. Goodbye.")
                .withShouldEndSession(true)
                .getResponse();
        }
    }
};

// The SkillBuilder acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        SetCommandIntentHandler,
        RestartIntentHandler,
        EventsReceivedRequestHandler,
        ExpiredRequestHandler,
        Common.HelpIntentHandler,
        Common.CancelAndStopIntentHandler,
        Common.SessionEndedRequestHandler,
    )
    .addRequestInterceptors(Common.RequestInterceptor)
    .addErrorHandlers(
        Common.ErrorHandler,
    )
    .lambda();

common.js

JavaScript
/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * You may not use this file except in compliance with the terms and conditions 
 * set forth in the accompanying LICENSE.TXT file.
 *
 * THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH 
 * RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
*/
'use strict'

const Alexa = require('ask-sdk-core');

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse();
    }
};

// The intent reflector is used for interaction model testing and debugging.
// It will simply repeat the intent the user said. You can create custom handlers
// for your intents by defining them above, then also adding them to the request
// handler chain below.
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt("I don't understand this command, try again")
            .getResponse();
    }
};

// Generic error handling to capture any syntax or routing errors. If you receive an error
// stating the request handler chain is not found, you have not implemented a handler for
// the intent being invoked or included it in the skill builder below.
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`~~~~ Error handled: ${error.stack}`);
        const speakOutput = `Sorry, I had trouble doing what you asked. Please try again.`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

// The request interceptor is used for request handling testing and debugging.
// It will simply log the request in raw json format before any processing is performed.
const RequestInterceptor = {
    process(handlerInput) {
        let { attributesManager, requestEnvelope } = handlerInput;
        let sessionAttributes = attributesManager.getSessionAttributes();

        // Log the request for debug purposes.
        console.log(`=====Request==${JSON.stringify(requestEnvelope)}`);
        console.log(`=========SessionAttributes==${JSON.stringify(sessionAttributes, null, 2)}`);
    }
};

module.exports = {
    HelpIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler,
    IntentReflectorHandler,
    ErrorHandler,
    RequestInterceptor
};

package.json

JSON
{
  "name": "agt-mindstorms",
  "version": "1.1.0",
  "description": "A sample skill demonstrating how to use AGT with Lego Mindstorms",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "ISC",
  "dependencies": {
    "ask-sdk-core": "^2.6.0",
    "ask-sdk-model": "^1.18.0",
    "aws-sdk": "^2.326.0",
    "request": "^2.81.0",
    "lodash": "^4.17.11"
  }
}

util.js

JavaScript
/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * You may not use this file except in compliance with the terms and conditions
 * set forth in the accompanying LICENSE.TXT file.
 *
 * THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH
 * RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
*/

'use strict';

const Https = require('https');
const AWS = require('aws-sdk');
const Escape = require('lodash/escape');

const s3SigV4Client = new AWS.S3({
    signatureVersion: 'v4'
});

/**
 * Get the authenticated URL to access the S3 Object. This URL expires after 60 seconds.
 * @param s3ObjectKey - the S3 object key
 * @returns {string} the pre-signed S3 URL
 */
exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) {

    const bucketName = process.env.S3_PERSISTENCE_BUCKET;
    return Escape(s3SigV4Client.getSignedUrl('getObject', {
        Bucket: bucketName,
        Key: s3ObjectKey,
        Expires: 60 // the Expires is capped for 1 minute
    }));
};

/**
 * Builds a directive to start the EventHandler.
 * @param token - a unique identifier to track the event handler
 * @param {number} timeout - the duration to wait before sending back the expiration
 * payload to the skill.
 * @param payload - the expiration json payload
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/receive-custom-event-from-gadget.html#start}
 */
exports.buildStartEventHandler = function (token, timeout = 30000, payload)  {
    return {
        type: "CustomInterfaceController.StartEventHandler",
        token: token,
        expiration : {
            durationInMilliseconds: timeout,
            expirationPayload: payload
        }
    };
};

/**
 *
 * Builds a directive to stops the active event handler.
 * The event handler is identified by the cached token in the session attribute.
 * @param {string} handlerInput - the context from Alexa Service
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/receive-custom-event-from-gadget.html#stop}
 */
exports.buildStopEventHandlerDirective = function (handlerInput) {

    let token = handlerInput.attributesManager.getSessionAttributes().token || '';
    return {
        "type": "CustomInterfaceController.StopEventHandler",
        "token": token
    }
};

/**
 * Build a custom directive payload to the gadget with the specified endpointId
 * @param {string} endpointId - the gadget endpoint Id
 * @param {string} namespace - the namespace of the skill
 * @param {string} name - the name of the skill within the scope of this namespace
 * @param {object} payload - the payload data
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/send-gadget-custom-directive-from-skill.html#respond}
 */
exports.build = function (endpointId, namespace, name, payload) {
    // Construct the custom directive that needs to be sent
    // Gadget should declare the capabilities in the discovery response to
    // receive the directives under the following namespace.
    return {
        type: 'CustomInterfaceController.SendDirective',
        header: {
            name: name,
            namespace: namespace
        },
        endpoint: {
            endpointId: endpointId
        },
        payload
    };
};

/**
 * A convenience routine to add the a key-value pair to the session attribute.
 * @param handlerInput - the context from Alexa Service
 * @param key - the key to be added
 * @param value - the value be added
 */
exports.putSessionAttribute = function(handlerInput, key, value) {
    const attributesManager = handlerInput.attributesManager;
    let sessionAttributes = attributesManager.getSessionAttributes();
    sessionAttributes[key] = value;
    attributesManager.setSessionAttributes(sessionAttributes);
};


/**
 * To get a list of all the gadgets that meet these conditions,
 * Call the Endpoint Enumeration API with the apiEndpoint and apiAccessToken to
 * retrieve the list of all connected gadgets.
 *
 * @param {string} apiEndpoint - the Endpoint API url
 * @param {string} apiAccessToken  - the token from the session object in the Alexa request
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/send-gadget-custom-directive-from-skill.html#call-endpoint-enumeration-api}
 */
exports.getConnectedEndpoints = function(apiEndpoint, apiAccessToken) {

    // The preceding https:// need to be stripped off before making the call
    apiEndpoint = (apiEndpoint || '').replace('https://', '');

    return new Promise(((resolve, reject) => {

        const options = {
            host: apiEndpoint,
            path: '/v1/endpoints',
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + apiAccessToken
            }
        };

        const request = Https.request(options, (response) => {
            response.setEncoding('utf8');
            let returnData = '';
            response.on('data', (chunk) => {
                returnData += chunk;
            });

            response.on('end', () => {
                resolve(JSON.parse(returnData));
            });

            response.on('error', (error) => {
                reject(error);
            });
        });
        request.end();
    }));
};

Credits

Anika Vuurzoon

Anika Vuurzoon

1 project • 12 followers
I build LEGO robotic animals as well as bringing life to LEGO Friends and LEGO Elves sets with the incorporation of LEGO MINDSTORMS.

Comments