Skip to main content

rust_ethernet_ip/
subscription.rs

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/// Configuration options for tag subscriptions
12#[derive(Debug, Clone)]
13pub struct SubscriptionOptions {
14    /// Update rate in milliseconds
15    pub update_rate: u32,
16    /// Absolute change threshold (deadband) applied to floating-point values: a
17    /// REAL/LREAL update notifies only when `|new - old| >= change_threshold`.
18    /// This is an absolute delta, not a percentage. Non-float types notify on
19    /// any change regardless of this value.
20    pub change_threshold: f32,
21    /// Timeout in milliseconds
22    pub timeout: u32,
23}
24
25impl Default for SubscriptionOptions {
26    fn default() -> Self {
27        Self {
28            update_rate: 100,        // 100ms default update rate
29            change_threshold: 0.001, // absolute deadband for REAL/LREAL
30            timeout: 5000,           // 5 second timeout
31        }
32    }
33}
34
35/// Represents a subscription to a PLC tag
36#[derive(Debug, Clone)]
37pub struct TagSubscription {
38    /// The path of the subscribed tag
39    pub tag_path: String,
40    /// Subscription configuration
41    pub options: SubscriptionOptions,
42    /// Last received value
43    pub last_value: Arc<Mutex<Option<PlcValue>>>,
44    /// Channel sender for value updates
45    pub sender: Arc<Mutex<mpsc::Sender<PlcValue>>>,
46    /// Channel receiver for value updates
47    pub receiver: Arc<Mutex<mpsc::Receiver<PlcValue>>>,
48    /// Whether the subscription is active
49    pub is_active: Arc<AtomicBool>,
50}
51
52/// Event emitted by a tag subscription poll loop.
53#[derive(Debug, Clone, PartialEq)]
54#[non_exhaustive]
55pub enum TagSubscriptionEvent {
56    /// A value update passed the subscription's deadband and was published.
57    Value(PlcValue),
58    /// A polling error occurred. `terminal` means the poll loop stopped.
59    Error {
60        /// Human-readable polling failure.
61        message: String,
62        /// Whether the subscription stopped after this error.
63        terminal: bool,
64    },
65}
66
67#[derive(Clone)]
68struct TagSubscriptionEventChannels {
69    sender: Arc<Mutex<mpsc::Sender<TagSubscriptionEvent>>>,
70    receiver: Arc<Mutex<mpsc::Receiver<TagSubscriptionEvent>>>,
71}
72
73static TAG_SUBSCRIPTION_EVENTS: LazyLock<StdMutex<HashMap<usize, TagSubscriptionEventChannels>>> =
74    LazyLock::new(|| StdMutex::new(HashMap::new()));
75
76impl TagSubscription {
77    /// Creates a new tag subscription
78    pub fn new(tag_name: String, options: SubscriptionOptions) -> Self {
79        let (sender, receiver) = mpsc::channel(100); // Buffer size of 100
80        let subscription = Self {
81            tag_path: tag_name,
82            options,
83            last_value: Arc::new(Mutex::new(None)),
84            sender: Arc::new(Mutex::new(sender)),
85            receiver: Arc::new(Mutex::new(receiver)),
86            is_active: Arc::new(AtomicBool::new(true)),
87        };
88        subscription.event_channels();
89        subscription
90    }
91
92    /// Checks if the subscription is active
93    pub fn is_active(&self) -> bool {
94        self.is_active.load(std::sync::atomic::Ordering::Relaxed)
95    }
96
97    /// Stops the subscription
98    pub fn stop(&self) {
99        self.is_active
100            .store(false, std::sync::atomic::Ordering::Relaxed);
101    }
102
103    /// Updates the subscription value.
104    ///
105    /// Update delivery is nonblocking. If a consumer falls behind and the
106    /// bounded channel is full, the oldest queued item is dropped where possible
107    /// so the background poll task can keep running.
108    pub async fn update_value(&self, value: &PlcValue) -> Result<()> {
109        let mut last_value = self.last_value.lock().await;
110
111        // Check if value has changed enough to notify
112        if let Some(old) = last_value.as_ref()
113            && !Self::value_changed(old, value, self.options.change_threshold)
114        {
115            return Ok(());
116        }
117
118        // Update value and send notification
119        *last_value = Some(value.clone());
120        drop(last_value);
121        try_send_drop_oldest(&self.sender, &self.receiver, value.clone())
122            .await
123            .map_err(|e| EtherNetIpError::Subscription(format!("Failed to send update: {e}")))?;
124        try_send_drop_oldest(
125            &self.event_channels().sender,
126            &self.event_channels().receiver,
127            TagSubscriptionEvent::Value(value.clone()),
128        )
129        .await
130        .map_err(|e| EtherNetIpError::Subscription(format!("Failed to send event: {e}")))?;
131
132        Ok(())
133    }
134
135    /// Publishes a polling error without blocking the poll task.
136    ///
137    /// If the event buffer is full, the oldest queued event is dropped where
138    /// possible. If `terminal` is true, the subscription is marked inactive.
139    pub async fn publish_error(&self, error: &EtherNetIpError, terminal: bool) -> Result<()> {
140        if terminal {
141            self.stop();
142        }
143
144        try_send_drop_oldest(
145            &self.event_channels().sender,
146            &self.event_channels().receiver,
147            TagSubscriptionEvent::Error {
148                message: error.to_string(),
149                terminal,
150            },
151        )
152        .await
153        .map_err(|e| EtherNetIpError::Subscription(format!("Failed to send event: {e}")))
154    }
155
156    /// Checks whether a value has changed enough to warrant a notification.
157    /// For floating-point types, uses the change_threshold as a deadband.
158    /// For all other types, triggers on any change.
159    fn value_changed(old: &PlcValue, new: &PlcValue, threshold: f32) -> bool {
160        match (old, new) {
161            (PlcValue::Real(o), PlcValue::Real(n)) => (*n - *o).abs() >= threshold,
162            (PlcValue::Lreal(o), PlcValue::Lreal(n)) => (*n - *o).abs() >= threshold as f64,
163            (PlcValue::Bool(o), PlcValue::Bool(n)) => o != n,
164            (PlcValue::Sint(o), PlcValue::Sint(n)) => o != n,
165            (PlcValue::Int(o), PlcValue::Int(n)) => o != n,
166            (PlcValue::Dint(o), PlcValue::Dint(n)) => o != n,
167            (PlcValue::Lint(o), PlcValue::Lint(n)) => o != n,
168            (PlcValue::Usint(o), PlcValue::Usint(n)) => o != n,
169            (PlcValue::Uint(o), PlcValue::Uint(n)) => o != n,
170            (PlcValue::Udint(o), PlcValue::Udint(n)) => o != n,
171            (PlcValue::Ulint(o), PlcValue::Ulint(n)) => o != n,
172            (PlcValue::String(o), PlcValue::String(n)) => o != n,
173            // Different types or UDTs — always notify
174            _ => true,
175        }
176    }
177
178    /// Waits for the next value update
179    pub async fn wait_for_update(&self) -> Result<PlcValue> {
180        let mut receiver = self.receiver.lock().await;
181        let next_value = receiver.recv().await;
182        drop(receiver);
183        next_value.ok_or_else(|| EtherNetIpError::Subscription("Channel closed".to_string()))
184    }
185
186    /// Waits for the next value/error event.
187    pub async fn wait_for_event(&self) -> Result<TagSubscriptionEvent> {
188        let channels = self.event_channels();
189        let mut receiver = channels.receiver.lock().await;
190        let next_event = receiver.recv().await;
191        drop(receiver);
192        next_event.ok_or_else(|| EtherNetIpError::Subscription("Channel closed".to_string()))
193    }
194
195    /// Gets the last value received
196    pub async fn get_last_value(&self) -> Option<PlcValue> {
197        self.last_value.lock().await.clone()
198    }
199
200    async fn recv_next_value(&self) -> Option<PlcValue> {
201        let mut receiver = self.receiver.lock().await;
202        let next_value = receiver.recv().await;
203        drop(receiver);
204        next_value
205    }
206
207    async fn recv_next_event(&self) -> Option<TagSubscriptionEvent> {
208        let channels = self.event_channels();
209        let mut receiver = channels.receiver.lock().await;
210        let next_event = receiver.recv().await;
211        drop(receiver);
212        next_event
213    }
214
215    /// Returns an async stream of value updates for this subscription.
216    ///
217    /// The stream yields each value as it is received from the background poll loop.
218    /// Use with `StreamExt` (e.g. `.next().await`) or `select!` for composition.
219    ///
220    /// # Example
221    ///
222    /// ```ignore
223    /// use futures_util::StreamExt;
224    ///
225    /// let subscription = client.subscribe_to_tag("MyTag", SubscriptionOptions::default()).await?;
226    /// let mut stream = subscription.into_stream();
227    /// while let Some(value) = stream.next().await {
228    ///     println!("Update: {:?}", value);
229    /// }
230    /// ```
231    pub fn into_stream(self: Arc<Self>) -> impl Stream<Item = PlcValue> + Send {
232        stream::unfold(self, |subscription| async move {
233            let next_value = subscription.recv_next_value().await;
234            next_value.map(|plc_value| (plc_value, subscription))
235        })
236    }
237
238    /// Returns an async stream of value/error events for this subscription.
239    pub fn into_event_stream(self: Arc<Self>) -> impl Stream<Item = TagSubscriptionEvent> + Send {
240        stream::unfold(self, |subscription| async move {
241            let next_event = subscription.recv_next_event().await;
242            next_event.map(|event| (event, subscription))
243        })
244    }
245
246    fn event_channels(&self) -> TagSubscriptionEventChannels {
247        let key = self.event_key();
248        let mut channels = TAG_SUBSCRIPTION_EVENTS
249            .lock()
250            .unwrap_or_else(std::sync::PoisonError::into_inner);
251        channels
252            .entry(key)
253            .or_insert_with(|| {
254                let (sender, receiver) = mpsc::channel(100);
255                TagSubscriptionEventChannels {
256                    sender: Arc::new(Mutex::new(sender)),
257                    receiver: Arc::new(Mutex::new(receiver)),
258                }
259            })
260            .clone()
261    }
262
263    fn event_key(&self) -> usize {
264        Arc::as_ptr(&self.is_active) as usize
265    }
266}
267
268impl Drop for TagSubscription {
269    fn drop(&mut self) {
270        if Arc::strong_count(&self.is_active) == 1
271            && let Ok(mut channels) = TAG_SUBSCRIPTION_EVENTS.lock()
272        {
273            channels.remove(&self.event_key());
274        }
275    }
276}
277
278pub(crate) async fn try_send_drop_oldest<T>(
279    sender: &Arc<Mutex<mpsc::Sender<T>>>,
280    receiver: &Arc<Mutex<mpsc::Receiver<T>>>,
281    value: T,
282) -> std::result::Result<(), String> {
283    let sender = {
284        let sender = sender.lock().await;
285        sender.clone()
286    };
287
288    match sender.try_send(value) {
289        Ok(()) => Ok(()),
290        Err(mpsc::error::TrySendError::Closed(_)) => Err("channel closed".to_string()),
291        Err(mpsc::error::TrySendError::Full(value)) => {
292            if let Ok(mut receiver) = receiver.try_lock() {
293                let _ = receiver.try_recv();
294                match sender.try_send(value) {
295                    Ok(()) => Ok(()),
296                    Err(mpsc::error::TrySendError::Closed(_)) => Err("channel closed".to_string()),
297                    Err(mpsc::error::TrySendError::Full(_)) => Ok(()),
298                }
299            } else {
300                Ok(())
301            }
302        }
303    }
304}
305
306/// Manages multiple tag subscriptions
307#[derive(Debug, Clone)]
308#[deprecated(
309    since = "1.2.0",
310    note = "SubscriptionManager is not used by EipClient; use EipClient subscription methods or Client tag groups instead. The type will be removed in 2.0."
311)]
312pub struct SubscriptionManager {
313    subscriptions: Arc<Mutex<Vec<TagSubscription>>>,
314}
315
316#[expect(
317    deprecated,
318    reason = "CODEX-AQ keeps SubscriptionManager compatibility until 2.0 removal"
319)]
320impl Default for SubscriptionManager {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326#[expect(
327    deprecated,
328    reason = "CODEX-AQ keeps SubscriptionManager compatibility until 2.0 removal"
329)]
330impl SubscriptionManager {
331    /// Creates a new subscription manager
332    pub fn new() -> Self {
333        Self {
334            subscriptions: Arc::new(Mutex::new(Vec::new())),
335        }
336    }
337
338    /// Adds a new subscription
339    pub async fn add_subscription(&self, subscription: TagSubscription) {
340        let mut subscriptions = self.subscriptions.lock().await;
341        subscriptions.push(subscription);
342    }
343
344    /// Removes a subscription
345    pub async fn remove_subscription(&self, tag_name: &str) {
346        let mut subscriptions = self.subscriptions.lock().await;
347        subscriptions.retain(|sub| sub.tag_path != tag_name);
348    }
349
350    /// Updates a value for all matching subscriptions
351    pub async fn update_value(&self, tag_name: &str, value: &PlcValue) -> Result<()> {
352        let subscriptions = {
353            let subscriptions = self.subscriptions.lock().await;
354            subscriptions.clone()
355        };
356        for subscription in &subscriptions {
357            if subscription.tag_path == tag_name && subscription.is_active() {
358                subscription.update_value(value).await?;
359            }
360        }
361        Ok(())
362    }
363
364    /// Gets all active subscriptions
365    pub async fn get_subscriptions(&self) -> Vec<TagSubscription> {
366        let subscriptions = self.subscriptions.lock().await;
367        subscriptions.clone()
368    }
369
370    /// Gets a specific subscription by tag name
371    pub async fn get_subscription(&self, tag_name: &str) -> Option<TagSubscription> {
372        let subscriptions = self.subscriptions.lock().await;
373        subscriptions
374            .iter()
375            .find(|sub| sub.tag_path == tag_name)
376            .cloned()
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn real_deadband_is_absolute_not_relative() {
386        // change_threshold is an absolute deadband: a delta below it is
387        // suppressed, a delta at/above it notifies, regardless of magnitude.
388        let threshold = 0.001_f32;
389        // Below the deadband -> no notification.
390        assert!(!TagSubscription::value_changed(
391            &PlcValue::Real(1000.0),
392            &PlcValue::Real(1000.0005),
393            threshold
394        ));
395        // At/above the deadband -> notify.
396        assert!(TagSubscription::value_changed(
397            &PlcValue::Real(1000.0),
398            &PlcValue::Real(1000.002),
399            threshold
400        ));
401        // Same absolute delta near zero behaves identically (proves absolute,
402        // not relative/percentage, semantics).
403        assert!(TagSubscription::value_changed(
404            &PlcValue::Real(0.0),
405            &PlcValue::Real(0.002),
406            threshold
407        ));
408    }
409
410    #[test]
411    fn non_float_types_notify_on_any_change() {
412        assert!(TagSubscription::value_changed(
413            &PlcValue::Dint(1),
414            &PlcValue::Dint(2),
415            0.001
416        ));
417        assert!(!TagSubscription::value_changed(
418            &PlcValue::Dint(5),
419            &PlcValue::Dint(5),
420            0.001
421        ));
422    }
423}