epics_base_rs/server/device_support.rs
1use crate::error::CaResult;
2use crate::server::record::{ProcessAction, Record, ScanType};
3
4/// Check if a DTYP string represents a soft/built-in device support
5/// that doesn't require an explicit device support registration.
6/// Matches C EPICS built-in soft device support names.
7pub fn is_soft_dtyp(dtyp: &str) -> bool {
8 dtyp.is_empty()
9 || dtyp == "Soft Channel"
10 || dtyp == "Raw Soft Channel"
11 || dtyp == "Async Soft Channel"
12 || dtyp == "Soft Timestamp"
13 || dtyp == "Sec Past Epoch"
14}
15
16/// Handle for waiting on asynchronous write completion.
17/// Returned by [`DeviceSupport::write_begin`] when the write is submitted
18/// to a worker queue rather than executed synchronously.
19pub trait WriteCompletion: Send + 'static {
20 /// Block until the write completes or timeout expires.
21 fn wait(&self, timeout: std::time::Duration) -> CaResult<()>;
22}
23
24/// Outcome of a device support read() call.
25///
26/// Allows device support to return side-effect actions (link writes,
27/// delayed reprocess) and signal that it has already performed the
28/// Result of a device support `read()` call.
29///
30/// # `ok()` vs `computed()`
31///
32/// This mirrors the C EPICS `read_ai()` return convention:
33///
34/// - **`ok()`** (C return 0): Device support wrote to RVAL. The record's
35/// `process()` will run its built-in conversion (e.g., ai applies
36/// `ROFF → ASLO/AOFF → LINR/ESLO/EOFF → smoothing` to produce VAL
37/// from RVAL).
38///
39/// - **`computed()`** (C return 2): Device support wrote to VAL directly.
40/// The record's `process()` will **skip** its conversion and use the
41/// VAL as-is. Use this when the device support provides engineering
42/// units directly (e.g., soft channel, asyn, custom drivers that
43/// call `record.put_field("VAL", ...)`).
44///
45/// **Common mistake:** returning `ok()` when VAL is set directly causes
46/// the record's conversion to overwrite VAL with a value derived from
47/// RVAL (typically 0), making the read appear broken.
48#[derive(Default)]
49pub struct DeviceReadOutcome {
50 /// Actions for the framework to execute (WriteDbLink, ReprocessAfter, etc.)
51 pub actions: Vec<ProcessAction>,
52 /// If true, the record's built-in conversion (e.g., ai RVAL→VAL)
53 /// is skipped. Set this when device support writes VAL directly.
54 pub did_compute: bool,
55}
56
57impl DeviceReadOutcome {
58 /// Device support wrote RVAL; record will run its conversion to produce VAL.
59 ///
60 /// C equivalent: `read_ai()` returns 0.
61 pub fn ok() -> Self {
62 Self::default()
63 }
64
65 /// Device support wrote VAL directly; record will skip conversion.
66 ///
67 /// C equivalent: `read_ai()` returns 2.
68 pub fn computed() -> Self {
69 Self {
70 did_compute: true,
71 actions: Vec::new(),
72 }
73 }
74
75 /// Shorthand for a computed read with actions.
76 pub fn computed_with(actions: Vec<ProcessAction>) -> Self {
77 Self {
78 did_compute: true,
79 actions,
80 }
81 }
82}
83
84/// Trait for custom device support implementations.
85/// When DTYP is set to something other than "" or "Soft Channel",
86/// the registered DeviceSupport is used instead of link resolution.
87pub trait DeviceSupport: Send + Sync + 'static {
88 fn init(&mut self, _record: &mut dyn Record) -> CaResult<()> {
89 Ok(())
90 }
91
92 /// Read from hardware into the record.
93 ///
94 /// Returns a `DeviceReadOutcome` containing:
95 /// - `actions`: side-effect actions (link writes, delayed reprocess)
96 /// that the framework will execute after process()
97 /// - `did_compute`: if true, the record's built-in compute was already
98 /// performed (e.g., device support ran PID), so process() should skip it
99 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
100 let _ = record;
101 Ok(DeviceReadOutcome::ok())
102 }
103
104 fn write(&mut self, record: &mut dyn Record) -> CaResult<()>;
105 fn dtyp(&self) -> &str;
106
107 /// Return the last alarm (status, severity) from the driver.
108 /// None means the driver does not override alarms.
109 fn last_alarm(&self) -> Option<(u16, u16)> {
110 None
111 }
112
113 /// Return the last timestamp from the driver.
114 /// None means the driver does not override timestamps.
115 fn last_timestamp(&self) -> Option<std::time::SystemTime> {
116 None
117 }
118
119 /// Called after init() with the record name and scan type.
120 fn set_record_info(&mut self, _name: &str, _scan: ScanType) {}
121
122 /// Return a receiver for I/O Intr scan notifications.
123 /// Only called for records with SCAN=I/O Intr.
124 fn io_intr_receiver(&mut self) -> Option<crate::runtime::sync::mpsc::Receiver<()>> {
125 None
126 }
127
128 /// Begin an asynchronous write (submit only, no blocking).
129 /// Returns `Some(handle)` if the write was submitted to a worker queue —
130 /// the caller should wait on the handle outside any record lock.
131 /// Returns `None` to fall back to synchronous [`write()`](DeviceSupport::write).
132 fn write_begin(
133 &mut self,
134 _record: &mut dyn Record,
135 ) -> CaResult<Option<Box<dyn WriteCompletion>>> {
136 Ok(None)
137 }
138
139 /// Handle a named command from the record's process() via
140 /// `ProcessAction::DeviceCommand`. This allows records to request
141 /// driver operations (e.g., scaler reset/arm/write_preset) without
142 /// holding a direct driver reference.
143 ///
144 /// Default: ignore.
145 fn handle_command(
146 &mut self,
147 _record: &mut dyn Record,
148 _command: &str,
149 _args: &[crate::types::EpicsValue],
150 ) -> CaResult<()> {
151 Ok(())
152 }
153}