Status: DRAFT (per-section freeze in effect) · Owner: project maintainer · Last updated: 2026-07-30
Sections are frozen one at a time before the stories that cite them start. A
frozen section MUST NOT change except via a [docs] amendment that lands before
any dependent code. Freezing is cumulative with the gates in the
public roadmap.
| Section | Requirement IDs | Freeze status | Freeze act | Notes |
|---|---|---|---|---|
| §5.1 Reliability | NFR-REL-1…5 | FROZEN v1.0 | G0 · 2026-07-01 · [docs] |
Wire-independent; unblocks X-03 (NFR-REL-4). |
| §5.6 Maintainability | NFR-MAINT-1…4 | FROZEN v1.0 | G0 · 2026-07-01 · [docs] |
Unblocks X-01 (NFR-MAINT-4) + module/layout freeze (NFR-MAINT-2). |
| §6 Constraints | CON-1…13 | FROZEN v1.0 | G0 · 2026-07-01 · [docs] |
Clean-room/build/carve-out; inherits no PBLE/1 opcode/UUID/status number. Unblocks X-01, X-02, F-15. |
| §8 Build, versioning & distribution | BLD-1…22 | FROZEN v1.0 (amended) | G0 · 2026-07-01; browser-release amendment 2026-07-29 · [docs] |
BLD-17…22 and the tightened BLD-5…8/13/14 freeze the X-10/X-11 public flasher contract before implementation without changing the v1.0 product contract. |
| §4 Functional (FR-BLE/PROTO/RUN/FS/CON/INFO/BOOT/MODE/IDENT) | FR-* | DRAFT (FR-BLE/FR-PROTO numbers frozen) | — | Inherits PBLE/1 numbers; frozen per M1 story as protocol.md §§ freeze. protocol.md §4 opcodes + §8 status are FROZEN (2026-07-01), closing OI-4 — FR-BLE-8/10 and FR-PROTO-1…10 opcode/status numbers are now stable (F-01/F-02 DoR met). protocol.md §6 (RUN-file) / §7 (HELLO caps) / §9 (version) froze 2026-07-01 (S3) — FR-INFO-1..6, FR-PROTO-7/10, FR-RUN (RUN-file) and the FR-IDENT-1 / FR-BLE-12 label bound (24 B) are now stable (F-03/F-16/F-22/F-04 DoR met). protocol.md §6 fully froze 2026-07-01 (S4) — RUN{source}, STOP, SOFT_REBOOT, CONSOLE_DATA/INPUT + the OI-6 identify remainder (SET_IDENTIFY_LED/IDENTIFY/caps) are now stable (F-05/F-06/F-07/F-23 DoR met), closing OI-6. protocol.md §5 froze 2026-07-01 (S5) — the file-transfer wire (read + windowed upload + workspace jail) is stable (FR-FS-1..16, F-08/F-09/F-17 DoR met); §2–§9 are now all frozen, only §10 (Security) remained DRAFT. The jail chokepoint (pble_fs_resolve + forbidden set: fs_root confinement, reserved .pbltmp suffix, reserved agent prefixes) is firmware-internal per ADR-0006 (the agent/overlay are embedded, not vfs paths). protocol.md §10 (Security) + §5 resume behaviour + the OI-5 auto_run cap / SET_AUTORUN 0x23 froze 2026-07-01 (S6) — SEC-1..11, FR-FS-7, FR-BOOT-1..6 / FR-MODE-1/4 / NFR-SAFE-3 are now stable (F-10/F-11/F-12/F-18 DoR met). PBLE/1 §2–§10 are now ALL frozen — the wire is complete for v1.0. |
| §5.2–§5.3 PERF/FP measurement contract | NFR-PERF/FP | METHOD FROZEN; profile thresholds pending | Pre-v1 qualification amendment · 2026-07-30 · [docs] |
Exact two-profile scope, metric definitions, workload, outward-rounding formulas, and evidence shape are frozen before measurement. Numeric thresholds remain OI-1 until derived from retained baseline evidence. |
| §5.4–§5.5 SAFE/OFF | NFR-SAFE/OFF | DRAFT | — | Frozen per dependent story. |
| §7 External interfaces, §9 Security | IF-, SEC- | §9 SEC-* FROZEN (2026-07-01, S6) | — | SEC-1..11 mirror the frozen protocol.md §10: pairing/encryption baseline (non-gating), connected-client-trust, single active writer (SEC-3), no MAC/label gating (SEC-7/11), no PII in adv (SEC-10), no telemetry (SEC-5). IF-* frozen per story alongside their protocol.md §§. |
PBLE/1 dependency: protocol.md §2 (transport), §3 (framing), §4 (opcodes), and §8 (status) are FROZEN for v1.0; the GATT UUID base, the §3.1 frame + §3.2 fragmentation, the opcode set + numbers, and the status set + numbers are now stable inputs to FR-BLE-1/8/10 and FR-PROTO-1…10. The §4/§8 freeze (2026-07-01) closes OI-4 and completes the DoR for F-01 and F-02. protocol.md §6/§7/§9 froze 2026-07-01 (S3) — HELLO/caps, the RUN-file path, the version policy, and the 24 B label bound are stable, meeting DoR for F-03/F-16/F-22/F-04. protocol.md §6 fully froze 2026-07-01 (S4) — STOP/SOFT_REBOOT/console/RUN-source + the identify encodings, closing OI-6 (F-05/F-06/F-07/F-23 DoR met).
This document is the detailed firmware requirements specification for the initial ESP32-family v1 PyBLE agent port. It derives from and expands PRD §10 (Functional Requirements: Agent Firmware) into individually-testable, traceable requirements, each carrying a stable requirement ID and a source/verification/story trace. It is the authority for what this port must do; the companion TDD.md (Technical Design Document) is the authority for how it does it. Future MicroPython + BLE MCU families implement the same PBLE/1/control-plane contract through a platform-specific target adapter and require their own derived requirements, resource gates, provisioning contract, and HIL matrix.
For the initial ESP32 port, the firmware covers Layers 1–3 of the four-layer model defined in firmware.md §1 and PRD §1A.4:
- Layer 1 — upstream MicroPython (ESP32 port), consumed as a pinned submodule.
- Layer 2 — the per-chip board overlay (
esp32/esp32-s3/esp32-c3). - Layer 3 — the PyBLE agent: the protected control plane (
pyble_ble,pyble_proto,pyble_runner,pyble_fs,pyble_console,pyble_info).
Layer 4 (the user workspace) is out of scope as firmware — it is content the agent serves and runs, not part of the control plane. The agent's relationship to Layer 4 (the workspace jail) is in scope and specified in §4.4.
The Flutter app (app.md, PRD §9) is out of scope.
Per the doc hierarchy ("the more specific spec wins on its own topic"):
- PRD §10 is the apex firmware requirement set; this document is its detailed expansion and MUST stay consistent with it.
- firmware.md is the firmware overview; this document is the detailed requirements layer beneath it.
- protocol.md (PBLE/1) owns the wire format (frame bytes, opcodes, status codes, UUIDs). This document MUST NOT redefine those; it references the relevant
§and states only firmware behaviour against them. - hardware.md owns platform eligibility, the current support matrix, and target specifications. This document references them; it does not restate datasheet facts.
Where this document and TDD.md touch the same topic, this document wins on requirements (what/why); TDD.md wins on design (how).
- PRD — apex requirements, especially §1A.4, §1B, §10, §11, §13, §14, §16.2, §17, §18.
- firmware.md — agent firmware overview (four-layer rule, modules, chip targets, runtime rules, build, footprint).
- browser-flashing.md — exact initial provisioning profiles, merged-image manifest, integrity/provenance bundle, recovery, and browser/HIL release gate.
- protocol.md (PBLE/1) — BLE wire protocol: §2 transport, §3 framing, §4 opcodes, §5 file transfer, §6 run/stop/console, §7 HELLO/caps, §8 status codes, §9 versioning, §10 security.
- hardware.md — supported chip families, board requirements, pin reference.
- architecture.md — three-piece system view, clean-room boundary.
firmware/versions.lock— pinned upstream MicroPython + ESP-IDF.firmware/upstream/README.md— clean-submodule rationale.- AGENTS.md, CLAUDE.md — repo governance, no-leak gate.
- Public roadmap and GitHub issues — firmware, protocol, and infrastructure work.
- Agent — the Layer-3
pyble_*module set; the board-side control plane. - Control plane — the always-running agent that services the BLE link and PBLE/1 commands, independent of any user program.
- Workspace — the Layer-4 user file region (
/main.py,/lib/*.py,/data/*, optional/project.json) rooted atfs_root. - Workspace jail — the constraint that PBLE/1 file commands may only read/write within
fs_root. - Runner — the task that executes user code (file or inline source).
- HIL — hardware-in-the-loop testing on every exact real-hardware profile
claimed by a release. The current v0.4.2 public-beta profile set is exactly
esp32-4mbandesp32-s3-n16r8: its supplemental production-browser rows passed, while its formal qualification matrix remains pending. The v1.0 matrix additionally requiresesp32-c3-4mb(PRD §1B.3, §10.12). - Frozen-Python agent — agent modules baked into the firmware image as
.py(frozen at build); the recommended first implementation. - Native agent — hot paths moved to a
USER_C_MODULEfor throughput/RAM, behind the unchanged PBLE/1 contract. - Verification categories (PRD §1B.3, cited in each requirement's
verify:): unit (host-side native/unit), conformance (PBLE/1 protocol conformance), build (build sanity / SHA gate), size (static application-image/partition gate), HIL (runtime hardware-in-the-loop resource and behaviour gates).
The initial ESP32 firmware realizes Layers 1–3 of the firmware.md §1 model:
Layer 1 Upstream MicroPython (ESP32 port) — pinned submodule, never edited in place
Layer 2 Board overlay — per-chip config: esp32 / esp32-s3 / esp32-c3
Layer 3 PyBLE agent (pyble_*) — the protected control plane (in scope)
Layer 4 User workspace — /main.py, /lib/*.py, /data/* (served, not part of the agent)
The agent is the control plane and user code is just a program it runs; a frozen while True: pass MUST NOT wedge BLE or block STOP (PRD §1A.3 rejection 5, firmware.md §5).
One agent codebase MUST build for three ESP-IDF targets — esp32, esp32s3, esp32c3 — all on NimBLE, driven by the single MicroPython + ESP-IDF pin in versions.lock. Chip facts are owned by hardware.md §1; ESP32-C3 is the footprint constraint (§5, PRD §10.13).
| In scope (this spec) | Out of scope |
|---|---|
| BLE peripheral + GATT (RX/TX/INFO), advertising, MTU, fragmentation | The app, lib/pble/ Dart client (app.md) |
| PBLE/1 engine: framing, CRC32, dispatch (behaviour only) | The PBLE/1 wire definition (protocol.md owns it) |
| Runner: run/stop/soft-reboot, RUN_STATE | User code semantics; user hardware drivers |
| Filesystem bridge + workspace jail | GPIO routing, actuator/lab/calibration/display logic |
| Console tee; device info / capabilities | Chip datasheets (hardware.md owns them) |
| Boot/lifecycle; execution modes | Wi-Fi / USB-serial runtime transports |
| Build, versioning, distribution, footprint gates | App store distribution of the app |
Requirement voice is MUST / SHOULD / MAY. Each line: ID — statement — (source; verify; story).
- FR-BLE-1 — The agent MUST expose exactly one primary GATT service on the PyBLE-owned 128-bit UUID base (
7079626c-…, encoding ASCIIpybl). — (source: PRD §10.7, protocol.md §2; verify: HIL, conformance; story: F-01) - FR-BLE-2 — The service MUST provide an RX characteristic (app → board) supporting Write and Write-Without-Response. — (source: PRD §10.7, protocol.md §2; verify: HIL; story: F-01)
- FR-BLE-3 — The service MUST provide a TX characteristic (board → app) supporting Notify. — (source: PRD §10.7, protocol.md §2; verify: HIL; story: F-01)
- FR-BLE-4 — The service MUST provide an INFO characteristic (board → app) supporting Read that returns a payload byte-equivalent to a
DEVICE_INFOresponse, so a client can identify a board before subscribing to TX. — (source: PRD §10.7, §18.3, protocol.md §2; verify: HIL, conformance; story: F-01, F-03) - FR-BLE-5 — On power-up the board MUST advertise the Service UUID together with a default device name
PyBLE-XXXX, whereXXXXis the last two bytes of the board's BLE MAC in uppercase hex (e.g.PyBLE-9F3A); this default MUST be stable across reboots and ~unique so a client can recognize the board. — (source: PRD §10.7, protocol.md §2; verify: HIL, conformance; story: F-01) - FR-BLE-6 — The advertisement MUST allow the app to scan filtered to the Service UUID, not a raw device list. — (source: PRD §8.1, §10.7; verify: HIL; story: F-01)
- FR-BLE-7 — The agent MUST accept an MTU request of 247. — (source: PRD §10.7, §13.4, protocol.md §2; verify: HIL; story: F-01)
- FR-BLE-8 — The agent MUST operate correctly across the negotiated MTU down to the BLE default, using a usable per-packet payload of
MTU − 3(ATT) minus the 1-byte fragmentation header. — (source: PRD §10.7, protocol.md §2, §3.2; verify: conformance, HIL; story: F-01, F-02) - FR-BLE-9 — The BLE peripheral MUST use NimBLE on all three targets (Bluedroid not built). — (source: PRD §10.1, §16.2, firmware.md §4; verify: build; story: F-01, F-13/14)
- FR-BLE-10 — The agent MUST implement PBLE/1 fragmentation/reassembly over RX/TX per protocol.md §3.2. — (source: PRD §10.7, protocol.md §3.2; verify: conformance; story: F-02, P-02)
- FR-BLE-11 — The BLE/agent task MUST keep servicing the link while a user program runs, so the link never depends on user-code progress. — (source: PRD §10.6, §13.3, firmware.md §5; verify: HIL; story: F-06)
- FR-BLE-12 — When a device label is set (§4.9,
SET_LABEL), the persisted label MUST replace the defaultPyBLE-XXXXas the advertised device name so it is visible in the scan list before connecting; clearing the label (empty value) MUST restore thePyBLE-XXXXdefault. The advertised label MUST be bounded to the same length limit the board enforces onSET_LABEL(FR-IDENT-1). — (source: PRD §10.7, protocol.md §2, §4; verify: HIL, conformance; story: F-01)
- FR-PROTO-1 — The agent MUST implement the full PBLE/1 v1.0 opcode set in protocol.md §4; none are optional in v1.0. — (source: PRD §10.8, protocol.md §4; verify: conformance; story: F-02)
- FR-PROTO-2 — The agent MUST encode and decode the §3.1 message frame (
VER/TYPE/OPCODE/ID/LEN/PAYLOAD/CRC32) per protocol.md §3.1; it MUST NOT redefine the wire format. — (source: PRD §10.8, protocol.md §3.1; verify: unit, conformance; story: F-02, P-01) - FR-PROTO-3 — The agent MUST validate the IEEE CRC-32 over
VER…PAYLOADof every reassembled message; a frame whose CRC fails MUST be dropped and answered withEVT ERROR(ECRC)referencing the opcode if known. — (source: PRD §10.7, §10.8, protocol.md §3.2, §8; verify: unit, conformance; story: F-02, P-01) - FR-PROTO-4 — The agent MUST correlate requests and responses by the 1-byte
ID, echoing the requestIDin the matchingRSP; events MUST useID = 0. — (source: protocol.md §3.1; verify: conformance; story: F-02) - FR-PROTO-5 — The agent MUST dispatch each decoded CMD to its handler by
OPCODE. — (source: PRD §10.3 (pyble_proto); verify: unit; story: F-02) - FR-PROTO-6 — Every command MUST return the correct PBLE/1 1-byte status in its
RSP, including error cases (EBADREQ,ENOENT,EACCES,ENOSPC,EIO,ENOMEM,EBUSY,ECRC,ERANGE,EUNSUPPORTED,EINTERNAL). — (source: PRD §10.8, protocol.md §8; verify: conformance; story: F-02, P-04) - FR-PROTO-7 — The agent MUST emit and accept only
VER = 0x01frames for PBLE/1 and MUST reject/refuse other versions per the versioning policy. — (source: PRD §18.1, protocol.md §3.1, §9; verify: conformance; story: F-02, P-03) - FR-PROTO-8 — A malformed or structurally invalid request MUST be answered with
EBADREQ. — (source: protocol.md §8; verify: conformance; story: F-02) - FR-PROTO-9 — A well-formed request for an opcode/feature the agent does not support MUST be answered with
EUNSUPPORTED. — (source: PRD §10.8, protocol.md §8; verify: conformance; story: F-02) - FR-PROTO-10 — The agent MUST NOT require or assume use of any capability it did not advertise in HELLO (
caps); the wire behaviour MUST match the advertised baseline. — (source: PRD §10.8, §18.3, protocol.md §7; verify: conformance; story: F-03, P-03)
- FR-RUN-1 —
RUN { mode: file }MUST execute a.pyfile from the workspace, replyRSP { status }, then emitRUN_STATE(running). — (source: PRD §8.2, §10.6, protocol.md §6; verify: HIL, conformance; story: F-04) - FR-RUN-2 —
RUN { mode: source }MUST execute an inline source snippet with the same lifecycle as a file run. — (source: PRD §10.8, protocol.md §6; verify: HIL, conformance; story: F-05) - FR-RUN-3 — The runner MUST execute user code on a task separate from the BLE/agent task. — (source: PRD §10.6, §13.3, firmware.md §5; verify: HIL; story: F-04, F-06)
- FR-RUN-4 — Only one user program MUST run at a time; a
RUNissued while a program is running MUST be answered withEBUSY. — (source: PRD §10.6, §14.1, protocol.md §8; verify: conformance, HIL; story: F-04) - FR-RUN-5 —
STOPMUST interrupt the runner by raisingKeyboardInterrupt, and MUST land promptly even against a tight loop (e.g.while True: pass). — (source: PRD §8.3, §10.6, §13.3, protocol.md §6, firmware.md §5; verify: HIL; story: F-06) - FR-RUN-6 — On
STOPor on an uncaught exception the runner MUST tear down cleanly and report the resultingRUN_STATE. — (source: PRD §8.2, §13.3, protocol.md §6; verify: HIL; story: F-06) - FR-RUN-7 — The agent MUST emit
RUN_STATEevents on every state transition (idle/running/done/error) so the app drives UI state without polling. — (source: PRD §10.6, protocol.md §6, §4; verify: conformance, HIL; story: F-04) - FR-RUN-8 —
SOFT_REBOOTMUST soft-reset the MicroPython VM (clearing interpreter state) and SHOULD keep the BLE link where possible. — (source: PRD §8.3, §10.8, protocol.md §4; verify: HIL; story: F-06) - FR-RUN-9 — Normal completion MUST yield
RUN_STATE(done); an uncaught exception MUST stream the traceback asCONSOLE_DATA(stderr, …)and then emitRUN_STATE(error). — (source: PRD §8.2, protocol.md §6; verify: HIL, conformance; story: F-04, F-07) - FR-RUN-10 — After
STOPthe agent MUST return the board toRUN_STATE(idle). — (source: PRD §8.3, §10.6, protocol.md §6; verify: HIL; story: F-06)
- FR-FS-1 —
FILE_LISTMUST list a workspace directory rooted atfs_root. — (source: PRD §8.4, §10.8, protocol.md §4; verify: conformance, HIL; story: F-08) - FR-FS-2 —
FILE_STATMUST return the size and CRC of one path (used for resume), andENOENTfor a missing path. — (source: PRD §8.4, §10.8, protocol.md §5; verify: conformance, HIL; story: F-08, F-10) - FR-FS-3 — Download (
FILE_GET_BEGIN/FILE_GET_DATA/FILE_GET_END) MUST stream a file from an offset and report a whole-file CRC for the app to verify. — (source: PRD §8.4, §10.8, protocol.md §5; verify: conformance, HIL; story: F-08) - FR-FS-4 — Upload (
FILE_PUT_BEGIN/FILE_PUT_DATA/FILE_PUT_END) MUST accept windowed chunks with a sliding window of up toWunacknowledged chunks (Wfrom HELLO; the reference agent advertises W=8 with receiver queue depthW+2). Clients readWfrom caps and adapt — "up to W" client semantics are unchanged. — (source: PRD §8.4, §10.8, §13.4, protocol.md §5; verify: conformance, HIL; story: F-09, F-11; reference-agent W raised 4→8 2026-07-04[docs]to lift stop-and-wait upload throughput — no wire change, §5 offset/watermark ACK is W-agnostic; RAM cost ≈ +1 kB (4 queue slots × ~250 B), within the ESP32-C3 heap floor per NFR-FP-HEAP) - FR-FS-5 — The agent MUST emit
FILE_PUT_ACK { ack_offset }carrying the highest contiguous byte offset written, so the app advances the window and retransmits gaps. — (source: PRD §8.4, protocol.md §5; verify: conformance, HIL; story: F-09) - FR-FS-6 —
FILE_PUT_END { crc32 }MUST verify the whole-file CRC and report a transferOKonly after a full-file CRC match. — (source: PRD §8.4, §10.8, §13.1, protocol.md §5; verify: conformance, HIL; story: F-09) - FR-FS-7 — On a
FILE_PUT_BEGINfor a path that already holds a verified partial prefix, the agent MUST returnresume_offset > 0so the upload resumes rather than restarting. — (source: PRD §8.4, §13.1, protocol.md §5; verify: conformance, HIL; story: F-10) - FR-FS-8 —
FILE_DELETE,MKDIR, andFILE_RENAMEMUST be supported, each returning the correct status. — (source: PRD §8.4, §10.8, protocol.md §4; verify: conformance, HIL; story: F-09) - FR-FS-9 — Uploads MUST use temp-write-then-rename so a file is never corrupted mid-transfer. — (source: PRD §8.4, §10.4, firmware.md §5; verify: HIL, unit; story: F-09)
- FR-FS-10 — File operations MUST be confined to
fs_root; any path that escapes it (..traversal, absolute paths outside the root) MUST be rejected withEACCES. — (source: PRD §10.4, protocol.md §8; verify: unit, conformance; story: F-09) - FR-FS-11 — The agent control plane (Layer 3) and board overlay (Layer 2) MUST be forbidden paths, not writable (or replaceable) via PBLE/1 file commands; such attempts MUST return
EACCES. — (source: PRD §10.2, §10.4, §14.1; verify: unit, conformance; story: F-09) - FR-FS-12 — The workspace bridge MUST be
.py/data only; the agent MUST NOT require, generate, or depend on.mpy/.pycin the workspace, nor accept them as transfer artifacts. — (source: PRD §1A.3 rejection 6, §10.4, §10.14; verify: unit, conformance; story: F-09) - FR-FS-13 — The agent MUST report the writable workspace root as
fs_rootinDEVICE_INFO/HELLO. — (source: PRD §10.4, §10.8, protocol.md §7; verify: conformance, HIL; story: F-03) - FR-FS-14 — A whole-file CRC mismatch on
FILE_PUT_ENDMUST returnECRCand MUST NOT replace the target file. — (source: PRD §10.8, protocol.md §8; verify: unit, conformance, HIL; story: F-09) - FR-FS-15 — Filesystem errors MUST map to their PBLE/1 status codes (
ENOENT,ENOSPC,EACCES,EIO,ERANGE) rather than failing silently. — (source: PRD §9.2, §10.8, protocol.md §8; verify: conformance; story: F-08, F-09) - FR-FS-16 — The jail constrains the PBLE/1 file bridge only; user code MAY touch the filesystem normally at runtime via standard MicroPython
os/vfs. — (source: PRD §10.4, firmware.md §5; verify: HIL; story: F-09)
- FR-CON-1 — The agent MUST tee the running program's
stdout/stderrto BLE asCONSOLE_DATAevents. — (source: PRD §8.2, §10.3, protocol.md §6; verify: HIL, conformance; story: F-07) - FR-CON-2 —
CONSOLE_DATAevents MUST distinguish thestdoutandstderrstreams. — (source: PRD §9.4, protocol.md §6; verify: conformance, HIL; story: F-07) - FR-CON-3 —
CONSOLE_INPUT { bytes }MUST feed bytes to a program blocked oninput()/sys.stdin. — (source: PRD §8.2, §10.8, protocol.md §6; verify: HIL; story: F-07) - FR-CON-4 — The console MUST be observe-anywhere:
stdout/stderrMUST stream regardless of which client triggered the run. — (source: PRD §8.2, §10.8, protocol.md §6; verify: HIL; story: F-07) - FR-CON-5 — The agent MAY also mirror
stdout/stderrto USB-serial when present, for local debugging only; USB serial MUST NOT be a runtime PBLE/1 transport. — (source: PRD §10.3, §11.2, firmware.md §5; verify: HIL; story: F-07)
- FR-INFO-1 —
DEVICE_INFOMUST report at leastchip, MicroPython version, free memory,fs_root, MTU, the stabledevice_id(the MAC-derived suffix, per FR-BLE-5), andlabel(the user-set device label, or empty when unset). — (source: PRD §8.1, §10.8, protocol.md §2, §4; verify: conformance, HIL; story: F-03) - FR-INFO-2 —
HELLOMUST be the first exchange after connect, performing protocol-version and capability negotiation per protocol.md §7. — (source: PRD §10.5, §18.3, protocol.md §7; verify: conformance, HIL; story: F-03) - FR-INFO-3 — The HELLO reply
capsMUST includechip,mpy_version,fs_root,max_file_size,put_window(W),chunk_size,has_sd,free_mem,device_id(the stable MAC-derived suffix),label(the user-set label, or empty),has_identify(whether the board supportsIDENTIFY), andidentify_led(the configured identify-LED GPIO, or null). The client MUST offer an Identify action only whenhas_identifyis set. These identity/identify caps are additive within PBLE/1; an older client simply ignores them. — (source: PRD §10.8, §18.3, protocol.md §7, §9; verify: conformance; story: F-03) - FR-INFO-4 — A read of the INFO characteristic MUST return a
DEVICE_INFO-equivalent payload so a client can identify a board before subscribing. — (source: PRD §10.7, §18.3, protocol.md §2; verify: HIL, conformance; story: F-01, F-03) - FR-INFO-5 — The agent MUST reply to
HELLOwith a chosenproto_versionit supports, and MUST refuse (rather than silently mis-speak) a client whose offered versions it cannot satisfy. — (source: PRD §18.3, protocol.md §7, §9; verify: conformance; story: F-03, P-03) - FR-INFO-6 —
caps.has_sdMUST reflect actual SD-card presence on the board. — (source: PRD §10.3 (pyble_info), §10.8, protocol.md §7; verify: HIL; story: F-03)
- FR-BOOT-1 — On power-up the agent MUST initialize, start BLE advertising, and wait for a connection. — (source: PRD §8.3, §10.5, firmware.md §5; verify: HIL; story: F-12)
- FR-BOOT-2 — By default the agent MUST NOT auto-run the user's
main.py; the board boots into agent mode regardless of workspace contents. — (source: PRD §1A.4, §8.3, §10.5; verify: HIL; story: F-12) - FR-BOOT-3 — Auto-run MUST be opt-in only, gated behind an explicit capability flag surfaced in
DEVICE_INFO/HELLO. — (source: PRD §10.5, §13.3, firmware.md §5; verify: HIL, conformance; story: F-12) - FR-BOOT-4 — The agent MUST reach the advertising state independently of user-workspace validity; a syntactically broken or infinite-loop
main.pyMUST NOT prevent advertising or connection. — (source: PRD §10.5, §13.1; verify: HIL; story: F-12) - FR-BOOT-5 — The agent MUST NOT depend on an editable
boot.py/main.pyfor its own operation. — (source: PRD §1A.3 rejection 5, §10.2; verify: HIL; story: F-12) - FR-BOOT-6 — A control-plane fault MUST fail safe to the advertising state rather than wedging the board. — (source: PRD §13.1, §10.5; verify: HIL; story: F-12)
- FR-MODE-1 — The agent MUST support agent mode (idle): advertising and/or connected, servicing PBLE/1 file/info/console-input commands with no user program executing;
RUN_STATEreportsidle. — (source: PRD §10.6, protocol.md §6; verify: HIL, conformance; story: F-03, F-12) - FR-MODE-2 — The agent MUST support run mode: a user program executes on the runner task while the BLE/agent task continues to service the link;
RUN_STATEreportsrunning, thendoneorerror. — (source: PRD §10.6, firmware.md §5; verify: HIL; story: F-04) - FR-MODE-3 — Every mode transition MUST be emitted as a
RUN_STATEevent. — (source: PRD §10.6, protocol.md §6; verify: conformance, HIL; story: F-04) - FR-MODE-4 — The agent MUST remain in agent mode (not auto-enter run mode) on cold boot unless the auto-run capability (FR-BOOT-3) is enabled. — (source: PRD §10.5, §10.6; verify: HIL; story: F-12)
These are per-device configuration the agent owns for its own screenless-pairing UX — a human-readable label and a single optional status-LED to blink. They map no hardware for user code, are not a routing/pin profile or board-capability map, and never gate access (see CON-13, SEC-10/11). They exist because many initial and future supported boards are screenless.
- FR-IDENT-1 —
SET_LABELMUST persist a bounded-length UTF-8 device label to NVS and, on success, MUST make that label the advertised device name (FR-BLE-12) and theDEVICE_INFO.label/HELLOlabelvalue; an empty label MUST clear the stored label, restoring thePyBLE-XXXXdefault. An over-length label MUST be rejected withERANGEand MUST NOT be stored. — (source: PRD §10.7, §14.2, protocol.md §4, §2, §10; verify: conformance, HIL; story: F-03) - FR-IDENT-2 —
SET_IDENTIFY_LEDMUST persist a single identify status-LED configuration — one GPIO number plus its active level — to NVS. This is device config for the IDENTIFY blink only: it MUST NOT be exposed to user code, MUST NOT map or reserve hardware for user-code routing, and MUST NOT be treated as a routing/pin profile. — (source: PRD §11.1, §11.3, protocol.md §4, hardware.md §4; verify: conformance, unit (structure); story: F-03) - FR-IDENT-3 —
IDENTIFYMUST blink the configured identify LED for a bounded duration so the user can spot the physical board, and MUST do so on a non-blocking path that does not stall the BLE/agent task or a running user program (FR-BLE-11, FR-RUN-3). — (source: PRD §10.6, §13.3, protocol.md §4, §6; verify: HIL, conformance; story: F-03) - FR-IDENT-4 —
IDENTIFYMUST returnEUNSUPPORTED(0x0A) when no identify LED has been configured, and the board MUST reporthas_identify = falseandidentify_led = nullin HELLO/DEVICE_INFOuntil one is configured. — (source: PRD §10.8, protocol.md §8, §7; verify: conformance; story: F-03) - FR-IDENT-5 — The device label and the identify-LED configuration MUST survive reboot (persisted in NVS), so the advertised name,
has_identify, andidentify_ledare stable across power cycles. — (source: PRD §10.7, §10.5; verify: HIL, conformance; story: F-03, F-12) - FR-IDENT-6 — The identify blink MUST be cosmetic only: it MUST NOT be used for, or be repurposable as, GPIO routing for user code, a board-capability map, or any access-gating signal. — (source: PRD §1A.3 rejection 3, §11.1, §11.3, hardware.md §4; verify: unit (structure); story: F-01, F-03)
FROZEN 2026-07-28 (
[docs], ADR-0018). This is an additive user-runtime/build contract. It changes no PBLE/1 byte, capability, agent module, or board profile.
- FR-LIB-1 — Every
esp32,esp32-s3, andesp32-c3firmware image MUST make the pinned upstream MicroPythonneopixel.NeoPixelAPI importable offline by user file/source runs and after a soft reboot. MUST (source: PRD §9.8, §11.3; verify: resolved-manifest/build/HIL; story: F-24/A-31) - FR-LIB-2 — The module MUST be selected from the pristine pinned MicroPython/micropython-lib tree through each target's frozen manifest; PyBLE MUST NOT copy, fork, patch, or replace it with a custom WS2812 driver. MUST (source: PRD §1A, §10.9, §10.10; verify: build/structure; story: F-24)
- FR-LIB-3 — Bundling NeoPixel MUST NOT add an agent GPIO abstraction, PBLE/1 opcode/capability, board/onboard-LED name, pin/count/colour default, or target-specific user-code routing. GPIO, pixel count, index, colour, timing, and physical suitability remain explicit user-program/runtime concerns. MUST (source: PRD §9.8, §11.3; verify: unit/no-leak/HIL; story: F-24/A-31)
- FR-LIB-4 — Release validation MUST resolve exactly one
neopixel.pyfor each of the three build targets, record the per-target firmware-size delta, and run a runtime import smoke on every exact profile included in that release. The current v0.4.2 formal runtime-qualification matrix is the two profiles in §2.2 and remains open beyond the supplemental browser run;esp32-c3-4mbruntime smoke remains required before that profile is enabled and before v1.0. Any visual LED smoke MUST take an operator-supplied GPIO, use a bounded dim sequence, and turn the pixel off on exit. MUST (source: PRD §10.11, §10.13, §13.3; verify: build/size/HIL; story: F-24)
This NeoPixel contract applies to the three initial ESP32-family images. A future platform port MUST NOT claim equivalent support until it validates the upstream package and required runtime primitive for that target.
FROZEN for v1.0 (G0 · 2026-07-01 ·
[docs]). Amend only via a[docs]commit before dependent code.
- NFR-REL-1 — The agent (control plane) MUST stay alive and BLE-responsive even when user code crashes, loops, or exhausts its own resources. — (source: PRD §13.1, §13.3; verify: HIL; story: F-06)
- NFR-REL-2 — A file transfer MUST be reported successful only after a whole-file CRC match; a partial transfer MUST be resumable from the verified offset, not restarted from zero by default. — (source: PRD §13.1, protocol.md §5; verify: HIL, conformance; story: F-10)
- NFR-REL-3 — File transfer MUST never silently corrupt or duplicate data across a dropped link. — (source: PRD §13.1; verify: HIL, unit; story: F-10, F-11)
- NFR-REL-4 — Firmware builds MUST be reproducible from the pinned versions: the same commit +
versions.lockMUST yield equivalent artifacts. — (source: PRD §13.1, §10.11; verify: build; story: X-03) - NFR-REL-5 — A multi-file upload MUST complete without dropping the connection (the v1.0 reliability acceptance). — (source: PRD §4.1, §10.1, §7.1; verify: HIL; story: F-11)
- NFR-PERF-1 — At MTU 247, BLE throughput MUST meet the frozen per-profile PUT and GET goodput floors in §5.3 on every exact profile included in a release. The ESP32-C3 floors MUST be validated before that profile is enabled and before v1.0. — (source: PRD §13.4, §10.13; verify: HIL; story: F-11, F-13/14)
- NFR-PERF-2 — File transfer MUST use windowed chunks (
Wadvertised in HELLO caps; reference-agent default windowW=8, chunk sized to one MTU) with cumulative-offset ACKs. — (source: PRD §13.4, protocol.md §5; verify: conformance, HIL; story: F-09; reference-agent W raised 4→8 2026-07-04[docs], no wire change) - NFR-PERF-3 — Interactive console latency (
CONSOLE_INPUT→ echo, andstdout→ event) MUST stay low enough to feel live. — (source: PRD §13.4; verify: HIL; story: F-07) - NFR-PERF-4 — The concrete boot-latency ceiling and PUT/GET goodput floors MUST be derived from retained HIL baseline samples by the frozen formulas in §5.3, then enforced against the final candidate. They are tracked requirements until frozen, not asserted up front. — (source: PRD §13.4, §10.13; verify: HIL; story: F-13/14)
FROZEN measurement contract (2026-07-30 ·
[docs]). This amendment freezes the release scope, metric meanings, workload, derivation formulas, and evidence schema before any threshold is selected. It does not invent or claim a numeric threshold.
The current v0.4.2 public-beta set is exactly, and in this order, esp32-4mb
and esp32-s3-n16r8. Production-browser installation and interrupted-flash
recovery passed for both under the bounded exception in
browser-flashing §10.
Each still MUST have a complete numeric policy and final-candidate HIL record
before the release may be called qualified. esp32-c3-4mb MUST NOT have a
threshold entry or HIL row in this pre-v1 policy. It remains a
build/source/license-audit target, but its numeric qualification remains open
and blocks enabling C3 and blocks v1.0. The v1.0 matrix remains all three
profiles.
- NFR-FP-FLASH — The total shipped application image MUST not exceed its frozen per-profile ceiling and MUST leave at least the frozen headroom in the factory application partition. This total-image gate, not an unmeasurable “agent overhead” estimate, is the normative flash gate. A matched no-agent delta MAY be reported as supplemental evidence only. — (source: PRD §10.13 (FP-FLASH), §11.2; verify: size, build; story: F-13/14)
- NFR-FP-HEAP — After connect + HELLO and after the frozen transfer
workloads, Python GC headroom and internal ESP-IDF heap MUST each meet their
frozen per-profile floors. Default-capability heap reported by the existing
free_memcap is diagnostic only and MUST NOT substitute for these gates, especially on a PSRAM-equipped S3. — (source: PRD §10.13 (FP-HEAP); verify: HIL; story: F-13/14) - NFR-FP-BOOT — Controlled reset release → first fresh on-air advertisement containing the PyBLE service UUID MUST not exceed the frozen per-profile ceiling. A separate physical power-cycle advertising check MUST pass once per final profile record. — (source: PRD §10.13 (FP-BOOT); verify: HIL; story: F-12, F-13/14)
- NFR-FP-TPUT — At an observed negotiated ATT MTU of exactly 247, committed PUT goodput and verified GET goodput MUST each meet the frozen per-profile floor, while the frozen reliability workload remains byte/CRC clean with no unexpected disconnect. — (source: PRD §10.13 (FP-TPUT); verify: HIL; story: F-11, F-13/14)
- NFR-FP-C3 — If the agent does not fit the ESP32-C3 flash/heap budget with usable user-code headroom, the design MUST change (e.g. native
USER_C_MODULEhot paths per §6), not the constraint. — (source: PRD §10.13, §10.2; verify: size, HIL; story: F-13/14) - NFR-FP-GATE — Once frozen, the application-image ceiling/headroom floor MUST be enforced during build/candidate validation, and every runtime heap/boot/goodput/reliability threshold MUST be evaluated by the machine-readable final-candidate HIL validator. A crossing MUST fail the applicable gate. — (source: PRD §10.13, §1B.3; verify: size, build, HIL; story: X-03, F-13/14)
- NFR-FP-CLOSE — Every exact profile included in a qualified release is release-blocking until all of its thresholds are frozen and its hash-locked final-candidate evidence passes. For a qualified two-profile pre-v1 release this means exactly the two profiles above. The exact v0.4.2 beta exception does not satisfy or waive this gate. The still-open C3 portion blocks any C3 release and v1.0. — (source: PRD §10.12, §10.13, §7.1; verify: size, HIL; story: F-13/14)
All quantities are non-negative JSON integers; booleans are not integers. Byte sizes are base-2 byte counts, durations use a monotonic host clock, and goodput is integer bytes per second.
| Key | Frozen definition | Gate direction |
|---|---|---|
application_image_bytes |
Exact byte length of the bundled application.bin, whose authoritative build input is micropython.bin. It is not merged firmware.bin, ELF size, physical flash capacity, or an estimated agent-only delta. |
<= application_image_max_bytes |
factory_partition_bytes |
Exact factory application-partition size parsed from the candidate partition table. | identity/arithmetic |
application_headroom_bytes |
factory_partition_bytes - application_image_bytes; a negative result is an unconditional failure. |
>= application_headroom_min_bytes |
gc_free_bytes |
gc.mem_free() immediately after gc.collect() in a bounded PBLE RUN probe executed on the MicroPython VM thread. |
>= gc_free_min_bytes |
gc_allocated_bytes |
gc.mem_alloc() from the same probe; retained as a diagnostic and not used as a substitute for gc_free_bytes. |
report only |
idf_internal_free_bytes |
Sum of the free fields returned by esp32.idf_heap_info(2052), where 2052 == MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT. |
>= idf_internal_free_min_bytes |
idf_internal_largest_block_bytes |
Maximum largest-free-block field across those internal 8-bit heap regions. | >= idf_internal_largest_block_min_bytes |
idf_internal_minimum_free_bytes |
Sum of the minimum-free-ever fields across those regions. | >= idf_internal_minimum_free_min_bytes |
heap_default_free_bytes |
Existing HELLO/INFO free_mem value (MALLOC_CAP_DEFAULT); on S3 it may include PSRAM. |
report only |
reset_to_service_advertisement_ms |
From controlled EN/reset release to the first fresh scanner event that contains the PyBLE service UUID, rounded up to whole milliseconds. | <= reset_to_service_advertisement_max_ms |
put_committed_goodput_bytes_per_second |
floor(65536 * 10^9 / duration_ns), with time from immediately before sending FILE_PUT_BEGIN through the successful FILE_PUT_END response; only unique committed payload bytes are the numerator and FILE_STAT verification is outside the timer. |
>= put_committed_goodput_min_bytes_per_second |
get_verified_goodput_bytes_per_second |
floor(65536 * 10^9 / duration_ns), with time from immediately before sending FILE_GET_BEGIN through a valid FILE_GET_END; offsets MUST be contiguous and unique, and returned size, bytes, and whole-file CRC MUST match. |
>= get_verified_goodput_min_bytes_per_second |
The heap probe MUST keep these measurements out of HELLO/INFO capabilities. It uses existing standard MicroPython APIs and therefore adds no dynamic PBLE/1 capability and no INFO/HELLO equivalence ambiguity.
For each exact profile and one immutable firmware/manifest candidate:
- Perform 10 controlled reset samples. Start a service-UUID-filtered BLE scan, hold EN/reset asserted for 1,000 ms, fail the sample if a matching advertisement is observed during that quiet interval, release reset, and record the first fresh matching advertisement. The per-sample discovery timeout is 15,000 ms; a timeout is a gate failure, not a latency sample. Connect and complete HELLO after each successful sample.
- Record one
heap_default_free_bytesdiagnostic and one gated heap snapshot (gc_free_bytes,gc_allocated_bytes, and the three internal-heap quantities) after each of those 10 HELLO exchanges. - On a connection that reports HELLO
mtu=247,window=8, andchunk=229, run 5 PUT+GET round trips of exactly 65,536 bytes with no chunk or window override. When the central backend exposes its negotiated ATT MTU, it MUST agree with the HELLO value; an unknown backend value MUST NOT be manufactured as evidence. Record one gated heap snapshot after each round trip. - The deterministic payload for zero-based sample
sis the first 65,536 bytes of concatenated SHA-256 blocksSHA256("PyBLE-OI1-v1\\0" || UTF8(profile_id) || "\\0" || u32le(s) || u32le(block_index)), withblock_indexstarting at zero. - Separately run the reliability workload once: 20 files × 16,384 bytes = 327,680 bytes. All 20 files MUST complete and match byte-for-byte, size, and CRC; unexpected disconnects, corruption, and failed statuses MUST all be zero. Retransmitted chunks/bytes and rewinds MAY be non-zero but MUST be counted. Record one final gated heap snapshot after this workload.
- Perform one real physical power-cycle check and require a fresh PyBLE service advertisement. This is a pass/fail safety check; human timing is not used as a numeric latency sample.
Thus each heap floor is derived from exactly 16 snapshots: 10 post-HELLO, 5 post-round-trip, and 1 post-reliability. The numeric qualification is a controlled-hard-reset proxy for cold boot because it provides an external, repeatable release-to-on-air boundary; the physical power-cycle check prevents that proxy from replacing real power-on behaviour.
The derivation algorithm is frozen before measurement. Define
floor_q(x) = q * floor(x/q) and ceil_q(x) = q * ceil(x/q).
application_image_max_bytesis the exact application byte count from two clean, independently retained build roots; the two application images MUST first be byte-identical.application_headroom_min_bytesis the exact correspondingfactory_partition_bytes - application_image_bytes.- Each GC/internal-heap floor is
floor_1024(min(all 16 samples)). reset_to_service_advertisement_max_msisceil_10(max(the 10 successful samples)).- Each PUT/GET goodput floor is
floor_100(min(the 5 samples)).
The outward quantization above is the only automatic tolerance: at most 1,023 bytes below an observed heap minimum, 9 ms above an observed boot maximum, or 99 bytes/s below an observed goodput minimum. No percentage, aspirational number, manually selected “comfortable/usable” value, discarded successful sample, or result from another profile may enter a threshold.
Raw baseline samples and environment metadata MUST be retained as redacted,
canonical JSON at
docs/validation/firmware/oi1/<40-hex-source-commit>.json.
Canonical bytes are UTF-8 JSON with lexicographically sorted object keys,
two-space indentation, LF line endings, no trailing whitespace, and one final
LF; arrays retain the normative order below.
That baseline object has exactly schema_version (integer 1),
measurement_contract (string "oi1-pre-v1-v1"), source_commit (the same
40 lowercase hex used in its filename), firmware_version, created_at (UTC
RFC3339), profile_order (the exact two-profile order), and profiles. Each
of its two profile objects has exactly:
profile_id
target
board_manufacturer
board_model
module_marking
device_flash_capacity_bytes
device_psram_capacity_bytes
firmware_sha256
manifest_sha256
environment
oi1_build
oi1_observation
environment has exactly desktop_os, ble_backend, ble_adapter, and
python_version, all non-empty strings. oi1_build and completed
oi1_observation use the exact objects in
browser-flashing.md §9.2.
The baseline is engineering derivation evidence, not a release HIL record: it
has no candidate release digest, operator approval, installer/recovery check,
or public-release status.
firmware/qualification/oi1-gates.json MUST then contain exactly:
schema_version: 1;qualification_scope: "pre-v1";profile_order: ["esp32-4mb", "esp32-s3-n16r8"];deferred_profiles: ["esp32-c3-4mb"];workload, with the exact constants and payload generator from §5.3.2;derivation, naming the exact algorithms and quantization units above;baseline_evidence, with the repository-relative path and exact lowercase SHA-256 of the canonical baseline JSON; andprofiles, inprofile_order, each containing exactprofile_id, buildtarget, and athresholdsobject with the nine gate keys defined above: application maximum, application headroom minimum, four heap minima, reset-to-advertisement maximum, and PUT/GET goodput minima.
The policy validator MUST require all nine numeric keys. All threshold values MUST be positive integers. The policy MUST contain no C3 threshold object. A final candidate is evaluated against this committed policy; the engineering baseline derives the policy but does not itself approve a release.
The release HIL document MUST use the exact
PYBLE_HIL_RECORDS_V2 contract in
browser-flashing.md §9.
The policy bytes, baseline-evidence digest, candidate identity, per-profile
build measurements, raw sample arrays, environment, and raw-log digest MUST be
machine-verifiable. Candidate generation freezes the policy and build
measurements; finalization may add HIL observations and operator sign-off but
MUST NOT change those frozen fields. A changed firmware, manifest, policy, or
candidate identity invalidates the affected evidence.
This is software-level safety of the IDE/agent, not hardware/actuator safety (out of scope, §6).
- NFR-SAFE-1 —
STOPMUST be authoritative: it MUST promptly interrupt the runner, tear down cleanly, and reportRUN_STATE. — (source: PRD §13.3, §8.3, protocol.md §6; verify: HIL; story: F-06) - NFR-SAFE-2 — User code MUST NOT be able to wedge BLE or the agent: the runner runs on a task separate from the BLE/agent task so the link and
STOPremain serviceable under any user-code behaviour. — (source: PRD §1A.3 rejection 5, §13.3, §10.2; verify: HIL; story: F-06) - NFR-SAFE-3 — Cold boot MUST be safe: advertise and wait, never auto-run
main.pyunless explicitly enabled (FR-BOOT-2/3). — (source: PRD §13.3, §10.5; verify: HIL; story: F-12) - NFR-SAFE-4 — The agent MUST NOT contain or imply any hardware-output, calibration, or other physical-safety guard; physical safety belongs to the user's own program. — (source: PRD §13.3, §11.3, §4.3; verify: unit (structure review); story: F-01)
- NFR-OFF-1 — The agent MUST function with no network connection of any kind; it MUST NOT require Wi-Fi onboarding or any server round-trip to operate. — (source: PRD §13.5, §1A.3 rejection 2, §1A.3 rejection 8; verify: HIL; story: F-01)
- NFR-OFF-2 — The agent MUST require no account, cloud sync, or telemetry to edit, run, or manage code on a board. — (source: PRD §13.5, §14.2, §15.1; verify: HIL, unit; story: F-01)
FROZEN for v1.0 (G0 · 2026-07-01 ·
[docs]). NFR-MAINT-2's six-module layout is frozen; the source-tree layout that realizes it is frozen in TDD.md §10.5.
- NFR-MAINT-1 — Within the initial ESP32 port, the shared agent core MUST contain no per-chip product logic; chip differences MUST live in the Layer-2 board overlay. Across MicroPython ports, platform-specific BLE, runtime, storage, identity, and build code MUST remain behind the broader target adapter boundary. — (source: PRD §10.1, §10.2, firmware.md §4; verify: build, unit; story: F-13/14)
- NFR-MAINT-2 — The agent MUST be organized as the six single-responsibility modules
pyble_ble/pyble_proto/pyble_runner/pyble_fs/pyble_console/pyble_info. — (source: PRD §10.3, firmware.md §3; verify: unit (structure); story: F-01…F-09) - NFR-MAINT-3 — Moving hot paths from frozen-Python to a native
USER_C_MODULEMUST NOT change the PBLE/1 wire contract. — (source: PRD §10.2, §16.2, firmware.md §2; verify: conformance; story: F-13/14) - NFR-MAINT-4 — Every PyBLE firmware source file MUST carry the
SPDX-License-Identifier: MITheader. — (source: PRD §15.1, AGENTS.md; verify: build (lint); story: X-01)
FROZEN for v1.0 (G0 · 2026-07-01 ·
[docs]). All CON-1…13 are clean-room/build/carve-out constraints that inherit no PBLE/1 opcode/UUID/status number; amend only via a[docs]commit before dependent code.
- CON-1 — Upstream MicroPython MUST be consumed as a pinned submodule and MUST NEVER be edited in place. — (source: PRD §1A.3 rejection 4, §10.9, §10.10,
firmware/upstream/README.md; verify: build; story: X-03) - CON-2 — PyBLE MUST NOT fork MicroPython; it builds an agent around upstream. — (source: PRD §1A.3 rejection 4, §4.3, §6.2; verify: build; story: X-03)
- CON-3 — The workspace and any future PyBLE package format MUST be
.pysource only; no.mpy/.pycin the workspace or in transfer. — (source: PRD §1A.3 rejection 6, §10.14, §4.3; verify: unit, conformance; story: F-09) - CON-4 — Per-chip configuration MUST live only in the Layer-2 board overlay, copied into the upstream
ports/esp32/boards/tree at build prep so the submodule stays pristine. — (source: PRD §10.2, §10.11; verify: build; story: X-03, F-13/14) - CON-5 — The initial ESP32 firmware MUST use NimBLE on all three v1 targets; Bluedroid MUST NOT be built in. A future platform port MAY use another conforming BLE peripheral stack. — (source: PRD §10.1, §16.2; verify: build; story: F-01)
- CON-6 — All firmware source MUST be MIT / clean-room: it MUST contain no closed-source wire protocol, opcodes, proprietary UUIDs/advertising prefixes, board/routing profiles, or lab-domain/calibration code; the no-leak gate MUST pass. — (source: PRD §1A.1, §1A.2, §16.3; verify: build (no-leak gate); story: X-02)
- CON-7 — The firmware MUST NOT impose, store, or transmit any board "routing profile" or pin profile, and MUST NOT gate by MAC or board identity. — (source: PRD §1A.3 rejection 3, §11.1, §11.3, hardware.md §4; verify: unit (structure); story: F-01)
- CON-8 — The firmware MUST NOT contain any GPIO-routing, hardware-output-safety, display/branding, calibration, or board-profile module. — (source: PRD §10.3, §11.3, §4.3; verify: unit (structure); story: F-01)
- CON-9 — The board's hardware MUST be exposed to user code only through standard MicroPython (
machine,os, etc.); the agent MUST NOT mediate or abstract GPIO. — (source: PRD §11.3, hardware.md §4; verify: unit (structure); story: F-01) - CON-10 — The agent (Layer 3) MUST NOT be editable or replaceable by user code (Layer 4). — (source: PRD §10.2, §10.4, §14.1; verify: conformance, unit; story: F-09)
- CON-11 — A single MicroPython + ESP-IDF pin (from
versions.lock) MUST drive all three chip targets. — (source: PRD §1A.4, §10.9, §10.11; verify: build; story: X-03) - CON-12 — The default upstream patch count MUST be zero; any unavoidable patch MUST be isolated under
firmware/patches/micropython-<tag>/with a written reason, applied only at build prep, and re-reviewed for retirement at every upgrade. — (source: PRD §1A.3 rejection 4, §10.10, firmware.md §6; verify: build; story: X-03) - CON-13 — The identify-LED configuration MUST be one optional config integer (a single GPIO + active level) owned by the agent for the IDENTIFY blink only. It is explicitly NOT a routing/pin profile, board-capability map, or actuator mapping, and MUST NOT be exposed to user-code routing or used to mediate/abstract GPIO for user code (see CON-7, CON-8, CON-9, FR-IDENT-2/6). — (source: PRD §1A.3 rejection 3, §11.1, §11.3, hardware.md §4; verify: unit (structure); story: F-01, F-03)
- IF-BLE — The board's primary external interface is the BLE GATT service (RX Write / TX Notify / INFO Read) carrying PBLE/1 frames; the wire definition is owned by protocol.md §2 and §3. — (source: PRD §10.7, §16.2, protocol.md §2; verify: HIL, conformance; story: F-01, F-02)
- IF-PROTO — All app↔board messages MUST conform to the PBLE/1 framing, opcode, and status definitions in protocol.md §3/§4/§8; the firmware references these and MUST NOT redefine them. — (source: PRD §10.8, protocol.md; verify: conformance; story: F-02)
- IF-USB — For the initial ESP32 port, USB serial is an interface for initial flashing (the natural esp-web-tools channel) and optional local debug console mirroring only; it MUST NOT be a runtime PBLE/1 transport. Future ports document their own one-time provisioning method. — (source: PRD §11.2, §1A.3 rejection 1, §10.3; verify: HIL; story: F-07)
- IF-FS — The agent MUST present the user workspace over a MicroPython VFS / LittleFS-style filesystem rooted at
fs_root. — (source: PRD §16.2, §10.4, firmware.md §3; verify: HIL; story: F-08) - IF-MACHINE — The board's hardware is exposed to user code via standard MicroPython runtime APIs (
machine,neopixel, etc.); this is the user-code interface, not an agent-mediated one (see CON-9 and FR-LIB). — (source: PRD §11.3, hardware.md §4; verify: HIL; story: F-04/F-24)
FROZEN v1.0 (amended) for the initial ESP32 v1 port (G0 · 2026-07-01; browser-release amendments 2026-07-29 through 2026-07-31 ·
[docs]). BLD-1…22 are the build/versioning contract build-smith implements. The 2026-07-29 amendments tighten BLD-5…8/13/14 and add BLD-17…22 before X-10/X-11 code; the 2026-07-30 amendment freezes the two-profile pre-v1 release subset and makes the immutable same-origin directory canonical for v0.x, with an optional byte-identical mirror, without weakening three-target or GitHub Release v1.0 parity. The concreteversions.lockvalues remain proposed under OI-2 until selected as candidate-frozen inputs before release builds/HIL; candidate-freezing is immutability, not HIL approval. The schema and rules are frozen. Future platform ports define equivalent pinned build, artifact, provisioning, and HIL contracts. Amend only via a[docs]commit before dependent code.
- BLD-1 —
firmware/versions.lockMUST be the single source of truth for the pinned MicroPython tag+commit and ESP-IDF version+commit. — (source: PRD §10.9, §17.1; verify: build; story: X-03) - BLD-2 — The build prep MUST verify the checked-out upstream submodule SHA against
versions.lockand refuse to proceed on mismatch (SHA-drift gate); CI MUST run this on every PR. — (source: PRD §10.9, §17.1; verify: build; story: X-03) - BLD-3 —
firmware/scripts/build.sh <target>MUST build exactly one chip (esp32|esp32-s3|esp32-c3);build_all.shMUST build all three. — (source: PRD §10.11, firmware.md §6; verify: build; story: X-03) - BLD-4 — The build MUST map each PyBLE target to its IDF target (
esp32→esp32,esp32-s3→esp32s3,esp32-c3→esp32c3) and apply the matching board overlay before invoking the port build; it MUST NOT silently substitute a different toolchain version. — (source: PRD §10.11,versions.lock; verify: build; story: X-03) - BLD-5 — Each successful per-target build MUST emit the merged
firmware.bin, bootloader, partition table, application image, and authoritative ESP-IDFflasher_args.json. Release packaging MUST normalize the application component name toapplication.binwithout changing its bytes, validate thatfirmware.binis the deterministic merge at the authoritative settings/offsets, and MUST NOT infer offsets from filenames. — (source: PRD §10.12, firmware.md §6, browser-flashing §3; verify: build; story: X-03, X-10) - BLD-6 — Release packaging MUST emit one profile-scoped
manifest.jsonper exact profile, each compatible with ESP Web Tools and containing exactly one build matching the schema, family, merged-image path, and base offset in browser-flashing §4, so a compatible-profile user can flash frompyble.dev/flashwith no local toolchain and a connected family other than the selected profile is rejected rather than offered another release image. — (source: PRD §10.12, §15.3, firmware.md §6; verify: build, HIL; story: X-10) - BLD-7 — One release MUST publish the complete, immutable, release-profile bundle at the canonical versioned same-origin path. A v0.x mirror is optional and every corresponding file and byte MUST be identical when one is published. v1.0 and later MUST additionally publish the matching byte-identical GitHub Release. The exact v0.4.2 public-beta bundle covers exactly the two enabled, not-yet-qualified profiles and its GitHub publication MUST be marked as a pre-release; v1.0 MUST restore three-target release parity. — (source: PRD §10.12, §18.2, browser-flashing §3; verify: build, release; story: X-11)
- BLD-8 — Every release MUST carry a mechanically generated
THIRD_PARTY_LICENSES.txtsatisfying browser-flashing §6; an unknown, missing, or license-incompatible resolved dependency MUST fail the build. The audit MUST reconcile compile commands with regular-file component sources and with only those ESP-IDF directory source markers that remain below their declaring component root; a marker covers compiled descendants but MUST NOT recursively admit uncompiled files. It MUST separately hash-bind the pinnedmaingenerated headers, reconcile every linked compile output exactly once to an archive member or exact direct-object linker-mapLOADand linker-command object, classify build-only outputs as unlinked, retain source/output/metadata hashes in the generatedmainbinding, repeat that observation after SBOM execution to close input races, and admit the pinned PyBLE, retained Berkeley DB, and zero-byte IDF ELF-anchor exceptions only in the exact roots, names, and object topologies frozen in browser-flashing §6. — (source: PRD §15.2, §15.3, firmware.md §6; verify: build; story: X-11) - BLD-9 — Upstream upgrades MUST go only through the controlled workflow (
firmware/scripts/upgrade_micropython.sh): bumpversions.lockin its own commit, rebuildmpy-cross, pass host + conformance + applicable footprint gates, candidate-freeze the exact updated lock before release-candidate generation, and validate that candidate on every exact profile included in the release — all three for v1.0 — before public-release approval. The workflow MUST never be replaced by hand-editing during a build. — (source: PRD §10.9, §17.1, §17.3; verify: build, HIL; story: X-03) - BLD-10 —
mpy-crossMUST be rebuilt from the pinned MicroPython. Every admitted and audit-proof rebuild MUST useSOURCE_DATE_EPOCHequal to the exact candidate PyBLE source commit's decimal committer timestamp recorded in build provenance; wall-clock date and caller overrides MUST NOT affect its bytes. — (source: PRD §10.9, §16.2,versions.lock; verify: build; story: X-03) - BLD-11 — ESP-IDF MUST be installed from the pin into a gitignored directory (not an outer submodule); MicroPython
lib/deps come from the standard port build. — (source: PRD §10.9, §17.1; verify: build; story: X-03) - BLD-12 — The firmware agent MUST follow SemVer
(
MAJOR.MINOR.PATCH); a backward-incompatible change bumps MAJOR.firmware/versions.lock[pyble].agent_versionis the canonical agent version for a source commit, and the importablepyble.__version__used byDEVICE_INFO/HELLO MUST equal it exactly. — (source: PRD §18.1; verify: build; story: X-11) - BLD-13 — A release MUST make the firmware-agent version, PBLE/1 version,
upstream MicroPython/ESP-IDF versions and commits, PyBLE source commit, image
profile, and artifact hashes recoverable from
DEVICE_INFO/HELLO,manifest.json,release.json, tag, and release notes as applicable. All surfaces MUST identify the same release. — (source: PRD §18.1, §18.2, §10.8; verify: build, conformance, release; story: X-11) - BLD-14 — Builds MUST be reproducible from a clean checkout given the
pinned versions. A public release requires two clean builds from the same
source and pinned toolchain to produce byte-identical released parts after
documented deterministic-build normalization. Within each reproducibility
build root, the three initial targets MUST build from independent retained
MicroPython checkouts at
.sources/<target>/micropython, where<target>is exactlyesp32,esp32-s3, oresp32-c3; the two roots MUST use that same relative layout. Each target build MUST be bound by its ESP-IDF application project description to the corresponding checkout at the exact locked commit, with the canonical locked origin and a clean tracked tree. A target MUST NOT share or overwrite another target's mutable source or ESP-IDF managed-component state. The checkouts MUST remain available through license audit and candidate validation, while proof inputs from the canonical candidate checkout remain independently hash-bound. Before application configuration or compilation, the runner MUST deterministically map the exact retained MicroPython checkout prefix to/MICROPYTHONand the exact PyBLE checkout prefix to/PYBLEin compiler debug, macro, and source-file paths, with the more-specific mapping winning when nested; preserve ESP-IDF's/IDF_BUILDmapping; and replace any ambient path-map flags. Neither clean source/build root may remain in the whole ELF; both whole-ELF hashes, embedded application-descriptor ELF hashes, application images, and merged images MUST be byte-identical. Root-local paths in non-shipped generated frozen-content comments may differ, but each root MUST independently reproduce its own generated input. — (source: PRD §10.11, §13.1, browser-flashing §2; verify: build; story: X-03, X-11) - BLD-15 — Any upstream patch MUST reside under
firmware/patches/micropython-<tag>/with a written reason and apply only at build prep (default zero, see CON-12). — (source: PRD §10.10, firmware.md §6; verify: build; story: X-03) - BLD-16 — The resolved frozen manifest for every initial ESP32-family
target MUST contain exactly one pinned upstream
neopixelmodule. Build verification MUST inspect generated frozen content or the running image, not stale intermediate.mpyfiles. — (source: FR-LIB, ADR-0018; verify: build/HIL; story: F-24) - BLD-17 — The v0.4.2 public-beta bundle MUST expose exactly
esp32-4mbandesp32-s3-n16r8, with the memory qualifications, merge settings, browser-image base offsets, and component offsets frozen in browser-flashing §1. Family detection MUST NOT be represented as proof of flash/PSRAM compatibility.esp32-c3-4mbMUST remain absent from release metadata, public artifacts, selection, and recovery commands and visibly unavailable until a new candidate passes its exact-profile HIL; v1.0 still requires all three. — (source: website §7, hardware §1.1; verify: build, HIL, website; story: X-10) - BLD-18 — Release packaging MUST generate the exact versioned layout,
schema-validated
release.json, SHA-256/size metadata, andSHA256SUMSdefined in browser-flashing §§3–5. Manifest paths MUST be relative, same-origin, version-confined, and free of redirects. — (source: website §7; verify: build, website; story: X-10, X-11) - BLD-19 — Release provenance MUST bind one SemVer, annotated tag, clean
PyBLE source commit, candidate-frozen
versions.lockbytes and upstream commits, exact toolchain, build environment, patch count, and artifact hashes as specified in browser-flashing §2. Any upstream pin change after candidate-freezing creates a new candidate and requires every build, audit, deployment, and exact-profile HIL gate to restart; candidate-freezing itself is not HIL or public-release approval. Any changed release byte outside the copy-on-write administrative promotion envelope frozen in browser-flashing §9 invalidates prior approval and HIL evidence. The envelope may change only the completed HIL report, its exact status/digest fields inrelease.json, and the correspondingSHA256SUMSentries; all other bytes remain identical to the protected candidate. — (source: PRD §17–§18; verify: build, release; story: X-11) - BLD-20 — Every bundle MUST include version-matched release notes, third-party license notices, recovery instructions, and a HIL report that satisfy browser-flashing §§6, 8, and 9. — (source: PRD §15.2–§15.3, website §7; verify: build, review, HIL; story: X-11)
- BLD-21 — The final hash-locked artifact set MUST pass the complete automated matrix and, on an access-controlled production-equivalent HTTPS candidate, browser install plus interrupted-flash recovery on real hardware for every exact profile included in that release. Except for the exact, digest-bound v0.4.2 public-beta exception in browser-flashing §10, the public action remains disabled until this passes. One chip, simulation, an older binary, or build-only evidence MUST NOT substitute for another profile. — (source: PRD §1B.3, website §7, browser-flashing §9; verify: build, HIL; story: X-10, X-11)
- BLD-22 — A version directory is immutable after publication. Activation and rollback MUST use the state transition and exact-version selection in browser-flashing §10; neither operation may mutate an existing bundle. — (source: website §6.1, §7; verify: release, production smoke; story: X-11)
- SEC-1 — v1.0 MUST use BLE link-layer pairing/encryption as the security baseline ("personal board on a workbench" model). — (source: PRD §14.1, protocol.md §10; verify: HIL; story: F-01)
- SEC-2 — v1.0 MUST NOT add application-layer authentication; at the application layer a connected client is trusted. No partial/implicit app-layer auth may ship in v1.0. — (source: PRD §14.1, §14.3, protocol.md §10; verify: conformance; story: F-01)
- SEC-3 — The agent MUST enforce a single active writer per connection: only one user program runs at a time (
EBUSYotherwise) and file/run operations are serialized so concurrent writers cannot corrupt workspace state. — (source: PRD §14.1, §10.6; verify: conformance, HIL; story: F-04, F-09) - SEC-4 — The agent MUST keep its control plane non-writable by user code, so "trusted client" never extends to overwriting the agent itself (see CON-10, FR-FS-11). — (source: PRD §14.1, §10.4; verify: conformance, unit; story: F-09)
- SEC-5 — The BLE advertisement MUST carry only the PyBLE Service UUID and a device name — the non-PII
PyBLE-XXXXdefault or, if set, the user's device label. The default name MUST contain no personal or user-identifying data; the board MUST NOT require a label to be set or require it to contain PII. — (source: PRD §14.2, protocol.md §2, §10; verify: HIL; story: F-01) - SEC-6 — On cold boot the board MUST be unowned: it advertises and waits, and the trust model is simply the connected client (no stored owner or board identity). — (source: PRD §14.1, §8.3, §10.5, protocol.md §10; verify: HIL; story: F-12)
- SEC-7 — The agent MUST NOT gate access by board identity or MAC, and MUST NOT build any remote registry of users or boards. — (source: PRD §14.2, §11.1; verify: unit (structure), HIL; story: F-01)
- SEC-8 — All user content (workspace files, console output) MUST stay on-device; the agent MUST NOT transmit anything off-device by default (no telemetry). — (source: PRD §13.5, §14.2, §15.1; verify: unit, HIL; story: F-01)
- SEC-9 — A future application-layer pairing token is deferred and undecided for v1.0; if added it MUST be negotiated through HELLO capabilities (additive, no breaking wire change within PBLE/1). — (source: PRD §14.3, §4.2, protocol.md §10, §9; verify: conformance; story: —)
- SEC-10 — Because the device label is broadcast in the advertisement, the board MUST bound the label length and MUST reject an over-length label with
ERANGE(FR-IDENT-1); the board MUST NOT require a label and the defaultPyBLE-XXXXis non-PII. (The app, out of scope here, warns the user against putting PII in a broadcast label.) — (source: PRD §14.2, protocol.md §10, §2; verify: conformance, HIL; story: F-01, F-03) - SEC-11 —
device_id, the device label, and the identify-LED configuration are for recognition/display only; the agent MUST NOT gate access, authorize commands, or branch its trust model on the MAC,device_id, or label (reinforces SEC-7, CON-7).SET_LABEL,SET_IDENTIFY_LED, andIDENTIFYare ordinary control commands under the v1 connected-client trust model. — (source: PRD §14.1, §14.2, §11.1, protocol.md §10; verify: unit (structure), conformance, HIL; story: F-01, F-03)
| Requirement ID(s) | PRD § | Story (F-/X-) | Verification method |
|---|---|---|---|
| FR-BLE-1…12 | §10.7, §10.6, §13.3 | F-01, F-02, F-06, F-13/14 | HIL, conformance, build |
| FR-PROTO-1…10 | §10.7, §10.8, §18.1 | F-02, F-03, P-01, P-03, P-04 | unit, conformance |
| FR-RUN-1…10 | §8.2, §8.3, §10.6 | F-04, F-05, F-06 | HIL, conformance |
| FR-FS-1…16 | §8.4, §10.4, §10.8 | F-08, F-09, F-10, F-11 | conformance, HIL, unit |
| FR-CON-1…5 | §8.2, §9.4, §10.3 | F-07 | HIL, conformance |
| FR-INFO-1…6 | §8.1, §10.8, §18.3 | F-03, P-03 | conformance, HIL |
| FR-BOOT-1…6 | §8.3, §10.5, §13.1 | F-12 | HIL, conformance |
| FR-MODE-1…4 | §10.5, §10.6 | F-03, F-04, F-12 | HIL, conformance |
| FR-IDENT-1…6 | §10.7, §11.1, §11.3, §14.2 | F-01, F-03, F-12 | conformance, HIL, unit |
| FR-LIB-1…4, BLD-16 | §9.8, §10.9, §10.11, §11.3 | F-24, A-31 | resolved manifest, build, size, HIL |
| NFR-REL-1…5 | §13.1, §13.3, §10.11 | F-06, F-10, F-11, X-03 | HIL, build, unit |
| NFR-PERF-1…4 | §13.4, §10.13 | F-07, F-11, F-13/14 | HIL, size |
| NFR-FP-FLASH/HEAP/BOOT/TPUT/C3/GATE/CLOSE | §10.13, §1B.3, §7.1 | F-12, F-13/14, X-03 | size, HIL, build |
| NFR-SAFE-1…4 | §13.3, §10.5, §11.3 | F-06, F-12, F-01 | HIL, unit |
| NFR-OFF-1…2 | §13.5, §14.2, §15.1 | F-01 | HIL, unit |
| NFR-MAINT-1…4 | §10.1, §10.2, §10.3, §15.1 | F-13/14, F-01…F-09, X-01 | build, unit, conformance |
| CON-1…13 | §1A.3, §10.9–§10.11, §10.14, §11.1, §11.3 | X-02, X-03, F-01, F-03, F-09, F-13/14 | build, unit, conformance |
| IF-BLE/PROTO/USB/FS/MACHINE | §10.7, §11.2, §16.2 | F-01, F-02, F-07, F-08, F-04 | HIL, conformance |
| BLD-1…22 | §10.9–§10.12, §15.2–§15.3, §17, §18 | X-03, X-10, X-11 | build, release, website, HIL |
| SEC-1…11 | §14, §10.4, §10.6, §11.1 | F-01, F-03, F-04, F-09, F-12 | HIL, conformance, unit |
These are tracked, release-blocking where noted; they MUST be closed before the v1.0 tag.
- OI-1 — Per-profile resource numbers pending HIL. The measurement method,
exact current scope, evidence contract, and threshold derivation are frozen
in §5.3. Numeric thresholds remain open. The current pre-v1 portion closes
only when
esp32-4mbandesp32-s3-n16r8each have committed evidence-derived policy values and passing final-candidate HIL. That state MUST be described as “qualified for the current two-profile pre-v1 release”, not as global OI-1 closure.esp32-c3-4mbremains open and release-blocking for C3 enablement and v1.0; it requires a later SemVer candidate and its own thresholds/evidence. — (verify: size, build, HIL) - OI-2 — v0.4.2 version-pin selection is closed; full release approval remains open.
The exact
versions.lockbytes for MicroPythonv1.28.0and ESP-IDFv5.5.1were selected and candidate-frozen for v0.4.2 (PRD §10.9, §17.1,versions.lock). That selection and the supplemental browser run are not complete hardware approval: the exact two-profile candidate still requires every remaining formal HIL and resource gate before qualification. C3 remains mandatory before C3 enablement and before v1.0. A pin change creates a new candidate and resets all candidate-bound evidence. — (verify: build, HIL) - OI-3 — Frozen → native split point TBD. The agent starts frozen-Python; the decision of which hot paths (BLE I/O, framing, file chunking) move to a native
USER_C_MODULE, and on which chip the budget forces it, is open and determined by HIL footprint/throughput measurement (firmware.md §2, PRD §10.2). The PBLE/1 wire contract MUST NOT change across the move (NFR-MAINT-3). — (verify: size, conformance, HIL) - OI-4 — PBLE/1 opcode/status freeze dependency. ✅ CLOSED 2026-07-01 (
[docs]). protocol.md §2/§3 froze at G0 and §4 (opcodes)/§8 (status) froze here — status-only, no wire byte changed. The opcode set + numbers and the 1-byte status set + numbers are now stable for v1.0, so FR-BLE-1/8/10 and FR-PROTO-1…10 no longer inherit provisional numbers; F-01/F-02 DoR is met. Each dependent story still MUST cite its frozen spec section per PRD §1B.4. Payload-level encodings for the identity/identify opcodes remain OI-6. — (verify: conformance) - OI-5 — Auto-run capability flag naming. ✅ CLOSED 2026-07-01 (
[docs]). The opt-inmain.pyauto-run cap isauto_run(u8, 0=off default / 1=on), set via the additiveSET_AUTORUN(0x23) opcode (protocol.md §7/§4), persisted at NVSpyble/autorun(owned bypble_boot), entry/main.py. FR-BOOT-3 DoR met (F-12). — (verify: conformance) - OI-6 — Label/identify wire encodings. ✅ FULLY CLOSED 2026-07-01 (
[docs]). Label half (S3): label max = 24 bytes UTF-8 (over-length →ERANGE), frozen in protocol.md §7. Identify remainder (S4):SET_IDENTIFY_LED=[gpio:u8][active_level:u8](empty clears;ERANGE/EBADREQ),IDENTIFY= optional[duration_ds:u8](1–50 ds, default 20, 5 Hz), and capshas_identify/identify_led(gpio byte or0xFF), frozen in protocol.md §4/§7. FR-IDENT-1..6 / FR-BLE-12 DoR met (F-22/F-23). — (verify: conformance)