Skip to main content

open_dis_rust/common/
pdu_header.rs

1//     open-dis-rust - Rust implementation of the IEEE 1278.1-2012 Distributed Interactive
2//                     Simulation (DIS) application protocol
3//     Copyright (C) 2025 Cameron Howell
4//     Copyright (C) 2026 Jack Laverty
5//
6//     Licensed under the BSD 2-Clause License
7
8#![allow(clippy::must_use_candidate)]
9
10use bytes::{Buf, BufMut, BytesMut};
11use chrono::{Timelike, Utc};
12use modular_bitfield::prelude::*;
13
14use crate::{
15    common::{
16        GenericHeader, SerializedLength,
17        enums::{
18            ActiveInterrogationIndicator, CoupledExtensionIndicator, DetonationTypeIndicator,
19            FireTypeIndicator, IntercomAttachedIndicator, LVCIndicator, PduStatusIFFSimulationMode,
20            PduType, ProtocolFamily, ProtocolVersion, RadioAttachedIndicator,
21            TransferredEntityIndicator,
22        },
23    },
24    pdu_macro::{FieldDeserialize, FieldLen, FieldSerialize},
25};
26
27#[bitfield(bits = 8)]
28#[derive(Copy, Clone, Debug, PartialEq, Eq)]
29pub struct PduStatusRecord {
30    pub tei: TransferredEntityIndicator,
31    pub lvc: LVCIndicator,
32    pub cei: CoupledExtensionIndicator,
33    pub bit4_5: B2,
34    #[skip]
35    __reserved: B2,
36}
37
38impl PduStatusRecord {
39    #[must_use]
40    pub const fn new_zero() -> Self {
41        Self::new()
42    }
43
44    #[must_use]
45    pub fn get_dti(&self) -> DetonationTypeIndicator {
46        DetonationTypeIndicator::from(self.bit4_5())
47    }
48
49    pub fn set_dti(&mut self, dti: DetonationTypeIndicator) {
50        self.set_bit4_5(dti.into());
51    }
52
53    #[must_use]
54    pub fn get_rai(&self) -> RadioAttachedIndicator {
55        RadioAttachedIndicator::from(self.bit4_5())
56    }
57
58    pub fn set_rai(&mut self, rai: RadioAttachedIndicator) {
59        self.set_bit4_5(rai.into());
60    }
61
62    #[must_use]
63    pub fn get_iai(&self) -> IntercomAttachedIndicator {
64        IntercomAttachedIndicator::from(self.bit4_5())
65    }
66
67    pub fn set_iai(&mut self, iai: IntercomAttachedIndicator) {
68        self.set_bit4_5(iai.into());
69    }
70
71    #[must_use]
72    pub fn get_fti(&self) -> FireTypeIndicator {
73        let bit4 = self.bit4_5() & 0b01;
74        if bit4 == 0 {
75            FireTypeIndicator::Munition
76        } else {
77            FireTypeIndicator::Expendable
78        }
79    }
80
81    pub fn set_fti(&mut self, fti: FireTypeIndicator) {
82        let mut v = self.bit4_5();
83        v = (v & 0b10) | (fti as u8 & 0x01);
84        self.set_bit4_5(v);
85    }
86
87    #[must_use]
88    pub fn get_ism(&self) -> PduStatusIFFSimulationMode {
89        let bit4 = self.bit4_5() & 0b01;
90        if bit4 == 0 {
91            PduStatusIFFSimulationMode::Regeneration
92        } else {
93            PduStatusIFFSimulationMode::Interactive
94        }
95    }
96
97    pub fn set_ism(&mut self, ism: PduStatusIFFSimulationMode) {
98        let mut v = self.bit4_5();
99        v = (v & 0b10) | (ism as u8 & 0x01);
100        self.set_bit4_5(v);
101    }
102
103    #[must_use]
104    pub fn get_aii(&self) -> ActiveInterrogationIndicator {
105        let bit5 = (self.bit4_5() >> 1) & 0b01;
106        if bit5 == 0 {
107            ActiveInterrogationIndicator::NotActive
108        } else {
109            ActiveInterrogationIndicator::Active
110        }
111    }
112
113    pub fn set_aii(&mut self, aii: ActiveInterrogationIndicator) {
114        let mut v = self.bit4_5();
115        v = (v & 0b01) | ((aii as u8) << 1);
116        self.set_bit4_5(v);
117    }
118
119    #[must_use]
120    pub const fn to_u8(&self) -> u8 {
121        self.into_bytes()[0]
122    }
123
124    #[must_use]
125    pub const fn from_u8(b: u8) -> Self {
126        Self::from_bytes([b])
127    }
128}
129
130impl Default for PduStatusRecord {
131    fn default() -> Self {
132        Self::new_zero()
133    }
134}
135
136#[derive(Copy, Clone, Debug, PartialEq, Eq)]
137pub struct PduHeader {
138    /// The version of the protocol
139    pub protocol_version: ProtocolVersion,
140    /// Exercise ID
141    pub exercise_id: u8,
142    /// Type of PDU, unique for each PDU class
143    pub pdu_type: PduType,
144    /// Value that refers to the protocol family
145    pub protocol_family: ProtocolFamily,
146    /// Timestamp value
147    pub timestamp: u32,
148    /// Length, in bytes, of the PDU
149    pub length: u16,
150    /// PDU status record
151    pub status_record: PduStatusRecord,
152    /// Padding
153    padding: u8,
154}
155
156impl Default for PduHeader {
157    fn default() -> Self {
158        Self {
159            protocol_version: ProtocolVersion::IEEE1278_1_2012,
160            exercise_id: 1,
161            pdu_type: PduType::default(),
162            protocol_family: ProtocolFamily::default(),
163            timestamp: Self::calculate_dis_timestamp(),
164            length: 0,
165            status_record: PduStatusRecord::default(),
166            padding: 0,
167        }
168    }
169}
170
171impl GenericHeader for PduHeader {
172    fn pdu_type(&self) -> PduType {
173        self.pdu_type
174    }
175
176    fn set_pdu_type(&mut self, value: PduType) {
177        self.pdu_type = value;
178    }
179
180    fn protocol_family(&self) -> ProtocolFamily {
181        self.protocol_family
182    }
183
184    fn set_protocol_family(&mut self, value: ProtocolFamily) {
185        self.protocol_family = value;
186    }
187
188    fn length(&self) -> u16 {
189        self.length
190    }
191
192    fn set_length(&mut self, value: u16) {
193        self.length = value;
194    }
195
196    fn serialize(&self, buf: &mut BytesMut) {
197        buf.put_u8(self.protocol_version as u8);
198        buf.put_u8(self.exercise_id);
199        buf.put_u8(self.pdu_type as u8);
200        buf.put_u8(self.protocol_family as u8);
201        buf.put_u32(self.timestamp);
202        buf.put_u16(self.length);
203        buf.put_u8(self.status_record.to_u8());
204        buf.put_u8(self.padding);
205    }
206
207    fn deserialize<B: Buf>(buf: &mut B) -> Self {
208        Self {
209            protocol_version: ProtocolVersion::deserialize(buf),
210            exercise_id: buf.get_u8(),
211            pdu_type: PduType::deserialize(buf),
212            protocol_family: ProtocolFamily::deserialize(buf),
213            timestamp: buf.get_u32(),
214            length: buf.get_u16(),
215            status_record: PduStatusRecord::from_u8(buf.get_u8()),
216            padding: buf.get_u8(),
217        }
218    }
219}
220
221impl PduHeader {
222    #[must_use]
223    pub fn new(
224        pdu_type: PduType,
225        protocol_family: ProtocolFamily,
226        exercise_id: u8,
227        length: u16,
228    ) -> Self {
229        Self {
230            protocol_version: ProtocolVersion::IEEE1278_1_2012,
231            exercise_id,
232            pdu_type,
233            protocol_family,
234            timestamp: Self::calculate_dis_timestamp(),
235            length,
236            status_record: PduStatusRecord::default(),
237            padding: 0,
238        }
239    }
240
241    /// Gets the current time in terms of IEEE-1278.1 DIS time units
242    #[must_use]
243    #[allow(
244        clippy::cast_precision_loss,
245        clippy::cast_possible_truncation,
246        clippy::cast_sign_loss
247    )]
248    pub fn calculate_dis_timestamp() -> u32 {
249        let minute_curr = u64::from((Utc::now().minute() * 60) * 1_000_000);
250        let second_curr = u64::from(Utc::now().second() * 1_000_000);
251        let nanosecond_curr = u64::from(Utc::now().nanosecond() / 1000);
252        let dis_time = (second_curr + minute_curr + nanosecond_curr) as f32 / 1.68;
253        dis_time as u32
254    }
255
256    /// Looks at the PDU type from a raw byte buffer.
257    /// Only deserializes the 12-byte header.
258    #[must_use]
259    pub fn get_pdu_type(bytes: &[u8]) -> Option<PduType> {
260        if bytes.len() < Self::LENGTH {
261            return None;
262        }
263        let mut buf = &bytes[..Self::LENGTH];
264        Some(Self::deserialize(&mut buf).pdu_type)
265    }
266}
267
268impl FieldSerialize for PduHeader {
269    fn serialize_field(&self, buf: &mut BytesMut) {
270        self.serialize(buf);
271    }
272}
273
274impl FieldDeserialize for PduHeader {
275    fn deserialize_field<B: Buf>(buf: &mut B) -> Self {
276        Self::deserialize(buf)
277    }
278}
279
280impl FieldLen for PduHeader {
281    fn field_len(&self) -> usize {
282        Self::LENGTH
283    }
284}
285
286impl SerializedLength for PduHeader {
287    const LENGTH: usize = 12;
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn get_pdu_type_returns_correct_type() {
296        let mut buf = BytesMut::new();
297        let header = PduHeader::new(PduType::EntityState, ProtocolFamily::default(), 1, 12);
298        header.serialize(&mut buf);
299        assert_eq!(PduHeader::get_pdu_type(&buf), Some(PduType::EntityState));
300    }
301
302    #[test]
303    fn get_pdu_type_too_short_returns_none() {
304        assert_eq!(PduHeader::get_pdu_type(&[0u8; 11]), None);
305    }
306
307    #[test]
308    fn get_pdu_type_ignores_trailing_bytes() {
309        let mut buf = BytesMut::new();
310        let header = PduHeader::new(PduType::Detonation, ProtocolFamily::default(), 1, 12);
311        header.serialize(&mut buf);
312        buf.extend_from_slice(&[0xFF; 100]); // simulate a full PDU body
313        assert_eq!(PduHeader::get_pdu_type(&buf), Some(PduType::Detonation));
314    }
315
316    #[test]
317    fn get_pdu_type_all_zeros_returns_other() {
318        // a zeroed-out header will resolve to PduType::Other
319        assert_eq!(PduHeader::get_pdu_type(&[0u8; 12]), Some(PduType::Other));
320    }
321}