epics-base-rs
Pure Rust implementation of EPICS Base — Channel Access protocol (client & server), IOC runtime, and iocsh.
No C dependencies. No libca. Just cargo build.
All binaries use the -rs suffix (e.g. caget-rs, caput-rs, softioc-rs) to avoid conflicts with the C EPICS tools.
Features
Client
caget-rs— read a PV valuecaput-rs— write a PV valuecamonitor-rs— subscribe to PV changescainfo-rs— display PV metadata- UDP name resolution and TCP virtual circuit
- Extended header support for payloads > 64 KB
Server (Soft IOC)
softioc-rsbinary — host PVs over Channel Access- 20 record types: ai, ao, bi, bo, stringin, stringout, longin, longout, mbbi, mbbo, waveform, calc, calcout, fanout, dfanout, seq, sel, compress, histogram, sub
.dbfile loader with macro substitution- Record processing with full link chain traversal (INP → process → OUT → FLNK)
- Periodic and event scan scheduling (Passive, Event, 10 Hz, 5 Hz, 2 Hz, 1 Hz, 0.5 Hz, 0.2 Hz, 0.1 Hz)
- CALC engine (arithmetic, comparison, logic, ternary, math functions)
- Monitor subscriptions with per-channel event delivery
- Beacon emitter
- Access security (ACF file parser, UAG/HAG/ASG rules, per-channel enforcement)
- Autosave/restore (periodic save, atomic write, type-aware restore)
- Device support trait for custom I/O drivers
- Subroutine registry for sub records
- IocApplication: st.cmd-style IOC lifecycle (C++ EPICS-compatible syntax)
- iocsh: Interactive shell with C++ EPICS function-call syntax and
$(VAR)substitution - Extended CA header for large payloads (> 64 KB) with 16 MB DoS limit
pvAccess (experimental)
pvaget-rs,pvaput-rs,pvamonitor-rs,pvainfo-rs— basic pvAccess client tools
What's New in v0.2
v0.2.0 — Declarative IOC Builder + Event Scheduler
Declarative IOC configuration — build IOCs entirely in Rust without .db files:
use IocApplication;
use AoRecord;
use BiRecord;
new
.record
.record
.run
.await?;
ScanSchedulerV2 — event-driven scan scheduling with coalescing:
ScanEventKind: Periodic, IoIntr, Event, Delayed, Pinisubmit_event()/submit_delayed()for external event injection- HashSet-based dedup within scan ticks — no duplicate processing
What's New in v0.3
v0.3.0 — Snapshot-Based Internal Model (GR/CTRL Metadata)
The problem: caget -d DBR_CTRL_DOUBLE <pv> returned zeroed units/limits/precision because GR/CTRL DBR types (21-34) were serialized identically to TIME — no metadata was populated.
The fix: A new Snapshot type serves as the single internal state representation, carrying value + alarm + timestamp + display/control/enum metadata. The CA serializer now encodes real metadata into GR/CTRL wire frames.
┌──────────────────────────────────────────────────────────┐
│ Snapshot │
│ value: EpicsValue │
│ alarm: AlarmInfo { status, severity } │
│ timestamp: SystemTime │
│ display: Option<DisplayInfo> ← EGU, PREC, HOPR/LOPR │
│ control: Option<ControlInfo> ← DRVH/DRVL │
│ enums: Option<EnumInfo> ← ZNAM/ONAM, ZRST..FFST │
└──────────────────────────────────────────────────────────┘
│
▼ encode_dbr(dbr_type, &snapshot)
┌──────────────────────────────────────────────────────────┐
│ DBR_PLAIN (0-6) → bare value bytes │
│ DBR_STS (7-13) → status + severity + value │
│ DBR_TIME (14-20) → status + severity + timestamp + val │
│ DBR_GR (21-27) → sts + units + prec + limits + val │ ← NEW: real data
│ DBR_CTRL (28-34) → sts + units + prec + limits + ctrl │ ← NEW: real data
│ limits + val │
└──────────────────────────────────────────────────────────┘
Metadata populated per record type:
| Record type | DisplayInfo | ControlInfo | EnumInfo |
|---|---|---|---|
| ai | EGU, PREC, HOPR/LOPR, alarm limits | HOPR/LOPR | — |
| ao | EGU, PREC, HOPR/LOPR, alarm limits | DRVH/DRVL | — |
| longin/longout | EGU, HOPR/LOPR, alarm limits | HOPR/LOPR or DRVH/DRVL | — |
| bi/bo | — | — | ZNAM, ONAM |
| mbbi/mbbo | — | — | ZRST..FFST (16 strings) |
Before (v0.2):
$ caget -d DBR_CTRL_DOUBLE TEMP
Units:
Precision: 0
Upper limit: 0
Lower limit: 0
After (v0.3):
$ caget -d DBR_CTRL_DOUBLE TEMP
Units: degC
Precision: 3
Upper limit: 100
Lower limit: -50
Differences from C EPICS
The Snapshot model departs from C EPICS internals while preserving wire compatibility:
| Aspect | C EPICS | epics-base-rs |
|---|---|---|
| Internal state | Metadata baked into each dbCommon / record struct as C struct fields; accessed via dbAddr pointer arithmetic |
Metadata assembled on-demand into a Snapshot from Record trait + CommonFields; no pointer arithmetic |
| GR/CTRL serialization | db_access.c reads fields directly from the record's memory via dbAddr offsets into the flat C struct |
encode_dbr() reads from Snapshot.display / Snapshot.control / Snapshot.enums; the serializer casts f64 → wire-native type (i16/f32/i32/f64) |
| Limit storage | Native type per record (e.g., dbr_ctrl_short stores dbr_short_t limits) |
All limits stored as f64 internally; cast to wire type at serialization time — matches the pattern used by C dbFastLinkConv |
| Enum strings | Fixed char[MAX_ENUM_STATES][MAX_ENUM_STRING_SIZE] in the record struct |
Vec<String> in EnumInfo; padded to 16×26-byte slots at encoding time |
| Timestamp | epicsTimeStamp (EPICS epoch, 2×u32) stored in record |
SystemTime stored in CommonFields; EPICS epoch conversion in serializer |
| Display precision | PREC field as epicsInt16 in individual record structs |
precision: i16 inside DisplayInfo; only present for numeric record types |
| Per-request allocation | Zero allocation — db_access.c fills a pre-sized buffer from record pointers |
Snapshot allocates String for EGU and Vec<String> for enums per request (<1μs; monitor path can cache) |
| Field dispatch | dbAccess.c switch on dbAddr->field_type + dbAddr->no_elements |
RecordInstance::snapshot_for_field() matches on record_type() string |
Wire compatibility: The exact same bytes appear on the wire as a C EPICS server would produce. A C client (caget, camonitor, cainfo) sees no difference.
Quick Start
Run a Soft IOC
# Simple PVs
# With records
# From a .db file
# Custom port
Client Tools
# Read
# Write
# Monitor
Library Usage
Embedded Server
use CaServer;
use AoRecord;
async
With .db File and Access Security
use HashMap;
use CaServer;
async
With Autosave
use PathBuf;
use Duration;
use CaServer;
use AutosaveConfig;
use AoRecord;
async
IocApplication (st.cmd-style IOC)
For driver developers building IOCs with custom device support — matching the C++ EPICS st.cmd startup pattern:
use IocApplication;
use *;
async
The startup script uses exact C++ EPICS syntax:
# ioc/st.cmd — identical to C++ EPICS
)
)
)
)
IOC lifecycle (matches C++ EPICS):
| Phase | Action | C++ Equivalent |
|---|---|---|
| Phase 1 | Execute st.cmd (epicsEnvSet, dbLoadRecords, driver config) |
st.cmd before iocInit() |
| Phase 2 | Wire device support, restore autosave, start scan tasks | iocInit() |
| Phase 3 | Interactive epics> shell (dbl, dbgf, dbpf, dbpr, ...) |
iocsh REPL |
IOC Module System (C++ .dbd → Rust Crates)
In C++ EPICS, .dbd files and Makefile control which modules (record types, device support, drivers) are included in an IOC. In Rust, this maps to Cargo's crate and feature system:
| C++ EPICS | Rust Equivalent |
|---|---|
.dbd files (module declarations) |
Cargo.toml [dependencies] |
Makefile xxx_DBD += (add/remove modules) |
Add/remove crate dependencies |
envPaths (build-time path generation) |
DB_DIR const via CARGO_MANIFEST_DIR |
< envPaths in st.cmd |
IOC binary set_var() at startup |
$(ADSIMDETECTOR)/db/file.template |
$(SIM_DETECTOR)/Db/file.db |
registrar() / device() in .dbd |
register_device_support() call |
#ifdef conditional include |
Cargo features |
Project Structure Convention
Each driver crate follows the C++ EPICS layout:
my-driver/
src/ ← xxxApp/src/ (driver source)
lib.rs
driver.rs
bin/my_ioc.rs ← IOC binary
Db/ ← xxxApp/Db/ (database templates, ship with driver)
myDriver.db
ioc/ ← iocBoot/iocXxx/ (deployment-specific startup)
st.cmd
Db/— database templates that belong to the driver (reusable across IOCs)ioc/— deployment-specific st.cmd and configurationsrc/— driver source code and IOC binary
Database Path Resolution
Each driver crate exports a DB_DIR constant with its absolute Db/ path:
// In driver crate's lib.rs
pub const DB_DIR: &str = concat!;
The IOC binary sets environment variables at startup (like C++ envPaths):
// In IOC binary main()
unsafe
Then st.cmd uses standard $(MODULE) references:
)
Client
use CaClient;
async
.db File Format
Standard EPICS database format with macro substitution:
record(ao, "$(P)$(R)") {
field(DESC, "Temperature setpoint")
field(VAL, "25.0")
field(HOPR, "100.0")
field(LOPR, "0.0")
field(HIHI, "90.0")
field(HIGH, "70.0")
field(LOW, "5.0")
field(LOLO, "2.0")
field(HHSV, "MAJOR")
field(HSV, "MINOR")
field(LSV, "MINOR")
field(LLSV, "MAJOR")
field(SCAN, "1 second")
field(INP, "SIM:RAW")
field(FLNK, "$(P)$(R):STATUS")
}
Access Security (ACF)
UAG(admins) { alice, bob }
HAG(control_room) { cr-pc1, cr-pc2 }
ASG(DEFAULT) {
RULE(1, WRITE)
RULE(1, READ)
}
ASG(RESTRICTED) {
RULE(1, WRITE) { UAG(admins) HAG(control_room) }
RULE(1, READ)
}
Record Types
| Type | Description | Value Type |
|---|---|---|
| ai | Analog input | Double |
| ao | Analog output | Double |
| bi | Binary input | Enum (u16) |
| bo | Binary output | Enum (u16) |
| longin | Long input | Long (i32) |
| longout | Long output | Long (i32) |
| mbbi | Multi-bit binary input | Enum (u16) |
| mbbo | Multi-bit binary output | Enum (u16) |
| stringin | String input | String |
| stringout | String output | String |
| waveform | Array data | DoubleArray / LongArray / CharArray |
| calc | Calculation | Double |
| calcout | Calculation with output | Double (OVAL) |
| fanout | Forward link fanout | — |
| dfanout | Data fanout | Double |
| seq | Sequence | Double |
| sel | Select | Double |
| compress | Circular buffer / N-to-1 compression | DoubleArray |
| histogram | Signal histogram | LongArray |
| sub | Subroutine | Double |
Architecture
epics-base-rs/
src/
client.rs # CA client (caget-rs/caput-rs/camonitor-rs)
protocol.rs # CA protocol codec (normal + extended header)
types.rs # EpicsValue, DbFieldType, serialize_dbr, encode_dbr
error.rs # Error types
channel.rs # Channel abstraction (AccessRights, ChannelInfo)
pva/ # pvAccess protocol (experimental)
server/
mod.rs # CaServer, CaServerBuilder
snapshot.rs # Snapshot, AlarmInfo, DisplayInfo, ControlInfo, EnumInfo
database.rs # PvDatabase, link chain processing, CP link tracking
record.rs # Record trait, CommonFields, RecordInstance (shared infrastructure)
tcp.rs # TCP virtual circuit handler (per-client state, dispatch)
udp.rs # UDP search responder
beacon.rs # Beacon emitter (exponential backoff)
scan.rs # Periodic/event scan scheduler
monitor.rs # Subscription/monitor system (mpsc-based event delivery)
calc_engine.rs # Expression evaluator (CALC/CALCOUT fields)
db_loader.rs # .db file parser with macro substitution
device_support.rs # DeviceSupport trait (init/read/write/I/O Intr)
ioc_app.rs # IocApplication (st.cmd lifecycle, 3-phase startup)
iocsh/ # Interactive shell (C++ syntax tokenizer, commands)
access_security.rs # ACF parser (UAG/HAG/ASG rules, per-channel enforcement)
autosave.rs # Save/restore (periodic save, atomic write, .req parsing)
pv.rs # ProcessVariable (simple PV, subscriber list)
records/ # Per-record-type implementations (25 files)
epics-macros/ # Proc macro for #[derive(EpicsRecord)]
Channel Access Protocol
The CA protocol (protocol.rs, types.rs) provides the wire format for all client-server
communication. Every message starts with a 16-byte big-endian header:
| Field | Size | Description |
|---|---|---|
cmmd |
u16 | Command code (message type) |
postsize |
u16 | Payload size (0xFFFF = extended) |
data_type |
u16 | DBR type code |
count |
u16 | Element count (0 = extended) |
cid |
u32 | Channel ID or status |
available |
u32 | Subscription ID or context |
When payload exceeds 64 KB or element count exceeds 65535, the extended header format is used:
postsize=0xFFFF, count=0, followed by 8 bytes of extended_postsize (u32) and
extended_count (u32). Maximum payload: 16 MB (DoS limit).
Key message types:
| Code | Name | Direction | Purpose |
|---|---|---|---|
| 0 | VERSION | Both | Protocol version negotiation |
| 6 | SEARCH | C→S (UDP) | PV name lookup |
| 18 | CREATE_CHAN | C→S | Open channel, get CID |
| 15 | READ_NOTIFY | C→S | Read PV value |
| 19 | WRITE_NOTIFY | C→S | Write PV value (with ack) |
| 1 | EVENT_ADD | Both | Subscribe to changes / deliver updates |
| 2 | EVENT_CANCEL | C→S | Unsubscribe |
| 13 | RSRV_IS_UP | S→C (UDP) | Beacon announcement |
| 23 | ECHO | Both | Connection keepalive |
| 12 | CLEAR_CHANNEL | C→S | Close channel |
DBR types encode both the value type and metadata level:
| Range | Level | Contents |
|---|---|---|
| 0–6 | PLAIN | Bare value |
| 7–13 | STS | + alarm status/severity |
| 14–20 | TIME | + EPICS timestamp |
| 21–27 | GR | + units, precision, display/alarm limits |
| 28–34 | CTRL | + control limits (DRVH/DRVL) |
Monitor masks control which changes trigger subscription updates:
DBE_VALUE (1), DBE_LOG (2), DBE_ALARM (4), DBE_PROPERTY (8).
CA Client
The client (client.rs, channel.rs) implements the CA consumer side:
- Name resolution: UDP broadcast
SEARCHtoEPICS_CA_ADDR_LIST(default: broadcast on port 5064) - Connection: TCP virtual circuit to the server's advertised port
- Channel creation:
CREATE_CHANwith PV name → server assigns CID + reports native type, element count, access rights - I/O:
READ_NOTIFY/WRITE_NOTIFYfor one-shot read/write;EVENT_ADDfor subscriptions - Cleanup:
EVENT_CANCEL,CLEAR_CHANNEL, TCP close
The CaClient API wraps this lifecycle:
let client = new.await?;
let = client.caget.await?; // search + connect + read
client.caput.await?; // write with callback
CA Server
The server consists of three network components running as tokio tasks:
UDP search responder (udp.rs) — Listens on port 5064 (configurable via
EPICS_CA_SERVER_PORT). Parses incoming SEARCH messages, checks PvDatabase::has_name(),
and responds with SEARCH reply containing the server's TCP port.
Beacon emitter (beacon.rs) — Broadcasts RSRV_IS_UP on port 5065 with exponential
backoff (20 ms → 15 s, doubling each step). Clients use beacons to detect new servers without
re-searching.
TCP virtual circuit handler (tcp.rs) — Accepts connections and manages per-client state:
The dispatch loop reads messages from the TCP stream and handles each command:
CREATE_CHAN→ look up PV, allocate CID, compute access rights, reply with type infoREAD_NOTIFY→ buildSnapshotfrom record/PV, encode to requested DBR type, replyWRITE_NOTIFY→ decode payload,put_field()on record, trigger process + links, replyEVENT_ADD→ create subscriber with mpsc channel, spawn monitor delivery taskEVENT_CANCEL→ drop subscriber, close channelECHO→ echo response (keepalive)
PvDatabase and Link Chains
PvDatabase (database.rs) is the central registry for all PVs and records:
PV name resolution parses "TEMP.EGU" into record name "TEMP" + field "EGU" (default
field is "VAL"). Field values resolve through a 3-level priority: record-specific field →
common field (SEVR, STAT, SCAN, etc.) → VAL fallback.
Link chain processing handles forward links (FLNK) and channel-process links (CP):
- When a record is processed,
process_record_with_links()follows FLNK to process downstream records in sequence, using a visited set to prevent cycles - CP links are tracked in
cp_links: when a source PV changes, all target records are automatically processed
Timestamp source (TSE field): TSE=0 uses system clock, TSE=-1 uses device-provided time, TSE=-2 preserves the existing TIME field.
Record System: record.rs vs records/
The record system is split into two layers:
record.rs contains shared infrastructure used by all record types. It is intentionally
a single file because these components are tightly coupled:
| Section | Lines | Contents |
|---|---|---|
| Types & enums | 1–170 | FieldDesc, AlarmSeverity, ScanType, AlarmStatus, field metadata |
CommonFields |
173–269 | Fields shared by every record: VAL, NAME, DESC, SCAN, PINI, alarm fields, etc. |
| Link parsing | 271–406 | parse_link(), CP/CPP/PP/MS modifiers, link chain resolution |
Record trait |
408–524 | The trait all record types implement: process(), read()/write(), special(), as_any_mut(), can_device_write() |
RecordInstance |
527–1377 | Core runtime (~850 lines): field get/put, alarm evaluation, process cycle, deadband, monitor subscriptions, snapshot generation |
| Tests | 1379–end | Unit tests for all of the above |
records/ contains one file per record type (e.g., ai.rs, ao.rs, bi.rs, motor.rs, …).
Each file defines its type-specific fields and implements the Record trait. The
#[derive(EpicsRecord)] proc macro generates the boilerplate (field descriptors, get/put dispatch)
so that each record file focuses only on its unique processing logic.
This separation means adding a new record type never touches record.rs — you create a new file
in records/, derive the macro, and implement Record.
Snapshot Model
Snapshot (snapshot.rs) is the single internal representation for PV state.
It carries everything needed to produce any DBR wire type:
Snapshot {
value: EpicsValue,
alarm: AlarmInfo { status, severity },
timestamp: SystemTime,
display: Option<DisplayInfo>, // EGU, PREC, HOPR/LOPR, alarm limits
control: Option<ControlInfo>, // DRVH/DRVL
enums: Option<EnumInfo>, // ZNAM/ONAM or ZRST..FFST
}
encode_dbr(dbr_type, &snapshot) serializes a Snapshot to any of the 35 DBR wire formats.
All limits are stored internally as f64 and cast to the wire-native type (i16/f32/i32/f64)
at serialization time.
Device Support
The DeviceSupport trait (device_support.rs) connects records to external I/O drivers:
Records with a DTYP field delegate their I/O to a matching DeviceSupport instance:
- init(): Called once during iocInit (Phase 2). Can inject driver state into the record via
as_any_mut()downcast. - read(): Called during record process when the record has an input link to the driver.
- write(): Called when a CA client writes to the record (if
can_device_write()returns true). - I/O Intr scanning:
io_intr_receiver()returns anmpsc::Receiver<()>. The scan system processes the record each time the driver sends a signal. - Async writes:
write_begin()submits the operation to a worker queue and returns a completion handle.
Scan System
The scan scheduler (scan.rs) drives periodic and event-based record processing:
| Scan Type | Trigger |
|---|---|
| Passive | Only processed via links (FLNK, CP) or CA writes |
| I/O Intr | Device support signals via mpsc channel |
| Event | External event injection via submit_event() |
| 10 Hz – 0.1 Hz | Periodic tokio tasks (100 ms – 10 s intervals) |
At IOC startup:
- Records with
PINI=YESare processed once (respecting PHAS ordering) - Periodic scan tasks are spawned (one tokio task per rate)
- I/O Intr receivers are collected from device support and monitored
Periodic scans use the scan_index (BTreeSet sorted by PHAS) for deterministic ordering.
A visited set prevents infinite loops in link chain traversal.
Monitor/Subscription System
The monitor system (monitor.rs, pv.rs) delivers value change notifications to CA clients:
Record process → put_field() → notify_subscribers()
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Subscriber 1 Subscriber 2 Subscriber N
(mpsc::tx) (mpsc::tx) (mpsc::tx)
│ │ │
▼ ▼ ▼
monitor task monitor task monitor task
(encode+send) (encode+send) (encode+send)
Each Subscriber holds a mask (DBE_VALUE | DBE_ALARM | ...), requested DBR type, and an
mpsc sender. When a PV value changes, notify_subscribers() sends a MonitorEvent (containing
the full Snapshot) to each subscriber's channel. A dedicated tokio task per subscriber encodes
the snapshot and writes the EVENT_ADD response to the TCP connection. Closed subscribers are
automatically removed on the next notify cycle.
IocApplication (st.cmd Lifecycle)
IocApplication (ioc_app.rs) provides the C EPICS-compatible IOC startup pattern:
| Phase | Thread | Action |
|---|---|---|
| Phase 1 | std::thread | Execute st.cmd: epicsEnvSet, dbLoadRecords, driver config commands |
| Phase 2 | tokio | iocInit: wire device support → records, start scan tasks, start CA server |
| Phase 3 | std::thread | Interactive epics> REPL for runtime inspection |
Phase 1 runs on a blocking std::thread because iocsh commands use Handle::block_on() to call
async database methods synchronously. Phase 2 and the CA server run on the tokio runtime.
Phase 3 spawns another blocking thread for the interactive REPL.
Key builder methods:
register_startup_command(CommandDef)— commands available during Phase 1register_shell_command(CommandDef)— commands available during Phase 3register_device_support(dtyp, factory)— static DTYP → factory mappingregister_dynamic_device_support(factory)— chained fallback factory (new factory tries first, falls back to existing)startup_script(path)— path to st.cmd file
iocsh (Interactive Shell)
The iocsh (iocsh/) provides a command-line interface with C++ EPICS-compatible syntax:
# C++ syntax (primary)
epicsEnvSet("PREFIX", "SIM1:")
dbLoadRecords("$(MY_DRIVER)/Db/sim.db", "P=$(PREFIX)")
myDriverConfig("SIM1", 256, 256)
# Macro substitution
$(VAR) # environment variable
$(VAR=default) # with default value
The tokenizer handles parentheses, comma-separated arguments, quoted strings, and
$(MACRO) expansion from environment variables. CommandContext provides the sync→async
bridge: block_on() runs futures from the REPL thread, runtime_handle() gives access
to the tokio handle for spawning tasks.
Built-in shell commands: dbl (list records), dbgf (get field), dbpf (put field),
dbpr (print record), help.
Database Loader
The db loader (db_loader.rs) parses .db / .template files:
record(ao, "$(P)$(R)") {
field(DESC, "Temperature")
field(VAL, "25.0")
field(SCAN, "1 second")
field(FLNK, "$(P)STATUS")
}
Parsing flow:
- Read file, apply macro substitution (
$(NAME),${NAME},$(NAME=default)) - Parse into
DbRecordDeflist (record type, name, fields) - For each definition, create record via built-in type map or
RecordFactoryregistry - Apply fields via
put_field()and common fields viaput_common_field() - Two-phase init:
init_record(0)theninit_record(1) - Register in
PvDatabase
External crates can register custom record types via register_record_type() to
override built-in stubs (e.g., asyn-rs registers asynRecord).
Access Security
The ACF system (access_security.rs) provides per-channel read/write control:
UAG(admins) { alice, bob }
HAG(control_room) { cr-pc1, cr-pc2 }
ASG(RESTRICTED) {
RULE(1, WRITE) { UAG(admins) HAG(control_room) }
RULE(1, READ)
}
- UAG (User Access Group): named set of usernames
- HAG (Host Access Group): named set of hostnames
- ASG (Access Security Group): collection of rules; records reference an ASG via the
ASGfield
Access check: for each rule in the ASG, if the client's username matches a UAG member and hostname matches a HAG member, grant the rule's level (Read or ReadWrite). No ACF configured = all channels get full access.
Autosave
The autosave system (autosave.rs) persists PV values across IOC restarts:
- Save: Periodically writes specified PVs to a file (one
PV_NAME valueper line), using atomic write (temp file + rename) for crash safety - Restore: On IOC startup, reads the save file and calls
put_field()for each entry before records are initialized - Request files:
.reqformat lists PVs to save, with$(MACRO)support - Backup: Optional timestamped rolling backups
CALC Engine
The expression evaluator (calc_engine.rs) powers calc and calcout records.
Supports the full C EPICS CALC syntax:
- Variables: A through L (12 inputs from INPA–INPL links)
- Arithmetic:
+,-,*,/,% - Comparison:
<,<=,>,>=,=,!=,#(not equal) - Logic:
&&,||,!,~(bitwise NOT),|,&,>>,<< - Ternary:
? :(C-style conditional) - Math functions:
ABS,SQR,SQRT,MIN,MAX,CEIL,FLOOR,LOG,LOGE,EXP,SIN,COS,TAN,ASIN,ACOS,ATAN,ATAN2,NINT,ISNAN,ISINF,FINITE,RANDOM
Data Flow: CA Read Request
Client tcp.rs record.rs / pv.rs types.rs
│ │ │ │
│ CA_PROTO_READ_NOTIFY │ │ │
│ (dbr_type=DBR_CTRL_DOUBLE) │ │
│────────────────────────>│ │ │
│ │ get_full_snapshot() │ │
│ │────────────────────────────>│ │
│ │ │ snapshot_for_field() │
│ │ │ ┌─ resolve_field() │
│ │ │ ├─ populate_display() │
│ │ │ ├─ populate_control() │
│ │ │ └─ populate_enum() │
│ │ Snapshot │ │
│ │<────────────────────────────│ │
│ │ │ │
│ │ encode_dbr(34, &snapshot) │ │
│ │────────────────────────────────────────────────────>│
│ │ │ encode_ctrl() │
│ │ │ ┌─ status/severity │
│ │ │ ├─ prec + units │
│ │ wire bytes │ ├─ 8 limits (f64) │
│ │<────────────────────────────────────────────────────│
│ CA response │ │ └─ value │
│<────────────────────────│ │ │
Testing
286 tests covering protocol encoding, wire format golden packets, snapshot generation, GR/CTRL metadata serialization, record processing, link chains, calc engine, .db parsing, access security, autosave, iocsh, declarative IOC builder, and event scheduling.
Requirements
- Rust 1.70+
- tokio runtime
License
MIT