Serving a .db file
✅ Verified ·
db_file.rs·example.db·demo_db_file.py· checkdocs_verify·The badge reports the whole
docs-verifysuite, 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
| Field | Effect |
|---|---|
VAL | initial value |
DESC | description, served in display.description |
EGU | engineering units |
PREC | display precision |
LOPR / HOPR | display limits (a GUI hint; nothing enforces them) |
LOW / HIGH | MINOR alarm limits — evaluated, given .compute_alarms(true) |
LOLO / HIHI | MAJOR alarm limits — same |
MDEL | monitor deadband |
ADEL | archive deadband |
DRVL / DRVH | drive limits — advisory only, spvirit does not clamp |
SCAN | "1 second" etc., for periodic reprocessing |
INP | input link, for scanned records that copy another PV |
FTVL / NELM | element type and count, for array records |
ZNAM / ONAM | the two state names of a bi/bo |
INDX / MALM | window 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.