use crate::types::{DbFieldType, EpicsValue, PvString, WallTime};
#[derive(Debug, Clone, Default)]
pub struct AlarmInfo {
pub status: u16,
pub severity: u16,
pub ackt: Option<u16>,
pub acks: Option<u16>,
pub amsg: String,
}
#[derive(Debug, Clone, Default)]
pub struct DisplayInfo {
pub units: PvString,
pub precision: i16,
pub upper_disp_limit: f64,
pub lower_disp_limit: f64,
pub upper_alarm_limit: f64,
pub upper_warning_limit: f64,
pub lower_warning_limit: f64,
pub lower_alarm_limit: f64,
pub form: i16,
pub description: String,
}
#[derive(Debug, Clone, Default)]
pub struct ControlInfo {
pub upper_ctrl_limit: f64,
pub lower_ctrl_limit: f64,
}
#[derive(Debug, Clone, Default)]
pub struct EnumStringForm {
pub slots: Vec<PvString>,
pub overflow: EnumOverflow,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EnumOverflow {
Text(PvString),
Decimal,
}
impl Default for EnumOverflow {
fn default() -> Self {
Self::Text(PvString::new())
}
}
impl EnumStringForm {
pub fn render(&self, index: u16) -> PvString {
match self.slots.get(index as usize) {
Some(slot) => slot.clone(),
None => match &self.overflow {
EnumOverflow::Text(text) => text.clone(),
EnumOverflow::Decimal => PvString::from(index.to_string().as_str()),
},
}
}
pub fn menu(choices: impl IntoIterator<Item = PvString>) -> Self {
Self {
slots: choices.into_iter().collect(),
overflow: EnumOverflow::Decimal,
}
}
pub fn device(choices: Vec<PvString>) -> Self {
Self {
slots: choices,
overflow: EnumOverflow::Text(PvString::new()),
}
}
pub fn states(slots: Vec<PvString>, sentinel: PvString) -> Self {
Self {
slots,
overflow: EnumOverflow::Text(sentinel),
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct EnumInfo {
pub strings: Vec<PvString>,
pub string_form: EnumStringForm,
}
impl EnumInfo {
pub fn new(strings: Vec<PvString>) -> Self {
Self {
string_form: EnumStringForm {
slots: strings.clone(),
overflow: EnumOverflow::default(),
},
strings,
}
}
pub fn with_string_form(strings: Vec<PvString>, string_form: EnumStringForm) -> Self {
Self {
strings,
string_form,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Snapshot {
pub value: EpicsValue,
pub alarm: AlarmInfo,
pub timestamp: WallTime,
pub display: Option<DisplayInfo>,
pub control: Option<ControlInfo>,
pub enums: Option<EnumInfo>,
pub properties: PropertySupport,
pub user_tag: i32,
pub class_name: Option<String>,
}
pub fn apply_nsec_mask(snap: &mut Snapshot, nsec_mask: u64) {
if nsec_mask == 0 {
return;
}
let mask = nsec_mask as u32;
let secs = snap.timestamp.unix_secs();
let nanos = snap.timestamp.subsec_nanos();
snap.user_tag = (nanos & mask) as i32;
snap.timestamp = WallTime::from_unix(secs, nanos & !mask);
}
impl Snapshot {
pub fn new(
value: EpicsValue,
status: u16,
severity: u16,
timestamp: impl Into<WallTime>,
) -> Self {
Self {
value,
alarm: AlarmInfo {
status,
severity,
ackt: None,
acks: None,
amsg: String::new(),
},
timestamp: timestamp.into(),
display: None,
control: None,
enums: None,
properties: PropertySupport::NONE,
user_tag: 0,
class_name: None,
}
}
pub fn units(&self) -> Option<&PvString> {
let d = self.display.as_ref()?;
self.properties.units.then_some(&d.units)
}
pub fn precision(&self) -> Option<i16> {
let d = self.display.as_ref()?;
self.properties.precision.then_some(d.precision)
}
pub fn graphic_limits(&self) -> Option<(f64, f64)> {
let d = self.display.as_ref()?;
self.properties
.graphic_double
.then_some((d.lower_disp_limit, d.upper_disp_limit))
}
pub fn control_limits(&self) -> Option<(f64, f64)> {
let c = self.control.as_ref()?;
self.properties
.control_double
.then_some((c.lower_ctrl_limit, c.upper_ctrl_limit))
}
pub fn display_limits(&self) -> Option<(f64, f64)> {
let d = self.display.as_ref()?;
self.properties
.graphic_double
.then_some((d.lower_disp_limit, d.upper_disp_limit))
}
pub fn alarm_limits(&self) -> Option<(f64, f64, f64, f64)> {
let d = self.display.as_ref()?;
self.properties.alarm_double.then_some((
d.lower_alarm_limit,
d.lower_warning_limit,
d.upper_warning_limit,
d.upper_alarm_limit,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PropertySupport {
pub units: bool,
pub precision: bool,
pub graphic_double: bool,
pub control_double: bool,
pub alarm_double: bool,
pub enum_strs: bool,
}
impl PropertySupport {
pub const NONE: Self = Self {
units: false,
precision: false,
graphic_double: false,
control_double: false,
alarm_double: false,
enum_strs: false,
};
pub const NUMERIC: Self = Self {
units: true,
precision: true,
graphic_double: true,
control_double: true,
alarm_double: true,
enum_strs: false,
};
pub fn narrowed_to_field(self, field_type: DbFieldType, is_menu_field: bool) -> Self {
Self {
precision: self.precision
&& matches!(field_type, DbFieldType::Float | DbFieldType::Double),
enum_strs: matches!(field_type, DbFieldType::Enum) && (self.enum_strs || is_menu_field),
..self
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DbrClass {
Plain,
Sts,
Time,
Gr,
Ctrl,
}
impl DbrClass {
pub fn from_dbr_type(dbr_type: u16) -> Option<Self> {
match dbr_type {
0..=6 => Some(DbrClass::Plain),
7..=13 => Some(DbrClass::Sts),
14..=20 => Some(DbrClass::Time),
21..=27 => Some(DbrClass::Gr),
28..=34 => Some(DbrClass::Ctrl),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::SystemTime;
#[test]
fn nsec_mask_extracts_user_tag() {
let nanos: u32 = 123_456_789; let ts = WallTime::from_unix(42, nanos);
let mut snap = Snapshot::new(EpicsValue::Double(0.0), 0, 0, ts);
apply_nsec_mask(&mut snap, (1 << 20) - 1);
let mask: u32 = (1 << 20) - 1;
let expected_user_tag = (nanos & mask) as i32;
let expected_nanos = nanos & !mask;
assert_eq!(snap.user_tag, expected_user_tag);
assert_eq!(snap.timestamp.unix_secs(), 42);
assert_eq!(snap.timestamp.subsec_nanos(), expected_nanos);
}
#[test]
fn nsec_mask_n1_keeps_high_bits() {
let ts = WallTime::from_unix(0, 7); let mut snap = Snapshot::new(EpicsValue::Double(0.0), 0, 0, ts);
apply_nsec_mask(&mut snap, 1);
assert_eq!(snap.user_tag, 1);
assert_eq!(snap.timestamp.subsec_nanos(), 6);
}
#[test]
fn nsec_mask_zero_is_a_no_op() {
let ts = WallTime::from_unix(9, 123_456_789);
let mut snap = Snapshot::new(EpicsValue::Double(0.0), 0, 0, ts);
snap.user_tag = -7; apply_nsec_mask(&mut snap, 0);
assert_eq!(snap.user_tag, -7);
assert_eq!(snap.timestamp.unix_secs(), 9);
assert_eq!(snap.timestamp.subsec_nanos(), 123_456_789);
}
#[test]
fn nsec_mask_31_moves_all_nanoseconds_to_user_tag() {
let nanos: u32 = 999_999_999;
let ts = WallTime::from_unix(1, nanos);
let mut snap = Snapshot::new(EpicsValue::Double(0.0), 0, 0, ts);
apply_nsec_mask(&mut snap, (1u64 << 31) - 1);
assert_eq!(snap.user_tag, nanos as i32);
assert_eq!(snap.timestamp.subsec_nanos(), 0);
assert_eq!(snap.timestamp.unix_secs(), 1);
}
#[test]
fn nsec_mask_wider_than_32_bits_clears_all_nanoseconds() {
let nanos: u32 = 123_456_789;
let ts = WallTime::from_unix(3, nanos);
let mut snap = Snapshot::new(EpicsValue::Double(0.0), 0, 0, ts);
apply_nsec_mask(&mut snap, (1u64 << 40) - 1);
assert_eq!(snap.user_tag, nanos as i32);
assert_eq!(snap.timestamp.subsec_nanos(), 0);
}
#[test]
fn test_snapshot_construction() {
let snap = Snapshot::new(EpicsValue::Double(42.0), 0, 0, SystemTime::UNIX_EPOCH);
assert_eq!(snap.alarm.status, 0);
assert_eq!(snap.alarm.severity, 0);
assert!(snap.display.is_none());
assert!(snap.control.is_none());
assert!(snap.enums.is_none());
}
#[test]
fn test_snapshot_with_metadata() {
let mut snap = Snapshot::new(EpicsValue::Double(3.14), 1, 2, SystemTime::UNIX_EPOCH);
snap.display = Some(DisplayInfo {
units: "degC".into(),
precision: 3,
upper_disp_limit: 100.0,
lower_disp_limit: -50.0,
upper_alarm_limit: 90.0,
upper_warning_limit: 80.0,
lower_warning_limit: -20.0,
lower_alarm_limit: -40.0,
..Default::default()
});
snap.control = Some(ControlInfo {
upper_ctrl_limit: 100.0,
lower_ctrl_limit: -50.0,
});
let disp = snap.display.as_ref().unwrap();
assert_eq!(disp.units, "degC");
assert_eq!(disp.precision, 3);
assert_eq!(snap.control.as_ref().unwrap().upper_ctrl_limit, 100.0);
}
#[test]
fn test_dbr_class_plain() {
for t in 0..=6 {
assert_eq!(DbrClass::from_dbr_type(t), Some(DbrClass::Plain));
}
}
#[test]
fn test_dbr_class_all_ranges() {
for t in 7..=13 {
assert_eq!(DbrClass::from_dbr_type(t), Some(DbrClass::Sts));
}
for t in 14..=20 {
assert_eq!(DbrClass::from_dbr_type(t), Some(DbrClass::Time));
}
for t in 21..=27 {
assert_eq!(DbrClass::from_dbr_type(t), Some(DbrClass::Gr));
}
for t in 28..=34 {
assert_eq!(DbrClass::from_dbr_type(t), Some(DbrClass::Ctrl));
}
}
#[test]
fn test_dbr_class_invalid() {
assert_eq!(DbrClass::from_dbr_type(35), None);
assert_eq!(DbrClass::from_dbr_type(100), None);
assert_eq!(DbrClass::from_dbr_type(u16::MAX), None);
}
}