use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use spvirit_codec::spvd_decode::{DecodedValue, FieldDesc, FieldType, StructureDesc, TypeCode};
use spvirit_server::PvaServer;
use spvirit_server::pvstore::{PvInfo, Source};
use spvirit_types::{NtPayload, NtScalar, ScalarValue};
use tokio::sync::{RwLock, mpsc};
struct WildcardSource {
prefix: String,
pvs: Arc<RwLock<HashMap<String, ScalarValue>>>,
subscribers: Arc<RwLock<HashMap<String, Vec<mpsc::Sender<NtPayload>>>>>,
}
impl WildcardSource {
fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
pvs: Arc::new(RwLock::new(HashMap::new())),
subscribers: Arc::new(RwLock::new(HashMap::new())),
}
}
fn matches(&self, name: &str) -> bool {
name.starts_with(&self.prefix)
}
}
fn f64_scalar_desc() -> StructureDesc {
StructureDesc {
struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
fields: vec![FieldDesc {
name: "value".to_string(),
field_type: FieldType::Scalar(TypeCode::Float64),
}],
}
}
impl Source for WildcardSource {
fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
let name = name.to_string();
Box::pin(async move {
if !self.matches(&name) {
return None;
}
Some(PvInfo {
descriptor: f64_scalar_desc(),
writable: true,
})
})
}
fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>> {
let name = name.to_string();
Box::pin(async move {
if !self.matches(&name) {
return None;
}
let pvs = self.pvs.read().await;
let val = pvs.get(&name).cloned().unwrap_or(ScalarValue::F64(0.0));
Some(NtPayload::Scalar(NtScalar::from_value(val)))
})
}
fn put(
&self,
name: &str,
value: &DecodedValue,
) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>> {
let name = name.to_string();
let value = value.clone();
Box::pin(async move {
if !self.matches(&name) {
return Err(format!("PV '{}' not in wildcard range", name));
}
let new_val = match &value {
DecodedValue::Float64(v) => ScalarValue::F64(*v),
DecodedValue::Int32(v) => ScalarValue::F64(*v as f64),
DecodedValue::Structure(fields) => fields
.iter()
.find(|(k, _)| k == "value")
.and_then(|(_, v)| match v {
DecodedValue::Float64(f) => Some(ScalarValue::F64(*f)),
DecodedValue::Int32(i) => Some(ScalarValue::F64(*i as f64)),
_ => None,
})
.unwrap_or(ScalarValue::F64(0.0)),
_ => return Err("unsupported value type".to_string()),
};
let created = {
let mut pvs = self.pvs.write().await;
let is_new = !pvs.contains_key(&name);
pvs.insert(name.clone(), new_val.clone());
is_new
};
if created {
println!("[wildcard] created PV '{}'", name);
} else {
println!("[wildcard] updated PV '{}' = {:?}", name, new_val);
}
let payload = NtPayload::Scalar(NtScalar::from_value(new_val));
let mut subs = self.subscribers.write().await;
if let Some(senders) = subs.get_mut(&name) {
senders.retain(|tx| tx.try_send(payload.clone()).is_ok());
}
Ok(vec![(name, payload)])
})
}
fn subscribe(
&self,
name: &str,
) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>> {
let name = name.to_string();
Box::pin(async move {
if !self.matches(&name) {
return None;
}
let (tx, rx) = mpsc::channel(64);
self.subscribers
.write()
.await
.entry(name)
.or_default()
.push(tx);
Some(rx)
})
}
fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>> {
Box::pin(async move {
let pvs = self.pvs.read().await;
pvs.keys().cloned().collect()
})
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = PvaServer::builder()
.ai("STATIC:HEARTBEAT", 0.0)
.source("wildcard", 10, Arc::new(WildcardSource::new("XYZ:")))
.build();
let store = server.store().clone();
tokio::spawn(async move {
let mut tick = 0u64;
loop {
store
.set_value("STATIC:HEARTBEAT", ScalarValue::F64(tick as f64))
.await;
tick += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
println!("Wildcard source server running on port 5075");
println!(" Built-in PV: STATIC:HEARTBEAT");
println!(" Wildcard PVs: XYZ:<anything> (created on first PUT)");
println!();
println!("Try: spput XYZ:MyValue 42.0");
println!(" spget XYZ:MyValue");
server.run().await
}