use crate::error::CaResult;
use crate::server::record::{AlarmSeverity, ProcessAction, Record, RecordInstance, ScanType};
pub fn is_soft_dtyp(dtyp: &str) -> bool {
dtyp.is_empty()
|| dtyp == "Soft Channel"
|| dtyp == "Raw Soft Channel"
|| dtyp == "Async Soft Channel"
|| dtyp == "Soft Timestamp"
|| dtyp == "Sec Past Epoch"
}
pub trait WriteCompletion: Send + 'static {
fn wait(&self, timeout: std::time::Duration) -> CaResult<()>;
}
#[derive(Default)]
pub struct DeviceReadOutcome {
pub actions: Vec<ProcessAction>,
pub did_compute: bool,
}
impl DeviceReadOutcome {
pub fn ok() -> Self {
Self::default()
}
pub fn computed() -> Self {
Self {
did_compute: true,
actions: Vec::new(),
}
}
pub fn computed_with(actions: Vec<ProcessAction>) -> Self {
Self {
did_compute: true,
actions,
}
}
}
pub trait DeviceSupport: Send + Sync + 'static {
fn init(&mut self, _record: &mut dyn Record) -> CaResult<()> {
Ok(())
}
fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
let _ = record;
Ok(DeviceReadOutcome::ok())
}
fn write(&mut self, record: &mut dyn Record) -> CaResult<()>;
fn dtyp(&self) -> &str;
fn last_alarm(&self) -> Option<(u16, u16)> {
None
}
fn last_timestamp(&self) -> Option<std::time::SystemTime> {
None
}
fn set_process_context(&mut self, _ctx: &crate::server::record::ProcessContext) {}
fn set_record_info(&mut self, _name: &str, _scan: ScanType) {}
fn apply_record_info(&mut self, _info: &std::collections::HashMap<String, String>) {}
fn io_intr_receiver(&mut self) -> Option<crate::runtime::sync::mpsc::Receiver<()>> {
None
}
fn write_begin(
&mut self,
_record: &mut dyn Record,
) -> CaResult<Option<Box<dyn WriteCompletion>>> {
Ok(None)
}
fn handle_command(
&mut self,
_record: &mut dyn Record,
_command: &str,
_args: &[crate::types::EpicsValue],
) -> CaResult<Vec<&'static str>> {
Ok(Vec::new())
}
}
pub fn wire_device_to_record(instance: &mut RecordInstance, mut dev: Box<dyn DeviceSupport>) {
let name = instance.name.clone();
dev.set_record_info(&name, instance.common.scan);
dev.apply_record_info(&instance.info);
match dev.init(&mut *instance.record) {
Ok(()) => {
if instance.record.val().is_some() {
instance.common.udf = false;
}
}
Err(e) => {
eprintln!(
"device support init failed for record '{name}' (DTYP '{}'): {e}",
instance.common.dtyp
);
instance.common.sevr = AlarmSeverity::Invalid;
instance.common.stat = crate::server::recgbl::alarm_status::SOFT_ALARM;
}
}
instance.device = Some(dev);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::CaError;
use crate::server::record::{AlarmSeverity, Record, RecordInstance, ScanType};
use crate::server::records::ai::AiRecord;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct WireObservation {
info_at_init: Vec<String>,
record_info_before_init: bool,
init_ran: bool,
}
struct ProbeDev {
obs: Arc<Mutex<WireObservation>>,
info: HashMap<String, String>,
record_info_set: bool,
fail_init: bool,
}
impl DeviceSupport for ProbeDev {
fn dtyp(&self) -> &str {
"ProbeDev"
}
fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
Ok(())
}
fn set_record_info(&mut self, _name: &str, _scan: ScanType) {
self.record_info_set = true;
}
fn apply_record_info(&mut self, info: &HashMap<String, String>) {
self.info = info.clone();
}
fn init(&mut self, _record: &mut dyn Record) -> CaResult<()> {
let mut o = self.obs.lock().unwrap();
o.init_ran = true;
o.record_info_before_init = self.record_info_set;
o.info_at_init = self.info.keys().cloned().collect();
if self.fail_init {
Err(CaError::InvalidValue("device init failed".into()))
} else {
Ok(())
}
}
}
#[test]
fn wire_device_init_failure_flags_record_invalid() {
let mut instance = RecordInstance::new("TEST:AI".to_string(), AiRecord::new(0.0));
instance.common.dtyp = "ProbeDev".to_string();
let obs = Arc::new(Mutex::new(WireObservation::default()));
let dev = Box::new(ProbeDev {
obs: obs.clone(),
info: HashMap::new(),
record_info_set: false,
fail_init: true,
});
wire_device_to_record(&mut instance, dev);
assert_eq!(
instance.common.sevr,
AlarmSeverity::Invalid,
"failed device init must flag the record INVALID"
);
assert_eq!(
instance.common.stat,
crate::server::recgbl::alarm_status::SOFT_ALARM,
);
assert!(
instance.device.is_some(),
"device is still attached so the record is addressable"
);
}
#[test]
fn wire_device_applies_info_and_record_info_before_init() {
let mut instance = RecordInstance::new("TEST:AI2".to_string(), AiRecord::new(0.0));
instance.common.dtyp = "ProbeDev".to_string();
instance.set_info("asyn:READBACK", "1");
let obs = Arc::new(Mutex::new(WireObservation::default()));
let dev = Box::new(ProbeDev {
obs: obs.clone(),
info: HashMap::new(),
record_info_set: false,
fail_init: false,
});
wire_device_to_record(&mut instance, dev);
let o = obs.lock().unwrap();
assert!(o.init_ran, "init must have run");
assert!(
o.record_info_before_init,
"set_record_info must run before init"
);
assert!(
o.info_at_init.iter().any(|k| k == "asyn:READBACK"),
"info(...) tags must be visible inside init()"
);
}
}