asyn-rs
Rust port of EPICS asyn — an async device I/O framework for hardware drivers.
No C dependencies. Pure Rust. Integrates with epics-ca via the optional epics feature.
Repository: https://github.com/epics-rs/epics-rs
Overview
asyn-rs provides the same driver model as C asyn, but uses Rust's type system and tokio for safety and concurrency:
- PortDriver trait — implement
read_*/write_*for your hardware - ParamList — named parameter cache with change tracking, timestamps, and alarm status
- InterruptManager — dual async (broadcast) + sync (mpsc) callback delivery
- PortManager — registry of named port drivers
- AsynDeviceSupport — bridges asyn-rs drivers to epics-ca
DeviceSupporttrait
What's New in v0.2
v0.2.0 — Actor Model + Typed Capabilities
Actor-based port driver execution — drivers are no longer accessed through Arc<Mutex<dyn PortDriver>>. Instead, each driver runs in its own actor thread with exclusive ownership:
- PortActor — owns the driver exclusively, dispatches requests via channel
- PortHandle — cloneable async handle with typed convenience methods (
read_int32(),write_float64(), etc.) - AsyncCompletionHandle —
Futureimpl +wait_blocking()for sync callers
Adapter migration — AsynDeviceSupport now supports both legacy (Arc<Mutex>) and actor (PortHandle) backends via PortBackend enum. New drivers should use from_handle().
Typed capability system:
InterfaceTypeenum with bidirectional string conversion (e.g."asynInt32"↔InterfaceType::Int32)Capabilityenum for declaring driver capabilities at type levelPortDriver::capabilities()/supports()default trait methods
Extended request types — RequestOp extended with DrvUserCreate, Enum, Int32Array, Float64Array. RequestResult gains alarm/timestamp metadata.
v0.2.1 — Protocol, Transport, Runtime
Pure-data protocol (src/protocol/) — serializable message types at all boundaries, no trait objects or closures:
| Type | Description |
|---|---|
PortCommand |
23-variant enum, 1:1 map from RequestOp |
PortReply |
Response envelope with typed ReplyPayload |
ParamValue |
Serializable value union (no GenericPointer) |
PortRequest |
Request envelope with RequestMeta |
PortEvent |
Event with EventPayload (value change / exception) |
All types derive serde::Serialize/Deserialize for future wire transport.
Pluggable transport (src/transport/) — RuntimeClient trait decouples callers from transport:
- InProcessClient — zero-cost fast path, direct enum pass-through (no serialization)
- Future:
UnixSocketClientfor multi-process deployments
Runtime module (src/runtime/) — promoted actors with lifecycle management:
- PortRuntime — promoted
PortActorwithRuntimeEventbroadcast (Started/Stopped/Connected/Disconnected/Error) and graceful shutdown - AxisRuntime — per-axis motor actor with event emission, poll loop, and I/O Intr notification
- Supervision — generic restart loop with configurable policy (
max_restarts,restart_window) - PortManager integration —
register_port_runtime()auto-registers both runtime handle and legacy port handle for backwards compatibility
Criterion benchmarks (benches/throughput.rs):
local_int32_read/local_float64_write/local_octet_roundtrip— legacy mutex pathactor_int32_read— PortHandle via actorconcurrent_32_producers— 32 threads on same portinterrupt_event_throughput— 1k events broadcast delivery
Architecture
┌─────────────────────────────────────────────┐
│ EPICS Records (ai, ao, longin, ...) │
│ ↕ DeviceSupport trait │
│ ┌─────────────────────────────────┐ │
│ │ AsynDeviceSupport (adapter) │ epics │
│ │ - alarm/timestamp propagation │ feature │
│ │ - I/O Intr scan bridging │ │
│ └──────────┬──────────────────────┘ │
└─────────────┼───────────────────────────────┘
↕
┌─────────────────────────────────────────────┐
│ RuntimeClient trait (transport layer) │
│ ├── InProcessClient (zero-cost fast path) │
│ └── [UnixSocketClient] (future) │
│ ↕ PortCommand / PortReply │
│ ┌─────────────────────────────────┐ │
│ │ PortRuntime / PortActor │ │
│ │ - exclusive driver ownership │ │
│ │ - RuntimeEvent broadcast │ │
│ │ - graceful shutdown │ │
│ └──────────┬──────────────────────┘ │
└─────────────┼───────────────────────────────┘
↕
┌─────────────────────────────────────────────┐
│ PortDriver trait │
│ - read/write: Int32, Float64, Octet, │
│ UInt32Digital, arrays │
│ - InterfaceType / Capability declarations │
│ │
│ PortDriverBase │
│ ├── ParamList (cache + change tracking) │
│ ├── InterruptManager (broadcast + mpsc) │
│ └── options: HashMap<String, String> │
└─────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────┐
│ Your Hardware Driver │
│ - Background tokio task polls device │
│ - set_*_param() + call_param_callbacks() │
│ - Default read_* returns cached values │
└─────────────────────────────────────────────┘
Quick Start
Add to Cargo.toml:
[]
= { = "../asyn-rs" }
# With EPICS integration:
# asyn-rs = { path = "../asyn-rs", features = ["epics"] }
Implementing a Driver
use ParamType;
use ;
use AsynResult;
Registering with PortManager
use PortManager;
let manager = new;
let port = manager.register_port;
// Access from anywhere via Arc<RwLock<dyn PortDriver>>
let p = manager.find_port.unwrap;
EPICS Integration
With the epics feature, use AsynDeviceSupport to bridge drivers to epics-ca records:
use ;
// In a DeviceSupport factory:
let link = parse_asyn_link.unwrap;
let port = manager.find_port.unwrap;
let adapter = new;
The adapter handles:
- Parameter resolution via
drvUserCreate - Value read/write through the port driver's cache
- Alarm status/severity propagation from driver to record
- Timestamp propagation (driver-supplied or auto-generated)
- I/O Intr scan support (broadcast → per-record mpsc bridge)
Modules
| Module | Description |
|---|---|
error |
AsynStatus, AsynError error types |
param |
ParamList — named parameter cache with types, change tracking, timestamps |
port |
PortDriverBase + PortDriver trait with cache-based I/O defaults |
interrupt |
InterruptManager — dual async/sync interrupt delivery |
manager |
PortManager — named port driver registry + runtime registration |
user |
AsynUser — per-request context (reason, addr) |
trace |
asyn_trace! macro for debug logging |
interfaces |
InterfaceType, Capability — typed interface/capability system |
port_actor |
PortActor — actor with exclusive driver ownership |
port_handle |
PortHandle — cloneable async handle with typed convenience methods |
protocol |
Pure-data message types: PortCommand, PortReply, ParamValue, PortEvent |
transport |
RuntimeClient trait, InProcessClient (zero-cost fast path) |
runtime |
PortRuntime, AxisRuntime, supervision, RuntimeEvent lifecycle |
adapter |
AsynDeviceSupport — epics-ca bridge (requires epics feature) |
I/O Model
asyn-rs uses a cache-based model instead of C asyn's queue/block model:
- A background task (e.g., tokio::spawn) polls the hardware
- Driver calls
set_*_param()to update cached values - Driver calls
call_param_callbacks()to notify subscribers - Default
read_*methods return the cached value immediately
This means can_block is preserved for compatibility but has no runtime effect. For command/response hardware, drivers manage their own async task and request queue.
Testing
License
The Rust code authored in this crate is licensed under MIT.
This crate also bundles third-party OPI/UI assets related to EPICS asynDriver.
See THIRD_PARTY_LICENSES for attribution and upstream
license text.