use crate::error::{CaError, CaResult};
use crate::server::record::{FieldMetadataOverride, InputFetchPolicy, ProcessOutcome, Record};
use crate::types::{EpicsValue, PvString};
const NUM_ARGS: usize = 21;
const FTYPE_DOUBLE: i16 = 10;
const SUFFIX: [char; NUM_ARGS] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U',
];
const EFLG_NEVER: i16 = 0;
const EFLG_ON_CHANGE: i16 = 1;
const EFLG_ALWAYS: i16 = 2;
const ASUB_EFLG_CHOICES: &[&str] = &["NEVER", "ON CHANGE", "ALWAYS"];
const ASUB_LFLG_CHOICES: &[&str] = &["IGNORE", "READ"];
const LFLG_IGNORE: i16 = 0;
fn asub_output_event_fields() -> &'static [&'static str] {
use std::sync::OnceLock;
static FIELDS: OnceLock<Vec<&'static str>> = OnceLock::new();
FIELDS.get_or_init(|| {
SUFFIX
.iter()
.flat_map(|&c| {
[
Box::leak(format!("VAL{c}").into_boxed_str()) as &'static str,
Box::leak(format!("NEV{c}").into_boxed_str()) as &'static str,
]
})
.collect()
})
}
pub struct ASubRecord {
pub val: i32,
pub snam: PvString,
pub inam: PvString,
pub inp: [String; NUM_ARGS],
pub a: [EpicsValue; NUM_ARGS],
pub vala: [EpicsValue; NUM_ARGS],
pub out: [String; NUM_ARGS],
pub fta: [i16; NUM_ARGS],
pub ftva: [i16; NUM_ARGS],
pub noa: [i32; NUM_ARGS],
pub nova: [i32; NUM_ARGS],
pub nea: [i32; NUM_ARGS],
pub neva: [i32; NUM_ARGS],
pub brsv: i16,
pub prec: i16,
pub eflg: i16,
pub lflg: i16,
pub subl: String,
pub onam: PvString,
sub_status: i64,
}
fn asub_output_links() -> &'static [(&'static str, &'static str)] {
use std::sync::OnceLock;
static PAIRS: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
PAIRS.get_or_init(|| {
SUFFIX
.iter()
.map(|&c| {
let link: &'static str = Box::leak(format!("OUT{c}").into_boxed_str());
let val: &'static str = Box::leak(format!("VAL{c}").into_boxed_str());
(link, val)
})
.collect()
})
}
impl Default for ASubRecord {
fn default() -> Self {
Self {
val: 0,
snam: PvString::new(),
inam: PvString::new(),
inp: std::array::from_fn(|_| String::new()),
a: std::array::from_fn(|_| EpicsValue::Double(0.0)),
vala: std::array::from_fn(|_| EpicsValue::DoubleArray(Vec::new())),
out: std::array::from_fn(|_| String::new()),
fta: [FTYPE_DOUBLE; NUM_ARGS],
ftva: [FTYPE_DOUBLE; NUM_ARGS],
noa: [1; NUM_ARGS],
nova: [1; NUM_ARGS],
nea: [1; NUM_ARGS],
neva: [1; NUM_ARGS],
brsv: 0,
prec: 0,
eflg: EFLG_ON_CHANGE,
lflg: 0,
subl: String::new(),
onam: PvString::new(),
sub_status: SUBROUTINE_NOT_RUN,
}
}
}
const SUBROUTINE_NOT_RUN: i64 = -1;
fn parse_channel(name: &str) -> Option<(&'static str, usize)> {
const PREFIXES: [&str; 7] = ["INP", "OUT", "VAL", "FTV", "FT", "NOV", "NO"];
if name.len() == 1 {
let c = name.chars().next().unwrap();
return SUFFIX.iter().position(|&s| s == c).map(|i| ("", i));
}
if let Some(rest) = name.strip_prefix("NEV") {
if rest.len() == 1 {
let c = rest.chars().next().unwrap();
return SUFFIX.iter().position(|&s| s == c).map(|i| ("NEV", i));
}
return None;
}
if let Some(rest) = name.strip_prefix("NE") {
if rest.len() == 1 {
let c = rest.chars().next().unwrap();
return SUFFIX.iter().position(|&s| s == c).map(|i| ("NE", i));
}
return None;
}
for prefix in PREFIXES {
if let Some(rest) = name.strip_prefix(prefix) {
if rest.len() == 1 {
let c = rest.chars().next().unwrap();
if let Some(i) = SUFFIX.iter().position(|&s| s == c) {
return Some((prefix, i));
}
}
}
}
None
}
fn channel_get(v: &EpicsValue) -> EpicsValue {
match v {
EpicsValue::DoubleArray(a) if a.is_empty() => EpicsValue::Double(0.0),
EpicsValue::DoubleArray(a) if a.len() == 1 => EpicsValue::Double(a[0]),
other => other.clone(),
}
}
impl Record for ASubRecord {
fn record_type(&self) -> &'static str {
"aSub"
}
fn clears_udf(&self) -> bool {
false
}
fn raises_udf_alarm(&self) -> bool {
false
}
fn process(&mut self) -> CaResult<ProcessOutcome> {
Ok(ProcessOutcome::complete())
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => return Some(EpicsValue::Long(self.val)),
"SNAM" => return Some(EpicsValue::String(self.snam.clone())),
"INAM" => return Some(EpicsValue::String(self.inam.clone())),
"BRSV" => return Some(EpicsValue::Short(self.brsv)),
"PREC" => return Some(EpicsValue::Short(self.prec)),
"EFLG" => return Some(EpicsValue::Short(self.eflg)),
"LFLG" => return Some(EpicsValue::Short(self.lflg)),
"SUBL" => return Some(EpicsValue::String(self.subl.clone().into())),
"ONAM" => return Some(EpicsValue::String(self.onam.clone())),
_ => {}
}
let (prefix, idx) = parse_channel(name)?;
Some(match prefix {
"" => self.a[idx].clone(), "INP" => EpicsValue::String(self.inp[idx].clone().into()),
"VAL" => channel_get(&self.vala[idx]),
"OUT" => EpicsValue::String(self.out[idx].clone().into()),
"FT" => EpicsValue::Short(self.fta[idx]),
"FTV" => EpicsValue::Short(self.ftva[idx]),
"NO" => EpicsValue::Long(self.noa[idx]),
"NOV" => EpicsValue::Long(self.nova[idx]),
"NE" => EpicsValue::Long(self.nea[idx]),
"NEV" => EpicsValue::Long(self.neva[idx]),
_ => return None,
})
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"VAL" => {
self.val = value
.to_dbf_i32()
.ok_or_else(|| CaError::TypeMismatch(name.into()))?;
return Ok(());
}
"SNAM" => {
return match value {
EpicsValue::String(s) => {
self.snam = s;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
"INAM" => {
return match value {
EpicsValue::String(s) => {
self.inam = s;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
"BRSV" => {
self.brsv = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("BRSV".into()))?
as i16;
return Ok(());
}
"PREC" => {
self.prec = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("PREC".into()))?
as i16;
return Ok(());
}
"EFLG" => {
self.eflg = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("EFLG".into()))?
as i16;
return Ok(());
}
"LFLG" => {
self.lflg = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("LFLG".into()))?
as i16;
return Ok(());
}
"SUBL" => {
return match value {
EpicsValue::String(s) => {
self.subl = s.as_str_lossy().into_owned();
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
"ONAM" => {
return match value {
EpicsValue::String(s) => {
self.onam = s;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
_ => {}
}
let (prefix, idx) =
parse_channel(name).ok_or_else(|| CaError::FieldNotFound(name.to_string()))?;
match prefix {
"" => {
self.nea[idx] = value.count().max(1) as i32;
self.a[idx] = value;
}
"INP" | "OUT" => {
let s = match value {
EpicsValue::String(s) => s.as_str_lossy().into_owned(),
_ => return Err(CaError::TypeMismatch(name.into())),
};
if prefix == "INP" {
self.inp[idx] = s;
} else {
self.out[idx] = s;
}
}
"VAL" => {
self.neva[idx] = value.count().max(1) as i32;
self.vala[idx] = value;
}
"FT" | "FTV" | "NO" | "NOV" | "NE" | "NEV" => {
let v = match value {
EpicsValue::Short(v) => v as i32,
EpicsValue::Long(v) => v,
other => other
.to_f64()
.map(|f| f as i32)
.ok_or_else(|| CaError::TypeMismatch(name.into()))?,
};
match prefix {
"FT" => self.fta[idx] = v as i16,
"FTV" => self.ftva[idx] = v as i16,
"NO" => self.noa[idx] = v,
"NOV" => self.nova[idx] = v,
"NE" => self.nea[idx] = v,
"NEV" => self.neva[idx] = v,
_ => unreachable!(),
}
}
_ => return Err(CaError::FieldNotFound(name.to_string())),
}
Ok(())
}
fn is_subroutine_name_field(&self, field: &str) -> bool {
field == "SNAM" && self.lflg == LFLG_IGNORE
}
fn menu_field_choices(&self, field: &str) -> Option<&'static [&'static str]> {
match field {
"EFLG" => Some(ASUB_EFLG_CHOICES),
"LFLG" => Some(ASUB_LFLG_CHOICES),
_ => None,
}
}
fn force_posted_fields(&self) -> &'static [&'static str] {
if self.eflg == EFLG_ALWAYS {
asub_output_event_fields()
} else {
&[]
}
}
fn event_posted_fields(&self) -> &'static [&'static str] {
if self.eflg == EFLG_NEVER {
asub_output_event_fields()
} else {
&[]
}
}
fn field_metadata_override(&self, field: &str) -> Option<FieldMetadataOverride> {
if field.eq_ignore_ascii_case("VAL") {
return Some(FieldMetadataOverride {
precision: Some(self.prec),
..Default::default()
});
}
None
}
fn constant_init_links(&self) -> Vec<crate::server::record::ConstantInitLink> {
let mut seeds = vec![crate::server::record::ConstantInitLink::new("SUBL", "SNAM")];
seeds.extend(crate::server::record::seed_input_links(
self.multi_input_links(),
));
seeds
}
fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
use std::sync::OnceLock;
static PAIRS: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
PAIRS.get_or_init(|| {
SUFFIX
.iter()
.map(|&c| {
let link: &'static str = Box::leak(format!("INP{c}").into_boxed_str());
let val: &'static str = Box::leak(c.to_string().into_boxed_str());
(link, val)
})
.collect()
})
}
fn input_fetch_policy(&self) -> InputFetchPolicy {
InputFetchPolicy::AbortOnFirstFailure
}
fn set_subroutine_status(&mut self, status: i64) {
self.sub_status = status;
}
fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
if self.sub_status == 0 {
asub_output_links()
} else {
&[]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channels_m_through_u_present() {
let mut rec = ASubRecord::default();
for name in ["M", "Q", "U"] {
rec.put_field(name, EpicsValue::Double(3.0)).unwrap();
assert_eq!(rec.get_field(name), Some(EpicsValue::Double(3.0)));
}
for name in ["VALM", "VALU"] {
rec.put_field(name, EpicsValue::DoubleArray(vec![1.0, 2.0]))
.unwrap();
assert_eq!(
rec.get_field(name),
Some(EpicsValue::DoubleArray(vec![1.0, 2.0]))
);
}
for name in ["NOM", "NOVU", "INPR", "OUTT"] {
assert!(rec.get_field(name).is_some(), "channel field must exist");
}
}
#[test]
fn non_double_channel_keeps_native_type() {
let mut rec = ASubRecord::default();
rec.put_field("FTA", EpicsValue::Short(0)).unwrap(); rec.put_field("A", EpicsValue::String("hello".into()))
.unwrap();
assert_eq!(rec.get_field("A"), Some(EpicsValue::String("hello".into())));
assert_eq!(rec.fta[0], 0);
rec.put_field("FTC", EpicsValue::Short(5)).unwrap(); rec.put_field("C", EpicsValue::LongArray(vec![10, 20, 30]))
.unwrap();
assert_eq!(
rec.get_field("C"),
Some(EpicsValue::LongArray(vec![10, 20, 30]))
);
assert_eq!(rec.get_field("NEC"), Some(EpicsValue::Long(3)));
}
#[test]
fn twenty_one_multi_input_links() {
let rec = ASubRecord::default();
assert_eq!(rec.multi_input_links().len(), 21);
}
#[test]
fn prec_round_trips_and_serves_val_precision() {
let mut rec = ASubRecord::default();
assert_eq!(rec.get_field("PREC"), Some(EpicsValue::Short(0)));
assert_eq!(
rec.field_metadata_override("VAL").and_then(|o| o.precision),
Some(0)
);
rec.put_field("PREC", EpicsValue::Short(4)).unwrap();
assert_eq!(rec.get_field("PREC"), Some(EpicsValue::Short(4)));
assert_eq!(
rec.field_metadata_override("VAL").and_then(|o| o.precision),
Some(4),
"PREC must be served as VAL display precision"
);
assert!(rec.field_metadata_override("SNAM").is_none());
}
#[test]
fn eflg_drives_output_event_posting_policy() {
let mut rec = ASubRecord::default();
assert_eq!(rec.eflg, EFLG_ON_CHANGE);
assert!(rec.force_posted_fields().is_empty());
assert!(rec.event_posted_fields().is_empty());
rec.put_field("EFLG", EpicsValue::Short(EFLG_ALWAYS))
.unwrap();
assert_eq!(rec.get_field("EFLG"), Some(EpicsValue::Short(2)));
assert_eq!(rec.force_posted_fields().len(), NUM_ARGS * 2);
assert!(rec.force_posted_fields().contains(&"VALA"));
assert!(rec.force_posted_fields().contains(&"NEVU"));
assert!(rec.event_posted_fields().is_empty());
rec.put_field("EFLG", EpicsValue::Short(EFLG_NEVER))
.unwrap();
assert!(rec.force_posted_fields().is_empty());
assert_eq!(rec.event_posted_fields().len(), NUM_ARGS * 2);
assert!(rec.event_posted_fields().contains(&"VALA"));
assert!(!rec.event_posted_fields().contains(&"VAL"));
}
#[test]
fn eflg_loads_from_menu_label() {
use crate::server::db_loader::{apply_fields, create_record};
let mut rec = create_record("aSub").unwrap();
let mut common = Vec::new();
apply_fields(&mut rec, &[("EFLG".into(), "ALWAYS".into())], &mut common)
.expect("EFLG menu label must load");
assert_eq!(rec.get_field("EFLG"), Some(EpicsValue::Short(2)));
}
#[test]
fn brsv_round_trips() {
let mut rec = ASubRecord::default();
assert_eq!(rec.get_field("BRSV"), Some(EpicsValue::Short(0)));
rec.put_field("BRSV", EpicsValue::Short(3)).unwrap();
assert_eq!(rec.get_field("BRSV"), Some(EpicsValue::Short(3)));
}
#[test]
fn lflg_subl_onam_round_trip() {
let mut rec = ASubRecord::default();
assert_eq!(rec.get_field("LFLG"), Some(EpicsValue::Short(0)));
assert_eq!(rec.get_field("SUBL"), Some(EpicsValue::String("".into())));
assert_eq!(rec.get_field("ONAM"), Some(EpicsValue::String("".into())));
rec.put_field("LFLG", EpicsValue::Short(1)).unwrap();
rec.put_field("SUBL", EpicsValue::String("HOLDER".into()))
.unwrap();
rec.put_field("ONAM", EpicsValue::String("prev".into()))
.unwrap();
assert_eq!(rec.get_field("LFLG"), Some(EpicsValue::Short(1)));
assert_eq!(
rec.get_field("SUBL"),
Some(EpicsValue::String("HOLDER".into()))
);
assert_eq!(
rec.get_field("ONAM"),
Some(EpicsValue::String("prev".into()))
);
}
#[test]
fn snam_is_a_subroutine_name_field_only_in_ignore_mode() {
let mut rec = ASubRecord::default();
assert_eq!(rec.lflg, LFLG_IGNORE);
assert!(rec.is_subroutine_name_field("SNAM"));
assert!(!rec.is_subroutine_name_field("INAM"));
assert!(!rec.is_subroutine_name_field("VAL"));
rec.put_field("LFLG", EpicsValue::Short(1)).unwrap(); assert!(
!rec.is_subroutine_name_field("SNAM"),
"READ mode reads the name from SUBL at process time; a SNAM put is not validated"
);
}
#[test]
fn lflg_subl_load_from_db() {
use crate::server::db_loader::{apply_fields, create_record};
let mut rec = create_record("aSub").unwrap();
let mut common = Vec::new();
apply_fields(
&mut rec,
&[
("LFLG".into(), "READ".into()),
("SUBL".into(), "NAME_HOLDER".into()),
],
&mut common,
)
.expect("LFLG menu label and SUBL link must load");
assert_eq!(rec.get_field("LFLG"), Some(EpicsValue::Short(1)));
assert_eq!(
rec.get_field("SUBL"),
Some(EpicsValue::String("NAME_HOLDER".into()))
);
}
}