one microcontroller one wire no control square
picoleaf
Drives Nanoleaf Canvas light squares from a Raspberry Pi Pico W, with no control square attached.
A Nanoleaf Canvas wall talks to its control square over one wire, and nobody has published what they say to each other — no capture, no baud rate, no framing, no command set. There was no control square on the bench either, so there was nothing to record. The squares had to be asked directly.
The base scope is met and the instrument is still a prototype. Thirteen squares run from a Pico W with no control square attached, colour and layout both, and the wall is in Home Assistant with effects and fades — from nine commits written on a single day.
It is not a product and does not try to be one. Several bytes of the protocol are still unexplained, and the repository names them rather than leaving them out.
what is here
Five things, and the last one is what nobody has settled
Nobody had written down how this bus works
No public capture, decode or replacement controller exists for the Canvas panel bus — no baud rate, no framing, no command set, no layout trace, no CRC. The Shapes bus is documented and Canvas is not, so the Shapes format went in as a hypothesis and the squares were asked directly.
The same microcontroller is the analyser and the controller
No control square was available, so there was no reference capture to work from and no logic analyser in the loop either. The Pico W does three jobs on one pin: a PIO edge recorder at 16 ns resolution, a PIO single-wire half-duplex UART driving the same GPIO, and the CYW43439 carrying the result onto the network.
Thirteen squares, driven from one GPIO
canvasbus.py stress --minutes 10ran a thirteen-square wall for 600 s: 14,963 colour frames at 24.9 Hz, 14,963 good polls, 0 sessions lost, longest poll gap 40.2 ms. The bar it was written against is 20 Hz for ten minutes with no dropout, blanking or reset, and both thresholds are constants in the tool rather than prose.Fourteen lights in Home Assistant, one per square and one for the wall
The wall arrives over MQTT as one device with fourteen lights:
light.nanoleaf_canvas_wallandlight.nanoleaf_canvas_tile_0through12. Eight effects run on the firmware rather than on the host — solid, colour cycle, breathe, twinkle, rainbow wave, colour wave, ripple and fire.What is still guesswork, written down as guesswork
Every claim in the repository's documents carries a tag: confirmed from a primary source, an unchecked community claim, or an inference made while writing the notes. The inferences are marked as inferences, and the things nobody has established have a file of their own.
01 · bus
Nobody had written down how this bus works
No public capture, decode or replacement controller exists for the Canvas panel bus — no baud rate, no framing, no command set, no layout trace, no CRC. The Shapes bus is documented and Canvas is not, so the Shapes format went in as a hypothesis and the squares were asked directly.
It is a single-wire half-duplex UART, 8N1 at 1 Mbaud. The line idles high and a square sends nothing on its own — no traffic in 16.5 s after power-up. 80 returns the layout and C0 polls; the reply's trailer is CRC-16/ARC, little-endian, verified on both. A session drops if it goes unpolled: 476 ms between polls kept it, 497 ms lost it.
.venv/bin/python tools/canvasbus.py cap 2000 --label idle.venv/bin/python tools/canvasbus.py seq 00 80 C0 --label layout80 layout -> C3 00 93 00 40C0 poll -> 00 00
80 on its own gets nothing back. 00 first, then 80, returns the layout — which is the kind of thing you only find by sending the wrong thing enough times and keeping every capture.
- signalling
- Single-wire half-duplex UART, 8N1 at 1 Mbaud, push-pull. The line idles high.
80— layout- Replies
C3 00 93 00 40for one square. The first byte starts 24.5 µs after our stop bit; the rest arrive individually with 19–54 µs between them. C0— poll- Two bytes per square, starting 27 µs after the frame. 26 bytes on the wall of thirteen.
- trailer
- CRC-16/ARC, poly 0x8005 reflected, init 0, little-endian, over the bytes after the leading
01. - session
- Times out after roughly 480–500 ms unpolled. 476 ms held it; 497 ms did not.
- open drain
- Does not survive 1 Mbaud: high pulses shrink to about 0.7 µs, because the internal pull-up is around 50 kΩ and too weak at that rate.
02 · instrument
The same microcontroller is the analyser and the controller
No control square was available, so there was no reference capture to work from and no logic analyser in the loop either. The Pico W does three jobs on one pin: a PIO edge recorder at 16 ns resolution, a PIO single-wire half-duplex UART driving the same GPIO, and the CYW43439 carrying the result onto the network.
Sixteen thousand level changes per capture, cut back from forty thousand to leave the radio its RAM. Every capture is written twice, once as JSON and once as a VCD you can open in PulseView. The first flash failed silently because three PIO programs need 43 instructions and one PIO block holds 32; the recorder moved to pio1, and the firmware now panics at boot if a program does not fit.
export PICO_SDK_PATH=/path/to/pico-sdkcmake -S pico -B pico/build -G Ninja && cmake --build pico/build
CI cross-compiles canvas_probe.uf2 on every push and hands it back as a build artefact. It has to: the PIO tests emulate the assembled programs cycle by cycle, and they read the header the build generates from bus.pio.
The firmware's own modules — the layout parser, the renderer, the UART decoder, the effects — are compiled for the host and driven through ctypes, so the logic is tested without a Pico on the desk. What is left needing hardware is the part that is genuinely about hardware.
03 · wall
Thirteen squares, driven from one GPIO
canvasbus.py stress --minutes 10 ran a thirteen-square wall for 600 s: 14,963 colour frames at 24.9 Hz, 14,963 good polls, 0 sessions lost, longest poll gap 40.2 ms. The bar it was written against is 20 Hz for ten minutes with no dropout, blanking or reset, and both thresholds are constants in the tool rather than prose.
The layout was read blind. The Pico was plugged into a wall nobody had described to it, and canvasbus.py layout parsed the reply into thirteen squares and one supply with no overlaps and no leftover bytes — the poll came back 26 bytes, two per square, matching the count. The drawing came out upside down because it uses the root square's own frame; turned 180° it matches the wall exactly, supply and Pico sides included.
Thirteen squares and one supply, drawn from the coordinates the tool parsed. The wire coming in on the right is the Pico's only connection to any of it: the square it enters is the root, and every other square is reached through its neighbours. The dithered block at the bottom left is the supply, which sits in the layout reply and takes no colour.
.venv/bin/python tools/canvasbus.py stress --minutes 10600.0 s 14963 frames 24.9 Hz 14963 polls0 sessions lost longest poll gap 40.2 ms PASS
The counts are the tool's own. A person watched part of the run and reported it looking fine, not all ten minutes of it, and the repository records that rather than rounding it up to a clean pass.
04 · wired
Fourteen lights in Home Assistant, one per square and one for the wall
The wall arrives over MQTT as one device with fourteen lights: light.nanoleaf_canvas_wall and light.nanoleaf_canvas_tile_0 through 12. Eight effects run on the firmware rather than on the host — solid, colour cycle, breathe, twinkle, rainbow wave, colour wave, ripple and fire.
The twenty-eight discovery and state messages go out one every 25 ms. Sent as one burst they exhausted lwIP's memory and stalled the connection until the broker's keep-alive gave up, which is the kind of thing that only shows up on hardware. On a cold boot the squares' hardware IDs read back in about half a second and the radio is on the broker within about five.
light.nanoleaf_canvas_wall sets all thirteen at once. light.nanoleaf_canvas_tile_0 to light.nanoleaf_canvas_tile_12 set them one at a time, and select.nanoleaf_canvas_layout_rotation turns the drawing the right way up when the root square's frame does not match the wall.
The wall says what the radio is doing, because a headless board on a shelf has nowhere else to say it: it shimmers green for five seconds at boot, flashes white the first time it reaches the broker, and blinks red three times if it has not got there thirty seconds in.
05 · open
What is still guesswork, written down as guesswork
Every claim in the repository's documents carries a tag: confirmed from a primary source, an unchecked community claim, or an inference made while writing the notes. The inferences are marked as inferences, and the things nobody has established have a file of their own.
The poll reply's first byte varies — 10, 11, 12 and 20 have been seen, otherwise 00 — and it may be touch data; nobody has shown that it is. Whether sides are numbered clockwise or counter-clockwise needs a third square to settle. The connector's pin order is unverified. A Wi-Fi outage has not been tested, only a broker restart and a power cycle.
- A
- Confirmed from a primary source: official documentation, FCC exhibits, datasheets, published captures, code or photographs.
- B
- A community claim with no evidence anybody here could check.
- C
- An inference drawn while compiling the notes. Treat it as a hypothesis, including the ones that turned out right.
The tag travels with the claim rather than sitting in a preface, so a sentence lifted out of these documents carries how far anyone had actually got with it.
start here
Clone it and run the tests without a Pico
git clone https://github.com/Xapicc/picoleafcd picoleafuv syncuv run pytest
The host tools and the firmware's own logic are tested on the host, so most of the repository runs on a laptop with nothing plugged in. The PIO emulation tests skip themselves until the firmware has been built once, because they read the header that build generates.
The panel rail is 42 V DC and it sits next to the 3.3 V data contact. Identify the pins with a meter first: a slip destroys a data line or the Pico, and the wiring the bench used — GP2 through 330 Ω to DATA, ground common, the 40 V line insulated and not connected — is written out with a diagram in docs/prototype-v1.md.
The repository also carries the survey the probing started from: what is actually known about the Canvas hardware, the network API, the firmware history, and the sibling buses that were close enough to guess with.