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}