Cameron Coward
Published © CC BY-NC-SA

LuvNoots

A wall-mounted display that shows text messages, so you can leave notes for your loved ones.

AdvancedFull instructions provided3 hours2,535
LuvNoots

Things used in this project

Hardware components

ESP32 Dev Board
×1
Waveshare 6" ePaper Screen
×1
Button
×1

Software apps and online services

Arduino IDE
Arduino IDE
Home Assistant
Home Assistant
Google Voice
Zapier

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Frame Front

No supports

Frame Back

Use supports

Code

Arduino Code

Arduino
The code for the ESP32, requires the ArduinoHA and GxEPD2 libraries and Adafruit FreeSans24pt7b font.
#include <WiFi.h>
#include <ArduinoHA.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeSans24pt7b.h>

#define BUTTON 22                                       // pin for the button
#define LED_PIN 2                                       // this is the pin for the built-in (blue) LED on this particular dev board
#define MQTT_SENSOR_DATA_LENGTH_MAX 1600                // the maximum number of characters we will receive in a payload, plus one for null terminator

// These are needed to configure the e-ink display:
#define ENABLE_GxEPD2_GFX 0
#define MAX_DISPLAY_BUFFER_SIZE 800 // 
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
GxEPD2_BW<GxEPD2_it60, MAX_HEIGHT(GxEPD2_it60)> display(GxEPD2_it60(/*CS=77*/ 5, /*DC=*/ 17, /*RST=*/ 26, /*BUSY=*/ 36));

int16_t  posX, posY;                                    // for calculating text bounds
uint16_t x, y, w, h;                                    // x and y are coordinates, w and h are for calculating text bounds

bool newMessage = false;                                // to control blue notification LED
unsigned long timeReceived = 0;                         // to turn off notification after a while
const unsigned long expiration = 43200000;              // number of milliseconds to wait before turning off notification LED

WiFiClient client;                                      // for WiFi
HADevice device;                                        // makes a Home Assistant device
HAMqtt mqtt(client, device);                            // create MQTT thing
byte mac[] = {0x00, 0x12, 0xFB, 0x6E, 0x3E, 0x2C};      // make up a MAC address for networking. Needs to be unique on the network.

String wifiSSID = "";                                   // WiFi network SSID
String wifiPass = "";                                   // WiFi password

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  Serial.begin(115200);                                 // initialize serial
  while(!Serial);                                       // wait for serial to begin
  Serial.println("Trying to start...");
  WiFi.macAddress(mac);                                 // turn on WiFi adapter with our custom MAC address
  device.setUniqueId(mac, sizeof(mac));                 // give this Home Assistant device the same MAC address

  WiFi.begin(wifiSSID, wifiPass);                       // connect to WiFi using your credentials
  while (WiFi.status() != WL_CONNECTED) {               // don't proceed unless we're connected
      delay(500);                                       // waiting for the connection
  }
  device.setName("LuvNoots");                           // give a name and software version to Home Assistant for this device
  device.setSoftwareVersion("1.0.0");
  mqtt.onMessage(onMqttMessage);                        // call the onMessage function when MQTT message received event occurs
  mqtt.onConnected(onMqttConnected);                    // call the onConnected function on successful connection with MQTT broker

  mqtt.begin("[ip]", "[user]", "[password]");           // initialize MQTT with your credentials

  display.init();                                       // start the display and set cursor coordinates
  x = 325;
  y = 300;
  display.setFont(&FreeSans24pt7b);                     // selects the font
  display.setRotation(0);                               // landscape orientation
  display.setTextColor(GxEPD_BLACK);                    // text color
  display.setFullWindow();                              // refresh the entire screen (instead of partial)
  display.firstPage();                                  // this library uses a paging display buffer system
  do {
    display.fillScreen(GxEPD_WHITE);                    // background color
    display.setCursor(x, y);
    display.print("LuvNoots");
  } while (display.nextPage());
}

void loop() {
  mqtt.loop();                                          // check for MQTT messages
  notificationLED();                                    // see if notification LED should be on
}

void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
  // This callback is called when message from MQTT broker is received.

  String topicStr = topic;                              // not necessary to convert, but makes life easier

  Serial.print(topicStr);
  Serial.print(": ");
  
  char payloadData[MQTT_SENSOR_DATA_LENGTH_MAX];        // gets rid of "noise" characters at the end of actual payload
  strncpy(payloadData, (const char *)payload, length);  // copies just the chars we want
  payloadData[length] = '\0';                           // terminate the payloadData as strncpy does not.

  Serial.println(payloadData);
  Serial.println(length);

  if (topicStr == "LuvNoots"){                          // topic name determines what we do with the payload
    printMessage(payloadData, length);
  } else if (topicStr == "DailyWeather"){
    printWeather(payloadData, length);
  }
}

void onMqttConnected() {                               
  // runs when first connected to broker
  
  Serial.println("Connected to the broker!");

  // subscribe to each topic
  mqtt.subscribe("LuvNoots");
  mqtt.subscribe("DailyWeather");
}

void printMessage(char* messageIn, int length) {
  newMessage = true;                                                            // turns on notification LED
  timeReceived = millis();                                                      // saves time LED turned on

  display.setPartialWindow(0, 0, 800, 400);                                     // we don't want to alter the black area where the weather shows
  display.setFont(&FreeSans24pt7b);
  display.setTextColor(GxEPD_BLACK);
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);                                            // set the background to white (fill the buffer with value for white)
  } while (display.nextPage());
  
  String printString = "";                                                      // blank string that we'll print to screen for each line
  int printStringEnd = 0;                                                       // the character position where each line ends
  String testString = "";                                                       // blank string we use to check if a new line will fit on screen
  int line = 1;                                                                 // tracks the current line number

  for (int i = 0; i < length; i++) {                                            // loop through every single received character
    if (messageIn[i] == '\n'){messageIn[i] = ' ';}                              // get rid of new line characters and replace with spaces

    if (messageIn[i] == ' ') {                                                  // look for a space, signifiying the end of a word
      display.getTextBounds(testString, 0, 300, &posX, &posY, &w, &h);          // get the width of the string WITH the new word

      if (w > 750) {                                                            // check if that width exceeds the screen (minus margins)

        i = printStringEnd;                                                     // to revert back to the i position BEFORE we added the word that didn't fit
        testString = "";                                                        // blank, because we're moving to the next line
        display.getTextBounds(printString, 0, 300, &posX, &posY, &w, &h);       // if we finish the for loop and line is still 0, that means the entire message fits on
        x = (800 - w) / 2;                                                      // to center that string on the screen
        y = line * 45;                                                          // sets the y coordinate by line                                                                                
        display.getTextBounds(printString, x, y, &posX, &posY, &w, &h);         // if we finish the for loop and line is still 0, that means the entire message fits on
        display.setPartialWindow(posX, posY, w, h);                             // partial window so we only print to that line of the screen                 
        display.firstPage();     
        do {
          display.setCursor(x, y);                                              // set cursor position
          display.print(printString);                                           // print the string WITHOUT the new word that wouldn't fit
        } while (display.nextPage());
        line++;                                                                 // move to the next line

      } else {
        printString = testString;                                               // because the new word fit on the line, it is now "safe" to add to the string to print later
        printStringEnd = i;                                                     // to "save" the i position where we know a "safe" line ends
        testString = testString + messageIn[i];                                 // because the line fit, we can add the space
      }

    } else {
      testString = testString + messageIn[i];                                   // add the next character (that isn't a space)
    }

  }

  display.getTextBounds(testString, 0, 300, &posX, &posY, &w, &h);              // find bounds of final line
  x = (800 - w) / 2;                                                            // to center that string on the screen
  y = line * 45;                                                                                                                               
  display.getTextBounds(testString, x, y, &posX, &posY, &w, &h);                // find bounds again in the actual position, so we can refresh just that
  display.setPartialWindow(posX, posY, w, h);                                                
  display.firstPage();                                                    
  do {
    display.setCursor(x, y);                                              
    display.print(testString);                                           
  } while (display.nextPage());
  Serial.println("Finished loop, should have displayed message.");
}

void printWeather(char* weatherData, int length){                               // prints the weather data at the bottom
  int commaPositions[3];                                                        // tracks the position of each comma in the string
  int commaTracker = 0;                                                         // the current comma
  String condition;                                                             // saves the condition (sunny, cloudy, rainy, etc.)
  String temp;                                                                  // saves the temp
  String humidity;                                                              // saves the humidity 
  String windSpeed;                                                             // saves the wind speed
  for (int i = 0; i < length; i++){                                             // iterates through the entire payload, saving positions of commas
    if (weatherData[i] == ','){
      commaPositions[commaTracker] = i;
      commaTracker++;
    }
  }

  for (int i = 2; i < (commaPositions[0] - 1); i++){                            // pulls the condition, based on comma position
    condition = condition + weatherData[i];
  }

  for (int i = (commaPositions[0] + 2); i < commaPositions[1]; i++){            // pulls the temp, based on comma position
    temp = temp + weatherData[i];
  }

  for (int i = (commaPositions[1] + 2); i < commaPositions[2]; i++){            // pulls the humidity, based on comma position
    humidity = humidity + weatherData[i];
  }

  for (int i = (commaPositions[2] + 2); i < (length - 1); i++){                 // pulls the wind speed, based on comma position
    windSpeed = windSpeed + weatherData[i];
  }

  temp = temp + "F";                                                           // formats temp
  humidity = humidity + "% humidity";                                           // formats humdidity
  windSpeed = windSpeed + "mph winds";                                          // formats wind speed

  String lineOne = condition + " and " + temp;                                  // assembles line one
  String lineTwo = humidity + " with " + windSpeed;                             // assembles line two

  Serial.println(condition);
  Serial.println(temp);
  Serial.println(humidity);
  Serial.println(windSpeed);

  display.setPartialWindow(0, 401, 800, 600);                               // just the bottom part of the screen
  display.setFont(&FreeSans24pt7b);
  display.setTextColor(GxEPD_WHITE);
  display.firstPage();
  do {
    display.fillScreen(GxEPD_BLACK);                                        // set the background to black
  } while (display.nextPage());

  display.getTextBounds(lineOne, 0, 300, &posX, &posY, &w, &h);             // get size of text area (line 1)
  x = (800 - w) / 2;                                                        // to center that string on the screen
  y = 475;                                                                                                                               
  display.getTextBounds(lineOne, x, y, &posX, &posY, &w, &h);               // gets actual text bounds (line 1)
  display.setPartialWindow(posX, posY, w, h);                                                
  display.firstPage();                                                    
  do {
    display.fillScreen(GxEPD_BLACK);
    display.setCursor(x, y);                                              
    display.print(lineOne);                                           
  } while (display.nextPage());

  display.getTextBounds(lineTwo, 0, 300, &posX, &posY, &w, &h);             // get size of text area (line 2)
  x = (800 - w) / 2;                                                        // to center that string on the screen
  y = 550;                                                                                                                               
  display.getTextBounds(lineTwo, x, y, &posX, &posY, &w, &h);               // gets actual text bounds (line 2)
  display.setPartialWindow(posX, posY, w, h);                                                
  display.firstPage();                                                    
  do {
    display.fillScreen(GxEPD_BLACK);
    display.setCursor(x, y);                                              
    display.print(lineTwo);                                           
  } while (display.nextPage());
}

void notificationLED(){                                                     // checks if the blue LED should be on
  if (newMessage == true) {
    digitalWrite(LED_PIN, HIGH);
  } else {
    digitalWrite(LED_PIN, LOW);
  }

  if ((millis() - timeReceived) > expiration) {                             // turns off LED after set interval
    newMessage = false;
  }

  if (digitalRead(BUTTON) == LOW) {                                         // turns off LED if button pressed
    Serial.println("Notification cleared");
    newMessage = false;
  }
}

Credits

Cameron Coward

Cameron Coward

16 projects • 1338 followers
Writer for Hackster News. Proud husband and dog dad. Maker and serial hobbyist. Check out my YouTube channel: Serial Hobbyism

Comments