1use broadcast_common::{Parse, Serialize};
5
6use crate::error::{Error, Result};
7
8pub const FRAME_LEN: usize = 10;
14
15pub const MAX_HOURS: u8 = 23;
17pub const MAX_MINUTES_SECONDS: u8 = 59;
19pub const MAX_FRAMES: u8 = 29;
27pub const MAX_BINARY_GROUP: u8 = 0x0F;
29
30pub const SYNC_WORD: [u8; 2] = [0xFC, 0xBF];
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50#[non_exhaustive]
51pub enum FrameRate {
52 Fps30,
54 Fps25,
56 Fps24,
58}
59
60impl FrameRate {
61 #[must_use]
63 pub fn name(&self) -> &'static str {
64 match self {
65 Self::Fps30 => "30-frame",
66 Self::Fps25 => "25-frame",
67 Self::Fps24 => "24-frame",
68 }
69 }
70}
71
72broadcast_common::impl_spec_display!(FrameRate);
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82#[non_exhaustive]
83pub enum BinaryGroupUsage {
84 UnspecifiedUnspecified,
87 UnspecifiedEightBitCodes,
90 ClockTimeUnspecified,
93 Reserved,
96 UnspecifiedDateTimeZone,
99 UnspecifiedPageLine,
102 ClockTimeDateTimeZone,
105 ClockTimePageLine,
108}
109
110impl BinaryGroupUsage {
111 #[must_use]
114 pub fn name(&self) -> &'static str {
115 match self {
116 Self::UnspecifiedUnspecified => "unspecified time address, unspecified binary group",
117 Self::UnspecifiedEightBitCodes => "unspecified time address, 8-bit codes",
118 Self::ClockTimeUnspecified => "clock time, unspecified binary group",
119 Self::Reserved => "reserved time address, reserved binary group",
120 Self::UnspecifiedDateTimeZone => "unspecified time address, date and time zone",
121 Self::UnspecifiedPageLine => "unspecified time address, page/line",
122 Self::ClockTimeDateTimeZone => "clock time, date and time zone",
123 Self::ClockTimePageLine => "clock time, page/line",
124 }
125 }
126
127 #[must_use]
129 pub fn from_flags(bgf2: bool, bgf1: bool, bgf0: bool) -> Self {
130 match (bgf2, bgf1, bgf0) {
131 (false, false, false) => Self::UnspecifiedUnspecified,
132 (false, false, true) => Self::UnspecifiedEightBitCodes,
133 (false, true, false) => Self::ClockTimeUnspecified,
134 (false, true, true) => Self::Reserved,
135 (true, false, false) => Self::UnspecifiedDateTimeZone,
136 (true, false, true) => Self::UnspecifiedPageLine,
137 (true, true, false) => Self::ClockTimeDateTimeZone,
138 (true, true, true) => Self::ClockTimePageLine,
139 }
140 }
141}
142
143broadcast_common::impl_spec_display!(BinaryGroupUsage);
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct BinaryGroupFlags {
150 pub bgf2: bool,
152 pub bgf1: bool,
154 pub bgf0: bool,
156}
157
158impl BinaryGroupFlags {
159 #[must_use]
161 pub fn usage(&self) -> BinaryGroupUsage {
162 BinaryGroupUsage::from_flags(self.bgf2, self.bgf1, self.bgf0)
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub struct LtcFrame {
182 pub hours: u8,
184 pub minutes: u8,
186 pub seconds: u8,
188 pub frames: u8,
191 pub drop_frame_flag: bool,
195 pub color_frame_flag: bool,
198 pub flag_bit_27: bool,
203 pub flag_bit_43: bool,
206 pub flag_bit_58: bool,
208 pub flag_bit_59: bool,
211 pub user_bits: [u8; 8],
216}
217
218impl LtcFrame {
219 #[must_use]
222 pub fn polarity_correction(&self, rate: FrameRate) -> bool {
223 match rate {
224 FrameRate::Fps25 => self.flag_bit_59,
225 FrameRate::Fps30 | FrameRate::Fps24 => self.flag_bit_27,
226 }
227 }
228
229 #[must_use]
232 pub fn binary_group_flags(&self, rate: FrameRate) -> BinaryGroupFlags {
233 let (bgf0, bgf2) = match rate {
234 FrameRate::Fps25 => (self.flag_bit_27, self.flag_bit_43),
235 FrameRate::Fps30 | FrameRate::Fps24 => (self.flag_bit_43, self.flag_bit_59),
236 };
237 BinaryGroupFlags {
238 bgf2,
239 bgf1: self.flag_bit_58,
240 bgf0,
241 }
242 }
243
244 fn validate(&self) -> Result<()> {
249 if self.hours > MAX_HOURS {
250 return Err(Error::InvalidValue {
251 field: "hours",
252 value: self.hours,
253 reason: "exceeds the 24-hour-clock maximum (23)",
254 });
255 }
256 if self.minutes > MAX_MINUTES_SECONDS {
257 return Err(Error::InvalidValue {
258 field: "minutes",
259 value: self.minutes,
260 reason: "exceeds the maximum (59)",
261 });
262 }
263 if self.seconds > MAX_MINUTES_SECONDS {
264 return Err(Error::InvalidValue {
265 field: "seconds",
266 value: self.seconds,
267 reason: "exceeds the maximum (59)",
268 });
269 }
270 if self.frames > MAX_FRAMES {
271 return Err(Error::InvalidValue {
272 field: "frames",
273 value: self.frames,
274 reason: "exceeds the widest supported frame-rate maximum (29)",
275 });
276 }
277 for (index, &value) in self.user_bits.iter().enumerate() {
278 if value > MAX_BINARY_GROUP {
279 return Err(Error::InvalidBinaryGroup { index, value });
280 }
281 }
282 Ok(())
283 }
284}
285
286impl<'a> Parse<'a> for LtcFrame {
287 type Error = Error;
288
289 fn parse(bytes: &'a [u8]) -> Result<Self> {
290 if bytes.len() < FRAME_LEN {
291 return Err(Error::BufferTooShort {
292 need: FRAME_LEN,
293 have: bytes.len(),
294 what: "LTC codeword",
295 });
296 }
297
298 if [bytes[8], bytes[9]] != SYNC_WORD {
299 return Err(Error::SyncWordMismatch {
300 expected: SYNC_WORD,
301 found: [bytes[8], bytes[9]],
302 });
303 }
304
305 let frame_units = bytes[0] & 0x0F;
306 let user_bits_1 = bytes[0] >> 4;
307
308 let frame_tens = bytes[1] & 0x03;
309 let drop_frame_flag = bytes[1] & 0x04 != 0;
310 let color_frame_flag = bytes[1] & 0x08 != 0;
311 let user_bits_2 = bytes[1] >> 4;
312
313 let seconds_units = bytes[2] & 0x0F;
314 let user_bits_3 = bytes[2] >> 4;
315
316 let seconds_tens = bytes[3] & 0x07;
317 let flag_bit_27 = bytes[3] & 0x08 != 0;
318 let user_bits_4 = bytes[3] >> 4;
319
320 let minutes_units = bytes[4] & 0x0F;
321 let user_bits_5 = bytes[4] >> 4;
322
323 let minutes_tens = bytes[5] & 0x07;
324 let flag_bit_43 = bytes[5] & 0x08 != 0;
325 let user_bits_6 = bytes[5] >> 4;
326
327 let hours_units = bytes[6] & 0x0F;
328 let user_bits_7 = bytes[6] >> 4;
329
330 let hours_tens = bytes[7] & 0x03;
331 let flag_bit_58 = bytes[7] & 0x04 != 0;
332 let flag_bit_59 = bytes[7] & 0x08 != 0;
333 let user_bits_8 = bytes[7] >> 4;
334
335 let frame = Self {
336 hours: hours_tens * 10 + hours_units,
337 minutes: minutes_tens * 10 + minutes_units,
338 seconds: seconds_tens * 10 + seconds_units,
339 frames: frame_tens * 10 + frame_units,
340 drop_frame_flag,
341 color_frame_flag,
342 flag_bit_27,
343 flag_bit_43,
344 flag_bit_58,
345 flag_bit_59,
346 user_bits: [
347 user_bits_1,
348 user_bits_2,
349 user_bits_3,
350 user_bits_4,
351 user_bits_5,
352 user_bits_6,
353 user_bits_7,
354 user_bits_8,
355 ],
356 };
357 frame.validate()?;
358 Ok(frame)
359 }
360}
361
362impl Serialize for LtcFrame {
363 type Error = Error;
364
365 fn serialized_len(&self) -> usize {
366 FRAME_LEN
367 }
368
369 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
370 if buf.len() < FRAME_LEN {
371 return Err(Error::BufferTooShort {
372 need: FRAME_LEN,
373 have: buf.len(),
374 what: "LTC codeword serialize output",
375 });
376 }
377 self.validate()?;
378
379 let frame_units = self.frames % 10;
380 let frame_tens = self.frames / 10;
381 let seconds_units = self.seconds % 10;
382 let seconds_tens = self.seconds / 10;
383 let minutes_units = self.minutes % 10;
384 let minutes_tens = self.minutes / 10;
385 let hours_units = self.hours % 10;
386 let hours_tens = self.hours / 10;
387
388 buf[0] = frame_units | (self.user_bits[0] << 4);
389 buf[1] = frame_tens
390 | (u8::from(self.drop_frame_flag) << 2)
391 | (u8::from(self.color_frame_flag) << 3)
392 | (self.user_bits[1] << 4);
393 buf[2] = seconds_units | (self.user_bits[2] << 4);
394 buf[3] = seconds_tens | (u8::from(self.flag_bit_27) << 3) | (self.user_bits[3] << 4);
395 buf[4] = minutes_units | (self.user_bits[4] << 4);
396 buf[5] = minutes_tens | (u8::from(self.flag_bit_43) << 3) | (self.user_bits[5] << 4);
397 buf[6] = hours_units | (self.user_bits[6] << 4);
398 buf[7] = hours_tens
399 | (u8::from(self.flag_bit_58) << 2)
400 | (u8::from(self.flag_bit_59) << 3)
401 | (self.user_bits[7] << 4);
402 buf[8] = SYNC_WORD[0];
403 buf[9] = SYNC_WORD[1];
404
405 Ok(FRAME_LEN)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 fn sample_frame() -> LtcFrame {
414 LtcFrame {
415 hours: 1,
416 minutes: 23,
417 seconds: 45,
418 frames: 13,
419 drop_frame_flag: false,
420 color_frame_flag: true,
421 flag_bit_27: true,
422 flag_bit_43: true,
423 flag_bit_58: false,
424 flag_bit_59: true,
425 user_bits: [1, 2, 3, 4, 5, 6, 7, 8],
426 }
427 }
428
429 #[test]
430 fn round_trip() {
431 let f = sample_frame();
432 let mut out = [0u8; FRAME_LEN];
433 f.serialize_into(&mut out).unwrap();
434 assert_eq!(LtcFrame::parse(&out).unwrap(), f);
435 }
436
437 #[test]
438 fn serialize_then_parse_matches_spec_vector() {
439 let f = sample_frame();
441 let mut out = [0u8; FRAME_LEN];
442 f.serialize_into(&mut out).unwrap();
443 assert_eq!(
444 out,
445 [0x13, 0x29, 0x35, 0x4C, 0x53, 0x6A, 0x71, 0x88, 0xFC, 0xBF]
446 );
447 }
448
449 #[test]
450 fn rejects_sync_word_mismatch() {
451 let mut bytes = [0x13, 0x29, 0x35, 0x4C, 0x53, 0x6A, 0x71, 0x88, 0xFC, 0xBF];
452 bytes[9] = 0x00;
453 assert!(matches!(
454 LtcFrame::parse(&bytes),
455 Err(Error::SyncWordMismatch { .. })
456 ));
457 }
458
459 #[test]
460 fn rejects_buffer_too_short() {
461 let bytes = [0u8; 9];
462 assert!(matches!(
463 LtcFrame::parse(&bytes),
464 Err(Error::BufferTooShort {
465 need: 10,
466 have: 9,
467 ..
468 })
469 ));
470 }
471
472 #[test]
473 fn rejects_hours_over_23() {
474 let mut f = sample_frame();
475 f.hours = 24;
476 let mut out = [0u8; FRAME_LEN];
477 assert!(matches!(
478 f.serialize_into(&mut out),
479 Err(Error::InvalidValue { field: "hours", .. })
480 ));
481 }
482
483 #[test]
484 fn rejects_frames_over_29() {
485 let mut f = sample_frame();
486 f.frames = 30;
487 let mut out = [0u8; FRAME_LEN];
488 assert!(matches!(
489 f.serialize_into(&mut out),
490 Err(Error::InvalidValue {
491 field: "frames",
492 ..
493 })
494 ));
495 }
496
497 #[test]
498 fn rejects_user_bit_over_15() {
499 let mut f = sample_frame();
500 f.user_bits[3] = 0x10;
501 let mut out = [0u8; FRAME_LEN];
502 assert!(matches!(
503 f.serialize_into(&mut out),
504 Err(Error::InvalidBinaryGroup {
505 index: 3,
506 value: 0x10
507 })
508 ));
509 }
510
511 #[test]
512 fn polarity_correction_and_bgf_swap_between_25_and_other_rates() {
513 let f = sample_frame(); assert!(f.polarity_correction(FrameRate::Fps30));
515 assert!(f.polarity_correction(FrameRate::Fps24));
516 assert!(f.polarity_correction(FrameRate::Fps25));
517
518 let bg30 = f.binary_group_flags(FrameRate::Fps30);
519 assert_eq!(
520 bg30,
521 BinaryGroupFlags {
522 bgf2: true,
523 bgf1: false,
524 bgf0: true
525 }
526 );
527 assert_eq!(bg30.usage(), BinaryGroupUsage::UnspecifiedPageLine);
528
529 let mut g = f;
534 g.flag_bit_27 = true; g.flag_bit_43 = false; g.flag_bit_58 = true; g.flag_bit_59 = false; assert!(g.polarity_correction(FrameRate::Fps30));
540 assert!(g.polarity_correction(FrameRate::Fps24));
541 assert!(!g.polarity_correction(FrameRate::Fps25));
542
543 let bg30 = g.binary_group_flags(FrameRate::Fps30);
544 assert_eq!(
545 bg30,
546 BinaryGroupFlags {
547 bgf2: false,
548 bgf1: true,
549 bgf0: false
550 }
551 );
552 let bg25 = g.binary_group_flags(FrameRate::Fps25);
553 assert_eq!(
554 bg25,
555 BinaryGroupFlags {
556 bgf2: false,
557 bgf1: true,
558 bgf0: true
559 }
560 );
561 assert_ne!(
562 bg30, bg25,
563 "25-frame's BGF0 must differ from 30/24-frame's here"
564 );
565 }
566
567 #[test]
568 fn binary_group_usage_covers_all_eight_combinations() {
569 for bgf2 in [false, true] {
570 for bgf1 in [false, true] {
571 for bgf0 in [false, true] {
572 let _ = BinaryGroupUsage::from_flags(bgf2, bgf1, bgf0);
574 }
575 }
576 }
577 }
578
579 #[test]
580 fn field_mutation_changes_bytes() {
581 let a = sample_frame();
582 let mut b = a;
583 b.seconds = a.seconds.wrapping_add(1);
584 let mut oa = [0u8; FRAME_LEN];
585 let mut ob = [0u8; FRAME_LEN];
586 a.serialize_into(&mut oa).unwrap();
587 b.serialize_into(&mut ob).unwrap();
588 assert_ne!(oa, ob);
589 }
590}