Kyle
Published © GPL3+

DWR - An Alexa voice enabled maze-solving LEGO robot

Use your voice to guide DWR through a re-configurable Lego maze to learn complex, multi-step instructions and spatial orientation concepts.

IntermediateFull instructions provided4 hours54

Things used in this project

Hardware components

EV3 Programming Brick / Kit
LEGO MindStorms EV3 Programming Brick / Kit
×1
LEGO 16x16 plate - 6004927
×16
LEGO 2x4 Brick - 4114319
This is enough pieces to build the surrounding wall plus four maze walls. Obviously any color combinations will work.
×562
LEGO 2x2 Brick - 4114306
Any color will work.
×24
LEGO 2x2 Flat Tile - 306801
I used mostly white tiles however you can mix and match colors to create interesting designs and or incorporate a color sensor into the maze design.
×1024
Edimax EW-7811Un N150 Wireless USB Adapter Nano
×1

Software apps and online services

ev3dev

Story

Read more

Code

model.json

JSON
This is the JSON model describing the Amazon Alexa skill for the voice command of DWR
{
  "interactionModel": {
    "languageModel": {
      "invocationName": "d. w. r.",
      "intents": [
        {
          "name": "AMAZON.CancelIntent",
          "samples": []
        },
        {
          "name": "AMAZON.HelpIntent",
          "samples": []
        },
        {
          "name": "AMAZON.StopIntent",
          "samples": []
        },
        {
          "name": "AMAZON.NavigateHomeIntent",
          "samples": []
        },
        {
          "name": "MoveIntent",
          "slots": [
            {
              "name": "One",
              "type": "CommandType"
            },
            {
              "name": "Two",
              "type": "CommandType"
            },
            {
              "name": "Three",
              "type": "CommandType"
            },
            {
              "name": "Four",
              "type": "CommandType"
            },
            {
              "name": "Five",
              "type": "CommandType"
            },
            {
              "name": "Six",
              "type": "CommandType"
            },
            {
              "name": "Seven",
              "type": "CommandType"
            },
            {
              "name": "Eight",
              "type": "CommandType"
            },
            {
              "name": "Nine",
              "type": "CommandType"
            },
            {
              "name": "Ten",
              "type": "CommandType"
            },
            {
              "name": "A",
              "type": "DistanceType"
            },
            {
              "name": "B",
              "type": "DistanceType"
            },
            {
              "name": "C",
              "type": "DistanceType"
            },
            {
              "name": "D",
              "type": "DistanceType"
            },
            {
              "name": "E",
              "type": "DistanceType"
            },
            {
              "name": "F",
              "type": "DistanceType"
            },
            {
              "name": "G",
              "type": "DistanceType"
            },
            {
              "name": "H",
              "type": "DistanceType"
            },
            {
              "name": "I",
              "type": "DistanceType"
            },
            {
              "name": "J",
              "type": "DistanceType"
            }
          ],
          "samples": [
            "go {One} {A}",
            "go {One} {A} then {Two} {B} ",
            "go {One} {A} then {Two} {B} then {Three} {C}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D} then {Five} {E}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D} then {Five} {E} then {Six} {F}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D} then {Five} {E} then {Six} {F} then {Seven} {G}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D} then {Five} {E} then {Six} {F} then {Seven} {G} then {Eight} {H}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D} then {Five} {E} then {Six} {F} then {Seven} {G} then {Eight} {H} then {Nine} {I}",
            "go {One} {A} then {Two} {B} then {Three} {C} then {Four} {D} then {Five} {E} then {Six} {F} then {Seven} {G} then {Eight} {H} then {Nine} {I} then {Ten} {J}",
            "go {One}",
            "go {One} then {Two}",
            "go {One} then {Two} then {Three}",
            "go {One} then {Two} then {Three} then {Four}",
            "go {One} then {Two} then {Three} then {Four} then {Five}",
            "go {One} then {Two} then {Three} then {Four} then {Five} then {Six}",
            "go {One} then {Two} then {Three} then {Four} then {Five} then {Six} then {Seven}",
            "go {One} then {Two} then {Three} then {Four} then {Five} then {Six} then {Seven} then {Eight}",
            "go {One} then {Two} then {Three} then {Four} then {Five} then {Six} then {Seven} then {Eight} then {Nine}",
            "go {One} then {Two} then {Three} then {Four} then {Five} then {Six} then {Seven} then {Eight} then {Nine} then {Ten}"
          ]
        }
      ],
      "types": [
        {
          "name": "DistanceType",
          "values": [
            {
              "name": {
                "value": "one"
              }
            },
            {
              "name": {
                "value": "two"
              }
            },
            {
              "name": {
                "value": "three"
              }
            },
            {
              "name": {
                "value": "four"
              }
            }
          ]
        },
        {
          "name": "CommandType",
          "values": [
            {
              "name": {
                "value": "none"
              }
            },
            {
              "name": {
                "value": "brake"
              }
            },
            {
              "name": {
                "value": "go backward"
              }
            },
            {
              "name": {
                "value": "go forward"
              }
            },
            {
              "name": {
                "value": "go right"
              }
            },
            {
              "name": {
                "value": "go left"
              }
            },
            {
              "name": {
                "value": "turn right"
              }
            },
            {
              "name": {
                "value": "turn left"
              }
            },
            {
              "name": {
                "value": "right"
              }
            },
            {
              "name": {
                "value": "left"
              }
            },
            {
              "name": {
                "value": "backwards"
              }
            },
            {
              "name": {
                "value": "backward"
              }
            },
            {
              "name": {
                "value": "forwards"
              }
            },
            {
              "name": {
                "value": "forward"
              }
            }
          ]
        }
      ]
    }
  }
}

robo-labrynth.py

Python
This python script runs on the LEGO Mindstorms EV3 brick running ev3dev os, receives JSON directives from an Alexa Skill and runs the Mindstorms motors to complete the commands in the directive.
#!/usr/bin/env python3

import time
import logging
import json
import random
import threading
import string

from agt import AlexaGadget

from ev3dev2.led import Leds
from ev3dev2.sound import Sound
from ev3dev2.motor import OUTPUT_A, OUTPUT_D, MoveTank 

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

lower_case = string.ascii_lowercase[:10] 

class MindstormsGadget(AlexaGadget):

    def __init__(self):

        super().__init__()
        
        self.leds = Leds()
        self.sound = Sound()
        self.tank = MoveTank(OUTPUT_A, 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
        """
        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):
        """
        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 _forward(self, dur):
        self.tank.on_for_rotations(50,50,1.3*dur)

    def _backward(self, dur):
        self.tank.on_for_rotations(-50,-50,1.3*dur)

    def _turn_right(self):
        self.tank.on_for_rotations(-50,-50,1.)
        self.tank.on_for_rotations(50,10,1.7)
        self.tank.on_for_rotations(-50,-50,.15)

    def _turn_left(self):
        self.tank.on_for_rotations(-50,-50,1.)
        self.tank.on_for_rotations(10,50,1.8)
        self.tank.on_for_rotations(-50,-50,.15)
        
    def on_custom_mindstorms_gadget_control(self, directive):
        payload = json.loads(directive.payload.decode("utf-8"))
        print("Control payload: {}".format(payload))
        try:
            for i, idx in enumerate(range(1,11)):
                idx_str = str(idx)
                duration = 1
                duration = payload[lower_case[i]]
                if duration == 'to':
                    duration = 2
                duration = int(duration)
                if payload[idx_str] in ['none']:
                    pass
                if payload[idx_str] in ['left','go left','turn left']:
                    self._turn_left()
                if payload[idx_str] in ['right','go right','turn right']:
                    self._turn_right()
                if payload[idx_str] in ['forward', 'go forward']:
                    self._forward(duration)
                if payload[idx_str] in ['backward', 'back', 'go backward']:
                    self._backward(duration)

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


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

    # Gadget main entry point
    gadget.main()

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

index.js

JavaScript
This is the main lambda javascript file that sends the directive to the EV3 gadget
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');

// The namespace of the custom directive to be sent by this skill
const NAMESPACE = 'Custom.Mindstorms.Gadget';

// The name of the custom directive to be sent this skill
const NAME_CONTROL = 'control';

const 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();
    }
};

// Construct and send a custom directive to the connected gadget with
// data from the MoveIntent request.
const MoveIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const one = Alexa.getSlotValue(request, 'One');

        // All others are optional, use default if not available
        const two = Alexa.getSlotValue(request, 'Two') || "none";
        const three = Alexa.getSlotValue(request, 'Three') || "none";
        const four = Alexa.getSlotValue(request, 'Four') || "none";
        const five = Alexa.getSlotValue(request, 'Five') || "none";
        const six = Alexa.getSlotValue(request, 'Six') || "none";
        const seven = Alexa.getSlotValue(request, 'Seven') || "none";
        const eight = Alexa.getSlotValue(request, 'Eight') || "none";
        const nine = Alexa.getSlotValue(request, 'Nine') || "none";
        const ten = Alexa.getSlotValue(request, 'Ten') || "none";
        const a = Alexa.getSlotValue(request, 'A') || "1";
        const b = Alexa.getSlotValue(request, 'B') || "1";
        const c = Alexa.getSlotValue(request, 'C') || "1";
        const d = Alexa.getSlotValue(request, 'D') || "1";
        const e = Alexa.getSlotValue(request, 'E') || "1";
        const f = Alexa.getSlotValue(request, 'F') || "1";
        const g = Alexa.getSlotValue(request, 'G') || "1";
        const h = Alexa.getSlotValue(request, 'H') || "1";
        const i = Alexa.getSlotValue(request, 'I') || "1";
        const j = Alexa.getSlotValue(request, 'J') || "1";

        // Get data from session attribute
        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,
            {
                type: 'move',
                1: one,
                2: two,
                3: three,
                4: four,
                5: five,
                6: six,
                7: seven,
                8: eight,
                9: nine,
                10: ten,
                a: a,
                b: b,
                c: c,
                d: d,
                e: e,
                f: f,
                g: g,
                h: h,
                i: i,
                j: j
            });

//        const speechOutput = `ok, doing ${one} then ${two} then ${three} then ${four} then ${five} then ${six} then ${seven} then ${eight} then ${nine} then ${ten}`;
        const speechOutput = `ok, here we go`;

        return handlerInput.responseBuilder
            .speak(speechOutput)
            .reprompt("awaiting command")
            .addDirective(directive)
            .getResponse();
    }
};

// The SkillBuilder acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        MoveIntentHandler,
        Common.HelpIntentHandler,
        Common.CancelAndStopIntentHandler,
        Common.SessionEndedRequestHandler,
        Common.IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
    )
    .addRequestInterceptors(Common.RequestInterceptor)
    .addErrorHandlers(
        Common.ErrorHandler,
    )
    .lambda();

common.js

JavaScript
Generic event handling methods for the Alexa skill
/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * You may not use this file except in compliance with the terms and conditions 
 * set forth in the accompanying LICENSE.TXT file.
 *
 * THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH 
 * RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
*/

'use strict'

const Alexa = require('ask-sdk-core');

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse();
    }
};

// The intent reflector is used for interaction model testing and debugging.
// It will simply repeat the intent the user said. You can create custom handlers
// for your intents by defining them above, then also adding them to the request
// handler chain below.
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt("I don't understand this command, try again")
            .getResponse();
    }
};

// Generic error handling to capture any syntax or routing errors. If you receive an error
// stating the request handler chain is not found, you have not implemented a handler for
// the intent being invoked or included it in the skill builder below.
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`~~~~ Error handled: ${error.stack}`);
        const speakOutput = `Sorry, I had trouble doing what you asked. Please try again.`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

// The request interceptor is used for request handling testing and debugging.
// It will simply log the request in raw json format before any processing is performed.
const RequestInterceptor = {
    process(handlerInput) {
        let { attributesManager, requestEnvelope } = handlerInput;
        let sessionAttributes = attributesManager.getSessionAttributes();

        // Log the request for debug purposes.
        console.log(`=====Request==${JSON.stringify(requestEnvelope)}`);
        console.log(`=========SessionAttributes==${JSON.stringify(sessionAttributes, null, 2)}`);
    }
};

module.exports = {
    HelpIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler,
    IntentReflectorHandler,
    ErrorHandler,
    RequestInterceptor
    };

util.js

JavaScript
Alexa skill utilities
/*
 * 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();
    }));
};

package.json

JSON
Alexa skill package description json
{
  "name": "agt-mindstorms",
  "version": "1.1.0",
  "description": "A sample skill demonstrating how to use AGT with Lego Mindstorms",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "ISC",
  "dependencies": {
    "ask-sdk-core": "^2.6.0",
    "ask-sdk-model": "^1.18.0",
    "aws-sdk": "^2.326.0",
    "request": "^2.81.0"
  }
}

Credits

Kyle

Kyle

1 project • 1 follower

Comments