Sub-issue of #755 (post-#750 follow-up meta).
Problem
SerialClientMessage::ClearBuffer and SerialClientMessage::GetInWaiting used to reach into the broadcast receiver rx from the same task that handled inbound WebSocket messages (pre-#750, single tokio::select! loop). PR #750 split that handler into three concurrent tasks (reader / writer / inbound) for throughput reasons — and as a side effect, the inbound task no longer has a handle to rx.
The post-#750 code makes both methods explicit logged no-ops:
Ok(SerialClientMessage::ClearBuffer) => {
tracing::debug!(... "clear_buffer requested (no-op post-#749 split; see comment)");
}
Ok(SerialClientMessage::GetInWaiting) => {
let _ = out_tx_inbound.send(SerialServerMessage::InWaiting { count: 0 });
}
Honest, but a regression for the FastLED #605 use case (pyserial-equivalent semantics over the WS bridge).
Fix design — ReaderControl channel
Add an internal mpsc from the inbound task to the reader task. Reader's tokio::select! learns one new branch.
// In handle_serial_ws():
let (control_tx, mut control_rx) = mpsc::unbounded_channel::<ReaderControl>();
enum ReaderControl {
Drain { reply: oneshot::Sender<usize> }, // for ClearBuffer
GetDepth { reply: oneshot::Sender<usize> }, // for GetInWaiting
}
// Reader task body:
loop {
tokio::select! {
result = rx.recv() => { /* existing Data/event forwarding */ }
Some(cmd) = control_rx.recv() => {
match cmd {
ReaderControl::Drain { reply } => {
let mut drained = 0usize;
while rx.try_recv().is_ok() { drained += 1; }
let _ = reply.send(drained);
}
ReaderControl::GetDepth { reply } => {
let _ = reply.send(rx.len());
}
}
}
}
}
// Inbound task — replace the current no-ops:
Ok(SerialClientMessage::ClearBuffer) => {
let (tx, rx_reply) = oneshot::channel();
let _ = control_tx.send(ReaderControl::Drain { reply: tx });
let drained = rx_reply.await.unwrap_or(0);
tracing::debug!(client_id = %client_id_owned, port = %port_owned, drained, "clear_buffer drained N lines via ReaderControl");
}
Ok(SerialClientMessage::GetInWaiting) => {
let (tx, rx_reply) = oneshot::channel();
let _ = control_tx.send(ReaderControl::GetDepth { reply: tx });
let count = rx_reply.await.unwrap_or(0);
let _ = out_tx_inbound.send(SerialServerMessage::InWaiting { count });
}
Roughly 40 LOC including the enum, the new select arm in reader, and the two inbound handlers. No public API change.
Test plan
- Unit: spawn the three-task harness with mock channels, exercise
Drain and GetDepth, assert reply values + that reader's main loop stays responsive (no head-of-line blocking on control).
- Integration: a python client that opens the serial-WS, pumps 200 lines from a fake serial source, then issues
ClearBuffer and asserts subsequent GetInWaiting returns 0.
Local editable install workflow (validation against FastLED)
fbuild lives at ~/dev/fbuild. FastLED consumes it via pyproject.toml and can be redirected to the local checkout for end-to-end validation BEFORE the upstream release ships.
Step 1 — make the change in fbuild
cd ~/dev/fbuild
git checkout -b fix/<n>-readercontrol main
# edit crates/fbuild-daemon/src/handlers/websockets.rs + add tests
FL_AGENT_ALLOW_BARE_CARGO=1 soldr cargo build -p fbuild-daemon
FL_AGENT_ALLOW_BARE_CARGO=1 soldr cargo test -p fbuild-daemon
Step 2 — point FastLED at the local checkout
In ~/dev/fastled/pyproject.toml, replace the fbuild==X.Y.Z pin with:
dependencies = [
"fbuild", # editable; resolved by [tool.uv.sources]
...
]
[tool.uv.sources]
fbuild = { path = "../fbuild", editable = true }
Then:
cd ~/dev/fastled
uv sync --reinstall-package fbuild
uv run fbuild --version # confirm it resolves to the local checkout
Step 3 — bench-validate on real hardware
# Teensy 4.0 on COM20, exercises serial-WS bridge under load
bash autoresearch teensy40 --object-fled --strip-sizes 5 --timeout 60
# Then exercise the new ClearBuffer/GetInWaiting paths directly:
uv run python ci/autoresearch/debug_find_pins.py COM20 # confirms findConnectedPins still round-trips
# (add a ClearBuffer/GetInWaiting smoke-test alongside debug_object_fled_capture.py)
Acceptance: 4/4 patterns flow through wrapper in seconds, and the new RPCs return non-trivial drain counts when fired mid-burst.
Step 4 — push the fbuild PR and admin-merge immediately
Once local validation is green:
cd ~/dev/fbuild
git push -u origin fix/<n>-readercontrol
gh pr create --title "feat(serial): ReaderControl restores ClearBuffer/GetInWaiting (closes #<n>)" --body-file <path>
gh pr merge <pr#> --squash --admin --delete-branch
(Same merge protocol used for #750 — admin merge bypasses the pre-existing master-level CI failures that aren't in our diff. Verify those failures still cite files outside crates/fbuild-daemon/src/handlers/websockets.rs before bypassing.)
Step 5 — bump FastLED to the new fbuild version
cd ~/dev/fastled
# Wait for release-auto.yml to publish fbuild vX.Y.Z+1
# (~5-10 min after merge; or check `gh release list` from ~/dev/fbuild)
# In pyproject.toml: revert [tool.uv.sources] and bump pin:
"fbuild==X.Y.Z+1",
uv sync --reinstall-package fbuild
Open a small follow-up PR on FastLED with just the pyproject.toml bump and reference this fbuild PR in the body.
Anti-goals
- Do not widen
ReaderControl to cover features that don't exist in SerialClientMessage today. The enum is exactly two variants because that's exactly what the protocol exposes. Future protocol additions get their own variants in their own PRs.
- Do not make the control mpsc bounded with a non-trivial capacity — the inbound task only sends one control message per client-side ClearBuffer/GetInWaiting, so even capacity=4 is overkill. Unbounded is fine and avoids deadlock corner cases.
Cross-links
Sub-issue of #755 (post-#750 follow-up meta).
Problem
SerialClientMessage::ClearBufferandSerialClientMessage::GetInWaitingused to reach into the broadcast receiverrxfrom the same task that handled inbound WebSocket messages (pre-#750, singletokio::select!loop). PR #750 split that handler into three concurrent tasks (reader / writer / inbound) for throughput reasons — and as a side effect, the inbound task no longer has a handle torx.The post-#750 code makes both methods explicit logged no-ops:
Honest, but a regression for the FastLED #605 use case (pyserial-equivalent semantics over the WS bridge).
Fix design —
ReaderControlchannelAdd an internal mpsc from the inbound task to the reader task. Reader's
tokio::select!learns one new branch.Roughly 40 LOC including the enum, the new select arm in reader, and the two inbound handlers. No public API change.
Test plan
DrainandGetDepth, assert reply values + that reader's main loop stays responsive (no head-of-line blocking on control).ClearBufferand asserts subsequentGetInWaitingreturns 0.Local editable install workflow (validation against FastLED)
fbuild lives at
~/dev/fbuild. FastLED consumes it viapyproject.tomland can be redirected to the local checkout for end-to-end validation BEFORE the upstream release ships.Step 1 — make the change in fbuild
Step 2 — point FastLED at the local checkout
In
~/dev/fastled/pyproject.toml, replace thefbuild==X.Y.Zpin with:Then:
Step 3 — bench-validate on real hardware
Acceptance: 4/4 patterns flow through wrapper in seconds, and the new RPCs return non-trivial drain counts when fired mid-burst.
Step 4 — push the fbuild PR and admin-merge immediately
Once local validation is green:
(Same merge protocol used for #750 — admin merge bypasses the pre-existing master-level CI failures that aren't in our diff. Verify those failures still cite files outside
crates/fbuild-daemon/src/handlers/websockets.rsbefore bypassing.)Step 5 — bump FastLED to the new fbuild version
Open a small follow-up PR on FastLED with just the pyproject.toml bump and reference this fbuild PR in the body.
Anti-goals
ReaderControlto cover features that don't exist inSerialClientMessagetoday. The enum is exactly two variants because that's exactly what the protocol exposes. Future protocol additions get their own variants in their own PRs.Cross-links