diff --git a/README.md b/README.md index 2a549f3..79e9366 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,62 @@ MicroClaw is the smallest member of the Claw family — a bare-metal AI agent th 🚧 **Pre-Alpha** — Architecture design phase. Looking for contributors! +## ESP32-C3 Firmware Port + +The default PlatformIO target is now `esp32c3`, using the +`esp32-c3-devkitm-1` board profile. The legacy `esp32` environment remains +available for generic ESP32 dev boards. + +The firmware reads: + +- one analog sensor on `MICROCLAW_ANALOG_PIN` (default `GPIO2` for ESP32-C3), +- one DHT-style temperature/humidity sensor on `MICROCLAW_DHT_PIN` (default + `GPIO4`, `DHT22`), +- Wi-Fi RSSI and uptime. + +When MQTT is configured, the node publishes compact JSON status payloads to +`MICROCLAW_MQTT_TOPIC`. When Wi-Fi is connected, ArduinoOTA is enabled by +default so test devices can receive firmware updates without a USB cable. + +### Configuration + +Configure credentials and endpoints with PlatformIO build flags or your local +environment. Do not commit private SSIDs, passwords, broker credentials, or OTA +passwords. + +Example local build flags: + +```ini +build_flags = + -D MICROCLAW_NODE_ID=\"greenhouse-node-01\" + -D MICROCLAW_WIFI_SSID=\"your-ssid\" + -D MICROCLAW_WIFI_PASSWORD=\"your-password\" + -D MICROCLAW_MQTT_HOST=\"192.0.2.10\" + -D MICROCLAW_MQTT_PORT=1883 + -D MICROCLAW_MQTT_TOPIC=\"clawland/microclaw/greenhouse-node-01/status\" +``` + +Useful optional flags: + +```ini + -D MICROCLAW_ANALOG_PIN=2 + -D MICROCLAW_DHT_PIN=4 + -D MICROCLAW_DHT_TYPE=DHT22 + -D MICROCLAW_REPORT_INTERVAL_MS=10000 + -D MICROCLAW_ENABLE_OTA=1 +``` + +### Validation + +```bash +pio run -e esp32c3 +pio run -e esp32 +``` + +Runtime validation requires physical ESP32-C3 hardware, a DHT-compatible +sensor, and an MQTT broker. Without hardware, the build verifies that the +firmware, dependencies, and board targets compile. + ## Contributing See the [Clawland Contributing Guide](https://github.com/Clawland-AI/.github/blob/main/CONTRIBUTING.md). diff --git a/platformio.ini b/platformio.ini index 640e4e9..2c70d65 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,9 +1,21 @@ -[env:esp32] +[platformio] +default_envs = esp32c3 + +[env] platform = espressif32 -board = esp32dev framework = arduino 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 + +[env:esp32] +board = esp32dev + +[env:esp32c3] +board = esp32-c3-devkitm-1 +build_flags = + -D MICROCLAW_BOARD_NAME=\"esp32-c3-devkitm-1\" + -D MICROCLAW_ANALOG_PIN=2 + -D MICROCLAW_DHT_PIN=4 diff --git a/src/main.cpp b/src/main.cpp index f217508..ae2b9b0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,254 @@ /** - * 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 - sensor-level edge agent for ESP32-class MCUs. + * + * This firmware keeps runtime configuration compile-time driven so example + * builds never commit Wi-Fi, MQTT, or OTA credentials. */ #include +#include +#include +#include #include +#include + +#ifndef MICROCLAW_BOARD_NAME +#define MICROCLAW_BOARD_NAME "esp32dev" +#endif + +#ifndef MICROCLAW_NODE_ID +#define MICROCLAW_NODE_ID "microclaw-001" +#endif + +#ifndef MICROCLAW_WIFI_SSID +#define MICROCLAW_WIFI_SSID "" +#endif + +#ifndef MICROCLAW_WIFI_PASSWORD +#define MICROCLAW_WIFI_PASSWORD "" +#endif + +#ifndef MICROCLAW_MQTT_HOST +#define MICROCLAW_MQTT_HOST "" +#endif + +#ifndef MICROCLAW_MQTT_PORT +#define MICROCLAW_MQTT_PORT 1883 +#endif + +#ifndef MICROCLAW_MQTT_TOPIC +#define MICROCLAW_MQTT_TOPIC "clawland/microclaw/status" +#endif + +#ifndef MICROCLAW_ANALOG_PIN +#define MICROCLAW_ANALOG_PIN 34 +#endif + +#ifndef MICROCLAW_DHT_PIN +#define MICROCLAW_DHT_PIN 4 +#endif + +#ifndef MICROCLAW_DHT_TYPE +#define MICROCLAW_DHT_TYPE DHT22 +#endif + +#ifndef MICROCLAW_REPORT_INTERVAL_MS +#define MICROCLAW_REPORT_INTERVAL_MS 10000UL +#endif + +#ifndef MICROCLAW_MQTT_RETRY_MS +#define MICROCLAW_MQTT_RETRY_MS 5000UL +#endif + +#ifndef MICROCLAW_ENABLE_OTA +#define MICROCLAW_ENABLE_OTA 1 +#endif const char* AGENT_NAME = "microclaw"; -const char* VERSION = "0.1.0"; +const char* VERSION = "0.2.0"; + +DHT dht(MICROCLAW_DHT_PIN, MICROCLAW_DHT_TYPE); +WiFiClient wifiClient; +PubSubClient mqtt(wifiClient); + +unsigned long lastReportAt = 0; +unsigned long lastMqttAttemptAt = 0; + +struct SensorSnapshot { + int analogRaw; + int analogMillivolts; + float temperatureC; + float humidityPct; + bool dhtOk; +}; + +static bool hasText(const char* value) { + return value != nullptr && value[0] != '\0'; +} + +static void connectWifi() { + if (!hasText(MICROCLAW_WIFI_SSID) || WiFi.status() == WL_CONNECTED) { + return; + } + + Serial.print("Connecting Wi-Fi SSID "); + Serial.println(MICROCLAW_WIFI_SSID); + WiFi.mode(WIFI_STA); + WiFi.begin(MICROCLAW_WIFI_SSID, MICROCLAW_WIFI_PASSWORD); + + const unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < 15000UL) { + delay(250); + Serial.print('.'); + } + Serial.println(); + + if (WiFi.status() == WL_CONNECTED) { + Serial.print("Wi-Fi connected: "); + Serial.println(WiFi.localIP()); + } else { + Serial.println("Wi-Fi unavailable; continuing offline."); + } +} + +static void setupOta() { +#if MICROCLAW_ENABLE_OTA + if (WiFi.status() != WL_CONNECTED) { + return; + } + + ArduinoOTA.setHostname(MICROCLAW_NODE_ID); + ArduinoOTA.onStart([]() { + Serial.println("OTA update starting"); + }); + ArduinoOTA.onEnd([]() { + Serial.println("OTA update complete"); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.print("OTA error: "); + Serial.println(static_cast(error)); + }); + ArduinoOTA.begin(); + Serial.println("OTA ready"); +#endif +} + +static void setupMqtt() { + if (!hasText(MICROCLAW_MQTT_HOST)) { + Serial.println("MQTT host not configured; status reporting disabled."); + return; + } + + mqtt.setServer(MICROCLAW_MQTT_HOST, MICROCLAW_MQTT_PORT); + mqtt.setBufferSize(384); +} + +static void connectMqtt() { + if (!hasText(MICROCLAW_MQTT_HOST) || WiFi.status() != WL_CONNECTED || mqtt.connected()) { + return; + } + + const unsigned long now = millis(); + if (now - lastMqttAttemptAt < MICROCLAW_MQTT_RETRY_MS) { + return; + } + lastMqttAttemptAt = now; + + Serial.print("Connecting MQTT "); + Serial.print(MICROCLAW_MQTT_HOST); + Serial.print(':'); + Serial.println(MICROCLAW_MQTT_PORT); + + if (mqtt.connect(MICROCLAW_NODE_ID)) { + Serial.println("MQTT connected"); + } else { + Serial.print("MQTT connect failed rc="); + Serial.println(mqtt.state()); + } +} + +static SensorSnapshot readSensors() { + SensorSnapshot snapshot; + snapshot.analogRaw = analogRead(MICROCLAW_ANALOG_PIN); + snapshot.analogMillivolts = analogReadMilliVolts(MICROCLAW_ANALOG_PIN); + snapshot.temperatureC = dht.readTemperature(); + snapshot.humidityPct = dht.readHumidity(); + snapshot.dhtOk = !isnan(snapshot.temperatureC) && !isnan(snapshot.humidityPct); + return snapshot; +} + +static void publishSnapshot(const SensorSnapshot& snapshot) { + char payload[320]; + char temperature[16]; + char humidity[16]; + + if (snapshot.dhtOk) { + dtostrf(snapshot.temperatureC, 0, 2, temperature); + dtostrf(snapshot.humidityPct, 0, 2, humidity); + } else { + strcpy(temperature, "null"); + strcpy(humidity, "null"); + } + + snprintf( + payload, + sizeof(payload), + "{\"nodeId\":\"%s\",\"agent\":\"%s\",\"version\":\"%s\",\"board\":\"%s\",\"uptimeMs\":%lu,\"analogRaw\":%d,\"analogMv\":%d,\"temperatureC\":%s,\"humidityPct\":%s,\"wifiRssi\":%d}", + MICROCLAW_NODE_ID, + AGENT_NAME, + VERSION, + MICROCLAW_BOARD_NAME, + millis(), + snapshot.analogRaw, + snapshot.analogMillivolts, + temperature, + humidity, + WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : 0 + ); + + Serial.println(payload); + if (mqtt.connected()) { + mqtt.publish(MICROCLAW_MQTT_TOPIC, payload, false); + } +} 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..."); + delay(100); + + Serial.println("MicroClaw Agent v0.2.0"); + Serial.print("Board: "); + Serial.println(MICROCLAW_BOARD_NAME); + Serial.print("Node: "); + Serial.println(MICROCLAW_NODE_ID); + + pinMode(MICROCLAW_ANALOG_PIN, INPUT); + analogReadResolution(12); + dht.begin(); + + connectWifi(); + setupOta(); + setupMqtt(); } void loop() { - delay(1000); -} \ No newline at end of file + if (WiFi.status() != WL_CONNECTED) { + connectWifi(); + } + +#if MICROCLAW_ENABLE_OTA + if (WiFi.status() == WL_CONNECTED) { + ArduinoOTA.handle(); + } +#endif + + connectMqtt(); + mqtt.loop(); + + const unsigned long now = millis(); + if (now - lastReportAt >= MICROCLAW_REPORT_INTERVAL_MS) { + lastReportAt = now; + publishSnapshot(readSensors()); + } + + delay(10); +}