This is a simple project to help get you started with Mindstorms EV3, the Alexa Skills Kit, and Amazon Gadgets. It's an intentionally simple design that only uses parts included with the EV3 Core Set, and can be reasonably completed in an afternoon without any prior experience. The design is compact and there are exposed mounting points. You can customize the tower to fit your favorite game or active campaign.
InstructionsThis design was unsuccessful and bulky. There are not enough pieces in the core set to create a rigid arm long enough for seven types of dice. It also suffered from occasional slips that resulted in inconsistent motion. The push mechanism was adapted from EV3STORM but only had three studs of actuation.
The new design is far more compact. By rotating, the carriage size is greatly reduced and we avoid the effects of friction. There is no large track, so the mechanism can be hidden inside of a Lego build. The push arm was also improved. Instead of a gear or wheel, this design uses a single technic lift-arm. Five studs was plenty of actuation for this project, but you can extend it to be any length you choose.
There are two dice trays included, but the EV3 Core Set includes enough parts to make and attach eight of them without buying additional pieces. The trays have rotating end stops that can be adjusted if dice fall out when loading or rolling. The tray angle can be adjusted without changing the mounting system or an extra rail can be added if extra dice are falling out. You can also adjust the mounting point of the push-motor to balance the system or increase the radius.
As an additional note, the demo does not currently use the touch sensor input. The servo is already accurate enough to roll dice from two trays. You may want to implement a zeroing function if you build additional dice trays.
SkillFor the skill, we will use a Alexa-hosted backend running NodeJS.
To create your skill, you can follow the guide in mission 3. Instead of using the code from mission 3, use the attached index.js, util.js, common.js and model.json.
The interaction model is simple and consists of one custom intent and a custom slot. The custom slot contains the dice types (to start D6 and D20) and is combined with a count (AMAZON.Number) to capture the type and number of die. Because dice rolling is handled by a simple directive, you can add your own intents to trigger a preset roll. For example, you can add a RollIntiativeIntent that sends the directive with count = 1 and type = D20.
New dice types can be added by modifying the custom slot type.
GadgetFollow the setup guide to get your EV3 brick flashed with EV3DEV and your development environment setup.
You should also complete mission 1 to pair your EV3 to your Alexa device.
Download the skill code (dice.py and dice.ini) into a directory on your computer. Follow the same steps as in mission 1, use Visual Studio Code to upload the files to your EV3 and run dice.py.
Remember to replace the amazonId and alexaGadgetSecret in dice.ini.
Because there's only one intent, there's also only one directive. The angle of rotation is determined using the number of die and the rotation of the push motor is determined by the count.
If you add a new type of dice, you'll need to change the gadget code (lines 66-70) to support them. Getting the value of estimated_pos can take some trial and error.
Once the device has paired, you're good to go!
Gadget (dice.py)
First, we import the necessary dependencies for dealing using the ports on the EV3 and connecting as an Alexa gadget.
import os
import sys
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_C, OUTPUT_D, MediumMotor, LargeMotor
from ev3dev2.sensor import INPUT_4
from ev3dev2.sensor.lego import TouchSensorWe can also add some basic logging so that we are able to tell when the device is connecting. This can be useful if you aren't sure the EV3 has paired correctly.
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(message)s')
logging.getLogger().addHandler(logging.StreamHandler(sys.stderr))
logger = logging.getLogger(__name__)The functionality of our gadget is contained in a MindstormsGadget class which inherits from the AlexaGadget.
class MindstormsGadget(AlexaGadget):In the __init__ method of the MindstormsGadget we call __init__ on the AlexaGadget parent class we're inheriting from and get access to the sound, leds, motors, and sensors we want to use.
def __init__(self):
super().__init__()
self.leds = Leds()
self.sound = Sound()
self.rotate = MediumMotor(OUTPUT_C)
self.push = LargeMotor(OUTPUT_D)
self.endstop = TouchSensor(INPUT_4)The on_connected and on_disconnected methods will be called when the gadget pairs/unpairs with an Alexa device. We'll log this information and turn on the LEDs while connected so that it's clear for debugging.
def on_connected(self, device_addr):
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):
self.leds.set_color("LEFT", "BLACK")
self.leds.set_color("RIGHT", "BLACK")
logger.info("{} disconnected from Echo device".format(self.friendly_name))All of the good stuff happens in on_custom_mindstorms_gadget_control. We get the information we need out of the payload of the directive. Then, the dice type is used to determine how far to rotate. The count determines the number of rotations for the push motor. Finally, the motors returns to the starting position. To be safe we catch KeyError in case the directive is malformed. This helps catch bugs if you decide to edit the skill and it's not rolling dice as you expected.
def on_custom_mindstorms_gadget_control(self, directive):
try:
payload = json.loads(directive.payload.decode("utf-8"))
print("Control payload: {}".format(payload), file=sys.stderr)
dice_type = payload["type"]
dice_count = payload["count"]
estimated_pos = 0
# You can modify this function to respond to different dice types and add more
# rotate to the proper location
if dice_type == "D6":
estimated_pos = -108
elif dice_type == "D20" or dice_type == "D 20":
estimated_pos = -143
self.rotate.on_for_degrees(20, estimated_pos, brake=True)
# roll the dice
for i in range(dice_count):
self.push.on_for_rotations(40, 1);
time.sleep(0.25)
# return to the zero position
self.rotate.on_for_degrees(20, -estimated_pos, brake=True)
except KeyError:
print("Missing expected parameters: {}".format(directive), file=sys.stderr)Finally, outside of the MindstormsGadget we create an instance of our gadget. The condition ensures this last bit of code won't run if we were to try to import our gadget for use in another file.
if __name__ == '__main__':
gadget = MindstormsGadget()
# Set LCD font and turn off blinking LEDs
os.system('setfont Lat7-Terminus12x6')
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
# 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")Skill (index.js)
We'll only worry about index.js because the rest of the skill code is just boilerplate.
As always, we start with our imports. We're using the Alexa Skills Kit SDK for NodeJS and our util.js and common.js.
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');Next, we specify the namespace and directive name we will send to the gadget. It's necessary that this matches our gadget code.
// 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';Each intent is given a handler consisting of a canHandle function which determines if the handler can be used and a handle function which handles the intent. The LauchRequestHandler runs when the skill is launched and ensures our gadget is paired.
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle: async function(handlerInput) {
let request = handlerInput.requestEnvelope;
let { apiEndpoint, apiAccessToken } = request.context.System;
let apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
if ((apiResponse.endpoints || []).length === 0) {
return handlerInput.responseBuilder
.speak(`I couldn't find an EV3 Brick connected to this Echo device. Please check to make sure your EV3 Brick is connected, and try again.`)
.getResponse();
}
// Store the gadget endpointId to be used in this skill session
let endpointId = apiResponse.endpoints[0].endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
return handlerInput.responseBuilder
.speak("Welcome, you can ask me to roll dice for you")
.reprompt("Try saying \"Roll 2D6\"")
.getResponse();
}
};The RollDiceIntentHandler gets the information from the intent slots and sends a directive to our gadget. Then, it echos back the roll requested as it rolls.
const RollDiceIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'RollDiceIntent';
},
handle: function (handlerInput) {
let count = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Count');
let type = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Type');
if (!count || ! type) {
return handlerInput.responseBuilder
.speak("Can you repeat that?")
.reprompt("What was that again?").getResponse();
}
const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Construct the directive with the payload containing the move parameters
let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
count: parseInt(count),
type
});
return handlerInput.responseBuilder
.speak(`Rolling ${count}${type}`)
.addDirective(directive)
.getResponse();
}
};Finally, we use the SDK to combine all the intent handlers
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
RollDiceIntentHandler,
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