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 = "_dsrc_2_2_1")]
657angle_to_deg!(dsrc_2_2_1::etsi_its_dsrc::Angle, u16, 80., 28800); // Unit: 0.0125 degrees
658#[cfg(feature = "_cdd_2_2_1")]
659angle_to_deg!(cdd_2_2_1::etsi_its_cdd::HeadingValue, u16, 10., 3601); // Unit: 0,1 degree
660#[cfg(feature = "_cdd_1_3_1_1")]
661angle_to_deg!(cdd_1_3_1_1::its_container::HeadingValue, u16, 10., 3601); // Unit: 0,1 degree
662
663#[cfg(feature = "_cdd_2_2_1")]
664angle_to_deg!(
665    cdd_2_2_1::etsi_its_cdd::SteeringWheelAngleValue,
666    i16,
667    (1. / 1.5),
668    512
669); // Unit: 1,5 degree
670#[cfg(feature = "_cdd_1_3_1_1")]
671angle_to_deg!(
672    cdd_1_3_1_1::its_container::SteeringWheelAngleValue,
673    i16,
674    (1. / 1.5),
675    512
676); // Unit: 1,5 degree
677
678/// Create conversions for ETSI type `t` with conversion factor `conv` and some "unavailable" value
679#[cfg(any(feature = "_cdd_2_2_1", feature = "_cdd_1_3_1_1"))]
680macro_rules! angle_to_degrate {
681    ($t:ty, $conv:expr, $unavailable:expr) => {
682        impl $t {
683            /// convert ETSI YawRateValue to degrees
684            #[must_use]
685            pub fn as_deg_rate(&self) -> f32 {
686                f32::from(self.0) / $conv
687            }
688
689            /// convert ETSI YawRateValue to degrees or `None` if "unavailable"
690            #[must_use]
691            pub fn try_as_deg(&self) -> Option<f32> {
692                if self.is_unavailable() {
693                    None
694                } else {
695                    Some(self.as_deg_rate())
696                }
697            }
698
699            /// create ETSI YawRateValue from degrees
700            ///
701            /// # Errors
702            /// human-readable string when input value is out of bounds
703            pub fn from_deg_rate(value: f32) -> Result<Self, alloc::string::String> {
704                use rasn::AsnType;
705
706                #[allow(clippy::cast_possible_truncation)]
707                let etsi_val = (value * $conv) as i16;
708
709                if let Some(constraints) = Self::CONSTRAINTS.value() {
710                    if !constraints.constraint.in_bound(&etsi_val) {
711                        return Err(alloc::format!("Value out of bounds"));
712                    }
713                }
714
715                // Not all "unavailable" values are positive, but always at the very edge of the allowed value range.
716                // So by checking for constraints first, we can use a strict equals condition.
717                if etsi_val == $unavailable {
718                    return Err(alloc::format!("Value out of bounds"));
719                }
720
721                Ok(Self(etsi_val))
722            }
723
724            /// create ETSI type with "unavailable" value
725            pub fn unavailable() -> Self {
726                Self($unavailable)
727            }
728
729            /// determines if the ETSI value is special "unavailable" value
730            pub fn is_unavailable(&self) -> bool {
731                self.0 == $unavailable
732            }
733        }
734
735        impl From<&$t> for f32 {
736            fn from(other: &$t) -> f32 {
737                other.as_deg_rate()
738            }
739        }
740        impl From<$t> for f32 {
741            fn from(other: $t) -> f32 {
742                other.as_deg_rate()
743            }
744        }
745
746        impl TryFrom<f32> for $t {
747            type Error = alloc::string::String;
748
749            fn try_from(value: f32) -> Result<Self, Self::Error> {
750                Self::from_deg_rate(value)
751            }
752        }
753    };
754}
755
756#[cfg(feature = "_cdd_2_2_1")]
757angle_to_degrate!(cdd_2_2_1::etsi_its_cdd::YawRateValue, 100., 32767); // Unit: 0,01 degree per second
758#[cfg(feature = "_cdd_1_3_1_1")]
759angle_to_degrate!(cdd_1_3_1_1::its_container::YawRateValue, 100., 32767); // Unit: 0,01 degree per second
760
761// DeltaTime: unit 10 seconds, clamping to -121 for <-20 minutes and +120 for >+20 minutes, -122 for unavailable
762#[cfg(feature = "_dsrc_2_2_1")]
763impl dsrc_2_2_1::etsi_its_dsrc::DeltaTime {
764    /// convert ETSI DeltaTime to seconds
765    #[must_use]
766    pub fn as_sec(&self) -> i16 {
767        i16::from(self.0) * 10
768    }
769
770    /// convert ETSI DeltaTime to seconds or `None` if "unavailable"
771    #[must_use]
772    pub fn try_as_sec(&self) -> Option<i16> {
773        if self.is_unavailable() {
774            None
775        } else {
776            Some(self.as_sec())
777        }
778    }
779
780    /// create ETSI DeltaTime from seconds, clamping at min. and max. bounds
781    #[must_use]
782    pub fn from_sec(value: i16) -> Self {
783        #[allow(clippy::cast_possible_truncation)]
784        let etsi_val = (value / 10) as i8;
785
786        Self(etsi_val.clamp(-121, 120))
787    }
788
789    /// create ETSI type with "unavailable" value
790    pub fn unavailable() -> Self {
791        Self(-122)
792    }
793
794    /// determines if the ETSI value is special "unavailable" value
795    pub fn is_unavailable(&self) -> bool {
796        self.0 == -122
797    }
798}
799
800#[cfg(feature = "_dsrc_2_2_1")]
801impl From<&dsrc_2_2_1::etsi_its_dsrc::DeltaTime> for i16 {
802    fn from(other: &dsrc_2_2_1::etsi_its_dsrc::DeltaTime) -> i16 {
803        other.as_sec()
804    }
805}
806#[cfg(feature = "_dsrc_2_2_1")]
807impl From<dsrc_2_2_1::etsi_its_dsrc::DeltaTime> for i16 {
808    fn from(other: dsrc_2_2_1::etsi_its_dsrc::DeltaTime) -> i16 {
809        other.as_sec()
810    }
811}
812
813// DSecond: unit milliseconds, 65535 for unavailable
814#[cfg(feature = "_dsrc_2_2_1")]
815impl dsrc_2_2_1::etsi_its_dsrc::DSecond {
816    /// convert ETSI DeltaTime to milliseconds
817    #[must_use]
818    pub fn as_millis(&self) -> u16 {
819        self.0
820    }
821
822    /// convert ETSI DeltaTime to milliseconds or `None` if "unavailable"
823    #[must_use]
824    pub fn try_as_millis(&self) -> Option<u16> {
825        if self.is_unavailable() {
826            None
827        } else {
828            Some(self.as_millis())
829        }
830    }
831
832    /// create ETSI DSecond from milliseconds
833    ///
834    /// # Errors
835    /// human-readable string when input value is out of bounds
836    pub fn from_millis(value: u16) -> Result<Self, alloc::string::String> {
837        // ASN.1 bounds are bigger than allowed values (0..59999 for normal values, 60000..60999 for leap seconds)
838
839        if value > 60999 {
840            return Err(alloc::format!("Value out of bounds"));
841        }
842
843        Ok(Self(value))
844    }
845
846    /// create ETSI type with "unavailable" value
847    pub fn unavailable() -> Self {
848        Self(65535)
849    }
850
851    /// determines if the ETSI value is special "unavailable" value
852    pub fn is_unavailable(&self) -> bool {
853        self.0 == 65535
854    }
855}
856
857#[cfg(feature = "_dsrc_2_2_1")]
858impl From<&dsrc_2_2_1::etsi_its_dsrc::DSecond> for u16 {
859    fn from(other: &dsrc_2_2_1::etsi_its_dsrc::DSecond) -> u16 {
860        other.as_millis()
861    }
862}
863#[cfg(feature = "_dsrc_2_2_1")]
864impl From<dsrc_2_2_1::etsi_its_dsrc::DSecond> for u16 {
865    fn from(other: dsrc_2_2_1::etsi_its_dsrc::DSecond) -> u16 {
866        other.as_millis()
867    }
868}
869
870// MinuteOfTheYear: unit minute, 527040 for invalid
871#[cfg(feature = "_dsrc_2_2_1")]
872impl dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear {
873    /// create ETSI MinuteOfTheYear with "invalid" value
874    pub fn invalid() -> Self {
875        Self(527040)
876    }
877
878    /// determines if the ETSI value is special "invalid" value
879    pub fn is_invalid(&self) -> bool {
880        self.0 == 527040
881    }
882}
883
884// MsgCount 0..127
885#[cfg(feature = "_dsrc_2_2_1")]
886impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::MsgCount {
887    pub fn increment(&self) -> Self {
888        Self((self.0 + 1) % 128)
889    }
890}
891#[cfg(feature = "_dsrc_2_2_1")]
892impl From<u8> for dsrc_2_2_1::etsi_its_dsrc::MsgCount {
893    fn from(value: u8) -> Self {
894        Self(value % 128)
895    }
896}
897
898// RequestID 0..255
899#[cfg(feature = "_dsrc_2_2_1")]
900impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::RequestID {
901    pub fn increment(&self) -> Self {
902        Self(self.0.wrapping_add(1))
903    }
904}
905#[cfg(feature = "_dsrc_2_2_1")]
906impl From<u8> for dsrc_2_2_1::etsi_its_dsrc::RequestID {
907    // for convenience and interface unification only
908    fn from(value: u8) -> Self {
909        Self(value)
910    }
911}
912
913// convenience getter
914#[cfg(feature = "_dsrc_2_2_1")]
915impl dsrc_2_2_1::etsi_its_dsrc::SpeedLimitList {
916    /// Extracts a certain speed limit in m/s, if existing
917    pub fn get_speed_limit_mps(
918        &self,
919        limit_type: dsrc_2_2_1::etsi_its_dsrc::SpeedLimitType,
920    ) -> Option<f32> {
921        self.0.iter().find_map(|item| {
922            if item.r_type == limit_type {
923                Some(item.speed.as_mps())
924            } else {
925                None
926            }
927        })
928    }
929}