September 2026
Building MajiGuard: what actually went wrong
A build log for my final year water quality monitor: two dead sensors, a voltage mismatch, a turbidity sensor that could not tell room light from cloudy water, and the machine learning that ended up separating real contamination from a broken probe.

MajiGuard is my final year project at the University of Nairobi: a water quality monitor built to sit in a jerrycan or storage tank and tell you, in plain language, whether the water is safe. This post is the build log, not the thesis. It is mostly about the parts that broke.
The problem I was actually solving
About 41% of rural Kenyan households draw drinking water from unimproved sources: rivers, unprotected wells, streams. The lab testing that exists is accurate but slow (24 to 72 hours) and expensive enough that most households and community boreholes never get tested at all. By the time a result comes back, the water it was measuring has usually already been drunk. I wanted to know whether a single-node sensor under KES 15,000 could close that gap: continuous readings, a model that tells contamination apart from a failing probe, and an alert that says what to actually do instead of just a number.
What it does
An ESP32 reads pH, turbidity, TDS and temperature every 30 seconds and pushes them to Firebase.
A blended Isolation Forest and River HalfSpaceTrees model scores every reading for anomalies, and separates a genuine contamination event from a sensor that has simply drifted.
Ridge Regression forecasts pH, turbidity and TDS one to six hours out; Prophet extends that to 24 hours.
A retrieval-augmented generation layer, FAISS over 20 curated WHO and KEBS chunks feeding LLaMA 3.3 70B, turns an anomaly into a plain-language safety advisory instead of a raw score.
A React Native app shows all of it as a single 0 to 100 quality score, with detail underneath for anyone who wants it.
The sensors kept dying
Before I had a real calibration procedure, I ran a quick sanity check: dunk the pH probe in soap water, since soap is alkaline and tap water is roughly neutral, and see if the reading moved. It moved. It also never came back. Surfactants strip the hydrated gel layer off a glass pH electrode and poison the KCl reference junction, and the same wash killed the turbidity sensor's optical baseline at the same time. Two sensors, one bad idea, about KES 4,950 in replacements.

The replacement pH module is built for 5V Arduino boards; the ESP32's ADC wants 3.3V. Wiring it straight in would have clipped every reading above that ceiling and risked frying the GPIO pin outright. The fix was a plain resistor voltage divider, 10k and 20k, bringing 5V down to 3.33V. Because the two-point calibration records whatever voltage the ADC actually sees, the divider ratio just gets absorbed into the calibration constants and never needs its own correction factor in firmware.
The turbidity sensor's problem was different and took longer to find: readings in clear tap water spiked to 10 to 25 NTU every evening for no obvious reason. Turning off the room lights made the spike disappear immediately. The sensor's optical chamber is not sealed against outside light, so ordinary room lighting was leaking into the photodetector and reading as turbidity. Two attempts to fix it physically both failed: aluminium foil left gaps at the cable entry that let river water seep in and contaminate the optical path during testing, and black heat shrink plus tape did not hold up to immersion either.

Rinsing the fouled sensor under a tap and then dunking it fully to flush it out finished it off: the module's signal-conditioning PCB is not waterproof, only the tip is meant to touch water, and full submersion shorted it for good. A second replacement, and a software workaround instead of a physical one: if turbidity reads high at night while every other parameter looks normal, the app now shows a note explaining the ambient light effect rather than treating it as a real result.
Calibration turned out to be the part that mattered
pH calibration is a two-point linear fit against 4.01 and 6.86 buffer solutions: submerge, wait 30 seconds for the voltage to settle, repeat, then solve for slope and offset. The replacement probe turned out to have an inverted response (higher voltage meant lower pH), which is just a sign flip in the firmware once you know to look for it. What made this workable for something meant to sit unattended in the field was making calibration remote: the mobile app writes new constants to a Firebase command node, and the firmware picks them up and applies them on its next cycle, no reflashing, no physical access to the device required.

Telling a broken sensor from a genuine problem
A single sensor reading a value outside its normal range is usually a hardware fault, not contamination. Real contamination tends to move more than one parameter at once. So the classifier does not just ask whether a reading is anomalous, it counts how many parameters are simultaneously implausible: one means SENSOR_DRIFT, two or more means CONTAMINATION. That distinction runs the whole way through the system, from the anomaly score to the advisory text, so a user who sees a drift alert knows to check the probe rather than discard water that was probably fine.
Where it landed
Across four controlled contamination experiments, fresh lemon juice for an acid spike, drinking chocolate powder for turbidity and TDS, and two real river water samples, the pipeline caught every single contamination event. On a labelled evaluation set of 1,373 readings that gave 82% precision, 100% recall, and an F1 of 0.90. Short-horizon forecasting held RMSE under 0.15 pH units, 8 NTU and 12 ppm TDS at the one-hour mark. The whole unit, sensors, enclosure, microcontroller, came in at KES 13,200.
What I would change
The system has never run anywhere but a lab bench and a bucket. The obvious next step is a real multi-week field deployment across a few different water sources, which would also fix the biggest gap in the current training data: everything so far comes from a single tap over a single day. After that, offline buffering (right now a lost Wi-Fi connection just means a lost reading), and eventually a way to get at bacteriological contamination directly instead of through turbidity as a proxy.
Comments
No comments yet.