FluoBoard: my CI status light since 2019, finally open source
Third episode of the FluoBoard series, after the original board
and its rewrite
. This time, what I made of it for myself. Back in 2019 I already had my own dev-and-test board, the one I used to tinker with beyond the status-light firmware itself and that is the one I repurposed: the inherited system showed meeting-room availability, I turned it into my own CI status light. In 2021 the company I worked for then gave me five or six more, so I could keep having fun. They stayed on my desk, for my own use alone: a small Python server bridged my CI pipelines at work and the LED strips, translating each build’s state into a color. So the concept is seven years old. What is new in 2026 is that the rebuild is open source
and the original protocol survived intact: the board still polls GET /lights/<mac> and reads one ASCII digit between <...>.
Let me admit it right away: if I kept this single-digit protocol instead of starting over with a plain state machine, it was out of loyalty to that original setup, not technical merit. Honestly, a state machine would have done the job better.
The same digit, a different master
The 2019 codes described meeting-room occupancy (<1> lime free, <4> red booked-and-busy). The 2026 server (server/pipeline_server.py, 367 lines) re-maps the digits to CI state:
CODE_ERROR = "0" # error / unknown / MAC not in config
CODE_SUCCESS = "1" # success
CODE_FAILED = "4" # failed / timed out
CODE_RUNNING = "6" # running / pending / queued / created
CODE_CANCEL = "8" # canceled / skipped / manual
CODE_NONE = "9" # no pipeline found
Same one-digit channel, new master: the server polls GitHub Actions and GitLab CI APIs every 30 seconds and answers the board’s poll with the derived digit. The strip turns green on success, red on failure, blinking orange while a pipeline runs. The board is a CI status light; the meeting rooms got their occupancy from the same digit years ago.
Two server behaviors are worth stealing for any poller: a StatusCache that keeps the last known state when the CI API blips (a 500 must not make the wall blink rainbow) and tokens that live only in environment variables (GITHUB_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_<HOST> for self-hosted). The whole thing is Python stdlib only: no framework, no dependencies, no pip on the box serving the office wall.
CI that compiles firmware
The repo’s own CI (.github/workflows/ci.yml, added July 2026) treats firmware like software, which it is:
- a matrix of four compile profiles (default, prod-minimal, prod-robuste, dev-verbose) so every config combination still builds;
- a flash-overflow guard:
FLASH_MAX=28672and the build fails if any profile exceeds it. Firmware regressions are silent until the upload fails on hardware; the guard makes them loud at PR time; - report-size-deltas commenting flash/RAM deltas on every PR, so you see the cost of your feature before merging it;
- a syntax lint of
pipeline_server.pyitself, because the server is part of the same repo.
Flashing stays manual and local on purpose: make flash compiles with a custom board definition (fluo:avr:fluoeth), auto-detects the port, uploads. CI never touches hardware. An office light that bricks itself because a pipeline got excited is not a status display, it is a liability.
The GRB war story, part two
The 2021 episode told how twelve color constants were commented // red, six of them, blues included. The 2025 rewrite put the truth in one comment block and the README now has a section that reads like a scar: “Do not ‘correct’ these values or change NEO_RGB to NEO_GRB”. The strip is physically wired GRB; the driver flag NEO_RGB sends bytes in G-R-B order; every Color() call follows the G-R-B convention. The first visitor who “fixes” this ships a green LED as red. The comment block exists because the fix has been attempted and the attempt has been reverted.
Seven years, one digit
- Protocols outlive code. The 2019 digit contract survived three firmware generations and two servers; the code around it did not and did not need to.
- The poller must be boring. Stdlib, env tokens, last-known-state cache. A status display that crashes on an API blip displays nothing.
- Firmware CI is a size problem first. The flash-overflow guard and the PR size deltas are the two checks that catch real regressions; everything else is build assurance.
- Write down your hardware lies. The GRB block is documentation of a decision someone will definitely try to undo. Document it like it will happen, because it will.
[edit: 2026] The Ansible implementation of all this: the gitlab-runner and github-runner roles.