Last active
June 30, 2026 07:56
-
-
Save MDAR/a314aaca20aad4077954594185b08b97 to your computer and use it in GitHub Desktop.
MQTT IR RX TX MQTT ESP8266
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Using a board like this | |
| https://www.aliexpress.com/item/1005009427293908.html | |
| I asked Claude Ai to create the Ardunio code to turn it into an MQTT device | |
| The basic concept is | |
| At first boot, without any WiFi details set, it starts a WiFi AP | |
| Connect with to it with the password in the PDF user guide. | |
| Set you SSID & MQTT broker details. | |
| Reboot. | |
| The device try the connect and subscribe to a topic formed from its MAC address, with TX & RX | |
| More details in the PDF (download the raw text and save it with .pdf suffix then open it in your normal PDF viewer or a web browser. | |
| Once it connects to your MQTT broker, you can harvest codes from your physical IR remote, then fire them back to the TX topic. | |
| After that, you can do wonders with your own home automatio n or software logic. | |
| If you want to take this project further, upload all files to a Claude AI session and ask it to adapt the code to your needs. | |
| EG | |
| Ask Claude to review all attached files and adapt the Arduino sketch to suit a different platform, like an ESP32 | |
| Likewise you can ask Claude to extend the functions. | |
| If your Arduino / ESP device has buttons, you might want to fire out an IR blaster when a button is pressed | |
| Long press the button to listen for a code | |
| Short press to fire out that code | |
| The options are endless. | |
| Have fun with it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================ | |
| // IR MQTT Bridge — ESP8266MOD | |
| // Board type NodeMCU 1.9 (ESP-12E) @ 115200 baud | |
| // Receives IR signals → publishes to MQTT | |
| // Subscribes to MQTT → transmits IR signals | |
| // | |
| // Topics (MAC = last 6 hex chars of ESP MAC address): | |
| // ir/{MAC}/rx — ESP publishes received IR here | |
| // ir/{MAC}/tx — ESP subscribes, send IR commands here | |
| // ir/{MAC}/status — Online/offline LWT heartbeat | |
| // | |
| // AP Config Portal: | |
| // Triggered after 5 consecutive boot failures | |
| // Connect to SSID: IR-Config-{MAC} Password: irconfig | |
| // Browse to: http://192.168.4.1 | |
| // | |
| // Libraries required (install via Arduino Library Manager): | |
| // - IRremoteESP8266 (by crankyoldgit) | |
| // - PubSubClient (by Nick O'Leary) | |
| // - ArduinoJson (by Benoit Blanchon) v6+ | |
| // LittleFS is built into ESP8266 Arduino core (no install needed) | |
| // ============================================================ | |
| #include <ESP8266WiFi.h> | |
| #include <ESP8266WebServer.h> | |
| #include <PubSubClient.h> | |
| #include <IRrecv.h> | |
| #include <IRsend.h> | |
| #include <IRutils.h> | |
| #include <IRremoteESP8266.h> | |
| #include <LittleFS.h> | |
| #include <ArduinoJson.h> | |
| // ------------------------------------------------------------ | |
| // PIN DEFINITIONS | |
| // Adjust these if your board is wired differently | |
| // ------------------------------------------------------------ | |
| #define IR_RECV_PIN 14 // GPIO14 = D5 — connect to TSOP OUT pin | |
| #define IR_SEND_PIN 4 // GPIO4 = D2 — connect to NPN base (via 100Ω resistor) | |
| #define CONFIG_LED 2 // GPIO2 = D4 — onboard LED, used to signal AP mode | |
| // ------------------------------------------------------------ | |
| // IR SETTINGS | |
| // ------------------------------------------------------------ | |
| #define IR_RECV_BUFSIZE 1024 // Buffer for raw IR timings | |
| #define IR_RECV_TIMEOUT 15 // ms gap that signals end of IR packet | |
| #define IR_SEND_KHZ 38 // Carrier frequency (38kHz is standard) | |
| // ------------------------------------------------------------ | |
| // BOOT FAILURE COUNTER | |
| // Stored in RTC memory — survives reboot, not power loss | |
| // After BOOT_FAIL_THRESHOLD failures, AP mode is launched | |
| // ------------------------------------------------------------ | |
| #define BOOT_FAIL_THRESHOLD 5 | |
| // RTC memory is 512 bytes; we use a small struct at slot 0 | |
| // The magic number confirms the memory contains valid data | |
| struct RtcData { | |
| uint32_t magic; | |
| uint8_t failCount; | |
| }; | |
| #define RTC_MAGIC 0xDEADBEEF | |
| RtcData rtcData; | |
| // ------------------------------------------------------------ | |
| // CONFIG — loaded from LittleFS /config.json | |
| // ------------------------------------------------------------ | |
| struct Config { | |
| char wifiSSID[64]; | |
| char wifiPass[64]; | |
| char brokerIP[40]; | |
| int brokerPort; | |
| char brokerUser[32]; | |
| char brokerPass[32]; | |
| }; | |
| Config cfg; | |
| // ------------------------------------------------------------ | |
| // GLOBALS | |
| // ------------------------------------------------------------ | |
| WiFiClient wifiClient; | |
| PubSubClient mqtt(wifiClient); | |
| IRrecv irRecv(IR_RECV_PIN, IR_RECV_BUFSIZE, IR_RECV_TIMEOUT, true); | |
| IRsend irSend(IR_SEND_PIN); | |
| ESP8266WebServer apServer(80); | |
| decode_results irResult; | |
| String macSuffix; // Last 6 chars of MAC e.g. "A0B1C2" | |
| String topicRX; // ir/{MAC}/rx | |
| String topicTX; // ir/{MAC}/tx | |
| String topicStatus; // ir/{MAC}/status | |
| bool apMode = false; | |
| // ------------------------------------------------------------ | |
| // FORWARD DECLARATIONS | |
| // ------------------------------------------------------------ | |
| void loadConfig(); | |
| void saveConfig(); | |
| bool connectWiFi(); | |
| bool connectMQTT(); | |
| void launchAPMode(); | |
| void handleAPRoot(); | |
| void handleAPSave(); | |
| void mqttCallback(char* topic, byte* payload, unsigned int length); | |
| void publishIR(decode_results* result); | |
| void sendIRFromJSON(JsonDocument& doc); | |
| void blinkLED(int times, int delayMs); | |
| void readRTC(); | |
| void writeRTC(); | |
| // ============================================================ | |
| // SETUP | |
| // ============================================================ | |
| void setup() { | |
| Serial.begin(115200); | |
| delay(200); | |
| Serial.println("\n\n=== IR MQTT Bridge booting ==="); | |
| pinMode(CONFIG_LED, OUTPUT); | |
| digitalWrite(CONFIG_LED, HIGH); // HIGH = LED off on most ESP8266 boards | |
| // -- Derive MAC suffix -- | |
| uint8_t mac[6]; | |
| WiFi.macAddress(mac); | |
| char macBuf[13]; | |
| snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X%02X%02X", | |
| mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); | |
| macSuffix = String(macBuf).substring(6); // Last 6 chars | |
| topicRX = "ir/" + macSuffix + "/rx"; | |
| topicTX = "ir/" + macSuffix + "/tx"; | |
| topicStatus = "ir/" + macSuffix + "/status"; | |
| Serial.println("MAC suffix : " + macSuffix); | |
| Serial.println("RX topic : " + topicRX); | |
| Serial.println("TX topic : " + topicTX); | |
| Serial.println("Status : " + topicStatus); | |
| // -- LittleFS -- | |
| if (!LittleFS.begin()) { | |
| Serial.println("[FS] LittleFS mount failed — formatting..."); | |
| LittleFS.format(); | |
| LittleFS.begin(); | |
| } | |
| // -- Load saved config -- | |
| loadConfig(); | |
| // -- Read boot failure counter from RTC memory -- | |
| readRTC(); | |
| Serial.printf("[Boot] Fail count: %d / %d\n", rtcData.failCount, BOOT_FAIL_THRESHOLD); | |
| // -- Check if we should enter AP mode -- | |
| // Go straight to AP if no SSID configured, or fail threshold reached | |
| if (strlen(cfg.wifiSSID) == 0 || rtcData.failCount >= BOOT_FAIL_THRESHOLD) { | |
| if (strlen(cfg.wifiSSID) == 0) { | |
| Serial.println("[Boot] No SSID configured — entering AP config mode"); | |
| } else { | |
| Serial.println("[Boot] Threshold reached — entering AP config mode"); | |
| } | |
| launchAPMode(); | |
| return; | |
| } | |
| // -- Attempt WiFi -- | |
| bool wifiOK = connectWiFi(); | |
| if (!wifiOK) { | |
| rtcData.failCount++; | |
| writeRTC(); | |
| Serial.printf("[Boot] WiFi failed. Fail count now %d\n", rtcData.failCount); | |
| delay(1000); | |
| ESP.restart(); | |
| return; | |
| } | |
| // -- Attempt MQTT -- | |
| mqtt.setServer(cfg.brokerIP, cfg.brokerPort); | |
| mqtt.setCallback(mqttCallback); | |
| mqtt.setBufferSize(1024); // Increase for raw IR payloads | |
| bool mqttOK = connectMQTT(); | |
| if (!mqttOK) { | |
| rtcData.failCount++; | |
| writeRTC(); | |
| Serial.printf("[Boot] MQTT failed. Fail count now %d\n", rtcData.failCount); | |
| delay(1000); | |
| ESP.restart(); | |
| return; | |
| } | |
| // -- All good — reset fail counter -- | |
| rtcData.failCount = 0; | |
| writeRTC(); | |
| Serial.println("[Boot] Connected successfully. Fail counter reset."); | |
| // -- Start IR -- | |
| irRecv.enableIRIn(); | |
| irSend.begin(); | |
| Serial.println("[IR] Receiver and sender initialised"); | |
| } | |
| // ============================================================ | |
| // MAIN LOOP | |
| // ============================================================ | |
| void loop() { | |
| // -- AP mode: just serve the web portal -- | |
| if (apMode) { | |
| apServer.handleClient(); | |
| // Blink LED slowly to indicate AP mode | |
| static unsigned long lastBlink = 0; | |
| if (millis() - lastBlink > 500) { | |
| digitalWrite(CONFIG_LED, !digitalRead(CONFIG_LED)); | |
| lastBlink = millis(); | |
| } | |
| return; | |
| } | |
| // -- Maintain MQTT connection -- | |
| if (!mqtt.connected()) { | |
| Serial.println("[MQTT] Disconnected — attempting reconnect..."); | |
| if (!connectMQTT()) { | |
| Serial.println("[MQTT] Reconnect failed — rebooting in 5s"); | |
| delay(5000); | |
| // Increment fail counter before reboot | |
| readRTC(); | |
| rtcData.failCount++; | |
| writeRTC(); | |
| ESP.restart(); | |
| } | |
| } | |
| mqtt.loop(); | |
| // -- Check for received IR signal -- | |
| if (irRecv.decode(&irResult)) { | |
| publishIR(&irResult); | |
| irRecv.resume(); // Ready for next signal | |
| } | |
| // -- Periodic status heartbeat every 60 seconds -- | |
| static unsigned long lastHeartbeat = 0; | |
| if (millis() - lastHeartbeat > 60000) { | |
| String hb = "{\"status\":\"online\",\"mac\":\"" + macSuffix + | |
| "\",\"ip\":\"" + WiFi.localIP().toString() + "\"}"; | |
| mqtt.publish(topicStatus.c_str(), hb.c_str(), true); // retained | |
| lastHeartbeat = millis(); | |
| } | |
| } | |
| // ============================================================ | |
| // IR — PUBLISH RECEIVED SIGNAL TO MQTT | |
| // ============================================================ | |
| void publishIR(decode_results* result) { | |
| // Build JSON payload | |
| StaticJsonDocument<2048> doc; | |
| // -- Decoded fields -- | |
| doc["protocol"] = typeToString(result->decode_type); | |
| doc["bits"] = result->bits; | |
| char hexVal[20]; | |
| snprintf(hexVal, sizeof(hexVal), "0x%llX", result->value); | |
| doc["value"] = hexVal; | |
| char hexAddr[12]; | |
| snprintf(hexAddr, sizeof(hexAddr), "0x%X", result->address); | |
| doc["address"] = hexAddr; | |
| char hexCmd[12]; | |
| snprintf(hexCmd, sizeof(hexCmd), "0x%X", result->command); | |
| doc["command"] = hexCmd; | |
| doc["repeat"] = result->repeat; | |
| // -- Raw timings array -- | |
| JsonArray raw = doc.createNestedArray("raw"); | |
| for (uint16_t i = 1; i < result->rawlen; i++) { | |
| // rawbuf stores values in 2µs ticks — multiply to get microseconds | |
| raw.add(result->rawbuf[i] * kRawTick); | |
| } | |
| // -- Serialise and publish -- | |
| String payload; | |
| serializeJson(doc, payload); | |
| Serial.println("[IR RX] " + payload); | |
| mqtt.publish(topicRX.c_str(), payload.c_str()); | |
| } | |
| // ============================================================ | |
| // IR — SEND SIGNAL FROM MQTT MESSAGE | |
| // | |
| // Expected TX payload (decoded): | |
| // { | |
| // "protocol": "NEC", | |
| // "bits": 32, | |
| // "value": "0x20DF10EF", | |
| // "repeat": 0 | |
| // } | |
| // | |
| // Expected TX payload (raw fallback): | |
| // { | |
| // "protocol": "RAW", | |
| // "raw": [9000, 4500, 560, ...], | |
| // "khz": 38 | |
| // } | |
| // ============================================================ | |
| void sendIRFromJSON(JsonDocument& doc) { | |
| String protocol = doc["protocol"] | "UNKNOWN"; | |
| protocol.toUpperCase(); | |
| Serial.println("[IR TX] Sending protocol: " + protocol); | |
| if (protocol == "RAW") { | |
| // -- RAW mode -- | |
| JsonArray rawArr = doc["raw"].as<JsonArray>(); | |
| if (rawArr.isNull() || rawArr.size() == 0) { | |
| Serial.println("[IR TX] RAW mode but no raw array found"); | |
| return; | |
| } | |
| uint16_t khz = doc["khz"] | 38; | |
| uint16_t rawData[rawArr.size()]; | |
| int i = 0; | |
| for (JsonVariant v : rawArr) { | |
| rawData[i++] = v.as<uint16_t>(); | |
| } | |
| irSend.sendRaw(rawData, rawArr.size(), khz); | |
| Serial.printf("[IR TX] Sent RAW (%d timings @ %dkHz)\n", i, khz); | |
| } else { | |
| // -- Decoded protocol mode -- | |
| uint32_t bits = doc["bits"] | 32; | |
| uint8_t repeat = doc["repeat"] | 0; | |
| // Value can arrive as string ("0x20DF10EF") or number | |
| uint64_t value = 0; | |
| if (doc["value"].is<const char*>()) { | |
| value = strtoull(doc["value"].as<const char*>(), nullptr, 16); | |
| } else { | |
| value = doc["value"].as<uint64_t>(); | |
| } | |
| // Map protocol string to enum and send | |
| decode_type_t protoEnum = strToDecodeType(protocol.c_str()); | |
| if (protoEnum == decode_type_t::UNKNOWN) { | |
| Serial.println("[IR TX] Unknown protocol — cannot send"); | |
| return; | |
| } | |
| irSend.send(protoEnum, value, bits, repeat); | |
| Serial.printf("[IR TX] Sent %s value=0x%llX bits=%d repeat=%d\n", | |
| protocol.c_str(), value, bits, repeat); | |
| } | |
| } | |
| // ============================================================ | |
| // MQTT CALLBACK — called when message arrives on subscribed topic | |
| // ============================================================ | |
| void mqttCallback(char* topic, byte* payload, unsigned int length) { | |
| Serial.printf("[MQTT] Message on topic: %s\n", topic); | |
| // Parse JSON | |
| StaticJsonDocument<1024> doc; | |
| DeserializationError err = deserializeJson(doc, payload, length); | |
| if (err) { | |
| Serial.println("[MQTT] JSON parse error: " + String(err.c_str())); | |
| return; | |
| } | |
| // Only act on TX topic | |
| if (String(topic) == topicTX) { | |
| sendIRFromJSON(doc); | |
| } | |
| } | |
| // ============================================================ | |
| // WIFI — CONNECT | |
| // ============================================================ | |
| bool connectWiFi() { | |
| Serial.printf("[WiFi] Connecting to: %s\n", cfg.wifiSSID); | |
| WiFi.mode(WIFI_STA); | |
| WiFi.begin(cfg.wifiSSID, cfg.wifiPass); | |
| unsigned long start = millis(); | |
| while (WiFi.status() != WL_CONNECTED) { | |
| if (millis() - start > 10000) { // 10s timeout | |
| Serial.println("[WiFi] Timeout"); | |
| return false; | |
| } | |
| delay(250); | |
| Serial.print("."); | |
| } | |
| Serial.println("\n[WiFi] Connected. IP: " + WiFi.localIP().toString()); | |
| return true; | |
| } | |
| // ============================================================ | |
| // MQTT — CONNECT (with LWT) | |
| // ============================================================ | |
| bool connectMQTT() { | |
| String clientID = "IR-" + macSuffix; | |
| // Build offline LWT payload | |
| String lwtPayload = "{\"status\":\"offline\",\"mac\":\"" + macSuffix + "\"}"; | |
| Serial.printf("[MQTT] Connecting to %s:%d as %s\n", | |
| cfg.brokerIP, cfg.brokerPort, clientID.c_str()); | |
| // LWT is passed directly into connect() | |
| // connect(clientID, user, pass, willTopic, willQoS, willRetain, willMessage) | |
| bool connected = false; | |
| if (strlen(cfg.brokerUser) > 0) { | |
| connected = mqtt.connect( | |
| clientID.c_str(), | |
| cfg.brokerUser, cfg.brokerPass, | |
| topicStatus.c_str(), 1, true, | |
| lwtPayload.c_str() | |
| ); | |
| } else { | |
| connected = mqtt.connect( | |
| clientID.c_str(), | |
| nullptr, nullptr, | |
| topicStatus.c_str(), 1, true, | |
| lwtPayload.c_str() | |
| ); | |
| } | |
| if (!connected) { | |
| Serial.printf("[MQTT] Failed. State: %d\n", mqtt.state()); | |
| return false; | |
| } | |
| Serial.println("[MQTT] Connected"); | |
| // Publish online status (retained) | |
| String onlinePayload = "{\"status\":\"online\",\"mac\":\"" + macSuffix + | |
| "\",\"ip\":\"" + WiFi.localIP().toString() + "\"}"; | |
| mqtt.publish(topicStatus.c_str(), onlinePayload.c_str(), true); | |
| // Subscribe to TX topic | |
| mqtt.subscribe(topicTX.c_str()); | |
| Serial.println("[MQTT] Subscribed to: " + topicTX); | |
| return true; | |
| } | |
| // ============================================================ | |
| // AP MODE — Launch config portal | |
| // ============================================================ | |
| void launchAPMode() { | |
| apMode = true; | |
| String apSSID = "IR-Config-" + macSuffix; | |
| WiFi.mode(WIFI_AP); | |
| WiFi.softAP(apSSID.c_str(), "irconfig"); // Password: irconfig | |
| Serial.println("[AP] Started SSID: " + apSSID); | |
| Serial.println("[AP] IP: " + WiFi.softAPIP().toString()); | |
| Serial.println("[AP] Browse to: http://192.168.4.1"); | |
| // Register web routes | |
| apServer.on("/", HTTP_GET, handleAPRoot); | |
| apServer.on("/save", HTTP_POST, handleAPSave); | |
| apServer.begin(); | |
| // Signal AP mode with LED | |
| blinkLED(5, 100); | |
| } | |
| // ============================================================ | |
| // AP MODE — Serve config form | |
| // ============================================================ | |
| void handleAPRoot() { | |
| // Pre-fill form with existing saved values where available | |
| String html = R"rawhtml( | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>IR Bridge Config</title> | |
| <style> | |
| body { font-family: sans-serif; max-width: 420px; margin: 40px auto; padding: 0 16px; background: #f4f4f4; } | |
| h2 { color: #333; } | |
| label { display:block; margin-top:14px; font-size:0.9em; color:#555; } | |
| input { width:100%; padding:8px; margin-top:4px; box-sizing:border-box; | |
| border:1px solid #ccc; border-radius:4px; font-size:1em; } | |
| .opt { color:#888; font-size:0.8em; } | |
| button { margin-top:24px; width:100%; padding:12px; background:#2a7ae2; | |
| color:#fff; border:none; border-radius:4px; font-size:1em; cursor:pointer; } | |
| button:hover { background:#1a5ab8; } | |
| .mac { font-size:0.8em; color:#999; margin-top:8px; } | |
| </style> | |
| </head> | |
| <body> | |
| <h2>🔌 IR Bridge Config</h2> | |
| <form action="/save" method="POST"> | |
| <label>WiFi SSID</label> | |
| <input type="text" name="wifiSSID" value=")rawhtml"; | |
| html += String(cfg.wifiSSID); | |
| html += R"rawhtml(" required> | |
| <label>WiFi Password</label> | |
| <input type="password" name="wifiPass" value=")rawhtml"; | |
| html += String(cfg.wifiPass); | |
| html += R"rawhtml("> | |
| <label>MQTT Broker IP</label> | |
| <input type="text" name="brokerIP" placeholder="192.168.1.x" value=")rawhtml"; | |
| html += String(cfg.brokerIP); | |
| html += R"rawhtml(" required> | |
| <label>MQTT Broker Port</label> | |
| <input type="number" name="brokerPort" value=")rawhtml"; | |
| html += String(cfg.brokerPort > 0 ? cfg.brokerPort : 1883); | |
| html += R"rawhtml(" required> | |
| <label>Broker Username <span class="opt">(optional)</span></label> | |
| <input type="text" name="brokerUser" value=")rawhtml"; | |
| html += String(cfg.brokerUser); | |
| html += R"rawhtml("> | |
| <label>Broker Password <span class="opt">(optional)</span></label> | |
| <input type="password" name="brokerPass" value=")rawhtml"; | |
| html += String(cfg.brokerPass); | |
| html += R"rawhtml("> | |
| <button type="submit">Save & Reboot</button> | |
| </form> | |
| <p class="mac">Device MAC: )rawhtml"; | |
| html += macSuffix; | |
| html += R"rawhtml(</p> | |
| </body> | |
| </html> | |
| )rawhtml"; | |
| apServer.send(200, "text/html", html); | |
| } | |
| // ============================================================ | |
| // AP MODE — Handle form submission | |
| // ============================================================ | |
| void handleAPSave() { | |
| // Read posted fields | |
| if (apServer.hasArg("wifiSSID")) apServer.arg("wifiSSID").toCharArray(cfg.wifiSSID, sizeof(cfg.wifiSSID)); | |
| if (apServer.hasArg("wifiPass")) apServer.arg("wifiPass").toCharArray(cfg.wifiPass, sizeof(cfg.wifiPass)); | |
| if (apServer.hasArg("brokerIP")) apServer.arg("brokerIP").toCharArray(cfg.brokerIP, sizeof(cfg.brokerIP)); | |
| if (apServer.hasArg("brokerUser")) apServer.arg("brokerUser").toCharArray(cfg.brokerUser, sizeof(cfg.brokerUser)); | |
| if (apServer.hasArg("brokerPass")) apServer.arg("brokerPass").toCharArray(cfg.brokerPass, sizeof(cfg.brokerPass)); | |
| cfg.brokerPort = apServer.hasArg("brokerPort") ? apServer.arg("brokerPort").toInt() : 1883; | |
| saveConfig(); | |
| // Reset boot fail counter | |
| rtcData.failCount = 0; | |
| writeRTC(); | |
| // Confirm page then reboot | |
| String html = R"rawhtml( | |
| <!DOCTYPE html><html><head><meta charset="UTF-8"> | |
| <meta http-equiv="refresh" content="5;url=/"> | |
| <style>body{font-family:sans-serif;text-align:center;margin-top:80px;}</style> | |
| </head><body> | |
| <h2>✓ Saved!</h2> | |
| <p>Rebooting now — reconnect to your WiFi network.</p> | |
| </body></html> | |
| )rawhtml"; | |
| apServer.send(200, "text/html", html); | |
| delay(2000); | |
| ESP.restart(); | |
| } | |
| // ============================================================ | |
| // CONFIG — Load from LittleFS /config.json | |
| // ============================================================ | |
| void loadConfig() { | |
| // Set safe defaults | |
| memset(&cfg, 0, sizeof(cfg)); | |
| cfg.brokerPort = 1883; | |
| File f = LittleFS.open("/config.json", "r"); | |
| if (!f) { | |
| Serial.println("[Config] No config.json found — using defaults"); | |
| return; | |
| } | |
| StaticJsonDocument<512> doc; | |
| DeserializationError err = deserializeJson(doc, f); | |
| f.close(); | |
| if (err) { | |
| Serial.println("[Config] JSON parse error: " + String(err.c_str())); | |
| return; | |
| } | |
| strlcpy(cfg.wifiSSID, doc["wifiSSID"] | "", sizeof(cfg.wifiSSID)); | |
| strlcpy(cfg.wifiPass, doc["wifiPass"] | "", sizeof(cfg.wifiPass)); | |
| strlcpy(cfg.brokerIP, doc["brokerIP"] | "", sizeof(cfg.brokerIP)); | |
| strlcpy(cfg.brokerUser, doc["brokerUser"] | "", sizeof(cfg.brokerUser)); | |
| strlcpy(cfg.brokerPass, doc["brokerPass"] | "", sizeof(cfg.brokerPass)); | |
| cfg.brokerPort = doc["brokerPort"] | 1883; | |
| Serial.println("[Config] Loaded from LittleFS"); | |
| } | |
| // ============================================================ | |
| // CONFIG — Save to LittleFS /config.json | |
| // ============================================================ | |
| void saveConfig() { | |
| StaticJsonDocument<512> doc; | |
| doc["wifiSSID"] = cfg.wifiSSID; | |
| doc["wifiPass"] = cfg.wifiPass; | |
| doc["brokerIP"] = cfg.brokerIP; | |
| doc["brokerPort"] = cfg.brokerPort; | |
| doc["brokerUser"] = cfg.brokerUser; | |
| doc["brokerPass"] = cfg.brokerPass; | |
| File f = LittleFS.open("/config.json", "w"); | |
| if (!f) { | |
| Serial.println("[Config] Failed to open config.json for writing"); | |
| return; | |
| } | |
| serializeJson(doc, f); | |
| f.close(); | |
| Serial.println("[Config] Saved to LittleFS"); | |
| } | |
| // ============================================================ | |
| // RTC MEMORY — Read boot fail counter | |
| // ============================================================ | |
| void readRTC() { | |
| ESP.rtcUserMemoryRead(0, (uint32_t*)&rtcData, sizeof(rtcData)); | |
| if (rtcData.magic != RTC_MAGIC) { | |
| // First ever boot — initialise | |
| rtcData.magic = RTC_MAGIC; | |
| rtcData.failCount = 0; | |
| } | |
| } | |
| // ============================================================ | |
| // RTC MEMORY — Write boot fail counter | |
| // ============================================================ | |
| void writeRTC() { | |
| rtcData.magic = RTC_MAGIC; | |
| ESP.rtcUserMemoryWrite(0, (uint32_t*)&rtcData, sizeof(rtcData)); | |
| } | |
| // ============================================================ | |
| // UTILITY — Blink onboard LED | |
| // ============================================================ | |
| void blinkLED(int times, int delayMs) { | |
| for (int i = 0; i < times; i++) { | |
| digitalWrite(CONFIG_LED, LOW); // LOW = on | |
| delay(delayMs); | |
| digitalWrite(CONFIG_LED, HIGH); // HIGH = off | |
| delay(delayMs); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| %PDF-1.4 | |
| %���� ReportLab Generated PDF document (opensource) | |
| 1 0 obj | |
| << | |
| /F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R /F5 7 0 R | |
| >> | |
| endobj | |
| 2 0 obj | |
| << | |
| /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font | |
| >> | |
| endobj | |
| 3 0 obj | |
| << | |
| /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font | |
| >> | |
| endobj | |
| 4 0 obj | |
| << | |
| /BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font | |
| >> | |
| endobj | |
| 5 0 obj | |
| << | |
| /Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << | |
| /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |
| >> /Rotate 0 /Trans << | |
| >> | |
| /Type /Page | |
| >> | |
| endobj | |
| 6 0 obj | |
| << | |
| /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font | |
| >> | |
| endobj | |
| 7 0 obj | |
| << | |
| /BaseFont /ZapfDingbats /Name /F5 /Subtype /Type1 /Type /Font | |
| >> | |
| endobj | |
| 8 0 obj | |
| << | |
| /Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << | |
| /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |
| >> /Rotate 0 /Trans << | |
| >> | |
| /Type /Page | |
| >> | |
| endobj | |
| 9 0 obj | |
| << | |
| /Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << | |
| /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |
| >> /Rotate 0 /Trans << | |
| >> | |
| /Type /Page | |
| >> | |
| endobj | |
| 10 0 obj | |
| << | |
| /Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << | |
| /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |
| >> /Rotate 0 /Trans << | |
| >> | |
| /Type /Page | |
| >> | |
| endobj | |
| 11 0 obj | |
| << | |
| /Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << | |
| /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |
| >> /Rotate 0 /Trans << | |
| >> | |
| /Type /Page | |
| >> | |
| endobj | |
| 12 0 obj | |
| << | |
| /Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << | |
| /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] | |
| >> /Rotate 0 /Trans << | |
| >> | |
| /Type /Page | |
| >> | |
| endobj | |
| 13 0 obj | |
| << | |
| /PageMode /UseNone /Pages 15 0 R /Type /Catalog | |
| >> | |
| endobj | |
| 14 0 obj | |
| << | |
| /Author (\(anonymous\)) /CreationDate (D:20260523113141+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260523113141+00'00') /Producer (ReportLab PDF Library - \(opensource\)) | |
| /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False | |
| >> | |
| endobj | |
| 15 0 obj | |
| << | |
| /Count 6 /Kids [ 5 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R ] /Type /Pages | |
| >> | |
| endobj | |
| 16 0 obj | |
| << | |
| /Filter [ /ASCII85Decode /FlateDecode ] /Length 1994 | |
| >> | |
| stream | |
| Gau`U>Bed\&:WeDbTl?qXs@5cm)u!_g@XG9BjU@>5_'bh;d[\hrq[YWfGKL&EO0;A2Q8haI<3l]-VAsJ2q:,_!PleW5$q-Z$jIB<Q=7Vf)U8SYT,kMk[$b?LbbO+KNo>$1`-%W+SI-,c'n1<78uQB$0Gm.Xr\eWQ\/*$3%YE,d*J4?/@)0,YlK:$$:(<Q#Ss#,-Hh@Y'%tanQ`,[sT7kj]=K26I!WJ+A;e/63<WaVe(K'hIML=+d1$kCh(d=ue9&eW#m-H?0ocndZMhn.+@DskmXZhE$;\RDZ.i35V?D]Q`Lr6OD7-e)o5Y\Z/Loium36bde364Fn4YKM`=o0V:r+`<DRC98/tV$9'.r"Q:H(sHO]p\Wh$=AC/%dF)k[PF"!N3lYE7.0,aV3*?$^J2E2%"T5J:C)BEh4GK3j;Pl5ro$c)1c,:I';IV?nneYo[&,#Peq:Wq'a=rUhLM(m[+j0p2#3!#XU5)D*NhS3AUD,YLi?UnR0ar3<)/tkX>bh\=79L&*-kP<LBb^?AN'^(lP#MR02'gi\m7H6:deT%O1.hEumD3Wr(<;L!B0D(1!hF"(5CCqfI6@':rbsJ4M'f4q45Wl-'?ZH&=)6Q"&O`SuZoX2A3@&V`qX_XM5;1eM>NY#sKZ2`5+f$:lP.tB:@8oO<Y[n6o8NINc/1Kmr$;3CO_]g&FAMSn7.OBI&'1r?_UEchcpb7_779o]:mAgMkh-rC8V%AG.nY\^Ud1BJcA#p8"e8Xh'.7BRZ:mF<fTH=$PnF^&#@Lflk/g6QY2BOl7,\I=Q.Aun;EGCgIIsM*_!FO57>oJd`B-skh)OO&8K=ZHb6AHD\N<^.k_@-0G"'&];KRq*:K3Qt[O3oMcAFc^0GS[-9%>Mo?f>`:)3K.0KZ&6,g\r*[l@;4\ETu2.Ea!(t:VE7jrm-O4Ii9tL+hm]!?gp^81A!Ie,L]'D9Th3TicX]lf?=b%G7_p8U7@;Y,!k##d&WH2!!8c^\8[cF74f$L^G'\"hI=5aPgY)#l%=E"!F%k7UhQ1WjT6g!cm3.$:$9'`)Wc_eb$09m&ld$SJP919?SQ\<9cK:jPO)2"\N9hUfrO[F*AYr]%##+dalT['=)VRu^fYHQ$A;Di:qrqf*/oT'^ATPM2)XMJ$CZdk+kk@5`gGn!/!BaCM_6ZZLXS@;H8E1g?&#`5[p?)g)F6W;]p"#.j.Q,hQI-TMDhj0s-@m=4T?Fn"@)Sg5.INeZWi4G\uab)(lqWuoq;[s<\g&PpTpB0(Q,PRu=I@"nN4UVJL$8E<BMN;79;:H`6is^c/&bEIpm[>^;lJjAngO3ajVerk,\/nUeQ*[5:Nj0<D\Hl^q_eBYS?4Yqn;FNr;fiot^4*MbuZ,\-C@^.:4lU!TOJ:E>]dYFb"M5l\?9N=U!lDTF7L4=\hgjokI01H-(#hF&t4fZ\%Gt0=eAE,PH-rs;^2H'1C,#XJu#U<.\iF==2Rj,EZ3?&TD/`7R5I#Z$%<j6$:ioO<>lcP""p4k]Ka#k=*4l0?-81Cn$;F.PggN!Qt+k+8W+<087o6.R5P*G+<@5fhe]F:=mpEmN[eK;kqDc2KN"4pC^XU$;"T>(8rZuA5q+s4YL0(6"3E!0Tm?B4;H3Ah8$gAHRW0="Cj5qO.qY!T*\56u1-FuX[1k\)iVqmjW4<i2UI-2Z+Rj%[DC"p2j+@mU$'\B*r\Hl9UU0Z_o]T7^eYM?#O]S*fEqMOP(pCD+'6#V1(LQhs5qCBJR.UK>EMP?X_^C:YEX&ba/:*<f=aAh(`"P7Y2h5i/#Ca^/F_`ja4lG@CJRNQS$qUaqCcON0n]+Okf0@sO,LR,?l/]#ce-aHj355p7;`1L@lh4Td9;jk/do\;*t]O]j%`X<FED/+096eW&&YY)3M\&',?&nmlHT<'77a@*"[VaHaDc.-.esM40_r*J`1.-dPe5qB;KpR0N;A%t"0tbn^cf#D$^.rs<oH&s"i*Vs`j:*3SBWp1*pQ7m#)n(O^-#dJ~>endstream | |
| endobj | |
| 17 0 obj | |
| << | |
| /Filter [ /ASCII85Decode /FlateDecode ] /Length 2352 | |
| >> | |
| stream | |
| Gb!#]D0+/c&H:NnZ(Gn8;d;t]h`>g#BPEhh/X.RjG;K3WfLfZ==M^;ShQO\ZhdRsQS=&0W;+b<$[&\V^g1OIud.)nPs8#B=M^b0U#nRHm3=r&BR,5X/o]5lU.7[Q@ZWuc.YUu^af5bjPMY15E4J&2M;CaV5\h"Q\\O'5o)dUL`oV)W@Y?W?C_iN/s[n2[8Z%2.a=7?Qohf,17T(bKEX-@F6KZOmoro3]uf<HE/!Jd.`Q?^OCnHs0IfR'^^LHI`Ga%]nkcp$L90&TE%1Fu-5E6SElXuVe:(li_'!T-[0U*Ikb1eMUsihp5L[_ig1Z'n1"@&BWcbV7ra:.dLi?_;!a,K.#UF%si_%/dK3akV3.DDl)]]psf5iD2JRC(?s"@>[q45.ssD'c>BdkQ4*C?GB]!AeZsh_-Q@HQ2B7F)2(j]a47q`[C;Q/LnrK:`=6J(EF-`??'_Q[au@Pp#I"N3@h(JhpBb16afd%8%^iBE#`%(%'/+f\(/aQ<b'quZI@g\CmkccXEKGbg#8i%V%tF'_Y4!MC+(Hrr:EeZ1#^BMj+0?/!KeK1-qtso2+7JH"2Oe6`b:)Fk\#0t*6O\>Nr?qC45UjD*q]`aDF,5t5cdO?<\J$Y5$*:F8'W-dBI`rC!ZWZQ@i=)@Q^dn-:j)2a\.==ZTgl(##r>.R3TnJ?FMuCU9)`;qg_<VBGM_'h0=Bp,CMNR[Bo'%BJIu]2;Jqm3h+pZ[hD1i0bT&e#2WuVTEJA9u:1^AoQp[:m7"+Jg+Y[$?uobbLj([4YFhKl:_"u_3X-rS::']EOJZ<@lV/RU/IX>3Q[/)N<4XhLu5;<AXU.rkMaaZk!S/+R<OD3J8k!N+'*!@eaebgqp%%'4)Xbf",`]XbFnQ&,ghGLgJ_B3qoL#VQKl_PP1\YU>F[l`eXR3fXO#pB%.@WPE<R9-.`b)<C@=g;>c>VXl[#[m+pd=26VMBTD+6@(7MH/$4dWU*PeF@]5Q_NEO(sPrBQW$C-!E,<5DW]_1oga#j\,f"38!<UXfoO@=P`^5#QWdaa8:8--2d?WuoSVO,=U5DnLB9,F]2POP38g=rZ>[hg!la-63&EdLY%C+E34B0l8]<ID)M^fl>1<k)Ck7!^/m<,^q=Y50?6LQAQ=W>#ZiY%qCZXAhAu41jK7fN:'eWNI<SC:`.*[78AJ/@/s4O=Fj!<>=\beVsEeMP%5[g%%/+bO?8[2kcHJ+&="Q@><H965\_pOUDY1Lt(gG0//KM/oAB/r_"RZN\1*i(k(Lsf'aMu*DNGdeX9naI]E[#D]`[`\P?kt3i3Rfr919g0p4@1LIm@K-%pq9i%j33/Fd8\LTo?&TiTcC$G%^Mau[YGcgDM+S)mdiiM?maYMksj#fobd4V2SsKp[I`\^]P@_Tu'OFAh\k8u`#J+W#3,T#)J&S`WMdRj4t6@89IJ,?#%G\XuGEe+1mKW4?l%?Deaef*IJE]hF;0K&G\?4Te_34A!A-JceY.hhQ"qYr1N_1PXkuQ2Gh4j%:n%PteAd"O(#<p&(t8@#%jfiu#Z.]&<)gBE5q6O_K8<6sT*Nc.+nkg+IhH+n8TA&3XfZE=a5-/I%AlJILGhf+Q];#P@bu(*1iTp@OtG"euTAD!deLE(dGr6ndB"pOJ"/.d2A_WLQMnlJ,jG@qs#0jb7N!?$LXH#fIlL//_@;P663[blKip;[gtr_[Zsa"V,gO^62rJ>%KeW=-4h>/q8c>l$P#;?/NCG:B-funip@#m9rs"i6U1Z,8K2E<LmG'[r3q)l^H/;?TWUf])NiP*USWb;S0'5)n5s6Q'\A(9!F:IQo%F"h;4M$3`i*6)fo(#oQ]tAKc/En=X&<laCpIc*`Z4SmFO\IebRl9E@EYuUG8l41bD^$+716;pWsb]?Xq(;@@WM`(6_CeGI],2^Xu0?F'](o#Z$7B=Upt%)6pb(n=R/)R!;0kJ&"3KXM/VNO,(P6@&&e7\h8PsI*HARd*n_`_1FK%?LY&AIC=8If@R2Na#/3XHp"K;Y6T7ClO?:Yh0]m52d6$c87.;A<\&*l>C!P4`E^`^XVnL7@B^Al26VJ-(>.<Z'A(U4ib!2>gbLDUn!7MO8o,L<.-U*Uf6?j\g"0Bq$5>DdO=U2-D8G2D`$KSeF8(q@2)X2XSM+I<Sq#)'K^UYYfGZXK`ljbH-m[]/TUh@m&3_UbmppJ;Xrk;"qZ#DUq@8ks.9=Zt&51k4O!+.^HWm%-N*$q,.nbkNS;,ZU#cBtGf*o:Vo3`,,?5.L0I<RpVrE&MFi.%Tk^^GaIV^[FI;2-oc&$A'1:t8Z9qfNkh]<,jifA<0)GQ7'?gO%-C@VeYm>O5%4AR7OL$Qop.!7&R6KA4$nLQd2gL?D#<~>endstream | |
| endobj | |
| 18 0 obj | |
| << | |
| /Filter [ /ASCII85Decode /FlateDecode ] /Length 2006 | |
| >> | |
| stream | |
| Gb!#\=\mdE'Ro4H*;(a[?tSqRl!'kCW`:u#=&jCR&&t$!Bl+B*+eYk8rLOnI90tO'/)@(>m$Y3)=hb7HJfeb-O&LSh+WR%NTF?QbTW[=ln,p<DmbR<Y)5U-!_"4L_&$A8e018"ZG!_NnhZG5??Joi$p6fkeaKP=njRWPbJigj2*"JBe>f-7^au#_.M'0u42TTH&jUu>ZOu13&p,MEd@6>*q[_L-T>Gh`DVL8cf8/N?0qM3;RMpBLo(k.8!9-a*<5Fl?-KL:Ec4"9qOMq*C]hf5XAG7sgCNh\'Q5ntdqi%((;^ralNq7D`6(8Z&XDg#"K8T/ds/oj0haTH'ONoUJq(`mda_c;,X>i&L2QN]cW3K[IDii#g;>N<D0,ZReX;k8uX8?>&9],eih(>$7?J)C!j!:%>^J*f69VRFPPUid&:huX_KJiE#_9=mp\f1ET5'[,a[*/!H7fGk-33%,7%&/`.TV%lN'>.`4#2_VN)Zc-0XNYeu4+2Yf*$p,K@IlW)+>(YLX=>1N?2h@[XKYrHj1smK)&8]\rrQ4FK$n(J#HFG4Be=gQ$AufmFZa%*EEZ6UZ/rJj&UK_KK0Z'*8o$JanXFP:''b_ufSO_9E/dEIt=XN_3ic<q-G9mr'I])Tfrjf\i]#6_P7W>-#Ql9sE((La1DT+DKnt8R5JYbAc+6#\3.P^rqQ(g+#5m4`i>&gQqWCdefF,1]G,gR@#lB-0hf4sVX%gSR0phKgb[jAh87u_F93/mYc7c3/GnH8M_`<:>&0H?t0^WaurJT0Q-fl2bF^JXOd?Jk#`6.")o]J^X1>+DeD1Sn*(?Ak"^,SrgMAa6^enF?/XL2/nPpD?AYC5I?iSj[i<UJD9E@rug$WM7cG9=cf#*_nCJ;Aj'5+U0A*(j2%d<"om6)+I$N/!PkK0Zjcn%S__GWcFi7dKV:#EOeN:6pMa/pnL`$To$s;9<^<RVT>b+q\Xp%#<XF',7g+l6T!b"Oq!"3T1M.38t-D%Fii*1PW($0Fts:\1KR?[QT51cs#msMi.Qp<:/o6$j87KjIZM?!<IU^$7=`gPeXSS_%KfSi7mF7TIN@PERpEO^Jn*OEB<@ZLmp.^-[7u'e#AT4&KpTnb!4BJg*+DdV_Y(1o9cFIYWiJN&'C=?bM2=>`#U&T?kcn]/aJGF9fH/n?2`;8k>c?s>9%MoNs#E@o2[<n1q(i$FoUm$sD2sI^6V&oYXoE2*j7#g=+8p1_k1I1B%)uN\2S5SGa8JFEQ.;?#bq'"0kW%/TA@!Z8$K`i^Y0,gf*fnQ)e7WaYK,Kk[%JUc4>Y>ZY(_$%ZGe)^X'8N[H$p-knqQMK-7N@$BX*tfm7]O7LQsj'$k,:.6faP2.A2UP0:iQrI42U+C\hD-`^A\o:OE<Ik'D<RBgkKL91A>Uh/Q/0'GH)MOlA1kqp8-GLM51`$3umpp6ukDc/\rN!dc^0JM.-cLq(:<g?'%)IL'Xi;Op6;*kLos<QIk2bShM--%Xc`jKi)?6E7Y/iRT.gVl9aV?q*4.<+V':+rDd<W_hDMP^P]%R_-UW:A%ij(^'2[=Icu$-=RAPD2uCpT_]H['"I/$QnKiKPFG-mgEq'(fo;AaOeeh"Jh[n-,$)/s<ir^2DlM!_7r72EoUD6lucKh/\_JTVq[L7[=he`RDCC^fdcCUr@YHKo,Hel5oc#eWM,9N;`fl`>]N/%smf:GX"mHpou[Fms\OS2r1!n$g^$V1A1F?Sml5o?8ur#$P=RkGc:,!it_gno&Ue&1+A29I7W4X-B!(H8lWFf^cCYNFG$]fgX\Y`e;Z0-^iES?4-QMip[bC*<80]K)hLmHGNXZcgX(?K/-HMKPBIh/is)Y_FZp2_lj!R)W!$a>H*-FFiDCO:ZFd\)`Fc(#^_q:bUTlJd_U%G5U&<c1!.n`*M%`.!JmIkLT'T`9(0g9>>R]ojZ:o2@-NC%D,5Fg=(L]NOAR2X@[5EAZ7<+%6%"(G.G@VI@?Y,+?Y$dJKi-lqW-`;;P)^\@IHF8Mnf;~>endstream | |
| endobj | |
| 19 0 obj | |
| << | |
| /Filter [ /ASCII85Decode /FlateDecode ] /Length 2251 | |
| >> | |
| stream | |
| Gatm<968iG&AJ$Cm.eq)3]7G/"@]184,l>h3&>iYj$V8t6pVG8MGsT&ZC8mbmfF1F;FJXS1eo6W])WWDm=)Z_$3YjWq`e?fgl:7SSc\Rn1^+eD+cIK#mHEQQ"l3o_^)&)E6=K'g4dnbFY!]I\YUGM5f`+pWM]?22)+N,6j!4ikK+]m:q;MK/_t3/Q@gE/R/V+-"ba\J@&o.Vi5ACBVqABM,$:b%P+.#bc/&gW9h3[3U-bnM:o(N(Rb.TjC;Mg[sAoB,4e\AYup^JnV_3t/5"%_'a5](0.8d(NpOn0u@*bG%_K9(BhX"r/f#+fC9ShAsm?s4s,o0lueF',EY'PW]oc`^ZI2C&W]Z]KuF*tg+cL;`"cY?Pe[pG)JgqfiMX^><)`=$d2'g3FF=rAQ:]HU4FfPR>/"]AJ^hr80Y)K(t+1JgEtnMN1IEIlcpWQ.A_'#VI5SG_,a%*IMqS.VY_AaPF\WRdF;rbltTcJ>k#a^_+gja<r]nV9k[W"Je/80n'feM/OaS'hl%:R@=9.[E':e&)J,tg24,HMu[b2i;VbgX3"@inNk<!VSo.4,4u?p^<$^G\['OAY$>NXG;o":3LFIJ"oaC7*qP/Lp"*UU7N7%7]=AY\GPZdM:*803qSo;]SJ43D+,11gK89rrd"Ul6K275k;Su<"^pS_`R:S&bg@1r[bmUVLXNq`U3,i"0DO'[-b)C(B@cEG)WTQSMK!5F0hs(`J](u3jp<C&cm0miNhhkhoc.l`<\BI$"CW&$dJ1el'<H9W)aQ6;n,W>ep[\TW0(Z""_[hg]jQ7re02h!cppe`-@lt/No[581m\RD+)MEJf2?g_NL/R7jg?iQGId5V&rrhWleld<ZFBG>gC$iS6hdC5DF?=r/<HGIHD[N.?p3%tBt$cRXe%`PPu:&IQL;Yo4)dHFDA@!R'C*.<:t3R/MC@Y#,'l,,t*nBr"DDIeXD%+B]@\#O@c9g$8u6Ui&t@8LON7-_/#F6"O)'i-rqn=,g'da+;fO;Gu>'/VF^#mbPp-8&_+R=hW+AhMp2\kibFmKuXGm9MPfeJqBOZ(:fm'5'P7Y&H8EANn#mb/G99fij1lek%>C7-1AkqP^FE/sX$4p&`qUC$`ei%XMhBM#BdDo<+G0%PVA8KbZNV@RNp*a>uZ6Mo+Bu:E(,Z6]_#YGJ$$`?5uDDI5H%ds.k4VNCYgXO<Kg@=Pp:[\cMGdOAZMGQ;3/u$^5n%+1'7mODIuq>>6'-n=\(+S]U,eC"1[Alt&7WN2HcYG'7I&2f;U[(;+B!rU1\En.YA&E=p]?OgKHF5&HT9bhLuVT\*7"L9^7J*;:Ql[M6F6V&$aWqGH6@(a&e(On\Eln"aibhR6>fIE]d;^:l)kh_k+O>I&QAY,EePD4VM0X>IdW5*:Q$kKbpC;&./^O7N-R!]$V6oX96J@Y/t[H)=AiE7X9_;hLELgM=CsJ-QFXiV#Dc/[8NGD[aLC[)f.cAt)EI29q1a]tV%V.kse$rFCTba*rF268NXjCu)m)K>hV[K:mN.*;'/rQuehY`I.i0oE`@N:M,S"bZn:gc6fb-rG?5#b?8Gi0@i$02eaW.GU'Is_O"/tMemHG_^(e2FO=4_8LTN0GZWtST[6_ECG?$9H)sl_md;(LIfB[Wc>j?9G(^a"brGladmVik?ud0^o1\q#4*eF]][S6GdA^d4PJE]6ZJLU?I#X\TZ3+(?2d^!gdn(B8A6FW/B.e%?^37V@%%?*ZB^D+BnS:3B0qBMd@;j`L1D*D<m6A(6jfIJm/osC_MpdBi^HsDM>08`aVS(,JB+:\-\KX",(2n"iJ5JG\;VOlI9?S'b!&D3WDCWgJ91iK4OK3YX]f!AR`U6n!?`N1m`j\66AB6fDEs/`+;1j38juW94KACiFM(^/1ERoVMK@O*Phc3U+AX9&I(9Fq%/Yfh%F:tG6BTVk:<&(JF6EXTSE>SAB_QFeXG&$M(c]DTiY)P$N3%nZe=66a1W6UX/6p<Eu@jU?/W30`%bj(He!JqnRoBefo*5/eq:oS%K*-Bn^4&VWpYlpsc=CZ(b-"f$-/FRJ,GGl%Y]0K:n`HAp7C@ofnYe.SUMH$pqSc/afk:IBZRc8]R/[N$nWWB7p<MS1u=[K)S*GT]q^jlAhGVN0I5Q5ib"gDlVQ"A^CgW'^d3>(K_aNeSL;=(7RCh+o_NlXCBU2X0JG!okB6s>]n<]VH%fSJaO)>:<NZ(4=b?:4["'iB-_o?c4[7tF%^MX0`=oRm$lMuNc\X7b'~>endstream | |
| endobj | |
| 20 0 obj | |
| << | |
| /Filter [ /ASCII85Decode /FlateDecode ] /Length 1724 | |
| >> | |
| stream | |
| Gb!<O>>s99'S,*:'kbs]C^\)M4hT/XB%Ugk5W\ej(YMXu\$kA?16C2Bs1V"/j4Hp3m^0S^,m2)JFEF[XN^COu(GUF"%,gL]:+lnb)eKI#m_jh4#Ip-AI6SqK9])H:;gV+Z#V>QWf,`ZgSD^p%K^VDE@ZdWc?t.lAlf!?6[t;s1\NI`'8Y\c]ZOCu\EHeO;D/0U#dg:CS'#'%>7'5BNbh:+o`VVl-p(_2C[=YlCFmqbrV*el?a4!*^:Wk[T>C&NW]]9^FRnY"SJANKFO!bMkfGnS(6q10#3%,qJ:b/R(;93YZ4L-fG<;^u&Rn[;!@_eRKQD^g7LnP5*<qo1efLGTi$;Fucas<Nn"/Ai6-tsj#OkJB7FU+%&k">o@TQ4@4i/.W3nHraT$J(pScEqa7N+h6<A("KFJr@T,-L7@b[:A-.AYVjKp``k,'#g5$;5gQ4I^*b%J?)HoIh'#NC,3U#JNF1Od-8ddht<HF3^\9RY^FZR!8h1=Gh)D'L'15*&<ir!SKS-m;2,!/`u*U:QmE8V#J(@E%q$PL"r'QWmgW*bfIbo?$J<r$RI_<VJIse#J<!=d0lFN<Pui=;:]cM0.Unf69L#`d-J*"/;kW2m-B6A?Sq<,al/]_*9URA(n2%loKh5bi<E;"t`Cr._j@ng.Wgs1%TMfB"?pHP3QaDB&p&*0\@.ukiMdm3#i;,DrS;,sK+;l_:)lYDRL>_mIO&"e,(T(IsCA/UR`g-#4=1n.WUops73b't.-ZS=5Zd'51nQ"K#>B/PGL1fS/7YhZK=!Cd[>`$`Nm6&'p;7Sf*;U>2kFb_]F]4US8A/Nk"g[;X.OsR#?Z*UUFl`.QkLUW``kBkqGPd>I&-<\1Dd_\<n5s$I8-7:/:n?>5iKDEpjEI-kLUXeE[DsF>#WBp$SD&$Y$&I;mhf>,Q\_Q"4V/2g0<XJH/f,$8u.+B>J!o'$u*rbCj9MQ3t4N"lM%i`H^jd)B.U.eJHDMf_+t,OX'ROKea5Fo?88rpuYeCJNaUFSuS4l+5?GDB/me3++\4g]c`-4YTp03\g0XL(Xf,U1n*4nbaEEF0S9-n0h9ZZu'!VI&_dFp9Gi:d!"tucaPj2o'(?`S<2'us7?6PqM,?E;d&H\fR[1#f;YG7!EJB/_84@8ZugdPV;=%:_KIY7R(S$W2O4c:F@`lKC/qDcg=FAaC&g7I,YkG?V#dIYC++g=ms5Y.9S6Y"3@&8br&g5a\7aS6K6;Tp`o#r1:)7OCgNG7!YHL(u>O),7L^pf:=8XS,e\R8KlY&>&_jEK[N/.;an!gUY&sZ+eFQ9O4/XZoSeT._L!$d'H%`i%0&Nk/^&4Wbj=ba>31^%$T.Si2pS%hJ('M?:f*M&1m0A_\K7o?"<d@"JdNuht<1AopRZE<kf`4EIs@EEOZe`b6RV\bZb]Sh:@e8)kKJ8YtMRumUn?CqJcG$prBrrq-REBCjU3_9*VH[\-9G$.hI3ol;%<$@i[\4Zc'W8=dAV(mVFZgB:1[_*0[,u*.]K>+""8@U,9HR9Ch>[OZ'+r(D=aA,fD+"asgSqOVuKV,0?gSUm_!#oSeE8TnJ?WK7qZr52CDXO?*0/hMi&\O9IZ,)eN?X?39;QNWX372SS`o7`p8Wb$_6V8S!Im!iia26G8B:oC7U!ucm6g81(A,XA>%n]o&2*H167#mF2;O#?cm,,=5Cir!\"4!pA;3WO`nLgrih7CFXSCP0U$XYmi(NnQfM?~>endstream | |
| endobj | |
| 21 0 obj | |
| << | |
| /Filter [ /ASCII85Decode /FlateDecode ] /Length 1687 | |
| >> | |
| stream | |
| Gb!kth/h:0&:`lHfZ=eD;hJq'9Q'8A?Za^fRO2J**^Up$Ohm#/EOHk([/&Tt-(>Fui/h:6V$@;nZD:e)G4jTq9PEJ@<1eUJE&)rEhZVD[5lbN;keQ*hc3K?D)eLhR[1'NWp$dn,)J7=_iAu@&oGlJ(D()m8UqsjaPo\I54Kmr,XuK#sGi-$IFXV4>1Hr*A?Asl.g?NmT-e2n2G;=_]5@_D53I;!`DpX0M@=1bVJYF#?;_#-d>O1fQbVG;%l@VdfCISu>5N3q+&)X>r%,S&bjqH,ic,_*gA?:dhWV,7m@MH0J=RB,A7gVh([^eU^mN,S<lTd\Q_23GPS_<,Po1n1'Fq5X/,beUfnoBGCR*:<0TWlD>)&'bMA3',U>(UW*]h,0D'"2B5Z7#DuoT/WCKLtJp)u6/!JG:ut%pIu'"A<0XUMYY;fZOJ:0>huQ]e/E/5d"au=au-^mTYqNdk)VD69sjk68oFIlXI&8"p8`#]!UA[Z7(um9s35896Cu+ek8@gM<I_B>RntF0eFCXNNC;Ph:j(2ETR%qAVK%#=cfCr'VSKOWblVBe-=ftpsIpY)fs+[7N3/N4YU^FP2+OO48I?QE\kaa[AFdp4"<Kh.4gkojS=RsAYm2JGXN5'!>N`[JjW4b5?05bO,I3HkNF#,>R8]%;@(sCc7",q&M5-e5b_ou5]JfH>(%;Q;73=Jj-[&*OBecL`&JgXe9DukVq3gHX6WC-[SO\Wb6^`>VVC@D>stcP?VE/D]T;^"JTr+JhQ=KqVQ!tp'PoqMJ\_ET8L!@gHk)KR_$`c5f<Sn).'X0p\k"1bWU)3*3_JJ<"D8KI6"/'&8+^U'Uu['$%2d?L!"nCT.7E-i`%t)J?J!7Vh+_#FNf'-6mYZmfN5d_Pnc",gUV>(mm,D(3(7q1YID3>mnus37f/1)l7V?f7<sfL#"l+$+,97PY*dPiF5k+8aqWq&,es%.X:1p&fCPlVuaBf^"'eZMA\R7q^'NcG+H2aqiWNX%&mTKoKjfk;?!a$&UIqN$3kU;q5!_bZ:F5:P4#pr-:^$ijCS\5;#^YI^F5t%%<hegP>'JeD%5p4q6NVHQ?WCWt7PJUURYE+C)H$PE_n&urVDJ(ichrbP'>Z-_=)9N8UA3s]#KrJ,i:<<nXF:*XLAT1:T&QucL]]D*l]"KDt(s#Z,PfZ?D?qqImcP&(eLG*>RO14:bW8B#TDm,\JoJDs/c7*"2=%_G][2F[4B_K0`7]E#5,Fr#]V+K,3J.ZIC)(g9#Y<6?A>)t,(?@=(Uk+Yh*Pff]!rj_6_jWnJnH#m0(LU+j%Dd1ijPg:,QlRQsh>ObfE@EQ2]Dn=rh[9B+6S0T[P($\VXqGV<2Op<9]96;_kbR,5%"*6"loKB6]o)7kKoEE^Ip!K9Bi_eo(egeFq.ODpQqV(Ds#kcfsBl@7ss1"_dAF`-_N#oheiI^qh3WrU7NC']hK9/Q)o1ACK'jgG>KjheF$q:r8U*%8ic/Qp00k<g<@Qd9uj&g!RL+>`0`dAa1=Gk5kFcSl:ZsCSh3c$1cRl8\l0`ftNJN;$Ogb99Kolp9>GtY;sOh#cuF1!0C]:BSZbmcNsc@8$b>bfoiJfdR0XFA=9D;e&k%9L9TPuc>i`@].V"AY,!4J@&e35>@U=g!*oI0PCpr$\'>5#Fc_,%qH2S44bSHk6>UW,#8g#E=35=Y_Rg~>endstream | |
| endobj | |
| xref | |
| 0 22 | |
| 0000000000 65535 f | |
| 0000000061 00000 n | |
| 0000000132 00000 n | |
| 0000000239 00000 n | |
| 0000000351 00000 n | |
| 0000000428 00000 n | |
| 0000000633 00000 n | |
| 0000000738 00000 n | |
| 0000000821 00000 n | |
| 0000001026 00000 n | |
| 0000001231 00000 n | |
| 0000001437 00000 n | |
| 0000001643 00000 n | |
| 0000001849 00000 n | |
| 0000001919 00000 n | |
| 0000002200 00000 n | |
| 0000002293 00000 n | |
| 0000004379 00000 n | |
| 0000006823 00000 n | |
| 0000008921 00000 n | |
| 0000011264 00000 n | |
| 0000013080 00000 n | |
| trailer | |
| << | |
| /ID | |
| [<57c82911784a4bfa49da6ee2c5adcc93><57c82911784a4bfa49da6ee2c5adcc93>] | |
| % ReportLab generated PDF document -- digest (opensource) | |
| /Info 14 0 R | |
| /Root 13 0 R | |
| /Size 22 | |
| >> | |
| startxref | |
| 14859 | |
| %%EOF |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ================================================================ | |
| IR MQTT BRIDGE — CLAUDE SESSION CONTEXT FILE | |
| Upload this file to a new Claude session to resume work | |
| exactly where this conversation left off. | |
| ================================================================ | |
| PROJECT SUMMARY | |
| --------------- | |
| We designed and built complete Arduino firmware for an ESP8266MOD | |
| board that acts as a two-way IR / MQTT bridge. The firmware is | |
| complete, compiled, flashed, and confirmed working. | |
| The user has the following files already generated: | |
| - ir_mqtt_esp8266.ino (complete Arduino sketch, working) | |
| - IR_MQTT_Bridge_UserGuide.pdf (user guide including Node-RED section) | |
| - IR_MQTT_Bridge_ClaudeContext.txt (this file) | |
| ================================================================ | |
| CONFIRMED HARDWARE | |
| ================================================================ | |
| Board: ESP_IR_TR_WIFI (303ESPIRTR3) | |
| AliExpress ESP-12F based IR transceiver board | |
| Chip marking reads ESP8266MOD on the module | |
| USB-C port present but requires CH340 driver on Win11 | |
| NO physical buttons on this board | |
| IR Receiver: Built into the board (38kHz demodulator) | |
| IR LED: Built into the board (with transistor driver) | |
| Status LED: GPIO2 (D4) onboard — blinks during AP mode | |
| Board MAC suffix (permanent, confirmed): A7A272 | |
| RX topic : ir/A7A272/rx | |
| TX topic : ir/A7A272/tx | |
| Status : ir/A7A272/status | |
| Pin Definitions (top of .ino file): | |
| #define IR_RECV_PIN 14 // D5 | |
| #define IR_SEND_PIN 4 // D2 | |
| #define CONFIG_LED 2 // D4 | |
| ================================================================ | |
| FLASHING / UPLOAD PROCEDURE | |
| ================================================================ | |
| Programmer: Prolific USB-to-TTL adapter | |
| Windows port: COM12 | |
| Wiring: TX -> RX, RX -> TX, GND -> GND, 3.3V -> 3.3V | |
| (DO NOT use 5V — ESP-12F is 3.3V only) | |
| To enter flash/bootloader mode (no buttons on this board): | |
| 1. Locate the IO0 and GND pads on the board header | |
| 2. Bridge IO0 to GND with a jumper wire | |
| 3. Apply power (plug in TTL adapter) | |
| 4. Wait one second, remove the jumper | |
| 5. Click Upload in Arduino IDE | |
| Arduino IDE settings (confirmed working): | |
| Board : NodeMCU 1.0 (ESP-12E Module) | |
| Port : COM12 | |
| Reset Method : nodemcu | |
| Upload Speed : 115200 | |
| Flash Mode : DOUT | |
| Flash Size : 4MB (FS:2MB OTA:~1019KB) | |
| Memory usage at compile (confirmed): | |
| RAM (globals) : 42% used — fine | |
| Flash (code) : 36% used — fine | |
| IRAM : 94% used — watch if adding large features (OTA etc) | |
| ================================================================ | |
| ARDUINO LIBRARIES REQUIRED | |
| ================================================================ | |
| Install all via Arduino IDE -> Tools -> Manage Libraries: | |
| 1. IRremoteESP8266 by crankyoldgit | |
| Purpose: IR send and receive, protocol decode/encode | |
| Supports NEC, Sony, Samsung, Panasonic, RC5, RC6, and 100+ more | |
| 2. PubSubClient by Nick O'Leary | |
| Purpose: MQTT client for ESP8266/ESP32 | |
| 3. ArduinoJson by Benoit Blanchon (VERSION 6 or later) | |
| Purpose: JSON serialisation and deserialisation | |
| Built-in (no install needed): | |
| - LittleFS (part of ESP8266 Arduino core) | |
| - ESP8266WiFi (part of ESP8266 Arduino core) | |
| - ESP8266WebServer (part of ESP8266 Arduino core) | |
| ================================================================ | |
| BUGS FIXED FROM ORIGINAL SKETCH | |
| ================================================================ | |
| BUG 1: PubSubClient setWill() does not exist | |
| Error: 'class PubSubClient' has no member named 'setWill' | |
| Fix: LWT passed directly into mqtt.connect() instead. | |
| Original broken code: | |
| mqtt.setWill(topicStatus.c_str(), lwtPayload.c_str(), true, 1); | |
| connected = mqtt.connect(clientID.c_str(), ...); | |
| Fixed code (in connectMQTT()): | |
| connected = mqtt.connect( | |
| clientID.c_str(), | |
| cfg.brokerUser, cfg.brokerPass, // or nullptr, nullptr | |
| topicStatus.c_str(), 1, true, | |
| lwtPayload.c_str() | |
| ); | |
| BUG 2: AP mode required 5 boot failures even on fresh flash | |
| Problem: RTC memory can reset between power cycles on this board, | |
| making the fail counter unreliable. Fresh flash had no | |
| config so it would loop forever rather than enter AP mode. | |
| Fix: Added immediate AP mode trigger if wifiSSID is empty. | |
| Added to setup() before the fail counter check: | |
| if (strlen(cfg.wifiSSID) == 0 || rtcData.failCount >= BOOT_FAIL_THRESHOLD) { | |
| if (strlen(cfg.wifiSSID) == 0) { | |
| Serial.println("[Boot] No SSID configured — entering AP config mode"); | |
| } else { | |
| Serial.println("[Boot] Threshold reached — entering AP config mode"); | |
| } | |
| launchAPMode(); | |
| return; | |
| } | |
| ================================================================ | |
| MQTT TOPIC DESIGN | |
| ================================================================ | |
| Topics use the last 6 hex chars of the board WiFi MAC address | |
| as a unique suffix. Confirmed MAC suffix for this board: A7A272 | |
| ir/{MAC}/rx ESP publishes received IR signals here | |
| ir/{MAC}/tx ESP subscribes — publish IR commands here | |
| ir/{MAC}/status Online/offline heartbeat (retained, QoS 1) | |
| The MAC suffix is printed to Serial on every boot: | |
| MAC suffix : A7A272 | |
| ================================================================ | |
| MQTT PAYLOAD FORMAT | |
| ================================================================ | |
| RX PAYLOAD (ESP to Broker, on ir/A7A272/rx): | |
| { | |
| "protocol": "NEC", | |
| "bits": 32, | |
| "value": "0x20DF10EF", | |
| "address": "0x04", | |
| "command": "0x08", | |
| "repeat": false, | |
| "raw": [9000, 4500, 560, 1690, 560, 560, ...] | |
| } | |
| TX PAYLOAD decoded protocol (Broker to ESP, on ir/A7A272/tx): | |
| { | |
| "protocol": "NEC", | |
| "bits": 32, | |
| "value": "0x20DF10EF", | |
| "repeat": 0 | |
| } | |
| TX PAYLOAD raw fallback: | |
| { | |
| "protocol": "RAW", | |
| "raw": [9000, 4500, 560, 1690, 560, 560], | |
| "khz": 38 | |
| } | |
| STATUS / LWT PAYLOADS (ir/A7A272/status, retained): | |
| Online: { "status": "online", "mac": "A7A272", "ip": "192.168.1.42" } | |
| Offline: { "status": "offline", "mac": "A7A272" } | |
| LWT is set with retain=true, QoS=1. Broker sends offline message | |
| automatically if the board drops without a clean disconnect. | |
| Heartbeat published every 60 seconds. | |
| ================================================================ | |
| CONFIGURATION / AP MODE DESIGN | |
| ================================================================ | |
| Config stored in LittleFS as /config.json: | |
| { | |
| "wifiSSID": "MyNetwork", | |
| "wifiPass": "mypassword", | |
| "brokerIP": "192.168.1.10", | |
| "brokerPort": 1883, | |
| "brokerUser": "", | |
| "brokerPass": "" | |
| } | |
| AP MODE TRIGGERS (either condition launches AP mode): | |
| 1. No SSID stored (fresh flash / wiped config) | |
| 2. Boot fail counter >= 5 consecutive failures | |
| Fail counter stored in RTC memory (survives reboot, not power loss) | |
| Note: RTC memory can be unreliable on this specific board between | |
| power cycles. The no-SSID check is the more reliable trigger. | |
| AP MODE DETAILS: | |
| SSID: IR-Config-A7A272 | |
| Password: irconfig | |
| IP: 192.168.4.1 | |
| Portal: http://192.168.4.1 (lightweight custom HTML form) | |
| On save: Writes config.json, resets fail counter, reboots | |
| ================================================================ | |
| BOOT SEQUENCE LOGIC | |
| ================================================================ | |
| Boot | |
| | | |
| |-- Read RTC fail counter | |
| |-- Load config.json from LittleFS | |
| |-- If no SSID stored OR counter >= 5 -> Launch AP mode (stops here) | |
| | | |
| |-- Attempt WiFi (10s timeout) | |
| | |-- Fail -> increment counter, write RTC, restart | |
| | | |
| |-- Attempt MQTT connect (with LWT passed into connect()) | |
| | |-- Fail -> increment counter, write RTC, restart | |
| | | |
| |-- Success -> reset counter to 0, write RTC | |
| |-- Publish online status (retained) | |
| |-- Subscribe to ir/A7A272/tx | |
| |-- Start IR receiver and sender | |
| ================================================================ | |
| MAIN LOOP BEHAVIOUR | |
| ================================================================ | |
| - If AP mode: serve web portal, blink LED at 500ms, nothing else | |
| - If normal mode: | |
| - Maintain MQTT connection (reconnect if dropped) | |
| - If MQTT reconnect fails: increment fail counter, reboot | |
| - Check IR receiver for incoming signals, publish JSON to rx topic | |
| - Handle MQTT messages on tx topic, fire IR signal | |
| - Publish heartbeat to status topic every 60 seconds | |
| ================================================================ | |
| NODE-RED INTEGRATION | |
| ================================================================ | |
| Catch all IR from all boards: | |
| MQTT In node, Topic: ir/#, QoS: 1 | |
| Wire to Debug node | |
| Catch only received IR from all boards: | |
| MQTT In node, Topic: ir/+/rx, QoS: 1 | |
| Wire to JSON node then Debug node | |
| Send to one specific board: | |
| Inject node, Payload: {"protocol":"NEC","bits":32,"value":"0x20DF10EF","repeat":0} | |
| MQTT Out node, Topic: ir/A7A272/tx, QoS: 1 | |
| Send to all boards (Function node approach): | |
| var boards = ["A7A272", "B1C2D3"]; | |
| var msgs = boards.map(function(mac) { | |
| return { topic: "ir/" + mac + "/tx", payload: msg.payload }; | |
| }); | |
| return msgs; | |
| Wire each output to an MQTT Out node (topic set from msg.topic) | |
| Learn a button (capture and replay): | |
| 1. Subscribe to ir/+/rx | |
| 2. Point remote at board, press button | |
| 3. Copy JSON from Debug panel | |
| 4. Paste as payload of Inject node wired to ir/A7A272/tx | |
| ================================================================ | |
| THINGS NOT YET DONE / POSSIBLE NEXT STEPS | |
| ================================================================ | |
| 1. OTA (Over-the-Air) firmware updates | |
| - Add ArduinoOTA library so you can reflash over WiFi | |
| - Useful once the board is mounted/installed | |
| - Would also reduce IRAM pressure concern | |
| 2. AC unit support | |
| - IRremoteESP8266 has special classes for AC protocols | |
| - e.g. IRDaikinESP, IRMitsubishiAC | |
| - These use structured objects (temperature, mode, fan speed) | |
| rather than simple hex values | |
| 3. Home Assistant MQTT Discovery | |
| - Auto-register device in HA without manual config.yaml edits | |
| - Publish to homeassistant/sensor/{MAC}/config on boot | |
| 4. Web UI for IR learning | |
| - Add a /learn page served in normal operating mode | |
| - Press a button on a remote, capture and display the code | |
| - No need to use Serial Monitor or Node-RED to learn codes | |
| 5. Button-triggered AP mode | |
| - This board has no buttons, but IO0 pad could be wired to one | |
| - Hold IO0 to GND for 3-5 seconds to trigger AP mode | |
| - Faster recovery than waiting for 5 boot failures | |
| 6. Multiple IR zones | |
| - Multiple boards, each with unique MAC suffix | |
| - Subscribe to ir/+/rx in Node-RED / HA to catch all of them | |
| - Each board independently connected and identified | |
| 7. IR repeat / blaster mode | |
| - Some devices need IR repeated N times to register | |
| - Could be added as a "repeat_count" field in tx payload | |
| ================================================================ | |
| REFERENCE: KEY CODE SECTIONS IN ir_mqtt_esp8266.ino | |
| ================================================================ | |
| setup() -- Boot sequence, MAC derivation, WiFi, MQTT init | |
| loop() -- AP portal handler, MQTT keepalive, IR receive loop | |
| publishIR() -- Builds and publishes rx JSON payload | |
| sendIRFromJSON() -- Parses tx JSON and fires IR signal | |
| mqttCallback() -- Called by PubSubClient on incoming tx message | |
| connectWiFi() -- WiFi connection with 10s timeout | |
| connectMQTT() -- MQTT connect with LWT inside connect(), publishes online | |
| launchAPMode() -- Starts AP, registers web routes | |
| handleAPRoot() -- Serves HTML config form | |
| handleAPSave() -- Processes form POST, saves config, reboots | |
| loadConfig() -- Reads /config.json from LittleFS | |
| saveConfig() -- Writes /config.json to LittleFS | |
| readRTC() -- Reads boot fail counter from RTC memory | |
| writeRTC() -- Writes boot fail counter to RTC memory | |
| ================================================================ | |
| HOW TO USE THIS FILE IN A NEW CLAUDE SESSION | |
| ================================================================ | |
| Upload ir_mqtt_esp8266.ino AND this context file to a new Claude | |
| chat, then say something like: | |
| "Here is the context file and sketch from a previous session. | |
| I need help with [your question]." | |
| Claude will have everything it needs to: | |
| - Continue debugging the sketch | |
| - Add new features (see next steps above) | |
| - Modify pin assignments or topic names | |
| - Help with Node-RED or Home Assistant integration | |
| - Explain any part of the code | |
| ================================================================ | |
| END OF CONTEXT FILE | |
| ================================================================ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment