Monitoring HHO with Lateralus
An HHO system is only as good as its monitoring. Without real-time visibility into gas flow, temperature, current, and efficiency, you're flying blind. Here's how we use Lateralus pipelines to monitor HHO systems end-to-end.
◉ The Monitoring Challenge
HHO systems have multiple interdependent variables:
- Cell voltage: 2.0-2.5V per cell for efficient electrolysis
- Current draw: Determines gas output rate
- Electrolyte temperature: 60-70°C optimal, danger above 85°C
- Gas flow rate: L/min output
- Pressure: Should stay under 5 PSI
- Water level: Consumption rate varies with load
Traditional approaches: individual sensors with separate displays, manual logging, no correlation between metrics. We wanted something better.
◉ Hardware Setup
Our monitoring stack:
| Sensor | Model | Interface | Cost |
|---|---|---|---|
| Temperature (x4) | DS18B20 | 1-Wire | $2 |
| Current | ACS712 30A | Analog | $3 |
| Voltage | Voltage divider | Analog | $0.50 |
| Flow (gas) | YF-S201 | Pulse | $4 |
| Pressure | MPX5010DP | Analog | $8 |
| Water level | Float switch | Digital | $3 |
Controller: RP2040 (Raspberry Pi Pico) running Lateralus firmware. Total sensor cost: ~$25.
◉ The Sensor Pipeline
Core monitoring logic in Lateralus:
@freestanding
module hho.monitor
import hw.adc
import hw.onewire
import hw.gpio
import data.ring_buffer
// Sensor reading structure
struct Reading {
timestamp: u64
voltage: float
current: float
temps: [float; 4]
flow_rate: float
pressure: float
water_ok: bool
}
// Ring buffer for 1 hour of data at 1Hz
let history = ring_buffer.new[Reading](3600)
fn read_all_sensors() -> Reading {
Reading {
timestamp: time.now_ms(),
voltage: adc.read(0) * 3.3 * 11.0, // Voltage divider ratio
current: (adc.read(1) - 2.5) * 20.0, // ACS712 scaling
temps: onewire.read_all_temps(),
flow_rate: flow.read_lpm(),
pressure: (adc.read(2) / 3.3) * 10.0, // kPa
water_ok: gpio.read(WATER_PIN)
}
}
fn calibrate(r: Reading) -> Reading {
// Apply sensor-specific corrections
Reading {
...r,
voltage: r.voltage * 1.02, // Calibrated against DMM
current: r.current + 0.15, // Zero offset
temps: r.temps |> map(fn(t) { t - 0.5 }), // DS18B20 typical offset
}
}
fn calculate_efficiency(r: Reading) -> Reading with { efficiency: float } {
// Theoretical: 1 L HHO requires ~2.4 Wh
let power_in = r.voltage * r.current // Watts
let power_theoretical = r.flow_rate * (60.0 / 1000.0) * 2400.0 // Wh/min to W
{
...r,
efficiency: match power_in > 0.0 {
true => (power_theoretical / power_in) * 100.0,
false => 0.0
}
}
}
◉ The Full Pipeline
Putting it together:
fn monitoring_loop() {
loop {
let reading = read_all_sensors()
|> calibrate()
|> calculate_efficiency()
history.push(reading)
// Log to SD card every minute
if reading.timestamp % 60000 == 0 {
log_to_sd(reading)
}
// Check for anomalies
reading |> check_anomalies() |> alert_if_needed()
// Update display
display.update(reading)
sleep_ms(1000)
}
}
fn check_anomalies(r: Reading) -> Option[Alert] {
// Temperature check
for i, temp in enumerate(r.temps) {
if temp > 80.0 {
return Some(Alert::OverTemp { sensor: i, value: temp })
}
}
// Pressure check
if r.pressure > 4.5 {
return Some(Alert::OverPressure { value: r.pressure })
}
// Water check
if !r.water_ok {
return Some(Alert::LowWater)
}
// Efficiency anomaly (sudden drop might indicate leak)
let avg_eff = history.last(60) |> map(_.efficiency) |> mean()
if r.efficiency < avg_eff * 0.8 && avg_eff > 50.0 {
return Some(Alert::EfficiencyDrop { current: r.efficiency, average: avg_eff })
}
None
}
◉ Remote Monitoring
For off-grid installations, we add WiFi connectivity:
fn upload_readings() {
let batch = history.last(60) // Last minute of readings
let payload = batch
|> map(fn(r) {
{ ts: r.timestamp, v: r.voltage, i: r.current,
t: r.temps[0], f: r.flow_rate, p: r.pressure }
})
|> json.encode()
// Upload to local server or cloud
match wifi.post("http://192.168.1.100/hho/data", payload) {
Ok(_) => led.blink(GREEN, 100),
Err(e) => log.warn("Upload failed: " + e.message)
}
}
◉ Dashboard
The receiver side runs a simple web dashboard (also in Lateralus):
// dashboard.ltl - runs on local server
import web
fn main() {
let app = web.new()
app.get("/", fn(req) {
web.render_template("dashboard.html", {
current: db.latest_reading(),
hourly: db.hourly_averages(24),
alerts: db.recent_alerts(10)
})
})
app.get("/api/readings", fn(req) {
let since = req.param("since", 0)
db.readings_since(since) |> json.response()
})
app.listen(8080)
}
◉ What We Learned
- Sample at 1Hz minimum: Slower sampling misses transients
- Log everything: Disk is cheap, debugging blind is hard
- Calibrate in-situ: Sensor specs aren't reality
- Alert thresholds evolve: Start conservative, tune over time
- Pipeline makes code readable: Each step is testable
◉ Results
With this monitoring in place, we've:
- Caught a failing seal before it became dangerous (pressure creep)
- Optimized electrolyte concentration (efficiency jumped 8%)
- Identified ideal operating temperature range for our specific cell
- Reduced water consumption by preemptive maintenance
Full source code at hho-monitor. See also the HHO page for system specifications.