Aditi Bhattamishra
Created December 26, 2019 © GPL3+

Evie

A picture is worth a thousand words - EVIE hopes to convey Alexa's emotions by giving her a face of her own.

IntermediateFull instructions provided2 hours133

Things used in this project

Hardware components

Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
×1
Echo Dot
Amazon Alexa Echo Dot
×1

Software apps and online services

VS Code
Microsoft VS Code
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
Alexa Gadgets Toolkit
Amazon Alexa Alexa Gadgets Toolkit

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Tape, Double Sided
Tape, Double Sided

Story

Read more

Custom parts and enclosures

Evie - Build Instructions

Basic instructions and parts to build Evie.

Evie - CAD Model

Complete 3D model of Evie

Schematics

Evie Wiring Instruction

Details the EV3 brick port assignment and cable connections steps.

Code

chimmy.py

Python
Evie's basic interactions with Alexa - this code does not use the Alexa skill.
#!/usr/bin/env python3

import os
import sys
import time
import logging
import json
import threading
from enum import Enum

from agt import AlexaGadget

from ev3dev2.sound import Sound
from ev3dev2.led import Leds
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D, SpeedDPS, LargeMotor
from ev3dev2.sensor.lego import ColorSensor

logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(message)s')
logging.getLogger().addHandler(logging.StreamHandler(sys.stderr))
logger = logging.getLogger(__name__)

class MindstormsGadget(AlexaGadget):
    """
    EV3 robot built for Amazon's Alexa; meant to give Alexa a face.
    """
    def __init__(self):
        """
        Performs Alexa Gadget initialization routines and ev3dev resource allocation.
        """
        super().__init__()

        self.leds = Leds()
        self.sound = Sound()
        self.left_motor = LargeMotor(OUTPUT_A)
        self.right_motor = LargeMotor(OUTPUT_D)
        self.center_motor = LargeMotor(OUTPUT_B)
        self.neck_motor = LargeMotor(OUTPUT_C)
        self.colour = ColorSensor()
        self.colour.mode = 'COL-AMBIENT'
        self.bpm = 0
        self.trigger_bpm = "off"

    def on_connected(self, device_addr):
        """
        For when EV3 is connected to Alexa.
        """
        self.leds.set_color("LEFT", "GREEN")
        self.leds.set_color("RIGHT", "GREEN")
        logger.info("{} connected to Echo device".format(self.friendly_name))

    def on_disconnected(self, device_addr):
        """
        For when EV3 is disconnected from Alexa.
        """
        self.leds.set_color("LEFT", "BLACK")
        self.leds.set_color("RIGHT", "BLACK")
        logger.info("{} disconnected from Echo device".format(self.friendly_name))
    
    def on_alexa_gadget_statelistener_stateupdate(self, directive):
        """
        Provides Evie's reaction to when Alexa is in use.
        """
        for state in directive.payload.states:
            if state.name == 'wakeword':
                if state.value == 'active':
                    print("Wake word active", file=sys.stderr)
                    self.neck_motor.on(15)
                    while state.value == 'active':
                        refl = self.colour.ambient_light_intensity
                        print("Col Val:{}".format(refl), file=sys.stderr)
                        if refl >= 4:
                            self.neck_motor.off() 
                            break                   
                    self.leds.set_color("LEFT", "GREEN")
                    self.leds.set_color("RIGHT", "GREEN")
                    time.sleep(0.5)
                    self._content

                elif state.value == 'cleared':
                    print("Wake word cleared", file=sys.stderr)
                    self.leds.set_color("LEFT", "RED")
                    self.leds.set_color("RIGHT", "RED") 
                    self._return_to_neutral
                    time.sleep(1.5)

    def on_alexa_gadget_musicdata_tempo(self, directive):
        """
        Provides the music tempo of the song currently playing on the Echo device.
        """
        tempo_data = directive.payload.tempoData
        for tempo in tempo_data:
            print("Collecting tempo from Echo device", file=sys.stderr)
            print("Tempo Value:{}".format(tempo.value), file=sys.stderr)
            if tempo.value > 0:
                self.leds.set_color("LEFT", "AMBER")
                self.leds.set_color("RIGHT", "AMBER")
                time.sleep(0.75)
                self.leds.set_color("LEFT", "GREEN")
                self.leds.set_color("RIGHT", "GREEN")

                self.trigger_bpm = "on"
                threading.Thread(target=self.exp_choice, args=(tempo.value,)).start()

            elif tempo.value == 0:
                self.trigger_bpm = "off"
                self.leds.set_color("LEFT", "BLACK")
                self.leds.set_color("RIGHT", "BLACK")
    
    def exp_choice(self, tempo):
        """
        Perform Evie's expression based on tempo of song playing.
        """
        print("Play the music!", file=sys.stderr)
        motor_speed = 400        
        while self.trigger_bpm == "on":

            motor_speed = -motor_speed
            if tempo > 0 and tempo <= 95:
                self.left_motor.on_for_degrees(80, -10)
                self.right_motor.on_for_degrees(80, -10)
                self.center_motor.on_for_degrees(80, 10)
                time.sleep(0.180)
                self.left_motor.on_for_degrees(80, 10)
                self.right_motor.on_for_degrees(80, 10)
                self.center_motor.on_for_degrees(80, -10)
            
                self.leds.set_color("LEFT", "RED")
                self.leds.set_color("RIGHT", "RED")
            
            elif tempo > 95 and tempo <= 130:
                self.left_motor.on_for_degrees(80, 20)
                self.right_motor.on_for_degrees(80, 20)
                self.center_motor.on_for_degrees(80, 20)
                time.sleep(0.180)
                self.left_motor.on_for_degrees(80, -20)
                self.right_motor.on_for_degrees(80, -20)
                self.center_motor.on_for_degrees(80, -20)
                
                self.leds.set_color("LEFT", "AMBER")
                self.leds.set_color("RIGHT", "AMBER")
            
            elif tempo > 130 and tempo <= 195:
                self.left_motor.on_for_degrees(80, 60)
                self.right_motor.on_for_degrees(80, 60)
                self.center_motor.on_for_degrees(80, -30)
                time.sleep(0.180)
                self.left_motor.on_for_degrees(80, -60)
                self.right_motor.on_for_degrees(80, -60)
                self.center_motor.on_for_degrees(80, 30)
               
                self.leds.set_color("LEFT", "YELLOW")
                self.leds.set_color("RIGHT", "YELLOW")
            
            elif tempo > 195 and tempo <= 250:
                self.left_motor.on_for_degrees(80, -90)
                self.right_motor.on_for_degrees(80, -90)
                self.center_motor.on_for_degrees(80, -60)
                time.sleep(0.180)
                self.left_motor.on_for_degrees(80, 90)
                self.right_motor.on_for_degrees(80, 90)
                self.center_motor.on_for_degrees(80, 60)
               
                self.leds.set_color("LEFT", "GREEN")
                self.leds.set_color("RIGHT", "GREEN")

        print("Aww... the music's stopped.", file=sys.stderr)
        self._return_to_neutral
        time.sleep(0.5)

if __name__ == '__main__':

    gadget = MindstormsGadget()

    # Set LCD font and turn off blinking green LEDs
    os.system('setfont Lat7-Terminus12x6')
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")

    # Startup sequence 
    gadget.leds.set_color("LEFT", "GREEN")
    gadget.leds.set_color("RIGHT", "GREEN")

    # Gadget main entry point
    gadget.main()

    # Shutdown sequence
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")

interaction_model.json

JSON
The code for the interaction model of the Alexa skill.
{
  "interactionModel": {
      "languageModel": {
          "invocationName": "evie storm",
          "intents": [
              {
                  "name": "AMAZON.CancelIntent",
                  "samples": []
              },
              {
                  "name": "AMAZON.HelpIntent",
                  "samples": []
              },
              {
                  "name": "AMAZON.StopIntent",
                  "samples": []
              },
              {
                  "name": "AMAZON.NavigateHomeIntent",
                  "samples": []
              },
              {
                  "name": "ShowEmotionIntent",
                  "slots": [
                      {
                          "name": "Emotion",
                          "type": "EmotionType"
                      }
                  ],
                  "samples": [
                      "show me how you look {Emotion}",
                      "show me your {Emotion} face ",
                      "show me {Emotion}"
                  ]
              }
          ],
          "types": [
              {
                  "name": "EmotionType",
                  "values": [
                      {
                          "name": {
                              "value": "upset"
                          }
                      },
                      {
                          "name": {
                              "value": "pleased"
                          }
                      },
                      {
                          "name": {
                              "value": "satisfied"
                          }
                      },
                      {
                          "name": {
                              "value": "calm"
                          }
                      },
                      {
                          "name": {
                              "value": "friendly"
                          }
                      },
                      {
                          "name": {
                              "value": "normal"
                          }
                      },
                      {
                          "name": {
                              "value": "asleep"
                          }
                      },
                      {
                          "name": {
                              "value": "sleeping"
                          }
                      },
                      {
                          "name": {
                              "value": "neutral"
                          }
                      },
                      {
                          "name": {
                              "value": "disappointed"
                          }
                      },
                      {
                          "name": {
                              "value": "tired"
                          }
                      },
                      {
                          "name": {
                              "value": "kind"
                          }
                      },
                      {
                          "name": {
                              "value": "content"
                          }
                      }
                  ]
              }
          ]
      }
  }
}

index.js

JavaScript
The Node.js file which is the body of the Alexa skill.
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');

const NAMESPACE = 'Custom.Evie.Gadget';
const NAME_CONTROL = 'control';

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

        let request = handlerInput.requestEnvelope;
        let { apiEndpoint, apiAccessToken } = request.context.System;
        let apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
        if ((apiResponse.endpoints || []).length === 0) {
            return handlerInput.responseBuilder
            .speak(`Uh oh, looks like Evie isn't connected.`)
            .getResponse();
        }

        let endpointId = apiResponse.endpoints[0].endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        return handlerInput.responseBuilder
            .speak("Evie is ready for display!")
            .reprompt("What should we start with?")
            .getResponse();
    }
};

const ShowEmotionIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'EmotionIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const emotion = Alexa.getSlotValue(request, 'Emotion');
        console.log("Emotion: " + emotion)

        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];
        const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'emotion',
                emotion: emotion
            });

        const speechOutput = `Here is ${emotion}.`
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .reprompt("Any requests?")
            .addDirective(directive)
            .getResponse();
    }
};

exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        ShowEmotionIntentHandler,
        Common.HelpIntentHandler,
        Common.CancelAndStopIntentHandler,
        Common.SessionEndedRequestHandler,
        Common.IntentReflectorHandler,
    )
    .addRequestInterceptors(Common.RequestInterceptor)
    .addErrorHandlers(
        Common.ErrorHandler,
    )
    .lambda();

chimmy.ini

Python
Corresponding .ini file for Evie's basic code.
[GadgetSettings]
amazonId = A2F96OSRJOJORQ
alexaGadgetSecret = 7BAB5EB4B73B80273460A3D0C3FA28A1EF50977120342E2CE329AE97F765DB20

[GadgetCapabilities]
Alexa.Gadget.StateListener = 1.0 - wakeword
Alexa.Gadget.MusicData = 1.0 - tempo

mang.ini

Python
Corresponding .ini file for Evie's code for the Alexa skill.
[GadgetSettings]
amazonId = A2F96OSRJOJORQ
alexaGadgetSecret = 7BAB5EB4B73B80273460A3D0C3FA28A1EF50977120342E2CE329AE97F765DB20

[GadgetCapabilities]
Custom.Evie.Gadget = 1.0

mang.py

Python
Evie's code which allows her to interact with the Alexa skill - displays a portion of the emotions she can express.
#!/usr/bin/env python3

import os
import sys
import time
import logging
import json
from enum import Enum

from agt import AlexaGadget

from ev3dev2.sound import Sound
from ev3dev2.led import Leds
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_D, SpeedDPS, LargeMotor

logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(message)s')
logging.getLogger().addHandler(logging.StreamHandler(sys.stderr))
logger = logging.getLogger(__name__)

class Emotion(Enum):
    
    #List which categorizes voice input terms into emotions.
    
    KIND = ['kind', 'friendly']
    CONTENT = ['pleased', 'content']
    TIRED = ['tired']
    DISAPPOINTED = ['disappointed', 'upset']
    NEUTRAL = ['neutral', 'normal']
    ASLEEP = ['asleep' 'sleeping']

class MindstormsGadget(AlexaGadget):
    """
    EV3 robot built for Amazon's Alexa; meant to give Alexa a face.
    """
    def __init__(self):
        """
        Performs Alexa Gadget initialization routines and ev3dev resource allocation.
        """
        super().__init__()

        self.leds = Leds()
        self.sound = Sound()
        self.left_motor = LargeMotor(OUTPUT_A)
        self.right_motor = LargeMotor(OUTPUT_D)
        self.center_motor = LargeMotor(OUTPUT_B)

    def on_connected(self, device_addr):
        """
        For when EV3 is connected to Alexa.
        """
        self.leds.set_color("LEFT", "GREEN")
        self.leds.set_color("RIGHT", "GREEN")
        logger.info("{} connected to Echo device".format(self.friendly_name))

    def on_disconnected(self, device_addr):
        """
        For when EV3 is disconnected from Alexa.
        """
        self.leds.set_color("LEFT", "BLACK")
        self.leds.set_color("RIGHT", "BLACK")
        logger.info("{} disconnected from Echo device".format(self.friendly_name))

    def on_custom_evie_gadget_control(self, directive):
        """
        Gathers requested emotion from Alexa and displays accordingly. 
        """        
        try:
            payload = json.loads(directive.payload.decode("utf-8"))
            print("Emotion: {}".format(payload), file=sys.stderr)
            response = payload["type"]
            emotive = payload["emotion"]
            
            if response == "emotion":
                print("Emotion: ({})".format(emotive), file=sys.stderr)
                if emotive in Emotion.KIND.value:
                    self.left_motor.on_for_degrees(100, 180)
                    self.right_motor.on_for_degrees(100, 180)
                    self.center_motor.on_for_degrees(100, 25)
                    time.sleep(1)
                    self.left_motor.on_for_degrees(100, -180)
                    self.right_motor.on_for_degrees(100, -180)
                    self.center_motor.on_for_degrees(100, -25)
                
                elif emotive in Emotion.CONTENT.value:
                    self.center_motor.on_for_degrees(100, 25)
                    time.sleep(1)
                    self.center_motor.on_for_degrees(100, -25)
                
                elif emotive in Emotion.TIRED.value:
                    self.left_motor.on_for_degrees(100, 180)
                    self.right_motor.on_for_degrees(100, 180)
                    self.center_motor.on_for_degrees(100, -25)
                    time.sleep(1)
                    self.left_motor.on_for_degrees(100, -180)
                    self.right_motor.on_for_degrees(100, -180)
                    self.center_motor.on_for_degrees(100, 25)
               
                elif emotive in Emotion.DISAPPOINTED.value:
                    self.center_motor.on_for_degrees(100, -25)
                    time.sleep(1)
                    self.center_motor.on_for_degrees(100, 25)
                
                elif emotive in Emotion.ASLEEP.value:
                    self.left_motor.on_for_degrees(100, -180)
                    self.right_motor.on_for_degrees(100, -180)
                    time.sleep(1)
                    self.left_motor.on_for_degrees(100, 180)
                    self.right_motor.on_for_degrees(100, 180)
                
                elif emotive in Emotion.NEUTRAL.value:
                    print("This is my neutral face!", file=sys.stderr) 

        except KeyError:
            print("No Emotion Specified: {}".format(directive), file=sys.stderr)


if __name__ == '__main__':

    gadget = MindstormsGadget()

    # Set LCD font and turn off blinking green LEDs
    os.system('setfont Lat7-Terminus12x6')
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")

    # Startup sequence 
    gadget.leds.set_color("LEFT", "GREEN")
    gadget.leds.set_color("RIGHT", "GREEN")

    # Gadget main entry point
    gadget.main()

    # Shutdown sequence
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")

Credits

Aditi Bhattamishra

Aditi Bhattamishra

1 project • 4 followers

Comments