Hackster is hosting Hackster Holidays, Ep. 4: Livestream & Giveaway Drawing. Start streaming on Wednesday!Stream Hackster Holidays, Ep. 4 on Wednesday!
Nard Strijbosch
Published © GPL3+

Color Game: Help Alexa the Lego Robot Find the Correct Color

Combine the power of Lego Mindstorms and Alexa: develop a platform that allows fun and interactive games by voice commanding a Lego robot

AdvancedFull instructions provided20 hours682

Things used in this project

Hardware components

Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
Lego Mindstorms: EV3 Brick, 2 motors and color sensor(s)
×1
Amazon Echo
Amazon Alexa Amazon Echo
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit

Story

Read more

Schematics

Field

Exampe field. Print to scale 60 cm x 108 cm

Code

alexa_color_game.py

Python
Python program running on Mindstorms EV3
# 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 time
import logging
import json
import random
import threading

from enum import Enum
from agt import AlexaGadget

import ev3dev.ev3 as ev3
from time import sleep

# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO)


class Direction(Enum):
    """
    The list of directional commands and their variations.
    These variations correspond to the skill slot values.
    """
    FORWARD = ['forward', 'forwards', 'go forward']
    LEFT = ['left', 'go left']
    RIGHT = ['right', 'go right']

class EventName(Enum):
    """
    The list of custom event name sent from this gadget
    """
    ARRIVED = "Arrived"
    START = "Start"

class Colors():

    LINE = [1]          # Black
    GOAL = [3,4,5,7]    # Red, Yellow, Green, Brown
    FIELD = [2]         # Blue
    OUTSIDE = [6]       # White


class MindstormsGadget(AlexaGadget):
    """
    A Mindstorms gadget that performs movement based on voice commands.
    Two types of commands are supported, directional movement and preset.
    """

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

        # Ev3dev initialization
        self.leds = ev3.Leds()
        self.shut_down = False
        
        # sensors
        self.lcs = ev3.ColorSensor('in2');      assert self.lcs.connected  
        self.rcs = ev3.ColorSensor('in3');      assert self.rcs.connected 

        self.lcs.mode = 'COL-COLOR'  # measure color
        self.rcs.mode = 'COL-COLOR'  # measure color
    
        # motors
        self.lm = ev3.LargeMotor('outB');  assert self.lm.connected  # left motor
        self.rm = ev3.LargeMotor('outC');  assert self.rm.connected  # right motor

        self.goal_color = 0
        self.current_color = 0

    def on_connected(self, device_addr):
        """
        Gadget connected to the paired Echo device.
        :param device_addr: the address of the device we connected to
        """
        gadget.leds.set_color(ev3.Leds.LEFT, ev3.Leds.GREEN)
        gadget.leds.set_color(ev3.Leds.RIGHT, ev3.Leds.GREEN)
        print("{} 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
        """
        
        gadget.leds.set_color(ev3.Leds.LEFT, ev3.Leds.BLACK)
        gadget.leds.set_color(ev3.Leds.RIGHT, ev3.Leds.BLACK)
        print("{} 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))
            control_type = payload["type"]
            if control_type == "move":

                # Expected params: [direction]
                self._move(payload["direction"])

            if control_type == "goal_color":
                # Expected params:  [goal_color]
                self._goal_color(payload["goal_color"])

        except KeyError:
            print("Missing expected parameters: {}".format(directive))

    def _goal_color(self, goal_color):
        # set goal color of current game
        self.goal_color = goal_color
        print('Goal Color Set to {}'.format(goal_color))

    def _move(self, direction):
        """
        Handles move commands from the directive.
        Right and left movement can under or over turn depending on the surface type.
        :param direction: the move direction
        :param duration: the duration in seconds
        :param speed: the speed percentage as an integer
        :param is_blocking: if set, motor run until duration expired before accepting another command
        """
        print("Move command: ({})".format(direction))
        if direction in Direction.FORWARD.value:
            print('Moving forward')
            self.Forward()

        if direction in Direction.RIGHT.value:
            print('Turning right')
            self.Turn(False)

        if direction in Direction.LEFT.value:
            print('Turning left')
            self.Turn(True)

    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 Forward(self):
        speed = 360/1.5     # deg/sec
        dt = 100            # milliseconds
        stop_action = "coast"  
        previous_color = False #For robustness a color must be observed two samples in a row 

        #First move outside square
        while self.lcs.value() in Colors.GOAL and self.rcs.value() in Colors.GOAL:
            self.lm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)
            self.rm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)

        #Follow line until next square
        while not ((self.lcs.value() in Colors.GOAL or self.rcs.value() in Colors.GOAL) and previous_color) and not self.lcs.value() in Colors.OUTSIDE and not self.rcs.value() in Colors.OUTSIDE:
            
            if self.lcs.value() in Colors.FIELD and self.rcs.value() in Colors.LINE:
                self.lm.run_timed(time_sp=dt,speed_sp=speed*2,stop_action=stop_action)
                self.rm.run_timed(time_sp=dt,speed_sp=speed*1,stop_action=stop_action) 
            elif self.rcs.value() in Colors.FIELD and self.lcs.value() in Colors.LINE:
                self.lm.run_timed(time_sp=dt,speed_sp=speed*1,stop_action=stop_action)
                self.rm.run_timed(time_sp=dt,speed_sp=speed*2,stop_action=stop_action)
            else:
                self.lm.run_timed(time_sp=dt,speed_sp=speed*1,stop_action=stop_action)
                self.rm.run_timed(time_sp=dt,speed_sp=speed*1,stop_action=stop_action)

            if self.lcs.value() in Colors.GOAL or self.rcs.value() in Colors.GOAL:
                previous_color = True
            else:
                if previous_color == True: #False color observation  
                    previous_color = False
            sleep(dt / 1000)

        #Move Onto Middle square
        if self.lcs.value() in Colors.OUTSIDE or self.rcs.value() in Colors.OUTSIDE: #If outside field observed
            self._send_event(EventName.ARRIVED, {'color': 6, 'previous_move':  0})
        elif (self.lcs.value() in Colors.GOAL or self.rcs.value() in Colors.GOAL):
            
            # Set color of square
            color = 0
            if self.lcs.value() in [3,4,5,7]:
                color = self.lcs.value()
            elif self.rcs.value() in [3,4,5,7]:
                color = self.rcs.value()

            # Move straight until sensors of square
            while self.lcs.value() in Colors.GOAL or self.rcs.value() in Colors.GOAL:
                self.lm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)
                self.rm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)

                sleep(dt / 1000)

            # Move a tiny bit more forward to get center of the robot on the center of the square
            stop_action="brake"
            dt=300
            self.lm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)
            self.rm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)
            sleep(dt/1000)

            # Square Reached
            print("Square Reached. Sending event to skill")
            self._send_event(EventName.ARRIVED, {'color': color, 'previous_move':  0})
            self.current_color=color

            # If square color is Goal Color: do a dance
            if color == self.goal_color:
                self.Dance()


    def Turn(self,direction = True):
        speed = 360/3
        dt = 1500
        stop_action= "brake"

        if direction==True: # Turn Right
            self.lm.run_timed(time_sp=dt,speed_sp=-speed,stop_action=stop_action)
            self.rm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)
        else: # Turn Left
            self.lm.run_timed(time_sp=dt,speed_sp=speed,stop_action=stop_action)
            self.rm.run_timed(time_sp=dt,speed_sp=-speed,stop_action=stop_action)

        sleep(dt/1000)

        print("Turn Complete. Sending event to skill")
        self._send_event(EventName.ARRIVED, {'color': self.current_color, 'previous_move':  1})
    
    def Dance(self):
        speed = 360/2
        dt = 250
        stop_action= "brake"
    	
        # Turn back and forth a few times
        for i in range(1,5,1):
            self.lm.run_timed(time_sp=dt,speed_sp=((-1)**i)*speed,stop_action=stop_action)
            self.rm.run_timed(time_sp=dt,speed_sp=-((-1)**i)*speed,stop_action=stop_action)
            
            sleep(dt/1000)


if __name__ == '__main__':

    # Startup sequence
    gadget = MindstormsGadget()
    #gadget.sound.play_song((('C4', 'e'), ('D4', 'e'), ('E5', 'q')))
    gadget.leds.set_color(ev3.Leds.LEFT, ev3.Leds.GREEN)
    gadget.leds.set_color(ev3.Leds.RIGHT, ev3.Leds.GREEN)

    # Gadget main entry point
    gadget.main()

    # Shutdown sequence
    #gadget.sound.play_song((('E5', 'e'), ('C4', 'e')))
    gadget.leds.set_color(ev3.Leds.LEFT, ev3.Leds.BLACK)
    gadget.leds.set_color(ev3.Leds.RIGHT, ev3.Leds.BLACK)

model.json

JSON
JSON file
{
  "interactionModel": {
      "languageModel": {
          "invocationName": "color game",
          "intents": [
              {
                  "name": "AMAZON.CancelIntent",
                  "samples": []
              },
              {
                  "name": "AMAZON.HelpIntent",
                  "samples": []
              },
              {
                  "name": "AMAZON.StopIntent",
                  "samples": []
              },
              {
                  "name": "AMAZON.NavigateHomeIntent",
                  "samples": []
              },
              {
                  "name": "MoveIntent",
                  "slots": [
                      {
                          "name": "Direction",
                          "type": "DirectionType"
                      }
                  ],
                  "samples": [
                      "turn {Direction}",
                      "move {Direction}",
                      "{Direction} now"
                  ]
              },
              {
                  "name": "RestartIntent",
                  "slots": [],
                  "samples": [
                      "Open Color Game",
                      "Restart Color Game",
                      "New Color Game"
                  ]
              }
          ],
          "types": [
              {
                  "name": "DirectionType",
                  "values": [
                      {
                          "name": {
                              "value": "go forward"
                          }
                      },
                      {
                          "name": {
                              "value": "go right"
                          }
                      },
                      {
                          "name": {
                              "value": "go left"
                          }
                      },
                      {
                          "name": {
                              "value": "right"
                          }
                      },
                      {
                          "name": {
                              "value": "left"
                          }
                      },
                      {
                          "name": {
                              "value": "forwards"
                          }
                      },
                      {
                          "name": {
                              "value": "forward"
                          }
                      }
                  ]
              }
          ]
      }
  }
}

common.js

JavaScript
common file
/*
 * 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
};

index.js

JavaScript
index file
/*
 * 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.
*/

// This skill sample demonstrates how to send directives and receive events from an Echo connected gadget.
// This skill uses the Alexa Skills Kit SDK (v2). Please visit https://alexa.design/cookbook for additional
// examples on implementing slots, dialog management, session persistence, api calls, and more.

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

// The audio tag to include WIN and GAMEOVER music
const WIN_MUSIC = '<audio src="soundbank://soundlibrary/video_tunes/video_tunes_10"/>';
const GAMEOVER_MUSIC = '<audio src="soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_negative_response_02"/>';

// 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 LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    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);

        // Define goal color (random selection out of 3 4 5)
        const goal_color = Math.floor(Math.random() * 3)+3;
        Util.putSessionAttribute(handlerInput, 'goal_color', goal_color);
        
        
        let speechOutput = "There is no goal color defined"
        if (goal_color === 3){
            speechOutput = "Hello, can you bring me to green";
        } else if (goal_color === 4){
            speechOutput = "Hello, can you bring me to yellow";
        } else if (goal_color === 5){
            speechOutput = "Hello, can you bring me to red";
        }
        
        // Make directive that sends goal color to EV3
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'goal_color',
                goal_color: goal_color
            });
        
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .addDirective(directive)
            .addDirective(Util.buildStartEventHandler(token,60000, {}))
            .withShouldEndSession(false)
            .reprompt("What should I do next?")
            .getResponse();
    }
};

// On restart request
const RestartIntentHandler = {
    canHandle(handlerInput) {
         return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RestartIntent';
    },
    handle: function (handlerInput) {
        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];

        const goal_color = Math.floor(Math.random() * 3)+3;
        
        Util.putSessionAttribute(handlerInput, 'goal_color', goal_color);
        
        let speechOutput = "No goal color defined"
        if (goal_color === 3){
            speechOutput = "Hello, can you bring me to green";
        } else if (goal_color === 4){
            speechOutput = "Hello, can you bring me to yellow";
        } else if (goal_color === 5){
            speechOutput = "Hello, can you bring me to red";
        }
        
        // Make directive that sends goal color to EV3
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'goal_color',
                goal_color: goal_color
            });
        
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .addDirective(directive)
            .reprompt("Awaiting commands")
            .withShouldEndSession(false)
            .getResponse();
    }
};

// On Move request
const MoveIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const direction = Alexa.getSlotValue(request, 'Direction');
        
        // If direction is not recognized correctly inform user        
        if (direction !== 'forward' && direction !== 'forwards' && direction !== 'go forward' && direction !== 'left'  && direction !== 'go left' && direction !== 'right' && direction !== 'go right'){
            return handlerInput.responseBuilder
                .speak("Can you repeat that?")
                .reprompt("You can only use the commands: forward, left and right") // Politely let user know which commands to use
                .getResponse();
        }


        // Get data from session attribute
        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];

        // Construct directive for move command 
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'move',
                direction: direction
            });

        // Inform user which command is activated
        let speechOutput = `Moving ${direction}`
        
        if (direction === 'left'  || direction === 'right'){
            speechOutput = `Turning ${direction}`
        }

        return handlerInput.responseBuilder
            .speak(speechOutput)
            .addDirective(directive)
            .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 === 'Arrived') {
            const attributesManager = handlerInput.attributesManager;
        const goal_color = attributesManager.getSessionAttributes().goal_color || 1;
        
            let color = parseInt(payload.color);
            let previous_move = parseInt(payload.previous_move)
        
            speechOutput = "Color not recognized" + previous_move;
            if (color===5){
                if (goal_color === 5 && previous_move === 0){
                    speechOutput = WIN_MUSIC + "Thank you, I reached red"
                } else {
                    if (previous_move === 0){
                        speechOutput = "I am on red, what should I do next?"
                    } else {
                        speechOutput = "What should I do next?"
                    }
                }
            } else if (color === 4){
                if (goal_color === 4 && previous_move === 0){
                    speechOutput = WIN_MUSIC + "Thank you, I reached yellow"
                } else {
                    if (previous_move === 0){
                        speechOutput = "I am on yellow, what should I do next?"
                    } else {
                        speechOutput = "What should I do next?"
                    }
                }
            } else if (color === 3){
                if (goal_color === 3 && previous_move === 0){
                    speechOutput = WIN_MUSIC + "Thank you, I reached green"
                } else {
                    if (previous_move === 0){
                        speechOutput = "I am on green, what should I do next?"
                    } else {
                        speechOutput = "What should I do next?"
                    }
                }
            } else if (color === 6){
                speechOutput = GAMEOVER_MUSIC + "Game over. Please put me back on a colored dot and restart"
            }
            return handlerInput.responseBuilder
                .speak(speechOutput, "REPLACE_ALL")
                .reprompt("What should I do next?")
                .getResponse();

        } else if (name === 'Start') {
            speechOutput = "Color not recognized";
            let color = parseInt(payload.color);
            if (color===5){
                speechOutput = "Bring me to red"
            } else if (color === 4){
                speechOutput = "Bring me to yellow";
            } else if (color === 3){
                speechOutput = "Bring me to green";
            }
            return handlerInput.responseBuilder
                .speak(speechOutput, "REPLACE_ALL")
                .reprompt("What should I do next?")
                .getResponse();

        } else {
            speechOutput = "Event not recognized. Awaiting new command.";
        }
        return handlerInput.responseBuilder
            .speak(speechOutput, "REPLACE_ALL")
            .reprompt("Awaiting commands")
            .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, 60000, {}))
                .getResponse();
        }
        else {
            // End skill session
            return handlerInput.responseBuilder
                .speak("Game ended. Goodbye.")
                .withShouldEndSession(true)
                .getResponse();
        }
    }
};

exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        MoveIntentHandler,
        RestartIntentHandler,
        EventsReceivedRequestHandler,
        ExpiredRequestHandler,
        Common.HelpIntentHandler,
        Common.CancelAndStopIntentHandler,
        Common.SessionEndedRequestHandler,
        Common.IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
    )
    .addRequestInterceptors(Common.RequestInterceptor)
    .addErrorHandlers(
        Common.ErrorHandler,
    )
    .lambda();

package.json

JSON
Package file
{
  "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
util file
/*
 * 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 JSON payload 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 handlerInput 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');
            response.shouldEndSession=true;
            let returnData = '';
            response.on('data', (chunk) => {
                returnData += chunk;
            });

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

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

Credits

Nard Strijbosch

Nard Strijbosch

1 project • 6 followers

Comments