Skip to main content

a2l_parse/
model.rs

1//! The A2L calibration data model: the parsed subset of the ASAP2 grammar
2//! the calibration stack consumes.
3//!
4//! Field names follow the task-domain vocabulary; the struct fields are
5//! the *required* positional grammar elements, with optional ASAP2
6//! keywords (e.g. `ECU_ADDRESS` on MEASUREMENT) captured where the model
7//! needs them and everything else skipped by the parser.
8use alloc::string::String;
9use alloc::vec::Vec;
10
11/// A parsed A2L database: the PROJECT name and its MODULEs.
12#[derive(Debug, Clone, PartialEq, Default)]
13pub struct A2lProject {
14    /// PROJECT name.
15    pub project: String,
16    /// All `/begin MODULE` blocks, in file order.
17    pub modules: Vec<A2lModule>,
18}
19
20impl A2lProject {
21    /// Parse an A2L (ASAP2) description. Keyword-driven and
22    /// block-structured: `/begin NAME … /end NAME`; unknown blocks and
23    /// unknown optional keywords are skipped, so real-world files with
24    /// vendor extensions parse as long as the core blocks are intact.
25    ///
26    /// # Errors
27    ///
28    /// [`A2lError`] — typed, with 1-based line numbers where applicable.
29    pub fn parse(input: &str) -> Result<Self, crate::A2lError> {
30        crate::parser::parse(input)
31    }
32
33    /// First module named `name`, in file order.
34    #[must_use]
35    pub fn module_by_name(&self, name: &str) -> Option<&A2lModule> {
36        self.modules.iter().find(|m| m.name == name)
37    }
38}
39
40/// A `/begin MODULE` block: one ECU's calibration description.
41#[derive(Debug, Clone, PartialEq, Default)]
42pub struct A2lModule {
43    /// MODULE name.
44    pub name: String,
45    /// CHARACTERISTIC blocks (calibration parameters: values, curves,
46    /// maps).
47    pub characteristics: Vec<Characteristic>,
48    /// MEASUREMENT blocks (ECU-internal quantities to measure).
49    pub measurements: Vec<Measurement>,
50    /// RECORD_LAYOUT blocks (memory deposit layouts).
51    pub record_layouts: Vec<RecordLayout>,
52    /// COMPU_METHOD blocks (raw ↔ physical conversion definitions).
53    pub compu_methods: Vec<CompuMethod>,
54}
55
56impl A2lModule {
57    /// The CHARACTERISTIC named `name`.
58    #[must_use]
59    pub fn characteristic_by_name(&self, name: &str) -> Option<&Characteristic> {
60        self.characteristics.iter().find(|c| c.name == name)
61    }
62
63    /// The MEASUREMENT named `name`.
64    #[must_use]
65    pub fn measurement_by_name(&self, name: &str) -> Option<&Measurement> {
66        self.measurements.iter().find(|m| m.name == name)
67    }
68
69    /// The RECORD_LAYOUT named `name`.
70    #[must_use]
71    pub fn record_layout_by_name(&self, name: &str) -> Option<&RecordLayout> {
72        self.record_layouts.iter().find(|r| r.name == name)
73    }
74
75    /// The COMPU_METHOD named `name`.
76    #[must_use]
77    pub fn compu_method_by_name(&self, name: &str) -> Option<&CompuMethod> {
78        self.compu_methods.iter().find(|c| c.name == name)
79    }
80}
81
82/// A CHARACTERISTIC: one adjustable calibration parameter.
83#[derive(Debug, Clone, PartialEq)]
84pub struct Characteristic {
85    /// CHARACTERISTIC name.
86    pub name: String,
87    /// Calibration value type (VALUE, CURVE, MAP, VAL_BLK, ASCII).
88    pub r#type: CharType,
89    /// ECU address of the parameter's deposit.
90    pub address: u32,
91    /// RECORD_LAYOUT reference (the deposit's memory layout).
92    pub deposit: String,
93    /// Maximum difference between two adjustment steps.
94    pub max_diff: f64,
95    /// COMPU_METHOD reference (raw ↔ physical conversion).
96    pub conversion: String,
97    /// Lower limit of the (physical) value range.
98    pub lower_limit: f64,
99    /// Upper limit of the (physical) value range.
100    pub upper_limit: f64,
101}
102
103/// The CHARACTERISTIC type keywords of the ASAP2 grammar.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105#[allow(clippy::exhaustive_enums)]
106pub enum CharType {
107    /// `VALUE` — a single scalar.
108    Value,
109    /// `CURVE` — one-dimensional table.
110    Curve,
111    /// `MAP` — two-dimensional table.
112    Map,
113    /// `VAL_BLK` — multi-dimensional value block without axes.
114    ValBlk,
115    /// `ASCII` — a string.
116    Ascii,
117}
118
119impl CharType {
120    /// Parse a CHARACTERISTIC type keyword.
121    #[must_use]
122    pub fn from_keyword(keyword: &str) -> Option<Self> {
123        Some(match keyword {
124            "VALUE" => Self::Value,
125            "CURVE" => Self::Curve,
126            "MAP" => Self::Map,
127            "VAL_BLK" => Self::ValBlk,
128            "ASCII" => Self::Ascii,
129            _ => return None,
130        })
131    }
132
133    /// The ASAP2 keyword.
134    #[must_use]
135    pub const fn keyword(self) -> &'static str {
136        match self {
137            Self::Value => "VALUE",
138            Self::Curve => "CURVE",
139            Self::Map => "MAP",
140            Self::ValBlk => "VAL_BLK",
141            Self::Ascii => "ASCII",
142        }
143    }
144}
145
146/// A MEASUREMENT: one ECU-internal quantity available for measurement.
147#[derive(Debug, Clone, PartialEq)]
148pub struct Measurement {
149    /// MEASUREMENT name.
150    pub name: String,
151    /// ASAP2 datatype of the raw value in ECU memory.
152    pub datatype: DataType,
153    /// ECU address (`ECU_ADDRESS` keyword; `0` when absent — the value
154    /// then lives only in the debugger/trace mapping).
155    pub address: u32,
156    /// COMPU_METHOD reference (raw ↔ physical conversion).
157    pub conversion: String,
158}
159
160/// The ASAP2 `DATATYPE` keywords — the raw memory representation.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162#[allow(clippy::exhaustive_enums)]
163pub enum DataType {
164    /// `UBYTE` — unsigned 8-bit.
165    Ubyte,
166    /// `SBYTE` — signed 8-bit.
167    Sbyte,
168    /// `UWORD` — unsigned 16-bit.
169    Uword,
170    /// `SWORD` — signed 16-bit.
171    Sword,
172    /// `ULONG` — unsigned 32-bit.
173    Ulong,
174    /// `SLONG` — signed 32-bit.
175    Slong,
176    /// `A_UINT64` — unsigned 64-bit.
177    Uint64,
178    /// `A_INT64` — signed 64-bit.
179    Int64,
180    /// `FLOAT32_IEEE` — IEEE-754 single precision.
181    Float32,
182    /// `FLOAT64_IEEE` — IEEE-754 double precision.
183    Float64,
184}
185
186impl DataType {
187    /// Parse an ASAP2 `DATATYPE` keyword.
188    #[must_use]
189    pub fn from_keyword(keyword: &str) -> Option<Self> {
190        Some(match keyword {
191            "UBYTE" => Self::Ubyte,
192            "SBYTE" => Self::Sbyte,
193            "UWORD" => Self::Uword,
194            "SWORD" => Self::Sword,
195            "ULONG" => Self::Ulong,
196            "SLONG" => Self::Slong,
197            "A_UINT64" => Self::Uint64,
198            "A_INT64" => Self::Int64,
199            "FLOAT32_IEEE" => Self::Float32,
200            "FLOAT64_IEEE" => Self::Float64,
201            _ => return None,
202        })
203    }
204
205    /// The ASAP2 keyword.
206    #[must_use]
207    pub const fn keyword(self) -> &'static str {
208        match self {
209            Self::Ubyte => "UBYTE",
210            Self::Sbyte => "SBYTE",
211            Self::Uword => "UWORD",
212            Self::Sword => "SWORD",
213            Self::Ulong => "ULONG",
214            Self::Slong => "SLONG",
215            Self::Uint64 => "A_UINT64",
216            Self::Int64 => "A_INT64",
217            Self::Float32 => "FLOAT32_IEEE",
218            Self::Float64 => "FLOAT64_IEEE",
219        }
220    }
221
222    /// Size of the raw value in ECU memory, in bytes.
223    #[must_use]
224    pub const fn size_bytes(self) -> usize {
225        match self {
226            Self::Ubyte | Self::Sbyte => 1,
227            Self::Uword | Self::Sword => 2,
228            Self::Ulong | Self::Slong | Self::Float32 => 4,
229            Self::Uint64 | Self::Int64 | Self::Float64 => 8,
230        }
231    }
232}
233
234/// A RECORD_LAYOUT: how a parameter's values deposit into ECU memory.
235#[derive(Debug, Clone, PartialEq)]
236pub struct RecordLayout {
237    /// RECORD_LAYOUT name.
238    pub name: String,
239    /// The `FNC_VALUES` description when present.
240    pub fnc_values: Option<FncValues>,
241}
242
243/// The RECORD_LAYOUT `FNC_VALUES` keyword: where and how the value array
244/// deposits.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub struct FncValues {
247    /// `POSITION` — the array's start offset in the deposit record.
248    pub position: u8,
249    /// `DATATYPE` — the element datatype.
250    pub datatype: DataType,
251}
252
253/// A COMPU_METHOD: the raw ↔ physical conversion for one quantity.
254#[derive(Debug, Clone, PartialEq)]
255pub struct CompuMethod {
256    /// COMPU_METHOD name.
257    pub name: String,
258    /// Conversion type (LINEAR, RAT_FUNC, TABLE, IDENTITY).
259    pub conversion_type: ConversionType,
260    /// Coefficients carried by the method (`COEFFS`/`COEFFS_LINEAR`);
261    /// [`Coeffs::None`] for TABLE/IDENTITY and for methods that reference
262    /// a table via `COMPU_TAB_REF` instead.
263    pub coeffs: Coeffs,
264    /// The `COMPU_TAB_REF` table name when the conversion is tabular.
265    pub tab_ref: Option<String>,
266    /// Physical unit string (may be empty).
267    pub unit: String,
268}
269
270impl CompuMethod {
271    /// Reduce the method to a linear raw → physical function when the
272    /// conversion is linear-or-reducibly-rational:
273    ///
274    /// - `LINEAR` (`COEFFS_LINEAR a b`): `f(x) = a·x + b` directly.
275    /// - `RAT_FUNC` with `d = e = 0` and `f ≠ 0`:
276    ///   `(a·x² + b·x + c) / f` reduces to linear with slope
277    ///   `a / f`, intercept `c / f` — the form nearly every real ECU uses.
278    /// - `IDENTITY`: `f(x) = x`.
279    /// - `TABLE` (and non-linear `RAT_FUNC`): not reducible → `None`.
280    #[must_use]
281    pub fn to_linear(&self) -> Option<LinearFn> {
282        match self.conversion_type {
283            ConversionType::Identity => Some(LinearFn {
284                slope: 1.0,
285                intercept: 0.0,
286            }),
287            ConversionType::Linear => match self.coeffs {
288                Coeffs::Linear([a, b]) => Some(LinearFn {
289                    slope: a,
290                    intercept: b,
291                }),
292                _ => None,
293            },
294            ConversionType::RatFunc => match self.coeffs {
295                // Reducibly rational: constant denominator (d = e = 0,
296                // f ≠ 0) and a *linear* numerator (a = 0), i.e.
297                // (b·x + c) / f. A non-zero x² term is genuinely
298                // non-linear and stays `None`.
299                Coeffs::RatFunc([a, b, c, d, e, f])
300                    if a == 0.0 && d == 0.0 && e == 0.0 && f != 0.0 =>
301                {
302                    Some(LinearFn {
303                        slope: b / f,
304                        intercept: c / f,
305                    })
306                }
307                _ => None,
308            },
309            ConversionType::Table => None,
310        }
311    }
312}
313
314/// A linear raw → physical function `f(x) = slope·x + intercept`.
315#[derive(Debug, Clone, Copy, PartialEq)]
316pub struct LinearFn {
317    /// Multiplicative term `a`.
318    pub slope: f64,
319    /// Additive term `b`.
320    pub intercept: f64,
321}
322
323impl LinearFn {
324    /// Apply the conversion to a raw value.
325    #[must_use]
326    pub fn apply(&self, raw: f64) -> f64 {
327        self.slope * raw + self.intercept
328    }
329}
330
331/// The COMPU_METHOD coefficient forms.
332#[derive(Debug, Clone, Copy, PartialEq, Default)]
333#[allow(clippy::exhaustive_enums)]
334pub enum Coeffs {
335    /// `COEFFS a b c d e f` — rational conversion
336    /// `f(x) = (a·x² + b·x + c) / (d·x² + e·x + f)`.
337    RatFunc([f64; 6]),
338    /// `COEFFS_LINEAR a b` — linear conversion `f(x) = a·x + b`.
339    Linear([f64; 2]),
340    /// No coefficients present (TABLE via `COMPU_TAB_REF`, IDENTITY, or
341    /// an abbreviated method).
342    #[default]
343    None,
344}
345
346/// The COMPU_METHOD conversion-type keywords.
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
348#[allow(clippy::exhaustive_enums)]
349pub enum ConversionType {
350    /// `LINEAR` — `COEFFS_LINEAR a b`.
351    Linear,
352    /// `RAT_FUNC` — `COEFFS a b c d e f`.
353    RatFunc,
354    /// `TABLE` — conversion via a `COMPU_TAB_REF` table.
355    Table,
356    /// `IDENTITY` (also accepted spelled `IDENTICAL`) — raw = physical.
357    Identity,
358}
359
360impl ConversionType {
361    /// Parse a conversion-type keyword (`IDENTICAL` is accepted as an
362    /// alias of `IDENTITY`).
363    #[must_use]
364    pub fn from_keyword(keyword: &str) -> Option<Self> {
365        Some(match keyword {
366            "LINEAR" => Self::Linear,
367            "RAT_FUNC" => Self::RatFunc,
368            "TABLE" => Self::Table,
369            "IDENTITY" | "IDENTICAL" => Self::Identity,
370            _ => return None,
371        })
372    }
373
374    /// The canonical ASAP2 keyword.
375    #[must_use]
376    pub const fn keyword(self) -> &'static str {
377        match self {
378            Self::Linear => "LINEAR",
379            Self::RatFunc => "RAT_FUNC",
380            Self::Table => "TABLE",
381            Self::Identity => "IDENTITY",
382        }
383    }
384}