Ufuk ARSLAN
Published © MIT

AlexaBoat

AlexaBoat is using Alexa Skills Kit, AWS Lambda, AWS IOT and Raspberry Pi to control your boat from anywhere.

IntermediateProtip8 hours3,058

Things used in this project

Hardware components

Amazon Tap
Amazon Alexa Amazon Tap
×1
Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
×1
Relay (generic)
×2
Huawei 3G Dongle
×1
Jumper wires (generic)
Jumper wires (generic)
×1
LED (generic)
LED (generic)
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
AWS Lambda
Amazon Web Services AWS Lambda
AWS IoT
Amazon Web Services AWS IoT
Raspberry Pi Raspbian Jessie

Story

Read more

Schematics

VUI Diagram

VUI diagram for the project
Alexaboat%20vui%20diagram

Connections

Relays I used had 1 more pin on them. So I'm not sure if I drew this right. But it shows the basic idea.

Code

Intent Schema.json

JSON
{
   "intents":[
      {
         "intent":"Open",
         "slots":[
            {
               "name":"Task",
               "type":"Input"
            }
         ]
      },
      {
         "intent":"Close",
         "slots":[
            {
               "name":"Task",
               "type":"Input"
            }
         ]
      },
      {
         "intent":"Anchor",
         "slots":[
            {
               "name":"Direction",
               "type":"LITERAL"
            },
            {
               "name":"Distance",
               "type":"NUMBER"
            }
         ]
      }
   ]
}

Sample Utterances.json

JSON
Open {Task}
Close {Task}
Anchor {up | Direction} {twenty five | Distance} meters
Anchor {down | Direction} {one | Distance} meters

package.json

JSON
for use with Lambda
{
  "name": "Bearcat",
  "version": "0.0.1",
  "description": "MQTT from lambda",
  "main": "index.js",
  "dependencies": {
    "aws-iot-device-sdk": "*"
  }
}

index.js

JavaScript
for use with Lambda
/**
 * Control your boat with your voice, using Amazon Alexa, Lambda, IOT, MQTT.
 */

var awsIot = require('aws-iot-device-sdk');
var config = require("./config");

var deviceName = "AlexaBoat";

var mqtt_config = {
    "keyPath": "./certs/YourKey-private.pem.key",
    "certPath": "./certs/YourKey-certificate.pem.crt",
    "caPath": "./certs/rootCA.key",
    "host": config.host,
    "port": 8883,
    "clientId": "RaspberryPi-" + deviceName, 
    "region":"us-west-1",
    "debug":true
};

var ctx = null;
var client = null;

// Route the incoming request based on type (LaunchRequest, IntentRequest, etc.) The JSON body of the request is provided in the event parameter.
exports.handler = function (event, context) {
    try {
        ctx = context;

        if (event.session.application.applicationId !== config.app_id) {
             ctx.fail("Invalid Application ID");
         }

        client = awsIot.device(mqtt_config);

        client.on("connect",function(){
            console.log("Connected to AWS IoT");
        });


        if (event.session.new) {
            onSessionStarted({requestId: event.request.requestId}, event.session);
        }

        if (event.request.type === "LaunchRequest") {
            onLaunch(event.request, event.session);
        }  else if (event.request.type === "IntentRequest") {
            onIntent(event.request, event.session);
        } else if (event.request.type === "SessionEndedRequest") {
            onSessionEnded(event.request, event.session);
            ctx.succeed();
        }
    } catch (e) {
        console.log("EXCEPTION in handler:  " + e);
        ctx.fail("Exception: " + e);
    }
};

/**
 * Called when the session starts.
 */
function onSessionStarted(sessionStartedRequest, session) {
}


/**
 * Called when the user launches the skill without specifying what they want.
 */
function onLaunch(launchRequest, session, callback) {

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session ) {
    try{
    var intent = intentRequest.intent,
    intentName = intentRequest.intent.name;

    console.log("REQUEST to string =" + JSON.stringify(intentRequest));

    console.log("Ufuk =" + intentName);

    var callback = null;
    // Dispatch to your skill's intent handlers
    if ("Open" === intentName) {
        doCommandIntent(intent, session);
    }
    else if ("Close" === intentName) {
        doCommandIntent(intent, session);
    }
    else if ("Anchor" === intentName) {
        anchorCommandIntent(intent, session);
    }
    else if("AMAZON.HelpIntent"=== intentName) {
        helpCommandIntent(session);
    }
    else if("AMAZON.StopIntent"=== intentName) {
        ctx.succeed(buildResponse(null, buildSpeechletResponse(null, "Goodbye", "", true)));
    }
    else if("AMAZON.CancelIntent"=== intentName) {
        ctx.succeed(buildResponse(null, buildSpeechletResponse(null, "Goodbye", "", true)));
    }
    else {
        ctx.succeed(buildResponse(null, buildSpeechletResponse(null, "I can't help you with that. Invalid Request", "", true)));
    }
    }
    catch(ex)
    {
        ctx.succeed(buildResponse(null, buildSpeechletResponse(null, "There is an error with the request. Please try again.", "", false)));
    }
}

/**
 * Called when the user ends the session.
 * Is not called when the skill returns shouldEndSession=true.
 */
function onSessionEnded(sessionEndedRequest, session) {
    // Add cleanup logic here
}

// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse() {
    // If we wanted to initialize the session to have some attributes we could add those here.
    var sessionAttributes = {};
    var cardTitle = "Welcome";
    var speechOutput = "Welcome to the Boat Hand. You can say Open Lights, or, you can say Anchor Up for Twenty Meters... What can I help you with?";

    var repromptText = "Your Boat is ready for command.";
    var shouldEndSession = false;

    ctx.succeed(buildResponse(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)));
}


function doCommandIntent(intent, session, callback) {
    var repromptText = null;
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "";

    repromptText = "Tell me what is the command for the boat.";
    
    var task = intent.slots.Task.value;
    
    var punctuationless = task.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
    var task = punctuationless.replace(/\s{2,}/g," ");
    
    var validTasks = [ "LIGHTS" ];
    
    if (validTasks.indexOf(task.toString().toUpperCase()) < 0)
    {
        speechOutput = "I couldn't understand the command "+task+". Please try again.";
        ctx.succeed(buildResponse(sessionAttributes, buildSpeechletResponse(null, speechOutput, repromptText, false)));
    }
    else
    {
        var cardTitle = "Executing Open command " + task ;
        speechOutput = "Executing command " + task;
        mqttPublish(intent, sessionAttributes, cardTitle, speechOutput, repromptText, shouldEndSession);
    }
}

function anchorCommandIntent(intent, session, callback) {
    var repromptText = null;
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "";

    repromptText = "Tell me what is the command for the boat.";

    var direction = intent.slots.Direction.value;
    var distance = intent.slots.Distance.value;
    
    
    var validDirections = [ "UP","DOWN" ];
    
    console.log("ufuk " + new RegExp(validDirections.join("|")).test(direction.toUpperCase()));
    
    if(distance === "?" || direction === "?")
    {
        throw "error";
    }
    else if(new RegExp(validDirections.join("|")).test(direction.toUpperCase()))
    {
        var cardTitle = "Executing command Anchor" ;
        speechOutput = "Executing Anchor " + direction + " for " + distance + " meters";
        mqttPublish(intent, sessionAttributes, cardTitle, speechOutput, repromptText, shouldEndSession);
    }
    else{
        speechOutput = "I couldn't understand the command "+ direction+". Please try again.";
        ctx.succeed(buildResponse(sessionAttributes, buildSpeechletResponse(null, speechOutput, repromptText, false)));
    }
}

function helpCommandIntent(intent, session, callback) {
    var repromptText = null;
    var sessionAttributes = {};
    var shouldEndSession = false;
    var speechOutput = "";

    var cardTitle = "Help" ;
    var speechOutput = "You can say Open Lights, or, you can say Anchor Up for Twenty Meters... What can I help you with?";
    var repromptText = "What can I help you with?";
        
    ctx.succeed(buildResponse(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)));
}

function mqttPublish(intent, sessionAttributes, cardTitle, speechOutput, repromptText, shouldEndSession)
{
    var strIntent = JSON.stringify(intent);
    console.log("mqttPublish:  INTENT text = " + strIntent);
    
    client.publish(config.topic, strIntent, function() {
        client.end();
    });
    
    client.on("close", (function () {
        console.log("MQTT CLIENT CLOSE - thinks it's done, successfully. ");
        ctx.succeed(buildResponse(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)));
    }));

    client.on("error", (function (err, granted) {
        console.log("MQTT CLIENT ERROR!!  " + err);
    }));
}


// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    if(title != null)
        return {
            outputSpeech: {
                type: "PlainText",
                text: output
            },
            card: {
                type: "Simple",
                title: title,
                content: output
            },
            reprompt: {
                outputSpeech: {
                    type: "PlainText",
                    text: repromptText
                }
            },
            shouldEndSession: shouldEndSession
        };
    else
        return {
            outputSpeech: {
                type: "PlainText",
                text: output
            },
            reprompt: {
                outputSpeech: {
                    type: "PlainText",
                    text: repromptText
                }
            },
            shouldEndSession: shouldEndSession
        }
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: "1.0",
        sessionAttributes: sessionAttributes,
        response: speechletResponse
    }
}

config.js

JavaScript
for use with Lambda
var config = {};

config.host = "Host Domain Here";
config.topic = "Topic Here";
config.app_id = "APP ID Here"

module.exports = config;

alexaTest.py

Python
For testing Relays
import RPi.GPIO as GPIO            # import RPi.GPIO module
from time import sleep             # lets us have a delay
GPIO.setmode(GPIO.BCM)             # choose BCM or BOARD
GPIO.setup(10, GPIO.OUT)           # set GPIO number as an output 

try:
    while True:
        GPIO.output(10, 1)         # set GPIO to 1/GPIO.HIGH/True
        print "HIGH!"
        sleep(4)                   # wait 4 seconds
        GPIO.output(10, 0)         # set GPIO to 0/GPIO.LOW/False
        print "LOW!"
        sleep(4)                   # wait 4 seconds

except KeyboardInterrupt:          # trap a CTRL+C keyboard interrupt
    GPIO.cleanup()                 # resets all GPIO ports used by this program

alexaBoat.py

Python
For Controlling Relays
#!/usr/bin/python

"""python boatstation for boat to be controlled by Amazon Alexa"""

import paho.mqtt.client as mqtt
import json, time, sys, ssl
import logging, logging.handlers
import RPi.GPIO as GPIO  

water = 24
lights = 17
navigation = 21
anchorDown = 11
anchorUp = 10

GPIO.setmode(GPIO.BCM)  

GPIO.setup(lights, GPIO.OUT)
GPIO.setup(water, GPIO.OUT)
GPIO.setup(navigation, GPIO.OUT)
GPIO.setup(anchorUp, GPIO.OUT)
GPIO.setup(anchorDown, GPIO.OUT)
GPIO.output(lights, GPIO.HIGH)
GPIO.output(water, GPIO.HIGH)
GPIO.output(navigation, GPIO.HIGH)
GPIO.output(anchorUp, GPIO.HIGH)
GPIO.output(anchorDown, GPIO.HIGH)

cert_path = "/home/pi/Desktop/pi_pi/certs/"
host = "Your Host"
topic = "Your Topic"
root_cert = cert_path + "rootCA.key"
cert_file = cert_path + "YourCert-certificate.pem.crt"
key_file = cert_path + "YourCert-private.pem.key"

globalmessage = ""  # to send status back to MQTT
isConnected = False
logger = logging.getLogger('alexaboat')

def do_command(data,cmd):  #   {"name":"CommandIntent","slots":{"Task":{"name":"Task","value":"launch"}}}
    task = str(data["slots"]["Task"]["value"])
    logger.info("TASK = " + task)
    global globalmessage

    if task.upper() == "LIGHTS":
        globalmessage = "executing command lights"
        print globalmessage
        toggle(lights,cmd)
    elif task.upper() == "WATER":
        globalmessage = "executing command water"
        print globalmessage
        toggle(water,cmd)
    elif task.upper() == "NAVIGATION":
        globalmessage = "executing command navigaiton"
        print globalmessage
        toggle(navigation,cmd)

def anchor_command(data):  #   {"name":"CommandIntent","slots":{"Task":{"name":"Task","value":"launch"}}}
    try:
        direction = str(data["slots"]["Direction"]["value"])
        distance = float(data["slots"]["Distance"]["value"])

        logger.info("Anchor = " + direction + " distance "+ str(distance))
        global globalmessage

        globalmessage = "executing command anchor" + direction+" " + str(distance) +" "+ str(distance/0.48)
        print globalmessage

        if direction.upper() == "UP":
   		anchor = anchorUp
	elif direction.upper() == "DOWN":
		anchor = anchorDown

        GPIO.output(anchor, GPIO.LOW)
        time.sleep(distance/0.48)
        GPIO.output(anchor, GPIO.HIGH)
        
    except (ValueError):
        logger.info("Anchor Error")
    
def toggle(gpio,cmd):
    logger.info("GPIO: " + str(gpio) + "CMD: " + str(cmd))
    GPIO.output(gpio, cmd)
        
# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    isConnected = True
    logger.info("connected with result code " + str(rc))
    # Subscribing in on_connect() means that if we lose the connection and      
    # reconnect then subscriptions will be renewed.    
    client.subscribe(topic)

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    logger.info("msg received, payload = " + str(msg.payload))
    data = json.loads(str(msg.payload))
    # Figure out INTENT -- was it Open, Close, Anchor, etc
    if "name" in data:
        if data["name"] == "Open":
            do_command(data,GPIO.LOW)
        if data["name"] == "Close":
            do_command(data,GPIO.HIGH)
        elif data["name"] == "Anchor":
            anchor_command(data)
        
def on_log(client, userdata, level, buf):
    logger.debug(buf)

def logger_init():
    logger.setLevel(logging.DEBUG)
    log_file_size = 1024 * 1024 * 1  # 1 MB
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(process)d - %(name)s : %(message)s')
    fh = logging.handlers.RotatingFileHandler('/home/pi/Desktop/pi_pi/logs/alexaboat.log', maxBytes=log_file_size, backupCount=5)
    fh.setFormatter(formatter)
    sh = logging.StreamHandler(sys.stdout)
    sh.setFormatter(formatter)
    logger.addHandler(fh)
    logger.addHandler(sh)
    logger.info('******************************************')
    logger.info('Starting up...')

logger_init()

client = mqtt.Client(client_id="alexaBoat.py")
client.on_connect = on_connect
client.on_message = on_message
client.on_log = on_log
client.tls_set(root_cert,
               certfile = cert_file,
               keyfile = key_file,
               cert_reqs=ssl.CERT_REQUIRED,
               tls_version=ssl.PROTOCOL_TLSv1_2,
               ciphers=None)

logger.info('connecting to mqtt broker')
client.connect(host, 8883, 60)

run = True

try:
    while run:
        client.loop()
        time.sleep(1)

        try:
            mypayload = '''{
                "StatusMessage": "%s"
            }''' % (globalmessage)

            if globalmessage != "":
                client.publish(topic, mypayload)
                globalmessage = ""

        except (TypeError):
            pass
    
except KeyboardInterrupt:
    print "Bye Bye!"
    GPIO.cleanup()    

Credits

Ufuk ARSLAN

Ufuk ARSLAN

1 project • 6 followers
Entrepreneur; Tech-Lover; Co-Founder at Mobilist; Proud member of CanavarIT

Comments