Skip to content
← All projects

Python · pylibftdi · MongoDB · CircleCI

Hydro sensor

A Python edge agent that speaks Atlas Scientific over FTDI, reads pH, conductivity, and temperature probes, and fans telemetry to independent sinks.

Role
Sole author
Year
Claims
code-verified
Repository
Source

Context and scope

An edge agent that reads environmental probes on greenhouse hardware and ships their readings to backend services. It runs on the device rather than beside it, so the two design concerns are talking to sensors over a serial protocol with no framing, and not losing a reading when a network destination is unavailable.

This overlaps directly with the professional greenhouse-monitoring work at IUNU: the code references the same device-identity files, internal database host, and API endpoints.

System design

Device protocol

AtlasDevice subclasses pylibftdi.Device to address Atlas Scientific temperature, pH, and electrical-conductivity probes over FTDI USB serial. The protocol is implemented by hand rather than through a vendor library.

Commands are terminated with a carriage return on send. Reads proceed byte by byte until a carriage return arrives, under a one-second timeout with explicit FtdiError handling, because the serial stream carries no length prefix or frame delimiter to read against.

The agent iterates a configured sensor list addressed by serial number, issues the read command to each, and parses the responses per sensor type: Celsius converted to Fahrenheit for temperature, conductivity split from parts per million for EC, and pH rounded.

Temperature compensation, attempted and not achieved

One probe is flagged as the reference, and its reading is retained so that a T,<temp> command can push it to the other probes before they are read. The reason is sound: uncompensated pH and conductivity readings drift with water temperature and would otherwise be recorded as real change.

The code does not do this. The compensation command is sent on the variable holding the current device, but that variable is not rebound to the pH or conductivity probe until after the command has already been sent, so T,<temp> always reaches the previously opened probe instead. Compensation goes to the temperature probe on the conductivity iteration and to the conductivity probe on the pH iteration, and neither the pH nor the conductivity probe ever receives it.

The ordering also means the loop depends on the temperature probe being listed first. It is, so the run works, but a reordered sensor list would send a command on an unbound variable and fail.

The readings this agent produced are therefore uncompensated. That is a defect in the implementation rather than a decision, and it is stated here because the reading it affects is the one the system existed to collect.

Telemetry fan-out

sensor_worker.py loads device identity and metadata from a local JSON identity file, then enriches every reading with hostname, role, room, sensor group, software version, and a UTC timestamp before dispatch.

Each reading is then written to several destinations: posted to an HTTP collection API with an asserted 201, written to MongoDB, and appended to a local file.

HTTP routing is exclusive rather than additive. A device belonging to a named customer, or sitting in one of two specific rooms, posts to that customer's endpoint instead of the default pair; every other device posts to both production and staging.

Orchestration

hydro_data.py sequences the run: read the sensors, format typed payloads, dispatch.

Sink-failure behavior

Destination availability is the failure mode that governs this design, because the agent runs on greenhouse hardware where the network is the least reliable component and a reading, once missed, cannot be taken again.

Every sink carries its own error handling, so an unreachable HTTP endpoint does not prevent the database write and a database outage does not prevent the file append. Failure is contained to the destination that failed rather than propagating to the dispatch.

The local file append is the last sink for that reason. It is the only destination that survives total network loss, which is the condition an edge device actually encounters, so it functions as the floor beneath the remote sinks rather than as a debugging convenience.

A probe that does not answer is handled separately, at the read rather than the dispatch: the byte-by-byte read carries a one-second timeout with explicit FtdiError handling, so a silent sensor costs its own reading instead of stalling the run.

One inversion in this design is worth stating, because it is the opposite of what the structure intends. The HTTP and database sinks report their failures; the local file append catches everything and discards it silently. The sink that exists as the last line of defence is the only one that cannot tell you it failed, so the case this design was built for, losing the network, is also the case where a second failure would go unrecorded.

Design decisions

DecisionRationale
Implement the serial protocol directlyThe probes expose a simple command protocol, and a hand-written reader made the timeout and error behavior explicit rather than inherited from a vendor abstraction.
Read byte by byte to a carriage return, under timeoutThe stream has no framing, so the terminator is the only reliable boundary, and the timeout is what prevents a silent probe from hanging the run.
Push a reference temperature before reading pH and ECUncompensated readings drift with water temperature, so without this the agent records an artifact of the water rather than a change in the chemistry. The intent is right and the implementation does not achieve it, as the section above describes.
Handle each sink's errors independentlyThe reading is the valuable artifact. One failing service should cost that service's copy, not the reading.
Append to a local file as the final sinkPreserves data through total network loss, which is the failure mode a device in a greenhouse actually experiences.

Verification status

Source-verified. The dispatch path was read directly rather than taken from a summary.

Confirmed in the source: the identity file lookup across two known paths, the enrichment with hostname, role, room, sensor group, version, and a UTC timestamp, the asserted 201 on the HTTP post, and independent error handling around each of the three sinks. A CircleCI configuration is present, and the agent ran against real hardware rather than a simulator.

Corrected against an earlier description of this project: temperature compensation is not applied to the pH or conductivity probes. The T,<temp> command is issued before the device variable is rebound to the probe being read, so it always reaches the previously opened device. An earlier version of this page claimed the compensation worked, which the source does not support.

Two further defects are visible in the same file. get_sensor_data calls read_sensors without returning its result, so it returns None to any caller, and read_sensors is invoked once at import time, so importing the module drives the hardware.

Corrected against an earlier description of this project: readings are not written to a MongoDB collection per sensor type. The per-type collection is selected into a variable and then never used; the insert targets a single fixed sensordata collection. That is a defect in the code rather than a design choice, and it means sensor type is carried in the record rather than in the collection.

Scale, stated plainly because the commit count suggests otherwise: the agent is about seven kilobytes of Python across four files, with one test file of under a thousand bytes. It is a small, specific piece of software that ran in production, not a large one.

Not verified: long-run reliability figures, data-loss rates, or how the fan-out behaves under sustained backend outage. No measurements of those are retained.

Known limitations

Runtime

The agent is written for Python 2.

Portability

Identity file paths, the database host as a hardcoded private address, the output file path, and the API endpoints are all specific to one deployment rather than configurable, so the code documents an approach more than it provides a reusable agent. A device missing both identity files fails partway through enrichment rather than exiting on a clear error.

Repository hygiene

Compiled bytecode and macOS directory metadata are committed, and the ignore file is effectively empty at ten bytes.

Contact

Available now. Permanent senior or staff platform roles, in Munich or remote within the EU.

Email is the fastest way to reach me.