A clean rewrite of the Chi.Bio open-source bioreactor
control system. The original app.py was a single ~100 KB Flask file with
hardware I/O, control loops, and view logic mixed throughout. This repo is
a from-scratch reimplementation behind a hexagonal (ports & adapters)
boundary so the same control algorithms can run unchanged on a BeagleBone
Black with real hardware or on a laptop against a physics simulator.
- Hardware abstraction. Control code depends only on the
BioreactorUnitPort/BioreactorFleetPortprotocols. The Flask app never imports an adapter class directly. - Run offline. A pure-Python
SimulatedBioreactorFleetwith logistic growth (Beer–Lambert transmittance) and Newton's-law-of-cooling thermal models lets the entire stack run on any platform. - No global state. Per-unit state lives in
UnitExperimentStatedataclasses; the oldsysData[M][...]global dict is gone. - Tested. Unit, integration, regression (against real M1 experiment data), and Playwright UI tests — all runnable in CI without hardware.
- Modern UI. Alpine.js + Chart.js (both vendored for offline use), replacing the jQuery + Google Charts frontend.
┌──────────────────────────────────────────────────────────────┐
│ app_new.py Flask routes — thin, no I/O │
├──────────────────────────────────────────────────────────────┤
│ chibio.experiment ExperimentRunner, UnitExperimentState, │
│ DataRecorder, PumpScheduler │
├──────────────────────────────────────────────────────────────┤
│ chibio.domain ODController (turbidostat / chemostat / │
│ custom), ThermostatController, types │
├──────────────────────────────────────────────────────────────┤
│ chibio.ports BioreactorUnitPort, BioreactorFleetPort │ ← boundary
├──────────────────────────────────────────────────────────────┤
│ chibio.adapters SimulatedBioreactorFleet (physics) │
│ I2CBioreactorFleet (TCA9548A + I2C) │
└──────────────────────────────────────────────────────────────┘
chibio.bootstrap.build_fleet() is the only place that picks between
the simulated and real-hardware adapter. It defaults to simulation
unless BEAGLEBONE_I2C=1 is set (or CHIBIO_MODE=hardware).
chibio/
├── domain/ pure logic, no I/O
│ ├── types.py UnitID, LED, Pump, SpectrumReading, …
│ ├── regulate_od.py ODController (turbidostat/chemostat/custom)
│ └── thermostat.py ThermostatController
├── ports/ Protocol definitions (the boundary)
│ └── hardware.py
├── adapters/
│ ├── simulated_adapter.py physics-based fake for dev/CI
│ └── i2c_adapter.py TCA9548A mux + I2C devices
├── experiment/
│ ├── state.py UnitExperimentState (replaces sysData)
│ ├── runner.py ExperimentRunner (threaded cycle loop)
│ ├── data_recorder.py CSV/JSON snapshots
│ └── pump_scheduler.py
└── bootstrap.py composition root
app_new.py Flask app wiring everything together
static/ Alpine.js, Chart.js (vendored), chibio.js, chibio.css
templates/ index_new.html (Alpine + Jinja macros)
tests/ pytest suites + Playwright UI tests
Requires Python ≥ 3.10.
git clone <this-repo> chibio
cd chibio
python -m venv .venv
source .venv/bin/activate
pip install -e .
pip install flask pytestpython app_new.pyOpen http://localhost:5000. All 8 simulated units (M0–M7) come up populated with the physics model. Start a turbidostat / chemostat experiment from the UI and watch the OD trace evolve in real time.
On a real Chi.Bio device:
BEAGLEBONE_I2C=1 python app_new.py
# or: CHIBIO_MODE=hardware python app_new.pyThe bootstrap probes each of M0–M7 over the TCA9548A multiplexer and skips any unit that doesn't respond, so partially-populated devices work.
pytest # unit + integration + regression
pytest -k regression # only the real-data regression suite
pytest tests/test_ui.py # static-asset / route smoke testsBrowser tests (require Playwright + a running app) live in
tests/playwright_ui_test.py:
pip install playwright
playwright install chromium
python tests/playwright_ui_test.py| Env var | Effect |
|---|---|
CHIBIO_MODE |
simulation or hardware — overrides auto-detection |
BEAGLEBONE_I2C=1 |
Force hardware mode (legacy alias) |
CHIBIO_DATA_DIR |
Where experiment CSV / JSON snapshots are written (default .) |
This is an in-progress rewrite. The original app.py is the authority
on what the device actually does on the bench; this repo's regression
test (tests/test_regression.py) replays a real M1 dataset through
the new controllers to make sure behaviour matches before any unit is
switched over.