Timur TentimishovSerdar Tentimishov
Published © GPL3+

LEGO puppy powered by Amazon Alexa

Teach your LEGO puppy some basic commands like "sit", "stay", "heel", or "speak" using Amazon Alexa.

IntermediateWork in progress5 hours2,242

Things used in this project

Hardware components

Echo Dot
Amazon Alexa Echo Dot
×1
Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
×1
USB-A to Mini-USB Cable
USB-A to Mini-USB Cable
×1
Flash Memory Card, SD Card
Flash Memory Card, SD Card
×1

Software apps and online services

ev3dev - linux os for EV3
Etcher
visual studio EDI from Microsoft
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit

Story

Read more

Schematics

Lego puppy building instructions

Code

Alexa Lambda source code

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

// This skill uses source code provided in https://www.hackster.io/alexagadgets/ 
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');

// Import language strings containing all skill voice messages
// e.g. handlerInput.t('WELCOME_MSG')
const i18n = require('i18next');
const languageStrings = require('./localisation');

// The audio tag to include background music
const DOG_SMALL_BARK_AUDIO = '<audio src="soundbank://soundlibrary/animals/amzn_sfx_dog_small_bark_2x_01"></audio>';

const COMMANDS_LIST = ['COMMAND_REMIND_MSG', 'HELP_COMMAND_MSG'];
// List of welcome greetings
const DOG_COMMAND_LIST = ['COMMAND_SIT_MSG','COMMAND_STAY_MSG','COMMAND_COME_MSG', 'COMMAND_HEEL_MSG', 'COMMAND_SPEAK_MSG'];

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

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

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
        const launchCount = sessionAttributes['launchCount'] || 0;

        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle: async function(handlerInput) {

        const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();

        const request = handlerInput.requestEnvelope;
        const { apiEndpoint, apiAccessToken } = request.context.System;
        const apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
        console.log("apiResponse: " + apiResponse);
        
        let speakOutput = handlerInput.t('NO_EV3_FOUND_MSG');
        if ((apiResponse.endpoints || []).length === 0) {
            return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
        }

        // Store the gadget endpointId to be used in this skill session
        const endpointId = apiResponse.endpoints[0].endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Set skill duration to 5 minutes (ten 30-seconds interval)
        //Util.putSessionAttribute(handlerInput, 'duration', 10);

        // Set the token to track the event handler
        const token = handlerInput.requestEnvelope.request.requestId;
        Util.putSessionAttribute(handlerInput, 'token', token);

        
        const launchCount = sessionAttributes['launchCount'];

        let speechOutput = '';
        if (launchCount === 1) {
            speechOutput = handlerInput.t('WELCOME_GREETING_MSG');
        } else {
            speechOutput = handlerInput.t('WELCOME_GREETING_MSG') + handlerInput.t(randomChoice(DOG_COMMAND_LIST));
        }
        // starting point
        return handlerInput.responseBuilder
            .speak(DOG_SMALL_BARK_AUDIO + speechOutput)
            .withShouldEndSession(false)
            .addDirective(Util.buildStartEventHandler(token,60000, {}))
            .getResponse();
    }
};


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

        let command = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Command');
        if (!command) {
            return handlerInput.responseBuilder
                .speak(handlerInput.t('REPEAT_COMMANDS_MSG'))
                .withShouldEndSession(false)
                .getResponse();
        }

        const attributesManager = handlerInput.attributesManager;
        let endpointId = attributesManager.getSessionAttributes().endpointId || [];

        // Construct the directive with the payload containing the command
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'command',
                command: command
            });
            

        let speechOutput = handlerInput.t('COMMAND_TEXT') + `${command}` + handlerInput.t('ACTIVATED_TEXT');

        if( command === 'speak' || command === 'laut') {
            speechOutput = DOG_SMALL_BARK_AUDIO;
        }
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .addDirective(directive)
            .getResponse();
    }
};

const EventsReceivedRequestHandler = {
    // Checks for a valid token and endpoint.
    canHandle(handlerInput) {
        let { request } = handlerInput.requestEnvelope;
        if (request.type !== 'CustomInterfaceController.EventsReceived') return false;

        const attributesManager = handlerInput.attributesManager;
        let sessionAttributes = attributesManager.getSessionAttributes();
        let customEvent = request.events[0];

        // Validate event token
        if (sessionAttributes.token !== request.token) {
            console.log("Event token doesn't match. Ignoring this event");
            return false;
        }

        // Validate endpoint
        let requestEndpoint = customEvent.endpoint.endpointId;
        if (requestEndpoint !== sessionAttributes.endpointId) {
            console.log("Event endpoint id doesn't match. Ignoring this event");
            return false;
        }
        return true;
    },
    handle(handlerInput) {

        console.log("== Received Custom Event ==");
        let customEvent = handlerInput.requestEnvelope.request.events[0];
        let payload = customEvent.payload;
        console.log(JSON.stringify(payload));
        let name = customEvent.header.name;

        let speechOutput;
        if (name === 'bark') {
            return handlerInput.responseBuilder
                .speak(DOG_SMALL_BARK_AUDIO)
                .withShouldEndSession(false)
                .getResponse();
        } else {
            speechOutput = handlerInput.t('UNKNOWN_EVENT_MSG');
        }
        return handlerInput.responseBuilder
            .speak(handlerInput.t(speechOutput))
            .getResponse();
    }
};
const ExpiredRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'CustomInterfaceController.Expired'
    },
    handle(handlerInput) {

        // Set the token to track the event handler
        const token = handlerInput.requestEnvelope.request.requestId;
        Util.putSessionAttribute(handlerInput, 'token', token);

        const attributesManager = handlerInput.attributesManager;
        let duration = attributesManager.getSessionAttributes().duration || 0;
        if (duration > 0) {
            Util.putSessionAttribute(handlerInput, 'duration', --duration);

            const speechOutput = handlerInput.t(randomChoice(COMMANDS_LIST));
            // Extends skill session
            return handlerInput.responseBuilder
                .addDirective(Util.buildStartEventHandler(token, 60000, {}))
                .speak(speechOutput)
                .getResponse();
        }
        else {
            // End skill session
            return handlerInput.responseBuilder
                .speak(handlerInput.t('GOODBYE_MSG') + DOG_SMALL_BARK_AUDIO)
                .withShouldEndSession(true)
                .getResponse();
        }
    }
};

// 2. Helper Functions ============================================================================

function randomChoice(array) {
  // the argument is an array [] of words or phrases
  const i = Math.floor(Math.random() * array.length);
  return (array[i]);
}

const LocalisationRequestInterceptor = {
    process(handlerInput) {
        i18n.init({
            lng: handlerInput.requestEnvelope.request.locale,
            resources: languageStrings
        }).then((t) => {
            handlerInput.t = (...args) => t(...args);
        });
    }
};

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

LEGO puppy source code

Python
#!/usr/bin/env python3

# -----------------------------------------------------------------------------
# Copyright (c) 2019 Timur Tentimishov <family.tentimishov@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# -----------------------------------------------------------------------------#
# This software demonstrates usage of motors, sensors, buttons, and leds and
# uses source code provided in https://www.hackster.io/alexagadgets/ 

import logging
import json
import threading
import time


from random import choice, randint
from enum import Enum
from agt import AlexaGadget

from ev3dev2.led import Leds
from ev3dev2.sound import Sound
from ev3dev2.motor import OUTPUT_A, SpeedPercent, MediumMotor, OUTPUT_B, OUTPUT_C, MoveTank, LargeMotor
from ev3dev2.sensor.lego import InfraredSensor
from ev3dev2.sensor.lego import TouchSensor
from ev3dev2.display import Display

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


class Command(Enum):
    """
    The list of preset commands and their invocation variation.
    These variations correspond to the skill slot values.
    """
    SENTRY = ['guard', 'protect', 'sentry', 'sentry mode','watch', 'watch mode']
    SIT = ['sitz', 'sit']
    STAY = ['bleib', 'stay', 'steh auf', 'stehen bleiben']
    HEEL = ['fuss', 'heel']
    COME = ['come to me', 'Komm', 'come']
    SPEAK = ['speak', 'laut']

class EventName(Enum):
    """
    The list of custom events sent from this gadget to Alexa
    """
    BARK = "bark"

class MindstormsGadget(AlexaGadget):
    """
    A Mindstorms gadget that performs movement based on voice commands.
    Four types of commands are supported: sit, stay, come, speak, heel.
    """

    def __init__(self):
        """
        Performs Alexa Gadget initialization routines and ev3dev resource allocation.
        """

        super().__init__()

        # Gadget state
        self.heel_mode = False
        self.patrol_mode = False
        self.sitting = False

        # Ev3dev initialization
        self.leds = Leds()
        self.sound = Sound()

        # Connect infrared and touch sensors.
        self.ir = InfraredSensor()
        self.ts = TouchSensor()
        # Init display
        self.screen = Display()

        # Connect medium motor on output port A:
        self.medium_motor = MediumMotor(OUTPUT_A)
        # Connect two large motors on output ports B and C:
        self.left_motor = LargeMotor(OUTPUT_B)
        self.right_motor = LargeMotor(OUTPUT_C)


        # Gadget states
        self.bpm = 0
        self.trigger_bpm = "off"

        # Start threads
        threading.Thread(target=self._patrol_thread, daemon=True).start()
        threading.Thread(target=self._heel_thread, daemon=True).start()
        threading.Thread(target=self._touchsensor_thread, daemon=True).start()

    # ------------------------------------------------
    # Callbacks
    # ------------------------------------------------
    def on_connected(self, device_addr):
        """
        Gadget connected to the paired Echo device.
        :param device_addr: the address of the device we connected to
        """
        print("{} Connected to Echo device".format(self.friendly_name))

        # Draw blinking eyes of the puppy
        threading.Thread(target=self._draweyes, daemon=True).start()

        # Turn lights on:
        for light in ('LEFT', 'RIGHT'):
            self.leds.set_color(light, 'GREEN')


    def on_disconnected(self, device_addr):
        """
        Gadget disconnected from the paired Echo device.
        :param device_addr: the address of the device we disconnected from
        """
        # Turn lights off:
        for light in ('LEFT', 'RIGHT'):
            self.leds.set_color(light, 'BLACK')

    def on_custom_mindstorms_gadget_control(self, directive):
        """
        Handles the Custom.Mindstorms.Gadget control directive.
        :param directive: the custom directive with the matching namespace and name
        """
        try:
            payload = json.loads(directive.payload.decode("utf-8"))
            print("Control payload: {}".format(payload))
            control_type = payload["type"]

            if control_type == "command":
                # Expected params: [command]
                self._activate(payload["command"])
            
        except KeyError:
            print("Missing expected parameters: {}".format(directive))


    # On Amazon music play
    def on_alexa_gadget_musicdata_tempo(self, directive):
        """
        Provides the music tempo of the song currently playing on the Echo device.
        :param directive: the music data directive containing the beat per minute value
        """
        tempo_data = directive.payload.tempoData
        for tempo in tempo_data:

            print("tempo value: {}".format(tempo.value))
            if tempo.value > 0:
                # dance pose
                #self.drive.on_for_seconds(SpeedPercent(5), SpeedPercent(25), 1)
                self.right_motor.run_timed(speed_sp=750, time_sp=2500)
                self.left_motor.run_timed(speed_sp=-750, time_sp=2500)
                # shake ev3 head
                threading.Thread(target=self._sitdown).start()
                
                self.leds.set_color("LEFT", "GREEN")
                self.leds.set_color("RIGHT", "GREEN")
                time.sleep(3)
                # starts the dance loop
                self.trigger_bpm = "on"
                threading.Thread(target=self._dance_loop, args=(tempo.value,)).start()

            elif tempo.value == 0:
                # stops the dance loop
                self.trigger_bpm = "off"
                self.leds.set_color("LEFT", "BLACK")
                self.leds.set_color("RIGHT", "BLACK")

    def _dance_loop(self, bpm):
        """
        Perform motor movement in sync with the beat per minute value from tempo data.
        :param bpm: beat per minute from AGT
        """
        color_list = ["GREEN", "RED", "AMBER", "YELLOW"]
        led_color = random.choice(color_list)
        motor_speed = 400
        milli_per_beat = min(1000, (round(60000 / bpm)) * 0.65)
        print("Adjusted milli_per_beat: {}".format(milli_per_beat))
        while self.trigger_bpm == "on":

            # Alternate led color and motor direction
            led_color = "BLACK" if led_color != "BLACK" else random.choice(color_list)
            motor_speed = -motor_speed

            self.leds.set_color("LEFT", led_color)
            self.leds.set_color("RIGHT", led_color)

            self.right_motor.run_timed(speed_sp=motor_speed, time_sp=150)
            self.left_motor.run_timed(speed_sp=-motor_speed, time_sp=150)
            time.sleep(milli_per_beat / 1000)

            self.left_motor.run_timed(speed_sp=-motor_speed, time_sp=150)
            self.right_motor.run_timed(speed_sp=motor_speed, time_sp=150)
            time.sleep(milli_per_beat / 1000)

            self.right_motor.run_timed(speed_sp=350, time_sp=300)
            self.left_motor.run_timed(speed_sp=-350, time_sp=300)
            time.sleep(milli_per_beat / 1000)

            self.right_motor.run_timed(speed_sp=motor_speed, time_sp=150)
            self.left_motor.run_timed(speed_sp=-motor_speed, time_sp=150)
            time.sleep(milli_per_beat / 1000)

    def _move(self, direction, duration: int, speed: int, is_blocking=False):
        """
        Handles move commands from the directive.
        Right and left movement can under or over turn depending on the surface type.
        :param direction: the move direction
        :param duration: the duration in seconds
        :param speed: the speed percentage as an integer
        :param is_blocking: if set, motor run until duration expired before accepting another command
        """
        print("Move command: ({}, {}, {}, {})".format(direction, speed, duration, is_blocking))
        if direction in Direction.FORWARD.value:
            #self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(speed), duration, block=is_blocking)
                self.right_motor.run_timed(speed_sp=-750, time_sp=2500)
                self.left_motor.run_timed(speed_sp=-750, time_sp=2500)

        if direction in Direction.BACKWARD.value:
            #self.drive.on_for_seconds(SpeedPercent(-speed), SpeedPercent(-speed), duration, block=is_blocking)
            self.right_motor.run_timed(speed_sp=750, time_sp=2500)
            self.left_motor.run_timed(speed_sp=750, time_sp=2500)

        if direction in (Direction.RIGHT.value + Direction.LEFT.value):
            self._turn(direction, speed)
            #self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(speed), duration, block=is_blocking)
            self.right_motor.run_timed(speed_sp=750, time_sp=2500)
            self.left_motor.run_timed(speed_sp=-750, time_sp=2500)

        if direction in Direction.STOP.value:
            #self.drive.off()
            self.right_motor.stop
            self.left_motor.stop
            self.heel_mode = False
            self.patrol_mode = False

    def _activate(self, command):
        """
        Handles preset commands.
        :param command: the preset command
        """
        print("Activate command: ({}".format(command))
        if command in Command.COME.value:
            #call _come method
            self.right_motor.run_timed(speed_sp=750, time_sp=2500)
            self.left_motor.run_timed(speed_sp=50, time_sp=100)

        if command in Command.HEEL.value:
            #call _hell method
            self.heel_mode = True

        if command in Command.SIT.value:
            # call _sit method
            self.heel_mode = False
            self._sitdown()

        if command in Command.STAY.value:
            # call _stay method
            self.heel_mode = False
            self._standup()


    def _turn(self, direction, speed):
        """
        Turns based on the specified direction and speed.
        Calibrated for hard smooth surface.
        :param direction: the turn direction
        :param speed: the turn speed
        """
        if direction in Direction.LEFT.value:
            #self.drive.on_for_seconds(SpeedPercent(0), SpeedPercent(speed), 2)
            self.right_motor.run_timed(speed_sp=0, time_sp=100)
            self.left_motor.run_timed(speed_sp=750, time_sp=100)

        if direction in Direction.RIGHT.value:
            #self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(0), 2)
            self.right_motor.run_timed(speed_sp=750, time_sp=100)
            self.left_motor.run_timed(speed_sp=0, time_sp=100)

    def _send_event(self, name: EventName, payload):
        """
        Sends a custom event to trigger a sentry action.
        :param name: the name of the custom event
        :param payload: the sentry JSON payload
        """
        self.send_custom_event('Custom.Mindstorms.Gadget', name.value, payload)

    def _heel_thread(self):
        """
        Monitors the distance between the puppy and an obstacle when heel command called.
        If the maximum distance is breached, decrease the distance by following an obstancle
        """
        while True:
            while self.heel_mode:
                distance = self.ir.proximity
                print("Proximity distance: {}".format(distance))
                # keep distance and make step back from the object
                if distance < 45:  
                    threading.Thread(target=self.__movebackwards).start()
                    self._send_event(EventName.BARK, {'distance': distance})
                    # follow the object
                if distance > 60:
                    threading.Thread(target=self.__moveforwards).start()
                    # otherwise stay still
                else: 
                    threading.Thread(target=self.__stay).start()
                time.sleep(0.2)
            time.sleep(1)

    def _touchsensor_thread(self):
        print("Touch sensor activated")
        while True:
            if self.ts.is_pressed:
                self.leds.set_color("LEFT", "RED")
                self.leds.set_color("RIGHT", "RED")
                if (self.sitting):
                    threading.Thread(target=self._standup).start()
                    self.sitting = False
                else:
                    threading.Thread(target=self._sitdown).start()
                    self.sitting = True
            else:
                self.leds.set_color("LEFT", "GREEN")
                self.leds.set_color("RIGHT", "GREEN")

    def _sitdown(self):
        self.medium_motor.on_for_rotations(SpeedPercent(20), 0.5)

    def _standup(self):
        # run the wheels backwards to help the puppy to stand up.
        threading.Thread(target=self.__back).start()
        self.medium_motor.on_for_rotations(SpeedPercent(50), -0.5)

    def __back(self):
        self.right_motor.run_timed(speed_sp=-350, time_sp=1000)
        self.left_motor.run_timed(speed_sp=-350, time_sp=1000)

    def __movebackwards(self):
        self.right_motor.run_timed(speed_sp=-650, time_sp=1000)
        self.left_motor.run_timed(speed_sp=-650, time_sp=1000)

    def __moveforwards(self):
        self.right_motor.run_timed(speed_sp=650, time_sp=1000)
        self.left_motor.run_timed(speed_sp=650, time_sp=1000)

    def __stay(self):
        self.right_motor.run_timed(speed_sp=0, time_sp=1000)
        self.left_motor.run_timed(speed_sp=0, time_sp=1000)

    def _draweyes(self):
        close = True

        while True:
            self.screen.clear()

            if close:
                #self.screen.draw.ellipse(( 5, 30,  75, 50),fill='white')
                #self.screen.draw.ellipse((103, 30, 173, 50), fill='white')
                self.screen.draw.rectangle(( 5, 60,  75, 50), fill='black')
                self.screen.draw.rectangle((103, 60, 173, 50), fill='black')
                
                #self.screen.draw.rectangle(( 5, 30,  75, 50), fill='black')
                #self.screen.draw.rectangle((103, 30, 173, 50), fill='black')
                time.sleep(10)
            else:
                #self.screen.draw.ellipse(( 5, 30,  75, 100))
                #self.screen.draw.ellipse((103, 30, 173, 100))
                #self.screen.draw.ellipse(( 35, 30,  105, 30),fill='black')
                #self.screen.draw.ellipse((133, 30, 203, 30), fill='black')
                self.screen.draw.rectangle(( 5, 10,  75, 100), fill='black')
                self.screen.draw.rectangle((103, 10, 173, 100), fill='black')

            close = not close  # toggle between True and False

            # Update screen display
            # Applies pending changes to the screen.
            # Nothing will be drawn on the screen screen
            # until this function is called.
            self.screen.update() 
            time.sleep(1)


    def _patrol_thread(self):
        """
        Performs random movement when patrol mode is activated.
        """
        while True:
            while self.patrol_mode:
                print("Patrol mode activated randomly picks a path")
                direction = random.choice(list(Direction))
                duration = random.randint(1, 5)
                speed = random.randint(1, 4) * 25

                while direction == Direction.STOP:
                    direction = random.choice(list(Direction))

                # direction: all except stop, duration: 1-5s, speed: 25, 50, 75, 100
                self._move(direction.value[0], duration, speed)
                time.sleep(duration)
            time.sleep(1)



if __name__ == '__main__':

    # Startup sequence
    gadget = MindstormsGadget()
    # Gadget main entry point
    gadget.main()

Lego puppy ini file

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

[GadgetSettings]
amazonId = YOUR_AMAZON_ID
alexaGadgetSecret = _YOUR_AMAZON_SECRET

[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0
Alexa.Gadget.MusicData = 1.0 - tempo
Alexa.Gadget.SpeechData = 1.0 - viseme
Alexa.Gadget.StateListener = 1.0 - timeinfo,  wakeword

Alexa Lambda source code

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

'use strict'

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

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = handlerInput.t('HELP_COMMAND_MSG');

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

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

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

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

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

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

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


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

Alexa Lambda source code

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

'use strict';

const Https = require('https');
const AWS = require('aws-sdk');
const Escape = require('lodash/escape');

const s3SigV4Client = new AWS.S3({
    signatureVersion: 'v4'
});

/**
 * Get the authenticated URL to access the S3 Object. This URL expires after 60 seconds.
 * @param s3ObjectKey - the S3 object key
 * @returns {string} the pre-signed S3 URL
 */
exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) {

    const bucketName = process.env.S3_PERSISTENCE_BUCKET;
    return Escape(s3SigV4Client.getSignedUrl('getObject', {
        Bucket: bucketName,
        Key: s3ObjectKey,
        Expires: 60 // the Expires is capped for 1 minute
    }));
};

/**
 * Builds a directive to start the EventHandler.
 * @param token - a unique identifier to track the event handler
 * @param {number} timeout - the duration to wait before sending back the expiration
 * payload to the skill.
 * @param payload - the expiration json payload
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/receive-custom-event-from-gadget.html#start}
 */
exports.buildStartEventHandler = function (token, timeout = 30000, payload)  {
    return {
        type: "CustomInterfaceController.StartEventHandler",
        token: token,
        expiration : {
            durationInMilliseconds: timeout,
            expirationPayload: payload
        }
    };
};

/**
 *
 * Builds a directive to stops the active event handler.
 * The event handler is identified by the cached token in the session attribute.
 * @param {string} handlerInput - the JSON payload from Alexa Service
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/receive-custom-event-from-gadget.html#stop}
 */
exports.buildStopEventHandlerDirective = function (handlerInput) {

    let token = handlerInput.attributesManager.getSessionAttributes().token || '';
    return {
        "type": "CustomInterfaceController.StopEventHandler",
        "token": token
    }
};

/**
 * Build a custom directive payload to the gadget with the specified endpointId
 * @param {string} endpointId - the gadget endpoint Id
 * @param {string} namespace - the namespace of the skill
 * @param {string} name - the name of the skill within the scope of this namespace
 * @param {object} payload - the payload data
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/send-gadget-custom-directive-from-skill.html#respond}
 */
exports.build = function (endpointId, namespace, name, payload) {
    // Construct the custom directive that needs to be sent
    // Gadget should declare the capabilities in the discovery response to
    // receive the directives under the following namespace.
    return {
        type: 'CustomInterfaceController.SendDirective',
        header: {
            name: name,
            namespace: namespace
        },
        endpoint: {
            endpointId: endpointId
        },
        payload
    };
};

/**
 * A convenience routine to add the a key-value pair to the session attribute.
 * @param handlerInput - the handlerInput from Alexa Service
 * @param key - the key to be added
 * @param value - the value be added
 */
exports.putSessionAttribute = function(handlerInput, key, value) {
    const attributesManager = handlerInput.attributesManager;
    let sessionAttributes = attributesManager.getSessionAttributes();
    sessionAttributes[key] = value;
    attributesManager.setSessionAttributes(sessionAttributes);
};

/**
 * To get a list of all the gadgets that meet these conditions,
 * Call the Endpoint Enumeration API with the apiEndpoint and apiAccessToken to
 * retrieve the list of all connected gadgets.
 *
 * @param {string} apiEndpoint - the Endpoint API url
 * @param {string} apiAccessToken  - the token from the session object in the Alexa request
 * @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/send-gadget-custom-directive-from-skill.html#call-endpoint-enumeration-api}
 */
exports.getConnectedEndpoints = function(apiEndpoint, apiAccessToken) {

    // The preceding https:// need to be stripped off before making the call
    apiEndpoint = (apiEndpoint || '').replace('https://', '');

    return new Promise(((resolve, reject) => {

        const options = {
            host: apiEndpoint,
            path: '/v1/endpoints',
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + apiAccessToken
            }
        };

        const request = Https.request(options, (response) => {
            response.setEncoding('utf8');
            let returnData = '';
            response.on('data', (chunk) => {
                returnData += chunk;
            });

            response.on('end', () => {
                resolve(JSON.parse(returnData));
            });

            response.on('error', (error) => {
                reject(error);
            });
        });
        request.end();
    }));
};

Alexa Lambda source code

JSON
features json file
{
    "name": "agt-mindstorms",
    "version": "1.1.0",
    "description": "A skill which uses AGT with Lego Mindstorms",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Amazon Alexa",
    "license": "ISC",
    "dependencies": {
      "ask-sdk-core": "^2.6.0",
      "ask-sdk-model": "^1.18.0",
      "aws-sdk": "^2.326.0",
      "request": "^2.81.0",
      "lodash": "^4.17.11",
      "i18next": "^15.0.5"
   }
  }
  

Alexa Lambda source code

JSON
Alexa Skill model
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "lego puppy",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": [
                        "finish",
                        "ciao",
                        "bye",
                        "stop"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "SetCommandIntent",
                    "slots": [
                        {
                            "name": "Command",
                            "type": "CommandType"
                        }
                    ],
                    "samples": [
                        "{Command}",
                        "do {Command}"
                    ]
                }
            ],
            "types": [
                {
                    "name": "CommandType",
                    "values": [
                        {
                            "name": {
                                "value": "speak"
                            }
                        },
                        {
                            "name": {
                                "value": "heel"
                            }
                        },
                        {
                            "name": {
                                "value": "go back"
                            }
                        },
                        {
                            "name": {
                                "value": "come to me"
                            }
                        },
                        {
                            "name": {
                                "value": "come"
                            }
                        },
                        {
                            "name": {
                                "value": "stay"
                            }
                        },
                        {
                            "name": {
                                "value": "sit"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Credits

Timur Tentimishov

Timur Tentimishov

2 projects • 2 followers
Serdar Tentimishov

Serdar Tentimishov

1 project • 0 followers

Comments