---
myst:
html_meta:
description: "Constructing instruments from JSON config files instead of Python setup code"
---
# Config Files
{.lead}
Constructing instruments from JSON config files instead of Python setup code
Every config-driven category (`InstroPSU`, `InstroDMM`, `InstroELoad`, `InstroAWG`, `InstroScope`, …) can be constructed directly from a JSON config file, a plain dict, or an already-built config object, instead of `driver=...` Python setup code:
```python
psu = InstroPSU(config="bench_psu.json")
psu = InstroPSU(config={"device": {"name": "bench_psu"}, "driver": {...}, ...})
psu = InstroPSU(config=my_psu_config)
```
`config` is mutually exclusive with `driver`/`num_channels`. A passed-in config object is copied up front, so later edits to the object you passed don't reach the instrument.
## Shape shared by every category
```json
{
"version": 1,
"instrument": "InstroPSU",
"device": {
"name": "bench_psu",
"description": "Bench channel 1 supply",
"manufacturer": "B&K Precision",
"model": "9115"
},
"driver": {
"name": "BK9115",
"num_channels": 1,
"connection_type": "visa",
"visa": { "visa_resource": "USB0::0x2A8D::0x0101::MY12345::INSTR" }
},
"timing": { "poll_interval": 1.0 },
"publishers": [
{ "type": "NominalCorePublisher", "dataset_rid": "" }
]
}
```
- **`device.name`** is required and used as the channel-name prefix when publishing. `description`, `manufacturer`, and `model` are optional descriptive metadata about the physical instrument.
- **`driver.name`** must match one of that category's registered vendor/model keys. The `visa` block accepts every `VisaConfig` field, so a non-default backend, timeout, or serial setting can be set from JSON too.
- **`driver.num_channels`** is required by the multi-channel categories (PSU, Scope, AWG) and rejected by the single-channel ones (DMM, ELoad). The `driver` block is category-specific, not a shared component: each category validates it strictly, so a config copied between categories will not validate unchanged.
- **`timing.poll_interval`** is optional and sets the background daemon's polling interval. Some categories require an initial-state block to be present before `timing` is meaningful (see each category below).
- **`publishers`** is optional and accepts a list of `NominalCorePublisher` and/or `FilePublisher` entries, each tagged by `type`.
- **`autostart=True`** opens the connection and starts background polling immediately: `InstroPSU(config="bench_psu.json", autostart=True)`.
- Every category's config model is validated strictly: fields outside the documented set are rejected, so there's no `**kwargs`-style escape hatch from JSON. Set default tags via the direct Python constructor instead.
Options the concrete driver doesn't support raise `NotImplementedError` when the config is applied, exactly as the equivalent runtime call would.
## Category-specific state blocks
Beyond the shared shape above, some categories accept an optional block that sets initial instrument state through the same public setters a manual call would use, so it publishes the same `.cmd` channels.
### PSU
`InstroPSU`'s config has no initial-state block — only `device`, `driver`, `timing`, and `publishers`. See the [PSU guide](/psu.md#creating-an-instropsu) for a full example.
### DMM
An optional `measurement` block:
- **`function`** (required within the block): one of `DC_VOLTAGE`, `AC_VOLTAGE`, `DC_CURRENT`, `AC_CURRENT`, `TWO_WIRE_RESISTANCE`, `FOUR_WIRE_RESISTANCE`.
- **`digits`**: resolution in digits. Omit to keep the instrument default. Driver support varies: the Agilent 34401A accepts it, while the Keysight 34461A rejects it in favor of `aperture_nplc`.
- **`aperture_nplc`** / **`aperture_seconds`**: integration time, in power-line cycles or seconds. At most one of the two.
- **`range`**: a number for a manual range in the function's units, or `"auto"` for auto-range. Omit to keep the instrument default.
Because background polling cannot start without a measurement function, a config that declares `timing` must also declare `measurement`.
### ELoad
An optional `load` block:
- **`mode`** (required within the block): one of `CC`, `CV`, `CP`, `CR`. Required because level and range cannot be set before a mode.
- **`level`**: operating level in the mode's units (CC: A, CV: V, CP: W, CR: Ω). Omit to keep the instrument default.
- **`range`**: operating range in the mode's units. Driver support for `range` on `CP`/`CR` modes varies by vendor.
- **`slew_rate`**: `{"direction": "RISE" | "FALL" | "BOTH", "rate": }`, mapping to `set_slewrate`.
:::{note}
The config never enables the input. The `load` block pre-arms the setpoint; enabling the input stays an explicit runtime call (`eload.output_enable(True)`). The polled measurements (`get_voltage`, `get_current`) work regardless of mode, so `timing` is valid without a `load` block for passive monitoring.
:::
### AWG
A required, non-empty `channels` dict, keyed by channel number as a string. Each entry is applied once per session on `open()` (a `close()` + `open()` cycle reapplies it):
- **`waveform`** (required): one of `sine`, `square`, `sawtooth`, `triangle`, `pulse`, `arbitrary`, or `static_value`, discriminated by a `shape` field. `arbitrary`'s `samples` accepts an inline array of at least 2 floats normalized to `[-1.0, 1.0]`, or a string path to a CSV file (read fresh on every `open()`; a relative path resolves against the process's working directory, not the config file).
- **`amplitude`** (optional): `{"value": , "unit": "VPP" | "VP" | "VRMS" | "DBM"}`; `unit` defaults to `VPP`.
- **`offset`** (optional): DC offset in volts.
- **`modulation`** / **`burst`** / **`sweep`** (optional): at most one may carry `"enable": true` per channel. See `set_modulation`, `set_burst*`, `set_sweep*` for the field semantics.
`output_enable` cannot be set from the config; call `awg.output_enable(channel, True)` explicitly after `open()`.
### Scope
Optional `channels`, `acquisition`, and `trigger` blocks, applied through the public setters when `open()` runs, in dependency order (coupling/attenuation before scale, trigger before acquisition, `average_count` before `mode`). If `acquisition.start_acquisition_on_open` is set, `run()` fires before the `acquisition` block applies, since some scopes only accept acquisition-mode changes while running.
- **`channels`**: per-channel initial state keyed by 1-based channel number. Accepts `vertical_scale`, `vertical_offset`, `coupling` (`"AC"`/`"DC"`), `probe_attenuation`, and a `measurements` list to poll from the background daemon.
- **`acquisition`**: `mode`, `average_count` (requires `mode: "AVERAGE"`), `horizontal_scale`, `start_acquisition_on_open` (default `false`).
- **`trigger`**: `source` (required within the block), `type`, `level`, `slope`, `mode`.
After the blocks apply, `open()` calls `sync_configuration()` and logs one warning per field the instrument reports differently from the config (scopes snap requested values to the nearest supported step). Trigger fields are not checked, since `sync_configuration()` does not read them back. `autostart` (host-side daemon) and `acquisition.start_acquisition_on_open` (instrument acquiring) are independent knobs.
See the [Scope guide](/scope.md#creating-an-instroscope) for a full example.