vita49 1.0.0

vita49 is a crate for parsing and creating packets compatible with the ANSI/VITA-49.2-2017 standard.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
// SPDX-FileCopyrightText: 2025 The vita49-rs Authors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
/*!
Data structures and methods related to the packet header format
(ANSI/VITA-49.2-2017 section 5.1.1).
*/

use deku::prelude::*;

use crate::VitaError;

/// Base packet header data structure.
#[derive(
    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DekuRead, DekuWrite,
)]
#[deku(endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PacketHeader {
    hword_1: u16,
    packet_size: u16,
}

/// The type of VRT packet being worked on.
///
/// Note: the packet type is used throughout this crate to determine
/// how to serialize and deserialize various fields, so it's important
/// that this field is correctly set.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DekuRead, DekuWrite)]
#[deku(id_type = "u8", endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PacketType {
    /// Signal data packet without a stream ID.
    #[deku(id = 0x0)]
    SignalDataWithoutStreamId,
    /// Signal data packet *with* a stream ID.
    #[deku(id = 0x1)]
    SignalData,
    /// Extension data packet without a stream ID.
    #[deku(id = 0x2)]
    ExtensionDataWithoutStreamId,
    /// Extension data packet *with* a stream ID.
    #[deku(id = 0x3)]
    ExtensionData,
    /// Context packet.
    #[deku(id = 0x4)]
    Context,
    /// Extension context packet.
    #[deku(id = 0x5)]
    ExtensionContext,
    /// Command packet.
    #[deku(id = 0x6)]
    Command,
    /// Extension command packet.
    #[deku(id = 0x7)]
    ExtensionCommand,
    // All other values are reserved
}

impl PacketType {
    /// Returns true if the packet type has a signal data-style payload.
    pub fn has_signal_data_payload(&self) -> bool {
        !matches!(
            &self,
            PacketType::SignalData
                | PacketType::ExtensionData
                | PacketType::SignalDataWithoutStreamId
                | PacketType::ExtensionDataWithoutStreamId
        )
    }
    /// Returns true if the packet type has a context-style payload.
    pub fn has_context_payload(&self) -> bool {
        !matches!(&self, PacketType::Context | PacketType::ExtensionContext)
    }
    /// Returns true if the packet type has a command-style payload.
    pub fn has_command_payload(&self) -> bool {
        !matches!(&self, PacketType::Command | PacketType::ExtensionCommand)
    }
}

impl TryFrom<u8> for PacketType {
    type Error = ();

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            x if x == PacketType::SignalDataWithoutStreamId as u8 => {
                Ok(PacketType::SignalDataWithoutStreamId)
            }
            x if x == PacketType::SignalData as u8 => Ok(PacketType::SignalData),
            x if x == PacketType::ExtensionDataWithoutStreamId as u8 => {
                Ok(PacketType::ExtensionDataWithoutStreamId)
            }
            x if x == PacketType::ExtensionData as u8 => Ok(PacketType::ExtensionData),
            x if x == PacketType::Context as u8 => Ok(PacketType::Context),
            x if x == PacketType::ExtensionContext as u8 => Ok(PacketType::ExtensionContext),
            x if x == PacketType::Command as u8 => Ok(PacketType::Command),
            x if x == PacketType::ExtensionCommand as u8 => Ok(PacketType::ExtensionCommand),
            _ => Err(()),
        }
    }
}

/// Indicator field enumeration. The three indicator bits
/// have different meaning depending on if the packet is a
/// signal data, context, or command packet.
///
/// See ANSI/VITA-49.2-2017 section 5.1.1.1 for more details.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DekuRead, DekuWrite)]
#[deku(
    endian = "endian",
    ctx = "endian: deku::ctx::Endian, packet_type: PacketType",
    id = "packet_type"
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Indicators {
    /// The bits represent signal data indicators.
    #[deku(id = "PacketType::SignalData")]
    SignalData(SignalDataIndicators),
    /// The bits represent context indicators.
    #[deku(id = "PacketType::Context")]
    Context(ContextIndicators),
    /// The bits represent command indicators.
    #[deku(id = "PacketType::Command")]
    Command(CommandIndicators),
}

/// Signal data indicator fields.
#[derive(
    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DekuRead, DekuWrite,
)]
#[deku(endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SignalDataIndicators {
    /// The packet includes a trailer.
    pub trailer_included: bool,
    /// The packet is not compliant with VITA 49.0. A VITA 49.0 parser may
    /// break if trying to parse this packet.
    pub not_a_vita490_packet: bool,
    /// The signal data represents spectral data.
    pub signal_spectral_data: bool,
}

/// Timestamp mode
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DekuRead, DekuWrite)]
#[deku(id_type = "u8", endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimestampMode {
    /// Used to convey the precise timing of events or Context changes.
    /// The resolution of this Timestamp Mode could be up to highest
    /// resolution supported by the TSF setting (either sample or picosecond
    /// resolution).
    #[deku(id = 0x0)]
    PreciseTiming,
    // Used to convey the general timing of events or Context changes.
    /// The resolution of this Timestamp Mode is the Data Sampling Interval
    /// of a Data packet.
    #[deku(id = 0x1)]
    GeneralTiming,
}

impl TryFrom<bool> for TimestampMode {
    type Error = ();

    fn try_from(value: bool) -> Result<Self, Self::Error> {
        if value {
            Ok(TimestampMode::GeneralTiming)
        } else {
            Ok(TimestampMode::PreciseTiming)
        }
    }
}

/// Context packet indicator fields.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContextIndicators {
    /// The packet is not compliant with VITA 49.0. A VITA 49.0 parser may
    /// break if trying to parse this packet.
    pub not_a_vita490_packet: bool,
    /// Context timestamp mode.
    pub timestamp_mode: TimestampMode,
}

/// Command packet indicators.
#[derive(
    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DekuRead, DekuWrite,
)]
#[deku(endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CommandIndicators {
    /// The command packet is an ACK packet.
    pub ack_packet: bool,
    /// The command packet is a cancellation packet.
    pub cancellation_packet: bool,
}

/// TimeStamp-Integer (TSI) field.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DekuRead, DekuWrite)]
#[deku(id_type = "u8", endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Tsi {
    /// Timestamp is not included.
    #[deku(id = 0x0)]
    Null,
    /// Timestamp represents UTC time.
    #[deku(id = 0x1)]
    Utc,
    /// Timestamp represents GPS time.
    #[deku(id = 0x2)]
    Gps,
    /// Timestamp represents some other time.
    #[deku(id = 0x3)]
    Other,
}

impl TryFrom<u8> for Tsi {
    type Error = ();

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            x if x == Tsi::Null as u8 => Ok(Tsi::Null),
            x if x == Tsi::Utc as u8 => Ok(Tsi::Utc),
            x if x == Tsi::Gps as u8 => Ok(Tsi::Gps),
            x if x == Tsi::Other as u8 => Ok(Tsi::Other),
            _ => Err(()),
        }
    }
}

/// TimeStamp-Fractional (TSF) field.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DekuRead, DekuWrite)]
#[deku(id_type = "u8", endian = "endian", ctx = "endian: deku::ctx::Endian")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Tsf {
    /// Timestamp is not included.
    #[deku(id = 0x0)]
    Null,
    /// Timestamp represents a sample counter.
    #[deku(id = 0x1)]
    SampleCount,
    /// Timestamp represents a real fractional time (in picoseconds).
    #[deku(id = 0x2)]
    RealTimePs,
    /// Timestamp represents a free-running count.
    #[deku(id = 0x3)]
    FreeRunningCount,
}

impl TryFrom<u8> for Tsf {
    type Error = ();

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            x if x == Tsf::Null as u8 => Ok(Tsf::Null),
            x if x == Tsf::SampleCount as u8 => Ok(Tsf::SampleCount),
            x if x == Tsf::RealTimePs as u8 => Ok(Tsf::RealTimePs),
            x if x == Tsf::FreeRunningCount as u8 => Ok(Tsf::FreeRunningCount),
            _ => Err(()),
        }
    }
}

impl PacketHeader {
    /// Gets the raw 32-bit value of the packet header.
    pub fn as_u32(&self) -> u32 {
        ((self.hword_1 as u32) << 16) | ((self.packet_size as u32) & 0xFFFF)
    }
    /// Gets the packet type.
    pub fn packet_type(&self) -> PacketType {
        (((self.hword_1 >> 12) & 0b1111) as u8).try_into().unwrap()
    }
    /// Sets the packet type.
    pub fn set_packet_type(&mut self, packet_type: PacketType) {
        self.hword_1 &= !(0b1111 << 12);
        self.hword_1 |= (packet_type as u16) << 12
    }
    /// Returns true if a class identifier is included in the packet.
    pub fn class_id_included(&self) -> bool {
        self.hword_1 & (1 << 11) > 0
    }

    /// Sets the class_id_included flag.
    pub(crate) fn set_class_id_included(&mut self, included: bool) {
        self.hword_1 = (self.hword_1 & !(1 << 11)) | ((included as u16) << 11);
    }

    /// Returns the packet indicators.
    /// Note: these indicators will be different depending on
    /// the type of packet you're working with, so you'll need
    /// to disambiguate.
    ///
    /// # Example
    /// ```
    /// # use vita49::prelude::*;
    /// use vita49::Indicators;
    /// # fn main() -> Result<(), VitaError> {
    /// # let mut packet = Vrt::new_signal_data_packet();
    /// match packet.header().indicators() {
    ///     Indicators::SignalData(i) => {
    ///         println!("Trailer included: {}", i.trailer_included);
    ///     },
    ///     _ => panic!("unexpected indicators")
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn indicators(&self) -> Indicators {
        let i1 = self.hword_1 & (1 << 10) > 1;
        let i2 = self.hword_1 & (1 << 9) > 1;
        let i3 = self.hword_1 & (1 << 8) > 1;
        match self.packet_type() {
            PacketType::SignalData
            | PacketType::SignalDataWithoutStreamId
            | PacketType::ExtensionData
            | PacketType::ExtensionDataWithoutStreamId => {
                Indicators::SignalData(SignalDataIndicators {
                    trailer_included: i1,
                    not_a_vita490_packet: i2,
                    signal_spectral_data: i3,
                })
            }
            PacketType::Context | PacketType::ExtensionContext => {
                Indicators::Context(ContextIndicators {
                    // i1 is reserved
                    not_a_vita490_packet: i2,
                    timestamp_mode: i3.try_into().unwrap(),
                })
            }
            PacketType::Command | PacketType::ExtensionCommand => {
                Indicators::Command(CommandIndicators {
                    ack_packet: i1,
                    // i2 is reserved
                    cancellation_packet: i3,
                })
            }
        }
    }
    /// Sets the header indicators.
    pub fn set_indicators(&mut self, indicators: Indicators) {
        match indicators {
            Indicators::SignalData(i) => {
                self.hword_1 |= (i.trailer_included as u16) << 10;
                self.hword_1 |= (i.not_a_vita490_packet as u16) << 9;
                self.hword_1 |= (i.signal_spectral_data as u16) << 8;
            }
            Indicators::Context(i) => {
                self.hword_1 |= (i.not_a_vita490_packet as u16) << 9;
                self.hword_1 |= (i.timestamp_mode as u16) << 8;
            }
            Indicators::Command(i) => {
                self.hword_1 |= (i.ack_packet as u16) << 10;
                self.hword_1 |= (i.cancellation_packet as u16) << 8;
            }
        }
    }
    /// Returns Ok(true) if the packet is an Ack packet, Ok(false) if
    /// it's some other kind of Command packet, and an error if it's
    /// some other type of packet entirely.
    pub fn is_ack_packet(&self) -> Result<bool, VitaError> {
        match self.indicators() {
            Indicators::Command(i) => Ok(i.ack_packet),
            _ => Err(VitaError::CommandOnly),
        }
    }
    /// Returns Ok(true) if the packet is an Ack packet, Ok(false) if
    /// it's some other kind of Command packet, and an error if it's
    /// some other type of packet entirely.
    pub fn is_cancellation_packet(&self) -> Result<bool, VitaError> {
        match self.indicators() {
            Indicators::Command(i) => Ok(i.cancellation_packet),
            _ => Err(VitaError::CommandOnly),
        }
    }
    /// Gets the TimeStamp-Integer (TSI) field.
    pub fn tsi(&self) -> Tsi {
        (((self.hword_1 >> 6) & 0b11) as u8).try_into().unwrap()
    }

    /// Sets the TimeStamp-Integer (TSI) field.
    pub(crate) fn set_tsi(&mut self, tsi: Tsi) {
        self.hword_1 = (self.hword_1 & !(0b11 << 6)) | ((tsi as u16) << 6);
    }

    /// Gets the TimeStamp-Fractional (TSF) field.
    pub fn tsf(&self) -> Tsf {
        (((self.hword_1 >> 4) & 0b11) as u8).try_into().unwrap()
    }

    /// Sets the TimeStamp-Fractional (TSF) field.
    pub(crate) fn set_tsf(&mut self, tsf: Tsf) {
        self.hword_1 = (self.hword_1 & !(0b11 << 4)) | ((tsf as u16) << 4);
    }

    /// Gets the modulo-16 packet counter field.
    pub fn packet_count(&self) -> u8 {
        (self.hword_1 & 0b1111) as u8
    }
    /// Sets the modulo-16 packet counter field.
    pub fn set_packet_count(&mut self, count: u8) {
        let masked_count = (count & 0b1111) as u16;
        self.hword_1 = (self.hword_1 & (!0b1111)) | masked_count;
    }
    /// Increments the packet counter by one (wrapping at 16).
    pub fn inc_packet_count(&mut self) {
        self.set_packet_count((self.packet_count() + 1) % 16);
    }

    /// Gets the packet size field (32-bit words).
    pub fn packet_size(&self) -> u16 {
        self.packet_size
    }
    /// Sets the packet size field (32-bit words).
    pub fn set_packet_size(&mut self, n_words: u16) {
        self.packet_size = n_words;
    }

    /// Returns true if a stream ID is included, false if not.
    pub fn stream_id_included(&self) -> bool {
        !matches!(
            &self.packet_type(),
            PacketType::SignalDataWithoutStreamId | PacketType::ExtensionDataWithoutStreamId
        )
    }

    /// Returns true if an integer timestamp is included, false if not.
    pub fn integer_timestamp_included(&self) -> bool {
        self.tsi() != Tsi::Null
    }

    /// Returns true if a fractional timestamp is included, false if not.
    pub fn fractional_timestamp_included(&self) -> bool {
        self.tsf() != Tsf::Null
    }

    /// Returns true if a trailer is included, false if not.
    pub fn trailer_included(&self) -> bool {
        match &self.indicators() {
            Indicators::SignalData(i) => i.trailer_included,
            _ => false,
        }
    }

    /// Returns the payload size in 32-bit words.
    pub fn payload_size_words(&self) -> usize {
        // Start with packet size minus 32 bits for the packet header
        let mut ret = self.packet_size as usize - 1;
        if self.stream_id_included() {
            ret -= 1;
        }
        if self.class_id_included() {
            ret -= 2;
        }
        if self.integer_timestamp_included() {
            ret -= 1;
        }
        if self.fractional_timestamp_included() {
            ret -= 2;
        }
        if self.trailer_included() {
            ret -= 1;
        }
        ret
    }

    /// Creates a new signal data packet header with some sane defaults.
    pub fn new_signal_data_header() -> PacketHeader {
        let mut ret = PacketHeader {
            hword_1: 0,
            packet_size: 0,
        };
        ret.set_packet_type(PacketType::SignalData);
        ret.set_indicators(Indicators::SignalData(SignalDataIndicators {
            trailer_included: false,
            not_a_vita490_packet: false,
            signal_spectral_data: false,
        }));
        ret
    }

    /// Creates a new context packet header with some sane defaults.
    pub fn new_context_header() -> PacketHeader {
        let mut ret = PacketHeader {
            hword_1: 0,
            packet_size: 0,
        };
        ret.set_packet_type(PacketType::Context);
        ret.set_indicators(Indicators::Context(ContextIndicators {
            not_a_vita490_packet: false,
            timestamp_mode: TimestampMode::GeneralTiming,
        }));
        ret
    }

    /// Creates a new control packet header.
    pub fn new_control_header() -> PacketHeader {
        let mut ret = PacketHeader::default();
        ret.set_packet_type(PacketType::Command);
        ret.set_indicators(Indicators::Command(CommandIndicators {
            ack_packet: false,
            cancellation_packet: false,
        }));
        ret
    }

    /// Creates a new cancellation packet header.
    pub fn new_cancellation_header() -> PacketHeader {
        let mut ret = PacketHeader::default();
        ret.set_packet_type(PacketType::Command);
        ret.set_indicators(Indicators::Command(CommandIndicators {
            ack_packet: false,
            cancellation_packet: true,
        }));
        ret
    }

    /// Creates a new ack packet header.
    pub fn new_ack_header() -> PacketHeader {
        let mut ret = PacketHeader::default();
        ret.set_packet_type(PacketType::Command);
        ret.set_indicators(Indicators::Command(CommandIndicators {
            ack_packet: true,
            cancellation_packet: false,
        }));
        ret
    }
}

#[cfg(test)]
mod tests {

    #[test]
    fn packet_header() {
        use crate::prelude::*;
        let packet = Vrt::new_control_packet();
        assert_eq!(packet.header().packet_type(), PacketType::Command);
        assert_eq!(packet.header().as_u32() >> 28, 0b0110);
    }

    #[test]
    fn set_class_id_sets_class_id_included_bit() {
        use crate::prelude::*;
        // Create a new packet (maybe SignalData or Context packet depending on your use case)
        let mut packet = Vrt::new_signal_data_packet();

        // Initially the class_id_included bit should be false
        assert!(!packet.header().class_id_included());

        // Set the class_id
        let class_id = Some(ClassIdentifier::default());
        packet.set_class_id(class_id);

        // Now the class_id_included bit should be true
        assert!(packet.header().class_id_included());
    }
}