Jonathan Wolter
Created December 30, 2019 © GPL3+

Retro Robot

A robot used to make old things "smart". Connect your lego with an Alexa and enjoy your ancient technology in a brand new way.

IntermediateShowcase (no instructions)1 hour170

Things used in this project

Hardware components

Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
×1
An "ancient" object
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
Alexa Gadgets Toolkit
Amazon Alexa Alexa Gadgets Toolkit

Hand tools and fabrication machines

Your Imagination

Story

Read more

Schematics

S9imple Doodle of how my Idea works

like a parody

Code

RetrRobot.py

Python
python script to be run on the ev3-Brick
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")

RetroRobot.ini

Python
The .ini-file that is required to run RetroRobot.py
[GadgetSettings]
amazonId = YourGadgetID
alexaGadgetSecret = YourAlexaGadgetSecret

[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0 - wakeword

model.json

JSON
Intents
{
    "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?"
                    }
                ]
            }
        ]
    }
}

package.json

JSON
Skill-component
{
    "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"
    }
  }
  

util.js

JavaScript
Skil-lcode
'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");
    }));
};

index.js

JavaScript
Main file (skill-code)
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();

common.js

JavaScript
Handles most of the Amazon-Standard-Intents
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
    };

Credits

Jonathan Wolter

Jonathan Wolter

1 project • 1 follower

Comments