Hackster is hosting Hackster Holidays, Ep. 4: Livestream & Giveaway Drawing. Start streaming on Wednesday!Stream Hackster Holidays, Ep. 4 on Wednesday!
Amine Amri
Published

Auto-Curtains

Affordable retrofit solution to automate your existing curtains with an EV3 brick.

BeginnerFull instructions provided2 hours2,046

Things used in this project

Hardware components

Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
×1
Amazon Echo
Amazon Alexa Amazon Echo
×1
Telephone Modular Cable, RJ12 Plug to RJ12 Plug
Telephone Modular Cable, RJ12 Plug to RJ12 Plug
×1

Software apps and online services

Visual Studio Code Extension for Arduino
Microsoft Visual Studio Code Extension for Arduino

Story

Read more

Custom parts and enclosures

Ev3 cable holder

Code

AutoCurtains.py

Python
Code to be executed in EV3 intelligent Brick
#!/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")

index.js

JavaScript
Alexa Skill Code
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();

model.json

JSON
{
    "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"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Credits

Amine Amri

Amine Amri

10 projects • 19 followers
I’m a financial risk engineer in Paris, France with a passion for computer science, electrical engineering, robotics and embedded systems.

Comments