// IoT-02_OtaMqttUpdater_02.ino // // Actualitzacio remota de firmware (OTA) via MQTT, sense webserver embegut. // // Funcionament: // - Es connecta a la WiFi (llista de xarxes a IoT-02_wifiCredentials.h) // i al broker MQTT (credencials a IoT-02_mqttCredentials.h). // - Es subscriu al topic /{MAC}/urlOtaBin // - El payload d'aquest topic ha de ser una URL HTTP/HTTPS que apunti // al binari de l'APLICACIO (fitxer *.ino.bin), MAI al *.ino.merged.bin // (aquest darrer inclou bootloader + partition table i NOMES serveix // per flashejar per USB/esptool; si s'escriu via Update.h a la particio // OTA es deixa la placa inservible). // Exemple de payload valid: // https://www.binefa.cat/IoT/IoT-02/codes/OTA/IoT-02_OTAWebUpdater_02b/build/esp32.esp32.esp32/IoT-02_OTAWebUpdater_02b.ino.bin // // - Abans de flashejar, es compara la URL rebuda amb l'ultima URL aplicada // amb exit (guardada a la NVS/Preferences). Si coincideix, s'ignora. // Aixo evita bucles d'actualitzacio si el missatge MQTT queda "retained" // al broker i el dispositiu el torna a rebre en reconnectar/reiniciar. // - L'estat de l'operacio (BOOT/START/OK/FAIL/SKIP) es publica a // /{MAC}/otaStatus, per poder seguir el proces sense consola serie. // // AVIS DE SEGURETAT: // Qualsevol client que pugui publicar al topic /{MAC}/urlOtaBin pot forçar // la placa a descarregar i executar el binari que indiqui. Amb un broker // public (com broker.emqx.io, usat aqui com a exemple) i credencials // trivials, aixo NO es segur per a un desplegament real. Per a producio, // useu un broker propi amb ACL per topic/usuari, i considereu restringir // els dominis acceptats a vBuildOtaTopics()/vDoOta(). // // Codi escrit per Jordi Binefa. #include #include #include #include #include #include #include "IoT-02_pinout.h" #include "IoT-02_wifiMng.h" #include "IoT-02_mqttCredentials.h" #define OTA_LED LED_G // IoT-02_OtaMqttUpdater_01 usa LED_W per distingir la placa #define MAC_SIZE 15 char sMac[MAC_SIZE]; #define TOPIC_OTA_URL "/urlOtaBin" // payload: URL del *.ino.bin a flashejar #define TOPIC_OTA_STATUS "/otaStatus" // sortida: BOOT / START / OK / FAIL / SKIP #define NVS_NAMESPACE "ota" #define NVS_KEY_LAST_URL "lastUrl" WiFiClient espClient; PubSubClient client(espClient); Preferences preferences; String szTopicOtaUrl, szTopicOtaStatus; unsigned long ulPreviousBlinkMillis = 0; const long lBlinkIntervalIdle = 1000; // parpelleig lent = en marxa, esperant const long lBlinkIntervalBusy = 100; // (no usat directament, OTA_LED queda fix en ON durant l'update) bool bUpdating = false; void vPublishStatus(const String &szMsg) { Serial.print("OTA status: "); Serial.println(szMsg); client.publish(szTopicOtaStatus.c_str(), szMsg.c_str()); } void vDoOta(const String &szUrl) { bUpdating = true; digitalWrite(OTA_LED, HIGH); String szLastUrl = preferences.getString(NVS_KEY_LAST_URL, ""); if (szUrl == szLastUrl) { vPublishStatus("SKIP: mateixa URL que la ultima aplicada"); bUpdating = false; digitalWrite(OTA_LED, LOW); return; } // Guardem la URL ABANS d'intentar l'OTA: si l'actualitzacio reeixeix, // HTTPUpdate reinicia el dispositiu tot seguit i no hi haura ocasio // d'escriure-la despres. preferences.putString(NVS_KEY_LAST_URL, szUrl); vPublishStatus("START: " + szUrl); httpUpdate.rebootOnUpdate(true); t_httpUpdate_return ret; if (szUrl.startsWith("https://")) { WiFiClientSecure secureClient; secureClient.setInsecure(); // No es valida el certificat del servidor (simplicitat) ret = httpUpdate.update(secureClient, szUrl); } else { WiFiClient plainClient; ret = httpUpdate.update(plainClient, szUrl); } switch (ret) { case HTTP_UPDATE_FAILED: vPublishStatus("FAIL (" + String(httpUpdate.getLastError()) + "): " + httpUpdate.getLastErrorString()); break; case HTTP_UPDATE_NO_UPDATES: vPublishStatus("NO_UPDATES"); break; case HTTP_UPDATE_OK: // Normalment no s'arriba aqui perque rebootOnUpdate ja ha reiniciat la placa. vPublishStatus("OK, reiniciant..."); delay(200); ESP.restart(); break; } bUpdating = false; digitalWrite(OTA_LED, LOW); } void receivedCallback(char* topic, byte* payload, unsigned int length) { String szTopic = String(topic), szPayload = ""; for (unsigned int i = 0; i < length; i++) szPayload += (char)payload[i]; szPayload.trim(); Serial.print("Topic: "); Serial.println(szTopic); Serial.print("Payload: "); Serial.println(szPayload); if (szTopic == szTopicOtaUrl && szPayload.length() > 0) { vDoOta(szPayload); } } void mqttconnect() { while (!client.connected()) { Serial.print("MQTT connecting ..."); String szClientId = "IoT-02_OTA_" + String(sMac); if (client.connect(szClientId.c_str(), mqtt_user, mqtt_password)) { Serial.println("connected"); client.subscribe(szTopicOtaUrl.c_str()); vPublishStatus("BOOT"); } else { Serial.print("failed, status code ="); Serial.print(client.state()); Serial.println(" try again in 5 seconds"); delay(5000); } } } void setup() { Serial.begin(115200); Serial.println(__FILE__); pinMode(OTA_LED, OUTPUT); digitalWrite(OTA_LED, LOW); preferences.begin(NVS_NAMESPACE, false); vSetupWifi(); szGetMac().toCharArray(sMac, MAC_SIZE); Serial.print("MAC: "); Serial.println(sMac); szTopicOtaUrl = "/" + String(sMac) + TOPIC_OTA_URL; szTopicOtaStatus = "/" + String(sMac) + TOPIC_OTA_STATUS; client.setServer(mqtt_server, mqtt_port); client.setCallback(receivedCallback); } void loop() { if (WiFi.status() != WL_CONNECTED) { vSetupWifi(); } if (!client.connected()) { mqttconnect(); } client.loop(); if (!bUpdating) { unsigned long ulNow = millis(); if (ulNow - ulPreviousBlinkMillis >= lBlinkIntervalIdle) { ulPreviousBlinkMillis = ulNow; digitalWrite(OTA_LED, !digitalRead(OTA_LED)); } } }