epics-base-rs
Pure Rust implementation of the EPICS IOC core — record system, database, processing engine, iocsh, .db loader, access security, autosave, and calc engine.
No C dependencies. No libCom. Just cargo build.
Repository: https://github.com/epics-rs/epics-rs
Overview
epics-base-rs is the foundation of the epics-rs workspace. It corresponds to the C EPICS Base modules dbStatic, dbCommon, recCore, iocsh, asLib, autosave, and calc — minus the wire protocol code (which lives in epics-ca-rs for Channel Access and epics-pva-rs for pvAccess).
┌──────────────────────────────────────────────┐
│ IocApplication │ ← high-level lifecycle
│ (st.cmd parser, device factories, builder) │
└────────────────┬─────────────────────────────┘
│
┌───────▼────────┐
│ PvDatabase │ ← record storage + processing
│ (Arc<RwLock>) │
└───────┬────────┘
│
┌────────────┼──────────────┐
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│ Records│ │ Links │ │ Subscr. │
│ (trait)│ │ (parsed) │ │ (mpsc) │
└────────┘ └──────────┘ └──────────┘
Features
Record System
- Record trait —
process(),get_field(),put_field(),field_list(),validate_put(),init_record(),special() - #[derive(EpicsRecord)] proc macro for boilerplate generation
- CommonFields — shared fields (NAME, RTYP, SCAN, PHAS, SEVR, STAT, TIME, DESC, etc.)
- RecordInstance — runtime wrapper with subscriber list, link state, processing flag, alarm evaluation
- ProcessOutcome / ProcessAction — pure state-machine records express side effects (link writes, delayed reprocess, device commands) as data
- Snapshot — bundled value + alarm + timestamp + display/control/enum metadata, assembled on demand
Record Types (23+)
| Category | Types |
|---|---|
| Analog | ai, ao |
| Binary | bi, bo |
| Multi-bit binary | mbbi, mbbo |
| Long integer | longin, longout |
| String | stringin, stringout |
| Array | waveform, compress, histogram |
| Calculation | calc, calcout, scalcout, sub, asub |
| Selection | sel, seq, sseq, transform |
| Fanout | fanout, dfanout |
| Misc | busy, asyn |
Database & Processing
- PvDatabase — Arc-shared record map with
add_record,get_record,process_record,process_record_with_links,put_record_field_from_ca - Link parsing — DB/CA/PVA/Constant links, INP/OUT/FLNK/SDIS/TSEL
- Scan engine — Passive, I/O Intr, Event, periodic (10/5/2/1/0.5/0.2/0.1 Hz), with PHAS ordering
- Alarm propagation — MS/NMS link maximize-severity, deadband filtering (MDEL/ADEL), state alarms (HIHI/HIGH/LOW/LOLO)
- DBE event mask — VALUE/LOG/ALARM/PROPERTY for fine-grained subscription
- Origin tracking — self-write filter for sequencer write-back loops
Database Loader (.db files)
- db_loader — full
.dbparser: record/grecord/info, macro expansion$(P)/${KEY=default}, environment variable fallback,includedirective - DbRecordDef — field definitions, common field application via
put_common_field - dbLoadRecords — iocsh-compatible loader with
P=,R=macro substitution
IOC Lifecycle
- IocBuilder — programmatic IOC setup:
pv(),record(),db_file(),register_device_support(),register_record_type(),autosave() - IocApplication — st.cmd-style lifecycle (Phase 1: pre-init script, Phase 2: device wiring + autosave restore, Phase 3: protocol runner)
- iocsh — interactive shell with command registration, st.cmd parser, expression evaluator
- Pluggable protocol runner — CA, PVA, or both via
app.run(|config| async { ... })
Direct Database Access
- DbChannel — in-process get/put without wire protocol round-trip (
get_f64,put_f64_process,put_f64_post) - DbSubscription — real monitor via
add_subscriber, returnsMonitorEventwith fullSnapshot - DbMultiMonitor — wait on multiple PVs simultaneously
- Origin filtering —
subscribe_filtered(ignore_origin)skips self-triggered events
Access Security
- ACF parser — UAG (user groups), HAG (host groups), ASG (access security groups) with READ/WRITE/READWRITE permissions
- PV-level enforcement — checked on CA put operations
- Per-instance overrides — record
ASGfield links to ACF rule
Calc Engine
- Numeric calc — infix-to-postfix compilation, 16 input variables (A–P), full math library (sqrt, sin, log, abs, floor, etc.)
- String calc — string concatenation, search, substring, format
- Array calc — element-wise operations, statistics (mean, sigma, min, max, median)
Autosave
- C-compatible iocsh commands:
set_requestfile_path,set_savefile_path,create_monitor_set,create_triggered_set,set_pass0_restoreFile,set_pass1_restoreFile,save_restoreSet_status_prefix,fdbsave,fdbrestore,fdblist - Pass0 (before device support init) and Pass1 (after) restore stages
.reqfile parsing withfileincludes, macro expansion, search path resolution, cycle detection- Periodic / triggered / on-change / manual save strategies
- Atomic file write (tmp → fsync → rename),
.savBbackup rotation - C autosave-compatible
.savfile format
Runtime Facade
epics_base_rs::runtime::sync—mpsc,Notify,RwLock,Mutex,Arcepics_base_rs::runtime::task—spawn,sleep,interval,timeoutepics_base_rs::runtime::select— async multiplexing#[epics_base_rs::epics_main]— IOC entry point (replaces#[tokio::main])#[epics_base_rs::epics_test]— async test (replaces#[tokio::test])
Driver authors should use this facade instead of depending on tokio directly.
Architecture
epics-base-rs/src/
├── lib.rs
├── error.rs # CaError, CaResult
├── runtime/ # async runtime facade (mpsc, Notify, spawn, select)
├── types/
│ ├── value.rs # EpicsValue (12 variants: scalar + array)
│ ├── dbr.rs # DbFieldType, DBR type ranges
│ └── codec.rs # DBR encoding/decoding (PLAIN/STS/TIME/GR/CTRL)
├── calc/ # expression engine (numeric/string/array)
└── server/
├── ioc_app.rs # IocApplication (high-level lifecycle)
├── ioc_builder.rs # IocBuilder (programmatic setup)
├── iocsh/ # interactive shell + st.cmd parser
├── database/
│ ├── mod.rs # PvDatabase + parse_pv_name
│ ├── field_io.rs # get_pv, put_pv, put_record_field_from_ca
│ ├── processing.rs # process_record_with_links (full link chain)
│ ├── links.rs # DB/CA/PVA/Constant link resolution
│ ├── scan_index.rs # SCAN scheduling (Passive/Periodic/IOIntr/Event)
│ └── db_access.rs # DbChannel, DbSubscription, DbMultiMonitor
├── db_loader/ # .db parser, macro expansion, info()
├── record/
│ ├── record_trait.rs # Record trait, FieldDesc, ProcessOutcome
│ └── record_instance.rs # CommonFields, snapshot_for_field, alarm eval
├── records/ # 23 record type implementations
├── snapshot.rs # Snapshot, AlarmInfo, DisplayInfo, ControlInfo, EnumInfo
├── pv.rs # ProcessVariable, MonitorEvent, Subscriber
├── recgbl.rs # EventMask (VALUE/LOG/ALARM/PROPERTY)
├── scan.rs # ScanType
├── scan_event.rs # event-driven scanning
├── access_security.rs # ACF parser + UAG/HAG/ASG
├── device_support.rs # DeviceSupport trait, DeviceSupportFactory
└── autosave/ # save/restore (Pass0/Pass1, request files)
Quick Start
use IocBuilder;
use AiRecord;
use EpicsValue;
async
Direct Database Access (no CA)
use ;
let ch = new;
ch.put_f64_process.await?;
let v = ch.get_f64.await;
let mut sub = subscribe.await.unwrap;
while let Some = sub.recv_snapshot.await
Custom Record Type
use Record;
use EpicsRecord;
Testing
Test coverage: record processing, alarm evaluation, deadband filtering, link chain execution, scan scheduling, db file parsing, macro expansion, calc engine (numeric/string/array), DBR encoding (golden packets), access security, autosave save/restore, iocsh command registration.
Dependencies
- chrono — timestamp formatting
- bytes — buffer management
- thiserror — error types
- tokio — async runtime (re-exported via
runtime::facade)
Requirements
- Rust 1.85+ (edition 2024)