Hardware components | ||||||
| × | 1 | ||||
| × | 1 |
This is an entry level project designed with the following objectives:
- Practical. It is designed as a proof-of-concept prototype to address a real world need.
- Simple. It is designed with minimal hardware components.
- Educational. it explores a new way of hardware-to-hardware voice communication for event handling
Here is a video demonstration of how it works:
Here is a video showing how the Lego creation can be built:
Alexa Gadget on EV3 - Stove Control
PythonThis is an Alexa Gadget on EV3 that processes directives sent from an Alexa Skill to shut off a stove based on a timer
#!/usr/bin/env python3
import shutil
import time
import logging
import json
import threading
from agt import AlexaGadget
from ev3dev2.led import Leds
from ev3dev2.sound import Sound
from ev3dev2.motor import OUTPUT_A, LargeMotor
# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO)
class MindstormsGadget(AlexaGadget):
"""
A Mindstorms gadget that shuts off a stove based on a timer configured by voice commands.
"""
def __init__(self):
"""
Performs Alexa Gadget initialization routines and ev3dev resource allocation.
"""
super().__init__()
# Gadget state
self.current_position = 12
self.remaining_sec = 0
self.flashing = 0
self.timer_end = time.time()
self.columns = shutil.get_terminal_size().columns
self.lines = shutil.get_terminal_size().lines
# Ev3dev initialization
self.leds = Leds()
self.sound = Sound()
self.drive = LargeMotor(OUTPUT_A)
def on_connected(self, device_addr):
"""
Gadget connected to the paired Echo device.
:param device_addr: the address of the device we connected to
"""
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
print("{} connected to Echo device".format(self.friendly_name))
def on_disconnected(self, device_addr):
"""
Gadget disconnected from the paired Echo device.
:param device_addr: the address of the device we disconnected from
"""
self.leds.set_color("LEFT", "BLACK")
self.leds.set_color("RIGHT", "BLACK")
print("{} disconnected from Echo device".format(self.friendly_name))
def 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 == "StoveOffTimer":
# Expected params: [delay]
self._stoveOffTimer(payload["delay"])
elif control_type == "Calibrate":
# Expected params: [position]
self._calibrate(int(payload["position"]))
elif control_type == "Rotate":
# Expected params: [degrees]
count = int(payload["degrees"])
self._rotate(count)
elif control_type == "ReverseRotate":
# Expected params: [degrees]
count = int(payload["degrees"])*-1
self._rotate(count)
elif control_type == "Move":
# Expected params: [position]
position = int(payload["position"])
if (position > 6 and position < 9):
sound.speak("{} is not a valid position".format(position))
return
if (position >=9 and position <= 12):
position = position - 12
self._move(position)
except KeyError:
print("Missing expected parameters: {}".format(directive))
def _rotate(self, count):
self.drive.on_for_degrees(speed=10, degrees=count)
def _move(self, position: int):
if ((position > 6 and position < 9) or position > 12):
self.sound.speak("{} is not a valid position".format(position))
return
to_move = (position - int(self.current_position)) * 30
self.current_position = position
self.drive.on_for_degrees(speed=10, degrees=to_move)
def _calibrate(self, position: int):
"""
Calibtrate the currnt position
"""
if ((position > 6 and position < 9) or position > 12):
self.sound.speak("{} is not a valid position".format(position))
return
if (position >=9 and position <= 12):
position = position - 12
self.current_position = position
print("The current knob position was calibrated as {}".format(self.current_position))
def _stoveOffTimer(self, delay: int):
"""
Handles stove commands from the directive.
"""
self.timer_end = time.time() + int(delay)
self.remaining_sec = int(delay)
mins = int(self.remaining_sec / 60)
secs = int(self.remaining_sec % 60)
mins_str = ""
secs_str = ""
if (mins != 0):
if (mins == 1):
mins_str = "one minute"
else:
mins_str = "{} minutes".format(mins)
if (secs != 0):
mins_str = mins_str + " and"
if (secs != 0):
if (secs == 1):
secs_str = " one second"
else:
secs_str = " {} seconds".format(secs)
print("The stove shutoff timer will be triggered in {}{}".format(mins_str, secs_str))
self.sound.speak("The stove shutoff timer will be triggered in {}{}".format(mins_str, secs_str))
c = 0
while (c < self.lines):
print("")
c = c + 1
self.flashing = 1
timer_flash = threading.Timer(0.001, ledsFlash)
timer_flash.start()
timer_off = threading.Timer(0.001, stoveShutOff)
timer_off.start()
def ledsFlash():
global gadget
if (gadget.flashing == 0):
return
if (gadget.flashing == 1):
if (gadget.remaining_sec > 15):
gadget.leds.set_color("LEFT", "YELLOW")
gadget.leds.set_color("RIGHT", "YELLOW")
else:
gadget.leds.set_color("LEFT", "RED")
gadget.leds.set_color("RIGHT", "RED")
gadget.flashing = 2
elif (gadget.flashing == 2):
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
gadget.flashing = 1
timer = threading.Timer(0.46, ledsFlash)
timer.start()
def stoveShutOff():
global gadget
gadget.remaining_sec = int(gadget.timer_end - time.time())
if (gadget.remaining_sec > 0):
timer_off = threading.Timer(1, stoveShutOff)
timer_off.start()
if (int(gadget.remaining_sec/5)*5 == gadget.remaining_sec):
print("{} seconds left".format(gadget.remaining_sec).center(gadget.columns))
return
print("Turning off the stove...".center(gadget.columns))
gadget.flashing = 0
to_move = (6-int(gadget.current_position))*30
gadget.sound.play_song((('E5', 'e'), ('E5', 'e')))
gadget.sound.speak("turning off the stove")
gadget.drive.on_for_degrees(speed=10, degrees=to_move)
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
gadget.sound.speak("Alexa", volume=100)
gadget.sound.play_file("/home/robot/speech.wav", volume=100)
print("Done.")
gadget = MindstormsGadget()
if __name__ == '__main__':
# Startup sequence
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")
Alexa Skill for EV3 - Stove Control
JavaScriptThis is an Alexa Skill that processes voice commands and sends directives to an Alexa Gadget on EV3 to shut off a stove based on a timer
/*
* This is an Alexa Skill named "Stove Control". Its main function is to turn off a stove by rotating the knob
* to its off position based on a timer.
*
* The main intent is "StoveOffTimerIntent".
*
* Other intents are used to calibrate the current position of the knob so that EV3 knows the offset to the off
* position.
*/
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');
// The audio tag to include background music
const BG_MUSIC = '<audio src="soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_waiting_loop_30s_01"></audio>';
// The namespace of the custom directive to be sent by this skill
const NAMESPACE = 'Custom.Mindstorms.Gadget';
// The name of the custom directive to be sent this skill
const NAME_CONTROL = 'control';
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle: async function(handlerInput) {
let request = handlerInput.requestEnvelope;
let { apiEndpoint, apiAccessToken } = request.context.System;
let apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
if ((apiResponse.endpoints || []).length === 0) {
return handlerInput.responseBuilder
.speak(`I couldn't find an EV3 Brick connected to this Echo device. Please check to make sure your EV3 Brick is connected, and try again.`)
.getResponse();
}
// Store the gadget endpointId to be used in this skill session
let endpointId = apiResponse.endpoints[0].endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// 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);
return handlerInput.responseBuilder
.speak("Welcome to smartified stove, you can start issuing stove control commands")
.reprompt("Awaiting commands" + BG_MUSIC)
.getResponse();
}
};
const StoveOffTimerIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'StoveOffTimerIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
var timer_mins = Alexa.getSlotValue(request, 'timer_mins');
var timer_secs = Alexa.getSlotValue(request, 'timer_secs');
if (timer_mins === undefined){
timer_mins = 0;
}
else {
timer_mins = parseInt(timer_mins);
}
if (timer_secs === undefined){
timer_secs = 0;
}
else {
timer_secs = parseInt(timer_secs);
}
const delay = parseInt(timer_mins) * 60 + parseInt(timer_secs);
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Construct the directive with the payload containing the stove control parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: "StoveOffTimer",
delay: delay
});
var secs_str = "";
if (timer_secs !== 0){
if (timer_secs === 1){
secs_str = "one second";
}
else {
secs_str = timer_secs + "seconds";
}
}
var mins_str = "";
if (timer_mins !== 0){
if (timer_mins === 1){
mins_str = "one minute";
}
else {
mins_str = timer_mins + " minutes";
}
if (timer_secs > 0){
mins_str = mins_str + " and ";
}
}
return handlerInput.responseBuilder
.speak("the stove will be turned off in " + mins_str + secs_str)
.addDirective(directive)
.getResponse();
}
};
const MoveIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveToPositionIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const hour = Alexa.getSlotValue(request, 'hour');
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Construct the directive with the payload containing the move parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'Move',
position: hour
});
return handlerInput.responseBuilder
.speak("Rotating the knob to " + hour + " o'clock")
.reprompt("awaiting command")
.addDirective(directive)
.getResponse();
}
};
const ReverseRotateIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'ReverseRotateIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const count = Alexa.getSlotValue(request, 'count');
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Construct the directive with the payload containing the move parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'ReverseRotate',
degrees: count
});
return handlerInput.responseBuilder
.speak("Rotating the knob " + count + " degrees counter-clockwise")
.reprompt("awaiting command")
.addDirective(directive)
.getResponse();
}
};
const RotateIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'RotateIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const count = Alexa.getSlotValue(request, 'count');
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Construct the directive with the payload containing the move parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'Rotate',
degrees: count
});
return handlerInput.responseBuilder
.speak("Rotating the knob " + count + " degrees clockwise")
.reprompt("awaiting command")
.addDirective(directive)
.getResponse();
}
};
const CalibrateIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'CalibrateIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const position = Alexa.getSlotValue(request, 'position');
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Construct the directive with the payload containing the move parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'Calibrate',
position: position
});
return handlerInput.responseBuilder
.speak("Calibrting the current position as " + position + " o'clock")
.reprompt("awaiting command")
.addDirective(directive)
.getResponse();
}
};
// The SkillBuilder acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
StoveOffTimerIntentHandler,
MoveIntentHandler,
RotateIntentHandler,
ReverseRotateIntentHandler,
CalibrateIntentHandler,
Common.HelpIntentHandler,
Common.CancelAndStopIntentHandler,
Common.SessionEndedRequestHandler,
Common.IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
)
.addRequestInterceptors(Common.RequestInterceptor)
.addErrorHandlers(
Common.ErrorHandler,
)
.lambda();
Comments