Skip to content

Commit 62627cb

Browse files
WaylandYangclaude
andcommitted
firmware: ESP8266 port — cross-compile clean, ~$2 MCU now reachable
ESP8266 has been EOL'd by Espressif in favour of the C3, but it's still the dominant chip in the cheap-and-cheerful hobbyist stratum (NodeMCU / Wemos D1 Mini boards routinely <$2 in bulk). Getting DCP to run there is two changes: 1. Sketch-level PWM API. The reference lamp example used Arduino-ESP32 core 3.x v3 LEDC API (ledcAttach / ledcWrite), which doesn't exist on ESP8266. Replaced direct calls with thin lamp_pwm_setup() / lamp_pwm_write() inline helpers that pick the right backend at compile time (LEDC on ESP32, analogWrite with analogWriteFreq / analogWriteRange on ESP8266, digital fallback elsewhere). Zero protocol-layer impact. 2. library.properties: architectures=esp32,esp8266,*. The DCPBle.cpp was already guarded with #if __has_include(<NimBLEDevice.h>), so the BLE side compiles to nothing on ESP8266 (which has no BLE radio anyway). Build matrix after this commit: ESP32-WROOM-32 Xtensa LX6 295 KB / 22.7 KB ESP32-C3 RV32IMC 289 KB / 13.4 KB ESP32-C6 RV32IMAC + HW-crypto 266 KB / 14.0 KB ESP32-H2 RV32IMAC + 802.15.4 292 KB / 14.0 KB ESP32-P4 RV32IMAFC dual-core 326 KB / 22.0 KB ESP8266 NodeMCU Xtensa LX106 (legacy) 242 KB / 28.9 KB ← new ESP8266 fit: 23% of flash, 36% of RAM, 93% of IRAM. Tight on IRAM (legitimate concern on this MCU) but well within budget. Helper-function refactor cost on ESP32: +88 bytes flash. No global RAM change. So adding ESP8266 support did not regress the ESP32 target in any measurable way. Side-effects: - docs/paper/main.tex §validation paragraph generalised from "ESP32 RISC-V family" to "ESP family" with ESP8266 row added - README cross-compile table extended with the ESP8266 row and a reproduce command - Roadmap bullet updated Runtime UART validation on the ESP8266 itself is still TODO (need a NodeMCU on the bench). Build artefacts are produced and verified by arduino-cli. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f258832 commit 62627cb

4 files changed

Lines changed: 66 additions & 30 deletions

File tree

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ measured / typical of the cited sources.*
9090
See [docs/RATIONALE.md §7](docs/RATIONALE.md) for what the hardware
9191
validation does and does not prove.
9292

93-
### Cross-compile clean on the ESP32 RISC-V family
93+
### Cross-compile clean across the ESP family (Xtensa + RISC-V + ESP8266)
9494

9595
The reference firmware is portable by design (Arduino `Stream` + a
96-
software SHA-256, no Xtensa-specific code paths). It cross-compiles
97-
unchanged for every RISC-V ESP32 variant, with end-to-end UART
98-
validation pending boards on the bench:
96+
software SHA-256, no SoC-specific code paths in `DCP.{h,cpp}`). It
97+
cross-compiles for every current ESP32 variant *and* for ESP8266,
98+
with end-to-end UART validation pending boards on the bench:
9999

100100
| Target | ISA | Flash (lamp+blink) | Globals | Status |
101101
|-------------------|-----------------------|--------------------|----------|---------------|
@@ -104,13 +104,19 @@ validation pending boards on the bench:
104104
| ESP32-C6 | RV32IMAC + HW-crypto | 266 KB | 14.0 KB | builds ✓ |
105105
| ESP32-H2 | RV32IMAC + 802.15.4 | 292 KB | 14.0 KB | builds ✓ |
106106
| ESP32-P4 | RV32IMAFC dual-core | 326 KB | 22.0 KB | builds ✓ |
107+
| ESP8266 NodeMCU | Xtensa LX106 (legacy) | 242 KB | 28.9 KB | builds ✓ |
107108

108-
All builds use Arduino-ESP32 core 3.3.8 + the same
109-
`firmware/esp32/` library, zero conditionals. Reproduce with:
109+
All builds use Arduino-ESP32 core 3.3.8 / Arduino-ESP8266 core 3.x
110+
+ the same `firmware/esp32/` library. The sketch picks PWM API at
111+
compile time (`ledcAttach`/`ledcWrite` on ESP32, `analogWrite` on
112+
ESP8266); the protocol layer itself has no `#ifdef`. Reproduce
113+
with:
110114

111115
```bash
112116
arduino-cli compile --clean --fqbn esp32:esp32:esp32c3 \
113117
--library firmware/esp32 firmware/esp32/examples/lamp
118+
arduino-cli compile --clean --fqbn esp8266:esp8266:nodemcuv2 \
119+
--library firmware/esp32 firmware/esp32/examples/lamp
114120
```
115121

116122
## Manifest
@@ -288,7 +294,7 @@ MIT.
288294
- [x] Codegen `--stubs`: emits handler signatures + binding table
289295
- [x] Quickstart video script ([docs/QUICKSTART_VIDEO.md](docs/QUICKSTART_VIDEO.md))
290296
- [x] Real-hardware UART validation (ESP32-WROOM-32, 13/13 round-trips)
291-
- [x] Cross-compile clean on ESP32 RISC-V family (C3, C6, H2, P4)
297+
- [x] Cross-compile clean on ESP32 RISC-V family (C3, C6, H2, P4) and ESP8266
292298
- [x] Public repo at `device-context-protocol/dcp` (v0.3.0 released)
293299
- [x] PyPI release (`pip install pydcp`)
294300
- [ ] T-Panel S3 + CAN bus demo (firmware ready, awaiting hardware)

docs/paper/main.tex

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,19 @@ \subsection{ESP32 reference firmware}
468468
the firmware-side CBOR float decode path. All thirteen cases pass against
469469
real hardware.
470470

471-
\paragraph{Portability across the ESP32 RISC-V family.} The same
472-
\texttt{firmware/esp32/} library compiles unchanged for every RISC-V
473-
variant of the ESP32 family --- C3 (RV32IMC, 289\,KB / 13.4\,KB), C6
471+
\paragraph{Portability across the ESP family.} The same
472+
\texttt{firmware/esp32/} library compiles unchanged across the
473+
current ESP32 family --- C3 (RV32IMC, 289\,KB / 13.4\,KB), C6
474474
(RV32IMAC, 266\,KB / 14.0\,KB), H2 (RV32IMAC + 802.15.4, 292\,KB /
475-
14.0\,KB), and P4 (RV32IMAFC dual-core, 326\,KB / 22.0\,KB) ---
476-
demonstrating that the design's portability claim holds beyond the
477-
single Xtensa target on which it was measured. End-to-end UART
478-
validation on these targets is pending bench availability, but the
479-
build artefacts are byte-identical in structure.
475+
14.0\,KB), and P4 (RV32IMAFC dual-core, 326\,KB / 22.0\,KB) --- and
476+
additionally on the legacy ESP8266 NodeMCU (Xtensa LX106, 242\,KB /
477+
28.9\,KB), where the lamp example sketch picks the PWM API at
478+
compile time (\texttt{ledcAttach}/\texttt{ledcWrite} on ESP32,
479+
\texttt{analogWrite} on ESP8266) while the protocol layer itself
480+
remains \texttt{\#ifdef}-free. End-to-end UART validation on these
481+
non-Xtensa-LX6 targets is pending bench availability, but the build
482+
artefacts confirm the portability claim beyond the single target on
483+
which the runtime was measured.
480484

481485
\subsection{Conformance suite}
482486

firmware/esp32/examples/lamp/lamp.ino

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,45 @@
55
// dcp serve examples/lamp_manifest.yaml --serial COM3 (Windows)
66
// dcp serve examples/lamp_manifest.yaml --serial /dev/ttyUSB0 (Linux)
77
//
8-
// Hardware: any ESP32 dev board. The built-in LED is on GPIO 2 on most
9-
// WROOM-32 dev kits (DOIT, NodeMCU-32, etc.). If your board has it on a
10-
// different pin, change LED_PIN below.
8+
// Hardware: any ESP32 dev board or ESP8266 (NodeMCU / Wemos D1). The
9+
// built-in LED is on GPIO 2 on most WROOM-32 dev kits. On ESP8266 the
10+
// onboard LED is usually GPIO 2 as well but ACTIVE-LOW — flip the duty
11+
// inversion below if your board has a different scheme.
1112
//
12-
// Requires Arduino-ESP32 core 3.0 or newer (uses the v3 LEDC API). For
13-
// core 2.x replace the ledcAttach/ledcWrite calls with the old
14-
// ledcSetup + ledcAttachPin + ledcWrite(channel, duty) sequence.
13+
// PWM API:
14+
// ESP32: Arduino-ESP32 core 3.x v3 LEDC API (ledcAttach / ledcWrite)
15+
// ESP8266: stock analogWrite() (range remapped to 0..255 to match)
16+
//
17+
// All other DCP code paths are platform-independent.
1518

1619
#include "DCP.h"
1720

1821
constexpr int LED_PIN = 2; // built-in LED on most WROOM-32 dev boards
1922
constexpr int PWM_FREQ_HZ = 5000;
2023
constexpr int PWM_BITS = 8;
24+
constexpr int PWM_MAX = (1 << PWM_BITS) - 1; // 255
25+
26+
static inline void lamp_pwm_setup() {
27+
#if defined(ESP32)
28+
ledcAttach(LED_PIN, PWM_FREQ_HZ, PWM_BITS); // core 3.x v3 API
29+
#elif defined(ESP8266)
30+
analogWriteFreq(PWM_FREQ_HZ);
31+
analogWriteRange(PWM_MAX);
32+
pinMode(LED_PIN, OUTPUT);
33+
#else
34+
pinMode(LED_PIN, OUTPUT); // fallback: digital only
35+
#endif
36+
}
37+
38+
static inline void lamp_pwm_write(uint32_t duty) {
39+
#if defined(ESP32)
40+
ledcWrite(LED_PIN, duty);
41+
#elif defined(ESP8266)
42+
analogWrite(LED_PIN, duty);
43+
#else
44+
digitalWrite(LED_PIN, duty > (PWM_MAX / 2) ? HIGH : LOW);
45+
#endif
46+
}
2147

2248
static float g_brightness = 0.0f;
2349
static uint8_t g_r = 255, g_g = 255, g_b = 255; // current target RGB (no real LED bound)
@@ -47,7 +73,7 @@ static dcp::Status handle_set_brightness(uint8_t kind,
4773

4874
g_brightness = (float)level;
4975
uint32_t duty = (uint32_t)(g_brightness * 2.55f);
50-
ledcWrite(LED_PIN, duty); // v3 API: pin, not channel
76+
lamp_pwm_write(duty);
5177
return dcp::STATUS_OK;
5278
}
5379

@@ -86,9 +112,9 @@ static dcp::Status handle_set_color(uint8_t kind,
86112
// The default WROOM-32 dev board has no RGB LED. We acknowledge by giving
87113
// a visible 80ms flash on the built-in brightness LED, then restoring it.
88114
uint32_t saved = (uint32_t)(g_brightness * 2.55f);
89-
ledcWrite(LED_PIN, 255); delay(80);
90-
ledcWrite(LED_PIN, 0); delay(80);
91-
ledcWrite(LED_PIN, saved);
115+
lamp_pwm_write(255); delay(80);
116+
lamp_pwm_write(0); delay(80);
117+
lamp_pwm_write(saved);
92118
return dcp::STATUS_OK;
93119
}
94120

@@ -123,10 +149,10 @@ static dcp::Status handle_blink(uint8_t kind,
123149
// Save current brightness so we can restore it after the blink.
124150
uint32_t saved = (uint32_t)(g_brightness * 2.55f);
125151
for (int64_t i = 0; i < times; ++i) {
126-
ledcWrite(LED_PIN, 255); delay(period);
127-
ledcWrite(LED_PIN, 0); delay(period);
152+
lamp_pwm_write(255); delay(period);
153+
lamp_pwm_write(0); delay(period);
128154
}
129-
ledcWrite(LED_PIN, saved);
155+
lamp_pwm_write(saved);
130156
return dcp::STATUS_OK;
131157
}
132158

@@ -144,7 +170,7 @@ static dcp::DCP* dcp_instance = nullptr;
144170
void setup() {
145171
Serial.begin(115200);
146172

147-
ledcAttach(LED_PIN, PWM_FREQ_HZ, PWM_BITS); // v3 API: pin + freq + resolution
173+
lamp_pwm_setup();
148174

149175
static dcp::DCP instance(Serial, bindings, sizeof(bindings) / sizeof(bindings[0]));
150176
dcp_instance = &instance;

firmware/esp32/library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ sentence=Device Context Protocol: bridge LLM agents to physical devices.
66
paragraph=Reference firmware for the DCP wire protocol. Parses COBS-framed CRC-checked DCP frames over Serial OR BLE GATT (with NimBLE-Arduino), decodes a tiny CBOR subset, dispatches to user-registered intent handlers, and optionally verifies per-frame HMAC-SHA256 signatures.
77
category=Communication
88
url=https://github.com/device-context-protocol
9-
architectures=esp32,*
9+
architectures=esp32,esp8266,*
1010
depends=NimBLE-Arduino
1111
includes=DCP.h

0 commit comments

Comments
 (0)