Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
| ||||||
|
Welcome to the world of LEGO dog powered by Amazon Alexa. It is a puppy made of Lego MINDSTORMS EV3 components and powered by Linux.
These instructions are created to show you how to connect your EV3 Brick to Alexa, and build custom voice-based interactions that incorporate EV3 motors and sensors.
LEGO puppy is a toy which you can train to sit, speak, stay, and even heel.
Steps:Follow instructions on the pages below:
- Build Lego puppy by following the building instructions ( see attachment )
- Install ev3dev and write it to the memory SD card using Etcher application.
- Instal Visual Studio Code for editing code.
- Install Alexa Gadgets Python Software on your EV3 Brick.
- Update ev3dev by following https://www.ev3dev.org/docs/tutorials/upgrading-ev3dev/
- Register Lego R2D2 as an Alexa gadget (you need have Amazon developer account)
- Prepare your Lego R2D2 to get the software (see attached files)
- Open provided software with Visual Studio Code and write the software to SD card. Run it on LEGO puppy
- Create custom Alexa skill ( follow the steps here) and paste the Node.js code to implement the Skill Logic
- After you create custom Amazon Alexa Skill, use attached lambda functions to create Lego R2D2 skill. Result should look as below
- Run the python software and Lego R2D2 by following this
And now let's train the LEGO puppy. Start by saying
"Alexa, open LEGO puppy"
Alexa should respond acknowledging the skill session is active and list possible dog commands. The LEGO puppy skill must be running in order to call dog commands:
"sit" - and LEGO puppy sits down
"heel" - and LEGO puppy stays close to you, but not to close :)
/*
* 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 uses source code provided in https://www.hackster.io/alexagadgets/
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');
// Import language strings containing all skill voice messages
// e.g. handlerInput.t('WELCOME_MSG')
const i18n = require('i18next');
const languageStrings = require('./localisation');
// The audio tag to include background music
const DOG_SMALL_BARK_AUDIO = '<audio src="soundbank://soundlibrary/animals/amzn_sfx_dog_small_bark_2x_01"></audio>';
const COMMANDS_LIST = ['COMMAND_REMIND_MSG', 'HELP_COMMAND_MSG'];
// List of welcome greetings
const DOG_COMMAND_LIST = ['COMMAND_SIT_MSG','COMMAND_STAY_MSG','COMMAND_COME_MSG', 'COMMAND_HEEL_MSG', 'COMMAND_SPEAK_MSG'];
// 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) {
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
const launchCount = sessionAttributes['launchCount'] || 0;
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle: async function(handlerInput) {
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
const request = handlerInput.requestEnvelope;
const { apiEndpoint, apiAccessToken } = request.context.System;
const apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
console.log("apiResponse: " + apiResponse);
let speakOutput = handlerInput.t('NO_EV3_FOUND_MSG');
if ((apiResponse.endpoints || []).length === 0) {
return handlerInput.responseBuilder
.speak(speakOutput)
.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);
const launchCount = sessionAttributes['launchCount'];
let speechOutput = '';
if (launchCount === 1) {
speechOutput = handlerInput.t('WELCOME_GREETING_MSG');
} else {
speechOutput = handlerInput.t('WELCOME_GREETING_MSG') + handlerInput.t(randomChoice(DOG_COMMAND_LIST));
}
// starting point
return handlerInput.responseBuilder
.speak(DOG_SMALL_BARK_AUDIO + speechOutput)
.withShouldEndSession(false)
.addDirective(Util.buildStartEventHandler(token,60000, {}))
.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(handlerInput.t('REPEAT_COMMANDS_MSG'))
.withShouldEndSession(false)
.getResponse();
}
const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Construct the directive with the payload containing the command
let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'command',
command: command
});
let speechOutput = handlerInput.t('COMMAND_TEXT') + `${command}` + handlerInput.t('ACTIVATED_TEXT');
if( command === 'speak' || command === 'laut') {
speechOutput = DOG_SMALL_BARK_AUDIO;
}
return handlerInput.responseBuilder
.speak(speechOutput)
.addDirective(directive)
.getResponse();
}
};
const EventsReceivedRequestHandler = {
// Checks for a valid token and endpoint.
canHandle(handlerInput) {
let { request } = 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;
console.log(JSON.stringify(payload));
let name = customEvent.header.name;
let speechOutput;
if (name === 'bark') {
return handlerInput.responseBuilder
.speak(DOG_SMALL_BARK_AUDIO)
.withShouldEndSession(false)
.getResponse();
} else {
speechOutput = handlerInput.t('UNKNOWN_EVENT_MSG');
}
return handlerInput.responseBuilder
.speak(handlerInput.t(speechOutput))
.getResponse();
}
};
const ExpiredRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'CustomInterfaceController.Expired'
},
handle(handlerInput) {
// 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);
const speechOutput = handlerInput.t(randomChoice(COMMANDS_LIST));
// Extends skill session
return handlerInput.responseBuilder
.addDirective(Util.buildStartEventHandler(token, 60000, {}))
.speak(speechOutput)
.getResponse();
}
else {
// End skill session
return handlerInput.responseBuilder
.speak(handlerInput.t('GOODBYE_MSG') + DOG_SMALL_BARK_AUDIO)
.withShouldEndSession(true)
.getResponse();
}
}
};
// 2. Helper Functions ============================================================================
function randomChoice(array) {
// the argument is an array [] of words or phrases
const i = Math.floor(Math.random() * array.length);
return (array[i]);
}
const LocalisationRequestInterceptor = {
process(handlerInput) {
i18n.init({
lng: handlerInput.requestEnvelope.request.locale,
resources: languageStrings
}).then((t) => {
handlerInput.t = (...args) => t(...args);
});
}
};
// 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,
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(
LocalisationRequestInterceptor,
Common.RequestInterceptor,
)
.addErrorHandlers(
Common.ErrorHandler,
)
.lambda();
#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# Copyright (c) 2019 Timur Tentimishov <family.tentimishov@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# -----------------------------------------------------------------------------#
# This software demonstrates usage of motors, sensors, buttons, and leds and
# uses source code provided in https://www.hackster.io/alexagadgets/
import logging
import json
import threading
import time
from random import choice, randint
from enum import Enum
from agt import AlexaGadget
from ev3dev2.led import Leds
from ev3dev2.sound import Sound
from ev3dev2.motor import OUTPUT_A, SpeedPercent, MediumMotor, OUTPUT_B, OUTPUT_C, MoveTank, LargeMotor
from ev3dev2.sensor.lego import InfraredSensor
from ev3dev2.sensor.lego import TouchSensor
from ev3dev2.display import Display
# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO)
class Command(Enum):
"""
The list of preset commands and their invocation variation.
These variations correspond to the skill slot values.
"""
SENTRY = ['guard', 'protect', 'sentry', 'sentry mode','watch', 'watch mode']
SIT = ['sitz', 'sit']
STAY = ['bleib', 'stay', 'steh auf', 'stehen bleiben']
HEEL = ['fuss', 'heel']
COME = ['come to me', 'Komm', 'come']
SPEAK = ['speak', 'laut']
class EventName(Enum):
"""
The list of custom events sent from this gadget to Alexa
"""
BARK = "bark"
class MindstormsGadget(AlexaGadget):
"""
A Mindstorms gadget that performs movement based on voice commands.
Four types of commands are supported: sit, stay, come, speak, heel.
"""
def __init__(self):
"""
Performs Alexa Gadget initialization routines and ev3dev resource allocation.
"""
super().__init__()
# Gadget state
self.heel_mode = False
self.patrol_mode = False
self.sitting = False
# Ev3dev initialization
self.leds = Leds()
self.sound = Sound()
# Connect infrared and touch sensors.
self.ir = InfraredSensor()
self.ts = TouchSensor()
# Init display
self.screen = Display()
# Connect medium motor on output port A:
self.medium_motor = MediumMotor(OUTPUT_A)
# Connect two large motors on output ports B and C:
self.left_motor = LargeMotor(OUTPUT_B)
self.right_motor = LargeMotor(OUTPUT_C)
# Gadget states
self.bpm = 0
self.trigger_bpm = "off"
# Start threads
threading.Thread(target=self._patrol_thread, daemon=True).start()
threading.Thread(target=self._heel_thread, daemon=True).start()
threading.Thread(target=self._touchsensor_thread, daemon=True).start()
# ------------------------------------------------
# Callbacks
# ------------------------------------------------
def on_connected(self, device_addr):
"""
Gadget connected to the paired Echo device.
:param device_addr: the address of the device we connected to
"""
print("{} Connected to Echo device".format(self.friendly_name))
# Draw blinking eyes of the puppy
threading.Thread(target=self._draweyes, daemon=True).start()
# Turn lights on:
for light in ('LEFT', 'RIGHT'):
self.leds.set_color(light, 'GREEN')
def on_disconnected(self, device_addr):
"""
Gadget disconnected from the paired Echo device.
:param device_addr: the address of the device we disconnected from
"""
# Turn lights off:
for light in ('LEFT', 'RIGHT'):
self.leds.set_color(light, 'BLACK')
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 == "command":
# Expected params: [command]
self._activate(payload["command"])
except KeyError:
print("Missing expected parameters: {}".format(directive))
# On Amazon music play
def on_alexa_gadget_musicdata_tempo(self, directive):
"""
Provides the music tempo of the song currently playing on the Echo device.
:param directive: the music data directive containing the beat per minute value
"""
tempo_data = directive.payload.tempoData
for tempo in tempo_data:
print("tempo value: {}".format(tempo.value))
if tempo.value > 0:
# dance pose
#self.drive.on_for_seconds(SpeedPercent(5), SpeedPercent(25), 1)
self.right_motor.run_timed(speed_sp=750, time_sp=2500)
self.left_motor.run_timed(speed_sp=-750, time_sp=2500)
# shake ev3 head
threading.Thread(target=self._sitdown).start()
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
time.sleep(3)
# starts the dance loop
self.trigger_bpm = "on"
threading.Thread(target=self._dance_loop, args=(tempo.value,)).start()
elif tempo.value == 0:
# stops the dance loop
self.trigger_bpm = "off"
self.leds.set_color("LEFT", "BLACK")
self.leds.set_color("RIGHT", "BLACK")
def _dance_loop(self, bpm):
"""
Perform motor movement in sync with the beat per minute value from tempo data.
:param bpm: beat per minute from AGT
"""
color_list = ["GREEN", "RED", "AMBER", "YELLOW"]
led_color = random.choice(color_list)
motor_speed = 400
milli_per_beat = min(1000, (round(60000 / bpm)) * 0.65)
print("Adjusted milli_per_beat: {}".format(milli_per_beat))
while self.trigger_bpm == "on":
# Alternate led color and motor direction
led_color = "BLACK" if led_color != "BLACK" else random.choice(color_list)
motor_speed = -motor_speed
self.leds.set_color("LEFT", led_color)
self.leds.set_color("RIGHT", led_color)
self.right_motor.run_timed(speed_sp=motor_speed, time_sp=150)
self.left_motor.run_timed(speed_sp=-motor_speed, time_sp=150)
time.sleep(milli_per_beat / 1000)
self.left_motor.run_timed(speed_sp=-motor_speed, time_sp=150)
self.right_motor.run_timed(speed_sp=motor_speed, time_sp=150)
time.sleep(milli_per_beat / 1000)
self.right_motor.run_timed(speed_sp=350, time_sp=300)
self.left_motor.run_timed(speed_sp=-350, time_sp=300)
time.sleep(milli_per_beat / 1000)
self.right_motor.run_timed(speed_sp=motor_speed, time_sp=150)
self.left_motor.run_timed(speed_sp=-motor_speed, time_sp=150)
time.sleep(milli_per_beat / 1000)
def _move(self, direction, duration: int, speed: int, is_blocking=False):
"""
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, speed, duration, is_blocking))
if direction in Direction.FORWARD.value:
#self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(speed), duration, block=is_blocking)
self.right_motor.run_timed(speed_sp=-750, time_sp=2500)
self.left_motor.run_timed(speed_sp=-750, time_sp=2500)
if direction in Direction.BACKWARD.value:
#self.drive.on_for_seconds(SpeedPercent(-speed), SpeedPercent(-speed), duration, block=is_blocking)
self.right_motor.run_timed(speed_sp=750, time_sp=2500)
self.left_motor.run_timed(speed_sp=750, time_sp=2500)
if direction in (Direction.RIGHT.value + Direction.LEFT.value):
self._turn(direction, speed)
#self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(speed), duration, block=is_blocking)
self.right_motor.run_timed(speed_sp=750, time_sp=2500)
self.left_motor.run_timed(speed_sp=-750, time_sp=2500)
if direction in Direction.STOP.value:
#self.drive.off()
self.right_motor.stop
self.left_motor.stop
self.heel_mode = False
self.patrol_mode = False
def _activate(self, command):
"""
Handles preset commands.
:param command: the preset command
"""
print("Activate command: ({}".format(command))
if command in Command.COME.value:
#call _come method
self.right_motor.run_timed(speed_sp=750, time_sp=2500)
self.left_motor.run_timed(speed_sp=50, time_sp=100)
if command in Command.HEEL.value:
#call _hell method
self.heel_mode = True
if command in Command.SIT.value:
# call _sit method
self.heel_mode = False
self._sitdown()
if command in Command.STAY.value:
# call _stay method
self.heel_mode = False
self._standup()
def _turn(self, direction, speed):
"""
Turns based on the specified direction and speed.
Calibrated for hard smooth surface.
:param direction: the turn direction
:param speed: the turn speed
"""
if direction in Direction.LEFT.value:
#self.drive.on_for_seconds(SpeedPercent(0), SpeedPercent(speed), 2)
self.right_motor.run_timed(speed_sp=0, time_sp=100)
self.left_motor.run_timed(speed_sp=750, time_sp=100)
if direction in Direction.RIGHT.value:
#self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(0), 2)
self.right_motor.run_timed(speed_sp=750, time_sp=100)
self.left_motor.run_timed(speed_sp=0, time_sp=100)
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 _heel_thread(self):
"""
Monitors the distance between the puppy and an obstacle when heel command called.
If the maximum distance is breached, decrease the distance by following an obstancle
"""
while True:
while self.heel_mode:
distance = self.ir.proximity
print("Proximity distance: {}".format(distance))
# keep distance and make step back from the object
if distance < 45:
threading.Thread(target=self.__movebackwards).start()
self._send_event(EventName.BARK, {'distance': distance})
# follow the object
if distance > 60:
threading.Thread(target=self.__moveforwards).start()
# otherwise stay still
else:
threading.Thread(target=self.__stay).start()
time.sleep(0.2)
time.sleep(1)
def _touchsensor_thread(self):
print("Touch sensor activated")
while True:
if self.ts.is_pressed:
self.leds.set_color("LEFT", "RED")
self.leds.set_color("RIGHT", "RED")
if (self.sitting):
threading.Thread(target=self._standup).start()
self.sitting = False
else:
threading.Thread(target=self._sitdown).start()
self.sitting = True
else:
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
def _sitdown(self):
self.medium_motor.on_for_rotations(SpeedPercent(20), 0.5)
def _standup(self):
# run the wheels backwards to help the puppy to stand up.
threading.Thread(target=self.__back).start()
self.medium_motor.on_for_rotations(SpeedPercent(50), -0.5)
def __back(self):
self.right_motor.run_timed(speed_sp=-350, time_sp=1000)
self.left_motor.run_timed(speed_sp=-350, time_sp=1000)
def __movebackwards(self):
self.right_motor.run_timed(speed_sp=-650, time_sp=1000)
self.left_motor.run_timed(speed_sp=-650, time_sp=1000)
def __moveforwards(self):
self.right_motor.run_timed(speed_sp=650, time_sp=1000)
self.left_motor.run_timed(speed_sp=650, time_sp=1000)
def __stay(self):
self.right_motor.run_timed(speed_sp=0, time_sp=1000)
self.left_motor.run_timed(speed_sp=0, time_sp=1000)
def _draweyes(self):
close = True
while True:
self.screen.clear()
if close:
#self.screen.draw.ellipse(( 5, 30, 75, 50),fill='white')
#self.screen.draw.ellipse((103, 30, 173, 50), fill='white')
self.screen.draw.rectangle(( 5, 60, 75, 50), fill='black')
self.screen.draw.rectangle((103, 60, 173, 50), fill='black')
#self.screen.draw.rectangle(( 5, 30, 75, 50), fill='black')
#self.screen.draw.rectangle((103, 30, 173, 50), fill='black')
time.sleep(10)
else:
#self.screen.draw.ellipse(( 5, 30, 75, 100))
#self.screen.draw.ellipse((103, 30, 173, 100))
#self.screen.draw.ellipse(( 35, 30, 105, 30),fill='black')
#self.screen.draw.ellipse((133, 30, 203, 30), fill='black')
self.screen.draw.rectangle(( 5, 10, 75, 100), fill='black')
self.screen.draw.rectangle((103, 10, 173, 100), fill='black')
close = not close # toggle between True and False
# Update screen display
# Applies pending changes to the screen.
# Nothing will be drawn on the screen screen
# until this function is called.
self.screen.update()
time.sleep(1)
def _patrol_thread(self):
"""
Performs random movement when patrol mode is activated.
"""
while True:
while self.patrol_mode:
print("Patrol mode activated randomly picks a path")
direction = random.choice(list(Direction))
duration = random.randint(1, 5)
speed = random.randint(1, 4) * 25
while direction == Direction.STOP:
direction = random.choice(list(Direction))
# direction: all except stop, duration: 1-5s, speed: 25, 50, 75, 100
self._move(direction.value[0], duration, speed)
time.sleep(duration)
time.sleep(1)
if __name__ == '__main__':
# Startup sequence
gadget = MindstormsGadget()
# Gadget main entry point
gadget.main()
# 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.
[GadgetSettings]
amazonId = YOUR_AMAZON_ID
alexaGadgetSecret = _YOUR_AMAZON_SECRET
[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0
Alexa.Gadget.MusicData = 1.0 - tempo
Alexa.Gadget.SpeechData = 1.0 - viseme
Alexa.Gadget.StateListener = 1.0 - timeinfo, wakeword
/*
* 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 = handlerInput.t('HELP_COMMAND_MSG');
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 = handlerInput.t('GOODBYE_MSG');
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(handlerInput.t('REPEAT_COMMANDS_MSG'))
.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 = handlerInput.t('ERROR_COMMANDS_MSG');
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
};
/*
* 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();
}));
};
{
"name": "agt-mindstorms",
"version": "1.1.0",
"description": "A skill which uses 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",
"i18next": "^15.0.5"
}
}
{
"interactionModel": {
"languageModel": {
"invocationName": "lego puppy",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": [
"finish",
"ciao",
"bye",
"stop"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "SetCommandIntent",
"slots": [
{
"name": "Command",
"type": "CommandType"
}
],
"samples": [
"{Command}",
"do {Command}"
]
}
],
"types": [
{
"name": "CommandType",
"values": [
{
"name": {
"value": "speak"
}
},
{
"name": {
"value": "heel"
}
},
{
"name": {
"value": "go back"
}
},
{
"name": {
"value": "come to me"
}
},
{
"name": {
"value": "come"
}
},
{
"name": {
"value": "stay"
}
},
{
"name": {
"value": "sit"
}
}
]
}
]
}
}
}
Comments