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}