You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The lamp manifest so far showed two intent shapes: idempotent write
with float/duration params (set_brightness, set_color) and read
(read_brightness). blink adds the third interesting shape:
non-idempotent write with a duration param that has to round-trip
through CBOR as a float. This makes the canonical example exercise
every code path in the bridge x firmware boundary worth testing.
Changes:
- examples/lamp_manifest.yaml: blink(times: int, period: duration ms)
with idempotent: false and dry_run: true; ranges [1,20] and
[50,2000]ms cap total real-time at 80s worst case
- firmware/esp32/examples/lamp/lamp.ino: handle_blink + binding,
~33 LOC. Saves/restores PWM duty so prior brightness survives the
blink. Blocking implementation; the comment notes a real product
would use a millis() state machine
- tools/test_uart_roundtrip.py: 3 blink cases folded into the
existing suite — now 13/13 round-trips pass against ESP32-WROOM-32
- README.md, docs/paper/main.tex: 10/10 → 13/13
- docs/ADDING_FEATURES.md: trap report + sync example ranges with
the shipped manifest. The trap: CBOR is type-strict, so a
`duration` param (encoded as CBOR float on the wire) must be
decoded with read_float() not read_int(); a mismatched read wedges
the parser and the handler returns STATUS_DENIED for both dry-run
and real calls. Worth documenting because the same symptom hits
anyone porting a third-party handler that assumed numeric flexibility.
The Python Bridge, MCP server wrapper, and GenericSimulator all
required zero changes — the manifest is the single source of truth.
That's the load-bearing claim of the protocol, and it held up under
the add-an-intent test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
// Belt-and-suspenders: re-check ranges. The Bridge already did this,
74
74
// but defending here means a misbehaving Bridge can't drive the LED
75
75
// outside the safe envelope.
76
-
if (times < 1 || times > 100) return dcp::STATUS_RANGE;
77
-
if (period < 50 || period > 5000) return dcp::STATUS_RANGE;
76
+
if (times < 1 || times > 20) return dcp::STATUS_RANGE;
77
+
if (period < 50 || period > 2000) return dcp::STATUS_RANGE;
78
78
79
79
// Dry-run: report what we would do, no side effects.
80
80
if (kind == dcp::KIND_DRY_RUN) {
@@ -233,12 +233,39 @@ The Bridge fans this out to any LLM session subscribed to `lamp.read`.
233
233
| Symptom | Cause | Fix |
234
234
|---|---|---|
235
235
| `denied` with empty data from device | error reply buffer too small in firmware | bump the `uint8_t buf[N]` in your handler |
236
+
| `denied` after Bridge says `ok` for the same call shape on dry-run | **CBOR type mismatch** — handler used `read_int` on a `duration`/`float` param; the reader wedges, next `next_key` returns false | use `read_float` for any param whose manifest type is `float` or `duration`; `read_int` only for `int`. See "CBOR is type-strict" note below |
236
237
| `unknown_intent` for an intent you swore is in the manifest | spelling mismatch — `DCP_ID("foo")` vs manifest `name: Foo` | strings are byte-exact; rename one to match |
237
238
| LLM keeps sending out-of-range values | you forgot `range:` in the manifest | add it; the Bridge picks it up after restart |
238
239
| Long handler causes `busy` from the Bridge | `delay()` exceeded the Bridge timeout (default 2s) | shorten, or run async via FreeRTOS task and ack-then-event |
239
240
| `set_color` works but no light changes | you don't have an RGB LED wired up | that's by design — the example saves state and flashes the brightness LED to acknowledge |
240
241
| Capability check fails with `capability_required` | the LLM session's token doesn't include that capability | re-issue a token: `dcp token mint --caps lamp.write,lamp.read,...`|
241
242
243
+
### CBOR is type-strict (the duration-as-float trap)
244
+
245
+
CBOR distinguishes integer and float at the major-type level. A `42`
246
+
and a `42.0` are **not** the same item. The Bridge encodes manifest
0 commit comments