use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use std::{collections::BTreeMap, default::Default};
use super::ManualInputMap;
use super::channel::{Channel, Endpoint};
use super::manual_inputs_default;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::sync::{Arc, RwLock};
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use tracing::warn;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
#[non_exhaustive]
pub enum Termination {
Timeout(Duration),
Scheduled(SystemTime),
}
#[cfg(feature = "python")]
#[pymethods]
impl Termination {
#[staticmethod]
pub fn timeout_s(s: f64) -> Self {
let duration_s = if s <= 0.0 {
warn!("Termination timeout is zero or negative; clamping to zero.");
0.0
} else {
s
};
Self::Timeout(Duration::from_secs_f64(duration_s))
}
#[staticmethod]
pub fn scheduled_epoch_ns(ns: u64) -> PyResult<Self> {
let when = SystemTime::UNIX_EPOCH
.checked_add(Duration::from_nanos(ns))
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid epoch nanoseconds"))?;
Ok(Self::Scheduled(when))
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub enum LossOfContactPolicy {
Terminate(),
Reconnect(Option<Duration>),
}
#[cfg(feature = "python")]
#[pymethods]
impl LossOfContactPolicy {
#[staticmethod]
pub fn terminate() -> Self {
Self::Terminate()
}
#[staticmethod]
pub fn reconnect_s(timeout_s: f64) -> Self {
let duration = if timeout_s.is_sign_negative() {
Duration::ZERO
} else {
Duration::from_secs_f64(timeout_s)
};
Self::Reconnect(Some(duration))
}
#[staticmethod]
pub fn reconnect_indefinite() -> Self {
Self::Reconnect(None)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub enum LoopMethod {
Performant,
Efficient,
}
#[cfg(feature = "python")]
#[pymethods]
impl LoopMethod {
#[staticmethod]
pub fn performant() -> Self {
Self::Performant
}
#[staticmethod]
pub fn efficient() -> Self {
Self::Efficient
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[non_exhaustive]
pub struct ControllerCtx {
pub op_name: String,
pub op_dir: PathBuf,
pub dt_ns: u32,
pub timeout_to_operating_ns: u32,
pub binding_timeout_ms: u16,
pub configuring_timeout_ms: u16,
pub peripheral_loss_of_contact_limit: u16,
pub controller_loss_of_contact_limit: u16,
pub termination_criteria: Option<Termination>,
pub loss_of_contact_policy: LossOfContactPolicy,
pub loop_method: LoopMethod,
pub user_ctx: BTreeMap<String, String>,
pub user_channels: Arc<RwLock<BTreeMap<String, Channel>>>,
#[serde(skip, default = "manual_inputs_default")]
pub manual_inputs: ManualInputMap,
pub enable_manual_inputs: bool,
#[serde(default)]
pub channel_units: Vec<Option<String>>,
}
impl ControllerCtx {
pub fn source_endpoint(&self, channel_name: &str) -> Endpoint {
let map = &self.user_channels;
let inner = map.deref();
let mut writer = inner.try_write().unwrap();
let channel = writer.entry(channel_name.to_owned()).or_default();
channel.source_endpoint()
}
pub fn sink_endpoint(&self, channel_name: &str) -> Endpoint {
let map = &self.user_channels;
let inner = map.deref();
let mut writer = inner.try_write().unwrap();
let channel = writer.entry(channel_name.to_owned()).or_default();
channel.sink_endpoint()
}
}
impl Default for ControllerCtx {
fn default() -> Self {
let op_name = DateTime::<Utc>::from(SystemTime::now())
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
.replace(":", "");
Self {
op_name,
op_dir: std::fs::canonicalize("./").unwrap_or_default(),
dt_ns: 0,
timeout_to_operating_ns: 100_000_000,
binding_timeout_ms: 10,
configuring_timeout_ms: 20,
peripheral_loss_of_contact_limit: 10,
controller_loss_of_contact_limit: 10,
termination_criteria: None,
loss_of_contact_policy: LossOfContactPolicy::Terminate(),
loop_method: LoopMethod::Performant,
user_ctx: BTreeMap::new(),
user_channels: Arc::new(RwLock::new(BTreeMap::new())),
manual_inputs: manual_inputs_default(),
enable_manual_inputs: true,
channel_units: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_channel_units_serde_roundtrip() {
let ctx = ControllerCtx {
channel_units: vec![
Some("K".to_owned()),
None,
Some("ohm".to_owned()),
None,
Some("V".to_owned()),
],
..Default::default()
};
let serialized = serde_json::to_string(&ctx).expect("serialization failed");
assert!(serialized.contains("\"K\""));
assert!(serialized.contains("\"ohm\""));
assert!(serialized.contains("\"V\""));
let deserialized: ControllerCtx =
serde_json::from_str(&serialized).expect("deserialization failed");
assert_eq!(
deserialized.channel_units,
vec![
Some("K".to_owned()),
None,
Some("ohm".to_owned()),
None,
Some("V".to_owned()),
],
);
}
}