epics-base-rs 0.8.2

Pure Rust EPICS IOC core — record system, database, iocsh, calc engine
Documentation

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 traitprocess(), 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 .db parser: record/grecord/info, macro expansion $(P)/${KEY=default}, environment variable fallback, include directive
  • 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, returns MonitorEvent with full Snapshot
  • DbMultiMonitor — wait on multiple PVs simultaneously
  • Origin filteringsubscribe_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 ASG field 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
  • .req file parsing with file includes, macro expansion, search path resolution, cycle detection
  • Periodic / triggered / on-change / manual save strategies
  • Atomic file write (tmp → fsync → rename), .savB backup rotation
  • C autosave-compatible .sav file format

Runtime Facade

  • epics_base_rs::runtime::syncmpsc, Notify, RwLock, Mutex, Arc
  • epics_base_rs::runtime::taskspawn, sleep, interval, timeout
  • epics_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 epics_base_rs::server::ioc_builder::IocBuilder;
use epics_base_rs::server::records::ai::AiRecord;
use epics_base_rs::types::EpicsValue;

#[epics_base_rs::epics_main]
async fn main() -> epics_base_rs::error::CaResult<()> {
    let (db, _autosave) = IocBuilder::new()
        .pv("MSG", EpicsValue::String("hello".into()))
        .record("TEMP", AiRecord::new())
        .build()
        .await?;

    // db is Arc<PvDatabase> — pass to a protocol runner
    Ok(())
}

Direct Database Access (no CA)

use epics_base_rs::server::database::db_access::{DbChannel, DbSubscription};

let ch = DbChannel::new(&db, "TEMP");
ch.put_f64_process(25.0).await?;
let v = ch.get_f64().await;

let mut sub = DbSubscription::subscribe(&db, "TEMP").await.unwrap();
while let Some(snap) = sub.recv_snapshot().await {
    println!("{:?}", snap.value);
}

Custom Record Type

use epics_base_rs::server::record::Record;
use epics_macros_rs::EpicsRecord;

#[derive(EpicsRecord, Default)]
#[record(type = "myrec")]
pub struct MyRecord {
    #[field(type = "Double")]
    pub val: f64,
    #[field(type = "String")]
    pub desc: String,
}

impl Record for MyRecord {
    fn process(&mut self) -> CaResult<ProcessOutcome> {
        // your logic
        Ok(ProcessOutcome::complete())
    }
    // ... rest auto-generated by derive
}

Testing

cargo test -p epics-base-rs

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)

License

EPICS Open License