Sci-SDK Library
SDK for Sci-Compiler projects
Loading...
Searching...
No Matches
BoardEmulator — User Guide

The SciSDK board emulator: a complete Sci-Compiler board, in software.

The emulator and Sci-Compiler Lab running side by side

‍Note on the screenshots. The product was previously called NiSciFakeBoard and the screenshots in this guide still show that name in title bars, device trees and paths. Everything is now called BoardEmulator; read the old name as the new one. The model string NiSciFakeBoard is still accepted by SciSDK as an alias of BoardEmulator, so existing code keeps working.

</blockquote>

Table of contents

  1. Introduction
  2. Quick start
  3. GUI tour
  4. Data sources
  5. Event timing
  6. Debug mode
  7. Custom data providers
  8. Working with Sci-Compiler Lab
  9. Headless and CI/CD
  10. Discovery APIs
  11. Troubleshooting

1. Introduction

BoardEmulator is a software emulator of a Sci-Compiler board. It listens on a TCP port and speaks the same wire protocol that the SciSDK HAL uses for a real Ethernet board, so SciSDK connects to it exactly as it connects to hardware: same AddNewDevice(), same RegisterFile.json, same component paths, same parameters, same buffers. Nothing in your application changes — only the connection string.

from scisdk.scisdk import SciSDK
sdk = SciSDK()
sdk.AddNewDevice("127.0.0.1:8888", "BoardEmulator", "RegisterFile.json", "board0")
# from here on, every line of SciSDK code is the same as on a real board

Why this matters

  • Develop without hardware. A board is expensive, shared between people, often in another room or another country, and frequently busy acquiring real data. With BoardEmulator every developer has a private board on their laptop, including on a train.
  • Develop downstream tools. Sci-Compiler Lab, LabView panels, custom acquisition software and scripts can be built and demoed end-to-end against the emulator: it produces physically plausible waveforms, growing spectra, event lists and live registers.
  • Test SciSDK itself. All 16 memory-mapped component types are emulated with the exact bus semantics of the SciSDK drivers, so a regression in a driver is caught by the emulator's end-to-end tests.
  • CI/CD. The emulator runs headless, with no Qt dependency, prints a single READY port=NNNN line and exits 0 on a clean shutdown — everything a CI job needs to start a "board", run the test suite against it and tear it down.
  • Reproducibility. The whole state (board, channels, components, data sources, register scripts, seeds) lives in one JSON file. A failing CI run can be replayed bit for bit.
  • Situations hardware cannot easily produce. Pile-up at 1 Mcps, a FIFO overflow, a transistor-reset preamplifier ramping into saturation, a 10-hour acquisition compressed into seconds, a single event stepped by hand — all one click away.

The opening screenshot shows the typical working setup: the emulator on the left with a component panel open, and Sci-Compiler Lab on the right connected to it over 127.0.0.1:8888, plotting the data the emulator is generating.

What is emulated

Area Covered
Component types Spectrum, TofSpectrum, Histogram2D, Oscilloscope, OscilloscopeDual, WaveDump/Digitizer, FFTMonitor, ListModule, CustomPacket, FrameTransfer, LogicAnalyzer, MCRatemeter, RegisterFile / MultichannelRegister, CitirocCfg, CitirocFrame, PetirocFrame, plus a generic fallback for future types
Board Name, model, listening port, N analog channels, per-channel AFE (gain, offset, shaper, input impedance, divider) mapped onto the internal register space
Registers Sparse register file, live table, Python on_read / on_write hooks
Data Analytic distributions, 58 measured spectra, detector pulse emulator, full function generator, file playback, packet field generator, user Python providers
Control Per-component and global pause / step / step N / run for; programmable event timing

2. Quick start

2.1 Install

Three ways, and which one you want depends on whether you intend to change it.

a) With the SciSDK installer (Windows) — nothing else to install. The setup ships the emulator as a component, frozen: Python, Qt and all. It lands in C:\OpenHardware\SciSDK\BoardEmulator\ and the Start Menu gets a BoardEmulator entry. Two executables in that folder, and the difference matters:

BoardEmulator.exe the GUI. No console window behind it
boardemulator-cli.exe the same program with a console, for --headless

Use the second one from scripts: headless mode announces itself with a line on standard output (READY port=NNNN) that a launcher waits for, and a windowed program has no standard output to write it to.

b) From the package — the way to script it, and what CI uses. Python ≥ 3.10; the core needs only numpy, the GUI adds PySide6 and pyqtgraph:

pip install boardemulator[gui] # from the internal package index
boardemulator --headless --resource RegisterFile.json

Without the [gui] extra you get a headless-only installation (numpy only) — that is what you want in CI.

c) From the sources — the way to change it.

cd BoardEmulator/emulator
python -m venv .venv
.venv/Scripts/pip install -e .[gui] # Windows
# source .venv/bin/activate && pip install -e .[gui] # Linux / macOS

‍Building the frozen executables yourself: pip install pyinstaller and then python -m PyInstaller --noconfirm packaging/boardemulator.spec from BoardEmulator/emulator. packaging/smoke-test.py dist/BoardEmulator then starts the result and talks to it over the wire — an executable that exists is not the same as an executable that works.

2.2 Start the GUI

python -m boardemulator # empty board
python -m boardemulator --resource RegisterFile.json # load a board description
python -m boardemulator --config myboard.json # restore a saved session

The server starts listening on port 8888 as soon as the window opens (the status bar shows Server: listening · Port: 8888 · Clients: 0).

2.3 Start headless

python -m boardemulator --headless --resource RegisterFile.json --port 8888

stdout contains exactly one machine-readable line:

READY port=8888

2.4 Connect from SciSDK — Python

import time
from scisdk.scisdk import SciSDK
sdk = SciSDK()
res = sdk.AddNewDevice("127.0.0.1:8888", "BoardEmulator", "RegisterFile.json", "board0")
assert res == 0
sp = "board0:/MMCComponents/Spectrum_0"
sdk.SetParameterUInteger(sp + ".rebin", 0)
sdk.SetParameterString(sp + ".limitmode", "freerun")
sdk.ExecuteCommand(sp + ".reset", "")
sdk.ExecuteCommand(sp + ".start", "")
res, buf = sdk.AllocateBuffer(sp)
time.sleep(1.0)
res, buf = sdk.ReadData(sp, buf)
total = sum(buf.data[i] for i in range(buf.info.total_bins))
print("counts:", total) # grows at every read, event by event
sdk.FreeBuffer(sp, buf)
sdk.DetachDevice("board0")

An oscilloscope, verbatim from the end-to-end test:

osc = "board0:/MMCComponents/Oscilloscope_0"
sdk.SetParameterInteger(osc + ".decimator", 0)
sdk.SetParameterInteger(osc + ".pretrigger", 150)
sdk.SetParameterString(osc + ".trigger_mode", "self") # self | analog | digital | ext | disabled
sdk.SetParameterInteger(osc + ".trigger_level", 300)
sdk.SetParameterInteger(osc + ".trigger_channel", 0)
sdk.SetParameterString(osc + ".trigger_polarity", "pos")
sdk.SetParameterString(osc + ".acq_mode", "blocking")
sdk.SetParameterInteger(osc + ".timeout", 5000)
sdk.SetParameterString(osc + ".data_processing", "decode")
sdk.SetParameterInteger(osc + ".auto_arm", 1)
res, obuf = sdk.AllocateBuffer(osc)
res, obuf = sdk.ReadData(osc, obuf)
print(obuf.info.channels, "ch x", obuf.info.samples_analog, "samples")
sdk.FreeBuffer(osc, obuf)

Registers behave exactly as on a board — note the two different namespaces:

# entries of the JSON "Registers" array: real register nodes
sdk.SetRegister("board0:/Registers/TIME_A", 0x1234)
err, value = sdk.GetRegister("board0:/Registers/TIME_A")
# entries of a RegisterFile MMC component: PARAMETERS of the component
sdk.SetParameterUInteger("board0:/MMCComponents/REGFILE_0.OFFSET", 1234)
err, value = sdk.GetParameterUInteger("board0:/MMCComponents/REGFILE_0.OFFSET")

2.5 Connect from SciSDK — C / C++

#include <scisdk/SciSDK_DLL.h>
void *sdk = SCISDK_InitLib();
int res = SCISDK_AddNewDevice("127.0.0.1:8888", "BoardEmulator",
"RegisterFile.json", "board0", sdk);
if (res != NI_OK) { /* SCISDK_GetErrorString / s_error */ }
SCISDK_SetParameterString("board0:/MMCComponents/Oscilloscope_0.trigger_mode", "self", sdk);
SCISDK_SetParameterInteger("board0:/MMCComponents/Oscilloscope_0.pretrigger", 150, sdk);
SCISDK_SetParameterString("board0:/MMCComponents/Oscilloscope_0.data_processing", "decode", sdk);
SCISDK_SetParameterString("board0:/MMCComponents/Oscilloscope_0.acq_mode", "blocking", sdk);
SCISDK_AllocateBuffer("board0:/MMCComponents/Oscilloscope_0",
T_BUFFER_TYPE_DECODED, (void **)&ob, sdk);
SCISDK_ReadData("board0:/MMCComponents/Oscilloscope_0", (void *)ob, sdk);
SCISDK_FreeBuffer("board0:/MMCComponents/Oscilloscope_0", T_BUFFER_TYPE_DECODED, (void *)ob, sdk);
SCISDK_DetachDevice("board0", sdk);
#define NI_OK
Definition NIErrorCode.h:8
SCISDK_DLL_API int SCISDK_SetParameterString(char *Path, char *value, void *handle)
Set the value of a parameter for the specific Sci-Compiler Memory Mapped Component or route the value...
SCISDK_DLL_API int SCISDK_SetParameterInteger(char *Path, int value, void *handle)
Set the value of a parameter for the specific Sci-Compiler Memory Mapped Component or route the value...
SCISDK_DLL_API int SCISDK_DetachDevice(char *name, void *handle)
Remove a device from the tree of memory mapped resources. The function will call the device specific ...
SCISDK_DLL_API int SCISDK_FreeLib(void *handle)
Release the SciSDK handles and free the resources.
SCISDK_DLL_API int SCISDK_AddNewDevice(char *DevicePath, char *DeviceModel, char *JSONFwFilePath, char *Name, void *handle)
Connect to and hardware device. The function tries to connect to the device, decode the Sci-Compiler ...
SCISDK_DLL_API void * SCISDK_InitLib()
Initialize the SciSDK handles accolating the resources. This function must be called before any other...
SCISDK_DLL_API int SCISDK_AllocateBuffer(char *Path, T_BUFFER_TYPE buffer_type, void **buffer, void *handle)
Allocate a buffer to be used to store data from the board for the specific Memory Mapped Componet (os...
SCISDK_DLL_API int SCISDK_FreeBuffer(char *Path, int buffer_type, void **buffer, void *handle)
Release the memory allocated by SCISDK_AllocateBuffer() or SCISDK_AllocateBufferSize()
SCISDK_DLL_API int SCISDK_ReadData(char *Path, void *buffer, void *handle)
Read data from the board for the specific Memory Mapped Componet (oscilloscope, spectrum,...
@ T_BUFFER_TYPE_DECODED
Definition scisdk_defines.h:230
Oscilloscope decoded data type.
Definition scisdk_defines.h:20

2.6 Connecting without a local RegisterFile.json

BoardEmulator is one of the boards that can serve its own firmware description ("registerfile_json": "from_device" in the board catalog). Pass an empty JSON path and SciSDK downloads the description from the emulator:

SCISDK_AddNewDevice("127.0.0.1:8888", "BoardEmulator", "", "board0", sdk);
sdk.AddNewDevice("127.0.0.1:8888", "BoardEmulator", "", "board0")

This is the fastest way to work: load any RegisterFile.json into the emulator, and every client connects with an empty path and always gets the current description. If a local path is given it always wins over the copy stored in the board. The JSON actually in use can be read back at any time with GetFirmwareJSON (see §10).

2.7 Two things that will bite you once

  • Port 8888. It is the default SciSDK Ethernet port and the emulator's default. For all the Ethernet boards except BoardEmulator the connection path is split on : and an explicit port is ignored; for BoardEmulator IP:port is honoured, so "127.0.0.1:9100" works if you started the emulator with --port 9100. If 8888 is already taken (a real board driver, another emulator instance, a leftover process), the server cannot start — pick another port.
  • Windows System32 shadowing. A system-wide SciSDK installation copies SciSDK_DLL.dll into C:\Windows\System32, which the loader searches before PATH. Putting your build folder in PATH is not enough: the installed (possibly old) driver wins, does not know the emulator model and silently falls back to the legacy FAKEBOARD stub — every read then returns uninitialised memory. Load your DLL by absolute path first so that a later plain-name load binds to the module already in memory:

    import ctypes
    ctypes.CDLL(r"C:\GIT\scisdk\SciSDK_VC++\bin\x64\Release\SciSDK_DLL.dll")
    from scisdk.scisdk import SciSDK # now binds to the DLL above

3. GUI tour

The window has a fixed toolbar at the top (global debug controls, see §6), a row of tabs, and a status bar showing server state, port, number of connected clients and the total event rate being generated.

3.1 Board page

Board page
  • Board identity — board name, board model (e.g. R5560SE, which selects the internal register map being emulated), listening port and number of analog channels.
  • Analog front end — one row per input channel: Gain, Offset, Shaper, Input impedance, Divider. These are the same values the internal register space exposes, so writing the AFE registers from SciSDK moves these fields and vice versa; the detector emulator uses gain and offset to scale pulse amplitude and baseline.
  • Bus traffic — a live log of every transaction the server routes: time, client address, operation (Read register, Write register, Read data block, ...), address, argument and result (status 0, 2048 words). Pause log and Clear log plus filters by address prefix and by operation. The bus is much faster than any table widget, so the footer tells you how many transactions were not displayed (5677 transactions not shown ...) — the emulator keeps the full log internally.

3.2 Components page

Components page

This is the board's content: one row per memory-mapped component with Name, Type (human readable: Energy spectrum, Oscilloscope, ...), Base address, State, Events generated so far and Buffer / errors.

Button What it does
Load register file... Load the RegisterFile.json of any board. Every component is instantiated with a sensible default preset and starts producing immediately
Add component... Build a board from scratch, one component at a time (type, name, base address, geometry)
Remove selected Drop a component
Export register file... Write out the current board as a RegisterFile.json — this is the file your application will use
Open panel Open the component's own tab (double-clicking the row does the same)

Loading a register file is the normal starting point: take the JSON your Sci-Compiler project generated, load it, and you have a working board in a second.

The File menu has Save settings... / Load settings... for the full session JSON (everything, seeds included); the Server menu starts and stops the TCP server.

3.3 Component panels

Each open component gets its own tab. All panels share the same layout:

  • Header — Name — Readable type, base address and a one-line explanation of what the component does, with the run state on the right.
  • Component box — read-only geometry taken from the RegisterFile: histogram bins, counter width, samples per acquisition, channels, ADC resolution, firmware version, base address...
  • Signal source — Provided by: Built-in generator or Custom Python provider; then the list of presets available for this component type, and the form for the selected preset (every parameter with its unit, range and tooltip with the formula). Apply source commits the change; the status line at the bottom left confirms it.
  • Event timing — the arrival pattern (see §5).
  • Live preview — pyqtgraph plots refreshed at ~10 Hz, showing exactly what the clients are reading.
  • ADC coding (waveform components only) — see §4.3.

Spectrum panel

Spectrum panel

The preview overlays the Configured distribution (dashed) and the Measured histogram (filled) so you can see the histogram converge onto the requested shape event by event — here 4096 channels, 5,257,650 counts, peak 23,459, with the two full-energy peaks sitting on the exponential Compton continuum. A Logarithmic counts checkbox switches the Y axis.

Oscilloscope panel

Oscilloscope panel

Two stacked plots: Source signal (free running) — the continuous stream the detector emulator produces, with the trigger level drawn as a dashed line — and Last acquisition — the frame the trigger actually captured, with the trigger position marked. The header line summarises the state: Idle · trigger analog ch0 · 13,964 acquisitions · 2 channels x 1024 samples. This is the fastest way to understand why a client is not triggering: if the dashed level sits above every pulse in the top plot, the bottom plot stays empty.

3.4 Registers page

Registers page
  • Left — the live register table: Name, Component the register belongs to ((board) for the board-level ones), Address, value in hexadecimal and in decimal. Values update live as clients read and write them, so you can watch a SciSDK session drive the board. Add register... / Remove selected and a free-text filter on name or component. The grouping by component makes a 30-register board readable at a glance: READ_STATUS, READ_POSITION, CONFIG_TRIGGER_MODE, CONFIG_PRETRIGGER, CONFIG_TRIGGER_LEVEL, CONFIG_ARM, CONFIG_DECIMATOR for Oscilloscope_0, STATUS for Spectrum_0, and so on.
  • Right — Register script. Attach Python to any address: the read hook may rewrite the value the client sees, the write hook may rewrite the value that gets stored.

The two hooks are plain functions:

# def on_read(addr, value) -> new_value_or_None
def on_read(addr, value):
return value * 18
# def on_write(addr, value) -> stored_value_or_None
def on_write(addr, value):
return value

Returning None keeps the default behaviour. Inside a hook you also have:

Name Meaning
regs[addr] read / write the register file directly (no hook recursion)
internal[wire] read / write the internal (board / AFE) register space
addr, value the address and value of the current transaction
log(...) append a line to the emulator log

Type the code, press Test the read hook / Test the write hook to run it once without side effects, then Attach to this register (Attached to this register. in green) or Detach. Scripts run on a worker thread with a wall-clock timeout and fully isolated errors: a broken script logs and is skipped, it never takes the server down.

A useful, working example — make a status register count the reads and mirror a configuration register:

def on_read(addr, value):
n = regs[0x900] + 1 # a scratch register used as a counter
regs[0x900] = n
log(f"status read #{n}")
return (n & 0xFFFF) | (regs[0x802] << 16) # low: counter, high: trigger mode

4. Data sources

Every component has a replaceable data source. The presets offered depend on the component type; all of them are sampled event by event, never filled with a static array.

4.1 Spectra (Spectrum, TofSpectrum)

Preset What you get
Detector spectrum (peaks + Compton continuum) Full-energy peaks on an exponential Compton continuum plus a flat background — what a real gamma detector produces. Parameters: Continuum amplitude (default 120), Continuum decay (bins) (bins/5), Flat background (10) and a table of peaks (Centre (bin), Sigma (bins), Amplitude); defaults are two peaks at 0.62 and 0.31 of the range with sigma = bins/160 and amplitudes 1000 / 400
Analytic distribution A closed-form probability density sampled through its quantised CDF
Measured spectrum (library) One of the 58 real measured spectra shipped with the emulator, resampled onto this component's bins
Histogram from file A histogram read from CSV or binary, used as the sampling distribution

The analytic shapes, with their parameters (labels as shown in the GUI):

Group Shapes
Continuous Gaussian, Uniform (flat), Exponential decay, Log-normal, Gamma, Beta, Weibull, Rayleigh, Maxwell-Boltzmann, Chi-squared, Student's t, Cauchy / Lorentzian, Laplace (double exponential), Triangular
Physics Landau, Detector spectrum (peaks + Compton continuum)
Discrete Poisson, Binomial, Geometric

The measured-spectrum library covers the isotopes and samples you would actually calibrate with — Cs-137, Co-60, Co-57, Na-22, Am-241, Ba-133, Eu-152, K-40, Ra-226, Lu-176, I-131, Tc-99m, Ac-225, Pb-210, ... — plus real-world samples (KCl, Fiestaware, RadiumDial, Pitchblende, Uranium-glaze, Sabbia_Tenerife, grass_chernobil, He3_tube_neutron_spectrum, HPGe backgrounds). Pick one and the emulator samples that spectrum as a distribution: shapes, peak ratios and continuum are those of a real measurement.

Sampling pipeline (identical for Histogram2D): source histogram → area-preserving linear resample onto the component's bins → CDF quantised to 32 bit → inverse-transform sampling with a seeded RNG. Same seed, same sequence of events.

4.2 Oscilloscope / Digitizer (Oscilloscope, OscilloscopeDual, WaveDump, FFTMonitor)

Preset What you get
Detector — continuous reset (RC discharge) Charge-sensitive preamplifier with a discharging feedback resistor: each event is a pulse that decays back to the baseline
Detector — transistor reset (ramp + reset) Charge integrator without feedback resistor: the baseline climbs one step per event until a reset discharges it
Function generator Deterministic laboratory waveform (sine, square, chirp, noise, ...)
Waveform from file Replay samples recorded in a CSV or binary file, in loop

The physics, briefly. A radiation detector delivers a packet of charge per event. In a continuous reset (resistive feedback) preamplifier, that charge is integrated on the feedback capacitor and then bled away through the feedback resistor: you see a pulse with a fast rise (tau_rise, tens of ns) and an exponential fall (tau_fall, µs), returning to baseline. Overlapping tails are pile-up, and they simply add — exactly as in the emulator. In a transistor reset (pulsed reset) preamplifier there is no feedback resistor, because a resistor adds thermal noise; the charge accumulates and the output is a staircase that climbs one step per event, each step proportional to the event energy. When the output approaches saturation a transistor shorts the capacitor and the baseline is dumped back to zero in a short ramp (or exponential), during which the channel is blind. Best noise performance, at the cost of a duty cycle. Both are reproduced, with the reset triggered when the baseline crosses the threshold, at a fixed period, or whichever comes first, and with a linear ramp or exponential discharge recovery shape.

Detector parameters (core names, GUI labels in brackets where they differ): rate_hz (event rate), sample_rate_hz (sampling frequency), pulse_shape (double_exp = Double exponential (rise + fall), single_exp = Single exponential (step + RC decay), charge_integrator = Charge integrator (staircase + reset)), tau_rise, tau_fall, baseline_offset, afe_gain, afe_offset, and for the charge integrator reset_mode, reset_threshold, reset_period_samples, reset_duration_samples, reset_shape. Amplitudes are drawn from the configured spectrum — the distribution you choose is the amplitude spectrum, so an oscilloscope and a spectrum on the same board can be made physically consistent.

The function generator covers, per group:

Group Waveforms
Periodic Sine, Cosine, Square (duty), Triangle (symmetry), Ramp up / Ramp down, Staircase, Half- and Full-rectified sine, Clock (with edge jitter)
Pulses Trapezoidal pulse (delay / rise / flat top / fall in samples), DC level, Sinc, Gaussian pulse, Lorentzian pulse, Trapezoid (relative timing), Impulse train
Detector-like Single exponential decay, Double exponential pulse, Step with overshoot, Step with undershoot, Ringing, Damped sine, Damped cosine, Step response
Noise White, Gaussian, Pink (1/f), Brown (random walk), Blue, Violet, Uniform (min/max), Exponential, Poisson, Impulse / shot, Quantisation, Flicker (1/f octaves), Popcorn / telegraph
Modulated AM, FM, PM, PWM, FSK, PSK, ASK, QPSK
Sweeps Linear chirp, Exponential chirp, Logarithmic chirp, Morlet wavelet, Mexican hat wavelet
Digital PRBS-7, PRBS-15, PRBS-23, NRZ, RZ, Manchester

Time base convention: one frame is nsamples samples, so a frequency of 1.0 means one cycle per frame and a time constant of 1.0 means one frame long. Amplitudes are relative: 1.0 is full scale. Additive noise can be layered on any waveform, and the generator is phase-continuous across calls — no discontinuity between one acquisition and the next.

The scale chain is:

counts = relative_amplitude * full_scale + offset (+ noise)
counts = clamp(counts, ADC_MIN, ADC_MAX)

full_scale is the size of the signal and offset its baseline; neither clips. The only clamp is the physical ADC range of the channel.

4.3 ADC coding

Waveform components (Oscilloscope, OscilloscopeDual, WaveDump) have an ADC coding box. Real boards exist with WordEnob of 16, 20, 24, 28 or 32 bits, and each channel can code its samples independently, so the coding is a per-channel choice:

Coding Range
Unsigned 0 .. 2^enob - 1
Signed (two's complement) -2^(enob-1) .. 2^(enob-1) - 1

The clamp range of the generators follows from this choice automatically (signed channels sit around zero, unsigned ones on a positive baseline), and the trigger level is interpreted in the same coding. The digitizer (WaveDump) packs two 16-bit samples per 32-bit word whatever WordSize says, so its coding is always evaluated at 16 bits.

4.4 Lists and custom packets (ListModule, CustomPacket, FrameTransfer, Citiroc/Petiroc frames)

Preset What you get
Field generator Each word of the packet is built by a named field generator
Packets from file Replay raw packet words recorded in a CSV (one column per field) or binary (stream of u32 little-endian) file, once or in loop

Field kinds:

Kind Parameters
Constant value value (hex or decimal)
Event counter start, step, wrap
Timestamp counter running at a simulated clock frequency
Random (uniform) min, max
Random (Gaussian) mean, sigma
Math expression expression over n (event index), t (time), ch and the other fields
Sampled from a spectrum samples the distribution generator — e.g. an energy field
Packed bit-field a list of named sub-fields with a width in bits, packed MSB-first into one 32-bit word

For CustomPacket the packet_size of the RegisterFile is honoured and an optional constant align word is prepended to every packet — the word SciSDK checks when check_align_word is enabled (0xFFFFFFFF in the shipped examples):

cp = "board0:/MMCComponents/CP_0"
sdk.SetParameterString(cp + ".data_processing", "decode")
sdk.SetParameterString(cp + ".acq_mode", "non-blocking")
sdk.SetParameterString(cp + ".check_align_word", "true")
sdk.ExecuteCommand(cp + ".start", "")
res, cbuf = sdk.AllocateBuffer(cp, 1024) # CustomPacket has no default size: pass one
res, cbuf = sdk.ReadData(cp, cbuf)
print(hex(cbuf.data[0].row[0])) # -> 0xffffffff

4.5 Histogram2D

Preset What you get
Correlated blobs (2-D Gaussians) A few correlated 2-D Gaussian spots — (x0, y0, sigma_x, sigma_y, rho, amplitude) — on a uniform background
Two independent distributions (X × Y) X and Y drawn from two independent 1-D distributions, each configured like a spectrum
Histogram from file CSV row-major, or binary u32

4.6 Ratemeter (MCRatemeter)

One preset, Per-channel rates: a fixed average counting rate per channel, refreshed by the producer with a small random fluctuation, exposed as one u32 per channel exactly as the driver expects.


5. Event timing

The timing engine decides how many events are produced in each tick; the data source decides what they look like. It is configured per component, in the Event timing box (GUI) or with the timing command / config JSON.

Mode (GUI label) Id Parameters Behaviour
Constant rate constant Event rate (ev/s) Fixed 1/rate intervals
Poisson (random arrivals) poisson Event rate (ev/s) Exponentially distributed inter-arrival times — realistic radiation counting
Burst burst Burst size (events), Rate inside the burst (ev/s), Pause between bursts (ms) A packet of events at the burst rate, then a pause, repeated
Burst with rate ramp burst_ramp as Burst plus Ramp target rate and ramp shape (Linear / Logarithmic) Burst whose rate ramps from the burst rate to the target rate

The same scheme applies to spectra and 2-D histograms (event accumulation rate) and to the detector emulator (pulse rate). One exception: components whose simulated time is measured in samples rather than seconds — the oscilloscope acquisition — bypass the timing engine and are paced by their own sample budget.

From the CLI or a scenario file:

timing Spectrum_0 mode=poisson rate=50000
timing List_0 mode=burst burst_size=100 burst_rate=1000000 pause_ms=50

6. Debug mode

Data generation can be frozen and advanced by hand, per component or globally. The bus stays coherent while paused: status registers keep answering, FIFOs simply stop growing.

Action GUI CLI / scenario Provider / script API
Pause Pause pause [component] ctx.component("X").pause()
Resume Resume resume [component] .resume()
One event Single step step [component] .step()
N events value box + Step events step_n N [component] .step_n(n)
Run for T ms then auto-pause ms box + Run for run_for MS [component] .run_for(ms)

The toolbar at the top of the window (All components: Pause | Resume | Single step | [10] Step events | [1000.0 ms] Run for) acts on everything at once; each component panel has the same controls for itself. Omitting the component name in a CLI command means "all components".

Start an emulator with everything frozen and advance it deterministically:

python -m boardemulator --headless --resource RegisterFile.json --pause-on-start --commands

then on stdin:

step_n 1000 Spectrum_0
status
resume

The debug state is part of the saved configuration, but a fresh start is always running.


7. Custom data providers

When no preset produces what you need — a specific detector, a replayed experiment, a protocol under test — plug in your own Python class. Built-in generators use the very same interface, so a user provider is a first-class citizen.

7.1 The contract

Subclass the base class matching the component kind:

Base class Components Method to implement
WaveProvider Oscilloscope, OscilloscopeDual, WaveDump, FFTMonitor next_n(n) -> np.ndarray[float64]
SpectrumProvider Spectrum, TofSpectrum sample_n(n) -> np.ndarray[int] (event amplitudes)
Spectrum2DProvider Histogram2D sample_n(n) -> (np.ndarray, np.ndarray)
PacketProvider ListModule, CustomPacket, FrameTransfer, Citiroc/PetirocFrame next_events(max_events) -> np.ndarray[uint32] (whole packets)

Optional hooks on every provider: attach(ctx) (once, keep ctx), detach(), configure(params: dict), reset(), on_register_write(name_or_addr, value), describe().

ProviderContext is the door to the rest of the board:

Member Use
ctx.registers["NAME"] / ctx.registers[addr] read and write bus-visible registers
ctx.internal[(addr, idx)] internal register space (AFE and friends)
ctx.subscribe_register(name_or_addr) enable on_register_write for that register
ctx.me handle of the component being served (.component, .pause(), .step_n(), ...)
ctx.component("Name") handle of another component
ctx.provider_of("Name") the provider instance of another component
ctx.board board / per-channel AFE state
ctx.channel the channel this instance serves (None for single-source components)
ctx.log(msg) write to the emulator log

Two rules worth remembering: the timing engine decides how many events are requested, the provider only decides what they look like; and the emulator never crashes because of a provider — any exception is logged and the component is paused, so check the emulator messages when a component stops unexpectedly.

7.2 Walkthrough: <tt>examples/providers/example_provider.py</tt>

The shipped template contains three classes; copy the file and edit it.

**RegisterDrivenPeak(SpectrumProvider)** — a Gaussian peak whose position and width are driven by board registers. This is the register ↔ generator interoperability in its simplest form:

class RegisterDrivenPeak(SpectrumProvider):
POS_REGISTER = "PEAK_POS"
SIGMA_REGISTER = "PEAK_SIGMA"
def attach(self, ctx):
super().attach(ctx)
handle = ctx.me
self.bins = int(getattr(handle.component, "bins", self.bins)) # geometry from the component
self.mean = self.bins * 0.5
self.sigma = max(1.0, self.bins * 0.02)
ctx.subscribe_register(self.POS_REGISTER) # react to writes
ctx.subscribe_register(self.SIGMA_REGISTER)
for name in (self.POS_REGISTER, self.SIGMA_REGISTER):
if name in ctx.registers: # seed from current values
self.on_register_write(name, ctx.registers[name])
ctx.log(f"RegisterDrivenPeak ready ({self.bins} bins)")
def on_register_write(self, name_or_addr, value):
if name_or_addr == self.POS_REGISTER and value:
self.mean = float(np.clip(value, 0, self.bins - 1))
elif name_or_addr == self.SIGMA_REGISTER and value:
self.sigma = float(max(1, value))
def sample_n(self, n): # n comes from the timing engine
...

Write PEAK_POS from SciSDK (or from the register table in the GUI) and the peak moves in the next events — a perfect target for testing an auto-calibration routine.

**EnergyTaggedEvents(PacketProvider)** — list events whose energy is drawn from another component's provider, via ctx.provider_of("Spectrum_0"), so the list and the spectrum stay physically consistent. It reads the packet width from the component itself (packet_size for CustomPacket, chunk_words for ListModule) and emits n × packet_size words with a timestamp, an energy and a channel id.

**SquareWave(WaveProvider)** — the minimal waveform provider; it also shows how to read the simulated AFE (ctx.board.channel(ch).gain) and how configure(params) maps GUI / config parameters onto attributes.

Providers may also write registers (status words, counters), which is how you emulate a firmware block that reports back to the host.

7.3 Loading a provider

From the GUI: open the component panel, set Provided by to Custom Python provider, pick the .py file, choose the class from the discovered list, press Apply source. The status line reports Custom provider attached. or the import error. There is also a reload button, so the edit → reload → observe loop needs no restart.

From code or configuration:

emulator.load_provider("Spectrum_0", "example_provider.py", "RegisterDrivenPeak")
emulator.load_provider("List_0", "example_provider.py", "EnergyTaggedEvents")

Providers are saved in the session JSON (path, class, params, channel) and restored on load, so a CI run can use them too.


8. Working with Sci-Compiler Lab

Sci-Compiler Lab is a SciSDK client like any other: point it at 127.0.0.1:8888 with model BoardEmulator and it connects, enumerates the firmware and starts plotting. Nothing in Lab knows it is not talking to hardware.

The device tree and the waveform

Sci-Compiler Lab: oscilloscope connected to the emulator

The Explorer shows the connected device — NiSciFakeBoard (127.0.0.1:8888) with a green "connected" dot (it will read BoardEmulator after the rename) — expanded into Registers (11) (TIME_A, ENABLE, IN1, IN2, SUMOUT, F_A, F_B, F_OUT, BIN_D, PULSE_PERIOD, ANALOG_OFFSET), Oscilloscope (1) and Spectrum (2). The oscilloscope panel is running with two channels and 1024 samples per acquisition; the detector pulse is captured right after the 150-sample pretrigger. The Properties pane on the right shows the parameters being pushed to the emulator: Trigger Mode Analog, Polarity Positive, Channel Channel 0, Level 300, Acquisition Pretrigger 150, Decimator 0, Data Processing Decode, Acq Mode Blocking. Those are exactly the parameters of §2.4, driven from a GUI instead of from Python.

The spectrum growing

Sci-Compiler Lab: spectrum from the emulator

Spectrum_0 displayed as a 1-D histogram, RUNNING, over 4096 channels: the exponential Compton continuum at low channel numbers and the two full-energy peaks at ~1270 and ~2540, matching the peak table configured in the emulator's spectrum panel. The counts climb as you watch. The regions & fits toolbar (Add region, Fit all, Energy calibration) works on this data like on any real measurement, which is exactly what you need to develop a fit or calibration feature without a source and a detector on the bench.

The register table

Sci-Compiler Lab: register table

A Register Table view with TIME_A (00000807), ENABLE (00000808), IN1 (00000809), SUMOUT (0000080B), each with a format selector, a Value Write field and Set / Get All / Set All buttons. Writing here produces a Write register row in the emulator's Bus traffic log and updates the emulator's register table instantly — and if you attached an on_write hook, its effect shows up here on the next read.

Typical firmware-less development flow

  1. Export the RegisterFile.json from your Sci-Compiler project (or design the board in the emulator's Components page and export it from there).
  2. Load it in BoardEmulator, keep the default presets, hit nothing else.
  3. Shape the data until it matches the experiment you are targeting: detector spectrum with your isotope's peaks, pulse shape and rate of your preamplifier, event timing of your trigger logic.
  4. Start Sci-Compiler Lab (or your own application) against 127.0.0.1:8888 and build the panels, the analysis, the calibration.
  5. Reproduce the awkward cases on purpose: pause the board mid-acquisition, step one event, push the rate to pile-up, switch a channel to signed coding, make a register script return an error status.
  6. Save the session JSON and commit it — the same board comes back on any machine, and in CI.
  7. When the real hardware arrives, change the connection string. Nothing else.

9. Headless and CI/CD

9.1 Command line

python -m boardemulator [options]
Flag Meaning
--config FILE Emulator settings JSON (the full persistent state)
--headless Run the TCP server only, no GUI (CI mode)
--port N TCP port to listen on (default 8888, or the one from --config)
--host ADDR Bind address (default 0.0.0.0)
--resource FILE RegisterFile.json to load; overrides the one inside --config
--duration S Run for S seconds, then exit cleanly (headless CI runs)
--seed N Master seed for the data generators (reproducible runs)
--save-config FILE Write the resulting full state to FILE and exit
--pause-on-start Start with every component paused (debug mode)
--commands Read debug commands from stdin
--scenario FILE Run a file of debug commands after startup
--quiet Suppress informational messages (they go to stderr)
--version Print the version and exit

Debug commands accepted on stdin and in a scenario file (lines starting with # are comments):

pause [comp] | resume [comp] | step [comp] | step_n N [comp] |
run_for MS [comp] | timing comp key=value... | status | quit | help

status prints a JSON snapshot (server stats, board, per-component state) on stdout — handy as an assertion source in a test.

9.2 The CI contract

  • stdout carries exactly one line, READY port=NNNN, flushed as soon as the server is listening. Wait for it; do not sleep.
  • Informational messages go to stderr, so stdout stays parseable.
  • Exit code 0 on a clean shutdown: SIGINT, SIGTERM, --duration expiry, or a quit command. Non-zero means the server could not start (1) or the arguments / config were invalid (2).
  • No PySide6 needed: the GUI is imported lazily, only when it is actually requested.
  • With --port 0 the OS picks a free port and READY port=NNNN tells you which — the safest option when several jobs share a runner.

9.3 Reproducible configuration

The whole state lives in one versioned JSON file, written by the GUI (File → Save settings...) or by --save-config:

{
"format": "...", "version": 1,
"seed": 12345,
"server": { "host": "0.0.0.0", "port": 8888 },
"board": { "...": "name, model, channels, AFE per channel" },
"resource": { "...": "a full RegisterFile.json document" },
"resource_path": "informative only",
"components": { "Spectrum_0": { "...": "source, timing, limits, debug state" } },
"providers": { "List_0": { "path": "my_provider.py", "class": "MyEvents" } },
"registers": { "values": { "2055": 4608 }, "scripts": { "2055": { "...": "" } } }
}

Seeds are applied before the presets, so a run is deterministic. Prepare the board interactively, save, commit the JSON, and CI replays it verbatim:

python -m boardemulator --config ci/board.json --headless --duration 120

--seed N on the command line overrides the seed for a one-off experiment.

9.4 A CI job

Pseudo-YAML, adapt to your runner:

test-scisdk-against-emulator:
image: python:3.11
steps:
- run: |
cd BoardEmulator/emulator
pip install -e . # no [gui] extra in CI: numpy only
- run: |
# start the emulator in the background and capture its stdout
python -m boardemulator --headless \
--config ci/board.json --port 8888 --seed 12345 \
> emulator.log 2>emulator.err &
echo $! > emulator.pid
# the CI contract: wait for the READY line, never sleep
timeout 30 bash -c 'until grep -q "^READY port=" emulator.log; do sleep 0.1; done'
cat emulator.log
- run: |
export SCISDK_DLL_PATH="$PWD/bin/x64/Release/SciSDK_DLL.dll" # absolute path!
export SCISDK_PY_WRAPPER="$PWD/wrapper/py_SciSDK/scisdk/src"
python -m pytest BoardEmulator/emulator/tests -q
- run: |
kill -INT "$(cat emulator.pid)" # clean shutdown, exit code 0
wait "$(cat emulator.pid)"
artifacts: [emulator.log, emulator.err]

The emulator's own test suite already contains tests/e2e_scisdk_real.py, which drives the real SciSDK C++ driver against an in-process emulator and skips cleanly when the native library is not available — the reference for how to wire this up in your own repository.


10. Discovery APIs

An application does not have to hard-code the list of models or the shape of a connection path; SciSDK can describe itself. Full reference in `docs/discovery.md`.

Function Purpose
SCISDK_GetSupportedBoardList ;-separated list of accepted model strings (aliases included)
SCISDK_GetSupportedBoardsInfo JSON description of every board: transports, path_format, default_port, whether the RegisterFile is required or from_device
SCISDK_Enumerate Boards physically connected right now (USB families only; Ethernet boards have no discovery protocol)
SCISDK_GetFirmwareJSON The RegisterFile JSON actually in use by a connected device
err, models = sdk.GetSupportedBoardList()
# "...;NiSciFakeBoard;FakeBoard;..." -- BoardEmulator and its aliases are in the list
err, info = sdk.GetSupportedBoardsInfo() # JSON: build a connection dialog from this
sdk.AddNewDevice("127.0.0.1:8888", "BoardEmulator", "", "board0") # empty JSON path
err, firmware = sdk.GetFirmwareJSON("board0") # the description downloaded from the emulator

Model strings are case-insensitive. In C, every one of these returns a string allocated by SciSDK that must be released with SCISDK_free_string().


11. Troubleshooting

SciSDK connects but every read returns garbage (Windows). A system-wide SciSDK installation put SciSDK_DLL.dll in C:\Windows\System32, which the loader searches before PATH. The old driver is loaded, does not know the emulator model, silently falls back to the legacy FAKEBOARD stub backend, and every read returns uninitialised memory. Load your build by absolute path first (ctypes.CDLL(r"...\SciSDK_DLL.dll") in Python, LoadLibrary with a full path in C++), or uninstall the system-wide SDK. SciSDK_DLL_Info.exe, run from your application folder, prints which library would actually be used.

The server does not start / "address already in use". Port 8888 is taken — another emulator instance, a real board driver, or a process that did not exit. Start with --port 9100 (and connect to 127.0.0.1:9100), or --port 0 to let the OS pick and read the port from the READY port=NNNN line.

**ERROR: the graphical interface requires PySide6 and pyqtgraph.** The package was installed without the GUI extra. Either pip install boardemulator[gui] (or pip install -e .[gui] from BoardEmulator/emulator), or run headless: python -m boardemulator --headless. This message is by design — CI installations deliberately have no Qt.

A component appears as "Generic register block". The Type in the RegisterFile is not one the emulator has a dedicated model for (a newer Sci-Compiler component, or a typo). The generic fallback still registers the address range and answers register reads and writes, so the connection and the rest of the board work; it just produces no data stream. Check the spelling of Type, and if the component is genuinely new, the bus semantics need to be added to the emulator (see `BoardEmulator/docs/BUS_CONTRACT.md` and `BoardEmulator/docs/EXTENSIBILITY.md`).

A component stopped producing on its own. A custom provider raised: the emulator logs the exception and pauses the component rather than crashing. Look at the emulator messages (stderr in headless, the log in the GUI), fix the provider, reload it, then Resume.

The connection works but the oscilloscope never triggers. Open the oscilloscope panel and look at the Source signal (free running) plot: if the dashed trigger level is above every pulse, nothing will ever fire. Lower trigger_level, check trigger_channel and trigger_polarity, or use trigger_mode = "self", which is an auto trigger and fires regardless of the level.

Everything feels slow, the GUI stutters. The bus is far faster than any table widget. The transaction log is batched and capped on purpose (the footer tells you how many rows were not displayed); if you are hammering the board, use Pause log or an address filter on the Board page. Emitting one Qt signal per transaction was measured to drag an oscilloscope from ~215 down to ~16 acquisitions per second — which is why the log batches at ~10 Hz. High event rates also cost CPU in the generators: lower the event rate, or run headless when you only need throughput.

Results are not reproducible between runs. Set a seed: --seed 12345, or save a configuration (which stores the seed) and reuse it. Note that wall-clock-paced components depend on machine load; for strict determinism use --pause-on-start with step_n.


Further reading

The internal technical documents of the emulator live next to its source, in BoardEmulator/docs/:

  • `BoardEmulator/docs/PROTOCOL.md` — TCP wire protocol v1 and the client embedded in SciSDK
  • `BoardEmulator/docs/BUS_CONTRACT.md` — the exact bus semantics of every component (source of truth)
  • `BoardEmulator/docs/GENERATORS.md` — the formulas behind every generator
  • `BoardEmulator/docs/EXTENSIBILITY.md` — providers, timing engine, debug mode
  • `BoardEmulator/docs/ARCHITECTURE.md` — how the code is organised
  • `docs/discovery.md` — board discovery and the board catalog