Repository navigation
Open
Labels
area: ciGitHub Actions, fbuild, toolchainsGitHub Actions, fbuild, toolchainsdeferredTriage pass could not resolve this; skipped by ci/oldest_issue.pyTriage pass could not resolve this; skipped by ci/oldest_issue.pyplatform: rp2040Raspberry Pi PicoRaspberry Pi Picostatus: blockedBlocked by external dependency or issueBlocked by external dependency or issue
Description
Context
Finish the RP2350 and RP2350W hardware bring-up on the currently available Pico 2 W fixture, using the existing AutoResearch/fbuild path and the board's soldered two-pin feedback bridge.
The 2026-08-18 inventory shows:
- historical RP2350W CDC identity: COM17,
2E8A:F00F, serial2DCB876B587EA334; Windows currently reports this saved record ashealth=phantom selectable=no, so current physical enumeration still must be established; - healthy Espressif peer: COM9,
303A:1001, serial8C:BF:EA:CF:87:B4; the USB PID is shared, so firmware-level chip detection must confirm ESP32-C6 before peer tests; - prior bench notes on feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832 and chore(rp2350w): hand off final #3832 hardware validation #3864 record the soldered bridge as GPIO0 (TX) -> GPIO1 (RX), but this run must confirm that mapping through live
findConnectedPins/GPIO evidence before relying on it.
Most source-side RP2350W foundation work has landed: board/deploy support, PIO0/1/2 channel selection, serial RPC, watchdog recovery, CYW43 networking, peer HTTP/JSON-RPC, OTA staging, and btstack BLE transport. What remains is a single evidence-backed closeout of every driver and subsystem reachable with the attached hardware.
Proposal
- Re-establish the RP2350W through
fbuild port scanand the normal RP BOOTSEL/CDC deployment path. Do not pin a stale COM17 record; omit--upload-portunless multiple RP boards are actually present, or useUF2=<volume>if the board is deliberately in BOOTSEL. - Run GPIO-only AutoResearch with pin discovery and record the physically observed bridge. The historical GPIO0 -> GPIO1 note is a hypothesis until the current firmware reports it.
- Validate both
rp2350andrp2350wprofiles on the Pico 2 W so common silicon support and W-specific transports are separately covered. - Exercise every RP driver reachable with the discovered bridge through the public FastLED/Channels API where available:
- PIO0, PIO1, and RP2350-only PIO2 clockless TX with PIO RX byte/edge evidence;
- PIO0 + PIO1 simultaneous resource/concurrency behavior;
- UART clockless output on any hardware UART route compatible with the discovered bridge (GPIO0 -> GPIO1 is UART0 TX/RX);
- AUTO and BIT_BANG fallback behavior through the user-facing API;
- arbitrary-pin PIO SPI if the harness supports capture on the bridge;
- fixed-function SPI0/SPI1 and APA102/SK9822 only when the present wiring can form the required MOSI -> MISO route; otherwise record the exact pin incompatibility as not reachable in this fixture, not as a pass.
- Complete the RP2350W-only gates already prepared by feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832: RPC smoke, IEEE-754, SIMD, watchdog/reconnect, RP dual-core concurrency, Wave2D, ESP32-C6 peer networking, BLE GATT, and peer OTA.
- Preserve exact command/output evidence on this issue and update parent feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832 with the final matrix.
Acceptance criteria
- A focused RED -> GREEN repro is recorded for every source/harness gap discovered while enabling RP2350 HIL (including any RP2040-only CLI restriction that must be generalized).
-
fbuild port scanrecords the live RP board identity/port and the healthy peer identity; firmware-level detection confirms the peer chip before ESP32-C6 use. - AutoResearch live pin discovery reports the soldered pair and
testGpioConnectionproves LOW/HIGH feedback; no pin mapping is accepted from historical notes alone. - The
rp2350andrp2350wbuild/deploy/serial RPC paths both pass on the attached Pico 2 W. - PIO0, PIO1, and PIO2 each produce decisive byte/edge loopback evidence through the public driver path.
- PIO0 + PIO1 simultaneous execution passes without resource collision, stale state machine, or DMA ownership leak.
- Every UART, SPI, AUTO, BIT_BANG, and PIO-SPI path physically reachable with the discovered bridge is tested; every unreachable path is listed with the exact wiring/peripheral constraint.
- RP2350W RPC smoke, IEEE-754, SIMD, watchdog/reconnect, concurrency, and Wave2D gates pass or produce a concrete tracked implementation gap.
- RP2350W <-> ESP32-C6 peer HTTP/JSON-RPC, >=4 KiB payloads in both directions, ten reconnect cycles, BLE GATT, and OTA recovery pass when the peer is firmware-confirmed as C6.
- Closeout records the exact commands, commits, wiring, ports, VID:PIDs/serials, and decisive
REMOTE:/RESULT:lines. - No raw flasher, raw pyserial path, one-off sketch, host-WiFi switch, guessed register map, or unsupported-peripheral shim is introduced.
- Relevant lint, focused tests, RP2350/RP2350W builds, code review, PR delivery, and post-merge live rerun are complete.
Decisions
- Treat the attached Pico 2 W as both the RP2350 common-silicon DUT and the RP2350W transport DUT; this covers both profiles without pretending a second board exists.
- Keep this issue focused on live driver/subsystem completion; feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832 remains the parent for the broader pin-free/network work.
- Treat prior GPIO0 -> GPIO1 documentation as a lead only; live discovery is the authority for the current physical bridge.
- Count a driver as complete only with live feedback evidence through the normal API. Compile-only or raw peripheral evidence is supporting evidence, not closeout.
- Mark physically unreachable fixed-pin routes explicitly rather than changing soldered wiring or inferring success.
Open questions
- Which present USB node/BOOTSEL volume belongs to the Pico 2 W after the user-reported reattachment? The saved COM17 record is not currently present, so this must be resolved from a fresh scan/deploy result.
- Whether the current two-pin bridge permits any fixed-function SPI route depends on the live-discovered pins; the historical GPIO0/GPIO1 pair supports UART0 and arbitrary-pin PIO tests but not the canonical SPI0/1 MOSI -> MISO pairs.
Related issues
- feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832 — parent RP2350W pin-free, network, and hardware bring-up
- chore(rp2350w): hand off final #3832 hardware validation #3864 — prior final-HIL handoff, including GPIO0 -> GPIO1 wiring history and RP BOOTSEL guidance
- feat(rp2350w): add btstack BLE GATT transport #3861 — RP2350W btstack BLE GATT transport and pending live proof
- meta(rp2040): RX-first channel-driver bring-up — PIO capture, UART/PIO/SPI TX, and SPI LEDs #3647 — RP2040 RX-first PIO/UART/SPI driver bring-up precedent
- meta(rp2040): transport, channel-driver implementation, and postmortem closeout #3648 — RP2040 transport/channel-driver closeout precedent
- test(rp2040): GPIO, PIO, and LED loopback bring-up after headers are soldered #3631 — RP2040 GPIO/PIO/LED loopback hardware precedent
- Support for Raspberry Pi Pico 2 - RP2350 #1673 — original Pico 2/RP2350 support
- meta: handoff — port diagnostics (#1279) and RP2350W HIL context fbuild#1282 — RP-series attachment/phantom-port handoff guidance
Metadata
Metadata
Assignees
Labels
area: ciGitHub Actions, fbuild, toolchainsGitHub Actions, fbuild, toolchainsdeferredTriage pass could not resolve this; skipped by ci/oldest_issue.pyTriage pass could not resolve this; skipped by ci/oldest_issue.pyplatform: rp2040Raspberry Pi PicoRaspberry Pi Picostatus: blockedBlocked by external dependency or issueBlocked by external dependency or issue
Type
Projects
- StatusShow more project fieldsTriage
Activity
test(rp2350): enable RP2xxx driver validation (#3901)
zackees commentedon Aug 19, 2026
Hardware status update (2026-08-18):
fbuild port scanstill reports the saved RP2350 application endpoint as phantom/non-selectable COM17; there is noRPI-RP2BOOTSEL volume.Consequently there is still no live evidence for the bridged GPIO pair, and no RP HIL result is being claimed from historical GPIO0/GPIO1 notes. Remaining acceptance is blocked until the Pico 2 W is reconnected as either application CDC or
RPI-RP2BOOTSEL; then pin discovery and UART/PIO/SPI/concurrency validation can resume.zackees commentedon Aug 19, 2026
Progress update after the attached-board investigation and merged tooling/driver work:
dd19c066f1.9e5fa8ab92.Remaining hardware blocker: the RP2350W stopped enumerating before the final UART0 diagnostic rerun. Released
fbuild 2.5.17reports stale/unselectable COM18, and Windows reports an unidentified USB device atPort_#0014.Hub_#0001with problem code 43 (descriptor request failure). Replug/BOOTSEL/reset attempts have not produced either a normal serial endpoint or PICOBOOT endpoint. No deploy/reset was attempted against the stale port.The issue should remain open until the board enumerates again (or is replaced) so UART0 and the remaining pin-free/peer-capable acceptance modes can be completed with physical evidence.
zackees commentedon Aug 21, 2026
Burn-down disposition: DEFER. The harness and several drivers are merged, but the required post-merge hardware matrix cannot be completed while the exact RP2350W no longer enumerates.
Completed evidence includes merged PRs #3901, #3907, #3908, and #3954 plus fbuild #1301/#1304: RP2350 validation/dispatch, indexed PIO/UART selection, fixed-SPI eligibility, typed diagnostics, individual PIO loopbacks, 50 no-replug deploy/RPC cycles, watchdog recovery, and one full RP2350W?ESP32-C6 network cycle.
Remaining acceptance:
2DCB876B587EA334; COM18 is currently phantom and Windows reports code 43 at the same direct root port.rp2350andrp2350wprofile build/deploy/RPC evidence.Restart after physical BOOTSEL/replug or the exact-device recovery tracked by FastLED/fbuild#1331 restores a selectable endpoint. Close only from retained post-merge HIL evidence, not compilation or stubs.
fix(rp): reject UART when the backend reports no usable clock (#3899)
test(rp): assert the exact UART refusal reason, not just that one exi…
fix(rp): derive UART baud ceiling from clk_peri, and say why UART dec…
test(autoresearch): arm the boot-loop bootloader escape (#3713) (#4172)
zackees commentedon Sep 7, 2026
Hardware bring-up complete — most of this issue's checklist is now evidence-backed
Ran the full reachable matrix on the attached fixture. Merged: #4169 (UART), #4172 (boot-loop escape + bring-up).
The fixture is a Pico 2 W — confirmed, not assumed
This issue recorded the W identity as historical and Windows-reported
health=phantom selectable=no, so "current physical enumeration still must be established". Established:2E8A:F00Fmatches the registry identity. The phantom/unselectable condition does not reproduce on Linux — the port is healthy and openable.Step 2: bridge confirmed by live discovery, not by the note
The issue required this be proven rather than inherited from #3832/#3864:
Driver matrix
recursiveMutexReady: truedriversreports PIO2 asRP2350 PIO2, enabled and ready, so the RP2350-only third PIO block is real and exercised.UART is not reachable — and not for the reason this issue assumed
The plan expected UART to work because "GPIO0 -> GPIO1 is UART0 TX/RX". The bridge is fine; the clocking is the blocker:
3,000,000 is exactly
clk_peri / 16, the PL011 hardware maximum — soclk_periis 48 MHz on arduino-pico RP2350, and WS2812 wave10 encoding needs more than 3 Mbaud. No software change can make UART clockless work on this board.#4169 fixed the surrounding defect: the engine hardcoded a 6.25 MHz ceiling that silently assumed ~100 MHz clocking, so the encoder picked a baud
uart_init()clamped and the achieved-baud check rejected it. The ceiling is now derived fromclk_peri/16via the peripheral, and the refusal says why instead of failing opaquely mid-start.Recommend recording UART as "not reachable on this fixture", the same category this issue already reserves for SPI.
SPI remains unreachable
SPI loopback needs GPIO3→GPIO0 (SPI0) or GPIO11→GPIO8 (SPI1). The fixture is wired GPIO0→GPIO1. Recording as the issue instructs, rather than as a failure.
Deployment
fbuild deployvia BOOTSEL/UF2 works and returns the application port. One note for the cross-variant case: deployingrp2350wwhile the board runs non-W firmware fails clean, without flashing —Correct behavior; the running identity did not match the target environment.
--upload-portresolves it, which is what this issue prescribes.Remaining
wifiConnecton the 7th cycle and watchdog-resets).Generated with Claude Code
https://claude.ai/code/session_01KkufoNxfnNRU9psT3R9F51
63 remaining items
zackees commentedon Sep 11, 2026
Resolved: the RX decoder does not handle 400 kHz — it is not the UART's timing deviation
On the UART0 result I listed two candidate causes for
bytes=0/3300and declined to guess between them:They separate cleanly with a control. PIO0 is a known-good transmitter — it passes this issue's loopback criterion at 800 kHz. Running it at the same 400 kHz geometry isolates the decoder from the transmitter:
Against
TIMING_WS2811_400KHZ(T0H 500, T1H 1200, period 2500):0high1highA textbook-perfect waveform decodes to zero bytes. So cause 2 is ruled out: the decoder fails on a signal with no timing deviation at all.
The controlled comparison
Same board, same bridge, same driver — only the geometry differs between rows 1 and 2. The RX capture/decode path handles 800 kHz and not 400 kHz.
What this means for the two entries
UART0: its decode failure is explained by the decoder, not by its own timing. That makes the UART finding cleaner than I recorded it — the backend reaches the only geometry it can, transmits, and the harness cannot read it back. The 200 ns T1H shortfall remains a genuine separate finding (outside the ±150 ns datasheet tolerance) but is not what fails the test.
A new harness gap:
--chipset ws2811-400cannot pass on any driver, because nothing the harness captures at 400 kHz decodes. That is worth knowing before someone spends time on a driver that is transmitting correctly.This is the "focused RED -> GREEN repro for every source/harness gap discovered" criterion — the repro is one command, and the control that makes it diagnostic is the 800 kHz run of the same driver.
I am not proposing a fix: whether the decoder derives thresholds from the requested timing or assumes 800 kHz is a question for whoever owns the capture path, and I have guessed at enough mechanisms this session.
zackees commentedon Sep 11, 2026
Refinement: the decode thresholds do adapt to 400 kHz and the signal fits them — so that is not the cause
My previous comment concluded "the RX capture/decode path handles 800 kHz and not 400 kHz". The empirical result stands, but it invites an assumption I have now checked and refuted: that the decoder's thresholds are hardwired for 800 kHz. They are not.
AutoResearchTest.cpp:840builds them from the requested chipset:tx_timing = fl::ChipsetTiming{timing.t1_ns, timing.t2_ns, timing.t3_ns, ...}; const uint32_t tolerance = (uses_wave8 || uses_wave3) ? 200 : 170; auto rx_timing = fl::make4PhaseTiming(tx_timing, tolerance);and
make4PhaseTiming(fl/channels/rx.cpp.hpp:72) derives four windows asvalue ± tolerance. ForTIMING_WS2811_400KHZ(T1=500, T2=700, T3=1300) at ±170 ns:Every phase of the measured signal falls inside its window. So the thresholds adapt correctly, the signal conforms to them, and the decode still returns zero bytes. Whatever fails is downstream of threshold selection.
What I could not get, and why it matters
The device logs the actual reason —
AutoResearchTest.cpp:866:That line never reaches the host, because
AutoResearch.ino'ssetup()callsfl::setLogLevel(FL_LOG_LEVEL_NONE). The one diagnostic that would name the cause is suppressed by default, and the host sees onlyDecode status: ok=-1 error=-1, which are unset sentinels rather than a reported error.That is worth noting on its own: the decode failure path has a diagnostic and the harness cannot see it. Same shape as the OTA fetch error that was being written to a closed serial link (#4318) and the UART decline that was being reported as a capture failure (#4310) — the information exists and does not reach whoever needs it.
Corrected status
I am flagging rather than fixing, and being explicit that my earlier phrasing pointed at a cause I have since ruled out.
zackees commentedon Sep 11, 2026
Retracting "the decoder does not handle 400 kHz" — it does, up to ~60 LEDs. And the advertised lane cap is not reachable.
I said the RX path handles 800 kHz and not 400 kHz. That is wrong. Driving
runSingleTestdirectly instead of through the harness default of 100 LEDs:400 kHz decodes correctly. My conclusion came from testing one LED count — the harness default — which happens to sit above the 400 kHz ceiling. Every earlier 400 kHz "decode gap" observation, UART0's included, is this.
The real shape: a size ceiling on both geometries
All reproduced 3/3:
A concrete defect: the advertised maximum does not work
The firmware rejects above 103 with an explicit error:
But 103 itself fails, silently, 3/3 —
passed=false,bytes=0, no error. So the cap the device advertises is one it cannot honour; the usable maximum at 800 kHz is somewhere in (100, 103). A caller who trustsmax 103gets a silent capture failure rather thanLaneSizeTooLarge, which is the worse of the two outcomes.What I could not determine
The invariant. It is not frame duration — 3.09 ms fails while 3.60 ms passes. It is not bit count — 2400 bits pass while 1560 fail. It is not LED count, which differs by geometry. Something geometry-dependent in the PIO_RX capture path sets it, and black-box probing cannot separate the candidates. The device does log the decode error code (
AutoResearchTest.cpp:866) butsetup()setsFL_LOG_LEVEL_NONE, so it never reaches the host;setDebugexists as an RPC and the harness never calls it.Corrected entries
--chipset ws2811-400— usable, at ≤60 LEDs. My "cannot pass on any driver" was wrong.max 103is advertised and 103 fails silently.This is the second conclusion I have had to withdraw here after testing a single point and generalising — the first being three "blocked" labels that dissolved on contact. One LED count is not a capability.
zackees commentedon Sep 11, 2026
Root of the silent-ceiling defect: a fixed
/ 32divisor expressing a geometry-dependent limitAutoResearchRemoteRunSingleTest.cpp:512:rx_bufferisfl::span<uint8_t>of 3300 bytes (matching thebytes=0/3300in the decode diagnostics), so the advertised cap is3300 / 32 = 103.That divisor cannot be right for more than one geometry. Decoded LED data is 3 bytes per LED, so 32 bytes per LED means the buffer holds raw capture samples — and the samples a frame costs scale with how long the frame lasts. A 400 kHz frame is twice as long as an 800 kHz frame of the same LED count. One constant cannot describe both, which is exactly what the measurements show (all 3/3 reproducible):
passed=false,bytes=0, no errorTwo distinct problems fall out of the one line:
max 103gets silence instead ofLaneSizeTooLarge, which is strictly the worse failure — the guard exists precisely to convert that into a clean error.Direction
runSingleTestalready receives the timing ("timing": "WS2811-400KHZ") and resolves it tot1_ns/t2_ns/t3_nsa few lines later formake4PhaseTiming. So the information needed to compute a per-geometry cap is present at the same call site — the bound could scale with the bit period rather than being a constant.I am not proposing a number. Deriving the true bytes-per-LED needs the PIO_RX sample rate and whether it is fixed or tracks the bit period, which I could not determine black-box: no single invariant fits both thresholds (3.09 ms fails while 3.60 ms passes; 2400 bits pass while 1560 fail). Whoever owns that path can size it correctly; the measurements above pin what the answer has to reproduce.
Worth noting the guard is doing its job in shape — a bounds check that returns a clear error — it is only the bound that is wrong. Raising or lowering a constant will not fix it; the bound has to depend on the geometry the caller asked for.
zackees commentedon Sep 11, 2026
Split the lane-cap defect out as #4371 so it is tracked on its own rather than living in this thread — it is a harness bug independent of the RP2350 HIL matrix, and the silent-failure band it creates is what sent me down a false trail about the 400 kHz decoder.
Net effect on this issue's matrix, after all the corrections above:
show_success, the right contract for a one-RX-pin fixture)attempted=True started=True actualBaud=2000000); its decode failure was the 100-LED default sitting above the 400 kHz ceiling, not a decoder or timing fault. Separate genuine finding: T1H runs 1000 ns against a 1200 ns datasheet value, outside ±150 ns toleranceGPIO3 -> GPIO0, one wire, and GPIO0 is already the correct MISOGPIO11 -> GPIO8, neither pin on the bridgeFour things I asserted in this thread and later had to withdraw, all listed above in corrected form: BLE "fully blocked" (only the host-central half was), SPI "needs jumpers" (true, but one of them is a single wire to an already-correct pin), UART0 "backend declines" (only for the chipset I happened to run), and "the decoder does not handle 400 kHz" (it does, below the ceiling). Each came from testing one point and generalising to a capability.
zackees commentedon Sep 11, 2026
UART0 resolved: it is the T1H shortfall, by 30 ns. My previous correction over-corrected.
When I found the lane-cap ceiling I attributed UART0's decode failure to it and said the 200 ns T1H shortfall "is not what fails the test". That was wrong — testing UART0 below the ceiling separates them cleanly.
runSingleTeston UART0 at 400 kHz, well under the ~60-LED ceiling:It fails at every size. PIO0 passes at all three. So the ceiling was never UART0's problem.
The failure signature
At 10 LEDs:
{"capturedBytes": 29, "totalBytes": 30, "decodeBytes": 0, "decodeOk": -1, "captureFailed": false, "errors": [{"led": 0, "actual": [255,255,255], "expected": [240,15,170]}, {"led": 1, "actual": [255,255,255], "expected": [240,15,170]}, ...], "rawEdgeSample": "H950 L1500 H1000 L1500 H1000 L1500 H1000 L1500 ..."}Every byte reads
0xFF— every bit classified as a one — not partial or random corruption.The arithmetic
make4PhaseTimingderives the1-bit high window asT1+T2 ± tolerance. ForWS2811_400KHZ(T1=500, T2=700) at the non-wave tolerance of 170 ns:UART0's
1symbols land 30 ns outside the bottom of the window. Its0symbols (H500) sit comfortably insidet0h[330, 670], which is why the capture runs to completion (captureFailed: false, 29 of 30 bytes) rather than aborting — the frame is captured, and the bit classification is what goes wrong.PIO0 at the identical configuration measures t1h = 1200 ns, dead centre, and passes.
Corrected entry
attempted=True started=True actualBaud=2000000), but its T1H is 1000 ns against a 1200 ns datasheet value — 30 ns below the decoder's accepted window and 200 ns outside the ±150 ns datasheet tolerance. Fails at every lane size, independent of the #4371 ceiling.This is the second time I have moved UART0's cause. First I blamed the backend declining (true only for the 800 kHz chipset), then the lane-cap ceiling (real, but not what fails UART0). Both times I had a plausible explanation and stopped before separating it from the alternative. The separation here was one command — run it below the ceiling — and it should have come first.
The underlying question for whoever owns the RP UART clockless encoder: whether a 2 Mbaud PL011 can express a 1200 ns T1H at all, or whether 1000 ns is the closest reachable quantisation. If it is the latter, the geometry is simply out of reach and the path is unreachable on this silicon rather than defective.
zackees commentedon Sep 11, 2026
Tested a fix for UART0's T1H and it does not work — plus a silent decline path
Following the 30 ns shortfall: the RP UART encoder (
fl/channels/uart_wave_encoder.cpp.hpp) evaluates two frame geometries and takes the one with smaller quantisation error.with
fit.total_err_ns = err_t0h + err_t1h— a sum. ForWS2811_400KHZ(period 2500 ns, T0H 500, T1H 1200):P=4 needs
175 + 50 < 200, which is false, so P=5 is chosen — and its T1H lands outside the decoder window [1030, 1370] while P=4's 1250 would sit inside it. The sum metric cannot see that one phase being out of tolerance is fatal while error spread across both is not.That looked like a clean fix: compare worst-phase error instead of the sum.
It does not work. Changing
total_err_nstomax(err_t0h, err_t1h)— consistently, so admission and construction cannot drift — selects P=4, and the RP UART then never starts:So P=5 with an out-of-window T1H is at least functional — it transmits and captures 29 of 30 bytes. P=4 does not transmit at all on this backend. Whatever rejects it is upstream of the start attempt, and the reason is not recorded.
(My first attempt at this experiment was invalid and I want to flag it: I forced the branch with
if (true), which made construction disagree with admission — the code comments warn about exactly that drift. It produced the same symptom for the wrong reason. The result above is from changing the metric itself, which keeps both paths consistent.)Incidental finding: a decline that reports nothing
rpUartStartAttempted: FalsewithrpUartLastError: ''. The documented decline path sets a message — that is how the 800 kHz case reports "chipset timing needs a baud above the UART backend maximum". This one sets nothing, so a P=4 rejection is indistinguishable from a driver that was never invoked.That is the fourth instance of this shape on this issue: the OTA fetch error written to a closed serial link (#4318), the UART decline classified as a capture failure (#4310), the decode error suppressed by
FL_LOG_LEVEL_NONE, and now a start rejection with an empty reason.Where UART0 stands
Unchanged as a result, but better understood: it transmits, its
1symbols land 30 ns below the decoder window because 1200 ns is not expressible on the P=5 grid (multiples of 500 ns), and the alternative grid that could express it is rejected by this backend for an unreported reason. Whether that rejection is a real hardware constraint — 6 data bits at 1.6 MBaud on PL011 — or a fixable gap is the open question.zackees commentedon Sep 11, 2026
Minor: multi-lane
runSingleTestreports PASS with evidence covering one laneLower-confidence than the other findings here, and on a non-default path, but it bears on evidence quality so it is worth recording.
--lanes Nmakesphases.py:1088buildlaneSizes = [strip_size] * lane_countforrunSingleTest. Driving that directly:[50, 50][60, 60][100, 100]captureEvidenceBytesis 12 bytes per LED of one lane in every case — 600 for a 50-LED lane, 1200 for a 100-LED lane — while the response reportslaneCount: 2, passed: true.That is expected physically: this fixture has a single RX pin (GPIO1), so only the bridged lane can be captured. The gap is that nothing in the response distinguishes "verified both lanes" from "verified one and drove the other".
captureEvidenceBytesis the only tell, and reading it requires knowing the expected byte count and computing it yourself.runParallelTesthandles the same constraint deliberately —phases.py:4376excludes capture evidence for concurrent runs and validates distinct TX pins plusshow_successinstead. The single-test multi-lane path appears not to have an equivalent contract.Also worth noting:
[100, 100]= 200 LEDs passes, while a single lane of 101 fails. So the per-lane cap in #4371 is applied per lane whilerx_bufferis shared — consistent with only one lane being captured, but it means the guard's arithmetic and the capture's actual behaviour are describing different things.I am not proposing a change. On a single-RX fixture there is no more evidence to be had; the question is whether a PASS should say so, and that is a call for whoever owns the criterion.
zackees commentedon Sep 11, 2026
No multi-frame degradation on the RP PIO path — including at the capacity boundary
A negative result, but on a dimension the harness has a dedicated flag for (
--frames, "Exposes second-frame degradation bugs"), so it is worth recording as closed rather than untested.Repeated frames per request, via
runSingleTest'siterations:Two things this establishes:
No second-frame degradation. Sixteen consecutive frames decode cleanly, and byte counts scale exactly linearly with iteration count — no drop-off, no partial frames.
The capture buffer is correctly reused between frames.
rx_bufferis 3300 bytes, yet 100 LEDs × 8 iterations reports 9600 bytes of evidence. So the accumulation is in the reported total, not in buffer occupancy — the per-frame capacity limit from #4371 applies per frame and does not tighten with repetition. Running at the exact capacity boundary for eight consecutive frames is the strongest form of that check available here, and it holds for both geometries.So the PIO0 entry in the matrix above is robust across repetition, not just a single-frame pass.
zackees commentedon Sep 11, 2026
Minor:
runSingleTest'spatternparameter is inert and unvalidatedSmall, and the practical impact cuts the safe way, but it is a diagnosability wart in the tool this issue's evidence comes from.
runSingleTestacceptspattern, echoes it back in the response, and does nothing with it. It is extracted atAutoResearchRemoteRunSingleTest.cpp:545-547, echoed at:1112, and never mapped to a pattern id. The test always runs the full internal sweep —AutoResearchTest.cpp:1563:Verified by passing values that cannot mean anything:
Nonsense and empty are both accepted without complaint and reflected back as though honoured.
Coverage is better than the parameter implies, not worse — all four patterns run every time, including Pattern B (
0x55, 0xFF, 0x00), which exercises the all-high and all-low byte boundaries. I went looking because the harness only ever sendsMSB_LSB_Aand I assumed patterns B and C were dead code; they are not, and I would have filed a bogus coverage gap if I had not checked.The risk is interpretive: someone narrowing down a pattern-specific failure will pass
MSB_LSB_B, see it echoed, and believe they isolated one pattern when all four ran — so a failure that only Pattern C triggers looks like it reproduces under B as well. A typo'd name silently behaves identically.Either validate the value and select on it, or drop it from the accepted schema and document that the sweep is fixed. Echoing an ignored parameter back is the part that misleads.
zackees commentedon Sep 12, 2026
Full driver matrix, run today on
master(731fb6f8ad) against the attached Pico 2 W. Every reachable path exercised, every unreachable one recorded with the exact wiring constraint it needs.Fixture —
/dev/ttyACM0,2E8A:F00F, serial2DCB876B587EA334. The historical COM17 record is gone; this is the live enumeration fromfbuild port scan. Peer:/dev/ttyACM2,303A:1001,8C:BF:EA:CF:87:B4, firmware-confirmedESP32-C6FH4 (QFN32) rev v0.1.The bridge, from live evidence only
findConnectedPinson the device, not from the historical note:{"found": true, "txPin": 0, "rxPin": 1, "autoApplied": true, "searchRange": [0, 8]}testGpioConnection, both directions plus a negative control:The GPIO20 row is what makes the other two mean something: an unconnected pin reads stuck HIGH, so LOW-follows-TX is real feedback and not a floating input that happened to agree.
Both profiles on the one board
status.platformrp2350wRaspberry Pi Pico 2 W (RP2350)RESULT: RPC smoke PASSrp2350(non-W)RP2350RESULT: RPC smoke PASSSame silicon, two board configurations, both deploy → boot → serial RPC clean.
Driver matrix
--flex-io --rp-pio-index 0TEST PIO0 lanes=1 leds=100 PASS 684ms, 4/4 patterns--flex-io --rp-pio-index 1TEST PIO1 lanes=1 leds=100 PASS 684ms, 4/4--flex-io --rp-pio-index 2TEST PIO2 lanes=1 leds=100 PASS 684ms, 4/4--flex-io --rp-pio-both --parallel--uart --rp-uart-index 0driver: "UART1"--rp-spi-loopback --rp-spi-index 0--rp-spi-loopback --rp-spi-index 1--rp-spi-public-apidriver: "AUTO"driver: "BIT_BANG"PIO0 + PIO1 parallel — a resource test, and it says so
{"passed": true, "show_success": true, "show_duration_us": 7872, "drivers": [{"driver":"PIO0","pinTx":0,"laneSizes":[100]}, {"driver":"PIO1","pinTx":1,"laneSizes":[100]}], "concurrent_resource_test": true, "captureEvidenceExpected": false, "rx_validation_attempted": false}Worth being explicit, because
captureEvidenceBytes: 0looks like a zero capture until you read the two lines above it: this configuration declares it is not a loopback test. Only one TX can reach the single RX pin, so re-entering capture here would test a different topology; both transmitters were already byte-validated individually in the rows above. The resource evidence is what matters, andtestRpPioParallelResourcessupplies it:{"success": true, "iterations": 8, "numLeds": 256, "baseline": {"freeDma": 15, "freeSmsPerBlock": [3,4,4]}, "busiestInFlight": {"freeDma": 14, "freeSmsPerBlock": [3,3,4]}, "released": {"freeDma": 15, "freeSmsPerBlock": [3,4,4]}, "collisionFree": true, "noOverClaim": true, "noLeak": true, "observedInFlight": true, "noResidueBetweenFrames": true}DMA and state-machine counts return exactly to baseline after eight frames, and
busiestInFlightshows a block genuinely occupied mid-frame rather than the sampler missing the window. (bothHeldAtOnce: falsein the same payload: across the sampled instants only one of the two ever held an SM — the shows are serialised by the manager, which is itself the answer to "no resource collision".)UART — one defect, one wiring constraint
UART0 transmits — the engine starts and clocks out a frame — but every symbol miscompares. That is #4379: the geometry selector picks P=5, putting T1H at 1000 ns against a 1200 ns spec. Fixed in #4381, where the same board runs 4/4 at 1, 2, 5, 10, 20, 40, 60 and 63 LEDs, 3/3 each, at baud 1600000 — the first byte-exact RP UART clockless loopback.
UART1 is a wiring constraint, not a defect. Valid TX pins are fixed per instance (
channel_engine_rp_uart.cpp.hpp):The bridge drives from GPIO0, which belongs to UART0 only. UART1 cannot reach GPIO0 on any mux setting, so it is unreachable on this fixture without re-soldering. Note the decline above carries an empty
erron master — that is #4375, fixed in #4376, which makes it say"RP UART: TX pin is not usable by this UART instance"instead of nothing.SPI — all three routes need pins this fixture does not have
GPIO0 is half of SPI0's route, but the other half is GPIO3 and the bridge's far end is GPIO1. No mux setting makes a two-pin 0↔1 bridge into a MOSI→MISO pair.
The public-API run is worth quoting, because the failure is on the return path only:
{"chipset": "APA102", "frameBytes": 1068, "actualClockHz": 8000000, "wireIdle": true, "success": false, "message": "RP SPI public API loopback failed."}1068 bytes is exactly right for 256 APA102 LEDs (4-byte start + 4 × 256 + 40-byte end), clocked at 8 MHz with the wire correctly idle. The transmitter is provably correct; only the MISO return the fixture cannot form is missing. That is a wiring limit, not an untested driver.
AUTO and BIT_BANG
The registered set on RP2350 is exactly PIO0, PIO1, PIO2, SPI0, SPI1, UART0, UART1 (from
drivers). Neither fallback is registered on this platform, so there is no public path to exercise — recorded as a platform fact rather than left as a blank row.Subsystem gates
RESULT: RPC smoke PASS (discovery, help, ping, payload, status, drivers, testNoSerial, RP concurrency, RP PIO contention, RP PIO0+PIO1 resources, error handling)85/85 passed, plustestSimdBenchmark(simd_us571 / 898 / 568)200/200,pico-sdk-mutex+arduino-core1, core1 loop counter 8077 → 8079noTheft,noLeak, 11 SMs free before and afterWhere that leaves the driver-coverage criterion
Every driver registered on RP2350 is now either byte-validated through the public path or listed with the specific pins it would need:
Nothing is left as "untested" without a reason a reader can check against the wiring.
zackees commentedon Sep 12, 2026
Subsystem gates, completing the three rows I left unclaimed in the driver matrix above. Same fixture, same
master(731fb6f8ad), Pico 2 W on/dev/ttyACM0.IEEE-754
35/35, no first-failure bit pattern to report.
Wave2D — and the listed gap is gone
bash autoresearch rp2350w --perf-wave2d 32x32 --upload-port /dev/ttyACM0That line is the result. #3832's current-gaps list carries "
--perf-wave2dincorrectly enters the GPIO-dependent path" — it does not any more. The mode now takes the pin-free path the way--simddoes, so the gate no longer depends on a bridge it never needed. Closed with live evidence rather than by inspection.The run itself is clean, sanity probes included:
Worth reading the two
loads_onlyrows against theircomputepartners, since that pairing is what makes the number trustworthy: FivePoint's compute cost is indistinguishable from its load cost (0.3750 vs 0.3760 µs/cell — memory-bound), while NinePointIsotropic's is 6.5× its loads (2.4570 vs 0.3760 — genuinely compute-bound). A solver that measured as pure load traffic in both stencils would mean the work was optimised away; it wasn't.perfProbeRepeatat 0.23% anduntrusted: falsesay the harness itself agrees the measurement is stable enough to keep.Timing drift
2954 shows with a 91 µs spread between fastest and slowest, and every iteration after the first landing on 2495 ms. The first at 1270 ms is the partial warm-up window, not drift.
Watchdog — deliberately not induced
The remaining gate is "watchdog acknowledgement, reset, CDC reacquisition, uptime reset, and post-reset RPC". The recovery half is evidenced continuously by this session's runs: every deploy resets the board and the next
pinganswers immediately with a fresh uptime andlastResetCause: "SOFTWARE", e.g.{"uptimeMs": 6476, "lastResetCause": "SOFTWARE", "lastResetWasWatchdog": false}taken right after a flash — CDC reacquired, uptime reset, RPC live.What I did not do is trip the watchdog on purpose. This bench is unattended for long stretches, and a deliberate hang is the one action here whose recovery I would not be present to confirm. The mechanism is already recorded live on #4173 (
lastResetCause: "WATCHDOG",lastResetWasWatchdog: true, board self-recovered toIDLE), so the evidence exists — it just is not mine to re-collect safely right now. Flagging it as the one gate wanting a supervised session rather than quietly ticking it.Gate status after this pass
untrusted: falsenoTheft,noLeak, baseline restoredfix(autoresearch): report a declined driver as declined, not as zero …
zackees commentedon Sep 12, 2026
Four of this issue's driver/harness fixes are now on
master, so the matrix above can be re-read against shipped code rather than branches.1987a5782afalsewith inherited numberse0b793f221pinRxno longer captures nothing while reporting success (#4374)8b965814ebdma_buffer_busy129e4cd7d4Plus #4372 (
a5fb9e670e, peer server stall budget) and #4343 (42a4d1f9e1, serial RPC dead-wait), which shorten the peer-network and RPC paths this issue's gates run on.Re-verified on the merged
masteragainst the attached Pico 2 W (2E8A:F00F,2DCB876B587EA334), so this is the combination rather than each fix in isolation:Every passing UCS7604 row captures exactly
15 + 6*ledsbytes per pattern — the encoded frame length to the byte. Host suite on the merged tree: 305/305 unit, 84/84 examples.Still in flight for this issue, all with stale
CHANGES_REQUESTEDreviews that predate their fixes and re-review requested:await()(fl::platforms::await() polls unbounded — an unresolvable promise hangs the caller forever (wedges RP boards until the watchdog fires) #4369).zackees commentedon Oct 2, 2026
Live evidence, 2026-10-02, Pico 2 (RP2350,
2E8A:000F, ser=2DCB876B587EA334), masterae0cf517c0plus #4667 (--allprunes unregistered drivers). Command:bash autoresearch rp2350 --all --upload-port /dev/ttyACM0 --timeout 240 --skip-lint.Discovered bridge: GPIO0 -> GPIO1.
canRepresentTimingForMaxBaudfinds no 4- or 5-pulse fit underRpUartPeripheral::maxBaudRate(). That value ismin(clk_peri/16, kRpUartBaudCeiling = 6.25 MHz). This is the remaining UART gap for the acceptance item "UART clockless output on any hardware UART route compatible with the discovered bridge".