#[derive(Debug, Clone, PartialEq)]
pub enum NDAttrSource {
Driver,
Param {
port_name: String,
param_name: String,
},
EpicsPV,
Function,
Constant,
Undefined,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NDAttrDataType {
Int8,
UInt8,
Int16,
UInt16,
Int32,
UInt32,
Int64,
UInt64,
Float32,
Float64,
String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum NDAttrValue {
Int8(i8),
UInt8(u8),
Int16(i16),
UInt16(u16),
Int32(i32),
UInt32(u32),
Int64(i64),
UInt64(u64),
Float32(f32),
Float64(f64),
String(String),
Undefined,
}
impl NDAttrValue {
pub fn data_type(&self) -> NDAttrDataType {
match self {
Self::Int8(_) => NDAttrDataType::Int8,
Self::UInt8(_) => NDAttrDataType::UInt8,
Self::Int16(_) => NDAttrDataType::Int16,
Self::UInt16(_) => NDAttrDataType::UInt16,
Self::Int32(_) => NDAttrDataType::Int32,
Self::UInt32(_) => NDAttrDataType::UInt32,
Self::Int64(_) => NDAttrDataType::Int64,
Self::UInt64(_) => NDAttrDataType::UInt64,
Self::Float32(_) => NDAttrDataType::Float32,
Self::Float64(_) => NDAttrDataType::Float64,
Self::String(_) => NDAttrDataType::String,
Self::Undefined => NDAttrDataType::Int32,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Self::Int8(v) => Some(*v as f64),
Self::UInt8(v) => Some(*v as f64),
Self::Int16(v) => Some(*v as f64),
Self::UInt16(v) => Some(*v as f64),
Self::Int32(v) => Some(*v as f64),
Self::UInt32(v) => Some(*v as f64),
Self::Int64(v) => Some(*v as f64),
Self::UInt64(v) => Some(*v as f64),
Self::Float32(v) => Some(*v as f64),
Self::Float64(v) => Some(*v),
Self::String(_) | Self::Undefined => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Self::Int8(v) => Some(*v as i64),
Self::UInt8(v) => Some(*v as i64),
Self::Int16(v) => Some(*v as i64),
Self::UInt16(v) => Some(*v as i64),
Self::Int32(v) => Some(*v as i64),
Self::UInt32(v) => Some(*v as i64),
Self::Int64(v) => Some(*v),
Self::UInt64(v) => Some(*v as i64),
Self::Float32(v) => Some(*v as i64),
Self::Float64(v) => Some(*v as i64),
Self::String(_) | Self::Undefined => None,
}
}
pub fn as_string(&self) -> String {
match self {
Self::Int8(v) => v.to_string(),
Self::UInt8(v) => v.to_string(),
Self::Int16(v) => v.to_string(),
Self::UInt16(v) => v.to_string(),
Self::Int32(v) => v.to_string(),
Self::UInt32(v) => v.to_string(),
Self::Int64(v) => v.to_string(),
Self::UInt64(v) => v.to_string(),
Self::Float32(v) => v.to_string(),
Self::Float64(v) => v.to_string(),
Self::String(v) => v.clone(),
Self::Undefined => String::new(),
}
}
}
pub trait NDAttributeSource: Send + Sync {
fn evaluate(&self) -> NDAttrValue;
fn as_any(&self) -> &dyn std::any::Any;
}
#[derive(Debug)]
pub struct LiveValueCell {
value: std::sync::RwLock<NDAttrValue>,
}
impl LiveValueCell {
pub fn new(initial: NDAttrValue) -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
value: std::sync::RwLock::new(initial),
})
}
pub fn set(&self, value: NDAttrValue) {
if let Ok(mut guard) = self.value.write() {
*guard = value;
}
}
pub fn get(&self) -> NDAttrValue {
self.value
.read()
.map(|g| g.clone())
.unwrap_or(NDAttrValue::Undefined)
}
}
impl NDAttributeSource for LiveValueCell {
fn evaluate(&self) -> NDAttrValue {
self.get()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug)]
pub struct ParamAttributeSource {
pub param_name: String,
pub addr: i32,
cell: std::sync::Arc<LiveValueCell>,
}
impl ParamAttributeSource {
pub fn new(param_name: impl Into<String>, addr: i32) -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
param_name: param_name.into(),
addr,
cell: LiveValueCell::new(NDAttrValue::Undefined),
})
}
pub fn cell(&self) -> &std::sync::Arc<LiveValueCell> {
&self.cell
}
}
impl NDAttributeSource for ParamAttributeSource {
fn evaluate(&self) -> NDAttrValue {
self.cell.get()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub type NDAttributeFunction = std::sync::Arc<dyn Fn(&str) -> NDAttrValue + Send + Sync>;
#[derive(Default)]
pub struct NDAttributeFunctionRegistry {
funcs: std::sync::RwLock<std::collections::HashMap<String, NDAttributeFunction>>,
}
impl NDAttributeFunctionRegistry {
pub fn new() -> std::sync::Arc<Self> {
std::sync::Arc::new(Self::default())
}
pub fn register(
&self,
name: impl Into<String>,
func: impl Fn(&str) -> NDAttrValue + Send + Sync + 'static,
) {
if let Ok(mut g) = self.funcs.write() {
g.insert(name.into(), std::sync::Arc::new(func));
}
}
pub fn find(&self, name: &str) -> Option<NDAttributeFunction> {
self.funcs.read().ok()?.get(name).cloned()
}
}
pub struct FunctionAttributeSource {
registry: std::sync::Arc<NDAttributeFunctionRegistry>,
function_name: String,
function_param: String,
}
impl FunctionAttributeSource {
pub fn new(
registry: std::sync::Arc<NDAttributeFunctionRegistry>,
function_name: impl Into<String>,
function_param: impl Into<String>,
) -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
registry,
function_name: function_name.into(),
function_param: function_param.into(),
})
}
}
impl NDAttributeSource for FunctionAttributeSource {
fn evaluate(&self) -> NDAttrValue {
match self.registry.find(&self.function_name) {
Some(f) => f(&self.function_param),
None => NDAttrValue::Undefined,
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug)]
pub struct EpicsPvAttributeSource {
pub pv_name: String,
cell: std::sync::Arc<LiveValueCell>,
}
impl EpicsPvAttributeSource {
pub fn new(pv_name: impl Into<String>) -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
pv_name: pv_name.into(),
cell: LiveValueCell::new(NDAttrValue::Undefined),
})
}
pub fn cell(&self) -> &std::sync::Arc<LiveValueCell> {
&self.cell
}
}
impl NDAttributeSource for EpicsPvAttributeSource {
fn evaluate(&self) -> NDAttrValue {
self.cell.get()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "ioc")]
pub fn spawn_ca_monitor(
client: std::sync::Arc<epics_ca_rs::client::CaClient>,
source: std::sync::Arc<EpicsPvAttributeSource>,
) -> tokio::task::JoinHandle<()> {
let pv_name = source.pv_name.clone();
let cell = source.cell().clone();
tokio::spawn(async move {
let _ = client
.camonitor(&pv_name, |value| {
cell.set(epics_value_to_nd_attr(&value));
})
.await;
})
}
#[cfg(feature = "ioc")]
fn epics_value_to_nd_attr(value: &epics_ca_rs::EpicsValue) -> NDAttrValue {
use epics_ca_rs::EpicsValue as E;
match value {
E::String(s) => NDAttrValue::String(s.clone()),
E::Short(v) => NDAttrValue::Int16(*v),
E::Float(v) => NDAttrValue::Float32(*v),
E::Enum(v) => NDAttrValue::UInt16(*v),
E::Char(v) => NDAttrValue::UInt8(*v),
E::Long(v) => NDAttrValue::Int32(*v),
E::Double(v) => NDAttrValue::Float64(*v),
E::Int64(v) => NDAttrValue::Int64(*v),
E::UInt64(v) => NDAttrValue::UInt64(*v),
E::ShortArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::Int16(*v)),
E::FloatArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::Float32(*v)),
E::EnumArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::UInt16(*v)),
E::DoubleArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::Float64(*v)),
E::LongArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::Int32(*v)),
E::CharArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::UInt8(*v)),
E::Int64Array(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::Int64(*v)),
E::UInt64Array(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::UInt64(*v)),
E::StringArray(a) => a
.first()
.map_or(NDAttrValue::Undefined, |v| NDAttrValue::String(v.clone())),
}
}
#[derive(Clone)]
pub struct NDAttribute {
pub name: String,
pub description: String,
pub source: NDAttrSource,
pub value: NDAttrValue,
pub source_impl: Option<std::sync::Arc<dyn NDAttributeSource>>,
}
impl std::fmt::Debug for NDAttribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NDAttribute")
.field("name", &self.name)
.field("description", &self.description)
.field("source", &self.source)
.field("value", &self.value)
.field("has_source_impl", &self.source_impl.is_some())
.finish()
}
}
impl NDAttribute {
pub fn new_static(
name: impl Into<String>,
description: impl Into<String>,
source: NDAttrSource,
value: NDAttrValue,
) -> Self {
Self {
name: name.into(),
description: description.into(),
source,
value,
source_impl: None,
}
}
pub fn new_with_source(
name: impl Into<String>,
description: impl Into<String>,
source: NDAttrSource,
source_impl: std::sync::Arc<dyn NDAttributeSource>,
) -> Self {
let value = source_impl.evaluate();
Self {
name: name.into(),
description: description.into(),
source,
value,
source_impl: Some(source_impl),
}
}
pub fn update(&mut self) {
if let Some(src) = &self.source_impl {
self.value = src.evaluate();
}
}
pub fn param_source(&self) -> Option<&ParamAttributeSource> {
self.source_impl
.as_ref()
.and_then(|s| s.as_any().downcast_ref::<ParamAttributeSource>())
}
pub fn epics_pv_source(&self) -> Option<&EpicsPvAttributeSource> {
self.source_impl
.as_ref()
.and_then(|s| s.as_any().downcast_ref::<EpicsPvAttributeSource>())
}
}
#[derive(Debug, Clone, Default)]
pub struct NDAttributeList {
attrs: Vec<NDAttribute>,
}
impl NDAttributeList {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, attr: NDAttribute) {
if let Some(existing) = self.attrs.iter_mut().find(|a| a.name == attr.name) {
*existing = attr;
} else {
self.attrs.push(attr);
}
}
pub fn get(&self, name: &str) -> Option<&NDAttribute> {
self.attrs.iter().find(|a| a.name == name)
}
pub fn remove(&mut self, name: &str) -> bool {
let len_before = self.attrs.len();
self.attrs.retain(|a| a.name != name);
self.attrs.len() < len_before
}
pub fn clear(&mut self) {
self.attrs.clear();
}
pub fn iter(&self) -> impl Iterator<Item = &NDAttribute> {
self.attrs.iter()
}
pub fn len(&self) -> usize {
self.attrs.len()
}
pub fn is_empty(&self) -> bool {
self.attrs.is_empty()
}
pub fn copy_from(&mut self, other: &NDAttributeList) {
for attr in other.iter() {
self.add(attr.clone());
}
}
pub fn update_values(&mut self) {
for attr in self.attrs.iter_mut() {
attr.update();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_get() {
let mut list = NDAttributeList::new();
list.add(NDAttribute {
name: "ColorMode".into(),
description: "Color mode".into(),
source: NDAttrSource::Driver,
value: NDAttrValue::Int32(0),
source_impl: None,
});
let attr = list.get("ColorMode").unwrap();
assert_eq!(attr.value, NDAttrValue::Int32(0));
}
#[test]
fn test_replace_existing() {
let mut list = NDAttributeList::new();
list.add(NDAttribute {
name: "Gain".into(),
description: "".into(),
source: NDAttrSource::Driver,
value: NDAttrValue::Float64(1.0),
source_impl: None,
});
list.add(NDAttribute {
name: "Gain".into(),
description: "".into(),
source: NDAttrSource::Driver,
value: NDAttrValue::Float64(2.5),
source_impl: None,
});
assert_eq!(list.len(), 1);
assert_eq!(list.get("Gain").unwrap().value, NDAttrValue::Float64(2.5));
}
#[test]
fn test_iter() {
let mut list = NDAttributeList::new();
list.add(NDAttribute {
name: "A".into(),
description: "".into(),
source: NDAttrSource::Constant,
value: NDAttrValue::Int32(1),
source_impl: None,
});
list.add(NDAttribute {
name: "B".into(),
description: "".into(),
source: NDAttrSource::Constant,
value: NDAttrValue::String("hello".into()),
source_impl: None,
});
let names: Vec<_> = list.iter().map(|a| a.name.as_str()).collect();
assert_eq!(names, vec!["A", "B"]);
}
#[test]
fn test_get_missing() {
let list = NDAttributeList::new();
assert!(list.get("nope").is_none());
}
#[test]
fn test_empty() {
let list = NDAttributeList::new();
assert!(list.is_empty());
assert_eq!(list.len(), 0);
}
#[test]
fn test_all_data_types() {
let values = vec![
NDAttrValue::Int8(-1),
NDAttrValue::UInt8(255),
NDAttrValue::Int16(-100),
NDAttrValue::UInt16(1000),
NDAttrValue::Int32(-50000),
NDAttrValue::UInt32(50000),
NDAttrValue::Int64(-1_000_000),
NDAttrValue::UInt64(1_000_000),
NDAttrValue::Float32(3.14),
NDAttrValue::Float64(2.718),
NDAttrValue::String("test".into()),
];
for v in &values {
assert_eq!(v.data_type(), v.data_type());
}
}
#[test]
fn test_source_tracking() {
let attr = NDAttribute {
name: "temp".into(),
description: "temperature".into(),
source: NDAttrSource::Param {
port_name: "SIM1".into(),
param_name: "TEMPERATURE".into(),
},
value: NDAttrValue::Float64(25.0),
source_impl: None,
};
match &attr.source {
NDAttrSource::Param {
port_name,
param_name,
} => {
assert_eq!(port_name, "SIM1");
assert_eq!(param_name, "TEMPERATURE");
}
_ => panic!("wrong source type"),
}
}
#[test]
fn test_value_conversions() {
let v = NDAttrValue::Int32(42);
assert_eq!(v.as_f64(), Some(42.0));
assert_eq!(v.as_i64(), Some(42));
assert_eq!(v.as_string(), "42");
let s = NDAttrValue::String("hello".into());
assert_eq!(s.as_f64(), None);
assert_eq!(s.as_i64(), None);
assert_eq!(s.as_string(), "hello");
}
#[test]
fn test_remove() {
let mut list = NDAttributeList::new();
list.add(NDAttribute {
name: "A".into(),
description: "".into(),
source: NDAttrSource::Driver,
value: NDAttrValue::Int32(1),
source_impl: None,
});
assert!(list.remove("A"));
assert!(list.is_empty());
assert!(!list.remove("A"));
}
struct CountingSource {
counter: std::sync::atomic::AtomicI32,
}
impl NDAttributeSource for CountingSource {
fn evaluate(&self) -> NDAttrValue {
let n = self
.counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ 1;
NDAttrValue::Int32(n)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[test]
fn test_attribute_update_reevaluates_source() {
let src = std::sync::Arc::new(CountingSource {
counter: std::sync::atomic::AtomicI32::new(0),
});
let mut attr =
NDAttribute::new_with_source("live", "live source", NDAttrSource::Function, src);
assert_eq!(attr.value, NDAttrValue::Int32(1));
attr.update();
assert_eq!(attr.value, NDAttrValue::Int32(2));
attr.update();
assert_eq!(attr.value, NDAttrValue::Int32(3));
}
#[test]
fn test_static_attribute_update_is_noop() {
let mut attr =
NDAttribute::new_static("static", "", NDAttrSource::Constant, NDAttrValue::Int32(42));
attr.update();
assert_eq!(attr.value, NDAttrValue::Int32(42));
}
#[test]
fn test_param_source_cell_reevaluates() {
let src = ParamAttributeSource::new("GAIN", 0);
let mut attr = NDAttribute::new_with_source(
"Gain",
"",
NDAttrSource::Param {
port_name: String::new(),
param_name: "GAIN".into(),
},
src.clone(),
);
assert_eq!(attr.value, NDAttrValue::Undefined);
src.cell().set(NDAttrValue::Float64(3.5));
attr.update();
assert_eq!(attr.value, NDAttrValue::Float64(3.5));
assert_eq!(attr.param_source().unwrap().param_name, "GAIN");
}
#[test]
fn test_function_source_via_registry() {
let registry = NDAttributeFunctionRegistry::new();
registry.register("double", |p: &str| {
NDAttrValue::Int32(p.parse::<i32>().unwrap_or(0) * 2)
});
let src = FunctionAttributeSource::new(registry.clone(), "double", "21");
let mut attr = NDAttribute::new_with_source("Doubled", "", NDAttrSource::Function, src);
assert_eq!(attr.value, NDAttrValue::Int32(42));
attr.update();
assert_eq!(attr.value, NDAttrValue::Int32(42));
let missing = FunctionAttributeSource::new(registry, "no_such", "");
assert_eq!(missing.evaluate(), NDAttrValue::Undefined);
}
#[test]
fn test_epics_pv_source_cell_feeding() {
let src = EpicsPvAttributeSource::new("TST:Temp");
assert_eq!(src.evaluate(), NDAttrValue::Undefined);
src.cell().set(NDAttrValue::Float64(22.4));
assert_eq!(src.evaluate(), NDAttrValue::Float64(22.4));
}
#[test]
fn test_attribute_list_update_values() {
let mut list = NDAttributeList::new();
list.add(NDAttribute::new_with_source(
"live",
"",
NDAttrSource::Function,
std::sync::Arc::new(CountingSource {
counter: std::sync::atomic::AtomicI32::new(0),
}),
));
list.add(NDAttribute::new_static(
"frozen",
"",
NDAttrSource::Constant,
NDAttrValue::Int32(7),
));
list.update_values();
assert_eq!(list.get("live").unwrap().value, NDAttrValue::Int32(2));
assert_eq!(list.get("frozen").unwrap().value, NDAttrValue::Int32(7));
}
}