ATI Straton Flex Firmware Rewrite

Following on from the ATI Straton Flex Reliability Notes -- this tank's two Straton Flex 153 fixtures (see the Acropora page) are now also running a from-scratch Node.js rewrite of the fixture's own web application, in place of ATI's original closed-source app, on both units as of 10 October 2026 (it ran on one unit as a live trial first -- see Rolling it out carefully below). Same hardware, same OpenWrt/Onion Omega2+ board, same LED driver underneath -- only the application layer on top has been replaced. SSH, networking, and the rest of OpenWrt are untouched.

Why a rewrite, not just more patches

The reliability investigation above started as a memory-watchdog crash and ended with a one-line config fix, a smarter scheduler, and automatic recovery -- all patches on top of ATI's own app. Reading through that app's own source to diagnose the crash meant reading all of it, and that turned up several further structural issues in how the application itself is built -- old, long-unpatched bundled dependencies and some genuinely concerning gaps in how a few features handle input. Same disclosure policy as the reliability page: no exploit detail here, and ATI is welcome to get in touch via the contact page for specifics. What those findings did change is the plan -- rather than patching a codebase with structural problems one hole at a time, building a clean replacement for the parts that matter (scheduling, the web UI, the device's own settings) felt like the more honest long-term fix for hardware I already own and run unattended, next to a few hundred liters of livestock.

What it actually is

A plain Node.js application, deployed as a drop-in replacement for the original app's own process on the exact same device -- same restart mechanism, same init script, same node --expose_gc invocation the hardware already runs. It talks to the fixture's real LED driver over the same serial protocol as the original (reverse-engineered directly from the original app's own behavior, not guessed), and stays compatible with this tank's existing automation: the same bearer-token API the reliability fixes above already added keeps working unchanged, so the Python scheduler that drives this tank's actual lighting curve never noticed the swap.

Built with an actual automated test suite -- several hundred tests, run on both a current Node release and, specifically, the exact old Node 8.10.0 this hardware itself runs, since a test passing on a modern Node proves nothing about a nine-year-old JavaScript engine on a 124MB-RAM board with no swap. More than one real compatibility gap (a method that simply doesn't exist yet on that old a Node version) was caught this way before it ever reached the real device.

What's new on top

  • A live temperature-based safety clamp -- the fixture's own LED-driver temperature sensors are polled in real time, smoothed, and wired into both a gradual power derating curve and a hard shutoff, rather than treating temperature as a display-only number.
  • A "Showcase" mode -- a time-boxed override for showing the tank off to visitors without touching or losing the real automated schedule underneath; it expires and reverts on its own.
  • A diagnostics bundle download, built the same way the original app's own "Download Support File" works (full logs, process state, memory info, config) but with real secrets (the device's own API token) redacted before the file is ever written, not left sitting in plain text in a file meant to be shared for troubleshooting.
  • Settings pages for things that previously meant SSHing into the device directly: picking the fixture's own timezone from a short list of real locations (Europe/Brussels, Europe/London, UTC, or a raw POSIX timezone string for anything else), and switching real-time log shipping to an off-device receiver on or off.
  • A proper two-tier password recovery path (an SSH-based reset, and a physical-button fallback) instead of a single shared recovery mechanism.
  • A light acclimation ramp. Corals don't tolerate a step change in light, so raising a tank's lighting means easing it up over days while watching how the animals respond. This adds that as a setting on the fixture itself: a start level, a finish level, a number of days, and a start date. The level steps once per calendar day and holds for the whole day, so one day's observation can actually be compared with the next -- a level drifting upward through the day would make that impossible. Day zero is the starting level, so switching a ramp on never changes the same evening's light. It is expressed as a percentage of the schedule rather than as an absolute brightness, and deliberately cannot exceed it: 100% means "run the schedule exactly as it is", and the setting can only ever scale output down. That direction matters -- if the setting is ever lost or reset, the fixture falls back to its normal schedule rather than jumping to full output.

Two details of where that ramp lives turned out to matter more than the ramp arithmetic itself. It is stored in its own file rather than inside the schedule, because this tank's external scheduler replaces the fixture's entire timeline every fifteen minutes -- a level written into the schedule would have been erased ninety-six times a day. And it is applied at the point where each channel's value is computed, before the fixture's own current-safety model adds that channel up, so the closed-loop current throttle sees the real, reduced draw. Applying it later, to the finished commands, would have left the safety loop correcting against a power draw that no longer existed.

Found along the way

Writing an independent implementation against the same real hardware surfaced a couple of its own bugs, caught and fixed before they mattered:

  • The schedule was running two hours behind real local time. The time-of-day calculation driving the lighting schedule was computed in UTC rather than the fixture's own local time -- invisible for most of the testing (the development environment itself happened to run in UTC too, masking the bug), until it showed up live as a visibly wrong schedule and a dark "Royal Blue" channel during what should have been moonlight. Fixed to compute against real local time instead.
  • A self-defeating hourly NTP restart, already covered on the reliability page -- found while chasing a small but real clock discrepancy during this rewrite's own testing, this one turned out to be a pre-existing issue in the stock firmware itself, not something the rewrite introduced, and the same fix now runs on both fixtures regardless of which application they're running.
  • Some LEDs would visibly flicker during live testing. Traced to how lighting commands reach the fixture's LED driver board over its serial connection: the original app queues each update and sends it one command at a time with a short pause in between; this rewrite was sending a whole batch back-to-back with no pause at all -- a difference invisible to automated testing (packet timing on real hardware isn't something a test simulates) but apparently enough for the driver board to occasionally drop or misread a command. Fixed by matching the original's own pacing; no flickering observed since.
  • A new setting saved correctly, displayed correctly, and did absolutely nothing. The acclimation ramp above resolved the right level, wrote the right file, survived a schedule replacement and reported itself correctly on its own page -- while changing the light not at all. The value was being handed to the component that runs each scheduler tick, which accepted it, unpacked it, and then simply never passed it on to the code that computes channel values. Every automated test still passed, because they each called that final computation directly and so never travelled the one hop that was broken. What caught it was refusing to accept a feature as working on the basis of its own status display: comparing the actual commands the fixture generated, on the fixture, with the setting on versus off. They were byte-for-byte identical. The missing hop is now a one-line fix, and the test that covers it was checked by reintroducing the bug and confirming the test fails -- a test that has never been seen to fail is only a guess about what it tests.
  • A frozen system clock silently took cron down with it for twelve days, and the first visible sign was a wattage number. One fixture's reported power draw against the other's, at the same point in the shared schedule, looked wrong enough on 18 October 2026 to be worth distrusting rather than writing off as normal unit-to-unit variance. Tracing it past a dead end (a 2026-10-04 finding, already fixed, about this same route going briefly missing during the rewrite rollout) led to the fixture's own system clock: stuck at 6 October, 13:44 UTC, twelve days behind real time, while the device otherwise ran completely normally -- Wi-Fi, DHCP, the lighting app itself. Busybox's cron schedules from wall-clock time, so a wall clock that stops advancing takes cron's own scheduling down with it, invisibly: the process stays alive, nothing crashes, it simply stops firing anything -- including the one hourly job that could have corrected the clock and let cron recover on its own. A self-locking failure with no exit except someone noticing. Fix: a small watchdog now runs inside the fixture's own long-lived Node process, on a timer Node itself schedules from the monotonic clock rather than wall time, so it keeps running through the exact freeze that took cron down. It checks every five minutes whether wall-clock time and monotonic time agree; if they've drifted apart by more than two minutes, it calls the NTP sync script directly rather than waiting for cron to notice. Five tests cover the frozen-clock case, the inverse (a sudden forward jump), and a guard against stacking a second correction attempt on top of a slow one. The same investigation also turned up the correction described on the reliability page: an earlier NTP fix that, nine days on, still hadn't actually reached this fixture.

Rolling it out carefully

This runs a live reef tank, so the rollout has been deliberately cautious rather than a one-shot swap. Every deploy backs up the currently-running version on the device first and can restore it automatically if the new version doesn't come back up healthy. Early live tests ran under an overnight automated monitor that watches both fixtures and reverts the trial fixture back to the original, genuine ATI application on its own if it detects a real, sustained problem -- a single transient blip is logged, not acted on, but a confirmed issue triggers an automatic rollback with no one needing to notice and intervene by hand. A memory watchdog matching the original app's own safety margin was added on the same reasoning once a direct side-by-side comparison showed the rewrite had no equivalent of its own.

Current status (updated 10 October 2026): both fixtures now run the rewrite. One unit ran it alongside the other, still on ATI's original application, for six days of direct side-by-side comparison before the second was switched over. Over that window both fixtures answered every one of 1,614 availability checks, neither needed an automatic recovery, and neither showed any sign of a memory leak -- both drifted by under 2 MB, and not consistently in one direction. The rewrite averaged roughly 5 MB lower memory use than the original app, with less than half the variance; switching the second unit over freed about 20 MB of RAM and a little flash on it, since the rewrite drops the original's bundled dependency tree entirely.

It's worth being precise about what that comparison does and doesn't show. It demonstrates the rewrite introduces no regressions -- it does not demonstrate that the rewrite fixes the original crash, because the original application didn't crash during the window either. The memory-limit change described in the reliability notes had already dealt with that: the unit still on ATI's app passed thirteen days of uptime, against the roughly six-day failures seen before. The case for the rewrite is that it's leaner and that I can actually read and maintain it, not that it rescued a crash.

With both units on the rewrite, its optional fixture-to-fixture schedule sync became usable for the first time. It's authenticated with the receiving fixture's own token, and off by default -- two tanks can legitimately want different lighting, so this is a per-fixture choice rather than an assumption that fixtures should always mirror each other. I've tested it end to end between the two units and then left it switched off here, because the 15-minute schedule push from my own automation already keeps both fixtures in step and makes it redundant in this particular setup.

Not published as an open-source project at this point -- built specifically against my own two units, not as a general-purpose release.

Download

A standalone technical overview of the rewrite -- scope, module architecture, what it adds, the deployment and rollback procedure, the side-by-side trial figures, and its limitations:

The source itself is not available for download, and that's deliberate rather than an oversight. Two reasons. It was written against two specific out-of-warranty units -- their board revision, their LED driver's serial behaviour, their OpenWrt build -- and published as a general release it would be a liability to anyone whose fixture differs in any of those. And the rewrite exists partly because reading the original application turned up things I've chosen to offer to ATI directly on request rather than publish, and a security-motivated rewrite discloses a fair amount about what it was motivated by simply through its own shape. The document above describes what it does and how it was validated, which is the part that's useful to another owner; the code stays with its two fixtures for now.

Same ownership stance as everything else on this page and the reliability writeup above: this hardware is out of warranty, and I consider inspecting, patching, and rewriting something I've paid for and run myself to be entirely my own call and my own risk to carry. If any of this is useful background for your own out-of-warranty unit, the same applies to you.