add MQTT and OTA update support #1

Open
burned42 wants to merge 6 commits from add_mqtt into main
4 changed files with 155 additions and 12 deletions

View File

@ -13,10 +13,11 @@ platform = espressif8266
framework = arduino
monitor_speed = 115200
upload_speed = 500000
upload_protocol = espota
Review

I presume with this change, flashing initially with USB is no longer an option? I.e., this needs to be commented? If so, I'd appreciate a short comment. It's been a really long time since I've used ArduinoOTA.

I presume with this change, flashing initially with USB is no longer an option? I.e., this needs to be commented? If so, I'd appreciate a short comment. It's been a really long time since I've used ArduinoOTA.
Review

I honestly have no idea. I was happy when I got this working with the platform io thingy.

I honestly have no idea. I was happy when I got this working with the platform io thingy.
upload_port = led-marquee-sign

Would it be sufficient just to target the mDNS hostname that by default should be led-marquee-sign.local?

Would it be sufficient just to target the mDNS hostname that by default should be `led-marquee-sign.local`?

IIRC that didn't work when we tried that.

IIRC that didn't work when we tried that.

Works when using just led-marquee-sign :)

Works when using just `led-marquee-sign` :)

That is some odd behavior, but then again, if it works... mDNS is pretty standard with these IoT devices nowadays. For example, WLED also advertises a (user-configurable) hostname. Perhaps PlatformIO applies some magic here.

That is some odd behavior, but then again, if it works... mDNS is pretty standard with these IoT devices nowadays. For example, [WLED](https://kno.wled.ge/) also advertises a (user-configurable) hostname. Perhaps PlatformIO applies some magic here.
# 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

View File

@ -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";
}

View File

@ -1,6 +1,11 @@
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266mDNS.h>
#include <ESP8266WebServer.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"
#include "credentials.h"
#include "webserver.h"
@ -10,6 +15,18 @@
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()
fmueller marked this conversation as resolved Outdated

Please add a blank line above.

Please add a blank line above.

done 👍

done 👍
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/set");
Adafruit_MQTT_Publish mqtt_publish_topic = Adafruit_MQTT_Publish(&mqtt, "homeassistant/marquee_sign/text/status");
void MQTT_connect();
void setup() {
// serial console, for use via USB (also exposed to TXD0/RXD0 GPIOs)
Serial.begin(115200);
@ -20,7 +37,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 +52,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 +70,43 @@ 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);
mqtt_publish_topic.publish(signText);
}
}
void MQTT_connect() {
int8_t ret;
// Stop if already connected.
if (mqtt.connected()) {
return;
Review

I'd highly appreciate if you'd avoid the use of inline comments.

I'd highly appreciate if you'd avoid the use of inline comments.
Review

I think I copied the whole function from some code in an example of one of the libraries. Feel free to adjust to your prefered code style.

I think I copied the whole function from some code in an example of one of the libraries. Feel free to adjust to your prefered code style.
Review

Will do. I'll also test the USB flashing locally.

Will do. I'll also test the USB flashing locally.
}
uint8_t retries = 6;
// connect will return 0 for connected
while ((ret = mqtt.connect()) != 0) {
fmueller marked this conversation as resolved Outdated

A chance to flash it while it's in the back-off? I'd appreciate a comment explaining this line.

A chance to flash it while it's in the back-off? I'd appreciate a comment explaining this line.

see above, I copied this from an example from one of the used libraries

see above, I copied this from an example from one of the used libraries
mqtt.disconnect();
// wait 10s till next try
for (int i = 0; i < 10000; i += 5) {
ArduinoOTA.handle();
delay(5);
fmueller marked this conversation as resolved
Review

Why not restart right away? I mean, sure, it's not quite necessary to do so, but it wouldn't hurt a lot either. Is there some rate limit?

Why not restart right away? I mean, sure, it's not quite necessary to do so, but it wouldn't hurt a lot either. Is there some rate limit?
Review

see above, I copied this from an example from one of the used libraries

see above, I copied this from an example from one of the used libraries
}
retries--;
if (retries == 0) {
ESP.restart();
}
}
}

View File

@ -1,5 +1,9 @@
#pragma once
#include <map>
#include <set>
#include <unordered_map>
// TODO: reverse engineer builtin and custom bitmaps
// seems like our sign listens to messages on this address
@ -246,6 +250,82 @@ enum class ExtendedChar {
GreekSmallLetterPhi = 0xed,
};
std::unordered_map<uint16_t, uint8_t> createUnicodeToCP437Map() {
std::unordered_map<uint16_t, uint8_t> 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<uint16_t, uint8_t> 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];
// skip 2 additional bytes
i += 2;
if (cp437_map.find(utf8_char) != cp437_map.end()) {
output += static_cast<char>(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];
// skip second byte
i++;
if (cp437_map.find(utf8_char) != cp437_map.end()) {
output += static_cast<char>(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;
// check if character is in UTF-16 range, fallback to ?
if (cp437_map.find(utf16_char) != cp437_map.end()) {
output += static_cast<char>(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 +367,6 @@ String hexDump(String in) {
* - stay time: 4
* - brightness: bright
*/
void sendTextToSign(
const String &text,
const Method method = Method::Cyclic,
@ -307,7 +386,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);