Change log for the two pieces of firmware that drive the Smatfarm G5 reef LED fixtures over this system's second tank: a small patch inside each fixture's own GD32F130 chip, which replaced the dead touch panel with a serial command interface, and the ESP32 controller that talks to it. The full story – how the firmware was read out, where the patch lives, and how to flash it yourself – is in the how-to, Smatfarm LED Touch Bypass. This page is the running record of what changed and why, in the same form as the doser's firmware changes. Not affiliated with Smatfarm.
Smatfarm Firmware Changes
Fixture patch, first version (GD32F130, impl.c)
- A serial command interface in place of the touch panel. Writing
0xA0–0xA5selects channel A–F and the next byte (0–100) sets its brightness, written into the same setpoint buffer the fixture's own controls use, at 115200 baud on the fixture's serial port. It was placed over a function believed to be dead touch-handling code. It was the fan controller (fan on whenever any channel is at 20% or more), so the cooling fan on the patched fixture never ran again. The misread came from a nearby value that belonged to the previous function, not this one; disassembling the original bytes would have shown it at once. Fixed in the second version below.
Fixture patch, second version (impl.c, impl.ld, tramp.s)
- The patch moved to empty flash, and the fan control came back. The original function's space now holds a single jump to the patch at
0x0800a000, in a 24 KB stretch of flash the stock firmware never uses. The patch first does what the overwritten function did – fan on when any channel setpoint is at 20% or more – and then handles the serial commands. Flashing writes only the pages that change, and writes the jump last: if the jump went in first and the patch write then failed, the fixture would jump into blank flash on every pass of its main loop. - The linker put the wrong function first, and it shipped. A jump to a fixed address needs the patch's entry function to be the very first byte of its page. A plain linker script does not guarantee that; the first build placed a small helper there, so the jump landed in the wrong function, the fan still never ran and no serial byte was ever read. It went unnoticed at first because the fixture had only been driven over the debug port, never over its serial line. Fixed by giving each function its own section, placing the entry function first explicitly, and adding a build-time check that fails if it is not at the start of the page.
- The patch's own variables ran past the end of the chip's memory. They were first placed where they extended beyond the top of the 8 KB of RAM. They now sit just below the top, above the stock firmware's stack, in RAM nothing in the stock firmware uses. The stock startup code only clears its own variables, so ours start as random values after power-up; a marker value written on first run tells the patch to initialise them.
- Readback: the controller can see what the fixture is doing. Sending
0xB0returns a frame:0xB0, a length byte, then the schedule mode (whether our control is holding or the fixture has taken back its own schedule), the fan state, the temperature (from the fixture's own thermistor, in tenths of a degree), a tick counter, the six commanded setpoints, the six live values after the fixture's internal fade, and an XOR checksum. The length byte lets a controller cope with the frame growing later. The schedule-mode byte is the important one: it is how the controller notices that the fixture lost power and quietly went back to its own schedule. - The fixture's own log output could land on top of the checksum. The fixture also prints its own debug messages on the same serial line, for example for every infrared remote code it receives. One of those characters was seen sitting exactly where the checksum should have been. The frame now ends with two harmless line-ending bytes after the checksum, which absorb that, and the controller resyncs on the
0xB0header rather than assuming a clean line. - Failsafe: if the controller goes quiet, the fixture takes over again.
0xB2hands control back to the fixture's own sunrise/sunset schedule immediately. Without any command for a set time (default one hour, changeable with0xB3), the patch hands back by itself, so a dead controller cannot leave the lights frozen at its last value over a reef. A readback also counts as a sign of life, because a controller only writes when a value changes and a midday plateau can go for hours without one. The stock main loop calls the patch once a second, so one tick is one second; a first draft assumed a much faster loop and would have set the default to over a year. - A "burst drain" that does not do what its comment says. Once the patch sees a byte waiting, it keeps reading for about half a millisecond to catch a burst of bytes in one pass. In practice that almost never helps: the chip's serial port holds only one byte, the patch runs once a second, and a burst arrives at a random moment, so by the time the patch looks, all but the last byte have been overwritten. It costs nothing and is left in, but the controller does not rely on it and paces every byte instead (item 11).
- Optional: hand the display to the controller. A build option sets the four display pins to inputs on every pass, so an external controller can drive the fixture's 128×32 OLED. Off in the published download, so it never blanks someone else's working screen.
Fixture #1: repairing what a bad firmware read destroyed
- The first firmware read was silently corrupt, and was written back. The read-out over the debug port zeroed parts of the upper half of flash in a repeating pattern – an artifact of the adapter's bulk read, not of the chip. Writing that image back to fixture #1 zeroed its fonts and menu labels, the temperature lookup table and the wireless pairing word for real. The lighting itself was never affected. On 2026-10-27 those pages were restored from the untouched fixture #2, read back one word at a time (a different path through the adapter) and cross-checked, and every page was verified after writing. The how-to now tells readers to verify a read before trusting it, and to write back only the pages the patch changes: Verify your dump before you trust it.
ESP32 controller (smatfarm.py, main.py, ui.html), version 1.1.6
- Every byte is paced more than a second apart. The fixture's serial port holds one byte and the patch reads it once a second, so sending a channel byte and its value together lost the first: the second overwrote it before the patch ever looked. The controller now leaves 1.15 s between bytes. A six-channel change takes about 14 seconds, which is slow but certain.
- A frame reader that expects interference. It looks for the
0xB0header, reads the length byte, checks the checksum and discards anything else, since the fixture's own log text shares the line. It waits up to 1.6 s for a reply, enough for one full poll of the fixture's main loop plus the frame. - It notices when the fixture takes back control. When the readback shows the fixture back on its own schedule – after a power cut, for example – the controller re-asserts the commanded values. "Hand back to fixture" (
/release) gives control back deliberately and stops the controller re-asserting for 12 hours;/failsafesets the patch's timeout. - The brightness sliders hold your change. The web page refreshes its status every few seconds, which used to snap a slider back to the old value while a 14-second write was still under way. A slider you moved now stays yours for three minutes, or until the fixture reports the value you asked for.
- Channel E is red, not "3000K warm white". The manual and the fixture's own menu call it warm white. Driving each channel on its own showed a red emitter. It is relabelled in the controller, and a reef lighting mix that ran it at 20% (assuming warm white) now runs it at 5%. Channel F was confirmed as 500 nm green the same way; the labels for A–D still come from the manual and have not been checked individually.
- Updates over the network, file by file. The controller downloads a manifest, fetches only changed files, checks each one's hash as it downloads, and renames them into place only once every file has arrived and verified – so a power cut or bad connection mid-update leaves the running version untouched.
Still open. Fixture #2 still runs its factory firmware and is kept that way as the reference copy until it is patched. Driving the OLED from the ESP32 is written but has not yet been tested on the fixture. The absolute light level has not been measured: the curve is right in shape, but setting it against a coral needs a PAR meter.
Current state: fixture #1 runs the second-version patch, driven by ESP32 controller version 1.1.6.