From f537c3f8e3e8d02981fdc911c2e1feb5b7502056 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Tue, 18 Aug 2026 14:04:45 +0200 Subject: [PATCH 01/14] fix the error type. was not from the correct file / scope --- .../src/value_information.rs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 1675d9d..13a4f65 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -28,13 +28,13 @@ macro_rules! unit { }; } -impl TryFrom<&[u8]> for ValueInformationBlock { +impl TryFrom<&[u8]> for ValueInformationBlock<'a> { type Error = DataInformationError; - fn try_from(data: &[u8]) -> Result { + fn try_from(data: &[u8]) -> Result { let mut vife = ArrayVec::::new(); let vif = - ValueInformationField::from(*data.first().ok_or(DataInformationError::DataTooShort)?); + ValueInformationField::from(*data.first().ok_or(ValueInformationError::DataTooShort)?); let mut plaintext_vife: Option> = None; #[cfg(not(feature = "plaintext-before-extension"))] @@ -44,7 +44,7 @@ impl TryFrom<&[u8]> for ValueInformationBlock { if !standard_plaintex_vib && vif.value_information_contains_ascii() { plaintext_vife = Some(extract_plaintext_vife( - data.get(1..).ok_or(DataInformationError::DataTooShort)?, + data.get(1..).ok_or(ValueInformationError::DataTooShort)?, )?); } @@ -56,7 +56,9 @@ impl TryFrom<&[u8]> for ValueInformationBlock { _ => 1, }; while offset < data.len() { - let vife_data = *data.get(offset).ok_or(DataInformationError::DataTooShort)?; + let vife_data = *data + .get(offset) + .ok_or(ValueInformationError::DataTooShort)?; let current_vife = ValueInformationFieldExtension { data: vife_data }; let has_extension = current_vife.has_extension(); vife.push(current_vife); @@ -65,13 +67,13 @@ impl TryFrom<&[u8]> for ValueInformationBlock { break; } if vife.len() > MAX_VIFE_RECORDS { - return Err(DataInformationError::InvalidValueInformation); + return Err(ValueInformationError::InvalidValueInformation); } } if standard_plaintex_vib && vif.value_information_contains_ascii() { plaintext_vife = Some(extract_plaintext_vife( data.get(offset..) - .ok_or(DataInformationError::DataTooShort)?, + .ok_or(ValueInformationError::DataTooShort)?, )?); } } @@ -84,12 +86,12 @@ impl TryFrom<&[u8]> for ValueInformationBlock { } } -fn extract_plaintext_vife(data: &[u8]) -> Result, DataInformationError> { - let ascii_length = *data.first().ok_or(DataInformationError::DataTooShort)? as usize; +fn extract_plaintext_vife(data: &[u8]) -> Result, ValueInformationError> { + let ascii_length = *data.first().ok_or(ValueInformationError::DataTooShort)? as usize; let mut ascii = ArrayVec::::new(); for item in data .get(1..=ascii_length) - .ok_or(DataInformationError::DataTooShort)? + .ok_or(ValueInformationError::DataTooShort)? { ascii.push(*item as char); } @@ -973,6 +975,7 @@ fn consume_orthhogonal_vife( #[non_exhaustive] pub enum ValueInformationError { InvalidValueInformation, + DataTooShort, } impl From for ValueInformationField { From 96d7193d5387f3ffc5e63fa418b09863b7192aad Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Wed, 19 Aug 2026 08:18:39 +0200 Subject: [PATCH 02/14] WIP --- .../src/data_record.rs | 2 +- .../src/value_information.rs | 162 ++++++++++++------ 2 files changed, 108 insertions(+), 56 deletions(-) diff --git a/crates/m-bus-application-layer/src/data_record.rs b/crates/m-bus-application-layer/src/data_record.rs index a450d86..789b628 100644 --- a/crates/m-bus-application-layer/src/data_record.rs +++ b/crates/m-bus-application-layer/src/data_record.rs @@ -9,7 +9,7 @@ use super::{ #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct RawDataRecordHeader<'a> { pub data_information_block: DataInformationBlock<'a>, - pub value_information_block: Option, + pub value_information_block: Option>, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq, Clone)] diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 13a4f65..476285b 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -28,14 +28,13 @@ macro_rules! unit { }; } -impl TryFrom<&[u8]> for ValueInformationBlock<'a> { +impl<'a> TryFrom<&'a [u8]> for ValueInformationBlock<'a> { type Error = DataInformationError; - fn try_from(data: &[u8]) -> Result { - let mut vife = ArrayVec::::new(); + fn try_from(data: &'a [u8]) -> Result { let vif = - ValueInformationField::from(*data.first().ok_or(ValueInformationError::DataTooShort)?); - let mut plaintext_vife: Option> = None; + ValueInformationField::from(*data.first().ok_or(DataInformationError::DataTooShort)?); + let mut plaintext_vife = None; #[cfg(not(feature = "plaintext-before-extension"))] let standard_plaintex_vib = true; @@ -43,44 +42,34 @@ impl TryFrom<&[u8]> for ValueInformationBlock<'a> { let standard_plaintex_vib = false; if !standard_plaintex_vib && vif.value_information_contains_ascii() { - plaintext_vife = Some(extract_plaintext_vife( - data.get(1..).ok_or(ValueInformationError::DataTooShort)?, - )?); + plaintext_vife = Some(PlainTextValueInformationExtension::new( + &data.get(1..).ok_or(DataInformationError::DataTooShort)?, + )) } + let mut offset = 0; if vif.has_extension() { // When the plaintext VIF precedes the extensions, the VIFE chain // starts after the ASCII length byte and string, not at offset 1. - let mut offset = match &plaintext_vife { - Some(chars) if !standard_plaintex_vib => 1 + 1 + chars.len(), + + offset = match &plaintext_vife { + Some(x) if !standard_plaintex_vib => 1 + 1 + x.ascii_len(), _ => 1, }; - while offset < data.len() { - let vife_data = *data - .get(offset) - .ok_or(ValueInformationError::DataTooShort)?; - let current_vife = ValueInformationFieldExtension { data: vife_data }; - let has_extension = current_vife.has_extension(); - vife.push(current_vife); - offset += 1; - if !has_extension { - break; - } - if vife.len() > MAX_VIFE_RECORDS { - return Err(ValueInformationError::InvalidValueInformation); - } - } if standard_plaintex_vib && vif.value_information_contains_ascii() { - plaintext_vife = Some(extract_plaintext_vife( + plaintext_vife = Some(PlainTextValueInformationExtension::new( data.get(offset..) - .ok_or(ValueInformationError::DataTooShort)?, - )?); + .ok_or(DataInformationError::DataTooShort)?, + )); } } Ok(Self { value_information: vif, - value_information_extension: if vife.is_empty() { None } else { Some(vife) }, + value_information_extension: Some(ValueInformationFieldExtensions::new( + data.get(..offset) + .ok_or(DataInformationError::DataTooShort)?, + )), plaintext_vife, }) } @@ -98,17 +87,16 @@ fn extract_plaintext_vife(data: &[u8]) -> Result, ValueInforma Ok(ascii) } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[derive(Debug, PartialEq, Clone)] -pub struct ValueInformationBlock { +pub struct ValueInformationBlock<'a> { pub value_information: ValueInformationField, - pub value_information_extension: - Option>, - pub plaintext_vife: Option>, + pub value_information_extension: Option>, + pub plaintext_vife: Option>, } #[cfg(feature = "defmt")] -impl defmt::Format for ValueInformationBlock { +impl<'a> defmt::Format for ValueInformationBlock<'a> { fn format(&self, f: defmt::Formatter) { defmt::write!( f, @@ -117,19 +105,11 @@ impl defmt::Format for ValueInformationBlock { ); if let Some(ext) = &self.value_information_extension { defmt::write!(f, ", value_information_extension: ["); - for (i, vife) in ext.iter().enumerate() { - if i != 0 { - defmt::write!(f, ", "); - } - defmt::write!(f, "{:?}", vife); - } + ext.iter().for_each(|x| defmt::write!(f, "{},", x)); defmt::write!(f, "]"); } if let Some(text) = &self.plaintext_vife { - defmt::write!(f, ", plaintext_vife: "); - for c in text { - defmt::write!(f, "{}", c); - } + defmt::write!(f, ", plaintext_vife: {}", text.as_ascii_str()); } defmt::write!(f, " }}"); } @@ -147,6 +127,69 @@ impl ValueInformationField { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ValueInformationFieldExtensions<'a>(&'a [u8]); +impl<'a> ValueInformationFieldExtensions<'a> { + const fn new(data: &'a [u8]) -> Self { + Self(data) + } +} + +impl Iterator for ValueInformationFieldExtensions<'_> { + type Item = ValueInformationFieldExtension; + fn next(&mut self) -> Option { + let (head, tail) = self.0.split_first()?; + self.0 = tail; + Some(ValueInformationFieldExtension { data: *head }) + } + fn size_hint(&self) -> (usize, Option) { + (self.0.len(), Some(self.0.len())) + } +} + +impl ExactSizeIterator for ValueInformationFieldExtensions<'_> {} +impl DoubleEndedIterator for ValueInformationFieldExtensions<'_> { + fn next_back(&mut self) -> Option { + let (end, start) = self.0.split_last()?; + self.0 = start; + Some(ValueInformationFieldExtension { data: *end }) + } +} + +impl<'a> ValueInformationFieldExtensions<'a> { + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().copied() + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PlainTextValueInformationExtension<'a>(&'a [u8]); +impl<'a> PlainTextValueInformationExtension<'a> { + const fn new(data: &'a [u8]) -> Self { + Self(data) + } + + pub const fn ascii_len(&self) -> usize { + if let Some(x) = self.0.first() { + *x as usize + } else { + 0 + } + } + + fn as_ascii_str(&self) -> Option<&str> { + if self.0.is_ascii() { + core::str::from_utf8(self.0).ok() + } else { + None + } + } +} + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -193,22 +236,34 @@ pub enum ValueInformationCoding { ManufacturerSpecific, } -impl ValueInformationBlock { +impl ValueInformationBlock<'_> { + pub fn new( + value_information: ValueInformationField, + value_information_extension: Option>, + plaintext_vife: Option>, + ) -> Self { + Self { + value_information, + value_information_extension, + plaintext_vife, + } + } + #[must_use] - pub const fn get_size(&self) -> usize { + pub fn get_size(&self) -> usize { let mut size = 1; if let Some(vife) = &self.value_information_extension { - size += vife.len(); + size += vife.iter().count(); } if let Some(plaintext_vife) = &self.plaintext_vife { // 1 byte for the length of the ASCII string - size += plaintext_vife.len() + 1; + size += plaintext_vife.ascii_len() + 1; } size } } -impl TryFrom<&ValueInformationBlock> for ValueInformation { +impl TryFrom<&ValueInformationBlock<'_>> for ValueInformation { type Error = DataInformationError; fn try_from( @@ -218,10 +273,7 @@ impl TryFrom<&ValueInformationBlock> for ValueInformation { let mut labels = ArrayVec::::new(); let mut decimal_scale_exponent: isize = 0; let mut decimal_offset_exponent = 0; - let vife_slice = match &value_information_block.value_information_extension { - Some(v) => v.as_slice(), - None => &[], - }; + let vife_slice = value_information_block.value_information_extension; match ValueInformationCoding::from(&value_information_block.value_information) { ValueInformationCoding::Primary => { match value_information_block.value_information.data & 0x7F { @@ -715,7 +767,7 @@ impl TryFrom<&ValueInformationBlock> for ValueInformation { } fn consume_orthhogonal_vife( - vife: &[ValueInformationFieldExtension], + vife: ValueInformationFieldExtension, labels: &mut ArrayVec, units: &mut ArrayVec, decimal_scale_exponent: &mut isize, From f6d9d851360353df0362de1fea995f6dd9c96f16 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Wed, 19 Aug 2026 22:04:01 +0200 Subject: [PATCH 03/14] refactored and now use itorator for vife --- .../src/value_information.rs | 719 ++++++++++-------- 1 file changed, 381 insertions(+), 338 deletions(-) diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 476285b..1c03a58 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -34,42 +34,42 @@ impl<'a> TryFrom<&'a [u8]> for ValueInformationBlock<'a> { fn try_from(data: &'a [u8]) -> Result { let vif = ValueInformationField::from(*data.first().ok_or(DataInformationError::DataTooShort)?); + let mut offset = 1; + let mut value_information_extension = None; let mut plaintext_vife = None; - #[cfg(not(feature = "plaintext-before-extension"))] - let standard_plaintex_vib = true; #[cfg(feature = "plaintext-before-extension")] - let standard_plaintex_vib = false; - - if !standard_plaintex_vib && vif.value_information_contains_ascii() { - plaintext_vife = Some(PlainTextValueInformationExtension::new( - &data.get(1..).ok_or(DataInformationError::DataTooShort)?, - )) + if vif.value_information_contains_ascii() { + let plaintext = PlainTextValueInformationExtension::new( + data.get(offset..) + .ok_or(DataInformationError::DataTooShort)?, + )?; + offset += plaintext.ascii_len() + 1; + plaintext_vife = Some(plaintext); } - let mut offset = 0; if vif.has_extension() { // When the plaintext VIF precedes the extensions, the VIFE chain // starts after the ASCII length byte and string, not at offset 1. + let extensions = ValueInformationFieldExtensions::new( + data.get(offset..) + .ok_or(DataInformationError::DataTooShort)?, + )?; + offset += extensions.len(); + value_information_extension = Some(extensions); + } - offset = match &plaintext_vife { - Some(x) if !standard_plaintex_vib => 1 + 1 + x.ascii_len(), - _ => 1, - }; - if standard_plaintex_vib && vif.value_information_contains_ascii() { - plaintext_vife = Some(PlainTextValueInformationExtension::new( - data.get(offset..) - .ok_or(DataInformationError::DataTooShort)?, - )); - } + #[cfg(not(feature = "plaintext-before-extension"))] + if vif.value_information_contains_ascii() { + plaintext_vife = Some(PlainTextValueInformationExtension::new( + data.get(offset..) + .ok_or(DataInformationError::DataTooShort)?, + )?); } Ok(Self { value_information: vif, - value_information_extension: Some(ValueInformationFieldExtensions::new( - data.get(..offset) - .ok_or(DataInformationError::DataTooShort)?, - )), + value_information_extension, plaintext_vife, }) } @@ -132,8 +132,28 @@ impl ValueInformationField { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ValueInformationFieldExtensions<'a>(&'a [u8]); impl<'a> ValueInformationFieldExtensions<'a> { - const fn new(data: &'a [u8]) -> Self { - Self(data) + fn new(data: &'a [u8]) -> Result { + let Some(last_index) = data + .iter() + .take(MAX_VIFE_RECORDS + 1) + .position(|byte| byte & 0x80 == 0) + else { + return Err(if data.len() > MAX_VIFE_RECORDS { + DataInformationError::InvalidValueInformation + } else { + DataInformationError::DataTooShort + }); + }; + + let length = last_index + 1; + if length > MAX_VIFE_RECORDS { + return Err(DataInformationError::InvalidValueInformation); + } + + Ok(Self( + data.get(..length) + .ok_or(DataInformationError::DataTooShort)?, + )) } } @@ -169,8 +189,22 @@ impl<'a> ValueInformationFieldExtensions<'a> { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PlainTextValueInformationExtension<'a>(&'a [u8]); impl<'a> PlainTextValueInformationExtension<'a> { - const fn new(data: &'a [u8]) -> Self { - Self(data) + fn new(data: &'a [u8]) -> Result { + let ascii_len = usize::from(*data.first().ok_or(DataInformationError::DataTooShort)?); + + if ascii_len > 9 { + return Err(DataInformationError::InvalidValueInformation); + } + + let encoded = data + .get(..ascii_len + 1) + .ok_or(DataInformationError::DataTooShort)?; + + if !encoded[1..].is_ascii() { + return Err(DataInformationError::InvalidValueInformation); + } + + Ok(Self(encoded)) } pub const fn ascii_len(&self) -> usize { @@ -182,11 +216,7 @@ impl<'a> PlainTextValueInformationExtension<'a> { } fn as_ascii_str(&self) -> Option<&str> { - if self.0.is_ascii() { - core::str::from_utf8(self.0).ok() - } else { - None - } + core::str::from_utf8(self.0.get(1..)?).ok() } } @@ -236,11 +266,11 @@ pub enum ValueInformationCoding { ManufacturerSpecific, } -impl ValueInformationBlock<'_> { +impl<'a> ValueInformationBlock<'a> { pub fn new( value_information: ValueInformationField, - value_information_extension: Option>, - plaintext_vife: Option>, + value_information_extension: Option>, + plaintext_vife: Option>, ) -> Self { Self { value_information, @@ -273,7 +303,7 @@ impl TryFrom<&ValueInformationBlock<'_>> for ValueInformation { let mut labels = ArrayVec::::new(); let mut decimal_scale_exponent: isize = 0; let mut decimal_offset_exponent = 0; - let vife_slice = value_information_block.value_information_extension; + let vife_slice = value_information_block.value_information_extension.clone(); match ValueInformationCoding::from(&value_information_block.value_information) { ValueInformationCoding::Primary => { match value_information_block.value_information.data & 0x7F { @@ -409,204 +439,213 @@ impl TryFrom<&ValueInformationBlock<'_>> for ValueInformation { }) } }; - consume_orthhogonal_vife( - vife_slice, - &mut labels, - &mut units, - &mut decimal_scale_exponent, - &mut decimal_offset_exponent, - ); + if let Some(x) = vife_slice { + consume_orthhogonal_vife( + x, + &mut labels, + &mut units, + &mut decimal_scale_exponent, + &mut decimal_offset_exponent, + ); + } } + ValueInformationCoding::MainVIFExtension => { - let first_vife_data = vife_slice - .first() - .ok_or(DataInformationError::DataTooShort)? - .data; - let second_vife_data = vife_slice.get(1).map(|v| v.data); - match first_vife_data & 0x7F { - 0x00..=0x03 => { - units.push(unit!(LocalMoneyCurrency)); - labels.push(ValueLabel::Credit); - decimal_scale_exponent = (first_vife_data & 0b11) as isize - 3; - } - 0x04..=0x07 => { - units.push(unit!(LocalMoneyCurrency)); - labels.push(ValueLabel::Debit); - decimal_scale_exponent = (first_vife_data & 0b11) as isize - 3; - } - 0x08 => labels.push(ValueLabel::UniqueMessageIdentificationOrAccessNumber), - 0x09 => labels.push(ValueLabel::DeviceType), - 0x0A => labels.push(ValueLabel::Manufacturer), - 0x0B => labels.push(ValueLabel::ParameterSetIdentification), - 0x0C => labels.push(ValueLabel::ModelOrVersion), - 0x0D => labels.push(ValueLabel::HardwareVersion), - 0x0E => labels.push(ValueLabel::MetrologyFirmwareVersion), - 0x0F => labels.push(ValueLabel::OtherSoftwareVersion), - 0x10 => labels.push(ValueLabel::CustomerLocation), - 0x11 => labels.push(ValueLabel::Customer), - 0x12 => labels.push(ValueLabel::AccessCodeUser), - 0x13 => labels.push(ValueLabel::AccessCodeOperator), - 0x14 => labels.push(ValueLabel::AccessCodeSystemOperator), - 0x15 => labels.push(ValueLabel::AccessCodeDeveloper), - 0x16 => labels.push(ValueLabel::Password), - 0x17 => labels.push(ValueLabel::ErrorFlags), - 0x18 => labels.push(ValueLabel::ErrorMask), - 0x19 => labels.push(ValueLabel::SecurityKey), - 0x1A => { - labels.push(ValueLabel::DigitalOutput); - labels.push(ValueLabel::Binary); - } - 0x1B => { - labels.push(ValueLabel::DigitalInput); - labels.push(ValueLabel::Binary); - } - 0x1C => { - units.push(unit!(Symbol)); - units.push(unit!(Second ^ -1)); - labels.push(ValueLabel::BaudRate); - } - 0x1D => { - units.push(unit!(BitTime)); - labels.push(ValueLabel::ResponseDelayTime); - } - 0x1E => labels.push(ValueLabel::Retry), - 0x1F => labels.push(ValueLabel::RemoteControl), - 0x20 => labels.push(ValueLabel::FirstStorageForCycleStorage), - 0x21 => labels.push(ValueLabel::LastStorageForCycleStorage), - 0x22 => labels.push(ValueLabel::SizeOfStorageBlock), - 0x23 => labels.push(ValueLabel::DescriptionOfTariffAndSubunit), - 0x24 => { - units.push(unit!(Second)); - labels.push(ValueLabel::StorageInterval); - } - 0x25 => { - units.push(unit!(Minute)); - labels.push(ValueLabel::StorageInterval); - } - 0x26 => { - units.push(unit!(Hour)); - labels.push(ValueLabel::StorageInterval); - } - 0x27 => { - units.push(unit!(Day)); - labels.push(ValueLabel::StorageInterval); - } - 0x28 => { - units.push(unit!(Month)); - labels.push(ValueLabel::StorageInterval); - } - 0x29 => { - units.push(unit!(Year)); - labels.push(ValueLabel::StorageInterval); - } - 0x30 => labels.push(ValueLabel::DimensionlessHCA), - 0x31 => labels.push(ValueLabel::DataContainerForWmbusProtocol), - 0x32 => { - units.push(unit!(Second)); - labels.push(ValueLabel::PeriodOfNormalDataTransmission); - } - 0x33 => { - units.push(unit!(Meter)); - labels.push(ValueLabel::PeriodOfNormalDataTransmission); - } - 0x34 => { - units.push(unit!(Hour)); - labels.push(ValueLabel::PeriodOfNormalDataTransmission); - } - 0x35 => { - units.push(unit!(Day)); - labels.push(ValueLabel::PeriodOfNormalDataTransmission); - } - 0x3A => labels.push(ValueLabel::Dimensionless), - 0x40..=0x4F => { - units.push(unit!(Volt)); - labels.push(ValueLabel::Voltage); - decimal_scale_exponent = (first_vife_data & 0b1111) as isize - 9; - } - 0x50..=0x5F => { - units.push(unit!(Ampere)); - labels.push(ValueLabel::Current); - decimal_scale_exponent = (first_vife_data & 0b1111) as isize - 12; - } - 0x60 => labels.push(ValueLabel::ResetCounter), - 0x61 => labels.push(ValueLabel::CumulationCounter), - 0x62 => labels.push(ValueLabel::ControlSignal), - 0x63 => labels.push(ValueLabel::DayOfWeek), - 0x64 => labels.push(ValueLabel::WeekNumber), - 0x65 => labels.push(ValueLabel::TimePointOfChangeOfTariff), - 0x66 => labels.push(ValueLabel::StateOfParameterActivation), - 0x67 => labels.push(ValueLabel::SpecialSupplierInformation), - 0x68 => { - units.push(unit!(Hour)); - labels.push(ValueLabel::DurationSinceLastCumulation); - } - 0x69 => { - units.push(unit!(Day)); - labels.push(ValueLabel::DurationSinceLastCumulation); - } - 0x6A => { - units.push(unit!(Month)); - labels.push(ValueLabel::DurationSinceLastCumulation); - } - 0x6B => { - units.push(unit!(Year)); - labels.push(ValueLabel::DurationSinceLastCumulation); - } - 0x6C => { - units.push(unit!(Hour)); - labels.push(ValueLabel::OperatingTimeBattery); - } - 0x6D => { - units.push(unit!(Day)); - labels.push(ValueLabel::OperatingTimeBattery); - } - 0x6E => { - units.push(unit!(Month)); - labels.push(ValueLabel::OperatingTimeBattery); - } - 0x6F => { - units.push(unit!(Hour)); - labels.push(ValueLabel::OperatingTimeBattery); - } - 0x70 => { - units.push(unit!(Second)); - labels.push(ValueLabel::DateAndTimeOfBatteryChange); - } - 0x71 => { - units.push(unit!(DecibelMilliWatt)); - labels.push(ValueLabel::RFPowerLevel); - } - 0x72 => labels.push(ValueLabel::DaylightSavingBeginningEndingDeviation), - 0x73 => labels.push(ValueLabel::ListeningWindowManagementData), - 0x74 => labels.push(ValueLabel::RemainingBatteryLifeTime), - 0x75 => labels.push(ValueLabel::NumberOfTimesTheMeterWasStopped), - 0x76 => labels.push(ValueLabel::DataContainerForManufacturerSpecificProtocol), - 0x7D => match second_vife_data.map(|s| s & 0x7F) { - Some(0x00) => labels.push(ValueLabel::CurrentlySelectedApplication), - Some(0x02) => { + if let Some(x) = vife_slice { + let mut inspect = x.clone(); + let first_vife_data = inspect + .next() + .ok_or(DataInformationError::DataTooShort)? + .data; + + let second_vife_data = inspect.next().map(|v| v.data); + match first_vife_data & 0x7F { + 0x00..=0x03 => { + units.push(unit!(LocalMoneyCurrency)); + labels.push(ValueLabel::Credit); + decimal_scale_exponent = (first_vife_data & 0b11) as isize - 3; + } + 0x04..=0x07 => { + units.push(unit!(LocalMoneyCurrency)); + labels.push(ValueLabel::Debit); + decimal_scale_exponent = (first_vife_data & 0b11) as isize - 3; + } + 0x08 => labels.push(ValueLabel::UniqueMessageIdentificationOrAccessNumber), + 0x09 => labels.push(ValueLabel::DeviceType), + 0x0A => labels.push(ValueLabel::Manufacturer), + 0x0B => labels.push(ValueLabel::ParameterSetIdentification), + 0x0C => labels.push(ValueLabel::ModelOrVersion), + 0x0D => labels.push(ValueLabel::HardwareVersion), + 0x0E => labels.push(ValueLabel::MetrologyFirmwareVersion), + 0x0F => labels.push(ValueLabel::OtherSoftwareVersion), + 0x10 => labels.push(ValueLabel::CustomerLocation), + 0x11 => labels.push(ValueLabel::Customer), + 0x12 => labels.push(ValueLabel::AccessCodeUser), + 0x13 => labels.push(ValueLabel::AccessCodeOperator), + 0x14 => labels.push(ValueLabel::AccessCodeSystemOperator), + 0x15 => labels.push(ValueLabel::AccessCodeDeveloper), + 0x16 => labels.push(ValueLabel::Password), + 0x17 => labels.push(ValueLabel::ErrorFlags), + 0x18 => labels.push(ValueLabel::ErrorMask), + 0x19 => labels.push(ValueLabel::SecurityKey), + 0x1A => { + labels.push(ValueLabel::DigitalOutput); + labels.push(ValueLabel::Binary); + } + 0x1B => { + labels.push(ValueLabel::DigitalInput); + labels.push(ValueLabel::Binary); + } + 0x1C => { + units.push(unit!(Symbol)); + units.push(unit!(Second ^ -1)); + labels.push(ValueLabel::BaudRate); + } + 0x1D => { + units.push(unit!(BitTime)); + labels.push(ValueLabel::ResponseDelayTime); + } + 0x1E => labels.push(ValueLabel::Retry), + 0x1F => labels.push(ValueLabel::RemoteControl), + 0x20 => labels.push(ValueLabel::FirstStorageForCycleStorage), + 0x21 => labels.push(ValueLabel::LastStorageForCycleStorage), + 0x22 => labels.push(ValueLabel::SizeOfStorageBlock), + 0x23 => labels.push(ValueLabel::DescriptionOfTariffAndSubunit), + 0x24 => { + units.push(unit!(Second)); + labels.push(ValueLabel::StorageInterval); + } + 0x25 => { + units.push(unit!(Minute)); + labels.push(ValueLabel::StorageInterval); + } + 0x26 => { + units.push(unit!(Hour)); + labels.push(ValueLabel::StorageInterval); + } + 0x27 => { + units.push(unit!(Day)); + labels.push(ValueLabel::StorageInterval); + } + 0x28 => { + units.push(unit!(Month)); + labels.push(ValueLabel::StorageInterval); + } + 0x29 => { + units.push(unit!(Year)); + labels.push(ValueLabel::StorageInterval); + } + 0x30 => labels.push(ValueLabel::DimensionlessHCA), + 0x31 => labels.push(ValueLabel::DataContainerForWmbusProtocol), + 0x32 => { + units.push(unit!(Second)); + labels.push(ValueLabel::PeriodOfNormalDataTransmission); + } + 0x33 => { + units.push(unit!(Meter)); + labels.push(ValueLabel::PeriodOfNormalDataTransmission); + } + 0x34 => { + units.push(unit!(Hour)); + labels.push(ValueLabel::PeriodOfNormalDataTransmission); + } + 0x35 => { + units.push(unit!(Day)); + labels.push(ValueLabel::PeriodOfNormalDataTransmission); + } + 0x3A => labels.push(ValueLabel::Dimensionless), + 0x40..=0x4F => { + units.push(unit!(Volt)); + labels.push(ValueLabel::Voltage); + decimal_scale_exponent = (first_vife_data & 0b1111) as isize - 9; + } + 0x50..=0x5F => { + units.push(unit!(Ampere)); + labels.push(ValueLabel::Current); + decimal_scale_exponent = (first_vife_data & 0b1111) as isize - 12; + } + 0x60 => labels.push(ValueLabel::ResetCounter), + 0x61 => labels.push(ValueLabel::CumulationCounter), + 0x62 => labels.push(ValueLabel::ControlSignal), + 0x63 => labels.push(ValueLabel::DayOfWeek), + 0x64 => labels.push(ValueLabel::WeekNumber), + 0x65 => labels.push(ValueLabel::TimePointOfChangeOfTariff), + 0x66 => labels.push(ValueLabel::StateOfParameterActivation), + 0x67 => labels.push(ValueLabel::SpecialSupplierInformation), + 0x68 => { + units.push(unit!(Hour)); + labels.push(ValueLabel::DurationSinceLastCumulation); + } + 0x69 => { + units.push(unit!(Day)); + labels.push(ValueLabel::DurationSinceLastCumulation); + } + 0x6A => { units.push(unit!(Month)); - labels.push(ValueLabel::RemainingBatteryLifeTime); + labels.push(ValueLabel::DurationSinceLastCumulation); } - Some(0x03) => { + 0x6B => { units.push(unit!(Year)); - labels.push(ValueLabel::RemainingBatteryLifeTime); + labels.push(ValueLabel::DurationSinceLastCumulation); + } + 0x6C => { + units.push(unit!(Hour)); + labels.push(ValueLabel::OperatingTimeBattery); } - Some(0x3E) => { - units.push(unit!(Percent)); - labels.push(ValueLabel::MoistureLevel); + 0x6D => { + units.push(unit!(Day)); + labels.push(ValueLabel::OperatingTimeBattery); } + 0x6E => { + units.push(unit!(Month)); + labels.push(ValueLabel::OperatingTimeBattery); + } + 0x6F => { + units.push(unit!(Hour)); + labels.push(ValueLabel::OperatingTimeBattery); + } + 0x70 => { + units.push(unit!(Second)); + labels.push(ValueLabel::DateAndTimeOfBatteryChange); + } + 0x71 => { + units.push(unit!(DecibelMilliWatt)); + labels.push(ValueLabel::RFPowerLevel); + } + 0x72 => labels.push(ValueLabel::DaylightSavingBeginningEndingDeviation), + 0x73 => labels.push(ValueLabel::ListeningWindowManagementData), + 0x74 => labels.push(ValueLabel::RemainingBatteryLifeTime), + 0x75 => labels.push(ValueLabel::NumberOfTimesTheMeterWasStopped), + 0x76 => { + labels.push(ValueLabel::DataContainerForManufacturerSpecificProtocol) + } + 0x7D => match second_vife_data.map(|s| s & 0x7F) { + Some(0x00) => labels.push(ValueLabel::CurrentlySelectedApplication), + Some(0x02) => { + units.push(unit!(Month)); + labels.push(ValueLabel::RemainingBatteryLifeTime); + } + Some(0x03) => { + units.push(unit!(Year)); + labels.push(ValueLabel::RemainingBatteryLifeTime); + } + Some(0x3E) => { + units.push(unit!(Percent)); + labels.push(ValueLabel::MoistureLevel); + } + _ => labels.push(ValueLabel::Reserved), + }, _ => labels.push(ValueLabel::Reserved), - }, - _ => labels.push(ValueLabel::Reserved), + } + // Skip vife_slice[0] — it's the true VIF (already consumed above) + consume_orthhogonal_vife( + x.skip(1), + &mut labels, + &mut units, + &mut decimal_scale_exponent, + &mut decimal_offset_exponent, + ); } - // Skip vife_slice[0] — it's the true VIF (already consumed above) - consume_orthhogonal_vife( - vife_slice.get(1..).unwrap_or(&[]), - &mut labels, - &mut units, - &mut decimal_scale_exponent, - &mut decimal_offset_exponent, - ); } ValueInformationCoding::AlternateVIFExtension => { use UnitName::*; @@ -639,118 +678,122 @@ impl TryFrom<&ValueInformationBlock<'_>> for ValueInformation { populate!(@snd $($rem)*) }}; } - let first_vife_data = vife_slice - .first() - .ok_or(DataInformationError::DataTooShort)? - .data; - match first_vife_data & 0x7F { - 0b0 => populate!(Watt / h, 3, dec: 5, Energy), - 0b000_0001 => populate!(Watt / h, 3, dec: 6, Energy), - 0b000_0010 => populate!(ReactiveWatt * h, 1, dec: 3, ReactiveEnergy), - 0b000_0011 => populate!(ReactiveWatt * h, 1, dec: 4, ReactiveEnergy), - 0b000_0100 => populate!(ApparentWatt * h, 1, dec: 3, ApparentEnergy), - 0b000_0101 => populate!(ApparentWatt * h, 1, dec: 4, ApparentEnergy), - 0b000_0110 => { - labels.push(CoefficientOfPerformance); - decimal_scale_exponent = -1; - } - 0b000_1000 => populate!(Joul, 1, dec: 8, Energy), - 0b000_1001 => populate!(Joul, 1, dec: 9, Energy), - 0b000_1100 => populate!(Calorie, 1, dec: 5, Energy), - 0b000_1101 => populate!(Calorie, 1, dec: 6, Energy), - 0b000_1110 => populate!(Calorie, 1, dec: 7, Energy), - 0b000_1111 => populate!(Calorie, 1, dec: 8, Energy), - 0b001_0000 => populate!(Meter, 3, dec: 2, Volume), - 0b001_0001 => populate!(Meter, 3, dec: 3, Volume), - 0b001_0100 => populate!(ReactiveWatt, 1, dec: 0, ReactivePower), - 0b001_0101 => populate!(ReactiveWatt, 1, dec: 1, ReactivePower), - 0b001_0110 => populate!(ReactiveWatt, 1, dec: 2, ReactivePower), - 0b001_0111 => populate!(ReactiveWatt, 1, dec: 3, ReactivePower), - 0b001_1000 => populate!(Tonne, 1, dec: 2, Mass), - 0b001_1001 => populate!(Tonne, 1, dec: 3, Mass), - 0b001_1010 => populate!(Percent, 1, dec: -1, RelativeHumidity), - 0b001_1011 => populate!(Percent, 1, dec: 0, RelativeHumidity), - 0b010_0000 => populate!(Feet, 3, dec: 0, Volume), - 0b010_0001 => populate!(Feet, 3, dec: -1, Volume), - 0b010_0011 => populate!(Degree, 1, dec: -1, PhaseItoU), - 0b010_1000 => populate!(Watt, 1, dec: 5, Power), - 0b010_1001 => populate!(Watt, 1, dec: 6, Power), - 0b010_1010 => populate!(Degree, 1, dec: -1, PhaseUtoU), - 0b010_1011 => populate!(Degree, 1, dec: -1, PhaseUtoI), - 0b010_1100 => populate!(Hertz, 1, dec: -3, Frequency), - 0b010_1101 => populate!(Hertz, 1, dec: -2, Frequency), - 0b010_1110 => populate!(Hertz, 1, dec: -1, Frequency), - 0b010_1111 => populate!(Hertz, 1, dec: 0, Frequency), - 0b011_0000 => populate!(Joul / h, 1, dec: 8, Power), - 0b011_0001 => populate!(Joul / h, 1, dec: 9, Power), - 0b011_0100 => populate!(ApparentWatt, 1, dec: 0, ApparentPower), - 0b011_0101 => populate!(ApparentWatt, 1, dec: 1, ApparentPower), - 0b011_0110 => populate!(ApparentWatt, 1, dec: 2, ApparentPower), - 0b011_0111 => populate!(ApparentWatt, 1, dec: 3, ApparentPower), - 0b101_1000 => populate!(Fahrenheit, 1, dec: -3, FlowTemperature), - 0b101_1001 => populate!(Fahrenheit, 1, dec: -2, FlowTemperature), - 0b101_1010 => populate!(Fahrenheit, 1, dec: -1, FlowTemperature), - 0b101_1011 => populate!(Fahrenheit, 1, dec: 0, FlowTemperature), - 0b101_1100 => populate!(Fahrenheit, 1, dec: -3, ReturnTemperature), - 0b101_1101 => populate!(Fahrenheit, 1, dec: -2, ReturnTemperature), - 0b101_1110 => populate!(Fahrenheit, 1, dec: -1, ReturnTemperature), - 0b101_1111 => populate!(Fahrenheit, 1, dec: 0, ReturnTemperature), - 0b110_0000 => populate!(Fahrenheit, 1, dec: -3, TemperatureDifference), - 0b110_0001 => populate!(Fahrenheit, 1, dec: -2, TemperatureDifference), - 0b110_0010 => populate!(Fahrenheit, 1, dec: -1, TemperatureDifference), - 0b110_0011 => populate!(Fahrenheit, 1, dec: 0, TemperatureDifference), - 0b110_0100 => populate!(Fahrenheit, 1, dec: -3, ExternalTemperature), - 0b110_0101 => populate!(Fahrenheit, 1, dec: -2, ExternalTemperature), - 0b110_0110 => populate!(Fahrenheit, 1, dec: -1, ExternalTemperature), - 0b110_0111 => populate!(Fahrenheit, 1, dec: 0, ExternalTemperature), - 0b111_0000 => populate!(Fahrenheit, 1, dec: -3, ColdWarmTemperatureLimit), - 0b111_0001 => populate!(Fahrenheit, 1, dec: -2, ColdWarmTemperatureLimit), - 0b111_0010 => populate!(Fahrenheit, 1, dec: -1, ColdWarmTemperatureLimit), - 0b111_0011 => populate!(Fahrenheit, 1, dec: 0, ColdWarmTemperatureLimit), - 0b111_0100 => populate!(Celsius, 1, dec: -3, ColdWarmTemperatureLimit), - 0b111_0101 => populate!(Celsius, 1, dec: -2, ColdWarmTemperatureLimit), - 0b111_0110 => populate!(Celsius, 1, dec: -1, ColdWarmTemperatureLimit), - 0b111_0111 => populate!(Celsius, 1, dec: 0, ColdWarmTemperatureLimit), - 0b111_1000 => populate!(Watt, 1, dec: -3, CumulativeMaximumOfActivePower), - 0b111_1001 => populate!(Watt, 1, dec: -2, CumulativeMaximumOfActivePower), - 0b111_1010 => populate!(Watt, 1, dec: -1, CumulativeMaximumOfActivePower), - 0b111_1011 => populate!(Watt, 1, dec: 0, CumulativeMaximumOfActivePower), - 0b111_1100 => populate!(Watt, 1, dec: 1, CumulativeMaximumOfActivePower), - 0b111_1101 => populate!(Watt, 1, dec: 2, CumulativeMaximumOfActivePower), - 0b111_1110 => populate!(Watt, 1, dec: 3, CumulativeMaximumOfActivePower), - 0b111_1111 => populate!(Watt, 1, dec: 4, CumulativeMaximumOfActivePower), - 0b110_1000 => populate!(HCAUnit, 1,dec: 0, ResultingRatingFactor), - 0b110_1001 => populate!(HCAUnit, 1,dec: 0, ThermalOutputRatingFactor), - 0b110_1010 => populate!(HCAUnit, 1,dec: 0, ThermalCouplingRatingFactorOverall), - 0b110_1011 => populate!(HCAUnit, 1,dec: 0, ThermalCouplingRatingRoomSide), - 0b110_1100 => { - populate!(HCAUnit, 1,dec: 0, ThermalCouplingRatingFactorHeatingSide) - } - 0b110_1101 => populate!(HCAUnit, 1,dec: 0, LowTemperatureRatingFactor), - 0b110_1110 => populate!(HCAUnit, 1,dec: 0, DisplayOutputScalingFactor), - _ => labels.push(ValueLabel::Reserved), - }; - // Skip vife_slice[0] — it's the true VIF (already consumed above) - consume_orthhogonal_vife( - vife_slice.get(1..).unwrap_or(&[]), - &mut labels, - &mut units, - &mut decimal_scale_exponent, - &mut decimal_offset_exponent, - ); + if let Some(mut x) = vife_slice { + let first_vife_data = x.next().ok_or(DataInformationError::DataTooShort)?.data; + match first_vife_data & 0x7F { + 0b0 => populate!(Watt / h, 3, dec: 5, Energy), + 0b000_0001 => populate!(Watt / h, 3, dec: 6, Energy), + 0b000_0010 => populate!(ReactiveWatt * h, 1, dec: 3, ReactiveEnergy), + 0b000_0011 => populate!(ReactiveWatt * h, 1, dec: 4, ReactiveEnergy), + 0b000_0100 => populate!(ApparentWatt * h, 1, dec: 3, ApparentEnergy), + 0b000_0101 => populate!(ApparentWatt * h, 1, dec: 4, ApparentEnergy), + 0b000_0110 => { + labels.push(CoefficientOfPerformance); + decimal_scale_exponent = -1; + } + 0b000_1000 => populate!(Joul, 1, dec: 8, Energy), + 0b000_1001 => populate!(Joul, 1, dec: 9, Energy), + 0b000_1100 => populate!(Calorie, 1, dec: 5, Energy), + 0b000_1101 => populate!(Calorie, 1, dec: 6, Energy), + 0b000_1110 => populate!(Calorie, 1, dec: 7, Energy), + 0b000_1111 => populate!(Calorie, 1, dec: 8, Energy), + 0b001_0000 => populate!(Meter, 3, dec: 2, Volume), + 0b001_0001 => populate!(Meter, 3, dec: 3, Volume), + 0b001_0100 => populate!(ReactiveWatt, 1, dec: 0, ReactivePower), + 0b001_0101 => populate!(ReactiveWatt, 1, dec: 1, ReactivePower), + 0b001_0110 => populate!(ReactiveWatt, 1, dec: 2, ReactivePower), + 0b001_0111 => populate!(ReactiveWatt, 1, dec: 3, ReactivePower), + 0b001_1000 => populate!(Tonne, 1, dec: 2, Mass), + 0b001_1001 => populate!(Tonne, 1, dec: 3, Mass), + 0b001_1010 => populate!(Percent, 1, dec: -1, RelativeHumidity), + 0b001_1011 => populate!(Percent, 1, dec: 0, RelativeHumidity), + 0b010_0000 => populate!(Feet, 3, dec: 0, Volume), + 0b010_0001 => populate!(Feet, 3, dec: -1, Volume), + 0b010_0011 => populate!(Degree, 1, dec: -1, PhaseItoU), + 0b010_1000 => populate!(Watt, 1, dec: 5, Power), + 0b010_1001 => populate!(Watt, 1, dec: 6, Power), + 0b010_1010 => populate!(Degree, 1, dec: -1, PhaseUtoU), + 0b010_1011 => populate!(Degree, 1, dec: -1, PhaseUtoI), + 0b010_1100 => populate!(Hertz, 1, dec: -3, Frequency), + 0b010_1101 => populate!(Hertz, 1, dec: -2, Frequency), + 0b010_1110 => populate!(Hertz, 1, dec: -1, Frequency), + 0b010_1111 => populate!(Hertz, 1, dec: 0, Frequency), + 0b011_0000 => populate!(Joul / h, 1, dec: 8, Power), + 0b011_0001 => populate!(Joul / h, 1, dec: 9, Power), + 0b011_0100 => populate!(ApparentWatt, 1, dec: 0, ApparentPower), + 0b011_0101 => populate!(ApparentWatt, 1, dec: 1, ApparentPower), + 0b011_0110 => populate!(ApparentWatt, 1, dec: 2, ApparentPower), + 0b011_0111 => populate!(ApparentWatt, 1, dec: 3, ApparentPower), + 0b101_1000 => populate!(Fahrenheit, 1, dec: -3, FlowTemperature), + 0b101_1001 => populate!(Fahrenheit, 1, dec: -2, FlowTemperature), + 0b101_1010 => populate!(Fahrenheit, 1, dec: -1, FlowTemperature), + 0b101_1011 => populate!(Fahrenheit, 1, dec: 0, FlowTemperature), + 0b101_1100 => populate!(Fahrenheit, 1, dec: -3, ReturnTemperature), + 0b101_1101 => populate!(Fahrenheit, 1, dec: -2, ReturnTemperature), + 0b101_1110 => populate!(Fahrenheit, 1, dec: -1, ReturnTemperature), + 0b101_1111 => populate!(Fahrenheit, 1, dec: 0, ReturnTemperature), + 0b110_0000 => populate!(Fahrenheit, 1, dec: -3, TemperatureDifference), + 0b110_0001 => populate!(Fahrenheit, 1, dec: -2, TemperatureDifference), + 0b110_0010 => populate!(Fahrenheit, 1, dec: -1, TemperatureDifference), + 0b110_0011 => populate!(Fahrenheit, 1, dec: 0, TemperatureDifference), + 0b110_0100 => populate!(Fahrenheit, 1, dec: -3, ExternalTemperature), + 0b110_0101 => populate!(Fahrenheit, 1, dec: -2, ExternalTemperature), + 0b110_0110 => populate!(Fahrenheit, 1, dec: -1, ExternalTemperature), + 0b110_0111 => populate!(Fahrenheit, 1, dec: 0, ExternalTemperature), + 0b111_0000 => populate!(Fahrenheit, 1, dec: -3, ColdWarmTemperatureLimit), + 0b111_0001 => populate!(Fahrenheit, 1, dec: -2, ColdWarmTemperatureLimit), + 0b111_0010 => populate!(Fahrenheit, 1, dec: -1, ColdWarmTemperatureLimit), + 0b111_0011 => populate!(Fahrenheit, 1, dec: 0, ColdWarmTemperatureLimit), + 0b111_0100 => populate!(Celsius, 1, dec: -3, ColdWarmTemperatureLimit), + 0b111_0101 => populate!(Celsius, 1, dec: -2, ColdWarmTemperatureLimit), + 0b111_0110 => populate!(Celsius, 1, dec: -1, ColdWarmTemperatureLimit), + 0b111_0111 => populate!(Celsius, 1, dec: 0, ColdWarmTemperatureLimit), + 0b111_1000 => populate!(Watt, 1, dec: -3, CumulativeMaximumOfActivePower), + 0b111_1001 => populate!(Watt, 1, dec: -2, CumulativeMaximumOfActivePower), + 0b111_1010 => populate!(Watt, 1, dec: -1, CumulativeMaximumOfActivePower), + 0b111_1011 => populate!(Watt, 1, dec: 0, CumulativeMaximumOfActivePower), + 0b111_1100 => populate!(Watt, 1, dec: 1, CumulativeMaximumOfActivePower), + 0b111_1101 => populate!(Watt, 1, dec: 2, CumulativeMaximumOfActivePower), + 0b111_1110 => populate!(Watt, 1, dec: 3, CumulativeMaximumOfActivePower), + 0b111_1111 => populate!(Watt, 1, dec: 4, CumulativeMaximumOfActivePower), + 0b110_1000 => populate!(HCAUnit, 1,dec: 0, ResultingRatingFactor), + 0b110_1001 => populate!(HCAUnit, 1,dec: 0, ThermalOutputRatingFactor), + 0b110_1010 => { + populate!(HCAUnit, 1,dec: 0, ThermalCouplingRatingFactorOverall) + } + 0b110_1011 => populate!(HCAUnit, 1,dec: 0, ThermalCouplingRatingRoomSide), + 0b110_1100 => { + populate!(HCAUnit, 1,dec: 0, ThermalCouplingRatingFactorHeatingSide) + } + 0b110_1101 => populate!(HCAUnit, 1,dec: 0, LowTemperatureRatingFactor), + 0b110_1110 => populate!(HCAUnit, 1,dec: 0, DisplayOutputScalingFactor), + + _ => labels.push(ValueLabel::Reserved), + }; + // Skip vife_slice[0] — it's the true VIF (already consumed above) + consume_orthhogonal_vife( + x.skip(1), + &mut labels, + &mut units, + &mut decimal_scale_exponent, + &mut decimal_offset_exponent, + ); + } } // we need to check if the next byte is equivalent to the length of the rest of the // the data. In this case it is very likely that, this is how the payload is built up. ValueInformationCoding::PlainText => { labels.push(ValueLabel::PlainText); - consume_orthhogonal_vife( - vife_slice, - &mut labels, - &mut units, - &mut decimal_scale_exponent, - &mut decimal_offset_exponent, - ); + if let Some(x) = vife_slice { + consume_orthhogonal_vife( + x, + &mut labels, + &mut units, + &mut decimal_scale_exponent, + &mut decimal_offset_exponent, + ); + } } ValueInformationCoding::ManufacturerSpecific => { labels.push(ValueLabel::ManufacturerSpecific) @@ -767,7 +810,7 @@ impl TryFrom<&ValueInformationBlock<'_>> for ValueInformation { } fn consume_orthhogonal_vife( - vife: ValueInformationFieldExtension, + vife: impl IntoIterator, labels: &mut ArrayVec, units: &mut ArrayVec, decimal_scale_exponent: &mut isize, @@ -1880,11 +1923,11 @@ mod tests { assert_eq!(vib.get_size(), 4); assert!(vib.plaintext_vife.is_none()); - let ext = vib.value_information_extension.as_ref().unwrap(); + let mut ext = vib.value_information_extension.unwrap(); assert_eq!(ext.len(), 3); - assert_eq!(ext[0].data, 0xD9); - assert_eq!(ext[1].data, 0xFC); - assert_eq!(ext[2].data, 0x01); + assert_eq!(ext.next().unwrap().data, 0xD9); + assert_eq!(ext.next().unwrap().data, 0xFC); + assert_eq!(ext.next().unwrap().data, 0x01); // Primary VIF with one orthogonal VIFE // VIF=0x96 (Volume + extension bit), VIFE=0x12 (Averaged) @@ -1893,9 +1936,9 @@ mod tests { assert_eq!(vib.value_information.data, 0x96); assert_eq!(vib.get_size(), 2); - let ext = vib.value_information_extension.as_ref().unwrap(); + let mut ext = vib.value_information_extension.unwrap(); assert_eq!(ext.len(), 1); - assert_eq!(ext[0].data, 0x12); + assert_eq!(ext.next().unwrap().data, 0x12); // Single primary VIF, no extension let vib = ValueInformationBlock::try_from([0x13].as_slice()).unwrap(); From c1367618b64f1a56718bfdfbf6abb8743f2c440f Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Wed, 19 Aug 2026 22:17:33 +0200 Subject: [PATCH 04/14] fixing tests and build --- .../src/value_information.rs | 39 +++++++------------ src/annotate.rs | 4 +- src/rscada_xml.rs | 3 +- tests/rscada_xml.rs | 10 ----- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 1c03a58..124bbe1 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -55,7 +55,10 @@ impl<'a> TryFrom<&'a [u8]> for ValueInformationBlock<'a> { data.get(offset..) .ok_or(DataInformationError::DataTooShort)?, )?; - offset += extensions.len(); + #[cfg(not(feature = "plaintext-before-extension"))] + { + offset += extensions.len(); + } value_information_extension = Some(extensions); } @@ -75,18 +78,6 @@ impl<'a> TryFrom<&'a [u8]> for ValueInformationBlock<'a> { } } -fn extract_plaintext_vife(data: &[u8]) -> Result, ValueInformationError> { - let ascii_length = *data.first().ok_or(ValueInformationError::DataTooShort)? as usize; - let mut ascii = ArrayVec::::new(); - for item in data - .get(1..=ascii_length) - .ok_or(ValueInformationError::DataTooShort)? - { - ascii.push(*item as char); - } - Ok(ascii) -} - #[cfg_attr(feature = "serde", derive(serde::Serialize))] #[derive(Debug, PartialEq, Clone)] pub struct ValueInformationBlock<'a> { @@ -179,8 +170,14 @@ impl DoubleEndedIterator for ValueInformationFieldExtensions<'_> { } impl<'a> ValueInformationFieldExtensions<'a> { - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().copied() + pub fn iter( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ + { + self.0 + .iter() + .copied() + .map(|data| ValueInformationFieldExtension { data }) } } @@ -215,7 +212,7 @@ impl<'a> PlainTextValueInformationExtension<'a> { } } - fn as_ascii_str(&self) -> Option<&str> { + pub fn as_ascii_str(&self) -> Option<&str> { core::str::from_utf8(self.0.get(1..)?).ok() } } @@ -249,12 +246,6 @@ impl ValueInformationField { } } -impl ValueInformationFieldExtension { - const fn has_extension(&self) -> bool { - self.data & 0x80 != 0 - } -} - #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] @@ -771,9 +762,9 @@ impl TryFrom<&ValueInformationBlock<'_>> for ValueInformation { _ => labels.push(ValueLabel::Reserved), }; - // Skip vife_slice[0] — it's the true VIF (already consumed above) + // The true VIF was already consumed from the iterator above. consume_orthhogonal_vife( - x.skip(1), + x, &mut labels, &mut units, &mut decimal_scale_exponent, diff --git a/src/annotate.rs b/src/annotate.rs index 5407076..659408f 100644 --- a/src/annotate.rs +++ b/src/annotate.rs @@ -932,14 +932,14 @@ pub(crate) fn annotate_data_records(segments: &mut Vec, base: usize // Plaintext VIF if let Some(plaintext) = &vib.plaintext_vife { - let pt_size = plaintext.len() + 1; // 1 for length byte + let pt_size = plaintext.ascii_len() + 1; // 1 for length byte segments.push(ByteSegment { start: base + offset, end: base + offset + pt_size, kind: SegmentKind::PlaintextVif, detail: Cow::Owned(format!( "Plaintext VIF: \"{}\"", - plaintext.iter().collect::() + plaintext.as_ascii_str().unwrap_or_default() )), group: Some(record_index), layer: Layer::RecordField, diff --git a/src/rscada_xml.rs b/src/rscada_xml.rs index f015f87..c3a1288 100644 --- a/src/rscada_xml.rs +++ b/src/rscada_xml.rs @@ -476,7 +476,8 @@ fn normalize_record(record: &user_data::data_record::DataRecord) -> Option = vib .plaintext_vife .as_ref() - .map(|chars| chars.iter().rev().collect()); + .and_then(|chars| chars.as_ascii_str()) + .map(|text| text.chars().rev().collect()); let value = variable_value_decode(dif, vif, vifes.first().copied(), data).ok()?; diff --git a/tests/rscada_xml.rs b/tests/rscada_xml.rs index 21c153e..e91345a 100644 --- a/tests/rscada_xml.rs +++ b/tests/rscada_xml.rs @@ -31,21 +31,11 @@ const KNOWN_MISMATCHES: &[&str] = &[ // feature: their meters emit the plaintext VIF directly after the VIF // byte (libmbus semantics) instead of after the VIFE chain. #[cfg(not(feature = "plaintext-before-extension"))] - "ACW_Itron-CYBLE-M-Bus-14", - #[cfg(not(feature = "plaintext-before-extension"))] - "EDC", - #[cfg(not(feature = "plaintext-before-extension"))] "ELV-Elvaco-CMa10", #[cfg(not(feature = "plaintext-before-extension"))] "THI_cma10", #[cfg(not(feature = "plaintext-before-extension"))] "elv_temp_humid", - #[cfg(not(feature = "plaintext-before-extension"))] - "itron_cyble_m-bus_v1.4_cold_water", - #[cfg(not(feature = "plaintext-before-extension"))] - "itron_cyble_m-bus_v1.4_gas", - #[cfg(not(feature = "plaintext-before-extension"))] - "itron_cyble_m-bus_v1.4_water", ]; fn first_difference(expected: &str, actual: &str) -> String { From 89e708e21ef32e217c60e448a52f9bd36c35920e Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 13:49:41 +0200 Subject: [PATCH 05/14] fixing the qemu build --- crates/m-bus-core/src/decryption.rs | 2 +- crates/m-bus-core/src/lib.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/m-bus-core/src/decryption.rs b/crates/m-bus-core/src/decryption.rs index 6511500..712fab7 100644 --- a/crates/m-bus-core/src/decryption.rs +++ b/crates/m-bus-core/src/decryption.rs @@ -30,7 +30,7 @@ pub enum DecryptionError { } impl core::fmt::Display for DecryptionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::UnsupportedMode(mode) => write!(f, "Unsupported security mode: {:?}", mode), Self::KeyNotFound => write!(f, "Decryption key not found"), diff --git a/crates/m-bus-core/src/lib.rs b/crates/m-bus-core/src/lib.rs index 188fc8d..7949d03 100644 --- a/crates/m-bus-core/src/lib.rs +++ b/crates/m-bus-core/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(any(feature = "std", test)), no_std)] + pub mod decryption; /// Serializes raw byte payloads as compact uppercase hex strings so that From aafbfb4331f456f14fa96027f305455584a630a5 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 14:52:39 +0200 Subject: [PATCH 06/14] fixing build for no_std --- crates/wired-mbus-link-layer/src/lib.rs | 2 ++ crates/wireless-mbus-link-layer/src/lib.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/wired-mbus-link-layer/src/lib.rs b/crates/wired-mbus-link-layer/src/lib.rs index 8b6743a..49d7ce3 100644 --- a/crates/wired-mbus-link-layer/src/lib.rs +++ b/crates/wired-mbus-link-layer/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(any(feature = "std", test)), no_std)] + //! is part of the MBUS data link layer //! It is used to encapsulate the application layer data use m_bus_core::{FrameError, Function}; diff --git a/crates/wireless-mbus-link-layer/src/lib.rs b/crates/wireless-mbus-link-layer/src/lib.rs index c3b5652..fb4e8e0 100644 --- a/crates/wireless-mbus-link-layer/src/lib.rs +++ b/crates/wireless-mbus-link-layer/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(any(feature = "std", test)), no_std)] + use m_bus_core::{DeviceType, Function, IdentificationNumber, ManufacturerCode}; /// CRC-16/EN13757 used in wireless M-Bus Format A frames. From b8e1929503c626572e8ced875ef5ece469af558b Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 14:55:46 +0200 Subject: [PATCH 07/14] adding metrics for stack usage, decode latency and linked footprint --- .github/workflows/parser-resources.yml | 108 ++++++ .gitignore | 2 + README.md | 9 + benches/bench.rs | 15 +- benches/full_parse_fixture.rs | 36 ++ benches/stack-usage/Cargo.lock | 281 +++++++++++++++ benches/stack-usage/Cargo.toml | 25 ++ benches/stack-usage/README.md | 46 +++ benches/stack-usage/measure.py | 330 ++++++++++++++++++ .../stack-usage/src/bin/parser_footprint.rs | 36 ++ benches/stack-usage/src/lib.rs | 8 + 11 files changed, 886 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/parser-resources.yml create mode 100644 benches/full_parse_fixture.rs create mode 100644 benches/stack-usage/Cargo.lock create mode 100644 benches/stack-usage/Cargo.toml create mode 100644 benches/stack-usage/README.md create mode 100644 benches/stack-usage/measure.py create mode 100644 benches/stack-usage/src/bin/parser_footprint.rs create mode 100644 benches/stack-usage/src/lib.rs diff --git a/.github/workflows/parser-resources.yml b/.github/workflows/parser-resources.yml new file mode 100644 index 0000000..d29df1c --- /dev/null +++ b/.github/workflows/parser-resources.yml @@ -0,0 +1,108 @@ +name: Parser resources + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: parser-resources-${{ github.ref }} + cancel-in-progress: false + +jobs: + measure: + runs-on: ubuntu-latest + permissions: + contents: read + env: + STACK_USAGE_TOOLCHAIN: nightly-2026-05-16 + CARGO_TERM_COLOR: never + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install pinned Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + toolchain: nightly-2026-05-16 + components: llvm-tools-preview + targets: thumbv7em-none-eabi + - name: Measure parser resources + run: python3 benches/stack-usage/measure.py --output parser-resources.json + - name: Measure decode speed + run: | + cargo +nightly-2026-05-16 bench --locked --bench bench -- \ + parse_full_frame_eager --exact --noplot --discard-baseline \ + --output-format bencher \ + --sample-size 50 --warm-up-time 1 --measurement-time 3 \ + --nresamples 10000 2>&1 | tee decode-speed.txt + - name: Summarize parser resources + uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + with: + name: Parser stack and footprint + tool: customSmallerIsBetter + output-file-path: parser-resources.json + save-data-file: false + summary-always: true + - name: Summarize decode speed + uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + with: + name: Parser decode speed + tool: cargo + output-file-path: decode-speed.txt + save-data-file: false + summary-always: true + - name: Upload measurement + uses: actions/upload-artifact@v4 + with: + name: parser-resources + path: | + parser-resources.json + decode-speed.txt + retention-days: 7 + + publish: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: measure + runs-on: ubuntu-latest + permissions: + contents: write + deployments: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download measurement + uses: actions/download-artifact@v4 + with: + name: parser-resources + - name: Store and plot main-branch history + uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + with: + name: Parser stack and footprint + tool: customSmallerIsBetter + output-file-path: parser-resources.json + gh-pages-branch: main + benchmark-data-dir-path: docs/dev/bench + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: true + alert-threshold: '105%' + fail-threshold: '110%' + fail-on-alert: true + summary-always: true + max-items-in-chart: 100 + - name: Store and plot decode speed + uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + with: + name: Parser decode speed + tool: cargo + output-file-path: decode-speed.txt + gh-pages-branch: main + benchmark-data-dir-path: docs/dev/bench + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: true + alert-threshold: '125%' + summary-always: true + max-items-in-chart: 100 diff --git a/.gitignore b/.gitignore index 49a3732..6f1a686 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,12 @@ # will have compiled files and executables debug/ target/ +__pycache__/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock +!benches/stack-usage/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/README.md b/README.md index fec2ecb..b8b9c93 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![License](https://img.shields.io/crates/l/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![Documentation](https://docs.rs/m-bus-parser/badge.svg)](https://docs.rs/m-bus-parser) [![Build Status](https://github.com/maebli/m-bus-parser/actions/workflows/rust.yml/badge.svg)](https://github.com/maebli/m-bus-parser/actions/workflows/rust.yml) +[![Parser resources](https://github.com/maebli/m-bus-parser/actions/workflows/parser-resources.yml/badge.svg)](https://maebli.github.io/m-bus-parser/dev/bench/) *For contributing see [CONTRIBUTING.md](./CONTRIBUTING.md), for change history see [CHANGELOG.md](./CHANGELOG.md).* @@ -32,6 +33,14 @@ An open-source parser (decoder/deserializer) for the **wired** and **wireless** - **`no_std` compatible** — runs on embedded targets (manufacturer lookup and output formats require `std`) - Available as a **Rust library**, **CLI**, **WebAssembly (npm)** and **Python bindings** +Stack usage, linked footprint, and decode latency are measured by eagerly +parsing a wired frame and consuming all of its application-layer records with a +pinned compiler and dependency set. The badge above links to the +[per-commit resource charts](https://maebli.github.io/m-bus-parser/dev/bench/), +including the total critical path and each of its component frames. The +measurement method and local command are documented in +[`benches/stack-usage/`](./benches/stack-usage/). + --- ## Deployments diff --git a/benches/bench.rs b/benches/bench.rs index 1e972d6..c8b9426 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,8 +1,10 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use m_bus_parser::mbus_data::MbusData; use m_bus_parser::WiredFrame; use std::hint::black_box; +mod full_parse_fixture; +use full_parse_fixture::{parse_full_wired_frame, FULL_FRAME}; + #[allow(clippy::unwrap_used)] fn frame_parse_benchmark(c: &mut Criterion) { let data: Vec = vec![0x68, 0x04, 0x04, 0x68, 0x53, 0x01, 0x00, 0x00, 0x54, 0x16]; @@ -16,16 +18,9 @@ fn frame_parse_benchmark(c: &mut Criterion) { #[allow(clippy::unwrap_used)] fn m_bus_parser_benchmark(c: &mut Criterion) { - let data: Vec = vec![ - 0x68, 0x3C, 0x3C, 0x68, 0x08, 0x08, 0x72, 0x78, 0x03, 0x49, 0x11, 0x77, 0x04, 0x0E, 0x16, - 0x0A, 0x00, 0x00, 0x00, 0x0C, 0x78, 0x78, 0x03, 0x49, 0x11, 0x04, 0x13, 0x31, 0xD4, 0x00, - 0x00, 0x42, 0x6C, 0x00, 0x00, 0x44, 0x13, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6D, 0x0B, 0x0B, - 0xCD, 0x13, 0x02, 0x27, 0x00, 0x00, 0x09, 0xFD, 0x0E, 0x02, 0x09, 0xFD, 0x0F, 0x06, 0x0F, - 0x00, 0x01, 0x75, 0x13, 0xD3, 0x16, - ]; - c.bench_function("parse", |b| { + c.bench_function("parse_full_frame_eager", |b| { b.iter(|| { - MbusData::::try_from(data.as_slice()).unwrap(); + black_box(parse_full_wired_frame(black_box(&FULL_FRAME)).unwrap()); }) }); } diff --git a/benches/full_parse_fixture.rs b/benches/full_parse_fixture.rs new file mode 100644 index 0000000..3c637c1 --- /dev/null +++ b/benches/full_parse_fixture.rs @@ -0,0 +1,36 @@ +use m_bus_parser::{mbus_data::MbusData, MbusError, WiredFrame}; + +pub const FULL_FRAME: [u8; 66] = [ + 0x68, 0x3C, 0x3C, 0x68, 0x08, 0x08, 0x72, 0x78, 0x03, 0x49, 0x11, 0x77, 0x04, 0x0E, 0x16, 0x0A, + 0x00, 0x00, 0x00, 0x0C, 0x78, 0x78, 0x03, 0x49, 0x11, 0x04, 0x13, 0x31, 0xD4, 0x00, 0x00, 0x42, + 0x6C, 0x00, 0x00, 0x44, 0x13, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6D, 0x0B, 0x0B, 0xCD, 0x13, 0x02, + 0x27, 0x00, 0x00, 0x09, 0xFD, 0x0E, 0x02, 0x09, 0xFD, 0x0F, 0x06, 0x0F, 0x00, 0x01, 0x75, 0x13, + 0xD3, 0x16, +]; + +/// Parses the complete wired frame and eagerly consumes every data record. +#[inline(never)] +pub fn parse_full_wired_frame(data: &[u8]) -> Result { + let parsed = MbusData::::try_from(data)?; + if let Some(error) = parsed.application_error { + return Err(error.into()); + } + + let mut record_count = 0; + if let Some(records) = parsed.data_records { + for record in records { + let record = record?; + core::hint::black_box(record); + record_count += 1; + } + } + Ok(record_count) +} + +#[cfg(test)] +mod tests { + #[test] + fn consumes_every_record() { + assert_eq!(super::parse_full_wired_frame(&super::FULL_FRAME), Ok(9)); + } +} diff --git a/benches/stack-usage/Cargo.lock b/benches/stack-usage/Cargo.lock new file mode 100644 index 0000000..500c564 --- /dev/null +++ b/benches/stack-usage/Cargo.lock @@ -0,0 +1,281 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "m-bus-application-layer" +version = "0.4.0" +dependencies = [ + "arrayvec", + "bitflags", + "m-bus-core", +] + +[[package]] +name = "m-bus-core" +version = "0.4.0" + +[[package]] +name = "m-bus-parser" +version = "0.4.3" +dependencies = [ + "arrayvec", + "bindgen", + "bitflags", + "m-bus-application-layer", + "m-bus-core", + "wired-mbus-link-layer", + "wireless-mbus-link-layer", +] + +[[package]] +name = "m-bus-stack-usage" +version = "0.0.0" +dependencies = [ + "m-bus-parser", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "wired-mbus-link-layer" +version = "0.4.0" +dependencies = [ + "m-bus-core", +] + +[[package]] +name = "wireless-mbus-link-layer" +version = "0.4.0" +dependencies = [ + "crc16", + "m-bus-core", +] diff --git a/benches/stack-usage/Cargo.toml b/benches/stack-usage/Cargo.toml new file mode 100644 index 0000000..a20b3b9 --- /dev/null +++ b/benches/stack-usage/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "m-bus-stack-usage" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +m-bus-parser = { path = "../.." } + +# Keep this benchmark independent from the repository workspace so its lockfile +# freezes the dependency graph used by the historical measurements. +[workspace] + +[profile.release] +opt-level = "z" +lto = false +codegen-units = 1 +debug = false +strip = false + +[[bin]] +name = "parser-footprint" +path = "src/bin/parser_footprint.rs" +test = false +bench = false diff --git a/benches/stack-usage/README.md b/benches/stack-usage/README.md new file mode 100644 index 0000000..9b8f27a --- /dev/null +++ b/benches/stack-usage/README.md @@ -0,0 +1,46 @@ +# Parser resource benchmark + +This benchmark eagerly parses one representative wired M-Bus frame and consumes +all nine application-layer data records. It uses a pinned compiler and +dependency lockfile and does not use QEMU. + +## Metrics + +- Full-parse stack, its nested setup and record paths, and each local frame on + the deepest path, compiled for + `thumbv7em-none-eabi` and read from LLVM `.stack_sizes` metadata. +- `DataRecord` and `ValueInformationBlock` value sizes on the same Thumb build. +- Linked eager-parser text and data size for `thumbv7em-none-eabi`, using + `opt-level=z`, fat LTO, and one codegen unit. +- The same eager full-frame decode latency, run natively with Criterion. + +Frame/application setup and record iteration are sequential, so the full stack +uses the larger nested path rather than adding both: + +```text +parse_full_wired_frame + max( + MbusData::try_from -> frame/application setup, + DataRecords::next -> DataRecord::try_from -> DataRecord::parse -> + DataRecordHeader::try_from -> ProcessedDataRecordHeader::try_from -> + ValueInformation::try_from -> consume_orthhogonal_vife +) +``` + +LLVM stack sizes are deterministic static lower bounds. They exclude interrupt +handlers, dynamic dispatch, call bookkeeping, and code outside this path. + +## Run locally + +Install the pinned nightly toolchain, LLVM tools, and Thumb target, then run: + +```console +rustup component add llvm-tools-preview --toolchain nightly-2026-05-16 +rustup target add thumbv7em-none-eabi --toolchain nightly-2026-05-16 +STACK_USAGE_TOOLCHAIN=nightly-2026-05-16 python3 benches/stack-usage/measure.py \ + --output parser-resources.json +cargo +nightly-2026-05-16 bench --bench bench -- parse_full_frame_eager --exact +``` + +Pull requests show the current values in the Actions summary and artifact. +Pushes to `main` add them to the +[parser resource trend dashboard](https://maebli.github.io/m-bus-parser/dev/bench/). diff --git a/benches/stack-usage/measure.py b/benches/stack-usage/measure.py new file mode 100644 index 0000000..e1c9ae0 --- /dev/null +++ b/benches/stack-usage/measure.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +"""Emit Thumb stack, type-size, and linked-footprint benchmark metrics.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + + +HERE = Path(__file__).resolve().parent +MANIFEST = HERE / "Cargo.toml" +TARGET = "thumbv7em-none-eabi" + +CRITICAL_PATH = ( + ("record_iteration", "DataRecords::next"), + ("data_record_try_from", "DataRecord::try_from"), + ("data_record_parse", "DataRecord::parse"), + ("record_header_parse", "DataRecordHeader::try_from"), + ("processed_header_parse", "ProcessedDataRecordHeader::try_from"), + ("value_information_parse", "ValueInformation::try_from"), + ("vife_consumer", "consume_orthhogonal_vife"), +) + +FRAME_SYMBOLS = { + "full_parse": "m_bus_stack_usage::full_parse_fixture::parse_full_wired_frame", + "mbus_parse": ( + " as " + "core::convert::TryFrom<&[u8]>>::try_from" + ), + "wired_frame_parse": ( + ">::try_from" + ), + "checksum": "wired_mbus_link_layer::validate_checksum", + "user_data_parse": ( + ">::try_from" + ), + "identification_parse": ( + "::from_bcd_hex_digits" + ), + "bcd_parse": "m_bus_core::bcd_hex_digits_to_u32", + "record_iteration": ( + "::next" + ), + "vif_block_parse": ( + ">::try_from" + ), + "record_header_parse": ( + ">::try_from" + ), + "processed_header_parse": ( + ">::try_from" + ), + "value_information_parse": ( + ">::try_from" + ), + "data_record_parse": "::parse", +} + + +def run(command: list[str], *, cwd: Path, env: dict[str, str]) -> str: + completed = subprocess.run( + command, + cwd=cwd, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + if completed.returncode: + sys.stderr.write(completed.stdout) + raise SystemExit(completed.returncode) + return completed.stdout + + +def one_file(paths: list[Path], description: str) -> Path: + if len(paths) != 1: + raise SystemExit(f"expected one {description}, found {len(paths)}") + return paths[0] + + +def parse_stack_sizes(output: str) -> dict[str, int]: + entries = re.findall( + r"Entry \{\s+Functions: \[(.*?)\]\s+Size: (0x[0-9A-Fa-f]+)\s+\}", + output, + flags=re.DOTALL, + ) + sizes = {symbol.strip(): int(size, 16) for symbol, size in entries} + + frames: dict[str, int] = {} + for name, symbol in FRAME_SYMBOLS.items(): + if symbol not in sizes: + raise SystemExit(f"stack size not found for {symbol}") + frames[name] = sizes[symbol] + + patterns = { + "data_record_try_from": ( + "::try_from", + ), + "vife_consumer": ( + "m_bus_application_layer::value_information::consume_orthhogonal_vife", + "", + ), + } + for name, (prefix, suffix) in patterns.items(): + matches = [ + size + for symbol, size in sizes.items() + if symbol.startswith(prefix) and symbol.endswith(suffix) + ] + if not matches: + raise SystemExit(f"stack size not found for {name}") + frames[name] = max(matches) + + return frames + + +def parse_type_size(output: str, pattern: str) -> int: + match = re.search( + rf"print-type-size type: `{pattern}`: ([0-9]+) bytes", + output, + ) + if match is None: + raise SystemExit(f"type size not found for {pattern}") + return int(match.group(1)) + + +def parse_footprint(output: str) -> int: + rows = [line.split() for line in output.splitlines() if line.strip()] + if len(rows) < 2 or rows[0][:3] != ["text", "data", "bss"]: + raise SystemExit("unexpected llvm-size output") + return int(rows[1][0]) + int(rows[1][1]) + + +def metric(name: str, value: int, extra: str) -> dict[str, object]: + return {"name": name, "unit": "bytes", "value": value, "extra": extra} + + +def measure_stack(temp: Path, base_env: dict[str, str]) -> tuple[dict[str, int], str]: + target_dir = temp / "stack-target" + env = base_env | { + "CARGO_TARGET_DIR": str(target_dir), + "RUSTFLAGS": " ".join( + ( + "-Z emit-stack-sizes", + "-Z print-type-sizes", + "-C link-dead-code=yes", + "-C embed-bitcode=no", + ) + ), + } + build_output = run( + [ + "cargo", + "build", + "--manifest-path", + str(MANIFEST), + "--locked", + "--release", + "--lib", + "--target", + TARGET, + ], + cwd=HERE, + env=env, + ) + + deps = target_dir / TARGET / "release" / "deps" + objects_dir = temp / "objects" + objects_dir.mkdir() + objects: list[Path] = [] + for crate in ( + "m_bus_stack_usage", + "m_bus_parser", + "wired_mbus_link_layer", + "m_bus_application_layer", + "m_bus_core", + ): + archive = one_file(sorted(deps.glob(f"lib{crate}-*.rlib")), f"{crate} archive") + crate_objects = objects_dir / crate + crate_objects.mkdir() + run(["rust-ar", "x", str(archive)], cwd=crate_objects, env=env) + objects.extend(sorted(crate_objects.glob("*.o"))) + if not objects: + raise SystemExit("no object files found in parser archives") + stack_output = run( + ["rust-readobj", "--stack-sizes", "--demangle", *map(str, objects)], + cwd=objects_dir, + env=env, + ) + return parse_stack_sizes(stack_output), build_output + + +def measure_footprint(temp: Path, base_env: dict[str, str]) -> int: + target_dir = temp / "footprint-target" + env = base_env | { + "CARGO_TARGET_DIR": str(target_dir), + "CARGO_PROFILE_RELEASE_OPT_LEVEL": "z", + "CARGO_PROFILE_RELEASE_LTO": "true", + "CARGO_PROFILE_RELEASE_CODEGEN_UNITS": "1", + "CARGO_PROFILE_RELEASE_PANIC": "abort", + } + env.pop("RUSTFLAGS", None) + run( + [ + "cargo", + "build", + "--manifest-path", + str(MANIFEST), + "--locked", + "--release", + "--bin", + "parser-footprint", + "--target", + TARGET, + ], + cwd=HERE, + env=env, + ) + + host_libdir = Path( + run(["rustc", "--print", "target-libdir"], cwd=HERE, env=base_env).strip() + ) + llvm_size = host_libdir.parent / "bin" / "llvm-size" + if not llvm_size.is_file(): + raise SystemExit(f"llvm-size not found at {llvm_size}") + binary = target_dir / TARGET / "release" / "parser-footprint" + output = run( + [str(llvm_size), "--format=berkeley", str(binary)], + cwd=HERE, + env=base_env, + ) + return parse_footprint(output) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, default=Path("parser-resources.json")) + args = parser.parse_args() + + toolchain = os.environ.get("STACK_USAGE_TOOLCHAIN", "nightly") + base_env = os.environ.copy() + base_env["RUSTUP_TOOLCHAIN"] = toolchain + + with tempfile.TemporaryDirectory(prefix="m-bus-parser-resources-") as temp: + temp_path = Path(temp) + frames, build_output = measure_stack(temp_path, base_env) + footprint = measure_footprint(temp_path, base_env) + + record_stack = sum(frames[name] for name, _ in CRITICAL_PATH) + wired_setup_stack = frames["wired_frame_parse"] + frames["checksum"] + application_setup_stack = ( + frames["user_data_parse"] + + frames["identification_parse"] + + frames["bcd_parse"] + ) + setup_stack = frames["mbus_parse"] + max( + wired_setup_stack, + application_setup_stack, + ) + full_stack = frames["full_parse"] + max(setup_stack, record_stack) + data_record_size = parse_type_size(build_output, r"data_record::DataRecord<'_>") + vif_block_size = parse_type_size( + build_output, + r"value_information::ValueInformationBlock(?:<'_>)?", + ) + context = f"target={TARGET}; toolchain={toolchain}" + setup_context = ( + f"{context}; mbus={frames['mbus_parse']} B; " + f"wired={wired_setup_stack} B; application={application_setup_stack} B" + ) + metrics = [ + metric( + "Eager full wired-frame parse stack", + full_stack, + ( + f"{context}; fixture={frames['full_parse']} B; " + f"max(setup={setup_stack} B,records={record_stack} B)" + ), + ), + metric("Frame and application setup nested stack", setup_stack, setup_context), + metric("Record decode nested stack", record_stack, context), + metric( + "Eager full-frame fixture local stack frame", + frames["full_parse"], + context, + ), + *[ + metric(f"{label} local stack frame", frames[name], context) + for name, label in CRITICAL_PATH + ], + metric( + "VIF block parser local stack frame", + frames["vif_block_parse"], + context, + ), + metric("DataRecord value size", data_record_size, context), + metric("VIF block value size", vif_block_size, context), + metric( + "Linked eager full parser text + data size", + footprint, + ( + f"{context}; profile=opt-level=z,lto=fat,codegen-units=1; " + "sections=text+data" + ), + ), + ] + args.output.write_text(json.dumps(metrics, indent=2) + "\n", encoding="utf-8") + print(f"Wrote {len(metrics)} metrics to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/benches/stack-usage/src/bin/parser_footprint.rs b/benches/stack-usage/src/bin/parser_footprint.rs new file mode 100644 index 0000000..27a8a64 --- /dev/null +++ b/benches/stack-usage/src/bin/parser_footprint.rs @@ -0,0 +1,36 @@ +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +use m_bus_stack_usage::{parse_full_wired_frame, FULL_FRAME}; + +#[cfg(target_os = "none")] +use core::hint::black_box; +#[cfg(not(target_os = "none"))] +use std::hint::black_box; + +fn parse_frame() { + let result = parse_full_wired_frame(black_box(&FULL_FRAME)); + let _ = black_box(result); +} + +#[cfg(not(target_os = "none"))] +fn main() { + parse_frame(); +} + +#[cfg(target_os = "none")] +#[no_mangle] +pub extern "C" fn _start() -> ! { + parse_frame(); + loop { + core::hint::spin_loop(); + } +} + +#[cfg(target_os = "none")] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo<'_>) -> ! { + loop { + core::hint::spin_loop(); + } +} diff --git a/benches/stack-usage/src/lib.rs b/benches/stack-usage/src/lib.rs new file mode 100644 index 0000000..240895f --- /dev/null +++ b/benches/stack-usage/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] + +//! Build-only fixture used by `measure.py`. + +#[path = "../../full_parse_fixture.rs"] +mod full_parse_fixture; + +pub use full_parse_fixture::{parse_full_wired_frame, FULL_FRAME}; From cc62ad31f468d9e7de0de738047d5fda0ec18061 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 17:36:27 +0200 Subject: [PATCH 08/14] fixing measurement pipeline --- benches/stack-usage/measure.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/benches/stack-usage/measure.py b/benches/stack-usage/measure.py index e1c9ae0..ebe3938 100644 --- a/benches/stack-usage/measure.py +++ b/benches/stack-usage/measure.py @@ -94,6 +94,18 @@ def one_file(paths: list[Path], description: str) -> Path: return paths[0] +def llvm_tool(name: str, env: dict[str, str]) -> Path: + host_libdir = Path( + run(["rustc", "--print", "target-libdir"], cwd=HERE, env=env).strip() + ) + tool = host_libdir.parent / "bin" / name + if not tool.is_file(): + raise SystemExit( + f"{name} not found at {tool}; install the llvm-tools-preview component" + ) + return tool + + def parse_stack_sizes(output: str) -> dict[str, int]: entries = re.findall( r"Entry \{\s+Functions: \[(.*?)\]\s+Size: (0x[0-9A-Fa-f]+)\s+\}", @@ -154,6 +166,8 @@ def metric(name: str, value: int, extra: str) -> dict[str, object]: def measure_stack(temp: Path, base_env: dict[str, str]) -> tuple[dict[str, int], str]: + llvm_ar = llvm_tool("llvm-ar", base_env) + llvm_readobj = llvm_tool("llvm-readobj", base_env) target_dir = temp / "stack-target" env = base_env | { "CARGO_TARGET_DIR": str(target_dir), @@ -196,12 +210,12 @@ def measure_stack(temp: Path, base_env: dict[str, str]) -> tuple[dict[str, int], archive = one_file(sorted(deps.glob(f"lib{crate}-*.rlib")), f"{crate} archive") crate_objects = objects_dir / crate crate_objects.mkdir() - run(["rust-ar", "x", str(archive)], cwd=crate_objects, env=env) + run([str(llvm_ar), "x", str(archive)], cwd=crate_objects, env=env) objects.extend(sorted(crate_objects.glob("*.o"))) if not objects: raise SystemExit("no object files found in parser archives") stack_output = run( - ["rust-readobj", "--stack-sizes", "--demangle", *map(str, objects)], + [str(llvm_readobj), "--stack-sizes", "--demangle", *map(str, objects)], cwd=objects_dir, env=env, ) @@ -235,12 +249,7 @@ def measure_footprint(temp: Path, base_env: dict[str, str]) -> int: env=env, ) - host_libdir = Path( - run(["rustc", "--print", "target-libdir"], cwd=HERE, env=base_env).strip() - ) - llvm_size = host_libdir.parent / "bin" / "llvm-size" - if not llvm_size.is_file(): - raise SystemExit(f"llvm-size not found at {llvm_size}") + llvm_size = llvm_tool("llvm-size", base_env) binary = target_dir / TARGET / "release" / "parser-footprint" output = run( [str(llvm_size), "--format=berkeley", str(binary)], From 1fb83d9df9a06ce3ce8cfb4bbb71fae2660e98e8 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 17:43:43 +0200 Subject: [PATCH 09/14] fix benchmark workflow publishing --- .github/workflows/parser-resources.yml | 35 +++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/parser-resources.yml b/.github/workflows/parser-resources.yml index d29df1c..bdacbf6 100644 --- a/.github/workflows/parser-resources.yml +++ b/.github/workflows/parser-resources.yml @@ -23,6 +23,15 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Check for benchmark history + id: benchmark-history + shell: bash + run: | + if git ls-remote --exit-code --heads origin gh-pages >/dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi - name: Install pinned Rust toolchain uses: dtolnay/rust-toolchain@nightly with: @@ -39,7 +48,8 @@ jobs: --sample-size 50 --warm-up-time 1 --measurement-time 3 \ --nresamples 10000 2>&1 | tee decode-speed.txt - name: Summarize parser resources - uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + if: steps.benchmark-history.outputs.exists == 'true' + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: name: Parser stack and footprint tool: customSmallerIsBetter @@ -47,7 +57,8 @@ jobs: save-data-file: false summary-always: true - name: Summarize decode speed - uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + if: steps.benchmark-history.outputs.exists == 'true' + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: name: Parser decode speed tool: cargo @@ -74,17 +85,29 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Initialize benchmark history branch + shell: bash + run: | + if ! git ls-remote --exit-code --heads origin gh-pages >/dev/null; then + source_ref=$(git rev-parse HEAD) + git switch --orphan gh-pages + git -c user.name=github-action-benchmark \ + -c user.email=github@users.noreply.github.com \ + commit --allow-empty -m "Initialize benchmark history" + git push origin gh-pages + git checkout --detach "$source_ref" + fi - name: Download measurement uses: actions/download-artifact@v4 with: name: parser-resources - name: Store and plot main-branch history - uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: name: Parser stack and footprint tool: customSmallerIsBetter output-file-path: parser-resources.json - gh-pages-branch: main + gh-pages-branch: gh-pages benchmark-data-dir-path: docs/dev/bench github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true @@ -94,12 +117,12 @@ jobs: summary-always: true max-items-in-chart: 100 - name: Store and plot decode speed - uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0 + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: name: Parser decode speed tool: cargo output-file-path: decode-speed.txt - gh-pages-branch: main + gh-pages-branch: gh-pages benchmark-data-dir-path: docs/dev/bench github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true From 0859111b476825b30ac98b4c650f798f44cf6060 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 18:01:04 +0200 Subject: [PATCH 10/14] track workspace lockfile for benchmarks --- .gitignore | 4 +- Cargo.lock | 1788 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1790 insertions(+), 2 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 6f1a686..a61bbe8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,9 @@ debug/ target/ __pycache__/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +# Keep the workspace and resource-benchmark dependency graphs reproducible in CI. Cargo.lock +!/Cargo.lock !benches/stack-usage/Cargo.lock # These are backup files generated by rustfmt diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..267c6bb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1788 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead 0.6.1", + "aes", + "cipher 0.5.2", + "ctr 0.10.1", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags 2.8.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead 0.5.2", + "cipher 0.4.4", + "ctr 0.9.2", + "subtle", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "is-terminal" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.8.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "m-bus-application-layer" +version = "0.4.0" +dependencies = [ + "arrayvec", + "bitflags 2.8.0", + "defmt", + "m-bus-core", + "serde", +] + +[[package]] +name = "m-bus-core" +version = "0.4.0" +dependencies = [ + "aes", + "cbc", + "cipher 0.5.2", + "defmt", + "serde", +] + +[[package]] +name = "m-bus-parser" +version = "0.4.3" +dependencies = [ + "aes", + "aes-gcm", + "arrayvec", + "bindgen", + "bitflags 2.8.0", + "cbc", + "ccm", + "cipher 0.5.2", + "criterion", + "defmt", + "hex", + "m-bus-application-layer", + "m-bus-core", + "prettytable-rs", + "serde", + "serde-xml-rs", + "serde_derive", + "serde_json", + "serde_yaml", + "unicode-width 0.2.2", + "walkdir", + "wired-mbus-link-layer", + "wireless-mbus-link-layer", +] + +[[package]] +name = "m-bus-parser-cli" +version = "0.4.3" +dependencies = [ + "clap", + "hex", + "m-bus-parser", + "terminal_size", +] + +[[package]] +name = "m-bus-parser-wasm-pack" +version = "0.4.3" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "m-bus-parser", + "serde", + "serde-wasm-bindgen", + "serde_json", + "syntect", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polyval" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b20f20e954175de5f463f67781b35583397d916b1d148738923711b2ad16bee8" +dependencies = [ + "cpubits", + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width 0.1.14", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pymbusparser" +version = "0.4.3" +dependencies = [ + "hex", + "m-bus-parser", + "pyo3", + "serde_json", +] + +[[package]] +name = "pyo3" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror 2.0.12", + "xml", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "regex-syntax", + "serde", + "serde_derive", + "thiserror 2.0.12", + "walkdir", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wired-mbus-link-layer" +version = "0.4.0" +dependencies = [ + "defmt", + "m-bus-core", + "serde", +] + +[[package]] +name = "wireless-mbus-link-layer" +version = "0.4.0" +dependencies = [ + "crc16", + "defmt", + "m-bus-core", + "serde", +] + +[[package]] +name = "xml" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" From c0be7fcc14561f9c64d798381f47c446373f091d Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 18:08:43 +0200 Subject: [PATCH 11/14] update crossbeam-epoch to patched release --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 267c6bb..4e7753e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,9 +474,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] From d7acec89bc3aa94a0da70843435171e05faab9af Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Sat, 29 Aug 2026 18:29:06 +0200 Subject: [PATCH 12/14] fix locked decode speed benchmark --- .github/workflows/parser-resources.yml | 1 + Cargo.lock | 102 +++++-------------------- 2 files changed, 20 insertions(+), 83 deletions(-) diff --git a/.github/workflows/parser-resources.yml b/.github/workflows/parser-resources.yml index bdacbf6..09fb9ad 100644 --- a/.github/workflows/parser-resources.yml +++ b/.github/workflows/parser-resources.yml @@ -42,6 +42,7 @@ jobs: run: python3 benches/stack-usage/measure.py --output parser-resources.json - name: Measure decode speed run: | + set -o pipefail cargo +nightly-2026-05-16 bench --locked --bench bench -- \ parse_full_frame_eager --exact --noplot --discard-baseline \ --output-format bencher \ diff --git a/Cargo.lock b/Cargo.lock index 4e7753e..21412d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,24 +8,14 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - [[package]] name = "aead" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common 0.2.2", - "inout 0.2.2", + "crypto-common", + "inout", ] [[package]] @@ -34,7 +24,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cipher 0.5.2", + "cipher", "cpubits", "cpufeatures", ] @@ -45,10 +35,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ - "aead 0.6.1", + "aead", "aes", - "cipher 0.5.2", - "ctr 0.10.1", + "cipher", + "ctr", "ghash", "subtle", ] @@ -237,7 +227,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher 0.5.2", + "cipher", ] [[package]] @@ -251,14 +241,14 @@ dependencies = [ [[package]] name = "ccm" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +checksum = "0be9b34d99d395af800e996c5784f810f8400ccd64202f91859dc677825b5378" dependencies = [ - "aead 0.5.2", - "cipher 0.4.4", - "ctr 0.9.2", - "subtle", + "aead", + "cipher", + "ctr", + "ctutils", ] [[package]] @@ -303,16 +293,6 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout 0.1.4", -] - [[package]] name = "cipher" version = "0.5.2" @@ -320,8 +300,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer", - "crypto-common 0.2.2", - "inout 0.2.2", + "crypto-common", + "inout", ] [[package]] @@ -493,16 +473,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -533,22 +503,13 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "ctr" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher 0.5.2", + "cipher", ] [[package]] @@ -668,16 +629,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -757,15 +708,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "inout" version = "0.2.2" @@ -879,7 +821,7 @@ version = "0.4.0" dependencies = [ "aes", "cbc", - "cipher 0.5.2", + "cipher", "defmt", "serde", ] @@ -895,7 +837,7 @@ dependencies = [ "bitflags 2.8.0", "cbc", "ccm", - "cipher 0.5.2", + "cipher", "criterion", "defmt", "hex", @@ -1515,7 +1457,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.2.2", + "crypto-common", "ctutils", ] @@ -1531,12 +1473,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" From 4182d6dcde351967862bc3ed575f172e73bbadf5 Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Tue, 1 Sep 2026 18:47:33 +0200 Subject: [PATCH 13/14] rename stack benchmark to parser resources --- .github/workflows/parser-resources.yml | 4 ++-- .gitignore | 2 +- README.md | 2 +- benches/{stack-usage => parser-resources}/Cargo.lock | 2 +- benches/{stack-usage => parser-resources}/Cargo.toml | 2 +- benches/{stack-usage => parser-resources}/README.md | 2 +- benches/{stack-usage => parser-resources}/measure.py | 6 +++--- .../src/bin/parser_footprint.rs | 2 +- benches/{stack-usage => parser-resources}/src/lib.rs | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) rename benches/{stack-usage => parser-resources}/Cargo.lock (99%) rename benches/{stack-usage => parser-resources}/Cargo.toml (93%) rename benches/{stack-usage => parser-resources}/README.md (95%) rename benches/{stack-usage => parser-resources}/measure.py (98%) rename benches/{stack-usage => parser-resources}/src/bin/parser_footprint.rs (91%) rename benches/{stack-usage => parser-resources}/src/lib.rs (68%) diff --git a/.github/workflows/parser-resources.yml b/.github/workflows/parser-resources.yml index 09fb9ad..27a19db 100644 --- a/.github/workflows/parser-resources.yml +++ b/.github/workflows/parser-resources.yml @@ -17,7 +17,7 @@ jobs: permissions: contents: read env: - STACK_USAGE_TOOLCHAIN: nightly-2026-05-16 + PARSER_RESOURCES_TOOLCHAIN: nightly-2026-05-16 CARGO_TERM_COLOR: never steps: - uses: actions/checkout@v4 @@ -39,7 +39,7 @@ jobs: components: llvm-tools-preview targets: thumbv7em-none-eabi - name: Measure parser resources - run: python3 benches/stack-usage/measure.py --output parser-resources.json + run: python3 benches/parser-resources/measure.py --output parser-resources.json - name: Measure decode speed run: | set -o pipefail diff --git a/.gitignore b/.gitignore index a61bbe8..6d48847 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ __pycache__/ # Keep the workspace and resource-benchmark dependency graphs reproducible in CI. Cargo.lock !/Cargo.lock -!benches/stack-usage/Cargo.lock +!benches/parser-resources/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/README.md b/README.md index b8b9c93..2597871 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ pinned compiler and dependency set. The badge above links to the [per-commit resource charts](https://maebli.github.io/m-bus-parser/dev/bench/), including the total critical path and each of its component frames. The measurement method and local command are documented in -[`benches/stack-usage/`](./benches/stack-usage/). +[`benches/parser-resources/`](./benches/parser-resources/). --- diff --git a/benches/stack-usage/Cargo.lock b/benches/parser-resources/Cargo.lock similarity index 99% rename from benches/stack-usage/Cargo.lock rename to benches/parser-resources/Cargo.lock index 500c564..689050a 100644 --- a/benches/stack-usage/Cargo.lock +++ b/benches/parser-resources/Cargo.lock @@ -145,7 +145,7 @@ dependencies = [ ] [[package]] -name = "m-bus-stack-usage" +name = "m-bus-parser-resources" version = "0.0.0" dependencies = [ "m-bus-parser", diff --git a/benches/stack-usage/Cargo.toml b/benches/parser-resources/Cargo.toml similarity index 93% rename from benches/stack-usage/Cargo.toml rename to benches/parser-resources/Cargo.toml index a20b3b9..4faa2e8 100644 --- a/benches/stack-usage/Cargo.toml +++ b/benches/parser-resources/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "m-bus-stack-usage" +name = "m-bus-parser-resources" version = "0.0.0" edition = "2021" publish = false diff --git a/benches/stack-usage/README.md b/benches/parser-resources/README.md similarity index 95% rename from benches/stack-usage/README.md rename to benches/parser-resources/README.md index 9b8f27a..d3929f6 100644 --- a/benches/stack-usage/README.md +++ b/benches/parser-resources/README.md @@ -36,7 +36,7 @@ Install the pinned nightly toolchain, LLVM tools, and Thumb target, then run: ```console rustup component add llvm-tools-preview --toolchain nightly-2026-05-16 rustup target add thumbv7em-none-eabi --toolchain nightly-2026-05-16 -STACK_USAGE_TOOLCHAIN=nightly-2026-05-16 python3 benches/stack-usage/measure.py \ +PARSER_RESOURCES_TOOLCHAIN=nightly-2026-05-16 python3 benches/parser-resources/measure.py \ --output parser-resources.json cargo +nightly-2026-05-16 bench --bench bench -- parse_full_frame_eager --exact ``` diff --git a/benches/stack-usage/measure.py b/benches/parser-resources/measure.py similarity index 98% rename from benches/stack-usage/measure.py rename to benches/parser-resources/measure.py index ebe3938..016a9eb 100644 --- a/benches/stack-usage/measure.py +++ b/benches/parser-resources/measure.py @@ -28,7 +28,7 @@ ) FRAME_SYMBOLS = { - "full_parse": "m_bus_stack_usage::full_parse_fixture::parse_full_wired_frame", + "full_parse": "m_bus_parser_resources::full_parse_fixture::parse_full_wired_frame", "mbus_parse": ( " as " "core::convert::TryFrom<&[u8]>>::try_from" @@ -201,7 +201,7 @@ def measure_stack(temp: Path, base_env: dict[str, str]) -> tuple[dict[str, int], objects_dir.mkdir() objects: list[Path] = [] for crate in ( - "m_bus_stack_usage", + "m_bus_parser_resources", "m_bus_parser", "wired_mbus_link_layer", "m_bus_application_layer", @@ -264,7 +264,7 @@ def main() -> None: parser.add_argument("--output", type=Path, default=Path("parser-resources.json")) args = parser.parse_args() - toolchain = os.environ.get("STACK_USAGE_TOOLCHAIN", "nightly") + toolchain = os.environ.get("PARSER_RESOURCES_TOOLCHAIN", "nightly") base_env = os.environ.copy() base_env["RUSTUP_TOOLCHAIN"] = toolchain diff --git a/benches/stack-usage/src/bin/parser_footprint.rs b/benches/parser-resources/src/bin/parser_footprint.rs similarity index 91% rename from benches/stack-usage/src/bin/parser_footprint.rs rename to benches/parser-resources/src/bin/parser_footprint.rs index 27a8a64..743b6e1 100644 --- a/benches/stack-usage/src/bin/parser_footprint.rs +++ b/benches/parser-resources/src/bin/parser_footprint.rs @@ -1,7 +1,7 @@ #![cfg_attr(target_os = "none", no_std)] #![cfg_attr(target_os = "none", no_main)] -use m_bus_stack_usage::{parse_full_wired_frame, FULL_FRAME}; +use m_bus_parser_resources::{parse_full_wired_frame, FULL_FRAME}; #[cfg(target_os = "none")] use core::hint::black_box; diff --git a/benches/stack-usage/src/lib.rs b/benches/parser-resources/src/lib.rs similarity index 68% rename from benches/stack-usage/src/lib.rs rename to benches/parser-resources/src/lib.rs index 240895f..b530608 100644 --- a/benches/stack-usage/src/lib.rs +++ b/benches/parser-resources/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -//! Build-only fixture used by `measure.py`. +//! Build-only fixture used by the parser resource measurements. #[path = "../../full_parse_fixture.rs"] mod full_parse_fixture; From ce67ad90e7ff166378df20b136694ac2194cc18b Mon Sep 17 00:00:00 2001 From: Michael Aebli Date: Tue, 1 Sep 2026 22:52:32 +0200 Subject: [PATCH 14/14] fix: preserve borrowed VIF serde output --- .../src/value_information.rs | 30 +++- tests/serde_compat.rs | 152 ++++++++++++++++++ 2 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 tests/serde_compat.rs diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 124bbe1..2e36f3d 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -118,10 +118,21 @@ impl ValueInformationField { } } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ValueInformationFieldExtensions<'a>(&'a [u8]); + +#[cfg(feature = "serde")] +impl serde::Serialize for ValueInformationFieldExtensions<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_seq(self.iter()) + } +} + impl<'a> ValueInformationFieldExtensions<'a> { fn new(data: &'a [u8]) -> Result { let Some(last_index) = data @@ -181,10 +192,25 @@ impl<'a> ValueInformationFieldExtensions<'a> { } } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PlainTextValueInformationExtension<'a>(&'a [u8]); + +#[cfg(feature = "serde")] +impl serde::Serialize for PlainTextValueInformationExtension<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let plaintext = self.as_ascii_str().ok_or_else(|| { + ::custom("invalid plaintext VIFE encoding") + })?; + + serializer.collect_seq(plaintext.chars()) + } +} + impl<'a> PlainTextValueInformationExtension<'a> { fn new(data: &'a [u8]) -> Result { let ascii_len = usize::from(*data.first().ok_or(DataInformationError::DataTooShort)?); diff --git a/tests/serde_compat.rs b/tests/serde_compat.rs new file mode 100644 index 0000000..a4d426a --- /dev/null +++ b/tests/serde_compat.rs @@ -0,0 +1,152 @@ +#![cfg(feature = "std")] +#![allow(clippy::unwrap_used)] + +use m_bus_parser::{serialize_mbus_data, user_data::value_information::ValueInformationBlock}; +use serde_json::{json, Value}; + +#[cfg(not(feature = "plaintext-before-extension"))] +const PLAINTEXT_VIF: &[u8] = &[0xFC, 0x74, 0x02, b'A', b'B']; +#[cfg(feature = "plaintext-before-extension")] +const PLAINTEXT_VIF: &[u8] = &[0xFC, 0x02, b'A', b'B', 0x74]; + +#[cfg(not(feature = "plaintext-before-extension"))] +const LEGACY_FRAME: &str = concat!( + "68 4D 4D 68 08 01 72 01 00 00 00 96 15 01 00 18 00 00 00 ", + "0C 78 56 00 00 00 01 FD 1B 00 02 FC 74 03 48 52 25 44 0D ", + "22 FC 74 03 48 52 25 F1 0C 12 FC 74 03 48 52 25 63 11 02 ", + "65 B4 09 22 65 86 09 12 65 B7 09 01 72 00 72 65 00 00 B2 ", + "01 65 00 00 1F B3 16" +); +#[cfg(feature = "plaintext-before-extension")] +const LEGACY_FRAME: &str = concat!( + "68 4D 4D 68 08 01 72 01 00 00 00 96 15 01 00 18 00 00 00 ", + "0C 78 56 00 00 00 01 FD 1B 00 02 FC 03 48 52 25 74 44 0D ", + "22 FC 03 48 52 25 74 F1 0C 12 FC 03 48 52 25 74 63 11 02 ", + "65 B4 09 22 65 86 09 12 65 B7 09 01 72 00 72 65 00 00 B2 ", + "01 65 00 00 1F B3 16" +); + +#[test] +fn borrowed_vif_fields_preserve_their_logical_serde_shape() { + let extension_bytes = [0xFD, 0xD9, 0xFC, 0x01]; + let extensions = ValueInformationBlock::try_from(extension_bytes.as_slice()).unwrap(); + + assert_eq!( + serde_json::to_value(extensions).unwrap(), + json!({ + "value_information": { "data": 253 }, + "value_information_extension": [ + { "data": 217 }, + { "data": 252 }, + { "data": 1 } + ], + "plaintext_vife": null + }) + ); + + let plaintext = ValueInformationBlock::try_from(PLAINTEXT_VIF).unwrap(); + + assert_eq!( + serde_json::to_value(plaintext).unwrap(), + json!({ + "value_information": { "data": 252 }, + "value_information_extension": [{ "data": 116 }], + "plaintext_vife": ["A", "B"] + }) + ); +} + +#[test] +fn legacy_json_and_yaml_preserve_borrowed_vif_field_shapes() { + let json_output: Value = + serde_json::from_str(&serialize_mbus_data(LEGACY_FRAME, "json-legacy", None)).unwrap(); + let yaml_output: serde_yaml::Value = + serde_yaml::from_str(&serialize_mbus_data(LEGACY_FRAME, "yaml-legacy", None)).unwrap(); + + let expected_extensions = json!([{ "data": 116 }]); + let encoded_extensions = json!([116]); + let expected_plaintext = json!(["H", "R", "%"]); + let encoded_plaintext = json!([3, 72, 82, 37]); + + assert!(json_contains_property( + &json_output, + "value_information_extension", + &expected_extensions + )); + assert!(!json_contains_property( + &json_output, + "value_information_extension", + &encoded_extensions + )); + assert!(json_contains_property( + &json_output, + "plaintext_vife", + &expected_plaintext + )); + assert!(!json_contains_property( + &json_output, + "plaintext_vife", + &encoded_plaintext + )); + + let expected_extensions = serde_yaml::from_str("[{data: 116}]").unwrap(); + let encoded_extensions = serde_yaml::from_str("[116]").unwrap(); + let expected_plaintext = serde_yaml::from_str("[H, R, '%']").unwrap(); + let encoded_plaintext = serde_yaml::from_str("[3, 72, 82, 37]").unwrap(); + + assert!(yaml_contains_property( + &yaml_output, + "value_information_extension", + &expected_extensions + )); + assert!(!yaml_contains_property( + &yaml_output, + "value_information_extension", + &encoded_extensions + )); + assert!(yaml_contains_property( + &yaml_output, + "plaintext_vife", + &expected_plaintext + )); + assert!(!yaml_contains_property( + &yaml_output, + "plaintext_vife", + &encoded_plaintext + )); +} + +fn json_contains_property(value: &Value, key: &str, expected: &Value) -> bool { + match value { + Value::Object(properties) => { + properties.get(key) == Some(expected) + || properties + .values() + .any(|value| json_contains_property(value, key, expected)) + } + Value::Array(values) => values + .iter() + .any(|value| json_contains_property(value, key, expected)), + _ => false, + } +} + +fn yaml_contains_property( + value: &serde_yaml::Value, + key: &str, + expected: &serde_yaml::Value, +) -> bool { + match value { + serde_yaml::Value::Mapping(properties) => { + properties.get(key) == Some(expected) + || properties + .values() + .any(|value| yaml_contains_property(value, key, expected)) + } + serde_yaml::Value::Sequence(values) => values + .iter() + .any(|value| yaml_contains_property(value, key, expected)), + serde_yaml::Value::Tagged(tagged) => yaml_contains_property(&tagged.value, key, expected), + _ => false, + } +}