use crate::error::CaResult;
use crate::server::record::{AlarmSeverity, ProcessAction, Record, RecordInstance, ScanType};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SoftDtyp {
Plain,
Raw,
Async,
}
pub fn classify_soft(dtyp: &str) -> Option<SoftDtyp> {
match dtyp {
"" | "Soft Channel" => Some(SoftDtyp::Plain),
"Raw Soft Channel" => Some(SoftDtyp::Raw),
"Async Soft Channel" => Some(SoftDtyp::Async),
_ => None,
}
}
pub fn is_soft_dtyp(dtyp: &str) -> bool {
classify_soft(dtyp).is_some()
}
#[derive(Clone, Debug)]
pub struct Dtyp {
name: String,
soft: Option<SoftDtyp>,
}
impl Dtyp {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
let soft = classify_soft(&name);
Self { name, soft }
}
pub fn as_str(&self) -> &str {
&self.name
}
pub fn soft(&self) -> Option<SoftDtyp> {
self.soft
}
pub fn is_soft(&self) -> bool {
self.soft.is_some()
}
}
impl Default for Dtyp {
fn default() -> Self {
Self::new("")
}
}
impl From<&str> for Dtyp {
fn from(name: &str) -> Self {
Self::new(name)
}
}
impl From<String> for Dtyp {
fn from(name: String) -> Self {
Self::new(name)
}
}
impl std::fmt::Display for Dtyp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.name)
}
}
impl PartialEq<str> for Dtyp {
fn eq(&self, other: &str) -> bool {
self.name == other
}
}
impl PartialEq<&str> for Dtyp {
fn eq(&self, other: &&str) -> bool {
self.name == *other
}
}
pub trait WriteCompletion: Send + 'static {
fn wait(&self, timeout: std::time::Duration) -> CaResult<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DeviceReadStatus {
#[default]
Converted,
Computed,
NoValue,
}
impl DeviceReadStatus {
pub fn skips_conversion(self) -> bool {
!matches!(self, Self::Converted)
}
pub fn read_failed(self) -> bool {
matches!(self, Self::NoValue)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DeviceUdf {
#[default]
Untouched,
Defined,
Undefined,
}
#[derive(Default)]
pub struct DeviceReadOutcome {
pub actions: Vec<ProcessAction>,
pub status: DeviceReadStatus,
udf: DeviceUdf,
}
impl DeviceReadOutcome {
pub fn ok() -> Self {
Self::default()
}
pub fn converted(udf: DeviceUdf) -> Self {
Self {
status: DeviceReadStatus::Converted,
actions: Vec::new(),
udf,
}
}
pub fn computed(udf: DeviceUdf) -> Self {
Self {
status: DeviceReadStatus::Computed,
actions: Vec::new(),
udf,
}
}
pub fn computed_with(udf: DeviceUdf, actions: Vec<ProcessAction>) -> Self {
Self {
status: DeviceReadStatus::Computed,
actions,
udf,
}
}
pub fn no_value(udf: DeviceUdf) -> Self {
Self {
status: DeviceReadStatus::NoValue,
actions: Vec::new(),
udf,
}
}
pub fn failed() -> Self {
Self::no_value(DeviceUdf::Untouched)
}
pub fn undefined() -> Self {
Self::no_value(DeviceUdf::Undefined)
}
pub fn udf(&self) -> DeviceUdf {
self.udf
}
pub fn asserts_undefined(&self) -> bool {
matches!(self.udf, DeviceUdf::Undefined)
}
pub fn did_compute(&self) -> bool {
self.status.skips_conversion()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DeviceInitOutcome {
#[default]
Live,
Dead {
alarm: Option<(u16, AlarmSeverity)>,
},
}
impl DeviceInitOutcome {
pub fn dead() -> Self {
Self::Dead { alarm: None }
}
pub fn dead_with_alarm(stat: u16, sevr: AlarmSeverity) -> Self {
Self::Dead {
alarm: Some((stat, sevr)),
}
}
}
#[derive(Debug, Clone)]
pub struct PropertyPost {
pub writes: Vec<(String, crate::types::EpicsValue)>,
pub post_field: String,
}
pub trait DeviceSupport: Send + Sync + 'static {
fn init(&mut self, _record: &mut dyn Record) -> CaResult<DeviceInitOutcome> {
Ok(DeviceInitOutcome::Live)
}
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 last_utag(&self) -> Option<u64> {
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 property_post_receiver(
&mut self,
) -> Option<crate::runtime::sync::mpsc::Receiver<PropertyPost>> {
None
}
fn io_intr_scan_independent(&self) -> bool {
false
}
fn arm_readback_callback(&mut self) {}
fn reconcile_readback_callback(&mut self) {}
fn output_callback_readback(&self) -> bool {
false
}
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, dev: Box<dyn DeviceSupport>) {
attach_device_to_record(instance, dev);
init_device_support(instance);
}
pub fn attach_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);
instance.device = Some(dev);
}
pub fn init_device_support(instance: &mut RecordInstance) {
let Some(mut dev) = instance.device.take() else {
return;
};
let name = instance.name.clone();
match dev.init(&mut *instance.record) {
Ok(DeviceInitOutcome::Live) => {}
Ok(DeviceInitOutcome::Dead { alarm }) => {
if let Some((stat, sevr)) = alarm {
crate::server::recgbl::rec_gbl_set_sevr(&mut instance.common, stat, sevr);
}
instance.enter_pact();
}
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,
}
#[derive(Clone, Copy)]
enum InitVerdict {
Live,
Dead,
Fail,
}
struct ProbeDev {
obs: Arc<Mutex<WireObservation>>,
info: HashMap<String, String>,
record_info_set: bool,
verdict: InitVerdict,
}
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<DeviceInitOutcome> {
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();
match self.verdict {
InitVerdict::Live => Ok(DeviceInitOutcome::Live),
InitVerdict::Dead => Ok(DeviceInitOutcome::dead()),
InitVerdict::Fail => Err(CaError::InvalidValue("device init failed".into())),
}
}
}
fn wire(verdict: InitVerdict) -> RecordInstance {
let mut instance = RecordInstance::new("TEST:DEAD".to_string(), AiRecord::new(0.0));
instance.common.dtyp = "ProbeDev".into();
wire_device_to_record(
&mut instance,
Box::new(ProbeDev {
obs: Arc::new(Mutex::new(WireObservation::default())),
info: HashMap::new(),
record_info_set: false,
verdict,
}),
);
instance
}
#[test]
fn wire_device_dead_init_leaves_the_record_in_pact() {
let instance = wire(InitVerdict::Dead);
assert!(
instance.is_processing(),
"C `bad: pr->pact = 1` — the record must be dead"
);
assert!(
instance.device.is_some(),
"a dead record is still addressable; only processing stops"
);
}
#[test]
fn wire_device_dead_init_raises_no_alarm_of_its_own() {
let dead = wire(InitVerdict::Dead);
let live = wire(InitVerdict::Live);
assert_eq!(dead.common.sevr, live.common.sevr);
assert_eq!(
dead.common.stat, live.common.stat,
"the dead arm leaves STAT at whatever the record was born with"
);
}
#[test]
fn wire_device_live_and_failed_inits_leave_the_record_processing() {
assert!(!wire(InitVerdict::Live).is_processing());
assert!(
!wire(InitVerdict::Fail).is_processing(),
"an errored init flags the record but must not kill it"
);
}
#[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".into();
let obs = Arc::new(Mutex::new(WireObservation::default()));
let dev = Box::new(ProbeDev {
obs: obs.clone(),
info: HashMap::new(),
record_info_set: false,
verdict: InitVerdict::Fail,
});
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".into();
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,
verdict: InitVerdict::Live,
});
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()"
);
}
#[test]
fn a_dtyp_carries_the_class_its_name_classifies_to() {
let names = [
"",
"Soft Channel",
"Raw Soft Channel",
"Async Soft Channel",
"Soft Timestamp",
"asynInt32",
"Db State",
];
for name in names {
for built in [
Dtyp::new(name),
Dtyp::from(name),
Dtyp::from(name.to_string()),
] {
assert_eq!(built.as_str(), name);
assert_eq!(built.soft(), classify_soft(name), "{name:?}");
assert_eq!(built.is_soft(), is_soft_dtyp(name), "{name:?}");
assert_eq!(built.clone().soft(), classify_soft(name), "{name:?} clone");
}
}
assert_eq!(Dtyp::default().as_str(), "");
assert_eq!(Dtyp::default().soft(), classify_soft(""));
}
}