1use crate::PlcValue;
2use crate::error::{EtherNetIpError, Result};
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::atomic::AtomicBool;
6use std::sync::{LazyLock, Mutex as StdMutex};
7use tokio::sync::{Mutex, mpsc};
8
9use futures::{Stream, stream};
10
11#[derive(Debug, Clone)]
13pub struct SubscriptionOptions {
14 pub update_rate: u32,
16 pub change_threshold: f32,
21 pub timeout: u32,
23}
24
25impl Default for SubscriptionOptions {
26 fn default() -> Self {
27 Self {
28 update_rate: 100, change_threshold: 0.001, timeout: 5000, }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct TagSubscription {
38 pub tag_path: String,
40 pub options: SubscriptionOptions,
42 pub last_value: Arc<Mutex<Option<PlcValue>>>,
44 pub sender: Arc<Mutex<mpsc::Sender<PlcValue>>>,
46 pub receiver: Arc<Mutex<mpsc::Receiver<PlcValue>>>,
48 pub is_active: Arc<AtomicBool>,
50}
51
52#[derive(Debug, Clone, PartialEq)]
54#[non_exhaustive]
55pub enum TagSubscriptionEvent {
56 Value(PlcValue),
58 Error { message: String, terminal: bool },
60}
61
62#[derive(Clone)]
63struct TagSubscriptionEventChannels {
64 sender: Arc<Mutex<mpsc::Sender<TagSubscriptionEvent>>>,
65 receiver: Arc<Mutex<mpsc::Receiver<TagSubscriptionEvent>>>,
66}
67
68static TAG_SUBSCRIPTION_EVENTS: LazyLock<StdMutex<HashMap<usize, TagSubscriptionEventChannels>>> =
69 LazyLock::new(|| StdMutex::new(HashMap::new()));
70
71impl TagSubscription {
72 pub fn new(tag_name: String, options: SubscriptionOptions) -> Self {
74 let (sender, receiver) = mpsc::channel(100); let subscription = Self {
76 tag_path: tag_name,
77 options,
78 last_value: Arc::new(Mutex::new(None)),
79 sender: Arc::new(Mutex::new(sender)),
80 receiver: Arc::new(Mutex::new(receiver)),
81 is_active: Arc::new(AtomicBool::new(true)),
82 };
83 subscription.event_channels();
84 subscription
85 }
86
87 pub fn is_active(&self) -> bool {
89 self.is_active.load(std::sync::atomic::Ordering::Relaxed)
90 }
91
92 pub fn stop(&self) {
94 self.is_active
95 .store(false, std::sync::atomic::Ordering::Relaxed);
96 }
97
98 pub async fn update_value(&self, value: &PlcValue) -> Result<()> {
104 let mut last_value = self.last_value.lock().await;
105
106 if let Some(old) = last_value.as_ref()
108 && !Self::value_changed(old, value, self.options.change_threshold)
109 {
110 return Ok(());
111 }
112
113 *last_value = Some(value.clone());
115 drop(last_value);
116 try_send_drop_oldest(&self.sender, &self.receiver, value.clone())
117 .await
118 .map_err(|e| EtherNetIpError::Subscription(format!("Failed to send update: {e}")))?;
119 try_send_drop_oldest(
120 &self.event_channels().sender,
121 &self.event_channels().receiver,
122 TagSubscriptionEvent::Value(value.clone()),
123 )
124 .await
125 .map_err(|e| EtherNetIpError::Subscription(format!("Failed to send event: {e}")))?;
126
127 Ok(())
128 }
129
130 pub async fn publish_error(&self, error: &EtherNetIpError, terminal: bool) -> Result<()> {
135 if terminal {
136 self.stop();
137 }
138
139 try_send_drop_oldest(
140 &self.event_channels().sender,
141 &self.event_channels().receiver,
142 TagSubscriptionEvent::Error {
143 message: error.to_string(),
144 terminal,
145 },
146 )
147 .await
148 .map_err(|e| EtherNetIpError::Subscription(format!("Failed to send event: {e}")))
149 }
150
151 fn value_changed(old: &PlcValue, new: &PlcValue, threshold: f32) -> bool {
155 match (old, new) {
156 (PlcValue::Real(o), PlcValue::Real(n)) => (*n - *o).abs() >= threshold,
157 (PlcValue::Lreal(o), PlcValue::Lreal(n)) => (*n - *o).abs() >= threshold as f64,
158 (PlcValue::Bool(o), PlcValue::Bool(n)) => o != n,
159 (PlcValue::Sint(o), PlcValue::Sint(n)) => o != n,
160 (PlcValue::Int(o), PlcValue::Int(n)) => o != n,
161 (PlcValue::Dint(o), PlcValue::Dint(n)) => o != n,
162 (PlcValue::Lint(o), PlcValue::Lint(n)) => o != n,
163 (PlcValue::Usint(o), PlcValue::Usint(n)) => o != n,
164 (PlcValue::Uint(o), PlcValue::Uint(n)) => o != n,
165 (PlcValue::Udint(o), PlcValue::Udint(n)) => o != n,
166 (PlcValue::Ulint(o), PlcValue::Ulint(n)) => o != n,
167 (PlcValue::String(o), PlcValue::String(n)) => o != n,
168 _ => true,
170 }
171 }
172
173 pub async fn wait_for_update(&self) -> Result<PlcValue> {
175 let mut receiver = self.receiver.lock().await;
176 let next_value = receiver.recv().await;
177 drop(receiver);
178 next_value.ok_or_else(|| EtherNetIpError::Subscription("Channel closed".to_string()))
179 }
180
181 pub async fn wait_for_event(&self) -> Result<TagSubscriptionEvent> {
183 let channels = self.event_channels();
184 let mut receiver = channels.receiver.lock().await;
185 let next_event = receiver.recv().await;
186 drop(receiver);
187 next_event.ok_or_else(|| EtherNetIpError::Subscription("Channel closed".to_string()))
188 }
189
190 pub async fn get_last_value(&self) -> Option<PlcValue> {
192 self.last_value.lock().await.clone()
193 }
194
195 async fn recv_next_value(&self) -> Option<PlcValue> {
196 let mut receiver = self.receiver.lock().await;
197 let next_value = receiver.recv().await;
198 drop(receiver);
199 next_value
200 }
201
202 async fn recv_next_event(&self) -> Option<TagSubscriptionEvent> {
203 let channels = self.event_channels();
204 let mut receiver = channels.receiver.lock().await;
205 let next_event = receiver.recv().await;
206 drop(receiver);
207 next_event
208 }
209
210 pub fn into_stream(self: Arc<Self>) -> impl Stream<Item = PlcValue> + Send {
227 stream::unfold(self, |subscription| async move {
228 let next_value = subscription.recv_next_value().await;
229 next_value.map(|plc_value| (plc_value, subscription))
230 })
231 }
232
233 pub fn into_event_stream(self: Arc<Self>) -> impl Stream<Item = TagSubscriptionEvent> + Send {
235 stream::unfold(self, |subscription| async move {
236 let next_event = subscription.recv_next_event().await;
237 next_event.map(|event| (event, subscription))
238 })
239 }
240
241 fn event_channels(&self) -> TagSubscriptionEventChannels {
242 let key = self.event_key();
243 let mut channels = TAG_SUBSCRIPTION_EVENTS
244 .lock()
245 .unwrap_or_else(std::sync::PoisonError::into_inner);
246 channels
247 .entry(key)
248 .or_insert_with(|| {
249 let (sender, receiver) = mpsc::channel(100);
250 TagSubscriptionEventChannels {
251 sender: Arc::new(Mutex::new(sender)),
252 receiver: Arc::new(Mutex::new(receiver)),
253 }
254 })
255 .clone()
256 }
257
258 fn event_key(&self) -> usize {
259 Arc::as_ptr(&self.is_active) as usize
260 }
261}
262
263impl Drop for TagSubscription {
264 fn drop(&mut self) {
265 if Arc::strong_count(&self.is_active) == 1
266 && let Ok(mut channels) = TAG_SUBSCRIPTION_EVENTS.lock()
267 {
268 channels.remove(&self.event_key());
269 }
270 }
271}
272
273pub(crate) async fn try_send_drop_oldest<T>(
274 sender: &Arc<Mutex<mpsc::Sender<T>>>,
275 receiver: &Arc<Mutex<mpsc::Receiver<T>>>,
276 value: T,
277) -> std::result::Result<(), String> {
278 let sender = {
279 let sender = sender.lock().await;
280 sender.clone()
281 };
282
283 match sender.try_send(value) {
284 Ok(()) => Ok(()),
285 Err(mpsc::error::TrySendError::Closed(_)) => Err("channel closed".to_string()),
286 Err(mpsc::error::TrySendError::Full(value)) => {
287 if let Ok(mut receiver) = receiver.try_lock() {
288 let _ = receiver.try_recv();
289 match sender.try_send(value) {
290 Ok(()) => Ok(()),
291 Err(mpsc::error::TrySendError::Closed(_)) => Err("channel closed".to_string()),
292 Err(mpsc::error::TrySendError::Full(_)) => Ok(()),
293 }
294 } else {
295 Ok(())
296 }
297 }
298 }
299}
300
301#[derive(Debug, Clone)]
303#[deprecated(
304 since = "1.2.0",
305 note = "SubscriptionManager is not used by EipClient; use EipClient subscription methods or Client tag groups instead. The type will be removed in 2.0."
306)]
307pub struct SubscriptionManager {
308 subscriptions: Arc<Mutex<Vec<TagSubscription>>>,
309}
310
311#[expect(
312 deprecated,
313 reason = "CODEX-AQ keeps SubscriptionManager compatibility until 2.0 removal"
314)]
315impl Default for SubscriptionManager {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321#[expect(
322 deprecated,
323 reason = "CODEX-AQ keeps SubscriptionManager compatibility until 2.0 removal"
324)]
325impl SubscriptionManager {
326 pub fn new() -> Self {
328 Self {
329 subscriptions: Arc::new(Mutex::new(Vec::new())),
330 }
331 }
332
333 pub async fn add_subscription(&self, subscription: TagSubscription) {
335 let mut subscriptions = self.subscriptions.lock().await;
336 subscriptions.push(subscription);
337 }
338
339 pub async fn remove_subscription(&self, tag_name: &str) {
341 let mut subscriptions = self.subscriptions.lock().await;
342 subscriptions.retain(|sub| sub.tag_path != tag_name);
343 }
344
345 pub async fn update_value(&self, tag_name: &str, value: &PlcValue) -> Result<()> {
347 let subscriptions = {
348 let subscriptions = self.subscriptions.lock().await;
349 subscriptions.clone()
350 };
351 for subscription in &subscriptions {
352 if subscription.tag_path == tag_name && subscription.is_active() {
353 subscription.update_value(value).await?;
354 }
355 }
356 Ok(())
357 }
358
359 pub async fn get_subscriptions(&self) -> Vec<TagSubscription> {
361 let subscriptions = self.subscriptions.lock().await;
362 subscriptions.clone()
363 }
364
365 pub async fn get_subscription(&self, tag_name: &str) -> Option<TagSubscription> {
367 let subscriptions = self.subscriptions.lock().await;
368 subscriptions
369 .iter()
370 .find(|sub| sub.tag_path == tag_name)
371 .cloned()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn real_deadband_is_absolute_not_relative() {
381 let threshold = 0.001_f32;
384 assert!(!TagSubscription::value_changed(
386 &PlcValue::Real(1000.0),
387 &PlcValue::Real(1000.0005),
388 threshold
389 ));
390 assert!(TagSubscription::value_changed(
392 &PlcValue::Real(1000.0),
393 &PlcValue::Real(1000.002),
394 threshold
395 ));
396 assert!(TagSubscription::value_changed(
399 &PlcValue::Real(0.0),
400 &PlcValue::Real(0.002),
401 threshold
402 ));
403 }
404
405 #[test]
406 fn non_float_types_notify_on_any_change() {
407 assert!(TagSubscription::value_changed(
408 &PlcValue::Dint(1),
409 &PlcValue::Dint(2),
410 0.001
411 ));
412 assert!(!TagSubscription::value_changed(
413 &PlcValue::Dint(5),
414 &PlcValue::Dint(5),
415 0.001
416 ));
417 }
418}