Akihiko Suyama
Published © Apache-2.0

Smart Message Board

You can set a message on a dot matrix display by Alexa!

AdvancedFull instructions provided3 hours5,790
Smart Message Board

Things used in this project

Hardware components

Arduino Yun
Arduino Yun
×1
Base Shield V2
Seeed Studio Base Shield V2
×1
Echo Dot
Amazon Alexa Echo Dot
×1
Seeed Studio Grove Rotary Angle Sensor
For scroll speed adjustment. optional
×1
Dot Matrix MAX7219 4 in 1 Module
one to eight MAX7219 modules can be controllable by this project.
×1
Jumper wires (generic)
Jumper wires (generic)
×8

Software apps and online services

Arduino IDE
Arduino IDE
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
AWS Lambda
Amazon Web Services AWS Lambda
AWS IoT
Amazon Web Services AWS IoT
AWS-IoT-Arduino-Yun-SDK

Story

Read more

Schematics

Circuit Design

DATA_PIN = 12; CLK_PIN = 11; CS_PIN = 10;
Rotary Angle Sensor signal = A0;

Code

aws_iot_config.h

C/C++
Place this code in the same directory of Arduino ino file.
/*
 * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

#ifndef config_usr_h
#define config_usr_h

// Copy and paste your configuration into this file
//===============================================================
#define AWS_IOT_MQTT_HOST "XXXXXXXXXXXX.iot.us-east-1.amazonaws.com"   // your endpoint
#define AWS_IOT_MQTT_PORT 8883                  // your port
#define AWS_IOT_CLIENT_ID "My_ClientID"           // your client ID
#define AWS_IOT_MY_THING_NAME "My_Board"            // your thing name
#define AWS_IOT_ROOT_CA_FILENAME "aws-iot-rootCA.crt"           // your root-CA filename
#define AWS_IOT_CERTIFICATE_FILENAME "cert.pem"                 // your certificate filename
#define AWS_IOT_PRIVATE_KEY_FILENAME "privkey.pem"              // your private key filename
//===============================================================
// SDK config, DO NOT modify it
#define AWS_IOT_PATH_PREFIX "../certs/"
#define AWS_IOT_ROOT_CA_PATH AWS_IOT_PATH_PREFIX AWS_IOT_ROOT_CA_FILENAME			// use this in config call
#define AWS_IOT_CERTIFICATE_PATH AWS_IOT_PATH_PREFIX AWS_IOT_CERTIFICATE_FILENAME	// use this in config call
#define AWS_IOT_PRIVATE_KEY_PATH AWS_IOT_PATH_PREFIX AWS_IOT_PRIVATE_KEY_FILENAME	// use this in config call

#endif

DotMatrixControl.cpp

C/C++
Place this code in the same directory of Arduino ino file.
#include "DotMatrixControl.h"

DotMatrixControl::DotMatrixControl(int dataPin, int clkPin, int csPin, int _displayCount) {
  DATA_PIN = dataPin;
  CLK_PIN = clkPin;
  CS_PIN = csPin;

  displayCount = _displayCount;

  if (displayCount <= 0) displayCount = 1;
  if (displayCount > MAX_DISPLAY_COUNT) displayCount = MAX_DISPLAY_COUNT;
}

void DotMatrixControl::setup() {
  digitalWrite(CS_PIN, HIGH);

  pinMode(DATA_PIN, OUTPUT);
  pinMode(CLK_PIN, OUTPUT);
  pinMode(CS_PIN, OUTPUT);

  for (int i = 0; i < sizeof(buffer); i++) buffer[i] = 0x00;

  //shutdown all display first
  for (int i = 0; i < displayCount; i++) shutdown(i, true);

  for (int i = 0; i < displayCount; i++) {
    send(i, REG_DISPLAY_TEST, 0);
    send(i, REG_SCAN_LIMIT, 7);
    send(i, REG_DECODE_MODE, 0);
    setIntensity(i, 0);
  }

  clearDisplay();

  //shutdown off
  for (int i = 0; i < displayCount; i++) shutdown(i, false);
}

void DotMatrixControl::shutdown(int index, bool isShutdown) {
  if ((index < 0) || (index >= displayCount)) return;

  send(index, REG_SHUTDOWN, isShutdown ? 0 : 1);
}

//intensity : 0-15
void DotMatrixControl::setIntensity(int intensity) {
  if ((intensity < 0) || (intensity >= 16)) return;
  for (int x = 0; x < displayCount; x++) send(x, REG_INTENSITY, intensity);
}

void DotMatrixControl::setIntensity(int index, int intensity) {
  if ((index < 0) || (index >= displayCount)) return;
  if ((intensity < 0) || (intensity >= 16)) return;

  send(index, REG_INTENSITY, intensity);
}

void DotMatrixControl::clearDisplay() {
  clearBuffer();
  transferAllDots();
}

void DotMatrixControl::clearDisplay(int index) {
  if ((index < 0) || (index >= displayCount)) return;

  clearBuffer(index);
  transferAllDots();
}

void DotMatrixControl::clearBuffer() {
  for (int x = 0; x < displayCount; x++) clearBuffer(x);
}

void DotMatrixControl::clearBuffer(int index) {
  if ((index < 0) || (index >= displayCount)) return;

  for (int i = 0; i < 8; i++) buffer[index * 8 + i] = 0;
}

//(0, 0) is right top corner
void DotMatrixControl::setDot(int x, int y, bool value) {
  int index = x / 8;
  int column = 7 - (x % 8);
  int row = y;

  setDot(index, row, column, value);
}

void DotMatrixControl::setDot(int index, int row, int column, bool value) {
  if ((index < 0) || (index >= displayCount)) return;
  if ((row < 0) || (row > 7)) return;
  if ((column < 0) || (column > 7)) return;

  int offset = index * 8;
  byte val = 1 <<  (7 - column);

  if (value) {
    buffer[offset + row] = buffer[offset + row] | val;
  } else {
    val = ~val;
    buffer[offset + row] = buffer[offset + row] & val;
  }
}

void DotMatrixControl::transferAllDots() {
  for (int i = 0; i < 8; i++) {//each line
    digitalWrite(CS_PIN, LOW);

    for (int x = 0; x < displayCount; x++) {//each device
      int offset = ((displayCount - 1) - x) * 8;
      shiftOut(DATA_PIN, CLK_PIN, MSBFIRST, REG_DIGIT_0 + i);
      shiftOut(DATA_PIN, CLK_PIN, MSBFIRST, buffer[offset + i]);
    }

    digitalWrite(CS_PIN, HIGH);
  }
}

void DotMatrixControl::shiftLeft() {
  for (int i = 0; i < 8; i++) {//each line
    for (int x = 0; x < displayCount; x++) {//each device
      int offset = ((displayCount - 1) - x) * 8;
      int mostBit = buffer[offset + i] >> 7;
      buffer[offset + i] <<= 1;

      if (x > 0) {
        int offsetLeftDevice = ((displayCount - 1) - (x - 1)) * 8;
        buffer[offsetLeftDevice + i] += mostBit;
      }
    }
  }
}

void DotMatrixControl::send(int index, byte reg, byte value) {
  int dataSize = displayCount * 2;
  byte sendData[MAX_DISPLAY_COUNT * 2];

  memset(sendData, 0, sizeof(sendData));

  sendData[index * 2] = reg;
  sendData[index * 2 + 1] = value;

  digitalWrite(CS_PIN, LOW);

  for (int i = 0; i < displayCount; i++) {
    shiftOut(DATA_PIN, CLK_PIN, MSBFIRST, sendData[i * 2]);     //reg
    shiftOut(DATA_PIN, CLK_PIN, MSBFIRST, sendData[i * 2 + 1]); //value
  }

  digitalWrite(CS_PIN, HIGH);
}

DotMatrixControl.h

C/C++
Place this code in the same directory of Arduino ino file.
#ifndef DotMatrixControl_h
#define DotMatrixControl_h

#include <avr/pgmspace.h>
#include <Arduino.h>


#define MAX_DISPLAY_COUNT 4

#define REG_NO_OP        0
#define REG_DIGIT_0      1
#define REG_DECODE_MODE  9
#define REG_INTENSITY   10
#define REG_SCAN_LIMIT  11
#define REG_SHUTDOWN    12
#define REG_DISPLAY_TEST 15


class DotMatrixControl {
  private:
    byte buffer[MAX_DISPLAY_COUNT * 8];

    int DATA_PIN;
    int CLK_PIN;
    int CS_PIN;

    int displayCount;

  public:
    DotMatrixControl(int dataPin, int clkPin, int csPin, int _displayCount);
    
    void setup();
    
    void shutdown(int index, bool isShutdown);
    void setIntensity(int intensity);
    void setIntensity(int index, int intensity);

    void clearDisplay();
    void clearDisplay(int index);
    void clearBuffer();
    void clearBuffer(int index);

    void setDot(int x, int y, bool value);
    void setDot(int index, int row, int col, bool value);
    
    void transferAllDots();
    void shiftLeft();

  private:
    void send(int index, byte reg, byte value);
};

#endif	//DotMatrixControl_h

MyMessageBoard.ino

Arduino
#include <aws_iot_mqtt.h>
#include <aws_iot_version.h>
#include "aws_iot_config.h"

#include "MAX7219_Dot_Matrix_font.h"  //https://github.com/nickgammon/MAX7219_Dot_Matrix/tree/master/src
#include "DotMatrixControl.h"

//Dot Matrix
int displayCount = 4;
DotMatrixControl dmc = DotMatrixControl(12, 11, 10, displayCount);
int delayTime = 0;
int animationStatus = 0;
char currentMessage[100] = "";
int sensorPin = A0;
char skillName[] = "My Message Board";

//AWS IoT
aws_iot_mqtt_client iotClient;
bool isIoTClientReady = false;
char messageReceived[100] = "";
int iotYieldTimerDefault = 1000;//ms
int iotYieldTimer = iotYieldTimerDefault;

//loop
int seqNum = 0;


void cmdCallback(char *message, unsigned int mesLen, Message_status_t messageStatus) {
  Serial.println("cmdCallback");
  if (messageStatus != STATUS_NORMAL) {
    Serial.print("AWS IoT cmdCallback messageStatus : ");
    Serial.println(messageStatus);
    return;
  }

  strncpy(messageReceived, message, sizeof(messageReceived));
  Serial.println(message);
}



void setupAwsIot() {

  //AWS IoT
  Serial.println("AWS IoT setup.") ;

  int progress = 0;
  dmc.setDot(++progress, 0, 1); 
  dmc.transferAllDots();


  if ((iotClient.setup(AWS_IOT_CLIENT_ID)) != 0) {
    Serial.println("AWS IoT setup error!") ;
    dmc.setDot(progress, 1, 1); 
    dmc.transferAllDots();
    return;
  }

  Serial.println("AWS IoT setup ok.") ;
  dmc.setDot(++progress, 0, 1); 
  dmc.transferAllDots();

  if (iotClient.config(AWS_IOT_MQTT_HOST, AWS_IOT_MQTT_PORT, AWS_IOT_ROOT_CA_PATH, AWS_IOT_PRIVATE_KEY_PATH, AWS_IOT_CERTIFICATE_PATH) != 0) {
    Serial.println("AWS IoT config error!");
    dmc.setDot(progress, 1, 1); 
    dmc.transferAllDots();
    return;
  }

  Serial.println("AWS IoT config ok.");
  dmc.setDot(++progress, 0, 1); 
  dmc.transferAllDots();

  if (iotClient.connect() != 0) {
    Serial.println("AWS IoT connect error!");
    dmc.setDot(progress, 1, 1); 
    dmc.transferAllDots();
    return;
  }

  Serial.println("AWS IoT connect ok.");
  dmc.setDot(++progress, 0, 1); 
  dmc.transferAllDots();


  isIoTClientReady = true;

  char topicName[] = "cmd";
  Serial.print("AWS IoT subscribing to topic:\"");
  Serial.print(topicName);
  Serial.print("\".\n");

  if (iotClient.subscribe(topicName, 1, cmdCallback) != 0) {
    Serial.println("AWS IoT subscribe error!");
    dmc.setDot(progress, 1, 1); 
    dmc.transferAllDots();
    return;
  }

  Serial.println("AWS IoT subscribe ok.");
  dmc.setDot(++progress, 0, 1); 
  dmc.transferAllDots();
}

void setup() {
  Serial.begin(115200) ;
  Serial.println("setup");

  randomSeed(analogRead(0));


  dmc.setup();
  dmc.setIntensity(0);  //0-15

  Serial.println("DotMatrixControl set up ok.");

  dmc.setDot(0, 0, 1);
  dmc.transferAllDots();

  currentMessage[0] = 0;
  messageReceived[0] = 0;
  sprintf(currentMessage, "No message. You can set a message by saying \"Alexa, open %s.\"", skillName);

  setupAwsIot();
}


void transposeCharData(const char *charDataSrc, char *charDataTrg) {
  for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 8; j++) {
      charDataTrg[7 - j] +=  ((charDataSrc[i] >> (7 - j)) & 0x01) << i;
    }
  }
}

void alignLeft(const char *charDataSrc, char *charDataTrg) {
  int pos = 0;

  for (int i = 0; i < 8; i++) {
    if (charDataSrc[i] == 0) continue;

    charDataTrg[pos++] = charDataSrc[i];
  }

  while (pos < 8) charDataTrg[pos++] = 0;
}

int getFontWidth(const char *charDataSrc) {
  for (int i = 0; i < 8; i++) {
    if (charDataSrc[7 - i] != 0) return 7 - i + 1;
  }

  return 0;
}


void loopScrollMessage() {
  int charCount = strlen(currentMessage);

  if (animationStatus == 0) dmc.clearBuffer();

  if (charCount == 0)return;

  if (animationStatus < (8 * charCount)) {
    int charIndex = animationStatus / 8;
    char c = currentMessage[charIndex];
    const byte *charDataSrc = MAX7219_Dot_Matrix_font[c];
    byte bufChars1[] = {0, 0, 0, 0, 0, 0, 0, 0};
    byte bufChars2[] = {0, 0, 0, 0, 0, 0, 0, 0};
    byte bufChars[] = {0, 0, 0, 0, 0, 0, 0, 0};
    int currentCharWidth = 0;

    //copy char data
    for (int i = 0; i < 8; i++) {
      bufChars2[i] = pgm_read_byte(charDataSrc + i);
    }

    alignLeft(bufChars2, bufChars1);  //remove white space of left side
    currentCharWidth = getFontWidth(bufChars1);
    transposeCharData(bufChars1, bufChars);//tranpose


    for (int y = 0; y < 8; y++) {
      int dot = (bufChars[y] >> (animationStatus % 8))  & 0x01;
      dmc.setDot(0, y, dot);
    }

    int width = currentCharWidth;

    if (width < 7) {//skip left side space
      if (width == 0) {
        //space
        if ((animationStatus % 8) == 3) animationStatus += 4;
      } else {
        if ((animationStatus % 8) == width) animationStatus += (8 - width) - 1;
      }
    }
  }

  dmc.transferAllDots();
  dmc.shiftLeft();

  animationStatus++;

  if (animationStatus > (8 * displayCount + (8 * charCount))) {
    animationStatus = 0;  //loop back to start
  }
}


void checkMessageReceive() {
  if (messageReceived[0] == 0) return;

  Serial.println("message received");

  //start new message scroll
  strncpy(currentMessage, messageReceived, sizeof(currentMessage));
  messageReceived[0] = 0;
  animationStatus = 0;
}


void loop() {
  checkMessageReceive();

  if (--iotYieldTimer < 0) {
    if (iotClient.yield() != 0) {
      Serial.println("Yield failed!");
    }

    iotYieldTimer = iotYieldTimerDefault;
  }

  int sensorValue = analogRead(sensorPin);
  int delayValue = 0;

  if (sensorValue < 100) {
    delayValue = sensorValue / 20;
  } else  if (sensorValue < 300) {
    delayValue = 5 + (sensorValue - 100) / 10;
  } else if (sensorValue < 800) {
    delayValue = 5 + 29 + (sensorValue - 300);
  } else {
    //no scroll
    return;
  }

  loopScrollMessage();

  delay(delayValue);

  iotYieldTimer -= delayValue;
}

AWS Lambda index.js

JavaScript
Create AWS Lambda Node.js function and paste and save this code.
'use strict';

/**
 * This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit.
 * The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as
 * testing instructions are located at http://amzn.to/1LzFrj6
 *
 * For additional samples, visit the Alexa Skills Kit Getting Started guide at
 * http://amzn.to/1LGWsLG
 */

var aws = require('aws-sdk');
var endpoint = 'XXXXXXXXX.iot.us-east-1.amazonaws.com'; //your endpoint
var iotdata = new aws.IotData( { endpoint: endpoint } );




// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `${title}`,
            content: `${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}


// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    const sessionAttributes = {};
    const cardTitle = 'Welcome';
    const speechOutput = 'Welcome to My Message Board. ' +
        'What message do you want to set?';
        
    // If the user either does not reply to the welcome message or says something that is not
    // understood, they will be prompted again with this text.
    const repromptText = 'What message do you want to set?';
    const shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = '';//'Session Ended';
    const speechOutput = 'See you!';
    // Setting this to true ends the session and exits the skill.
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

function createFavoriteColorAttributes(favoriteColor) {
    return {
        favoriteColor,
    };
}

/**
 * Sets the color in the session and prepares the speech to reply to the user.
 */
function setMessage(intent, session, callback) {
    const cardTitle = 'New Massage';//intent.name;
    const message = intent.slots.message;
    let repromptText = '';
    let sessionAttributes = {};
    let  shouldEndSession = false;
    let speechOutput = '';

    if (message) {
        const messageValue = message.value;
        sessionAttributes = createFavoriteColorAttributes(messageValue);
        speechOutput = `This message is set: ${messageValue}.`;
        repromptText = null;
        
            
            
    
        var params = {
          topic: 'cmd', /* required */
          payload: messageValue,
          qos: 1
        };
        
        iotdata.publish(params, function(err, data) {
          if (err) {
              console.log(err, err.stack); // an error occurred
          }
          else{     
              console.log(data);           // successful response
          }
          
          shouldEndSession = true;
            callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
             
        });        
        
        
    } else {
        speechOutput = "What message do you want to set?";
        repromptText = "What message do you want to set?";

            callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }
}

// --------------- Events -----------------------

/**
 * Called when the session starts.
 */
function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

/**
 * Called when the user launches the skill without specifying what they want.
 */
function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if (intentName === 'AnyMessage') {
        setMessage(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else {
        throw new Error('Invalid intent');
    }
}

/**
 * Called when the user ends the session.
 * Is not called when the skill returns shouldEndSession=true.
 */
function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
    // Add cleanup logic here
}


// --------------- Main handler -----------------------

// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        /**
         * Uncomment this if statement and populate with your skill's application ID to
         * prevent someone else from configuring a skill that sends requests to this function.
         */
        /*
        if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') {
             callback('Invalid Application ID');
        }
        */

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }
};

MyMessageBoard_IM.json

JSON
Alexa Skill Kit Skill Builder Code(Interaction Model)
{
    "languageModel": {
        "types": [
                  {
                  "name": "MESSAGE",
                  "values": [
                             {
                             "id": null,
                             "name": {
                             "value": "hello",
                             "synonyms": []
                             }
                             },
                             {
                             "id": null,
                             "name": {
                             "value": "good night",
                             "synonyms": []
                             }
                             },
                             {
                             "id": null,
                             "name": {
                             "value": "Thank you",
                             "synonyms": []
                             }
                             },
                             {
                             "id": null,
                             "name": {
                             "value": "I'll be home at 5pm.",
                             "synonyms": []
                             }
                             }
                             ]
                  }
                  ],
        "intents": [
                    {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                    },
                    {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                    },
                    {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                    },
                    {
                    "name": "AnyMessage",
                    "samples": [
                                "{message}"
                                ],
                    "slots": [
                              {
                              "name": "message",
                              "type": "MESSAGE"
                              }
                              ]
                    }
                    ],
        "invocationName": "my message board"
    }
}

Credits

Akihiko Suyama

Akihiko Suyama

1 project • 3 followers
Thanks to Nick Gammon, Eberhard Fahle, and AWS.

Comments