Skip to content

refactor(fw-esp32): one home for the serial io_task JSON half - #245

Merged
Yona-Appletree merged 3 commits into
mainfrom
claude/vigilant-heisenberg-d30702
Aug 1, 2026
Merged

refactor(fw-esp32): one home for the serial io_task JSON half#245
Yona-Appletree merged 3 commits into
mainfrom
claude/vigilant-heisenberg-d30702

Conversation

@Yona-Appletree

Copy link
Copy Markdown
Member

The serial io_task JSON-serialization half — StackJsonWriter,
timed_write_server_msg, server_message_detail and the chunked+timeout
writer underneath them — existed in three hand-synced copies. The S3 copy
carried a comment explaining which single line diverged from the C6's, so the
two would stay diffable; fw-esp32v3 (PR #239) documents the same duplication
under a "Known duplication" header. The code is chip-agnostic. This gives it
one home in fw-esp32-common and migrates the two crates that exist on main.

What moved

Two new modules under fw-esp32-common/src/serial/:

  • chunked_write.rsnot gated on server, because the outgoing-log
    path uses it with the feature off. WritePolicy (timeout, chunk size, link
    name) with a USB_SERIAL_JTAG const carrying the old 250 ms / 256 B values,
    and ChunkedWriter with write_all / write_all_with — the same loop,
    byte for byte.
  • server_msg.rs#[cfg(feature = "server")]. StackJsonWriter +
    StackJsonError, write_server_msg / write_full_server_msg as an inherent
    impl on ChunkedWriter, server_message_detail and its two helpers.

PROBE_TIMEOUT / PROBE_INTERVAL stayed behind — they belong to the io loop,
which did not move. UsbConnectionMonitor also stays: the write path only ever
saw it as a connected: bool.

The one seam

timed_write_all_with ticked recovery::watchdog::note_io_alive() per chunk,
so a slow host cannot starve the watchdog feeder into resetting the device.
That tick did not move down: the RWDT is an esp-hal peripheral, so it is
a chip fact fw-esp32-common is forbidden to hold, and fw-esp32v3 has no
watchdog module at all — moving it would have handed the third consumer an
unusable dependency. It is now a hook the writer calls in exactly the old
position, bound by a small link_writer() in each crate.

The hook is FnMut, not fn(), because v3's per-chunk work is a UART RX-FIFO
drain that needs captured state. It is returned as an opaque impl FnMut()
rather than a named fn() pointer for a measured reason, noted in the code:
the pointer form cost 256 B of un-inlinable indirect calls.

Size

just fw-esp32c6-size-check: +272 B (+0.01%) — image 2,862,432 → 2,862,704 B,
headroom 283,296 → 283,024 B against a 65,536 B margin. The residue is
WritePolicy's fields being struct loads instead of literal constants at two
call sites; one measurement round halved it from 528 B and I stopped there.

Verification

Command Result
just check pass (incl. the test_espnow --no-default-features config, which builds fw-esp32-common with server off)
just fw-esp32c6-size-check pass, numbers above
just build-fw-esp32s3 pass
just clippy-fw-esp32c6 + -harnesses pass
just clippy-fw-esp32s3 pass (all 6 configs)

The two io_task.rs files now differ in only the board import, one word of
prose, and the C6's extra harness cfgs.

Follow-up: fw-esp32v3

That crate lives on #239 and is not on main yet, so its migration is a
separate PR. The API was designed against its copy so the move is nearly
mechanical, but three things there are not pure delete-and-import:

  1. UartLink::write_chunked becomes a ChunkedWriter, which needs poll_rx
    turned into a free function so the closure can capture rx/read_buffer
    disjointly from &mut tx.
  2. The RX drain shifts from after each chunk to before it — same
    frequency, one chunk of phase. Believed behaviour-neutral since the io loop
    drains at the top of the next iteration, but it is a real change.
  3. v3's two log::warn!s inside write_chunked lose their after {offset} of {n} B detail, since the shared writer returns a bare bool by design.

Risk

Compile- and size-verified only; neither board was flashed. The write path is
byte-identical by construction, but a smoke run on a C6 and an S3 would be
worth having before merge.

🤖 Generated with Claude Code

Yona-Appletree and others added 3 commits July 31, 2026 22:36
`serial::io_task`'s server-message half — the stack JSON writer, the framed
write, and the overflow-warning detail strings — was copied verbatim between
fw-esp32c6 and fw-esp32s3, and a third copy is in flight for the classic
ESP32. None of it is a chip fact.

The one thing that is: each firmware does something small between write
chunks. The C6 and S3 tick the RTC watchdog's liveness flag so a slow host
cannot starve the feeder; the UART-based v3 drains its 128-byte RX FIFO,
which would otherwise overflow during a multi-second write. `ChunkedWriter`
takes that as an `FnMut()` hook called before every chunk — a `FnMut` rather
than a `fn()` precisely so the second kind, which needs the RX half and the
line buffer, fits without a second writer. `WritePolicy` carries the other
two divergences (chunk size, and what to call the link in error text) so the
v3 migration is an import swap rather than a fork.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deletes ~200 lines that now live in `fw_esp32_common::serial`. What is left
in `io_task` is the part that is actually about this chip: the
USB-Serial-JTAG peripheral, the connection monitor, the not-draining probe,
and the channels.

`link_writer` is the seam. It binds the shared writer to
`recovery::watchdog::note_io_alive`, which is still called once per chunk,
before the chunk, exactly as before — the RWDT is an esp-hal peripheral and
fw-esp32-common may not hold chip facts, so the tick goes down as a hook.

`ser-write-json` leaves the dependency list with the code that used it.
Feature resolution is unchanged: `lpc-wire` already pulls the same version
with the same `alloc` feature, so this is a declaration cleanup and not an
image change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The same ~200 lines the C6 just dropped, and with them the reason the header
carried a "sole divergence from fw-esp32c6's copy" note: `format!` was only
ever imported for the server-gated write paths, and those paths now live one
crate down. The two `io_task.rs` files still differ, but only in the three
places where the boards actually differ — the board module, and the two
`fw_harness` gates.

`ser-write-json` leaves the dependency list with the code that used it;
`lpc-wire` already pulls the same version with the same `alloc` feature, so
feature resolution and the image are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Yona-Appletree
Yona-Appletree merged commit e85da31 into main Aug 1, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant