2017/6/30

Play Red & White Flags Game with Alexa


In this project, I put two servo motors, one for red flag and the other for white flag. The voice command, e.g., hold up the red flag, is passed and upload to IoT database through ASK/Lambda, then the ESP-01 module query the IoT database to get the flag code, and finally attach the servo motors to react the up/down flag action.




Please note that this article does not attempt to tell you how to implement the smart home things, but rather to share with you that we can extend our scope and connect more things that support the REST API, such as Wikipedia, Google map, Flickr, some government’s database and etc.



Red & White Flags Command
Our intention is to design a flag game and one computer player (Alexa) that can recognize the following commands.

Alexa, ask flag game,

Hold up the red flag.
Hold up the white flag.
Hold up the red flag and the white flag together.
Put down the red flag.
Put down the white flag.
Put down the red flag and the white flag together.
Don't hold up the red flag.
Don't put down the red flag.
Don't hold up the white flag.
Don't put down the white flag.
Don't hold up the red flag and the white flag together.
Don't put down the red flag and the white flag together.




ASK and AWS/Lambda Program Framework
For more detail about how to implement Alexa Skill setp by step, please refer to my previous project named ‘The first Alexa Skill – Hello World’. Assume our APP (Alexa Skill) name is ‘light config’.


ASK/Intent Schema
I expect that user can start the game by asking ‘flag game’ with different combinations of commands to raise the red/white flag or put down the red/white flag.

{
  "intents": [
    {
      "slots": [
        {
          "name": "colorFlag",
          "type": "COLOR_LIST"
        },
        {
          "name": "invState",
          "type": "INV_STATE"
        }
      ],
      "intent": "HoldUpFlagIntent"
    },
    {
      "slots": [
        {
          "name": "colorFlag",
          "type": "COLOR_LIST"
        },
        {
          "name": "invState",
          "type": "INV_STATE"
        }
      ],
      "intent": "PutDownFlagIntent"
    },
    {
      "intent": "GameOverIntent"
    }
  ]
}



Custom Slot
Two custom slot types were defined and enumerated below. The ‘INV_STATE’ variable is used to execute a negative command sentence, e.g., ‘don’t hold up the red flag’ means to put down the red flag.




ASK/Sample Utterances
In order to improve the recognition of our new voice commands for Alexa, which can trigger our custom intents successfully, we enumerate some possible utterances.

HoldUpFlagIntent Hold up the {colorFlag} flag
HoldUpFlagIntent Hold up the red {colorFlag} flag and the {colorFlag} flag together
HoldUpFlagIntent {invState} hold up the {colorFlag} flag
HoldUpFlagIntent {invState} hold up the {colorFlag} flag and the {colorFlag} flag together
PutDownFlagIntent Put down the {colorFlag} flag
PutDownFlagIntent Put down the {colorFlag} flag and the {colorFlag} flag together
PutDownFlagIntent {invState} put down the {colorFlag} flag
PutDownFlagIntent {invState} put down the {colorFlag} flag and the {colorFlag} flag together
GameOverIntent game over



AWS/Lambda Script
Please refer to my previous project about ‘To access ThingSpeak by making a HTTPS/GET request’, in which I utilize the package proposed by Kathryn Hodge. First, you can download the Archive.zip from her website and upload it to AWS/Lambda as a beginning. Second, you can replace the index.js with the JavaScript described below. Please fill in your Channel and APIKEY registered from ThingSpeak.

var request = require("request");

// 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 {
        console.log("event.session.application.applicationId=" +
            event.session.application.applicationId);

        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);
    }
};

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

// Called when the user invokes the skill without specifying what they want.
function onLaunch(launchRequest, session, callback) {
    getWelcomeResponse(callback);
}

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

    // dispatch custom intents to handlers here
    if (intentName == "HoldUpFlagIntent") {
        holdUpFlagHandler(intent, session, callback);
    } else if (intentName == "PutDownFlagIntent") {
        putDownFlagHandler(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse(callback);
    } else if (intentName === 'GameOverIntent') {
        handleSessionEndRequest(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else {
         throw "Invalid intent";
    }
}

// Called when the user ends the session.
function onSessionEnded(sessionEndedRequest, session) {
}

function getWelcomeResponse(callback) {
    var speechOutput = "Welcome! Let's play the red and white flag game.";
    var reprompt = speechOutput;
    var header = "Flag Game";
    var shouldEndSession = false;
    var flagSet = {"red":0, "white":0}; // init flag state
    callback(flagSet,
        buildSpeechletResponse(header, speechOutput, reprompt, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you for playing the flag game. Have a nice day!';
    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, true));
}

function holdUpFlagHandler(intent, session, callback) {
    var invState = intent.slots.invState.value?1:0;
    var flagColor = intent.slots.colorFlag.value;
    var flagSet = session.attributes;
    flagColor.toLowerCase().split(' ').forEach(function(key){
       flagSet[key] = invState?1:8; // 0,1,2:down 7,8,9:up to prevent single-bit transmission error
    });
    var flagVal = flagSet.red.toString()+flagSet.white.toString();
    var speechOutput = "";
    var reprompt = "";
    var url = "https://api.thingspeak.com/update?api_key=APIKEY&field1=" + flagVal;
    request.get(url, function(error, response, body) {
        callback(flagSet,
            buildSpeechletResponse("holdUpFlagHandler", speechOutput, reprompt, false));
    });
}

function putDownFlagHandler(intent, session, callback) {
    var invState = intent.slots.invState.value?1:0;
    var flagColor = intent.slots.colorFlag.value;
    var flagSet = session.attributes;
    flagColor.toLowerCase().split(' ').forEach(function(key){
       flagSet[key] = invState?8:1; // 0,1,2:down 7,8,9:up to prevent single-bit transmission error
    });
    var flagVal = flagSet.red.toString()+flagSet.white.toString();
    var speechOutput = "";
    var reprompt = "";
    var url = "https://api.thingspeak.com/update?api_key=APIKEY&field1=" + flagVal;
    request.get(url, function(error, response, body) {
        callback(flagSet,
            buildSpeechletResponse("holdUpFlagHandler", speechOutput, reprompt, false));
    });
}

// ------- Helper functions to build responses for Alexa -------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: "PlainText",
            text: output
        },
        card: {
            type: "Simple",
            title: title,
            content: output
        },
        reprompt: {
            outputSpeech: {
                type: "PlainText",
                text: repromptText
            }
        },
        shouldEndSession: shouldEndSession
    };
}

function buildSpeechletResponseWithoutCard(output, repromptText, shouldEndSession) {
    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
    };
}

function capitalizeFirst(s) {
    return s.charAt(0).toUpperCase() + s.slice(1);
}





Arduino Board Connection
The pin definition of the servo motor is shown below.




We can define one servo as the right hand side (hold Red flag) and connect its PWM pin to the Arduino pin 5. Similarily, we define another servo as the left hand side (hold White flag) and connect its PWM pin to the Arduino pin 6, as shown below.








































Servo Test

#include <Servo.h>

Servo servoL;  // pin6 (White flag)
Servo servoR;  // pin5 (Red flag)

// code[R][L]
void setColorFlag(char* code) {
    int udR = code[0]-48;
    int udL = code[1]-48;
    if ( udR<0 || udR>9 || udL<0 || udL>9 ) { // invalidate value
        return;
    }
    int angR = udR<5?90:180;
    int angL = udL<5?90:0;
    Serial.println("Code>" + String(code[0]) + String(code[1]) );
    Serial.println("Code>" + String(udR) + String(udL) );
    servoR.attach(5);
    servoL.attach(6);
    servoR.write(angR);
    servoL.write(angL);
    delay(500);
    servoR.detach();
    servoL.detach();
}

void setup() {
}

void loop() {
    if ( Serial.available() ) {
        char ch = Serial.read();
        switch (ch) {
            case 'a': // red down white down
                setColorFlag("11");
                break;
            case 'b': // red down white up
                setColorFlag("18");
                break;
            case 'c': // red up white down
                setColorFlag("81");
                break;
            case 'd': // red down white up
                setColorFlag("88");
                break;
        }
    }
}




Arduino Program
In this example, we send an intermittent request to ThingSpeak every 5 seconds.

#include <SoftwareSerial.h>
#include <Servo.h>

SoftwareSerial ESP(3, 2); // ESP8266 ESP-01: Tx, Rx
Servo servoL;  // pin6 (Red flag)
Servo servoR;  // pin5 (White flag)

bool wifiState = false;  // check wifi connection
bool swState = false;  // interrupt switch

void sendATcmd(String cmd, unsigned int msDelay,String& espMsg) {
    unsigned long timeout = millis()+msDelay;
    Serial.print(">>ESP-01: "+String(cmd)); // debug
    ESP.print(cmd); // send AT command to ESP-01 
    espMsg = "";
    while ( ESP.available() || millis()<timeout ) {
        while ( ESP.available() ) {
            char ch = (char)ESP.read();
            espMsg += ch;
        }
    }
}

bool connectWAN(char* ssidName, char* passWord) {
    bool status = false;
    String recMsg;
    String joinCmd = "AT+CWJAP=\"" + String(ssidName) + "\",\"" + String(passWord) + "\"\r\n";
    sendATcmd("AT+RST\r\n",2000,recMsg);      // reset ESP-01
    sendATcmd("AT+CWMODE=3\r\n",1000,recMsg); // config ESP-01 as AP+STA both
    sendATcmd(joinCmd,1000,recMsg);   // wait the server to ack "OK"
    for (int loop=0; loop<10; loop++ ) {
        if ( ESP.find("OK") ) {
            status = true;
            Serial.println("join ESP-01 to WAN success ...");
            break;
        }
        setDelay(1000);
    }
    return status;
}

// Alexa Channel used red/white flag game
void queryThingSpeak(char* ipName,char* channelName, char* apiKey) {
    String recMsg;
    String tcpCmd = "AT+CIPSTART=\"TCP\",\"" + String(ipName) + "\",80\r\n";
    String getCmd = "GET /channels/" + String(channelName) +
                    "/feeds/last.json?key=" + String(apiKey) + "\r\n";
    String cipCmd = "AT+CIPSEND=" + String(getCmd.length()) + "\r\n";
   
    sendATcmd(tcpCmd,1000,recMsg); // ask HTTP connection, and wait the server to ack "OK"
    sendATcmd(cipCmd,1000,recMsg); // start CIPSEND request, and wait the server to ack "OK"
    sendATcmd(getCmd,1000,recMsg); // send GET command
    Serial.println("recMsg>" + recMsg);

    // get flagCode(RL) from JSON
    if ( recMsg.indexOf("+IP")>0 ) {
        char* code = recMsg.c_str();
        char* p = NULL;
        if ( (p=strrchr(code,':'))==NULL && (p=strrchr(code,';'))==NULL ) ;
        else {
            code = (p+2);
            *(p+4) = '\0';
        }
        setColorFlag(code);
    }
}

// code[R][L]
void setColorFlag(char* code) {
    int udR = code[0]-48;
    int udL = code[1]-48;
    if ( udR<0 || udR>9 || udL<0 || udL>9 ) { // invalidate value
        return;
    }
    int angR = udR<5?90:180;
    int angL = udL<5?90:0;
    Serial.println("Code>" + String(code[0]) + String(code[1]) );
    Serial.println("Code>" + String(udR) + String(udL) );
    servoR.attach(5);
    servoL.attach(6);
    servoR.write(angR);
    servoL.write(angL);
    delay(500);
    servoR.detach();
    servoL.detach();
}

void setDelay(unsigned int ms) {
    unsigned long timeout = ms+millis();
    while( millis()<timeout ) {
        checkSwitchButton();
    }
}

bool checkSwitchButton() {
    if ( digitalRead(7)==LOW ) {
        swState = !swState;
        Serial.println("\nInterrupt: Switch Button = " + String(swState) );
        delay(300); // debounce delay (depending on your behavior)
    }
    return swState;
}

void setup() {
    pinMode(7, INPUT_PULLUP); // set pin7 as the interrupt button
    Serial.begin(115200);
    ESP.begin(115200);
    delay(1000);

    // init color Flag
    setColorFlag("88"); // up
    setColorFlag("11"); // down
   
    Serial.println("ESP-01 start ...");
    Serial.println("try to connect ESP-01 to WAN (through home AP) ...");
    if ( (wifiState=connectWAN(SSID,PASSWORD)) ) { // join ESP-01 to WAN home AP
        Serial.println("press switch button to start ...");
    } else {
        Serial.println("fail to join WAN ...");
    }
}

void loop() {
    if ( wifiState && swState ) {
        queryThingSpeak(
            "184.106.153.149",  // ThingSpeak IP
            ChannelID,         // Alexa Channel
            ReadAPIKEY       // read API-KEY
        );
        Serial.println("press button to stop ...");
        setDelay(1000);
    }
    setDelay(300); // monitor hardware interrupt
}