Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Crucible Manual

Crucible simulates a bench of measurement instruments. You describe each instrument in a YAML file and Crucible serves it over the network speaking real SCPI, just like the physical device. Your test software —PyVISA, LabVIEW, a sequencer such as Anvil— connects to it as if the bench were plugged in, without knowing it is not.

This manual is also in Spanish: the ES button in the top bar takes you to the same page in the other language.

This manual explains how to use it: install it, start a bench, connect your software and describe your own instruments.

Version 0.1.0. Written and checked against Crucible 0.1.0 on Linux x86-64. Crucible is young: the chapter What it does not do (yet) says plainly where it falls short.

Where to start

  • I want to see it working: chapters 2 to 4, in order.
  • I want to simulate my bench: then 5 to 9. Chapter 9 is the step-by-step recipe.
  • I am looking for a field or an option: Reference.
  • Something does not answer: Troubleshooting.

Conventions

Commands start with $ and are run from your bench folder, a copy of the example bench (chapter 5 shows how to make it), with the bench running in another terminal, unless the text says otherwise. The lines after a command are what it printed.

Terminal examples use nc (netcat) because every Linux has it. On Windows, chapter 4 gives the PowerShell equivalent.

Crucible speaks Spanish, for now

Crucible 0.1.0 prints its messages in Spanish, and the file format uses Spanish keys: a bench is banco, a profile has estado, comandos and modelos. This manual shows everything exactly as Crucible prints and reads it, and explains what it means. The keys are not translatable: write them as shown.

KeyMeaning
banco, dispositivosbench, devices
perfil, transporte, puertoprofile, transport, port
estado, estado_inicialstate, initial state
comandos, patron, muta, respuestacommands, pattern, mutates, response
modelos, cuando, formatomodels, when, format

How this manual stays true

Nothing a command prints was typed by hand. Every terminal session is a file in listings/sesiones/, shared by the Spanish and English editions and included as it is, and check.sh runs them all again against the binary and fails if any no longer matches. The example bench and profiles shown in these pages are the repository files, not copies.

$ docs/manual/check.sh target/debug/crucible

What could not be run here is marked not verified, in those words.

For how Crucible is built inside —the format as a standard, the design decisions— see the repository documentation (in Spanish).

What Crucible is

Crucible lets you test test software without the hardware. Instead of needing the power supply, the multimeter and the SMU on the desk to develop your sequence, you describe how they behave in a few text files and Crucible puts them on the network.

Your software cannot tell the difference, because there is none in what it sees: it opens a VISA resource, sends *IDN?, configures VOLT 5, asks MEAS:CURR? and gets answers in the real instrument’s format. With the same errors, too: a mistyped command leaves its entry in SYST:ERR?, as on a real instrument.

What it simulates and what it does not

A test instrument is, almost always, a state machine with responses: it receives a command, changes something inside and answers. Crucible models that:

  • State: the programmed voltage, whether the output is on, the range…
  • Commands: what the instrument understands and what each one changes.
  • Models: how it computes what it “measures”, with a formula over the state. A supply’s current can be Ohm’s law with a little noise.

It does not simulate continuous physics or circuits: the supply does not really power the multimeter. If your test needs a value to go from one instrument to another, your script sets it (chapter 8 shows how).

The words

WordWhat it means here
bench (banco)The instruments simulated together. A banco.yaml file.
profile (perfil)The description of one model of instrument: its *IDN?, its state, its commands and its models. One YAML file per model.
device (dispositivo)One unit on the bench: a profile served on a port. Two identical multimeters are two devices with the same profile.
state (estado)The variables the instrument remembers. There is one per device, shared by every connection.
model (modelo)A formula that computes a query’s response from the state.
VISA resourceThe address your software opens the instrument with: TCPIP0::127.0.0.1::5025::SOCKET.

What you need

  • Linux x86-64 or Windows. Everything in this manual was run on Linux. On Windows the commands are the same; the installation and the PowerShell test in chapter 4 are run by continuous integration on every release, the rest is not verified on Windows.
  • To try it without your software: nc (netcat) on Linux, or PowerShell on Windows.
  • For the Python example: Python 3 with pyvisa and pyvisa-py.

No instrument, no licence and no special VISA driver.

Installing

The installers are on the GitHub releases page.

Linux (Debian, Ubuntu and derivatives)

$ sudo apt install ./crucible_0.1.0_amd64.deb

It leaves:

  • the crucible command in /usr/bin;
  • the example bench in /usr/share/crucible/banco, read-only (chapter 5 shows how to copy it to edit it).

Check the installation:

$ crucible --version
crucible 0.1.0

Windows

  1. Run crucible-0.1.0-setup.exe. It does not ask for administrator rights.
  2. The installer leaves:
    • In the Start menu → Crucible: Arrancar banco (start bench), Abrir carpeta del banco (open the bench folder), Terminal de Crucible (a console showing the command’s help) and Manual (opens this website).
    • The crucible command on the PATH, for any new terminal (cmd or PowerShell).
    • The example bench in %LOCALAPPDATA%\Programs\Crucible\banco, which you can edit right there.

Upgrading to a new version does not overwrite bench files that already exist: your changes are kept.

From source

With Rust 1.90 or later:

$ git clone https://github.com/anlaco/Crucible
$ cd Crucible
$ cargo build --release -p crucible

The binary ends up in target/release/crucible and the example bench in banco/. Replace crucible with that path in the rest of the manual, or add it to your PATH.

First start

Crucible ships an example bench with three instruments: a DC power supply, a multimeter and a Keithley 2400. It is there to see it working before you describe anything.

Start the bench

  • Linux: run crucible with no arguments.
  • Windows: Start menu → Crucible → Arrancar banco.
$ crucible
Crucible 0.1.0 — banco.yaml

  ID          MODELO         RECURSO VISA
  fuente      FUENTE-DC      TCPIP0::127.0.0.1::5025::SOCKET
  multimetro  MULTIMETRO     TCPIP0::127.0.0.1::5026::SOCKET
  smu         KEITHLEY-2400  TCPIP0::127.0.0.1::5027::SOCKET

Listo. Ctrl+C para parar.

Each row is an instrument listening on its port (the columns: ID, model, VISA resource). While the window stays open they are “switched on”; every incoming connection is logged below. Ctrl+C switches them off (Listo means ready).

With no arguments, crucible uses the first bench it finds from this list:

  1. banco.yaml in the folder you run it from.
  2. banco/banco.yaml next to the executable (Windows installation).
  3. /usr/share/crucible/banco/banco.yaml (Linux installation).

To start a specific one: crucible path/to/banco.yaml.

Ask each instrument who it is

In another terminal, with the bench running:

$ printf '*IDN?\n' | nc -q1 127.0.0.1 5025
Crucible,FUENTE-DC,SIM0001,1.0
$ printf '*IDN?\n' | nc -q1 127.0.0.1 5026
Crucible,MULTIMETRO,SIM0002,1.0
$ printf '*IDN?\n' | nc -q1 127.0.0.1 5027
Keithley,2400,1234567,A1.2

Each port is a different instrument, and each one answers with its own *IDN?.

-q1 matters. Without it, Debian’s and Ubuntu’s nc does not close the connection after sending, and the command waits forever.

That is all you need to get going: they are TCP sockets speaking SCPI terminated by a newline. The next chapter connects a real program.

Connecting your software

Each instrument on the bench is a TCP socket on 127.0.0.1, on its own port. For VISA, the resource is:

TCPIP0::127.0.0.1::<port>::SOCKET

Termination: messages end with a newline (\n, 0x0A) in both directions. With ::SOCKET resources VISA does not add it by default: set it on your session, or your software will wait for an answer that Crucible does not process until it receives the newline.

PyVISA

pip install pyvisa pyvisa-py is enough; NI-VISA is not needed. This script talks to the supply and the multimeter on the example bench:

import pyvisa

rm = pyvisa.ResourceManager("@py")  # o ResourceManager() si usas NI-VISA
opciones = dict(read_termination="\n", write_termination="\n")

fuente = rm.open_resource("TCPIP0::127.0.0.1::5025::SOCKET", **opciones)
dmm = rm.open_resource("TCPIP0::127.0.0.1::5026::SOCKET", **opciones)

print(fuente.query("*IDN?"))
fuente.write("VOLT 5")
fuente.write("OUTP ON")
print(fuente.query("MEAS:CURR?"))  # 5 V sobre la carga de 10 ohmios
print(dmm.query("MEAS:VOLT?"))
$ python3 conectar.py
Crucible,FUENTE-DC,SIM0001,1.0
0.50001
+3.300139E+00

The supply has a simulated 10 Ω load connected, so at 5 V it gives half an ampere. The multimeter reads 3.3 V because the example bench sets that when it starts (chapter 5). The last digit is Gaussian noise, which repeats the same every time you start the bench (chapter 7).

NI MAX and LabVIEW

Not verified in this manual.

Add the instrument in NI MAX as VISA TCP/IP Resource → Manual Entry of Raw Socket, with host 127.0.0.1 and the bench port. In LabVIEW, when opening the VISA session, enable the \n (0x0A) termination character for reads (Termination Character Enabled) and end every write with \n.

Windows, without installing anything

From PowerShell. It is the same test continuous integration runs against the Windows installer on every release:

$c = New-Object Net.Sockets.TcpClient('127.0.0.1', 5025); $s = $c.GetStream()
$w = New-Object IO.StreamWriter($s); $w.AutoFlush = $true; $w.WriteLine('*IDN?')
(New-Object IO.StreamReader($s)).ReadLine(); $c.Close()

If your software points at the real bench’s IPs

Software that today opens TCPIP0::192.168.1.10::5025::SOCKET, TCPIP0::192.168.1.11::5025::SOCKET… has to open TCPIP0::127.0.0.1::5025::SOCKET, TCPIP0::127.0.0.1::5026::SOCKET… Change the addresses in its configuration; you choose the ports in banco.yaml.

If it uses ::INSTR (VXI-11) resources, you can keep them: declare the vxi11 transport in the bench (see below) or, if you would rather keep things simple, change them to ::SOCKET.

VXI-11 (::INSTR)

If your software cannot change resource strings, set tipo: vxi11 in the transport and give each instrument its own IP, like the real bench:

- id: generador
  perfil: perfiles/n5171b.yaml
  transporte: { tipo: vxi11, host: 127.0.0.2, puerto: 5025, device: inst0 }

- id: fuente
  perfil: perfiles/n5767a.yaml
  transporte: { tipo: vxi11, host: 127.0.0.3, puerto: 5025, device: inst0 }
TCPIP0::127.0.0.2::inst0::INSTR
TCPIP0::127.0.0.3::inst0::INSTR

That is exactly the shape of the physical bench: every instrument is a box with its own IP, its own portmapper on UDP 111 and its device inst0. Going from the real bench to the simulated one, only the addresses change, neither the port nor the device.

On Linux every 127.x.x.x address is local and needs no setting up. On Windows only 127.0.0.1 answers. There, put the instruments on that IP, on the same port, and tell them apart by their device:

transporte: { tipo: vxi11, host: 127.0.0.1, puerto: 5025, device: inst0 }
transporte: { tipo: vxi11, host: 127.0.0.1, puerto: 5025, device: inst1 }

That second shape is the one of a VXI chassis or a LAN/GPIB gateway: a single address for several instruments. It works just as well, but it does force you to touch your software’s resource strings.

UDP port 111 is required, and it is privileged. On Linux, once:

sudo setcap 'cap_net_bind_service=+ep' /path/to/crucible

On Windows, start as administrator. Otherwise the bench does not start and says exactly this: it would rather not start than advertise instruments nobody can then find.

Anvil

The Anvil sequencer speaks SCPI over a socket, so it connects to Crucible with nothing special. The guide Anvil measures against a Crucible instrument (in Spanish) walks through it end to end.

Building your bench

A bench is a folder shaped like this:

mi_banco/
├── banco.yaml             ← which instruments there are and on which port
└── perfiles/
    ├── fuente_xyz.yaml    ← one profile per instrument MODEL
    └── dmm_abc.yaml

Start by copying the example

The quickest way is to start from the example bench and change it:

  • Linux:

    $ cp -r /usr/share/crucible/banco ~/mi_banco
    $ cd ~/mi_banco
    
  • Windows: Start menu → Crucible → Abrir carpeta del banco, and edit it there or copy the folder somewhere else.

From that folder, crucible with no arguments starts your bench: it is the first place it looks.

banco.yaml

This is the example’s (its comments are in Spanish; the text below explains every field):

# Banco de ejemplo de Crucible.
#
# Un banco es la lista de instrumentos que se simulan a la vez. Cada uno apunta
# a su perfil (qué instrumento es y cómo responde) y dice en qué puerto escucha.
#
# Para tu banco: copia esta carpeta, añade un perfil por cada modelo de
# instrumento y una entrada por cada unidad física. Dos unidades del mismo
# modelo comparten perfil, pero cada una necesita su propio puerto.
#
# Comprueba el banco sin arrancarlo:   crucible --validar banco.yaml
# Arráncalo:                           crucible banco.yaml

banco:
  nombre: banco-de-ejemplo
  dispositivos:
    - id: fuente
      perfil: perfiles/fuente_dc.yaml
      transporte: { tipo: tcp, puerto: 5025 }

    - id: multimetro
      perfil: perfiles/multimetro.yaml
      transporte: { tipo: tcp, puerto: 5026 }
      # Se puede cambiar el estado de arranque de una unidad sin tocar su perfil.
      estado_inicial:
        tension_entrada: 3.3

    - id: smu
      perfil: perfiles/keithley_2400_demo.yaml
      transporte: { tipo: tcp, puerto: 5027 }

Each entry under dispositivos is one unit:

FieldWhat it is
idA name for you. It appears in the start-up table and in errors.
perfilThe instrument’s profile, with its path relative to banco.yaml.
transporteWhere it listens: { tipo: tcp, puerto: 5025 }. Today tipo can only be tcp.
estado_inicialOptional. Values that replace those in the profile’s estado for this unit only.

Two units of the same model share a profile and use different ports, and, if needed, a different estado_inicial:

    - id: dmm_entrada
      perfil: perfiles/dmm_abc.yaml
      transporte: { tipo: tcp, puerto: 5026 }

    - id: dmm_salida
      perfil: perfiles/dmm_abc.yaml
      transporte: { tipo: tcp, puerto: 5027 }
      estado_inicial:
        tension_entrada: 12.0

Validate before starting

$ crucible --validar banco.yaml
banco.yaml es válido:

  ID          MODELO         RECURSO VISA
  fuente      FUENTE-DC      TCPIP0::127.0.0.1::5025::SOCKET
  multimetro  MULTIMETRO     TCPIP0::127.0.0.1::5026::SOCKET
  smu         KEITHLEY-2400  TCPIP0::127.0.0.1::5027::SOCKET

--validar loads the bench and all its profiles, checks patterns, formulas and ports, and shows the resource table without opening any port, so you can use it while the bench is running. Do it every time you edit a YAML file: the errors say which file, which command and what is wrong.

Each instrument needs its own port. With this bench, which repeats 5025:

banco:
  nombre: dos-fuentes
  dispositivos:
    - id: fuente_a
      perfil: perfiles/fuente_dc.yaml
      transporte: { tipo: tcp, puerto: 5025 }

    - id: fuente_b
      perfil: perfiles/fuente_dc.yaml
      transporte: { tipo: tcp, puerto: 5025 }
$ crucible --validar puerto_repetido.yaml

error: 'fuente_a' y 'fuente_b' usan los dos 127.0.0.1:5025; cada instrumento necesita su puerto

The error says both devices use 127.0.0.1:5025 and each instrument needs its own port.

Three rules worth knowing

If one profile has an error, the whole bench does not start. On purpose: a bench missing an instrument would give false results without anyone noticing.

*RST returns the unit to the profile’s values, not to estado_inicial. The example multimeter starts with 3.3 V on its terminals, but after *RST it reads 0 V (you will see it in chapter 8). If your software sends *RST at the start, set the value in the profile or set it again over SCPI afterwards.

A misspelled variable in estado_inicial is not an error. Here tension_entarda does not exist in the multimeter profile, and still:

banco:
  nombre: errata
  dispositivos:
    - id: dmm
      perfil: perfiles/multimetro.yaml
      transporte: { tipo: tcp, puerto: 5030 }
      estado_inicial:
        tension_entarda: 12.0
$ crucible --validar errata.yaml
errata.yaml es válido:

  ID   MODELO      RECURSO VISA
  dmm  MULTIMETRO  TCPIP0::127.0.0.1::5030::SOCKET

(es válido means is valid.) The multimeter will start reading 0 V. If a unit does not start with the value you expected, check the spelling first.

Describing an instrument

A profile describes an instrument model: who it is, what it remembers, which commands it understands and what it does with each one. It has five sections:

dispositivo:   # who it is
protocolo:     # how it is spoken to; today, always scpi
estado:        # its internal variables and their power-on value
comandos:      # what it understands and what it does with each thing
modelos:       # how it computes what it "measures" (chapter 7)

This is the profile of the supply on the example bench. It is worth reading in full (the comments are in Spanish): the rest of the chapter takes it apart.

# Fuente de alimentación DC de un canal, con una carga resistiva conectada.
#
# Es una plantilla: sustituye el `idn` y los comandos por los de tu fuente real.
# Declara solo lo que usa tu software de medida, no el manual entero.

dispositivo:
  modelo: FUENTE-DC
  tipo: instrumento
  # Lo que contesta *IDN?. Pon el de tu instrumento real si tu software lo comprueba.
  idn: "Crucible,FUENTE-DC,SIM0001,1.0"

protocolo: scpi

# Variables internas del instrumento y su valor al encender (y tras *RST).
estado:
  tension: 0.0          # V programados
  limite_corriente: 1.0 # A
  salida: false
  carga_ohm: 10.0       # la resistencia que "hay conectada" a la salida

comandos:
  # VOLT 5  /  SOUR:VOLT:LEV 5  /  volt 5  → todas equivalen.
  - patron: "[SOURce:]VOLTage[:LEVel][:IMMediate][:AMPLitude]"
    args: [v]
    muta: { tension: "<v>" }
  - patron: "[SOURce:]VOLTage[:LEVel][:IMMediate][:AMPLitude]"
    query: true
    respuesta: "{tension}"

  - patron: "[SOURce:]CURRent[:LEVel][:IMMediate][:AMPLitude]"
    args: [i]
    muta: { limite_corriente: "<i>" }
  - patron: "[SOURce:]CURRent[:LEVel][:IMMediate][:AMPLitude]"
    query: true
    respuesta: "{limite_corriente}"

  # OUTP ON / OUTP OFF / OUTP 1 / OUTP 0
  - patron: "OUTPut[:STATe]"
    args: [estado]
    muta: { salida: "<estado>" }
  - patron: "OUTPut[:STATe]"
    query: true
    modelo: salida_como_numero

  - patron: "MEASure[:SCALar]:VOLTage[:DC]"
    query: true
    modelo: tension_medida
  - patron: "MEASure[:SCALar]:CURRent[:DC]"
    query: true
    modelo: corriente_medida

modelos:
  # Los instrumentos SCPI contestan 1/0, no true/false ni 1.0.
  salida_como_numero:
    tipo: formula
    cuando: { salida: true }
    expr: "1"
    fallback: 0
    formato: entero

  tension_medida:
    tipo: formula
    cuando: { salida: true }
    expr: "tension + gauss(0, 0.0005)"
    fallback: 0.0
    formato: fijo:4

  # Ley de Ohm, recortada al límite de corriente (la fuente entra en modo CC).
  corriente_medida:
    tipo: formula
    cuando: { salida: true }
    expr: "min(tension / carga_ohm, limite_corriente) + gauss(0, 1e-5)"
    fallback: 0.0
    formato: fijo:5

dispositivo

dispositivo:
  modelo: DMM-ABC                        # required; shown in the start-up table
  idn: "ACME,DMM-ABC,SN12345,FW2.01"     # what *IDN? answers

If your software checks *IDN? to recognise the instrument, copy the real device’s answer here exactly.

protocolo

scpi. The format reserves other values (modbus_tcp, serial_ascii…) for what is coming, but today a profile that is not scpi does not start.

estado

The variables the instrument remembers, with their power-on value, the one they return to on *RST:

estado:
  tension: 0.0
  salida: false
  funcion: VOLT

Numbers, true/false and text are accepted. The state is one per device, shared by every connection: chapter 8 explains what that implies.

comandos

Each entry is a command or a query. If a command has both forms, like VOLT 5 and VOLT?, it is declared twice:

comandos:
  # Command: VOLT 5  →  stores 5 in the 'tension' variable
  - patron: "[SOURce:]VOLTage[:LEVel]"
    args: [v]
    muta: { tension: "<v>" }

  # Query: VOLT?  →  answers with the value of 'tension'
  - patron: "[SOURce:]VOLTage[:LEVel]"
    query: true
    respuesta: "{tension}"

  # Computed query: MEAS:VOLT?  →  answered by the 'lectura' model
  - patron: "MEASure:VOLTage[:DC]"
    query: true
    modelo: lectura
FieldWhat it is
patronThe header in manual notation (below). No ? and no arguments.
querytrue if it is the query. Otherwise, it is a command.
argsNames for incoming arguments, in order: with args: [v], in VOLT 5 <v> is 5.
mutaState variables it changes: { tension: "<v>" }, or a fixed value, { funcion: VOLT }.
respuesta(queries) The text to answer. {variable} is replaced by the state and <arg> by the argument.
modelo(queries) The model in modelos that computes the answer.

Every query needs respuesta or modelo. A command may carry only muta, or nothing if being accepted without error is enough.

Pattern notation

It is the notation of manufacturers’ manuals:

  • Upper case is the required short form, and lower case completes the long form. VOLTage accepts VOLT and VOLTAGE, in upper or lower case.
  • Brackets mark nodes that can be left out. [SOURce:]VOLTage[:LEVel] accepts VOLT, SOUR:VOLT, volt:lev, SOURCE:VOLTAGE:LEVEL…

One pattern thus covers every form the real instrument accepts, without listing them. Against the example supply:

$ printf 'VOLT 5\n' | nc -q1 127.0.0.1 5025
$ printf 'VOLT?\n' | nc -q1 127.0.0.1 5025
5.0
$ printf 'source:voltage:level 7.5\n' | nc -q1 127.0.0.1 5025
$ printf 'SOUR:VOLT?\n' | nc -q1 127.0.0.1 5025
7.5

Commands answer nothing, as on a real instrument: that is why no line appears after VOLT 5.

Copy patterns from the manufacturer’s manual (MEASure:VOLTage[:DC]), not the short form your software uses (MEAS:VOLT). That way the profile accepts whatever any other program sends.

Arguments

  • ON, OFF, 1 and 0 are stored as true or false, and models treat them the same.
  • If a declared argument does not arrive (CONF:VOLT with no range), the variable stays as it was.
  • Without names in args, arguments are referred to by position: <0>, <1>.
  • A reference that does not exist, such as {no_such_variable}, is answered literally, braces included. If you see braces in an answer, there is a typo in the profile.

Multi-channel instruments

One pattern per channel, with the number inside:

  - patron: "SOURce1:VOLTage"
    args: [v]
    muta: { tension_1: "<v>" }
  - patron: "SOURce2:VOLTage"
    args: [v]
    muta: { tension_2: "<v>" }

SOUR1:VOLT 3 goes to channel 1 and SOUR2:VOLT 7 to channel 2. SOUR:VOLT, with no number, is not taken as channel 1: if your software sends it that way, declare the unnumbered pattern too. A channel list such as MEAS:VOLT? (@2) arrives as an argument with the text (@2).

When a command is not in the profile

The connection is not dropped and nothing is answered: the error goes into the error queue, as the instrument would do. VOLT:PROT is not in the supply’s profile:

$ printf 'VOLT:PROT 30\n' | nc -q1 127.0.0.1 5025
$ printf 'SYST:ERR?\n' | nc -q1 127.0.0.1 5025
-113,"Undefined header;VOLT:PROT"
$ printf 'SYST:ERR?\n' | nc -q1 127.0.0.1 5025
0,"No error"

SYST:ERR? is the first tool for completing a profile: it tells you which header it did not recognise. Each read takes one error off the queue; when it is empty, it answers 0,"No error".

What you do not declare

*IDN?, *RST, SYST:ERR? and the other common commands are handled by Crucible for every instrument (full list in chapter 8). Declaring them is an error:

$ crucible --validar perfiles/rst_declarado.yaml

error: perfiles/rst_declarado.yaml: perfil inválido: el patrón '*RST' es un comando común de IEEE 488.2. Ya no se declaran en el perfil: los resuelve el motor. '*IDN?' sale de 'dispositivo.idn'

The message says *RST is an IEEE 488.2 common command, which the engine resolves, and that *IDN? comes from dispositivo.idn.

Models: what the instrument measures

A query with respuesta: "{tension}" returns what was configured. A measurement is something else: the current a supply delivers depends on the voltage, the load and whether the output is on. That is a model.

modelos:
  corriente_medida:
    tipo: formula
    cuando: { salida: true }
    expr: "min(tension / carga_ohm, limite_corriente) + gauss(0, 1e-5)"
    fallback: 0.0
    formato: fijo:5

And the query that uses it:

  - patron: "MEASure[:SCALar]:CURRent[:DC]"
    query: true
    modelo: corriente_medida

Fields

FieldWhat it is
tipoAlways formula.
exprThe formula.
cuandoConditions on the state. If any is not met, fallback is answered. If there are several, all must hold.
fallbackThe answer when cuando is not met. Defaults to 0.0. Number or text.
formatoHow the number is written. Optional.

What goes in expr

  • State variables, by name: tension, carga_ohm.
  • Numbers: 10, 0.5, 1e-3, 2.5E+2.
  • + - * / with the usual precedence, parentheses and unary minus.
  • Functions:
FunctionResult
gauss(mean, sigma)Gaussian noise. It changes on every read, but the series repeats the same each time you start Crucible, so automated tests are reproducible.
abs(x)Absolute value.
sqrt(x)Square root.
min(a, b), max(a, b)Minimum and maximum.

There are no conditionals inside the formula: cuando only chooses between the formula and a fixed fallback.

The supply’s model, running

Against a freshly started bench:

$ printf 'MEAS:CURR?\n' | nc -q1 127.0.0.1 5025
0.00000
$ printf 'OUTP?\n' | nc -q1 127.0.0.1 5025
0
$ printf 'VOLT 5;:OUTP ON;:MEAS:CURR?\n' | nc -q1 127.0.0.1 5025
0.50001
$ printf 'VOLT 20;:MEAS:CURR?;:OUTP?\n' | nc -q1 127.0.0.1 5025
0.99999;1
$ printf 'MEAS:VOLT?\n' | nc -q1 127.0.0.1 5026
+3.300139E+00

Step by step:

  1. With the output off cuando is not met, and the measurement is the fallback, written in the model’s format: 0.00000.
  2. OUTP? answers 0, not false: it is answered by the salida_como_numero model, with formato: entero, like a real instrument.
  3. At 5 V across 10 Ω, half an ampere plus noise.
  4. At 20 V it would be 2 A, but min clips it to the 1 A limit: the supply goes into constant current.
  5. The multimeter writes scientific notation, like most do.

formato

Your software expects numbers written the way the real instrument writes them:

formatoExample answerTypical use
(not set)1.0, 4.501385029307777—
entero (integer)1, 0, -3States (OUTP?) and counters.
fijo:N (fixed)fijo:4 → 4.5014Supplies that answer with fixed decimals.
cientifico:N (scientific)cientifico:6 → +4.501385E+00Multimeters and most SCPI measurements.

The format also applies to fallback when it is numeric.

Queries with respuesta: "{variable}" return the value as it is (5.0). If your software expects another form, use a model whose expr is the variable, with the right formato.

Errors in formulas

Syntax errors —an unclosed parenthesis, a function that does not exist— show up when validating or starting, with the position:

dispositivo:
  modelo: DMM-ABC
  idn: "ACME,DMM-ABC,SN12345,FW2.01"

protocolo: scpi

estado:
  tension_entrada: 0.0

comandos:
  - patron: "MEASure:VOLTage[:DC]"
    query: true
    modelo: lectura

modelos:
  lectura:
    tipo: formula
    expr: "tension_entrada + gaus(0, 0.0001)"
$ crucible --validar perfiles/formula_rota.yaml

error: perfiles/formula_rota.yaml: perfil inválido: modelo 'lectura': error de evaluación: fórmula 'tension_entrada + gaus(0, 0.0001)': función 'gaus' desconocida; existen: gauss, abs, sqrt, min, max (posición 23)

The message says the function gaus is unknown, lists the ones that exist and gives the position.

A variable that does not exist in the state, however, is only detected when the query is made: no answer arrives and SYST:ERR? returns -200,"Execution error;…la variable '…' no existe en el estado…" (the variable does not exist in the state). Declare in estado every variable your formulas use.

What every instrument already does

Some things every SCPI instrument does the same way, and Crucible provides them ready-made. They are not declared in the profile.

Common commands

CommandWhat it does
*IDN?Returns dispositivo.idn.
*RSTReturns the state to the profile’s values.
*CLSClears the error queue and the status register.
*OPC, *OPC?, *WAISynchronisation. Always ready: nothing takes time.
*ESE, *ESE?, *ESR?, *SRE, *SRE?, *STB?, *TST?IEEE 488.2 registers.
SYSTem:ERRor[:NEXT]?The next error in the queue, e.g. -113,"Undefined header;FOO".
SYSTem:VERSion?The SCPI version.

Compound messages

Several commands separated by ; run in order, and query answers come back on one line, also separated by ; (you saw it in chapter 7: 0.99999;1).

There is an SCPI rule worth being clear about. After a ;, the next header is relative to the previous one’s node, unless it starts with ::

$ printf 'VOLT 5;OUTP ON\n' | nc -q1 127.0.0.1 5025
$ printf 'OUTP?\n' | nc -q1 127.0.0.1 5025
1
$ printf 'SOUR:VOLT 3;OUTP OFF\n' | nc -q1 127.0.0.1 5025
$ printf 'SYST:ERR?\n' | nc -q1 127.0.0.1 5025
-113,"Undefined header;SOUR:OUTP"
$ printf 'SOUR:VOLT 3;:OUTP OFF;:OUTP?\n' | nc -q1 127.0.0.1 5025
0
  • VOLT 5;OUTP ON works: VOLT hangs off the root, so OUTP does too.
  • SOUR:VOLT 3;OUTP OFF does not: the current node is SOUR, and OUTP is read as SOUR:OUTP, which does not exist. The output stays on.
  • With a leading : (;:OUTP OFF) you go back to the root, and it always works.

A real instrument does exactly the same. When in doubt, write ;:.

The state is shared

Each device has a single state, shared by every connection. It is not cloned per client or reset on disconnect: just as a real device does not reset when someone else picks it up.

The example multimeter has a command the real one does not, SIMulate:INPut, to set the voltage on its terminals from a script. Every command in this session is a separate connection:

$ printf 'SIM:INP 12\n' | nc -q1 127.0.0.1 5026
$ printf 'MEAS:VOLT?\n' | nc -q1 127.0.0.1 5026
+1.200014E+01
$ printf '*RST\n' | nc -q1 127.0.0.1 5026
$ printf 'MEAS:VOLT?\n' | nc -q1 127.0.0.1 5026
-6.313870E-05

The 12 V set by one connection is measured by the next. And after *RST it goes back to 0 V —the profile’s value—, not to the 3.3 V of the estado_inicial the bench started it with.

In practice:

  • If your software configures something and exits, the next run finds it configured. Send *RST at the start, or restart Crucible, if you want to start from scratch.
  • SIMulate:INPut is the way to chain instruments: devices do not see each other, so your script reads the supply and passes the value to the multimeter. You can add commands like it to any profile.

Errors do not drop the connection

An unknown command, a missing argument or a formula that cannot be evaluated leaves its error in the queue and the connection stays open. Your software sees it as on the real instrument: by asking SYST:ERR?.

Recipe: from a real instrument to its profile

You do not need to describe the instrument’s whole manual. You need to describe what your software uses, and describe it well.

  1. Find out which commands your software sends.
    • If you have the real instrument, capture the traffic: NI I/O Trace, your application’s log, or Wireshark on the instrument’s port.
    • If not, search the code for write(, query(, VISA Write…
  2. Copy the real device’s *IDN? into dispositivo.idn.
  3. For each configuration command (VOLT 5, RANG 10):
    • a variable in estado, with its power-on value;
    • a command with muta;
    • and, if your software also reads it back, a query with respuesta: "{variable}".
  4. For each measurement (MEAS:VOLT?, READ?): a model with the simplest physics your test needs. A constant with some noise, or Ohm’s law with the supply voltage. Match formato to the device.
  5. Add the instrument to the bench with its port (chapter 5).
  6. Validate: crucible --validar banco.yaml.
  7. Start it and run your software. If something does not answer, ask that instrument SYST:ERR?: it tells you which header it did not recognise. Add it to the profile and repeat.

Tips

  • Patterns from the manual, not from your code. MEASure:VOLTage[:DC] accepts MEAS:VOLT, measure:voltage:dc and every other form; MEAS:VOLT only accepts itself.
  • One profile per model, one scenario per bench. If a test needs the instrument to start already configured, do not touch the profile: use estado_inicial on the bench. The profile describes the device; the bench, the situation.
  • Simulation commands. To provoke scenarios from the script (an out-of-range voltage, a failing sensor) add commands the real device does not have, like the example multimeter’s SIMulate:INPut.
  • Start from the example profiles. fuente_dc.yaml and multimetro.yaml are meant as templates and commented line by line.

Reference

The crucible command

$ crucible --help
Crucible — simulador de bancos de instrumentos SCPI por red

USO
  crucible                         arranca el banco por defecto (ver abajo)
  crucible <banco.yaml>            arranca todos los instrumentos del banco
  crucible <perfil.yaml> [puerto]  arranca un solo instrumento (puerto 5025)
  crucible --validar <fichero>     comprueba un banco o perfil sin arrancarlo
  crucible --version

BANCO POR DEFECTO (sin argumentos), el primero que exista de:
  1. banco.yaml en el directorio actual
  2. banco/banco.yaml junto al ejecutable            (instalación Windows)
  3. ../share/crucible/banco/banco.yaml               (instalación Linux)

Cada instrumento tiene su dirección. Desde VISA:
  TCPIP0::127.0.0.1::<puerto>::SOCKET  (transporte tcp)
  TCPIP0::127.0.0.2::inst0::INSTR      (transporte vxi11)

Como en un banco real, cada instrumento puede tener su propia IP: en Linux
todo 127.x.x.x es local y no hay que configurar nada; en Windows solo
responde 127.0.0.1, y ahí se distinguen por el puerto.

Los instrumentos vxi11 necesitan además el portmapper en UDP 111 de su IP,
que es un puerto privilegiado: en Linux hace falta sudo o setcap.

Manual: https://anlaco.github.io/Crucible/
FormWhat it does
crucibleStarts the first bench it finds: banco.yaml in the current folder, then next to the executable, then the installed one.
crucible <banco.yaml>Starts every instrument on the bench.
crucible <profile.yaml> [port]Starts a single instrument, without a bench. Port 5025 if not given.
crucible --validar <file>Checks a bench or a profile without opening ports. Exits with code 1 on errors.
crucible --versionThe version.
crucible --helpThe help. Also -h and --ayuda.

Whether the file is a bench or a profile is decided by its content, not by an option: it has banco: or it has dispositivo:.

Any error makes crucible exit with code 1. With no arguments —usually a double-click on Windows— it also waits for you to press Enter, so the window does not close before you can read why.

banco.yaml

banco:
  nombre: <text>                    # optional
  dispositivos:
    - id: <text>                    # required
      perfil: <path>                # required; relative to banco.yaml
      transporte:
        tipo: tcp                   # required; today only tcp
        puerto: <number>            # required; different for each device
        host: <address>             # optional; 127.0.0.1 by default
      estado_inicial:               # optional
        <variable>: <value>

host decides which interface the device listens on. By default 127.0.0.1, this computer only. With host: 0.0.0.0 it listens on all of them, and another computer on the network can connect; in that case the start-up table shows TCPIP0::0.0.0.0::…, which is not an address you can connect to: use the IP of the computer running Crucible. Access from another computer is not verified in this manual.

Profile

dispositivo:
  modelo: <text>                    # required
  idn: <text>                       # answer to *IDN?
protocolo: scpi                     # required

estado:
  <variable>: <number | true | false | text>

comandos:
  - patron: <header>                # required; no '?', no arguments, no '*'
    query: <true | false>           # false by default
    args: [<name>, ...]
    muta: { <variable>: <value | "<arg>"> }
    respuesta: <text with {variable} and <arg>>   # queries
    modelo: <model name>                          # queries

modelos:
  <name>:
    tipo: formula                   # required
    expr: <formula>                 # required
    cuando: { <variable>: <value> }
    fallback: <number | text>       # 0.0 by default
    formato: <entero | fijo:N | cientifico:N>

Formulas

ElementExamples
Variablestension, carga_ohm
Numbers10, 0.5, 1e-3, 2.5E+2
Operators+ - * /, parentheses, unary minus
Functionsgauss(mean, sigma), abs(x), sqrt(x), min(a, b), max(a, b)

SCPI errors you will see

ErrorWhen
0,"No error"The queue is empty.
-113,"Undefined header;…"The header is not in the profile, or not as a command or query.
-200,"Execution error;…"A formula could not be evaluated, e.g. a variable that is not in estado.

What it does not do (yet)

Worth knowing before you describe your bench.

LimitationConsequence
Only SCPI over a TCP socket (::SOCKET) and VXI-11 (::INSTR). No HiSLIP, USB, GPIB or serial.A bench on GPIB or USB cannot be simulated yet.
VXI-11 needs UDP port 111, which is privileged.You must start with permissions (sudo setcap 'cap_net_bind_service=+ep' crucible on Linux, administrator on Windows) or the bench does not start.
One IP per instrument only on Linux. Windows answers on 127.0.0.1 only.On Windows the instruments share an IP and are told apart by their port (tcp) or their device (vxi11).
No Modbus or custom serial protocols. The format anticipates them, but a profile that is not scpi does not start.A thermal camera or a fixture with its own protocol cannot be simulated yet.
Instruments do not see each other. The supply does not power the multimeter.To chain values, your script sets them (chapter 8).
No conditionals in formulas. cuando only chooses between the formula and a fixed fallback.A READ? that changes with the configured function (voltage or current) cannot be modelled; simulate the one your test uses.
No binary data: # blocks, waveforms, screenshots.Text answers only.
No timing: everything answers instantly. No slow *OPC, no simulated timeouts.It cannot test how your software handles timeouts.
Noise is reproducible: the same series on every start.Good for automated tests; do not expect different values between runs.
Messages and file keys are in Spanish.This manual translates them where they matter.
YAML files are read at start-up.After editing a file, stop Crucible and start it again.

If any of these blocks your bench, open an issue with the concrete case: which instrument and which commands.

Troubleshooting

«no puede escuchar en 127.0.0.1:5025»

Cannot listen on 127.0.0.1:5025.

$ crucible banco.yaml

error: 'fuente' no puede escuchar en 127.0.0.1:5025 (Address already in use (os error 98)). ¿Hay otro programa, u otro Crucible, usando ese puerto?

Another Crucible, or another program, already has that port. Close the other window or change the port in banco.yaml. To see who has it:

  • Linux: ss -ltnp | grep :5025
  • Windows: netstat -ano | findstr :5025

«no encuentro ningún banco»

I cannot find any bench.

$ cd /tmp
$ crucible

error: no encuentro ningún banco. He buscado en:
  banco.yaml
  /usr/bin/banco/banco.yaml
  /usr/share/crucible/banco/banco.yaml

Indica uno: crucible <banco.yaml>

Pulsa Enter para cerrar.

You ran crucible with no arguments in a folder without banco.yaml, and there is no example bench installed where it looks (the message lists the places it searched). Give the path: crucible ~/mi_banco/banco.yaml. (With no arguments, Crucible waits for Enter —Pulsa Enter para cerrar— in a terminal too.)

The Windows window closes on double-click

On an error, the window waits for you to press Enter. If it still closes, open it from a terminal (cmd) to read the message.

nc hangs

-q1 is missing: Debian’s and Ubuntu’s nc does not close the connection after sending. printf '*IDN?\n' | nc -q1 127.0.0.1 5025.

My software times out

  • Missing termination \n on writes: Crucible does not process the line until the newline. With ::SOCKET resources, set it in VISA (chapter 4).
  • It is a command, with no ?: it answers nothing, like a real instrument. Do not read after a command.
  • The query is not in the profile, or a formula failed: no answer arrives and the error is queued. Ask SYST:ERR?.

SYST:ERR? returns -113,"Undefined header;…"

That header is not in the profile. Check the pattern, and whether it is a command or a query (query: true).

A chained command does nothing

SOUR:VOLT 3;OUTP OFF leaves a -113 with SOUR:OUTP. It is the SCPI rule: after ;, the header is relative to the previous node. Write ;:OUTP OFF (chapter 8).

MEAS:…? always returns 0.0

The model’s cuando is not met, for instance because the output is off. Check the state with its query (OUTP?).

An answer comes out with braces: {tensoin}

The variable does not exist in the state: there is a typo in respuesta.

A unit does not start with its estado_inicial value

The variable name is misspelled: estado_inicial does not warn about variables that do not exist (chapter 5). Or something sent *RST, which returns to the profile’s values.

I changed a YAML file and nothing changed

Crucible reads the files at start-up. Stop it with Ctrl+C and start it again.

A configured value “stuck” from a previous run

The state is shared and lasts while Crucible is running (chapter 8). Send *RST at the start of your tests or restart Crucible.