Kevin Le Brun
Created September 25, 2019 © MIT

Lego EV3 Orbital Tracker

I'm using a voice controlled device that points to and tracks orbital bodies (planets, satellite, etc.).

IntermediateFull instructions provided2 hours31
Lego EV3 Orbital Tracker

Things used in this project

Hardware components

Mindstorms EV3 Programming Brick / Kit
LEGO Mindstorms EV3 Programming Brick / Kit
I'm using the education kit with an additional large turntable. The retail set 31313 will do just fine with this additional part.
×1
Echo Dot
Amazon Alexa Echo Dot
×1

Story

Read more

Code

index.js

JavaScript
The main Node.js lambda file that interprets the intents.
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 ne trouve pas de brique EV3. Veuillez vérifier que votre brique EV3 est connectée et reéssayer.`)
            .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("Quelle cible souhaitez vous que je vous montre ?")
            .reprompt("Quelle cible souhaitez vous que je vous montre ?")
            .getResponse();
    }
};

const MoveTargetHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveIntent';
    },
    handle: async function(handlerInput) {
        const request = handlerInput.requestEnvelope;
        const target = Alexa.getSlotValue(request, 'Target');
    
        // 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',
                target: target
            });

        return handlerInput.responseBuilder
            .speak(`Voici la position de ${target} dans le ciel.`)
            .reprompt("Quelle cible souhaitez vous que je vous montre ?")
            .addDirective(directive)
            .getResponse();
    }
};

const TrackTargetHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'TrackIntent';
    },
    handle: async function(handlerInput) {
        const request = handlerInput.requestEnvelope;
        const target = Alexa.getSlotValue(request, 'Target');
    
        // 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: 'track',
                target: target
            });

        return handlerInput.responseBuilder
            .speak(`Le tracker suit la position de ${target}.`)
            .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,
        MoveTargetHandler,
        TrackTargetHandler,
        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
The intents and slot definitions for the Alexa Skill.
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "tracker",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "MoveIntent",
                    "slots": [
                        {
                            "name": "Target",
                            "type": "TargetType"
                        }
                    ],
                    "samples": [
                        "pointe vers {Target}",
                        "montre moi {Target}",
                        "où est {Target}",
                        "où se touve {Target}",
                        "dans quelle direction est {Target}"
                    ]
                },
                {
                    "name": "TrackIntent",
                    "slots": [
                        {
                            "name": "Target",
                            "type": "TargetType"
                        }
                    ],
                    "samples": [
                        "tracke {Target}",
                        "suit {Target}"
                    ]
                }
            ],
            "types": [
                {
                    "name": "TargetType",
                    "values": [
                        {
                            "name": {
                                "value": "le soleil"
                            }
                        },
                        {
                            "name": {
                                "value": "la lune"
                            }
                        },
                        {
                            "name": {
                                "value": "mercure"
                            }
                        },
                        {
                            "name": {
                                "value": "venus"
                            }
                        },
                        {
                            "name": {
                                "value": "mars"
                            }
                        },
                        {
                            "name": {
                                "value": "jupiter"
                            }
                        },
                        {
                            "name": {
                                "value": "saturne"
                            }
                        },
                        {
                            "name": {
                                "value": "uranus"
                            }
                        },
                        {
                            "name": {
                                "value": "neptune"
                            }
                        },
                        {
                            "name": {
                                "value": "l'étoile polaire"
                            }
                        },
                        {
                            "name": {
                                "value": "la station spatiale internationale"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

mission-05.py

Python
The main code of the fifth mission to deploy on the EV3 device.
import datetime
from enum import Enum
import json
from math import degrees as deg
import logging
import threading
from time import sleep

import requests
import ephem

from ev3dev2.button import Button
from ev3dev2.led import Leds
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, LargeMotor
from ev3dev2.sound import Sound

from agt import AlexaGadget

# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO)

# Current GPS coordinates for better accuracy
# Those can be acquired with https://www.mooncalc.org/ by filling your address.
# We could also use an API call to fetch those (google places for ex.) but relying on the
# ISP is rather inprecise in my case.
lat = '48.9146689'
lon = '2.244790'
alt = '55'

azimuthGearRatio = 7
altitudeGearRatio = 1

class Target(Enum):
    """
    The list of targets their french variations.
    These variations correspond to the skill slot values.
    """
    MOON = ['la lune']
    SUN = ['le soleil']
    MERCURY = ['mercure']
    VENUS = ['venus']
    MARS = ['mars']
    JUPITER = ['jupiter']
    SATURN = ['saturn']
    URANUS = ['uranus']
    NEPTUNE = ['neptune']
    ISS = ['la station spatiale internationale', 'iss']
    POLARIS = ["l'étoile polaire"]


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.azimuthMotor = LargeMotor(OUTPUT_A)
        self.elevationMotor = LargeMotor(OUTPUT_B)

        self.currentAzimuth = 0 # assuming we boot with pointer to north position, at 0
        self.currentElevation = 0 # assuming we boot with elevation // to the ground, at 0

        self.trackerBody = None

        # Start threads
        threading.Thread(target=self._trackerThread, daemon=True).start()

    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 == "move":
                # Expected params: [target]
                self._move(payload["target"])

            if control_type == "track":
                # Expected params: [target]
                self._track(payload["target"])

        except KeyError:
            print("Missing expected parameters: {}".format(directive))

    def _move(self, target, is_blocking=False):
        """
        Handles move commands from the directive.
        :param target: the target to track
        :param is_blocking: if set, motor run until duration expired before accepting another command
        """
        print("Move command: ({}, {})".format(target, is_blocking))

        body = self._bodyFromTarget(target)
        if body is not None:
            self.trackerBody = None
            self._moveTowards(body)

    def _track(self, target, is_blocking=False):
        """
        Handles track commands from the directive.
        :param target: the target to track
        :param is_blocking: if set, motor run until duration expired before accepting another command
        """
        print("Track command: ({}, {})".format(target, is_blocking))

        body = self._bodyFromTarget(target)
        if body is not None:
            self.trackerBody = body

    def _bodyFromTarget(self, target):
        if target in Target.MOON.value:
            return ephem.Moon()

        if target in Target.SUN.value:
            return ephem.Sun()

        if target in Target.MERCURY.value:
            return ephem.Mercury()

        if target in Target.VENUS.value:
            return ephem.Venus()

        if target in Target.MARS.value:
            return ephem.Mars()

        if target in Target.JUPITER.value:
            return ephem.Jupiter()

        if target in Target.SATURN.value:
            return ephem.Saturn()

        if target in Target.URANUS.value:
            return ephem.Uranus()

        if target in Target.NEPTUNE.value:
            return ephem.Neptune()

        if target in Target.ISS.value:
            # The TLE can be found on NASA website: http://spaceflight.nasa.gov/realdata/sightings/SSapplications/Post/JavaSSOP/orbit/ISS/SVPOST.html
            return ephem.readtle('ISS',
                '1 25544U 98067A   19357.53566238  .00016717  00000-0  10270-3 0  9000',
                '2 25544  51.6386 142.6138 0007507  66.8859 293.3082 15.50125057  4627'
            )

        if target in Target.POLARIS.value:
            return ephem.readdb("Polaris,f|M|F7,2:31:48.704,89:15:50.72,2.02,2000")

        return None

    def _moveTowards(self, body):
        home = ephem.Observer()
        home.lat, home.lon = lat, lon
        home.date = datetime.datetime.utcnow()

        body.compute(home)
        azimuth  = round(deg(float(body.az)),1)
        altitude = round(deg(float(body.alt)),1)

        diffAzMotorAngle = azimuth - self.currentAzimuth
        diffAltMotorAngle = altitude - self.currentElevation
        
        self.azimuthMotor.on_for_degrees(30, diffAzMotorAngle * azimuthGearRatio)
        self.elevationMotor.on_for_degrees(30, diffAltMotorAngle * altitudeGearRatio)

        self.currentAzimuth = azimuth
        self.currentElevation = altitude

    def _trackerThread(self):
        """
        Continuously move forward a given body
        """
        while True:
            while self.trackerBody is not None:
                self._moveTowards(self.trackerBody)
                sleep(5)
            sleep(1)


if __name__ == '__main__':

    # Startup sequence
    gadget = MindstormsGadget()
    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")

Credits

Kevin Le Brun

Kevin Le Brun

2 projects • 1 follower

Comments