use std::time::{Duration, Instant, SystemTime};
use std::sync::{Arc, Mutex};
use std::cmp::Ordering;
use std::borrow::Cow;
use tracing::warn;
use crate::dev::{AttrConfig, AttrDeltaConfig, AttrInfo, AttrMemorized, AttrMode, AttrNumber, AttrQuality, AttrRangeConfig, AttrRead,
AttrValueEvent, Named};
use crate::value::{AttrDataKind, DevEnum, Spectrum, Value, ValueFormat};
use crate::error::{reason, Error, ErrorKind, Result};
use crate::buf::Element;
use super::def::{AttrDef, ReadFn, WriteFn};
use super::dev::{Access, Dev, ModelRef};
use super::Model;
pub struct Attr<M: Model> {
def: Arc<AttrDef<M>>,
w_value: Mutex<Option<Value>>,
w_time: Mutex<Option<Instant>>,
setup: Mutex<AttrSetup>,
}
struct AttrSetup {
config: AttrConfig,
error: Option<String>,
loaded: bool,
restore: Option<Value>,
mem_error: Option<String>,
}
impl<M: Model> Attr<M> {
pub(super) fn new(def: Arc<AttrDef<M>>) -> Self {
let setup = AttrSetup {
config: def.config.clone(),
error: None,
loaded: false,
restore: None,
mem_error: None,
};
Self {
def,
w_value: Mutex::new(None),
w_time: Mutex::new(None),
setup: Mutex::new(setup),
}
}
#[inline]
pub fn name(&self) -> &str {
&self.def.name
}
pub fn config(&self) -> AttrConfig {
self.setup.lock().unwrap().config.clone()
}
pub(super) fn is_loaded(&self) -> bool {
self.setup.lock().unwrap().loaded
}
pub(super) fn has_alarms(&self) -> bool {
let config = &self.setup.lock().unwrap().config;
config.alarm.is_set() || config.warning.is_set() || config.delta.is_some()
}
pub(super) fn load_config(&self, dev: &Dev<M>, class: PropsResult<'_>, device: PropsResult<'_>) {
let mut loader = ConfigLoader::new(&self.def.config, self.def.kind);
loader.apply_level("class", class);
loader.apply_level("device", device);
let (config, mut error) = loader.finish();
if let Some(incoherence) = incoherence(&config) {
error = Some(match error {
Some(error) => format!("{error}, {incoherence}"),
None => incoherence,
});
}
if let Some(error) = &error {
warn!(dev = %dev.name(), attr = %self.name(), err = %error, "Failed to configure the attribute from the database");
}
let mut setup = self.setup.lock().unwrap();
if setup.loaded {
setup.config = config;
setup.error = error;
return;
}
let mut restore = None;
let mut mem_error = None;
let mem_value = device.ok()
.and_then(|props| props
.iter()
.find(|prop| prop.name.value().eq_ignore_ascii_case(MEM_VALUE)));
if let (Some(mode), Some(mem_value)) = (self.def.memorized, mem_value) {
match parse_mem_value(self.def.kind.as_ref(), &mem_value.value) {
Ok(None) => {}
Ok(Some(value)) => match mode {
AttrMemorized::RestoreSetPoint => *self.w_value.lock().unwrap() = Some(value),
AttrMemorized::RestoreAndWrite => restore = Some(value),
}
Err(err) => {
warn!(dev = %dev.name(), attr = %self.name(), %err, "Failed to parse the memorized value");
mem_error = Some(format!("the memorized value cannot be parsed: {err}"));
}
}
}
*setup = AttrSetup {
config,
error,
loaded: true,
restore,
mem_error,
};
}
pub(super) fn change_config(&self, class: &[Named<Spectrum<str>>], requested: &AttrConfig) -> std::result::Result<ConfigChanges, String> {
let mut config = ConfigLoader::new(&self.def.config, self.def.kind);
config.apply_level("class", Ok(class));
let (mut config, class_error) = config.finish();
if let Some(prop) = unsupported_prop(requested, self.def.kind) {
return Err(format!("its property {:?} is {}", prop.name(), unsupported(self.def.kind)));
}
let w_value = self.def.memorized.and_then(|_| self.w_value.lock().unwrap().clone());
let mut setup = self.setup.lock().unwrap();
let mut changes = ConfigChanges::default();
for prop in ConfigProp::ALL {
let target = prop_value(requested, prop);
let now = prop_value(&setup.config, prop);
let inherited = prop_value(&config, prop);
if target.is_unset() || target == inherited {
if now != inherited {
changes.del.push(prop);
}
} else {
if target != now {
changes.put.push((prop, target.to_values()));
}
copy_prop(&mut config, requested, prop);
}
}
if let Some(incoherence) = incoherence(&config) {
return Err(incoherence);
}
if let Some(w_value) = &w_value {
if any_number(w_value, |number| outside(&config.range, number)) {
return Err("its memorized value would be outside of its range".to_string());
}
}
setup.config = config;
setup.error = class_error;
Ok(changes)
}
pub(super) async fn restore(&self, dev: &Dev<M>, model: &mut M) {
let Some(value) = self.setup.lock().unwrap().restore.take() else {
return;
};
if self.check_config(dev, false).is_err() {
return;
}
if let Err(err) = self.write_inner(dev, &mut ModelRef::Exclusive(model), value).await {
warn!(dev = %dev.name(), attr = %self.name(), %err, "Failed to write the memorized value back");
self.setup.lock().unwrap().mem_error = Some(format!("the memorized value cannot be \
written back: {}", err.description()));
}
}
fn check_config(&self, dev: &Dev<M>, check_mem: bool) -> Result<()> {
let setup = self.setup.lock().unwrap();
if let Some(error) = &setup.error {
return Err(Error::new(ErrorKind::Reason(reason::API_AttrConfig))
.with_description(format!("The attribute {:?} of the device {:?} has a wrong \
configuration in the database: {error}", self.name(), dev.name())));
}
if check_mem && let Some(error) = &setup.mem_error {
return Err(Error::new(ErrorKind::Reason(reason::API_MemAttFailedDuringInit))
.with_description(format!("The attribute {:?} of the device {:?} has no set \
point, {error}, it has to be written again", self.name(), dev.name())));
}
Ok(())
}
#[inline]
pub fn is_memorized(&self) -> bool {
self.def.memorized.is_some()
}
pub(super) async fn set_w_value(&self, dev: &Dev<M>, value: Value) -> Result<()> {
*self.w_value.lock().unwrap() = Some(value.clone());
self.setup.lock().unwrap().mem_error = None;
self.store(dev, &value).await
}
async fn store(&self, dev: &Dev<M>, value: &Value) -> Result<()> {
if self.def.memorized.is_none() {
return Ok(());
}
let mem_value = mem_value_str(value)
.map_err(|err| Error::new(ErrorKind::Reason(reason::API_AttrConfig))
.with_description(format!("The attribute {:?} of the device {:?} cannot be \
memorized: {err}", self.name(), dev.name())))?;
dev.server().database()
.set_dev_attr_prop_async(dev.name(), (self.name(), (MEM_VALUE, mem_value)))
.await
}
pub(super) fn read_access(&self) -> Access {
match &self.def.read {
Some(ReadFn::ExclusiveSync(_) | ReadFn::ExclusiveAsync(_)) => Access::Exclusive,
_ => Access::Shared,
}
}
pub(super) fn write_access(&self) -> Access {
match &self.def.write {
Some(WriteFn::ExclusiveSync(_) | WriteFn::ExclusiveAsync(_)) => Access::Exclusive,
_ => Access::Shared,
}
}
pub(super) fn read_may_block(&self) -> bool {
matches!(&self.def.read, Some(ReadFn::SharedSync(_) | ReadFn::ExclusiveSync(_)))
}
pub(super) fn write_may_block(&self) -> bool {
matches!(&self.def.write, Some(WriteFn::SharedSync(_) | WriteFn::ExclusiveSync(_)))
}
pub fn info(&self) -> Result<AttrInfo> {
self.info_as(match (&self.def.read, &self.def.write) {
(Some(_), Some(_)) => AttrMode::ReadWrite,
(Some(_), None) => AttrMode::Read,
(None, Some(_)) => AttrMode::Write,
(None, None) => AttrMode::ReadWrite,
})
}
fn no_handler_err(&self, dev: &Dev<M>) -> Error {
Error::not_implemented()
.with_description(format!("The attribute {} of the device {} has no handler defined",
self.name(), dev.name()))
}
pub(super) fn check_push_event(&self, dev: &Dev<M>, event: AttrValueEvent) -> Result<()> {
let pushed = match event {
AttrValueEvent::Change => self.def.pushed.change.is_some(),
AttrValueEvent::Archive => self.def.pushed.archive.is_some(),
AttrValueEvent::Periodic => false,
AttrValueEvent::User |
AttrValueEvent::Alarm => true,
};
if pushed {
return Ok(());
}
Err(Error::new(ErrorKind::Reason(reason::API_AttributePollingNotStarted))
.with_description(format!("The polling (necessary to send events) for the attribute {} \
of the device {} is not started, and the device does not push its {event:?} events",
self.name(),
dev.name())))
}
pub(super) fn info_as(&self, mode: AttrMode) -> Result<AttrInfo> {
Ok(AttrInfo {
config: self.config(),
mode,
data_kind: self.def.kind
.ok_or_else(|| Error::new(ErrorKind::Internal)
.with_description(format!("The attribute {} does not advertise the kind \
of its data", self.name())))?,
data_format: self.def.format
.ok_or_else(|| Error::new(ErrorKind::Internal)
.with_description(format!("The attribute {} does not advertise the \
format of its data", self.name())))?,
display_level: self.def.display_level,
memorized: self.def.memorized,
})
}
fn alarm(&self, read: &AttrRead<Value>) -> Option<AttrAlarm> {
let value = match (read.quality, &read.value) {
(AttrQuality::Alarm | AttrQuality::Warning, _) => return Some(AttrAlarm::Device(read.quality)),
(AttrQuality::Valid, Some(value)) => value,
_ => return None,
};
let (alarm, warning, delta) = {
let config = &self.setup.lock().unwrap().config;
(config.alarm, config.warning, config.delta)
};
if any_number(value, |number| outside(&alarm, number)) {
return Some(AttrAlarm::Alarm);
}
if any_number(value, |number| outside(&warning, number)) {
return Some(AttrAlarm::Warning);
}
if let (Some(delta), Some(w_value)) = (delta, &read.w_value) {
let written = *self.w_time.lock().unwrap();
let settled = written.is_some_and(|time| time.elapsed() >= delta.time.unwrap_or_default());
if settled && drifted(value, w_value, delta.value) {
return Some(AttrAlarm::Delta);
}
}
None
}
pub(super) async fn read_with_alarm(&self, dev: &Dev<M>, model: &mut ModelRef<'_, M>) -> Result<(AttrRead<Value>, Option<AttrAlarm>)> {
let mut read = self.read_inner(dev, model).await?;
let alarm = self.alarm(&read);
if let Some(alarm) = alarm {
read.quality = alarm.quality();
}
Ok((read, alarm))
}
pub(super) async fn read(&self, dev: &Dev<M>, model: &mut ModelRef<'_, M>) -> Result<AttrRead<Value>> {
self.read_with_alarm(dev, model).await.map(|(read, _)| read)
}
async fn read_inner(&self, dev: &Dev<M>, model: &mut ModelRef<'_, M>) -> Result<AttrRead<Value>> {
self.check_config(dev, true)?;
let mut read = match &self.def.read {
Some(ReadFn::SharedSync(func)) => func(model.shared(), dev)?,
Some(ReadFn::ExclusiveSync(func)) => func(model.exclusive()?, dev)?,
Some(ReadFn::SharedAsync(func)) => func(model.shared(), dev).await?,
Some(ReadFn::ExclusiveAsync(func)) => func(model.exclusive()?, dev).await?,
None if self.def.write.is_none() => return Err(self.no_handler_err(dev)),
None => AttrRead {
value: None,
w_value: None,
quality: AttrQuality::Valid,
time: SystemTime::now(),
},
};
self.merge_w_value(&mut read);
Ok(read)
}
pub(super) fn merge_w_value(&self, read: &mut AttrRead<Value>) {
match &read.w_value {
None => read.w_value = self.w_value.lock().unwrap().clone(),
Some(w_value) => *self.w_value.lock().unwrap() = Some(w_value.clone()),
}
}
pub(super) async fn write(&self, dev: &Dev<M>, model: &mut ModelRef<'_, M>, value: Value) -> Result<()> {
self.check_config(dev, false)?;
self.write_inner(dev, model, value.clone()).await?;
self.store(dev, &value).await
}
async fn write_inner(&self, dev: &Dev<M>, model: &mut ModelRef<'_, M>, value: Value) -> Result<()> {
self.check_limits(dev, &value)?;
match &self.def.write {
Some(WriteFn::SharedSync(func)) => func(model.shared(), dev, value.clone())?,
Some(WriteFn::ExclusiveSync(func)) => func(model.exclusive()?, dev, value.clone())?,
Some(WriteFn::SharedAsync(func)) => func(model.shared(), dev, value.clone()).await?,
Some(WriteFn::ExclusiveAsync(func)) => func(model.exclusive()?, dev, value.clone()).await?,
None if self.def.read.is_none() => return Err(self.no_handler_err(dev)),
None => return Err(Error::new(ErrorKind::AttrNotWritable)
.with_append_description(format_args!("Attribute {:?} of the device {:?}",
self.name(), dev.name()))),
}
*self.w_value.lock().unwrap() = Some(value);
*self.w_time.lock().unwrap() = Some(Instant::now());
self.setup.lock().unwrap().mem_error = None;
Ok(())
}
fn check_limits(&self, dev: &Dev<M>, value: &Value) -> Result<()> {
let range = self.setup.lock().unwrap().config.range;
if !any_number(value, |number| outside(&range, number)) {
return Ok(());
}
let bound = |bound: Option<AttrNumber>| bound.map_or_else(|| "-".to_string(), |bound| bound.to_string());
Err(Error::new(ErrorKind::Reason(reason::API_WAttrOutsideLimit))
.with_description(format!("The value written to the attribute {:?} of the device {:?} is \
outside of its range [{}, {}]", self.name(), dev.name(), bound(range.min), bound(range.max))))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttrAlarm {
Alarm,
Warning,
Delta,
Device(AttrQuality),
}
impl AttrAlarm {
pub fn quality(self) -> AttrQuality {
match self {
Self::Alarm | Self::Delta => AttrQuality::Alarm,
Self::Warning => AttrQuality::Warning,
Self::Device(quality) => quality,
}
}
pub fn describe(self, name: &str) -> String {
match self {
Self::Alarm => format!("Alarm: the attribute {name} is outside of its alarm range"),
Self::Warning => format!("Warning: the attribute {name} is outside of its warning range"),
Self::Delta => format!("Alarm: the attribute {name} is too far from the value written to it"),
Self::Device(AttrQuality::Warning) => format!("Warning: the attribute {name} is in WARNING"),
Self::Device(_) => format!("Alarm: the attribute {name} is in ALARM"),
}
}
}
fn any_number(value: &Value, mut pred: impl FnMut(AttrNumber) -> bool) -> bool {
macro_rules! any {
($format:expr) => {
match $format {
ValueFormat::Scalar(data) => data.iter().any(|number| pred(number.into())),
ValueFormat::Spectrum(data) => data.iter().any(|number| pred(number.into())),
ValueFormat::Image(data) => data.iter().any(|number| pred(number.into())),
}
};
}
match value {
Value::Octet(format) => any!(format),
Value::UShort(format) => any!(format),
Value::Short(format) => any!(format),
Value::ULong(format) => any!(format),
Value::Long(format) => any!(format),
Value::ULong64(format) => any!(format),
Value::Long64(format) => any!(format),
Value::Float(format) => any!(format),
Value::Double(format) => any!(format),
_ => false,
}
}
fn outside(range: &AttrRangeConfig, number: AttrNumber) -> bool {
let below = range.min.is_some_and(|min| number.partial_cmp(&min).is_none_or(Ordering::is_lt));
let above = range.max.is_some_and(|max| number.partial_cmp(&max).is_none_or(Ordering::is_gt));
below || above
}
fn drifted(read: &Value, written: &Value, delta: AttrNumber) -> bool {
let mut written_numbers = Vec::new();
any_number(written, |number| {
written_numbers.push(number.as_f64());
false
});
let delta = delta.as_f64();
let mut written_numbers = written_numbers.into_iter();
any_number(read, |number| written_numbers.next()
.is_some_and(|written| (number.as_f64() - written).abs() > delta))
}
pub type PropsResult<'a> = std::result::Result<&'a [Named<Spectrum<str>>], &'a Error>;
const NOT_SPECIFIED: &str = "Not specified";
const NOT_A_NUMBER: &str = "NaN";
struct ConfigLoader {
config: AttrConfig,
kind: Option<AttrDataKind>,
delta_value: Option<AttrNumber>,
delta_time: Option<Duration>,
errors: Vec<String>,
}
impl ConfigLoader {
fn new(config: &AttrConfig, kind: Option<AttrDataKind>) -> Self {
Self {
config: config.clone(),
kind,
delta_value: config.delta.map(|delta| delta.value),
delta_time: config.delta.and_then(|delta| delta.time),
errors: Vec::new(),
}
}
fn apply_level(&mut self, level: &str, props: PropsResult<'_>) {
match props {
Ok(props) => {
for prop in props {
if let Err(err) = self.apply(prop.name.value(), &prop.value) {
self.errors.push(format!("{level} property {:?}: {err}", prop.name.value()));
}
}
}
Err(err) => self.errors.push(format!("{level} properties: {}", err.description())),
}
}
fn apply(&mut self, name: &str, values: &Spectrum<str>) -> std::result::Result<(), String> {
match ConfigProp::from_name(name) {
Some(prop) => self.apply_prop(prop, values),
None => Ok(()),
}
}
fn apply_prop(&mut self, prop: ConfigProp, values: &Spectrum<str>) -> std::result::Result<(), String> {
if !prop.supported_by(self.kind) {
return match values.iter().all(|value| is_unset(value.trim())) {
true => Ok(()),
false => Err(unsupported(self.kind)),
};
}
let config = &mut self.config;
let limits = limit_kind(self.kind);
match prop {
ConfigProp::Description => config.doc = text(values),
ConfigProp::Label => config.label = text(values),
ConfigProp::Unit => config.unit = text(values),
ConfigProp::StandardUnit => config.standard_unit = number(values)?,
ConfigProp::DisplayUnit => config.display_unit = text(values),
ConfigProp::Format => config.display_format = text(values),
ConfigProp::MinValue => config.range.min = limit(values, limits)?,
ConfigProp::MaxValue => config.range.max = limit(values, limits)?,
ConfigProp::MinAlarm => config.alarm.min = limit(values, limits)?,
ConfigProp::MaxAlarm => config.alarm.max = limit(values, limits)?,
ConfigProp::MinWarning => config.warning.min = limit(values, limits)?,
ConfigProp::MaxWarning => config.warning.max = limit(values, limits)?,
ConfigProp::DeltaVal => self.delta_value = limit(values, limits)?,
ConfigProp::DeltaT => self.delta_time = duration(values)?.filter(|time| !time.is_zero()),
ConfigProp::RelChange => config.change_event.rel_change = change(values)?,
ConfigProp::AbsChange => config.change_event.abs_change = change(values)?,
ConfigProp::EventPeriod => config.periodic_event.period = duration(values)?,
ConfigProp::ArchiveRelChange => config.archive_event.rel_change = change(values)?,
ConfigProp::ArchiveAbsChange => config.archive_event.abs_change = change(values)?,
ConfigProp::ArchivePeriod => config.archive_event.period = duration(values)?,
ConfigProp::EnumLabels => config.enum_labels = labels(values),
}
Ok(())
}
fn finish(self) -> (AttrConfig, Option<String>) {
let mut config = self.config;
config.delta = self.delta_value.map(|value| AttrDeltaConfig { value, time: self.delta_time });
let error = (!self.errors.is_empty()).then(|| self.errors.join(", "));
(config, error)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ConfigPropValue<'a> {
Text(Option<&'a str>),
Number(Option<f64>),
Limit(Option<AttrNumber>),
Duration(Option<Duration>),
Labels(&'a [Cow<'static, str>]),
}
impl ConfigPropValue<'_> {
fn is_unset(self) -> bool {
match self {
Self::Text(text) => text.is_none(),
Self::Number(number) => number.is_none(),
Self::Limit(limit) => limit.is_none(),
Self::Duration(duration) => duration.is_none(),
Self::Labels(labels) => labels.is_empty(),
}
}
fn to_values(self) -> Vec<String> {
if self.is_unset() {
return vec![NOT_SPECIFIED.to_string()];
}
match self {
Self::Text(text) => text.unwrap_or_default().split('\n').map(str::to_string).collect(),
Self::Number(number) => number.iter().map(f64::to_string).collect(),
Self::Limit(limit) => limit.iter().map(AttrNumber::to_string).collect(),
Self::Duration(duration) => duration.iter().map(|duration| (duration.as_secs_f64() * 1000.0).to_string()).collect(),
Self::Labels(labels) => labels.iter().map(|label| label.to_string()).collect(),
}
}
}
fn prop_value(config: &AttrConfig, prop: ConfigProp) -> ConfigPropValue<'_> {
match prop {
ConfigProp::Description => ConfigPropValue::Text(config.doc.as_deref()),
ConfigProp::Label => ConfigPropValue::Text(config.label.as_deref()),
ConfigProp::Unit => ConfigPropValue::Text(config.unit.as_deref()),
ConfigProp::StandardUnit => ConfigPropValue::Number(config.standard_unit),
ConfigProp::DisplayUnit => ConfigPropValue::Text(config.display_unit.as_deref()),
ConfigProp::Format => ConfigPropValue::Text(config.display_format.as_deref()),
ConfigProp::MinValue => ConfigPropValue::Limit(config.range.min),
ConfigProp::MaxValue => ConfigPropValue::Limit(config.range.max),
ConfigProp::MinAlarm => ConfigPropValue::Limit(config.alarm.min),
ConfigProp::MaxAlarm => ConfigPropValue::Limit(config.alarm.max),
ConfigProp::MinWarning => ConfigPropValue::Limit(config.warning.min),
ConfigProp::MaxWarning => ConfigPropValue::Limit(config.warning.max),
ConfigProp::DeltaVal => ConfigPropValue::Limit(config.delta.map(|delta| delta.value)),
ConfigProp::DeltaT => ConfigPropValue::Duration(config.delta.and_then(|delta| delta.time)),
ConfigProp::RelChange => ConfigPropValue::Number(config.change_event.rel_change),
ConfigProp::AbsChange => ConfigPropValue::Number(config.change_event.abs_change),
ConfigProp::EventPeriod => ConfigPropValue::Duration(config.periodic_event.period),
ConfigProp::ArchiveRelChange => ConfigPropValue::Number(config.archive_event.rel_change),
ConfigProp::ArchiveAbsChange => ConfigPropValue::Number(config.archive_event.abs_change),
ConfigProp::ArchivePeriod => ConfigPropValue::Duration(config.archive_event.period),
ConfigProp::EnumLabels => ConfigPropValue::Labels(&config.enum_labels),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigProp {
Description,
Label,
Unit,
StandardUnit,
DisplayUnit,
Format,
MinValue,
MaxValue,
MinAlarm,
MaxAlarm,
MinWarning,
MaxWarning,
DeltaVal,
DeltaT,
RelChange,
AbsChange,
EventPeriod,
ArchiveRelChange,
ArchiveAbsChange,
ArchivePeriod,
EnumLabels,
}
impl ConfigProp {
const ALL: [Self; 21] = [
Self::Description,
Self::Label,
Self::Unit,
Self::StandardUnit,
Self::DisplayUnit,
Self::Format,
Self::MinValue,
Self::MaxValue,
Self::MinAlarm,
Self::MaxAlarm,
Self::MinWarning,
Self::MaxWarning,
Self::DeltaVal,
Self::DeltaT,
Self::RelChange,
Self::AbsChange,
Self::EventPeriod,
Self::ArchiveRelChange,
Self::ArchiveAbsChange,
Self::ArchivePeriod,
Self::EnumLabels,
];
pub fn name(self) -> &'static str {
match self {
Self::Description => "description",
Self::Label => "label",
Self::Unit => "unit",
Self::StandardUnit => "standard_unit",
Self::DisplayUnit => "display_unit",
Self::Format => "format",
Self::MinValue => "min_value",
Self::MaxValue => "max_value",
Self::MinAlarm => "min_alarm",
Self::MaxAlarm => "max_alarm",
Self::MinWarning => "min_warning",
Self::MaxWarning => "max_warning",
Self::DeltaVal => "delta_val",
Self::DeltaT => "delta_t",
Self::RelChange => "rel_change",
Self::AbsChange => "abs_change",
Self::EventPeriod => "event_period",
Self::ArchiveRelChange => "archive_rel_change",
Self::ArchiveAbsChange => "archive_abs_change",
Self::ArchivePeriod => "archive_period",
Self::EnumLabels => "enum_labels",
}
}
fn supported_by(self, kind: Option<AttrDataKind>) -> bool {
let numeric = !matches!(kind, None | Some(AttrDataKind::Bool | AttrDataKind::State | AttrDataKind::String | AttrDataKind::Encoded));
match self {
Self::MinAlarm |
Self::MaxAlarm |
Self::MinWarning |
Self::MaxWarning => numeric && kind != Some(AttrDataKind::Enum),
Self::MinValue |
Self::MaxValue |
Self::DeltaVal |
Self::DeltaT |
Self::RelChange |
Self::AbsChange |
Self::ArchiveRelChange |
Self::ArchiveAbsChange => numeric,
_ => true,
}
}
fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|prop| name.eq_ignore_ascii_case(prop.name()))
}
}
fn is_unset(value: &str) -> bool {
value.is_empty() || value.eq_ignore_ascii_case(NOT_SPECIFIED) || value.eq_ignore_ascii_case(NOT_A_NUMBER)
}
fn text(values: &Spectrum<str>) -> Option<Cow<'static, str>> {
let text = values.iter().collect::<Vec<_>>().join("\n");
(!is_unset(text.trim())).then_some(Cow::Owned(text))
}
fn single(values: &Spectrum<str>) -> std::result::Result<Option<&str>, String> {
let mut iter = values.iter().map(str::trim);
match (iter.next(), iter.next()) {
(None, _) => Ok(None),
(Some(value), None) => Ok((!is_unset(value)).then_some(value)),
(Some(_), Some(_)) => Err(format!("expected a single value, got {}", values.len())),
}
}
fn parse_number(value: &str) -> std::result::Result<f64, String> {
value.parse::<f64>().ok()
.filter(|value| value.is_finite())
.ok_or_else(|| format!("{value:?} is not a number"))
}
fn number(values: &Spectrum<str>) -> std::result::Result<Option<f64>, String> {
single(values)?.map(parse_number).transpose()
}
fn limit(values: &Spectrum<str>, kind: LimitKind) -> std::result::Result<Option<AttrNumber>, String> {
let Some(value) = single(values)? else {
return Ok(None);
};
let typed = match kind {
LimitKind::Long => value.parse::<i64>().ok().map(AttrNumber::Long),
LimitKind::ULong => value.parse::<u64>().ok().map(AttrNumber::ULong),
LimitKind::Double => None,
};
match typed {
Some(typed) => Ok(Some(typed)),
None => parse_number(value).map(|value| Some(AttrNumber::Double(value))),
}
}
fn duration(values: &Spectrum<str>) -> std::result::Result<Option<Duration>, String> {
let Some(millis) = number(values)? else {
return Ok(None);
};
Duration::try_from_secs_f64(millis / 1000.0)
.map(Some)
.map_err(|_| format!("{millis} is not a duration in milliseconds"))
}
fn change(values: &Spectrum<str>) -> std::result::Result<Option<f64>, String> {
let changes = values.iter()
.flat_map(|value| value.split(','))
.map(str::trim)
.filter(|value| !is_unset(value))
.collect::<Vec<_>>();
if changes.len() > 2 {
return Err(format!("expected at most two values, got {}", changes.len()));
}
let mut min = None::<f64>;
for change in changes {
let change = parse_number(change)?.abs();
min = Some(min.map_or(change, |min| min.min(change)));
}
Ok(min)
}
fn labels(values: &Spectrum<str>) -> Vec<Cow<'static, str>> {
match single(values) {
Ok(None) => Vec::new(),
_ => values.iter().map(|label| Cow::Owned(label.to_string())).collect(),
}
}
fn limit_kind(kind: Option<AttrDataKind>) -> LimitKind {
match kind {
Some(AttrDataKind::Short | AttrDataKind::Long | AttrDataKind::Long64 | AttrDataKind::Enum) => LimitKind::Long,
Some(AttrDataKind::Octet | AttrDataKind::UShort | AttrDataKind::ULong | AttrDataKind::ULong64) => LimitKind::ULong,
_ => LimitKind::Double,
}
}
pub fn unsupported_prop(config: &AttrConfig, kind: Option<AttrDataKind>) -> Option<ConfigProp> {
ConfigProp::ALL.into_iter().find(|&prop| !prop.supported_by(kind) && !prop_value(config, prop).is_unset())
}
pub fn unsupported(kind: Option<AttrDataKind>) -> String {
match kind {
Some(kind) => format!("not supported by an attribute of type {kind:?}"),
None => "not supported by an attribute of no type".to_string(),
}
}
#[derive(Debug, Clone, Copy)]
enum LimitKind {
Double,
Long,
ULong,
}
#[derive(Debug, Default, PartialEq)]
pub struct ConfigChanges {
pub put: Vec<(ConfigProp, Vec<String>)>,
pub del: Vec<ConfigProp>,
}
impl ConfigChanges {
pub fn is_empty(&self) -> bool {
self.put.is_empty() && self.del.is_empty()
}
}
fn copy_prop(dst: &mut AttrConfig, src: &AttrConfig, prop: ConfigProp) {
match prop {
ConfigProp::Description => dst.doc = src.doc.clone(),
ConfigProp::Label => dst.label = src.label.clone(),
ConfigProp::Unit => dst.unit = src.unit.clone(),
ConfigProp::StandardUnit => dst.standard_unit = src.standard_unit,
ConfigProp::DisplayUnit => dst.display_unit = src.display_unit.clone(),
ConfigProp::Format => dst.display_format = src.display_format.clone(),
ConfigProp::MinValue => dst.range.min = src.range.min,
ConfigProp::MaxValue => dst.range.max = src.range.max,
ConfigProp::MinAlarm => dst.alarm.min = src.alarm.min,
ConfigProp::MaxAlarm => dst.alarm.max = src.alarm.max,
ConfigProp::MinWarning => dst.warning.min = src.warning.min,
ConfigProp::MaxWarning => dst.warning.max = src.warning.max,
ConfigProp::DeltaVal => {
let time = dst.delta.and_then(|delta| delta.time);
dst.delta = src.delta.map(|delta| AttrDeltaConfig { value: delta.value, time });
}
ConfigProp::DeltaT => {
if let Some(delta) = &mut dst.delta {
delta.time = src.delta.and_then(|delta| delta.time);
}
}
ConfigProp::RelChange => dst.change_event.rel_change = src.change_event.rel_change,
ConfigProp::AbsChange => dst.change_event.abs_change = src.change_event.abs_change,
ConfigProp::EventPeriod => dst.periodic_event.period = src.periodic_event.period,
ConfigProp::ArchiveRelChange => dst.archive_event.rel_change = src.archive_event.rel_change,
ConfigProp::ArchiveAbsChange => dst.archive_event.abs_change = src.archive_event.abs_change,
ConfigProp::ArchivePeriod => dst.archive_event.period = src.archive_event.period,
ConfigProp::EnumLabels => dst.enum_labels = src.enum_labels.clone(),
}
}
fn incoherence(config: &AttrConfig) -> Option<String> {
for (what, range) in [
("value", &config.range),
("alarm", &config.alarm),
("warning", &config.warning)
] {
if let (Some(min), Some(max)) = (range.min, range.max) {
if !(min < max) {
return Some(format!("the minimum of its {what} range ({min}) is not below its maximum ({max})"));
}
}
}
None
}
const MEM_VALUE: &str = "__value";
const MEM_NOT_USED: &str = "Not used yet";
fn mem_value_str(value: &Value) -> std::result::Result<String, String> {
fn scalar<T: Element + ?Sized>(format: &ValueFormat<T>) -> std::result::Result<T::Ref<'_>, String> {
format.scalar().map(|scalar| scalar.value()).ok_or_else(|| "only a scalar can be memorized".to_string())
}
Ok(match value {
Value::Bool(format) => scalar(format)?.to_string(),
Value::Octet(format) => scalar(format)?.to_string(),
Value::UShort(format) => scalar(format)?.to_string(),
Value::Short(format) => scalar(format)?.to_string(),
Value::ULong(format) => scalar(format)?.to_string(),
Value::Long(format) => scalar(format)?.to_string(),
Value::ULong64(format) => scalar(format)?.to_string(),
Value::Long64(format) => scalar(format)?.to_string(),
Value::Float(format) => scalar(format)?.to_string(),
Value::Double(format) => scalar(format)?.to_string(),
Value::Enum(format) => scalar(format)?.0.to_string(),
Value::String(format) => scalar(format)?.to_string(),
_ => return Err("a value of that kind cannot be memorized".to_string()),
})
}
fn parse_mem_value(kind: Option<&AttrDataKind>, values: &Spectrum<str>) -> std::result::Result<Option<Value>, String> {
let mut iter = values.iter();
let value = match (iter.next(), iter.next()) {
(None, _) => return Ok(None),
(Some(value), None) => value,
(Some(_), Some(_)) => return Err(format!("expected a single value, got {}", values.len())),
};
if value == MEM_NOT_USED {
return Ok(None);
}
fn parse<T: std::str::FromStr>(value: &str) -> std::result::Result<T, String> {
value.trim().parse::<T>().map_err(|_| format!("{value:?} is not a value of the attribute"))
}
Ok(Some(match kind {
Some(AttrDataKind::Bool) => match value.trim() {
value if value.eq_ignore_ascii_case("true") || value == "1" => Value::from(true),
value if value.eq_ignore_ascii_case("false") || value == "0" => Value::from(false),
_ => return Err(format!("{value:?} is not a boolean")),
},
Some(AttrDataKind::Octet) => Value::from(parse::<u8>(value)?),
Some(AttrDataKind::UShort) => Value::from(parse::<u16>(value)?),
Some(AttrDataKind::Short) => Value::from(parse::<i16>(value)?),
Some(AttrDataKind::ULong) => Value::from(parse::<u32>(value)?),
Some(AttrDataKind::Long) => Value::from(parse::<i32>(value)?),
Some(AttrDataKind::ULong64) => Value::from(parse::<u64>(value)?),
Some(AttrDataKind::Long64) => Value::from(parse::<i64>(value)?),
Some(AttrDataKind::Float) => Value::from(parse::<f32>(value)?),
Some(AttrDataKind::Double) => Value::from(parse::<f64>(value)?),
Some(AttrDataKind::Enum) => Value::from(DevEnum(parse::<i16>(value)?)),
Some(AttrDataKind::String) => Value::from(value),
_ => return Err("a value of that kind cannot be memorized".to_string()),
}))
}
#[cfg(test)]
mod tests {
use crate::dev::{AttrRangeConfig, AttrChangeEventConfig};
use super::*;
fn current() -> AttrConfig {
AttrConfig {
label: Some("Position".into()),
unit: Some("cm".into()),
range: AttrRangeConfig { min: Some(AttrNumber::Long(-5)), max: None },
..AttrConfig::default()
}
}
fn changes(change: impl FnOnce(&mut AttrConfig)) -> (ConfigChanges, AttrConfig) {
let attr = Attr::new(Arc::new(AttrDef::<()>::new("Position").kind(AttrDataKind::Long).label("Position")));
attr.setup.lock().unwrap().config = current();
let class = [Named::new("unit".into(), Spectrum::from_strs(["mm"]).unwrap())];
let mut requested = current();
change(&mut requested);
let changes = attr.change_config(&class, &requested).unwrap();
(changes, attr.config())
}
fn put(pairs: &[(ConfigProp, &str)]) -> ConfigChanges {
ConfigChanges {
put: pairs.iter().map(|&(prop, value)| (prop, vec![value.to_string()])).collect(),
del: Vec::new(),
}
}
fn del(props: &[ConfigProp]) -> ConfigChanges {
ConfigChanges { put: Vec::new(), del: props.to_vec() }
}
#[test]
fn range_bounds() {
let range = AttrRangeConfig { min: Some(AttrNumber::Long(-1)), max: Some(AttrNumber::Double(2.5)) };
assert!(!outside(&range, AttrNumber::ULong(2)));
assert!(!outside(&range, AttrNumber::Long(-1)));
assert!(outside(&range, AttrNumber::ULong(3)));
assert!(outside(&range, AttrNumber::Double(f64::NAN)));
let range = AttrRangeConfig { min: None, max: Some(AttrNumber::ULong(u64::MAX - 1)) };
assert!(outside(&range, AttrNumber::ULong(u64::MAX)));
assert!(!outside(&range, AttrNumber::Long(i64::MIN)));
assert!(!any_number(&Value::from("text"), |_| true));
}
#[test]
fn range_incoherence() {
let range = |min: i64, max: i64| AttrRangeConfig { min: Some(AttrNumber::Long(min)), max: Some(AttrNumber::Long(max)) };
assert_eq!(incoherence(&AttrConfig { range: range(-1, 1), ..AttrConfig::default() }), None);
assert!(incoherence(&AttrConfig { warning: range(1, 1), ..AttrConfig::default() }).unwrap().contains("warning"));
assert!(incoherence(&AttrConfig { alarm: range(2, 1), ..AttrConfig::default() }).unwrap().contains("alarm"));
let open = AttrRangeConfig { min: Some(AttrNumber::Long(1)), max: None };
assert_eq!(incoherence(&AttrConfig { range: open, ..AttrConfig::default() }), None);
}
#[test]
fn value_changes() {
assert_eq!(changes(|_| ()).0, ConfigChanges::default());
assert_eq!(changes(|config| config.label = Some("Pos".into())).0, put(&[(ConfigProp::Label, "Pos")]));
assert_eq!(changes(|config| config.range.max = Some(AttrNumber::Long(5))).0, put(&[(ConfigProp::MaxValue, "5")]));
assert_eq!(changes(|config| config.unit = Some("mm".into())).0, del(&[ConfigProp::Unit]));
}
#[test]
fn reset_changes() {
let (unit, config) = changes(|config| config.unit = None);
assert_eq!((unit, config.unit.as_deref()), (del(&[ConfigProp::Unit]), Some("mm")));
let (min, config) = changes(|config| config.range.min = None);
assert_eq!((min, config.range.min), (del(&[ConfigProp::MinValue]), None));
let (label, config) = changes(|config| config.label = None);
assert_eq!((label, config.label.as_deref()), (ConfigChanges::default(), Some("Position")));
}
#[test]
fn unsupported_props() {
let load = |kind, name: &str, value: &str| {
let props = [Named::new(name.into(), Spectrum::from_strs([value]).unwrap())];
let mut loader = ConfigLoader::new(&AttrConfig::default(), Some(kind));
loader.apply_level("device", Ok(&props));
loader.finish().1
};
assert!(load(AttrDataKind::String, "max_alarm", "5").unwrap().contains("not supported by an attribute of type String"));
assert_eq!(load(AttrDataKind::String, "max_alarm", "Not specified"), None);
assert_eq!(load(AttrDataKind::String, "label", "Name"), None);
assert!(load(AttrDataKind::Enum, "max_alarm", "2").is_some());
assert_eq!(load(AttrDataKind::Enum, "max_value", "2"), None);
}
#[test]
fn prop_round_trip() {
let config = AttrConfig {
doc: Some("First line\nSecond line".into()),
standard_unit: Some(0.001),
range: AttrRangeConfig { min: Some(AttrNumber::Long(-5)), max: Some(AttrNumber::Long(5)) },
delta: Some(AttrDeltaConfig { value: AttrNumber::Long(1), time: Some(Duration::from_millis(250)) }),
change_event: AttrChangeEventConfig { rel_change: Some(0.5), abs_change: None },
enum_labels: vec!["Off".into(), "On".into()],
..AttrConfig::default()
};
let attr = Attr::new(Arc::new(AttrDef::<()>::new("Position").kind(AttrDataKind::Long)));
let changes = attr.change_config(&[], &config).unwrap();
assert_eq!(attr.config(), config);
let props = changes.put.into_iter()
.map(|(prop, values)| Named::new(prop.name().into(), Spectrum::from_strs(values).unwrap()))
.collect::<Vec<_>>();
let mut loader = ConfigLoader::new(&AttrConfig::default(), Some(AttrDataKind::Long));
loader.apply_level("device", Ok(&props));
assert_eq!(loader.finish(), (config, None));
}
}