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:
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
device.nameis required and used as the channel-name prefix when publishing.description,manufacturer, andmodelare optional descriptive metadata about the physical instrument.driver.namemust match one of that category’s registered vendor/model keys. Thevisablock accepts everyVisaConfigfield, so a non-default backend, timeout, or serial setting can be set from JSON too.driver.num_channelsis required by the multi-channel categories (PSU, Scope, AWG) and rejected by the single-channel ones (DMM, ELoad). Thedriverblock is category-specific, not a shared component: each category validates it strictly, so a config copied between categories will not validate unchanged.timing.poll_intervalis optional and sets the background daemon’s polling interval. Some categories require an initial-state block to be present beforetimingis meaningful (see each category below).publishersis optional and accepts a list ofNominalCorePublisherand/orFilePublisherentries, each tagged bytype.autostart=Trueopens 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.
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 for a full example.
DMM
An optionalmeasurement block:
function(required within the block): one ofDC_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 ofaperture_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.
timing must also declare measurement.
ELoad
An optionalload block:
mode(required within the block): one ofCC,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 forrangeonCP/CRmodes varies by vendor.slew_rate:{"direction": "RISE" | "FALL" | "BOTH", "rate": <A/µs>}, mapping toset_slewrate.
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-emptychannels 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 ofsine,square,sawtooth,triangle,pulse,arbitrary, orstatic_value, discriminated by ashapefield.arbitrary’ssamplesaccepts 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 everyopen(); a relative path resolves against the process’s working directory, not the config file).amplitude(optional):{"value": <float>, "unit": "VPP" | "VP" | "VRMS" | "DBM"};unitdefaults toVPP.offset(optional): DC offset in volts.modulation/burst/sweep(optional): at most one may carry"enable": trueper channel. Seeset_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
Optionalchannels, 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. Acceptsvertical_scale,vertical_offset,coupling("AC"/"DC"),probe_attenuation, and ameasurementslist to poll from the background daemon.acquisition:mode,average_count(requiresmode: "AVERAGE"),horizontal_scale,start_acquisition_on_open(defaultfalse).trigger:source(required within the block),type,level,slope,mode.
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 for a full example.