1use bincode::de::Decoder;
2use bincode::enc::Encoder;
3use bincode::error::{DecodeError, EncodeError};
4use bincode::{Decode, Encode};
5use core::fmt::Debug;
6use core::marker::PhantomData;
7use cu29::prelude::*;
8use cu29::units::si::f32::Length;
9use cu29::units::si::length::meter;
10use serde::{Deserialize, Serialize, Serializer};
11
12#[derive(
17 Default, Debug, Encode, Decode, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect,
18)]
19pub struct CuDepthMapFormat {
20 pub width: u32,
21 pub height: u32,
22 pub stride: u32,
23}
24
25impl CuDepthMapFormat {
26 pub fn is_valid(&self) -> bool {
27 self.width <= self.stride
28 }
29
30 pub fn required_elements(&self) -> usize {
31 self.stride as usize * self.height as usize
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
37#[serde(rename_all = "snake_case")]
38pub enum CuDepthInvalidValue {
39 Unsigned(u64),
40 Signed(i64),
41 FloatBits(u64),
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
46pub struct CuDepthEncodingDescriptor {
47 pub units_per_meter: f32,
49 pub invalid: Option<CuDepthInvalidValue>,
51}
52
53pub trait CuDepthEncoding: Copy + Debug + Default + Send + Sync + 'static {
55 type Sample: ElementType;
56
57 const DESCRIPTOR: CuDepthEncodingDescriptor;
58 const SERIALIZED_MAP_SCHEMA: &'static str = GENERIC_DEPTH_MAP_SERIALIZED_SCHEMA;
59 const TYPE_PATH: &'static str;
60 const SHORT_TYPE_PATH: &'static str;
61 const TYPE_IDENT: &'static str;
62
63 fn decode(sample: Self::Sample) -> Option<Length>;
67
68 fn encode_sample(sample: Option<Length>) -> Self::Sample;
72}
73
74#[derive(Debug, Default, Clone, Copy)]
76pub struct CuDepthLength;
77
78impl CuDepthEncoding for CuDepthLength {
79 type Sample = Length;
80
81 const DESCRIPTOR: CuDepthEncodingDescriptor = CuDepthEncodingDescriptor {
82 units_per_meter: 1.0,
83 invalid: None,
84 };
85 const SERIALIZED_MAP_SCHEMA: &'static str = LENGTH_DEPTH_MAP_SERIALIZED_SCHEMA;
86 const TYPE_PATH: &'static str = "cu_sensor_payloads::CuDepthMap";
87 const SHORT_TYPE_PATH: &'static str = "CuDepthMap";
88 const TYPE_IDENT: &'static str = "CuDepthMap";
89
90 fn decode(sample: Self::Sample) -> Option<Length> {
91 Some(sample)
92 }
93
94 fn encode_sample(sample: Option<Length>) -> Self::Sample {
95 sample.unwrap_or_else(|| Length::new::<meter>(f32::NAN))
96 }
97}
98
99pub const fn depth_resolution_meters(meters: f32) -> Length {
104 Length { value: meters }
105}
106
107pub trait CuDepthScale: Copy + Debug + Default + Send + Sync + 'static {
109 const RESOLUTION: Length;
110 const UNITS_PER_METER: f32 = 1.0 / Self::RESOLUTION.value;
111}
112
113#[derive(Debug, Default, Clone, Copy)]
118pub struct CuDepthScaleRatio<const NUMERATOR: u64, const DENOMINATOR: u64>;
119
120impl<const NUMERATOR: u64, const DENOMINATOR: u64> CuDepthScale
121 for CuDepthScaleRatio<NUMERATOR, DENOMINATOR>
122{
123 const RESOLUTION: Length = depth_resolution_meters(NUMERATOR as f32 / DENOMINATOR as f32);
124 const UNITS_PER_METER: f32 = DENOMINATOR as f32 / NUMERATOR as f32;
125}
126
127pub type CuDepthMillimeter = CuDepthScaleRatio<1, 1_000>;
129
130pub type CuDepthDecimeter = CuDepthScaleRatio<1, 10>;
132
133pub trait CuDepthIntegerSample: ElementType + 'static {
135 const MAX: u64;
136
137 fn to_u64(self) -> u64;
138 fn from_u64(value: u64) -> Self;
139}
140
141macro_rules! impl_depth_integer_sample {
142 ($($sample:ty),+ $(,)?) => {
143 $(
144 impl CuDepthIntegerSample for $sample {
145 const MAX: u64 = <$sample>::MAX as u64;
146
147 fn to_u64(self) -> u64 {
148 self as u64
149 }
150
151 fn from_u64(value: u64) -> Self {
152 value as Self
153 }
154 }
155 )+
156 };
157}
158
159impl_depth_integer_sample!(u8, u16, u32, u64);
160
161#[derive(Debug, Default, Clone, Copy)]
178pub struct CuDepthInteger<T, S>(PhantomData<(T, S)>);
179
180impl<T, S> CuDepthEncoding for CuDepthInteger<T, S>
181where
182 T: CuDepthIntegerSample,
183 S: CuDepthScale,
184{
185 type Sample = T;
186
187 const DESCRIPTOR: CuDepthEncodingDescriptor = CuDepthEncodingDescriptor {
188 units_per_meter: S::UNITS_PER_METER,
189 invalid: Some(CuDepthInvalidValue::Unsigned(0)),
190 };
191 const SERIALIZED_MAP_SCHEMA: &'static str = INTEGER_DEPTH_MAP_SERIALIZED_SCHEMA;
192 const TYPE_PATH: &'static str = "cu_sensor_payloads::CuDepthMapInteger";
193 const SHORT_TYPE_PATH: &'static str = "CuDepthMapInteger";
194 const TYPE_IDENT: &'static str = "CuDepthMapInteger";
195
196 fn decode(sample: Self::Sample) -> Option<Length> {
197 let raw = sample.to_u64();
198 (raw != 0).then(|| depth_resolution_meters(raw as f32 / S::UNITS_PER_METER))
199 }
200
201 fn encode_sample(sample: Option<Length>) -> Self::Sample {
202 let Some(meters) = sample.map(|sample| sample.get::<meter>()) else {
203 return T::from_u64(0);
204 };
205 if !meters.is_finite() || meters <= 0.0 {
206 return T::from_u64(0);
207 }
208 let raw = (meters * S::UNITS_PER_METER)
209 .round()
210 .clamp(1.0, T::MAX as f32) as u64;
211 T::from_u64(raw)
212 }
213}
214
215#[derive(Debug, Default, Clone, Reflect)]
221#[reflect(
222 from_reflect = false,
223 no_field_bounds,
224 type_path = false,
225 SerializedPayloadSchema
226)]
227pub struct CuDepthMap<A, E = CuDepthLength>
228where
229 E: CuDepthEncoding,
230 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
231{
232 pub format: CuDepthMapFormat,
233 #[reflect(ignore)]
234 pub buffer_handle: CuHandle<A>,
235 #[reflect(ignore)]
236 encoding: PhantomData<E>,
237}
238
239impl<A, E> TypePath for CuDepthMap<A, E>
240where
241 E: CuDepthEncoding,
242 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
243{
244 fn type_path() -> &'static str {
245 E::TYPE_PATH
246 }
247
248 fn short_type_path() -> &'static str {
249 E::SHORT_TYPE_PATH
250 }
251
252 fn type_ident() -> Option<&'static str> {
253 Some(E::TYPE_IDENT)
254 }
255
256 fn crate_name() -> Option<&'static str> {
257 Some("cu_sensor_payloads")
258 }
259
260 fn module_path() -> Option<&'static str> {
261 Some("cu_sensor_payloads")
262 }
263}
264
265impl<A, E> Encode for CuDepthMap<A, E>
266where
267 E: CuDepthEncoding,
268 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
269 CuHandle<A>: Encode,
270{
271 fn encode<Enc: Encoder>(&self, encoder: &mut Enc) -> Result<(), EncodeError> {
272 Encode::encode(&self.format, encoder)?;
273 Encode::encode(&self.buffer_handle, encoder)
274 }
275}
276
277impl<A, E> Decode<()> for CuDepthMap<A, E>
278where
279 E: CuDepthEncoding,
280 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
281 CuHandle<A>: Decode<()>,
282{
283 fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
284 Ok(Self {
285 format: Decode::decode(decoder)?,
286 buffer_handle: Decode::decode(decoder)?,
287 encoding: PhantomData,
288 })
289 }
290}
291
292impl<'de, A, E> Deserialize<'de> for CuDepthMap<A, E>
293where
294 E: CuDepthEncoding,
295 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
296 CuHandle<A>: Deserialize<'de>,
297{
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: serde::Deserializer<'de>,
301 {
302 #[derive(Deserialize)]
303 struct Wire<H> {
304 format: CuDepthMapFormat,
305 encoding: Option<CuDepthEncodingDescriptor>,
306 handle: H,
307 }
308
309 let wire = Wire::<CuHandle<A>>::deserialize(deserializer)?;
310 if let Some(encoding) = wire.encoding
311 && encoding != E::DESCRIPTOR
312 {
313 return Err(serde::de::Error::custom(format!(
314 "Depth encoding {:?} does not match {}",
315 encoding,
316 E::SHORT_TYPE_PATH
317 )));
318 }
319 Ok(Self {
320 format: wire.format,
321 buffer_handle: wire.handle,
322 encoding: PhantomData,
323 })
324 }
325}
326
327impl<A, E> Serialize for CuDepthMap<A, E>
328where
329 E: CuDepthEncoding,
330 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
331 CuHandle<A>: Serialize,
332{
333 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
334 where
335 S: Serializer,
336 {
337 use serde::ser::SerializeStruct;
338 let mut state = serializer.serialize_struct(E::SHORT_TYPE_PATH, 3)?;
339 state.serialize_field("format", &self.format)?;
340 state.serialize_field("encoding", &E::DESCRIPTOR)?;
341 state.serialize_field("handle", &self.buffer_handle)?;
342 state.end()
343 }
344}
345
346impl<A, E> CuDepthMap<A, E>
347where
348 E: CuDepthEncoding,
349 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
350{
351 pub const fn encoding_descriptor() -> CuDepthEncodingDescriptor {
352 E::DESCRIPTOR
353 }
354
355 pub fn new_encoded(format: CuDepthMapFormat, buffer_handle: CuHandle<A>) -> Self {
356 assert!(
357 E::DESCRIPTOR.units_per_meter.is_finite() && E::DESCRIPTOR.units_per_meter > 0.0,
358 "Depth encoding must have a finite positive resolution."
359 );
360 assert!(
361 format.is_valid(),
362 "Depth-map stride must be at least its width."
363 );
364 assert!(
365 format.required_elements() <= buffer_handle.with_inner(|inner| inner.len()),
366 "Buffer size must at least match the depth-map format."
367 );
368 Self {
369 format,
370 buffer_handle,
371 encoding: PhantomData,
372 }
373 }
374
375 pub fn get_raw(&self, x: u32, y: u32) -> Option<E::Sample> {
377 if x >= self.format.width || y >= self.format.height {
378 return None;
379 }
380 let index = y as usize * self.format.stride as usize + x as usize;
381 self.buffer_handle
382 .with_inner(|inner| inner.as_ref().get(index).copied())
383 }
384
385 pub fn decode_sample(sample: E::Sample) -> Option<Length> {
387 E::decode(sample)
388 }
389
390 pub fn get(&self, x: u32, y: u32) -> Option<Length> {
394 Self::decode_sample(self.get_raw(x, y)?)
395 }
396
397 pub fn get_meters(&self, x: u32, y: u32) -> Option<f32> {
399 self.get(x, y).map(|depth| depth.get::<meter>())
400 }
401
402 pub fn convert_into<B, F>(&self, target: &mut CuDepthMap<B, F>) -> CuResult<()>
407 where
408 F: CuDepthEncoding,
409 B: ArrayLike<Element = F::Sample> + Send + Sync + 'static,
410 {
411 if self.format.width != target.format.width || self.format.height != target.format.height {
412 return Err(CuError::from(format!(
413 "Depth-map dimensions differ: source is {}x{}, target is {}x{}",
414 self.format.width, self.format.height, target.format.width, target.format.height
415 )));
416 }
417 if self.buffer_handle.storage_id() == target.buffer_handle.storage_id() {
418 return Err(CuError::from(
419 "Depth-map conversion requires distinct source and target storage",
420 ));
421 }
422
423 let convert = |source: &[E::Sample],
424 source_format: CuDepthMapFormat,
425 destination: &mut [F::Sample],
426 destination_format: CuDepthMapFormat| {
427 for y in 0..source_format.height as usize {
428 let source_row = y * source_format.stride as usize;
429 let destination_row = y * destination_format.stride as usize;
430 for x in 0..source_format.width as usize {
431 destination[destination_row + x] =
432 F::encode_sample(E::decode(source[source_row + x]));
433 }
434 }
435 };
436
437 if self.buffer_handle.storage_id() < target.buffer_handle.storage_id() {
438 self.with_samples(|source, source_format| {
439 target.with_samples_mut(|destination, destination_format| {
440 convert(source, source_format, destination, destination_format);
441 });
442 });
443 } else {
444 target.with_samples_mut(|destination, destination_format| {
445 self.with_samples(|source, source_format| {
446 convert(source, source_format, destination, destination_format);
447 });
448 });
449 }
450 Ok(())
451 }
452
453 pub fn with_samples<R>(&self, f: impl FnOnce(&[E::Sample], CuDepthMapFormat) -> R) -> R {
458 let format = self.format;
459 self.buffer_handle
460 .with_inner(|inner| f(&inner[..format.required_elements()], format))
461 }
462
463 pub fn with_samples_mut<R>(
468 &mut self,
469 f: impl FnOnce(&mut [E::Sample], CuDepthMapFormat) -> R,
470 ) -> R {
471 let format = self.format;
472 self.buffer_handle
473 .with_inner_mut(|inner| f(&mut inner[..format.required_elements()], format))
474 }
475
476 pub fn payload_should_log(&self) -> bool {
477 self.buffer_handle.payload_should_log()
478 }
479
480 pub fn apply_handle_content_policy(&self, mode: cu29::pool::HandleContent) {
481 self.buffer_handle.apply_handle_content_policy(mode);
482 }
483
484 pub fn mark_touched(&self) {
485 self.buffer_handle.mark_touched();
486 }
487}
488
489impl<A> CuDepthMap<A, CuDepthLength>
490where
491 A: ArrayLike<Element = Length> + Send + Sync + 'static,
492{
493 pub fn new(format: CuDepthMapFormat, buffer_handle: CuHandle<A>) -> Self {
494 Self::new_encoded(format, buffer_handle)
495 }
496}
497
498impl<A, T, S> CuDepthMap<A, CuDepthInteger<T, S>>
499where
500 T: CuDepthIntegerSample,
501 S: CuDepthScale,
502 A: ArrayLike<Element = T> + Send + Sync + 'static,
503{
504 pub fn from_integer(format: CuDepthMapFormat, buffer_handle: CuHandle<A>) -> Self {
505 Self::new_encoded(format, buffer_handle)
506 }
507}
508
509impl<A, E> cu29::pool::HandleContentAware for CuDepthMap<A, E>
510where
511 E: CuDepthEncoding,
512 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
513{
514}
515
516macro_rules! depth_map_serialized_schema {
517 ($sample_schema:literal, $invalid_schema:literal) => {
518 concat!(
519 r#"{"$schema":"https://json-schema.org/draft-07/schema#","type":"object","properties":{
520"format":{"type":"object","properties":{"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"stride":{"type":"integer","minimum":0}},"required":["width","height","stride"],"additionalProperties":false},
521"encoding":{"type":"object","properties":{"units_per_meter":{"type":"number"},"invalid":"#,
522 $invalid_schema,
523 r#"},"required":["units_per_meter","invalid"],"additionalProperties":false},
524"handle":{"type":"array","items":"#,
525 $sample_schema,
526 r#"}},"required":["format","encoding","handle"],"additionalProperties":false}"#
527 )
528 };
529}
530
531const LENGTH_DEPTH_MAP_SERIALIZED_SCHEMA: &str =
532 depth_map_serialized_schema!(r#"{"type":"number"}"#, r#"{"type":"null"}"#);
533
534const INTEGER_DEPTH_MAP_SERIALIZED_SCHEMA: &str = depth_map_serialized_schema!(
535 r#"{"type":"integer","minimum":0}"#,
536 r#"{"type":"object","properties":{"unsigned":{"type":"integer","const":0}},"required":["unsigned"],"additionalProperties":false}"#
537);
538
539const GENERIC_DEPTH_MAP_SERIALIZED_SCHEMA: &str = depth_map_serialized_schema!(r#"{}"#, r#"{}"#);
540
541impl<A, E> SerializedPayloadSchema for CuDepthMap<A, E>
542where
543 E: CuDepthEncoding,
544 A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
545{
546 fn serialized_payload_schema() -> &'static str {
547 E::SERIALIZED_MAP_SCHEMA
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 type U16MillimeterDepth = CuDepthMap<Vec<u16>, CuDepthInteger<u16, CuDepthMillimeter>>;
556
557 const FORMAT: CuDepthMapFormat = CuDepthMapFormat {
558 width: 3,
559 height: 2,
560 stride: 4,
561 };
562
563 #[test]
564 fn padded_depth_map_indexes_distances_in_meters() {
565 let depth = CuDepthMap::new(
566 FORMAT,
567 CuHandle::new_detached(
568 [1.0, 1.5, f32::NAN, 99.0, 2.0, 2.5, 3.0, 99.0]
569 .map(Length::new::<meter>)
570 .to_vec(),
571 ),
572 );
573
574 assert_eq!(depth.get(0, 0), Some(Length::new::<meter>(1.0)));
575 assert_eq!(depth.get_meters(1, 1), Some(2.5));
576 assert!(depth.get_meters(2, 0).expect("in bounds").is_nan());
577 assert_eq!(depth.get(3, 0), None);
578 assert_eq!(depth.get(0, 2), None);
579 }
580
581 #[test]
582 fn sample_slice_covers_the_declared_layout_under_one_access() {
583 let depth = CuDepthMap::new(
584 FORMAT,
585 CuHandle::new_detached(
586 [1.0, 1.5, 2.0, 99.0, 2.5, 3.0, 3.5, 99.0, 100.0]
587 .map(Length::new::<meter>)
588 .to_vec(),
589 ),
590 );
591
592 depth.with_samples(|samples, format| {
593 assert_eq!(format, FORMAT);
594 assert_eq!(samples.len(), FORMAT.required_elements());
595 assert_eq!(samples[format.stride as usize], Length::new::<meter>(2.5));
596 assert_eq!(samples[3], Length::new::<meter>(99.0));
597 });
598 }
599
600 #[test]
601 fn sample_slice_can_be_mutated_under_one_access() {
602 let mut depth = CuDepthMap::new(
603 FORMAT,
604 CuHandle::new_detached(vec![Length::new::<meter>(0.0); FORMAT.required_elements()]),
605 );
606
607 depth.with_samples_mut(|samples, format| {
608 let index = format.stride as usize + 1;
609 samples[index] = Length::new::<meter>(4.25);
610 });
611
612 assert_eq!(depth.get_meters(1, 1), Some(4.25));
613 }
614
615 #[test]
616 fn compact_millimeter_depth_decodes_without_changing_raw_storage() {
617 let depth = U16MillimeterDepth::from_integer(
618 FORMAT,
619 CuHandle::new_detached(vec![1_000, 1_500, 0, 99, 2_000, 2_500, 3_000, 99]),
620 );
621
622 assert_eq!(depth.get_raw(1, 0), Some(1_500));
623 assert_eq!(depth.get_meters(1, 0), Some(1.5));
624 assert_eq!(depth.get_raw(2, 0), Some(0));
625 assert_eq!(depth.get(2, 0), None);
626 depth.with_samples(|samples, format| {
627 assert_eq!(format, FORMAT);
628 assert_eq!(samples, &[1_000, 1_500, 0, 99, 2_000, 2_500, 3_000, 99]);
629 });
630 }
631
632 #[test]
633 fn depth_maps_convert_between_encodings_without_touching_padding() {
634 let source = CuDepthMap::new(
635 FORMAT,
636 CuHandle::new_detached(
637 [1.234, f32::NAN, 2.0, -999.0, 3.5, 4.0, 5.0, -999.0]
638 .map(Length::new::<meter>)
639 .to_vec(),
640 ),
641 );
642 let target_format = CuDepthMapFormat {
643 width: FORMAT.width,
644 height: FORMAT.height,
645 stride: 5,
646 };
647 let mut compact = U16MillimeterDepth::from_integer(
648 target_format,
649 CuHandle::new_detached(vec![777u16; target_format.required_elements()]),
650 );
651
652 source.convert_into(&mut compact).expect("convert to u16");
653 compact.with_samples(|samples, _| {
654 assert_eq!(
655 samples,
656 &[1_234, 0, 2_000, 777, 777, 3_500, 4_000, 5_000, 777, 777]
657 );
658 });
659
660 let mut typed = CuDepthMap::new(
661 FORMAT,
662 CuHandle::new_detached(vec![Length::new::<meter>(-1.0); FORMAT.required_elements()]),
663 );
664 compact.convert_into(&mut typed).expect("convert to Length");
665 assert_eq!(typed.get_meters(0, 0), Some(1.234));
666 assert!(
667 typed
668 .get_raw(1, 0)
669 .expect("in bounds")
670 .get::<meter>()
671 .is_nan()
672 );
673 }
674
675 #[test]
676 fn depth_encodings_have_distinct_type_paths() {
677 assert_eq!(
678 <CuDepthMap<Vec<Length>> as TypePath>::type_path(),
679 "cu_sensor_payloads::CuDepthMap"
680 );
681 assert_eq!(
682 <U16MillimeterDepth as TypePath>::type_path(),
683 "cu_sensor_payloads::CuDepthMapInteger"
684 );
685 }
686
687 #[test]
688 fn integer_depth_supports_all_widths_and_arbitrary_length_scales() {
689 fn assert_integer_sample<T: CuDepthIntegerSample>() {}
690 assert_integer_sample::<u8>();
691 assert_integer_sample::<u16>();
692 assert_integer_sample::<u32>();
693 assert_integer_sample::<u64>();
694
695 type U16DecimeterDepth = CuDepthMap<Vec<u16>, CuDepthInteger<u16, CuDepthDecimeter>>;
696 let format = CuDepthMapFormat {
697 width: 1,
698 height: 1,
699 stride: 1,
700 };
701 let depth = U16DecimeterDepth::from_integer(format, CuHandle::new_detached(vec![2_000]));
702
703 assert_eq!(CuDepthDecimeter::RESOLUTION, Length::new::<meter>(0.1));
704 assert_eq!(
705 U16DecimeterDepth::encoding_descriptor().units_per_meter,
706 10.0
707 );
708 assert_eq!(depth.get_meters(0, 0), Some(200.0));
709 }
710
711 #[test]
712 fn format_rejects_short_stride() {
713 assert!(
714 !CuDepthMapFormat {
715 width: 4,
716 height: 2,
717 stride: 3,
718 }
719 .is_valid()
720 );
721 }
722
723 #[test]
724 fn serde_wire_uses_handle_field() {
725 let depth = CuDepthMap::new(
726 FORMAT,
727 CuHandle::new_detached(
728 [1.0, 1.5, 2.0, 0.0, 2.5, 3.0, 3.5, 0.0]
729 .map(Length::new::<meter>)
730 .to_vec(),
731 ),
732 );
733 let value = serde_json::to_value(depth).expect("serialize depth");
734 assert_eq!(value["format"]["stride"], 4);
735 assert_eq!(value["encoding"]["units_per_meter"], 1.0);
736 assert!(value["encoding"]["invalid"].is_null());
737 assert!(value.get("handle").is_some());
738 assert!(value.get("buffer_handle").is_none());
739 assert!(value.get("seq").is_none());
740 }
741
742 #[test]
743 fn compact_serde_wire_describes_units_and_invalid_value() {
744 let depth = U16MillimeterDepth::from_integer(
745 FORMAT,
746 CuHandle::new_detached(vec![0u16; FORMAT.required_elements()]),
747 );
748 let value = serde_json::to_value(depth).expect("serialize compact depth");
749
750 assert_eq!(value["encoding"]["units_per_meter"], 1_000.0);
751 assert_eq!(value["encoding"]["invalid"]["unsigned"], 0);
752 }
753
754 #[test]
755 fn compact_serialized_schema_matches_depth_wire_fields() {
756 let schema = serde_json::from_str::<serde_json::Value>(
757 U16MillimeterDepth::serialized_payload_schema(),
758 )
759 .expect("parse compact depth schema");
760
761 assert_eq!(schema["properties"]["handle"]["items"]["type"], "integer");
762 assert_eq!(
763 schema["properties"]["encoding"]["properties"]["invalid"]["properties"]["unsigned"]["const"],
764 0
765 );
766 assert_eq!(
767 schema["required"],
768 serde_json::json!(["format", "encoding", "handle"])
769 );
770 }
771}