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

Serving a .db file

Verified · db_file.rs · example.db · demo_db_file.py · check docs_verify · docs-verify

The badge reports the whole docs-verify suite, not this chapter alone.

What you'll build

A soft IOC defined the EPICS way — as a database file rather than as code.

This is the point where spvirit stops being "a PVAccess library" and starts being "an IOC you can drop into an existing control system". The same file an EPICS IOC would load, spvirit serves.

The database

# A minimal EPICS database, loadable by both `spserver --db` and
# `PvaServer::builder().db_file(...)`. Field names are the ordinary EPICS
# ones; fields spvirit does not model are ignored rather than rejected.
#
# Note: mbbi/mbbo are NOT loadable from .db in spvirit today - build enum
# records in code instead. See the Enums chapter.

# Read-only analog input. Clients can get and monitor it; puts are refused.
record(ai, "DEMO:TEMP") {
    field(DESC, "Sample block temperature")
    field(VAL,  "22.5")
    field(EGU,  "degC")
    field(PREC, "2")
    field(LOPR, "0")      # display low - a hint for GUIs
    field(HOPR, "50")     # display high
    field(MDEL, "0.5")    # monitor deadband
}

# Writable analog output, with alarm limits the server evaluates.
record(ao, "DEMO:SETPOINT") {
    field(DESC, "Demanded temperature")
    field(VAL,  "25.0")
    field(EGU,  "degC")
    field(PREC, "1")
    field(LOW,  "10")     # MINOR below this
    field(HIGH, "40")     # MINOR above this
    field(LOLO, "5")      # MAJOR below this
    field(HIHI, "45")     # MAJOR above this
    field(DRVL, "0")      # advisory only - spvirit does not clamp
    field(DRVH, "100")
}

record(bo, "DEMO:ENABLE") {
    field(DESC, "Master enable")
    field(VAL,  "0")
    field(ZNAM, "DISABLED")
    field(ONAM, "ENABLED")
}

record(waveform, "DEMO:SPECTRUM") {
    field(DESC, "8-point detector trace")
    field(FTVL, "DOUBLE")
    field(NELM, "8")
    field(VAL,  "0, 1, 4, 9, 16, 9, 4, 1")
}

A .db file is a list of record(type, "NAME") { field(FIELD, "value") } blocks. Fields spvirit does not model are ignored rather than rejected, so a database written for a real IOC generally loads unchanged — you just get fewer behaviours than EPICS Base would give you.

Rust

#![allow(unused)]
fn main() {
    let server = PvaServer::builder()
        .db_file("spvirit-server/examples/example.db")
        // .db LOW/HIGH/LOLO/HIHI are only evaluated when this is on.
        .compute_alarms(true)
        .build();
}

.db_string(content) takes the same syntax from a string, which is convenient in tests.

Python

db_file= and db_string= are keyword arguments on Server, and mix freely with pvs=[...] handles in the same server:

server = spvirit.Server(
    db_file="spvirit-server/examples/example.db",
    # .db LOW/HIGH/LOLO/HIHI are only evaluated when this is on.
    compute_alarms=True,
)
# `db_string="record(ai, \"X\") { field(VAL, \"1\") }"` takes the same
# syntax inline, which is what you want in a test.

A .db record arrives without a handle. Server.pv(name) mints one, typed from the record's wire type:

# A .db-loaded record has no handle. `Server.pv()` mints one, typed from
# the record's wire type, so you can drive it like any other:
temp = server.pv("DEMO:TEMP")
temp.set(23.4)
print(f"DEMO:TEMP = {temp.get()}")

# The handle is *already bound* to the served record, so `on_put`, `scan`
# and `calc` are silently ignored on it — those must be attached to an
# unbound handle before the server is built. A .db record therefore cannot
# have a write validator; declare it with `spvirit.ao(...)` if it needs one.

That handle is already bound, which is the catch worth knowing before you plan around it: on_put, scan and calc are only honoured on an unbound handle, so attaching one to a .db record does nothing (the core logs a warning and carries on). A record that needs a write validator or a scan callback has to be declared in code with spvirit.ao(...) rather than loaded from the file. The same is true in Rust — PvaServer::pv() attaches, it does not re-declare. See Reacting to writes.

From the command line

No code at all:

spserver --db spvirit-server/examples/example.db

Fields that do something

FieldEffect
VALinitial value
DESCdescription, served in display.description
EGUengineering units
PRECdisplay precision
LOPR / HOPRdisplay limits (a GUI hint; nothing enforces them)
LOW / HIGHMINOR alarm limits — evaluated, given .compute_alarms(true)
LOLO / HIHIMAJOR alarm limits — same
MDELmonitor deadband
ADELarchive deadband
DRVL / DRVHdrive limits — advisory only, spvirit does not clamp
SCAN"1 second" etc., for periodic reprocessing
INPinput link, for scanned records that copy another PV
FTVL / NELMelement type and count, for array records
ZNAM / ONAMthe two state names of a bi/bo
INDX / MALMwindow offset and max length, for subArray

What to notice

.db is the only route to computed alarms. As Alarms explains, the handle API's .alarm_limits() publishes limits without evaluating them. LOW/HIGH/LOLO/HIHI in a .db file are evaluated. If you want the server to derive severity from the value, this is how.

Input records refuse writes. ai, bi, stringin, aai are read-only on the wire, and the refusal is explicit rather than silent:

$ spput DEMO:TEMP 30
DEMO:TEMP ERROR protocol error: PUT init error: Write access denied

Use the o variants — ao, bo, stringout, aao, waveform — for anything a client should set.

mbbi/mbbo cannot be loaded from .db. They parse and are then rejected at construction. Build enum records in code — see Enums.

longin/longout are not recognised by the .db parser either. They exist in the handle API only (spvirit-server/src/types.rs:48).

Loading is best-effort per record. A record spvirit cannot build logs to stderr and is skipped; the rest of the file still serves. Check the log rather than assuming every PV made it.

Run it

# Terminal 1
cargo run -p spvirit-server --example db_file
# or: python spvirit-py/examples/demo_db_file.py

# Terminal 2
splist
spget DEMO:TEMP
spput DEMO:SETPOINT 46
spget DEMO:SETPOINT        # MAJOR HIHI
$ splist 127.0.0.1:5075
DEMO:ENABLE
DEMO:SETPOINT
DEMO:SPECTRUM
DEMO:TEMP
__pvlist

$ spget DEMO:TEMP
DEMO:TEMP 2026-08-06 09:14:58.052 22.5

$ spput DEMO:SETPOINT 46
DEMO:SETPOINT OK

$ spget DEMO:SETPOINT
DEMO:SETPOINT 2026-08-06 09:14:58.052  46 MAJOR READ HIHI

$ spput DEMO:SETPOINT 25
DEMO:SETPOINT OK

$ spget DEMO:SETPOINT
DEMO:SETPOINT 2026-08-06 09:14:58.052  25

Four PVs and no Rust that names any of them — every field, including the HIHI limit that turned 46 into a MAJOR alarm, came out of the .db file. The alarm clears on its own when the value drops back inside the limits; nothing acknowledged it.

__pvlist in the listing is the server's own directory record, not one of yours. It is how splist works at all.

Next

Tables and images.