1use ad_core_rs::ndarray::NDArray;
8use ad_core_rs::ndarray_pool::NDArrayPool;
9use ad_core_rs::plugin::runtime::{
10 NDPluginProcess, ParamChangeResult, ParamChangeValue, ParamUpdate, PluginParamSnapshot,
11 ProcessResult,
12};
13use asyn_rs::error::AsynError;
14use asyn_rs::param::ParamType;
15use asyn_rs::port::PortDriverBase;
16
17use crate::time_series::{TimeSeriesData, TimeSeriesSender};
18
19#[derive(Clone, Copy, Default)]
21pub struct AttributeParams {
22 pub attr_name: usize,
23 pub value: usize,
24 pub value_sum: usize,
25 pub reset: usize,
26}
27
28#[derive(Clone)]
30struct AttrChannel {
31 name: String,
32 value: f64,
33 value_sum: f64,
34}
35
36impl Default for AttrChannel {
37 fn default() -> Self {
38 Self {
39 name: String::new(),
40 value: 0.0,
41 value_sum: 0.0,
42 }
43 }
44}
45
46impl AttrChannel {
47 fn extract_value(&self, array: &NDArray) -> Option<f64> {
48 if self.name.is_empty() {
49 return None;
50 }
51 match self.name.as_str() {
52 "NDArrayUniqueId" => Some(array.unique_id as f64),
53 "NDArrayTimeStamp" => Some(array.timestamp.as_f64()),
54 "NDArrayEpicsTSSec" => Some(array.timestamp.sec as f64),
55 "NDArrayEpicsTSnSec" => Some(array.timestamp.nsec as f64),
56 _ => array
57 .attributes
58 .get(&self.name)
59 .and_then(|attr| attr.value.as_f64()),
60 }
61 }
62}
63
64pub struct AttributeProcessor {
66 channels: Vec<AttrChannel>,
67 params: AttributeParams,
68 ts_sender: Option<TimeSeriesSender>,
69}
70
71impl AttributeProcessor {
72 pub fn new(attr_name: &str, num_channels: usize) -> Self {
75 let mut channels = vec![AttrChannel::default(); num_channels.max(1)];
76 channels[0].name = attr_name.to_string();
77 Self {
78 channels,
79 params: AttributeParams::default(),
80 ts_sender: None,
81 }
82 }
83
84 pub fn set_ts_sender(&mut self, sender: TimeSeriesSender) {
85 self.ts_sender = Some(sender);
86 }
87
88 pub fn params(&self) -> &AttributeParams {
90 &self.params
91 }
92
93 pub fn reset(&mut self) {
95 for ch in self.channels.iter_mut() {
96 ch.value = 0.0;
97 ch.value_sum = 0.0;
98 }
99 }
100
101 pub fn value(&self) -> f64 {
103 self.channels[0].value
104 }
105
106 pub fn value_sum(&self) -> f64 {
108 self.channels[0].value_sum
109 }
110
111 pub fn attr_name(&self) -> &str {
113 &self.channels[0].name
114 }
115
116 pub fn set_attr_name(&mut self, name: &str) {
118 self.channels[0].name = name.to_string();
119 }
120}
121
122impl NDPluginProcess for AttributeProcessor {
123 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
124 let mut updates = Vec::new();
125
126 for (i, ch) in self.channels.iter_mut().enumerate() {
127 if ch.name.is_empty() {
128 continue;
129 }
130 if let Some(val) = ch.extract_value(array) {
135 ch.value = val;
136 ch.value_sum += val;
137 let addr = i as i32;
138 updates.push(ParamUpdate::float64_addr(self.params.value, addr, ch.value));
139 updates.push(ParamUpdate::float64_addr(
140 self.params.value_sum,
141 addr,
142 ch.value_sum,
143 ));
144 }
145 }
146
147 if let Some(ref sender) = self.ts_sender {
149 let values: Vec<f64> = self.channels.iter().map(|ch| ch.value).collect();
150 let _ = sender.try_send(TimeSeriesData { values });
151 }
152
153 ProcessResult::sink(updates)
154 }
155
156 fn plugin_type(&self) -> &str {
157 "NDPluginAttribute"
158 }
159
160 fn does_array_callbacks(&self) -> bool {
163 false
164 }
165
166 fn register_params(&mut self, base: &mut PortDriverBase) -> Result<(), AsynError> {
167 self.params.attr_name = base.create_param("ATTR_ATTRNAME", ParamType::Octet)?;
168 self.params.value = base.create_param("ATTR_VAL", ParamType::Float64)?;
169 self.params.value_sum = base.create_param("ATTR_VAL_SUM", ParamType::Float64)?;
170 self.params.reset = base.create_param("ATTR_RESET", ParamType::Int32)?;
171 Ok(())
172 }
173
174 fn on_param_change(
175 &mut self,
176 reason: usize,
177 params: &PluginParamSnapshot,
178 ) -> ParamChangeResult {
179 let addr = params.addr as usize;
180
181 if reason == self.params.attr_name {
182 if addr < self.channels.len() {
183 if let ParamChangeValue::Octet(s) = ¶ms.value {
184 self.channels[addr].name = s.clone();
185 }
186 }
187 } else if reason == self.params.reset {
188 let mut updates = Vec::new();
191 for (i, ch) in self.channels.iter_mut().enumerate() {
192 ch.value = 0.0;
193 ch.value_sum = 0.0;
194 let a = i as i32;
195 updates.push(ParamUpdate::float64_addr(self.params.value, a, 0.0));
196 updates.push(ParamUpdate::float64_addr(self.params.value_sum, a, 0.0));
197 }
198 return ParamChangeResult::updates(updates);
199 }
200
201 ParamChangeResult::updates(vec![])
202 }
203}
204
205pub fn attr_ts_channel_names(num_channels: usize) -> Vec<String> {
209 (0..num_channels.max(1))
210 .map(|i| {
211 if i == 0 {
212 "TSArrayValue".to_string()
213 } else {
214 format!("TSArrayValue{i}")
215 }
216 })
217 .collect()
218}
219
220pub fn create_attribute_runtime(
223 port_name: &str,
224 pool: std::sync::Arc<ad_core_rs::ndarray_pool::NDArrayPool>,
225 queue_size: usize,
226 ndarray_port: &str,
227 wiring: std::sync::Arc<ad_core_rs::plugin::wiring::WiringRegistry>,
228 ts_registry: &crate::time_series::TsReceiverRegistry,
229 max_attributes: i32,
230) -> (
231 ad_core_rs::plugin::runtime::PluginRuntimeHandle,
232 std::thread::JoinHandle<()>,
233) {
234 let num_channels = max_attributes.max(1) as usize;
238 let num_addr = max_attributes.max(2) as usize;
239
240 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
241
242 let mut processor = AttributeProcessor::new("", num_channels);
243 processor.set_ts_sender(ts_tx);
244
245 let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime_multi_addr(
246 port_name,
247 processor,
248 pool,
249 queue_size,
250 ndarray_port,
251 wiring,
252 num_addr,
253 );
254
255 ts_registry.store(port_name, ts_rx, attr_ts_channel_names(num_channels));
256
257 (handle, data_jh)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
264 use ad_core_rs::ndarray::{NDDataType, NDDimension};
265
266 fn make_array_with_attr(name: &str, value: f64, uid: i32) -> NDArray {
267 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
268 arr.unique_id = uid;
269 arr.attributes.add(NDAttribute::new_static(
270 name,
271 String::new(),
272 NDAttrSource::Driver,
273 NDAttrValue::Float64(value),
274 ));
275 arr
276 }
277
278 #[test]
279 fn test_extract_named_attribute() {
280 let mut proc = AttributeProcessor::new("Temperature", 8);
281 let pool = NDArrayPool::new(1_000_000);
282
283 let arr = make_array_with_attr("Temperature", 25.5, 1);
284 let result = proc.process_array(&arr, &pool);
285
286 assert!(
287 result.output_arrays.is_empty(),
288 "attribute plugin is a sink"
289 );
290 assert!((proc.value() - 25.5).abs() < 1e-10);
291 assert!((proc.value_sum() - 25.5).abs() < 1e-10);
292 }
293
294 #[test]
295 fn test_sum_accumulation() {
296 let mut proc = AttributeProcessor::new("Intensity", 8);
297 let pool = NDArrayPool::new(1_000_000);
298
299 let arr1 = make_array_with_attr("Intensity", 10.0, 1);
300 proc.process_array(&arr1, &pool);
301 assert!((proc.value_sum() - 10.0).abs() < 1e-10);
302
303 let arr2 = make_array_with_attr("Intensity", 20.0, 2);
304 proc.process_array(&arr2, &pool);
305 assert!((proc.value() - 20.0).abs() < 1e-10);
306 assert!((proc.value_sum() - 30.0).abs() < 1e-10);
307 }
308
309 #[test]
310 fn test_reset() {
311 let mut proc = AttributeProcessor::new("Count", 8);
312 let pool = NDArrayPool::new(1_000_000);
313
314 let arr1 = make_array_with_attr("Count", 100.0, 1);
315 proc.process_array(&arr1, &pool);
316 assert!((proc.value_sum() - 100.0).abs() < 1e-10);
317
318 proc.reset();
319 assert!((proc.value_sum() - 0.0).abs() < 1e-10);
320 assert!((proc.value() - 0.0).abs() < 1e-10);
321 }
322
323 #[test]
324 fn test_special_attr_unique_id() {
325 let mut proc = AttributeProcessor::new("NDArrayUniqueId", 8);
326 let pool = NDArrayPool::new(1_000_000);
327
328 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
329 arr.unique_id = 42;
330
331 proc.process_array(&arr, &pool);
332 assert!((proc.value() - 42.0).abs() < 1e-10);
333 }
334
335 #[test]
336 fn test_special_attr_timestamp() {
337 let mut proc = AttributeProcessor::new("NDArrayTimeStamp", 8);
338 let pool = NDArrayPool::new(1_000_000);
339
340 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
341 arr.timestamp = ad_core_rs::timestamp::EpicsTimestamp {
342 sec: 100,
343 nsec: 500_000_000,
344 };
345
346 proc.process_array(&arr, &pool);
347 assert!((proc.value() - 100.5).abs() < 1e-9);
348 }
349
350 #[test]
351 fn test_missing_attribute() {
352 let mut proc = AttributeProcessor::new("NonExistent", 8);
353 let pool = NDArrayPool::new(1_000_000);
354
355 let arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
356 proc.process_array(&arr, &pool);
357
358 assert!((proc.value() - 0.0).abs() < 1e-10);
359 assert!((proc.value_sum() - 0.0).abs() < 1e-10);
360 }
361
362 #[test]
363 fn test_string_attribute_ignored() {
364 let mut proc = AttributeProcessor::new("Label", 8);
365 let pool = NDArrayPool::new(1_000_000);
366
367 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
368 arr.attributes.add(NDAttribute::new_static(
369 "Label",
370 String::new(),
371 NDAttrSource::Driver,
372 NDAttrValue::String("hello".to_string()),
373 ));
374
375 proc.process_array(&arr, &pool);
376 assert!((proc.value() - 0.0).abs() < 1e-10);
377 }
378
379 #[test]
380 fn test_int32_attribute() {
381 let mut proc = AttributeProcessor::new("Counter", 8);
382 let pool = NDArrayPool::new(1_000_000);
383
384 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
385 arr.attributes.add(NDAttribute::new_static(
386 "Counter",
387 String::new(),
388 NDAttrSource::Driver,
389 NDAttrValue::Int32(7),
390 ));
391
392 proc.process_array(&arr, &pool);
393 assert!((proc.value() - 7.0).abs() < 1e-10);
394 }
395
396 #[test]
397 fn test_channel_count_follows_max_attributes() {
398 assert_eq!(attr_ts_channel_names(16).len(), 16);
401 assert_eq!(attr_ts_channel_names(2).len(), 2);
402 assert_eq!(attr_ts_channel_names(0).len(), 1); let mut proc = AttributeProcessor::new("Temp", 16);
405 proc.params.value = 2;
406 proc.params.value_sum = 3;
407 proc.channels[15].name = "High".to_string();
408
409 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
410 arr.attributes.add(NDAttribute::new_static(
411 "Temp",
412 String::new(),
413 NDAttrSource::Driver,
414 NDAttrValue::Float64(1.0),
415 ));
416 arr.attributes.add(NDAttribute::new_static(
417 "High",
418 String::new(),
419 NDAttrSource::Driver,
420 NDAttrValue::Float64(9.0),
421 ));
422
423 let r = proc.process_array(&arr, &NDArrayPool::new(1_000_000));
424 assert!(
426 r.param_updates.iter().any(|u| matches!(
427 u,
428 ParamUpdate::Float64 { reason: 2, addr: 15, value } if *value == 9.0
429 )),
430 "channel 15 must post with a 16-channel processor"
431 );
432 }
433
434 #[test]
435 fn test_missing_attribute_skips_post() {
436 let mut proc = AttributeProcessor::new("Temp", 8);
439 proc.params.value = 2;
440 proc.params.value_sum = 3;
441 let pool = NDArrayPool::new(1_000_000);
442
443 let r1 = proc.process_array(&make_array_with_attr("Temp", 5.0, 1), &pool);
444 assert!(
445 r1.param_updates
446 .iter()
447 .any(|u| matches!(u, ParamUpdate::Float64 { reason: 2, .. })),
448 "present attribute must post ATTR_VAL"
449 );
450
451 let bare = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
452 let r2 = proc.process_array(&bare, &pool);
453 assert!(
454 !r2.param_updates
455 .iter()
456 .any(|u| matches!(u, ParamUpdate::Float64 { reason: 2, .. })),
457 "missing attribute must not re-post stale ATTR_VAL"
458 );
459 assert!((proc.value() - 5.0).abs() < 1e-10);
461 }
462
463 #[test]
464 fn test_reset_clears_on_zero_write() {
465 let mut proc = AttributeProcessor::new("Count", 8);
468 proc.params.value = 2;
469 proc.params.value_sum = 3;
470 proc.params.reset = 7;
471
472 let pool = NDArrayPool::new(1_000_000);
473 proc.process_array(&make_array_with_attr("Count", 100.0, 1), &pool);
474 assert!((proc.value_sum() - 100.0).abs() < 1e-10);
475
476 let snapshot = PluginParamSnapshot {
477 enable_callbacks: true,
478 reason: 7,
479 addr: 0,
480 value: ParamChangeValue::Int32(0),
481 };
482 let result = proc.on_param_change(7, &snapshot);
483
484 assert!((proc.value() - 0.0).abs() < 1e-10);
485 assert!((proc.value_sum() - 0.0).abs() < 1e-10);
486 assert!(
487 result.param_updates.iter().any(|u| matches!(
488 u,
489 ParamUpdate::Float64 {
490 reason: 2,
491 value,
492 ..
493 } if *value == 0.0
494 )),
495 "zero write must post cleared ATTR_VAL"
496 );
497 }
498
499 #[test]
500 fn test_set_attr_name() {
501 let mut proc = AttributeProcessor::new("A", 8);
502 assert_eq!(proc.attr_name(), "A");
503
504 proc.set_attr_name("B");
505 assert_eq!(proc.attr_name(), "B");
506
507 let pool = NDArrayPool::new(1_000_000);
508 let arr = make_array_with_attr("B", 99.0, 1);
509 proc.process_array(&arr, &pool);
510 assert!((proc.value() - 99.0).abs() < 1e-10);
511 }
512}