diff --git a/.gitignore b/.gitignore index 2396616..d859523 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .pio/ .vscode/ -lib/ -include/ \ No newline at end of file +compile_commands.json diff --git a/README.md b/README.md index 2a549f3..274b622 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,78 @@ MicroClaw is the smallest member of the Claw family — a bare-metal AI agent th - **Mesh Communication** — ESP-NOW, LoRa, Zigbee, or UART to upstream agents - **OTA Updates** — Over-the-air firmware updates via PicClaw +## ESP32-C3 Port + +This repo includes a PlatformIO-based ESP32-C3 port that turns MicroClaw into a battery-powered sensor node: + +- DHT22 temperature/humidity driver with explicit read failure handling. +- MQTT reporting to `clawland/{node_id}/sensors/{sensor_type}`. +- Low-battery alerts to `clawland/{node_id}/alerts/critical`. +- Configurable wake -> read sensors -> report -> OTA window -> deep sleep cycle. +- ADC battery voltage monitoring through a 2:1 divider. +- Short ArduinoOTA maintenance window on every wake cycle. +- Wiring guide for DHT22, DS18B20, PIR, and relay modules. + +### Configure + +Edit `platformio.ini` build flags before flashing: + +| Build flag | Purpose | Default | +| --- | --- | --- | +| `MICROCLAW_NODE_ID` | Stable node id used in MQTT topics | `microclaw-esp32c3-001` | +| `MICROCLAW_WIFI_SSID` | Wi-Fi network name | `CHANGE_ME` | +| `MICROCLAW_WIFI_PASSWORD` | Wi-Fi password | `CHANGE_ME` | +| `MICROCLAW_MQTT_HOST` | PicoClaw/NanoClaw MQTT broker host | `192.168.1.10` | +| `MICROCLAW_MQTT_PORT` | MQTT broker port | `1883` | +| `MICROCLAW_DHT_PIN` | DHT22 data GPIO | `4` | +| `MICROCLAW_BATTERY_PIN` | ADC GPIO for battery divider | `0` | +| `MICROCLAW_LOW_BATTERY_MV` | Critical alert threshold | `3300` | +| `MICROCLAW_SLEEP_SECONDS` | Deep-sleep interval | `300` | +| `MICROCLAW_OTA_WINDOW_SECONDS` | OTA maintenance window per wake | `20` | + +### Build and Flash + +```sh +platformio run -e esp32c3 +platformio run -e esp32 +platformio run -e esp32c3 -t upload +platformio device monitor -e esp32c3 +``` + +### MQTT Payloads + +Status and sensor reports are retained JSON messages on the `clawland/{node_id}/...` topic tree: + +```json +{ + "node_id": "microclaw-esp32c3-001", + "state": "awake", + "battery_mv": 3812, + "sleep_seconds": 300, + "ota_window_seconds": 20, + "firmware": "0.2.0" +} +``` + +```json +{ + "node_id": "microclaw-esp32c3-001", + "sensor_type": "dht22", + "temperature_c": 24.1, + "humidity_percent": 61.4, + "battery_mv": 3812, + "firmware": "0.2.0" +} +``` + +Validation without ESP32 hardware: + +```sh +node scripts/validate-esp32-port.mjs +``` + +See [`docs/wiring.md`](docs/wiring.md) for sensor wiring and [`docs/telemetry-contract.md`](docs/telemetry-contract.md) for the MQTT topic and payload contract. + ## Supported Hardware | MCU | RAM | Flash | Price | Notes | diff --git a/docs/telemetry-contract.md b/docs/telemetry-contract.md new file mode 100644 index 0000000..f84129f --- /dev/null +++ b/docs/telemetry-contract.md @@ -0,0 +1,70 @@ +# MicroClaw MQTT Telemetry Contract + +This contract documents the retained MQTT messages emitted by the ESP32-C3 and ESP32 PlatformIO environments. + +## Topic Tree + +| Topic | Retained | Purpose | +| --- | --- | --- | +| `clawland/{node_id}/status` | yes | Wake/sleep lifecycle and firmware status. | +| `clawland/{node_id}/sensors/dht22` | yes | Latest DHT22 temperature and humidity reading. | +| `clawland/{node_id}/alerts/warning` | yes | Recoverable sensor or runtime warnings. | +| `clawland/{node_id}/alerts/critical` | yes | Battery or deployment-critical alerts. | + +`node_id` comes from `MICROCLAW_NODE_ID`, so each deployed board publishes to a stable namespace. + +## Status Payload + +```json +{ + "node_id": "microclaw-esp32c3-001", + "state": "awake", + "battery_mv": 3812, + "sleep_seconds": 300, + "ota_window_seconds": 20, + "firmware": "0.2.0" +} +``` + +Valid `state` values are `awake` and `sleeping`. + +## DHT22 Sensor Payload + +```json +{ + "node_id": "microclaw-esp32c3-001", + "sensor_type": "dht22", + "temperature_c": 24.1, + "humidity_percent": 61.4, + "battery_mv": 3812, + "firmware": "0.2.0" +} +``` + +## Alert Payload + +```json +{ + "node_id": "microclaw-esp32c3-001", + "code": "battery_low", + "battery_mv": 3290 +} +``` + +Known alert codes include `battery_low` and DHT22 read failure codes returned by the sensor wrapper. + +## Wake Cycle + +1. Wake from timer or reset. +2. Read battery and DHT22. +3. Connect Wi-Fi and MQTT. +4. Publish `status` with `state=awake`. +5. Publish sensor or warning payload. +6. Publish critical low-battery alert when needed. +7. Keep ArduinoOTA open for the configured maintenance window. +8. Publish `status` with `state=sleeping`. +9. Enter deep sleep for `MICROCLAW_SLEEP_SECONDS`. + +## Validation Notes + +The local validation script checks that both `esp32c3` and `esp32` PlatformIO environments are present, the telemetry contract is documented, and the firmware publishes status, sensor, and alert messages. Hardware behavior is not marked hardware-tested until the PR is run on a physical board. diff --git a/docs/wiring.md b/docs/wiring.md new file mode 100644 index 0000000..a231141 --- /dev/null +++ b/docs/wiring.md @@ -0,0 +1,50 @@ +# MicroClaw ESP32-C3 Wiring Guide + +This guide documents the default wiring used by the ESP32-C3 PlatformIO port. Pins can be changed through `platformio.ini` build flags. + +## Default Pin Reference + +| Signal | ESP32-C3 GPIO | Notes | +| --- | --- | --- | +| DHT22 data | GPIO4 | Add a 10 kOhm pull-up from DATA to 3V3. | +| Battery sense | GPIO0 ADC | Use a 2:1 divider for single-cell LiPo voltage. | +| DS18B20 data | GPIO5 | Add a 4.7 kOhm pull-up from DATA to 3V3. | +| PIR output | GPIO6 | Use 3V3-compatible PIR modules. | +| Relay control | GPIO7 | Drive through a transistor or opto-isolated relay board. | +| MQTT/Wi-Fi | onboard radio | Configure SSID and broker in `platformio.ini`. | + +## DHT22 Temperature/Humidity + +```text +ESP32-C3 3V3 -> DHT22 VCC +ESP32-C3 GND -> DHT22 GND +ESP32-C3 GPIO4 -> DHT22 DATA +3V3 -- 10 kOhm -- DATA +``` + +## DS18B20 Waterproof Temperature Probe + +```text +ESP32-C3 3V3 -> DS18B20 VDD +ESP32-C3 GND -> DS18B20 GND +ESP32-C3 GPIO5 -> DS18B20 DATA +3V3 -- 4.7 kOhm -- DATA +``` + +## PIR Motion Sensor + +```text +ESP32-C3 3V3 -> PIR VCC +ESP32-C3 GND -> PIR GND +ESP32-C3 GPIO6 <- PIR OUT +``` + +## Relay Module + +```text +ESP32-C3 GPIO7 -> relay IN +ESP32-C3 GND -> relay GND +Relay VCC -> external 5V or module-rated supply +``` + +Do not power pumps, heaters, or aerators directly from the ESP32-C3 board. Use a rated relay or MOSFET module and isolate high-voltage loads. diff --git a/include/MicroClawConfig.h b/include/MicroClawConfig.h new file mode 100644 index 0000000..880b1ae --- /dev/null +++ b/include/MicroClawConfig.h @@ -0,0 +1,52 @@ +#pragma once + +#ifndef MICROCLAW_NODE_ID +#define MICROCLAW_NODE_ID "microclaw-esp32c3-001" +#endif + +#ifndef MICROCLAW_WIFI_SSID +#define MICROCLAW_WIFI_SSID "CHANGE_ME" +#endif + +#ifndef MICROCLAW_WIFI_PASSWORD +#define MICROCLAW_WIFI_PASSWORD "CHANGE_ME" +#endif + +#ifndef MICROCLAW_MQTT_HOST +#define MICROCLAW_MQTT_HOST "192.168.1.10" +#endif + +#ifndef MICROCLAW_MQTT_PORT +#define MICROCLAW_MQTT_PORT 1883 +#endif + +#ifndef MICROCLAW_DHT_PIN +#define MICROCLAW_DHT_PIN 4 +#endif + +#ifndef MICROCLAW_BATTERY_PIN +#define MICROCLAW_BATTERY_PIN 0 +#endif + +#ifndef MICROCLAW_LOW_BATTERY_MV +#define MICROCLAW_LOW_BATTERY_MV 3300 +#endif + +#ifndef MICROCLAW_SLEEP_SECONDS +#define MICROCLAW_SLEEP_SECONDS 300 +#endif + +#ifndef MICROCLAW_OTA_WINDOW_SECONDS +#define MICROCLAW_OTA_WINDOW_SECONDS 20 +#endif + +constexpr const char* kNodeId = MICROCLAW_NODE_ID; +constexpr const char* kWifiSsid = MICROCLAW_WIFI_SSID; +constexpr const char* kWifiPassword = MICROCLAW_WIFI_PASSWORD; +constexpr const char* kMqttHost = MICROCLAW_MQTT_HOST; +constexpr uint16_t kMqttPort = MICROCLAW_MQTT_PORT; +constexpr uint8_t kDhtPin = MICROCLAW_DHT_PIN; +constexpr uint8_t kBatteryPin = MICROCLAW_BATTERY_PIN; +constexpr uint16_t kLowBatteryMv = MICROCLAW_LOW_BATTERY_MV; +constexpr uint32_t kSleepSeconds = MICROCLAW_SLEEP_SECONDS; +constexpr uint32_t kOtaWindowSeconds = MICROCLAW_OTA_WINDOW_SECONDS; diff --git a/lib/BatteryMonitor/BatteryMonitor.cpp b/lib/BatteryMonitor/BatteryMonitor.cpp new file mode 100644 index 0000000..c498bb8 --- /dev/null +++ b/lib/BatteryMonitor/BatteryMonitor.cpp @@ -0,0 +1,24 @@ +#include "BatteryMonitor.h" + +BatteryMonitor::BatteryMonitor(uint8_t adcPin, float dividerRatio) + : adcPin_(adcPin), dividerRatio_(dividerRatio) {} + +void BatteryMonitor::begin() const { + pinMode(adcPin_, INPUT); + analogReadResolution(12); +} + +uint16_t BatteryMonitor::readMillivolts(uint8_t samples) const { + uint32_t total = 0; + const uint8_t sampleCount = samples == 0 ? 1 : samples; + for (uint8_t i = 0; i < sampleCount; i++) { + total += analogReadMilliVolts(adcPin_); + delay(4); + } + const float averageMv = static_cast(total) / sampleCount; + return static_cast(averageMv * dividerRatio_); +} + +bool BatteryMonitor::isLow(uint16_t millivolts, uint16_t thresholdMv) const { + return millivolts > 0 && millivolts < thresholdMv; +} diff --git a/lib/BatteryMonitor/BatteryMonitor.h b/lib/BatteryMonitor/BatteryMonitor.h new file mode 100644 index 0000000..8dca6d2 --- /dev/null +++ b/lib/BatteryMonitor/BatteryMonitor.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class BatteryMonitor { + public: + BatteryMonitor(uint8_t adcPin, float dividerRatio); + + void begin() const; + uint16_t readMillivolts(uint8_t samples = 8) const; + bool isLow(uint16_t millivolts, uint16_t thresholdMv) const; + + private: + uint8_t adcPin_; + float dividerRatio_; +}; diff --git a/lib/DHT22Sensor/DHT22Sensor.cpp b/lib/DHT22Sensor/DHT22Sensor.cpp new file mode 100644 index 0000000..eab981a --- /dev/null +++ b/lib/DHT22Sensor/DHT22Sensor.cpp @@ -0,0 +1,16 @@ +#include "DHT22Sensor.h" + +DHT22Sensor::DHT22Sensor(uint8_t pin) : dht_(pin, DHT22) {} + +void DHT22Sensor::begin() { + dht_.begin(); +} + +DHT22Reading DHT22Sensor::read() { + const float humidity = dht_.readHumidity(); + const float temperature = dht_.readTemperature(); + if (isnan(humidity) || isnan(temperature)) { + return {false, 0.0f, 0.0f, "dht22_read_failed"}; + } + return {true, temperature, humidity, ""}; +} diff --git a/lib/DHT22Sensor/DHT22Sensor.h b/lib/DHT22Sensor/DHT22Sensor.h new file mode 100644 index 0000000..a049242 --- /dev/null +++ b/lib/DHT22Sensor/DHT22Sensor.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +struct DHT22Reading { + bool ok; + float temperatureC; + float humidityPercent; + const char* error; +}; + +class DHT22Sensor { + public: + explicit DHT22Sensor(uint8_t pin); + + void begin(); + DHT22Reading read(); + + private: + DHT dht_; +}; diff --git a/lib/MicroClawMqtt/MicroClawMqtt.cpp b/lib/MicroClawMqtt/MicroClawMqtt.cpp new file mode 100644 index 0000000..f7005aa --- /dev/null +++ b/lib/MicroClawMqtt/MicroClawMqtt.cpp @@ -0,0 +1,42 @@ +#include "MicroClawMqtt.h" + +MicroClawMqtt::MicroClawMqtt(const char* nodeId, const char* host, uint16_t port) + : nodeId_(nodeId), host_(host), port_(port), mqtt_(wifi_) {} + +void MicroClawMqtt::begin() { + mqtt_.setServer(host_, port_); + mqtt_.setBufferSize(512); +} + +bool MicroClawMqtt::ensureConnected(uint32_t timeoutMs) { + const uint32_t deadline = millis() + timeoutMs; + while (!mqtt_.connected() && millis() < deadline) { + if (mqtt_.connect(nodeId_)) { + return true; + } + delay(400); + } + return mqtt_.connected(); +} + +bool MicroClawMqtt::publishStatus(const String& json) { + return ensureConnected() && mqtt_.publish(topic("status").c_str(), json.c_str(), true); +} + +bool MicroClawMqtt::publishSensor(const char* sensorType, const String& json) { + const String suffix = String("sensors/") + sensorType; + return ensureConnected() && mqtt_.publish(topic(suffix.c_str()).c_str(), json.c_str(), true); +} + +bool MicroClawMqtt::publishAlert(const char* level, const String& json) { + const String suffix = String("alerts/") + level; + return ensureConnected() && mqtt_.publish(topic(suffix.c_str()).c_str(), json.c_str(), true); +} + +void MicroClawMqtt::loop() { + mqtt_.loop(); +} + +String MicroClawMqtt::topic(const char* suffix) const { + return String("clawland/") + nodeId_ + "/" + suffix; +} diff --git a/lib/MicroClawMqtt/MicroClawMqtt.h b/lib/MicroClawMqtt/MicroClawMqtt.h new file mode 100644 index 0000000..21e1129 --- /dev/null +++ b/lib/MicroClawMqtt/MicroClawMqtt.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class MicroClawMqtt { + public: + MicroClawMqtt(const char* nodeId, const char* host, uint16_t port); + + void begin(); + bool ensureConnected(uint32_t timeoutMs = 8000); + bool publishStatus(const String& json); + bool publishSensor(const char* sensorType, const String& json); + bool publishAlert(const char* level, const String& json); + void loop(); + + private: + String topic(const char* suffix) const; + + const char* nodeId_; + const char* host_; + uint16_t port_; + WiFiClient wifi_; + PubSubClient mqtt_; +}; diff --git a/platformio.ini b/platformio.ini index 640e4e9..0d6416f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,3 +1,24 @@ +[env:esp32c3] +platform = espressif32 +board = esp32-c3-devkitm-1 +framework = arduino +monitor_speed = 115200 +lib_deps = + knolleary/PubSubClient@^2.8 + adafruit/DHT sensor library@^1.4 + adafruit/Adafruit Unified Sensor@^1.1 +build_flags = + -D MICROCLAW_NODE_ID=\"microclaw-esp32c3-001\" + -D MICROCLAW_WIFI_SSID=\"CHANGE_ME\" + -D MICROCLAW_WIFI_PASSWORD=\"CHANGE_ME\" + -D MICROCLAW_MQTT_HOST=\"192.168.1.10\" + -D MICROCLAW_MQTT_PORT=1883 + -D MICROCLAW_DHT_PIN=4 + -D MICROCLAW_BATTERY_PIN=0 + -D MICROCLAW_LOW_BATTERY_MV=3300 + -D MICROCLAW_SLEEP_SECONDS=300 + -D MICROCLAW_OTA_WINDOW_SECONDS=20 + [env:esp32] platform = espressif32 board = esp32dev @@ -6,4 +27,15 @@ monitor_speed = 115200 lib_deps = knolleary/PubSubClient@^2.8 adafruit/DHT sensor library@^1.4 - adafruit/Adafruit Unified Sensor@^1.1 \ No newline at end of file + adafruit/Adafruit Unified Sensor@^1.1 +build_flags = + -D MICROCLAW_NODE_ID=\"microclaw-esp32-001\" + -D MICROCLAW_WIFI_SSID=\"CHANGE_ME\" + -D MICROCLAW_WIFI_PASSWORD=\"CHANGE_ME\" + -D MICROCLAW_MQTT_HOST=\"192.168.1.10\" + -D MICROCLAW_MQTT_PORT=1883 + -D MICROCLAW_DHT_PIN=4 + -D MICROCLAW_BATTERY_PIN=34 + -D MICROCLAW_LOW_BATTERY_MV=3300 + -D MICROCLAW_SLEEP_SECONDS=300 + -D MICROCLAW_OTA_WINDOW_SECONDS=20 diff --git a/scripts/validate-esp32-port.mjs b/scripts/validate-esp32-port.mjs new file mode 100644 index 0000000..ec640de --- /dev/null +++ b/scripts/validate-esp32-port.mjs @@ -0,0 +1,46 @@ +import fs from "node:fs"; + +const requiredFiles = [ + "platformio.ini", + "src/main.cpp", + "include/MicroClawConfig.h", + "lib/DHT22Sensor/DHT22Sensor.h", + "lib/DHT22Sensor/DHT22Sensor.cpp", + "lib/MicroClawMqtt/MicroClawMqtt.h", + "lib/MicroClawMqtt/MicroClawMqtt.cpp", + "lib/BatteryMonitor/BatteryMonitor.h", + "lib/BatteryMonitor/BatteryMonitor.cpp", + "docs/wiring.md", + "docs/telemetry-contract.md", +]; + +const requiredTerms = [ + ["platformio.ini", "esp32-c3-devkitm-1"], + ["platformio.ini", "esp32dev"], + ["platformio.ini", "PubSubClient"], + ["src/main.cpp", "ArduinoOTA"], + ["src/main.cpp", "esp_deep_sleep_start"], + ["src/main.cpp", "publishStatus"], + ["src/main.cpp", "publishSensor"], + ["src/main.cpp", "battery_low"], + ["lib/DHT22Sensor/DHT22Sensor.cpp", "DHT22"], + ["lib/MicroClawMqtt/MicroClawMqtt.cpp", "clawland/"], + ["docs/wiring.md", "DHT22"], + ["docs/wiring.md", "Relay"], + ["docs/telemetry-contract.md", "clawland/{node_id}/status"], + ["docs/telemetry-contract.md", "hardware-tested"], +]; + +const missing = requiredFiles.filter((file) => !fs.existsSync(file)); +if (missing.length) { + throw new Error(`Missing required files: ${missing.join(", ")}`); +} + +for (const [file, term] of requiredTerms) { + const text = fs.readFileSync(file, "utf8"); + if (!text.includes(term)) { + throw new Error(`${file} does not include required term: ${term}`); + } +} + +console.log("MicroClaw ESP32-C3 port structure validated"); diff --git a/src/main.cpp b/src/main.cpp index f217508..a9474f2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,129 @@ /** - * MicroClaw 鈥?Sensor-level micro AI Agent - * Runs on ESP32 with <1MB RAM, $2-5 hardware cost. - * Part of the Clawland edge AI agent network. + * MicroClaw ESP32-C3 port. + * + * Reads local sensors, reports upstream through MQTT, exposes a short OTA + * window, and returns to deep sleep for battery-powered deployments. */ #include +#include #include +#include -const char* AGENT_NAME = "microclaw"; -const char* VERSION = "0.1.0"; +#include "BatteryMonitor.h" +#include "DHT22Sensor.h" +#include "MicroClawConfig.h" +#include "MicroClawMqtt.h" + +const char* kVersion = "0.2.0"; +constexpr float kBatteryDividerRatio = 2.0f; + +DHT22Sensor dht22(kDhtPin); +BatteryMonitor battery(kBatteryPin, kBatteryDividerRatio); +MicroClawMqtt mqtt(kNodeId, kMqttHost, kMqttPort); + +static bool connectWiFi(uint32_t timeoutMs) { + WiFi.mode(WIFI_STA); + WiFi.begin(kWifiSsid, kWifiPassword); + + const uint32_t deadline = millis() + timeoutMs; + while (WiFi.status() != WL_CONNECTED && millis() < deadline) { + delay(250); + } + return WiFi.status() == WL_CONNECTED; +} + +static String sensorJson(const DHT22Reading& reading, uint16_t batteryMv) { + String json = "{"; + json += "\"node_id\":\"" + String(kNodeId) + "\","; + json += "\"sensor_type\":\"dht22\","; + json += "\"temperature_c\":" + String(reading.temperatureC, 1) + ","; + json += "\"humidity_percent\":" + String(reading.humidityPercent, 1) + ","; + json += "\"battery_mv\":" + String(batteryMv) + ","; + json += "\"firmware\":\"" + String(kVersion) + "\""; + json += "}"; + return json; +} + +static String statusJson(const char* state, uint16_t batteryMv) { + String json = "{"; + json += "\"node_id\":\"" + String(kNodeId) + "\","; + json += "\"state\":\"" + String(state) + "\","; + json += "\"battery_mv\":" + String(batteryMv) + ","; + json += "\"sleep_seconds\":" + String(kSleepSeconds) + ","; + json += "\"ota_window_seconds\":" + String(kOtaWindowSeconds) + ","; + json += "\"firmware\":\"" + String(kVersion) + "\""; + json += "}"; + return json; +} + +static String errorJson(const char* code, uint16_t batteryMv) { + String json = "{"; + json += "\"node_id\":\"" + String(kNodeId) + "\","; + json += "\"code\":\"" + String(code) + "\","; + json += "\"battery_mv\":" + String(batteryMv); + json += "}"; + return json; +} + +static void configureOTA() { + ArduinoOTA.setHostname(kNodeId); + ArduinoOTA.begin(); +} + +static void runOTAWindow() { + const uint32_t deadline = millis() + (kOtaWindowSeconds * 1000UL); + while (millis() < deadline) { + ArduinoOTA.handle(); + mqtt.loop(); + delay(20); + } +} + +static void sleepUntilNextCycle() { + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + esp_sleep_enable_timer_wakeup(static_cast(kSleepSeconds) * 1000000ULL); + Serial.printf("Sleeping for %lu seconds\n", static_cast(kSleepSeconds)); + esp_deep_sleep_start(); +} void setup() { - Serial.begin(115200); - Serial.println("馃 MicroClaw Agent v0.1.0"); - Serial.println(" MCU-level sensor agent starting..."); - Serial.println(" Waiting for sensor configuration..."); + Serial.begin(115200); + delay(200); + Serial.printf("MicroClaw Agent %s starting as %s\n", kVersion, kNodeId); + + dht22.begin(); + battery.begin(); + + const uint16_t batteryMv = battery.readMillivolts(); + const DHT22Reading reading = dht22.read(); + + if (!connectWiFi(12000)) { + Serial.println("WiFi unavailable; returning to deep sleep"); + sleepUntilNextCycle(); + } + + mqtt.begin(); + configureOTA(); + + mqtt.publishStatus(statusJson("awake", batteryMv)); + + if (reading.ok) { + mqtt.publishSensor("dht22", sensorJson(reading, batteryMv)); + } else { + mqtt.publishAlert("warning", errorJson(reading.error, batteryMv)); + } + + if (battery.isLow(batteryMv, kLowBatteryMv)) { + mqtt.publishAlert("critical", errorJson("battery_low", batteryMv)); + } + + runOTAWindow(); + mqtt.publishStatus(statusJson("sleeping", batteryMv)); + sleepUntilNextCycle(); } void loop() { - delay(1000); -} \ No newline at end of file + ArduinoOTA.handle(); + mqtt.loop(); +}