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