Lee Zhong
Created December 13, 2019 © GPL3+

LEGO Plantbot

Using Lego and Amazon Echo to take care of the plant

ExpertFull instructions providedOver 1 day100

Things used in this project

Story

Read more

Schematics

Plantbot LEGO Build Instructions

Plantbot LEGO build instructions

peristaltic pump

peristaltic pump

Wiring diagram

wiring diagram for the plantbot

Code

plantbot.py

Python
plantbot.py using ev3dev2 to control ev3
import os
import sys
import time
import logging
import json
import random
import threading
from enum import Enum
from smbus2 import SMBus, i2c_msg
from agt import AlexaGadget
from ev3dev2.led import Leds
from ev3dev2.sound import Sound
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 EventName(Enum):
    """
    The list of custom event name sent from this gadget
    """
    HEALTH = "Health"
    PLANT = "Plant"

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__()
        # the default I2C address of the sensor
        self.I2C_ADDRESS = 0x21  
        
        # setup the buses
        self.moisbus = SMBus(3)
        self.relaybus = SMBus(4)

        #setup the moisbus and relaybus
        self.moisbus.write_byte_data(self.I2C_ADDRESS, 0x42, 0x01)
        self.relaybus.write_byte_data(self.I2C_ADDRESS, 0x42, 0x02)

        self.isDry = False
        self.isWet = False

        #setup the lastmois so we can track it well
        self.lastmois = 0
        self.count = 0
        # Robot state
        self.auto_mode = False

        self.sound = Sound()
        self.leds = Leds()
        self.color = ColorSensor()

        # Start threads
        threading.Thread(target=self._autowater_thread, 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")
        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 == "plant":
                self._plant_handler()
            elif control_type == "water":
                self._water_handler()
            elif control_type == "health":
                self._health_handler()
            elif control_type == "auto":
                self._auto_handler(payload["command"])
        except KeyError:
            print("Missing expected parameters: {}".format(directive), file=sys.stderr)

    def _plant_handler(self):
        #turning plants
        if self.lastmois < 300:
            self._send_event(EventName.PLANT, {'speech': "Soil moisture has is very dry, please water the plant"})
        else:
            self._send_event(EventName.PLANT, {'speech': "Soil moisture is doing very good, and the plant is very happy"})
    
    def _water_handler(self):
        while True:
            part1 = self.moisbus.read_byte_data(self.I2C_ADDRESS, 0x44)
            part2 = self.moisbus.read_byte_data(self.I2C_ADDRESS, 0x45)
            result = (part1 << 2) + part2
            if result < 300:
                #turn on the pump
                self.relaybus.write_byte_data(self.I2C_ADDRESS, 0x42, 0x03)
            else:
                break
        #turn off the pump
        self.relaybus.write_byte_data(self.I2C_ADDRESS, 0x42, 0x02)

    def _health_handler(self):
        print(self.color.color)
        if self.color.color == ColorSensor.COLOR_GREEN:
            self._send_event(EventName.PLANT, {'speech': "The plant is green and healthy"})
        elif self.color.color == ColorSensor.COLOR_YELLOW or self.color.color == ColorSensor.COLOR_BROWN:
            self._send_event(EventName.PLANT, {'speech': "The plant leaves appears yellow, it may have diseases"})

    def _auto_handler(self, onoff):
        if onoff == "on":
            self.auto_mode = True
        else:
            self.auto_mode = False

    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)

    def _autowater_thread(self):
        """
        Performs random movement when patrol mode is activated.
        """
        while True:
            part1 = self.moisbus.read_byte_data(self.I2C_ADDRESS, 0x44)
            part2 = self.moisbus.read_byte_data(self.I2C_ADDRESS, 0x45)
            result = (part1 << 2) + part2
            print(result)

            if self.count > 0:
                if self.isDry and result <= 300:
                    gadget.leds.set_color("LEFT", "RED")
                    gadget.leds.set_color("RIGHT", "RED")
                    self.count = 0
                    self.isDry = False
                    if self.auto_mode:
                        self.relaybus.write_byte_data(self.I2C_ADDRESS, 0x42, 0x03)
                        self._send_event(EventName.PLANT, {'speech': "Auto watering the plant because soil moisture has just turned very dry"})
                    else:
                        self._send_event(EventName.PLANT, {'speech': "Soil moisture has just turned very dry, please water the plant"})
                elif self.isWet and result >= 300:
                    gadget.leds.set_color("LEFT", "GREEN")
                    gadget.leds.set_color("RIGHT", "GREEN")
                    self.count = 0
                    self.isWet = False
                    if self.auto_mode:
                        self.relaybus.write_byte_data(self.I2C_ADDRESS, 0x42, 0x02)
                        self._send_event(EventName.PLANT, {'speech': "Stopped  auto watering because soil moist is high now"})
                    else:
                        self._send_event(EventName.PLANT, {'speech': "Soil Moisture just turned wet, we are stopping watering the plant"})
                else:
                    self.count = 0
                    self.isDry = False
                    self.isWet = False

            if result <= 300 and self.lastmois > 300:
                self.count = self.count + 1
                self.isDry = True
                #turns on when it gets too dry
            elif result >= 300 and self.lastmois < 300:
                self.count = self.count + 1
                self.isWet = True
                #turns off when it's wet already
                    
            self.lastmois = result
            time.sleep(1)


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")

model.json

JSON
model for Alexa
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "plant bot",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "PlantIntent",
                    "slots": [],
                    "samples": [
                        "How is the plant doing",
                        "What's the status for the plant"
                    ]
                },
                {
                    "name": "WaterIntent",
                    "slots": [],
                    "samples": [
                        "Water the plant",
                        "Pump some water",
                        "Water it"
                    ]
                },
                {
                    "name": "HealthIntent",
                    "slots": [],
                    "samples": [
                        "What's the color of the leaf",
                        "How is plant health",
                        "How is the health of the plant"
                    ]
                },
                {
                    "name": "AutoIntent",
                    "slots": [
                        {
                            "name": "OnOff",
                            "type": "OnOffType"
                        }
                    ],
                    "samples": [
                        "Turn {OnOff} auto watering",
                        "Turn {OnOff} auto",
                        "Turn {OnOff} auto mode"
                    ]
                }
            ],
            "types": [
                {
                    "name": "OnOffType",
                    "values": [
                        {
                            "name": {
                                "value": "on"
                            }
                        },
                        {
                            "name": {
                                "value": "off"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

index.js

JavaScript
Alexa index.js file, rest of the Alex file is similar to mission-04
/*
 * 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.
*/

// This skill sample demonstrates how to send directives and receive events from an Echo connected gadget.
// This skill uses the Alexa Skills Kit SDK (v2). Please visit https://alexa.design/cookbook for additional
// examples on implementing slots, dialog management, session persistence, api calls, and more.

const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');

// The audio tag to include background music
const BG_MUSIC = '<audio src="soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_waiting_loop_30s_01"></audio>';

// 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, Plant bot is here to help";
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(Util.buildStartEventHandler(token,60000, {}))
            .getResponse();
    }
};


// Construct and send a custom directive to the connected gadget with data from
// the ComeIntent.
const PlantIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlantIntent';
    },
    handle: function (handlerInput) {
        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: 'plant',
                command: 'plant',
                speed: speed
            });

        let speechOutput = 'Checking moisture level for the plant';
        
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(directive)
            .getResponse();
    }
};

// Construct and send a custom directive to the connected gadget with data from
// the WaterIntent.
const WaterIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'WaterIntent';
    },
    handle: function (handlerInput) {

        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: 'water',
                command: 'water',
                speed: speed
            });

        let speechOutput = 'Watering the plant until soil is moist';
        
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(directive)
            .getResponse();
    }
};


// Construct and send a custom directive to the connected gadget with data from
// the HealthIntentHandler.
const HealthIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HealthIntent';
    },
    handle: function (handlerInput) {

        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: 'health',
                command: 'health',
                speed: speed
            });

        let speechOutput = 'Checking health of the plant, please wait';
        
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(directive)
            .getResponse();
    }
};


// Construct and send a custom directive to the connected gadget with data from
// the AutohIntentHandler.
const AutoIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AutoIntent';
    },
    handle: function (handlerInput) {

        let onoff = Alexa.getSlotValue(handlerInput.requestEnvelope, 'OnOff');
        if (!onoff) {
            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: 'auto',
                command: onoff,
                speed: speed
            });

        let speechOutput = 'Turning auto watering ' + onoff;
        
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(directive)
            .getResponse();
    }
};

// Add the speed value to the session attribute.
// This allows other intent handler to use the specified speed value
// without asking the user for input.
const SetSpeedIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetSpeedIntent';
    },
    handle: function (handlerInput) {

        // Bound speed to (1-100)
        let speed = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Speed');
        speed = Math.max(1, Math.min(100, parseInt(speed)));
        Util.putSessionAttribute(handlerInput, 'speed', speed);

        let speechOutput = `speed set to ${speed} percent.`;
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .getResponse();
    }
};

// Construct and send a custom directive to the connected gadget with
// data from the MoveIntent.
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');

        // Duration is optional, use default if not available
        const duration = Alexa.getSlotValue(request, 'Duration') || "2";

        // Get data from session attribute
        const attributesManager = handlerInput.attributesManager;
        const speed = attributesManager.getSessionAttributes().speed || "50";
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];

        // Construct the directive with the payload containing the move parameters
        let directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'move',
                direction: direction,
                duration: duration,
                speed: speed
            });

        const speechOutput = (direction === "brake")
            ?  "Applying brake"
            : `${direction} ${duration} seconds at ${speed} percent speed`;

        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .addDirective(directive)
            .getResponse();
    }
};

// Construct and send a custom directive to the connected gadget with data from
// the SetCommandIntent.
const SetCommandIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SetCommandIntent';
    },
    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`;
        if (command === 'sentry' || command === 'sentry mode') {
            speechOutput = '';
        }
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC)
            .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 === 'Plant') {
            speechOutput = payload.speech;
        }
        return handlerInput.responseBuilder
            .speak(speechOutput + BG_MUSIC, "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 + BG_MUSIC)
                .getResponse();
        }
        else {
            // End skill session
            return handlerInput.responseBuilder
                .speak("Skill duration expired. Goodbye.")
                .withShouldEndSession(true)
                .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,
        PlantIntentHandler,
        WaterIntentHandler,
        HealthIntentHandler,
        AutoIntentHandler,
        EventsReceivedRequestHandler,
        ExpiredRequestHandler,
        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();

plantbot.zip

Python
plantbot code base
No preview (download only).

Credits

Lee Zhong

Lee Zhong

1 project • 0 followers
13 year old maker, trying to go to the best high school possible
Thanks to Peter Ma.

Comments