use crate::error::{CaError, CaResult};
use crate::server::record::{
FieldDesc, MENU_YES_NO, ParsedLink, ProcessAction, Record, parse_link_v2,
};
use crate::types::{DbFieldType, EpicsValue, PvString};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayKind {
Waveform,
Aai,
Aao,
SubArray,
}
impl ArrayKind {
pub fn as_record_type(self) -> &'static str {
match self {
Self::Waveform => "waveform",
Self::Aai => "aai",
Self::Aao => "aao",
Self::SubArray => "subArray",
}
}
pub fn is_output(self) -> bool {
matches!(self, Self::Aao)
}
}
pub struct WaveformRecord {
pub kind: ArrayKind,
pub val: EpicsValue,
pub nelm: i32,
pub nord: i32,
pub ftvl: i16,
pub mpst: i16, pub apst: i16, pub hash: u32, pub busy: bool,
pub rarm: i16,
pub egu: PvString,
pub hopr: f64,
pub lopr: f64,
pub prec: i16,
pub indx: i32,
pub malm: i32,
pub simm: i16,
pub siml: String,
pub siol: String,
pub sims: i16,
pub oldsimm: i16,
pub omsl: i16,
pub dol: String,
}
pub type AaiRecord = WaveformRecord;
pub type AaoRecord = WaveformRecord;
pub type SubArrayRecord = WaveformRecord;
const MENU_FTYPE_DOUBLE: i16 = 10;
const WAVEFORM_POST: &[&str] = &["Always", "On Change"];
const WAVEFORM_POST_ALWAYS: i16 = 0;
const WAVEFORM_POST_ONCHANGE: i16 = 1;
const MAX_STRING_SIZE: usize = 40;
fn epics_mem_hash(bytes: &[u8], seed: u32) -> u32 {
let mut hash = seed;
for (i, &b) in bytes.iter().enumerate() {
let c = b as i8 as u32;
if i % 2 == 0 {
hash ^= !((hash << 11) ^ c ^ (hash >> 5));
} else {
hash ^= (hash << 7) ^ c ^ (hash >> 3);
}
}
hash
}
impl Default for WaveformRecord {
fn default() -> Self {
Self {
kind: ArrayKind::Waveform,
val: EpicsValue::DoubleArray(Vec::new()),
nelm: 1,
nord: 0,
ftvl: MENU_FTYPE_DOUBLE,
mpst: 0,
apst: 0,
hash: 0,
busy: false,
rarm: 0,
egu: PvString::new(),
hopr: 0.0,
lopr: 0.0,
prec: 0,
indx: 0,
malm: 1,
simm: 0,
siml: String::new(),
siol: String::new(),
sims: 0,
oldsimm: 0,
omsl: 0,
dol: String::new(),
}
}
}
impl WaveformRecord {
pub fn with_kind(kind: ArrayKind) -> Self {
Self {
kind,
..Default::default()
}
}
fn has_sim_block(&self) -> bool {
!matches!(self.kind, ArrayKind::SubArray)
}
}
impl WaveformRecord {
pub fn new(nelm: i32, ftvl: DbFieldType) -> Self {
let (val, ftvl_idx) = match ftvl {
DbFieldType::Char => (EpicsValue::CharArray(vec![0; nelm as usize]), 1), DbFieldType::UChar => (EpicsValue::UCharArray(vec![0; nelm as usize]), 2), DbFieldType::Short => (EpicsValue::ShortArray(vec![0; nelm as usize]), 3), DbFieldType::Long => (EpicsValue::LongArray(vec![0; nelm as usize]), 5), DbFieldType::Int64 => (EpicsValue::Int64Array(vec![0; nelm as usize]), 7), DbFieldType::UInt64 => (EpicsValue::UInt64Array(vec![0; nelm as usize]), 8), DbFieldType::Float => (EpicsValue::FloatArray(vec![0.0; nelm as usize]), 9), DbFieldType::Double => (EpicsValue::DoubleArray(vec![0.0; nelm as usize]), 10), _ => (EpicsValue::DoubleArray(vec![0.0; nelm as usize]), 10),
};
Self {
val,
nelm,
nord: 0,
ftvl: ftvl_idx,
..Default::default()
}
}
fn reallocate_val(&mut self) {
let n = self.nelm.max(0) as usize;
self.val = match self.ftvl {
1 => EpicsValue::CharArray(vec![0; n]), 2 => EpicsValue::UCharArray(vec![0; n]), 3 | 4 => EpicsValue::ShortArray(vec![0; n]), 5 | 6 => EpicsValue::LongArray(vec![0; n]), 7 => EpicsValue::Int64Array(vec![0; n]), 8 => EpicsValue::UInt64Array(vec![0; n]), 9 => EpicsValue::FloatArray(vec![0.0; n]), _ => EpicsValue::DoubleArray(vec![0.0; n]), };
self.nord = 0;
}
fn resize_val_preserving(&mut self) {
let n = self.nelm.max(0) as usize;
match &mut self.val {
EpicsValue::CharArray(v) => v.resize(n, 0),
EpicsValue::UCharArray(v) => v.resize(n, 0),
EpicsValue::ShortArray(v) => v.resize(n, 0),
EpicsValue::LongArray(v) => v.resize(n, 0),
EpicsValue::Int64Array(v) => v.resize(n, 0),
EpicsValue::UInt64Array(v) => v.resize(n, 0),
EpicsValue::FloatArray(v) => v.resize(n, 0.0),
EpicsValue::DoubleArray(v) => v.resize(n, 0.0),
EpicsValue::EnumArray(v) => v.resize(n, 0),
EpicsValue::StringArray(v) => v.resize(n, PvString::new()),
_ => {
self.reallocate_val();
return;
}
}
if (self.nord as usize) > n {
self.nord = n as i32;
}
}
fn array_content_bytes(&self) -> Vec<u8> {
let n = self.nord.max(0) as usize;
let mut out = Vec::new();
match &self.val {
EpicsValue::CharArray(v) => out.extend(v.iter().take(n).copied()),
EpicsValue::ShortArray(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::UShortArray(v) | EpicsValue::EnumArray(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::LongArray(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::ULongArray(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::FloatArray(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::DoubleArray(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::Int64Array(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::UInt64Array(v) => {
for x in v.iter().take(n) {
out.extend_from_slice(&x.to_le_bytes());
}
}
EpicsValue::StringArray(v) => {
for s in v.iter().take(n) {
let mut slot = [0u8; MAX_STRING_SIZE];
let bytes = s.as_bytes();
let copy = bytes.len().min(MAX_STRING_SIZE - 1);
slot[..copy].copy_from_slice(&bytes[..copy]);
out.extend_from_slice(&slot);
}
}
_ => {}
}
out
}
}
static WAVEFORM_FIELDS_CHAR: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Char,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
static WAVEFORM_FIELDS_SHORT: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
static WAVEFORM_FIELDS_LONG: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Long,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
static WAVEFORM_FIELDS_INT64: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Int64,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
static WAVEFORM_FIELDS_UINT64: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::UInt64,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
static WAVEFORM_FIELDS_FLOAT: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Float,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
static WAVEFORM_FIELDS_DOUBLE: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "RARM",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "BUSY",
dbf_type: DbFieldType::Short,
read_only: true,
},
];
macro_rules! subarray_field_list {
($valty:expr) => {
&[
FieldDesc {
name: "VAL",
dbf_type: $valty,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: false,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "MALM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "INDX",
dbf_type: DbFieldType::Long,
read_only: false,
},
]
};
}
static SUBARRAY_FIELDS_CHAR: &[FieldDesc] = subarray_field_list!(DbFieldType::Char);
static SUBARRAY_FIELDS_SHORT: &[FieldDesc] = subarray_field_list!(DbFieldType::Short);
static SUBARRAY_FIELDS_LONG: &[FieldDesc] = subarray_field_list!(DbFieldType::Long);
static SUBARRAY_FIELDS_INT64: &[FieldDesc] = subarray_field_list!(DbFieldType::Int64);
static SUBARRAY_FIELDS_UINT64: &[FieldDesc] = subarray_field_list!(DbFieldType::UInt64);
static SUBARRAY_FIELDS_FLOAT: &[FieldDesc] = subarray_field_list!(DbFieldType::Float);
static SUBARRAY_FIELDS_DOUBLE: &[FieldDesc] = subarray_field_list!(DbFieldType::Double);
const MENU_OMSL_CLOSED_LOOP: i16 = 1;
fn dol_is_constant(dol: &str) -> bool {
let trimmed = dol.trim();
if trimmed.is_empty() {
return true;
}
if matches!(parse_link_v2(dol), ParsedLink::Constant(_)) {
return true;
}
trimmed
.split_whitespace()
.all(|tok| tok.parse::<f64>().is_ok())
}
macro_rules! aao_field_list {
($valty:expr) => {
&[
FieldDesc {
name: "VAL",
dbf_type: $valty,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "OMSL",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "DOL",
dbf_type: DbFieldType::String,
read_only: false,
},
]
};
}
static AAO_FIELDS_CHAR: &[FieldDesc] = aao_field_list!(DbFieldType::Char);
static AAO_FIELDS_SHORT: &[FieldDesc] = aao_field_list!(DbFieldType::Short);
static AAO_FIELDS_LONG: &[FieldDesc] = aao_field_list!(DbFieldType::Long);
static AAO_FIELDS_INT64: &[FieldDesc] = aao_field_list!(DbFieldType::Int64);
static AAO_FIELDS_UINT64: &[FieldDesc] = aao_field_list!(DbFieldType::UInt64);
static AAO_FIELDS_FLOAT: &[FieldDesc] = aao_field_list!(DbFieldType::Float);
static AAO_FIELDS_DOUBLE: &[FieldDesc] = aao_field_list!(DbFieldType::Double);
macro_rules! aai_field_list {
($valty:expr) => {
&[
FieldDesc {
name: "VAL",
dbf_type: $valty,
read_only: false,
},
FieldDesc {
name: "NELM",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "NORD",
dbf_type: DbFieldType::Long,
read_only: true,
},
FieldDesc {
name: "FTVL",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "EGU",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
]
};
}
static AAI_FIELDS_CHAR: &[FieldDesc] = aai_field_list!(DbFieldType::Char);
static AAI_FIELDS_SHORT: &[FieldDesc] = aai_field_list!(DbFieldType::Short);
static AAI_FIELDS_LONG: &[FieldDesc] = aai_field_list!(DbFieldType::Long);
static AAI_FIELDS_INT64: &[FieldDesc] = aai_field_list!(DbFieldType::Int64);
static AAI_FIELDS_UINT64: &[FieldDesc] = aai_field_list!(DbFieldType::UInt64);
static AAI_FIELDS_FLOAT: &[FieldDesc] = aai_field_list!(DbFieldType::Float);
static AAI_FIELDS_DOUBLE: &[FieldDesc] = aai_field_list!(DbFieldType::Double);
impl Record for WaveformRecord {
fn record_type(&self) -> &'static str {
self.kind.as_record_type()
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
fn init_record(&mut self, pass: u8) -> CaResult<()> {
if pass == 0 && matches!(self.kind, ArrayKind::SubArray) {
if self.malm < 1 {
self.malm = 1;
}
if self.nelm > self.malm {
self.nelm = self.malm;
self.reallocate_val();
}
}
Ok(())
}
fn can_device_write(&self) -> bool {
self.kind.is_output()
}
fn menu_field_choices(&self, field: &str) -> Option<&'static [&'static str]> {
match field {
"MPST" | "APST" => Some(WAVEFORM_POST),
"SIMM" if self.has_sim_block() => Some(MENU_YES_NO),
_ => None,
}
}
fn array_monitor_post(&mut self) -> Option<crate::server::record::ArrayMonitorPost> {
if matches!(self.kind, ArrayKind::SubArray) {
return None;
}
let mut post_value = self.mpst == WAVEFORM_POST_ALWAYS;
let mut post_archive = self.apst == WAVEFORM_POST_ALWAYS;
let mut hash_changed = false;
if self.mpst == WAVEFORM_POST_ONCHANGE || self.apst == WAVEFORM_POST_ONCHANGE {
let h = epics_mem_hash(&self.array_content_bytes(), 0);
if h != self.hash {
self.hash = h;
hash_changed = true;
if self.mpst == WAVEFORM_POST_ONCHANGE {
post_value = true;
}
if self.apst == WAVEFORM_POST_ONCHANGE {
post_archive = true;
}
}
}
Some(crate::server::record::ArrayMonitorPost {
post_value,
post_archive,
hash_changed,
})
}
fn event_posted_fields(&self) -> &'static [&'static str] {
if matches!(self.kind, ArrayKind::SubArray) {
&[]
} else {
&["HASH"]
}
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => {
let mut val = self.val.clone();
val.truncate(self.nord.max(0) as usize);
Some(val)
}
"NELM" => Some(EpicsValue::Long(self.nelm)),
"NORD" => Some(EpicsValue::Long(self.nord)),
"FTVL" => Some(EpicsValue::Short(self.ftvl)),
"MPST" => Some(EpicsValue::Short(self.mpst)),
"APST" => Some(EpicsValue::Short(self.apst)),
"HASH" if !matches!(self.kind, ArrayKind::SubArray) => {
Some(EpicsValue::ULong(self.hash))
}
"INDX" if matches!(self.kind, ArrayKind::SubArray) => Some(EpicsValue::Long(self.indx)),
"MALM" if matches!(self.kind, ArrayKind::SubArray) => Some(EpicsValue::Long(self.malm)),
"RARM" if matches!(self.kind, ArrayKind::Waveform) => {
Some(EpicsValue::Short(self.rarm))
}
"BUSY" if matches!(self.kind, ArrayKind::Waveform) => {
Some(EpicsValue::Short(self.busy as i16))
}
"EGU" => Some(EpicsValue::String(self.egu.clone())),
"HOPR" => Some(EpicsValue::Double(self.hopr)),
"LOPR" => Some(EpicsValue::Double(self.lopr)),
"PREC" => Some(EpicsValue::Short(self.prec)),
"SIMM" if self.has_sim_block() => Some(EpicsValue::Short(self.simm)),
"SIML" if self.has_sim_block() => Some(EpicsValue::String(self.siml.clone().into())),
"SIOL" if self.has_sim_block() => Some(EpicsValue::String(self.siol.clone().into())),
"SIMS" if self.has_sim_block() => Some(EpicsValue::Short(self.sims)),
"OLDSIMM" if self.has_sim_block() => Some(EpicsValue::Short(self.oldsimm)),
"OMSL" if matches!(self.kind, ArrayKind::Aao) => Some(EpicsValue::Short(self.omsl)),
"DOL" if matches!(self.kind, ArrayKind::Aao) => {
Some(EpicsValue::String(self.dol.clone().into()))
}
_ => None,
}
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"VAL" => {
let value = match (&value, self.ftvl) {
(EpicsValue::String(s), 1) => EpicsValue::CharArray(s.as_bytes().to_vec()),
(EpicsValue::String(s), 2) => EpicsValue::UCharArray(s.as_bytes().to_vec()),
_ => value,
};
let nelm = self.nelm.max(0) as usize;
match value {
EpicsValue::CharArray(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0);
self.val = EpicsValue::CharArray(arr);
}
EpicsValue::UCharArray(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0);
self.val = EpicsValue::UCharArray(arr);
}
EpicsValue::ShortArray(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0);
self.val = EpicsValue::ShortArray(arr);
}
EpicsValue::LongArray(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0);
self.val = EpicsValue::LongArray(arr);
}
EpicsValue::Int64Array(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0);
self.val = EpicsValue::Int64Array(arr);
}
EpicsValue::UInt64Array(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0);
self.val = EpicsValue::UInt64Array(arr);
}
EpicsValue::FloatArray(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0.0);
self.val = EpicsValue::FloatArray(arr);
}
EpicsValue::DoubleArray(mut arr) => {
self.nord = arr.len().min(nelm) as i32;
arr.resize(nelm, 0.0);
self.val = EpicsValue::DoubleArray(arr);
}
other => {
self.nord = 1;
self.val = other;
}
}
Ok(())
}
"NELM" => {
if let EpicsValue::Long(n) = value {
if n <= 0 {
return Err(CaError::InvalidValue(format!(
"NELM must be positive, got {n}"
)));
}
if matches!(self.kind, ArrayKind::SubArray) {
self.nelm = n;
self.reallocate_val();
} else {
self.nelm = n;
self.resize_val_preserving();
}
Ok(())
} else {
Err(CaError::InvalidValue(format!(
"NELM requires Long, got {value:?}"
)))
}
}
"FTVL" => {
if let EpicsValue::Short(v) = value {
self.ftvl = v;
self.reallocate_val();
Ok(())
} else {
Err(CaError::InvalidValue(format!(
"FTVL requires Short, got {value:?}"
)))
}
}
"MPST" => {
if let EpicsValue::Short(v) = value {
self.mpst = v;
Ok(())
} else {
Err(CaError::TypeMismatch("MPST".into()))
}
}
"APST" => {
if let EpicsValue::Short(v) = value {
self.apst = v;
Ok(())
} else {
Err(CaError::TypeMismatch("APST".into()))
}
}
"NORD" => Err(CaError::ReadOnlyField(name.to_string())),
"INDX" if matches!(self.kind, ArrayKind::SubArray) => {
let v = match value {
EpicsValue::Long(v) => v,
EpicsValue::Short(v) => v as i32,
_ => return Err(CaError::TypeMismatch("INDX".into())),
};
self.indx = v.max(0);
Ok(())
}
"MALM" if matches!(self.kind, ArrayKind::SubArray) => {
let v = match value {
EpicsValue::Long(v) => v,
EpicsValue::Short(v) => v as i32,
_ => return Err(CaError::TypeMismatch("MALM".into())),
};
self.malm = v.max(1);
Ok(())
}
"RARM" if matches!(self.kind, ArrayKind::Waveform) => {
let v = match value {
EpicsValue::Short(v) => v,
EpicsValue::Long(v) => v as i16,
_ => return Err(CaError::TypeMismatch("RARM".into())),
};
self.rarm = v;
Ok(())
}
"BUSY" if matches!(self.kind, ArrayKind::Waveform) => {
Err(CaError::ReadOnlyField(name.to_string()))
}
"EGU" => {
if let EpicsValue::String(s) = value {
self.egu = s;
Ok(())
} else {
Err(CaError::TypeMismatch("EGU".into()))
}
}
"HOPR" => {
self.hopr = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("HOPR".into()))?;
Ok(())
}
"LOPR" => {
self.lopr = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("LOPR".into()))?;
Ok(())
}
"PREC" => {
self.prec = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("PREC".into()))?
as i16;
Ok(())
}
"SIMM" if self.has_sim_block() => match value {
EpicsValue::Short(v) => {
self.simm = v;
Ok(())
}
_ => Err(CaError::TypeMismatch("SIMM".into())),
},
"SIML" if self.has_sim_block() => match value {
EpicsValue::String(s) => {
self.siml = s.as_str_lossy().into_owned();
Ok(())
}
_ => Err(CaError::TypeMismatch("SIML".into())),
},
"SIOL" if self.has_sim_block() => match value {
EpicsValue::String(s) => {
self.siol = s.as_str_lossy().into_owned();
Ok(())
}
_ => Err(CaError::TypeMismatch("SIOL".into())),
},
"SIMS" if self.has_sim_block() => match value {
EpicsValue::Short(v) => {
self.sims = v;
Ok(())
}
_ => Err(CaError::TypeMismatch("SIMS".into())),
},
"OLDSIMM" if self.has_sim_block() => Err(CaError::ReadOnlyField(name.to_string())),
"OMSL" if matches!(self.kind, ArrayKind::Aao) => match value {
EpicsValue::Short(v) => {
self.omsl = v;
Ok(())
}
_ => Err(CaError::TypeMismatch("OMSL".into())),
},
"DOL" if matches!(self.kind, ArrayKind::Aao) => match value {
EpicsValue::String(s) => {
self.dol = s.as_str_lossy().into_owned();
Ok(())
}
_ => Err(CaError::TypeMismatch("DOL".into())),
},
_ => Err(CaError::FieldNotFound(name.to_string())),
}
}
fn field_list(&self) -> &'static [FieldDesc] {
if matches!(self.kind, ArrayKind::SubArray) {
return match self.ftvl {
1 | 2 => SUBARRAY_FIELDS_CHAR,
3 | 4 => SUBARRAY_FIELDS_SHORT,
5 | 6 => SUBARRAY_FIELDS_LONG,
7 => SUBARRAY_FIELDS_INT64,
8 => SUBARRAY_FIELDS_UINT64,
9 => SUBARRAY_FIELDS_FLOAT,
_ => SUBARRAY_FIELDS_DOUBLE,
};
}
if matches!(self.kind, ArrayKind::Aao) {
return match self.ftvl {
1 | 2 => AAO_FIELDS_CHAR,
3 | 4 => AAO_FIELDS_SHORT,
5 | 6 => AAO_FIELDS_LONG,
7 => AAO_FIELDS_INT64,
8 => AAO_FIELDS_UINT64,
9 => AAO_FIELDS_FLOAT,
_ => AAO_FIELDS_DOUBLE,
};
}
if matches!(self.kind, ArrayKind::Aai) {
return match self.ftvl {
1 | 2 => AAI_FIELDS_CHAR,
3 | 4 => AAI_FIELDS_SHORT,
5 | 6 => AAI_FIELDS_LONG,
7 => AAI_FIELDS_INT64,
8 => AAI_FIELDS_UINT64,
9 => AAI_FIELDS_FLOAT,
_ => AAI_FIELDS_DOUBLE,
};
}
match self.ftvl {
1 | 2 => WAVEFORM_FIELDS_CHAR,
3 | 4 => WAVEFORM_FIELDS_SHORT,
5 | 6 => WAVEFORM_FIELDS_LONG,
7 => WAVEFORM_FIELDS_INT64,
8 => WAVEFORM_FIELDS_UINT64,
9 => WAVEFORM_FIELDS_FLOAT,
_ => WAVEFORM_FIELDS_DOUBLE,
}
}
fn pre_input_link_actions(&mut self) -> Vec<ProcessAction> {
if !matches!(self.kind, ArrayKind::Aao) || self.omsl != MENU_OMSL_CLOSED_LOOP {
return Vec::new();
}
if dol_is_constant(&self.dol) {
return Vec::new();
}
vec![ProcessAction::ReadDbLink {
link_field: "DOL",
target_field: "VAL",
}]
}
fn set_val(&mut self, value: EpicsValue) -> CaResult<()> {
if !matches!(self.kind, ArrayKind::SubArray) {
return match self.put_field("VAL", value.clone()) {
Ok(()) => Ok(()),
Err(CaError::TypeMismatch(_)) => {
let target = self
.get_field("VAL")
.map(|v| v.db_field_type())
.unwrap_or(DbFieldType::Double);
let coerced = value.convert_to(target);
self.put_field("VAL", coerced)
}
Err(e) => Err(e),
};
}
let malm = self.malm.max(1);
if self.nelm > malm {
self.nelm = malm;
}
if self.indx >= malm {
self.indx = malm - 1;
}
let start = self.indx.max(0) as usize;
let take = self.nelm.max(0) as usize;
let malm_cap = malm as usize; let nelm_buf = take; macro_rules! slice {
($v:ident, $arr:ident, $variant:ident, $zero:expr) => {{
let src_len = $arr.len().min(malm_cap);
let end = (start + take).min(src_len);
let valid = if start >= src_len { 0 } else { end - start };
let mut out: Vec<_> = if valid > 0 {
$arr[start..end].to_vec()
} else {
Vec::new()
};
out.resize(nelm_buf, $zero);
self.nord = valid as i32;
self.val = EpicsValue::$variant(out);
}};
}
match value {
EpicsValue::CharArray(arr) => slice!(value, arr, CharArray, 0u8),
EpicsValue::UCharArray(arr) => slice!(value, arr, UCharArray, 0u8),
EpicsValue::ShortArray(arr) => slice!(value, arr, ShortArray, 0i16),
EpicsValue::LongArray(arr) => slice!(value, arr, LongArray, 0i32),
EpicsValue::FloatArray(arr) => slice!(value, arr, FloatArray, 0.0f32),
EpicsValue::DoubleArray(arr) => slice!(value, arr, DoubleArray, 0.0f64),
other => {
if start == 0 {
self.nord = 1;
self.val = other;
} else {
self.nord = 0;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod array_kind_tests {
use super::*;
#[test]
fn epics_mem_hash_matches_c_reference_vectors() {
let mut da = Vec::new();
da.extend_from_slice(&1.0f64.to_le_bytes());
da.extend_from_slice(&2.0f64.to_le_bytes());
assert_eq!(epics_mem_hash(&da, 0), 0xa23a_aba6);
let mut la = Vec::new();
for x in [1i32, 2, 3] {
la.extend_from_slice(&x.to_le_bytes());
}
assert_eq!(epics_mem_hash(&la, 0), 0x3429_76d1);
assert_eq!(epics_mem_hash(&[0x00, 0x80, 0xFF, 0x7F], 0), 0x7be0_007f);
assert_eq!(epics_mem_hash(&[0xAA, 0xBB, 0xCC], 0), 0x06ab_0bfc);
assert_eq!(epics_mem_hash(&[], 0), 0);
}
#[test]
fn waveform_default_kind() {
let r = WaveformRecord::default();
assert_eq!(r.record_type(), "waveform");
assert!(!r.can_device_write(), "waveform is input-only");
}
#[test]
fn aai_record_type_and_input() {
let r = WaveformRecord::with_kind(ArrayKind::Aai);
assert_eq!(r.record_type(), "aai");
assert!(!r.can_device_write(), "aai is input");
}
#[test]
fn aao_is_output() {
let r = WaveformRecord::with_kind(ArrayKind::Aao);
assert_eq!(r.record_type(), "aao");
assert!(r.can_device_write(), "aao must take the device-write path");
}
#[test]
fn sub_array_record_type() {
let r = WaveformRecord::with_kind(ArrayKind::SubArray);
assert_eq!(r.record_type(), "subArray");
assert!(!r.can_device_write(), "subArray is input");
}
#[test]
fn aliases_resolve_to_waveform_record() {
let a: AaiRecord = WaveformRecord::with_kind(ArrayKind::Aai);
let b: AaoRecord = WaveformRecord::with_kind(ArrayKind::Aao);
let c: SubArrayRecord = WaveformRecord::with_kind(ArrayKind::SubArray);
assert_eq!(a.record_type(), "aai");
assert_eq!(b.record_type(), "aao");
assert_eq!(c.record_type(), "subArray");
}
#[test]
fn aao_field_list_includes_omsl_dol_other_kinds_do_not() {
let aao = WaveformRecord::with_kind(ArrayKind::Aao);
let names: Vec<&str> = aao.field_list().iter().map(|f| f.name).collect();
assert!(names.contains(&"OMSL"), "aao field_list must carry OMSL");
assert!(names.contains(&"DOL"), "aao field_list must carry DOL");
for kind in [ArrayKind::Waveform, ArrayKind::Aai, ArrayKind::SubArray] {
let r = WaveformRecord::with_kind(kind);
let names: Vec<&str> = r.field_list().iter().map(|f| f.name).collect();
assert!(
!names.contains(&"OMSL") && !names.contains(&"DOL"),
"{kind:?} must not declare OMSL/DOL"
);
}
}
#[test]
fn waveform_rarm_busy_fields_waveform_only() {
let wf = WaveformRecord::with_kind(ArrayKind::Waveform);
let rarm = wf
.field_list()
.iter()
.find(|f| f.name == "RARM")
.expect("waveform field_list must carry RARM");
let busy = wf
.field_list()
.iter()
.find(|f| f.name == "BUSY")
.expect("waveform field_list must carry BUSY");
assert!(!rarm.read_only, "RARM is pp(TRUE) — client-settable");
assert!(busy.read_only, "BUSY is special(SPC_NOMOD) — read-only");
let mut wf = WaveformRecord::with_kind(ArrayKind::Waveform);
wf.put_field("RARM", EpicsValue::Short(1)).unwrap();
assert_eq!(wf.get_field("RARM"), Some(EpicsValue::Short(1)));
assert_eq!(wf.get_field("BUSY"), Some(EpicsValue::Short(0)));
assert!(
wf.put_field("BUSY", EpicsValue::Short(1)).is_err(),
"BUSY must reject CA puts (SPC_NOMOD)"
);
wf.busy = true;
assert_eq!(wf.get_field("BUSY"), Some(EpicsValue::Short(1)));
for kind in [ArrayKind::Aai, ArrayKind::Aao, ArrayKind::SubArray] {
let mut r = WaveformRecord::with_kind(kind);
let names: Vec<&str> = r.field_list().iter().map(|f| f.name).collect();
assert!(
!names.contains(&"RARM") && !names.contains(&"BUSY"),
"{kind:?} must not declare RARM/BUSY"
);
assert_eq!(r.get_field("RARM"), None, "{kind:?} RARM get must be None");
assert_eq!(r.get_field("BUSY"), None, "{kind:?} BUSY get must be None");
assert!(r.put_field("RARM", EpicsValue::Short(1)).is_err());
}
}
#[test]
fn waveform_as_any_mut_downcasts_to_concrete_record() {
let mut r: Box<dyn Record> = Box::new(WaveformRecord::with_kind(ArrayKind::Waveform));
let wf = r
.as_any_mut()
.and_then(|a| a.downcast_mut::<WaveformRecord>())
.expect("waveform must expose itself via as_any_mut");
wf.busy = true;
wf.rarm = 0;
assert_eq!(r.get_field("BUSY"), Some(EpicsValue::Short(1)));
assert_eq!(r.get_field("RARM"), Some(EpicsValue::Short(0)));
}
#[test]
fn aao_omsl_dol_round_trip_and_kind_gated() {
let mut aao = WaveformRecord::with_kind(ArrayKind::Aao);
aao.put_field("OMSL", EpicsValue::Short(MENU_OMSL_CLOSED_LOOP))
.unwrap();
aao.put_field("DOL", EpicsValue::String("src.VAL".into()))
.unwrap();
assert_eq!(
aao.get_field("OMSL"),
Some(EpicsValue::Short(MENU_OMSL_CLOSED_LOOP))
);
assert_eq!(
aao.get_field("DOL"),
Some(EpicsValue::String("src.VAL".into()))
);
let mut wf = WaveformRecord::with_kind(ArrayKind::Waveform);
assert_eq!(wf.get_field("OMSL"), None);
assert_eq!(wf.get_field("DOL"), None);
assert!(wf.put_field("OMSL", EpicsValue::Short(1)).is_err());
assert!(
wf.put_field("DOL", EpicsValue::String("src.VAL".into()))
.is_err()
);
}
#[test]
fn aao_closed_loop_real_link_emits_read_db_link() {
let mut aao = WaveformRecord::with_kind(ArrayKind::Aao);
aao.omsl = MENU_OMSL_CLOSED_LOOP;
aao.dol = "srcWaveform.VAL".to_string();
assert_eq!(
aao.pre_input_link_actions(),
vec![ProcessAction::ReadDbLink {
link_field: "DOL",
target_field: "VAL",
}]
);
aao.dol = "srcWaveform".to_string();
assert_eq!(aao.pre_input_link_actions().len(), 1);
}
#[test]
fn aao_closed_loop_constant_or_empty_dol_emits_nothing() {
let mut aao = WaveformRecord::with_kind(ArrayKind::Aao);
aao.omsl = MENU_OMSL_CLOSED_LOOP;
aao.dol = "1 2 3".to_string(); assert!(aao.pre_input_link_actions().is_empty());
aao.dol = "42".to_string(); assert!(aao.pre_input_link_actions().is_empty());
aao.dol = String::new(); assert!(aao.pre_input_link_actions().is_empty());
aao.dol = " ".to_string(); assert!(aao.pre_input_link_actions().is_empty());
}
#[test]
fn supervisory_and_non_aao_kinds_emit_nothing() {
let mut aao = WaveformRecord::with_kind(ArrayKind::Aao);
aao.omsl = 0; aao.dol = "srcWaveform.VAL".to_string();
assert!(aao.pre_input_link_actions().is_empty());
for kind in [ArrayKind::Waveform, ArrayKind::Aai, ArrayKind::SubArray] {
let mut r = WaveformRecord::with_kind(kind);
r.omsl = MENU_OMSL_CLOSED_LOOP;
r.dol = "srcWaveform.VAL".to_string();
assert!(
r.pre_input_link_actions().is_empty(),
"{kind:?} has no OMSL/DOL and must not fetch"
);
}
}
#[test]
fn aao_val_put_sets_nord_for_dol_pull() {
let mut aao = WaveformRecord::with_kind(ArrayKind::Aao);
aao.nelm = 8;
aao.put_field("VAL", EpicsValue::DoubleArray(vec![1.0, 2.0, 3.0]))
.unwrap();
assert_eq!(aao.nord, 3, "NORD must equal the pulled element count");
}
#[test]
fn put_val_caps_nord_at_nelm() {
let mut wf = WaveformRecord::with_kind(ArrayKind::Waveform);
wf.nelm = 4;
wf.put_field("VAL", EpicsValue::LongArray(vec![1, 2]))
.unwrap();
assert_eq!(wf.nord, 2, "source < NELM: NORD == source length");
wf.put_field("VAL", EpicsValue::LongArray(vec![1, 2, 3, 4]))
.unwrap();
assert_eq!(wf.nord, 4, "source == NELM: NORD == NELM");
wf.put_field("VAL", EpicsValue::LongArray(vec![1, 2, 3, 4, 5, 6, 7]))
.unwrap();
assert_eq!(wf.nord, 4, "source > NELM: NORD must clamp to NELM");
let val = wf.get_field("VAL").unwrap();
if let EpicsValue::LongArray(v) = val {
assert_eq!(v, vec![1, 2, 3, 4], "VAL serves exactly NELM elements");
} else {
panic!("VAL should be LongArray, got {val:?}");
}
}
#[test]
fn subarray_slices_input_at_indx_with_nelm_take() {
let mut r = WaveformRecord::with_kind(ArrayKind::SubArray);
r.put_field("MALM", EpicsValue::Long(6)).unwrap();
r.put_field("NELM", EpicsValue::Long(4)).unwrap();
r.put_field("INDX", EpicsValue::Long(2)).unwrap();
let source = EpicsValue::DoubleArray(vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0]);
r.set_val(source).unwrap();
assert_eq!(r.nord, 4, "should copy 4 elements from offset 2");
let val = r.get_field("VAL").unwrap();
if let EpicsValue::DoubleArray(v) = val {
assert_eq!(v, vec![12.0, 13.0, 14.0, 15.0]);
} else {
panic!("VAL should be DoubleArray, got {val:?}");
}
}
#[test]
fn subarray_indx_out_of_range_yields_nord_zero() {
let mut r = WaveformRecord::with_kind(ArrayKind::SubArray);
r.put_field("MALM", EpicsValue::Long(20)).unwrap();
r.put_field("NELM", EpicsValue::Long(3)).unwrap();
r.put_field("INDX", EpicsValue::Long(10)).unwrap();
let source = EpicsValue::LongArray(vec![1, 2, 3]);
r.put_field("FTVL", EpicsValue::Short(5)).unwrap(); r.set_val(source).unwrap();
assert_eq!(r.nord, 0, "INDX past source.len must zero NORD");
}
#[test]
fn subarray_partial_tail_zero_pads_to_nelm() {
let mut r = WaveformRecord::with_kind(ArrayKind::SubArray);
r.put_field("MALM", EpicsValue::Long(5)).unwrap();
r.put_field("NELM", EpicsValue::Long(5)).unwrap();
r.put_field("INDX", EpicsValue::Long(3)).unwrap();
let source = EpicsValue::DoubleArray(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
r.set_val(source).unwrap();
assert_eq!(r.nord, 2, "only 2 elements available from offset 3");
if let Some(EpicsValue::DoubleArray(v)) = r.get_field("VAL") {
assert_eq!(v, vec![4.0, 5.0]);
} else {
panic!("VAL must be DoubleArray of valid tail");
}
}
#[test]
fn subarray_malm_caps_visible_source_length() {
let mut r = WaveformRecord::with_kind(ArrayKind::SubArray);
r.put_field("NELM", EpicsValue::Long(4)).unwrap();
r.put_field("INDX", EpicsValue::Long(0)).unwrap();
r.put_field("MALM", EpicsValue::Long(3)).unwrap();
let source = EpicsValue::DoubleArray(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
r.set_val(source).unwrap();
assert_eq!(r.nord, 3, "MALM=3 limits visible source to 3 elements");
if let Some(EpicsValue::DoubleArray(v)) = r.get_field("VAL") {
assert_eq!(v, vec![1.0, 2.0, 3.0]);
} else {
panic!("VAL truncated to MALM-bound prefix");
}
}
#[test]
fn subarray_indx_malm_fields_round_trip() {
let mut r = WaveformRecord::with_kind(ArrayKind::SubArray);
r.put_field("INDX", EpicsValue::Long(5)).unwrap();
r.put_field("MALM", EpicsValue::Long(100)).unwrap();
assert_eq!(r.get_field("INDX"), Some(EpicsValue::Long(5)));
assert_eq!(r.get_field("MALM"), Some(EpicsValue::Long(100)));
}
#[test]
fn subarray_malm_defaults_to_one_and_floors_zero() {
let r = WaveformRecord::with_kind(ArrayKind::SubArray);
assert_eq!(r.get_field("MALM"), Some(EpicsValue::Long(1)));
let mut z = WaveformRecord::with_kind(ArrayKind::SubArray);
z.put_field("MALM", EpicsValue::Long(0)).unwrap();
assert_eq!(
z.get_field("MALM"),
Some(EpicsValue::Long(1)),
"MALM put of 0 floors back to 1"
);
}
#[test]
fn subarray_init_record_clamps_nelm_to_malm_independent_of_load_order() {
let mut nelm_first = WaveformRecord::with_kind(ArrayKind::SubArray);
nelm_first.put_field("NELM", EpicsValue::Long(50)).unwrap();
nelm_first.put_field("MALM", EpicsValue::Long(8)).unwrap();
nelm_first.init_record(0).unwrap();
assert_eq!(nelm_first.nelm, 8, "NELM clamped down to MALM at init");
let mut malm_first = WaveformRecord::with_kind(ArrayKind::SubArray);
malm_first.put_field("MALM", EpicsValue::Long(8)).unwrap();
malm_first.put_field("NELM", EpicsValue::Long(50)).unwrap();
malm_first.init_record(0).unwrap();
assert_eq!(
malm_first.nelm, 8,
"same clamp regardless of NELM/MALM .db load order"
);
}
#[test]
fn waveform_does_not_expose_indx_malm() {
let r = WaveformRecord::with_kind(ArrayKind::Waveform);
assert!(r.get_field("INDX").is_none());
assert!(r.get_field("MALM").is_none());
}
#[test]
fn br_r13_waveform_ftvl_uint64_storage_and_field_type() {
let mut r = WaveformRecord::with_kind(ArrayKind::Waveform);
r.put_field("NELM", EpicsValue::Long(3)).unwrap();
r.put_field("FTVL", EpicsValue::Short(8)).unwrap();
match r.get_field("VAL") {
Some(EpicsValue::UInt64Array(_)) => {}
other => panic!("FTVL=UINT64 VAL must be UInt64Array, got {other:?}"),
}
let val_dbf = r
.field_list()
.iter()
.find(|f| f.name == "VAL")
.map(|f| f.dbf_type);
assert_eq!(val_dbf, Some(DbFieldType::UInt64));
let big = u64::MAX - 9;
r.put_field("VAL", EpicsValue::UInt64Array(vec![big, 0, 1]))
.unwrap();
match r.get_field("VAL") {
Some(EpicsValue::UInt64Array(v)) => assert_eq!(v[0], big),
other => panic!("expected UInt64Array, got {other:?}"),
}
let mut r2 = WaveformRecord::with_kind(ArrayKind::Waveform);
r2.put_field("NELM", EpicsValue::Long(2)).unwrap();
r2.put_field("FTVL", EpicsValue::Short(7)).unwrap(); assert!(matches!(
r2.get_field("VAL"),
Some(EpicsValue::Int64Array(_))
));
let i64_dbf = r2
.field_list()
.iter()
.find(|f| f.name == "VAL")
.map(|f| f.dbf_type);
assert_eq!(i64_dbf, Some(DbFieldType::Int64));
}
#[test]
fn waveform_mpst_apst_snapshot_is_enum_with_reversed_post_labels() {
use crate::server::record::RecordInstance;
let mut rec = WaveformRecord::with_kind(ArrayKind::Waveform);
rec.put_field("MPST", EpicsValue::Short(0)).unwrap();
assert_eq!(rec.get_field("MPST"), Some(EpicsValue::Short(0)));
let inst = RecordInstance::new("WF:MPST".into(), rec);
let snap = inst.snapshot_for_field("MPST").unwrap();
assert_eq!(snap.value, EpicsValue::Enum(0));
assert_eq!(
snap.enums.as_ref().unwrap().strings,
vec!["Always", "On Change"],
"waveformPOST index 0 must be \"Always\" (reverse of menuPost)"
);
}
#[test]
fn waveform_sim_block_served_per_kind() {
use crate::server::record::RecordInstance;
for kind in [ArrayKind::Waveform, ArrayKind::Aai, ArrayKind::Aao] {
let mut rec = WaveformRecord::with_kind(kind);
rec.put_field("SIMM", EpicsValue::Short(1)).unwrap();
assert_eq!(rec.get_field("SIMM"), Some(EpicsValue::Short(1)));
rec.put_field("SIML", EpicsValue::String("sim:mode".into()))
.unwrap();
assert_eq!(
rec.get_field("SIML"),
Some(EpicsValue::String("sim:mode".into()))
);
rec.put_field("SIOL", EpicsValue::String("sim:in".into()))
.unwrap();
assert_eq!(
rec.get_field("SIOL"),
Some(EpicsValue::String("sim:in".into()))
);
rec.put_field("SIMS", EpicsValue::Short(2)).unwrap();
assert_eq!(rec.get_field("SIMS"), Some(EpicsValue::Short(2)));
assert!(matches!(
rec.put_field("OLDSIMM", EpicsValue::Short(1)),
Err(crate::error::CaError::ReadOnlyField(_))
));
assert_eq!(rec.get_field("OLDSIMM"), Some(EpicsValue::Short(0)));
}
let mut wf = WaveformRecord::with_kind(ArrayKind::Waveform);
wf.put_field("SIMM", EpicsValue::Short(1)).unwrap();
let inst = RecordInstance::new("WF:SIMM".into(), wf);
let snap = inst.snapshot_for_field("SIMM").unwrap();
assert_eq!(snap.value, EpicsValue::Enum(1));
assert_eq!(snap.enums.as_ref().unwrap().strings, vec!["NO", "YES"]);
let snap_old = inst.snapshot_for_field("OLDSIMM").unwrap();
assert_eq!(
snap_old.enums.as_ref().unwrap().strings,
vec!["NO", "YES", "RAW"]
);
let sub = WaveformRecord::with_kind(ArrayKind::SubArray);
assert_eq!(sub.get_field("SIMM"), None);
assert_eq!(sub.get_field("OLDSIMM"), None);
let mut sub_mut = WaveformRecord::with_kind(ArrayKind::SubArray);
assert!(matches!(
sub_mut.put_field("SIMM", EpicsValue::Short(1)),
Err(crate::error::CaError::FieldNotFound(_))
));
}
#[test]
fn br_r13_waveform_new_from_uint64_dbf_type() {
let r = WaveformRecord::new(4, DbFieldType::UInt64);
assert_eq!(r.ftvl, 8);
assert!(matches!(&r.val, EpicsValue::UInt64Array(v) if v.len() == 4));
}
}