Skip to main content

c_its_parser/standards/
conversions.rs

1// Copyright (c) 2025 consider it GmbH
2
3//! Conversions between ETSI ASN.1 values and common (SI) units
4//!
5//! Take a look at the individual data types in [`crate::standards`] to discover available conversion methods and initialization functions.
6
7pub const MPS_TO_KMH_FACTOR: f32 = 3.6;
8
9#[cfg(feature = "_cdd_1_3_1_1")]
10use crate::standards::cdd_1_3_1_1;
11#[cfg(feature = "_cdd_2_2_1")]
12use crate::standards::cdd_2_2_1;
13#[cfg(feature = "cpm_1")]
14use crate::standards::cpm_1;
15#[cfg(feature = "_dsrc_2_2_1")]
16use crate::standards::dsrc_2_2_1;
17
18/// Create conversions for ETSI type `t` and some "unavailable" value
19macro_rules! latlon_to_deg {
20    ($t:ty, $unavailable:expr) => {
21        impl $t {
22            /// convert ETSI Latitude/ Longitude to degrees
23            #[must_use]
24            pub fn as_deg(&self) -> f64 {
25                f64::from(self.0) / 10_000_000.
26            }
27
28            /// convert ETSI Latitude/ Longitude to degrees or `None` if "unavailable"
29            #[must_use]
30            pub fn try_as_deg(&self) -> Option<f64> {
31                if self.is_unavailable() {
32                    None
33                } else {
34                    Some(self.as_deg())
35                }
36            }
37
38            /// convert ETSI Latitude/ Longitude to degrees
39            #[must_use]
40            pub fn from_deg(other: f64) -> Self {
41                Self((other * 10_000_000.) as i32)
42            }
43
44            /// create ETSI type with "unavailable" value
45            pub fn unavailable() -> Self {
46                Self($unavailable)
47            }
48
49            /// determines if the ETSI value is special "unavailable" value
50            pub fn is_unavailable(&self) -> bool {
51                self.0 == $unavailable
52            }
53        }
54    };
55}
56
57#[cfg(feature = "_cdd_1_3_1_1")]
58latlon_to_deg!(cdd_1_3_1_1::its_container::Longitude, 1_800_000_001);
59#[cfg(feature = "_cdd_1_3_1_1")]
60latlon_to_deg!(cdd_1_3_1_1::its_container::Latitude, 900_000_001);
61#[cfg(feature = "_cdd_2_2_1")]
62latlon_to_deg!(cdd_2_2_1::etsi_its_cdd::Longitude, 1_800_000_001);
63#[cfg(feature = "_cdd_2_2_1")]
64latlon_to_deg!(cdd_2_2_1::etsi_its_cdd::Latitude, 900_000_001);
65
66#[cfg(feature = "_cdd_1_3_1_1")]
67latlon_to_deg!(cdd_1_3_1_1::its_container::DeltaLongitude, 131_072);
68#[cfg(feature = "_cdd_1_3_1_1")]
69latlon_to_deg!(cdd_1_3_1_1::its_container::DeltaLatitude, 131_072);
70#[cfg(feature = "_cdd_2_2_1")]
71latlon_to_deg!(cdd_2_2_1::etsi_its_cdd::DeltaLongitude, 131_072);
72#[cfg(feature = "_cdd_2_2_1")]
73latlon_to_deg!(cdd_2_2_1::etsi_its_cdd::DeltaLatitude, 131_072);
74
75/// Create conversions for ETSI type `t` (which has underlying data type `tt`) with conversion factor `conv`
76#[cfg(feature = "cpm_1")]
77macro_rules! etsi_to_meters {
78    ($t:ty, $tt:ty, $conv:expr) => {
79        impl $t {
80            /// convert ETSI data to meters
81            #[must_use]
82            pub fn as_meters(&self) -> f32 {
83                self.0 as f32 / $conv
84            }
85
86            /// create ETSI data from meters
87            ///
88            /// # Errors
89            /// human-readable string when input value is out of bounds
90            pub fn from_meters(value: f32) -> Result<Self, alloc::string::String> {
91                use rasn::AsnType;
92
93                #[allow(clippy::cast_possible_truncation)]
94                let etsi_val = (value * $conv) as $tt;
95
96                if let Some(constraints) = Self::CONSTRAINTS.value() {
97                    if !constraints.constraint.in_bound(&etsi_val) {
98                        return Err(alloc::format!("Value out of bounds"));
99                    }
100                }
101
102                Ok(Self(etsi_val))
103            }
104        }
105
106        impl From<&$t> for f32 {
107            fn from(other: &$t) -> f32 {
108                other.as_meters()
109            }
110        }
111        impl From<$t> for f32 {
112            fn from(other: $t) -> f32 {
113                other.as_meters()
114            }
115        }
116
117        impl TryFrom<f32> for $t {
118            type Error = alloc::string::String;
119
120            fn try_from(value: f32) -> Result<Self, Self::Error> {
121                Self::from_meters(value)
122            }
123        }
124    };
125}
126
127/// Create conversions for ETSI type `t` (which has underlying data type `tt`) with conversion factor `conv` and some "unavailable" value
128#[cfg(any(
129    feature = "_cdd_1_3_1_1",
130    feature = "_cdd_2_2_1",
131    feature = "_dsrc_2_2_1"
132))]
133macro_rules! etsi_to_meters_unavailable {
134    ($t:ty, $tt:ty, $conv:expr, $unavailable:expr) => {
135        impl $t {
136            /// convert ETSI data to meters
137            #[must_use]
138            pub fn as_meters(&self) -> f32 {
139                self.0 as f32 / $conv
140            }
141
142            /// convert ETSI data to meters or `None` if "unavailable"
143            #[must_use]
144            pub fn try_as_meters(&self) -> Option<f32> {
145                if self.is_unavailable() {
146                    None
147                } else {
148                    Some(self.as_meters())
149                }
150            }
151
152            /// create ETSI data from meters
153            ///
154            /// # Errors
155            /// human-readable string when input value is out of bounds
156            pub fn from_meters(value: f32) -> Result<Self, alloc::string::String> {
157                use rasn::AsnType;
158
159                #[allow(clippy::cast_possible_truncation)]
160                let etsi_val = (value * $conv) as $tt;
161
162                if let Some(constraints) = Self::CONSTRAINTS.value() {
163                    if !constraints.constraint.in_bound(&etsi_val) {
164                        return Err(alloc::format!("Value out of bounds"));
165                    }
166                }
167
168                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
169                // So by checking for constraints first, we can use a strict equals condition.
170                if etsi_val == $unavailable {
171                    return Err(alloc::format!("Value out of bounds"));
172                }
173
174                Ok(Self(etsi_val))
175            }
176
177            /// create ETSI type with "unavailable" value
178            pub fn unavailable() -> Self {
179                Self($unavailable)
180            }
181
182            /// determines if the ETSI value is special "unavailable" value
183            pub fn is_unavailable(&self) -> bool {
184                self.0 == $unavailable
185            }
186        }
187
188        impl From<&$t> for f32 {
189            fn from(other: &$t) -> f32 {
190                other.as_meters()
191            }
192        }
193        impl From<$t> for f32 {
194            fn from(other: $t) -> f32 {
195                other.as_meters()
196            }
197        }
198
199        impl TryFrom<f32> for $t {
200            type Error = alloc::string::String;
201
202            fn try_from(value: f32) -> Result<Self, Self::Error> {
203                Self::from_meters(value)
204            }
205        }
206    };
207}
208
209#[cfg(feature = "_dsrc_2_2_1")]
210etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB09, i16, 100., -256);
211#[cfg(feature = "_dsrc_2_2_1")]
212etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB10, i16, 100., -512);
213#[cfg(feature = "_dsrc_2_2_1")]
214etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB11, i16, 100., -1024);
215#[cfg(feature = "_dsrc_2_2_1")]
216etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB12, i16, 100., -2048);
217#[cfg(feature = "_dsrc_2_2_1")]
218etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB13, i16, 100., -4096);
219#[cfg(feature = "_dsrc_2_2_1")]
220etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB14, i16, 100., -8192);
221#[cfg(feature = "_dsrc_2_2_1")]
222etsi_to_meters_unavailable!(dsrc_2_2_1::etsi_its_dsrc::OffsetB16, i16, 100., -32768);
223
224#[cfg(feature = "cpm_1")]
225etsi_to_meters!(cpm_1::cpm_pdu_descriptions::DistanceValue, i32, 100.);
226#[cfg(feature = "_cdd_2_2_1")]
227etsi_to_meters_unavailable!(cdd_2_2_1::etsi_its_cdd::ObjectDimensionValue, u16, 10., 256);
228#[cfg(feature = "cpm_1")]
229etsi_to_meters!(cpm_1::cpm_pdu_descriptions::ObjectDimensionValue, u16, 10.);
230#[cfg(feature = "cpm_1")]
231etsi_to_meters!(cpm_1::cpm_pdu_descriptions::Radius, u16, 10.);
232#[cfg(feature = "cpm_1")]
233etsi_to_meters!(cpm_1::cpm_pdu_descriptions::Range, u16, 10.);
234#[cfg(feature = "cpm_1")]
235etsi_to_meters!(cpm_1::cpm_pdu_descriptions::SemiRangeLength, u16, 10.);
236
237#[cfg(feature = "_cdd_1_3_1_1")]
238etsi_to_meters_unavailable!(cdd_1_3_1_1::its_container::VehicleWidth, u8, 10., 62);
239#[cfg(feature = "_cdd_2_2_1")]
240etsi_to_meters_unavailable!(cdd_2_2_1::etsi_its_cdd::VehicleWidth, u8, 10., 62);
241#[cfg(feature = "_cdd_1_3_1_1")]
242etsi_to_meters_unavailable!(
243    cdd_1_3_1_1::its_container::VehicleLengthValue,
244    u16,
245    10.,
246    1023
247);
248#[cfg(feature = "_cdd_2_2_1")]
249etsi_to_meters_unavailable!(cdd_2_2_1::etsi_its_cdd::VehicleLengthValue, u16, 10., 1023);
250
251/// Create conversions for ETSI type `t` (which has underlying data type `tt`) with conversion factor `conv` and some "unavailable" value
252macro_rules! etsi_to_mps {
253    ($t:ty, $tt:ty, $conv:expr, $unavailable:expr) => {
254        impl $t {
255            /// convert ETSI speed to m/s
256            #[must_use]
257            pub fn as_mps(&self) -> f32 {
258                f32::from(self.0) / $conv
259            }
260
261            /// convert ETSI speed to m/s or `None` if "unavailable"
262            #[must_use]
263            pub fn try_as_mps(&self) -> Option<f32> {
264                if self.is_unavailable() {
265                    None
266                } else {
267                    Some(self.as_mps())
268                }
269            }
270
271            /// create ETSI speed from m/s
272            ///
273            /// # Errors
274            /// human-readable string when input value is out of bounds
275            pub fn from_mps(value: f32) -> Result<Self, alloc::string::String> {
276                use rasn::AsnType;
277
278                #[allow(clippy::cast_possible_truncation)]
279                let etsi_val = (value * $conv) as $tt;
280
281                if let Some(constraints) = Self::CONSTRAINTS.value() {
282                    if !constraints.constraint.in_bound(&etsi_val) {
283                        return Err(alloc::format!("Value out of bounds"));
284                    }
285                }
286
287                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
288                // So by checking for constraints first, we can use a strict equals condition.
289                if etsi_val == $unavailable {
290                    return Err(alloc::format!("Value out of bounds"));
291                }
292
293                Ok(Self(etsi_val))
294            }
295
296            /// convert ETSI speed to km/h
297            #[must_use]
298            pub fn as_kmh(&self) -> f32 {
299                self.as_mps() * MPS_TO_KMH_FACTOR
300            }
301
302            /// convert ETSI speed to km/h or `None` if "unavailable"
303            #[must_use]
304            pub fn try_as_kmh(&self) -> Option<f32> {
305                if self.is_unavailable() {
306                    None
307                } else {
308                    Some(self.as_kmh())
309                }
310            }
311
312            /// create ETSI speed from km/h
313            ///
314            /// # Errors
315            /// human-readable string when input value is out of bounds
316            pub fn from_kmh(value: f32) -> Result<Self, alloc::string::String> {
317                Self::from_mps(value / MPS_TO_KMH_FACTOR)
318            }
319
320            /// create ETSI type with "unavailable" value
321            pub fn unavailable() -> Self {
322                Self($unavailable)
323            }
324
325            /// determines if the ETSI value is special "unavailable" value
326            pub fn is_unavailable(&self) -> bool {
327                self.0 == $unavailable
328            }
329        }
330
331        impl From<&$t> for f32 {
332            fn from(other: &$t) -> f32 {
333                other.as_mps()
334            }
335        }
336        impl From<$t> for f32 {
337            fn from(other: $t) -> f32 {
338                other.as_mps()
339            }
340        }
341
342        impl TryFrom<f32> for $t {
343            type Error = alloc::string::String;
344
345            fn try_from(value: f32) -> Result<Self, Self::Error> {
346                Self::from_mps(value)
347            }
348        }
349    };
350}
351
352#[cfg(feature = "cpm_1")]
353etsi_to_mps!(
354    cpm_1::cpm_pdu_descriptions::SpeedValueExtended,
355    i16,
356    100.,
357    16_383
358); // Unit: 0,01 m/s
359#[cfg(feature = "_cdd_1_3_1_1")]
360etsi_to_mps!(cdd_1_3_1_1::its_container::SpeedValue, u16, 100., 16_383); // Unit: 0,01 m/s
361#[cfg(feature = "_cdd_2_2_1")]
362etsi_to_mps!(cdd_2_2_1::etsi_its_cdd::SpeedValue, u16, 100., 16_383); // Unit: 0,01 m/s
363
364#[cfg(feature = "_dsrc_2_2_1")]
365etsi_to_mps!(dsrc_2_2_1::etsi_its_dsrc::Velocity, u16, 50., 8191); // Unit: 0.02 m/s
366
367/// Create conversions for ETSI type `t` (which has underlying data type `tt`) with conversion factor `conv` and some "unavailable" value
368#[cfg(any(feature = "_cdd_1_3_1_1", feature = "_cdd_2_2_1"))]
369macro_rules! etsi_to_mpss {
370    ($t:ty, $tt:ty, $conv:expr, $unavailable:expr) => {
371        impl $t {
372            /// convert ETSI acceleration to m/s/s
373            #[must_use]
374            pub fn as_mpss(&self) -> f32 {
375                f32::from(self.0) / $conv
376            }
377
378            /// convert ETSI acceleration to m/s/s or `None` if "unavailable"
379            #[must_use]
380            pub fn try_as_mpss(&self) -> Option<f32> {
381                if self.is_unavailable() {
382                    None
383                } else {
384                    Some(self.as_mpss())
385                }
386            }
387
388            /// create ETSI acceleration from m/s/s
389            ///
390            /// # Errors
391            /// human-readable string when input value is out of bounds
392            pub fn from_mpss(value: f32) -> Result<Self, alloc::string::String> {
393                use rasn::AsnType;
394
395                #[allow(clippy::cast_possible_truncation)]
396                let etsi_val = (value * $conv) as $tt;
397
398                if let Some(constraints) = Self::CONSTRAINTS.value() {
399                    if !constraints.constraint.in_bound(&etsi_val) {
400                        return Err(alloc::format!("Value out of bounds"));
401                    }
402                }
403
404                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
405                // So by checking for constraints first, we can use a strict equals condition.
406                if etsi_val == $unavailable {
407                    return Err(alloc::format!("Value out of bounds"));
408                }
409
410                Ok(Self(etsi_val))
411            }
412
413            /// create ETSI type with "unavailable" value
414            pub fn unavailable() -> Self {
415                Self($unavailable)
416            }
417
418            /// determines if the ETSI value is special "unavailable" value
419            pub fn is_unavailable(&self) -> bool {
420                self.0 == $unavailable
421            }
422        }
423
424        impl From<&$t> for f32 {
425            fn from(other: &$t) -> f32 {
426                other.as_mpss()
427            }
428        }
429        impl From<$t> for f32 {
430            fn from(other: $t) -> f32 {
431                other.as_mpss()
432            }
433        }
434
435        impl TryFrom<f32> for $t {
436            type Error = alloc::string::String;
437
438            fn try_from(value: f32) -> Result<Self, Self::Error> {
439                Self::from_mpss(value)
440            }
441        }
442    };
443}
444
445#[cfg(feature = "_cdd_1_3_1_1")]
446etsi_to_mpss!(
447    cdd_1_3_1_1::its_container::LongitudinalAccelerationValue,
448    i16,
449    10.,
450    161
451); // Unit: 0,1 m/s^2
452#[cfg(feature = "_cdd_2_2_1")]
453etsi_to_mpss!(
454    cdd_2_2_1::etsi_its_cdd::LongitudinalAccelerationValue,
455    i16,
456    10.,
457    161
458); // Unit: 0,1 m/s^2
459#[cfg(feature = "_cdd_1_3_1_1")]
460etsi_to_mpss!(
461    cdd_1_3_1_1::its_container::LateralAccelerationValue,
462    i16,
463    10.,
464    161
465); // Unit: 0,1 m/s^2
466#[cfg(feature = "_cdd_2_2_1")]
467etsi_to_mpss!(
468    cdd_2_2_1::etsi_its_cdd::LateralAccelerationValue,
469    i16,
470    10.,
471    161
472); // Unit: 0,1 m/s^2
473#[cfg(feature = "_cdd_1_3_1_1")]
474etsi_to_mpss!(
475    cdd_1_3_1_1::its_container::VerticalAccelerationValue,
476    i16,
477    10.,
478    161
479); // Unit: 0,1 m/s^2
480#[cfg(feature = "_cdd_2_2_1")]
481etsi_to_mpss!(
482    cdd_2_2_1::etsi_its_cdd::VerticalAccelerationValue,
483    i16,
484    10.,
485    161
486); // Unit: 0,1 m/s^2
487
488/// Check for unavailable data of ETSI type `t` (which has underlying data type `tt`)
489#[cfg(any(feature = "_cdd_1_3_1_1", feature = "_cdd_2_2_1"))]
490macro_rules! etsi_raw_unavailable {
491    ($t:ty, $tt:ty, $unavailable:expr) => {
492        impl $t {
493            /// convert ETSI acceleration to m/s/s or `None` if "unavailable"
494            #[must_use]
495            pub fn try_as_raw(&self) -> Option<$tt> {
496                if self.is_unavailable() {
497                    None
498                } else {
499                    Some(self.0)
500                }
501            }
502
503            /// create ETSI acceleration from raw value
504            ///
505            /// # Errors
506            /// human-readable string when input value is out of bounds
507            pub fn from_raw(value: $tt) -> Result<Self, alloc::string::String> {
508                use rasn::AsnType;
509
510                if let Some(constraints) = Self::CONSTRAINTS.value() {
511                    if !constraints.constraint.in_bound(&value) {
512                        return Err(alloc::format!("Value out of bounds"));
513                    }
514                }
515
516                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
517                // So by checking for constraints first, we can use a strict equals condition.
518                if value == $unavailable {
519                    return Err(alloc::format!("Value out of bounds"));
520                }
521
522                Ok(Self(value))
523            }
524
525            /// create ETSI type with "unavailable" value
526            pub fn unavailable() -> Self {
527                Self($unavailable)
528            }
529
530            /// determines if the ETSI value is special "unavailable" value
531            pub fn is_unavailable(&self) -> bool {
532                self.0 == $unavailable
533            }
534        }
535
536        impl From<&$t> for $tt {
537            fn from(other: &$t) -> $tt {
538                other.0
539            }
540        }
541        impl From<$t> for $tt {
542            fn from(other: $t) -> $tt {
543                other.0
544            }
545        }
546
547        impl TryFrom<$tt> for $t {
548            type Error = alloc::string::String;
549
550            fn try_from(value: $tt) -> Result<Self, Self::Error> {
551                Self::from_raw(value)
552            }
553        }
554    };
555}
556
557#[cfg(feature = "_cdd_1_3_1_1")]
558etsi_raw_unavailable!(cdd_1_3_1_1::its_container::CurvatureValue, i16, 1023);
559#[cfg(feature = "_cdd_2_2_1")]
560etsi_raw_unavailable!(cdd_2_2_1::etsi_its_cdd::CurvatureValue, i16, 1023);
561
562/// Create conversions for ETSI type `t` with conversion factor `conv` and some "unavailable" value
563#[cfg(any(
564    feature = "cpm_1",
565    feature = "_cdd_1_3_1_1",
566    feature = "_cdd_2_2_1",
567    feature = "_dsrc_2_2_1"
568))]
569macro_rules! angle_to_deg {
570    ($t:ty, $tt:ty, $conv:expr, $unavailable:expr) => {
571        impl $t {
572            /// convert ETSI WGS84AngleValue/ CartesianAngleValue to degrees
573            #[must_use]
574            pub fn as_deg(&self) -> f32 {
575                f32::from(self.0) / $conv
576            }
577
578            /// convert ETSI WGS84AngleValue/ CartesianAngleValue to degrees or `None` if "unavailable"
579            #[must_use]
580            pub fn try_as_deg(&self) -> Option<f32> {
581                if self.is_unavailable() {
582                    None
583                } else {
584                    Some(self.as_deg())
585                }
586            }
587
588            /// create ETSI WGS84AngleValue/ CartesianAngleValue from degrees
589            ///
590            /// # Errors
591            /// human-readable string when input value is out of bounds
592            pub fn from_deg(value: f32) -> Result<Self, alloc::string::String> {
593                use rasn::AsnType;
594
595                #[allow(clippy::cast_possible_truncation)]
596                let etsi_val = (value * $conv) as $tt;
597
598                if let Some(constraints) = Self::CONSTRAINTS.value() {
599                    if !constraints.constraint.in_bound(&etsi_val) {
600                        return Err(alloc::format!("Value out of bounds"));
601                    }
602                }
603
604                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
605                // So by checking for constraints first, we can use a strict equals condition.
606                if etsi_val == $unavailable {
607                    return Err(alloc::format!("Value out of bounds"));
608                }
609
610                Ok(Self(etsi_val))
611            }
612
613            /// create ETSI type with "unavailable" value
614            pub fn unavailable() -> Self {
615                Self($unavailable)
616            }
617
618            /// determines if the ETSI value is special "unavailable" value
619            pub fn is_unavailable(&self) -> bool {
620                self.0 == $unavailable
621            }
622        }
623
624        impl From<&$t> for f32 {
625            fn from(other: &$t) -> f32 {
626                other.as_deg()
627            }
628        }
629        impl From<$t> for f32 {
630            fn from(other: $t) -> f32 {
631                other.as_deg()
632            }
633        }
634
635        impl TryFrom<f32> for $t {
636            type Error = alloc::string::String;
637
638            fn try_from(value: f32) -> Result<Self, Self::Error> {
639                Self::from_deg(value)
640            }
641        }
642    };
643}
644
645#[cfg(feature = "_cdd_2_2_1")]
646angle_to_deg!(cdd_2_2_1::etsi_its_cdd::CartesianAngleValue, u16, 10., 3601); // Unit: 0,1 degrees
647#[cfg(feature = "cpm_1")]
648angle_to_deg!(
649    cpm_1::cpm_pdu_descriptions::CartesianAngleValue,
650    u16,
651    10.,
652    3601
653); // Unit: 0,1 degrees
654#[cfg(feature = "cpm_1")]
655angle_to_deg!(cpm_1::cpm_pdu_descriptions::WGS84AngleValue, u16, 10., 3601); // Unit: 0,1 degrees
656#[cfg(feature = "_cdd_2_2_1")]
657angle_to_deg!(cdd_2_2_1::etsi_its_cdd::Wgs84AngleValue, u16, 10., 3601); // Unit: 0,1 degrees
658#[cfg(feature = "_dsrc_2_2_1")]
659angle_to_deg!(dsrc_2_2_1::etsi_its_dsrc::Angle, u16, 80., 28800); // Unit: 0.0125 degrees
660#[cfg(feature = "_cdd_2_2_1")]
661angle_to_deg!(cdd_2_2_1::etsi_its_cdd::HeadingValue, u16, 10., 3601); // Unit: 0,1 degree
662#[cfg(feature = "_cdd_1_3_1_1")]
663angle_to_deg!(cdd_1_3_1_1::its_container::HeadingValue, u16, 10., 3601); // Unit: 0,1 degree
664
665#[cfg(feature = "_cdd_2_2_1")]
666angle_to_deg!(
667    cdd_2_2_1::etsi_its_cdd::SteeringWheelAngleValue,
668    i16,
669    (1. / 1.5),
670    512
671); // Unit: 1,5 degree
672#[cfg(feature = "_cdd_1_3_1_1")]
673angle_to_deg!(
674    cdd_1_3_1_1::its_container::SteeringWheelAngleValue,
675    i16,
676    (1. / 1.5),
677    512
678); // Unit: 1,5 degree
679
680/// Create conversions for ETSI type `t` with conversion factor `conv` and some "unavailable" value
681#[cfg(any(feature = "_cdd_2_2_1", feature = "_cdd_1_3_1_1"))]
682macro_rules! angle_to_degrate {
683    ($t:ty, $conv:expr, $unavailable:expr) => {
684        impl $t {
685            /// convert ETSI YawRateValue to degrees
686            #[must_use]
687            pub fn as_deg_rate(&self) -> f32 {
688                f32::from(self.0) / $conv
689            }
690
691            /// convert ETSI YawRateValue to degrees or `None` if "unavailable"
692            #[must_use]
693            pub fn try_as_deg(&self) -> Option<f32> {
694                if self.is_unavailable() {
695                    None
696                } else {
697                    Some(self.as_deg_rate())
698                }
699            }
700
701            /// create ETSI YawRateValue from degrees
702            ///
703            /// # Errors
704            /// human-readable string when input value is out of bounds
705            pub fn from_deg_rate(value: f32) -> Result<Self, alloc::string::String> {
706                use rasn::AsnType;
707
708                #[allow(clippy::cast_possible_truncation)]
709                let etsi_val = (value * $conv) as i16;
710
711                if let Some(constraints) = Self::CONSTRAINTS.value() {
712                    if !constraints.constraint.in_bound(&etsi_val) {
713                        return Err(alloc::format!("Value out of bounds"));
714                    }
715                }
716
717                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
718                // So by checking for constraints first, we can use a strict equals condition.
719                if etsi_val == $unavailable {
720                    return Err(alloc::format!("Value out of bounds"));
721                }
722
723                Ok(Self(etsi_val))
724            }
725
726            /// create ETSI type with "unavailable" value
727            pub fn unavailable() -> Self {
728                Self($unavailable)
729            }
730
731            /// determines if the ETSI value is special "unavailable" value
732            pub fn is_unavailable(&self) -> bool {
733                self.0 == $unavailable
734            }
735        }
736
737        impl From<&$t> for f32 {
738            fn from(other: &$t) -> f32 {
739                other.as_deg_rate()
740            }
741        }
742        impl From<$t> for f32 {
743            fn from(other: $t) -> f32 {
744                other.as_deg_rate()
745            }
746        }
747
748        impl TryFrom<f32> for $t {
749            type Error = alloc::string::String;
750
751            fn try_from(value: f32) -> Result<Self, Self::Error> {
752                Self::from_deg_rate(value)
753            }
754        }
755    };
756}
757
758#[cfg(feature = "_cdd_2_2_1")]
759angle_to_degrate!(cdd_2_2_1::etsi_its_cdd::YawRateValue, 100., 32767); // Unit: 0,01 degree per second
760#[cfg(feature = "_cdd_1_3_1_1")]
761angle_to_degrate!(cdd_1_3_1_1::its_container::YawRateValue, 100., 32767); // Unit: 0,01 degree per second
762
763// DeltaTime: unit 10 seconds, clamping to -121 for <-20 minutes and +120 for >+20 minutes, -122 for unavailable
764#[cfg(feature = "_dsrc_2_2_1")]
765impl dsrc_2_2_1::etsi_its_dsrc::DeltaTime {
766    /// convert ETSI DeltaTime to seconds
767    #[must_use]
768    pub fn as_sec(&self) -> i16 {
769        i16::from(self.0) * 10
770    }
771
772    /// convert ETSI DeltaTime to seconds or `None` if "unavailable"
773    #[must_use]
774    pub fn try_as_sec(&self) -> Option<i16> {
775        if self.is_unavailable() {
776            None
777        } else {
778            Some(self.as_sec())
779        }
780    }
781
782    /// create ETSI DeltaTime from seconds, clamping at min. and max. bounds
783    #[must_use]
784    pub fn from_sec(value: i16) -> Self {
785        #[allow(clippy::cast_possible_truncation)]
786        let etsi_val = (value / 10) as i8;
787
788        Self(etsi_val.clamp(-121, 120))
789    }
790
791    /// create ETSI type with "unavailable" value
792    pub fn unavailable() -> Self {
793        Self(-122)
794    }
795
796    /// determines if the ETSI value is special "unavailable" value
797    pub fn is_unavailable(&self) -> bool {
798        self.0 == -122
799    }
800}
801
802#[cfg(feature = "_dsrc_2_2_1")]
803impl From<&dsrc_2_2_1::etsi_its_dsrc::DeltaTime> for i16 {
804    fn from(other: &dsrc_2_2_1::etsi_its_dsrc::DeltaTime) -> i16 {
805        other.as_sec()
806    }
807}
808#[cfg(feature = "_dsrc_2_2_1")]
809impl From<dsrc_2_2_1::etsi_its_dsrc::DeltaTime> for i16 {
810    fn from(other: dsrc_2_2_1::etsi_its_dsrc::DeltaTime) -> i16 {
811        other.as_sec()
812    }
813}
814
815// DSecond: unit milliseconds, 65535 for unavailable
816#[cfg(feature = "_dsrc_2_2_1")]
817impl dsrc_2_2_1::etsi_its_dsrc::DSecond {
818    /// convert ETSI DeltaTime to milliseconds
819    #[must_use]
820    pub fn as_millis(&self) -> u16 {
821        self.0
822    }
823
824    /// convert ETSI DeltaTime to milliseconds or `None` if "unavailable"
825    #[must_use]
826    pub fn try_as_millis(&self) -> Option<u16> {
827        if self.is_unavailable() {
828            None
829        } else {
830            Some(self.as_millis())
831        }
832    }
833
834    /// create ETSI DSecond from milliseconds
835    ///
836    /// # Errors
837    /// human-readable string when input value is out of bounds
838    pub fn from_millis(value: u16) -> Result<Self, alloc::string::String> {
839        // ASN.1 bounds are bigger than allowed values (0..59999 for normal values, 60000..60999 for leap seconds)
840
841        if value > 60999 {
842            return Err(alloc::format!("Value out of bounds"));
843        }
844
845        Ok(Self(value))
846    }
847
848    /// create ETSI type with "unavailable" value
849    pub fn unavailable() -> Self {
850        Self(65535)
851    }
852
853    /// determines if the ETSI value is special "unavailable" value
854    pub fn is_unavailable(&self) -> bool {
855        self.0 == 65535
856    }
857}
858
859#[cfg(feature = "_dsrc_2_2_1")]
860impl From<&dsrc_2_2_1::etsi_its_dsrc::DSecond> for u16 {
861    fn from(other: &dsrc_2_2_1::etsi_its_dsrc::DSecond) -> u16 {
862        other.as_millis()
863    }
864}
865#[cfg(feature = "_dsrc_2_2_1")]
866impl From<dsrc_2_2_1::etsi_its_dsrc::DSecond> for u16 {
867    fn from(other: dsrc_2_2_1::etsi_its_dsrc::DSecond) -> u16 {
868        other.as_millis()
869    }
870}
871
872// TimeMark: unit 1/10 of a second, 36001 for unknown, 36000 for out-of-range
873#[cfg(feature = "_dsrc_2_2_1")]
874impl dsrc_2_2_1::etsi_its_dsrc::TimeMark {
875    const CONV_FACTOR: u32 = 100;
876    const UNKNOWN: u16 = 36001;
877    const OUT_OF_RANGE: u16 = 36000;
878
879    /// convert ETSI TimeMark to milliseconds
880    #[must_use]
881    pub fn as_millis(&self) -> u32 {
882        self.0 as u32 * Self::CONV_FACTOR
883    }
884
885    /// convert ETSI TimeMark to milliseconds or `None` if "unknown"
886    #[must_use]
887    pub fn try_as_millis(&self) -> Option<u32> {
888        if self.is_unknown() {
889            None
890        } else {
891            Some(self.as_millis())
892        }
893    }
894
895    /// create ETSI TimeMark from milliseconds
896    ///
897    /// # Errors
898    /// human-readable string when input value is out of bounds
899    pub fn from_millis(value: u32) -> Result<Self, alloc::string::String> {
900        #[allow(clippy::cast_possible_truncation)]
901        let etsi_val = (value / Self::CONV_FACTOR) as u16;
902
903        // ASN.1 bounds are bigger than allowed values (0..35990 for normal values, 35991..35999 for leap seconds)
904        if etsi_val > 35999 {
905            return Err(alloc::format!("Value out of bounds"));
906        }
907
908        Ok(Self(etsi_val))
909    }
910
911    /// create ETSI type with "unknown" value
912    pub fn unknown() -> Self {
913        Self(Self::UNKNOWN)
914    }
915
916    /// determines if the ETSI value is special "unknown" value
917    pub fn is_unknown(&self) -> bool {
918        self.0 == Self::UNKNOWN
919    }
920
921    /// create ETSI type with "out-of-range" value
922    pub fn out_of_range() -> Self {
923        Self(Self::OUT_OF_RANGE)
924    }
925
926    /// determines if the ETSI value is special "out-of-range" value
927    pub fn is_out_of_range(&self) -> bool {
928        self.0 == Self::OUT_OF_RANGE
929    }
930}
931
932#[cfg(feature = "_dsrc_2_2_1")]
933impl From<&dsrc_2_2_1::etsi_its_dsrc::TimeMark> for u32 {
934    fn from(other: &dsrc_2_2_1::etsi_its_dsrc::TimeMark) -> u32 {
935        other.as_millis()
936    }
937}
938#[cfg(feature = "_dsrc_2_2_1")]
939impl From<dsrc_2_2_1::etsi_its_dsrc::TimeMark> for u32 {
940    fn from(other: dsrc_2_2_1::etsi_its_dsrc::TimeMark) -> u32 {
941        other.as_millis()
942    }
943}
944
945// MinuteOfTheYear: unit minute, 527040 for invalid
946#[cfg(feature = "_dsrc_2_2_1")]
947impl dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear {
948    /// create ETSI MinuteOfTheYear with "invalid" value
949    pub fn invalid() -> Self {
950        Self(527040)
951    }
952
953    /// determines if the ETSI value is special "invalid" value
954    pub fn is_invalid(&self) -> bool {
955        self.0 == 527040
956    }
957}
958
959// MsgCount 0..127
960#[cfg(feature = "_dsrc_2_2_1")]
961impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::MsgCount {
962    pub fn increment(&self) -> Self {
963        Self((self.0 + 1) % 128)
964    }
965}
966#[cfg(feature = "_dsrc_2_2_1")]
967impl From<u8> for dsrc_2_2_1::etsi_its_dsrc::MsgCount {
968    fn from(value: u8) -> Self {
969        Self(value % 128)
970    }
971}
972
973// RequestID 0..255
974#[cfg(feature = "_dsrc_2_2_1")]
975impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::RequestID {
976    pub fn increment(&self) -> Self {
977        Self(self.0.wrapping_add(1))
978    }
979}
980#[cfg(feature = "_dsrc_2_2_1")]
981impl From<u8> for dsrc_2_2_1::etsi_its_dsrc::RequestID {
982    // for convenience and interface unification only
983    fn from(value: u8) -> Self {
984        Self(value)
985    }
986}
987
988// convenience getter
989#[cfg(feature = "_dsrc_2_2_1")]
990impl dsrc_2_2_1::etsi_its_dsrc::SpeedLimitList {
991    /// Extracts a certain speed limit in m/s, if existing
992    pub fn get_speed_limit_mps(
993        &self,
994        limit_type: dsrc_2_2_1::etsi_its_dsrc::SpeedLimitType,
995    ) -> Option<f32> {
996        self.0.iter().find_map(|item| {
997            if item.r_type == limit_type {
998                Some(item.speed.as_mps())
999            } else {
1000                None
1001            }
1002        })
1003    }
1004}