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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.pio/
.vscode/
lib/
include/
compile_commands.json
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
70 changes: 70 additions & 0 deletions docs/telemetry-contract.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions docs/wiring.md
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 52 additions & 0 deletions include/MicroClawConfig.h
Original file line number Diff line number Diff line change
@@ -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;
24 changes: 24 additions & 0 deletions lib/BatteryMonitor/BatteryMonitor.cpp
Original file line number Diff line number Diff line change
@@ -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<float>(total) / sampleCount;
return static_cast<uint16_t>(averageMv * dividerRatio_);
}

bool BatteryMonitor::isLow(uint16_t millivolts, uint16_t thresholdMv) const {
return millivolts > 0 && millivolts < thresholdMv;
}
16 changes: 16 additions & 0 deletions lib/BatteryMonitor/BatteryMonitor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <Arduino.h>

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_;
};
16 changes: 16 additions & 0 deletions lib/DHT22Sensor/DHT22Sensor.cpp
Original file line number Diff line number Diff line change
@@ -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, ""};
}
22 changes: 22 additions & 0 deletions lib/DHT22Sensor/DHT22Sensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <Arduino.h>
#include <DHT.h>

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_;
};
42 changes: 42 additions & 0 deletions lib/MicroClawMqtt/MicroClawMqtt.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
Loading