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 { 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    /// Creates a new tag subscription
73    pub fn new(tag_name: String, options: SubscriptionOptions) -> Self {
74        let (sender, receiver) = mpsc::channel(100); // Buffer size of 100
75        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    /// Checks if the subscription is active
88    pub fn is_active(&self) -> bool {
89        self.is_active.load(std::sync::atomic::Ordering::Relaxed)
90    }
91
92    /// Stops the subscription
93    pub fn stop(&self) {
94        self.is_active
95            .store(false, std::sync::atomic::Ordering::Relaxed);
96    }
97
98    /// Updates the subscription value.
99    ///
100    /// Update delivery is nonblocking. If a consumer falls behind and the
101    /// bounded channel is full, the oldest queued item is dropped where possible
102    /// so the background poll task can keep running.
103    pub async fn update_value(&self, value: &PlcValue) -> Result<()> {
104        let mut last_value = self.last_value.lock().await;
105
106        // Check if value has changed enough to notify
107        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        // Update value and send notification
114        *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    /// Publishes a polling error without blocking the poll task.
131    ///
132    /// If the event buffer is full, the oldest queued event is dropped where
133    /// possible. If `terminal` is true, the subscription is marked inactive.
134    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    /// Checks whether a value has changed enough to warrant a notification.
152    /// For floating-point types, uses the change_threshold as a deadband.
153    /// For all other types, triggers on any change.
154    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            // Different types or UDTs — always notify
169            _ => true,
170        }
171    }
172
173    /// Waits for the next value update
174    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    /// Waits for the next value/error event.
182    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    /// Gets the last value received
191    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    /// Returns an async stream of value updates for this subscription.
211    ///
212    /// The stream yields each value as it is received from the background poll loop.
213    /// Use with `StreamExt` (e.g. `.next().await`) or `select!` for composition.
214    ///
215    /// # Example
216    ///
217    /// ```ignore
218    /// use futures_util::StreamExt;
219    ///
220    /// let subscription = client.subscribe_to_tag("MyTag", SubscriptionOptions::default()).await?;
221    /// let mut stream = subscription.into_stream();
222    /// while let Some(value) = stream.next().await {
223    ///     println!("Update: {:?}", value);
224    /// }
225    /// ```
226    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    /// Returns an async stream of value/error events for this subscription.
234    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/// Manages multiple tag subscriptions
302#[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    /// Creates a new subscription manager
327    pub fn new() -> Self {
328        Self {
329            subscriptions: Arc::new(Mutex::new(Vec::new())),
330        }
331    }
332
333    /// Adds a new subscription
334    pub async fn add_subscription(&self, subscription: TagSubscription) {
335        let mut subscriptions = self.subscriptions.lock().await;
336        subscriptions.push(subscription);
337    }
338
339    /// Removes a subscription
340    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    /// Updates a value for all matching subscriptions
346    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    /// Gets all active subscriptions
360    pub async fn get_subscriptions(&self) -> Vec<TagSubscription> {
361        let subscriptions = self.subscriptions.lock().await;
362        subscriptions.clone()
363    }
364
365    /// Gets a specific subscription by tag name
366    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        // change_threshold is an absolute deadband: a delta below it is
382        // suppressed, a delta at/above it notifies, regardless of magnitude.
383        let threshold = 0.001_f32;
384        // Below the deadband -> no notification.
385        assert!(!TagSubscription::value_changed(
386            &PlcValue::Real(1000.0),
387            &PlcValue::Real(1000.0005),
388            threshold
389        ));
390        // At/above the deadband -> notify.
391        assert!(TagSubscription::value_changed(
392            &PlcValue::Real(1000.0),
393            &PlcValue::Real(1000.002),
394            threshold
395        ));
396        // Same absolute delta near zero behaves identically (proves absolute,
397        // not relative/percentage, semantics).
398        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}