use std::time::{SystemTime, UNIX_EPOCH};
use epics_base_rs::server::snapshot::{ControlInfo, DisplayInfo, Snapshot};
use epics_base_rs::types::EpicsValue;
use epics_pva_rs::pvdata::{FieldDesc, PvField, PvStructure, ScalarType, ScalarValue};
use crate::convert::{epics_to_pv_field, epics_to_scalar};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldMapping {
Scalar,
Plain,
Meta,
Any,
Proc,
Structure,
Const,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NtType {
Scalar,
Enum,
ScalarArray,
}
impl NtType {
pub fn from_record_type(rtyp: &str) -> Self {
match rtyp {
"bi" | "bo" | "mbbi" | "mbbo" => NtType::Enum,
"waveform" | "compress" | "histogram" => NtType::ScalarArray,
_ => NtType::Scalar,
}
}
}
const FORM_CHOICES: [&str; 7] = [
"Default",
"String",
"Binary",
"Decimal",
"Hex",
"Exponential",
"Engineering",
];
fn value_scalar_type(value: &EpicsValue) -> ScalarType {
match value {
EpicsValue::String(_) | EpicsValue::StringArray(_) => ScalarType::String,
EpicsValue::Short(_) | EpicsValue::ShortArray(_) => ScalarType::Short,
EpicsValue::Float(_) | EpicsValue::FloatArray(_) => ScalarType::Float,
EpicsValue::Enum(_) | EpicsValue::EnumArray(_) => ScalarType::UShort,
EpicsValue::Char(_) | EpicsValue::CharArray(_) => ScalarType::Byte,
EpicsValue::Long(_) | EpicsValue::LongArray(_) => ScalarType::Int,
EpicsValue::Double(_) | EpicsValue::DoubleArray(_) => ScalarType::Double,
EpicsValue::Int64(_) | EpicsValue::Int64Array(_) => ScalarType::Long,
EpicsValue::UInt64(_) | EpicsValue::UInt64Array(_) => ScalarType::ULong,
}
}
fn is_numeric_scalar(t: ScalarType) -> bool {
!matches!(t, ScalarType::String)
}
fn limit_scalar(t: ScalarType, v: f64) -> ScalarValue {
match t {
ScalarType::Boolean => ScalarValue::Boolean(v != 0.0),
ScalarType::Byte => ScalarValue::Byte(v as i8),
ScalarType::Short => ScalarValue::Short(v as i16),
ScalarType::Int => ScalarValue::Int(v as i32),
ScalarType::Long => ScalarValue::Long(v as i64),
ScalarType::UByte => ScalarValue::UByte(v as u8),
ScalarType::UShort => ScalarValue::UShort(v as u16),
ScalarType::UInt => ScalarValue::UInt(v as u32),
ScalarType::ULong => ScalarValue::ULong(v as u64),
ScalarType::Float => ScalarValue::Float(v as f32),
ScalarType::Double => ScalarValue::Double(v),
ScalarType::String => ScalarValue::String(v.to_string()),
}
}
pub fn snapshot_to_nt_scalar(snapshot: &Snapshot) -> PvStructure {
let mut pv = PvStructure::new("epics:nt/NTScalar:1.0");
let empty_array = is_empty_array(&snapshot.value);
let scalar_type = value_scalar_type(&snapshot.value);
let numeric = is_numeric_scalar(scalar_type);
pv.fields.push((
"value".into(),
PvField::Scalar(epics_to_scalar(&snapshot.value)),
));
let alarm_struct = if empty_array {
build_alarm_overlay(snapshot, 3, 17)
} else {
build_alarm(snapshot)
};
pv.fields
.push(("alarm".into(), PvField::Structure(alarm_struct)));
pv.fields.push((
"timeStamp".into(),
PvField::Structure(build_timestamp(snapshot.timestamp, snapshot.user_tag)),
));
if let Some(ref disp) = snapshot.display {
pv.fields.push((
"display".into(),
PvField::Structure(build_display(disp, scalar_type, numeric)),
));
}
if numeric {
if let Some(ref ctrl) = snapshot.control {
pv.fields.push((
"control".into(),
PvField::Structure(build_control(ctrl, scalar_type)),
));
}
}
if numeric {
if let Some(ref disp) = snapshot.display {
pv.fields.push((
"valueAlarm".into(),
PvField::Structure(build_value_alarm(disp, scalar_type)),
));
}
}
pv
}
pub fn snapshot_to_nt_enum(snapshot: &Snapshot) -> PvStructure {
let mut pv = PvStructure::new("epics:nt/NTEnum:1.0");
let index = match &snapshot.value {
EpicsValue::Enum(v) => *v as i32,
EpicsValue::Short(v) => *v as i32,
other => other.to_f64().map(|f| f as i32).unwrap_or(0),
};
let choices: Vec<ScalarValue> = snapshot
.enums
.as_ref()
.map(|e| {
e.strings
.iter()
.map(|s| ScalarValue::String(s.clone()))
.collect()
})
.unwrap_or_default();
let mut value_struct = PvStructure::new("enum_t");
value_struct
.fields
.push(("index".into(), PvField::Scalar(ScalarValue::Int(index))));
value_struct
.fields
.push(("choices".into(), PvField::ScalarArray(choices)));
pv.fields
.push(("value".into(), PvField::Structure(value_struct)));
pv.fields
.push(("alarm".into(), PvField::Structure(build_alarm(snapshot))));
pv.fields.push((
"timeStamp".into(),
PvField::Structure(build_timestamp(snapshot.timestamp, snapshot.user_tag)),
));
let mut display = PvStructure::new("");
display.fields.push((
"description".into(),
PvField::Scalar(ScalarValue::String(
snapshot
.display
.as_ref()
.map(|d| d.description.clone())
.unwrap_or_default(),
)),
));
pv.fields
.push(("display".into(), PvField::Structure(display)));
pv
}
pub fn snapshot_to_nt_scalar_array(snapshot: &Snapshot) -> PvStructure {
let mut pv = PvStructure::new("epics:nt/NTScalarArray:1.0");
let scalar_type = value_scalar_type(&snapshot.value);
let numeric = is_numeric_scalar(scalar_type);
pv.fields
.push(("value".into(), epics_to_pv_field(&snapshot.value)));
pv.fields
.push(("alarm".into(), PvField::Structure(build_alarm(snapshot))));
pv.fields.push((
"timeStamp".into(),
PvField::Structure(build_timestamp(snapshot.timestamp, snapshot.user_tag)),
));
if let Some(ref disp) = snapshot.display {
pv.fields.push((
"display".into(),
PvField::Structure(build_display(disp, scalar_type, numeric)),
));
}
if numeric {
if let Some(ref ctrl) = snapshot.control {
pv.fields.push((
"control".into(),
PvField::Structure(build_control(ctrl, scalar_type)),
));
}
}
if numeric {
if let Some(ref disp) = snapshot.display {
pv.fields.push((
"valueAlarm".into(),
PvField::Structure(build_value_alarm(disp, scalar_type)),
));
}
}
pv
}
pub fn snapshot_to_pv_structure(snapshot: &Snapshot, nt_type: NtType) -> PvStructure {
match nt_type {
NtType::Scalar => snapshot_to_nt_scalar(snapshot),
NtType::Enum => snapshot_to_nt_enum(snapshot),
NtType::ScalarArray => snapshot_to_nt_scalar_array(snapshot),
}
}
pub fn pv_structure_to_epics(pv: &PvStructure) -> Option<EpicsValue> {
let field = pv.get_field("value")?;
match field {
PvField::Scalar(sv) => Some(crate::convert::scalar_to_epics(sv)),
PvField::ScalarArray(_) | PvField::ScalarArrayTyped(_) => {
crate::convert::pv_field_to_epics(field)
}
PvField::Structure(s) => {
if let Some(PvField::Scalar(sv)) = s.get_field("index") {
let idx = crate::convert::scalar_to_epics(sv);
match idx {
EpicsValue::Enum(v) => Some(EpicsValue::Enum(v)),
other => Some(EpicsValue::Enum(
other.to_f64().map(|f| f as u16).unwrap_or(0),
)),
}
} else {
None
}
}
PvField::StructureArray(_)
| PvField::Union { .. }
| PvField::UnionArray(_)
| PvField::Variant(_)
| PvField::VariantArray(_)
| PvField::Null => None,
}
}
pub fn filter_by_request(pv: &PvStructure, request: &PvStructure) -> PvStructure {
let field_spec = match request.get_field("field") {
Some(PvField::Structure(s)) => s,
_ => return pv.clone(), };
filter_by_spec(pv, field_spec)
}
fn filter_by_spec(pv: &PvStructure, spec: &PvStructure) -> PvStructure {
if spec.fields.is_empty() {
return pv.clone();
}
let mut result = PvStructure::new(&pv.struct_id);
for (name, value) in &pv.fields {
let sub_spec = match spec.get_field(name) {
Some(s) => s,
None => continue, };
match (sub_spec, value) {
(PvField::Structure(s_spec), PvField::Structure(s_val)) => {
result.fields.push((
name.clone(),
PvField::Structure(filter_by_spec(s_val, s_spec)),
));
}
(_, _) => {
result.fields.push((name.clone(), value.clone()));
}
}
}
result
}
pub fn build_nt_scalar_desc(scalar_type: ScalarType) -> FieldDesc {
let numeric = is_numeric_scalar(scalar_type);
let mut fields = vec![
("value".into(), FieldDesc::Scalar(scalar_type)),
("alarm".into(), alarm_desc()),
("timeStamp".into(), timestamp_desc()),
("display".into(), display_desc(scalar_type, numeric)),
];
if numeric {
fields.push(("control".into(), control_desc(scalar_type)));
fields.push(("valueAlarm".into(), value_alarm_desc(scalar_type)));
}
FieldDesc::Structure {
struct_id: "epics:nt/NTScalar:1.0".into(),
fields,
}
}
pub fn build_nt_enum_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "epics:nt/NTEnum:1.0".into(),
fields: vec![
(
"value".into(),
FieldDesc::Structure {
struct_id: "enum_t".into(),
fields: vec![
("index".into(), FieldDesc::Scalar(ScalarType::Int)),
("choices".into(), FieldDesc::ScalarArray(ScalarType::String)),
],
},
),
("alarm".into(), alarm_desc()),
("timeStamp".into(), timestamp_desc()),
(
"display".into(),
FieldDesc::Structure {
struct_id: String::new(),
fields: vec![("description".into(), FieldDesc::Scalar(ScalarType::String))],
},
),
],
}
}
pub fn build_nt_scalar_array_desc(element_type: ScalarType) -> FieldDesc {
let numeric = is_numeric_scalar(element_type);
let mut fields = vec![
("value".into(), FieldDesc::ScalarArray(element_type)),
("alarm".into(), alarm_desc()),
("timeStamp".into(), timestamp_desc()),
("display".into(), display_desc(element_type, numeric)),
];
if numeric {
fields.push(("control".into(), control_desc(element_type)));
fields.push(("valueAlarm".into(), value_alarm_desc(element_type)));
}
FieldDesc::Structure {
struct_id: "epics:nt/NTScalarArray:1.0".into(),
fields,
}
}
pub fn build_field_desc_for_nt(nt_type: NtType, scalar_type: ScalarType) -> FieldDesc {
match nt_type {
NtType::Scalar => build_nt_scalar_desc(scalar_type),
NtType::Enum => build_nt_enum_desc(),
NtType::ScalarArray => build_nt_scalar_array_desc(scalar_type),
}
}
fn build_alarm(snapshot: &Snapshot) -> PvStructure {
let mut alarm = PvStructure::new("alarm_t");
alarm.fields.push((
"severity".into(),
PvField::Scalar(ScalarValue::Int(snapshot.alarm.severity as i32)),
));
alarm.fields.push((
"status".into(),
PvField::Scalar(ScalarValue::Int(snapshot.alarm.status as i32)),
));
alarm.fields.push((
"message".into(),
PvField::Scalar(ScalarValue::String(alarm_severity_string(
snapshot.alarm.severity,
))),
));
alarm
}
fn build_alarm_overlay(snapshot: &Snapshot, severity: u16, status: u16) -> PvStructure {
let eff_severity = snapshot.alarm.severity.max(severity);
let eff_status = if snapshot.alarm.status == 0 {
status
} else {
snapshot.alarm.status
};
let mut alarm = PvStructure::new("alarm_t");
alarm.fields.push((
"severity".into(),
PvField::Scalar(ScalarValue::Int(eff_severity as i32)),
));
alarm.fields.push((
"status".into(),
PvField::Scalar(ScalarValue::Int(eff_status as i32)),
));
alarm.fields.push((
"message".into(),
PvField::Scalar(ScalarValue::String(alarm_severity_string(eff_severity))),
));
alarm
}
fn is_empty_array(value: &EpicsValue) -> bool {
matches!(
value,
EpicsValue::ShortArray(a) if a.is_empty()
) || matches!(
value,
EpicsValue::FloatArray(a) if a.is_empty()
) || matches!(
value,
EpicsValue::EnumArray(a) if a.is_empty()
) || matches!(
value,
EpicsValue::DoubleArray(a) if a.is_empty()
) || matches!(
value,
EpicsValue::LongArray(a) if a.is_empty()
) || matches!(
value,
EpicsValue::CharArray(a) if a.is_empty()
) || matches!(
value,
EpicsValue::StringArray(a) if a.is_empty()
)
}
fn build_timestamp(time: SystemTime, user_tag: i32) -> PvStructure {
let mut ts = PvStructure::new("time_t");
let (secs, nanos) = match time.duration_since(UNIX_EPOCH) {
Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as i32),
Err(_) => (0, 0),
};
ts.fields.push((
"secondsPastEpoch".into(),
PvField::Scalar(ScalarValue::Long(secs)),
));
ts.fields.push((
"nanoseconds".into(),
PvField::Scalar(ScalarValue::Int(nanos)),
));
ts.fields.push((
"userTag".into(),
PvField::Scalar(ScalarValue::Int(user_tag)),
));
ts
}
fn build_form(form: i16) -> PvStructure {
let mut f = PvStructure::new("enum_t");
f.fields.push((
"index".into(),
PvField::Scalar(ScalarValue::Int(form as i32)),
));
f.fields.push((
"choices".into(),
PvField::ScalarArray(
FORM_CHOICES
.iter()
.map(|s| ScalarValue::String((*s).to_string()))
.collect(),
),
));
f
}
fn build_display(disp: &DisplayInfo, scalar_type: ScalarType, numeric: bool) -> PvStructure {
let mut d = PvStructure::new("display_t");
if numeric {
d.fields.push((
"limitLow".into(),
PvField::Scalar(limit_scalar(scalar_type, disp.lower_disp_limit)),
));
d.fields.push((
"limitHigh".into(),
PvField::Scalar(limit_scalar(scalar_type, disp.upper_disp_limit)),
));
}
d.fields.push((
"description".into(),
PvField::Scalar(ScalarValue::String(disp.description.clone())),
));
d.fields.push((
"units".into(),
PvField::Scalar(ScalarValue::String(disp.units.clone())),
));
if numeric {
d.fields.push((
"precision".into(),
PvField::Scalar(ScalarValue::Int(disp.precision as i32)),
));
d.fields
.push(("form".into(), PvField::Structure(build_form(disp.form))));
}
d
}
fn build_control(ctrl: &ControlInfo, scalar_type: ScalarType) -> PvStructure {
let mut c = PvStructure::new("control_t");
c.fields.push((
"limitLow".into(),
PvField::Scalar(limit_scalar(scalar_type, ctrl.lower_ctrl_limit)),
));
c.fields.push((
"limitHigh".into(),
PvField::Scalar(limit_scalar(scalar_type, ctrl.upper_ctrl_limit)),
));
c.fields.push((
"minStep".into(),
PvField::Scalar(limit_scalar(scalar_type, 0.0)),
));
c
}
fn alarm_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "alarm_t".into(),
fields: vec![
("severity".into(), FieldDesc::Scalar(ScalarType::Int)),
("status".into(), FieldDesc::Scalar(ScalarType::Int)),
("message".into(), FieldDesc::Scalar(ScalarType::String)),
],
}
}
fn timestamp_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "time_t".into(),
fields: vec![
(
"secondsPastEpoch".into(),
FieldDesc::Scalar(ScalarType::Long),
),
("nanoseconds".into(), FieldDesc::Scalar(ScalarType::Int)),
("userTag".into(), FieldDesc::Scalar(ScalarType::Int)),
],
}
}
fn form_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "enum_t".into(),
fields: vec![
("index".into(), FieldDesc::Scalar(ScalarType::Int)),
("choices".into(), FieldDesc::ScalarArray(ScalarType::String)),
],
}
}
fn display_desc(scalar_type: ScalarType, numeric: bool) -> FieldDesc {
let mut fields: Vec<(String, FieldDesc)> = Vec::new();
if numeric {
fields.push(("limitLow".into(), FieldDesc::Scalar(scalar_type)));
fields.push(("limitHigh".into(), FieldDesc::Scalar(scalar_type)));
}
fields.push(("description".into(), FieldDesc::Scalar(ScalarType::String)));
fields.push(("units".into(), FieldDesc::Scalar(ScalarType::String)));
if numeric {
fields.push(("precision".into(), FieldDesc::Scalar(ScalarType::Int)));
fields.push(("form".into(), form_desc()));
}
FieldDesc::Structure {
struct_id: "display_t".into(),
fields,
}
}
fn control_desc(scalar_type: ScalarType) -> FieldDesc {
FieldDesc::Structure {
struct_id: "control_t".into(),
fields: vec![
("limitLow".into(), FieldDesc::Scalar(scalar_type)),
("limitHigh".into(), FieldDesc::Scalar(scalar_type)),
("minStep".into(), FieldDesc::Scalar(scalar_type)),
],
}
}
fn build_value_alarm(disp: &DisplayInfo, scalar_type: ScalarType) -> PvStructure {
let mut va = PvStructure::new("valueAlarm_t");
va.fields.push((
"active".into(),
PvField::Scalar(ScalarValue::Boolean(false)),
));
va.fields.push((
"lowAlarmLimit".into(),
PvField::Scalar(limit_scalar(scalar_type, disp.lower_alarm_limit)),
));
va.fields.push((
"lowWarningLimit".into(),
PvField::Scalar(limit_scalar(scalar_type, disp.lower_warning_limit)),
));
va.fields.push((
"highWarningLimit".into(),
PvField::Scalar(limit_scalar(scalar_type, disp.upper_warning_limit)),
));
va.fields.push((
"highAlarmLimit".into(),
PvField::Scalar(limit_scalar(scalar_type, disp.upper_alarm_limit)),
));
va.fields.push((
"lowAlarmSeverity".into(),
PvField::Scalar(ScalarValue::Int(0)),
));
va.fields.push((
"lowWarningSeverity".into(),
PvField::Scalar(ScalarValue::Int(0)),
));
va.fields.push((
"highWarningSeverity".into(),
PvField::Scalar(ScalarValue::Int(0)),
));
va.fields.push((
"highAlarmSeverity".into(),
PvField::Scalar(ScalarValue::Int(0)),
));
va.fields.push((
"hysteresis".into(),
PvField::Scalar(ScalarValue::Double(0.0)),
));
va
}
fn value_alarm_desc(scalar_type: ScalarType) -> FieldDesc {
FieldDesc::Structure {
struct_id: "valueAlarm_t".into(),
fields: vec![
("active".into(), FieldDesc::Scalar(ScalarType::Boolean)),
("lowAlarmLimit".into(), FieldDesc::Scalar(scalar_type)),
("lowWarningLimit".into(), FieldDesc::Scalar(scalar_type)),
("highWarningLimit".into(), FieldDesc::Scalar(scalar_type)),
("highAlarmLimit".into(), FieldDesc::Scalar(scalar_type)),
(
"lowAlarmSeverity".into(),
FieldDesc::Scalar(ScalarType::Int),
),
(
"lowWarningSeverity".into(),
FieldDesc::Scalar(ScalarType::Int),
),
(
"highWarningSeverity".into(),
FieldDesc::Scalar(ScalarType::Int),
),
(
"highAlarmSeverity".into(),
FieldDesc::Scalar(ScalarType::Int),
),
("hysteresis".into(), FieldDesc::Scalar(ScalarType::Double)),
],
}
}
fn alarm_severity_string(severity: u16) -> String {
match severity {
0 => "NO_ALARM".into(),
1 => "MINOR".into(),
2 => "MAJOR".into(),
3 => "INVALID".into(),
_ => format!("UNKNOWN({severity})"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use epics_base_rs::server::snapshot::{EnumInfo, Snapshot};
fn test_snapshot(value: EpicsValue) -> Snapshot {
let mut snap = Snapshot::new(value, 0, 0, UNIX_EPOCH);
snap.display = Some(DisplayInfo {
units: "degC".into(),
precision: 3,
upper_disp_limit: 100.0,
lower_disp_limit: 0.0,
upper_alarm_limit: 90.0,
upper_warning_limit: 80.0,
lower_warning_limit: 10.0,
lower_alarm_limit: 5.0,
..Default::default()
});
snap.control = Some(ControlInfo {
upper_ctrl_limit: 100.0,
lower_ctrl_limit: 0.0,
});
snap
}
#[test]
fn nt_scalar_structure() {
let snap = test_snapshot(EpicsValue::Double(42.5));
let pv = snapshot_to_nt_scalar(&snap);
assert_eq!(pv.struct_id, "epics:nt/NTScalar:1.0");
assert_eq!(pv.get_value(), Some(&ScalarValue::Double(42.5)));
assert!(pv.get_alarm().is_some());
assert!(pv.get_timestamp().is_some());
assert!(pv.get_field("display").is_some());
assert!(pv.get_field("control").is_some());
let va = pv.get_field("valueAlarm");
assert!(va.is_some());
if let Some(PvField::Structure(va_struct)) = va {
assert!(va_struct.get_field("lowAlarmLimit").is_some());
assert!(va_struct.get_field("highAlarmLimit").is_some());
assert!(va_struct.get_field("lowWarningLimit").is_some());
assert!(va_struct.get_field("highWarningLimit").is_some());
} else {
panic!("expected valueAlarm structure");
}
}
#[test]
fn nt_scalar_empty_array_marks_invalid_udf() {
let snap = test_snapshot(EpicsValue::DoubleArray(vec![]));
let pv = snapshot_to_nt_scalar(&snap);
if let Some(PvField::Structure(alarm)) = pv.get_field("alarm") {
let sev = alarm.get_field("severity");
let st = alarm.get_field("status");
assert!(matches!(sev, Some(PvField::Scalar(ScalarValue::Int(3)))));
assert!(matches!(st, Some(PvField::Scalar(ScalarValue::Int(17)))));
} else {
panic!("expected alarm structure");
}
}
#[test]
fn nt_scalar_non_empty_keeps_original_alarm() {
let snap = test_snapshot(EpicsValue::Double(1.0));
let pv = snapshot_to_nt_scalar(&snap);
if let Some(PvField::Structure(alarm)) = pv.get_field("alarm") {
let sev = alarm.get_field("severity");
assert!(matches!(sev, Some(PvField::Scalar(ScalarValue::Int(0)))));
} else {
panic!("expected alarm structure");
}
}
#[test]
fn nt_enum_structure() {
let mut snap = Snapshot::new(EpicsValue::Enum(1), 0, 0, UNIX_EPOCH);
snap.enums = Some(EnumInfo {
strings: vec!["Off".into(), "On".into()],
});
let pv = snapshot_to_nt_enum(&snap);
assert_eq!(pv.struct_id, "epics:nt/NTEnum:1.0");
if let Some(PvField::Structure(val)) = pv.get_field("value") {
if let Some(PvField::Scalar(ScalarValue::Int(idx))) = val.get_field("index") {
assert_eq!(*idx, 1);
} else {
panic!(
"expected int32_t index scalar, got {:?}",
val.get_field("index")
);
}
if let Some(PvField::ScalarArray(choices)) = val.get_field("choices") {
assert_eq!(choices.len(), 2);
} else {
panic!("expected choices array");
}
} else {
panic!("expected value structure");
}
if let Some(PvField::Structure(d)) = pv.get_field("display") {
assert!(
matches!(
d.get_field("description"),
Some(PvField::Scalar(ScalarValue::String(_)))
),
"expected display.description string"
);
} else {
panic!("expected display structure");
}
}
#[test]
fn nt_scalar_array_structure() {
let snap = test_snapshot(EpicsValue::DoubleArray(vec![1.0, 2.0, 3.0]));
let pv = snapshot_to_nt_scalar_array(&snap);
assert_eq!(pv.struct_id, "epics:nt/NTScalarArray:1.0");
if let Some(PvField::ScalarArray(arr)) = pv.get_field("value") {
assert_eq!(arr.len(), 3);
} else {
panic!("expected value array");
}
}
#[test]
fn put_roundtrip_scalar() {
let snap = test_snapshot(EpicsValue::Double(99.0));
let pv = snapshot_to_nt_scalar(&snap);
let back = pv_structure_to_epics(&pv).unwrap();
assert_eq!(back, EpicsValue::Double(99.0));
}
#[test]
fn put_roundtrip_enum() {
let mut snap = Snapshot::new(EpicsValue::Enum(2), 0, 0, UNIX_EPOCH);
snap.enums = Some(EnumInfo {
strings: vec!["A".into(), "B".into(), "C".into()],
});
let pv = snapshot_to_nt_enum(&snap);
let back = pv_structure_to_epics(&pv).unwrap();
assert_eq!(back, EpicsValue::Enum(2));
}
#[test]
fn nt_type_from_record_type() {
assert_eq!(NtType::from_record_type("ai"), NtType::Scalar);
assert_eq!(NtType::from_record_type("bi"), NtType::Enum);
assert_eq!(NtType::from_record_type("waveform"), NtType::ScalarArray);
assert_eq!(NtType::from_record_type("calc"), NtType::Scalar);
assert_eq!(NtType::from_record_type("mbbi"), NtType::Enum);
}
#[test]
fn field_desc_nt_scalar() {
let desc = build_nt_scalar_desc(ScalarType::Double);
assert_eq!(desc.value_scalar_type(), Some(ScalarType::Double));
assert_eq!(desc.field_count(), 6); }
#[test]
fn filter_by_request_empty() {
let snap = test_snapshot(EpicsValue::Double(1.0));
let pv = snapshot_to_nt_scalar(&snap);
let req = PvStructure::new("");
let filtered = filter_by_request(&pv, &req);
assert_eq!(filtered.fields.len(), pv.fields.len());
}
#[test]
fn filter_by_request_value_only() {
let snap = test_snapshot(EpicsValue::Double(1.0));
let pv = snapshot_to_nt_scalar(&snap);
let mut field_spec = PvStructure::new("");
field_spec
.fields
.push(("value".into(), PvField::Structure(PvStructure::new(""))));
let mut req = PvStructure::new("");
req.fields
.push(("field".into(), PvField::Structure(field_spec)));
let filtered = filter_by_request(&pv, &req);
assert_eq!(filtered.fields.len(), 1);
assert_eq!(filtered.fields[0].0, "value");
}
#[test]
fn filter_by_request_multiple_fields() {
let snap = test_snapshot(EpicsValue::Double(1.0));
let pv = snapshot_to_nt_scalar(&snap);
let mut field_spec = PvStructure::new("");
field_spec
.fields
.push(("value".into(), PvField::Structure(PvStructure::new(""))));
field_spec
.fields
.push(("alarm".into(), PvField::Structure(PvStructure::new(""))));
let mut req = PvStructure::new("");
req.fields
.push(("field".into(), PvField::Structure(field_spec)));
let filtered = filter_by_request(&pv, &req);
assert_eq!(filtered.fields.len(), 2);
}
#[test]
fn filter_by_request_nested_subfield() {
let snap = test_snapshot(EpicsValue::Double(1.0));
let pv = snapshot_to_nt_scalar(&snap);
let mut alarm_spec = PvStructure::new("");
alarm_spec
.fields
.push(("severity".into(), PvField::Structure(PvStructure::new(""))));
let mut field_spec = PvStructure::new("");
field_spec
.fields
.push(("alarm".into(), PvField::Structure(alarm_spec)));
let mut req = PvStructure::new("");
req.fields
.push(("field".into(), PvField::Structure(field_spec)));
let filtered = filter_by_request(&pv, &req);
assert_eq!(filtered.fields.len(), 1);
assert_eq!(filtered.fields[0].0, "alarm");
if let PvField::Structure(alarm) = &filtered.fields[0].1 {
assert_eq!(alarm.fields.len(), 1);
assert_eq!(alarm.fields[0].0, "severity");
} else {
panic!("expected alarm structure");
}
}
#[test]
fn br_r12_array_metadata_shape_matches_pvxs() {
let snap = test_snapshot(EpicsValue::LongArray(vec![4, 5, 6, 7]));
let pv = snapshot_to_nt_scalar_array(&snap);
let control = pv.get_field("control").expect("array must carry control");
let value_alarm = pv
.get_field("valueAlarm")
.expect("array must carry valueAlarm");
if let Some(PvField::Structure(disp)) = pv.get_field("display") {
assert!(
matches!(
disp.get_field("limitLow"),
Some(PvField::Scalar(ScalarValue::Int(_)))
),
"display.limitLow must be Int for an int32 array"
);
match disp.get_field("form") {
Some(PvField::Structure(form)) => {
assert_eq!(form.struct_id, "enum_t");
assert!(matches!(
form.get_field("index"),
Some(PvField::Scalar(ScalarValue::Int(_)))
));
if let Some(PvField::ScalarArray(choices)) = form.get_field("choices") {
assert_eq!(choices.len(), 7, "form.choices is the fixed 7-entry menu");
} else {
panic!("display.form.choices must be a string array");
}
}
_ => panic!("display.form must be an enum_t structure"),
}
} else {
panic!("expected display structure");
}
if let PvField::Structure(c) = control {
assert!(matches!(
c.get_field("limitLow"),
Some(PvField::Scalar(ScalarValue::Int(_)))
));
} else {
panic!("expected control structure");
}
if let PvField::Structure(va) = value_alarm {
assert!(matches!(
va.get_field("active"),
Some(PvField::Scalar(ScalarValue::Boolean(false)))
));
assert!(matches!(
va.get_field("lowAlarmLimit"),
Some(PvField::Scalar(ScalarValue::Int(_)))
));
assert!(va.get_field("lowAlarmSeverity").is_some());
assert!(va.get_field("highAlarmSeverity").is_some());
assert!(matches!(
va.get_field("hysteresis"),
Some(PvField::Scalar(ScalarValue::Double(_)))
));
} else {
panic!("expected valueAlarm structure");
}
let desc = build_nt_scalar_array_desc(ScalarType::Int);
if let FieldDesc::Structure { fields, .. } = &desc {
let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"control"), "array desc must carry control");
assert!(
names.contains(&"valueAlarm"),
"array desc must carry valueAlarm"
);
} else {
panic!("expected structure descriptor");
}
}
#[test]
fn br_r12_string_value_omits_numeric_metadata() {
let snap = test_snapshot(EpicsValue::String("Analog input".into()));
let pv = snapshot_to_nt_scalar(&snap);
assert!(
pv.get_field("control").is_none(),
"string value must not carry control"
);
assert!(
pv.get_field("valueAlarm").is_none(),
"string value must not carry valueAlarm"
);
if let Some(PvField::Structure(disp)) = pv.get_field("display") {
assert!(disp.get_field("limitLow").is_none());
assert!(disp.get_field("form").is_none());
assert!(disp.get_field("description").is_some());
assert!(disp.get_field("units").is_some());
} else {
panic!("expected display structure");
}
let desc = build_nt_scalar_desc(ScalarType::String);
if let FieldDesc::Structure { fields, .. } = &desc {
let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
assert!(!names.contains(&"control"));
assert!(!names.contains(&"valueAlarm"));
} else {
panic!("expected structure descriptor");
}
}
#[test]
fn br_r13_uint64_array_qsrv_descriptor_uses_ulong() {
let snap = test_snapshot(EpicsValue::UInt64Array(vec![
11111111111111111,
222222222222222,
]));
let pv = snapshot_to_nt_scalar_array(&snap);
match pv.get_field("value") {
Some(PvField::ScalarArray(vs)) => {
assert!(matches!(vs[0], ScalarValue::ULong(11111111111111111)));
}
other => panic!("expected ulong ScalarArray, got {other:?}"),
}
if let Some(PvField::Structure(disp)) = pv.get_field("display") {
assert!(
matches!(
disp.get_field("limitLow"),
Some(PvField::Scalar(ScalarValue::ULong(_)))
),
"uint64 array display.limitLow must be ulong"
);
} else {
panic!("expected display structure");
}
let desc = build_nt_scalar_array_desc(ScalarType::ULong);
if let FieldDesc::Structure { fields, .. } = &desc {
let value_desc = fields.iter().find(|(n, _)| n == "value").map(|(_, d)| d);
assert!(matches!(
value_desc,
Some(FieldDesc::ScalarArray(ScalarType::ULong))
));
let control = fields.iter().find(|(n, _)| n == "control").map(|(_, d)| d);
if let Some(FieldDesc::Structure { fields: cf, .. }) = control {
assert!(matches!(
cf.iter().find(|(n, _)| n == "limitLow").map(|(_, d)| d),
Some(FieldDesc::Scalar(ScalarType::ULong))
));
} else {
panic!("expected control descriptor");
}
} else {
panic!("expected structure descriptor");
}
}
#[test]
fn field_desc_nt_enum_index_int() {
let desc = build_nt_enum_desc();
if let FieldDesc::Structure { fields, .. } = &desc {
if let Some((
_,
FieldDesc::Structure {
fields: val_fields, ..
},
)) = fields.iter().find(|(n, _)| n == "value")
{
let index_field = val_fields.iter().find(|(n, _)| n == "index");
assert!(
matches!(index_field, Some((_, FieldDesc::Scalar(ScalarType::Int)))),
"expected NTEnum value.index Int32, got {index_field:?}"
);
} else {
panic!("expected value structure");
}
if let Some((
_,
FieldDesc::Structure {
fields: disp_fields,
..
},
)) = fields.iter().find(|(n, _)| n == "display")
{
let desc_field = disp_fields.iter().find(|(n, _)| n == "description");
assert!(
matches!(desc_field, Some((_, FieldDesc::Scalar(ScalarType::String)))),
"expected display.description String"
);
} else {
panic!("expected display sub-structure");
}
} else {
panic!("expected NTEnum top-level structure");
}
}
}