Hackster is hosting Hackster Holidays, Ep. 4: Livestream & Giveaway Drawing. Start streaming on Wednesday!Stream Hackster Holidays, Ep. 4 on Wednesday!
Dax Haslam
Created December 27, 2019

LEGO Bombsquad

A LEGO Bomb that can only be deactivated by a brave soul through a series of interactions with LEGO contraptions and Alexa guiding them.

IntermediateFull instructions provided10 hours101

Things used in this project

Story

Read more

Custom parts and enclosures

LEGO Studio V2 Build Instructions

This will give detailed instructions on the LEGO build

LEGO BombSquad Build PDF

Here is a step by step build instruction. May need to reference the .io file for better views.

Code

bombsquad.py

Python
This is the file that you will run on the EV3 to provide interaction with Alexa Skill
# 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

from ev3dev2.led import Leds
from ev3dev2.sound import Sound
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, LargeMotor, MediumMotor, SpeedRPM
from ev3dev2.sensor.lego import InfraredSensor, TouchSensor, ColorSensor, GyroSensor, UltrasonicSensor

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



class EventName(Enum):
    """
    The list of custom event name sent from this gadget
    """
    BUTTON = "Button"    
    COLOR = "Color"
    SPEECH = "Speech"
    ULTRASONIC_DISTANCE = "UltrasonicDistance"
    IR_DISTANCE = "InfraRedDistance"
    INIT = "Init"


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__()
        self.session = None

        self.sound = Sound()
        self.leds = Leds()
        self.touch = TouchSensor()
        self.ir = InfraredSensor()        
        self.color = ColorSensor()
        self.distance = UltrasonicSensor()

        self.timeStart = None
        self.numberOfSeconds = 60

        
        self.fusecover = MediumMotor(OUTPUT_B)

        #Thread control variables
        self._kill_distance_thread = False
        self._kill_proximity_thread = False
        self._kill_touch_thread = False
        
    def on_connected(self, device_addr):
        """
        Gadget connected to the paired Echo device.
        :param device_addr: the address of the device we connected to
        """
        self.leds.set_color("LEFT", "BLACK")
        self.leds.set_color("RIGHT", "BLACK")
        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
        """
        self.leds.set_color("LEFT", "BLACK")
        self.leds.set_color("RIGHT", "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"]
            control_command = payload["command"]
            
            
            
            if control_type == "query" and control_command == "color":
                time.sleep(6)
                if (self.color.color_name in ["Green", "Red", "Yellow"]):
                    print(self.color.color_name)
                    self._send_event(EventName.COLOR, {'colorSeen': self.color.color_name})
                else:
                    self._send_event(EventName.SPEECH, {'speechOut': 'Careful!  You need to make sure the laser cutter is pointed directly at color of wire you want to cut when you activate the laser cutter'})

            if control_type == "query" and control_command == "init":
                initObject = {'colorSeen': self.color.color_name, 'proximity': self.ir.distance(), 'ultrasonic': self.distance.distance_centimeters}
                print("Init Object: " + json.dumps(initObject))

                self._send_event(EventName.INIT, initObject)
            
            if control_type == "session":
                #the correct wire was cut expose the fuse
                #if (self.session and (self.session['wireCut'] != payload["session"]["wireCut"])):
                #    print("Wire was cut")
                
                if (self.session and self.session['bombExploded']):
                    print("Explosion was detected")
                    self._kill_distance_thread = True
                    self._kill_proximity_thread = True
                    self._kill_touch_thread = True
                    self._kill_timer_thread = True
                    

                self.session = payload["session"]
                print("Session: {}".format(self.session))

                if (payload["newSession"]):                    
                    self.leds.set_color("LEFT", self.session['colorOfWire'].upper(), 1)
                    self.leds.set_color("RIGHT", self.session['colorOfWire'].upper(), 1)



                    self._kill_distance_thread = True
                    self._kill_proximity_thread = True
                    self._kill_touch_thread = True
                    self._kill_timer_thread = True
                    #wait for threads to die and talking to finish
                    time.sleep(8)  

                    
                    self.timeStart = int(time.time())
                    self._kill_distance_thread = False
                    self._kill_touch_thread = False                    
                    self._kill_proximity_thread = False   
                    self._kill_timer_thread = False    
                    threading.Thread(target=self._touch_thread, daemon=True).start()                    
                    threading.Thread(target=self._proximity_thread, daemon=True).start()                    
                    threading.Thread(target=self._distance_thread, daemon=True).start()
                    threading.Thread(target=self._timer_thread, daemon=True).start()


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


    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
        """ 
        print("I sent an event of type: " + name.value + " : " + json.dumps(payload))       
        self.send_custom_event('Custom.Mindstorms.Gadget', name.value, payload)

    def _proximity_thread(self):
        currentIRDistance = None
        currentIRDistance = self.ir.distance()
        while not self._kill_proximity_thread:
            #print("IR Distance: " + str(self.ir.distance()))
            if (currentIRDistance != self.ir.distance() and not self.session['bombDiffused']):
                currentIRDistance = self.ir.distance()
                print("IR Distance: " + str(self.ir.distance()))            
                if (currentIRDistance == None):
                    self._send_event(EventName.IR_DISTANCE, {'distance': 1000, 'disconnected':'true', 'timeLeft': self.secondsLeft()})
                else:
                    self._send_event(EventName.IR_DISTANCE, {'distance': currentIRDistance, 'disconnected':'false', 'timeLeft': self.secondsLeft()})
            time.sleep(1)
            print("Proximity Thread is polling")
        print("Exiting Proximity Thread")

    def _timer_thread(self):
        while not self._kill_timer_thread:
            while self.secondsLeft() > 0:
                if self.session['bombDiffused'] or self.session['bombExploded']:
                    break                
                elif self.secondsLeft() < 10:
                    self.fusecover.on_for_seconds(SpeedRPM(125), 2)                
                elif self.session['wireCut']:
                    self.fusecover.on_for_seconds(SpeedRPM(100), 2)                
                elif self.session['remoteDetonationBlocked']:
                    self.fusecover.on_for_seconds(SpeedRPM(75), 2)
                else:
                    self.fusecover.on_for_seconds(SpeedRPM(50), 2)

                if (self.secondsLeft() < 10):
                    self._send_event(EventName.BUTTON, {'timeLeft': self.secondsLeft()})
                print("Motor Thread is running" + str(self.secondsLeft()))
            self._send_event(EventName.BUTTON, {'timeLeft': self.secondsLeft()})
            self._kill_distance_thread = True
            self._kill_proximity_thread = True
            self._kill_touch_thread = True
            self._kill_timer_thread = True
            print("Time is Up")
            break
        print("Exiting Motor Thread")

    def secondsLeft(self):
        return max(0,self.numberOfSeconds - (int(time.time()) - self.timeStart))

    def _touch_thread(self):
        """
        Performs random movement when patrol mode is activated.
        """
        while not self._kill_touch_thread:
            try:
                while not self.touch.value():                
                    time.sleep(1)
                while self.touch.value():
                    print("Button Pushed")
                self._send_event(EventName.BUTTON, {'timeLeft': self.secondsLeft()})
                time.sleep(1)
                print("Touch Thread is polling")
            except:
                print("Caught an exception")
        print("Exiting Touch Thread")

    def _distance_thread(self):
        """
        Performs random movement when patrol mode is activated.
        """
        #Set a default distance
        currentDistance = 1000
        while not self._kill_distance_thread:
            measurement = self.distance.distance_centimeters
            #measurement = self.session['distanceBlock']
            if measurement and measurement > 0:
                if round(measurement) != currentDistance:
                    currentDistance = round(measurement)
                    print("UltraSonic Distance: " + str(currentDistance))
                    if (currentDistance and not self.session['remoteDetonationBlocked']):
                        self._send_event(EventName.ULTRASONIC_DISTANCE, {'distance': currentDistance, 'timeLeft': self.secondsLeft()})
                        time.sleep(2)
                    elif (currentDistance and abs(self.session['distanceBlock'] - currentDistance) > 1 and self.session['remoteDetonationBlocked']):
                        self._send_event(EventName.ULTRASONIC_DISTANCE, {'distance': currentDistance, 'timeLeft': self.secondsLeft()})
                        time.sleep(2)
                        print("UltraSonic Distance: " + str(currentDistance))
            time.sleep(2)
            print("Ultrasonic Thread is polling")
        print("Exiting Ultrasonic Thread")

            

    def _color_thread(self):
        """
        Performs random movement when patrol mode is activated.
        """
        while True:
            if (self.color.color_name in ["Green", "Red", "Yellow"]):
                print(self.color.color_name)
                self._send_event(EventName.COLOR, {'colorSeen': self.color.color_name, 'timeLeft': self.secondsLeft()})
            time.sleep(1)


            

if __name__ == '__main__':
    # Startup sequence
    gadget = MindstormsGadget()
    gadget.sound.play_song((('C4', 'e'), ('D4', 'e'), ('E5', 'q')))
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")

    # Gadget main entry point
    gadget.main()

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

index.js

JavaScript
This is the main Alexa Skill code
/*
 * 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 sample demonstrates sending directives to an Echo connected gadget from an Alexa skill
// using 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 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 PLAY_BEHAVIOR = "REPLACE_ENQUEUED"

//acceptable colors of the wires
const colors = ['Red', 'Yellow', 'Green'];

const RESTART_PHRASE = `Say "Reset the Bomb" to try again or "Exit" to leave`
const SUCCESS_PHRASE = `<voice name="Matthew">You successfully disarmed the bomb!  <audio src="soundbank://soundlibrary/human/amzn_sfx_crowd_cheer_med_01"></audio>` + RESTART_PHRASE + `</voice>`

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

        let request = handlerInput.requestEnvelope;
        let { apiEndpoint, apiAccessToken } = request.context.System;
        let 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
        let 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);

        return handlerInput.responseBuilder
            .speak('<voice name="Matthew"><amazon:emotion name="excited" intensity="medium">Welcome to LEGO Bomb Squad!</amazon:emotion> What\'s your name rookie?</voice>')
            .addDirective(Util.buildStartEventHandler(token,60000, {}))
            .reprompt('<voice name="Matthew">I\'m going to need your name to continue</voice>')
            .withShouldEndSession(false)
            .getResponse();
    }
};

// Add the speed value to the session attribute.
// This allows other intent handler to use the specified speed value
// without asking the user for input.
const FirstNameIntentHandler = {
    canHandle(handlerInput) {
        const attributesManager = handlerInput.attributesManager;
        
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetName'
    },
    handle: function (handlerInput) {

        const attributesManager = handlerInput.attributesManager;
        let endpointId = attributesManager.getSessionAttributes().endpointId || [];
        
        if ('name' in attributesManager.getSessionAttributes()) {
            return handlerInput.responseBuilder
            .speak(`<audio src="soundbank://soundlibrary/radios_static/radios_static_03"></audio>`)
            .withShouldEndSession(false)
            .getResponse();
        }
        
        
        let name = Alexa.getSlotValue(handlerInput.requestEnvelope, 'FirstName');
        Util.putSessionAttribute(handlerInput, 'name', name);
        
        
                // Construct the directive with the payload containing the move parameters
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'query',
                command: 'init'
            });
        
        return handlerInput.responseBuilder
            .speak(`<voice name="Matthew">Before we get started we're going to make sure the environment is safe to work in.</voice>`)
            .withShouldEndSession(false)
            .addDirective(directive)
            .getResponse();
        
    }
};


// Add the speed value to the session attribute.
// This allows other intent handler to use the specified speed value
// without asking the user for input.
const CutTheWireIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'CutWire';
    },
    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: 'query',
                command: 'color'
            });
        
        return handlerInput.responseBuilder
            .speak(`<voice name="Matthew">activating laser cutter!  Continue to point it the colored wire, until I confirm which one you cut</voice>`)
            .addDirective(directive)
            .withShouldEndSession(false)
            //.reprompt(currentStatusSpeech(handlerInput))
            .getResponse();
        
    }
};

const ResetTheBombIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'ResetBomb';
    },
    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: 'query',
                command: 'init'
            });
        
        return handlerInput.responseBuilder
            .speak(`<voice name="Matthew">Let's make sure the environment is safe to work in.</voice>`)
            .addDirective(directive)
            .withShouldEndSession(false)
            .getResponse();
            
        
        
    }
};

const GetStartedIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetStarted';
    },
    handle: function (handlerInput) {


        Util.putSessionAttribute(handlerInput, 'distanceBlock', Math.ceil(Math.random() * 6) + 4);
        Util.putSessionAttribute(handlerInput, 'colorOfWire', colors[Math.floor(Math.random()*colors.length)])
        Util.putSessionAttribute(handlerInput, 'remoteDetonationBlocked', false);
        Util.putSessionAttribute(handlerInput, 'wireCut', false);
        Util.putSessionAttribute(handlerInput, 'bombDiffused', false);
        Util.putSessionAttribute(handlerInput, 'fuseDisconnected', false);
        Util.putSessionAttribute(handlerInput, 'bombExploded', false);
        
        sendStateToEV3(handlerInput, true);


       return handlerInput.responseBuilder
            .speak(currentStatusSpeech(handlerInput))
            .withShouldEndSession(false)
            .getResponse();
        
    }
};

function sendStateToEV3(handlerInput, firsttime=false) {

        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: 'session',
                command: 'update',
                newSession: firsttime,
                session: attributesManager.getSessionAttributes()
            });
        
        return handlerInput.responseBuilder
            .addDirective(directive);
}

function currentStatusSpeech(handlerInput, payload=null) {
    const attributesManager = handlerInput.attributesManager;
    console.log("Payload In CurrentStatusSpeech: ")
    console.log(payload)
    let timeLeft = 60
    
    let speechOutput = ""
    if (payload && 'timeLeft' in payload) {
        timeLeft = payload.timeLeft
        if (timeLeft) {
            console.log("Set Speech Output for timeLeft")
         speechOutput = `<voice name="Matthew">You have ${timeLeft} seconds left</voice>`;
        }
    }
    
    if (timeLeft === 0) {
        console.log("timeLeft was 0 - you blew up")
        return `<voice name="Matthew">You ran out of time!<audio src="soundbank://soundlibrary/explosions/explosions/explosions_01"></audio>` + RESTART_PHRASE + `</voice>`
    }
    
    if (attributesManager.getSessionAttributes().bombDiffused)
    {
        return SUCCESS_PHRASE;
    }
    
    
    
    
    if (attributesManager.getSessionAttributes().remoteDetonationBlocked) {
        if (attributesManager.getSessionAttributes().wireCut) {
            speechOutput += `<voice name="Matthew">You're so close!  You need to deactivate the fuse.  Simply push the large gray button once underneath the moving arm.<audio src="soundbank://soundlibrary/radios_static/radios_static_03"></audio></voice> `
        } else {
            speechOutput += `<voice name="Matthew">You need to cut one of the red, yellow or green wires.  Simply point the laser cutter at the color wire you want to cut and say: 'activate the laser cutter'  Look around for clues to help you know what color to cut.  Cut the wrong one and the bomb will blow up. If I don't seem to be listening you may need to push the wheel on the top to activate my comm link again<audio src="soundbank://soundlibrary/radios_static/radios_static_03"></audio></voice>`
        }
        
    } else {
        speechOutput += `<voice name="Matthew"> You need to adjust the satellite to block the detonation signal before you attempt to disarm the bomb.<audio src="soundbank://soundlibrary/radios_static/radios_static_03"></audio> </voice>`
    }
    return speechOutput;
}

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) {
        const attributesManager = handlerInput.attributesManager;
        console.log("== Received Custom Event ==");
        let customEvent = handlerInput.requestEnvelope.request.events[0];
        let payload = customEvent.payload;
        let name = customEvent.header.name;
        let directive = null;
        let speechOutput;
        
        if (attributesManager.getSessionAttributes().bombExploded) {
             speechOutput = `<voice name="Matthew"><audio src="soundbank://soundlibrary/explosions/explosions/explosions_01"></audio>` + RESTART_PHRASE + `</voice>`
        } else {
        
            if (name === 'Button') {
                
                console.log("Processing Button")
                console.log("Payload in button handling: " + payload)
                speechOutput = currentStatusSpeech(handlerInput, payload);
    
            } else if (name === 'Color') {
                
                var colorSeen = payload.colorSeen;
                
                speechOutput = `<voice name="Matthew">You cut the ${colorSeen} wire! </voice>`;
                
                if (attributesManager.getSessionAttributes().remoteDetonationBlocked) {
                    if (String(attributesManager.getSessionAttributes().colorOfWire) == (colorSeen)) {
                        
                        speechOutput += `<voice name="Matthew">Great Job!  Now you need to deactivate the fuse by pushing the button on the remote.  Careful with your hand!</voice>`;
                        Util.putSessionAttribute(handlerInput, 'wireCut', true);
                        
                        
                    } else {
                        speechOutput += `<voice name="Matthew">This was the wrong wire!<audio src="soundbank://soundlibrary/explosions/explosions/explosions_01"></audio>` + RESTART_PHRASE + `</voice>`;
                        Util.putSessionAttribute(handlerInput, 'bombExploded', true);
                    }
                } else {
                    speechOutput += `<voice name="Matthew">You didn't deactivate the tamper detection!<audio src="soundbank://soundlibrary/explosions/explosions/explosions_01"></audio>` + RESTART_PHRASE + `</voice>`;
                    Util.putSessionAttribute(handlerInput, 'bombExploded', true);
                }
                
                
                
            } else if (name === 'Speech') {
                speechOutput = payload.speechOut;
    
            } else if (name === 'Init') {
                console.log("Payload in Init: " + payload);
                if (payload.proximity === null) {
                    speechOutput = "looks like we're not quite ready yet.  Please raise the black arm above the remote and press the large gray button on remote once so you can see the green light and then say reset the bomb."
                    
                    
                    
                    return handlerInput.responseBuilder
                        .speak(speechOutput, PLAY_BEHAVIOR)
                        .withShouldEndSession(false)
                        .reprompt(`Say reset the bomb when you're ready`)
                        .getResponse();
                }
                
                //Reasonable maximum distance that wouldn't show up once satellite is on the track
                if (payload.ultrasonic > 14) {
                    speechOutput = "Looks like the satellite to block the signal isn't on the track.  Be sure it's built and put into place on the track and say reset the bomb.  "
                    
                    return handlerInput.responseBuilder
                        .speak(speechOutput)
                        .withShouldEndSession(false)
                        .reprompt(`Say reset the bomb when you're ready`)
                        .getResponse();
                }
                
                //No problems with initialization
                
                let firstName = attributesManager.getSessionAttributes().name
                return handlerInput.responseBuilder
                .speak(`<voice name="Matthew">Hey there ${firstName} looks like we're good to go ! Pay close attention to your surroundings as there may be something here that saves your life.  
                Police were called to the scene and found the contraption you see before you.  It looks like a bomb of some sort.   There's only 60 seconds on the timer and no time for the bomb squad to arrive.  
                It's up to you to disarm it.  Our techs have done preliminary analysis and have determined that we need to first jam the receiver to prevent remote detonation.  
                At any point you can push the wheel at the top of the contraption to activate the comm-link between us.  I will tell you how much time you have left, and remind you of your next task.
                We're getting a lot of interference so we both may have to repeat ourselves.  If I don't seem to be listening press the wheel again.
                We need to block that signal!  Say: 'Let's get Started'<audio src="soundbank://soundlibrary/radios_static/radios_static_06"></audio></voice>`)
                .withShouldEndSession(false)
                .reprompt(`<voice name="Matthew">Time's ticking away.  Say Let's get started<audio src="soundbank://soundlibrary/radios_static/radios_static_03"></audio></voice>`, PLAY_BEHAVIOR)
                .getResponse();
    
            } else if (name === 'UltrasonicDistance') {
                var difference = attributesManager.getSessionAttributes().distanceBlock - payload.distance
                if (difference > 1) {
                    speechOutput = `<voice name="Matthew">Move it farther</voice>`;
                    Util.putSessionAttribute(handlerInput, 'remoteDetonationBlocked', false);
                } else if (difference < -1) {
                    speechOutput = `<voice name="Matthew">Move it closer</voice>`;
                    Util.putSessionAttribute(handlerInput, 'remoteDetonationBlocked', false);
                } else {
                    speechOutput = `<voice name="Matthew">Great - You've blocked the remote detonation!  It's time to cut either the red, yellow or green wire.  Look around for a clue on what wire to cut.  Point the laser cutter at one of the color wires and say 'Activate Laser Cutter'</voice>`;
                     Util.putSessionAttribute(handlerInput, 'remoteDetonationBlocked', true);
                }
    
            } else if (name === 'InfraRedDistance') {
               
                Util.putSessionAttribute(handlerInput, 'fuseDisconnected', payload.disconnected);
                
                if (payload.disconnected)
                {
                
                    if (attributesManager.getSessionAttributes().remoteDetonationBlocked && attributesManager.getSessionAttributes().wireCut) {
                        speechOutput = SUCCESS_PHRASE
                        Util.putSessionAttribute(handlerInput, 'bombDiffused', true);
                    } else {
                        if (!attributesManager.getSessionAttributes().remoteDetonationBlocked) {
                            speechOutput = `<voice name="Matthew">You forgot to disarm the tamper detection.  <audio src="soundbank://soundlibrary/explosions/explosions/explosions_01"></audio></voice>`
                        } else if (!attributesManager.getSessionAttributes().wireCut) {
                            speechOutput = `<voice name="Matthew">You forgot to cut the ` + attributesManager.getSessionAttributes().colorOfWire + ` wire before deactivating the fuse.<audio src="soundbank://soundlibrary/explosions/explosions/explosions_01"></audio></voice>`
                        }
                    }
                }
    
            } else {
                speechOutput = `<voice name="Matthew">Not sure what you said... try again<audio src="soundbank://soundlibrary/radios_static/radios_static_03"></audio></voice>`;
            }
        }
        
        sendStateToEV3(handlerInput);
        
        return handlerInput.responseBuilder
        .speak(speechOutput, PLAY_BEHAVIOR)
        .withShouldEndSession(false)
        //.reprompt(currentStatusSpeech(handlerInput))
        .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
            const speechOutput = `${duration} minutes remaining.`;
            return handlerInput.responseBuilder
                .addDirective(Util.buildStartEventHandler(token, 60000, {}))
                //.speak(speechOutput)
                .withShouldEndSession(false)
                .getResponse();
        }
        else {
            // End skill session
            return handlerInput.responseBuilder
                .speak(`<voice name="Matthew">Thanks for playing LEGO Bombsquad</voice>`)
                .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,
        FirstNameIntentHandler,
        GetStartedIntentHandler,
        CutTheWireIntentHandler,
        ResetTheBombIntentHandler,
        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();

model.json

JSON
This is the intent model for Alexa Skill
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "lego bomb squad",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "GetName",
                    "slots": [
                        {
                            "name": "FirstName",
                            "type": "AMAZON.FirstName",
                            "samples": [
                                "Sally",
                                "John",
                                "Dax",
                                "my name is John",
                                "my name is Dax"
                            ]
                        }
                    ],
                    "samples": [
                        "{FirstName}",
                        "i am {FirstName}",
                        "my name is {FirstName}"
                    ]
                },
                {
                    "name": "CutWire",
                    "slots": [],
                    "samples": [
                        "activate laser cutter",
                        "Snip the wire",
                        "Cut the wire"
                    ]
                },
                {
                    "name": "ResetBomb",
                    "slots": [],
                    "samples": [
                        "start over",
                        "reset the bomb",
                        "Calibrate the Bomb"
                    ]
                },
                {
                    "name": "GetStarted",
                    "slots": [],
                    "samples": [
                        "go go go",
                        "I'm ready to go",
                        "Let's get started"
                    ]
                }
            ],
            "types": []
        },
        "dialog": {
            "intents": [
                {
                    "name": "GetName",
                    "confirmationRequired": false,
                    "prompts": {},
                    "slots": [
                        {
                            "name": "FirstName",
                            "type": "AMAZON.FirstName",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.64301562174.719241417911"
                            }
                        }
                    ]
                }
            ],
            "delegationStrategy": "ALWAYS"
        },
        "prompts": [
            {
                "id": "Elicit.Slot.64301562174.719241417911",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "What's your name rookie?"
                    }
                ]
            }
        ]
    }
}

common.js

JavaScript
These are common/util files that were provided by framework/contest
/*
 * 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
    };

util.js

JavaScript
Common/Util code to support Alexa skill. Provided by framework/context
/*
 * 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');
            let returnData = '';
            response.on('data', (chunk) => {
                returnData += chunk;
            });

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

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

package.json

JSON
package.json for lambda skill 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"
    }
  }
  

Credits

Dax Haslam

Dax Haslam

1 project • 2 followers

Comments