From 34bd9746d17acebea285b0eb574b3eb2e9da5896 Mon Sep 17 00:00:00 2001 From: Bernd Stellwag Date: Tue, 10 Sep 2024 20:37:41 +0200 Subject: [PATCH 1/6] add MQTT and OTA update support --- platformio.ini | 7 +++--- src/main.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/platformio.ini b/platformio.ini index 03517ed..44248c5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,10 +13,11 @@ platform = espressif8266 framework = arduino monitor_speed = 115200 upload_speed = 500000 +upload_protocol = espota +upload_port = led-marquee-sign.fablab.local # Serial1 (sign data) maps to D4 by default [env:d1_mini] board = d1_mini - -[env:nodemcuv2] -board = nodemcuv2 +lib_deps = adafruit/Adafruit MQTT Library@^2.5.8 +lib_ignore = WiFi101 diff --git a/src/main.cpp b/src/main.cpp index f367f2c..70f2593 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,11 @@ +#include #include #include #include +#include +#include +#include "Adafruit_MQTT.h" +#include "Adafruit_MQTT_Client.h" #include "credentials.h" #include "webserver.h" @@ -10,6 +15,16 @@ static ESP8266WiFiMulti wifiMulti; static SocketServer socketServer; +#define AIO_SERVER credentials::mqtt_hostname.c_str() +#define AIO_SERVERPORT credentials::mqtt_port +#define AIO_USERNAME credentials::mqtt_username.c_str() +#define AIO_KEY credentials::mqtt_password.c_str() +WiFiClient client; +Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY); +Adafruit_MQTT_Subscribe mqtt_subscribe_topic = Adafruit_MQTT_Subscribe(&mqtt, "homeassistant/marquee_sign/text"); + +void MQTT_connect(); + void setup() { // serial console, for use via USB (also exposed to TXD0/RXD0 GPIOs) Serial.begin(115200); @@ -20,7 +35,10 @@ void setup() { Serial1.begin(9600); // configure WiFi and connect to the network + WiFi.mode(WIFI_STA); + WiFi.hostname(credentials::mdnsHostname); wifiMulti.addAP(credentials::ssid.c_str(), credentials::psk.c_str()); + Serial.println("Connecting to Wi-Fi network..."); while (wifiMulti.run() != WL_CONNECTED) { delay(1000); @@ -32,16 +50,13 @@ void setup() { Serial.print(", IP address: "); Serial.println(WiFi.localIP()); + ArduinoOTA.begin(); + + mqtt.subscribe(&mqtt_subscribe_topic); + // might be handy to have the IP written on the sign sendTextToSign(WiFi.localIP().toString()); - // publish this service using mDNS so that it's easy to find it in the LAN - if (MDNS.begin(credentials::mdnsHostname)) { - Serial.println("mDNS started up successfully"); - } else { - Serial.println("Error: failed to start mDNS"); - } - // configure HTTP endpoints configureWebServer(); @@ -53,4 +68,40 @@ void setup() { void loop() { webServer.handleClient(); socketServer.handleClient(); + + ArduinoOTA.handle(); + + MQTT_connect(); + + Adafruit_MQTT_Subscribe *subscription; + while ((subscription = mqtt.readSubscription(5000))) { + const char* signText = (char *)mqtt_subscribe_topic.lastread; + + sendTextToSign(signText); + } +} + +void MQTT_connect() { + int8_t ret; + + // Stop if already connected. + if (mqtt.connected()) { + return; + } + + uint8_t retries = 6; + while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected + mqtt.disconnect(); + + // wait 10s till next try + for (int i = 0; i < 10000; i += 5) { + ArduinoOTA.handle(); + delay(5); + } + + retries--; + if (retries == 0) { + ESP.restart(); + } + } } -- 2.45.2 From c78fde5d9f216d065254466fdd131e4777ce611e Mon Sep 17 00:00:00 2001 From: Bernd Stellwag Date: Tue, 10 Sep 2024 21:04:28 +0200 Subject: [PATCH 2/6] update credentials.h example file --- src/credentials.h.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/credentials.h.example b/src/credentials.h.example index dd11ebf..ef764c5 100644 --- a/src/credentials.h.example +++ b/src/credentials.h.example @@ -5,4 +5,9 @@ namespace credentials { const String psk = "my PSK"; const String mdnsHostname = "led-marquee-sign"; + + const String mqtt_hostname = "mqtt-broker"; + const uint16_t mqtt_port = 1883; + const String mqtt_username = "user"; + const String mqtt_password = "pass"; } -- 2.45.2 From 610f86750b6341865e1c996e727cdde930d86b45 Mon Sep 17 00:00:00 2001 From: Bernd Stellwag Date: Wed, 2 Oct 2024 17:50:53 +0200 Subject: [PATCH 3/6] before sending the text to the sign, translate any unicode characters to codepage 437 representation --- src/util.h | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/util.h b/src/util.h index 586d0ec..32ab2a1 100644 --- a/src/util.h +++ b/src/util.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + // TODO: reverse engineer builtin and custom bitmaps // seems like our sign listens to messages on this address @@ -246,6 +250,78 @@ enum class ExtendedChar { GreekSmallLetterPhi = 0xed, }; +std::unordered_map createUnicodeToCP437Map() { + std::unordered_map cp437_map; + + cp437_map[0x00E4] = 0x84; // ä in UTF-16 + cp437_map[0xC3A4] = 0x84; // ä in UTF-8 + cp437_map[0x00F6] = 0x94; // ö in UTF-16 + cp437_map[0xC3B6] = 0x94; // ö in UTF-8 + cp437_map[0x00FC] = 0x81; // ü in UTF-16 + cp437_map[0xC3BC] = 0x81; // ü in UTF-8 + cp437_map[0x00C4] = 0x8E; // Ä in UTF-16 + cp437_map[0xC384] = 0x8E; // Ä in UTF-8 + cp437_map[0x00D6] = 0x99; // Ö in UTF-16 + cp437_map[0xC396] = 0x99; // Ö in UTF-8 + cp437_map[0x00DC] = 0x9A; // Ü in UTF-16 + cp437_map[0xC39C] = 0x9A; // Ü in UTF-8 + cp437_map[0x00DF] = 0xE1; // ß in UTF-16 + cp437_map[0xC39F] = 0xE1; // ß in UTF-8 + cp437_map[0x20AC] = 0xB1; // € in UTF-16 + cp437_map[0xE282AC] = 0xB1; // € in UTF-8 (3 Bytes) + cp437_map[0x00B5] = 0xE6; // µ in UTF-16 + cp437_map[0xC2B5] = 0xE6; // µ in UTF-8 + + return cp437_map; +} + +String convertToCP437(const String& input) { + std::unordered_map cp437_map = createUnicodeToCP437Map(); + String output = ""; + size_t len = input.length(); + + for (size_t i = 0; i < len; i++) { + char ch = input[i]; + + // UTF-8 detection: check for 3 byte UTF-8 + if ((ch & 0xF0) == 0xE0) { + if (i + 2 < len) { + uint32_t utf8_char = ((uint8_t)input[i] << 16) | ((uint8_t)input[i + 1] << 8) | (uint8_t)input[i + 2]; + i += 2; // skip 2 additional bytes + if (cp437_map.find(utf8_char) != cp437_map.end()) { + output += static_cast(cp437_map[utf8_char]); + continue; + } + } + } + // UTF-8 detection: check for 2 byte UTF-8 + else if ((ch & 0xE0) == 0xC0) { + if (i + 1 < len) { + uint16_t utf8_char = ((uint8_t)input[i] << 8) | (uint8_t)input[i + 1]; + i++; // skip second byte + if (cp437_map.find(utf8_char) != cp437_map.end()) { + output += static_cast(cp437_map[utf8_char]); + continue; + } + } + } + // single UTF-16 character (or plain ANSI character) + else if ((uint8_t)ch < 0x80) { // keep ASCII as is + output += ch; + } + else { + uint16_t utf16_char = (uint8_t)ch; // UTF-16 range + if (cp437_map.find(utf16_char) != cp437_map.end()) { + output += static_cast(cp437_map[utf16_char]); + } else { + output += '?'; + } + } + } + + return output; +} + String hexDump(String in) { String out; for (size_t i = 0; i < in.length(); ++i) { @@ -287,7 +363,6 @@ String hexDump(String in) { * - stay time: 4 * - brightness: bright */ - void sendTextToSign( const String &text, const Method method = Method::Cyclic, @@ -307,7 +382,9 @@ void sendTextToSign( Serial.println("Sending text: \"" + text + "\""); - String toSend = "~" + String(SIGN_ADDRESS, DEC) + "~f01" + (char) method + "\\" + (char) brightness + "\\Y" + String(speed, DEC) + "\\Z" + String(stayTime, DEC) + "\\" + (char) fontType + text + "\r\r\r"; + String translatedText = convertToCP437(text); + + String toSend = "~" + String(SIGN_ADDRESS, DEC) + "~f01" + (char) method + "\\" + (char) brightness + "\\Y" + String(speed, DEC) + "\\Z" + String(stayTime, DEC) + "\\" + (char) fontType + translatedText + "\r\r\r"; Serial1.print(toSend); -- 2.45.2 From ad4ee16f195b0da001a0acb5284d472f4d229467 Mon Sep 17 00:00:00 2001 From: Bernd Stellwag Date: Wed, 2 Oct 2024 17:53:47 +0200 Subject: [PATCH 4/6] use mdns hostname instead of fqdn --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 44248c5..9fff67a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,7 +14,7 @@ framework = arduino monitor_speed = 115200 upload_speed = 500000 upload_protocol = espota -upload_port = led-marquee-sign.fablab.local +upload_port = led-marquee-sign # Serial1 (sign data) maps to D4 by default [env:d1_mini] -- 2.45.2 From 165540c1205e4330d4b241968aca07fb536fe5ce Mon Sep 17 00:00:00 2001 From: Bernd Stellwag Date: Wed, 2 Oct 2024 17:55:52 +0200 Subject: [PATCH 5/6] reformat comments --- src/main.cpp | 4 +++- src/util.h | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 70f2593..fbed474 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,7 @@ static SocketServer socketServer; #define AIO_SERVERPORT credentials::mqtt_port #define AIO_USERNAME credentials::mqtt_username.c_str() #define AIO_KEY credentials::mqtt_password.c_str() + WiFiClient client; Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY); Adafruit_MQTT_Subscribe mqtt_subscribe_topic = Adafruit_MQTT_Subscribe(&mqtt, "homeassistant/marquee_sign/text"); @@ -90,7 +91,8 @@ void MQTT_connect() { } uint8_t retries = 6; - while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected + // connect will return 0 for connected + while ((ret = mqtt.connect()) != 0) { mqtt.disconnect(); // wait 10s till next try diff --git a/src/util.h b/src/util.h index 32ab2a1..2fefc99 100644 --- a/src/util.h +++ b/src/util.h @@ -287,7 +287,8 @@ String convertToCP437(const String& input) { if ((ch & 0xF0) == 0xE0) { if (i + 2 < len) { uint32_t utf8_char = ((uint8_t)input[i] << 16) | ((uint8_t)input[i + 1] << 8) | (uint8_t)input[i + 2]; - i += 2; // skip 2 additional bytes + // skip 2 additional bytes + i += 2; if (cp437_map.find(utf8_char) != cp437_map.end()) { output += static_cast(cp437_map[utf8_char]); continue; @@ -298,7 +299,8 @@ String convertToCP437(const String& input) { else if ((ch & 0xE0) == 0xC0) { if (i + 1 < len) { uint16_t utf8_char = ((uint8_t)input[i] << 8) | (uint8_t)input[i + 1]; - i++; // skip second byte + // skip second byte + i++; if (cp437_map.find(utf8_char) != cp437_map.end()) { output += static_cast(cp437_map[utf8_char]); continue; @@ -306,11 +308,13 @@ String convertToCP437(const String& input) { } } // single UTF-16 character (or plain ANSI character) - else if ((uint8_t)ch < 0x80) { // keep ASCII as is + else if ((uint8_t)ch < 0x80) { + // keep ASCII as is output += ch; } else { - uint16_t utf16_char = (uint8_t)ch; // UTF-16 range + uint16_t utf16_char = (uint8_t)ch; + // check if character is in UTF-16 range, fallback to ? if (cp437_map.find(utf16_char) != cp437_map.end()) { output += static_cast(cp437_map[utf16_char]); } else { -- 2.45.2 From 60a82b896a37346d66b577c7a62f2c5a44481f6e Mon Sep 17 00:00:00 2001 From: Bernd Stellwag Date: Tue, 22 Oct 2024 19:28:39 +0200 Subject: [PATCH 6/6] add status topic and publish after sending text to sign --- src/main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index fbed474..6eeeb30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,8 @@ static SocketServer socketServer; WiFiClient client; Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY); -Adafruit_MQTT_Subscribe mqtt_subscribe_topic = Adafruit_MQTT_Subscribe(&mqtt, "homeassistant/marquee_sign/text"); +Adafruit_MQTT_Subscribe mqtt_subscribe_topic = Adafruit_MQTT_Subscribe(&mqtt, "homeassistant/marquee_sign/text/set"); +Adafruit_MQTT_Publish mqtt_publish_topic = Adafruit_MQTT_Publish(&mqtt, "homeassistant/marquee_sign/text/status"); void MQTT_connect(); @@ -79,6 +80,8 @@ void loop() { const char* signText = (char *)mqtt_subscribe_topic.lastread; sendTextToSign(signText); + + mqtt_publish_topic.publish(signText); } } -- 2.45.2