Skip to content
Go back

labmon, in beta

Updated:

Any experimentalist working in a lab requiring a finely controlled environment knows the value of having an efficient monitoring and alerting system. This is for instance typically the case in quantum optics and cold-atoms labs, where our machines involve a large amount of precisely tuned optics — sensitive to temperature and mechanical stress — but also high vacuum gauges, frequency and wavelength meters… all of these need babysitting, as not catching a single unexpected drift can lead to wasting precious experiment uptime. Conversely, when something goes wrong in the results, it is of course much nicer to have the machine’s health at a glance — on a control panel or inside a searchable logging system.

This is what I tried to support with the labmon project. Over time, we’ve seen many variations of that idea, independently developed by different teams: the ambition of labmon is to centralize and unify the efforts of all labs into a single, easy-to-use tool. I also wanted that infrastructure to be reliable, trackable and secure:

I am strongly attached to the idea that science should be universal, open, transparent and reproducible. I’d like to see labmon become a modest contribution to that effort: in the same way as any published experimental result should be delivered with the details of the experimental techniques/material used and the raw data, we could imagine that it also should be delivered with a snapshot of the lab status and health during the acquisitions.

🎉 The first beta is out, which means the feature set has settled even though the interfaces may still move.

Core idea of the stack

Readings go into a time-series database and come out on a dashboard that anyone in the room can open in a browser. Underneath it is InfluxDB and Grafana — neither of which I am reimplementing — wired together and pre-configured, so the part you would otherwise spend a fortnight on is already done.

Units, checked before anything is stored

labmon is unit-aware, and supports a calibration layer for the sensors it fetches from.

A board sends counts. What a count means lives on the computer, in a small text file:

[channels.A0]
sensor_id = "cryo-77k"
measurement = "temperature"
conversion_factor = "42.5 kelvin / volt"

That declaration is dimensional. volt × (kelvin/volt) yields kelvin, and it yields it by analysis rather than by convention — so a factor declared in the wrong units does not quietly produce a plausible number. Every conversion is trial-applied when the file loads, which means a dimensional mistake fails at startup with a message, rather than after a week of recording.

Real sensors are not always linear, so there are five calibration modes: linear, affine, spline, piecewise-linear, and arbitrary expressions. Recalibrating consists simply of editing a text file, and all the calibrations are tracked and recorded — tied to a unique and automatically generated hash — making the future log archaeology easy, and letting you return to a previous calibration state in one edit.

Not losing data

A single write to InfluxDB costs about a second, because the database waits until the data is safely on disk before answering. Sampling at any useful rate through a call like that is impossible, so nothing samples through it: readings go onto a queue, and a background thread batches them out. That is roughly a millisecond per point, and the acquisition loop never waits on the network at all.

The same indirection covers outages. A server restart or a network hiccup is retried with backoff while sensors keep sampling, and the backlog drains afterwards. Readings that genuinely cannot be written are counted and reported, so a channel that has gone quiet is distinguishable from a channel that is fine and reading zero — which are very different situations but would look identical on a flat trace.

Why logs are in scope

Measurements tell you a reading stopped. They never tell you why. The reason is in some container’s output — an expired token, an instrument reporting busy, a traceback from a vendor SDK — and by default that is reachable only by running docker compose logs on the right machine, with no history.

So all the logs are collected as well — into the same central server that runs the labmon stack. They can be browsed and filtered into the same Grafana instance. A detail I think earns its place: a log line is labelled by the reading it describes, not the process that emitted it. One script reading six channels produces six labelled streams, so {sensor_id="cryo-77k"} finds that sensor’s logs whether it runs on the server, on a Raspberry Pi across the lab, or under systemd on a machine that cannot run containers.

⚠️ What the beta does not cover

The serial acquisition path has never met a physical board. It is tested end to end against a virtual serial port and against a feeder speaking the firmware’s wire format, but the last centimetre — an actual Arduino Due on an actual USB port — is unproven. Check readings against a known voltage before trusting them.

There is also no simple export command yet. For now, getting data out means a SQL query or Grafana’s per-panel CSV… Not nice, I know, but a labmon-export CLI is on the way.

Update, 25 August 2026 — it landed, and it is on main: labmon export and labmon query take the same four selection flags and write CSV, Parquet, Feather or netCDF, one row per reading with its unit carried as a column rather than as metadata — because both pandas and polars drop Arrow schema metadata on read, and kelvin silently believed to be celsius is exactly the kind of mistake the rest of the stack exists to prevent.

🎯 What 1.0 would mean

Verified against real hardware, and running in at least two different labs for at least six months.

That is a deliberately slow bar. Monitoring software is trusted by default once installed — nobody re-derives whether the number on the dashboard is right — and the failure mode of getting it wrong is a plausible-looking number that is quietly incorrect. I would rather the version number stayed honest about how much of this has been proven outside my own bench.

If you run it, I would like to hear what broke. Issues and discussion are on GitHub.


Share this post:

Previous Post
Notes, starting here