--- myst: html_meta: description: "Try and test instro with no hardware: virtual instruments that run in-process, or bundled SCPI simulators" --- # Simulated Instruments {.lead} Try and test instro with no hardware: virtual instruments that run in-process, or bundled SCPI simulators ## Virtual instruments The quickest way to try `instro`: a virtual driver simulates readings in-process, with no hardware, server, or second terminal. It works with every method of its instrument class. ```python from instro.dmm import InstroDMM from instro.dmm.drivers import VirtualDMM with InstroDMM(name="dmm", driver=VirtualDMM()) as dmm: print(dmm.read_dc_voltage().latest) ``` Virtual drivers: `VirtualDMM`, `VirtualELoad`, `VirtualPSU`, `VirtualScope`. In a JSON config, use `"driver": {"connection_type": "virtual", "name": "VirtualDMM"}` (or `"VirtualELoad"`); a PSU or scope also takes its channel count, as in `"driver": {"connection_type": "virtual", "name": "VirtualPSU", "num_channels": 2}` (or `"VirtualScope"`). ## SCPI simulators `instro` ships local SCPI simulators for select instrument categories, so you can develop and test without owning the physical hardware. Each simulator runs as a TCP server and pairs with a `Simulated` driver: ```python from instro.psu import InstroPSU from instro.psu.drivers import SimulatedPSU with InstroPSU( name="bench_psu", driver=SimulatedPSU("TCPIP0::127.0.0.1::5025::SOCKET"), num_channels=1, ) as psu: psu.set_voltage(5.0, channel=1) ``` ## PSU simulator The PSU simulator is a local SCPI server for development, tests, and demos. It listens on a TCP socket, exposes a programmable power-supply command set, and includes a terminal UI for editing channel loads, probe resistance, and channel limits while your code is running. ### Quickstart Start the simulator in one terminal: ```bash python -m instro.psu.scpi_sim_server ``` It binds to `127.0.0.1:5025` by default. If you are working from a source checkout, run the same command through `uv`: ```bash uv run python -m instro.psu.scpi_sim_server ``` Connect from another terminal or script with the `SimulatedPSU` driver. This small Python client opens an `InstroPSU`, configures channel 1, and reads voltage and current measurements: ```python from instro.psu import InstroPSU from instro.psu.drivers import SimulatedPSU with InstroPSU( name="bench_psu", driver=SimulatedPSU("TCPIP0::127.0.0.1::5025::SOCKET"), num_channels=2, ) as psu: psu.set_voltage(5.0, channel=1) psu.set_current_limit(1.0, channel=1) psu.output_enable(True, channel=1) voltage = psu.get_voltage(channel=1) current = psu.get_current(channel=1) print(f"V: {voltage.latest:.3f} V") print(f"I: {current.latest:.3f} A") psu.output_enable(False, channel=1) ``` Use CLI flags to change the bind address, port, or initial channel count: ```bash python -m instro.psu.scpi_sim_server --host 127.0.0.1 --port 5025 --channels 2 ``` ### Usage The simulator runs a terminal UI while the TCP server listens in the background. The UI is organized by panel header. ![PSU simulator terminal UI showing the PSU panel, channel panels, and SCPI log](/images/psu-simulator-overview.svg) Filled labels are selectable actions. Use the arrow keys to move between controls. Focused actions use a light highlight. Press Enter to select an action, Escape to close an edit prompt, and `q` to quit. #### PSU The **⟢ NOMINAL PSU** panel shows the simulator identity and VISA resource string. Use the resource string when driver code connects to the simulator. #### Channel Each **CHANNEL** panel shows the live state for one output. The panel is split into status, voltage, and current sections. | Header | Functionality | | --- | --- | | **STATUS** | Shows output mode, output state, sense mode, and protection trips. Select **REMOVE** to delete the channel from the simulator. | | **VOLTAGE** | Shows terminal voltage, programmed voltage, voltage limit, OVP state, and OVP level. Select **V LIMIT** to edit the channel voltage ceiling. | | **CURRENT** | Shows output current, programmed current limit, current limit, OCP state, and OCP level. Select **I LIMIT** to edit the channel current ceiling. | Changing **V LIMIT** or **I LIMIT** runs `*RST`. The output turns off, programmed voltage and current return to zero, protection state turns off, and OVP/OCP levels return to the configured V/I limits. #### Probe The **PROBE** panel shows sense lead resistance. Select **PROBE R** to edit the probe resistance in ohms. #### Load The **LOAD** panel shows the external load model. Select **R** to edit load resistance in ohms. Select **EMF** to edit the series electromotive force in volts. #### + channel The **+ CHANNEL** panel adds a new output channel with the next available channel ID. #### SCPI log The **SCPI LOG** panel records incoming commands, responses, and queued errors. It is read-only. ### Programming guide Brackets mark optional SCPI path components. For example, `[SOURce:]VOLTage[:LEVel]` accepts `VOLT`, `VOLT:LEV`, `SOUR:VOLT`, and `SOUR:VOLT:LEV`. Angle brackets mark command arguments. They are notation only; do not include the angle brackets in the command. - <NRf+> is a positive flexible numeric value, such as `0`, `1.25`, or `1e-3`. - <literal|literal> is one literal value from the listed options. The pipe character (`|`) means OR. For example, <LOC|REM> accepts `LOC` or `REM`, and <NRf+|MIN|MAX> accepts one numeric value, `MIN`, or `MAX`. Do not send the pipe character in the command. `MIN` maps to `0`. `MAX` maps to **V LIMIT** for voltage and OVP commands, and **I LIMIT** for current and OCP commands. Do not send `MIN` or `MAX` to queries. #### Defaults and reset On startup and after `*RST`, each channel uses the reset operating state: | Setting | Reset value | | --- | --- | | Programmed voltage | `0` | | Current limit | `0` | | Output state | `OFF` | | OVP state | `OFF` | | OCP state | `OFF` | | OVP level | `MAX` | | OCP level | `MAX` | `*RST` preserves simulator configuration, including channel count, channel IDs, load resistance, series EMF, probe resistance, sense mode, and configured channel limits. #### Error queue Errors are queued and returned one at a time by `SYSTem:ERRor?`. The simulator can emit these error codes: | Code | Message | Cause | | --- | --- | --- | | `0` | `No error` | The queue is empty | | `-108` | `Parameter not allowed` | A query received an argument | | `-109` | `Missing parameter` | A command was sent without a required argument | | `-113` | `Undefined header` | The command header is not supported | | `-114` | `Header suffix out of range` | The command targets a channel that does not exist | | `-141` | `Invalid character data` | A numeric argument could not be parsed | | `-222` | `Data out of range` | A numeric input is outside its accepted range | | `-224` | `Illegal parameter value` | A literal argument is not accepted for that command | | `301` | `PV Above OVP` | Programmed voltage is above the OVP level | | `303` | `PC Above OCP` | Current limit is above the OCP level | | `304` | `OVP Below PV` | OVP level is below programmed voltage | | `305` | `OCP Below PC` | OCP level is below current limit | | `323` | `Overcurrent protection tripped` | OCP tripped while enabled | | `324` | `Overvoltage protection tripped` | OVP tripped while enabled | #### Common commands
*IDN?
Query simulator identity Response: String
*RST
Reset channel operating state and clear errors Response: None
*CLS
Clear the error queue Response: None
#### MEASure subsystem
MEASure:CURRent?
Measure output current Response: Number
MEASure:VOLTage?
Measure output voltage Response: Number
#### OUTPut subsystem
OUTPut[:STATe] <0|1|OFF|ON>
Enable or disable output Response: None
OUTPut[:STATe]?
Query output state Response: 0 or 1
OUTPut:PROTection:CLEar
Clear latched OVP and OCP faults Response: None
OUTPut:PROTection:TRIPped?
Query latched OVP or OCP fault state Response: 0 or 1
#### SOURce subsystem
[SOURce:]CURRent[:LEVel][:IMMediate][:AMPLitude] <NRf+|MIN|MAX>
Set current limit Response: None
[SOURce:]CURRent[:LEVel][:IMMediate][:AMPLitude]?
Query current limit Response: Number
[SOURce:]CURRent:PROTection[:LEVel] <NRf+|MIN|MAX>
Set OCP level Response: None
[SOURce:]CURRent:PROTection[:LEVel]?
Query OCP level Response: Number
[SOURce:]CURRent:PROTection:STATe <0|1|OFF|ON>
Enable or disable OCP Response: None
[SOURce:]CURRent:PROTection:STATe?
Query OCP state Response: 0 or 1
[SOURce:]VOLTage[:LEVel][:IMMediate][:AMPLitude] <NRf+|MIN|MAX>
Set programmed voltage Response: None
[SOURce:]VOLTage[:LEVel][:IMMediate][:AMPLitude]?
Query programmed voltage Response: Number
[SOURce:]VOLTage:PROTection[:LEVel] <NRf+|MIN|MAX>
Set OVP level Response: None
[SOURce:]VOLTage:PROTection[:LEVel]?
Query OVP level Response: Number
[SOURce:]VOLTage:PROTection:STATe <0|1|OFF|ON>
Enable or disable OVP Response: None
[SOURce:]VOLTage:PROTection:STATe?
Query OVP state Response: 0 or 1
#### SYSTem subsystem
SYSTem:ERRor?
Pop one error from the error queue Response: code,"message"
SYSTem:SENSe[:STATe] <LOC|REM>
Select local or remote sense Response: None
SYSTem:SENSe[:STATe]?
Query sense mode Response: LOC or REM
## DMM simulator The DMM simulator is a local SCPI server for development, tests, and demos. It listens on a TCP socket and exposes a digital-multimeter command set: function switching plus the five primary measurements (DC/AC voltage, DC/AC current, and two-wire resistance). Each function measures a configurable stimulus value returned with Gaussian noise, so repeated reads look like a real bench instrument. Unlike the [PSU simulator](#psu-simulator), the DMM simulator is headless: there is no terminal UI. Set stimulus values with CLI flags at startup, or through the Python API when embedding the simulator in tests. ### Quickstart Start the simulator in one terminal: ```bash python -m instro.dmm.scpi_sim_server ``` It binds to `127.0.0.1:5026` by default. If you are working from a source checkout, run the same command through `uv`: ```bash uv run python -m instro.dmm.scpi_sim_server ``` Connect from another terminal or script with the `SimulatedDMM` driver. This small Python client opens an `InstroDMM`, switches functions, and reads measurements: ```python from instro.dmm import InstroDMM from instro.dmm.drivers import SimulatedDMM with InstroDMM( name="bench_dmm", driver=SimulatedDMM("TCPIP0::127.0.0.1::5026::SOCKET"), ) as dmm: print(f"V: {dmm.read_dc_voltage().latest:.4f} V") print(f"R: {dmm.read_resistance().latest:.1f} ohm") ``` Use CLI flags to change the bind address, port, or the stimulus each function measures: ```bash python -m instro.dmm.scpi_sim_server --port 5026 --dc-voltage 3.3 --resistance 4700 ``` ### Stimulus model The stimulus is the simulator's stand-in for the physical quantity on the probes (the DMM analog of the PSU simulator's load model). Each measurement function has one stimulus value, and every measurement returns that value with Gaussian noise (±0.5% at 3 standard deviations). | Function | CLI flag | Default | | --- | --- | --- | | DC voltage | `--dc-voltage` | `1.25` V | | AC voltage | `--ac-voltage` | `0.5` V | | DC current | `--dc-current` | `0.1` A | | AC current | `--ac-current` | `0.05` A | | Resistance | `--resistance` | `1000` Ω | When embedding the simulator in Python (for example in tests), construct the server in-process and set stimulus values at runtime: ```python from instro.dmm.scpi_sim_server import ( FUNCTION_RESISTANCE, SimulatedDMM, SimulatedDMMServer, ) sim = SimulatedDMM() server = SimulatedDMMServer(sim, port=0) # port 0 = pick a free port server.start() print(f"TCPIP0::127.0.0.1::{server.port}::SOCKET") sim.set_stimulus(FUNCTION_RESISTANCE, 4700.0) # change what the "probes" see # ... drive it with SimulatedDMM / InstroDMM ... server.shutdown() ``` Stimulus values persist across `*RST`: reset returns the instrument state (active function, error queue) to defaults, but the simulated physical world stays put. ### Programming guide Brackets mark optional SCPI path components. For example, `[SENSe:]FUNCtion` accepts `FUNC`, `FUNCTION`, and `SENS:FUNC`. Keywords match in short or long form, in any case. | Command | Functionality | | --- | --- | | `*IDN?` | Returns `NOMINAL,SIMULATED_DMM,000001,1.0`. | | `*RST` | Resets the active function to DC voltage and clears the error queue. Stimulus values are preserved. | | `*CLS` | Clears the error queue. | | `SYSTem:ERRor?` | Pops the oldest queued error, or `0,"No error"`. | | `[SENSe:]FUNCtion ""` | Sets the active function: `VOLT:DC`, `VOLT:AC`, `CURR:DC`, `CURR:AC`, or `RES`. Bare `VOLT`/`CURR` select DC. Quotes optional. | | `[SENSe:]FUNCtion?` | Returns the active function, quoted (e.g. `"VOLT:DC"`). | | `CONFigure:VOLTage[:DC]` | Sets the active function to DC voltage. Same pattern for `:AC`, `CURRent[:DC]`, `CURRent:AC`, and `RESistance`. | | `MEASure:VOLTage[:DC]?` | Switches to the function and returns a noisy measurement of its stimulus. Same pattern for the other four functions. | | `READ?` | Returns a noisy measurement using the active function. | Unknown headers queue `-113,"Undefined header"`; an unknown function name queues `-224,"Illegal parameter value"`. The `SimulatedDMM` driver checks `SYST:ERR?` after every transaction and raises on any queued error.