The Chi.Bio operating system: a Flask web app that controls Chi.Bio bioreactors over an I2C bus. It runs on the device (a BeagleBone Black), not on a dev machine — see https://chi.bio/software/.
This is a fork of HarrisonSteel/ChiBio,
refactored and hardened. The original single-file app.py (~2300 lines) was split
into the modules below and retired; it remains in git history if ever needed.
-
Modular layout. The monolith is split into
app.py(Flask routes + output control) plus focusedchibio_*.pymodules (state, hardware/I2C, optics, measurements, experiment loops, control helpers). SeeCLAUDE.mdfor the full architecture map. -
Robustness fixes. Long-running control loops are persistent threads instead of being re-spawned every iteration; the I2C/CSV lock is released via
try/finally; a pre-existingCharacteriseDevicecrash is fixed. -
Token auth for remote access (see below), with zero-touch USB access preserved.
-
I2C on
smbus2. The device layer was migrated offAdafruit_GPIOto a smallsmbus2wrapper inchibio_hardware.py. GPIO/PWM still useAdafruit_BBIO. -
Pinned dependencies (
requirements.txt) and an EOL-safe provisioning path. -
UI: self-hosted uPlot charts (no CDNs), a dark-mode toggle, a per-chart ln y-axis toggle (true base e — exponential growth reads as a straight line, slope = μ; on the OD/FP/growth charts), legends that show the latest value at rest and the cursor value on hover, and a fluorescence-assist panel (scan → recommended excite/emit bands → apply to an FP slot).
-
Data integrity. Sensor-read failures set a
validflag and logNaNonly in the CSV (never a rawNaNin the live state); OD/FP take a 3× median + spread; a per-experiment metadata sidecar (<start>_<M>_meta.json) plus a mid-run events log (<start>_<M>_events.json) make each dataset self-describing when FP bands or the OD blank change during a run. A near-saturation guard flags fluorescence reads whose CLEAR base rides the ADC ceiling. -
Simulation mode.
CHIBIO_SIM=1fakes the I2C bus and runs the real initialisation and control code on top of it, so the whole UI — charts, controls, running experiments — works with no reactors attached, and on an ordinary laptop. See Running without hardware.
By default you cannot run this on a macOS/Linux dev machine. Importing
app.pytriggersinitialiseAll(), which talks to GPIO/I2C hardware and spawns the watchdog. Edit on your machine; run on the device — or setCHIBIO_SIM=1to run the interface anywhere (see below). The device remains the reference you design against; the simulator is not a substitute for it.
app.py— Flask routes and the core output-control functions.chibio_state.py— the shared global dicts (sysData,sysDevices,sysItems) and the globallock.chibio_hardware.py—I2CCom, the single chokepoint for all I2C traffic; PWM; watchdog.chibio_optics.py/chibio_measurements.py— AS7341 spectrometer reads; OD/FP/temperature.chibio_experiment.py— the long-running control threads (main loop, thermostat, pumps, turbidostat, zigzag).chibio_control_helpers.py— user-editable optogenetic programs, CSV logging, metadata/events sidecars, downsampling.chibio_fluorescence.py— the fluorescence configuration assist (excitation scan → recommended FP settings).chibio_auth.py— token/cookie auth and the trusted-subnet rules.chibio_sim.py— theCHIBIO_SIM=1simulator: a fake I2C bus plus substituted optics, with a culture and thermal model behind them.templates/+static/— the browser UI (polls/getSysdata/over AJAX).
Runs on the Chi.Bio Debian 10.5 / Linux 4.19 image with Python 3.7. Two paths:
- Golden image (preferred). Flash once, provision once, then snapshot the
eMMC/SD and restore that image forever after. See
make-golden-image.md. This removes the dependency on EOL apt at provisioning time. setup.sh(fresh provision). Repoints apt atarchive.debian.org(Debian 10 "buster" is EOL and left the main mirrors), installs the pinned deps fromrequirements.txt, builds the bundledAdafruit_BBIO-1.2.0.tar.gzfrom source against the baked kernel, and copiesapp.py+ thechibio_*.pymodules,static/, andtemplates/into~/chibio. Run once on a fresh board.
Adafruit_BBIO is intentionally not in requirements.txt — it is kernel-matched
and built from the tarball, not installed from PyPI.
./cb.sh starts the server:
gunicorn -b 0.0.0.0:5000 app:application
Binding 0.0.0.0 serves the UI on both the USB point-to-point link and the LAN.
To run detached, either uncomment the screen line in cb.sh, or start it under
tmux (installed on the device) and leave cb.sh in the foreground:
tmux new-session -d -s chibio "bash -lc './cb.sh 2>&1 | tee /tmp/chibio.log'"
tmux attach -t chibio # watch it
./cb-stop.sh stop 5000 # stop it — never hand-roll ss|kill
Set CHIBIO_TOKEN to require an X-Auth-Token header (or ?token=) on remote
requests. cb.sh reads it from a gitignored .chibio_token file next to the script
(root-only, 600); with no token set, all remote access is denied (fail closed).
- USB point-to-point (192.168.7.x / 192.168.6.x) and loopback are trusted — no token needed.
- Remote / LAN: open once with
?token=…. The device sets an HttpOnlychibio_tokencookie (30-day), and the browser then sends it automatically on every request, including all control POSTs — no per-request token handling in the UI.
Caveat: HTTP-only (no TLS), so the token is visible to anyone sniffing the wire.
This is LAN access control, not wire encryption. See chibio_auth.py.
The device run dir is a git checkout, but it is not updated via git pull — the
board doesn't fetch from GitHub. Deploy by rsync from your machine:
rsync -az --delete \
--exclude='.DS_Store' --exclude='.chibio_token' --exclude='.claude' \
--exclude='selftest-*.json' --exclude='.c9' --exclude='__pycache__' \
-e ssh ./ <device>:/root/chibio/
Always exclude .chibio_token (or --delete wipes the device's secret), the
selftest-*.json artifacts, and the device's untracked .c9/ (Cloud9 IDE dir).
.git is deliberately not excluded: syncing it keeps the run dir's git ref
pointing at exactly what is deployed, which is where the metadata sidecar reads its
commit hash from. Deploy to /root/chibio-staging first, verify there, then promote
to /root/chibio.
device_selftest.py <label> exercises every I2C code path against a running server
on the device and dumps a per-device snapshot to selftest-<label>.json, so a
"before" run can be diffed against an "after" run. Use it to confirm sensor-path
changes don't silently regress readings. It safely discovers connected reactors
first (via /scanDevices/all + presentDevices) rather than measuring absent
devices, which would trip the watchdog kill.
There is no linter and no build step. A suite of ten test_*.py files runs
off-device, covering the logic that doesn't need hardware — CSV schema,
metadata/events sidecars, read-validity, auto-ranging, replicate aggregation, the FP
saturation guard, the fluorescence analysis, and the simulator itself:
python3 -m venv v && v/bin/pip install flask numpy smbus2 simplejson
CHIBIO_MOCK_HW=1 v/bin/python test_fluorescence.py # (and the other test_*.py)
test_sim.py reaches furthest: because CHIBIO_SIM fakes only the bus, it exercises
the real initialisation, presence-scan and measurement code end-to-end without a
board (it sets its own env vars, so the invocation above still works).
Hardware paths still need the device (device_selftest.py, above). The device
is otherwise the reference you design against — CHIBIO_MOCK_HW is only an import
shim, not a development target.
Two situations where the server won't otherwise start: a controller with no
reactors plugged in (nothing on the bus answers, so the I2C multiplexer never
ACKs, the watchdog kills the process, and gunicorn reports App failed to load
with exit code 4 — that is the watchdog working, not a bug), and an ordinary
laptop with no BeagleBone at all. CHIBIO_SIM=1 handles both:
CHIBIO_SIM=1 ./cb.sh # on the device
CHIBIO_SIM=1 python3 -c "import app; app.application.run(port=5000)" # on a laptop
It fakes the bus — a stand-in smbus2.SMBus for the multiplexer, both MCP9808
thermometers, the IR thermometer, the DAC and the two PWM chips, plus substituted
AS7341 optics — and then runs the real initialiseAll() on top. So the presence
scan, LED V1/V2 detection (and the FP3 excitation remap that follows from it), OD
calibration and dark correction, the FP ratio and its near-saturation guard, the
thermostat, the turbidostat, CSV logging and the fluorescence assist are all the
actual product code. Behind the fake optics sits a logistic culture model — diluted
by whatever the input pump is really doing — and a first-order heater model driven by
the real heat output, so experiments genuinely run and the control loops close.
| Variable | Default | Meaning |
|---|---|---|
CHIBIO_SIM_LED_VERSION |
2 |
LED board version to present (V1 and V2 expose different excitation panels) |
CHIBIO_SIM_REACTORS |
M0,M1,M2,M3,M4 |
Which reactors answer; the rest scan absent through the real failure path |
CHIBIO_SIM_HOURS |
12 |
Hours of synthetic history pre-loaded so charts open with data; pressing Start resumes into it |
CHIBIO_SIM_SEED |
1 |
Otherwise deterministic |
Simulated reactors carry a SIM- device ID and a SIMULATION MODE - no hardware attached terminal line, so a screenshot can't be mistaken for real hardware.
This is not a substitute for the device. It is a fake bus with a plausible culture behind it: it cannot tell you what real readings look like, which LEDs are actually fitted, or how the optics behave. Use it to work on the interface when the rig is unavailable — not to design against.