Skip to main content

zerodds_dcps/
sample_info.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `SampleInfo` — Metadaten pro Sample, die `DataReader::read`/`take`
4//! mit jedem Sample mitliefern.
5//!
6//! Spec-Referenz: OMG DDS-DCPS 1.4 §2.2.2.5.1 `SampleInfo`. Die Spec
7//! definiert 11 Felder, die zusammen das **Statechart** eines Samples
8//! beschreiben:
9//!
10//! 1. `sample_state`: ob der Reader das Sample schon einmal gelesen hat
11//!    (`READ`) oder nicht (`NOT_READ`).
12//! 2. `view_state`: ob die Reader-Instanz neu ist (`NEW`) oder schon
13//!    bekannt (`NOT_NEW`).
14//! 3. `instance_state`: Lifecycle-Zustand der Instanz (`ALIVE`,
15//!    `NOT_ALIVE_DISPOSED`, `NOT_ALIVE_NO_WRITERS`).
16//! 4. `disposed_generation_count`: Anzahl der Uebergaenge
17//!    `NOT_ALIVE_DISPOSED → ALIVE` seit dem ersten Sample dieser Instanz.
18//! 5. `no_writers_generation_count`: Anzahl der Uebergaenge
19//!    `NOT_ALIVE_NO_WRITERS → ALIVE` seit dem ersten Sample dieser Instanz.
20//! 6. `sample_rank`: Anzahl Samples in derselben Instanz, die nach
21//!    diesem im Cache stehen (Spec §2.2.2.5.1.5).
22//! 7. `generation_rank`: Differenz der Generation-Counts zwischen diesem
23//!    und dem letzten Sample der Instanz im selben Read-Set.
24//! 8. `absolute_generation_rank`: wie `generation_rank`, aber relativ
25//!    zum **aktuellen** Generation-Count.
26//! 9. `source_timestamp`: Wall-Clock-Zeitpunkt der Schreiboperation.
27//! 10. `instance_handle`: lokaler Handle der Instanz (Key-basiert).
28//! 11. `publication_handle`: lokaler Handle des sendenden DataWriters.
29//! 12. `valid_data`: `false` fuer Dispose-/Unregister-Markers ohne
30//!     Nutzdaten (Spec §2.2.2.5.1.13).
31//!
32//! Hinweis: die Spec listet `valid_data` als 12. Feld; einige Texte
33//! zaehlen es nicht mit, daher findet man sowohl "11" als auch "12"
34//! Felder in der Doku. Wir tragen alle.
35
36extern crate alloc;
37
38use crate::instance_handle::{HANDLE_NIL, InstanceHandle};
39use crate::time::Time;
40
41/// `SampleStateKind` (DDS 1.4 §2.2.2.5.1.1) — pro Reader gepflegt.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub enum SampleStateKind {
44    /// Sample wurde noch nie via `read`/`take` an die Application
45    /// uebergeben.
46    NotRead,
47    /// Sample wurde schon mindestens einmal via `read` ausgeliefert
48    /// (nach `take` ist es weg, daher relevant nur fuer `read`).
49    Read,
50}
51
52impl SampleStateKind {
53    /// `true` wenn dies ein noch ungelesenes Sample ist.
54    #[must_use]
55    pub const fn is_not_read(&self) -> bool {
56        matches!(self, Self::NotRead)
57    }
58}
59
60impl Default for SampleStateKind {
61    fn default() -> Self {
62        Self::NotRead
63    }
64}
65
66/// `ViewStateKind` (DDS 1.4 §2.2.2.5.1.2) — pro Instanz gepflegt.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub enum ViewStateKind {
69    /// Erstes Sample dieser Instanz (oder erstes nach
70    /// `NOT_ALIVE_NO_WRITERS → ALIVE`).
71    New,
72    /// Reader hat fuer diese Instanz schon Samples ausgeliefert.
73    NotNew,
74}
75
76impl Default for ViewStateKind {
77    fn default() -> Self {
78        Self::New
79    }
80}
81
82/// `InstanceStateKind` (DDS 1.4 §2.2.2.5.1.3) — pro Instanz gepflegt.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
84pub enum InstanceStateKind {
85    /// Mindestens ein DataWriter hat die Instanz registriert und sie
86    /// wurde nicht disposed.
87    Alive,
88    /// Mindestens ein DataWriter hat die Instanz disposed.
89    NotAliveDisposed,
90    /// Alle DataWriter, die die Instanz registriert hatten, haben
91    /// `unregister_instance` aufgerufen oder sind weg.
92    NotAliveNoWriters,
93}
94
95impl InstanceStateKind {
96    /// `true` wenn die Instanz noch lebt.
97    #[must_use]
98    pub const fn is_alive(&self) -> bool {
99        matches!(self, Self::Alive)
100    }
101    /// `true` wenn die Instanz entweder disposed oder no-writers ist.
102    #[must_use]
103    pub const fn is_not_alive(&self) -> bool {
104        !self.is_alive()
105    }
106}
107
108impl Default for InstanceStateKind {
109    fn default() -> Self {
110        Self::Alive
111    }
112}
113
114/// Bitmask-Maske fuer Sample-State-Filter in `read`/`take`-Calls
115/// (DDS 1.4 §2.2.2.5.1.4 `SampleStateMask`).
116pub mod sample_state_mask {
117    /// `NOT_READ` only.
118    pub const NOT_READ: u32 = 1 << 0;
119    /// `READ` only.
120    pub const READ: u32 = 1 << 1;
121    /// `READ | NOT_READ` — beide.
122    pub const ANY: u32 = NOT_READ | READ;
123}
124
125/// Bitmask-Maske fuer View-State-Filter (§2.2.2.5.1.4 `ViewStateMask`).
126pub mod view_state_mask {
127    /// `NEW`.
128    pub const NEW: u32 = 1 << 0;
129    /// `NOT_NEW`.
130    pub const NOT_NEW: u32 = 1 << 1;
131    /// Beide.
132    pub const ANY: u32 = NEW | NOT_NEW;
133}
134
135/// Bitmask-Maske fuer Instance-State-Filter (§2.2.2.5.1.4
136/// `InstanceStateMask`).
137pub mod instance_state_mask {
138    /// `ALIVE`.
139    pub const ALIVE: u32 = 1 << 0;
140    /// `NOT_ALIVE_DISPOSED`.
141    pub const NOT_ALIVE_DISPOSED: u32 = 1 << 1;
142    /// `NOT_ALIVE_NO_WRITERS`.
143    pub const NOT_ALIVE_NO_WRITERS: u32 = 1 << 2;
144    /// `NOT_ALIVE_DISPOSED | NOT_ALIVE_NO_WRITERS`.
145    pub const NOT_ALIVE: u32 = NOT_ALIVE_DISPOSED | NOT_ALIVE_NO_WRITERS;
146    /// Alle drei.
147    pub const ANY: u32 = ALIVE | NOT_ALIVE;
148}
149
150/// `SampleInfo` (DDS 1.4 §2.2.2.5.1) — Metadaten pro Sample.
151///
152/// Wird vom DataReader im `take`/`read`-Pfad gefuellt. Application-
153/// Code soll sich auf diese Felder verlassen, um:
154/// * neue Samples vs. wiederholt gelesene zu unterscheiden
155///   (`sample_state`),
156/// * neue Instanzen zu erkennen (`view_state == NEW`),
157/// * Lifecycle-Events zu sehen (`instance_state`, `valid_data == false`),
158/// * Reordering / Generations-Wechsel zu folgen (`*_generation_count`,
159///   `*_rank`).
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub struct SampleInfo {
162    /// Sample-State (`READ` / `NOT_READ`).
163    pub sample_state: SampleStateKind,
164    /// View-State (`NEW` / `NOT_NEW`).
165    pub view_state: ViewStateKind,
166    /// Instance-State (`ALIVE` / `NOT_ALIVE_*`).
167    pub instance_state: InstanceStateKind,
168    /// Wieviele Mal die Instanz `NOT_ALIVE_DISPOSED → ALIVE` durchlaufen
169    /// hat seit ihrem ersten Sample.
170    pub disposed_generation_count: i32,
171    /// Wieviele Mal die Instanz `NOT_ALIVE_NO_WRITERS → ALIVE`
172    /// durchlaufen hat seit ihrem ersten Sample.
173    pub no_writers_generation_count: i32,
174    /// Anzahl Samples in derselben Instanz, die nach diesem im Cache
175    /// stehen (§2.2.2.5.1.5).
176    pub sample_rank: i32,
177    /// Differenz `generation_count` zwischen diesem und dem letzten
178    /// Sample der Instanz im selben Read-Set.
179    pub generation_rank: i32,
180    /// Differenz `generation_count` zwischen diesem und dem **aktuellen**
181    /// Generation-Count.
182    pub absolute_generation_rank: i32,
183    /// Wall-Clock-Zeitpunkt des `write`/`dispose`/`unregister`.
184    pub source_timestamp: Time,
185    /// Lokaler Handle der Instanz.
186    pub instance_handle: InstanceHandle,
187    /// Lokaler Handle des sendenden DataWriters (im offline / unbekannten
188    /// Fall `HANDLE_NIL`).
189    pub publication_handle: InstanceHandle,
190    /// `true` wenn das Sample Nutzdaten enthaelt; `false` bei
191    /// reinem Dispose-/Unregister-Marker (§2.2.2.5.1.13).
192    pub valid_data: bool,
193}
194
195impl Default for SampleInfo {
196    fn default() -> Self {
197        Self {
198            sample_state: SampleStateKind::NotRead,
199            view_state: ViewStateKind::New,
200            instance_state: InstanceStateKind::Alive,
201            disposed_generation_count: 0,
202            no_writers_generation_count: 0,
203            sample_rank: 0,
204            generation_rank: 0,
205            absolute_generation_rank: 0,
206            source_timestamp: Time::default(),
207            instance_handle: HANDLE_NIL,
208            publication_handle: HANDLE_NIL,
209            valid_data: true,
210        }
211    }
212}
213
214impl SampleInfo {
215    /// Konstruiert einen Default-Info-Block fuer ein frisches Sample
216    /// einer **neuen** Instanz mit `valid_data = true`.
217    #[must_use]
218    pub fn new_alive(
219        instance: InstanceHandle,
220        publication: InstanceHandle,
221        timestamp: Time,
222    ) -> Self {
223        Self {
224            sample_state: SampleStateKind::NotRead,
225            view_state: ViewStateKind::New,
226            instance_state: InstanceStateKind::Alive,
227            instance_handle: instance,
228            publication_handle: publication,
229            source_timestamp: timestamp,
230            valid_data: true,
231            ..Self::default()
232        }
233    }
234
235    /// Prueft, ob dieses `SampleInfo` durch die uebergebenen
236    /// State-Masks akzeptiert wird (Spec §2.2.2.5.3.1, `read_w_condition`).
237    #[must_use]
238    pub fn matches_states(&self, sample_mask: u32, view_mask: u32, instance_mask: u32) -> bool {
239        let sample_bit = match self.sample_state {
240            SampleStateKind::NotRead => sample_state_mask::NOT_READ,
241            SampleStateKind::Read => sample_state_mask::READ,
242        };
243        let view_bit = match self.view_state {
244            ViewStateKind::New => view_state_mask::NEW,
245            ViewStateKind::NotNew => view_state_mask::NOT_NEW,
246        };
247        let inst_bit = match self.instance_state {
248            InstanceStateKind::Alive => instance_state_mask::ALIVE,
249            InstanceStateKind::NotAliveDisposed => instance_state_mask::NOT_ALIVE_DISPOSED,
250            InstanceStateKind::NotAliveNoWriters => instance_state_mask::NOT_ALIVE_NO_WRITERS,
251        };
252        (sample_mask & sample_bit) != 0
253            && (view_mask & view_bit) != 0
254            && (instance_mask & inst_bit) != 0
255    }
256}
257
258#[cfg(test)]
259#[allow(clippy::expect_used, clippy::unwrap_used)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn defaults_are_alive_new_not_read() {
265        let info = SampleInfo::default();
266        assert_eq!(info.sample_state, SampleStateKind::NotRead);
267        assert_eq!(info.view_state, ViewStateKind::New);
268        assert_eq!(info.instance_state, InstanceStateKind::Alive);
269        assert!(info.valid_data);
270        assert_eq!(info.disposed_generation_count, 0);
271        assert_eq!(info.no_writers_generation_count, 0);
272        assert_eq!(info.instance_handle, HANDLE_NIL);
273        assert_eq!(info.publication_handle, HANDLE_NIL);
274    }
275
276    #[test]
277    fn instance_state_predicates() {
278        assert!(InstanceStateKind::Alive.is_alive());
279        assert!(!InstanceStateKind::Alive.is_not_alive());
280        assert!(InstanceStateKind::NotAliveDisposed.is_not_alive());
281        assert!(InstanceStateKind::NotAliveNoWriters.is_not_alive());
282    }
283
284    #[test]
285    fn sample_state_predicate() {
286        assert!(SampleStateKind::NotRead.is_not_read());
287        assert!(!SampleStateKind::Read.is_not_read());
288    }
289
290    #[test]
291    fn matches_states_filter() {
292        let info = SampleInfo::default();
293        assert!(info.matches_states(
294            sample_state_mask::ANY,
295            view_state_mask::ANY,
296            instance_state_mask::ANY,
297        ));
298        assert!(info.matches_states(
299            sample_state_mask::NOT_READ,
300            view_state_mask::NEW,
301            instance_state_mask::ALIVE,
302        ));
303        assert!(!info.matches_states(
304            sample_state_mask::READ,
305            view_state_mask::ANY,
306            instance_state_mask::ANY,
307        ));
308        assert!(!info.matches_states(
309            sample_state_mask::ANY,
310            view_state_mask::NOT_NEW,
311            instance_state_mask::ANY,
312        ));
313        assert!(!info.matches_states(
314            sample_state_mask::ANY,
315            view_state_mask::ANY,
316            instance_state_mask::NOT_ALIVE,
317        ));
318    }
319
320    #[test]
321    fn new_alive_constructor_sets_handles_and_timestamp() {
322        let h = InstanceHandle::from_raw(7);
323        let pub_h = InstanceHandle::from_raw(42);
324        let ts = Time::new(1, 2);
325        let info = SampleInfo::new_alive(h, pub_h, ts);
326        assert_eq!(info.instance_handle, h);
327        assert_eq!(info.publication_handle, pub_h);
328        assert_eq!(info.source_timestamp, ts);
329        assert!(info.valid_data);
330        assert_eq!(info.instance_state, InstanceStateKind::Alive);
331    }
332
333    // ---- §2.2.2.5.5 alle 12 SampleInfo-Felder verfuegbar ----
334
335    #[test]
336    fn sample_info_all_spec_fields_accessible() {
337        // Spec §2.2.2.5.5 listet 12 Felder; alle muessen lesbar +
338        // setzbar sein. Test ueberprueft die Feld-Identitaet via
339        // konkrete Werte.
340        let info = SampleInfo {
341            sample_state: SampleStateKind::Read,
342            view_state: ViewStateKind::NotNew,
343            instance_state: InstanceStateKind::NotAliveDisposed,
344            disposed_generation_count: 3,
345            no_writers_generation_count: 5,
346            sample_rank: 7,
347            generation_rank: 9,
348            absolute_generation_rank: 11,
349            source_timestamp: Time::new(1, 2),
350            instance_handle: InstanceHandle::from_raw(0xCAFE),
351            publication_handle: InstanceHandle::from_raw(0xBEEF),
352            valid_data: false,
353        };
354
355        assert_eq!(info.sample_state, SampleStateKind::Read);
356        assert_eq!(info.view_state, ViewStateKind::NotNew);
357        assert_eq!(info.instance_state, InstanceStateKind::NotAliveDisposed);
358        assert_eq!(info.disposed_generation_count, 3);
359        assert_eq!(info.no_writers_generation_count, 5);
360        assert_eq!(info.sample_rank, 7);
361        assert_eq!(info.generation_rank, 9);
362        assert_eq!(info.absolute_generation_rank, 11);
363        assert_eq!(info.source_timestamp, Time::new(1, 2));
364        assert_eq!(info.instance_handle, InstanceHandle::from_raw(0xCAFE));
365        assert_eq!(info.publication_handle, InstanceHandle::from_raw(0xBEEF));
366        assert!(!info.valid_data);
367    }
368
369    #[test]
370    fn sample_info_dispose_marker_has_invalid_data() {
371        // §2.2.2.5.1.13 — Dispose-/Unregister-Marker hat valid_data=false.
372        // Negativ: ein Sample mit valid_data=false hat keine Nutzdaten,
373        // der Caller MUSS sample.data nicht auswerten.
374        let info = SampleInfo {
375            valid_data: false,
376            instance_state: InstanceStateKind::NotAliveDisposed,
377            ..SampleInfo::default()
378        };
379        assert!(!info.valid_data);
380        assert!(info.instance_state.is_not_alive());
381    }
382
383    #[test]
384    fn sample_info_three_state_dimensions_independent() {
385        // Spec §2.2.2.5.1 — drei orthogonale State-Dimensionen.
386        // matches_states muss auf jede einzeln filtern.
387        let info = SampleInfo {
388            sample_state: SampleStateKind::Read,
389            view_state: ViewStateKind::NotNew,
390            instance_state: InstanceStateKind::Alive,
391            ..SampleInfo::default()
392        };
393        // Read+NotNew+Alive matched.
394        assert!(info.matches_states(
395            sample_state_mask::READ,
396            view_state_mask::NOT_NEW,
397            instance_state_mask::ALIVE,
398        ));
399        // sample_state mismatch → reject.
400        assert!(!info.matches_states(
401            sample_state_mask::NOT_READ,
402            view_state_mask::ANY,
403            instance_state_mask::ANY,
404        ));
405    }
406
407    #[test]
408    fn sample_info_generation_rank_starts_zero() {
409        // Default-Sample: alle Generation-Counter 0.
410        let info = SampleInfo::default();
411        assert_eq!(info.disposed_generation_count, 0);
412        assert_eq!(info.no_writers_generation_count, 0);
413        assert_eq!(info.sample_rank, 0);
414        assert_eq!(info.generation_rank, 0);
415        assert_eq!(info.absolute_generation_rank, 0);
416    }
417
418    #[test]
419    fn enum_default_impls() {
420        assert_eq!(SampleStateKind::default(), SampleStateKind::NotRead);
421        assert_eq!(ViewStateKind::default(), ViewStateKind::New);
422        assert_eq!(InstanceStateKind::default(), InstanceStateKind::Alive);
423    }
424}