Hardware components | ||||||
| × | 2 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
| ||||||
|
Have you ever wanted to sort Christmas ornaments so you can decorate your tree how you want? It takes a lot of time and effort to go through every ornament. Instead, I have developed a sorter using the EV3 and Alexa. You say the command, Alexa will tell EV3 what to do, then EV3 will sort it.
DescriptionThe Ornament Sorter is based off the educator bot for EV3 set #45544. I have also included the attachment which will hold the ornament using a gear system to close and open the claw. It has a color sensor over the claw to read the color and sort it accordingly. This uses 1 EV3 brick, 2 large motors, 1 medium motor, and 1 color sensor as the main motors and sensors.
Walkthrough of the project.I wanted to make something easy and simple, also I wanted to sort my ornaments. Then I thought of the idea of letting the EV3 sort the ornaments. I first built the educator bot, since it was a good position for the motors and it also has an opening to put the medium motor. I based off an idea from The Lego Mindstorms EV3 Idea Book. I added the color sensor and made the claws better so it can hold the ornament better. We also used some pieces at the bottom to lift the ornament off the ground, so that the ornament does not get broken.
IssuesFaced
I had many issues while working on the project. Here are some of them:
- SD card was not flashed correctly:RESOLVED: Used Win-32 Disk Imager to reflash ev3dev image
- Bluetooth Connectivity Issue: RESOLVED: Re-flashed ev3dev image, Updated Linux to latest on brick.
- Code conversion Issues: The code always reported errors because it could not deploy onto the brick.RESOLVED: Converted file format using dos2Unix command.
- Color recognition issues: Alexa would not say the color because the brick would not report it correctly to lambdaRESOLVED: Re-coded python file to identify color.
Ornament claw attachment
Putting together and FINAL
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# You may not use this file except in compliance with the terms and conditions
# set forth in the accompanying LICENSE.TXT file.
#
# THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH
# RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
# THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
[GadgetSettings]
amazonId = <<YOUR AMAZON ID>>
alexaGadgetSecret = <<YOUR GADGET SECRET ID>>
[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0
{
"interactionModel": {
"languageModel": {
"invocationName": "ornament sorter",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "SetSortIntent",
"slots": [
{
"name": "Command",
"type": "CommandType"
}
],
"samples": [
"open {Command}",
"start {Command} mode",
"start {Command}",
"activate {Command} mode",
"activate {Command}"
]
}
],
"types": [
{
"name": "CommandType",
"values": [
{
"name": {
"value": "sorter"
}
}
]
}
]
}
}
}
const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');
// 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) {
const request = handlerInput.requestEnvelope;
const { apiEndpoint, apiAccessToken } = request.context.System;
const 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
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);
let speechOutput = "Welcome to Christmas Ornament Sorter. An unique Lego EV3 brick model to sort Christmas ornaments. What would you like to do?";
return handlerInput.responseBuilder
.speak(speechOutput)
.addDirective(Util.buildStartEventHandler(token,60000, {}))
.reprompt()
.getResponse();
}
};
const SetSortIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetSortIntent';
},
handle: function (handlerInput) {
let command = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Command');
if (!command) {
return handlerInput.responseBuilder
.speak("Can you repeat that?")
.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 = `command ${command} activated.`;
return handlerInput.responseBuilder
.speak(speechOutput)
.addDirective(directive)
.getResponse();
}
};
const EventsReceivedRequestHandler = {
// Checks for a valid token and endpoint.
canHandle(handlerInput) {
let { request } = handlerInput.requestEnvelope;
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;
let name = customEvent.header.name;
let speechOutput;
if (name === 'Speech') {
speechOutput = payload.speechOut;
} else {
speechOutput = "Event not recognized. Awaiting new command.";
}
return handlerInput.responseBuilder
.speak(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);
// Extends skill session
const speechOutput = `${duration} minutes remaining.`;
return handlerInput.responseBuilder
.addDirective(Util.buildStartEventHandler(token, 60000, {}))
.speak(speechOutput)
.getResponse();
}
else {
// End skill session
return handlerInput.responseBuilder
.speak("Skill duration expired. Goodbye.")
.withShouldEndSession(true)
.getResponse();
}
}
};
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
SetSortIntentHandler,
EventsReceivedRequestHandler,
ExpiredRequestHandler,
Common.HelpIntentHandler,
Common.CancelAndStopIntentHandler,
Common.SessionEndedRequestHandler,
Common.IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
)
.addErrorHandlers(
Common.ErrorHandler,
)
.lambda();
util.js
JavaScriptNo preview (download only).
/*
* 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 Util = require('./util');
const HelpIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speakOutput = 'You can say hello to me! How can I help?';
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 = '';
Util.putSessionAttribute(handlerInput, 'duration', 0);
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("I don't understand this command, try again")
.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 = `Sorry, I had trouble doing what you asked. Please try again.`;
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
};
#!/usr/bin/env python3
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# Made by Krishna Vemuri last update 12/25/19
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_A, OUTPUT_B, OUTPUT_C, MoveTank, SpeedPercent, MediumMotor
from ev3dev2.sensor.lego import ColorSensor
# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(message)s')
logging.getLogger().addHandler(logging.StreamHandler(sys.stderr))
logger = logging.getLogger(__name__)
class Command(Enum):
"""
The list of preset commands and their invocation variation.
These variations correspond to the skill slot values.
"""
SORT = ['sorter']
class Direction(Enum):
"""
The list of directional commands and their variations.
These variations correspond to the skill slot values.
"""
FORWARD = ['forward', 'forwards', 'go forward']
BACKWARD = ['back', 'backward', 'backwards', 'go backward']
LEFT = ['left', 'go left']
RIGHT = ['right', 'go right']
STOP = ['stop', 'brake', 'halt']
class EventName(Enum):
"""
The list of custom event name sent from this gadget
"""
SORT = "Sort"
SPEECH = "Speech"
class MindstormsGadget(AlexaGadget):
"""
A Mindstorms gadget that can perform bi-directional interaction with an Alexa skill.
"""
def __init__(self):
"""
Performs Alexa Gadget initialization routines and ev3dev resource allocation.
"""
super().__init__()
# Connect two large motors on output ports B and C
self.drive = MoveTank(OUTPUT_B, OUTPUT_C)
self.lift = MediumMotor(OUTPUT_A)
self.sound = Sound()
self.leds = Leds()
self.cs = ColorSensor()
self.cs.mode = 'COL-COLOR'
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")
self.lift.on_for_rotations(SpeedPercent(20), 1)
logger.info("{} 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")
logger.info("{} 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), file=sys.stderr)
control_type = payload["type"]
if control_type == "command":
# Expected params: [command]
self._activate(payload["command"])
except KeyError:
print("Missing expected parameters: {}".format(directive))
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), file=sys.stderr)
if command in Command.SORT.value:
self.lift.on_for_rotations(SpeedPercent(20), -1)
time.sleep(0.3)
if(int (self.cs.value())==5):
threading.Thread(target=self._send_event(EventName.SPEECH, {'speechOut': "Just sorted red colored ornament"}), daemon=True).start()
logger.info("Color is Red, {}".format(self.cs.value()))
time.sleep(1)
self._move('forward', 2, speed, is_blocking=True)
time.sleep(0.3)
self._move('right', 1, 20, is_blocking=True)
self._move('forward', 2, speed, is_blocking=True)
time.sleep(0.3)
self.lift.on_for_rotations(SpeedPercent(20), 1)
self._move('backward', 2, speed, is_blocking=True)
self.lift.on_for_rotations(SpeedPercent(20), -1)
self._move('left', 1, 20, is_blocking=True)
self._move('backward', 2, speed, is_blocking=True)
self.lift.on_for_rotations(SpeedPercent(20), 1)
elif(int (self.cs.value())==6):
threading.Thread(target=self._send_event(EventName.SPEECH, {'speechOut': "Just sorted white colored ornament"}), daemon=True).start()
logger.info("Color is White, {}".format(self.cs.value()))
time.sleep(1)
self._move('forward', 4, speed, is_blocking=True)
time.sleep(0.3)
self.lift.on_for_rotations(SpeedPercent(20), 1)
self._move('backward', 2, speed, is_blocking=True)
self.lift.on_for_rotations(SpeedPercent(20), -1)
self._move('backward', 2, speed, is_blocking=True)
self.lift.on_for_rotations(SpeedPercent(20), 1)
elif(int (self.cs.value())==2):
threading.Thread(target=self._send_event(EventName.SPEECH, {'speechOut': "Just sorted blue colored ornament"}), daemon=True).start()
logger.info("Color is Blue, {}".format(self.cs.value()))
time.sleep(1)
self._move('forward', 2, speed, is_blocking=True)
time.sleep(0.3)
self._move('left', 1, 20, is_blocking=True)
self._move('forward', 2, speed, is_blocking=True)
time.sleep(0.3)
self.lift.on_for_rotations(SpeedPercent(20), 1)
self._move('backward', 2, speed, is_blocking=True)
self.lift.on_for_rotations(SpeedPercent(20), -1)
self._move('right', 1, 20, is_blocking=True)
self._move('backward', 2, speed, is_blocking=True)
self.lift.on_for_rotations(SpeedPercent(20), 1)
# Indicate readiness through LEDs
self.leds.set_color("LEFT", "YELLOW", 1)
self.leds.set_color("RIGHT", "YELLOW", 1)
time.sleep(0.3)
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), file=sys.stderr)
if direction in Direction.FORWARD.value:
self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(speed), duration, block=is_blocking)
if direction in Direction.BACKWARD.value:
self.drive.on_for_seconds(SpeedPercent(-speed), SpeedPercent(-speed), duration, block=is_blocking)
if direction in (Direction.RIGHT.value + Direction.LEFT.value):
self._turn(direction, speed)
if direction in Direction.STOP.value:
self.drive.off()
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)
if direction in Direction.RIGHT.value:
self.drive.on_for_seconds(SpeedPercent(speed), SpeedPercent(0), 2)
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)
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")
{
"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"
}
}
Comments