/* Session 20 — Over-The-Air (OTA) firmware updates + health heartbeat (ESP32) After the first USB upload, the node appears in Arduino IDE -> Tools -> Port -> "kfs-field-a at 192.168.1.x". Upload again over Wi-Fi without removing the node from the field. The OTA password protects it. Heartbeat: every 5 min publish uptime, free heap, RSSI, reset reason, firmware version (retained MQTT). */ #include #include #include #include #define FW_VERSION "1.4.0" const char* WIFI_SSID = "KFS-Farm-IoT"; const char* WIFI_PASS = "change-me"; const char* HOSTNAME = "kfs-field-a"; const char* OTA_PASS = "farm-ota-2026"; const char* BROKER = "192.168.1.10"; WiFiClient net; PubSubClient mqtt(net); unsigned long lastBeat = 0; void setup() { Serial.begin(115200); WiFi.setHostname(HOSTNAME); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) delay(300); ArduinoOTA.setHostname(HOSTNAME); ArduinoOTA.setPassword(OTA_PASS); ArduinoOTA.onStart([]() { digitalWrite(26, HIGH); Serial.println("OTA start - pump relay forced OFF"); }); ArduinoOTA.onProgress([](unsigned int p, unsigned int t) { Serial.printf("OTA %u%%\r", p * 100 / t); }); ArduinoOTA.onEnd([]() { Serial.println("\nOTA done, rebooting"); }); ArduinoOTA.onError([](ota_error_t e) { Serial.printf("OTA error %u\n", e); }); ArduinoOTA.begin(); mqtt.setServer(BROKER, 1883); Serial.printf("fw %s ready at %s\n", FW_VERSION, WiFi.localIP().toString().c_str()); } void loop() { ArduinoOTA.handle(); // must be called often if (!mqtt.connected()) mqtt.connect(HOSTNAME); mqtt.loop(); if (millis() - lastBeat > 300000 || lastBeat == 0) { lastBeat = millis(); char buf[200]; snprintf(buf, sizeof(buf), "{\"fw\":\"%s\",\"uptime_s\":%lu,\"heap\":%u,\"rssi\":%d,\"reset_reason\":%d}", FW_VERSION, millis() / 1000, ESP.getFreeHeap(), WiFi.RSSI(), (int)esp_reset_reason()); mqtt.publish("kfs/farm/field_A/health", buf, true); // retained: the dashboard always sees the last state } }