use crate::error::CaResult;
use crate::types::c_parse::Converted;
use crate::types::{DbFieldType, DbfCode, EpicsValue, PvString, c_parse};
use super::alarm::AlarmLimit;
use super::scan::ScanType;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RawSoftEntry {
InitConstant,
Read,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i16)]
pub enum Special {
None = 0,
NoMod = 1,
DbAddr = 2,
Scan = 3,
AlarmAck = 5,
As = 6,
Attribute = 7,
Mod = 100,
Reset = 101,
LinConv = 102,
Calc = 103,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Asl {
Asl0,
Asl1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Base {
Decimal,
Hex,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FieldSlot(pub u16);
#[derive(Debug, Clone)]
pub struct FieldDesc {
pub name: &'static str,
pub dbf_type: DbFieldType,
pub declared_dbf: DbfCode,
pub runtime_typed: bool,
pub read_only: bool,
pub special: Special,
pub declared_special: Special,
pub pp: bool,
pub asl: Asl,
pub size: u16,
pub extra: Option<&'static str>,
pub menu: Option<&'static [&'static str]>,
pub initial: Option<&'static str>,
pub interest: u8,
pub prop: bool,
pub prompt: Option<&'static str>,
pub promptgroup: Option<&'static str>,
pub base: Base,
}
impl FieldDesc {
pub const fn new(name: &'static str, dbf_type: DbFieldType, read_only: bool) -> Self {
Self {
name,
dbf_type,
declared_dbf: dbf_type.dbf_code(),
runtime_typed: false,
read_only,
special: if read_only {
Special::NoMod
} else {
Special::None
},
declared_special: if read_only {
Special::NoMod
} else {
Special::None
},
pp: false,
asl: Asl::Asl1,
size: 0,
extra: None,
menu: None,
initial: None,
interest: 0,
prop: false,
prompt: None,
promptgroup: None,
base: Base::Decimal,
}
}
pub const fn no_access(&self) -> bool {
matches!(self.declared_dbf, DbfCode::NoAccess)
}
pub const fn unreadable(&self) -> bool {
self.no_access() && !matches!(self.declared_special, Special::DbAddr)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConstantInitLink {
pub link_field: &'static str,
pub target_field: &'static str,
pub clears_udf: bool,
pub normalize_bool: bool,
}
impl ConstantInitLink {
pub const fn new(link_field: &'static str, target_field: &'static str) -> Self {
Self {
link_field,
target_field,
clears_udf: false,
normalize_bool: false,
}
}
pub const fn dol_to_val(link_field: &'static str, target_field: &'static str) -> Self {
Self {
link_field,
target_field,
clears_udf: true,
normalize_bool: false,
}
}
pub const fn dol_to_bool_val(link_field: &'static str, target_field: &'static str) -> Self {
Self {
link_field,
target_field,
clears_udf: true,
normalize_bool: true,
}
}
}
pub fn seed_input_links(pairs: &[(&'static str, &'static str)]) -> Vec<ConstantInitLink> {
pairs
.iter()
.map(|(link, value)| ConstantInitLink::new(link, value))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutTarget {
pub field_type: Option<DbFieldType>,
pub element_count: i64,
pub is_ca_link: bool,
pub puts_as_string: bool,
}
impl OutTarget {
pub const UNRESOLVED: Self = Self {
field_type: None,
element_count: 1,
is_ca_link: false,
puts_as_string: false,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkReadAs {
Native,
String,
Double,
CharArrayAsString { max_elements: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputLinkRequest {
As(LinkReadAs),
NotRead,
FromSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValuePostGate {
OnChange,
WithValue,
OnChangeForced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CyclePostMask {
Value,
ValueLog,
MonitorValueLog,
}
pub(crate) fn value_gate(
value_masked: &'static [(&'static str, ValuePostGate)],
field: &str,
) -> Option<ValuePostGate> {
value_masked
.iter()
.find(|(name, _)| *name == field)
.map(|(_, gate)| *gate)
}
pub fn input_link_slots_of<S: AsRef<str>>(texts: &[S]) -> Option<(u64, u64)> {
debug_assert!(texts.len() <= u64::BITS as usize);
let mut set = 0u64;
for (slot, text) in texts.iter().enumerate() {
if !text.as_ref().is_empty() {
set |= 1 << slot;
}
}
Some((set, 0))
}
#[derive(Clone, Copy)]
pub(crate) struct AuxPostMask {
value_only: &'static [&'static str],
monitor_masked: &'static [&'static str],
no_alarm_bits: &'static [&'static str],
}
impl AuxPostMask {
pub(crate) fn of(record: &dyn Record) -> Self {
Self {
value_only: record.value_only_change_fields(),
monitor_masked: record.fields_posted_with_monitor_mask(),
no_alarm_bits: record.fields_posted_without_alarm_bits(),
}
}
pub(crate) fn mask_for(
&self,
field: &str,
alarm_bits: crate::server::recgbl::EventMask,
deadband_mask: crate::server::recgbl::EventMask,
) -> crate::server::recgbl::EventMask {
use crate::server::recgbl::EventMask;
if self.value_only.contains(&field) {
alarm_bits | EventMask::VALUE
} else if self.monitor_masked.contains(&field) {
deadband_mask | EventMask::VALUE
} else if self.no_alarm_bits.contains(&field) {
EventMask::VALUE | EventMask::LOG
} else {
alarm_bits | EventMask::VALUE | EventMask::LOG
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ArrayMonitorPost {
pub post_value: bool,
pub post_archive: bool,
pub hash_changed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RsetDefaultArm {
RecGblRange,
Seed,
}
pub fn control_default_arm(rtype: &str) -> RsetDefaultArm {
match rtype {
"acalcout" | "scalcout" => RsetDefaultArm::Seed,
_ => RsetDefaultArm::RecGblRange,
}
}
pub fn graphic_default_arm(rtype: &str) -> RsetDefaultArm {
match rtype {
"aSub" | "acalcout" | "scalcout" => RsetDefaultArm::Seed,
_ => RsetDefaultArm::RecGblRange,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlarmValArm {
Gated,
Unconditional,
}
pub fn alarm_explicit_fields(rtype: &str) -> &'static [&'static str] {
match rtype {
"seq" | "aSub" | "swait" => &[],
"motor" => &["VAL", "DVAL"],
_ => &["VAL"],
}
}
pub fn alarm_val_arm(rtype: &str) -> AlarmValArm {
match rtype {
"int64in" | "int64out" | "scalcout" | "acalcout" | "motor" | "epid" => {
AlarmValArm::Unconditional
}
_ => AlarmValArm::Gated,
}
}
pub fn graphic_limit_fields(rtype: &str) -> (&'static str, &'static str) {
match rtype {
"motor" => ("HLM", "LLM"),
_ => ("HOPR", "LOPR"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlLimitSource {
Drive,
DriveWhenSet,
SoftLimits,
Operator,
}
pub fn control_limit_source(rtype: &str) -> ControlLimitSource {
match rtype {
"ao" => ControlLimitSource::Drive,
"longout" | "int64out" => ControlLimitSource::DriveWhenSet,
"motor" => ControlLimitSource::SoftLimits,
_ => ControlLimitSource::Operator,
}
}
pub fn default_property_support(rtype: &str) -> crate::server::snapshot::PropertySupport {
use crate::server::snapshot::PropertySupport as P;
match rtype {
"ai" | "ao" | "calc" | "calcout" | "sel" | "sub" | "dfanout" | "seq" => P::NUMERIC,
"longin" | "longout" | "int64in" | "int64out" => P {
precision: false,
..P::NUMERIC
},
"waveform" | "aai" | "aao" | "subArray" | "compress" | "histogram" => P {
alarm_double: false,
..P::NUMERIC
},
"stringin" | "stringout" | "lsi" | "lso" | "event" | "permissive" | "state" | "printf"
| "fanout" | "timestamp" => P::NONE,
"bi" | "mbbi" | "mbbo" => P {
enum_strs: true,
..P::NONE
},
"bo" => P {
units: true,
precision: true,
control_double: true,
enum_strs: true,
..P::NONE
},
"busy" => P {
precision: true,
enum_strs: true,
..P::NONE
},
"mbbiDirect" | "mbboDirect" => P {
precision: true,
..P::NONE
},
"scalcout" | "acalcout" | "motor" | "epid" | "aSub" => P::NUMERIC,
"scaler" => P {
precision: true,
..P::NONE
},
"table" => P {
alarm_double: false,
..P::NUMERIC
},
"swait" => P {
units: false,
control_double: false,
..P::NUMERIC
},
"transform" | "sseq" => P {
precision: true,
..P::NONE
},
"throttle" => P {
precision: true,
graphic_double: true,
..P::NONE
},
_ => P::NUMERIC,
}
}
#[derive(Debug, Clone, Default)]
pub struct FieldMetadataOverride {
pub units: Option<crate::types::PvString>,
pub precision: Option<i16>,
pub disp_limits: Option<(f64, f64)>,
pub ctrl_limits: Option<(f64, f64)>,
pub alarm_limits: Option<(f64, f64, f64, f64)>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProcessAction {
WriteDbLink {
link_field: &'static str,
value: EpicsValue,
},
ResolveOutTarget { link_field: &'static str },
ReadDbLink {
link_field: &'static str,
target_field: &'static str,
},
ReprocessAfter(std::time::Duration),
DelayedCallbackAfter(std::time::Duration),
ScanOnce,
DeviceCommand {
command: &'static str,
args: Vec<EpicsValue>,
},
WriteDbLinkNotify {
link_field: &'static str,
value: EpicsValue,
},
ArmWatchdog,
CancelReprocess,
}
#[derive(Debug)]
pub struct CycleList<T>(std::mem::ManuallyDrop<Vec<T>>);
pub type ProcessActions = CycleList<ProcessAction>;
impl<T> CycleList<T> {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, entry: T) {
self.0.push(entry);
}
pub fn into_vec(mut self) -> Vec<T> {
std::mem::take(&mut *self.0)
}
#[cold]
#[inline(never)]
fn release(entries: &mut std::mem::ManuallyDrop<Vec<T>>) {
unsafe { std::mem::ManuallyDrop::drop(entries) }
}
}
impl<T> Default for CycleList<T> {
fn default() -> Self {
Self(std::mem::ManuallyDrop::new(Vec::new()))
}
}
impl<T> From<Vec<T>> for CycleList<T> {
fn from(entries: Vec<T>) -> Self {
Self(std::mem::ManuallyDrop::new(entries))
}
}
impl<T> Drop for CycleList<T> {
#[inline]
fn drop(&mut self) {
if self.0.capacity() != 0 {
Self::release(&mut self.0);
}
}
}
impl<T> std::ops::Deref for CycleList<T> {
type Target = [T];
fn deref(&self) -> &[T] {
&self.0
}
}
impl<T> IntoIterator for CycleList<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.into_vec().into_iter()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DelayedCallbackOutcome {
Reprocess,
Rearm(std::time::Duration),
Drop,
}
#[derive(Clone, Debug, PartialEq)]
pub enum RecordProcessResult {
Complete,
AsyncPending,
AsyncPendingNotify(Vec<(String, EpicsValue)>),
CompleteNoEmit,
CompleteDeferOutput,
CompleteAlarmOnly,
}
#[derive(Clone, Debug)]
pub struct ProcessOutcome {
pub result: RecordProcessResult,
pub actions: Vec<ProcessAction>,
pub device_did_compute: bool,
pub post_write_fields: Vec<(String, EpicsValue)>,
}
impl ProcessOutcome {
pub fn complete() -> Self {
Self {
result: RecordProcessResult::Complete,
actions: Vec::new(),
device_did_compute: false,
post_write_fields: Vec::new(),
}
}
pub fn complete_with(actions: Vec<ProcessAction>) -> Self {
Self {
result: RecordProcessResult::Complete,
actions,
device_did_compute: false,
post_write_fields: Vec::new(),
}
}
pub fn complete_no_emit() -> Self {
Self {
result: RecordProcessResult::CompleteNoEmit,
actions: Vec::new(),
device_did_compute: false,
post_write_fields: Vec::new(),
}
}
pub fn complete_alarm_only() -> Self {
Self {
result: RecordProcessResult::CompleteAlarmOnly,
actions: Vec::new(),
device_did_compute: false,
post_write_fields: Vec::new(),
}
}
pub fn async_pending() -> Self {
Self {
result: RecordProcessResult::AsyncPending,
actions: Vec::new(),
device_did_compute: false,
post_write_fields: Vec::new(),
}
}
}
impl Default for ProcessOutcome {
fn default() -> Self {
Self::complete()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommonFieldPutResult {
NoChange,
ScanChanged {
old_scan: ScanType,
new_scan: ScanType,
phas: i16,
},
PhasChanged {
scan: ScanType,
old_phas: i16,
new_phas: i16,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProcessContext<'a> {
pub udf: bool,
pub udfs: crate::server::record::AlarmSeverity,
pub nsev: crate::server::record::AlarmSeverity,
pub phas: i16,
pub tse: i16,
pub time: std::time::SystemTime,
pub dtyp: &'a str,
pub callback_priority: crate::runtime::task::CallbackPriority,
}
pub const EPICS_TIME_EVENT_DEVICE_TIME: i16 = -2;
pub type FieldPost = (
std::borrow::Cow<'static, str>,
EpicsValue,
crate::server::recgbl::EventMask,
);
#[derive(Default)]
pub struct ProcessSnapshot {
head: Option<FieldPost>,
rest: Vec<FieldPost>,
}
impl ProcessSnapshot {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, post: FieldPost) {
match self.head {
None => self.head = Some(post),
Some(_) => self.rest.push(post),
}
}
pub fn is_empty(&self) -> bool {
self.head.is_none()
}
pub fn len(&self) -> usize {
usize::from(self.head.is_some()) + self.rest.len()
}
pub fn iter(&self) -> impl Iterator<Item = &FieldPost> {
self.head.iter().chain(self.rest.iter())
}
pub fn retain(&mut self, mut keep: impl FnMut(&FieldPost) -> bool) {
if self.head.as_ref().is_some_and(|post| !keep(post)) {
self.head = None;
}
self.rest.retain(|post| keep(post));
if self.head.is_none() && !self.rest.is_empty() {
self.head = Some(self.rest.remove(0));
}
}
pub fn published_mask(&self) -> crate::server::recgbl::EventMask {
self.iter()
.fold(crate::server::recgbl::EventMask::NONE, |acc, (_, _, m)| {
acc | *m
})
}
}
impl Extend<FieldPost> for ProcessSnapshot {
fn extend<I: IntoIterator<Item = FieldPost>>(&mut self, iter: I) {
for post in iter {
self.push(post);
}
}
}
impl FromIterator<FieldPost> for ProcessSnapshot {
fn from_iter<I: IntoIterator<Item = FieldPost>>(iter: I) -> Self {
let mut out = Self::new();
out.extend(iter);
out
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum InputFetchPolicy {
#[default]
ReadAll,
ReadAllGateOnFailure,
AbortOnFirstFailure,
ReadAllGateOnLastFailure,
}
pub fn arg_letter_offset(field: &str, prefix: &str, nargs: u8) -> Option<u8> {
let rest = field.strip_prefix(prefix)?;
let &[c] = rest.as_bytes() else { return None };
if !c.is_ascii_uppercase() {
return None;
}
let n = c - b'A';
(n < nargs).then_some(n)
}
pub fn arg_link_field(prefix: &str, offset: u8) -> String {
format!("{prefix}{}", (b'A' + offset) as char)
}
pub const CALC_CLASS_NARGS: u8 = 21;
pub fn calc_class_link_backed_metadata_field(field: &str) -> Option<String> {
arg_letter_offset(field, "", CALC_CLASS_NARGS)
.or_else(|| arg_letter_offset(field, "L", CALC_CLASS_NARGS))
.map(|n| arg_link_field("INP", n))
}
pub trait FieldDeclaration {
fn field_list(&self) -> &'static [FieldDesc];
fn noaccess_names(&self) -> &'static [&'static str];
fn field_native_count(&self, field: &str) -> Option<u32>;
fn field_is_dbaddr(&self, field: &str) -> bool;
}
impl<R: Record + ?Sized> FieldDeclaration for R {
fn field_list(&self) -> &'static [FieldDesc] {
super::dbd_generated::record_fields(self.record_type())
.unwrap_or_else(|| self.declared_fields())
}
fn noaccess_names(&self) -> &'static [&'static str] {
super::dbd_generated::record_noaccess_fields(self.record_type())
.unwrap_or_else(|| self.declared_noaccess_fields())
}
fn field_native_count(&self, field: &str) -> Option<u32> {
if !self.field_is_dbaddr(field) {
return None;
}
self.dbaddr_capacity(field)
}
fn field_is_dbaddr(&self, field: &str) -> bool {
self.field_list()
.iter()
.any(|d| d.name.eq_ignore_ascii_case(field) && d.special == Special::DbAddr)
|| self
.long_string_fields()
.iter()
.any(|f| f.eq_ignore_ascii_case(field))
}
}
#[derive(Clone, Copy)]
pub struct ResolvedInputLinks<'a> {
multi: &'static [(&'static str, &'static str)],
mask: u64,
pre: &'a [&'static str],
}
impl<'a> ResolvedInputLinks<'a> {
pub(crate) fn new(
multi: &'static [(&'static str, &'static str)],
mask: u64,
pre: &'a [&'static str],
) -> Self {
ResolvedInputLinks { multi, mask, pre }
}
pub fn of_names(names: &'a [&'static str]) -> Self {
ResolvedInputLinks {
multi: &[],
mask: 0,
pre: names,
}
}
pub fn contains(&self, link_field: &str) -> bool {
self.multi_names().any(|name| name == link_field) || self.pre.contains(&link_field)
}
pub fn names(&self) -> impl Iterator<Item = &'static str> + 'a {
self.multi_names().chain(self.pre.iter().copied())
}
fn multi_names(&self) -> impl Iterator<Item = &'static str> + 'a {
let multi = self.multi;
let mut mask = self.mask;
std::iter::from_fn(move || {
if mask == 0 {
return None;
}
let slot = mask.trailing_zeros() as usize;
mask &= mask - 1;
Some(multi[slot].0)
})
}
}
pub trait Record: Send + Sync + 'static {
fn record_type(&self) -> &'static str;
fn process(&mut self) -> CaResult<ProcessOutcome> {
Ok(ProcessOutcome::complete())
}
fn took_metadata_change(&mut self) -> bool {
false
}
fn get_field(&self, name: &str) -> Option<EpicsValue>;
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()>;
fn declared_fields(&self) -> &'static [FieldDesc] {
&[]
}
fn declared_noaccess_fields(&self) -> &'static [&'static str] {
&[]
}
fn implements_field(&self, name: &str) -> bool {
self.get_field(name).is_some()
}
fn field_no_mod(&self, _field: &str) -> bool {
false
}
fn menu_field_choices(&self, _field: &str) -> Option<&'static [&'static str]> {
None
}
fn field_metadata_override(&self, _field: &str) -> Option<FieldMetadataOverride> {
None
}
fn link_backed_metadata_field(&self, _field: &str) -> Option<String> {
None
}
fn property_support(&self) -> crate::server::snapshot::PropertySupport {
default_property_support(self.record_type())
}
fn long_string_fields(&self) -> &'static [&'static str] {
&[]
}
fn process_passive_fields(&self) -> &'static [&'static str] {
super::process_passive::pp_fields_for(self.record_type())
}
fn processes_after_put(&self, field: &str) -> bool {
self.process_passive_fields()
.iter()
.any(|f| f.eq_ignore_ascii_case(field))
}
fn enum_state_strings(&self) -> Option<Vec<PvString>> {
None
}
fn enum_string_form(&self) -> Option<crate::server::snapshot::EnumStringForm> {
None
}
fn validate_put(&self, _field: &str, _value: &EpicsValue) -> CaResult<()> {
Ok(())
}
fn on_put(&mut self, _field: &str) {}
fn is_subroutine_name_field(&self, _field: &str) -> bool {
false
}
fn primary_field(&self) -> &'static str {
"VAL"
}
fn is_udf_defining_put(&self, field: &str) -> bool {
field == self.primary_field()
}
fn val(&self) -> Option<EpicsValue> {
self.get_field(self.primary_field())
}
fn set_val(&mut self, value: EpicsValue) -> CaResult<()> {
let field = self.primary_field();
self.put_field_internal(field, value)
}
fn input_read_by_device_support(&self) -> bool {
true
}
fn soft_input_dset_init(&mut self, loaded: bool) {
let _ = loaded;
}
fn soft_input_read(&mut self, value: Option<EpicsValue>) -> CaResult<()> {
match value {
Some(value) => self.set_val(value),
None => Ok(()),
}
}
fn raw_soft_input(&mut self, entry: RawSoftEntry, value: EpicsValue) -> Option<CaResult<()>> {
let _ = (entry, value);
None
}
fn raw_soft_output_value(&self) -> Option<EpicsValue> {
None
}
fn apply_raw_readback(&mut self, _raw: i32) -> bool {
false
}
fn apply_float64_readback(&mut self, _raw: f64) -> bool {
false
}
fn install_breaktable_registry(
&mut self,
_registry: std::sync::Arc<crate::server::cvt_bpt::BreakTableRegistry>,
) {
}
fn apply_invalid_output_value(&mut self, ivov: EpicsValue) -> CaResult<()> {
self.set_val(ivov)
}
fn can_device_write(&self) -> bool {
matches!(
self.record_type(),
"ao" | "bo"
| "longout"
| "int64out"
| "mbbo"
| "mbboDirect"
| "stringout"
| "lso"
| "printf"
| "aao"
)
}
fn is_put_complete(&self) -> bool {
true
}
fn should_fire_forward_link(&self) -> bool {
true
}
fn restamps_time_after_completion(&self) -> bool {
false
}
fn should_output(&self) -> bool {
true
}
fn after_output_decision(&mut self) {}
fn redecides_after_output(&self) -> bool {
false
}
fn uses_monitor_deadband(&self) -> bool {
true
}
fn process_posts_value_monitor(&self) -> bool {
true
}
fn monitor_value_changed(&mut self) -> Option<bool> {
None
}
fn monitor_always_post(&self) -> (bool, bool) {
(false, false)
}
fn monitor_deadband_value(&self) -> Option<f64> {
self.val().and_then(|v| v.to_f64())
}
fn monitor_deadband_field(&self) -> &'static str {
self.primary_field()
}
fn alarm_cycle_monitored_fields(&self) -> &'static [&'static str] {
&[]
}
fn force_posted_fields(&self) -> &'static [&'static str] {
&[]
}
fn take_cycle_posted_fields(&mut self) -> Vec<(&'static str, CyclePostMask)> {
Vec::new()
}
fn take_first_monitor_cycle(&mut self) -> Vec<(&'static str, CyclePostMask)> {
Vec::new()
}
fn fields_posted_only_when_marked(&self) -> &'static [&'static str] {
&[]
}
fn log_swept_fields(&self) -> &'static [&'static str] {
&[]
}
fn value_only_change_fields(&self) -> &'static [&'static str] {
&[]
}
fn fields_posted_with_value_mask(&self) -> &'static [(&'static str, ValuePostGate)] {
&[]
}
fn take_secondary_value_mask(&mut self) -> crate::server::recgbl::EventMask {
crate::server::recgbl::EventMask::NONE
}
fn take_secondary_value_change(&mut self, field: &str) -> bool {
let _ = field;
false
}
fn fields_posted_with_monitor_mask(&self) -> &'static [&'static str] {
&[]
}
fn fields_posted_without_alarm_bits(&self) -> &'static [&'static str] {
&[]
}
fn array_monitor_post(&mut self) -> Option<ArrayMonitorPost> {
None
}
fn process_posted_fields(&self) -> Option<&'static [&'static str]> {
None
}
fn event_posted_fields(&self) -> &'static [&'static str] {
&[]
}
fn init_record(&mut self, _pass: u8) -> CaResult<()> {
Ok(())
}
fn parks_pact(&self) -> bool {
false
}
fn pact_park_fields(&self) -> &'static [&'static str] {
&[]
}
fn post_init_finalize_undef(&mut self, _udf: &mut bool) -> CaResult<()> {
Ok(())
}
fn init_resets_alarms(&self) -> bool {
false
}
fn dbaddr_capacity(&self, _field: &str) -> Option<u32> {
None
}
fn init_record_tail(&mut self) {}
fn seed_deadband_tracking(&mut self) {
let seed = match self.monitor_deadband_value() {
Some(v) if v.is_finite() => v,
_ => return,
};
for field in ["MLST", "ALST", "LALM"] {
let Some(current) = self.get_field(field) else {
continue;
};
let coerced = EpicsValue::Double(seed).convert_to(current.db_field_type());
let _ = self.put_field(field, coerced);
}
}
fn set_resolved_input_links(&mut self, _resolved: ResolvedInputLinks<'_>) {}
fn set_fetch_gate_failed(&mut self, _failed: bool) {}
fn multi_input_fetch_is_db_get_link(&self) -> bool {
true
}
fn input_links_generation(&self) -> Option<u64> {
None
}
fn link_text_ref(&self, _link_field: &str) -> Option<&str> {
None
}
fn input_link_failure_is_inert(&self, _link_field: &str) -> bool {
false
}
fn monitor_deadband_cells(&self) -> MonitorDeadbandCells {
MonitorDeadbandCells {
mdel: self.get_field("MDEL").and_then(|v| v.to_f64()),
adel: self.get_field("ADEL").and_then(|v| v.to_f64()),
mlst: self.get_field("MLST").and_then(|v| v.to_f64()),
alst: self.get_field("ALST").and_then(|v| v.to_f64()),
}
}
fn store_monitor_last_posted(&mut self, val: f64, mlst: bool, alst: bool) {
for (field, store) in [("ALST", alst), ("MLST", mlst)] {
if !store {
continue;
}
let target = self
.get_field(field)
.map(|v| v.db_field_type())
.unwrap_or(crate::types::DbFieldType::Double);
let _ = self.put_field(field, EpicsValue::Double(val).convert_to(target));
}
}
fn analog_alarm_input(&self) -> Option<AnalogAlarmInput> {
let val = AlarmLimit::from_ladder_value(&self.val()?)?;
Some(AnalogAlarmInput {
val,
hyst: self
.get_field("HYST")
.and_then(|v| AlarmLimit::from_stored(&v)),
lalm: self
.get_field("LALM")
.and_then(|v| AlarmLimit::from_stored(&v)),
})
}
fn alarm_filter_cells(&self) -> Option<(f64, f64)> {
let aftc = self.get_field("AFTC")?.to_f64().unwrap_or(0.0);
let afvl = self
.get_field("AFVL")
.and_then(|v| v.to_f64())
.unwrap_or(0.0);
Some((aftc, afvl))
}
fn store_alarm_filter_value(&mut self, afvl: f64) {
let _ = self.put_field("AFVL", EpicsValue::Double(afvl));
}
fn store_analog_lalm(&mut self, lalm: AlarmLimit) {
let target = self
.get_field("LALM")
.map(|v| v.db_field_type())
.unwrap_or(crate::types::DbFieldType::Double);
let _ = self.put_field("LALM", lalm.to_epics_value().convert_to(target));
}
fn set_input_link_slots(&self) -> Option<(u64, u64)> {
let links = self.multi_input_links();
if links.len() > u64::BITS as usize {
return None;
}
let (mut set, mut unknown) = (0u64, 0u64);
for (slot, (link_field, _)) in links.iter().enumerate() {
match self.link_text_ref(link_field) {
Some("") => {}
Some(_) => set |= 1 << slot,
None => unknown |= 1 << slot,
}
}
Some((set, unknown))
}
fn fetches_dol_closed_loop(&self) -> bool {
false
}
fn closed_loop_dol_read_failed(&mut self) {}
fn soft_input_read_failed(&mut self) {}
fn set_subroutine_status(&mut self, _status: i64) {}
fn special(&mut self, _field: &str, _after: bool) -> CaResult<()> {
Ok(())
}
fn watchdog_interval(&self) -> Option<std::time::Duration> {
None
}
fn watchdog_fire(&mut self) -> &'static [&'static str] {
&[]
}
fn delayed_callback_fire(&mut self, _pact: bool) -> DelayedCallbackOutcome {
DelayedCallbackOutcome::Reprocess
}
fn uses_recgbl_simm_helpers(&self) -> bool {
crate::server::recgbl::simm::record_type_has_sscn(self.record_type())
}
fn aborts_on_failed_siml_read(&self) -> bool {
false
}
fn set_io_intr_scan(&mut self, _active: bool) {}
fn take_special_actions(&mut self) -> Vec<ProcessAction> {
Vec::new()
}
fn take_udf_clear(&mut self) -> bool {
false
}
fn monitor_side_effect_fields(&self, _put_field: &str) -> &'static [&'static str] {
&[]
}
fn special_commits_alarms(&self, _put_field: &str) -> bool {
false
}
fn special_checks_alarms(&self, _put_field: &str) -> bool {
false
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
None
}
fn clears_udf(&self) -> bool {
true
}
fn rederives_udf_on_computed_read(&self) -> bool {
true
}
fn clears_udf_unconditionally(&self) -> bool {
false
}
fn derives_udf_on_read_failure(&self) -> bool {
false
}
fn declares_inp_link(&self) -> bool {
true
}
fn read_constant_inp(&mut self, _value: Option<EpicsValue>) -> bool {
false
}
fn raises_udf_alarm(&self) -> bool {
true
}
fn udf_alarm_on_exact_one(&self) -> bool {
false
}
fn udf_alarm_severity(&self) -> Option<crate::server::record::AlarmSeverity> {
None
}
fn udf_alarm_message(&self) -> &'static str {
""
}
fn value_is_undefined(&self) -> bool {
match self.val() {
Some(EpicsValue::Double(v)) => v.is_nan(),
Some(EpicsValue::Float(v)) => v.is_nan(),
Some(_) => false,
None => true,
}
}
fn check_alarms(&mut self, _common: &mut crate::server::record::CommonFields) {}
fn multi_input_links(&self) -> &'static [(&'static str, &'static str)] {
&[]
}
fn put_multi_input_f64(&mut self, field: &'static str, value: f64) -> CaResult<()> {
self.put_field_internal(field, EpicsValue::Double(value))
}
fn field_slot(&self, _field: &str) -> Option<FieldSlot> {
None
}
fn get_slot_f64(&self, _slot: FieldSlot) -> Option<f64> {
None
}
fn put_slot_f64(&mut self, _slot: FieldSlot, _value: f64) -> bool {
false
}
fn special_reseed_input_links(&self) -> &[(&'static str, &'static str)] {
&[]
}
fn special_reseed_post_mask(&self) -> crate::server::recgbl::EventMask {
crate::server::recgbl::EventMask::VALUE
}
fn constant_init_links(&self) -> Vec<ConstantInitLink> {
Vec::new()
}
fn constant_ls_link(&self) -> Option<&'static str> {
None
}
fn apply_ls_load(&mut self, _load: crate::server::record::LsLoad) -> u32 {
0
}
fn constant_inputs_deliver_at_process(&self) -> bool {
false
}
fn select_input_links(
&self,
_selector: Option<u16>,
) -> Option<&'static [(&'static str, &'static str)]> {
None
}
fn narrows_input_links(&self) -> bool {
false
}
fn simulation_substitutes_input_stage(&self) -> bool {
false
}
fn land_simulated_value(&mut self, value: EpicsValue) -> CaResult<()> {
self.set_val(value)
}
fn rejects_illegal_sim_mode(&self) -> bool {
true
}
fn raises_simm_after_read(&self) -> bool {
false
}
fn set_simulation_active(&mut self, _active: bool) {}
fn input_fetch_policy(&self) -> InputFetchPolicy {
InputFetchPolicy::ReadAll
}
fn string_input_links(&self) -> &'static [(&'static str, &'static str)] {
&[]
}
fn output_time_input_links(&self) -> &'static [(&'static str, &'static str)] {
&[]
}
fn output_link_value(&self) -> Option<EpicsValue> {
self.get_field("OVAL").or_else(|| self.val())
}
fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
&[]
}
fn declares_multi_output_links(&self) -> bool {
false
}
fn multi_output_buffer(
&self,
link_field: &str,
staged: EpicsValue,
target: &OutTarget,
) -> EpicsValue {
let _ = (link_field, target);
staged
}
fn typed_output_buffer(&self, link_field: &str, target: &OutTarget) -> Option<EpicsValue> {
let _ = (link_field, target);
None
}
fn set_resolved_out_target(&mut self, link_field: &str, target: OutTarget) {
let _ = (link_field, target);
}
fn input_link_request(&self, link_field: &str) -> InputLinkRequest {
let _ = link_field;
InputLinkRequest::As(LinkReadAs::Native)
}
fn input_link_answers_fixed_at_type(&self) -> bool {
true
}
fn input_link_read_as_from_source(
&self,
link_field: &str,
source: &OutTarget,
) -> Option<LinkReadAs> {
let _ = (link_field, source);
Some(LinkReadAs::Native)
}
fn output_event(&self) -> Option<String> {
None
}
fn put_field_internal(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
put_field_internal_default(self, name, value)
}
fn pre_process_actions(&mut self) -> ProcessActions {
ProcessActions::new()
}
fn pre_input_link_actions(&mut self) -> ProcessActions {
ProcessActions::new()
}
fn set_process_context(&mut self, _ctx: &ProcessContext) {}
fn set_process_continuation(&mut self, _continuation: bool) {}
fn set_out_link_write_status(
&mut self,
_link_field: &'static str,
_value: &EpicsValue,
_failed: bool,
) {
}
fn set_async_context(&mut self, _name: String, _db: crate::server::database::AsyncDbHandle) {}
fn init_links(&mut self, _common: &crate::server::record::CommonFields) {}
fn set_device_did_compute(&mut self, _did_compute: bool) {}
fn soft_channel_skips_convert(&self) -> bool {
false
}
fn skips_forward_convert_when_undefined(&self) -> bool {
false
}
fn skips_timestamp_when_undefined(&self) -> bool {
false
}
}
pub fn put_value_in_field_shape<R: Record + ?Sized>(
record: &R,
field: &str,
value: EpicsValue,
) -> EpicsValue {
let Some(dest_is_array) = shaped_destination(record, field, &value) else {
return value;
};
match (dest_is_array, value.is_array()) {
(false, true) => value.first_element().unwrap_or(value),
(true, false) if !is_long_string_field(record, field) => {
one_element_buffer(&value).unwrap_or(value)
}
_ => value,
}
}
fn is_long_string_field<R: Record + ?Sized>(record: &R, field: &str) -> bool {
record
.long_string_fields()
.iter()
.any(|f| f.eq_ignore_ascii_case(field))
}
pub fn link_value_in_field_shape<R: Record + ?Sized>(
record: &R,
field: &str,
value: EpicsValue,
) -> EpicsValue {
match shaped_destination(record, field, &value) {
Some(false) if value.is_array() => value.first_element().unwrap_or(value),
_ => value,
}
}
fn shaped_destination<R: Record + ?Sized>(
record: &R,
field: &str,
value: &EpicsValue,
) -> Option<bool> {
let target = record
.get_field(field)
.map(|v| v.db_field_type())
.or_else(|| super::record_instance::declared_field_type_of(record, field));
if matches!(value, EpicsValue::CharArray(_)) && target == Some(DbFieldType::String) {
return None;
}
Some(record.get_field(field).is_some_and(|v| v.is_array()))
}
pub fn dbput_coerce_value<R: Record + ?Sized>(
record: &R,
field: &str,
target: DbFieldType,
value: EpicsValue,
) -> CaResult<Converted> {
let value = put_value_in_field_shape(record, field, value);
coerce_put_value(record, field, target, value)
}
fn one_element_buffer(value: &EpicsValue) -> Option<EpicsValue> {
Some(match value {
EpicsValue::Short(v) => EpicsValue::ShortArray(vec![*v]),
EpicsValue::Float(v) => EpicsValue::FloatArray(vec![*v]),
EpicsValue::Enum(v) => EpicsValue::EnumArray(vec![*v]),
EpicsValue::Double(v) => EpicsValue::DoubleArray(vec![*v]),
EpicsValue::Long(v) => EpicsValue::LongArray(vec![*v]),
EpicsValue::Int64(v) => EpicsValue::Int64Array(vec![*v]),
EpicsValue::UInt64(v) => EpicsValue::UInt64Array(vec![*v]),
EpicsValue::UShort(v) => EpicsValue::UShortArray(vec![*v]),
EpicsValue::ULong(v) => EpicsValue::ULongArray(vec![*v]),
EpicsValue::UChar(v) => EpicsValue::UCharArray(vec![*v]),
EpicsValue::Char(v) => EpicsValue::CharArray(vec![*v]),
EpicsValue::String(v) => EpicsValue::StringArray(vec![v.clone()]),
EpicsValue::EnumWithChoices { .. } => return None,
_ => return None,
})
}
pub fn put_field_internal_default<R: Record + ?Sized>(
record: &mut R,
name: &str,
value: EpicsValue,
) -> CaResult<()> {
let target_type = record
.get_field(name)
.map(|v| v.db_field_type())
.or_else(|| crate::server::record::record_instance::declared_field_type_of(record, name));
let value = link_value_in_field_shape(&*record, name, value);
let is_enum_carrier = matches!(value, EpicsValue::EnumWithChoices { .. });
let value = match target_type {
Some(target)
if is_enum_carrier
|| ((value.db_field_type() != target || target == DbFieldType::String)
&& !value.is_empty_array()) =>
{
match coerce_put_value(record, name, target, value)? {
Converted::Stored(v) => v,
Converted::Unchanged => return Ok(()),
}
}
None if is_enum_carrier => value.convert_to(DbFieldType::Long),
_ => value,
};
record.put_field(name, value)
}
fn put_string_array_row<R: Record + ?Sized>(
record: &R,
field: &str,
target: DbFieldType,
value: &EpicsValue,
) -> CaResult<Option<Converted>> {
let texts: &[crate::types::PvString] = match value {
EpicsValue::String(s) => std::slice::from_ref(s),
EpicsValue::StringArray(a) => a,
_ => return Ok(None),
};
if !record.get_field(field).is_some_and(|v| v.is_array()) {
return Ok(None);
}
if record
.long_string_fields()
.iter()
.any(|f| f.eq_ignore_ascii_case(field))
{
return Ok(Some(Converted::Stored(value.clone())));
}
let Some(numeric) = c_parse::NumericField::of(target) else {
return Ok(Some(Converted::Stored(value.clone())));
};
c_parse::put_string_elements(field, numeric, texts).map(Some)
}
pub fn coerce_put_value<R: Record + ?Sized>(
record: &R,
field: &str,
target: DbFieldType,
value: EpicsValue,
) -> CaResult<Converted> {
if let Some(converted) = put_string_array_row(record, field, target, &value)? {
return Ok(converted);
}
if let EpicsValue::String(s) = &value {
if field.eq_ignore_ascii_case("DTYP") {
let choices = super::merged_device_menu(record.record_type());
if !choices.is_empty() {
return super::resolve_menu_field_string(
field,
&choices,
target,
&s.as_str_lossy(),
)
.map(Converted::Stored);
}
} else if let Some(choices) = super::record_instance::menu_choices_of(record, field) {
return super::resolve_menu_field_string(field, choices, target, &s.as_str_lossy())
.map(Converted::Stored);
}
if target == DbFieldType::Enum {
return super::resolve_enum_state_string(
field,
record.enum_state_strings().as_deref(),
s,
)
.map(Converted::Stored);
}
if let Some(numeric) = c_parse::NumericField::of(target) {
return c_parse::put_string(field, numeric, &s.as_str_lossy());
}
if target == DbFieldType::String {
return Ok(Converted::Stored(EpicsValue::String(
cap_string_to_field_size(record, field, s),
)));
}
}
if target == DbFieldType::String
&& let Some(rendered) =
crate::types::codec::dbr_string_at_precision(&value, put_string_precision(record))
{
return Ok(Converted::Stored(rendered));
}
Ok(Converted::Stored(value.convert_to(target)))
}
fn put_string_precision<R: Record + ?Sized>(record: &R) -> u16 {
match record.record_type() {
"histogram" => 6,
"asyn" => 0,
_ => record
.get_field("PREC")
.and_then(|v| v.as_int_i64())
.map_or(6, |p| p as i16 as u16),
}
}
fn cap_string_to_field_size<R: Record + ?Sized>(record: &R, field: &str, s: &PvString) -> PvString {
match super::record_instance::field_desc_of(record, field) {
Some(desc) if desc.size > 0 => {
let cap = (desc.size as usize).saturating_sub(1);
let bytes = s.as_bytes();
if bytes.len() > cap {
PvString::from_bytes(bytes[..cap].to_vec())
} else {
s.clone()
}
}
_ => s.clone(),
}
}
pub type SubroutineFn = Box<dyn Fn(&mut dyn Record) -> CaResult<i64> + Send + Sync>;
#[cfg(test)]
mod declaration_numbering_tests {
use super::*;
use crate::server::record::dbd_generated::{DB_COMMON_FIELDS, record_fields};
const C_SPECIAL: [(Special, i16, &str); 11] = [
(Special::None, 0, "(none)"),
(Special::NoMod, 1, "SPC_NOMOD"),
(Special::DbAddr, 2, "SPC_DBADDR"),
(Special::Scan, 3, "SPC_SCAN"),
(Special::AlarmAck, 5, "SPC_ALARMACK"),
(Special::As, 6, "SPC_AS"),
(Special::Attribute, 7, "SPC_ATTRIBUTE"),
(Special::Mod, 100, "SPC_MOD"),
(Special::Reset, 101, "SPC_RESET"),
(Special::LinConv, 102, "SPC_LINCONV"),
(Special::Calc, 103, "SPC_CALC"),
];
fn _the_enum_has_no_variant_outside_the_table(s: Special) {
match s {
Special::None
| Special::NoMod
| Special::DbAddr
| Special::Scan
| Special::AlarmAck
| Special::As
| Special::Attribute
| Special::Mod
| Special::Reset
| Special::LinConv
| Special::Calc => (),
}
}
#[test]
fn every_special_carries_cs_spc_number() {
for (spc, n, name) in C_SPECIAL {
assert_eq!(spc as i16, n, "{name}: wrong SPC_ number");
}
for (a, (x, ..)) in C_SPECIAL.iter().enumerate() {
for (y, ..) in C_SPECIAL.iter().skip(a + 1) {
assert_ne!(x, y, "{x:?} listed twice");
}
}
assert!(!C_SPECIAL.iter().any(|(_, n, _)| *n == 4), "C has no SPC 4");
assert_eq!(
C_SPECIAL
.iter()
.filter(|(s, ..)| !matches!(s, Special::None | Special::Attribute))
.count(),
9,
);
}
#[test]
fn the_declared_token_survives_the_generators_collapse() {
let field = |rec: &str, name: &str| {
record_fields(rec)
.unwrap_or_else(|| panic!("no field table for {rec}"))
.iter()
.find(|f| f.name == name)
.unwrap_or_else(|| panic!("{rec}.{name} not in the table"))
};
let sseq_lnk1 = field("sseq", "LNK1");
let fanout_lnk1 = field("fanout", "LNK1");
let ai_inp = field("ai", "INP");
let flnk = DB_COMMON_FIELDS
.iter()
.find(|f| f.name == "FLNK")
.expect("dbCommon.FLNK");
for f in [sseq_lnk1, fanout_lnk1, ai_inp, flnk] {
assert_eq!(f.dbf_type, DbFieldType::String, "{}: served type", f.name);
}
assert_eq!(sseq_lnk1.declared_dbf, DbfCode::Outlink);
assert_eq!(fanout_lnk1.declared_dbf, DbfCode::Fwdlink);
assert_eq!(ai_inp.declared_dbf, DbfCode::Inlink);
assert_eq!(flnk.declared_dbf, DbfCode::Fwdlink);
let dtyp = DB_COMMON_FIELDS
.iter()
.find(|f| f.name == "DTYP")
.expect("dbCommon.DTYP");
let scan = DB_COMMON_FIELDS
.iter()
.find(|f| f.name == "SCAN")
.expect("dbCommon.SCAN");
assert_eq!(dtyp.dbf_type, DbFieldType::Enum);
assert_eq!(dtyp.declared_dbf, DbfCode::Device);
assert_eq!(scan.dbf_type, DbFieldType::Enum);
assert_eq!(scan.declared_dbf, DbfCode::Menu);
}
#[test]
fn no_access_reads_the_declaration_not_the_served_type() {
let val = record_fields("waveform")
.expect("waveform")
.iter()
.find(|f| f.name == "VAL")
.expect("waveform.VAL");
assert_eq!(val.declared_dbf, DbfCode::NoAccess);
assert!(val.no_access());
assert_ne!(val.dbf_type, DbFieldType::String, "re-typed by cvt_dbaddr");
let mbbo_val = record_fields("mbbo")
.expect("mbbo")
.iter()
.find(|f| f.name == "VAL")
.expect("mbbo.VAL");
assert!(!mbbo_val.no_access());
let hand = FieldDesc::new("VAL", DbFieldType::Double, false);
assert_eq!(hand.declared_dbf, DbfCode::Double);
assert!(!hand.no_access());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::records::compress::CompressRecord;
#[test]
fn a_record_that_reads_its_own_link_slots_answers_what_the_name_walk_would() {
use crate::server::record::dbd_generated::RECORD_TYPES;
for record_type in RECORD_TYPES {
let Ok(probe) = crate::server::db_loader::create_record(record_type) else {
continue;
};
let links = probe.multi_input_links();
if links.is_empty() || links.len() > u64::BITS as usize {
continue;
}
let wirings = std::iter::once(Vec::new())
.chain((0..links.len()).map(|i| vec![i]))
.chain(std::iter::once((0..links.len()).collect::<Vec<_>>()));
for wired in wirings {
let Ok(mut record) = crate::server::db_loader::create_record(record_type) else {
continue;
};
for &slot in &wired {
let _ = record.put_field(links[slot].0, EpicsValue::String("SRC".into()));
}
let walked = {
let (mut set, mut unknown) = (0u64, 0u64);
for (slot, (link_field, _)) in links.iter().enumerate() {
match record.link_text_ref(link_field) {
Some("") => {}
Some(_) => set |= 1 << slot,
None => unknown |= 1 << slot,
}
}
Some((set, unknown))
};
assert_eq!(
record.set_input_link_slots(),
walked,
"{record_type}: set_input_link_slots() disagrees with the name walk \
with slots {wired:?} wired"
);
}
}
}
#[test]
fn a_type_that_never_narrows_its_inputs_says_so() {
use crate::server::record::dbd_generated::RECORD_TYPES;
for record_type in RECORD_TYPES {
let Ok(record) = crate::server::db_loader::create_record(record_type) else {
continue;
};
if record.narrows_input_links() {
continue;
}
for selector in std::iter::once(None).chain((0..=20u16).map(Some)) {
assert!(
record.select_input_links(selector).is_none(),
"{record_type}: narrows_input_links() is false but \
select_input_links({selector:?}) narrowed the list"
);
}
}
}
#[test]
fn a_type_that_redecides_after_output_says_so() {
use crate::server::record::dbd_generated::RECORD_TYPES;
for record_type in RECORD_TYPES {
let (Ok(mut ran), Ok(untouched)) = (
crate::server::db_loader::create_record(record_type),
crate::server::db_loader::create_record(record_type),
) else {
continue;
};
let _ = ran.put_field("VAL", EpicsValue::Long(7));
let before = field_dump(&*ran);
ran.after_output_decision();
let changed = field_dump(&*ran) != before;
let diff: Vec<_> = field_dump(&*ran)
.into_iter()
.zip(before.iter())
.filter(|(a, b)| a.1 != b.1)
.map(|(a, b)| (a.0, b.1.clone(), a.1.clone()))
.collect();
assert_eq!(
changed,
untouched.redecides_after_output(),
"{record_type}: after_output_decision changed {diff:?}, but \
redecides_after_output said {}",
untouched.redecides_after_output()
);
}
}
fn field_dump(r: &dyn Record) -> Vec<(&'static str, String)> {
r.field_list()
.iter()
.map(|d| (d.name, format!("{:?}", r.get_field(d.name))))
.collect()
}
#[test]
fn every_record_answers_the_cycle_cells_as_get_field_does() {
use crate::server::record::dbd_generated::RECORD_TYPES;
let f64_of = |r: &dyn Record, field: &str| r.get_field(field).and_then(|v| v.to_f64());
for record_type in RECORD_TYPES {
let Ok(mut r) = crate::server::db_loader::create_record(record_type) else {
continue;
};
seed_cycle_cells(&mut *r);
let r = &*r;
let deadband = r.monitor_deadband_cells();
assert_eq!(deadband.mdel, f64_of(r, "MDEL"), "{record_type} MDEL");
assert_eq!(deadband.adel, f64_of(r, "ADEL"), "{record_type} ADEL");
assert_eq!(deadband.mlst, f64_of(r, "MLST"), "{record_type} MLST");
assert_eq!(deadband.alst, f64_of(r, "ALST"), "{record_type} ALST");
let ladder = r
.val()
.and_then(|v| AlarmLimit::from_ladder_value(&v))
.map(|val| AnalogAlarmInput {
val,
hyst: r
.get_field("HYST")
.and_then(|v| AlarmLimit::from_stored(&v)),
lalm: r
.get_field("LALM")
.and_then(|v| AlarmLimit::from_stored(&v)),
});
assert_eq!(
r.analog_alarm_input(),
ladder,
"{record_type} analog_alarm_input"
);
let filter = r.get_field("AFTC").map(|aftc| {
(
aftc.to_f64().unwrap_or(0.0),
f64_of(r, "AFVL").unwrap_or(0.0),
)
});
assert_eq!(r.alarm_filter_cells(), filter, "{record_type} AFTC/AFVL");
}
}
#[test]
fn every_record_stores_the_cycle_cells_as_put_field_does() {
use crate::server::record::dbd_generated::RECORD_TYPES;
for record_type in RECORD_TYPES {
let (Ok(mut fast), Ok(mut slow)) = (
crate::server::db_loader::create_record(record_type),
crate::server::db_loader::create_record(record_type),
) else {
continue;
};
seed_cycle_cells(&mut *fast);
seed_cycle_cells(&mut *slow);
fast.store_monitor_last_posted(23.0, true, true);
for field in ["ALST", "MLST"] {
let target = slow
.get_field(field)
.map(|v| v.db_field_type())
.unwrap_or(crate::types::DbFieldType::Double);
let _ = slow.put_field(field, EpicsValue::Double(23.0).convert_to(target));
}
fast.store_alarm_filter_value(29.0);
let _ = slow.put_field("AFVL", EpicsValue::Double(29.0));
fast.store_analog_lalm(AlarmLimit::Double(31.0));
let target = slow
.get_field("LALM")
.map(|v| v.db_field_type())
.unwrap_or(crate::types::DbFieldType::Double);
let _ = slow.put_field("LALM", EpicsValue::Double(31.0).convert_to(target));
for field in ["MLST", "ALST", "AFVL", "LALM"] {
assert_eq!(
fast.get_field(field),
slow.get_field(field),
"{record_type} {field}"
);
}
}
}
#[test]
fn a_speed_override_of_val_answers_what_the_default_would() {
const SPEED_OVERRIDES: &[&str] = &["calc"];
for record_type in SPEED_OVERRIDES {
let mut r = crate::server::db_loader::create_record(record_type)
.expect("a listed type is creatable");
seed_cycle_cells(&mut *r);
let _ = r.put_field("VAL", EpicsValue::Double(41.0));
let r = &*r;
assert_eq!(r.val(), r.get_field(r.primary_field()), "{record_type} val");
assert_eq!(
r.output_link_value(),
r.get_field("OVAL").or_else(|| r.val()),
"{record_type} output_link_value"
);
}
}
fn seed_cycle_cells(r: &mut dyn Record) {
let cells = [
"MDEL", "ADEL", "MLST", "ALST", "HYST", "LALM", "AFTC", "AFVL", "OVAL",
];
for (i, field) in cells.iter().enumerate() {
let Some(target) = r.get_field(field).map(|v| v.db_field_type()) else {
continue;
};
let _ = r.put_field(
field,
EpicsValue::Double(11.0 + i as f64).convert_to(target),
);
}
}
#[test]
fn a_one_sample_link_delivery_is_not_a_one_element_buffer() {
let mut rec = CompressRecord::new(4, 0);
rec.put_field("N", EpicsValue::Long(0)).unwrap();
assert_eq!(rec.get_field("N"), Some(EpicsValue::ULong(0)));
put_field_internal_default(&mut rec, "VAL", EpicsValue::Long(5)).unwrap();
assert_eq!(
rec.get_field("N"),
Some(EpicsValue::ULong(0)),
"compressRecord.c:273-304 never touches prec->n"
);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnalogAlarmInput {
pub val: AlarmLimit,
pub hyst: Option<AlarmLimit>,
pub lalm: Option<AlarmLimit>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MonitorDeadbandCells {
pub mdel: Option<f64>,
pub adel: Option<f64>,
pub mlst: Option<f64>,
pub alst: Option<f64>,
}