1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Implementation of data change filters, and `Deadband`
use crate::{DataChangeFilter, DataChangeTrigger, DataValue, DeadbandType, StatusCode, Variant};
#[derive(Debug, Clone)]
/// Parsed percent deadband. Percent deadband works by applying a deadband
/// based on the EURange of the node.
pub struct PercentDeadband {
// Computed from high and low. high = low + range
low: f64,
range: f64,
// Trigger, between 0 and 1
trigger: f64,
}
#[derive(Debug, Clone)]
/// Deadband type, used in data change filters.
pub enum Deadband {
/// No deadband.
None,
/// A positive number deadbanding on absolute change.
Absolute(f64),
/// Deadband based on a percent of EURange.
Percent(PercentDeadband),
}
impl Deadband {
fn is_changed_option(&self, v1: Option<&Variant>, v2: Option<&Variant>) -> bool {
match (v1, v2) {
(Some(_), None) | (None, Some(_)) => true,
(None, None) => {
// If it's always none then it hasn't changed
false
}
(Some(v1), Some(v2)) => {
// Otherwise test the filter
self.is_changed(v1, v2)
}
}
}
fn is_changed(&self, v1: &Variant, v2: &Variant) -> bool {
if let (Some(v1), Some(v2)) = (v1.as_array(), v2.as_array()) {
// From the standard:
// "If the item is an array of values, the entire array is returned if
// any array element exceeds the AbsoluteDeadband, or the size or dimension of the
// array changes.". Equivalent for PercentDeadband.
if v1.len() != v2.len() {
return true;
}
for (v1, v2) in v1.iter().zip(v2.iter()) {
if self.is_changed(v1, v2) {
return true;
}
}
false
} else {
match self {
Deadband::None => v1 != v2,
Deadband::Absolute(deadband) => {
let (Some(v1), Some(v2)) = (v1.as_f64(), v2.as_f64()) else {
return true;
};
(v1 - v2).abs() > *deadband
}
Deadband::Percent(percent_deadband) => {
let (Some(v1), Some(v2)) = (v1.as_f64(), v2.as_f64()) else {
return true;
};
let v1_pct = (v1 - percent_deadband.low) / percent_deadband.range;
let v2_pct = (v2 - percent_deadband.low) / percent_deadband.range;
(v1_pct - v2_pct).abs() > percent_deadband.trigger
}
}
}
}
}
#[derive(Debug, Clone)]
/// Parsed variant of the data change filter that can be directly checked for data changes.
pub struct ParsedDataChangeFilter {
/// What is considered a change.
pub trigger: DataChangeTrigger,
/// Deadband filter.
pub deadband: Deadband,
}
impl ParsedDataChangeFilter {
/// Check if this data change filter considers `v1` different from `v2`.
pub fn is_changed(&self, v1: &DataValue, v2: &DataValue) -> bool {
match self.trigger {
DataChangeTrigger::Status => v1.status != v2.status,
DataChangeTrigger::StatusValue => {
v1.status != v2.status
|| self
.deadband
.is_changed_option(v1.value.as_ref(), v2.value.as_ref())
}
DataChangeTrigger::StatusValueTimestamp => {
v1.status != v2.status
|| v1.source_timestamp != v2.source_timestamp
|| v1.source_picoseconds != v2.source_picoseconds
|| self
.deadband
.is_changed_option(v1.value.as_ref(), v2.value.as_ref())
}
}
}
/// Parse from a raw data change filter and the EURange of the node, if present.
pub fn parse(
filter: DataChangeFilter,
eu_range: Option<(f64, f64)>,
) -> Result<Self, StatusCode> {
let as_int: i32 = filter
.deadband_type
.try_into()
.map_err(|_| StatusCode::BadDeadbandFilterInvalid)?;
let ty =
DeadbandType::try_from(as_int).map_err(|_| StatusCode::BadDeadbandFilterInvalid)?;
let deadband = match ty {
DeadbandType::None => Deadband::None,
DeadbandType::Absolute => {
if filter.deadband_value < 0.0 {
return Err(StatusCode::BadDeadbandFilterInvalid);
}
Deadband::Absolute(filter.deadband_value)
}
DeadbandType::Percent => {
if filter.deadband_value < 0.0 || filter.deadband_value > 100.0 {
return Err(StatusCode::BadDeadbandFilterInvalid);
}
let Some((low, high)) = eu_range else {
return Err(StatusCode::BadDeadbandFilterInvalid);
};
if low >= high {
return Err(StatusCode::BadDeadbandFilterInvalid);
}
Deadband::Percent(PercentDeadband {
low,
range: high - low,
trigger: filter.deadband_value / 100.0,
})
}
};
Ok(Self {
trigger: filter.trigger,
deadband,
})
}
}