add MQTT and OTA update support #1
@@ -13,10 +13,11 @@ platform = espressif8266
 | 
			
		||||
framework = arduino
 | 
			
		||||
monitor_speed = 115200
 | 
			
		||||
upload_speed = 500000
 | 
			
		||||
upload_protocol = espota
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 | 
			||||
upload_port = led-marquee-sign
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 
				
					
						fmueller
						commented  
			
		Would it be sufficient just to target the mDNS hostname that by default should be  Would it be sufficient just to target the mDNS hostname that by default should be `led-marquee-sign.local`? 
			
			
		
				
					
						burned42
						commented  
			
		IIRC that didn't work when we tried that. IIRC that didn't work when we tried that. 
			
			
		
				
					
						burned42
						commented  
			
		Works when using just  Works when using just `led-marquee-sign` :) 
			
			
		
				
					
						fmueller
						commented  
			
		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
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						@@ -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
						
					
				 
				
				
					
						fmueller
						commented  
			
		Please add a blank line above. Please add a blank line above. 
			
			
		
				
					
						burned42
						commented  
			
		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;
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 
				
					
						fmueller
						commented  
			
		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. 
			
			
		
				
					
						burned42
						commented  
			
		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. 
			
			
		
				
					
						fmueller
						commented  
			
		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
						
					
				 
				
				
					
						fmueller
						commented  
			
		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. 
			
			
		
				
					
						burned42
						commented  
			
		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
					
				 
				
				
					
						fmueller
						commented  
			
		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? 
			
			
		
				
					
						burned42
						commented  
			
		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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								src/util.h
									
									
									
									
									
								
							
							
						
						@@ -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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
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 honestly have no idea. I was happy when I got this working with the platform io thingy.