Things Used In This Project
- 1 x PHPoC Blue
- 1 x Amazon Alexa Echo Dot
- 2 x PHPoC Stepper Motor Controller Ⅱ (T-type)
- 2 x Stepper motor SE-SM243
- Cocktail Machine DIY
- Jumper wires
System Architecture
How System Works
When an user says to Amazon Echo Dot: "Alexa, ask Cocktail Machine to give me cocktail_name", the following works will be done:
- Amazon Echo streams this sentence to Alexa Service
- Alexa Service process this sentence based on configuration of Cocktail Custom Skill and then send "cocktail_name" to AWS Lambda function via endpoint of the Lambda function (URL or Amazon Resource Names (ARNs)). This Endpoint has been configure in Custom Skill configuration
- Lambda function publish a message which contains cocktail_name to a topic in MQTT broker.
- PHPoC (a IoT Hardware platform, which already subscribed the topic) receives this message, It gets cocktail_name and control the step motor to make cocktail based on the recipe.
I have an article (Amazon Echo – Control DIY IoT devices: https://www.hackster.io/phpoc_man/am...h-phpoc-037b11 ) which explains in detail and shows step by step how to use Amazon Echo to control DIY IoT devices.
Every step of this project is almost the same, there are only some difference on interaction model configuration and source code. You can do step by step on that link. I only show the differences in this article.
Alexa Service - Custom Skill Configuration
Skill Information
Interaction Model
- Intent Schema
Code:{ "intents": [ { "slots": [ { "name": "drinkName", "type": "DRINK_TYPE" } ], "intent": "cocktail" } ] }
- Custom Slot Types : DRINK_TYPE
Code:summer rain screw driver black Russian black Russian two sweet Martini Martini
- Sample Utterances
Code:cocktail give me {drinkName} cocktail make {drinkName} for me
Lambda Source Code
This is Node.JS code and will be run when Alexa Service make the request to Lambda.
Each lambda code function have and Endpoint, called Amazon Resource Names (ARNs) (like URL). We provide this endpoint to Alexa Service Skill when we configure the Skill.
For example, When an user says to Amazon Echo Dot: "Alexa, ask Cocktail Machine to give me black russian 2", Alexa Service will send a message which contains "black russian 2" to the Lambda function via the endpoint.
Lambda function retrieves the "black russian 2", convert it to "BLACK_RUSSIAN_2" and then publish this content to a MQTT topic.
In this example, I published cocktail name to topic: alexa/phpoc/cocktail in MQTT broker of iot.eclipse.org. It's free.
<Lamda code index.js>
Code:
'use strict'; var mqtt = require('mqtt'); // --------------- Helpers that build all of the responses ----------------------- function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { return { outputSpeech: { type: 'PlainText', text: output, }, card: { type: 'Simple', title: "SessionSpeechlet - " + title, content: "SessionSpeechlet - " + output, }, reprompt: { outputSpeech: { type: 'PlainText', text: repromptText, }, }, shouldEndSession: shouldEndSession }; } function buildResponse(sessionAttributes, speechletResponse) { return { version: '1.0', sessionAttributes, response: speechletResponse, }; } // --------------- Functions that control the skill's behavior ----------------------- function getWelcomeResponse(callback) { // If we wanted to initialize the session to have some attributes we could add those here. const sessionAttributes = {}; const cardTitle = 'Welcome'; const speechOutput = "Welcome to P H P o C. How can I help you?" // If the user either does not reply to the welcome message or says something that is not // understood, they will be prompted again with this text. const repromptText = "How can I help you?"; const shouldEndSession = false; callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); } function handleSessionEndRequest(callback) { const cardTitle = 'Session Ended'; const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!'; // Setting this to true ends the session and exits the skill. const shouldEndSession = true; callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession)); } function createAttributes(cocktail) { return { cocktail: cocktail }; } /** * Send data to mqtt and prepares the speech to reply to the user. */ function cocktailInSession(intent, session, callback) { const cardTitle = intent.name; const drinkNameRequest = intent.slots.drinkName; let repromptText = ''; let sessionAttributes = {}; const shouldEndSession = true; let speechOutput = ""; var drinkName = ""; var requestName = ""; if(drinkNameRequest) { requestName = drinkNameRequest.value; console.log("cocktail: " + requestName); drinkName = requestName.toUpperCase(); drinkName = drinkName.replace(/ /g, "_"); switch(drinkName) { case "SUMMER_RAIN": case "SCREW_DRIVER": case "BLACK_RUSSIAN": case "BLACK_RUSSIAN_2": case "SWEET_MARTINI": case "MARTINI": break; default: drinkName = ""; } } if(drinkName !== "") { speechOutput = "Ok, I will tell cocktail machine make " + requestName + " for you"; //Update var mqttpromise = new Promise( function(resolve,reject){ var client = mqtt.connect({port:1883,host:'iot.eclipse.org'}) client.on('connect', function() { // When connected // publish a message to any mqtt topic client.publish('alexa/phpoc/cocktail', drinkName) client.end() resolve('Done Sending'); }); }); mqttpromise.then( function(data) { console.log('Function called succesfully:', data); sessionAttributes = createAttributes(data); repromptText = speechOutput; callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); }, function(err) { console.log('An error occurred:', err); } ); }else{ speechOutput = "Please try again"; repromptText = "Please try again"; callback(sessionAttributes,buildSpeechletResponse( cardTitle, speechOutput, repromptText, shouldEndSession)); } } // --------------- Events ----------------------- /** * Called when the session starts. */ function onSessionStarted(sessionStartedRequest, session) { console.log("onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}"); } /** * Called when the user launches the skill without specifying what they want. */ function onLaunch(launchRequest, session, callback) { console.log("onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}"); // Dispatch to your skill's launch. getWelcomeResponse(callback); } /** * Called when the user specifies an intent for this skill. */ function onIntent(intentRequest, session, callback) { console.log("onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}"); const intent = intentRequest.intent; const intentName = intentRequest.intent.name; // Dispatch to your skill's intent handlers if (intentName === 'cocktail') { cocktailInSession(intent, session, callback); } else if (intentName === 'AMAZON.HelpIntent') { getWelcomeResponse(callback); } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') { handleSessionEndRequest(callback); } else { throw new Error('Invalid intent'); } } /** * Called when the user ends the session. * Is not called when the skill returns shouldEndSession=true. */ function onSessionEnded(sessionEndedRequest, session) { console.log("onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}"); // Add cleanup logic here } // --------------- Main handler ----------------------- // Route the incoming request based on type (LaunchRequest, IntentRequest, // etc.) The JSON body of the request is provided in the event parameter. exports.handler = (event, context) => { try { console.log("event.session.application.applicationId=${event.se ssion.application.applicationId}"); /** * Uncomment this if statement and populate with your skill's application ID to * prevent someone else from configuring a skill that sends requests to this function. */ /* if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') { context.fail("Invalid Application ID"); } */ if (event.session.new) { onSessionStarted({ requestId: event.request.requestId }, event.session); } if (event.request.type === 'LaunchRequest') { onLaunch(event.request, event.session, function callback(sessionAttributes, speechletResponse) { context.succeed(buildResponse(sessionAttributes, speechletResponse)); }); } else if (event.request.type === 'IntentRequest') { onIntent(event.request, event.session, function callback(sessionAttributes, speechletResponse) { context.succeed(buildResponse(sessionAttributes, speechletResponse)); }); } else if (event.request.type === 'SessionEndedRequest') { onSessionEnded(event.request, event.session); context.succeed(); } } catch (e) { context.fail("Exception: " + e); } };
PHPoC Code (device code)
This code subscribes a MQTT topic.
When receiving and MQTT message, for example, message with content: "BLACK_RUSSIAN_2", based on the recipe, it will control two step motors to get VODKA two times and KAHLUA one time.
<PHPoC code task0.php>
PHP Code:
<?php
include_once "/lib/sd_spc.php";
include_once "/lib/sn_dns.php";
include_once "/lib/vn_mqtt.php";
define("SID_X", 13);
define("SID_Y", 14);
define("COCKTAIL_FLOW_TIME", 3); // time for cocktail flow into cup in second
define("COCKTAIL_WAIT_TIME", 3);
function step_wait($sid)
{
while((int)spc_request_dev($sid, "get state") > 1)
usleep(1);
}
function spc_check_did($sid, $did)
{
$resp = spc_request_csv($sid, 0, "get did");
if($resp === false)
{
echo "spc_check_did: sid$sid - device not found\r\n";
return false;
}
if($resp[1] != "40002405")
{
echo "spc_check_did: unknown device ", $resp[2], "\r\n";
return false;
}
return true;
}
function cooktail_get_id($drink_name)
{
global $names;
for($i = 0; $i < 4; $i++)
{
if($names[$i] == $drink_name)
return $i;
}
return -1;
}
function cocktail_get($recipe)
{
global $pos;
spc_request_dev(SID_X, "goto -sw1 20000 1000000");
step_wait(SID_X);
spc_request_dev(SID_X, "reset");
for($step_motor = 0; $step_motor < 4; $step_motor++) // get component one by one
{
$amount = $recipe[$step_motor];
if($amount > 0)
{
$ps = $pos[$step_motor];
spc_request_dev(SID_X, "goto +$ps 20000 1000000");
step_wait(SID_X);
for($i = 1; $i <= $amount; $i++)
{
if($i != 1)
sleep(COCKTAIL_WAIT_TIME);
spc_request_dev(SID_Y, "goto -sw0 30000 1000000");
step_wait(SID_Y);
sleep(COCKTAIL_FLOW_TIME);
spc_request_dev(SID_Y, "reset");
spc_request_dev(SID_Y, "goto 90000 30000 1000000");
step_wait(SID_Y);
}
spc_request_dev(SID_Y, "goto 140000 30000 1000000");
step_wait(SID_Y);
}
}
}
function cocktail_init()
{
spc_reset();
spc_sync_baud(460800);
if(!spc_check_did(SID_X, "40002405"))
return;
if(!spc_check_did(SID_Y, "40002405"))
return;
spc_request_dev(SID_X, "set vref stop 4");
spc_request_dev(SID_X, "set vref drive 15");
spc_request_dev(SID_X, "set mode 32");
spc_request_dev(SID_X, "set rsnc 120 250");
spc_request_dev(SID_Y, "set vref stop 4");
spc_request_dev(SID_Y, "set vref drive 15");
spc_request_dev(SID_Y, "set mode 32");
spc_request_dev(SID_Y, "set rsnc 120 250");
spc_request_dev(SID_X, "goto +sw0 20000 1000000");
step_wait(SID_X);
spc_request_dev(SID_Y, "goto -sw0 30000 1000000");
step_wait(SID_Y);
spc_request_dev(SID_X, "reset");
spc_request_dev(SID_Y, "reset");
spc_request_dev(SID_Y, "goto 140000 30000 1000000");
step_wait(SID_Y);
spc_request_dev(SID_X, "goto +sw0 20000 1000000");
step_wait(SID_X);
spc_request_dev(SID_X, "reset");
spc_request_dev(SID_Y, "reset");
}
$host_name = "iot.eclipse.org";
$port = 1883;
$will = "";
$username = "";
$password = "";
$subc_topics = array(array("alexa/phpoc/cocktail", 0));
$topic = "";
$content = "";
$retain = 0;
$names = array("SUMMER_RAIN", "SCREW_DRIVER", "BLACK_RUSSIAN", "BLACK_RUSSIAN_2", "SWEET_MARTINI", "MARTINI");
$recipe_list = array(//MALIBU | VODKA | OGRANGE_JUICE | KAHLUA | GIN ||
array( 2, 1, 0, 0, 0), //||SUMMER_RAIN
array( 0, 1, 2, 0, 0), //||SCREW_DRIVER
array( 0, 1, 0, 2, 0), //||BLACK_RUSSIAN
array( 0, 2, 0, 1, 0), //||BLACK_RUSSIAN_2
);
// motor pos: //MALIBU | VODKA | OGRANGE_JUICE | KAHLUA | GIN |
$pos = array( 1000, 22000, 41000, 63000, 82000);
mqtt_setup(0, "PHPoC-MQTT Sub Example", $host_name, $port);
mqtt_connect(true, $will, $username, $password);
if(mqtt_state() == MQTT_CONNECTED)
mqtt_subscribe($subc_topics);
cocktail_init();
while(1)
{
if(mqtt_state() == MQTT_DISCONNECTED)
while(mqtt_reconnect() == false);
if(mqtt_loop($topic, $content, $retain))
{
if($retain == 1)
continue;// a stale message
echo $content;
$drink_name = $content;
$id = cooktail_get_id($drink_name);
if($id >= 0)
{
$recipe = $recipe_list[$id];
cocktail_get($recipe);
}
else
echo "cocktail: The drink is not found\r\n";
spc_request_dev(SID_X, "goto -sw1 20000 1000000");
step_wait(SID_X);
spc_request_dev(SID_X, "reset");
}
}
?>
For detail how to wiring among PHPoC, Step motor controller and Step motor, refer to this manual: http://www.phpoc.com/support/manual/....php?id=layout