Monday: the boat did not know how deep she was

We started with paperwork, which is how all the best disasters begin.

Enja had a specification sheet inherited from her previous owner — a spreadsheet of the kind that has been copied between three computers and lost its column alignment somewhere around 2011. Hull length, sail areas, part numbers, and a DC panel whose circuits had drifted a column to the left, so that the "Radar" switch appeared to control the radar. (It does not. It controls the chartplotter at the chart table. There is a switch labelled "Navigator" for the other chartplotter. This is fine. This is normal. Nobody panic.)

The hull number decoded to a Comfortina 42, hull 37, and with a class to check against, one number stopped agreeing with the others.

The draft. The papers said 2.10 m. The builder said 2.10 m. A broker's listing for a sister ship said 2.10 m. The logbook said 2.25 m — and when I went looking for where it got that, the answer was a comment in a config file saying, cheerfully, that the figure came "from her own AIS broadcast."

Which it did. Signal K was broadcasting 2.25 m to every vessel and VTS within radio range, because somebody had once typed 2.25 into a settings page, and thereafter the boat had been citing herself as a primary source. She was fifteen centimetres deeper in her own imagination than in the water. The one thing she was sure of, she had made up.

While in there, we also gave her a sail-fit calculator — the kind of thing you want when a second-hand genoa appears online with three measurements and no context. A sail is a triangle and a rig is a set of limits on one; you can answer the whole question with Heron's formula and a shred of honesty about what the furler drum eats. It will tell you, kindly, that the bargain sail from Bergen is 40 cm too long, before you drive four hours to discover this in a stranger's garage.

Tuesday: the passage that would not end

Then a real trip: Husnes to Leirvik, 11 nautical miles.

She tied up at 12:18. The passage closed at 16:10 — the moment somebody remembered there was a button. For nearly four hours the boat believed she was at sea, which meant any photo taken alongside would have been filed against a voyage that had ended at lunchtime.

The infuriating part is that the boat already knew. The logbook has segmented legs on movement since day one — half an hour under half a knot ends a leg — and it had put the arrival at 12:18, which is exactly right. The knowledge existed. It simply arrived four hours late and from the wrong direction, like a postcard from yourself.

So the rule moved aboard: thirty minutes below half a knot and she closes her own passage, stamped at the moment she stopped, not at the moment the timer noticed. Pressing the button now backdates the same way, because the tap reports the trip — it does not time it.

The interesting failure mode came free with the hardware. Enja's instruments live on their own breaker, and the breaker gets killed when you tie up. Signal K then serves the last thing it heard, forever, with a stale timestamp — and the last thing it heard may well be "five knots." My first version asked "is she moving?", got told "yes, eternally", and would have kept the passage open until the heat death of the marina. The fix is to stop asking whether she is moving and start asking when she was last seen moving — at which point the instruments going dark stops being a bug and becomes exactly what arriving looks like.

Husnes → Leirvik, 12 July: when the passage ended

Enja tied up at 12:18, and her own leg segmentation had it right to the minute. The passage stayed open until somebody pressed the button at 16:10 — three hours and fifty-two minutes in which any photograph taken alongside would have been filed against a trip that had ended at lunchtime. The pilot vessel STEVEN passed 37 metres off the pontoon at 17:15, an hour later again, and never appeared at all: the logbook was reading a stale copy of the data that stopped at 16:13.

The rule nobody had ever measured

The project's own house rules said: never build on the Pi, it's too small.

Nobody had checked. I checked. The Python data stack installs in 76 seconds. The entire logbook builds in 58 seconds at a peak of 595 MB — byte-identical to the laptop's — on a Raspberry Pi with 2.8 GB free. The website build adds thirty seconds.

The rule was a superstition with a Makefile.

The real constraint was never size; it is that a heavy build must not compete with the recorder, whose data cannot be taken again. So the boat now builds when she is stopped, refuses outright while a passage is open, and runs at low priority with idle-class I/O. She publishes her own voyage about two minutes after tying up — wind, motoring, depth, every vessel that passed — with no laptop involved and, quite often, no internet.

A Raspberry Pi in a white case with a blue PICAN-M board on top, ringed by the boat's wiring and terminal blocks
The computer the house rules said was too small to build on: a Raspberry Pi under a PICAN-M, wedged in among the boat's own wiring. It builds the whole logbook in 58 seconds.

The switch panel has been telling us about the nav lights 199,296 times an hour

Then to the recordings, which were consuming 70 MB a day and heading for a metered 4G SIM.

The premise was that a moored boat is boring. The data disagreed in a more interesting way: an hour at anchor is 422,058 rows and an hour crossing a fjord is 432,611. A 2% difference. The recorder does not care what you are doing; it will describe you doing nothing with exactly the same enthusiasm.

And the single biggest item in every hour Enja records — 47% of the file, 199,296 rows — is her switch panel restating that the nav lights are still off. Of those, 720 are actual changes. The remaining 198,576 are the boat clearing its throat.

Deleting repeated values is free — a repeat in a timestamped stream carries no information — and it alone made everything three and a half times smaller. Add deadbands tuned to what each instrument can actually resolve, and it is 9.5× smaller: 70 MB a day becomes about 9. The whole boat now fits comfortably inside the SIM's allowance, continuously, which is the difference between a backup that runs and a backup that gets switched off in month two.

But the compaction is not the good part. The good part is what the acceptance test did to it.

One hour of recording, by content

An hour at anchor is 422,058 rows and an hour crossing a fjord is 432,611 — a 2% difference. The switch panel is 199,296 of them, 47%, restating that the nav lights are still off; AIS is 6%; everything else is the remaining 47%. Of the panel's 199,296 rows, 720 are actual changes and 198,576 are repeats — the sliver below is those 720, drawn at minimum visible width rather than its true third of a pixel.

Enja's own recorder, 11–12 July 2026.

One day of recording, before and after compaction

A repeat in a timestamped stream carries no information, and deleting the repeats alone made everything 3.5× smaller — 70 MB a day to about 20. Deadbands set to what each instrument can genuinely resolve take it to 9.5× smaller, about 9 MB a day, which fits inside the 4G SIM's monthly allowance continuously. The middle figure is the post's 3.5× applied to 70 MB rather than a measured number.

In which the test refuses three of my ideas in a row

The test is brutally simple: build the logbook from the raw recording, build it from the compacted one, and compare every number a human actually reads. If the two disagree, the compaction is wrong. Not the test.

It failed. Three times.

Once, because the compactor was lossless and the logbook was not. The logbook buckets readings into one-second bins and averages what lands in each — and a second where an instrument happened not to speak came back as NaN and was silently dropped from every mean and integral. The boat was treated as having no depth in that second. She had a depth. Nobody had restated it. navigation.log — her odometer — reports the same number 2,226 times an hour at a mooring, and the moment you delete those repeats, the leg's first reading lands after the leg starts, and the through-water distance comes out short. Her own odometer, treated as though it had forgotten where it was.

Twice, because I gave the compacted stream a 300-second heartbeat while the logbook only holds a value for 60. Four minutes in every five simply ceased to exist. The motoring estimate fell from 70% to 47%, and — this is the part worth being afraid of — nothing errored. The boat's behaviour hadn't changed. Only how much of it the logbook could see.

Three times, because I thinned the AIS, and two vessels vanished from the logbook entirely. The logbook pairs another boat's position to ours within sixty seconds; a vessel reduced to a report every ten minutes cannot be paired with anything. She had simply never been there. AIS is 6% of the file. Leave it alone.

Every one of those would have shipped, silently, and quietly falsified a year of voyages.

"We motored the whole way"

Then came the one thing the software had no way to know: both legs had been 100% engine, the whole way.

The logbook said 70% and 96%.

The old rule asked "is there wind, and is she flat?" and called everything else sailing. On the 11th it therefore recorded twenty-five minutes as sailing during which Enja was making 7.9 knots in 6 knots of apparent wind, heeling one tenth of a degree. Six knots of wind cannot push nine tonnes at eight knots, and a boat driven by her sails leans over. The logbook was watching a motorboat and writing poetry about it.

The question that separates an engine from a sail is not "is there wind?" but "could this wind possibly produce this speed?" So there is a polar now — no-go angle, hull speed, what she makes reaching and running — and a minute counts as motoring when she is going faster than the wind could ever account for. That fixed most of it. The last seven minutes were a perfectly sailable speed on a beam reach in eight knots of breeze, except that she was heeling 0.2 of a degree: forty-four square metres of mainsail driving nine tonnes makes a boat lean, and sails that are doing nothing do not.

70% → 97%. 96% → 100%. And I stopped there, because two legs of ground truth — both motored — is not enough evidence to keep tuning against. It has been proven not to miss motoring. It has never been proven not to invent it. That test requires actual sailing, which is scheduled.

Motoring, per leg: the old rule against the polar

The old rule asked whether there was wind and whether she was upright, and called the rest sailing — so it logged twenty-five minutes of the 11th as sailing while Enja made 7.9 knots in 6 knots of apparent wind, heeling one tenth of a degree. Asking instead whether the wind could possibly produce that speed takes the two legs from 70% and 96% to 97% and 100%. Both were entirely under engine, which is the line at 100%. Tuning stopped there: the rule has been shown not to miss motoring, and never shown not to invent it.

She now refuses to run code I didn't sign

By Tuesday evening she updates herself: fetches from GitHub, verifies the release tag against one public key she holds, runs the entire test suite on her own ARM hardware, rebuilds, health-checks, and rolls back if the site doesn't answer.

The security claim is not a comment. I generated an attacker's key aboard, signed a tag with it, and made it sort newest — everything a person with full write access to the repository could do:

REFUSED — signed, but not by a key this boat trusts.

She holds a read-only deploy key and no credential that can write to anything, anywhere. She also declines to update while she is sailing, or on a battery below 50%, because a boat is not a server rack and an update that reboots the web server mid-passage costs a photograph.

And then a pilot boat named STEVEN blew the whole thing open

Last thing at night, from the cockpit: a vessel over 15 m passed right in front of us in the harbour, and she's not in the AIS list. Name was Stevens.

She was called STEVEN, she was a pilot vessel, and she had passed 37 metres off the pontoon at 20 knots. The AIS had recorded her perfectly: 262 position fixes, name, callsign, type.

What was broken was everything downstream of a symlink.

ingest/data/parquet is a symlink onto the SSD. A deploy had replaced it with a real, empty directory — because rsync --delete transfers a directory even when you exclude its contents, and will cheerfully write over a symlink to do it. So the ingest kept writing fresh parquet to the SSD, and the logbook kept reading a stale copy under /opt that stopped at 16:13. The harbour stay ended four hours before the boat did. STEVEN arrived at 17:15, into a window that had already closed while everyone was still sitting in it.

Nothing errored. Nothing ever does. That is the whole lesson of the two days: not one of these bugs was a crash. Every single one was a piece of software reading, with total confidence, from the wrong copy of the truth — a stale directory, a repeated value, a sensor that had gone quiet, an AIS broadcast citing itself as a source.

The boat now checks her own wiring on every rebuild and repairs it, and deletes the stale copies rather than keeping them, on the grounds that a second copy of a regenerable thing is not a backup. It is a thing that can be read by mistake.

Where she stands tonight

She records herself. She notices when she has arrived. She compacts her own data nine-fold, publishes her own voyages two minutes after tying up, backs herself up to Cloudflare within a SIM's budget, and updates her own code — while refusing to delete a recording that isn't safely offsite, or to run a line of code nobody signed.

Also she has stopped lying to passing ships about her draft, which felt like the least we could do.

Next: a tape measure to the depth transducer, a USB cable to the AIS transponder, and — at some point — an actual sail, so we can find out whether the boat can still recognise one.