Chris Meade
Published © CC BY

Secure Package Delivery Trunk for Your Front Porch

Alexa-enabled, Arduino IoT project to keep your packages safe and secure from theft, water damage and prying eyes when you are not home.

AdvancedFull instructions providedOver 1 day2,452
Secure Package Delivery Trunk for Your Front Porch

Things used in this project

Hardware components

Arduino UNO & Genuino UNO
Arduino UNO & Genuino UNO
×1
Echo Dot
Amazon Alexa Echo Dot
×1
NodeMCU ESP8266 Breakout Board
NodeMCU ESP8266 Breakout Board
×1
SparkFun Logic Level Converter - Bi-Directional
SparkFun Logic Level Converter - Bi-Directional
×1
Adafruit 3x4 Phone-style Matrix Keypad
×1
4-CHANNEL RELAY CONTROLLER FOR I2C
ControlEverything.com 4-CHANNEL RELAY CONTROLLER FOR I2C
×1
DFRobot DFPlayer - A Mini MP3 Player For Arduino
×1
PIR Motion Sensor (generic)
PIR Motion Sensor (generic)
×1
Speaker: 0.25W, 8 ohms
Speaker: 0.25W, 8 ohms
×1
Solderless Breadboard Full Size
Solderless Breadboard Full Size
×1
Buzzer
Buzzer
×1
UCEC MB102 3.3V/5V Breadboard Power Supply Module
×1
12V Cabinet Door Electric Lock Assembly Solenoid
×1
Red LED Momentary Push Button Switch Stainless Waterproof
×1
Tubular Linear Actuators - 3in Stroke
×2
100-240V to DC 10A Switching Power Supply Adapter + Power Splitter Cable
×1
Barrel Top Toy Box
×1
microSD 8GB Memory Card
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
Arduino IDE
Arduino IDE
AWS Lambda
Amazon Web Services AWS Lambda
AWS IoT
Amazon Web Services AWS IoT
Amazon Web Services Amazon Polly

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Breadboard Electrical Components and Wiring Diagram

Wiring diagram for all components. NOTE: hardware must be mounted to the trunk so this does not represent scale or final layout.
Castlelocker bb gwshqh39ro

Code

Alexa ASK Custom Skill Lambda Function - node.js

JavaScript
The Lambda function handles the Alexa Skill Intents and sends & receives commands and settings from the AWS IOT thing shadow for the trunk. You must also create a website and database with which to track each unique trunk ID and user account so the skill knows and can only open a user's trunk and not one they do not own!
//*****************************************************************************
// * CastleLocker Alexa Skill - Lambda Function Intent Handler
// * 
// * Copyright (c) 2018, Vocal Intent, LLC
// *
// * This program and the accompanying materials are made available under the 
// * terms of the Creative Commons Attribution 4.0 International License
// *
// * The Creative Commons Attribution 4.0 International License is available at 
// *    https://creativecommons.org/licenses/by/4.0/legalcode
// *
// * Contributors:
// *    Chris Meade - initial contribution
// *
// ****************************************************************************
 
const Alexa = require('alexa-sdk');

const options = {
    TITLE: 'Castle Locker'
};

const config = {};

config.IOT_BROKER_ENDPOINT      = "XXXXXXXXXXXXXX.iot.us-east-1.amazonaws.com";
config.IOT_BROKER_REGION        = "us-east-1";
config.IOT_THING_NAME           = "castleLockerIOTThing";


const languageStrings = {
    'en-US': {
        'translation': {
            'TITLE'  : "Castle Locker",
            'WELCOME_LAUNCH':"Welcome to %s Control. To open your Castle Locker secure trunk, say 'open trunk', to close it, say 'close trunk'.",
            'OPENING_TRUNK': "I am sending the command to open your %s secure trunk.  The trunk will remain open for 15 seconds.",
            'CLOSING_TRUNK': "I am sending the command to close your %s secure trunk.",
            'TRUNK_STATE': "The trunk state is %s.  What would you like to do now?",
            'HELP_MESSAGE': "The %s Skill is the companion voice interface for the %s secure, package delivery trunk, for your smart home.  You can use your voice to say things like, 'open trunk', 'close trunk, or 'is the trunk open?' ",
            'EXIT_MESSAGE': "Thanks for using %s.  Have a great day!"
        }
    },
    // 'de-DE': {
    //     'translation' : {
    //         'TITLE'   : "Schloss Schließfach",
    //         'HELP_MESSAGE': ""
    //     }
    // }
};


exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.resources = languageStrings;

    // alexa.appId = 'amzn1.echo-sdk-ams.app.1234';
    ///alexa.dynamoDBTableName = 'YourTableName'; // creates new table for session.attributes

    alexa.registerHandlers(
          handlers
    );
    alexa.execute();
};

var handlers = {    
    'NewSession': function() {
        var accessToken = this.event.session.user.accessToken;
        console.log('accessToken: ' + accessToken);
        if ((accessToken === null) || (accessToken == undefined)){
        
        const speechOutput = 'Your account is not linked to a Castle Locker secure trunk.  Please visit the account linking page in the Alexa App for this skill to complete the account linking process.';
        
        this.emit(':tellWithLinkAccountCard', speechOutput);
        
        } else {
            console.log(this.event.request.type);
              if (this.event.request.type === 'IntentRequest') {
                  console.log(this.event.request.intent.name);
                  this.emit(this.event.request.intent.name); 
              } else
              {
              this.emit('LaunchRequest');
              }
        }
    },
    'LaunchRequest': function() {
        this.attributes['accessCode'] = 0;
        this.response.speak(this.t('WELCOME_LAUNCH', this.t("TITLE")))
        .listen(this.t('WELCOME_LAUNCH', this.t("TITLE")));
        this.emit(':responseReady');
    },
    "openTrunkIntent": function() {
        var newState = {'voiceCmd2CastleLocker':1};
        updateShadow(newState, status => {
            this.response.speak(this.t("OPENING_TRUNK", this.t("TITLE")));
            this.emit(':responseReady');
        });        
    },
    "closeTrunkIntent": function() {
        var newState = {'voiceCmd2CastleLocker':2};
        updateShadow(newState, status => {
            this.response.speak(this.t("CLOSING_TRUNK", this.t("TITLE")));
            this.emit(':responseReady');
        });        
    },   
    "trunkStatusIntent": function() {
        //console.log("Entered Trunk Status Intent");
        getShadowUpdate(status => {
            this.response.speak(this.t("TRUNK_STATE", status))
            .listen(this.t('TRUNK_STATE', status));
            this.emit(':responseReady');
        });         
    },     
    "AMAZON.HelpIntent": function() {
        this.response.speak(this.t("HELP_MESSAGE", this.t("TITLE"), this.t("TITLE")))
            .listen(this.t("HELP_MESSAGE", this.t("TITLE"), this.t("TITLE")));
        this.emit(':responseReady');
    },
    "goodbyeIntent": function() {
        this.response.speak(this.t("EXIT_MESSAGE", this.t("TITLE")));
        this.emit(':responseReady');
    },    
    "AMAZON.CancelIntent": function() {
        this.response.speak(this.t("EXIT_MESSAGE", this.t("TITLE")));
        this.emit(':responseReady');
    },
    "AMAZON.StopIntent": function() {
        this.response.speak(this.t("EXIT_MESSAGE", this.t("TITLE")));
        this.emit(':responseReady');
    },
    'Unhandled': function() {  // if we get any intents other than the above
        this.response.speak('Sorry, I didn\'t get that.').listen('Try again');
        this.emit(':responseReady');
    }    
};


function updateShadow(desiredState, callback) {
    // update AWS IOT thing shadow
    var AWS = require('aws-sdk');
    AWS.config.region = config.IOT_BROKER_REGION;


    var paramsUpdate = {
        "thingName" : config.IOT_THING_NAME,
        "payload" : JSON.stringify(
            { "state":
                { "desired": desiredState             // Example: desiredState = {"voiceCmd2CastleLocker":1}, this will send the Open Trunk Cmd to the thing shadow
                }
            }
        )
    };

    var iotData = new AWS.IotData({endpoint: config.IOT_BROKER_ENDPOINT});

    iotData.updateThingShadow(paramsUpdate, function(err, data)  {
        if (err){
            console.log(err);
            callback("not ok");
        }
        else {
            console.log("updated thing shadow " + config.IOT_THING_NAME + ' to state ' + paramsUpdate.payload);
            callback("ok");
        }
    });
}

function getShadowUpdate(callback)
{
     // get AWS IOT thing shadow value
    var AWS = require('aws-sdk');
    AWS.config.region = config.IOT_BROKER_REGION;

    var iotData = new AWS.IotData({endpoint: config.IOT_BROKER_ENDPOINT});
    
    var params = {thingName: config.IOT_THING_NAME};
    
    var status = 0;
    console.log("Checking for Shadow Value");
    iotData.getThingShadow(params, function(err, data) {
        if (err) {
            console.log("Error!!!!");
            callback("unknown");
        } else {
            console.log("Shadow Data Returned!");
            console.log(data);
            var jsonPayload = JSON.parse(data.payload);
            status = jsonPayload.state.reported.castleLockerState;
            console.log('castleLockerState: ' + status);
                if (status == 1)
                {
                    callback("open");
                }
                else if (status == 2)
                {
                    callback("closed");
                }
                else
                {
                    callback("unknown");
                } 
        }
    });
}

Amazon Polly - Text to Speech Scripts

Plain text
These are the scripts to use to create Text-to-Speech (TTS) mp3 files that we will load onto the DFRobot mp3 player. The mp3 files are triggered by events at the trunk and broadcast over the speaker we have integrated into the Castle Locker trunk to instruct the delivery personnel on how to leave a package and what is happening as we send commands to/from the trunk and our web services.
//*****************************************************************************
// * CastleLocker Amazon Polly Voice Scripts
// * 
// * Copyright (c) 2018, Vocal Intent, LLC
// *
// * This program and the accompanying materials are made available under the 
// * terms of the Creative Commons Attribution 4.0 International License
// *
// * The Creative Commons Attribution 4.0 International License is available at 
// *    https://creativecommons.org/licenses/by/4.0/legalcode
// *
// * Contributors:
// *    Chris Meade - initial contribution
// *
// ****************************************************************************

Welcome Message:
Hi!  Welcome to our home!  To deliver a package, please enter the security code on the keypad, for the Castle Locker secure trunk, to your right.  If you do not have a security code for this home, please check the second line of the address label, on the package, as it may contain the code for delivery.

Invalid Code Message(s):
I am afraid the code you have entered is invalid.  Please check the package label for a valid code, or consult the homeowner's instructions for delivery, in your records.

Uh Oh.  I do not recognize the code you have entered.  Please enter a valid code on the keypad.

Hmmm.  I do not recognize that security code.  Please try again.

Reprompt Delivery:
To deliver a package, please enter the access code on the keypad on the Castle Locker secure trunk to your right.

Valid Code:
Thank you, the code has been accepted and the trunk is opening.  Please place the package in the trunk and then press the red button on the trunk to close it.  The trunk will automatically close in 15 seconds if you have not pressed the red button.  Thanks for delivering our package!

Close Trunk:
Please step back.  I am now going to close the Castle Locker secure trunk.

Thanks for delivering:
Thanks for delivering our package!  Have a great day!

Lock out:
I am sorry you are having trouble entering a valid code.  You have entered an invalid code more than three times in a row.  As a result, access to the Castle Locker secure trunk has been disabled, until the homeowner's can reset it.

Speak with someone:
If you need to speak with someone, please press 0, star, on the keypad.

Wait:
Please wait while I summon the Lord and Lady of the House.

No one home:
I am sorry, no one can come to the door right now.  If the purpose of your visit is urgent, please leave a message for the household by calling, (XXX) XXX-XXXX.  That number again is, (XXX) XXX-XXXX.  I am afraid that I can not be of further assistance to you at this time.  Have a nice day!

----------------------------------------
NOTE: The below messages are just examples.  The number before the undescore "_" character is the important part - you can name them anything after the underscore.  The numbering must correspond to the values used to launch them in the Arduino code for the DFRobot player.  For example, to play the first file, you would use the command: myDFPlayer.play(1);


**NOTE: The dfRobot MP3 Player sometimes ignores the naming of the files and will play them by the order in which they were added to the sd card.  So, I found that if I copied them one at a time starting with the first file, I got consistent results when executing a command like DFPlayer.play(1).


mp3 numbering/naming/layout for SD Card
----------------------------------------
1_welcomeMsg2.mp3
2_welcomeMsg.mp3
3_repromptDeliverPackage.mp3
4_codeAccepted.mp3
5_pleaseStepBack_AutoCloseMsg.mp3
6_thanksForDeliveringMsg.mp3
7_badcodeErrorMsg3.mp3
8_badcodeErrorMsg.mp3
9_badcodeErrorMsg2.mp3
10_lockedOutMsg.mp3
11_pressZeroStarMsgNew.mp3
12_pleaseWaitMsg.mp3
13_noOneHomeMsg.mp3
14_beep-mid.mp3
15_electronic-beep.mp3
16_ding.mp3
17_ding2.mp3

Arduino Uno Castle Locker Trunk Controller Code

Arduino
This is the code for the Arduino Uno. The Arduino Uno does most of the heavy lifting of controlling access to the Castle Locker trunk. mp3 files are launched when motion is detected, access codes are entered, or a command is received from Alexa. Keypad functions are processed here and the actuators and lock are controlled using this board. Messages from state changes on the IOT thing shadow and vocal commands from Alexa are passed in from the nodeMCU board to the Arduino Uno.
/******************************************************************************
 * CastleLocker Trunk Controller
 * 
 * Copyright (c) 2018, Vocal Intent, LLC
 *
 * This program and the accompanying materials are made available under the 
 * terms of the Creative Commons Attribution 4.0 International License
 *
 * The Creative Commons Attribution 4.0 International License is available at: 
 *    https://creativecommons.org/licenses/by/4.0/legalcode
 *
 * Contributors:
 *    Chris Meade - initial contribution
 *
 *****************************************************************************/

#include "Arduino.h"
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"
#include <Keypad.h>
#include <elapsedMillis.h>
#include <Password.h>
#include <EEPROM.h>

// 4 x 4 Keypad Code - NOT USED IN THIS PROJECT, BUT HERE IN CASE YOU USE A 4x4 format (this affects other pin assignments!)
//const byte ROWS = 4; //four rows
//const byte COLS = 4; //four columns
////define the cymbols on the buttons of the keypads
//char hexaKeys[ROWS][COLS] = {
//  {'1','2','3','A'},
//  {'4','5','6','B'},
//  {'7','8','9','C'},
//  {'*','0','#','D'}
//};
//byte rowPins[ROWS] = {9, 8, 7, 6}; //connect to the row pinouts of the keypad
//byte colPins[COLS] = {5, 4, 3, 2}; //connect to the column pinouts of the keypad

// 3 x 4 Keypad Code
const byte ROWS = 4; //four rows
const byte COLS = 3; //three columns
char keys[ROWS][COLS] = {
  {'1','2','3'},
  {'4','5','6'},
  {'7','8','9'},
  {'*','0','#'}
};
byte rowPins[ROWS] = {5, 6, 7, 8}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {2, 3, 4}; //connect to the column pinouts of the keypad

//initialize an instance of class NewKeypad
Keypad customKeypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS); 

const int porchMotionPin = 9;     // the number of the PIR Sensor input
const int closeBtnPin = 10;

SoftwareSerial mySoftwareSerial(11, 12); // RX, TX
const int mp3PlayerBusyPin = 13;     // the number of the MP3 Player Busy Pin

//PINS ASSIGNMENTS TO TRANSMIT AND RECEIVE INFORMATION WITH nodeMCU Board
#define PIN_SW_SERIAL_RX 14 
#define PIN_SW_SERIAL_TX 15

SoftwareSerial board2BoardSerial(PIN_SW_SERIAL_RX, PIN_SW_SERIAL_TX); // RX, TX

const int relayLockPin = 16;     // the pin for triggering the Lock Relay to Open

const int speakerPin = 17; //For playing tones when keys are pressed on the keypad

#define CW 18 //CW is relay for open/up
#define CCW 19 //CCW is relay for close/down

DFRobotDFPlayerMini myDFPlayer;
void printDetail(uint8_t type, int value);

int buttonCWState = 0;  
int buttonCCWState = 0;  
int porchMotionState = 0;
int motionDetected = LOW;

elapsedMillis timeElapsed; //declare global if you don't want it reset every time loop runs
elapsedMillis openCloseElapsed; //tracks time elapsed while opening/closing trunk
elapsedMillis trunkOpenElapsed; //tracks time trunk has been open, to autoclose the trunk

// delay in milliseconds between motion events
unsigned int interval = 20000;
unsigned int openInterval = 2100;
unsigned int closeInterval = 2600;

unsigned int trunkStaysOpenInterval = 25000;

bool autoOpen = false;
bool autoClose = false;
bool trunkOpen = false;

// Create a Password Array to store multiple Passwords
const byte MAXPASSWD = 3;
Password passwordArray[MAXPASSWD] = { Password( "0"), Password( "1234"), Password("4321")};
//"0" will be for doorbell function and must be first in the array

//DEFINE THE IOT COMMUNICATION CONSTANTS IN HUMAN READABLE FORM
//RECEPTION VALUES
#define STATE_TRUNK_UNKNOWN 0
#define STATE_TRUNK_OPEN 1
#define STATE_TRUNK_CLOSED 2
#define STATE_TRUNK_CHANGING 3

#define CMD_IDLE 0
#define CMD_OPEN_TRUNK 1
#define CMD_CLOSE_TRUNK 2
#define CMD_PARTIAL_OPEN 3
#define CMD_PARTIAL_CLOSE 4

#define CMD_ADD_ACCESS_CODE 5
#define CMD_DELETE_ACCESS_CODE 6
#define CMD_ERASE_ACCESS_CODES 7

#define CMD_GET_TRUNK_STATE 8
#define CMD_PLAY_FILE 9
#define CMD_HOMEOWNER_NOT_AVAILABLE 10
#define CMD_DISMISS_DOORBELL 11
#define CMD_UNLOCK_KEYPAD 12

//TRANSMISSION VALUES
#define NOTICE_IDLE 0
#define NOTICE_TRUNK_OPENED 1
#define NOTICE_TRUNK_CLOSED 2
#define NOTICE_DOORBELL_ACTIVATED 3
#define NOTICE_MOTION_ACTIVATED 4
#define NOTICE_INVALID_CODE 5
#define NOTICE_CORRECT_CODE 6
#define NOTICE_KEYPAD_LOCKED 7
#define NOTICE_CONTROLLER_ERROR 8
#define NOTICE_CONTROLLER_RESTARTED 9
#define NOTICE_EEPROM_UPDATED 10

String inData = "";

//////////////////////////////////////////
// Set-up procedure to initialize program
//////////////////////////////////////////
void setup() { //Setup initalizes comm ports and i/o pins

  mySoftwareSerial.begin(9600);
  board2BoardSerial.begin(9600);
  Serial.begin(115200);
  
  Serial.println();
  Serial.println(F("DFRobot DFPlayer Mini Demo"));
  Serial.println(F("Initializing DFPlayer ... (May take 3~5 seconds)"));

  mySoftwareSerial.listen();
  if (!myDFPlayer.begin(mySoftwareSerial)) {  //Use softwareSerial to communicate with mp3.
    Serial.println(F("Unable to begin:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while(true);
  }
  Serial.println(F("DFPlayer Mini online."));
  
  myDFPlayer.volume(25);  //Set volume value. From 0 to 30

  //Set up pins
  pinMode(CW, OUTPUT);
  pinMode(CCW, OUTPUT);
  pinMode(relayLockPin, OUTPUT);

  pinMode(PIN_SW_SERIAL_TX, OUTPUT);
  pinMode(PIN_SW_SERIAL_RX, INPUT);
  
  digitalWrite(CW, HIGH);
  digitalWrite(CCW, HIGH);
  digitalWrite(relayLockPin, HIGH);
  
  pinMode(speakerPin, OUTPUT);
    
//  pinMode(buttonCWPin, INPUT_PULLUP);
//  pinMode(buttonCCWPin, INPUT_PULLUP);
  
  pinMode(porchMotionPin, INPUT);
  pinMode(mp3PlayerBusyPin, INPUT);

  pinMode(closeBtnPin, INPUT_PULLUP);
  
  customKeypad.addEventListener(keypadEvent); //add an event listener for this keypad

  printEEPROM();
}


//////////////////////////////////////////
// Main Program Loop
//////////////////////////////////////////
void loop() { 

  customKeypad.getKey();

  if (autoOpen)
  {
    digitalWrite(relayLockPin,LOW);
    if (openCloseElapsed < openInterval)
    {
      digitalWrite(CW,LOW); //Motor runs clockwise
      autoClose = false;
    }
    else
    {
      digitalWrite(relayLockPin,HIGH);
      autoOpen = false;
      trunkOpen = true;
      trunkOpenElapsed = 0;
      //Finished Opening - send notice to nodeMCU Board to put IOT shadow into Idle State
      board2BoardSerial.print(NOTICE_IDLE);
    }
  }
  else if (autoClose)
  {
    if (openCloseElapsed < closeInterval)
    {
      digitalWrite(CCW,LOW); //Motor runs counter-clockwise
      autoOpen = false;
    }
    else
    {
      autoClose = false;
      trunkOpen = false;
      myDFPlayer.play(6);
      //Finished Closing - send notice to nodeMCU Board to put IOT shadow into Idle State
      board2BoardSerial.print(NOTICE_IDLE);
    }    
  }

  if (trunkOpen)
  {
    if (trunkOpenElapsed > trunkStaysOpenInterval)
    {
      //time to close trunk
      trunkOpen = false;
      myDFPlayer.play(16); //Play ding sound effect
      delay(2000);
      myDFPlayer.play(5);  //Auto close notice
      delay(5500);
      openCloseElapsed = 0;
      autoClose = true;
    }
  }

  if (digitalRead(closeBtnPin)==LOW) //Red Button Pressed
  {
    closeTrunk();
  }
  
  if ((!autoOpen) && (!autoClose))  // Do not allow manual movements if autoOpen/Close running
  {
      digitalWrite(relayLockPin,HIGH); //Locked, no power to solenoid
      // stop motor both directions
      digitalWrite(CCW, HIGH); //Motor stops
      digitalWrite(CW, HIGH); //Motor stops
  }

  porchMotionState = digitalRead(porchMotionPin);
  if (porchMotionState == HIGH) //PIR senses motion
  {
     if (motionDetected == LOW) //Make sure PIR has been static for at least interval time period before playing welcome msg
     {
      Serial.println("Motion detected");
      resetAllPasswords();
      motionDetected = HIGH;
      board2BoardSerial.print(NOTICE_MOTION_ACTIVATED); //Let nodeMCU Board know there was motion detected
      myDFPlayer.play(16); //Play ding sound effect
      delay(2000);
      myDFPlayer.play(1);  //Play the first mp3
     }
     timeElapsed = 0;
     //Serial.println("Motion continuing");
  }
  else //PIR is not reporting motion
  {
    porchMotionState = LOW; // PIR software state setting to shadow hardware state
    if (timeElapsed > interval) //Do not allow new PIR motion detection to trip welcome message for a defined interval
    { 
      motionDetected = LOW;
      //Serial.println("Motion sensor reset");      
      timeElapsed = 0;  // reset the counter to 0 so the counting starts over...
    }
  }

  if (digitalRead(mp3PlayerBusyPin) == LOW)
  {
    timeElapsed = 0; //Don't allow motion detector to activate while playing a file
  }

// CONFLICT WITH TX/RX, too many software serials - so just comment it out unless debugging mp3 player
//  mySoftwareSerial.listen();
//  if (myDFPlayer.available()) {
//    printDetail(myDFPlayer.readType(), myDFPlayer.read()); //Print the detail message from DFPlayer to handle different errors and states.
//  }

  //PROCESS ANY INBOUND NOTICES/COMMANDS FROM THE nodeMCU Board
  byte incomingByte = 0; 

  board2BoardSerial.listen();
  if (board2BoardSerial.available() > 0) 
  {
    while (board2BoardSerial.available() > 0)
    {
           // read the incoming byte:
           incomingByte = board2BoardSerial.read()-'0';

           // say what you got:
           Serial.print("I received: ");
           Serial.println(incomingByte, DEC);

           //NOTE: the incoming characters from the nodeMCU are being converted into the 200 range, but this works
            if (incomingByte == 208)
            {
                Serial.println(F("Go Idle"));
            }
            else if (incomingByte == 209)
            {
                Serial.println(F("Open Trunk"));
                openTrunk();
            }
            else if (incomingByte == 210)
            {
                Serial.println(F("Close Trunk"));
                closeTrunk();
            }
            else
            {
                Serial.println(F("UNKNOWN CMD"));
            }             
      }
   }

//   //Use this code to manually send a character cmd from this board to the nodeMCU board from a serial window
//   if (Serial.available()) {
//     board2BoardSerial.write(Serial.read());
//   }

}


//////////////////////////////////////////
//Handle key presses on keypad
//////////////////////////////////////////
void keypadEvent(KeypadEvent eKey){
   switch (customKeypad.getState()){
    case PRESSED:
      timeElapsed = 0; 
      Serial.println(eKey);
      switch (eKey){
        case '*': beep(speakerPin,2637,100); checkAllPasswords(); break;  // check guessed pswd against passwordArray
        case '#': beep(speakerPin,2349,100); resetAllPasswords(); break;  // clean all guessed buffers on passwordArray
        default:  beep(speakerPin,2637,100); appendAllPasswords(eKey);     // update all guessed buffers on passwordArray
      }
   }
}


//////////////////////////////////////////
//Tones for the keypad
//////////////////////////////////////////
void beep (unsigned char speakerPin, int frequencyInHertz, long timeInMilliseconds){ // the sound producing function
  int x;
  long delayAmount = (long)(1000000/frequencyInHertz);
  long loopTime = (long)((timeInMilliseconds*1000)/(delayAmount*2));
  for (x=0;x<loopTime;x++)
    {
      digitalWrite(speakerPin,HIGH);
      delayMicroseconds(delayAmount);
      digitalWrite(speakerPin,LOW);
      delayMicroseconds(delayAmount);
    }
}


//////////////////////////////////////////
//Verify if code entered matches valid passwords array
//////////////////////////////////////////
void checkAllPasswords(){
  int i;
  int passcodeMatchedflag = 0;
  
  for (i = 0; i < MAXPASSWD; i++) {
    if (passwordArray[i].evaluate()){
      passcodeMatchedflag=1;
      if (i==0)
      {
        // Doorbell function
        board2BoardSerial.print(NOTICE_DOORBELL_ACTIVATED); //Let nodeMCU Board know the trunk is broadcasting a doorbell activation
        myDFPlayer.volume(25);
        myDFPlayer.play(12);  //Play the doorbell please wait message
        Serial.println("Doorbell Function Called!");     
        resetAllPasswords();
        return;
      }
    }
  } 

  if (passcodeMatchedflag == 1)
  {
    openTrunk();
  }
  else
  {
    // Password not matched
    myDFPlayer.volume(25);
    myDFPlayer.play(7);  //Play the error code Msg
    Serial.println("Incorrect Password!");
  }
  
  resetAllPasswords();
  
} 

//////////////////////////////////////////
//Open the Trunk
//////////////////////////////////////////
void openTrunk()
{
    // Password matched
    myDFPlayer.volume(25);
    myDFPlayer.play(4);  //Play the correct code Msg
    Serial.println("Correct Password Entered!");
    delay(8000);
    openCloseElapsed = 0;
    autoOpen = true;  
    board2BoardSerial.print(NOTICE_TRUNK_OPENED); //Let nodeMCU Board know the trunk is executing an open command
}

//////////////////////////////////////////
//Close the Trunk
//////////////////////////////////////////
void closeTrunk()
{
    if (trunkOpen) //Don't do anything if the trunk isn't open!
    {
      trunkOpen = false;
      myDFPlayer.play(16); //Play ding sound effect
      delay(2000);
      myDFPlayer.play(5);  //Auto close notice
      delay(5500);
      openCloseElapsed = 0;
      autoClose = true;
      board2BoardSerial.print(NOTICE_TRUNK_CLOSED); //Let the nodeMCU Board know the trunk is executing a close command
    } 
}


//////////////////////////////////////////
//Reset all guessed passwords
//////////////////////////////////////////
void resetAllPasswords() {
  int i;
  for (i = 0; i < MAXPASSWD; i++) {
     passwordArray[i].reset(); 
  } 
} 

void appendAllPasswords(KeypadEvent eKey) {
  int i;
  for (i = 0; i < MAXPASSWD; i++) {
     passwordArray[i].append(eKey); 
  } 
} 

//////////////////////////////////////////
//Reset all guessed passwords
//////////////////////////////////////////
void printEEPROM()
{
  byte value;

  Serial.println("EEPROM Values:");
  for (int index = 0 ; index < EEPROM.length() ; index++) {

      value = EEPROM.read(index);

      Serial.print(index);
      Serial.print("\t");
      Serial.print(value, DEC);
      Serial.println();
  }
}

//////////////////////////////////////////
//Print detailed messages from mp3 player
//////////////////////////////////////////
void printDetail(uint8_t type, int value){
  switch (type) {
    case TimeOut:
      Serial.println(F("Time Out!"));
      break;
    case WrongStack:
      Serial.println(F("Stack Wrong!"));
      break;
    case DFPlayerCardInserted:
      Serial.println(F("Card Inserted!"));
      break;
    case DFPlayerCardRemoved:
      Serial.println(F("Card Removed!"));
      break;
    case DFPlayerCardOnline:
      Serial.println(F("Card Online!"));
      break;
    case DFPlayerPlayFinished:
      Serial.print(F("Number:"));
      Serial.print(value);
      Serial.println(F(" Play Finished!"));
      break;
    case DFPlayerError:
      Serial.print(F("DFPlayerError:"));
      switch (value) {
        case Busy:
          Serial.println(F("Card not found"));
          break;
        case Sleeping:
          Serial.println(F("Sleeping"));
          break;
        case SerialWrongStack:
          Serial.println(F("Get Wrong Stack"));
          break;
        case CheckSumNotMatch:
          Serial.println(F("Check Sum Not Match"));
          break;
        case FileIndexOut:
          Serial.println(F("File Index Out of Bound"));
          break;
        case FileMismatch:
          Serial.println(F("Cannot Find File"));
          break;
        case Advertise:
          Serial.println(F("In Advertise"));
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

nodeMCU Castle Locker Amazon IOT MQTT Controller

Arduino
This is the nodeMCU board controller code. The nodeMCU handles wifi connectivity from the trunk to the internet and the AWS IOT and Alexa handlers. It uses MQTT messages to read/write state information to/from the IOT thing shadow for the Castle Locker trunk. It also communicates (RxTx) with the Arduino Uno Board to pass along commands from Alexa and to publish trunk status to IOT and Alexa.
/******************************************************************************
 * CastleLocker nodeMCU Board Controller
 * 
 * Copyright (c) 2018, Vocal Intent, LLC
 *
 * This program and the accompanying materials are made available under the 
 * terms of the Creative Commons Attribution 4.0 International License
 *
 * The Creative Commons Attribution 4.0 International License is available at: 
 *    https://creativecommons.org/licenses/by/4.0/legalcode
 *
 * Contributors:
 *    Chris Meade - initial contribution
 *
 *****************************************************************************/
//NOTES:
// 1. if you have trouble flashing this board, try disconnecting the Rx/Tx pins as these may interfere during program loading
// 2. if the board will not take flashing from the Arduino IDE - download the flasher program from the nodeMCU git https://github.com/nodemcu/nodemcu-flasher to reset the board

#include <Arduino.h>
#include <Stream.h>
#include <ArduinoJson.h> //see https://arduinojson.org
#include "SoftwareSerial.h" //for "talking" to the Arduino Control board

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

//AWS
#include "sha256.h"
#include "Utils.h"

//WEBSockets
#include <Hash.h>
#include <WebSocketsClient.h>

//MQTT PAHO
#include <SPI.h>
#include <IPStack.h>
#include <Countdown.h>
#include <MQTTClient.h>


//AWS MQTT Websocket
#include "Client.h"
#include "AWSWebSocketClient.h"
#include "CircularByteBuffer.h"

#define PIN_SW_SERIAL_RX 13 
#define PIN_SW_SERIAL_TX 15
SoftwareSerial board2BoardSerial(PIN_SW_SERIAL_RX, PIN_SW_SERIAL_TX); // RX, TX

int trunkOpenIndicatorPin = 5;

#define THING_NAME "castleLockerIOTThing" //Change as needed

//AWS IOT config, change these:
char wifi_ssid[]       = "YOUR_WIFI_SSID"; //change as needed
char wifi_password[]   = "YOUR_WIFI_PASSWORD"; //change as needed
char aws_endpoint[]    = "YOURENDPOINT.iot.us-east-1.amazonaws.com"; //change as needed
char aws_key[]         = "YOUR_AWS_KEY"; //change as needed
char aws_secret[]      = "YOUR_AWS_SECRET"; //change as needed
char aws_region[]      = "YOUR_AWS_REGION"; //change as needed.  e.g., us-east-1
const char* aws_topic  = "$aws/things/" THING_NAME "/shadow/update";

char *subscribeTopic[5] = {
  "$aws/things/" THING_NAME "/shadow/update/accepted",
  "$aws/things/" THING_NAME "/shadow/update/rejected",
  "$aws/things/" THING_NAME "/shadow/update/delta",
  "$aws/things/" THING_NAME "/shadow/get/accepted",
  "$aws/things/" THING_NAME "/shadow/get/rejected"
};
int port = 443;

//MQTT config
const int maxMQTTpackageSize = 1024; //Make sure this is big enough to handle IOT message (state values + metadata from AWS)
const int maxMQTTMessageHandlers = 1;

// If stuff isn't working right, watch the console:
#define DEBUG_PRINT 1

//DEFINE THE IOT COMMUNICATION CONSTANTS IN HUMAN READABLE FORM (NOTE: these are NOT all implemented!)
//TRANSMISSION VALUES
#define STATE_TRUNK_UNKNOWN 0
#define STATE_TRUNK_OPEN 1
#define STATE_TRUNK_CLOSED 2
#define STATE_TRUNK_CHANGING 3

#define CMD_IDLE 0
#define CMD_OPEN_TRUNK 1
#define CMD_CLOSE_TRUNK 2
#define CMD_PARTIAL_OPEN 3
#define CMD_PARTIAL_CLOSE 4

#define CMD_ADD_ACCESS_CODE 5
#define CMD_DELETE_ACCESS_CODE 6
#define CMD_ERASE_ACCESS_CODES 7

#define CMD_GET_TRUNK_STATE 8
#define CMD_PLAY_FILE 9
#define CMD_HOMEOWNER_NOT_AVAILABLE 10
#define CMD_DISMISS_DOORBELL 11
#define CMD_UNLOCK_KEYPAD 12

//RECEPTION VALUES
#define NOTICE_IDLE 0
#define NOTICE_TRUNK_OPENED 1
#define NOTICE_TRUNK_CLOSED 2
#define NOTICE_DOORBELL_ACTIVATED 3
#define NOTICE_MOTION_ACTIVATED 4
#define NOTICE_INVALID_CODE 5
#define NOTICE_CORRECT_CODE 6
#define NOTICE_KEYPAD_LOCKED 7
#define NOTICE_CONTROLLER_ERROR 8
#define NOTICE_CONTROLLER_RESTARTED 9
#define NOTICE_EEPROM_UPDATED 10

//Set initial local values for IOT Shadow values
int voiceCmd2CastleLockerCurrent = CMD_IDLE; //Idle
int castleLockerStateCurrent = STATE_TRUNK_UNKNOWN; //CastleLocker Trunk Open/Close State
int noticeFromCastleLockerCurrent = NOTICE_IDLE; //Idle

char cmdToSend = 254; //If 254, means there is no command to send.  Change to send a command to the Arduino Board

ESP8266WiFiMulti WiFiMulti;

AWSWebSocketClient awsWSclient(1000); 

IPStack ipstack(awsWSclient);
MQTT::Client<IPStack, Countdown, maxMQTTpackageSize, maxMQTTMessageHandlers> *client = NULL;

//# of connections
long connection = 0;

//generate random mqtt clientID
char* generateClientID () {
  char* cID = new char[23]();
  for (int i=0; i<22; i+=1)
    cID[i]=(char)random(1, 256);
  return cID;
}

//count messages arrived
int arrivedcount = 0;

//callback to handle mqtt messages
void messageArrived(MQTT::MessageData& md)
{
  MQTT::Message &message = md.message;
  
  char *pch;
  
  Serial.println ("-------------------");
  Serial.print("Message ");
  Serial.print(++arrivedcount);
  Serial.print(" arrived: qos ");
  Serial.print(message.qos);
  Serial.print(", retained ");
  Serial.print(message.retained);
  Serial.print(", dup ");
  Serial.print(message.dup);
  Serial.print(", packetid ");
  Serial.println(message.id);
  Serial.print("Payload ");
  char* msg = new char[message.payloadlen+1]();
  memcpy (msg,message.payload,message.payloadlen);
  Serial.println(msg);
  //Serial.println (ESP.getFreeHeap ());

  const size_t bufferSize = 6*JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + 6*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(4) + 610; //calculated from https://arduinojson.org/assistant/
  DynamicJsonBuffer jsonBuffer(bufferSize);
  
  const char* json = msg;
  
  JsonObject& root = jsonBuffer.parseObject(json);

  if (!root.success()) 
  {
    // Parsing failed
      Serial.println ("Parsing msg json object failed!");
      delete msg;
      return;
  }
  
  JsonObject& state = root["state"];

  if (state.containsKey("desired")) {
    JsonObject& state_desired = state["desired"];
    int state_desired_voiceCmd2CastleLocker = state_desired["voiceCmd2CastleLocker"]; 
    int state_desired_castleLockerState = state_desired["castleLockerState"]; 
    int state_desired_noticeFromCastleLocker = state_desired["noticeFromCastleLocker"]; 
  
    bool updateNeededFlag = false;
  
//    if (state_desired_voiceCmd2CastleLocker != voiceCmd2CastleLockerCurrent) {
      Serial.print("NEW CMD RECEIVED - EXECUTE: ");
      Serial.print(state_desired_voiceCmd2CastleLocker); 
      Serial.print("\t\n");   
      
      switch (state_desired_voiceCmd2CastleLocker) {
        case CMD_IDLE:
          Serial.println(F("Go Idle"));
          cmdToSend = CMD_IDLE;
          voiceCmd2CastleLockerCurrent = state_desired_voiceCmd2CastleLocker;
          updateNeededFlag = true;
          break;
        case CMD_OPEN_TRUNK:
          Serial.println(F("Open Trunk Command Requested"));
          if (castleLockerStateCurrent == STATE_TRUNK_CLOSED)
          {
            cmdToSend = CMD_OPEN_TRUNK;
            voiceCmd2CastleLockerCurrent = state_desired_voiceCmd2CastleLocker;
            updateNeededFlag = true;            
          }
          else
          {
            Serial.println(F("Trunk is not closed - request to open ignored!"));
          }          
          break;
        case CMD_CLOSE_TRUNK:
          Serial.println(F("Close Trunk Command Requested"));
          if (castleLockerStateCurrent == STATE_TRUNK_OPEN)
          {
            cmdToSend = CMD_CLOSE_TRUNK;
            voiceCmd2CastleLockerCurrent = state_desired_voiceCmd2CastleLocker;
            updateNeededFlag = true;            
          }
          else
          {
            Serial.println(F("Trunk is not open - request to close ignored!"));
          }
          break;
        default:
          break;
      }

//    }
    
    if ((state_desired_castleLockerState > 0) && (state_desired_castleLockerState != castleLockerStateCurrent)) {
      Serial.print("NEW TRUNK STATE RECEIVED - EXECUTE: ");
      Serial.print(state_desired_castleLockerState); 
      Serial.print("\t\n");   
      updateNeededFlag = true;
    }
    
    if (state_desired_noticeFromCastleLocker != noticeFromCastleLockerCurrent) {
      Serial.print("NEW NOTICE RECEIVED - EXECUTE: ");
      Serial.print(state_desired_noticeFromCastleLocker); 
      Serial.print("\t\n");   
      noticeFromCastleLockerCurrent = state_desired_noticeFromCastleLocker;
      updateNeededFlag = true;
    }  
  
    if (updateNeededFlag)
    {
      sendReportedStateMessage();       
    }
  }
  
  Serial.println ("--------***--------");

  delete msg;
}

//connects to websocket layer and mqtt layer
bool connect () {

    if (client == NULL) {
      client = new MQTT::Client<IPStack, Countdown, maxMQTTpackageSize, maxMQTTMessageHandlers>(ipstack);
    } else {

      if (client->isConnected ()) {    
        client->disconnect ();
      }  
      delete client;
      client = new MQTT::Client<IPStack, Countdown, maxMQTTpackageSize, maxMQTTMessageHandlers>(ipstack);
    }

    //delay is not necessary... it just help us to get a "trustful" heap space value
    delay (1000);
    Serial.print (millis ());
    Serial.print (" - conn: ");
    Serial.print (++connection);
    Serial.print (" - Free Heap (");
    Serial.print (ESP.getFreeHeap ());
    Serial.println (")");

   int rc = ipstack.connect(aws_endpoint, port);
    if (rc != 1)
    {
      Serial.println("error connection to the websocket server");
      return false;
    } else {
      Serial.println("websocket layer connected");
    }

    Serial.println("MQTT connecting");
    MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
    data.MQTTVersion = 3;
    char* clientID = generateClientID ();
    data.clientID.cstring = clientID;
    rc = client->connect(data);
    delete[] clientID;
    if (rc != 0)
    {
      Serial.print("error connection to MQTT server");
      Serial.println(rc);
      return false;
    }
    Serial.println("MQTT connected");
    return true;
}

//subscribe to a mqtt topic(s)
void subscribe () {

      for (int i=0; i<5; i++) {
        int rc = client->subscribe(subscribeTopic[i], MQTT::QOS0, messageArrived);
        if (rc != 0) {
          Serial.print("rc from MQTT subscribe is ");
          Serial.println(rc);
          break;
        }
        Serial.println("MQTT Topic subscribed");
      }
    
}

//send a message to a mqtt topic
void sendReportedStateMessage() {
    //send a message
    Serial.println("MQTT Updating IOT Topic from device");
    //Serial.println (ESP.getFreeHeap ());
    
    MQTT::Message message;

    String cmd = String(voiceCmd2CastleLockerCurrent);
    String state = String(castleLockerStateCurrent);
    String notice = String(noticeFromCastleLockerCurrent);
          
    if (isnan(voiceCmd2CastleLockerCurrent)) 
    {
      Serial.println("Failed to read voiceCmd2CastleLocker!");
      return;
    } 
    else if (isnan(castleLockerStateCurrent)) 
    {
      Serial.println("Failed to read castleLockerState!");
      return;
    }
    else if (isnan(noticeFromCastleLockerCurrent)) 
    {
      Serial.println("Failed to read noticeFromCastleLocker!");
      return;
    }
    
    String values = "{\"state\":{\"reported\":{\"voiceCmd2CastleLocker\": " + cmd + ",\"castleLockerState\": " + state + ",\"noticeFromCastleLocker\": " + notice + "}}}";
      // http://stackoverflow.com/questions/31614364/arduino-joining-string-and-char
    const char *publish_message = values.c_str();
    char buf[1000];
    strcpy(buf, publish_message);
    
    message.qos = MQTT::QOS0;
    message.retained = false;
    message.dup = false;
    message.payload = (void*)buf;
    message.payloadlen = strlen(buf)+1;
    int rc = client->publish(aws_topic, message); 
}

//send a message to a mqtt topic
void sendDesiredStateMessage() {
    //send a message
    Serial.println("MQTT Updating IOT Topic from device");
    //Serial.println (ESP.getFreeHeap ());
    
    MQTT::Message message;

    String cmd = String(voiceCmd2CastleLockerCurrent);
    String state = String(castleLockerStateCurrent);
    String notice = String(noticeFromCastleLockerCurrent);
          
    if (isnan(voiceCmd2CastleLockerCurrent)) 
    {
      Serial.println("Failed to read voiceCmd2CastleLocker!");
      return;
    } 
    else if (isnan(castleLockerStateCurrent)) 
    {
      Serial.println("Failed to read castleLockerState!");
      return;
    }
    else if (isnan(noticeFromCastleLockerCurrent)) 
    {
      Serial.println("Failed to read noticeFromCastleLocker!");
      return;
    }
    
    String values = "{\"state\":{\"desired\":{\"voiceCmd2CastleLocker\": " + cmd + ",\"castleLockerState\": " + state + ",\"noticeFromCastleLocker\": " + notice + "}}}";
      // http://stackoverflow.com/questions/31614364/arduino-joining-string-and-char
    const char *publish_message = values.c_str();
    char buf[1000];
    strcpy(buf, publish_message);
    
    message.qos = MQTT::QOS0;
    message.retained = false;
    message.dup = false;
    message.payload = (void*)buf;
    message.payloadlen = strlen(buf)+1;
    int rc = client->publish(aws_topic, message); 
}


void setup() {
    Serial.begin (115200);
    delay (2000);
    Serial.setDebugOutput(1);

    pinMode(PIN_SW_SERIAL_TX, OUTPUT);
    pinMode(PIN_SW_SERIAL_RX, INPUT);

    pinMode(trunkOpenIndicatorPin, INPUT_PULLUP);

    board2BoardSerial.begin(9600);

    //fill with ssid and wifi password
    WiFiMulti.addAP(wifi_ssid, wifi_password);
    Serial.println ("connecting to wifi");
    while(WiFiMulti.run() != WL_CONNECTED) {
        delay(100);
        Serial.print (".");
    }
    Serial.println ("\nwifi connected");

    //fill AWS parameters    
    awsWSclient.setAWSRegion(aws_region);
    awsWSclient.setAWSDomain(aws_endpoint);
    awsWSclient.setAWSKeyID(aws_key);
    awsWSclient.setAWSSecretKey(aws_secret);
    awsWSclient.setUseSSL(true);

    if (connect ())
    {
      subscribe (); //subscribe to all MQTT topics

      //Grab current trunk open/closed state and report it to IOT shadow
      int buttonState = digitalRead(trunkOpenIndicatorPin);
      if (buttonState==LOW) // the magnetic contact switch is closed, lid is down
      {
        //Trunk is closed
        castleLockerStateCurrent = STATE_TRUNK_CLOSED;
      }
      else // the lid is up
      {
        //Trunk is open
        castleLockerStateCurrent = STATE_TRUNK_OPEN;
      }
       
      sendReportedStateMessage(); //intial update msg to IOT thing
    }
}

void loop() {

  // keep the mqtt up and running
  if (awsWSclient.connected ()) {    
      client->yield(100L); //Keep this value short, or our loop will be SLOW!
  } else {
    //handle reconnection
    if (connect ()){ 
      subscribe ();   
    }
  }

  //If a command is queued, send the command to the Arduino Control board
  if (cmdToSend != 254)
  {
    Serial.print("sending: ");
    Serial.println(cmdToSend);
    board2BoardSerial.print(cmdToSend);
    cmdToSend = 254;
  }

  //Handle updates to trunk Open/Close state
  int buttonState = digitalRead(trunkOpenIndicatorPin);
      if (buttonState==LOW) // the magnetic contact switch is closed, lid is down
      {
        //Trunk is closed
        if (castleLockerStateCurrent != STATE_TRUNK_CLOSED) //if state has changed for the local variable, update the IOT shadow document
        {
          castleLockerStateCurrent = STATE_TRUNK_CLOSED;
          sendReportedStateMessage();
        }
      }
      else // the lid is up
      {
        Serial.print(".");
        //Trunk is open
        if (castleLockerStateCurrent != STATE_TRUNK_OPEN) //if state has changed for the local variable, update the IOT shadow document
        {
          castleLockerStateCurrent = STATE_TRUNK_OPEN;
          sendReportedStateMessage();
        }
      }


  //PROCESS ANY INBOUND NOTICES/COMMANDS FROM THE Arduino Board
  byte incomingByte = 0; 

  board2BoardSerial.listen();
  if (board2BoardSerial.available() > 0) 
  {
    while (board2BoardSerial.available() > 0)
    {
           // read the incoming byte:
           incomingByte = board2BoardSerial.read()-'0';

           // say what you got:
           Serial.print("I received: ");
           Serial.println(incomingByte, DEC);
           
            if (incomingByte == NOTICE_IDLE)
            {
                Serial.println(F("Going Idle"));
                voiceCmd2CastleLockerCurrent = CMD_IDLE;
                sendDesiredStateMessage(); //Update IOT Shadow document
            }
            else if (incomingByte == NOTICE_TRUNK_OPENED)
            {
                Serial.println(F("Opening Trunk"));
                voiceCmd2CastleLockerCurrent = CMD_OPEN_TRUNK;
                sendReportedStateMessage(); //Update IOT Shadow document
            }
            else if (incomingByte == NOTICE_TRUNK_CLOSED)
            {
                Serial.println(F("Closing Trunk"));
                voiceCmd2CastleLockerCurrent = CMD_CLOSE_TRUNK;
                sendReportedStateMessage(); //Update IOT Shadow document
            }
            else if (incomingByte == NOTICE_DOORBELL_ACTIVATED)
            {
                Serial.println(F("Doorbell Activated"));
                //TO DO - Add function to send doorbell notice to IOT Shadow and Alexa
            }
            else if (incomingByte == NOTICE_MOTION_ACTIVATED)
            {
                Serial.println(F("Motion Detected"));
                //TO DO - Add function to report motion to IOT Shadow and Alexa
            }                        
            else
            {
                Serial.println(F("UNKNOWN CMD"));
            }             
      }
   }
      
}

Alexa Castle Locker Custom Skill Interaction Model (json file)

JSON
This is the Interaction Model for the Castle Locker custom skill. A very basic skill that gets you up and running with Alexa to Open/Close the Trunk using your voice and determines if the state of the trunk is open/closed/or unknown.
{
  "languageModel": {
    "intents": [
      {
        "name": "AMAZON.CancelIntent",
        "samples": []
      },
      {
        "name": "AMAZON.HelpIntent",
        "samples": []
      },
      {
        "name": "AMAZON.StopIntent",
        "samples": []
      },
      {
        "name": "closeTrunkIntent",
        "samples": [
          "close trunk",
          "close package trunk",
          "please close trunk",
          "to close trunk",
          "close my trunk"
        ],
        "slots": []
      },
      {
        "name": "goodbyeIntent",
        "samples": [
          "goodbye",
          "bye",
          "never mind",
          "exit"
        ],
        "slots": []
      },
      {
        "name": "openTrunkIntent",
        "samples": [
          "open secure trunk",
          "open my trunk",
          "get my packages",
          "open castle locker trunk",
          "open trunk",
          "retrieve packages",
          "please open trunk"
        ],
        "slots": []
      },
      {
        "name": "trunkStatusIntent",
        "samples": [
          "what is the status of my trunk",
          "is the trunk open",
          "is the trunk closed",
          "trunk status",
          "what is my trunk's status",
          "is my trunk open",
          "is my trunk closed"
        ],
        "slots": []
      }
    ],
    "invocationName": "castle locker"
  },
  "prompts": [
    {
      "id": "Confirm.Intent-openTrunkIntent",
      "variations": [
        {
          "type": "PlainText",
          "value": "Are you sure you want to open your Castle Locker secure trunk"
        }
      ]
    }
  ],
  "dialog": {
    "intents": [
      {
        "name": "openTrunkIntent",
        "confirmationRequired": true,
        "prompts": {
          "confirmation": "Confirm.Intent-openTrunkIntent"
        },
        "slots": []
      }
    ]
  }
}

linkalexa.aspx

HTML
The web form aspx page I created for account linking to connect Alexa with a specific user/trunk
<%@ Page Title="Link Account to Alexa" Language="C#" MasterPageFile="~/Blank.Master" AutoEventWireup="true" CodeBehind="linkAlexa.aspx.cs" Inherits="castleLockerWebsite.linkAlexa" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <p>
        <asp:Image ID="Image1" runat="server" ImageUrl="~/images/apple-icon-60x60.png" /></p>
                <h3>Log into Your Castle Locker Account</h3>
              <p>Screen Name <br />
                  <asp:TextBox ID="screenNameTextBox" runat="server" TextMode="SingleLine" Width="270px" BackColor="#EBF3FA" BorderColor="#00AEEF" BorderStyle="Inset" Height="20px" TabIndex="1"></asp:TextBox><br />
                  <asp:Label ID="errorScreenName" runat="server" class="errorBox" Width="275px"></asp:Label>
                  </p>
              <p>Password<br />
                  <asp:TextBox ID="passwordTextBox" runat="server" TextMode="Password" Width="270px" BackColor="#EBF3FA" BorderColor="#00AEEF" BorderStyle="Inset" Height="20px" TabIndex="2"></asp:TextBox><br />
                  <asp:Label ID="errorPassword" runat="server" class="errorBox" Width="275px"></asp:Label>
                  </p>
               
                <p class="buttonArea">
                    <asp:ImageButton ID="loginBtn" runat="server" ImageUrl="images/loginButton.png" onmouseover="this.src='images/loginButton.png'" onmouseout="this.src='images/loginButton.png'" OnClick="loginBtn_Click" TabIndex="3"/>
                </p>

    <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:castleLockerDBConnectionString %>" 
        SelectCommand="SELECT * FROM [userAccounts] WHERE ((LOWER([userName]) = LOWER(@userName)) AND ([userPassword] = @userPassword))"
        UpdateCommand="UPDATE [userAccounts] SET lastLogin = GETDATE() WHERE  (LOWER([userName]) = LOWER(@userName)) AND ([userPassword] = @userPassword)">
        <SelectParameters>
            <asp:ControlParameter ControlID="screenNameTextBox" Name="userName" PropertyName="Text" Type="String" />
            <asp:ControlParameter ControlID="passwordTextBox" Name="userPassword" PropertyName="Text" Type="String" />
        </SelectParameters>
        <UpdateParameters>
            <asp:ControlParameter ControlID="screenNameTextBox" Name="userName" PropertyName="Text" />
            <asp:ControlParameter ControlID="passwordTextBox" Name="userPassword" PropertyName="Text" />
        </UpdateParameters>
</asp:SqlDataSource>
</asp:Content>

linkalexa.cs

C#
The code behind file associated with linkalexa.aspx to enable account linking between Alexa and a specific user/trunk. Note the QueryString values passed in the request obect from Alexa when redirecting to this login page. You must return the userID from your system and Alexa access token(s) in the response object once you validate the user. This is just one example of an approach to account linking.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Text;

namespace castleLockerWebsite
{
    public partial class linkAlexa : System.Web.UI.Page
    {
        public string alexa_state;
        public string alexa_client_id;
        public string alexa_response_type;
        public string alexa_scope;
        public string alexa_redirect_uri;
        public string alexa_code;

        protected void Page_Load(object sender, EventArgs e)
        {
            Page.Form.DefaultButton = loginBtn.UniqueID;

            alexa_state = Request.QueryString["state"];
            alexa_client_id = Request.QueryString["client_id"];
            alexa_response_type = Request.QueryString["response_type"];
            alexa_scope = Request.QueryString["scope"];
            alexa_redirect_uri = Request.QueryString["redirect_uri"];


            if (!Page.IsPostBack)
            {
                errorScreenName.Text = "";
                errorPassword.Text = "";
            }
            else
            {
                if (screenNameTextBox.Text.ToString() == "")
                {
                    errorScreenName.Text = "Screen name cannot be blank";
                }
                if (passwordTextBox.Text.ToString() == "")
                {
                    errorPassword.Text = "Password cannot be blank";
                }
            }

        }

        protected void loginBtn_Click(object sender, ImageClickEventArgs e)
        {
            if (screenNameTextBox.Text.ToString() == "")
            {
                errorScreenName.Text = "Screen name cannot be blank";
                return;
            }
            if (passwordTextBox.Text.ToString() == "")
            {
                errorPassword.Text = "Password cannot be blank";
                return;
            }
            SqlDataSource1.DataBind();
            DataView dvSql = (DataView)SqlDataSource1.Select(DataSourceSelectArguments.Empty);
            if (dvSql != null)
            {
                foreach (DataRowView drvSql in dvSql)
                {
                    SqlDataSource1.Update();
                    Session["userID"] = drvSql["userID"].ToString();
                    Session["userName"] = drvSql["userName"].ToString();

                    var accessToken = drvSql["userID"].ToString() + "klr" + RandomString(12, true);
                    Response.Redirect(alexa_redirect_uri + "#state=" + alexa_state + "&access_token=" + accessToken + "&token_type=Bearer");

                }
                errorScreenName.Text = "Screen Name or Password not recognized";
            }
            else
            {
                errorScreenName.Text = "Screen Name or Password not recognized";
            }

        }

        //Generates a random string
        private string RandomString(int size, bool lowerCase)
        {
            StringBuilder builder = new StringBuilder();
            Random random = new Random();
            char ch;
            for (int i = 0; i < size; i++)
            {
                ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65)));
                builder.Append(ch);
            }
            if (lowerCase)
                return builder.ToString().ToLower();
            return builder.ToString();
        }

        //Generates a randum number
        private int RandomNumber(int min, int max)
        {
            Random random = new Random();
            return random.Next(min, max);
        }
    }
}

Credits

Chris Meade

Chris Meade

1 project • 2 followers

Comments