This is v1 of the migration guide. It covers four changes that shipped in
the same migration window: the mechanical rename from
nominal-instro to
instro (automated via the codemod), the v1.0
channel-naming standardization (no code change required: set
legacy_naming=True
on each instrument to preserve the pre-v1.0 wire format),
NI physical channel naming (NI DAQmx channels
must now be fully qualified, with no compatibility flag), and the
DAQ digital channel configuration split
(configure_digital_channel becomes configure_digital_line /
configure_digital_port).What changed
The Python packagenominal-instro has been renamed to instro. This affects:
- PyPI distribution names:
nominal-instro→instro, and every workspace package (nominal-instro-daq-ni,nominal-instro-daq-labjack, etc.) drops itsnominal-prefix. - Python import path:
from nominal_instro...→from instro.... - Class names: instrument facades drop the
Nominalprefix in favor ofInstro(e.g.NominalDAQ→InstroDAQ). The base classNominalInstrumentbecomesInstrument.NominalI2CandNominalModbusbecomeI2CInterfaceandModbusDevice(bare protocol names, noInstroprefix: see Folder layout). - Folder layout: the library now uses a category-first layout: each category owns its instrument class and its drivers together (
instro/daq/,instro/dmm/, etc.), and I2C and Modbus lift to their own top-level packages because they are protocols, not role-abstractions. See Folder layout & import paths below.
Nominal prefix because the prefix refers to the Nominal platform they integrate with, not the package namespace:
NominalCorePublisherNominalConnectPublisher
Package rename table
Old (<=0.6.x) | New (>=0.7) |
|---|---|
nominal-instro | instro |
nominal-instro-daq-ni | instro-daq-ni |
nominal-instro-daq-labjack | instro-daq-labjack |
nominal-instro-daq-mcc | instro-daq-mcc |
nominal-instro-i2c-aardvark | instro-i2c-aardvark |
nominal-instro-unstable | instro-unstable |
nominal-instro-ethernetip-python | instro-ethernetip-python |
pip install nominal-instro[daq]) keep the same extras names, just on the new package: pip install instro[daq].
Folder layout & import paths
The oldnominal_instro/instruments/<category>/ + nominal_instro/drivers/<category>/<vendor>/ split has been collapsed into one folder per category, with concrete drivers living under each category’s own drivers/ subfolder. Two protocols (I2C and Modbus) leave instruments/protocols for top-level packages because the protocol is the device identity, with no swappable wire format underneath.
| Old import | New import |
|---|---|
from nominal_instro.instruments.daq import NominalDAQ | from instro.daq import InstroDAQ |
from nominal_instro.instruments.daq.driver import DAQDriverBase | from instro.daq import DAQDriverBase |
from nominal_instro.drivers.daq.keysight import Keysight34980A | from instro.daq.drivers import Keysight34980A |
from nominal_instro.drivers.daq.labjack import LabJackTSeriesDriver | from instro.daq.drivers.labjack import LabJackTSeriesDriver |
from nominal_instro.instruments.dmm import NominalDMM | from instro.dmm import InstroDMM |
from nominal_instro.drivers.dmm.keithley import Keithley2400 | from instro.dmm.drivers import Keithley2400 |
from nominal_instro.instruments.eload import NominalELoad | from instro.eload import InstroELoad |
from nominal_instro.drivers.eload.bk import BK85XXB | from instro.eload.drivers import BK85XXB |
from nominal_instro.instruments.psu import NominalPSU | from instro.psu import InstroPSU |
from nominal_instro.drivers.psu.rigol import RigolDP800 | from instro.psu.drivers import RigolDP800 |
from nominal_instro.instruments.i2c import NominalI2C | from instro.i2c import I2CInterface |
from nominal_instro.drivers.i2c.totalphase import Aardvark | from instro.i2c.drivers.totalphase import Aardvark |
from nominal_instro.protocols.modbus import NominalModbus | from instro.modbus import ModbusDevice |
from nominal_instro.lib import VisaDriver, Instrument | from instro.lib import VisaDriver, Instrument |
from nominal_instro.protocols.common_types import DeviceInfo | from instro.lib.types import DeviceInfo |
- Each category root (
instro.daq,instro.dmm, …) re-exports the instrument class and the category base driver, so custom-driver authors only need to remember one import path per category. - Concrete drivers shipped in the main
instrodistribution live flat under<category>/drivers/(no per-vendor subfolder). Workspace packages likeinstro-daq-labjackkeep their vendor folder (instro.daq.drivers.labjack) because that’s where their__init__.pylives. instro.libis the home of cross-category building blocks:VisaDriver,Instrument,Measurement,LinearScale, etc.
Class rename table
| Old | New |
|---|---|
NominalDAQ | InstroDAQ |
NominalDMM | InstroDMM |
NominalPSU | InstroPSU |
NominalI2C | I2CInterface |
NominalELoad | InstroELoad |
NominalModbus | ModbusDevice |
NominalScope | InstroScope |
NominalInstrument | Instrument |
NominalInstrumentationErrorCodes | InstrumentationErrorCodes |
NominalCorePublisher | (unchanged) |
NominalConnectPublisher | (unchanged) |
Removed classes
These facade classes were removed entirely as part of the INSTRO-231 driver-shape refactor. They have no replacement. The codemod deliberately leaves these names unchanged so you hit a clearImportError and can migrate away from the facade pattern (drivers now compose VisaDriver directly).
| Removed | Removed in | Migration |
|---|---|---|
NominalDMMFacade | INSTRO-158 | Drop the facade. Driver methods take the configuration they need directly. |
NominalPSUFacade | INSTRO-157 | Drop the facade. Driver methods take the configuration they need directly. |
NominalI2CFacade | INSTRO-156 | Drop the facade. Driver methods take the configuration they need directly. |
NominalScopeFacade | INSTRO-171 | Drop the facade. Driver methods take the configuration they need directly. |
NominalDAQFacade | INSTRO-311 | Drop the facade. Drivers track their own channels and timing state. |
Automated codemod
instro ships a standalone Python script that applies all of the above renames to your codebase. It requires no dependencies beyond the standard library and works on any Python 3.10+ installation.
1. Download the script
2. Preview the changes
Always start with a dry run so you can review what will change.3. Apply the changes
git diff the result and revert with git checkout . if anything looks wrong.
4. Reinstall and verify
What the codemod handles
The script walks every.py, .pyi, .toml, .md, .mdx, .yml, .yaml, .txt, .cfg, and .ini file (skipping .venv, .git, target, dist, build, __pycache__, .mypy_cache, node_modules, .tox, .pytest_cache) and applies the literal package, import, and class renames. The renames are applied in a fixed order to avoid prefix collisions: for example, NominalInstrumentationErrorCodes is rewritten before NominalInstrument so the longer name isn’t partially mangled.
Both NominalCorePublisher and NominalConnectPublisher do not match any rewrite pattern, so they pass through untouched.
The codemod does not touch channel-name strings (see Channel name changes). Those are easier to handle via legacy_naming=True, or by hand with the translation table below if you want to fully migrate now.
What you may need to do by hand
The codemod is mechanical. It will not:- Update lock files. Re-run
uv lock/pip-compile/poetry lockafter the migration. - Rename files or directories in your repo named after
nominal_instro(e.g.my_nominal_instro_helpers.py). Rename those yourself if you have any. - Edit binary files such as compiled artifacts,
.whl,.so, or images. - Adjust CI scripts that wget/curl URLs containing
nominal-instroif they live outside your code tree. - Migrate channel-name string literals in your code (dashboards, publisher filters, test asserts). See Channel name changes below for the recommended path.
Channel name changes
Published channel names changed for PSU, ELoad, I2C, DAQ digital channels, and Scope. The change touches Python source and downstream consumers (dashboards, recorded datasets, alert configs that key off channel names), and the downstream side is the part automation can’t fix.Recommended: set legacy_naming=True and migrate at your own pace
Every category instrument accepts a legacy_naming: bool = False constructor kwarg. Setting it True makes the instrument publish channels under their pre-v1.0 names, with no Python rewrites and no dashboard updates. Migrate the flag-flip first, then update consumers on your schedule.
After the migration
- Every part of a channel name is dot-separated. The legacy underscore separator inside I2C channel keys and the leading bare
{alias}form for DAQ digital channels are gone. - Every command channel ends in
.cmd. Previously I2C used_cmd. That’s been normalized. - Cryptic single-letter suffixes (
_v,_i,_en) on PSU and ELoad telemetry were expanded to descriptive words (.voltage,.current,.enabled). - Scope channels switched their separator from
_to.. The abbreviationprobe_attenwas expanded toprobe_attenuation.
Translation table
{name} is the instrument instance name you pass to the constructor. {N} is the channel number. {periph}, {reg}, {field}, and {alias} come from your own configuration.
| Category | Old | New |
|---|---|---|
| PSU (telemetry) | {name}.ch{N}_v | {name}.ch{N}.voltage |
| PSU (telemetry) | {name}.ch{N}_i | {name}.ch{N}.current |
| PSU (telemetry) | {name}.ch{N}_en | {name}.ch{N}.enabled |
| PSU (commands) | {name}.ch{N}_v.cmd | {name}.ch{N}.voltage.cmd |
| PSU (commands) | {name}.ch{N}_i.cmd | {name}.ch{N}.current.cmd |
| PSU (commands) | {name}.ch{N}_en.cmd | {name}.ch{N}.enabled.cmd |
| ELoad (telemetry) | {name}.ch{N}_v / _i | {name}.ch{N}.voltage / .current |
| ELoad (commands) | {name}.ch{N}_mode.cmd / _level / _short / _range / _slewrate / _enable | {name}.ch{N}.mode.cmd / .level / .short / .range / .slewrate / .enabled |
| I2C (telemetry) | {name}_{periph}_{reg}[_{field}] | {name}.{periph}.{reg}[.{field}] |
| I2C (commands) | {name}_{periph}_{reg}_cmd | {name}.{periph}.{reg}.cmd |
| DAQ (digital read) | bare {alias} | {name}.{alias} |
| DAQ (digital write) | bare {alias} | {name}.{alias}.cmd |
| Scope (telemetry) | {name}.ch{N}_vrms / _vpp / _vscale / _voffset / _coupling / _waveform | {name}.ch{N}.vrms / .vpp / .vscale / .voffset / .coupling / .waveform |
| Scope (telemetry) | {name}.ch{N}_probe_atten | {name}.ch{N}.probe_attenuation |
Migrating Python source by hand
If you’d rather droplegacy_naming=True entirely and embrace the v1.0 names in your code, the PSU/ELoad/Scope renames are mechanical enough to handle with ripgrep. The patterns below are anchored on a leading .ch followed by digits and a trailing word boundary, so bare identifiers like local_ch1_v = 5 are left alone.
sed -E -i '' form is BSD/macOS. On GNU sed, use sed -E -i (no empty string). Run on a clean git working tree so you can review the diff.
I2C and DAQ digital are not regex-friendly. I2C’s {name}_{periph}_{reg} form requires knowing your peripheral/register names to know where to insert dots. DAQ digital’s bare-alias form is indistinguishable from random other string literals. For those, find references manually:
legacy_naming=True is most valuable for.
Custom instrument authors
If you subclassedInstrument to build your own instrument type, the publish pipeline now goes through two decorators on the base class:
@publish_command: wraps methods that return aCommand. Publishes the result and raisesTypeErrorif the method returned the wrong type.@publish_measurement: wraps methods that return aMeasurement, alist[Measurement], orNone. Publishes each item.Noneis passed through.
Instrument build the right object:
self._package_command(channel, data, timestamp, **tags): returns aCommandkeyed under{self.name}.{channel}. Passchannelwith the trailing.cmdso the literal string appears at the call site.self._package_measurement(channel, data, timestamp, **tags): returns aMeasurementkeyed under{self.name}.{channel}. Do not include.cmdinchannel.
_read_to_command / _read_to_measurement helper pattern into your subclass, delete those copies. The base class methods replace them, and the decorators enforce the channel-naming contract at runtime.
NI physical channel naming
This change is separate from the published-name standardization above. It affects thephysical_channel value you pass when configuring a channel, not the name the channel publishes under. legacy_naming does not gate it, and there is no compatibility flag: NI channel strings must be updated before the driver will configure them.
The NI DAQmx driver (NIDAQDriver) no longer prepends its device_id constructor argument to physical_channel. The value now reaches NI-DAQmx verbatim, so it must be the fully qualified name (Dev1/ai0, Dev1/port0/line0) rather than the device-relative form (ai0, port0/line0).
device_id argument stays on the constructor: the driver still uses it to verify the device is present and to name its DAQmx tasks. Channel aliases and published names are unchanged, so dashboards and datasets keyed off aliases need no update. Only the NI driver is affected. LabJack, MCC, and Keysight already passed physical_channel through verbatim.
Pass one fully qualified channel per call. The NI driver hands physical_channel to NI-DAQmx verbatim and configures it as a single channel, so NI-DAQmx range and list syntax (Dev1/ai0:3, Dev1/ai0,Dev1/ai2) is not supported; call configure_analog_channel once per channel instead. Digital ports (Dev1/port0) are supported as a single port channel via configure_digital_port (see DAQ digital channel configuration below). NI’s physical channel naming reference documents how DAQmx names devices, channels, ports, and lines. To confirm the device and channel names on a given system, including ones renamed from the defaults, use NI MAX or the NI Hardware Configuration Utility.
There is no reliable regex for these string literals, the same as DAQ digital aliases above. Find your NI channel strings by hand and prefix each with the device name.
DAQ digital channel configuration
InstroDAQ.configure_digital_channel(...) is gone. It used an optional port_width argument to decide at call time whether you were configuring a single line or a whole port. That one entry point is now two, split by shape:
configure_digital_line(direction, physical_channel, logic, logic_level=None, alias=None)— a single digital line.configure_digital_port(direction, physical_channel, logic, port_width, logic_level=None, alias=None)— a whole port.port_widthis now a required positional argument, not an optional discriminator.
DAQDriverBase subclass, the driver-side contract changed to match. define_digital_channel, configure_di_channel, and configure_do_channel are removed. Implement the four direction/shape methods instead: configure_di_line_channel and configure_do_line_channel (required), plus configure_di_port_channel and configure_do_port_channel (optional — the base raises NotImplementedError, so override only the ones your hardware supports). Each method parses physical_channel and programs the device directly; the old define_* step and the isinstance/port_width dispatch inside the driver are gone.
Rollback
git revert <commit>.
Source
The codemod script lives atdocs/guides/migration/migrate_to_instro.py
in the instro repository. File issues or improvements against that path.