use std::sync::Arc;
use crate::types::EpicsValue;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkDbfType {
Char,
UChar,
Short,
UShort,
Long,
ULong,
Int64,
UInt64,
Float,
Double,
String,
Enum,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LinkPutOp {
#[default]
Plain,
Async,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PutAdmission {
Connected,
Refused,
Unopened,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LinkDiagnostics {
pub connected: bool,
pub host: String,
pub read_access: bool,
pub write_access: bool,
pub n_disconnect: u64,
pub input_native: bool,
pub input_string: bool,
pub output_native: bool,
pub output_string: bool,
}
#[derive(Clone, Copy)]
pub struct LinkBacking<'a>(Backing<'a>);
#[derive(Clone, Copy)]
enum Backing<'a> {
Unresolved,
Resolved(&'a std::collections::HashMap<String, LinkMetadata>),
Empty,
Declined,
}
impl<'a> LinkBacking<'a> {
pub const fn none() -> Self {
Self(Backing::Unresolved)
}
pub(crate) const fn declined() -> Self {
Self(Backing::Declined)
}
pub const fn resolved(resolved: &'a std::collections::HashMap<String, LinkMetadata>) -> Self {
Self(Backing::Resolved(resolved))
}
pub(crate) const fn empty() -> Self {
Self(Backing::Empty)
}
pub(crate) fn metadata(&self, link_field: &str) -> Option<&'a LinkMetadata> {
match self.0 {
Backing::Resolved(m) => m.get(link_field),
Backing::Empty | Backing::Unresolved | Backing::Declined => None,
}
}
pub(crate) const fn is_unresolved(&self) -> bool {
matches!(self.0, Backing::Unresolved)
}
pub(crate) const fn is_declined(&self) -> bool {
matches!(self.0, Backing::Declined)
}
}
pub enum PostBacking {
Resolved(std::collections::HashMap<String, LinkMetadata>),
Empty,
Declined,
}
impl PostBacking {
pub(crate) const fn resolved(map: std::collections::HashMap<String, LinkMetadata>) -> Self {
Self::Resolved(map)
}
pub(crate) const fn empty() -> Self {
Self::Empty
}
pub(crate) const fn declined() -> Self {
Self::Declined
}
pub fn as_link_backing(&self) -> LinkBacking<'_> {
match self {
Self::Resolved(map) => LinkBacking::resolved(map),
Self::Empty => LinkBacking::empty(),
Self::Declined => LinkBacking::declined(),
}
}
}
pub(crate) enum MetadataPlan {
Empty,
Declined,
Links(Vec<(String, std::sync::Arc<crate::server::record::ParsedLink>)>),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LinkMetadata {
pub dbf_type: Option<LinkDbfType>,
pub element_count: Option<i64>,
pub graphic_limits: Option<(f64, f64)>,
pub control_limits: Option<(f64, f64)>,
pub alarm_limits: Option<(f64, f64, f64, f64)>,
pub precision: Option<i16>,
pub units: Option<String>,
pub description: Option<String>,
pub enum_choices: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RemoteAlarm {
pub severity: i32,
pub status: i32,
pub message: String,
}
impl RemoteAlarm {
pub fn from_severity_message(severity: i32, message: String) -> Self {
let status = if severity != 0 {
crate::server::recgbl::alarm_status::LINK_ALARM as i32
} else {
crate::server::recgbl::alarm_status::NO_ALARM as i32
};
Self {
severity,
status,
message,
}
}
}
#[async_trait::async_trait]
pub trait LinkSet: Send + Sync {
fn is_connected(&self, name: &str) -> bool;
fn init_ready(&self, name: &str) -> bool {
self.is_connected(name)
}
async fn get_value(&self, name: &str) -> Option<EpicsValue>;
fn get_cached_value(&self, name: &str) -> Option<EpicsValue> {
let _ = name;
None
}
async fn connect_link(&self, name: &str) {
let _ = self.get_value(name).await;
}
fn put_admission(&self, name: &str) -> PutAdmission {
if self.is_connected(name) {
PutAdmission::Connected
} else {
PutAdmission::Refused
}
}
async fn put_value(&self, name: &str, value: EpicsValue, op: LinkPutOp) -> Result<(), String> {
let _ = (name, value, op);
Err("link set is read-only".into())
}
fn scan_forward(&self, name: &str) -> Result<(), String> {
let _ = name;
Ok(())
}
async fn flush_puts(&self) {}
fn alarm_message(&self, _name: &str) -> Option<String> {
None
}
fn alarm_severity(&self, _name: &str) -> Option<i32> {
None
}
fn alarm_status(&self, _name: &str) -> Option<i32> {
None
}
fn remote_alarm(&self, _name: &str) -> Option<RemoteAlarm> {
None
}
fn time_stamp(&self, _name: &str) -> Option<(i64, i32, u64)> {
None
}
fn link_metadata(&self, _name: &str) -> Option<LinkMetadata> {
None
}
fn link_names(&self) -> Vec<String> {
Vec::new()
}
async fn link_diagnostics(&self, _name: &str) -> Option<LinkDiagnostics> {
None
}
}
pub type DynLinkSet = Arc<dyn LinkSet>;
#[derive(Clone, Default)]
pub struct LinkSetRegistry {
inner: std::collections::HashMap<String, DynLinkSet>,
}
impl LinkSetRegistry {
pub fn new() -> Self {
Self {
inner: std::collections::HashMap::new(),
}
}
pub fn register(&mut self, scheme: &str, lset: DynLinkSet) {
self.inner.insert(scheme.to_string(), lset);
}
pub fn get(&self, scheme: &str) -> Option<DynLinkSet> {
self.inner.get(scheme).cloned()
}
pub fn schemes(&self) -> Vec<String> {
self.inner.keys().cloned().collect()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct StubLset;
#[async_trait::async_trait]
impl LinkSet for StubLset {
fn is_connected(&self, _: &str) -> bool {
true
}
fn get_cached_value(&self, _: &str) -> Option<EpicsValue> {
Some(EpicsValue::Long(42))
}
async fn get_value(&self, name: &str) -> Option<EpicsValue> {
self.get_cached_value(name)
}
}
#[epics_macros_rs::epics_test]
async fn register_and_lookup() {
let mut reg = LinkSetRegistry::new();
assert!(reg.is_empty());
reg.register("pva", Arc::new(StubLset));
assert_eq!(reg.len(), 1);
let lset = reg.get("pva").expect("registered");
assert!(lset.is_connected("anything"));
assert_eq!(lset.get_value("anything").await, Some(EpicsValue::Long(42)));
}
#[test]
fn unknown_scheme_returns_none() {
let reg = LinkSetRegistry::new();
assert!(reg.get("missing").is_none());
}
#[epics_macros_rs::epics_test]
async fn a_late_subscriber_is_refused_rather_than_served_the_records_own_seed() {
use crate::server::recgbl::EventMask;
use crate::types::DbFieldType;
let db = crate::server::database::PvDatabase::new();
let mut src = crate::server::records::ai::AiRecord::new(1.0);
src.egu = "mm".into();
src.prec = 1;
db.add_record("SRC", Box::new(src)).await.unwrap();
let mut calc = crate::server::records::calc::CalcRecord::default();
calc.egu = "V".into();
calc.prec = 7;
calc.set_inp_link(0, "SRC");
calc.calc = "A+1".into();
db.add_record("CALC", Box::new(calc)).await.unwrap();
db.ioc_init().await;
let rec = db.get_record("CALC").expect("record exists");
let backing = db.resolve_link_backed_metadata_for_posts(&rec);
assert!(
backing.as_link_backing().is_declined(),
"with no subscribers the poster door declines rather than resolving"
);
let full = (EventMask::VALUE | EventMask::LOG).bits();
let mut a_rx = rec
.write()
.add_subscriber("A", 1, DbFieldType::Double, full)
.expect("A subscriber");
let mut val_rx = rec
.write()
.add_subscriber("VAL", 2, DbFieldType::Double, full)
.expect("VAL subscriber");
rec.write()
.notify_field_with_origin("A", EventMask::VALUE, 0, backing.as_link_backing());
rec.write()
.notify_field_with_origin("VAL", EventMask::VALUE, 0, backing.as_link_backing());
assert!(
a_rx.try_recv().is_err(),
"A is link-backed and nothing was resolved: the event is refused, not sent carrying CALC's own PREC 7 where SRC's PREC 1 belongs"
);
assert!(
val_rx.try_recv().is_ok(),
"VAL is not link-backed: a declined backing costs it nothing"
);
}
}