# 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](https://github.com/epics-rs/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+)
| 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 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 `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::sync` — `mpsc`, `Notify`, `RwLock`, `Mutex`, `Arc`
- `epics_base_rs::runtime::task` — `spawn`, `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
```rust
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)
```rust
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
```rust
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
```bash
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](../../LICENSE)