use crate::server::record::{AlarmSeverity, CommonFields};
use crate::types::DbFieldType;
pub mod simm;
pub mod alarm_status {
pub const NO_ALARM: u16 = 0;
pub const READ_ALARM: u16 = 1;
pub const WRITE_ALARM: u16 = 2;
pub const HIHI_ALARM: u16 = 3;
pub const HIGH_ALARM: u16 = 4;
pub const LOLO_ALARM: u16 = 5;
pub const LOW_ALARM: u16 = 6;
pub const STATE_ALARM: u16 = 7;
pub const COS_ALARM: u16 = 8;
pub const COMM_ALARM: u16 = 9;
pub const TIMEOUT_ALARM: u16 = 10;
pub const HW_LIMIT_ALARM: u16 = 11;
pub const CALC_ALARM: u16 = 12;
pub const SCAN_ALARM: u16 = 13;
pub const LINK_ALARM: u16 = 14;
pub const SOFT_ALARM: u16 = 15;
pub const BAD_SUB_ALARM: u16 = 16;
pub const UDF_ALARM: u16 = 17;
pub const DISABLE_ALARM: u16 = 18;
pub const SIMM_ALARM: u16 = 19;
pub const READ_ACCESS_ALARM: u16 = 20;
pub const WRITE_ACCESS_ALARM: u16 = 21;
}
pub fn alarm_condition_string(status: u16) -> &'static str {
const STRINGS: [&str; 22] = [
"NO_ALARM",
"READ",
"WRITE",
"HIHI",
"HIGH",
"LOLO",
"LOW",
"STATE",
"COS",
"COMM",
"TIMEOUT",
"HWLIMIT",
"CALC",
"SCAN",
"LINK",
"SOFT",
"BAD_SUB",
"UDF",
"DISABLE",
"SIMM",
"READ_ACCESS",
"WRITE_ACCESS",
];
STRINGS.get(status as usize).copied().unwrap_or("")
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EventMask(u16);
impl EventMask {
pub const NONE: Self = Self(0);
pub const VALUE: Self = Self(0x01);
pub const LOG: Self = Self(0x02);
pub const ALARM: Self = Self(0x04);
pub const PROPERTY: Self = Self(0x08);
pub fn is_empty(self) -> bool {
self.0 == 0
}
pub fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn intersects(self, other: Self) -> bool {
(self.0 & other.0) != 0
}
pub fn bits(self) -> u16 {
self.0
}
pub fn from_bits(bits: u16) -> Self {
Self(bits)
}
}
impl std::ops::BitOr for EventMask {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for EventMask {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl std::ops::BitAnd for EventMask {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
Self(self.0 & rhs.0)
}
}
pub const RECGBL_POSTED_ALARM_FIELDS: [&str; 4] = ["SEVR", "STAT", "AMSG", "ACKS"];
pub struct AlarmResetResult {
pub alarm_changed: bool,
pub prev_sevr: AlarmSeverity,
pub prev_stat: u16,
pub amsg_changed: bool,
pub acks_posted: bool,
}
pub fn rec_gbl_set_sevr(common: &mut CommonFields, stat: u16, sevr: AlarmSeverity) {
if (sevr as u16) > (common.nsev as u16) {
common.nsta = stat;
common.nsev = sevr;
common.namsg.clear();
}
}
pub fn rec_gbl_set_sevr_raw(common: &mut CommonFields, stat: u16, raw_sevr: u16) {
if raw_sevr > (common.nsev as u16) {
common.nsta = stat;
common.nsev = AlarmSeverity::from_u16(raw_sevr);
common.namsg.clear();
}
}
pub fn rec_gbl_set_sevr_msg(
common: &mut CommonFields,
stat: u16,
sevr: AlarmSeverity,
msg: impl Into<String>,
) {
if (sevr as u16) > (common.nsev as u16) {
common.nsta = stat;
common.nsev = sevr;
common.namsg = msg.into();
}
}
pub fn rec_gbl_set_link_alarm(common: &mut CommonFields, field: &str) {
rec_gbl_set_sevr_msg(
common,
alarm_status::LINK_ALARM,
AlarmSeverity::Invalid,
format!("field {field}"),
);
}
pub fn rec_gbl_reset_alarms(common: &mut CommonFields) -> AlarmResetResult {
let prev_sevr = common.sevr;
let prev_stat = common.stat;
if (common.nsev as u16) > (AlarmSeverity::Invalid as u16) {
common.nsev = AlarmSeverity::Invalid;
}
let amsg_changed = common.namsg != common.amsg;
if amsg_changed {
common.amsg = std::mem::take(&mut common.namsg);
}
common.sevr = common.nsev;
common.stat = common.nsta;
common.nsev = AlarmSeverity::NoAlarm;
common.nsta = alarm_status::NO_ALARM;
let alarm_changed = common.sevr != prev_sevr || common.stat != prev_stat;
let mut acks_posted = false;
if (alarm_changed || amsg_changed)
&& (!common.ackt || (common.sevr as u16) >= (common.acks as u16))
{
common.acks = common.sevr;
acks_posted = true;
}
AlarmResetResult {
alarm_changed,
prev_sevr,
prev_stat,
amsg_changed,
acks_posted,
}
}
pub fn udf_alarm_active(udf: u8, exact_one: bool) -> bool {
if exact_one { udf == 1 } else { udf != 0 }
}
pub fn rec_gbl_check_udf(common: &mut CommonFields, exact_one: bool, msg: &str) {
if udf_alarm_active(common.udf, exact_one) {
rec_gbl_set_sevr_msg(
common,
alarm_status::UDF_ALARM,
AlarmSeverity::from_u16(common.udfs as u16),
msg,
);
}
}
pub fn get_time_stamp(tse: i16, device_time: std::time::SystemTime) -> std::time::SystemTime {
match tse {
0 => {
crate::runtime::general_time::get_current()
}
-1 => {
crate::runtime::general_time::get_event(-1)
}
-2 => {
device_time
}
_ => {
crate::runtime::general_time::get_event(tse as i32)
}
}
}
fn get_max_range_values(
field_type: Option<DbFieldType>,
is_menu_field: bool,
) -> Option<(f64, f64)> {
if is_menu_field {
return None;
}
let field_type = field_type?;
Some(match field_type {
DbFieldType::Char => (f64::from(i8::MAX), f64::from(i8::MIN)),
DbFieldType::UChar => (f64::from(u8::MAX), 0.0),
DbFieldType::Short => (f64::from(i16::MAX), f64::from(i16::MIN)),
DbFieldType::Enum | DbFieldType::UShort => (f64::from(u16::MAX), 0.0),
DbFieldType::Long => (2147483647.0, -2147483648.0),
DbFieldType::ULong => (4294967295.0, 0.0),
DbFieldType::Int64 => (9223372036854775808.0, -9223372036854775808.0),
DbFieldType::UInt64 => (18446744073709551615.0, 0.0),
DbFieldType::Float => (1e30, -1e30),
DbFieldType::Double => (1e300, -1e300),
DbFieldType::String => return None,
})
}
pub fn rec_gbl_get_graphic_double(
field_type: Option<DbFieldType>,
is_menu_field: bool,
) -> Option<(f64, f64)> {
get_max_range_values(field_type, is_menu_field)
}
pub fn rec_gbl_get_control_double(
field_type: Option<DbFieldType>,
is_menu_field: bool,
) -> Option<(f64, f64)> {
get_max_range_values(field_type, is_menu_field)
}
pub fn rec_gbl_get_alarm_double() -> (f64, f64, f64, f64) {
(f64::NAN, f64::NAN, f64::NAN, f64::NAN)
}
pub fn rec_gbl_get_prec(field_type: Option<DbFieldType>, seed: i16) -> i16 {
match field_type {
Some(
DbFieldType::Char
| DbFieldType::UChar
| DbFieldType::Short
| DbFieldType::UShort
| DbFieldType::Long
| DbFieldType::ULong
| DbFieldType::Int64
| DbFieldType::UInt64,
) => 0,
Some(DbFieldType::Float | DbFieldType::Double) => {
if !(0..=15).contains(&seed) {
15
} else {
seed
}
}
_ => seed,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_max_range_values_matches_the_c_switch() {
let r = |t| get_max_range_values(Some(t), false);
assert_eq!(r(DbFieldType::Char), Some((127.0, -128.0)));
assert_eq!(r(DbFieldType::UChar), Some((255.0, 0.0)));
assert_eq!(r(DbFieldType::Short), Some((32767.0, -32768.0)));
assert_eq!(r(DbFieldType::UShort), Some((65535.0, 0.0)));
assert_eq!(r(DbFieldType::Enum), Some((65535.0, 0.0)));
assert_eq!(r(DbFieldType::Long), Some((2147483647.0, -2147483648.0)));
assert_eq!(r(DbFieldType::ULong), Some((4294967295.0, 0.0)));
assert_eq!(r(DbFieldType::Float), Some((1e30, -1e30)));
assert_eq!(r(DbFieldType::Double), Some((1e300, -1e300)));
}
#[test]
fn rec_gbl_get_prec_matches_the_c_switch() {
for t in [
DbFieldType::Char,
DbFieldType::UChar,
DbFieldType::Short,
DbFieldType::UShort,
DbFieldType::Long,
DbFieldType::ULong,
DbFieldType::Int64,
DbFieldType::UInt64,
] {
assert_eq!(rec_gbl_get_prec(Some(t), 7), 0, "{t:?}");
assert_eq!(rec_gbl_get_prec(Some(t), 0), 0, "{t:?}");
}
for t in [DbFieldType::Float, DbFieldType::Double] {
assert_eq!(rec_gbl_get_prec(Some(t), 7), 7, "{t:?}");
assert_eq!(rec_gbl_get_prec(Some(t), 0), 0, "{t:?}");
assert_eq!(rec_gbl_get_prec(Some(t), 15), 15, "{t:?}");
assert_eq!(rec_gbl_get_prec(Some(t), 16), 15, "{t:?}");
assert_eq!(rec_gbl_get_prec(Some(t), -1), 15, "{t:?}");
}
}
#[test]
fn rec_gbl_get_prec_leaves_the_seed_where_c_has_no_case() {
for t in [
Some(DbFieldType::String),
Some(DbFieldType::Enum),
None,
] {
assert_eq!(rec_gbl_get_prec(t, 7), 7, "{t:?}");
assert_eq!(rec_gbl_get_prec(t, 99), 99, "{t:?}");
}
}
#[test]
fn int64_upper_range_is_two_to_the_63_not_int64_max() {
let (upper, lower) = get_max_range_values(Some(DbFieldType::Int64), false).unwrap();
assert_eq!(upper, 9223372036854775808.0);
assert_eq!(upper, 2.0f64.powi(63));
assert!(upper > i64::MAX as f64 || upper == i64::MAX as f64 + 1.0);
assert_eq!(lower, -9223372036854775808.0);
assert_eq!(lower, i64::MIN as f64);
let (upper, lower) = get_max_range_values(Some(DbFieldType::UInt64), false).unwrap();
assert_eq!(upper, 18446744073709551615.0);
assert_eq!(lower, 0.0);
}
#[test]
fn types_with_no_case_in_the_c_switch_get_no_range() {
assert_eq!(get_max_range_values(Some(DbFieldType::String), false), None);
assert_eq!(get_max_range_values(None, false), None);
assert_eq!(get_max_range_values(Some(DbFieldType::Enum), true), None);
}
#[test]
fn graphic_and_control_defaults_are_the_same_table() {
for t in [
DbFieldType::Char,
DbFieldType::UChar,
DbFieldType::Short,
DbFieldType::UShort,
DbFieldType::Enum,
DbFieldType::Long,
DbFieldType::ULong,
DbFieldType::Int64,
DbFieldType::UInt64,
DbFieldType::Float,
DbFieldType::Double,
DbFieldType::String,
] {
assert_eq!(
rec_gbl_get_graphic_double(Some(t), false),
rec_gbl_get_control_double(Some(t), false),
"graphic/control default diverged for {t:?}",
);
}
}
#[test]
fn alarm_default_is_four_nans() {
let (hihi, high, low, lolo) = rec_gbl_get_alarm_double();
assert!(hihi.is_nan() && high.is_nan() && low.is_nan() && lolo.is_nan());
}
#[test]
fn alarm_condition_string_matches_c_table() {
assert_eq!(alarm_condition_string(alarm_status::NO_ALARM), "NO_ALARM");
assert_eq!(alarm_condition_string(alarm_status::HIHI_ALARM), "HIHI");
assert_eq!(
alarm_condition_string(alarm_status::HW_LIMIT_ALARM),
"HWLIMIT"
);
assert_eq!(alarm_condition_string(alarm_status::LINK_ALARM), "LINK");
assert_eq!(
alarm_condition_string(alarm_status::WRITE_ACCESS_ALARM),
"WRITE_ACCESS"
);
assert_eq!(alarm_condition_string(22), "");
assert_eq!(alarm_condition_string(9999), "");
}
#[test]
fn test_set_sevr_raises() {
let mut common = CommonFields::default();
assert_eq!(common.nsev, AlarmSeverity::NoAlarm);
rec_gbl_set_sevr(&mut common, alarm_status::HIGH_ALARM, AlarmSeverity::Minor);
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::HIGH_ALARM);
}
#[test]
fn test_set_sevr_only_raises() {
let mut common = CommonFields::default();
rec_gbl_set_sevr(&mut common, alarm_status::HIHI_ALARM, AlarmSeverity::Major);
rec_gbl_set_sevr(&mut common, alarm_status::HIGH_ALARM, AlarmSeverity::Minor);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.nsta, alarm_status::HIHI_ALARM);
}
#[test]
fn test_reset_alarms_transfers() {
let mut common = CommonFields::default();
rec_gbl_set_sevr(&mut common, alarm_status::HIHI_ALARM, AlarmSeverity::Major);
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.alarm_changed);
assert_eq!(result.prev_sevr, AlarmSeverity::NoAlarm);
assert_eq!(common.sevr, AlarmSeverity::Major);
assert_eq!(common.stat, alarm_status::HIHI_ALARM);
assert_eq!(common.nsev, AlarmSeverity::NoAlarm);
assert_eq!(common.nsta, alarm_status::NO_ALARM);
}
#[test]
fn test_reset_alarms_no_change() {
let mut common = CommonFields::default();
let first = rec_gbl_reset_alarms(&mut common);
assert!(first.alarm_changed);
assert_eq!(common.stat, alarm_status::NO_ALARM);
let result = rec_gbl_reset_alarms(&mut common);
assert!(!result.alarm_changed);
}
#[test]
fn test_reset_alarms_clears() {
let mut common = CommonFields::default();
common.sevr = AlarmSeverity::Major;
common.stat = alarm_status::HIHI_ALARM;
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.alarm_changed);
assert_eq!(result.prev_sevr, AlarmSeverity::Major);
assert_eq!(common.sevr, AlarmSeverity::NoAlarm);
}
#[test]
fn test_check_udf() {
let mut common = CommonFields::default();
assert!(common.udf != 0);
rec_gbl_check_udf(&mut common, false, "");
assert_eq!(common.nsev, AlarmSeverity::Invalid);
assert_eq!(common.nsta, alarm_status::UDF_ALARM);
assert_eq!(common.namsg, "");
}
#[test]
fn test_check_udf_uses_udfs() {
let mut common = CommonFields::default();
assert!(common.udf != 0);
common.udfs = AlarmSeverity::Minor as i16;
rec_gbl_check_udf(&mut common, false, "");
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::UDF_ALARM);
}
#[test]
fn test_check_udf_exact_one_ignores_non_one_byte() {
assert!(udf_alarm_active(255, false), "truthy: 255 raises");
assert!(
!udf_alarm_active(255, true),
"exact-one: 255 does not raise"
);
assert!(udf_alarm_active(1, true), "exact-one: 1 still raises");
assert!(!udf_alarm_active(0, true), "exact-one: 0 never raises");
let mut common = CommonFields::default();
common.udf = 255;
rec_gbl_check_udf(&mut common, true, "");
assert_eq!(common.nsev, AlarmSeverity::NoAlarm);
assert_eq!(common.nsta, alarm_status::NO_ALARM);
}
#[test]
fn test_check_udf_default_udfs_is_invalid() {
let common = CommonFields::default();
assert_eq!(common.udfs, AlarmSeverity::Invalid as i16);
}
#[test]
fn set_sevr_msg_writes_namsg_when_raised() {
let mut common = CommonFields::default();
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::HIGH_ALARM,
AlarmSeverity::Minor,
"above HIGH threshold",
);
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::HIGH_ALARM);
assert_eq!(common.namsg, "above HIGH threshold");
}
#[test]
fn set_sevr_msg_keeps_higher_message() {
let mut common = CommonFields::default();
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::HIHI_ALARM,
AlarmSeverity::Major,
"above HIHI",
);
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::HIGH_ALARM,
AlarmSeverity::Minor,
"would-be lower message",
);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.namsg, "above HIHI");
}
#[test]
fn reset_alarms_transfers_amsg_and_clears_namsg() {
let mut common = CommonFields::default();
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::HIHI_ALARM,
AlarmSeverity::Major,
"above HIHI",
);
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.alarm_changed);
assert!(result.amsg_changed);
assert_eq!(common.amsg, "above HIHI");
assert_eq!(common.namsg, "");
}
#[test]
fn reset_alarms_clears_amsg_when_no_new_message() {
let mut common = CommonFields::default();
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::HIGH_ALARM,
AlarmSeverity::Minor,
"first cycle",
);
rec_gbl_reset_alarms(&mut common);
assert_eq!(common.amsg, "first cycle");
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.amsg_changed);
assert_eq!(common.amsg, "");
}
#[test]
fn reset_alarms_keeps_amsg_when_the_same_message_repeats() {
let mut common = CommonFields::default();
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::CALC_ALARM,
AlarmSeverity::Invalid,
"calcPerform",
);
let c1 = rec_gbl_reset_alarms(&mut common);
assert!(c1.amsg_changed);
assert_eq!(common.sevr, AlarmSeverity::Invalid);
assert_eq!(common.amsg, "calcPerform");
assert_eq!(common.namsg, "");
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::CALC_ALARM,
AlarmSeverity::Invalid,
"calcPerform",
);
let c2 = rec_gbl_reset_alarms(&mut common);
assert!(!c2.alarm_changed);
assert!(
!c2.amsg_changed,
"an unchanged message is not an AMSG event"
);
assert_eq!(common.amsg, "calcPerform");
assert_eq!(
common.namsg, "calcPerform",
"C does NOT clear namsg when it equals amsg (recGbl.c:191-195)"
);
let c3 = rec_gbl_reset_alarms(&mut common);
assert!(c3.alarm_changed, "severity dropped INVALID -> NO_ALARM");
assert!(
!c3.amsg_changed,
"namsg still equals amsg, so recGblResetAlarms does not touch it"
);
assert_eq!(common.sevr, AlarmSeverity::NoAlarm);
assert_eq!(
common.amsg, "calcPerform",
"AMSG keeps the last alarm text after the alarm clears"
);
}
#[test]
fn test_event_mask_ops() {
let mask = EventMask::VALUE | EventMask::ALARM;
assert!(mask.contains(EventMask::VALUE));
assert!(mask.contains(EventMask::ALARM));
assert!(!mask.contains(EventMask::LOG));
assert!(mask.intersects(EventMask::VALUE));
assert!(!mask.intersects(EventMask::PROPERTY));
assert!(!mask.is_empty());
assert!(EventMask::NONE.is_empty());
}
#[test]
fn reset_alarms_raises_acks_to_new_severity() {
let mut common = CommonFields::default();
assert_eq!(common.acks, AlarmSeverity::NoAlarm);
assert!(common.ackt, "ACKT defaults to true (sticky)");
rec_gbl_set_sevr(&mut common, alarm_status::HIHI_ALARM, AlarmSeverity::Major);
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.alarm_changed);
assert!(result.acks_posted, "first alarm raise must update acks");
assert_eq!(common.acks, AlarmSeverity::Major);
}
#[test]
fn reset_alarms_keeps_acks_sticky_when_ackt_true_and_severity_drops() {
let mut common = CommonFields::default();
rec_gbl_set_sevr(&mut common, alarm_status::HIHI_ALARM, AlarmSeverity::Major);
rec_gbl_reset_alarms(&mut common);
assert_eq!(common.acks, AlarmSeverity::Major);
rec_gbl_set_sevr(&mut common, alarm_status::HIGH_ALARM, AlarmSeverity::Minor);
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.alarm_changed);
assert!(
!result.acks_posted,
"ackt=true must NOT lower acks when severity drops"
);
assert_eq!(common.acks, AlarmSeverity::Major);
}
#[test]
fn reset_alarms_posts_acks_on_a_stat_only_transition_at_constant_severity() {
let mut common = CommonFields::default();
assert!(common.ackt, "ACKT defaults to true");
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::LINK_ALARM,
AlarmSeverity::Invalid,
"field INP",
);
let c1 = rec_gbl_reset_alarms(&mut common);
assert!(c1.acks_posted);
assert_eq!(common.acks, AlarmSeverity::Invalid);
rec_gbl_set_sevr_msg(
&mut common,
alarm_status::CALC_ALARM,
AlarmSeverity::Invalid,
"calcPerform",
);
let c2 = rec_gbl_reset_alarms(&mut common);
assert!(c2.alarm_changed, "STAT moved LINK -> CALC");
assert_eq!(common.sevr, c2.prev_sevr, "SEVR did not move");
assert_eq!(
common.acks,
AlarmSeverity::Invalid,
"ACKS value is unchanged — it was already equal"
);
assert!(
c2.acks_posted,
"C posts ACKS whenever the ack rule fires, not only on a value change"
);
}
#[test]
fn reset_alarms_posts_no_acks_when_the_rule_does_not_fire() {
let mut common = CommonFields::default();
rec_gbl_set_sevr(&mut common, alarm_status::HIHI_ALARM, AlarmSeverity::Major);
rec_gbl_reset_alarms(&mut common);
assert_eq!(common.acks, AlarmSeverity::Major);
rec_gbl_set_sevr(&mut common, alarm_status::HIGH_ALARM, AlarmSeverity::Minor);
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.alarm_changed);
assert!(!result.acks_posted);
assert_eq!(common.acks, AlarmSeverity::Major);
}
#[test]
fn reset_alarms_drops_acks_when_ackt_false() {
let mut common = CommonFields::default();
common.ackt = false; rec_gbl_set_sevr(&mut common, alarm_status::HIHI_ALARM, AlarmSeverity::Major);
rec_gbl_reset_alarms(&mut common);
assert_eq!(common.acks, AlarmSeverity::Major);
rec_gbl_set_sevr(&mut common, alarm_status::HIGH_ALARM, AlarmSeverity::Minor);
let result = rec_gbl_reset_alarms(&mut common);
assert!(result.acks_posted);
assert_eq!(
common.acks,
AlarmSeverity::Minor,
"ackt=false drops acks with severity"
);
}
}