Demonstration




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

Click image for larger version  Name:	cocktail_system architecture.JPG Views:	1 Size:	41.3 KB ID:	743



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

Click image for larger version  Name:	cocktail_Skill Information.jpg Views:	1 Size:	37.2 KB ID:	741


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.

Click image for larger version  Name:	cocktail_recipe.jpg Views:	1 Size:	17.7 KB ID:	742

<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($sid0"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