1use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5
6use super::block_ids;
7use super::dnu::{f32_or_none, f64_or_none, F32_DNU};
8use super::SbfBlockParse;
9
10#[cfg(test)]
11use super::dnu::F64_DNU;
12
13#[derive(Debug, Clone)]
21pub struct AttEulerBlock {
22 tow_ms: u32,
23 wnc: u16,
24 nr_sv: u8,
25 error: u8,
26 mode: u16,
27 datum: u8,
28 heading_deg: f32,
29 pitch_deg: f32,
30 roll_deg: f32,
31 pitch_rate_dps: f32,
32 roll_rate_dps: f32,
33 heading_rate_dps: f32,
34}
35
36impl AttEulerBlock {
37 pub fn tow_seconds(&self) -> f64 {
38 self.tow_ms as f64 * 0.001
39 }
40 pub fn tow_ms(&self) -> u32 {
41 self.tow_ms
42 }
43 pub fn wnc(&self) -> u16 {
44 self.wnc
45 }
46
47 pub fn num_satellites(&self) -> u8 {
48 self.nr_sv
49 }
50 pub fn error_raw(&self) -> u8 {
51 self.error
52 }
53 pub fn mode_raw(&self) -> u16 {
54 self.mode
55 }
56 pub fn datum(&self) -> u8 {
57 self.datum
58 }
59
60 pub fn heading_deg(&self) -> Option<f32> {
61 f32_or_none(self.heading_deg)
62 }
63 pub fn pitch_deg(&self) -> Option<f32> {
64 f32_or_none(self.pitch_deg)
65 }
66 pub fn roll_deg(&self) -> Option<f32> {
67 f32_or_none(self.roll_deg)
68 }
69 pub fn pitch_rate_dps(&self) -> Option<f32> {
70 f32_or_none(self.pitch_rate_dps)
71 }
72 pub fn roll_rate_dps(&self) -> Option<f32> {
73 f32_or_none(self.roll_rate_dps)
74 }
75 pub fn heading_rate_dps(&self) -> Option<f32> {
76 f32_or_none(self.heading_rate_dps)
77 }
78}
79
80impl SbfBlockParse for AttEulerBlock {
81 const BLOCK_ID: u16 = block_ids::ATT_EULER;
82
83 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
84 if data.len() < 42 {
85 return Err(SbfError::ParseError("AttEuler too short".into()));
86 }
87
88 let nr_sv = data[12];
102 let error = data[13];
103 let mode = u16::from_le_bytes([data[14], data[15]]);
104 let datum = data[16];
105
106 let heading_deg = f32::from_le_bytes(data[18..22].try_into().unwrap());
107 let pitch_deg = f32::from_le_bytes(data[22..26].try_into().unwrap());
108 let roll_deg = f32::from_le_bytes(data[26..30].try_into().unwrap());
109 let pitch_rate_dps = f32::from_le_bytes(data[30..34].try_into().unwrap());
110 let roll_rate_dps = f32::from_le_bytes(data[34..38].try_into().unwrap());
111 let heading_rate_dps = f32::from_le_bytes(data[38..42].try_into().unwrap());
112
113 Ok(Self {
114 tow_ms: header.tow_ms,
115 wnc: header.wnc,
116 nr_sv,
117 error,
118 mode,
119 datum,
120 heading_deg,
121 pitch_deg,
122 roll_deg,
123 pitch_rate_dps,
124 roll_rate_dps,
125 heading_rate_dps,
126 })
127 }
128}
129
130#[derive(Debug, Clone)]
138pub struct AttCovEulerBlock {
139 tow_ms: u32,
140 wnc: u16,
141 error: u8,
142 pub cov_head_head: f32,
144 pub cov_pitch_pitch: f32,
146 pub cov_roll_roll: f32,
148 pub cov_head_pitch: f32,
150 pub cov_head_roll: f32,
152 pub cov_pitch_roll: f32,
154}
155
156impl AttCovEulerBlock {
157 pub fn tow_seconds(&self) -> f64 {
158 self.tow_ms as f64 * 0.001
159 }
160 pub fn tow_ms(&self) -> u32 {
161 self.tow_ms
162 }
163 pub fn wnc(&self) -> u16 {
164 self.wnc
165 }
166
167 pub fn error_raw(&self) -> u8 {
168 self.error
169 }
170
171 pub fn heading_std_deg(&self) -> Option<f32> {
172 if self.cov_head_head == F32_DNU || self.cov_head_head < 0.0 {
173 None
174 } else {
175 Some(self.cov_head_head.sqrt())
176 }
177 }
178 pub fn pitch_std_deg(&self) -> Option<f32> {
179 if self.cov_pitch_pitch == F32_DNU || self.cov_pitch_pitch < 0.0 {
180 None
181 } else {
182 Some(self.cov_pitch_pitch.sqrt())
183 }
184 }
185 pub fn roll_std_deg(&self) -> Option<f32> {
186 if self.cov_roll_roll == F32_DNU || self.cov_roll_roll < 0.0 {
187 None
188 } else {
189 Some(self.cov_roll_roll.sqrt())
190 }
191 }
192}
193
194impl SbfBlockParse for AttCovEulerBlock {
195 const BLOCK_ID: u16 = block_ids::ATT_COV_EULER;
196
197 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
198 if data.len() < 38 {
199 return Err(SbfError::ParseError("AttCovEuler too short".into()));
200 }
201
202 let error = data[13];
213 let cov_head_head = f32::from_le_bytes(data[14..18].try_into().unwrap());
214 let cov_pitch_pitch = f32::from_le_bytes(data[18..22].try_into().unwrap());
215 let cov_roll_roll = f32::from_le_bytes(data[22..26].try_into().unwrap());
216 let cov_head_pitch = f32::from_le_bytes(data[26..30].try_into().unwrap());
217 let cov_head_roll = f32::from_le_bytes(data[30..34].try_into().unwrap());
218 let cov_pitch_roll = f32::from_le_bytes(data[34..38].try_into().unwrap());
219
220 Ok(Self {
221 tow_ms: header.tow_ms,
222 wnc: header.wnc,
223 error,
224 cov_head_head,
225 cov_pitch_pitch,
226 cov_roll_roll,
227 cov_head_pitch,
228 cov_head_roll,
229 cov_pitch_roll,
230 })
231 }
232}
233
234#[derive(Debug, Clone)]
245pub struct AuxAntPosition {
246 pub nr_sv: u8,
247 pub error: u8,
248 pub ambiguity_type: u8,
249 pub aux_ant_id: u8,
250 d_east_m: f64,
251 d_north_m: f64,
252 d_up_m: f64,
253 east_vel_mps: f64,
254 north_vel_mps: f64,
255 up_vel_mps: f64,
256}
257
258impl AuxAntPosition {
259 pub fn d_east_m(&self) -> Option<f64> {
260 f64_or_none(self.d_east_m)
261 }
262 pub fn d_north_m(&self) -> Option<f64> {
263 f64_or_none(self.d_north_m)
264 }
265 pub fn d_up_m(&self) -> Option<f64> {
266 f64_or_none(self.d_up_m)
267 }
268 pub fn velocity_east_mps(&self) -> Option<f64> {
269 f64_or_none(self.east_vel_mps)
270 }
271 pub fn velocity_north_mps(&self) -> Option<f64> {
272 f64_or_none(self.north_vel_mps)
273 }
274 pub fn velocity_up_mps(&self) -> Option<f64> {
275 f64_or_none(self.up_vel_mps)
276 }
277}
278
279#[derive(Debug, Clone)]
281pub struct AuxAntPositionsBlock {
282 tow_ms: u32,
283 wnc: u16,
284 pub positions: Vec<AuxAntPosition>,
285}
286
287impl AuxAntPositionsBlock {
288 pub fn tow_seconds(&self) -> f64 {
289 self.tow_ms as f64 * 0.001
290 }
291 pub fn tow_ms(&self) -> u32 {
292 self.tow_ms
293 }
294 pub fn wnc(&self) -> u16 {
295 self.wnc
296 }
297
298 pub fn num_positions(&self) -> usize {
299 self.positions.len()
300 }
301}
302
303impl SbfBlockParse for AuxAntPositionsBlock {
304 const BLOCK_ID: u16 = block_ids::AUX_ANT_POSITIONS;
305
306 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
307 if data.len() < 14 {
308 return Err(SbfError::ParseError("AuxAntPositions too short".into()));
309 }
310
311 let n = data[12] as usize;
312 let sb_length = data[13] as usize;
313
314 if sb_length < 52 {
315 return Err(SbfError::ParseError(
316 "AuxAntPositions SBLength too small".into(),
317 ));
318 }
319
320 let mut positions = Vec::new();
321 let mut offset = 14;
322
323 for _ in 0..n {
324 if offset + sb_length > data.len() {
325 break;
326 }
327
328 let nr_sv = data[offset];
329 let error = data[offset + 1];
330 let ambiguity_type = data[offset + 2];
331 let aux_ant_id = data[offset + 3];
332
333 let d_east_m = f64::from_le_bytes(data[offset + 4..offset + 12].try_into().unwrap());
334 let d_north_m = f64::from_le_bytes(data[offset + 12..offset + 20].try_into().unwrap());
335 let d_up_m = f64::from_le_bytes(data[offset + 20..offset + 28].try_into().unwrap());
336 let east_vel_mps =
337 f64::from_le_bytes(data[offset + 28..offset + 36].try_into().unwrap());
338 let north_vel_mps =
339 f64::from_le_bytes(data[offset + 36..offset + 44].try_into().unwrap());
340 let up_vel_mps = f64::from_le_bytes(data[offset + 44..offset + 52].try_into().unwrap());
341
342 positions.push(AuxAntPosition {
343 nr_sv,
344 error,
345 ambiguity_type,
346 aux_ant_id,
347 d_east_m,
348 d_north_m,
349 d_up_m,
350 east_vel_mps,
351 north_vel_mps,
352 up_vel_mps,
353 });
354
355 offset += sb_length;
356 }
357
358 Ok(Self {
359 tow_ms: header.tow_ms,
360 wnc: header.wnc,
361 positions,
362 })
363 }
364}
365
366#[derive(Debug, Clone)]
372pub struct EndOfAttBlock {
373 tow_ms: u32,
374 wnc: u16,
375}
376
377impl EndOfAttBlock {
378 pub fn tow_seconds(&self) -> f64 {
379 self.tow_ms as f64 * 0.001
380 }
381 pub fn tow_ms(&self) -> u32 {
382 self.tow_ms
383 }
384 pub fn wnc(&self) -> u16 {
385 self.wnc
386 }
387}
388
389impl SbfBlockParse for EndOfAttBlock {
390 const BLOCK_ID: u16 = block_ids::END_OF_ATT;
391
392 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
393 if data.len() < 12 {
394 return Err(SbfError::ParseError("EndOfAtt too short".into()));
395 }
396
397 Ok(Self {
398 tow_ms: header.tow_ms,
399 wnc: header.wnc,
400 })
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::header::SbfHeader;
408
409 fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
410 SbfHeader {
411 crc: 0,
412 block_id,
413 block_rev: 0,
414 length: (data_len + 2) as u16,
415 tow_ms,
416 wnc,
417 }
418 }
419
420 #[test]
421 fn test_att_euler_scaled_accessors() {
422 let block = AttEulerBlock {
423 tow_ms: 1000,
424 wnc: 2000,
425 nr_sv: 12,
426 error: 0,
427 mode: 3,
428 datum: 1,
429 heading_deg: 45.0,
430 pitch_deg: -2.0,
431 roll_deg: 1.5,
432 pitch_rate_dps: 0.1,
433 roll_rate_dps: 0.2,
434 heading_rate_dps: -0.3,
435 };
436
437 assert!((block.tow_seconds() - 1.0).abs() < 1e-6);
438 assert_eq!(block.num_satellites(), 12);
439 assert_eq!(block.mode_raw(), 3);
440 assert!((block.heading_deg().unwrap() - 45.0).abs() < 1e-6);
441 assert!((block.roll_rate_dps().unwrap() - 0.2).abs() < 1e-6);
442 }
443
444 #[test]
445 fn test_att_euler_dnu_handling() {
446 let block = AttEulerBlock {
447 tow_ms: 0,
448 wnc: 0,
449 nr_sv: 0,
450 error: 0,
451 mode: 0,
452 datum: 0,
453 heading_deg: F32_DNU,
454 pitch_deg: F32_DNU,
455 roll_deg: 1.0,
456 pitch_rate_dps: F32_DNU,
457 roll_rate_dps: 0.0,
458 heading_rate_dps: F32_DNU,
459 };
460
461 assert!(block.heading_deg().is_none());
462 assert!(block.pitch_deg().is_none());
463 assert!(block.roll_deg().is_some());
464 assert!(block.pitch_rate_dps().is_none());
465 assert!(block.heading_rate_dps().is_none());
466 }
467
468 #[test]
469 fn test_att_euler_parse() {
470 let mut data = vec![0u8; 42];
471 data[12] = 8; data[13] = 1; data[14..16].copy_from_slice(&500_u16.to_le_bytes());
474 data[16] = 2; data[18..22].copy_from_slice(&10.5_f32.to_le_bytes());
476 data[22..26].copy_from_slice(&(-1.25_f32).to_le_bytes());
477 data[26..30].copy_from_slice(&0.75_f32.to_le_bytes());
478 data[30..34].copy_from_slice(&0.1_f32.to_le_bytes());
479 data[34..38].copy_from_slice(&0.2_f32.to_le_bytes());
480 data[38..42].copy_from_slice(&0.3_f32.to_le_bytes());
481
482 let header = header_for(block_ids::ATT_EULER, data.len(), 123456, 2048);
483 let block = AttEulerBlock::parse(&header, &data).unwrap();
484
485 assert_eq!(block.num_satellites(), 8);
486 assert_eq!(block.error_raw(), 1);
487 assert_eq!(block.mode_raw(), 500);
488 assert!((block.heading_deg().unwrap() - 10.5).abs() < 1e-6);
489 assert!((block.pitch_rate_dps().unwrap() - 0.1).abs() < 1e-6);
490 }
491
492 #[test]
493 fn test_att_cov_euler_std_accessors() {
494 let block = AttCovEulerBlock {
495 tow_ms: 2000,
496 wnc: 100,
497 error: 0,
498 cov_head_head: 4.0,
499 cov_pitch_pitch: 9.0,
500 cov_roll_roll: 16.0,
501 cov_head_pitch: 0.0,
502 cov_head_roll: 0.0,
503 cov_pitch_roll: 0.0,
504 };
505
506 assert!((block.heading_std_deg().unwrap() - 2.0).abs() < 1e-6);
507 assert!((block.pitch_std_deg().unwrap() - 3.0).abs() < 1e-6);
508 assert!((block.roll_std_deg().unwrap() - 4.0).abs() < 1e-6);
509 assert!((block.tow_seconds() - 2.0).abs() < 1e-6);
510 }
511
512 #[test]
513 fn test_att_cov_euler_dnu_handling() {
514 let block = AttCovEulerBlock {
515 tow_ms: 0,
516 wnc: 0,
517 error: 0,
518 cov_head_head: F32_DNU,
519 cov_pitch_pitch: -1.0,
520 cov_roll_roll: 1.0,
521 cov_head_pitch: 0.0,
522 cov_head_roll: 0.0,
523 cov_pitch_roll: 0.0,
524 };
525
526 assert!(block.heading_std_deg().is_none());
527 assert!(block.pitch_std_deg().is_none());
528 assert!(block.roll_std_deg().is_some());
529 }
530
531 #[test]
532 fn test_att_cov_euler_parse() {
533 let mut data = vec![0u8; 38];
534 data[13] = 2; data[14..18].copy_from_slice(&1.0_f32.to_le_bytes());
536 data[18..22].copy_from_slice(&4.0_f32.to_le_bytes());
537 data[22..26].copy_from_slice(&9.0_f32.to_le_bytes());
538 data[26..30].copy_from_slice(&0.1_f32.to_le_bytes());
539 data[30..34].copy_from_slice(&0.2_f32.to_le_bytes());
540 data[34..38].copy_from_slice(&0.3_f32.to_le_bytes());
541
542 let header = header_for(block_ids::ATT_COV_EULER, data.len(), 654321, 1024);
543 let block = AttCovEulerBlock::parse(&header, &data).unwrap();
544
545 assert_eq!(block.error_raw(), 2);
546 assert!((block.heading_std_deg().unwrap() - 1.0).abs() < 1e-6);
547 }
548
549 #[test]
550 fn test_aux_ant_positions_accessors() {
551 let info = AuxAntPosition {
552 nr_sv: 7,
553 error: 0,
554 ambiguity_type: 1,
555 aux_ant_id: 2,
556 d_east_m: 1.5,
557 d_north_m: -2.5,
558 d_up_m: 0.5,
559 east_vel_mps: 0.1,
560 north_vel_mps: -0.2,
561 up_vel_mps: 0.0,
562 };
563
564 assert_eq!(info.nr_sv, 7);
565 assert!((info.d_east_m().unwrap() - 1.5).abs() < 1e-6);
566 assert!((info.velocity_north_mps().unwrap() + 0.2).abs() < 1e-6);
567 }
568
569 #[test]
570 fn test_aux_ant_positions_dnu_handling() {
571 let info = AuxAntPosition {
572 nr_sv: 0,
573 error: 0,
574 ambiguity_type: 0,
575 aux_ant_id: 0,
576 d_east_m: F64_DNU,
577 d_north_m: 1.0,
578 d_up_m: F64_DNU,
579 east_vel_mps: F64_DNU,
580 north_vel_mps: 0.0,
581 up_vel_mps: F64_DNU,
582 };
583
584 assert!(info.d_east_m().is_none());
585 assert!(info.d_up_m().is_none());
586 assert!(info.velocity_east_mps().is_none());
587 assert!(info.velocity_up_mps().is_none());
588 assert!(info.velocity_north_mps().is_some());
589 }
590
591 #[test]
592 fn test_aux_ant_positions_parse() {
593 let mut data = vec![0u8; 14 + 52];
594 data[12] = 1; data[13] = 52; let offset = 14;
598 data[offset] = 5; data[offset + 1] = 1; data[offset + 2] = 2; data[offset + 3] = 3; data[offset + 4..offset + 12].copy_from_slice(&1.0_f64.to_le_bytes());
603 data[offset + 12..offset + 20].copy_from_slice(&2.0_f64.to_le_bytes());
604 data[offset + 20..offset + 28].copy_from_slice(&3.0_f64.to_le_bytes());
605 data[offset + 28..offset + 36].copy_from_slice(&0.1_f64.to_le_bytes());
606 data[offset + 36..offset + 44].copy_from_slice(&0.2_f64.to_le_bytes());
607 data[offset + 44..offset + 52].copy_from_slice(&0.3_f64.to_le_bytes());
608
609 let header = header_for(block_ids::AUX_ANT_POSITIONS, data.len(), 9999, 55);
610 let block = AuxAntPositionsBlock::parse(&header, &data).unwrap();
611
612 assert_eq!(block.num_positions(), 1);
613 let entry = &block.positions[0];
614 assert_eq!(entry.aux_ant_id, 3);
615 assert!((entry.d_north_m().unwrap() - 2.0).abs() < 1e-6);
616 }
617
618 #[test]
619 fn test_end_of_att_parse() {
620 let data = vec![0u8; 14];
621 let header = header_for(block_ids::END_OF_ATT, data.len(), 2500, 42);
622 let block = EndOfAttBlock::parse(&header, &data).unwrap();
623
624 assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
625 assert_eq!(block.wnc(), 42);
626 }
627}