Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
18 changes: 15 additions & 3 deletions platformio.ini
Original file line number Diff line number Diff line change
@@ -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
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
251 changes: 242 additions & 9 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -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 <Arduino.h>
#include <ArduinoOTA.h>
#include <DHT.h>
#include <PubSubClient.h>
#include <WiFi.h>
#include <cstring>

#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<int>(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);
}
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);
}