Skip to main content

zerodds_dcps/
status.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Communication-Status-Strukturen (DDS DCPS 1.4 §2.2.4.1, Tab. 2.10).
4//!
5//! Die Spec definiert **13 Standard-Communication-Statuses**, die als
6//! Bitmask in `StatusMask` zusammengeführt werden. Jeder Status hat eine
7//! zugeordnete Datenstruktur, die `get_<status>()` auf der jeweiligen
8//! Entity zurückliefert. Die Bitmask-Konstanten (zur Verbindung Status ↔
9//! Bit) leben in [`crate::psm_constants::status`].
10//!
11//! ## Klassifikation der 13 Statuses (Spec §2.2.4.1):
12//!
13//! - **PLAIN**-Statuses (nur `total_count` + `total_count_change`):
14//!   - `INCONSISTENT_TOPIC`         (Topic)
15//!   - `SAMPLE_LOST`                (DataReader)
16//!   - `LIVELINESS_LOST`            (DataWriter)
17//!   - `OFFERED_DEADLINE_MISSED`    (DataWriter)
18//!   - `REQUESTED_DEADLINE_MISSED`  (DataReader)
19//!
20//! - **STATEFUL**-Statuses (mit `last_*` + Detail-Feldern):
21//!   - `SAMPLE_REJECTED`            (DataReader)
22//!   - `LIVELINESS_CHANGED`         (DataReader)
23//!   - `PUBLICATION_MATCHED`        (DataWriter)
24//!   - `SUBSCRIPTION_MATCHED`       (DataReader)
25//!   - `OFFERED_INCOMPATIBLE_QOS`   (DataWriter)
26//!   - `REQUESTED_INCOMPATIBLE_QOS` (DataReader)
27//!
28//! - **SIGNAL**-Statuses (rein als Bit, ohne Datenstruktur — wir
29//!   spiegeln sie als Marker-Structs für die Vollständigkeit der
30//!   Tabelle und für eine einheitliche `get_*`-API):
31//!   - `DATA_AVAILABLE`             (DataReader)
32//!   - `DATA_ON_READERS`            (Subscriber)
33//!
34//! `total_count_change` ist als `i32` getypt: die Spec sagt
35//! "incremental count since the last time the listener was called or
36//! the status was read", was negativ werden kann, wenn der Reader
37//! Liveliness wieder gewinnt (LIVELINESS_CHANGED). Wir bleiben bei
38//! `i32` für alle `*_count_change`-Felder, um spec-konform zu sein.
39
40extern crate alloc;
41
42use alloc::vec::Vec;
43
44use crate::instance_handle::InstanceHandle;
45
46// ============================================================================
47// Plain-Counter-Statuses
48// ============================================================================
49
50/// `INCONSISTENT_TOPIC_STATUS` — Spec §2.2.4.1 Tab. 2.10 + §2.2.2.3.2.
51///
52/// "Another topic exists with the same name but different
53/// characteristics." Wird auf `Topic`-Ebene gepflegt.
54#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct InconsistentTopicStatus {
56    /// Total cumulative count of inconsistencies detected.
57    pub total_count: i32,
58    /// Increment since last read.
59    pub total_count_change: i32,
60}
61
62/// `SAMPLE_LOST_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
63///
64/// "All samples that have been lost (never received) by the
65/// DataReader."
66#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
67pub struct SampleLostStatus {
68    /// Total cumulative count of all lost samples.
69    pub total_count: i32,
70    /// Increment since last read.
71    pub total_count_change: i32,
72}
73
74/// `LIVELINESS_LOST_STATUS` — Spec §2.2.4.1 + §2.2.2.4.2.
75///
76/// Counter, wie oft der DataWriter aus Sicht des LIVELINESS-QoS-Vertrags
77/// als "not alive" deklariert worden ist (Writer-Seite).
78#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
79pub struct LivelinessLostStatus {
80    /// Total cumulative count of times the writer was declared
81    /// not-alive.
82    pub total_count: i32,
83    /// Increment since last read.
84    pub total_count_change: i32,
85}
86
87/// `OFFERED_DEADLINE_MISSED_STATUS` — Spec §2.2.4.1 + §2.2.2.4.2.
88///
89/// Counter, wie oft der Writer das offered DEADLINE-Versprechen nicht
90/// einhalten konnte. Pflegt zusätzlich den `last_instance_handle`,
91/// gegen den die Verletzung gezählt wurde.
92#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
93pub struct OfferedDeadlineMissedStatus {
94    /// Total cumulative count of offered-deadline misses.
95    pub total_count: i32,
96    /// Increment since last read.
97    pub total_count_change: i32,
98    /// Handle of the last instance for which the deadline was missed.
99    pub last_instance_handle: InstanceHandle,
100}
101
102/// `REQUESTED_DEADLINE_MISSED_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
103///
104/// Reader-Seite. Counter, wie oft der Reader keine Sample innerhalb des
105/// requested DEADLINE bekommen hat.
106#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct RequestedDeadlineMissedStatus {
108    /// Total cumulative count of requested-deadline misses.
109    pub total_count: i32,
110    /// Increment since last read.
111    pub total_count_change: i32,
112    /// Handle of the last instance for which the deadline was missed.
113    pub last_instance_handle: InstanceHandle,
114}
115
116// ============================================================================
117// Stateful-Statuses (mit `last_*` + Detail-Feldern)
118// ============================================================================
119
120/// `SampleRejectedStatusKind` — Reason warum der letzte Sample rejected
121/// wurde. Spec §2.2.4.1 Tab. 2.10 (kind enum unter `SAMPLE_REJECTED`).
122#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum SampleRejectedStatusKind {
124    /// Kein Sample wurde rejected (Default).
125    #[default]
126    NotRejected,
127    /// Reader-Resource-Limit `max_instances` überschritten.
128    RejectedByInstancesLimit,
129    /// Reader-Resource-Limit `max_samples` überschritten.
130    RejectedBySamplesLimit,
131    /// Reader-Resource-Limit `max_samples_per_instance` überschritten.
132    RejectedBySamplesPerInstanceLimit,
133}
134
135/// `SAMPLE_REJECTED_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
136///
137/// Wird ausgelöst, wenn der Reader ein Sample wegen einer
138/// RESOURCE_LIMITS-Verletzung verworfen hat.
139#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
140pub struct SampleRejectedStatus {
141    /// Total cumulative count of rejected samples.
142    pub total_count: i32,
143    /// Increment since last read.
144    pub total_count_change: i32,
145    /// Reason for the most recent rejection.
146    pub last_reason: SampleRejectedStatusKind,
147    /// Handle of the instance that was the target of the most recent
148    /// rejection.
149    pub last_instance_handle: InstanceHandle,
150}
151
152/// `LIVELINESS_CHANGED_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
153///
154/// Reader-Seite: "Reports the status of the liveliness of one or more
155/// `DataWriter` objects that are matched with the `DataReader`."
156///
157/// Anders als die Plain-Counter-Statuses dürfen `*_count_change` hier
158/// **negativ** werden, wenn z.B. ein als "alive" deklarierter Writer
159/// jetzt als "not_alive" zählt (übersprungen von `alive_count` zu
160/// `not_alive_count`).
161#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
162pub struct LivelinessChangedStatus {
163    /// Number of currently-alive matched writers.
164    pub alive_count: i32,
165    /// Number of currently-not-alive matched writers.
166    pub not_alive_count: i32,
167    /// Change in `alive_count` since the last read.
168    pub alive_count_change: i32,
169    /// Change in `not_alive_count` since the last read.
170    pub not_alive_count_change: i32,
171    /// Handle of the last writer that triggered the change.
172    pub last_publication_handle: InstanceHandle,
173}
174
175/// `PUBLICATION_MATCHED_STATUS` — Spec §2.2.4.1 + §2.2.2.4.2.
176///
177/// Writer-Seite: "Reports the discovery of a new compatible
178/// DataReader / the loss of one."
179#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
180pub struct PublicationMatchedStatus {
181    /// Total cumulative count of compatible DataReaders that have been
182    /// discovered so far (monoton steigend).
183    pub total_count: i32,
184    /// Change in `total_count` since the last read.
185    pub total_count_change: i32,
186    /// Currently matched DataReaders (kann fallen, wenn Reader weggeht).
187    pub current_count: i32,
188    /// Change in `current_count` since the last read (darf negativ
189    /// werden).
190    pub current_count_change: i32,
191    /// Handle of the last DataReader that matched the writer.
192    pub last_subscription_handle: InstanceHandle,
193}
194
195/// `SUBSCRIPTION_MATCHED_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
196///
197/// Reader-Seite: spiegelt PublicationMatched. Felder gleichlaufend.
198#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
199pub struct SubscriptionMatchedStatus {
200    /// Total cumulative count of compatible DataWriters discovered.
201    pub total_count: i32,
202    /// Change in `total_count` since last read.
203    pub total_count_change: i32,
204    /// Currently matched DataWriters.
205    pub current_count: i32,
206    /// Change in `current_count` since last read.
207    pub current_count_change: i32,
208    /// Handle of the last DataWriter that matched.
209    pub last_publication_handle: InstanceHandle,
210}
211
212/// `QosPolicyCount` — Sub-Element von `*IncompatibleQosStatus`.
213///
214/// Pro QoS-Policy-Id ein Counter, wie oft genau diese Policy zur
215/// Inkompatibilität geführt hat. Spec §2.2.4.1 Tab. 2.10.
216#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
217pub struct QosPolicyCount {
218    /// Policy-Id (siehe [`crate::psm_constants::qos_policy_id`]).
219    pub policy_id: u32,
220    /// Wie oft diese Policy inkompatibel war.
221    pub count: i32,
222}
223
224impl QosPolicyCount {
225    /// Konstruktor.
226    #[must_use]
227    pub const fn new(policy_id: u32, count: i32) -> Self {
228        Self { policy_id, count }
229    }
230}
231
232/// `OFFERED_INCOMPATIBLE_QOS_STATUS` — Spec §2.2.4.1 + §2.2.2.4.2.
233///
234/// Writer-Seite: ein Reader wurde gefunden, dessen `requested QoS`
235/// nicht zum `offered QoS` des Writers passt.
236#[derive(Debug, Default, Clone, PartialEq, Eq)]
237pub struct OfferedIncompatibleQosStatus {
238    /// Total cumulative count of incompatible-QoS detections.
239    pub total_count: i32,
240    /// Change in `total_count` since last read.
241    pub total_count_change: i32,
242    /// Policy-Id of the *most recent* policy that caused the
243    /// incompatibility.
244    pub last_policy_id: u32,
245    /// Per-policy counters.
246    pub policies: Vec<QosPolicyCount>,
247}
248
249/// `REQUESTED_INCOMPATIBLE_QOS_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
250///
251/// Reader-Seite: ein Writer wurde gefunden, dessen `offered QoS` nicht
252/// zum `requested QoS` des Readers passt.
253#[derive(Debug, Default, Clone, PartialEq, Eq)]
254pub struct RequestedIncompatibleQosStatus {
255    /// Total cumulative count of incompatible-QoS detections.
256    pub total_count: i32,
257    /// Change in `total_count` since last read.
258    pub total_count_change: i32,
259    /// Policy-Id of the *most recent* policy that caused the
260    /// incompatibility.
261    pub last_policy_id: u32,
262    /// Per-policy counters.
263    pub policies: Vec<QosPolicyCount>,
264}
265
266// ============================================================================
267// Signal-Statuses (Marker-Structs)
268// ============================================================================
269
270/// `DATA_AVAILABLE_STATUS` — Spec §2.2.4.1.
271///
272/// Reines Signal: "new data has arrived in the DataReader". Es gibt
273/// keine Spec-Datenstruktur dafür — wir spiegeln den Status als
274/// leeres Marker-Struct, damit ein einheitlicher
275/// `get_data_available_status()`-Pfad existiert.
276#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
277pub struct DataAvailableStatus;
278
279/// `DATA_ON_READERS_STATUS` — Spec §2.2.4.1.
280///
281/// Reines Signal: "new data has arrived in **any** DataReader of the
282/// Subscriber". Wie [`DataAvailableStatus`] ohne Datenstruktur.
283#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
284pub struct DataOnReadersStatus;
285
286// ============================================================================
287// Helper-Funktionen
288// ============================================================================
289
290/// Hängt einen `policy_id`-Counter in einen `policies`-Vec ein. Wenn
291/// der Eintrag existiert, wird `count` inkrementiert; sonst neu
292/// angefügt. Genutzt vom Runtime-Layer beim Verteilen eines
293/// IncompatibleQos-Events.
294pub fn bump_policy_count(policies: &mut Vec<QosPolicyCount>, policy_id: u32) {
295    if let Some(slot) = policies.iter_mut().find(|p| p.policy_id == policy_id) {
296        slot.count = slot.count.saturating_add(1);
297        return;
298    }
299    policies.push(QosPolicyCount::new(policy_id, 1));
300}
301
302#[cfg(test)]
303#[allow(clippy::expect_used, clippy::unwrap_used)]
304mod tests {
305    use super::*;
306    use crate::psm_constants::qos_policy_id;
307
308    #[test]
309    fn inconsistent_topic_default_is_zero() {
310        let s = InconsistentTopicStatus::default();
311        assert_eq!(s.total_count, 0);
312        assert_eq!(s.total_count_change, 0);
313    }
314
315    #[test]
316    fn sample_lost_clone_roundtrip() {
317        let s = SampleLostStatus {
318            total_count: 5,
319            total_count_change: 2,
320        };
321        let c = s;
322        assert_eq!(s, c);
323    }
324
325    #[test]
326    fn sample_rejected_status_default_kind_is_not_rejected() {
327        let s = SampleRejectedStatus::default();
328        assert_eq!(s.last_reason, SampleRejectedStatusKind::NotRejected);
329        assert!(s.last_instance_handle.is_nil());
330    }
331
332    #[test]
333    fn sample_rejected_kind_variants_are_distinct() {
334        // Vollständigkeit der Spec-Enum-Variants.
335        let kinds = [
336            SampleRejectedStatusKind::NotRejected,
337            SampleRejectedStatusKind::RejectedByInstancesLimit,
338            SampleRejectedStatusKind::RejectedBySamplesLimit,
339            SampleRejectedStatusKind::RejectedBySamplesPerInstanceLimit,
340        ];
341        for (i, a) in kinds.iter().enumerate() {
342            for b in &kinds[i + 1..] {
343                assert_ne!(a, b);
344            }
345        }
346    }
347
348    #[test]
349    fn liveliness_lost_default() {
350        let s = LivelinessLostStatus::default();
351        assert_eq!(s.total_count, 0);
352    }
353
354    #[test]
355    fn liveliness_changed_negative_count_change_allowed() {
356        // Spec §2.2.4.1: alive_count_change kann negativ sein, wenn
357        // ein Writer von "alive" zu "not_alive" wechselt.
358        let s = LivelinessChangedStatus {
359            alive_count: 0,
360            not_alive_count: 1,
361            alive_count_change: -1,
362            not_alive_count_change: 1,
363            last_publication_handle: InstanceHandle::from_raw(42),
364        };
365        assert_eq!(s.alive_count_change, -1);
366        assert_eq!(s.not_alive_count_change, 1);
367        assert_eq!(s.last_publication_handle.as_raw(), 42);
368    }
369
370    #[test]
371    fn publication_matched_with_handle_roundtrip() {
372        let h = InstanceHandle::from_raw(99);
373        let s = PublicationMatchedStatus {
374            total_count: 3,
375            total_count_change: 1,
376            current_count: 2,
377            current_count_change: 1,
378            last_subscription_handle: h,
379        };
380        let c = s;
381        assert_eq!(c.last_subscription_handle, h);
382        assert_eq!(c.current_count, 2);
383    }
384
385    #[test]
386    fn subscription_matched_with_handle_roundtrip() {
387        let h = InstanceHandle::from_raw(7);
388        let s = SubscriptionMatchedStatus {
389            total_count: 5,
390            total_count_change: 0,
391            current_count: 5,
392            current_count_change: 0,
393            last_publication_handle: h,
394        };
395        assert_eq!(s.last_publication_handle, h);
396    }
397
398    #[test]
399    fn offered_deadline_missed_default_handle_is_nil() {
400        let s = OfferedDeadlineMissedStatus::default();
401        assert!(s.last_instance_handle.is_nil());
402    }
403
404    #[test]
405    fn requested_deadline_missed_default_handle_is_nil() {
406        let s = RequestedDeadlineMissedStatus::default();
407        assert!(s.last_instance_handle.is_nil());
408    }
409
410    #[test]
411    fn offered_incompatible_qos_clone_roundtrip() {
412        let s = OfferedIncompatibleQosStatus {
413            total_count: 2,
414            total_count_change: 1,
415            last_policy_id: qos_policy_id::DURABILITY,
416            policies: alloc::vec![QosPolicyCount::new(qos_policy_id::DURABILITY, 2)],
417        };
418        let c = s.clone();
419        assert_eq!(c, s);
420        assert_eq!(c.policies.len(), 1);
421        assert_eq!(c.policies[0].policy_id, qos_policy_id::DURABILITY);
422    }
423
424    #[test]
425    fn requested_incompatible_qos_clone_roundtrip() {
426        let s = RequestedIncompatibleQosStatus {
427            total_count: 1,
428            total_count_change: 1,
429            last_policy_id: qos_policy_id::RELIABILITY,
430            policies: alloc::vec![QosPolicyCount::new(qos_policy_id::RELIABILITY, 1)],
431        };
432        let c = s.clone();
433        assert_eq!(c, s);
434    }
435
436    #[test]
437    fn bump_policy_count_inserts_then_increments() {
438        let mut v = alloc::vec::Vec::<QosPolicyCount>::new();
439        bump_policy_count(&mut v, qos_policy_id::DURABILITY);
440        assert_eq!(v.len(), 1);
441        assert_eq!(v[0].count, 1);
442        bump_policy_count(&mut v, qos_policy_id::DURABILITY);
443        assert_eq!(v.len(), 1);
444        assert_eq!(v[0].count, 2);
445        bump_policy_count(&mut v, qos_policy_id::RELIABILITY);
446        assert_eq!(v.len(), 2);
447    }
448
449    #[test]
450    fn data_available_and_data_on_readers_are_zero_sized_markers() {
451        // Spec §2.2.4.1: pure Signals — keine Felder.
452        // Wir checken die Marker-Semantik via Default+Eq.
453        let a1 = DataAvailableStatus;
454        let a2 = DataAvailableStatus;
455        assert_eq!(a1, a2);
456        let r1 = DataOnReadersStatus;
457        let r2 = DataOnReadersStatus;
458        assert_eq!(r1, r2);
459        // Marker-Structs haben Größe 0 (kompakt).
460        assert_eq!(core::mem::size_of::<DataAvailableStatus>(), 0);
461        assert_eq!(core::mem::size_of::<DataOnReadersStatus>(), 0);
462    }
463}