Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
Hand tools and fabrication machines | ||||||
|
The Retro Robot
a lego Roboter that makes ancient Technology Smart
by Jonathan Wolter
We have ZIGBEE-bulbs at home, our coffee-machine is smart and the bathrooms heat is controlled conversationally. But some things can’t be controlled with voice, for example the old tape deck and that annoys me. So I invented the "retro robot". Build instructions are nearly unnecessary because you’d have to fit your retro-robot to your individual tape deck and that’s one reason why I like my project – because it is so modular, efficient and simple to use. But the Retro Robot isn’t limited to your tape deck – you could retro-fit your Microwave, washing-machine or even your board-games, e. g. the turning wheel in Twister. In my opinion, my project isn’t just a Lego-Robot. It is an Idea, a Motivation, inspiring everybody to make their everyday things smart and talk with them while making the everyday life easier.
Planning and Building the Retro Robot was very difficult. Not only did I have to learn a completely new programming language in a few days, designing a Chatbot is very difficult (aka conversational design). I followed "the voice first" practices e. g. I forced my mother to talk with the Retro Robot to look how the Interaction feels for somebody who didn't program it ("wizard of oz"-method). This feedback helped me finding user-input variations (intents and utterances) and making the answers more interesting and engaging.
But this was only a part from all the difficulties I had;
But in the end I managed to build a decent Robot that feels intuitiv to speak with and is very simple to use and modify. So, that it fits into everybody's everyday life seamlessly:
How to add your own commands:
Step 1. Start some good music. Setting up your Lego Mindstorms ev3 could take a while, so don’t rush and enjoy your music while concentrating
Step 2. Modify the scripts:
If you want additional commands;
1. Add a Motor to your Brick. Make sure to import Ouput_A/B/C/D
2. Add a Movement in the _actions function in RetroRobot.py
3. Add an Intent that includes the command you want to add
4. Go in index.js and add a intenthandler that canHandle your Intent. You can just copy (and edit) this:
const IntentNameHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'Intentname';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
command: „YourCommand“,
});
return handlerInput.responseBuilder
.speak(`What should Alexa say if youre Intent gets successfully triggere)
.reprompt("expecting instructions for the tape deck")
.addDirective(directive)
.getResponse();
}
};
5. Make sure to make the slot “required to fulfill the Intent”
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_D, OUTPUT_B, OUTPUT_C, SpeedPercent, LargeMotor
# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO)
class Command(Enum):
PLAY = ['play', 'start', 'continue', 'go', 'start', 'on', 'forward']
PAUSE = ['pause', 'break', 'off', 'stop', 'turn off']
#optional for rewind an off/on options. requires additional motors. watch out for duplicates like 'on' or 'off'
REWIND = ['back', 'rewind', 'scroll', 'go', 'go back']
ONOFF = ['turn off', 'on', 'power', 'off', 'shutdown']
class MindstormsGadget(AlexaGadget):
def __init__(self):
super().__init__()
#'lastdirection' used for cassette decks that can go in 2 directions. Needs an additional motor to press the "backwards"/"reverse"-button
global lastdirection
lastdirection = "forward"
self.leds = Leds()
self.sound = Sound()
self.motor_play = LargeMotor(OUTPUT_C)
self.motor_pause = LargeMotor(OUTPUT_D)
#For implementing Rewind and an On/Off function later.
#self.motor_onoff = LargeMotor(OUTPUT_A)
#self.motor_rewind = LargeMotor(OUTPUT_B)
def on_connected(self, device_addr):
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
print("{} connected to Echo device".format(self.friendly_name))
def on_disconnected(self, device_addr):
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):
global seconds
try:
payload = json.loads(directive.payload.decode("utf-8"))
print("Control payload: {}".format(payload))
command = payload["command"]
seconds = int(payload["seconds"])
except KeyError:
print("Looks like someone stole some of our paramaters. We are missing: {}".format(directive))
if command not in Command.REWIND.value:
try:
seconds = int(payload["seconds"])
print("Seconds to rewind: {}".format(seconds))
except KeyError:
print ("Seconds couldn't be found. We are currently sending out a rescue squad.")
seconds = 0
self._activate(command, seconds)
def _activate(self, command, seconds):
print("Activate command: ({})".format(command))
if command in Command.PLAY.value:
print("playing")
self.motor_play.on_for_seconds(SpeedPercent(40), .2, brake=False, block=True)
self.motor_play.on_for_seconds(SpeedPercent(-20), .1, brake=False, block=True)
lastdirection = "forward"
if command in Command.PAUSE.value:
print("pausing")
self.motor_pause.on_for_seconds(SpeedPercent(40), .2, brake=False, block=True)
self.motor_pause.on_for_seconds(SpeedPercent(-20), .1, brake=False, block=True)
"""
custom functions can just be added by copying the action for play and
swapping play with the name you want to callt it.
Rewind is not as easy, thats why I placed it here:
if command in Command.REWIND.value:
print("pausing")
self.motor_rewind.on_for_seconds(SpeedPercent(40), .2, brake=False, block=True)
self.motor_rewind.on_for_seconds(SpeedPercent(-20), .1, brake=False, block=True)
timeout(seconds)
self.motor_rewind.on_for_seconds(SpeedPercent(40), .2, brake=False, block=True)
self.motor_rewind.on_for_seconds(SpeedPercent(-20), .1, brake=False, block=True)
"""
def on_alexa_gadget_statelistener_stateupdate(self, directive):
print("statelistener...")
for state in directive.payload.states:
if state.name == 'wakeword':
print("wakeword.state.value: {}".format(state.value))
if state.value == 'active':
print("You try to speak to alexa")
self.leds.set_color("LEFT", "AMBER")
self.leds.set_color("RIGHT", "AMBER")
print("pausing")
self._activate('pause', 0)
elif state.value == 'cleared':
print("You stopped speaking with alexa")
self.leds.set_color("LEFT", "BLACK")
self.leds.set_color("RIGHT", "BLACK")
if lastdirection == 'forward':
print("playing")
self._activate('play', 0)
#add the reverse function in here
#if lastdirection == "reverse":
# self._activate('reverse', 0)
if __name__ == '__main__':
# Startup sequence
gadget = MindstormsGadget()
gadget.sound.play_song((('D3', 'e3'), ('D3', 'e3'), ('D3', 'e3'), ('G3', 'h'), ('D4', 'h')))
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", "RED")
gadget.leds.set_color("RIGHT", "RED")
gadget.sound.play_song((('A4', 'q'), ('A4', 'q'), ('A4', 'q'), ('F4', 'e'), ('C5', 'e'), ('A4', 'h')))
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
[GadgetSettings]
amazonId = YourGadgetID
alexaGadgetSecret = YourAlexaGadgetSecret
[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0 - wakeword
{
"interactionModel": {
"languageModel": {
"invocationName": "retro robot",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "CommandIntent",
"slots": [
{
"name": "Command",
"type": "CommandType",
"samples": [
"{Command} the {CassetteSynonym}",
"{Command} playing",
"{Command}"
]
},
{
"name": "CassetteSynonym",
"type": "CassetteSynonyms"
}
],
"samples": [
"{Command} my {CassetteSynonym}",
"{Command} the {CassetteSynonym}",
"let the retro robot hit {Command} please",
"{Command} the {CassetteSynonym} please",
"{Command} please",
"{Command}",
"let the retro robot hit {Command}",
"{Command} the {CassetteSynonym}",
"Let the {CassetteSynonym} {Command}",
"{Command} the {CassetteSynonym}"
]
},
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "WaitIntent",
"slots": [],
"samples": [
"hold on right there...",
"hold on please...",
"wait a minute",
"wait a second"
]
}
],
"types": [
{
"name": "CommandType",
"values": [
{
"name": {
"value": "freeze"
}
},
{
"name": {
"value": "hold on"
}
},
{
"name": {
"value": "turn on"
}
},
{
"name": {
"value": "begin"
}
},
{
"name": {
"value": "forward"
}
},
{
"name": {
"value": "go"
}
},
{
"name": {
"value": "turn off"
}
},
{
"name": {
"value": "play"
}
},
{
"name": {
"value": "continue"
}
},
{
"name": {
"value": "start"
}
},
{
"name": {
"value": "pause"
}
},
{
"name": {
"value": "break"
}
}
]
},
{
"name": "CassetteSynonyms",
"values": [
{
"name": {
"value": "Tune"
}
},
{
"name": {
"value": "Recording"
}
},
{
"name": {
"value": "Music"
}
},
{
"name": {
"value": "Song"
}
},
{
"name": {
"value": "Audio Book"
}
},
{
"name": {
"value": "Cassette"
}
},
{
"name": {
"value": "Tape"
}
}
]
}
]
},
"dialog": {
"intents": [
{
"name": "CommandIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "Command",
"type": "CommandType",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.1161912138601.1574986024770"
}
},
{
"name": "CassetteSynonym",
"type": "CassetteSynonyms",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.1161912138601.1574986024770",
"variations": [
{
"type": "PlainText",
"value": "Please repeat yourself"
},
{
"type": "PlainText",
"value": "What was the command again?"
}
]
}
]
}
}
{
"name": "Retro Robot",
"version": "1.3.5",
"description": "Super cool retro skill",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jonathan Wolter",
"license": "ISC",
"dependencies": {
"ask-sdk-core": "^2.6.0",
"ask-sdk-model": "^1.18.0",
"aws-sdk": "^2.326.0",
"request": "^2.81.0"
}
}
'use strict';
const Https = require('https');
/**
* @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) {
console.log("BUILDING THE ENDPOINT THING" + endpointId + namespace + name + payload);
return {
type: 'CustomInterfaceController.SendDirective',
header: {
name: name,
namespace: namespace
},
endpoint: {
endpointId: endpointId
},
payload
};
};
/**
* @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);
console.log("putSessionAttribute's end reached");
};
/**
* @param {string} apiEndpoint - the Endpoint API url
* @param {string} apiAccessToken - the token from the session object in the Alexa request
*/
exports.getConnectedEndpoints = function(apiEndpoint, apiAccessToken) {
// The preceding https:// need to be stripped off before making the call
apiEndpoint = (apiEndpoint || '').replace('https://', '');
console.log(apiEndpoint);
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) => {
console.log(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();
console.log("request ended");
}));
};
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');
const NAMESPACE = 'Custom.Mindstorms.Gadget';
const NAME_CONTROL = 'control';
//Seconds for Rewind
let seconds = 0;
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(`Looks like there is no EV3 connected to me. You may retry later`)
.getResponse();
}
//gagdegt endpointId
let endpointId = apiResponse.endpoints[0].endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
return handlerInput.responseBuilder
.speak("You may start instructing your Tape Deck now")
.reprompt("What can i do to the cassette for you?")
.getResponse();
}
};
//implement a rewind-function here. It is needed to use a given amount of time.
const RewindIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'RewindIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const seconds = Alexa.getSlotValue(request, 'seconds') || "2";
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Construct the directive with the payload containing the move parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
command: 'rewind',
seconds: seconds,
});
return handlerInput.responseBuilder
.speak(`rewinding ${seconds} seconds`)
.reprompt("expecting instructions for the tape deck")
.addDirective(directive)
.getResponse();
}
};
const FallbackIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'Amazon.FallbackIntent';
},
handle: function (handlerInput) {const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
const speakoutput_choices = ['woops. I think you have said something I am not apable of understanding. Plese repeat', "I dont know what you're trying to say. Please repeat yourself in a different way"];
return handlerInput.responseBuilder
.speak(speakoutput_choices[Math.floor(Math.random() * speakoutput_choices.length)])
.reprompt("expecting instructions for the cassette recorder")
.getResponse();
}
};
const CommandIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'CommandIntent';
},
handle: function (handlerInput) {
const seconds = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Seconds') || "2";
const command = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Command');
if (!command) {
return handlerInput.responseBuilder
.speak("Can you repeat that?")
.reprompt("What was that again?").getResponse();
}
const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Construct the directive with the payload containing the move parameters
let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
command: command,
seconds: seconds
});
const speakOutputOptions = ["Ok. I will ask the cassette to " + command + " the tape", "Ok. The tape will " + command];
const random = Math.floor(Math.random() * speakOutputOptions.length);
const speakOutput = speakOutputOptions[random];
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt("expecting instructions for the cassette recorder")
.addDirective(directive)
.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 attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
command: 'pause',
seconds: 0
});
const speakOutput = 'See you later, alligator!';
return handlerInput.responseBuilder
.speak(speakOutput)
.addDirective(directive)
.getResponse();
}
};
const WaitIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'WaitIntent';
},
handle: function (handlerInput) {const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
return handlerInput.responseBuilder
.speak("ok. I will just talk a bit and you dont have to listen to me. due to the amazon skill restriction it is not possible that a skill doesnt respond for a given amount of time. So, are you finished yet with thinking or whatever you are doing? no? ok, doesnt matter, I dont want to talk anymore. Go one, instruct the cassetteplayer.")
.reprompt("expecting instructions for the cassette recorder")
.getResponse();
}
};
// The SkillBuilder
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
RewindIntentHandler,
CommandIntentHandler,
WaitIntentHandler,
FallbackIntentHandler,
CancelAndStopIntentHandler,
Common.HelpIntentHandler,
Common.CancelAndStopIntentHandler,
Common.SessionEndedRequestHandler,
Common.IntentReflectorHandler
)
.addRequestInterceptors(Common.RequestInterceptor)
.addErrorHandlers(
Common.ErrorHandler,
)
.lambda();
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 = 'Try issuing commands like "play" and "pause" to control your tape deck!';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
},
handle(handlerInput) {
// Any cleanup logic goes here.
return handlerInput.responseBuilder.getResponse();
}
};
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("Instruct your tape deck by issuning commands like play and pause")
.getResponse();
}
};
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
const goodbye_sentence = [`Excuse me. I think i didn't understand you.`, "Woops, i think i might not have understood you correctly"];
console.log(`~~~~ Error handled: ${error.stack}`);
const random = Math.floor(Math.random() * goodbye_sentence.length);
const speakOutput = goodbye_sentence[random];
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,
SessionEndedRequestHandler,
IntentReflectorHandler,
ErrorHandler,
RequestInterceptor
};
Comments