A mixed SPS reef tank, dosed and lit by a self-built automation system rather than fixed daily programs. Chemistry (calcium, magnesium, alkalinity) is held on a peristaltic doser with schedules recalculated from actual tank measurements, and the tank's two lighting fixtures follow real solar elevation, live cloud cover and real moon phase for a reference Coral Triangle reef -- the same weather-tracking philosophy as the Heliamphora growroom, applied to a saltwater tank instead. This page documents the dosing and lighting automation and its live measured output; the photos further down are just the tank itself.
Chemistry & Dosing
Calcium and magnesium are held by a self-built multi-channel peristaltic doser on a 750L system, built on top of ReefRhythm-SmartDoser (MIT licensed, ESP32-S3 + MKS SERVO42C stepper-driven pump firmware) rather than written from scratch. Testing against this tank's own MKS SERVO42C stepper drivers surfaced several real bugs in the upstream dosing math and driver protocol -- most seriously, a low-flow calibration curve that could extrapolate the wrong direction and silently skip the runtime-shortening safety check meant for near-zero doses, and an RPM-snap case where the driver's achievable minimum speed was used without rescaling the dose runtime to match, both of which meant a small requested dose could silently run closer to a full-size one. Also fixed a UART CRC check that summed decoded values instead of the actual transmitted bytes (rejecting valid replies for any multi-byte field), and an OTA versioning bug where a build with an unset version tag could make a real firmware fix silently fail to take effect even after a "successful" update. Full writeup of all 11 fixes: Firmware Changes. Rather than a fixed daily total, each pump's per-dose amount is recalculated from an actual measured reading against target -- e.g. calcium's current schedule holds roughly 289mL/day of a CaCl2·2H2O solution (130.55g/L) split into 46 small doses rather than a few large ones, keeping the swing between doses small since large single dumps of calcium/alkalinity risk a local precipitation spike. Magnesium runs the same way, dosing a Tropic Marin Bio-Magnesium stock solution. Alkalinity is buffered separately with a NaOH solution (74g/L). Firmware updates over the air rather than needing physical access -- the build hosted here
(version v1.1.15-manual-20260922023824-live-dose-modal, sha256 d95d30ec...a9cc4af in the
manifest below) is built from the source tree with all the fixes above already applied, verified
by matching that same sha256 against the local build output before publishing it -- not just
documented fixes sitting unused in a checkout somewhere:
micropython.bin,
bootloader.bin,
partition-table.bin,
manifest.
Current dosing (live from the doser; ramps change these numbers day by day)
Element
Solution
Per dose
Doses/day
Total/day
Cumulative dosed*
Calcium
CaCl2·2H2O, 130.55g/L
3.23 mL
46
~149 mL
18.38 L
Magnesium
Tropic Marin Bio-Magnesium, 25 scoops/L
9.47 mL
46
~436 mL
3.4 L
Ionic balance
Tropic Marin Part C, 47.4g/L
6 mL
40
~240 mL
5.27 L
Alkalinity
NaOH, 74g/L
1.48 mL
43
~64 mL
1.57 L
Nitrate/Phosphate export†
White vinegar, 8% acetic acid
3.56 mL
12
~43 mL
628.7 mL
*A true lifetime total: pulled live from the doser's own real dosing counter (not estimated from the schedule) via a firmware endpoint added specifically for this (see Firmware Changes item 12), and carried forward across every container refill since this tracking began, so refilling never resets it. No exact calendar start date is recorded for the accumulation, just the running total itself.
†Unlike the rows above, this isn't replenishing a depleted element -- it's carbon dosing: the vinegar feeds bacteria that consume excess nitrate/phosphate, exported via the skimmer. Dose is ramped up gradually and tracked against nitrate/phosphate test results rather than held at a fixed target level. Started 2026-09-20.
Water Parameters
Manually tested and logged into the same tool that computes the dosing corrections above -- these are real readings, not estimates. "In range" for Ca/Mg/Alk means within 5% of target; for NO3/PO4 it means inside the target band used by the vinegar (carbon dosing) plan.
Parameter
Current
Target
Last tested
Status
Calcium
440 ppm
440 ppm
2026-10-09
● in range
Magnesium
1320 ppm
1350 ppm
2026-10-10
● in range
Alkalinity
7 dKH
7 dKH
2026-10-09
● in range
Nitrate
42.3 ppm
2-5 ppm
2026-10-10
● out of range
Phosphate
0.1 ppm
0.03-0.1 ppm
2026-10-06
● in range
Salinity
1.0265 SG
1.026-1.027 SG
2026-10-06
● in range
Lighting
2x ATI Straton Flex 153 fixtures (150cm), driven by a Python script rather than the manufacturer app's own static timeline editor. Intensity follows a real solar-elevation curve computed for a reference Coral Triangle reef (Raja Ampat, Indonesia -- consistent ~12h day length, no daylight-saving shifts), then time-shifted so sunrise and sunset land at sensible local hours rather than Indonesia's own clock. The live cloud-cover forecast is averaged across two Coral Triangle reference points (Raja Ampat and Bunaken, Indonesia, about 700km apart) for a steadier reading than either location alone, and tolerates one of the two being unavailable. Both fixtures' web interfaces went silently unresponsive after several days of otherwise normal operation -- traced and fixed at the firmware level -- full writeup: ATI Straton Flex Reliability Notes. On top of that:
Live cloud cover from that same reef's real weather forecast dims the tank on overcast days, floored so a fully overcast day doesn't black it out -- real diffuse light still reaches a reef underwater too.
A moon-phase-driven night cycle replaces a flat "lights off" floor, brighter near full moon and dimmer near new moon, with a printed notice during the commonly-cited post-full-moon window some SPS species use as a spawning cue.
Spectrum blends smoothly through the day -- no discrete preset jumps anywhere, including the moon-to-daylight transition -- built around the ~470nm coral fluorescence excitation peak reported in the coral photobiology literature (Bay et al. 2010), with extra UV/violet at twilight eased back at midday.
Hard-capped at 75% of the fixtures' maximum output, and the whole schedule recomputes every 15 minutes via cron rather than running a fixed program once a day.
The fixture's own closed-source web application has since been replaced on one unit with a from-scratch, independently-built rewrite, running as a live trial -- full writeup: ATI Straton Flex Firmware Rewrite.
● Master fixture last updated 2026-10-10T19:45:02+00:00 ● Secondary fixture last updated 2026-10-10T19:45:04+00:00
Today's Lighting Curve
The planned 0-75% intensity schedule for today, recomputed every 15 minutes from real solar elevation, the live cloud forecast, and moon phase -- this is what's actually queued up to run, not a measurement (see the PAR section below for that). Same curve drives both fixtures. The dashed line shows what today would look like with zero cloud cover, so the gap between the two lines is exactly what clouds are costing right now.
2026-10-10:
7-Day Lighting History
Real logged history (one reading every 15 minutes, not a forward-looking plan like the chart above) of the actual commanded intensity vs. what it would have been with zero cloud cover, over the past week. A wide gap between the two lines means heavy cloud cover that day; the lines converging means clear skies.
Coloured stretches: the weather forecast service was unreachable for those runs, so the schedule used a fallback -- cached forecast.
Current Spectrum Mix
The actual LED channel blend queued for right now (21:45 local), each channel shown as % of its own maximum output and already scaled by the real-time intensity curve (0% right now -- this includes live cloud-cover attenuation, not just the time-of-day shape). This is what's commanded, not a spectrometer measurement -- see the Lighting section above for how the day's color blend is computed (UV/Violet pushed harder at twilight, eased back at midday around the RB/B/LC/W fluorescence band).
0%
UV 405nm
0%
Violet 420nm
0%
Royal Blue 450nm
0%
Blue 470nm
0%
Cyan ~500nm
0%
White broad
0%
Red ~660nm
Synthesized spectral curve from the channel percentages above -- each narrow-band channel modeled as a peak at its real wavelength, White as a blue pump + broad phosphor hump (typical of phosphor-converted white LEDs). Illustrative, not a spectrometer trace.
Measured Light Output (PAR)
Neither fixture has an actual photometric sensor. What's below instead comes from the fixture's own real measurement of LED driver current draw, run through ATI's own factory-calibrated current-to-PAR conversion at four fixed distances -- the exact same calculation the manufacturer's own app displays in its status bar, traced out of its source and pulled here directly. Treat it as a real, continuously-logged proxy for output good enough to catch genuine trends, not a lab-grade PUR reading: it's a per-single-lamp figure (ATI's own tooltip warns combined output from both fixtures together is "significantly higher"), and it's calibrated against ATI's factory spectrum rather than this tank's custom channel mix.
Latest reading per fixture
Fixture
Driver draw
Est. PAR @30cm
@45cm
@60cm
@75cm
Master fixture
5 W
15
10
8
6
Secondary fixture
5 W
15
10
8
6
Estimated PAR @60cm, both fixtures, last 7 days:
The System
Protein skimmer
Foam fractionation is handled by a Deltec 1000i in-sump protein skimmer, rated for aquariums of 600 to 1000 litres, which suits this 750L system. It is the export half of the nutrient control described above: the bacteria that grow on the vinegar are removed with the skimmate, so nitrate and phosphate actually leave the system instead of just moving from one form to another.
Heating
The system is heated by a 3kW inverter heat pump of the kind normally sold for swimming pools, not a conventional aquarium heater. Its condenser (the part the tank water flows through) is made of titanium, which does not corrode in salt water. An inverter heat pump varies its compressor speed instead of switching fully on and off, so on a 750L system it can hold the temperature steadily with small, gentle adjustments, and it moves several times more heat than the electricity it uses. A pool unit is far larger than this tank needs, so it spends most of its time running at low output. The setpoint is 25°C.
Power backup
The tank equipment runs behind a Victron inverter/charger with a bank of three 100Ah LiFePO₄ batteries (300Ah in total). Mains power normally passes straight through while the charger keeps the batteries topped up. If the mains fails, the inverter takes over from the batteries within moments, so the return pumps, one in-tank flow pump and the protein skimmer keep running through an outage. Those three are the ones that matter most: without the return pumps the sump and display stop exchanging water, oxygen and heat, the flow pump keeps water moving around the corals so oxygen still reaches them, and the skimmer keeps exporting nutrients and adding aeration.
The heat pump is deliberately not on the battery backup, so heating stops during a mains failure. With 750 litres of water the temperature drifts only slowly over a short outage, and the circulation and aeration that keep the animals alive carry on regardless.
LiFePO₄ (lithium iron phosphate) was chosen over lead-acid because it tolerates deep discharge, lasts for thousands of cycles and holds a steady voltage until it is nearly empty, which suits a bank that mostly sits fully charged and is only occasionally called on.
● Mains present, battery on standby — battery 100% (13.8V), 139W currently drawn from mains.