AgriPulse HMI
not publicly deployedA real-time mobile control panel for a smart irrigation pump, built to behave like an industrial controller rather than a dashboard.
Architecture
- React Native 0.86 (operator app), client. The operator's control panel: live telemetry, pump control, alert acknowledgement, and an SVG gauge drawn for the industrial look. Connected from Android native module over bridge. Connected to Safety interlocks over Socket.IO, operator namespace.
- Android native module (NetworkDiagnosticsModule, Java), client. OS-level network diagnostics: connection type, reachability, and latency to the backend, measured with raw sockets. Connected to React Native 0.86 over bridge. Connected to Safety interlocks over TCP probes.
- Safety interlocks (server-authoritative), service. The gate in front of every pump command: tank level, device online, emergency stop latch, already-running state, and a maximum-runtime auto-stop. Connected from React Native 0.86 over Socket.IO, operator namespace. Connected from Android native module over TCP probes. Connected to In-memory device state cache.
- Weather automation (pure rule function), service. Decides irrigation from weather and telemetry as a pure rule function, and stores every decision with its inputs and its reasoning. Connected to In-memory device state cache. Connected to Open-Meteo over HTTPS.
- In-memory device state cache, service. Holds current device state in memory to serve the hot paths: safety checks and automation decisions. Connected from Virtual IoT device simulator over Socket.IO, device namespace. Connected from Safety interlocks. Connected from Weather automation. Connected to Throttled writes.
- Throttled writes, service. Flushes the telemetry stream to PostgreSQL at a bounded rate as the durable record. Connected from In-memory device state cache. Connected to PostgreSQL 16 over Prisma.
- PostgreSQL 16 (via Prisma, 7 tables), datastore. The durable record: telemetry history, decisions with reasoning, alerts, and device configuration, with native enums and indexed time series. Connected from Throttled writes over Prisma.
- Virtual IoT device simulator (swaps for ESP32 / Pi), service. A deterministic, physically grounded stand-in for the pump controller, speaking the exact protocol real hardware would. Connected to In-memory device state cache over Socket.IO, device namespace.
- Open-Meteo, external. Supplies the forecast the automation engine folds into its irrigation decisions. Connected from Weather automation over HTTPS.
- Boundary: Node 22 + Express backend.
Decisions
Two namespaces, two audiences
- The decision
- Device traffic and operator apps connect over two separate Socket.IO namespaces.
- The constraint that forced it
- Devices and operators have different message vocabularies and failure modes, and a real device-fleet backend keeps them apart.
- Rejected
- One namespace with message-type switching, which lets a buggy device broadcast into operator channels.
- Consequence accepted
- Two connection paths to test, and the separation the schematic shows survives into the code.
The server is the authority
- The decision
- Every safety-relevant decision runs server-side: low tank, offline device, latched emergency stop, already-running state, and a maximum-runtime auto-stop.
- The constraint that forced it
- The client can be stale, buggy, or hostile, and the pump is a physical actuator.
- Rejected
- Client-side guards in the app.
- Consequence accepted
- The server carries the full state machine, and UI affordances can only ever be hints.
Hot state in memory, history on disk
- The decision
- An in-memory device state cache serves safety checks and automation, with throttled writes to PostgreSQL as the durable record.
- The constraint that forced it
- A high-frequency telemetry stream would otherwise put a database round trip inside the safety path and grow the database without bound.
- Rejected
- Reading and writing Postgres on every message.
- Consequence accepted
- Bounded database growth and fast checks, in exchange for buffered-loss risk on crash and a single-process cache.
Decisions carry their reasoning
- The decision
- The automation engine is a pure rule function, and every decision is stored with its inputs and its reasoning.
- The constraint that forced it
- An automation that cannot explain a skipped irrigation is an automation operators will not trust.
- Rejected
- Logging outcomes only.
- Consequence accepted
- "Why did it skip irrigation at 6am" has an answer in the database, and the function is unit-testable without hardware.
A simulator with physics
- The decision
- The simulator is deterministic and physically grounded: a fixed daily temperature curve, relative humidity computed from temperature via the Magnus equation, temperature-scaled evaporation, and a stochastic rain probability model.
- The constraint that forced it
- There is no physical pump, and the system still has to be built and tested end to end.
- Rejected
- Canned telemetry replay, which cannot respond to commands.
- Consequence accepted
- It connects over the exact protocol a real ESP32 or Raspberry Pi controller would use, so hardware can replace it without changing the backend or the app.
Native code where JavaScript cannot go
- The decision
- A custom Android module in Java performs OS-level network diagnostics, running blocking checks on a background thread.
- The constraint that forced it
- Connection type, reachability, and honest latency to the backend are invisible to JavaScript.
- Rejected
- A community library reporting what the OS volunteers.
- Consequence accepted
- Real diagnostics in the app, and an iOS implementation still owed behind the same bridge.
Charts drawn by hand
- The decision
- Charts and the radial gauge are drawn directly in SVG rather than pulled from a charting library.
- The constraint that forced it
- The industrial look needs exact control of ticks, needles, and thresholds that chart libraries abstract away.
- Rejected
- A charting library themed to approximate the look.
- Consequence accepted
- More drawing code owned in the repository, zero library weight, and a gauge that reads as an instrument instead of a widget.
Known limitations
- The device is simulated. That is a strength in one sense, a hardware-ready architecture built without hardware, but it must never be mistaken for a physical deployment.
- There is no authentication on the operator app.
- There is no fleet model: the system supervises one device.