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.
| Key | Meaning |
|---|---|
banco, dispositivos | bench, devices |
perfil, transporte, puerto | profile, transport, port |
estado, estado_inicial | state, initial state |
comandos, patron, muta, respuesta | commands, pattern, mutates, response |
modelos, cuando, formato | models, 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
| Word | What 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 resource | The 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
pyvisaandpyvisa-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
cruciblecommand 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
- Run
crucible-0.1.0-setup.exe. It does not ask for administrator rights. - 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
cruciblecommand on thePATH, 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
cruciblewith 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:
banco.yamlin the folder you run it from.banco/banco.yamlnext to the executable (Windows installation)./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?.
-q1matters. Without it, Debian’s and Ubuntu’sncdoes 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:
| Field | What it is |
|---|---|
id | A name for you. It appears in the start-up table and in errors. |
perfil | The instrument’s profile, with its path relative to banco.yaml. |
transporte | Where it listens: { tipo: tcp, puerto: 5025 }. Today tipo can only be tcp. |
estado_inicial | Optional. 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
| Field | What it is |
|---|---|
patron | The header in manual notation (below). No ? and no arguments. |
query | true if it is the query. Otherwise, it is a command. |
args | Names for incoming arguments, in order: with args: [v], in VOLT 5 <v> is 5. |
muta | State 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.
VOLTageacceptsVOLTandVOLTAGE, in upper or lower case. - Brackets mark nodes that can be left out.
[SOURce:]VOLTage[:LEVel]acceptsVOLT,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,1and0are stored as true or false, and models treat them the same.- If a declared argument does not arrive (
CONF:VOLTwith 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
| Field | What it is |
|---|---|
tipo | Always formula. |
expr | The formula. |
cuando | Conditions on the state. If any is not met, fallback is answered. If there are several, all must hold. |
fallback | The answer when cuando is not met. Defaults to 0.0. Number or text. |
formato | How 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:
| Function | Result |
|---|---|
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:
- With the output off
cuandois not met, and the measurement is thefallback, written in the model’s format:0.00000. OUTP?answers0, notfalse: it is answered by thesalida_como_numeromodel, withformato: entero, like a real instrument.- At 5 V across 10 Ω, half an ampere plus noise.
- At 20 V it would be 2 A, but
minclips it to the 1 A limit: the supply goes into constant current. - The multimeter writes scientific notation, like most do.
formato
Your software expects numbers written the way the real instrument writes them:
formato | Example answer | Typical use |
|---|---|---|
| (not set) | 1.0, 4.501385029307777 | — |
entero (integer) | 1, 0, -3 | States (OUTP?) and counters. |
fijo:N (fixed) | fijo:4 → 4.5014 | Supplies that answer with fixed decimals. |
cientifico:N (scientific) | cientifico:6 → +4.501385E+00 | Multimeters 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
| Command | What it does |
|---|---|
*IDN? | Returns dispositivo.idn. |
*RST | Returns the state to the profile’s values. |
*CLS | Clears the error queue and the status register. |
*OPC, *OPC?, *WAI | Synchronisation. 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 ONworks:VOLThangs off the root, soOUTPdoes too.SOUR:VOLT 3;OUTP OFFdoes not: the current node isSOUR, andOUTPis read asSOUR: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
*RSTat the start, or restart Crucible, if you want to start from scratch. SIMulate:INPutis 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.
- 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…
- Copy the real device’s
*IDN?intodispositivo.idn. - 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}".
- a variable in
- 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. Matchformatoto the device. - Add the instrument to the bench with its port (chapter 5).
- Validate:
crucible --validar banco.yaml. - 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]acceptsMEAS:VOLT,measure:voltage:dcand every other form;MEAS:VOLTonly 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_inicialon 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.yamlandmultimetro.yamlare 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/
| Form | What it does |
|---|---|
crucible | Starts 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 --version | The version. |
crucible --help | The 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
| Element | Examples |
|---|---|
| Variables | tension, carga_ohm |
| Numbers | 10, 0.5, 1e-3, 2.5E+2 |
| Operators | + - * /, parentheses, unary minus |
| Functions | gauss(mean, sigma), abs(x), sqrt(x), min(a, b), max(a, b) |
SCPI errors you will see
| Error | When |
|---|---|
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.
| Limitation | Consequence |
|---|---|
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
\non writes: Crucible does not process the line until the newline. With::SOCKETresources, 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.