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 super::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,
}
}
}
pub fn snapshot_to_nt_scalar(snapshot: &Snapshot) -> PvStructure {
let mut pv = PvStructure::new("epics:nt/NTScalar:1.0");
pv.fields.push((
"value".into(),
PvField::Scalar(epics_to_scalar(&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))));
}
if let Some(ref ctrl) = snapshot.control {
pv.fields
.push(("control".into(), PvField::Structure(build_control(ctrl))));
}
if let Some(ref disp) = snapshot.display {
pv.fields.push((
"valueAlarm".into(),
PvField::Structure(build_value_alarm(disp)),
));
}
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,
EpicsValue::Short(v) => *v as u16,
other => other.to_f64().map(|f| f as u16).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::UShort(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)),
));
pv
}
pub fn snapshot_to_nt_scalar_array(snapshot: &Snapshot) -> PvStructure {
let mut pv = PvStructure::new("epics:nt/NTScalarArray:1.0");
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))));
}
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(super::convert::scalar_to_epics(sv)),
PvField::ScalarArray(_) => super::convert::pv_field_to_epics(field),
PvField::Structure(s) => {
if let Some(PvField::Scalar(sv)) = s.get_field("index") {
let idx = super::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
}
}
}
}
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 {
FieldDesc::Structure {
struct_id: "epics:nt/NTScalar:1.0".into(),
fields: vec![
("value".into(), FieldDesc::Scalar(scalar_type)),
("alarm".into(), alarm_desc()),
("timeStamp".into(), timestamp_desc()),
("display".into(), display_desc()),
("control".into(), control_desc()),
("valueAlarm".into(), value_alarm_desc()),
],
}
}
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::UShort)),
("choices".into(), FieldDesc::ScalarArray(ScalarType::String)),
],
},
),
("alarm".into(), alarm_desc()),
("timeStamp".into(), timestamp_desc()),
],
}
}
pub fn build_nt_scalar_array_desc(element_type: ScalarType) -> FieldDesc {
FieldDesc::Structure {
struct_id: "epics:nt/NTScalarArray:1.0".into(),
fields: vec![
("value".into(), FieldDesc::ScalarArray(element_type)),
("alarm".into(), alarm_desc()),
("timeStamp".into(), timestamp_desc()),
("display".into(), display_desc()),
],
}
}
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_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_display(disp: &DisplayInfo) -> PvStructure {
let mut d = PvStructure::new("display_t");
d.fields.push((
"limitLow".into(),
PvField::Scalar(ScalarValue::Double(disp.lower_disp_limit)),
));
d.fields.push((
"limitHigh".into(),
PvField::Scalar(ScalarValue::Double(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())),
));
d.fields.push((
"precision".into(),
PvField::Scalar(ScalarValue::Int(disp.precision as i32)),
));
d.fields.push((
"form".into(),
PvField::Scalar(ScalarValue::Int(disp.form as i32)),
));
d
}
fn build_control(ctrl: &ControlInfo) -> PvStructure {
let mut c = PvStructure::new("control_t");
c.fields.push((
"limitLow".into(),
PvField::Scalar(ScalarValue::Double(ctrl.lower_ctrl_limit)),
));
c.fields.push((
"limitHigh".into(),
PvField::Scalar(ScalarValue::Double(ctrl.upper_ctrl_limit)),
));
c.fields
.push(("minStep".into(), PvField::Scalar(ScalarValue::Double(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 display_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "display_t".into(),
fields: vec![
("limitLow".into(), FieldDesc::Scalar(ScalarType::Double)),
("limitHigh".into(), FieldDesc::Scalar(ScalarType::Double)),
("description".into(), FieldDesc::Scalar(ScalarType::String)),
("units".into(), FieldDesc::Scalar(ScalarType::String)),
("precision".into(), FieldDesc::Scalar(ScalarType::Int)),
("form".into(), FieldDesc::Scalar(ScalarType::Int)),
],
}
}
fn control_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "control_t".into(),
fields: vec![
("limitLow".into(), FieldDesc::Scalar(ScalarType::Double)),
("limitHigh".into(), FieldDesc::Scalar(ScalarType::Double)),
("minStep".into(), FieldDesc::Scalar(ScalarType::Double)),
],
}
}
fn build_value_alarm(disp: &DisplayInfo) -> PvStructure {
let mut va = PvStructure::new("valueAlarm_t");
va.fields.push((
"lowAlarmLimit".into(),
PvField::Scalar(ScalarValue::Double(disp.lower_alarm_limit)),
));
va.fields.push((
"lowWarningLimit".into(),
PvField::Scalar(ScalarValue::Double(disp.lower_warning_limit)),
));
va.fields.push((
"highWarningLimit".into(),
PvField::Scalar(ScalarValue::Double(disp.upper_warning_limit)),
));
va.fields.push((
"highAlarmLimit".into(),
PvField::Scalar(ScalarValue::Double(disp.upper_alarm_limit)),
));
va
}
fn value_alarm_desc() -> FieldDesc {
FieldDesc::Structure {
struct_id: "valueAlarm_t".into(),
fields: vec![
(
"lowAlarmLimit".into(),
FieldDesc::Scalar(ScalarType::Double),
),
(
"lowWarningLimit".into(),
FieldDesc::Scalar(ScalarType::Double),
),
(
"highWarningLimit".into(),
FieldDesc::Scalar(ScalarType::Double),
),
(
"highAlarmLimit".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::{AlarmInfo, EnumInfo, Snapshot};
fn test_snapshot(value: EpicsValue) -> Snapshot {
Snapshot {
value,
alarm: AlarmInfo {
status: 0,
severity: 0,
},
timestamp: UNIX_EPOCH,
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()
}),
control: Some(ControlInfo {
upper_ctrl_limit: 100.0,
lower_ctrl_limit: 0.0,
}),
enums: None,
user_tag: 0,
}
}
#[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_enum_structure() {
let snap = Snapshot {
value: EpicsValue::Enum(1),
alarm: AlarmInfo {
status: 0,
severity: 0,
},
timestamp: UNIX_EPOCH,
display: None,
control: None,
enums: Some(EnumInfo {
strings: vec!["Off".into(), "On".into()],
}),
user_tag: 0,
};
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::UShort(idx))) = val.get_field("index") {
assert_eq!(*idx, 1);
} else {
panic!("expected index scalar");
}
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");
}
}
#[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 snap = Snapshot {
value: EpicsValue::Enum(2),
alarm: AlarmInfo {
status: 0,
severity: 0,
},
timestamp: UNIX_EPOCH,
display: None,
control: None,
enums: Some(EnumInfo {
strings: vec!["A".into(), "B".into(), "C".into()],
}),
user_tag: 0,
};
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 field_desc_nt_enum_index_ushort() {
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::UShort)))
));
} else {
panic!("expected value structure");
}
} else {
panic!("expected structure");
}
}
}