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