← ALL WORK
built · bench-validated Autonomous Underwater Vehicle 2026 — present

Mongla

An AUV autonomy stack that has to be right when nobody is watching.

Mongla simulator — pool overview SOURCE — fh1m/mongla_ws

An independent ROS 2 control, mission and vision stack for AUVs — a control board with firmware I wrote, a Hailo-8 neural accelerator that does nothing but see, and a rule that no number appears without the measurement that produced it.

Mongla is the stack I would build if I controlled the whole architecture. Reflexes live on the board — a 500 Hz control loop on an ESP32 core handling sensors, control and DShot — and thinking lives on a Raspberry Pi 5 with a Hailo-8 AI HAT. The two are joined by a single USB-C cable speaking MAVLink 2, and exactly one node is allowed to touch the board.

The philosophy is the point: absence renders as “--”, never 0.0; a verb that reports success while the vehicle does nothing is treated as the worst failure there is; and every shipped threshold is backed by a measurement with its method and conditions written next to it. The test suite is 3,756 checks, and the honest status badge reads “in water: never”.

Vision runs at a measured 53.9 Hz through the ROS graph (18.0 ms photon-to-detection), with per-camera calibration measured in water — 46.7°, not the datasheet’s 63.8° in air. A right-invariant EKF predicts on the board IMU and corrects with depth, optical-flow velocity and heading, replaying late measurements at the instant they describe rather than on arrival.

anatomy / mongla

The machine, part by part — with the numbers that built each one.

Every stat here came off the running stack, not a spec sheet. Scroll — the console follows the story.

the board
the board

The split no one sees

The first fight was with myself, not the hardware: I kept wanting one brain for the whole vehicle. It’s the wrong instinct. Reflexes belong on hardware that cannot be interrupted, so the ESP32 core got sensors, control and DShot at a fixed 500 Hz — nothing else runs there, ever.

The Pi does nothing but see. The two meet over one USB-C cable, and exactly one node is allowed to touch the board. I didn’t believe the split was actually clean until I watched the control loop’s jitter on a live plot while vision spiked — and it didn’t move.

500 Hz control loop, fixed
0 control jitter change under vision load

// mongla — srot/Hailo split, live telemetry

the eye

Decode on demand, not on arrival

The Hailo-8 has a scarce budget of TOPS, and my first version wasted it — decoding every frame the camera handed over, whether the inference worker was ready or not. I rewrote the rule: decode only when the worker is idle, so the chip never spends its compute on a frame that’s already stale.

I didn’t trust the fix until the instrumentation said it out loud, unprompted, every ten seconds: across 778 inferences, mean frame age at infer-start was 11.5 ms, worst case 19.2 ms — and zero frames were ever decoded and then never used.

778 inferences, instrumented
11.5 ms mean frame age at infer-start
0 decoded-but-never-inferred

// mongla — chip-efficiency readout

the python problem

Nothing in the hot path holds the lock

Python has one lock, and a vehicle can’t afford a stage that holds it while it works — every other stage freezes with it. Instead of chasing stalls one symptom at a time, I audited every hot-path stage by hold × rate: the fraction of wall-time the process is actually frozen (tools/gil_audit.py).

The verdict: every hot-path stage releases the lock — hailo run_async at 78.3% duty frees it, cv2.imdecode at 32.0% frees it, the only holder left is the viewer feed, off the mission path. That one session of work took the vision pipeline from 28 ms to 18 ms, detections from 36.5 Hz to 77 Hz, at about 30% less CPU.

28 → 18 ms vision pipeline, one session
36.5 → 77 Hz detections
~30% less CPU

// mongla — tools/gil_audit.py

the honest ceiling

The sensor is the limit — not the code

Once the pipeline stopped being the bottleneck, I had to prove it rather than assume it. Authority at p99 came back 1.000 on both cameras: at the 99th percentile, the freshest frame the sensor could physically provide is the frame the controller acts on. There’s no software latency left to chase.

The forward camera runs at 73.4 Hz, the downward at 27.6 Hz — and the calibration that feeds both had to be redone once I put the vehicle in water: the datasheet says 63.8° field of view; measured wet, it’s 46.7°. Water bends light the manual doesn’t know about.

1.000 authority at p99, both cameras
73.4 / 27.6 Hz forward / downward camera rate
46.7° vs 63.8° measured wet vs datasheet FOV

// mongla — photon-to-command latency, live hardware

the brain

Time is the hard part, not the math

The Kalman filter equations were the easy part — a weekend of linear algebra. What actually took the effort was time: a right-invariant EKF that predicts on the board IMU and corrects with depth, optical-flow velocity and heading has to replay a late measurement at the instant it happened, not the instant it arrived.

That rule forced a harder one on the whole codebase: absence renders as “--”, never 0.0. A number that reports success while the vehicle does nothing is treated as the worst failure there is — because a silently-wrong zero is indistinguishable from a real reading, and an estimator that can’t tell the difference will lie to the controller with total confidence.

// mongla — right-invariant EKF, board IMU + depth + optical flow

the honest badge

Built. Not yet proven. It says so.

Every thruster on this vehicle has been bench-tested, every control surface exercised, every subsystem exercised against 3,756 tests across 1,038 commits. None of that is the same claim as “it works in the water,” so the status badge doesn’t make that claim — it reads “in water: never,” in the same amber as every other unverified number on this site.

I’d rather ship an honest “not yet” than a comforting maybe. The next milestone for this machine isn’t a feature. It’s the pool.

1,038 commits
3,756 tests, all green on the bench
never in water — still true

// mongla — README status badges