|
| 1 | +--- |
| 2 | +title: Customize your Pioreactor hardware |
| 3 | +slug: /developer-guide/custom-hardware |
| 4 | +description: Extend or override Pioreactor hardware definitions by layering YAML files that hardware.py reads. |
| 5 | +hide_table_of_contents: true |
| 6 | +--- |
| 7 | + |
| 8 | +Pioreactor's hardware layer is intentionally data-driven. Everything in [`core/pioreactor/hardware.py`](https://github.com/pioreactor/pioreactor/blob/main/core/pioreactor/hardware.py) loads user-editable YAML files from `~/.pioreactor/hardware/` (or the folder pointed to by the `DOT_PIOREACTOR` env var). By editing these files you can rewire pins, add new peripherals, or describe an entirely new bioreactor model without touching the Python code. Pair these configs with [custom bioreactor model definitions](/custom-bioreactor-models) so the UI, safety limits, and wiring stay in sync. |
| 9 | + |
| 10 | +## How the loader works |
| 11 | + |
| 12 | +`hardware.py` deep-merges two directories for each **mod** (subsystem): |
| 13 | + |
| 14 | +1. `~/.pioreactor/hardware/hats/<hat_version>/<mod>.yaml` – physical wiring that ships with a specific HAT revision (`hardware_version_info`). |
| 15 | +2. `~/.pioreactor/hardware/models/<model>/<version>/<mod>.yaml` – intent for a particular bioreactor model returned by `get_pioreactor_model()`. |
| 16 | + |
| 17 | +Later layers override earlier ones. Missing files are allowed; missing keys throw `HardwareError`s when helpers such as `get_pwm_to_pin_map()` run. |
| 18 | + |
| 19 | +Common mods defined inside `hardware.py` are: |
| 20 | + |
| 21 | +- `pwm.yaml` (`get_pwm_controller`, `get_heater_pwm_channel`, `get_pwm_to_pin_map`) |
| 22 | +- `gpio.yaml` (`get_pcb_led_pin`, `get_hall_sensor_pin`, `get_sda_pin`, `get_scl_pin`) |
| 23 | +- `temp.yaml` (`get_temp_address`, used by `is_heating_pcb_present`) |
| 24 | +- `adc.yaml` (`get_adc_curriers`, which subsequently powers PD channel helpers) |
| 25 | +- `dac.yaml` (`get_dac_address`) |
| 26 | + |
| 27 | +You can add additional mods with any key/value structure—`get_layered_mod_config("my_mod")` will still combine the YAML files. |
| 28 | + |
| 29 | +## Step-by-step: add a custom hardware profile |
| 30 | + |
| 31 | +1. **Create directories** for your HAT and model version. |
| 32 | + ```bash |
| 33 | + mkdir -p ~/.pioreactor/hardware/hats/<hat_version> |
| 34 | + mkdir -p ~/.pioreactor/hardware/models/<model_name>/<model_version> |
| 35 | + ``` |
| 36 | + Use `pioreactor.version.hardware_version_info` (for hats) and your `pioreactor.models` entry for models. |
| 37 | + |
| 38 | +2. **Describe the wiring** (hat layer). Example `~/.pioreactor/hardware/hats/0.2/pwm.yaml`: |
| 39 | + ```yaml |
| 40 | + controller: hat_mcu |
| 41 | + heater_pwm_channel: "5" |
| 42 | + pwm_to_pin: |
| 43 | + "1": 17 |
| 44 | + "2": 13 |
| 45 | + "5": 18 |
| 46 | + ``` |
| 47 | + These keys map directly to `_load_pwm_cfg()` in `hardware.py`. |
| 48 | + |
| 49 | +3. **Describe the capabilities** (model layer). Suppose you added a new auxiliary photodiode ADC: |
| 50 | + ```yaml title="~/.pioreactor/hardware/models/my_model/v1.0/adc.yaml" |
| 51 | + pd1: |
| 52 | + driver: ADS1115 |
| 53 | + address: 0x48 |
| 54 | + channel: 0 |
| 55 | + aux: |
| 56 | + driver: pico |
| 57 | + address: 0x40 |
| 58 | + channel: 0 |
| 59 | + salty: |
| 60 | + driver: ADS1115 |
| 61 | + address: 0x49 |
| 62 | + channel: 2 |
| 63 | + ``` |
| 64 | + `get_adc_curriers()` will automatically expose `.keys()` for every entry (`pd1`, `aux`, `salty`). Downstream helpers, like `get_available_pd_channels()`, treat `pd*` names as photodiode channels. |
| 65 | + |
| 66 | +4. **Reference new mods in code**. Anywhere in your plugin/automation you can import: |
| 67 | + ```python |
| 68 | + from pioreactor.hardware import get_layered_mod_config |
| 69 | +
|
| 70 | + stir_cfg = get_layered_mod_config("stirrer") # merges hats/<hat>/stirrer.yaml and models/<model>/stirrer.yaml |
| 71 | + if stir_cfg.get("controller") == "pico": |
| 72 | + ... |
| 73 | + ``` |
| 74 | + This keeps custom hardware logic outside Pioreactor core. |
| 75 | + |
| 76 | +## Tips for customization |
| 77 | + |
| 78 | +- Use integers for all addresses and pins; `hardware.py` casts everything via `int()` and will raise if parsing fails. |
| 79 | +- Keep YAML minimal—only override keys that differ from the hat defaults to take advantage of deep-merge layering. |
| 80 | +- The helper predicates (`is_ADC_present`, `is_heating_pcb_present`, etc.) are safe ways to gate optional hardware in your jobs. |
| 81 | +- Store supporting scripts, cad files, or calibration notes under `~/.pioreactor/hardware/<...>/README.md` so teammates know how to reproduce the wiring. |
| 82 | + |
| 83 | +Once you describe the hardware this way, the rest of the stack—jobs, automations, plugins—just asks `hardware.py` for the capabilities it needs, so new hardware becomes a configuration exercise instead of a fork of the core codebase. |
0 commit comments