FluoLight is an Arduino-based smart lighting controller for networked status indicators. It uses addressable RGB LEDs to display different status colors based on HTTP responses from a server.
This project is designed to create a visual indicator system that can be controlled remotely through a network interface. The device connects to a server via Ethernet, periodically polls for status updates, and displays corresponding colored lights on an LED strip. It is particularly useful for room status indicators, availability displays, or any application requiring visual status feedback.
- FLUOboard (Fluo Technology, ATmega32U4 with built-in Ethernet)
- Addressable LED strip (NeoPixel compatible, WS2812)
- I2C EEPROM (for storing MAC address when using automatic MAC mode)
- USB cable with data lines (not a charge-only cable)
- Ethernet connectivity with both DHCP and static IP configuration options
- Remote control via HTTP requests
- Multiple status display modes with various color patterns
- Configurable refresh intervals and networking parameters
- Error recovery with optional watchdog and reset capabilities
- Detailed serial logging with configurable verbosity levels
- LED effects including color wipes, blinks, and fades
- arduino-cli installed and in
PATH - FLUOboard core (
fluo:avr) installed (see Board Setup below) - Required libraries installed (see Dependencies below)
python3for the test server
Add the FLUOboard package URL to arduino-cli once:
arduino-cli config add board_manager.additional_urls \
https://raw.githubusercontent.com/VashTheProgrammer/FLUOboard/master/package_fluo_index.json
arduino-cli core update-index
arduino-cli core install fluo:avrBoard FQBN: fluo:avr:fluoeth
make # show available targets
make compile # compile only
make flash # compile + upload (port auto-detected)
make monitor # open serial monitor at 115200 baud
make server # run the local test server
make board # list connected boardsThe serial port is auto-detected from arduino-cli board list (FQBN
match, then VID fallback). Override if needed:
make flash PORT=/dev/ttyACM1arduino-cli compile --fqbn fluo:avr:fluoeth .
arduino-cli upload -p /dev/ttyACM0 --fqbn fluo:avr:fluoeth .
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200All configuration is done through preprocessor definitions in config.h.
#define VERBOSE 10- No output (hardened production)1- Errors only (production) - flash impact: ~+1%2- Errors, warnings, info with serial sync (debug) - flash impact: ~+9%3- All output including debug - flash impact: ~+11%
Note: when VERBOSE > 1, the board waits for a serial connection before starting (while(!Serial)). Set VERBOSE to 0 or 1 for standalone deployment without a PC.
#define SERVER "your-server-address"
#define SERVERPORT 8080SERVER- FQDN or IP address of the HTTP serverSERVERPORT- Port number of the HTTP server (default:8080, matchesserver/test_server.py)
The board makes GET /lights/<mac> requests to this server every HTTP_REQ_INTERVAL ms and expects a response body containing <N> where N is a digit 0-9 (see Status Display Codes).
#define MACSET 0
#define MAC_ADDRESS {0x00,0x00,0xDE,0xAD,0xBE,0xEF}MACSET0- Auto (read from I2C EEPROM) - flash impact: ~+5%1- Manual (useMAC_ADDRESSbelow)
MAC_ADDRESS- Six-byte MAC address (only used ifMACSET=1)
#define DHCP 1
#define IP_ADDRESS {192,168,001,127}
#define IP_SUBNET {255,255,255,000}
#define IP_GATEWAY {192,168,001,254}
#define IP_DNS {192,168,000,254}DHCP0- Manual (use static IP configuration below)1- Auto (use DHCP) - flash impact: ~+13%
Note: with DHCP=0, Ethernet.begin() blocks for up to ~60s at boot if no network cable is connected.
#define EXT_LINK_CHECK 0
#define EXT_LINK_SERVER {9,9,9,9}
#define EXT_LINK_PORT 53EXT_LINK_CHECK0- Disabled1- Auto (use DNS server from DHCP) - flash impact: ~+1%2- Manual (useEXT_LINK_SERVER) - flash impact: ~+1%
#define WATCHDOG 0
#define WATCHDOG_TIMER WDTO_8SWATCHDOG0- Disabled (enableRESET_ON_FAILinstead for network error recovery)1- Enabled (hardware watchdog viaavr/wdt.h, auto-reboot on CPU freeze, 8s timeout)
#define RESET_ON_FAIL 0
#define REBOOT_TIMEOUT 4000
#define MAX_RETRY 10RESET_ON_FAIL0- Disabled (enableWATCHDOGinstead for CPU freeze recovery)1- Reset on link failure - flash impact: ~+1%2- Reset on link, DHCP, or external link failures afterMAX_RETRYattempts - flash impact: ~+2%3- Reset on all errors afterMAX_RETRYattempts - flash impact: ~+2%
REBOOT_TIMEOUT- Time in ms before a long reset triggersMAX_RETRY- Number of consecutive failures before reset
Production recommendation: enable at least one of
WATCHDOGorRESET_ON_FAIL.WATCHDOGcovers CPU freeze (hardware timer);RESET_ON_FAILcovers recurring network errors (link down, DHCP fail, external link unreachable). Enabling both is safe and recommended for unattended operation.
# Production: errors only, hardware watchdog, auto-reset on network errors
make flash VERBOSE=1 WATCHDOG=1 RESET_ON_FAIL=2
# Full debug: max verbosity, external link check on 9.9.9.9
make flash VERBOSE=3 EXT_LINK_CHECK=2
# All features enabled (debug + watchdog + reset on fail + ext link auto)
make flash VERBOSE=3 DHCP=1 EXT_LINK_CHECK=1 WATCHDOG=1 RESET_ON_FAIL=2
# Minimal flash footprint: no watchdog, no reset, no serial output
make compile VERBOSE=0 DHCP=1 EXT_LINK_CHECK=0 RESET_ON_FAIL=0 WATCHDOG=0#define LED_PIN 6
#define LED_COUNT 7
#define BRIGHTNESS 16
#define FADE_SPEED 5
#define WIPE_SPEED 50The LED strip is physically wired GRB (green-red-blue), not RGB. The driver is
initialized with NEO_RGB so that bytes are sent in G-R-B order to match the hardware.
As a result, every Color() and setPixelColor() call in the sketch passes arguments
as (G, R, B) rather than the more common (R, G, B). For example, pure red is
Color(0, 255, 0) and pure green is Color(255, 0, 0).
Do not "correct" these values or change NEO_RGB to NEO_GRB. The colors render
correctly on the hardware as-is. Changing either the flag or the argument order would
invert red and green on the strip.
#define LINK_CHECK_INTERVAL 500
#define EVENT_DISP_INTERVAL 50
#define DHCP_STATUS_INTERVAL 10*60000
#define HTTP_READ_INTERVAL 600
#define HTTP_REQ_INTERVAL 3000
#define EXT_LINK_CHECK_INTERVAL 25000
#define MAIN_LOOP_INTERVAL 0The device interprets numeric codes received from the server (inside <N>).
All app animations are non-blocking: the HTTP polling loop keeps running during
any LED effect. An animation replays only when the server code changes.
| Code | Meaning | LED effect |
|---|---|---|
0 |
Server error | Continuous fade red |
1 |
Available + vacant | Wipe green |
2 |
Available + occupied | Wipe orange |
3 |
Booked + vacant | Wipe pink |
4 |
Booked + occupied | Wipe red |
5 |
Soon in use + vacant | Wipe orange |
6 |
Soon in use + occupied | Continuous toggle green/orange |
7 |
(user-defined) | Wipe blue |
8 |
(user-defined) | Wipe white |
9 |
Off | Wipe black |
The board emits its own LED signals independently of the server to indicate network and hardware state. These events are also non-blocking and use the same animation state machine.
| Event | Semantic | LED effect | Trigger |
|---|---|---|---|
| 1 | Board error | Fade red | Any network error when RESET_ON_FAIL > 0 |
| 2 | Ethernet init OK | Wipe green (1 pixel) | Boot, after DHCP/static IP assigned |
| 3 | Ext TCP check OK | Flash white | EXT_LINK_CHECK < 2, every 25s, target reachable |
| 4 | Ext TCP check KO | Flash orange | EXT_LINK_CHECK < 2, every 25s, target unreachable |
| 5 | DHCP renew/rebind OK | Fade green | DHCP=1, every 10min, lease renewed |
| 6 | DHCP renew/rebind KO | Fade red | DHCP=1, every 10min, lease failed |
Board reset: the strip briefly fills blue (~300ms) just before the watchdog
triggers a reboot. This happens in reset() directly, independently of the
event system.
The device makes HTTP requests in the following format:
GET /lights/<mac_address> HTTP/1.1
Host: <SERVER>
Connection: close
The server must respond with a body containing <N> where N is a digit 0-9, for example <1>.
A local test server is provided to validate the LED display without a production server.
make server
# or directly:
python3 server/test_server.pyThe server listens on 0.0.0.0:8080 and cycles automatically through all 10 status codes (one per request received from the board). Each GET /lights/<mac> request advances the cycle by one step.
Make sure port 8080 is open in your firewall:
sudo ufw allow from 192.168.1.0/24 to any port 8080 proto tcpA dedicated CI/CD status server (server/pipeline_server.py) is available
for production deployments. See server/README.md for configuration,
authentication, and usage instructions.
- Install arduino-cli and the FLUOboard core (see Board Setup above).
- Install the required libraries (see Dependencies below).
- Configure
config.h(server address, port, MAC mode, network settings). - Compile and upload:
make flash - Connect the board to your network (Ethernet cable required at boot when using DHCP).
- Open the serial monitor to verify startup:
make monitor - The LED strip displays blue during startup, then follows server responses.
- Board not detected at all (
/dev/ttyACM0missing): the USB cable is charge-only (no data lines). Replace with a full USB cable. - No serial output after connect: with
VERBOSE > 1the board waits for a serial connection before running. Open the monitor then press the reset button on the board. - LED stuck on orange/red at boot, no HTTP activity: board is waiting for DHCP with no network cable connected. Connect the Ethernet cable before powering on.
ERR:HTTP:SND:KO: server unreachable. CheckSERVER/SERVERPORTinconfig.h, verify the server is running, and check the firewall on the server machine (port 8080).- LED displays pulsing red: board error event (system event 1) or server error code
<0>. Check serial output forERR:lines. - Upload fails with "port not found": the 32U4 bootloader may have timed out. Double-press the reset button to force bootloader mode, then retry
make flash. brlttyinstalled on Ubuntu 24.04: this can interfere with CH340-based boards but does not affect the FLUOboard (ATmega32U4,cdc_acmdriver).
- SPI (bundled with Arduino AVR core)
- Ethernet (bundled with Arduino AVR core)
- I2C_EEPROM
- Adafruit NeoPixel
- TimedAction (installed automatically by
make compileviaarduino-cli lib install --git-url) - avr/wdt (bundled, used when
WATCHDOG=1orRESET_ON_FAIL > 0)
This sketch targets the FLUOboard (Fluo Technology, ATmega32U4 with built-in Ethernet).
Board Manager URL:
https://raw.githubusercontent.com/VashTheProgrammer/FLUOboard/master/package_fluo_index.json
With the default configuration (VERBOSE=1, DHCP=0, EXT_LINK_CHECK=0, RESET_ON_FAIL=0, WATCHDOG=0):
- Program storage: ~25,880 / 28,672 bytes (90%)
- Dynamic memory: ~912 / 2,560 bytes (35%)
Flash usage is sensitive to verbosity and network options (see flash impact notes in each section above). With VERBOSE=1 approximately 9% of flash is recovered.
The five main options (VERBOSE, DHCP, EXT_LINK_CHECK, RESET_ON_FAIL, WATCHDOG) are guarded by #ifndef in config.h and can be overridden at compile time:
arduino-cli compile --fqbn fluo:avr:fluoeth \
--build-property compiler.cpp.extra_flags="-DVERBOSE=1 -DDHCP=0 -DRESET_ON_FAIL=2 -DWATCHDOG=1" .