Skip to main content

zerodds_dcps/
status.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Communication status structures (DDS DCPS 1.4 §2.2.4.1, Table 2.10).
4//!
5//! The spec defines **13 standard communication statuses**, which are
6//! combined into a bitmask in `StatusMask`. Each status has an
7//! associated data structure that `get_<status>()` returns on the
8//! respective entity. The bitmask constants (linking status ↔ bit) live
9//! in [`crate::psm_constants::status`].
10//!
11//! ## Classification of the 13 statuses (spec §2.2.4.1):
12//!
13//! - **PLAIN** statuses (only `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 (with `last_*` + detail fields):
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 (pure bits, no data structure — we mirror them
29//!   as marker structs for completeness of the table and for a uniform
30//!   `get_*` API):
31//!   - `DATA_AVAILABLE`             (DataReader)
32//!   - `DATA_ON_READERS`            (Subscriber)
33//!
34//! `total_count_change` is typed as `i32`: the spec says "incremental
35//! count since the last time the listener was called or the status was
36//! read", which can go negative when the reader regains liveliness
37//! (LIVELINESS_CHANGED). We keep `i32` for all `*_count_change` fields to
38//! stay spec-compliant.
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." Maintained at the `Topic` level.
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 of how often the DataWriter has been declared "not alive"
77/// under the LIVELINESS QoS contract (writer side).
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 of how often the writer failed to honor its offered DEADLINE
90/// promise. Also maintains the `last_instance_handle` against which the
91/// violation was counted.
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 side. Counter of how often the reader did not receive a sample
105/// within the requested DEADLINE.
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 (with `last_*` + detail fields)
118// ============================================================================
119
120/// `SampleRejectedStatusKind` — reason why the last sample was rejected.
121/// Spec §2.2.4.1 Table 2.10 (kind enum under `SAMPLE_REJECTED`).
122#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum SampleRejectedStatusKind {
124    /// No sample was rejected (default).
125    #[default]
126    NotRejected,
127    /// Reader resource limit `max_instances` exceeded.
128    RejectedByInstancesLimit,
129    /// Reader resource limit `max_samples` exceeded.
130    RejectedBySamplesLimit,
131    /// Reader resource limit `max_samples_per_instance` exceeded.
132    RejectedBySamplesPerInstanceLimit,
133}
134
135/// `SAMPLE_REJECTED_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
136///
137/// Raised when the reader has dropped a sample due to a RESOURCE_LIMITS
138/// violation.
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/// Unlike the plain counter statuses, `*_count_change` here may go
158/// **negative**, for example when a writer previously declared "alive"
159/// now counts as "not_alive" (moved from `alive_count` to
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 (monotonically increasing).
183    pub total_count: i32,
184    /// Change in `total_count` since the last read.
185    pub total_count_change: i32,
186    /// Currently matched DataReaders (can drop when a reader leaves).
187    pub current_count: i32,
188    /// Change in `current_count` since the last read (may go negative).
189    pub current_count_change: i32,
190    /// Handle of the last DataReader that matched the writer.
191    pub last_subscription_handle: InstanceHandle,
192}
193
194/// `SUBSCRIPTION_MATCHED_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
195///
196/// Reader side: mirrors PublicationMatched. Fields run in parallel.
197#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
198pub struct SubscriptionMatchedStatus {
199    /// Total cumulative count of compatible DataWriters discovered.
200    pub total_count: i32,
201    /// Change in `total_count` since last read.
202    pub total_count_change: i32,
203    /// Currently matched DataWriters.
204    pub current_count: i32,
205    /// Change in `current_count` since last read.
206    pub current_count_change: i32,
207    /// Handle of the last DataWriter that matched.
208    pub last_publication_handle: InstanceHandle,
209}
210
211/// `QosPolicyCount` — sub-element of `*IncompatibleQosStatus`.
212///
213/// Per QoS policy id, a counter of how often exactly that policy caused
214/// an incompatibility. Spec §2.2.4.1 Table 2.10.
215#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
216pub struct QosPolicyCount {
217    /// Policy id (see [`crate::psm_constants::qos_policy_id`]).
218    pub policy_id: u32,
219    /// How often this policy was incompatible.
220    pub count: i32,
221}
222
223impl QosPolicyCount {
224    /// Constructor.
225    #[must_use]
226    pub const fn new(policy_id: u32, count: i32) -> Self {
227        Self { policy_id, count }
228    }
229}
230
231/// `OFFERED_INCOMPATIBLE_QOS_STATUS` — Spec §2.2.4.1 + §2.2.2.4.2.
232///
233/// Writer side: a reader was found whose `requested QoS` does not match
234/// the writer's `offered QoS`.
235#[derive(Debug, Default, Clone, PartialEq, Eq)]
236pub struct OfferedIncompatibleQosStatus {
237    /// Total cumulative count of incompatible-QoS detections.
238    pub total_count: i32,
239    /// Change in `total_count` since last read.
240    pub total_count_change: i32,
241    /// Policy-Id of the *most recent* policy that caused the
242    /// incompatibility.
243    pub last_policy_id: u32,
244    /// Per-policy counters.
245    pub policies: Vec<QosPolicyCount>,
246}
247
248/// `REQUESTED_INCOMPATIBLE_QOS_STATUS` — Spec §2.2.4.1 + §2.2.2.5.6.
249///
250/// Reader side: a writer was found whose `offered QoS` does not match
251/// the reader's `requested QoS`.
252#[derive(Debug, Default, Clone, PartialEq, Eq)]
253pub struct RequestedIncompatibleQosStatus {
254    /// Total cumulative count of incompatible-QoS detections.
255    pub total_count: i32,
256    /// Change in `total_count` since last read.
257    pub total_count_change: i32,
258    /// Policy-Id of the *most recent* policy that caused the
259    /// incompatibility.
260    pub last_policy_id: u32,
261    /// Per-policy counters.
262    pub policies: Vec<QosPolicyCount>,
263}
264
265// ============================================================================
266// Signal statuses (marker structs)
267// ============================================================================
268
269/// `DATA_AVAILABLE_STATUS` — Spec §2.2.4.1.
270///
271/// Pure signal: "new data has arrived in the DataReader". There is no
272/// spec data structure for it — we mirror the status as an empty marker
273/// struct so that a uniform `get_data_available_status()` path exists.
274#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
275pub struct DataAvailableStatus;
276
277/// `DATA_ON_READERS_STATUS` — Spec §2.2.4.1.
278///
279/// Pure signal: "new data has arrived in **any** DataReader of the
280/// Subscriber". Like [`DataAvailableStatus`], without a data structure.
281#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
282pub struct DataOnReadersStatus;
283
284// ============================================================================
285// Helper functions
286// ============================================================================
287
288/// Adds a `policy_id` counter into a `policies` vec. If the entry
289/// exists, `count` is incremented; otherwise a new one is appended. Used
290/// by the runtime layer when distributing an IncompatibleQos event.
291pub fn bump_policy_count(policies: &mut Vec<QosPolicyCount>, policy_id: u32) {
292    if let Some(slot) = policies.iter_mut().find(|p| p.policy_id == policy_id) {
293        slot.count = slot.count.saturating_add(1);
294        return;
295    }
296    policies.push(QosPolicyCount::new(policy_id, 1));
297}
298
299#[cfg(test)]
300#[allow(clippy::expect_used, clippy::unwrap_used)]
301mod tests {
302    use super::*;
303    use crate::psm_constants::qos_policy_id;
304
305    #[test]
306    fn inconsistent_topic_default_is_zero() {
307        let s = InconsistentTopicStatus::default();
308        assert_eq!(s.total_count, 0);
309        assert_eq!(s.total_count_change, 0);
310    }
311
312    #[test]
313    fn sample_lost_clone_roundtrip() {
314        let s = SampleLostStatus {
315            total_count: 5,
316            total_count_change: 2,
317        };
318        let c = s;
319        assert_eq!(s, c);
320    }
321
322    #[test]
323    fn sample_rejected_status_default_kind_is_not_rejected() {
324        let s = SampleRejectedStatus::default();
325        assert_eq!(s.last_reason, SampleRejectedStatusKind::NotRejected);
326        assert!(s.last_instance_handle.is_nil());
327    }
328
329    #[test]
330    fn sample_rejected_kind_variants_are_distinct() {
331        // Completeness of the spec enum variants.
332        let kinds = [
333            SampleRejectedStatusKind::NotRejected,
334            SampleRejectedStatusKind::RejectedByInstancesLimit,
335            SampleRejectedStatusKind::RejectedBySamplesLimit,
336            SampleRejectedStatusKind::RejectedBySamplesPerInstanceLimit,
337        ];
338        for (i, a) in kinds.iter().enumerate() {
339            for b in &kinds[i + 1..] {
340                assert_ne!(a, b);
341            }
342        }
343    }
344
345    #[test]
346    fn liveliness_lost_default() {
347        let s = LivelinessLostStatus::default();
348        assert_eq!(s.total_count, 0);
349    }
350
351    #[test]
352    fn liveliness_changed_negative_count_change_allowed() {
353        // Spec §2.2.4.1: alive_count_change can be negative when a writer
354        // transitions from "alive" to "not_alive".
355        let s = LivelinessChangedStatus {
356            alive_count: 0,
357            not_alive_count: 1,
358            alive_count_change: -1,
359            not_alive_count_change: 1,
360            last_publication_handle: InstanceHandle::from_raw(42),
361        };
362        assert_eq!(s.alive_count_change, -1);
363        assert_eq!(s.not_alive_count_change, 1);
364        assert_eq!(s.last_publication_handle.as_raw(), 42);
365    }
366
367    #[test]
368    fn publication_matched_with_handle_roundtrip() {
369        let h = InstanceHandle::from_raw(99);
370        let s = PublicationMatchedStatus {
371            total_count: 3,
372            total_count_change: 1,
373            current_count: 2,
374            current_count_change: 1,
375            last_subscription_handle: h,
376        };
377        let c = s;
378        assert_eq!(c.last_subscription_handle, h);
379        assert_eq!(c.current_count, 2);
380    }
381
382    #[test]
383    fn subscription_matched_with_handle_roundtrip() {
384        let h = InstanceHandle::from_raw(7);
385        let s = SubscriptionMatchedStatus {
386            total_count: 5,
387            total_count_change: 0,
388            current_count: 5,
389            current_count_change: 0,
390            last_publication_handle: h,
391        };
392        assert_eq!(s.last_publication_handle, h);
393    }
394
395    #[test]
396    fn offered_deadline_missed_default_handle_is_nil() {
397        let s = OfferedDeadlineMissedStatus::default();
398        assert!(s.last_instance_handle.is_nil());
399    }
400
401    #[test]
402    fn requested_deadline_missed_default_handle_is_nil() {
403        let s = RequestedDeadlineMissedStatus::default();
404        assert!(s.last_instance_handle.is_nil());
405    }
406
407    #[test]
408    fn offered_incompatible_qos_clone_roundtrip() {
409        let s = OfferedIncompatibleQosStatus {
410            total_count: 2,
411            total_count_change: 1,
412            last_policy_id: qos_policy_id::DURABILITY,
413            policies: alloc::vec![QosPolicyCount::new(qos_policy_id::DURABILITY, 2)],
414        };
415        let c = s.clone();
416        assert_eq!(c, s);
417        assert_eq!(c.policies.len(), 1);
418        assert_eq!(c.policies[0].policy_id, qos_policy_id::DURABILITY);
419    }
420
421    #[test]
422    fn requested_incompatible_qos_clone_roundtrip() {
423        let s = RequestedIncompatibleQosStatus {
424            total_count: 1,
425            total_count_change: 1,
426            last_policy_id: qos_policy_id::RELIABILITY,
427            policies: alloc::vec![QosPolicyCount::new(qos_policy_id::RELIABILITY, 1)],
428        };
429        let c = s.clone();
430        assert_eq!(c, s);
431    }
432
433    #[test]
434    fn bump_policy_count_inserts_then_increments() {
435        let mut v = alloc::vec::Vec::<QosPolicyCount>::new();
436        bump_policy_count(&mut v, qos_policy_id::DURABILITY);
437        assert_eq!(v.len(), 1);
438        assert_eq!(v[0].count, 1);
439        bump_policy_count(&mut v, qos_policy_id::DURABILITY);
440        assert_eq!(v.len(), 1);
441        assert_eq!(v[0].count, 2);
442        bump_policy_count(&mut v, qos_policy_id::RELIABILITY);
443        assert_eq!(v.len(), 2);
444    }
445
446    #[test]
447    fn data_available_and_data_on_readers_are_zero_sized_markers() {
448        // Spec §2.2.4.1: pure signals — no fields.
449        // We check the marker semantics via Default+Eq.
450        let a1 = DataAvailableStatus;
451        let a2 = DataAvailableStatus;
452        assert_eq!(a1, a2);
453        let r1 = DataOnReadersStatus;
454        let r2 = DataOnReadersStatus;
455        assert_eq!(r1, r2);
456        // Marker structs have size 0 (compact).
457        assert_eq!(core::mem::size_of::<DataAvailableStatus>(), 0);
458        assert_eq!(core::mem::size_of::<DataOnReadersStatus>(), 0);
459    }
460}