Timur TentimishovSerdar Tentimishov
Published © GPL3+

Lego R2D2 powered by Alexa

Alexa powered R2D2 droid made of LEGO Mindstorms - your loyal friend in the whole galaxy. It sings and dances for you and makes you smile.

IntermediateFull instructions provided6 hours1,143
Lego R2D2 powered by Alexa

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
USB cable (the one that comes with the EV3)
×1
Flash Memory Card, SD Card
Flash Memory Card, SD Card
2GB or larger
×1

Software apps and online services

ev3dev
Linux based EV3DEV project
Etcher

Hand tools and fabrication machines

visual studio EDI from Microsoft

Story

Read more

Schematics

Lego R2D2 building instructions

Building instructions by Philippe Hurbain

Code

Lego R2D2 source code

Python
Use this python code to control EV3
#!/usr/bin/env python3
# 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.

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_A, OUTPUT_B, OUTPUT_C, MoveTank, SpeedPercent, MediumMotor, LargeMotor
from ev3dev2.sensor.lego import InfraredSensor
from ev3dev2.sensor.lego import TouchSensor

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


class Direction(Enum):
    """
    The list of directional commands and their variations.
    These variations correspond to the skill slot values.
    """
    FORWARD = ['forward', 'forwards', 'go forward','vor','vorwrts', 'gerade aus','fahre']
    BACKWARD = ['back', 'backward', 'backwards', 'go backward','rckwards','fahre zurck','zurck']
    LEFT = ['left', 'go left', 'links','nach links']
    RIGHT = ['right', 'go right', 'rechts','nach rechts']
    STOP = ['stop', 'brake','halt', 'anhalten']

class Command(Enum):
    """
    The list of preset commands and their invocation variation.
    These variations correspond to the skill slot values.
    """
    MOVE_CIRCLE = ['circle', 'spin']
    MOVE_SQUARE = ['square']
    PATROL = ['patrol']
    FIRE_ONE = ['fire', 'shot', '1 shot', 'one shot','feuer','schieen']
    FIRE_ALL = ['all shot']
    SING = ['song','sing','sing a song']
    SECOND_SONG = ['second song']
    SENTRY = ['guard', 'protect', 'sentry', 'sentry mode','watch', 'watch mode']

class EventName(Enum):
    """
    The list of custom events sent from this gadget to Alexa
    """
    SENTRY = "Sentry"
    PROXIMITY = "Proximity"
    SPEECH = "Speech"

class MindstormsGadget(AlexaGadget):
    """
    A Mindstorms gadget that performs movement based on voice commands.
    Two types of commands are supported, directional movement and preset.
    """

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

        super().__init__()

        # Gadget state
        self.sentry_mode = False
        self.patrol_mode = False

        # Ev3dev initialization
        self.leds = Leds()
        self.sound = Sound()
        self.ir = InfraredSensor()
        self.ts = TouchSensor()
        self.head = MediumMotor(OUTPUT_A)
        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._proximity_thread, daemon=True).start()
        threading.Thread(target=self._touchsensor_thread, daemon=True).start()



    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")
        # shake ev3 head
        threading.Thread(target=self._shakehead).start()
        self.sound.speak('R2D2')


    def on_disconnected(self, device_addr):
        """
        Gadget disconnected from the paired Echo device.
        :param device_addr: the address of the device we disconnected from
        """
        self.leds.set_color("LEFT", "BLACK")
        self.leds.set_color("RIGHT", "BLACK")
        print("{} disconnected from Echo device".format(self.friendly_name))

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


            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._shakehead).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.sentry_mode = False
            self.patrol_mode = False

    def _activate(self, command, speed=50):
        """
        Handles preset commands.
        :param command: the preset command
        :param speed: the speed if applicable
        """
        print("Activate command: ({}, {})".format(command, speed))
        if command in Command.MOVE_CIRCLE.value:
            #self.drive.on_for_seconds(SpeedPercent(int(speed)), SpeedPercent(5), 12)
            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.MOVE_SQUARE.value:
            for i in range(4):
                self._move("right", 2, speed, is_blocking=True)

        if command in Command.PATROL.value:
            # Set patrol mode to resume patrol thread processing
            self.patrol_mode = True
            self._send_event(EventName.SPEECH, {'speechOut': "PATROL_MODE_ACTIVATED_MSG"})

        if command in Command.FIRE_ONE.value:
            self.head.on_for_rotations(SpeedPercent(100), 3)

        if command in Command.FIRE_ALL.value:
            self.head.on_for_rotations(SpeedPercent(100), 10)

        if command in Command.SING.value:
            threading.Thread(target=self._marchaimp).start()

        if command in Command.SECOND_SONG.value:
            threading.Thread(target=self._smallSong).start()

        if command in Command.SENTRY.value:
            self.sentry_mode = True
            self._send_event(EventName.SPEECH, {'speechOut': "SENTRY_MODE_ACTIVATED_MSG"})


    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 _proximity_thread(self):
        """
        Monitors the distance between the robot and an obstacle when sentry mode is activated.
        If the minimum distance is breached, send a custom event to trigger action on
        the Alexa skill.
        """
        count = 0
        while True:
            while self.sentry_mode:
                distance = self.ir.proximity
                print("Proximity: {}".format(distance))
                count = count + 1 if distance < 10 else 0
                if count > 3:
                    print("Proximity breached. Sending event to skill")
                    self.leds.set_color("LEFT", "RED", 1)
                    self.leds.set_color("RIGHT", "RED", 1)

                    self._send_event(EventName.PROXIMITY, {'distance': distance})
                    self.sentry_mode = False

                time.sleep(0.2)
            time.sleep(1)

    def _touchsensor_thread(self):
        print("Touch sensor activated")
        while True:
            if self.ts.is_pressed:
                self._send_event(EventName.SPEECH, {'speechOut': "SENSOR_TOUCH_ACTIVATED_MSG"})
                self.leds.set_color("LEFT", "RED")
                self.leds.set_color("RIGHT", "RED")
            else:
                self.leds.set_color("LEFT", "GREEN")
                self.leds.set_color("RIGHT", "GREEN")

    def _shakehead(self):
        # turn robot head left and right
        self.head.on_for_rotations(SpeedPercent(30), 0.3)
        self.head.on_for_rotations(SpeedPercent(30), -0.3)

    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)


    def _cabezaderecha(self):
        self.head.run_forever(speed=450)


    def _cabezaizquierda(self):
        self.head.run_forever(speed=-450)


    def _marchaimp(self):
        self.sound.tone([
        (392, 350, 100), (392, 350, 100), (392, 350, 100), (311.1, 250, 100),
        (466.2, 25, 100), (392, 350, 100), (311.1, 250, 100), (466.2, 25, 100),
        (392, 700, 100), (587.32, 350, 100), (587.32, 350, 100),
        (587.32, 350, 100), (622.26, 250, 100), (466.2, 25, 100),
        (369.99, 350, 100), (311.1, 250, 100), (466.2, 25, 100), (392, 700, 100),
        (784, 350, 100), (392, 250, 100), (392, 25, 100), (784, 350, 100),
        (739.98, 250, 100), (698.46, 25, 100), (659.26, 25, 100),
        (622.26, 25, 100), (659.26, 50, 400), (415.3, 25, 200), (554.36, 350, 100),
        (523.25, 250, 100), (493.88, 25, 100), (466.16, 25, 100), (440, 25, 100),
        (466.16, 50, 400), (311.13, 25, 200), (369.99, 350, 100),
        (311.13, 250, 100), (392, 25, 100), (466.16, 350, 100), (392, 250, 100),
        (466.16, 25, 100), (587.32, 700, 100), (784, 350, 100), (392, 250, 100),
        (392, 25, 100), (784, 350, 100), (739.98, 250, 100), (698.46, 25, 100),
        (659.26, 25, 100), (622.26, 25, 100), (659.26, 50, 400), (415.3, 25, 200),
        (554.36, 350, 100), (523.25, 250, 100), (493.88, 25, 100),
        (466.16, 25, 100), (440, 25, 100), (466.16, 50, 400), (311.13, 25, 200),
        (392, 350, 100), (311.13, 250, 100), (466.16, 25, 100),
        (392.00, 300, 150), (311.13, 250, 100), (466.16, 25, 100), (392, 700)
        ])

    def _smallSong(self):
        self.sound.play_song((
            ('D4', 'e3'), # A long time ago in a galaxy far,
            ('D4', 'e3'), # far away...
            ('D4', 'e3'),
            ('G4', 'h'),
            ('D5', 'h'),
            ('C5', 'e3'),
            ('B4', 'e3'),
            ('A4', 'e3'),
            ('G5', 'h'),
            ('D5', 'q'),
            ('C5', 'e3'),      
            ('B4', 'e3'),
            ('A4', 'e3'),
            ('G5', 'h'),
            ('D5', 'q'),
            ('C5', 'e3'),      
            ('B4', 'e3'),
            ('C5', 'e3'),
            ('A4', 'h.'),
        ))





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")

r2d2.ini

INI
Ini file required to run the R2D2 and Amazon Alexa
# 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 = AMAZON_ID
alexaGadgetSecret = ALEXA_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

localisation.js

JavaScript
messages and text used in Lego R2D2 project
module.exports = {
    en: {
        translation: {
            COMMAND_TEXT: `command`,
            ACTIVATED_TEXT: `activated`,
            NO_EV3_FOUND_MSG: `<say-as interpret-as="interjection">oh boy, </say-as> I couldn't find LEGO Artoo-Detoo connected to this Echo device. Please check your LEGO Artoo-Detoo is connected, and try again.`,
            AWAIT_COMMANDS_MSG: `<voice name="Matthew"> Awaiting your commands </voice>`,
            WELCOME_GREETING_1_MSG: `<voice name="Matthew">  Weclome, I am Artoo-Detoo. I am very good in dancing, singing, and marchal arts. Just say Artoo-Detoo dance, or Artoo-Detoo sing. /voice>`,
            WELCOME_GREETING_2_MSG: `<voice name="Matthew">Hi, I am Artoo-Detoo. I am not a human but I am very good in dancing, singing. Just say Alexa, let's dance, or please sing. </voice>`,
            WELCOME_GREETING_3_MSG: `<say-as interpret-as="interjection">aloha, </say-as><voice name="Matthew"> Artoo-Detoo, your best friend in the whole galaxy. Say dance or let's sing.</voice>`,
            WELCOME_GREETING_4_MSG: `<voice name="Matthew"> Artoo-Detoo welcomes you. </voice> <amazon:effect name="whispered">Or just Eitoo.</amazon:effect> I am good in dancing and singing. Just tell Alexa let's dance or sing a song.`,
            WELCOME_BACK_GREETING_1_MSG: `<voice name="Matthew"> Weclome back! What should we do now.<break time="2s"/>  Say, Alexa let's dance.</voice>`,
            WELCOME_BACK_GREETING_2_MSG: `<voice name="Matthew"> howdy, Artoo-Detoo is happy to play with you again. <s>Shoud we dance ?</s> Just say dance.</voice>`,
            WELCOME_BACK_GREETING_3_MSG: `<voice name="Matthew"> Artoo-Detoo can dance and sing. Say dance.</voice>`,
            WELCOME_BACK_GREETING_4_MSG: `<voice name="Matthew"> Hello there. You can ask me to tell you about Emperor Palpatine</voice> `,
            REPEAT_COMMANDS_MSG: `<voice name="Matthew">Sorry, I could not understand your command.  Can you repeat that? </voice>`,
            ERROR_COMMANDS_MSG: `<voice name="Matthew"> Sorry, I had trouble doing what you asked. Please try again.</voice>`,
            VOICE_INTERFACE_ACTIV_MSG:`Voice interface activated.`,
            PATROL_MODE_ACTIVATED_MSG : `<voice name="Matthew"> Patrol mode activated </voice>`,
            SENTRY_MODE_ACTIVATED_MSG: `<voice name="Matthew">  Sentry mode is now activated </voice>`,
            BRAKE_MSG: `<voice name="Matthew">Applying brake </voice>`, 
            OBJECT_DETECTED_MSG: `<voice name="Matthew"> Object detected </voice>`,
            OBJECT_ELIMINATED_MSG: `<voice name="Matthew">Threat eliminated </voice>`, 
            UNKNOWN_EVENT_MSG: `<voice name="Matthew"> Event not recognized. Awaiting new command.</voice>`,
			GOODBYE_MSG: `<voice name="Matthew"><emphasis level="strong">Goodbye</emphasis> Artoo-Detoo goes sleeping. To wake me up, say Alexa start lego robot</voice>`, 
			COMMAND_REMIND_MSG: `<voice name="Matthew">If you want me to sing, say sing a song. or you want me to dance, then say lets dance. If you want a story, say tell me story.</voice>`,
			HELP_COMMAND_MSG: `<voice name="Matthew">you can say, turn left or go forward. to change my speed, say set speed 60 percent.</voice>`,
			SENSOR_TOUCH_ACTIVATED_MSG:`<voice name="Matthew"><say-as interpret-as="interjection">ha ha</say-as> i can feel that you touched me</voice>`,
			PALPATINE: `<voice name="Matthew">Let me tell you something about Emperor Palpatine. Sheev Palpatine was the sole engineer of his rise to power. 
			Being a former apprentice to Darth Plagueis the Wise, a Sith Lord who was a master of arcane and unnatural knowledge, he could plan events far in the future and use the Force to foresee the results.
			He began as a Naboo Senator in the Galactic Republic and used several conflicts he had constructed to gain more power.</voice>`,
			VADER: `<voice name="Matthew"> Ok, I will tell you something about Darth Vader. Darth Vader was the Chosen One, a Jedi legend who brings balance to the Force. 
			Vader was born as Anakin Skywalker on Tatooine. He was first slave, then became a Jedi and eventually fell to the Dark Side. 
			Along the way, he earned a great name for himself by saving the Galactic Republic more than a few times. He liked his Jedi Master, Obi-Wan Kenobi and he loved Padme Amidala.
			Darth Vader turned his back on the Jedi and sweared his allegiance to the Emperor.</voice>`,
			MAUL:`<voice name="Matthew"> <say-as interpret-as="interjection">oh boy, </say-as> Darth Maul was an Iridonian, born on Iridonia. At an early age he was trained as a Jedi, but was kidnapped by Darth Sidious, who conditioned him to forget everything he had learned from the Jedi. 
			He then began to teach him the ways of the Dark Side. Maul had absolutely no memories of his homeworld of Iridonia and was incapable of feeling emotions outside of bloodlust and rage. 
			Sidious turned him into a brutal killing machine and a weapon of pure hatred, severely punishing any show of fear or mercy. 
			Maul is described as the ultimate tool of the dark side, with Sith tattoos covering his entire body. 
			The reason that Maul was Sidious' apprentice is because of the martial powers of the Zabraks and how they'd hire themselves to the Sith.</voice>`
			
        }
    },
    de: {
        translation: {
            COMMAND_TEXT: `Befehl`,
            ACTIVATED_TEXT: `aktiviert`,
            NO_EV3_FOUND_MSG: ` <say-as interpret-as="interjection">oh mann </say-as> LEGO r zwo d zwo ist mit deinem Echo Gert nicht verbunden. berprfe auf deinem LEGO r zwo d zwo die Bluetooth Verbindung und probiere es nochmal.`,
            AWAIT_COMMANDS_MSG: `<voice name="Hans"> Warte auf deine Befehle </voice>`,
            WELCOME_GREETING_1_MSG: `<voice name="Hans">  Hallo und Willkommen zu r zwo d zwo.</voice>`,
            WELCOME_GREETING_2_MSG: `<voice name="Hans"> hallihallo. Ich bin r zwo d zwo. Ich bin kein Mensch aber auch nicht einfach Maschine.</voice>`,
            WELCOME_GREETING_3_MSG: `<voice name="Hans"> r zwo d zwo - dein bester Freund.</voice>`,
            WELCOME_GREETING_4_MSG: `<voice name="Hans"> Gr Gott, ich bn rr zwoo du zwoo.</voice>`,
            WELCOME_BACK_GREETING_1_MSG: `<voice name="Hans"> Ich freue mich dass du wieder da bist. </voice>`,
            WELCOME_BACK_GREETING_2_MSG: `<voice name="Hans"> Hallo, lass uns zusammen was  unternehmen. Ich habe auf dich gewartet.</voice>`,
            WELCOME_BACK_GREETING_3_MSG: `<voice name="Hans"> LEGO r zwo d zwo </voice><amazon:effect name="whispered">Wusstes du dass ich eigentlich LEGO r zwo hie? </amazon:effect>`,
            WELCOME_BACK_GREETING_4_MSG: `<voice name="Hans"> Cool, du bist wieder da. Lass uns singen, tanzen, oder Geschichten erzhlen.</voice> `,
            REPEAT_COMMANDS_MSG: `<voice name="Hans"> Es tut mir leid.  Ich kenne dieses Kommando noch nicht, kannst du nochmal wiederholen ? </voice>`,
            VOICE_INTERFACE_ACTIV_MSG: `<voice name="Hans">Sprach-Modus aktiviert </voice>`,
            PATROL_MODE_ACTIVATED_MSG : `<voice name="Hans"> Wache Modus aktiviert </voice>`,
            SENTRY_MODE_ACTIVATED_MSG: `<voice name="Hans">  berwachung aktiviert </voice>`,
            BRAKE_MSG: `<voice name="Hans"> Bremsen aktiviert </voice>`, 
            OBJECT_DETECTED_MSG: `<audio src="soundbank://soundlibrary/scifi/amzn_sfx_scifi_alarm_02"/> Sprachen-Modus aktiviert`,
            OBJECT_ELIMINATED_MSG: `<voice name="Matthew">Gefahr eliminiert </voice>`, 
            UNKNOWN_EVENT_MSG: `<voice name="Hans"> Was ist passiert, ich kenne dieses Kommando nicht. Warte auf neue Befehle.</voice>`,
			GOODBYE_MSG: `<voice name="Hans"> <emphasis level="strong">Tschss </emphasis> Meine Zeit ist aus. Mich wieder zu starten, sage: Alexa, starte lego robot.</voice>`, 
			DURATION_REMIND_MSG: `<voice name="Hans"><say-as interpret-as="interjection">oh boy, lets play</say-as> Otherwise Artoo-Detoo goes sleeping.</voice>`,
			COMMAND_REMIND_MSG: `<voice name="Hans"> Soll ich singen, sag singe. Falls du mich tanzen siehen willst, sag tanze. Falls du Geschichten magst, sag erzhle Geschichte.</voice>`,
			HELP_COMMAND_MSG: `<voice name="Hans"> Willst du dass ich mich bewege, dann sag fahre rechts oder links. Du kannst meine Geschwindigket ndern indem du mir sagst, ndere Geschwindigket auf 60 Prozent.</voice>`,
			SENSOR_TOUCH_ACTIVATED_MSG:`<voice name="Hans"><say-as interpret-as="interjection">au weia</say-as> Du hast mich gepickst</voice>`,
			PALPATINE: `<voice name="Hans">Kaiser Sheev Palpatine, auch bekannt als Darth Sidious, wurde von Darth Plageuis trainiert. 
			Nachdem er alles gelernt hatte, was er wusste, ttete er ihn im Schlaf und begann, Darth Maul als Lehrling aufzunehmen.
			Palpatine schien zunchst ein liebenswrdiger Politiker und ein harmloser, aber wohlmeinender Mensch zu sein. Dies war ein Vorwand, um die Mitglieder des Galaktischen Senats zu tuschen, dass er die besten Absichten fr die Galaxis hatte.
			In Wahrheit war Palpatine jedoch ein arroganter, ehrgeiziger, machtgieriger und machiavellistischer Intrigant. 
			Er wnschte sich absolute Macht und Ausschluss aus dem trgerischen Glauben, ber allem anderen zu stehen. Trotz seiner Arroganz war er uerst intelligent, geduldig, gerissen und manipulativ.</voice>`,
			VADER: `<voice name="Hans"> Ok, Ich erzhle dir was ber Darth Vader. Darth Vader war zunchst ein Sklave, geboren in Tatooine. 
			Damals hie er Anakin Skywalker. Er wurde Jedi-Ritter Anakin Skywalker aber spter wechselte er an die dunkle Seite.
			Vader war kalt, brutal und unbarmherzig gegenber seinen Feinden, da er durchaus bereit war, sie kaltbltig zu terrorisieren, zu foltern und sogar zu ermorden. 
			 Als Auserwhlter war Vader immens mchtig und hatte eine erstaunlich starke Verbindung zur Macht. Strker als jeder Force-Benutzer, der jemals existierte, verfgte er auch ber das Potenzial, der mchtigste Force-Benutzer in der Geschichte der Galaxis zu werden. 
			Laut Obi-Wan Kenobi, Vaders Krfte bertrafen die von Yoda und Palpatin.Nur sein Sohn Luke Skywalker hat ihn bertroffen. </voice>`,
			MAUL: `<voice name="Hans">  Maul war ein Lehrling von Palpatine.
			Maul lebte in den letzten Tagen der Galaktischen Republik und am Vorabend der Regierungszeit des Galaktischen Reiches . 
			Maul war Sohn von Talzin und auf Dathomir geboren. Er wurde von Darth Sidious als Sith-Lehrling aufgenommen und erhielt den Namen Darth Maul.
			Die Trainingsmethoden von Sidious waren oft unvershnlich und hart. Mauls Leben und Beweggrnde beruhten auf Rache. 
			Als Sith-Lord suchte Maul Rache an den Jedi fr ihren Triumph ber die Sith und wollte den Sith-Orden wieder an die Macht ber die Galaxis bringen.
			Nach seinem Exil und der Wiederherstellung seines Geistes wandte Maul seine Gedanken an Rache der Galaxis zu.
			Manchmal war Maul animalisch, was sich in seiner Fhigkeit uerte, wie ein wildes Tier zu knurren. Dies knnte ihm gelegentlich erlauben, seine Gegner psychisch zu verunsichern.</voice>`
        }
    }
}

common.js

JavaScript
/*
 * 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 = 'Ok';

        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 = 'Ciao!';
        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
    };

index.js

JavaScript
/*
 * 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 BG_MUSIC = '<audio src="soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_waiting_loop_30s_01"/>';
const EUREKA_AUDIO= '<audio src="https://www.jovo.tech/audio/MzFl4xyr-eureka-r2d2.mp3"/>';
const CONFIRMATION_AUDIO = '<audio src="https://www.jovo.tech/audio/xUKox0Df-happy-confirmation.mp3"/>';
const LAUGH_AUDIO = '<audio src="https://www.jovo.tech/audio/wna3ztb3-laughing-r2d2.mp3"/>';
const PROCESSING_AUDIO = '<audio src="https://www.jovo.tech/audio/huqkWkdP-processing-r2d2.mp3"/>';
const BEEPING_AUDIO = '<audio src="https://www.jovo.tech/audio/WZaiiPHa-r2-beeping-5.mp3"/>';
const PUTTING_TOGETHER_AUDIO = '<audio src="https://www.jovo.tech/audio/wFIPR2pX-putting-it-together.mp3"/>';
const SAD_AUDIO =  '<audio src="https://www.jovo.tech/audio/os8CFK0M-sad-r2d2.mp3"/>';
const GRUMBLING_AUDIO =  '<audio src="https://www.jovo.tech/audio/NrdaEx4K-unconvined-grumbling.mp3"/>';
const AUDIO = [EUREKA_AUDIO, LAUGH_AUDIO, PROCESSING_AUDIO, PUTTING_TOGETHER_AUDIO, GRUMBLING_AUDIO]; 
const COMMANDS_LIST = ['COMMAND_REMIND_MSG', 'HELP_COMMAND_MSG'];
// Array of welcome greetings
const welcomeGreetings = ['WELCOME_GREETING_1_MSG','WELCOME_GREETING_2_MSG','WELCOME_GREETING_3_MSG', 'WELCOME_GREETING_4_MSG']; 
const welcomeAgainGreetings = ['WELCOME_BACK_GREETING_1_MSG','WELCOME_BACK_GREETING_2_MSG','WELCOME_BACK_GREETING_3_MSG', 'WELCOME_BACK_GREETING_4_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) {
        console.log("Inside LaunchRequest");

        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('VOICE_INTERFACE_ACTIV_MSG') + handlerInput.t(randomChoice(welcomeGreetings));
        } else {
            speechOutput = handlerInput.t('VOICE_INTERFACE_ACTIV_MSG') + handlerInput.t(randomChoice(welcomeAgainGreetings));
        }
        // starting point
        return handlerInput.responseBuilder
            .speak(randomChoice(AUDIO) + speechOutput)
            .addDirective(Util.buildStartEventHandler(token,60000, {}))
            .getResponse();
    }
};

// Add the speed value to the session attribute.
// This allows other intent handler to use the specified speed value
// without asking the user for input.
const SetSpeedIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetSpeedIntent';
    },
    handle: function (handlerInput) {

        // Bound speed to (1-100)
        let speed = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Speed');
        speed = Math.max(1, Math.min(100, parseInt(speed)));
        Util.putSessionAttribute(handlerInput, 'speed', speed);

        let speechOutput = `speed set to ${speed} percent.`;
        return handlerInput.responseBuilder
            .speak(PROCESSING_AUDIO + speechOutput + CONFIRMATION_AUDIO)
            .getResponse();
    }
};

// Construct and send a custom directive to the connected gadget with
// data from the MoveIntent.
const MoveIntentHandler = {
    canHandle(handlerInput) {
        console.log("Inside MoveIntentHandler");

        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const direction = Alexa.getSlotValue(request, 'Direction');
        console.log("MoveIntent request: " + JSON.stringify(request));

        // Duration is optional, use default if not available
        const duration = Alexa.getSlotValue(request, 'Duration') || "2";

        // Get data from session attribute
        const attributesManager = handlerInput.attributesManager;
        const speed = attributesManager.getSessionAttributes().speed || "50";
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];

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

        const speechOutput = (direction === "brake")
            ?  handlerInput.t('BRAKE_MSG')
            : `${direction} ${duration} seconds at ${speed} percent speed`;

        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(directive)
            .getResponse();
    }
};

const TellStoryIntentHandler = {
    canHandle(handlerInput) {
        console.log("Inside TellStoryIntentHandler");

        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'TellStoryIntent';
    },
    handle: function (handlerInput) {
        const story = ['PALPATINE','VADER','MAUL']; 
        
        const request = handlerInput.requestEnvelope;
        console.log(`Telling a story about ${story}`);
        
        // Get data from session attribute
        let speechOutput = handlerInput.t(randomChoice(story));
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .withShouldEndSession(false)
            .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 || [];
        let speed = attributesManager.getSessionAttributes().speed || "50";

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

        let speechOutput = handlerInput.t('COMMAND_TEXT') + `${command}` + handlerInput.t('ACTIVATED_TEXT');
        return handlerInput.responseBuilder
            .speak(speechOutput + randomChoice(AUDIO))
            .addDirective(directive)
            .getResponse();
    }
};

const EventsReceivedRequestHandler = {
    // Checks for a valid token and endpoint.
    canHandle(handlerInput) {
        let { request } = handlerInput.requestEnvelope;
        console.log("Inside EventsReceivedRequestHandler");
        console.log('Request type: ' + Alexa.getRequestType(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 === 'Proximity') {
            let distance = parseInt(payload.distance);
            if (distance < 10) {
                let speechOutput = handlerInput.t('OBJECT_DETECTED_MSG');
                return handlerInput.responseBuilder
                    .speak(speechOutput, "REPLACE_ALL")
                    .withShouldEndSession(false)
                    .getResponse();
            }
        } else if (name === 'Sentry') {
            if ('fire' in payload) {
                speechOutput = handlerInput.t('OBJECT_ELIMINATED_MSG');
            }
        } else if (name === 'Speech') {
            speechOutput = payload.speechOut;

        } else {
            speechOutput = handlerInput.t('UNKNOWN_EVENT_MSG');
        }
        return handlerInput.responseBuilder
            .speak(LAUGH_AUDIO + handlerInput.t(speechOutput), "REPLACE_ALL")
            .getResponse();
    }
};
const ExpiredRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'CustomInterfaceController.Expired'
    },
    handle(handlerInput) {
        console.log("== Custom Event Expiration Input ==");

        // 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(randomChoice(AUDIO) + speechOutput + randomChoice(AUDIO))
                .getResponse();
        }
        else {
            // End skill session
            return handlerInput.responseBuilder
                .speak(handlerInput.t('GOODBYE_MSG'))
                .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,
        SetSpeedIntentHandler,
        SetCommandIntentHandler,
        TellStoryIntentHandler,
        MoveIntentHandler,
        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();

util.js

JavaScript
/*
 * 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
{
  "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",
    "lodash": "^4.17.11",
    "i18next": "^15.0.5"
 }
}

Credits

Timur Tentimishov

Timur Tentimishov

2 projects • 2 followers
Serdar Tentimishov

Serdar Tentimishov

1 project • 0 followers

Comments