Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
|
Is there anything worse in life than having to get up in the morning and drag your curtains open. This device, based on Mindstorms EV3 and Alexa, will let you do all that work for you by voice. The device, attaches right to your curtain rod and automatically moves them open or closed. The curtain opening robot uses a wheel on the top of it that drags itself back and forth automatically. It is an affordable retrofit solution to automate your existing curtains. It is easy to build and install.
It consists of two parts : a fixed one : the EV3 brick attached to the end of the rod and a mobile part : a dual motors with a spinning wheel that drags along the rod.
The issue I faced to build this project is that the cables that come with Mindstorms EV3 are too short. Thus, I've used a regular RJ12 cable, removed the latch (center position) and 3d printed a new latch that attach to the cable socket (right side).
The 3d STL file is available on Thingverse : https://www.thingiverse.com/thing:3145299
FIxed part:
There are many ways to attach the EV3 brick to the rod, I've chosen a structure with (a double inverted U)
Mobile part:
The mobile part consists of two large motors attached to a Tire.
Each motor has two arms to prevent the wheel from being blocked by the curtains.
How to use:
First, start Alexa Mindstorms skill: "Alexa, open Mindstorms"Then you can issue a command.
Open or close / left or right curtain / for xx centimeters
#!/usr/bin/env python3
import os
import sys
import time
import logging
import json
import random
import threading
from math import pi
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_C, SpeedPercent, MoveTank, OUTPUT_B, OUTPUT_D
# 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 Direction(Enum):
"""
The list of directional commands and their variations.
These variations correspond to the skill slot values.
"""
LEFT = ['gauche']
RIGHT = ['droit']
STOP = ['stop', 'arrêt', 'arrête']
class Command(Enum):
"""
The list of preset commands and their invocation variation.
These variations correspond to the skill slot values.
"""
OPEN = ['ouvre']
CLOSE = ['ferme']
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__()
# Ev3dev initialization
self.leds = Leds()
self.sound = Sound()
self.driveLeft = MoveTank(OUTPUT_A, OUTPUT_C) # left curtain
#self.driveRight = MoveTank(OUTPUT_B, OUTPUT_D) # right curtain
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")
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 == "move":
# Expected params: [direction, distance, command]
self._move(payload["direction"], int(payload["distance"]), payload["command"])
except KeyError:
print("Missing expected parameters: {}".format(directive), file=sys.stderr)
def _move(self, direction, distance: int, command, 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 distance: the distance in seconds
:param speed: the speed percentage as an integer
:param command: open or close command
:param is_blocking: if set, motor run until duration expired before accepting another command
"""
print("Move command: ({}, {}, {}, {})".format(direction, distance, command, is_blocking), file=sys.stderr)
speed = 90
distancePerRotation = 43.2 * pi
rotations = 10 * distance / distancePerRotation
""" if direction in Direction.RIGHT.value:
if command in Command.OPEN.value:
self.driveRight.on_for_seconds(SpeedPercent(speed), SpeedPercent(speed), rotations, block=is_blocking)
elif command in Command.CLOSE.value:
self.driveRight.on_for_seconds(SpeedPercent(-speed), SpeedPercent(-speed), rotations, block=is_blocking) """
if direction in Direction.LEFT.value:
if command in Command.OPEN.value:
self.driveLeft.on_for_rotations(SpeedPercent(speed), SpeedPercent(speed), rotations, block=is_blocking)
elif command in Command.CLOSE.value:
self.driveLeft.on_for_seconds(SpeedPercent(-speed), SpeedPercent(-speed), rotations, block=is_blocking)
if direction in Direction.STOP.value:
#self.driveRight.off()
self.driveRight.off()
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")
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) {
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(`Je n'arrive pas à trouver une brique EV3 connectée à Echo. Merci de vérifier que votre brique est bien connecté et ré-essayer.`)
.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("Bonjour, vous pouvez lancer des commandes")
.reprompt("J'attends vos commandes")
.getResponse();
}
};
// Construct and send a custom directive to the connected gadget with
// data from the MoveIntent request.
const MoveIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveIntent';
},
handle: function (handlerInput) {
const request = handlerInput.requestEnvelope;
const direction = Alexa.getSlotValue(request, 'Direction') || "gauche";
const command = Alexa.getSlotValue(request, 'Command') || "stop";
// Duration is optional, use default if not available
const distance = Alexa.getSlotValue(request, 'Distance') || "50";
// Get data from session attribute
const attributesManager = handlerInput.attributesManager;
const endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Construct the directive with the payload containing the move parameters
const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
{
type: 'move',
direction: direction,
distance: distance,
command: command
});
const speechOutput = (direction === "brake")
? "Arrêt en cours"
: `${direction} ${distance} centimètres`;
return handlerInput.responseBuilder
.speak(speechOutput)
.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,
MoveIntentHandler,
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();
{
"interactionModel": {
"languageModel": {
"invocationName": "mindstorms",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "MoveIntent",
"slots": [
{
"name": "Direction",
"type": "DirectionType"
},
{
"name": "Distance",
"type": "AMAZON.NUMBER"
},
{
"name": "Command",
"type": "CommandType"
}
],
"samples": [
"{Command} rideau {Direction}",
"{Command} le rideau {Direction}",
"{Command} {Direction}",
"{Command} {Direction} {Distance} centimètres",
"{Command} rideau {Direction} pour {Distance} centimètres",
"{Command} le rideau {Direction} pour {Distance} centimètres",
"{Command} {Direction} pour {Distance} centimètres",
"{Command}"
]
}
],
"types": [
{
"name": "DirectionType",
"values": [
{
"name": {
"value": "gauche"
}
},
{
"name": {
"value": "droit"
}
},
{
"name": {
"value": "stop"
}
},
{
"name": {
"value": "arrêt"
}
},
{
"name": {
"value": "arrête"
}
}
]
},
{
"name": "CommandType",
"values": [
{
"name": {
"value": "ouvre"
}
},
{
"name": {
"value": "ferme"
}
}
]
}
]
}
}
}
Comments