Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
|
FULL BUILD
INTRODUCTION...the lure of flying is the lure of beauty.
Amelia Earhart
Flight - breaking the bonds of gravity beyond our imaginations - never ceases to bring me awe; the confluence of science, engineering, and creativity stirs what I believe are the unique elements of human potential.
One only needs to consider this challenge; bringing a toy (indeed, an engineering and robotic system) to life at one's voice; instructions synthesized thousands of miles back and forth before relayed to the creation a meter or so away from the speaker - an incredible idea now reality.
Of course, truly taking to the sky with LEGO is yet beyond this creator's ingenuity. However, controlling flight with a voice assistant seems perfectly natural; a tribute to professionals around the world watching from earth, keeping the clockwork of aviation ticking, safely. The name of this entry, FlightSimulator, is not a misnomer; flight, in the modern world, is not a singular activity, but the coordinated work of many systems and people - captured (in a small part) by this submission.
FIRST STEPSMy initial thoughts during development was a swiveling system with two large EV3 motors to control pitch and a medium motor for roll (Figure 1). Tests were promising (especially turning the contraption to respond to changes in desired heading). Unfortunately, the points connecting to the aircraft (a modified 60104) were very weak, and the model would flop over over when powered down, or fall off entirely.
I abandoned this design and began sketching a roving system that could not only simulate flight, but also taxiing, taking off, and landing.
LEGO ONE & AUTONOMOUS TWOThere are three simple rules for making a smooth landing. Unfortunately, no one knows what they are.
After the first ill-fated concept, I set to work on what would become LEGO ONE and AUTONOMOUS TWO (the latter borrowing the name of Technic 42081, from which I took parts).
I took the heavy-duty actuators to fashion what would become the roll mechanism (FIGURE 2). Connecting the fuselage at this point with three long pins and rotating along a center point (tan pin) made it much stronger, and I could operate roll with just one medium motor. The tricky part was threading the driving axle and universal joint between the actuators (the joint handles a near-perpendicular orientation, but it was not ideal [FIGURE 3]).
The lift assembly tilts the aircraft to simulate a nose-up attitude or level flight. It is fixed to a large EV3 motor in the center of the vehicle, and connected by two shock absorbers to reduce jerking motion when activating the motor (FIGURE 4).
The front wheel acts like a shopping cart and affords the vehicle a near-zero turning radius - which helps taxiing runways (FIGURE 5).
Overall, the system uses four motors (2 large EV3 for driving, 1 large EV3 for pitch, and 1 medium for roll) and two sensors (1 gyro for heading and 1 ultrasonic for collision avoidance/warning lights). I mounted the EV3 brick with the motor ports facing forward to reduce pinching cables. The bodywork was an all-night affair to get right (FIGURE 6, FIGURE 7).
When I embarked on the the LEGO MINDSTORMS Voice Challenge, I took to the initial missions to set up a development environment, become acquainted with Alexa, and gather requirements for the simulator application. During this time, I learned how to flash an SD card with the EV3DEV software, add WiFi to my EV3 brick (NetGear WNA1100), and receive numerous Bluetooth errors when the Echo Dot tried to communicate with EV3 (this was caused by the gyro sensor - I often had to reboot with the sensor unplugged, then plug it back in).
The first Alexa "intent" I created while developing the first prototype was to change the aircraft's heading; this required reading the gyro sensor's value and turning the motor accordingly.
When air traffic controllers or pilots call out new headings, they say each digit in a three-digit pattern (e.g. 30 would be zero-three-zero). To my surprise, Alexa recognized the term "niner" in place of "nine".
The "turn" instruction was neat enough - reading sensor values, engaging drive motors and tipping the wings. Then more difficult work came with taking off and landing: timings were essential for a realistic experience. FIGURE 9 shows how to use the ultrasonic sensor to flash red LEDs when an aircraft takes off or is in flight:
Ironically, I cannot use this mode in autopilot (when LEGO ONE) is supposed to be in flight and avoid obstacles. But it was fun to use the sensor in a different way.
While Alexa (as pilot) engages with Tower (you), it tries to add realism to experience - for example, when changing airspeed. FIGURE 10 shows validations on input value - the aircraft may not exceed 550 knots or fall below 160 knots during flight.
Finally, when landing, Alexa will reproduce the automated altimeter call outs from Airbus planes (500, 400, 300...).
CONCLUSIONSometimes you have to go up really high to see how small you are.
Felix Baumgartner
This challenge tested my imagination, inventiveness, and my LEGO collection. The solution demanded long hours, maddening dedication, and learning new skills across disciplines such as software development and robotics. After the first concept failed, I took to paper, then to LEGO, to begin again. I was fortunate to travel by air during the course of this process; I realized that these toils - the currency of innovation - invariably led to achievements like flight, computing, and even telling a toy plane it is cleared for takeoff.
{
"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": "Andy Merhaut",
"license": "ISC",
"dependencies": {
"ask-sdk-core": "^2.6.0",
"ask-sdk-model": "^1.18.0",
"aws-sdk": "^2.326.0",
"request": "^2.81.0"
}
}
/*
* 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');
/**
* 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();
}));
};
##################################################################################
# LEGO MINDSTORMS VOICE CHALLENGE #
# FLIGHT SIMULATOR #
# Andy Merhaut #
##################################################################################
import time
import logging
import json
import random
import threading
import asyncio
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, OUTPUT_C, OUTPUT_D, SpeedRPM,SpeedPercent, Motor, MoveSteering, MoveTank, MotorSet
from ev3dev2.sensor.lego import GyroSensor, UltrasonicSensor
#########################################
# SENSORS #
#########################################
yawSensor = GyroSensor('in1')
yawSensor.mode='GYRO-ANG'
collisionSensor = UltrasonicSensor('in4')
#########################################
# LOGGING #
#########################################
logging.basicConfig(level=logging.INFO)
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__()
# Gadget state
self.patrol_mode = False
# EV3DEV initialization
self.leds = Leds()
self.sound = Sound()
self.rollMotor = Motor(OUTPUT_A)
self.pitchMotor = Motor(OUTPUT_B)
self.drive = MoveSteering(OUTPUT_C, OUTPUT_D)
self.taxi = MoveTank(OUTPUT_C, OUTPUT_D)
self.drive1 = Motor(OUTPUT_C)
self.drive2 = Motor(OUTPUT_D)
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))
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"]
if control_type == "airspeed":
self._airspeed(payload["speed"])
if control_type == "landing":
self._landing(payload["runway"])
if control_type == "takeoff":
self._takeoff(payload["runway"])
if control_type == "turn":
self._turn(int(payload["heading"]))
if control_type == "autopilot":
self._autopilot()
if control_type == "reset":
self._reset()
except KeyError:
print("Missing expected parameters: {}".format(directive))
def _airspeed(self, speed):
self.taxi.on(SpeedPercent(-1 * speed),SpeedPercent(-1 * speed))
def _turn(self, direction):
"""
Turns based on the specified direction.
Calibrated for hard smooth surface.
:param direction: the turn direction
"""
yaw = yawSensor.value()
print("Current yaw angle: {} degrees; heading: {}".format(yaw, direction))
self.pitchMotor.on_for_rotations(SpeedPercent(50), 1, brake=True, block=False)
steering = 0
if yaw <= 0:
steering = 100 #turn right
self.rollMotor.polarity = "inversed"
elif yaw < direction:
steering = 100 #turn right
self.rollMotor.polarity = "inversed"
elif yaw > direction:
steering = -100 #turn left
self.rollMotor.polarity = "normal"
#START ROLL
self.rollMotor.on_for_rotations(SpeedPercent(90), 30, brake=False, block=False)
#TURN
self.drive.on(steering, SpeedPercent(5))
while (abs(yawSensor.value() - direction) >= 5):
if abs(yawSensor.value() - direction) <= 5:
self.taxi.reset()
break
if self.rollMotor.polarity in ["normal"]:
self.rollMotor.polarity = "inversed"
else:
self.rollMotor.polarity = "normal"
#END ROLL
self.rollMotor.on_for_rotations(SpeedPercent(90), 30, brake=False, block=False)
#CRUISE
self.taxi.on(SpeedPercent(-10),SpeedPercent(-10))
def _takeoff(self, runway):
#If takeoff runway is "R", taxi then turn right
direction = 100 if runway in ["right"] else -100
#TAXI RUNWAY
self.pitchMotor.on_for_seconds(SpeedPercent(50), 1, brake=True, block=True)
self.taxi.on_for_seconds(SpeedPercent(-10), SpeedPercent(-10), 32, brake=False, block=True)
self.drive.on_for_seconds(direction, SpeedPercent(10), 4.6, brake=False, block=True)
#START FLASHING LEDS
self._flash()
#FULL THRUST
accelerate = 0
while (accelerate > -50):
self.drive1.on(SpeedPercent(accelerate), brake=False, block=False)
self.drive2.on(SpeedPercent(accelerate), brake=False, block=False)
time.sleep(0.0222) #accelerate in 2 seconds
accelerate = accelerate - 1
#NOSE UP
accelerate = 0
while (accelerate > -90):
self.pitchMotor.on(SpeedPercent(accelerate * 0.333), brake=True, block=True)
time.sleep(0.0222) #accelerate to 90 in 2 seconds
accelerate = accelerate - 1
#FULL SPEED
accelerate = -50
while (accelerate < -20):
self.taxi.on(SpeedPercent(accelerate),SpeedPercent(accelerate))
time.sleep(0.0142857) #decel from -90 to -20 in 1 seconds
accelerate = accelerate + 1
#LEVEL
self.pitchMotor.on_for_rotations(SpeedPercent(50), 1, brake=True, block=False)
#CRUISE
self.taxi.on(SpeedPercent(-10),SpeedPercent(-10))
def _landing(self, runway):
#LEVEL
self.pitchMotor.on_for_rotations(SpeedPercent(50), 1, brake=True, block=False)
#START FLASHING LEDS (1s)
self._flash()
#Full speed
accelerate = -10
while (accelerate > -80):
self.taxi.on(SpeedPercent(accelerate),SpeedPercent(accelerate))
time.sleep(0.0142857) #accel in 1 seconds
accelerate = accelerate - 1
#NOSE UP
accelerate = 0
while (accelerate > -90):
self.pitchMotor.on(SpeedPercent(accelerate * 0.6), brake=True, block=True)
time.sleep(0.0333) #accelerate to -90 in 2 seconds
accelerate = accelerate - 1
#LEVEL
self.pitchMotor.on_for_rotations(SpeedPercent(50), 1, brake=True, block=False)
#BRAKE
accelerate = -80
while (accelerate < -10):
self.taxi.on(SpeedPercent(accelerate),SpeedPercent(accelerate))
time.sleep(0.042857) #decel in 3 seconds
accelerate = accelerate + 1
#TAXI RUNWAY
#If takeoff runway is "R", taxi then turn right
direction = 100 if runway in ["right"] else -100
self.taxi.on_for_seconds(SpeedPercent(-10), SpeedPercent(-10), 10, brake=False, block=True)
self.drive.on_for_seconds(direction, SpeedPercent(10), 4.6, brake=False, block=True)
collisionSensor.mode = "US-DIST-CM" # Use sensor
def _autopilot(self):
collisionSensor.mode = "US-DIST-CM" # Use sensor
#LEVEL
self.pitchMotor.on_for_rotations(SpeedPercent(50), 1, brake=True, block=False)
accelerate = 0
while (accelerate > -20):
self.drive1.on(SpeedPercent(accelerate), brake=False, block=False)
self.drive2.on(SpeedPercent(accelerate), brake=False, block=False)
time.sleep(0.1) #accelerate to 20 in 2 seconds
accelerate = accelerate - 1
# -100 left +100 right
timeValue = 0
while (timeValue < 400): #2 minutes
distance = collisionSensor.distance_centimeters
print("Distance: {}cm".format(distance))
if distance < 60:
accelerate = 0
steering = distance - 100
while accelerate > steering: #turning circle
self.drive.on(accelerate, SpeedPercent(10))
accelerate = accelerate - 1
time.sleep(1/(abs(steering) + 1))
else:
self.taxi.on(SpeedPercent(-20),SpeedPercent(-20))
time.sleep(0.3)
timeValue = timeValue + 1
def _reset(self):
self.pitchMotor.reset()
self.taxi.reset()
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
collisionSensor.mode = "US-DIST-CM"
def _flash(self):
collisionSensor.mode = "US-LISTEN" # Flash ultrasonic sensor
counter = 0
while (counter < 10):
self.leds.set_color("LEFT", "BLACK")
self.leds.set_color("RIGHT", "RED")
time.sleep(0.2)
self.leds.set_color("LEFT", "RED")
self.leds.set_color("RIGHT", "BLACK")
time.sleep(0.1)
counter = counter + 1
if __name__ == '__main__':
# Startup sequence
gadget = MindstormsGadget()
gadget.leds.set_color("LEFT", "GREEN")
gadget.leds.set_color("RIGHT", "GREEN")
# Gadget main entry point
gadget.main()
# Shutdown sequence
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
{
"interactionModel": {
"languageModel": {
"invocationName": "mindstorms",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "SetSpeedIntent",
"slots": [
{
"name": "Speed",
"type": "AMAZON.NUMBER",
"samples": [
"LEGO One Tower new speed {Speed} knots"
]
}
],
"samples": [
"LEGO One Tower reduce speed to {Speed} if unable advise",
"LEGO One Tower Maintain {Speed} knots",
"LEGO One this is tower reduce speed to {Speed}",
"LEGO One Tower increase speed to {Speed} knots"
]
},
{
"name": "SetHeadingIntent",
"slots": [
{
"name": "headingA",
"type": "HeadingValue",
"samples": [
"turn to {headingA} {headingB} {headingC}",
"new heading {headingA} {headingB} {headingC}",
"heading {headingA} {headingB} {headingC}"
]
},
{
"name": "headingB",
"type": "HeadingValue",
"samples": [
"heading {headingA} {headingB} {headingC}",
"new heading {headingA} {headingB} {headingC}",
"turn to {headingA} {headingB} {headingC}"
]
},
{
"name": "headingC",
"type": "HeadingValue",
"samples": [
"heading {headingA} {headingB} {headingC}",
"new heading {headingA} {headingB} {headingC}",
"turn to {headingA} {headingB} {headingC}"
]
}
],
"samples": [
"lego one this is tower new heading {headingA} {headingB} {headingC}",
"lego one tower new heading {headingA} {headingB} {headingC}",
"LEGO One tower heading {headingA} {headingB} {headingC}",
"LEGO One turn to {headingA} {headingB} {headingC}"
]
},
{
"name": "TakeoffIntent",
"slots": [
{
"name": "runway",
"type": "RunwayDirection",
"samples": [
"LEGO One Tower clear for takeoff {runwayA} {runwayB} {runway}",
"LEGO One Tower clear for takeoff runway {runwayA} {runwayB} {runway}",
"LEGO One Tower depart {runwayA} {runwayB} {runway}"
]
},
{
"name": "runwayA",
"type": "HeadingValue"
},
{
"name": "runwayB",
"type": "HeadingValue"
}
],
"samples": [
"LEGO One Tower go for takeoff runway {runwayA} {runwayB} {runway}",
"LEGO One Tower clear for takeoff runway {runwayA} {runwayB} {runway}",
"LEGO One this is Ground you are cleared for takeoff {runwayA} {runwayB} {runway}"
]
},
{
"name": "AutoPilotIntent",
"slots": [],
"samples": [
"Engage autopilot",
"Turn on autopilot"
]
},
{
"name": "ResetIntent",
"slots": [],
"samples": [
"Reset"
]
},
{
"name": "LandingIntent",
"slots": [
{
"name": "runwayA",
"type": "HeadingValue"
},
{
"name": "runwayB",
"type": "HeadingValue"
},
{
"name": "runway",
"type": "RunwayDirection"
}
],
"samples": [
"Runway {runwayA} {runwayB} {runway} cleared to land",
"LEGO One Tower {runwayA} {runwayB} {runway} cleared to land",
"LEGO One Tower cleared to land runway {runwayA} {runwayB} {runway}"
]
}
],
"types": [
{
"name": "DirectionType",
"values": [
{
"name": {
"value": "brake"
}
},
{
"name": {
"value": "go backward"
}
},
{
"name": {
"value": "go forward"
}
},
{
"name": {
"value": "go right"
}
},
{
"name": {
"value": "go left"
}
},
{
"name": {
"value": "right"
}
},
{
"name": {
"value": "left"
}
},
{
"name": {
"value": "backwards"
}
},
{
"name": {
"value": "backward"
}
},
{
"name": {
"value": "forwards"
}
},
{
"name": {
"value": "forward"
}
}
]
},
{
"name": "CommandType",
"values": [
{
"name": {
"value": "circle"
}
},
{
"name": {
"value": "square"
}
},
{
"name": {
"value": "patrol"
}
},
{
"name": {
"value": "cannon"
}
},
{
"name": {
"value": "all shot"
}
},
{
"name": {
"value": "one shot"
}
}
]
},
{
"name": "HeadingValue",
"values": [
{
"name": {
"value": "9"
}
},
{
"name": {
"value": "8"
}
},
{
"name": {
"value": "7"
}
},
{
"name": {
"value": "6"
}
},
{
"name": {
"value": "5"
}
},
{
"name": {
"value": "4"
}
},
{
"name": {
"value": "3"
}
},
{
"name": {
"value": "2"
}
},
{
"name": {
"value": "1"
}
},
{
"name": {
"value": "0"
}
}
]
},
{
"name": "UpDownValue",
"values": [
{
"id": "1",
"name": {
"value": "Maintain",
"synonyms": [
"remain",
"stay"
]
}
},
{
"id": "0",
"name": {
"value": "Decrease",
"synonyms": [
"reduce"
]
}
},
{
"id": "2",
"name": {
"value": "Increase"
}
}
]
},
{
"name": "RunwayDirection",
"values": [
{
"name": {
"value": "Center"
}
},
{
"name": {
"value": "Right"
}
},
{
"name": {
"value": "Left"
}
}
]
}
]
},
"dialog": {
"intents": [
{
"name": "SetHeadingIntent",
"confirmationRequired": true,
"prompts": {
"confirmation": "Confirm.Intent.743889462359"
},
"slots": [
{
"name": "headingA",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.732301228136.1546499721780"
}
},
{
"name": "headingB",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.1440849301641.1304054795354"
}
},
{
"name": "headingC",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.1440849301641.1181669313456"
}
}
]
},
{
"name": "TakeoffIntent",
"confirmationRequired": true,
"prompts": {
"confirmation": "Confirm.Intent.1563286004246"
},
"slots": [
{
"name": "runway",
"type": "RunwayDirection",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.30722260886.1156290942273"
}
},
{
"name": "runwayA",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
},
{
"name": "runwayB",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
}
]
},
{
"name": "LandingIntent",
"confirmationRequired": true,
"prompts": {
"confirmation": "Confirm.Intent.445899441332"
},
"slots": [
{
"name": "runwayA",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
},
{
"name": "runwayB",
"type": "HeadingValue",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
},
{
"name": "runway",
"type": "RunwayDirection",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
}
]
},
{
"name": "SetSpeedIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "Speed",
"type": "AMAZON.NUMBER",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.137219819843.204633308907"
},
"validations": [
{
"type": "isLessThan",
"prompt": "Slot.Validation.137219819843.204633308907.221381148916",
"value": "550"
},
{
"type": "isGreaterThan",
"prompt": "Slot.Validation.137219819843.204633308907.221557497054",
"value": "160"
}
]
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.732301228136.1546499721780",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One repeat new heading?"
}
]
},
{
"id": "Elicit.Slot.1440849301641.1181669313456",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One repeat new heading?"
}
]
},
{
"id": "Elicit.Slot.1440849301641.1304054795354",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One repeat new heading?"
}
]
},
{
"id": "Confirm.Slot.1440849301641.1536874964390",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One confirm {headingA} {headingB} {headingC}"
}
]
},
{
"id": "Confirm.Intent.743889462359",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One. Confirm {headingA} {headingB} {headingC}"
}
]
},
{
"id": "Confirm.Intent.1539523755367",
"variations": [
{
"type": "PlainText",
"value": "Tower. LEGO One. {upDown} altitude {altitude} feet."
}
]
},
{
"id": "Elicit.Slot.105486094802.1372282560033",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One repeat new altitude?"
}
]
},
{
"id": "Elicit.Slot.105486094802.429049922393",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One repeat new altitude?"
}
]
},
{
"id": "Confirm.Intent.1563286004246",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One confirm {runwayA} {runwayB} {runway} . "
}
]
},
{
"id": "Elicit.Slot.30722260886.1156290942273",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO repeat runway for departure"
}
]
},
{
"id": "Confirm.Intent.445899441332",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One heavy confirm {runwayA} {runwayB} {runway} . "
}
]
},
{
"id": "Slot.Validation.137219819843.204633308907.221381148916",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One we can't go that fast please advise"
}
]
},
{
"id": "Slot.Validation.137219819843.204633308907.221557497054",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One that's gonna be below our stall - unable to maintain that airspeed"
}
]
},
{
"id": "Elicit.Slot.137219819843.204633308907",
"variations": [
{
"type": "PlainText",
"value": "Tower LEGO One repeat new airspeed"
}
]
}
]
}
}
/*
* 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
};
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');
const NAMESPACE = 'Custom.Mindstorms.Gadget';
const NAME_CONTROL = 'control';
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);
return handlerInput.responseBuilder
.speak("Welcome, you can start issuing move commands")
.reprompt("Awaiting commands")
.getResponse();
}
};
const SetAutoPilotIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AutoPilotIntent';
},
handle: function (handlerInput) {
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'autopilot'
});
return handlerInput.responseBuilder
.speak(`LEGO ONE autopilot confirmed. Autonomous Two will do its best to avoid obstacles, but accidents happen.`)
.addDirective(directive)
.reprompt("awaiting command")
.getResponse();
}
};
const ResetIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'ResetIntent';
},
handle: function (handlerInput) {
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'reset'
});
return handlerInput.responseBuilder
.speak(`Resetting motors`)
.addDirective(directive)
.reprompt("awaiting command")
.getResponse();
}
};
const SetSpeedIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetSpeedIntent';
},
handle: function (handlerInput) {
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
let speedValue = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Speed');
let speed = parseInt(speedValue) / 10; //convert knots to percentage
Util.putSessionAttribute(handlerInput, 'speed', speed);
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'airspeed',
speed: speed
});
return handlerInput.responseBuilder
.speak(`Tower LEGO One new airspeed ${speedValue} knots.`)
.addDirective(directive)
.reprompt("ready when you are")
.getResponse();
}
};
const LandingIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'LandingIntent';
},
handle: function (handlerInput) {
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
let runway = Alexa.getSlotValue(handlerInput.requestEnvelope, 'runway');
Util.putSessionAttribute(handlerInput, 'runway', runway);
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'landing',
runway: runway
});
return handlerInput.responseBuilder
.speak(`Five hundred. Four hundred. Three hundred. Approaching minimums. Two hundred. Minimums. Continue. One hundred. Fifty, forty, thirty, twenty. Retard. Retard. Retard. Spoilers. Reverse green. Manual brakes.`)
.addDirective(directive)
.reprompt("awaiting command")
.getResponse();
}
};
const SetHeadingIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetHeadingIntent';
},
handle: function (handlerInput) {
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
let headingA = Alexa.getSlotValue(handlerInput.requestEnvelope, 'headingA');
let headingB = Alexa.getSlotValue(handlerInput.requestEnvelope, 'headingB');
let headingC = Alexa.getSlotValue(handlerInput.requestEnvelope, 'headingC');
let heading = `${headingA}${headingB}${headingC} `;
Util.putSessionAttribute(handlerInput, 'heading', heading);
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'turn',
heading: heading,
speed: "1"
});
return handlerInput.responseBuilder
.speak(`Tower LEGO One heading ${headingA} ${headingB} ${headingC}.`)
.addDirective(directive)
.reprompt("awaiting command")
.getResponse();
}
};
const TakeoffIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'TakeoffIntent';
},
handle: function (handlerInput) {
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
let runway = Alexa.getSlotValue(handlerInput.requestEnvelope, 'runway');
Util.putSessionAttribute(handlerInput, 'runway', runway);
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'takeoff',
runway: runway
});
return handlerInput.responseBuilder
.speak(`On behalf of the flight crew we would like to thank you for your business and we hope you enjoy your simulation. Flight attendants please prepare the cabin for departure.`)
.addDirective(directive)
.reprompt("awaiting command")
.getResponse();
}
};
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
SetHeadingIntentHandler,
TakeoffIntentHandler,
SetAutoPilotIntentHandler,
ResetIntentHandler,
LandingIntentHandler,
SetSpeedIntentHandler,
Common.HelpIntentHandler,
Common.CancelAndStopIntentHandler,
Common.SessionEndedRequestHandler,
Common.IntentReflectorHandler,
)
.addRequestInterceptors(Common.RequestInterceptor)
.addErrorHandlers(
Common.ErrorHandler,
)
.lambda();
Comments