Skip to main content

ad_plugins/
attribute.rs

1//! NDPluginAttribute: extracts a single named attribute value from each array.
2//!
3//! This plugin reads an attribute by name from incoming arrays and tracks
4//! the value and cumulative sum. It supports special pseudo-attribute names
5//! "NDArrayUniqueId" and "NDArrayTimeStamp" that read from the array header
6//! instead of the attribute list.
7
8use ad_core::ndarray::NDArray;
9use ad_core::ndarray_pool::NDArrayPool;
10use ad_core::plugin::runtime::{NDPluginProcess, ParamUpdate, ProcessResult};
11use asyn_rs::error::AsynError;
12use asyn_rs::param::ParamType;
13use asyn_rs::port::PortDriverBase;
14
15/// Parameter indices for NDPluginAttribute.
16#[derive(Clone, Copy, Default)]
17pub struct AttributeParams {
18    pub attr_name: usize,
19    pub value: usize,
20    pub value_sum: usize,
21    pub reset: usize,
22}
23
24/// Processor that extracts a single attribute value from each array.
25pub struct AttributeProcessor {
26    attr_name: String,
27    value: f64,
28    value_sum: f64,
29    params: AttributeParams,
30}
31
32impl AttributeProcessor {
33    pub fn new(attr_name: &str) -> Self {
34        Self {
35            attr_name: attr_name.to_string(),
36            value: 0.0,
37            value_sum: 0.0,
38            params: AttributeParams::default(),
39        }
40    }
41
42    /// Reset the accumulated sum to zero.
43    pub fn reset(&mut self) {
44        self.value_sum = 0.0;
45    }
46
47    /// Current extracted value.
48    pub fn value(&self) -> f64 {
49        self.value
50    }
51
52    /// Current accumulated sum.
53    pub fn value_sum(&self) -> f64 {
54        self.value_sum
55    }
56
57    /// The attribute name being tracked.
58    pub fn attr_name(&self) -> &str {
59        &self.attr_name
60    }
61
62    /// Set the attribute name to track.
63    pub fn set_attr_name(&mut self, name: &str) {
64        self.attr_name = name.to_string();
65    }
66
67    /// Extract the value for the configured attribute from an array.
68    fn extract_value(&self, array: &NDArray) -> Option<f64> {
69        match self.attr_name.as_str() {
70            "NDArrayUniqueId" => Some(array.unique_id as f64),
71            "NDArrayTimeStamp" => Some(array.timestamp.as_f64()),
72            _ => {
73                array.attributes.get(&self.attr_name)
74                    .and_then(|attr| attr.value.as_f64())
75            }
76        }
77    }
78}
79
80impl NDPluginProcess for AttributeProcessor {
81    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
82        if let Some(val) = self.extract_value(array) {
83            self.value = val;
84            self.value_sum += val;
85        }
86
87        let updates = vec![
88            ParamUpdate::float64(self.params.value, self.value),
89            ParamUpdate::float64(self.params.value_sum, self.value_sum),
90        ];
91
92        ProcessResult::sink(updates)
93    }
94
95    fn plugin_type(&self) -> &str {
96        "NDPluginAttribute"
97    }
98
99    fn register_params(&mut self, base: &mut PortDriverBase) -> Result<(), AsynError> {
100        self.params.attr_name = base.create_param("ATTR_NAME", ParamType::Octet)?;
101        self.params.value = base.create_param("ATTR_VAL", ParamType::Float64)?;
102        self.params.value_sum = base.create_param("ATTR_VAL_SUM", ParamType::Float64)?;
103        self.params.reset = base.create_param("ATTR_RESET", ParamType::Int32)?;
104        Ok(())
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use ad_core::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
112    use ad_core::ndarray::{NDDataType, NDDimension};
113
114    fn make_array_with_attr(name: &str, value: f64, uid: i32) -> NDArray {
115        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
116        arr.unique_id = uid;
117        arr.attributes.add(NDAttribute {
118            name: name.to_string(),
119            description: String::new(),
120            source: NDAttrSource::Driver,
121            value: NDAttrValue::Float64(value),
122        });
123        arr
124    }
125
126    #[test]
127    fn test_extract_named_attribute() {
128        let mut proc = AttributeProcessor::new("Temperature");
129        let pool = NDArrayPool::new(1_000_000);
130
131        let arr = make_array_with_attr("Temperature", 25.5, 1);
132        let result = proc.process_array(&arr, &pool);
133
134        assert!(result.output_arrays.is_empty(), "attribute plugin is a sink");
135        assert!((proc.value() - 25.5).abs() < 1e-10);
136        assert!((proc.value_sum() - 25.5).abs() < 1e-10);
137    }
138
139    #[test]
140    fn test_sum_accumulation() {
141        let mut proc = AttributeProcessor::new("Intensity");
142        let pool = NDArrayPool::new(1_000_000);
143
144        let arr1 = make_array_with_attr("Intensity", 10.0, 1);
145        proc.process_array(&arr1, &pool);
146        assert!((proc.value_sum() - 10.0).abs() < 1e-10);
147
148        let arr2 = make_array_with_attr("Intensity", 20.0, 2);
149        proc.process_array(&arr2, &pool);
150        assert!((proc.value() - 20.0).abs() < 1e-10);
151        assert!((proc.value_sum() - 30.0).abs() < 1e-10);
152
153        let arr3 = make_array_with_attr("Intensity", 5.0, 3);
154        proc.process_array(&arr3, &pool);
155        assert!((proc.value() - 5.0).abs() < 1e-10);
156        assert!((proc.value_sum() - 35.0).abs() < 1e-10);
157    }
158
159    #[test]
160    fn test_reset() {
161        let mut proc = AttributeProcessor::new("Count");
162        let pool = NDArrayPool::new(1_000_000);
163
164        let arr1 = make_array_with_attr("Count", 100.0, 1);
165        proc.process_array(&arr1, &pool);
166        assert!((proc.value_sum() - 100.0).abs() < 1e-10);
167
168        proc.reset();
169        assert!((proc.value_sum() - 0.0).abs() < 1e-10);
170        // value retains last reading
171        assert!((proc.value() - 100.0).abs() < 1e-10);
172
173        let arr2 = make_array_with_attr("Count", 50.0, 2);
174        proc.process_array(&arr2, &pool);
175        assert!((proc.value_sum() - 50.0).abs() < 1e-10);
176    }
177
178    #[test]
179    fn test_special_attr_unique_id() {
180        let mut proc = AttributeProcessor::new("NDArrayUniqueId");
181        let pool = NDArrayPool::new(1_000_000);
182
183        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
184        arr.unique_id = 42;
185
186        proc.process_array(&arr, &pool);
187        assert!((proc.value() - 42.0).abs() < 1e-10);
188    }
189
190    #[test]
191    fn test_special_attr_timestamp() {
192        let mut proc = AttributeProcessor::new("NDArrayTimeStamp");
193        let pool = NDArrayPool::new(1_000_000);
194
195        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
196        arr.timestamp = ad_core::timestamp::EpicsTimestamp { sec: 100, nsec: 500_000_000 };
197
198        proc.process_array(&arr, &pool);
199        assert!((proc.value() - 100.5).abs() < 1e-9);
200    }
201
202    #[test]
203    fn test_missing_attribute() {
204        let mut proc = AttributeProcessor::new("NonExistent");
205        let pool = NDArrayPool::new(1_000_000);
206
207        let arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
208        proc.process_array(&arr, &pool);
209
210        // value remains at default (0.0) when attribute is not found
211        assert!((proc.value() - 0.0).abs() < 1e-10);
212        assert!((proc.value_sum() - 0.0).abs() < 1e-10);
213    }
214
215    #[test]
216    fn test_string_attribute_ignored() {
217        let mut proc = AttributeProcessor::new("Label");
218        let pool = NDArrayPool::new(1_000_000);
219
220        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
221        arr.attributes.add(NDAttribute {
222            name: "Label".to_string(),
223            description: String::new(),
224            source: NDAttrSource::Driver,
225            value: NDAttrValue::String("hello".to_string()),
226        });
227
228        proc.process_array(&arr, &pool);
229        // String attrs return None from as_f64(), so value stays 0.0
230        assert!((proc.value() - 0.0).abs() < 1e-10);
231    }
232
233    #[test]
234    fn test_int32_attribute() {
235        let mut proc = AttributeProcessor::new("Counter");
236        let pool = NDArrayPool::new(1_000_000);
237
238        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
239        arr.attributes.add(NDAttribute {
240            name: "Counter".to_string(),
241            description: String::new(),
242            source: NDAttrSource::Driver,
243            value: NDAttrValue::Int32(7),
244        });
245
246        proc.process_array(&arr, &pool);
247        assert!((proc.value() - 7.0).abs() < 1e-10);
248    }
249
250    #[test]
251    fn test_set_attr_name() {
252        let mut proc = AttributeProcessor::new("A");
253        assert_eq!(proc.attr_name(), "A");
254
255        proc.set_attr_name("B");
256        assert_eq!(proc.attr_name(), "B");
257
258        let pool = NDArrayPool::new(1_000_000);
259        let arr = make_array_with_attr("B", 99.0, 1);
260        proc.process_array(&arr, &pool);
261        assert!((proc.value() - 99.0).abs() < 1e-10);
262    }
263}