Field Notes

Engineering notes from real deployments of the EDGRON platform. Dated, honest, occasionally embarrassing. That is what hardware development actually looks like.

2026-09-12 · Auckland, New Zealand

Twenty-eight hours of flat pH, and the number the loop could not learn

Yesterday's post ended with the new dosing loop switched on at 14:14. This is what the first twenty-eight hours look like, what the loop learned, what it could not learn, and the two changes that went to the greenhouse controller tonight because of it.

Two stacked line charts, 10 to 12 September. Top: reservoir pH falls steadily from 5.89 to 5.59 over the first day under the old loop, jumps to 5.85 after a 20 ml pH-up shot by hand at 12:22, and from 14:14 on 11 September, when the self-learning loop starts, holds between 5.82 and 5.87 for the next twenty-eight hours. Bottom: EC sits between 1290 and 1300 under the old loop, then between 1300 and 1310 under the new one, with three sharp dips to about 1280 at float-valve top-ups that recover within minutes
Same reservoir, same probes. Left of the dashed line the pH is managed by hand; right of it the loop doses 2 ml of pH-up every hour or so and the reading does not leave a 0.05 window. The three EC dips are the float valve adding rainwater.

The numbers

The day before the switch, pH fell from 5.89 to 5.59, a quarter of a unit, because this fertiliser recipe pushes it down and nothing was pushing back except me. In the twenty-eight hours since, the one-minute readings have all been between 5.82 and 5.87, standard deviation 0.007 pH. That is one minimum shot wide, which is what the design said it would be: the loop fires 2 ml whenever its running estimate says the tank is about 0.03 short, and 2 ml moves the tank about 0.03. EC held 1300 to 1310 against a target of 1300, standard deviation 3 µS/cm. Twice today the float valve added three and a half litres of rainwater and EC dropped to 1280; both times the loop had it back above 1295 within four minutes of the drop, because it fired the moment the reading came in rather than waiting for the next scheduled decision.

The loop also learned. The A/B gain it started with, 14 µS per 50 ml of each part, was measured on the old liquid fertiliser; after nine shots it had moved the stored value to about 20 µS per 50 ml, which is what the dry-salt mix actually does. Individual shots are noisy, a 16 ml shot moves the reading by five to eight counts on a one-count meter, but the two larger shots it fired after the top-ups landed on the same number as the average, so I believe it. The EC drift it learned, about 70 µS per day of plant uptake, matches the daily A/B total to within a few per cent.

The number it could not learn

The pH-up pump's gain never changed from its starting value, and it took me an hour of staring at the event log to see why. The loop only trusts a shot as a measurement of the pump when the expected movement is at least 0.04 pH, four counts, because the probe's last digit is noise. But the loop, doing its job well, never needed more than 2 ml at a time, and 2 ml is expected to move 0.026. Every shot was below the threshold. Meanwhile the drift estimator saw "pump fired, reading did not move" and booked the whole thing to drift, which crept from the seed value of 0.15 pH per day to 0.43 per day and was still rising when I looked.

The control result is fine either way; the tank does not care which of the two numbers is right as long as their ratio is. But the two are not separable from minimum-sized shots alone, and the wrong split has a real consequence: I told you yesterday the litre of working solution would last half a year at 12 ml a day. The pump has actually delivered 35 ml a day, so it is two months. Either nitrification really did speed up when pH went from 5.6 to 5.85, which is plausible, or a two-second pump pulse delivers less than the calibration says, which is also plausible. I do not know yet, and the loop as written would never find out.

Two changes, pushed tonight

The first is a probe shot. When five consecutive pH shots have been too small to learn from, the next one that was going to fire anyway is enlarged to the smallest size the learner accepts, 4 ml at the current gain. No extra shots, one slightly bigger one every seven hours or so, an overshoot of 0.03 at most, and in return a real measurement of the pump. The first one should land tomorrow morning.

The second closes the top-up gap properly. The controller already counts the water meter on the fill line. From tonight, every 0.1 litre the float valve lets in is converted into an EC debt, volume times target divided by tank volume, and paid back through the learned A/B gain as soon as it adds up to a minimum shot and the pumps are free. A three-and-a-half-litre fill owes about 70 ml of each part; the pumps start on it while the valve is still open and finish in about three minutes, instead of the loop noticing a dip and chasing it afterwards. The main loop holds its decision while the valve is open and restarts its settling clock after each compensation shot, so the two cannot both pay the same debt. The tank volume became a setting; the daily A/B cap went up because the compensation rides the same counter.

Honesty section

Twenty-eight hours is twenty-eight hours. The flat line is real and it is the best this reservoir has ever looked, but the learned drift is a number I have just told you is probably wrong, and the top-up code has not seen a top-up yet. The learned values survived tonight's firmware swap and reboot intact, which was the one thing about persistence I had not yet tested. I will report the first probe shot and the first automatic fill compensation when they happen, including if they do something silly.

2026-09-11 · Auckland, New Zealand

A fourth pump, a wrong prediction, and a dosing loop that now learns

Three things today. The controller got a pH-up pump, which I had said two weeks ago it would not need. The dosing loop was rewritten from "wait for the edge of the band, then fire" to a loop that estimates the drift, doses ahead of it, and measures its own pumps. And I pulled five plants to look at their roots, because a shop page said falling pH means root disease. It does not, here.

The prediction that was wrong

On the 29th of August I worked out from the bottle label that the liquid A/B fertiliser carried a lot of its nitrogen as ammonium, which is why the reservoir pH fell every day. The plan was to switch to dry salts and let the pH float back up on its own, as nitrate-fed lettuce normally does. The dry salts went in last week and the pH kept falling, 0.1 to 0.2 a day. The reason is on the second bag: the potassium-side product lists 15 % nitrogen as 11.3 % nitrate and 3.7 % ammoniacal. Mixed with calcium nitrate that is about 16 % of the nitrogen as ammonium, which is above the roughly 10 % where the net effect flips from pushing pH up to pushing it down. I had checked one bag and assumed the other.

So the direction a reservoir drifts is set by the fertiliser recipe, not by the water or the crop, and a controller that only has an acid pump is half a controller. That is a product lesson as much as a farm one: the pH channel has to be bidirectional, and which pump does the daily work depends on what the grower buys.

The fourth pump

A fourth peristaltic pump went onto output 4 of the relay module, fed from a bottle of potassium hydroxide "pH Up" diluted about 1:30. The concentrate is 48 % KOH, strong enough that a millilitre of it would overshoot the whole tank, so it is diluted first and the pump only ever sees the working solution. The first shot was 20 ml by hand at 12:22, with the tank at 5.59: the pH rose to 5.85 and levelled out in about twelve minutes, and EC did not move. That one measurement, 0.013 pH per millilitre, became the pump's starting calibration.

Line chart of reservoir pH from 12:00 to 16:00. Flat at 5.59 until 12:22, then a sharp rise to 5.85 over ten minutes after a 20 ml pH-up shot. A slow decline to 5.81 by 14:00, a small step up at 14:28 when the float valve tops up the tank with rainwater, and another small step at 15:04 when the controller makes its first automatic 2 ml shot. The 5.80 to 5.90 target band is shaded
The 20 ml hand shot at 12:22 is the calibration. The two small steps later are a rainwater top-up and the loop's first automatic shot, 2 ml.

A loop that doses ahead of the drift and measures itself

Until this afternoon the dosing loop was the simplest thing that works: if the value sits outside the band for five minutes, fire a fixed shot, wait twenty minutes for mixing, look again. It is safe and it is easy to reason about, but the ripple is the width of the band by construction, and a fixed shot is either too big for a small error or too small for a large one. The new loop does three things differently.

It works in cycles of twelve minutes, the settling time measured this morning, and makes one decision per cycle. The decision starts from the error, target minus reading, and adds a feed-forward term: the drift it expects over the next cycle, carried over from cycle to cycle until it is worth a shot. The shot size is that need divided by the pump's gain, so a 0.03 error gets 2 ml and a 0.1 error gets 8 ml. Nothing fires until the computed dose reaches a minimum, 2 ml for the pH pumps and 15 ml for A and B, which puts a floor under the ripple of about 0.03 pH rather than the 0.1 to 0.2 of the old band. Only one variable is dosed per cycle, EC first, so that when the next reading comes in the loop knows which pump to credit.

That credit is the learning. After each shot the loop takes the change over the cycle, subtracts what the drift alone would have done, and divides by the millilitres. That is a fresh measurement of the pump's gain, and it is blended into the stored gain, 30 % per observation, clamped to a fifth and five times of the starting value so a stuck probe cannot teach it anything absurd. Drift is learned the same way from three-hour windows, from the residual once the shots are accounted for. A cycle in which the float valve added water, or somebody poured something by hand, or the circulation stopped, is thrown out. Three consecutive shots that should have moved the reading and did not is a fault and the loop switches itself off: empty bottle, tube off the pump, dead probe.

Two rules came from the person who runs this farm, and both are right. The learned values are written to flash every time they change, so a reboot does not start from ignorance; and the starting values are the measurements already made here, not textbook guesses. The first learning event landed at 15:53: an 18 ml A/B shot moved EC by 6 µS/cm where 5 was expected, and the stored gain moved from 0.28 to 0.31 µS per millilitre. Every one of these is a line in the event log, so the chart will show what the loop believed and when it changed its mind.

Honesty section: the roots

The reason I was reading a shop page in the first place is that it said a falling pH "could be root diseases attacking your plants". With the water having been above 24 °C on three afternoons this month, that deserved a look rather than an argument. Five plants came out. The roots are cream to tan, not white, which is what a month in nutrient solution with chelated iron does to them, and the tests that matter all passed: distinct strands that do not slough off when you run them through your fingers, new white tips, root hairs along the new growth, and no smell. The water consumption has held at 6.5 to 7.5 litres a day all week, which is the other thing rotting roots do not do. The drift is the fertiliser. The air pump from Monday is still the right precaution.

2026-09-08 · Auckland, New Zealand

The blocked gutter, an air pump, and a rule about water temperature

Three small things today, each of which is the kind of thing a farm system is actually made of: a flow meter that had been telling the truth for a week while I explained it away, an air pump that got its own rule, and a fertiliser bottle that ran dry on paper before it ran dry in fact. All of it is in the public data.

Gutter 3 was never slow. It was half blocked

Each of the four NFT channels has a small pulse flow meter on its feed line. For a week the dashboard showed gutters 1, 2 and 4 at 1.4 L/min and gutter 3 at 1.2. When I measured them with a jug on the 4th, gutter 3 came out at 1.0. I wrote that down as the true value and moved on, as if the meters had personalities. This morning I put a hand into the feed end of gutter 3, pulled out a wad of root and leaf that had drifted down from the lettuce, and the meter went to 1.4 within a minute. Same as the other three. The 1.0 in the jug and the 1.2 on the chart were the same blockage seen at two stages of growth.

Line chart of four gutter flow meters from 06:00 to 11:50. Gutters 1, 2 and 4 overlap on one grey line at 1.4 L/min. Gutter 3, in orange, sits at 1.2 all morning, dips to 1.0 at 11:22 when it is cleaned, then rises to 1.4 and stays there
Gutter 3 (orange) at 1.2 L/min all morning against the other three at 1.4. The dip at 11:22 is my hand in the channel. After that all four meters agree.

Two lessons. The clean baseline for this pump and these channels is 1.4 L/min on every line, so from now on anything below that is a blockage question, not a meter question. And the blocked-line alarm, which fires when a gutter stays under 1.2 L/min for two minutes, never fired, because 1.2 was exactly where the blocked gutter had settled. I had set that line from the jug numbers, and the jug numbers already had the blockage in them. The line can move up now that I know what clean looks like.

An air pump, and why it needed a rule

Looking down into the blue reservoir barrel: a stream of fine bubbles rising from an air stone on the bottom, next to the white float valve
The air stone on the floor of the reservoir barrel.
A dark blue aquarium air pump on a wooden shelf with two clear tubes leaving it, mounted next to the three blue peristaltic dosing pumps
The pump lives next to the dosing pumps. It is a mains pump, so it is switched through a small interposing relay in the cabinet.

An aquarium air pump and an air stone went into the reservoir today, switched by output 9 of the relay module. The first version of the control logic was "on". That lasted about an hour, until I thought about what happens after a reset. This controller resets itself when it decides its network stack is wedged, and it has done so twice in the last week. Outputs come up off after a reset, which is the right default for pumps that dose acid, and the fan, and the roof. But nothing in the software knew the air pump existed, so nothing would have turned it back on. The pump would have stopped one night and stayed stopped until somebody looked in the barrel and wondered where the bubbles went.

So the pump got a rule, the same shape as the roof vent and the dosing loop. It runs when the water is at or above a set temperature for one minute, and stops when the water has been a degree below that for five minutes. The first valid reading after a boot decides immediately, so a reset cannot leave it silently off. If both water temperature probes are lost for ten minutes it runs anyway, because bubbling a cool tank costs nothing and not bubbling a warm one costs roots. The set temperature is a setting, not a constant, and it is logged when it changes.

I set it to 20 °C. The usual target for nutrient solution is 18 to 22, and the organisms that cause root rot get going in earnest above 24. Dissolved oxygen falls as water warms while the roots want more of it, so a warm tank is the one that needs air. Aeration does not cool anything; if the tank ever sits above 24 in summer the answer is shade and cooling, not a lower setpoint. Starting the pump at 20 just means it is already running by the time the risk begins. Today the water was 22.7 when the rule went live, so the pump has been on since. It should switch off some time after midnight when the tank drops under 19, and back on tomorrow morning. One prediction to check: bubbling strips dissolved carbon dioxide, so the pH should creep up a little while the pump runs. With a fertiliser that pulls pH down all day, that would be welcome.

The bottle that ran out on paper

The fertiliser stock on the dashboard is a ledger, not a sensor: each refill I log is a starting volume, and every dose the controller makes is subtracted from it. Last night the ledger for part A reached zero and the phone said so. The bottle was not actually empty, because I had topped it up the day before and never told the system. This morning it got 1.5 litres and the log entry to match, and the ledger says four days at the current rate. The refill was logged from a new box on the dashboard itself, from the phone, standing next to the barrel. Until today that took a script on the laptop, which is exactly why the entry was missing. The same box takes free-text notes, which is how the gutter cleaning got its timestamp on the chart.

Also today

The air pump is the reason a new module went on the drawing board this afternoon. The existing 16-channel output module switches 24 V with solid-state outputs, which is right for valves and small pumps and wrong for anything on mains. Today that gap was covered by one interposing relay on a DIN rail. A 16-channel relay output module that fits the same housing family is now being designed. More on it when there is a board to photograph.

2026-09-07 · Auckland, New Zealand

Four probes in a bucket, and the alarm that sent me back to the greenhouse

Two days ago I promised to put all four temperature probes into one bucket of water and report what they said. I did, and one of them was wrong enough to change a conclusion. Later in the afternoon I knocked a conductivity probe out of the flow cell without noticing, and the system noticed for me. Both stories are in the public data.

The bucket test

At 12:34 all four PT100 probes went into the same bucket. I had checked the water with a separate thermometer: 21.7 °C. The two flow-cell probes read 21.5 and 21.6. The reservoir probe read 22.0. The inside-air probe, the one behind the "colder inside than outside" paragraph, read 20.7. A spread of 1.3 °C between four probes of the same type is not what I guessed on Friday. I said they would agree within 0.3 and that the night-time inversion was real. Half right. The air probe was reading a full degree low, so the 2.1 °C inversion I reported was really about 1.1. The effect is still there, the film still lets heat radiate out on a clear night, but it is half the size I claimed. The paragraph on the 5th stays as written, because that is what I believed at the time; this one is the correction.

The fix is a one-line offset per channel. Each probe got told "the water is 21.7" and the module worked out its own correction and saved it. Afterwards all four read 21.7 or 21.8. Then the two flow-cell probes went back to their places and settled 0.25 °C apart, which they had not been in the bucket. So the flow cell has a small real gradient between the two positions. I nudged them to their midpoint anyway, because two water temperatures on one dashboard that disagree by a quarter of a degree make people distrust both. That is cosmetic and I am saying so. The absolute reference is the bucket, not the nudge.

16:05: probe out, pumps held, phone rings

Working in the tunnel in the afternoon I caught the cable of EC probe 2 and lifted the electrode out of the flow cell. I did not notice. The controller did, in six seconds. With the electrode half in air the reading fell to 839 µS/cm against 1292 on probe 1, the two disagreed by more than the allowed gap, and the dosing loop went on hold. Six seconds later the module declared probe 2 invalid outright and stopped publishing it at all, which is why the orange line simply disappears in the chart. From that point the loop was back to running on probe 1 alone, the designed fallback when one probe is bad rather than merely different. It dosed once at 16:21 after the float valve had topped the tank up with 3.5 L. That top-up was real; the water meter counted it.

The phone alarm came at 16:15, ten minutes after the fault. That delay is on purpose. An invalid probe has to stay invalid for ten minutes before anyone gets paged, otherwise rinsing an electrode or swapping a bottle would ring the phone every time. I was walking out of the greenhouse, looked at the message, went back in, and there was the electrode hanging in the air. Back in the water at 16:35 it read 1366 for the first sample, an air bubble in the electrode cavity, then 1284 a minute later, seven µS/cm from probe 1. Alarm cleared at 16:37. I should have taken a photograph. The chart will have to do.

Two panels. Top: four temperature probe traces converging when placed in one bucket at 12:34, corrected to 21.7 °C at 12:40, then diverging again as they return to their places. Bottom: EC probe 1 steady around 1290 while EC probe 2 drops off the chart at 16:05, stays blank for thirty minutes, spikes when reinserted at 16:35 and settles next to probe 1.
Top: the bucket test. The yellow inside-air probe arrives a degree low, all four are corrected at 12:40, and the reservoir probe (purple) goes back to its cooler tank. Bottom: probe 2 leaves the water at 16:05 and nothing is published for it until it is back at 16:35. The blue line dips at 16:17 because the float valve added 3.5 L of rainwater, not because of the probe.

Honesty section: the day the phone was quiet

The reason I checked the phone at all is that it had been too quiet. The morning report had not arrived, and neither had the stock warning for the fertiliser bottles that I knew were nearly empty. The server log said both had been sent. They had not. Two days earlier, adding a small feature to the notification settings, I rewrote a configuration file and dropped one line from it, the line that says where the messages go. Nothing changed at the time because the running process still had the old setting in memory. At a restart the next night it reloaded the file, the destination was blank, and from then on every message was written to the log and sent nowhere. There was no error, because sending to nowhere is not an error. Fixed in the morning, and the bottles got their 15 litres of B in the afternoon. The lesson is old: after changing a config file, check what the running process actually has, not what the file says. The absence of a message is the only symptom you get.

Also today

The greenhouse camera spent the morning stuck in infrared mode, black and white in full daylight, so the canopy-cover number skipped two frames. A power cycle fixed it, and while it was reconnecting the cloud service handed my capture script a white "device offline" placard as if it were a video frame. The script accepted it. It now checks for near-white, colourless frames and throws them away.

At 15:08 the controller lost its cloud connection for thirty minutes and reset itself, which is what it has been told to do since the August incident where a stuck network stack kept the board silent for hours. It was back in under a minute, the vent logic re-read the air temperature on boot and reopened the roof to full. The dosing loop waited its twenty-minute grace period before making any decision. Nobody in the greenhouse would have noticed, and that is the point.

2026-09-05 · Auckland, New Zealand

A slow probe, a narrower band, and the coriander goes back to soil

Nothing broke today. This is what most days look like once a system is running: one sensor is getting worse, one setting needed changing, one crop didn't work out, and one chart was giving the wrong impression. All the numbers are in the public data if you want to check them.

pH probe 2 is wearing out

Both pH electrodes were recalibrated yesterday, in the same buffers, one after the other. Overnight they read 0.05 pH apart and stayed there while the water cooled from 22 to 15.5 °C. That is what a calibration leaves behind, a fixed offset. Today was different. In six hours in the same tank, probe 1 moved 0.03 pH. Probe 2 went up 0.06 and then down 0.12. The gap has nothing to do with water temperature, because it sat still through last night's 6.5 degree drop. The electrode is just wandering. Its slope also came out under 90 % of theoretical yesterday. Put those together and you have an old glass membrane: slow to respond, drifting, and only correct at the moment you calibrate it. I am going to replace it rather than calibrate it again. The dosing loop already runs on probe 1 and only uses probe 2 to check for disagreement, so the plants don't notice.

Part of this is on me. During yesterday's calibration the reading in the pH 7 buffer was still moving by a few tenths when I accepted the point. A slow electrode needs minutes to settle and I gave it seconds. The calibration tool will get a settling check, so a point can't be taken until the reading has held within 0.01 for a minute.

Dead band cut in half

The feed loop dosed five times last night. Every dose was triggered at the bottom of the ±20 µS/cm band around the 1300 target, and EC never got back to the middle. One 50 ml shot of each part adds about 14 µS/cm, so with a 40 wide band the loop was happy to sit on the floor. I changed the band to ±10. That should mean about twice as many shots of the same small size, and the tank should sit around 1300 instead of just above 1280. The daily fertiliser cap is still there as a limit.

Colder inside than outside

Clear night, clear morning. At 07:00 the air in the tunnel was 12 °C. At 08:00 it was 22. The roof went from closed to stage 3 in twenty minutes starting at 08:20, which is the first time it has climbed that fast in the morning. What surprised me is the night. For the last two nights the inside reading has been below the outside reading all night, by as much as 2.1 °C at 05:00. There is a real explanation. Plastic film lets infrared through, so on a clear still night the ground and leaves inside radiate heat to the sky as if the film wasn't there, and the film stops the air mixing that would bring warmer air back in. It is a known effect in unheated tunnels. It is also exactly what two probes with different offsets would look like. So tomorrow all four temperature probes go into one bucket of water for twenty minutes. My guess is they agree within 0.3 °C and the inversion is real. If not, I will correct this paragraph.

Two days of dashboard panels: roof vent stage stepping to 3 in the morning, top-up flow spikes, four gutter flows near 1.4 L/min, pH with the orange probe drifting below the blue one, EC bouncing along 1280, and temperatures with the greenhouse trace dipping under the outdoor trace overnight
Two days on the public dashboard. Roof vent at the top, going 0 to 3 in twenty minutes this morning. Top-up flow is now a rate, see below. In the pH panel the orange probe 2 drifts away from probe 1 through the afternoon. EC sits along the old lower edge of the band. In the temperature panel the yellow inside-air line runs under the green outdoor line all night.

Coriander: ten days, no new roots

Six coriander plants came into the channels from soil ten days ago. None of them grew a single white water root. Coriander has a taproot and does not like being moved at all, and going from soil to water on top of that was too much. They went into the garden bed today. Next time coriander gets sown straight into the foam or not at all. In their place there is one pea seedling from a primary school science experiment. It arrived today and will need a string to climb within two weeks.

On the way past I pulled a lettuce with wilting outer leaves to look at its roots. The old soil plug in the collar was black and matted, the fine roots inside it were brown, and there was a grey fuzz on the surface. It looked like root rot. But the new roots outside the plug were white, firm and translucent, and there was no smell. Rot smells sour or like sulphur. Dead potting mix smells of nothing. Two hand tests are enough: smell it, and pinch a brown root. If the skin slides off and leaves a thread behind, that is rot. This was a waterlogged soil plug and a healthy plant. I rinsed the plug off, kept the white roots, and put it back.

Hand holding a blue foam collar with the plant's root ball pulled out: dark old soil plug with brown roots and grey fuzz, and white new roots trailing from it
Looks like rot but isn't. Black soil plug, brown roots inside it, white roots outside it, no smell.
Six small coriander plants freshly transplanted into a garden bed of dark soil beside a wooden edge
The coriander, back in soil after ten days in the water without a root to show for it.
A young pea seedling in a blue foam collar lying on the white NFT channel lid, strawberry plants in the next channel
The new pea. It came from a school project and will want a string soon.

A chart that gave the wrong impression

The water meter panel used to plot cumulative litres. That is a staircase that only goes up, and at a glance it looks like the tank is being topped up all the time. It now plots litres per minute, so a top-up is a spike and the rest of the day is a flat zero. The cumulative total is still in the raw files, which is where the daily totals come from.

Next: the four probes in a bucket, a new pH electrode, whether the narrower band actually puts EC on 1300, and a string for the pea.

2026-09-02 · Auckland, New Zealand

Pull a probe out and nothing happens. That is the feature

Today the system got attacked on purpose. Probes yanked out of the tank while the control loop watched, reboots dissected for fingerprints, and a day of numbers handed over to the person the numbers are actually for. Every step below is timestamped in the public data.

The pull test: two probes, two directions

The verdict experiment promised yesterday. Pull probe 2 out of the tank for eight minutes: the survivor moved less than 0.01 pH, which is the resolution floor. Pull probe 1: same result for its survivor. Zero electrical cross-effect in both directions, which is the number a customer should demand.

Dashboard charts of the pull test: top panel shows pH with the orange probe spiking down and the blue probe spiking up while the partner trace stays flat; bottom panel shows EC steady through both events
The pull test as the dashboard recorded it. Top: orange (probe 2) dives toward pH 5.0 when pulled, blue (probe 1) climbs toward 6.3 in its turn, and in each case the probe still in the water doesn't move. Bottom: EC through the same window, unbothered. Two personalities, zero cross-talk.

The entertaining part is what the pulled probes did. A glass electrode out of the tank keeps a wet film for minutes, and reads its own film instead of the water: probe 2 drifted smoothly down to pH 5.0, probe 1, a different brand with a different junction, drifted up towards 6.3. Two spikes on the chart, one down, one up, each electrode's personal signature, while the neighbour's trace stayed ruler-flat. That asymmetry is also a warning: a moist probe in air is electrically indistinguishable from a probe in water, so software cannot detect the pull for several minutes, and probe 1's air-drift climbed to within 0.04 pH of the acid-dosing trigger. The dosing loop now cross-checks the two probes and freezes acid dosing whenever they disagree beyond a configurable band. Configurable, because an installation that splits its probes across two tanks would find the cross-check meaningless.

Three reboots with no fingerprints

Morning drama: the controller silently reset three times in forty minutes. Not the watchdog, not a fault handler, not a commanded reboot, not a stack overflow. Every instrumented path came back clean, which was itself the alarming part. The response was to make anonymity impossible: every intentional reset in the firmware now writes its reason into battery-backed RAM before pulling the trigger, the fault handlers' black box is read out and journaled at every boot, and a reset that arrives with neither leaves the word UNNAMED in the log as a confession that something jumped the rails. The epidemic stopped the same day and has not recurred; the trap stays armed. Sometimes the deliverable is not the culprit but the forensics kit.

Meanwhile, for the farmer

The dashboard grew a stats page: daily water, feed and acid consumption computed from the journals, fertiliser stock countdown from refill notes, feed cost per day (four cents, it turns out), growing degree days, comfort hours, a red/yellow/green day status, and the local forecast. The idea is that a grower should get the morning report from one glance, not from asking an engineer to pull data. The water meter now journals every top-up with its volume, which immediately sorted a real 2.5 L refill from a calibration artifact that chemistry alone had confused. And a camera went up over the channels: timelapse first, canopy-coverage curves next. Transpiration and pixels agreeing about growth is the plan.

Fixed camera view down four NFT channels: green and red lettuce on the left, young bok choy in blue foam collars on the right, blue tape covering empty plant holes
The camera's fixed view, first day on the job: four channels, forty-odd plant sites, lettuce left and bok choy right. This exact frame, three times a day. The blue collars against green leaves make the future pixel-counting almost unfairly easy.

Next: whether the new nitrate-heavy fertiliser really pushes pH upward like the chemistry says it should, the first canopy-coverage numbers, and a week of the farmer's stats page in daily use.

2026-09-01 · Auckland, New Zealand

The greenhouse learns to feed itself

At 19:43 tonight the controller decided, on its own, that the reservoir was hungry. EC had sat below the trigger line for five straight minutes, so it ran pump A for 63 seconds, then pump B for 63 seconds, fifty millilitres of each fertiliser part, and wrote the dose to the journal as ec-low instead of manual. Twenty minutes of mixing later the curve landed where the arithmetic said it would, and an hour after that it decided it wanted one more round, and took it. Eleven days after the first seedling went into a channel, the system feeds itself.

Three blue peristaltic dosing pumps mounted through a wooden board, silicone tubing running down into a white PVC conduit
The dosing rig: three peristaltic pumps for part A, part B and acid, screwed through a board above the reservoir. Calibrated with a measuring cup and a stopwatch: 100 ml in 2 minutes 8 seconds, so 47 ml/min went into the controller and every timed shot is now a volume.
Float-valve feed line entering the blue reservoir barrel through a bulkhead fitting, with an inline brass pulse water meter and a red PVC ball valve
The top-up line got its promised water meter: a brass pulse meter between the shut-off valve and the barrel wall. Every litre the float valve lets in is now 660 pulses on a counter the controller reads.
Close-up of the brass pulse water meter and red ball valve on the reservoir top-up line
Up close: hall-sensor pulse meter, dumb as a brick, which is exactly the qualification the job needs (see below).
Four NFT channels replanted with young bok choy, lettuce and celery seedlings in blue foam collars
Succession planting went in today too. The holes the first harvest is gradually emptying get refilled, so the transpiration curve never gets a rest.
Hydroponic strawberry plant in a foam collar with open flowers and the first green fruit set
One of the four strawberries: flowering hard, first fruit set. The channels were built for lettuce; the strawberries are the long game.

Three pumps and a posture

Chemicals get a stricter contract than windows. The pumps boot off after any restart and a human has to re-arm the loop each time; one pump runs at a time; each pump has a daily cap; a single manual shot has a ceiling; and if the sensor reading is missing or implausible the loop refuses to dose at all. A probe reading air must never be allowed to conclude the tank needs a whole bottle of fertiliser. The auto rule itself is the same one I had been executing by hand for a week: below band for five minutes → one measured dose of A then B → wait out the mixing → look again. The controller just does it without sighing.

The water meter takes the stand

Its first day on the pipe, the meter did no metering and a lot of testifying. Twice today the EC chart stepped down and the software wrote top-up detected in the journal. Dilution, says the chemistry. Both times the meter had counted zero pulses. No water had entered the tank. Both steps turned out to be the measurement scale itself moving (next section), and the detector had been fooled, which is now provable, because a pulse counter on a pipe outranks any clever inference from chemistry. This is why you put a meter on every line you can afford to.

The thermometer that could only count in 0.8 °C steps

Two temperature probes in the same tank kept disagreeing by half a degree, and the disagreement wandered. Calibration chased it all day and never pinned it. The raw readings finally confessed: each channel only ever produced two values, exactly 0.8 °C apart. That is one ADC step. A PT100 on this front end moves the converter about 1.3 counts per degree, and the firmware was averaging 64 samples and then throwing the fraction away in an integer division, buying sub-step resolution and discarding it on the same line of code. Keeping the fraction fixed it: steps are now 0.05 °C and the two probes agree to a tenth. Side effect worth admitting: the old truncation had been biasing every temperature low by up to a degree, so the whole scale stepped up when the fix landed, and since EC is temperature-compensated, the EC reading stepped down ~2.5% at the same instant. That step is what the top-up detector mistook for water. One fix, one fake dilution, one acquittal by flow meter.

Honesty section: three firmware versions before midnight

Pulling one pH probe out of the tank moves the other by 0.02–0.04 pH. Small, but as a customer I would want it to be zero, so tonight I tried to make it zero. The two probes' reference contacts already sit on separate solid-state switches, so the firmware can tie one reference at a time and let each channel read purely against its own. Shipped it, pulled a probe, and the surviving channel wandered a quarter of a pH unit for minutes instead of 0.04 for a second. The physics I had ignored: the water itself needs at least one connected reference pinning its electrical potential at every instant. Naive turn-taking leaves the tank adrift every time the absent probe's turn comes around, and a glass electrode takes minutes to forgive that. Version three, an hour later, grants a measurement turn only to probes whose own readings prove they are wet. Pull one out and the survivor keeps the water pinned full-time. It is live now; the verdict experiment runs tomorrow in daylight. All three versions and both failed tests are on today's charts, timestamped.

Also today

The water-quality board was swapped for the new hardware revision, and the greenhouse gained an outdoor temperature channel in the bargain. The public dashboard now shows outdoor, greenhouse air, and two water temperatures as separate curves, which will make the vent controller's decisions much easier to audit against the weather.

Next: the pull-test verdict, the first full day of closed-loop dosing, and what a water meter says about a float valve's honesty over a whole week.

2026-08-29 · Auckland, New Zealand

Day eight: the first bowl

Eight days after the first plants went into the channels, the greenhouse fed us. Outer leaves off the bok choy and the lettuce, every coriander top, one bowl. Nothing was pulled: the plants stay in their collars and keep growing, so the same 84 crowns will be cut again in a few days. It is a small harvest and it changes nothing on the dashboard, except that from now on the transpiration curve is measuring a crop that is also being eaten.

Hands with kitchen scissors cutting outer leaves from a bok choy plant in the NFT channel
The tool of the day is a pair of kitchen scissors. Outer leaves only, cut at the base, crown untouched.
Stainless bowl full of freshly cut bok choy, lettuce and coriander leaves held over the NFT channels
One bowl: bok choy, green and red lettuce, coriander. Eight days from channel to kitchen.
NFT channels after the first harvest: bok choy and lettuce with outer leaves removed, red lettuce, coriander stubs
After the first cut. Cut-and-come-again: take the outer leaves, leave the heart, leave the roots in the film. Some of the bok choy got cut harder than it should have. Three or four big leaves left per plant is the rule from now on.
Five clumps of dormant garlic chive crowns set directly into the channel holes at the gully ends, no foam collars
New long-term tenants: five clumps of garlic chives, dug from the garden still half-dormant, ten to fifteen shoots each. A clump that size wedges into a 70 mm hole on its own, no collar needed. They sit at the downstream end of the channels, because in a year the root mass will be big enough to dam a 2 mm film.

Why chives, in a lettuce system

Garlic chives are the least demanding thing you can put in nutrient film: perennial, frost-hardy, indifferent to the pH and EC a lettuce wants, and happy with the nitrogen form that gives lettuce trouble. Cut them and they tiller; by late summer a fifteen-shoot clump is thirty or forty. They also make a useful control: they share the reservoir with everything else, so if the chives ever sulk, the water is the problem. If only the lettuce sulks, the lettuce is.

The pH verdict is postponed

Yesterday's post hung a called shot on today: pH was supposed to turn back up within 48 hours of the big fertiliser day. It did not. This morning it was still sliding, so I raised it deliberately with 0.9 g of soda ash into 165 L, predicted +0.2, measured +0.18 and +0.19 on the two probes three minutes later, which at least says the reservoir's buffer capacity is now a known number. Then the day did something the day before had not: pH drifted up through the afternoon and kept drifting up after dark, on a day that got three fertiliser doses, when the previous day with no doses at all had drifted down. Two consecutive days, opposite behaviour, and I have several candidate explanations that each fit one day and not the other. I am not going to pick one tonight. Tomorrow's overnight curve is the tiebreaker, and it gets its own post, including the fertiliser label arithmetic, once I know whether it survives contact with the data.

Two days of public dashboard curves: pH falling all through the first day, then stepped up by a base dose and drifting upward through the second day and evening; EC showing three fertiliser steps and a top-up drop; event journal below
Two days, side by side, from the public dashboard. Left day, no inputs: pH slides all day and all night. Right day: the morning base dose is the big step, the three fertiliser doses are the EC steps, and pH, with fresh fertiliser in the water, drifts up into the night. Same reservoir, same plants, opposite slope. The journal under the charts lists every input with its predicted effect, so you can check the arithmetic yourself.

Next: the pH tiebreaker, the fertiliser label, potassium bicarbonate versus soda ash, and the water meter.

2026-08-28 · Auckland, New Zealand

The ladder walks both ways

Yesterday the vent controller made its first decision: it opened a window. Opening is the easy half. A thermostat proves itself on the retreat, knowing when the heat is spent, giving ground a stage at a time, and shutting the house before the night gets in. Today it did all of that, and not the way I predicted it would.

Wrong about my own controller

The control law has two ways down. A patient one: air at or below 22 °C sustained ten minutes, drop one stage, repeat. And a blunt one: at or below 20 °C for two minutes, close everything from whatever stage you're at. Between 22 and 25 is a hold band, no climbing, no retreating.

Around midday I confidently explained that at this time of year the patient path is decorative: a late-winter evening falls through 22 to 20 faster than the ten-minute step-down can accumulate, so the blunt rule always wins and the log will just read 3->0 close. The greenhouse spent the afternoon preparing a correction. A mild, slow cooldown parked the air between 20.7 and 21.2 °C for half an hour, and the journal came back with this:

12:48  ACTION  vent 1->2  t2=25.5C  slow-cool

12:58  ACTION  vent 2->3  t2=26.9C  slow-cool

17:24  ACTION  vent 3->2  t2=21.2C  step-down

17:34  ACTION  vent 2->1  t2=21.0C  step-down

17:44  ACTION  vent 1->0  t2=20.7C  step-down

Every rung, both directions, exactly ten minutes apart on the way down. The climb was legitimate too, 26.9 °C at the second escalation, real hot air, not yesterday's evening-shaped false alarm. Day two of autonomy and the machine's log is already correcting its author's forecasts. That's the right direction for the errors to flow.

A fan joins the night shift

Second air-moving device on site: a 12 W circulation fan, hung just above the top canopy, aimed down the 8-metre axis, on its own power supply, running around the clock. It looks like an afterthought. It's doing three jobs, and the subtle one is the reason it went in.

The obvious two: mornings here start with the film dripping wet, and clear cold nights leave the air in layers, warm at the ridge, cold pooling around the bottom channel. Moving air dries the film and stirs the layers flat. The subtle one is about calcium. Still air wraps each leaf in a saturated boundary layer; transpiration stalls; and calcium, which only travels with transpired water, stops reaching the leaf tips. In lettuce that arrives as tipburn, and on a site that runs entirely on rainwater, every milligram of calcium comes from the nutrient bottle. A gentle draught that keeps the leaves barely trembling is the cheapest tipburn insurance there is. Acceptance test: stand at the far end, hold a hand at canopy height, feel for the breeze. Tomorrow's film is the first audit.

Small white circulation fan hung above the top NFT channel
The night shift: 12 W of horizontal airflow above the top canopy, running 24/7. Its job is to make sure the air, the film, and the leaf tips never quite reach equilibrium.
View down the greenhouse: four tiers of NFT channels with lettuce, bok choy and coriander, vent actuator on the far wall
Down the axis the fan blows: four tiers, 84 salads plus the newcomers, vent actuator on the far wall. One end moves the air, the other end lets it out.

The float valve keeps its own diary

The top-up auto-detector logged two entries today: a −25 µS step at 09:21, about four litres, and a −16 µS step at 18:14, another two and a half. Add the 3 a.m. signature from two nights ago and the picture is complete: the valve quietly replaces six-odd litres of transpiration a day on its own schedule, morning, evening and small hours, the same silent mechanism that stands accused in the short-dose case. The detector can now spot every refill from conductivity alone; what it can't do is measure one. The pulse water meter stays on order, and every diary entry the valve writes is another line in the argument for it.

Honesty section: that called shot is still in the air

Yesterday I wrote that the pH drift was "already turning". It turned, wobbled, and kept sliding: the morning rise to 5.87 didn't hold, and the day closed at 5.84 on one probe, 5.81 on the other. Before anyone scores that a miss, the window isn't shut. The heaviest fertiliser day of the week was yesterday (500 ml a side), so the 24–48 hour ammonium clock runs to tomorrow afternoon; and the slide is decelerating hard, from −0.07/day overnight to −0.02 today, which is exactly what a finite fuel burning out looks like. The absolute level, for the record, is textbook. 5.8 is where lettuce wants to live. What's on trial is only whether the trend stops on schedule.

So, in public, the tripwires: if pH breaks 5.70, or the slide steepens instead of flattening, the mechanism goes back on trial and I'll write up whatever the re-investigation finds. Otherwise tomorrow's entry gets to say "landed" and mean it.

Postscript, same evening: the deceleration was fake

A few hours after this entry went up, a closer read of the minute-level curves broke the paragraph above. Both of today's upward pH moves, the morning rise I was ready to credit to the crop and the evening rebound, align to the minute with the float valve's two top-ups. Both probes step together within a sample or two of the EC drop, sharply, and then hold. Two independent electrodes don't hallucinate the same step at the same minute twice in one day: that's real chemistry. The top-up water carries alkalinity.

Run it backwards through the acid calibration's buffer capacity and each refill delivered roughly 0.6–1 meq/L of carbonate, which pure rain doesn't have. Somewhere in storage this water picks up lime: the tank, the roof, mortar in between. A titration of a tank sample goes on the list. (Silver lining: lime travels as calcium bicarbonate, and on an all-rainwater site a little free calcium is a gift, see the tipburn section above.)

Strip those two bumps out and the underlying slide is still running about −0.09/day, same as last night. Not decelerating. The "finite fuel burning out" evidence is withdrawn; the mechanism window (tomorrow afternoon) and the 5.70 tripwire stand unchanged. And the model gains a standing term: every litre the crop transpires comes back, on the valve's own schedule, as a small dose of alkali. The reservoir ledger just grew a column nobody ordered.

Two days of public dashboard curves: pH and EC panels with two top-up steps aligned, vent stage ladder, and the event journal listing the auto-detected top-ups
The evidence, exactly as the public dashboard shows it: on the right-hand day, both small pH up-steps sit on the same minute as the EC drops from the two auto-detected top-ups (listed in the journal below the charts, next to the vent ladder's ACTION entries). The "recovery" had a plumbing signature, not a biology one.

Next: the fan's first night versus the condensation, the pH verdict, a tank-water titration, and the water meter's arrival.

2026-08-27 · Auckland, New Zealand

The greenhouse closes the loop

For its first week on site, the controller was allowed to watch but not touch: instrumentation only, every decision made by a human with a jug. Two entries ago we promised a vent actuator. Today it went in, and by mid-afternoon the controller had made, and journaled, its first autonomous decision.

The actuator, and why "off" means "closed"

The hardware is a 24 V linear actuator on the end-wall vent. It is the spare arm from a double-swing gate opener kit that only ever had one gate to open, cost to this project: zero. It sits behind a small driver board that accepts three exclusive stage inputs from the relay expansion module: assert one output and the window drives to that opening and holds; assert none and it closes. That mapping was chosen for what it does when things go wrong. The relay module carries a 3-second safe state. Lose the controller, lose the backplane, lose power to the logic, and the outputs clear, so the window closes itself. The screw drive self-locks with no holding power, which doubles as a free night latch. Fail-safe by wiring, before a single line of control code.

24V linear actuator mounted on the hoop house end wall
The vent actuator on the end wall. Three relay outputs, three openings; all-off = closed. If the controller ever stops talking, physics and a safe-state timer shut the window.

A thermostat with a ladder, not a switch

The control law is a pure temperature ladder on greenhouse air: open one stage after 25 °C holds for three minutes; sit at each stage for ten minutes and judge the cooling slope. If opening didn't actually cool, climb a stage; jump straight to stage two past 30 °C; step back down through 22, close fully at 20. Sensor lost for ten minutes? Close and raise a fault. Deliberately absent: a clock. Night close falls out of the physics as the air cools, which means no timezone code, no daylight-saving edge case, nothing to misconfigure.

Every stage change writes an event into the on-board journal with the temperature and the reason, which means every decision shows up as a flag on the public dashboard, next to the curves that caused it. The called-shots policy now applies to the machine itself.

First decision

The firmware went out over signed dual-bank OTA in the afternoon, site running, nobody driving to the greenhouse. The reboot closed the window (fail-safe doing its job), and nine minutes later the journal read:

ACTION  vent 0->1  t2=26.0C  open

The nine minutes are the interesting part. Air sat at 25.3 °C right after boot, above the threshold, and the controller refused to act, because the reading wobbled below 25 before the three-minute hold completed. It opened only when the heat was sustained and real. That hold was designed on paper to stop hunting; watching it decline a marginal trigger on live data was the first sign the tuning is roughly right.

Roughly. Honesty section: through the slow natural cooldown of late afternoon, the ladder climbed all the way to stage three. The slope criterion can't yet tell "opening isn't working" from "evening is coming", so it kept escalating at a time of day when more airflow was harmless but pointless. The windows still closed on schedule as the air fell through 20 °C. Next tune: escalation will also require the air to still be hot, not merely cooling slowly. One constant, one line.

The dose that came up short, and what it turned out to measure

Morning dosing had its own detective story, still open. First dose of the day: predicted +70 µS, curve stopped at +44 and never grew a tail. A second, identical dose an hour later hit +66, so the calibration constant is fine, and the first dose genuinely lost something. Two suspects remain. One is the float valve topping up on its own schedule at exactly the wrong moment: the same data sweep found its fingerprint elsewhere, a clean −15 µS step at 3 a.m., overnight transpiration being quietly topped back up, and roughly four litres mid-dose would explain the gap to the litre. But doses go in at the greenhouse return line, nowhere near the barrel, so nothing we did could have tripped it; if it opened, it opened on its own clock, and that makes the case circumstantial. The other suspect is simply a weak pour. The verdict waits for hardware:

Another called shot, already landing

Yesterday was a heavy fertiliser day, and overnight the pH slid steadily, the fertiliser's ammonium and phosphate acidity outrunning the crop's usual alkaline drift from nitrate uptake. This morning we posted the mechanism and a prediction on the public chart: the drift reverses within 24–48 hours as the ammonium is consumed. By afternoon it was already turning. Zero-buffer rainwater makes the chemistry fast, but it also makes it legible.

Root verdict, and a tenant who likes it hot

Spinach plug pulled from the channel showing new white roots radiating from the old dark root ball
Transplant verdict, one plug of many: the dark core is the old soil-grown root ball, retired; the white spears radiating out of it are new water roots. The conversion is working, plant by plant.
Mature pepper plant in a foam collar in the top NFT channel
Today's transplant gamble: a mature pepper, root-washed into the top channel. It thrives at exactly the temperatures that make lettuce sulk. The vent controller now serves two constituencies, and the cold nights are its test.

A week after the collar-and-cover retrofit, the channels are also telling a quieter story: the film is running clear, and there's no algae where the light used to leak in. A few dollars of pool noodle continue to outperform their price class.

Next: the stilling well, the water meter, fan stages on the same ladder, and EC's arrival at 1.4.

2026-08-26 · Auckland, New Zealand

Called shots, public notes, and strawberries in the spare holes

Dosing day. Five nutrient doses between breakfast and dinner, EC walked from 0.82 up to 1.02 mS/cm on its way to a new 1.4 target, and every single dose was predicted before it was poured. The +14 µS per 50 ml slope from last week's calibration has now held from 50 ml doses all the way to 280 ml, five for five, most landing within a couple of µS of the forecast.

The event journal is now public

Until today the operator's notes on what was dosed, when and why stayed in the private view, and a visitor watching the public dashboard saw EC steps appear out of nowhere. That's fixed: the on-board event journal now shows on the public curves, flags and all. When we pour, you see the note; when the float valve tops up, the auto-detector writes its own.

Which enables a new sport. The afternoon doses were logged with the prediction in the note, before the dose hit the water: "baseline EC 925, predict +28 to ~953". Two minutes later the curve stopped at 952. The 280 ml evening dose was called at ~1030 and landed at 1024, with the gap expected to close overnight as settled solids dissolve out of the dead zone (the suspension-type concentrate drops a little solid ballast; it re-dissolves slowly once diluted, and we've learned to treat the few-µS creep after each plateau as part of the dose, not a mystery). If you want to audit us, watch the next flag: the number is written down before the curve moves.

Acid respects no calibration, because rainwater has no buffer

The site runs entirely on rainwater, which means zero carbonate alkalinity: nothing in the water pushes back against acid. Back at pH 6.8, 50 ml of dilute acid moved the reservoir −0.3. Today at pH 6.3, 30 ml did −0.33, the same result for barely half the dose. Dose response roughly doubles as pH falls, because what little buffering exists (mostly the phosphate in the nutrients) thins out on the way down. The working rule is now: whatever the calculation says, pour half, wait ten minutes, look at the curve. The calculation said 80–90 ml to reach 5.8; the halved first pour alone landed us at 5.95.

Strawberries in the spare holes

The channels have more holes than the salad crop needs, and it's bare-root strawberry season here. So four plants came out of the garden bed, got their roots washed completely clean of soil, the one non-negotiable, since soil in a shared reservoir is both a clog and a pathogen inoculum. They went into the bottom channel with the usual foam collars, crowns kept high and dry above the film.

Strawberry plants with foam collars in the bottom NFT channel, salad greens above
Four garden strawberries starting their hydroponic careers in the bottom channel. Soil roots will die back and be replaced by white water roots over the next two weeks. The droop in the meantime is expected, and the reservoir chemistry (pH 5.9, EC heading for 1.4) happens to sit inside strawberry range without changing a thing.

It's a transplant gamble, and we're treating it as one: two weeks of watching for the soil-to-water root conversion, with the greens upstream as the healthy control group. If it works, spring flowers meet a monitored reservoir. If it doesn't, it'll be in these notes too.

Next: the pH night-drift check decides the second acid dose, a vent actuator with a cumulative-exposure setpoint, and strawberry watch, week one.

2026-08-25 · Auckland, New Zealand

Week one: dose curves, a dead zone, and a few dollars of pool noodle

The first entry ended with an open question and 72 freshly planted seedlings. One week in, the question is answered, the reservoir turned out to have a secret, the lettuce held a vote on temperature, and the cheapest part on the whole site fixed a real problem. In order:

The open question, answered

Last time: overnight EC declined in step with air temperature, more than probe temperature compensation should allow. Instrument drift, or something real? The test we were waiting for was the morning warm-up. Drift retraces, biology doesn't.

The answer: air climbed from 11 °C back to 19 °C and EC did not retrace. The decline accelerated, from about −0.5 µS/cm per hour in the middle of the night to −2.5 once light hit the canopy. That's not a coefficient problem; that's plants drinking, on a light schedule. The probes were telling the truth the whole time. No settings were harmed in the resolution of this mystery.

Calibrating the nutrient dose the hard way

With acid dosing already quantified, we did the same for nutrients. The end result: +14 µS/cm per 50 ml of each part (A and B, pre-diluted 1:10, in a 165 L reservoir), and recent doses land within a couple of µS of prediction.

The first attempt taught us more than the number did. Dose poured on the surface: EC spiked for nine minutes, then… vanished. Diluted concentrate is denser than water. It slid straight past the pump inlet (20 cm off the barrel floor) into the still water below it. About a quarter of the reservoir turns out to be a circulation dead zone. Mass balance eventually closed: the "lost" dose leaked back out of the dead zone as a slow +10 µS creep over the whole afternoon. And the rescue attempt, stirring the barrel, sloshed the float valve into topping up ~8 L of fresh water, which the dashboard dutifully flagged and which ruined that calibration run entirely.

Heat, votes, and one-fifth of a door

Midday hit 33.5 °C inside the hoop house. Opening the door one-fifth of the way broke the peak in nine minutes, roughly 2 °C per ten minutes, and held the afternoon in the mid-twenties. Meanwhile the crop voted on what heat means: bok choy shrugged, spinach complained, coriander and lettuce wilted. The log adds a nuance the thermometer alone wouldn't: leaf damage correlates with cumulative hours above 25 °C (2.6 h that day), not with the instantaneous peak. That single observation just promoted ventilation to the top of the automation queue, and gave us the metric the vent controller should actually regulate.

Mechanical day: a few dollars of pool noodle

The channels came with 70 mm planting holes, big enough that sparse young canopies let direct sun down onto the root zone. NFT film has zero thermal mass, so a lit hole is a localised root heater, plus an open invitation to algae. The fix came from the pool section of the hardware store: noodle slices, about 30 mm thick, split as collars around each stem; flat covers over every unplanted hole. Light leak closed, seedlings snug, total spend in single digits.

Pool noodles leaning against the hoop house wall
Precision horticultural tooling, as purchased.
Lettuce seedling held by a blue foam collar in the channel hole
A noodle slice as a collar: stem supported, hole shaded, roots in the dark where they belong.
NFT channels with foam collars and hole covers after the retrofit
After the retrofit: every plant collared, every empty hole covered.
Root ball close-up showing white root hairs
Root check. The white fuzz worried us for a second. It's root hairs and fresh root tips, not mould. Mould grows in patches across the medium; root hairs grow along the roots. The alarm signature is brown, slimy and sour-smelling. None present.

The week on one screen

Dashboard: pH, EC and temperature over 48 hours
A 48-hour window: pH drifting gently as the plants work, EC declining overnight (that's uptake, see above), air temperature swinging ~20 °C while the half-buried reservoir moves ~7. Every step has a story. The ones we didn't write ourselves, the event journal did.

The public read-only dashboard is still live. watch the greenhouse here.

Ongoing-honesty section. Early one morning this week the site dropped off the MQTT broker and stayed off for four hours before reconnecting. Nothing was lost: the controller logs every sample to its own SD journal on a 60-second cadence, and the dashboard back-fills from the device's storage when the link returns, so a cloud outage costs live view, never data. The root cause is a TCP-stack corner case we have under forensic investigation; when it's closed, it gets the full post-mortem here, same as the last one.

Next: seedling sponges and a no-transplant second crop, a stilling well for that float valve, and a vent actuator with a cumulative-exposure setpoint.

2026-08-22 · Auckland, New Zealand

First 72 plants in the water

Our controller platform grew up on the bench. This week it moved outdoors: an NFT hydroponic test site in a hoop house: four channels, a half-buried reservoir for thermal mass, and an Industry 757 with three expansion modules running the instrumentation. On day three we planted the first 72 heads of lettuce, coriander and bok choy.

Hoop house with half-buried reservoir in front
The site. The blue barrel is the nutrient reservoir, a 200 L drum holding about 165 L of solution, half-buried for thermal mass.
Empty NFT channels mounted on timber frame
Four NFT channels going onto the frame.
NFT channels planted with seedlings
Planting day: lettuce, coriander and bok choy, straight into the film.
Close-up of seedlings in NFT channels
Bare-rooted seedlings, roots combed into the nutrient film.
pH, EC and temperature probes in a flow cell fed by the channel return
The flow cell: pH, conductivity and temperature probes sit in the channel return flow, measuring what the roots actually see.
Outdoor cabinet with Industry 757 controller and expansion modules
The outdoor cabinet: Industry 757 with PH-EC, 16DI and 16O expansion modules on the backplane, logging to SD and streaming to the cloud.

What 48 hours of data looks like

Cloud dashboard: pH, EC and temperature curves over 48 hours
The cloud dashboard, rolling 48-hour view: pH (two probes), conductivity (two probes), and four temperature channels. Every step in these curves is a real event.

This isn't a screenshot-only story. The dashboard has a public read-only view, so you can watch the greenhouse live, right now.

A dashboard earns its keep when the curves start telling you things you didn't ask. Some of what this one told us in its first two days:

The part where it broke

Honesty section. At solar noon on day two, the board's first 60 °C day in the cabinet, the controller's backplane receiver wedged: it could still command outputs, but stopped hearing module replies. Remote diagnostics narrowed it down the same afternoon: a defect in our error-recovery path, where a failed receiver re-arm was silently discarded. One rejection and the port stayed deaf.

The fix, a layered recovery ladder plus a bus-silence watchdog that rebuilds the port if nothing is heard for ten seconds, went out the same day as signed dual-bank OTA updates, with the site running throughout. No ladder truck, no site visit, and the bug never got a second day.

More notes as the crop, and the platform, grows.

Follow the Build

Leave an email and we'll write when there is something worth your time: availability, engineering notes, no noise.

No marketing chains, no sharing. One-click unsubscribe in every mail.