Project tutorial
Secure Package Delivery Trunk for Your Front Porch

Secure Package Delivery Trunk for Your Front Porch © CC BY

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

  • 3,463 views
  • 4 comments
  • 21 respects

Components and supplies

Necessary tools and machines

09507 01
Soldering iron (generic)

Apps and online services

About this project

Your home is your Castle. Increasingly, we enjoy faster and faster deliveries to our homes, but we cannot always be there to take delivery of our packages (since we cannot all afford to hire a personal doorman!) So what happens with all those packages we need delivered?

Do we try to pick up our parcels elsewhere and add another stop to our already over-busy days? Do we put the burden of accepting deliveries on one of our neighbors? Can we trust enough to let random delivery personnel into our homes? Or do we just cross our fingers and hope that delivered packages will still be there, safe and sound (and dry!) when we get home?

We need a better and more secure solution for unattended package deliveries that balances our need for security without sacrificing home delivery convenience, that reduces redeliveries, and that gives us control and insight into our deliveries. The "Castle Locker" secure, voice-controlled, trunk project is my prototype for securing package deliveries at the front door of our castles, err, homes!

Overview Video

Quick demonstration video of the trunk in action

Project Approach

This project is broken into 6 major steps/phases:

1. Build the Trunk

2. Set-up an AWS IOT Thing

3. Create the Arduino Trunk Controller

4. Create the nodeMCU IOT Communication Controller

5. Create website/page to enable Alexa Account linking (optional)

6. Create Alexa Custom Skill

System Architecture

STEP 1. Build the Trunk

Your trunk could be made from almost any materials and of whatever scale you choose. For the Castle Locker, I selected a toy chest I had purchased last year from Amazon, but never used.

Then I "pimped" the trunk with two linear actuators actuators from Firgelli Automations to automate the opening/closing - and to make it hard to force the trunk open.

I also added a solenoid lock, close button, 4x3 matrix keypad, PIR motion sensor, open/close magnetic contact switch/sensor, keypad passive buzzer (for key pad sounds) and a speaker (for "talking to" delivery people). These items will be mounted all dependent on the trunk that you use. Below are a few shots so you get the idea with regard to my chosen layout on this trunk.

STEP 2. Set up an AWS IOT Thing

I will not reproduce Amazon's very good documentation on this. Just follow the steps at https://docs.aws.amazon.com/iot/latest/developerguide/register-device.html to create a thing. Here are some notes that helped:

1. Copy the Thing Name, AWS Endpoint and Region for your thing. You will need these in the Arduino code and Alexa skill in future steps.

2. You will need to create an IAM user and attach an IOT services policy that permits access to the IOT thing. Be sure to copy the IAM access key ID and secret access key when you create the user, because you cannot get the secret access key later (you will have to recreate the user if you lose it!).

3. If you want, you can create a thing shadow document now and play around with changing the desired and reported states. The desired and reported values will be created later by the code, if you have not already defined the values you want to track in the shadow document. We are going to use three settings including:

  • "voiceCmd2CastleLocker", for sending voice commands from Alexa to the trunk
  • "castleLockerState", for tracking the open/close status of the trunk
  • "noticeFromCastleLocker", for future notices like an incorrect code entry from the trunk to Alexa

STEP 3. Create the Arduino Trunk Controller

See the attached code and circuit diagrams for the set-up for this step. The purpose of the Arduino Trunk Controller is to provide standalone (no WIFI connection) control of the trunk. The Arduino Uno Board is a good choice for the many inputs/outputs for sensors and feedback to the user. The trunk controller provides these major functions:

1. Detects motion when someone approaches the trunk.

2. Plays a greeting message directing the delivery person to enter an access code on the trunk's keypad to leave the package in the secure trunk.

3. Handles the keypad access code entry, providing feedback, as needed, for correct/incorrect entries.

4. Opens the trunk when a correct code is entered (disengages the solenoid lock, and powers the actuators to open the lid).

5. Monitors the trunk open state for 15 seconds and closes and locks the trunk automatically if the red button has not been pushed.

6. Listens for and communicates with the nodeMCU IOT Controller to maintain status between the boards and to accept commands to open/close the trunk initiated via Alexa.

As part of this step, you will need to generate the .mp3 files that the trunk uses to engage with the delivery person. I elected to use Amazon Polly to generate these files rather than trying to record them from a live voice actor. Amazon Polly has the advantage of creating sound files at consistent volumes and without background noise. Also, it is VERY fast to create professional sounding recordings and tweak them as needed simply by editing your script and regenerating the mp3. I found "Matthew's" voice to be the most natural for this project.

TIP: As noted in the code, name the mp3 files sequentially and copy them to the microSD card one at a time, and in order, so the DFRobot mp3 player plays the correct file. Also know that if you change the number or order of the files, you need to update the Arduino code statements to call the appropriate file number.

TIP: Furthermore, the DFRobot player is VERY temperamental about power. Make sure you fully power it with 5V and enough current. Otherwise the board will behave erratically, sometimes playing files and even stalling the main program loop. Pay particular attention to sudden power draws or, better yet, power the DFRobot mp3 player breakout board separately from other components.

TIP: It would be better to store the Access Codes in the EEPROM rather than hard coding them into the software. That will enable future features such as dynamically adding codes, deleting codes, and expiring codes from Alexa.

STEP 4. Create the nodeMCU IOT Communication Controller

See the attached code and circuit diagrams for the set-up for this step. The purpose of the nodeMCU IOT Communication Controller is to provide internet accessibility for the trunk and to communicate trunk status to/from the AWS IOT thing shadow via MQTT pub/sub messages. Alexa will also update the thing shadow in order to send along voice commands to the trunk. After trying to put all the WIFI and MQTT IOT functions into the Arduino Uno Board with a WIFI breakout, I finally settled on the need for a separate board. By separating the WIFI and IOT functions onto the nodeMCU board, I also gained the advantage of the Arduino controller being more stable and less likely to fail due to a loss of internet signal. The nodeMCU IOT controller provides the following major functions:

1. Sets up and maintains a WIFI connection/reconnection.

2. Sets up and maintains subscriptions to the AWS IOT thing.

3. Uses MQTT messages and callbacks to process changes to the thing shadow - such as a voice command to open the trunk from Alexa. Note: I am using a variety of libraries in this board's program to provide these functions (see code include statements).

4. Powers the trunk open/close magnet contact switch/sensor to know whether the trunk is open or closed. The nodeMCU board also ensures we do not send an open command to the trunk if it is already open and vice versa for the closed state.

5. Listens for and communicates with the Arduino Trunk Controller to maintain status between the boards and to accept notices initiated from the Arduino controller that may need to be sent to IOT and Alexa (or other devices - like a text message via Twillio).

TIP: The nodeMCU board labels pins differently from the Arduino numbering of boards like the Uno. Use the pinout reference above to translate the pins into their GPIO numbers you define in the Arduino code.

TIP: The nodeMCU board can sometimes get itself into a state where you cannot flash the board from the Arduino IDE. First, disconnect all the pin leads and just use the microUSB port to try and upload code. If that still fails, you will probably need to re-flash the board from the manufacturer's GitHub site: https://github.com/nodemcu/nodemcu-flasher

TIP: If you want to see what is happening on both the Arduino Board and the nodeMCU Board from the serial monitors on your development PC, you will need to launch two separate instances of the Arduino IDE (use the MS Windows Programs menu to open the program twice!) so you can have each sketch have its own Board core. You will also need a second serial monitor program to watch one of the boards, as you cannot open two serials monitors from the Arduino IDE at the same time. I found PuTTY to be the perfect choice for this scenario.

STEP 5. Create website/page to enable Alexa Account linking [OPTIONAL]

If you are only going to control your own trunk and keep your Alexa skill private, you will not need to complete this step. If, however, you want to have multiple trunks or users, your Alexa skill will need someway of distinguishing between the users, trunks, or locations, and this is where account linking may come into play. I published the Castle Locker skill as a public one, anticipating that I may want to try and bring this product to market (I also knew that going through the certification process helps point out potential problems in the skill and ensure the skill follows voice-first best practices). As a result, I needed to create an account linking webpage to work with the Alexa account linking function. Suffice it to say, but the webpage just needs to allow a user to login and then return a token that identifies (see the linkAlexa.aspx page and the codebehind file linkAlexa.cs both attached) the user to Alexa and then within your skill when someone invokes the skill. I chose an Azure website with a simple SQL Server based user table for my purposes, but you could accomplish this in almost any website platform, so long as the website can be secured with an SSL certificate.

STEP 6. Create Alexa Custom Skill

The final step in this build is to create the custom Alexa skill to control the trunk with voice commands. The attached JSON intent schema and Lambda node.js code provides the functionality for Alexa to understand voice commands related to the Castle Locker trunk and to communicate to/from the AWS IOT thing. You will create the Lambda Intent Handler and use it's ARN as the endpoint in the Alexa Skill.

The attached skill code has passed certification, so it should serve as a good template for building similar skills that have an IOT interface as well (examples of which I found to be limited or one-sided in their interaction with IOT services). The following vocal commands are included in the skill at this time:

1. Alexa, open Castle Locker

2. Open my trunk

3. Close my trunk

4. Is my trunk open (or closed)

5. Help

6. Goodbye

I envision extending these functions to be able to add/delete access codes for the trunk using your voice. I also see opportunities for receiving notifications on Alexa devices as to trunk access, incorrect entries, detected motion, the doorbell function from the keypad, and other delivery and porch related activities.

NEXT STEPS

There are many enhancements that would bring this project along to a robust complete product. I hope my work-to-date can serve as a good foundation for a crowdfunding project. To bring the Castle Locker secure trunk to market I envision adding/refining the following things:

1. Move all electronic components from prototype breadboards and long wires to a custom PCB.

2. Design a truly waterproof trunk that provides a more robust integrated locking mechanism.

3. Add more event logging and control features to the controller code to allow temporary, one-time, and expiring access codes.

4. Enhance the Alexa skill to enable notifications from the trunk

5. Develop the Arduino and nodeMCU controllers into a stand-alone control unit that could be fitted to multiple trunks or other containers from 3rd party manufacturers.

Code

Alexa ASK Custom Skill Lambda Function - node.jsJavaScript
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 ScriptsPlain 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 CodeArduino
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 ControllerArduino
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.aspxHTML
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.csC#
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);
        }
    }
}

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

Comments

Similar projects you might like

Herb Box Eco System

Project tutorial by Walter Heger

  • 51,655 views
  • 40 comments
  • 296 respects

Alexa BBQ/Kitchen Thermometer with IoT Arduino and e-Paper

Project tutorial by Roger Theriault

  • 3,393 views
  • 0 comments
  • 10 respects

Arduino Home Controller Activated by Alexa

Project tutorial by Jose Cruz

  • 6,422 views
  • 8 comments
  • 13 respects

Intelligent Door Lock

Project in progress by Md. Khairul Alam

  • 24,478 views
  • 24 comments
  • 117 respects

Smart Pool: Alexa Controlled Pool Manager

Project tutorial by Benjamin Winiarski

  • 3,264 views
  • 3 comments
  • 9 respects

Wise Shower Driven by Alexa Skill

Project in progress by Virgilio Enrique Aray Arteaga

  • 2,852 views
  • 1 comment
  • 4 respects
Add projectSign up / Login