Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 46 additions & 20 deletions crates/m-bus-application-layer/src/data_information.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ impl DataInformationField {
const fn has_extension(&self) -> bool {
self.data & 0x80 != 0
}
}

impl DataInformationFieldExtension {
const fn special_function(&self) -> SpecialFunctions {
pub const fn is_special_function(&self) -> bool {
self.data & 0x0F == 0x0F
}

pub const fn special_function(&self) -> SpecialFunctions {
match self.data {
0x0F => SpecialFunctions::ManufacturerSpecific,
0x1F => SpecialFunctions::MoreRecordsFollow,
Expand Down Expand Up @@ -208,10 +210,7 @@ impl TryFrom<&DataInformationBlock<'_>> for DataInformation {
let mut extension_index = 1;
let mut tariff = 0;
let mut device = 0;
let mut first_dife = None;

if let Some(difes) = possible_difes {
first_dife = difes.clone().next();
let mut tariff_index = 0;
for (device_index, dife) in difes.clone().enumerate() {
if extension_index > MAXIMUM_DATA_INFORMATION_SIZE {
Expand Down Expand Up @@ -250,8 +249,8 @@ impl TryFrom<&DataInformationBlock<'_>> for DataInformation {
0b1101 => DataFieldCoding::VariableLength,
0b1110 => DataFieldCoding::BCDDigit12,
0b1111 => DataFieldCoding::SpecialFunctions(
first_dife
.ok_or(DataInformationError::DataTooShort)?
data_information_block
.data_information_field
.special_function(),
),
_ => unreachable!(), // This case should never occur due to the 4-bit width
Expand Down Expand Up @@ -293,17 +292,15 @@ impl PartialEq<str> for TextUnit<'_> {
#[cfg(feature = "std")]
impl std::fmt::Display for TextUnit<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value: Vec<u8> = self.0.iter().copied().rev().collect();
let value = String::from_utf8(value).unwrap_or_default();
let value: String = self.0.iter().rev().map(|&b| b as char).collect();
write!(f, "{}", value)
}
}

#[cfg(feature = "std")]
impl From<TextUnit<'_>> for String {
fn from(value: TextUnit<'_>) -> Self {
let value: Vec<u8> = value.0.iter().copied().rev().collect();
String::from_utf8(value).unwrap_or_default()
value.0.iter().rev().map(|&b| b as char).collect()
}
}

Expand Down Expand Up @@ -733,14 +730,25 @@ impl DataFieldCoding {
}
}

Self::SpecialFunctions(_code) => {
// Special functions parsing based on the code
Err(DataRecordError::DataInformationError(
DataInformationError::Unimplemented {
feature: "Special functions data parsing",
},
))
}
Self::SpecialFunctions(code) => match code {
SpecialFunctions::ManufacturerSpecific | SpecialFunctions::MoreRecordsFollow => {
Ok(Data {
value: Some(DataType::ManufacturerSpecific(input)),
size: input.len(),
})
}
SpecialFunctions::IdleFiller => Ok(Data {
value: None,
size: 0,
}),
SpecialFunctions::GlobalReadoutRequest => Ok(Data {
value: None,
size: 0,
}),
SpecialFunctions::Reserved => Err(DataRecordError::DataInformationError(
DataInformationError::InvalidValueInformation,
)),
},

Self::DateTypeG => {
let day = parse_single_or_every!(
Expand Down Expand Up @@ -1004,6 +1012,24 @@ mod tests {
assert_eq!(&parsed, "igal");
}

#[cfg(feature = "std")]
#[test]
fn text_unit_latin1_swedish_characters() {
// "Malmö" in Latin-1 (reversed byte order per M-Bus)
let bytes = [0xF6, 0x6D, 0x6C, 0x61, 0x4D]; // ö m l a M
let text = TextUnit::new(&bytes);
assert_eq!(String::from(text), "Malmö");
}

#[cfg(feature = "std")]
#[test]
fn text_unit_latin1_superscript_three() {
// "m³/h" in Latin-1 (reversed byte order per M-Bus)
let bytes = [0x68, 0x2F, 0xB3, 0x6D]; // h / ³ m
let text = TextUnit::new(&bytes);
assert_eq!(String::from(text), "m³/h");
}

#[test]
fn test_invalid_data_information() {
let data = [
Expand Down
52 changes: 23 additions & 29 deletions crates/m-bus-application-layer/src/data_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,43 +69,29 @@ impl<'a> DataRecord<'a> {
fixed_data_header: Option<&'a LongTplHeader>,
) -> Result<Self, DataRecordError> {
let data_record_header = DataRecordHeader::try_from(data)?;
let mut header_size = data_record_header.get_size();
if data_record_header
.raw_data_record_header
.data_information_block
.data_information_field
.data
== 0x0F
{
header_size = 0;
}
let header_size = data_record_header.get_size();
if data.len() < header_size {
return Err(DataRecordError::InsufficientData);
}
let offset = header_size;
let mut data_out = Data {
value: Some(DataType::ManufacturerSpecific(
let data_out = if let Some(data_info) = &data_record_header
.processed_data_record_header
.data_information
{
data_info.data_field_coding.parse(
data.get(offset..)
.ok_or(DataRecordError::InsufficientData)?,
)),
size: data.len() - offset,
};
if data_record_header
.raw_data_record_header
.value_information_block
.is_some()
{
if let Some(data_info) = &data_record_header
.processed_data_record_header
.data_information
{
data_out = data_info.data_field_coding.parse(
fixed_data_header,
)?
} else {
Data {
value: Some(DataType::ManufacturerSpecific(
data.get(offset..)
.ok_or(DataRecordError::InsufficientData)?,
fixed_data_header,
)?;
)),
size: data.len() - offset,
}
}
};

let mut record_size = data_record_header.get_size() + data_out.get_size();
if record_size > data.len() {
Expand Down Expand Up @@ -154,7 +140,7 @@ impl<'a> TryFrom<&'a [u8]> for RawDataRecordHeader<'a> {

let mut vifb = None;

if difb.data_information_field.data != 0x0F {
if !difb.data_information_field.is_special_function() {
vifb = Some(ValueInformationBlock::try_from(
data.get(offset..)
.ok_or(DataRecordError::InsufficientData)?,
Expand Down Expand Up @@ -194,6 +180,14 @@ impl TryFrom<&RawDataRecordHeader<'_>> for ProcessedDataRecordHeader {

value_information = Some(v);
data_information = Some(d);
} else if raw_data_record_header
.data_information_block
.data_information_field
.is_special_function()
{
data_information = Some(DataInformation::try_from(
&raw_data_record_header.data_information_block,
)?);
}

Ok(Self {
Expand Down
70 changes: 49 additions & 21 deletions crates/m-bus-application-layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,31 +46,51 @@ impl<'a> Iterator for DataRecords<'a> {
type Item = Result<DataRecord<'a>, DataRecordError>;

fn next(&mut self) -> Option<Self::Item> {
let mut _more_records_follow = false;

while self.offset < self.data.len() {
match self.data.get(self.offset)? {
0x1F => {
/* TODO: parse manufacturer specific */
_more_records_follow = true;
self.offset = self.data.len();
}
0x2F => {
self.offset += 1;
}
_ => {
let record = if let Some(long_tpl_header) = self.long_tpl_header {
DataRecord::try_from((self.data.get(self.offset..)?, long_tpl_header))
} else {
DataRecord::try_from(self.data.get(self.offset..)?)
};
if let Ok(record) = record {
self.offset += record.get_size();
return Some(Ok(record));
} else {
let dif = data_information::DataInformationField::from(*self.data.get(self.offset)?);

if dif.is_special_function() {
match dif.special_function() {
data_information::SpecialFunctions::IdleFiller => {
self.offset += 1;
}
data_information::SpecialFunctions::ManufacturerSpecific
| data_information::SpecialFunctions::MoreRecordsFollow => {
let remaining = self.data.get(self.offset..)?;
self.offset = self.data.len();
let record = if let Some(long_tpl_header) = self.long_tpl_header {
DataRecord::try_from((remaining, long_tpl_header))
} else {
DataRecord::try_from(remaining)
};
return Some(record);
}
data_information::SpecialFunctions::GlobalReadoutRequest => {
let remaining = self.data.get(self.offset..)?;
self.offset += 1;
let record = if let Some(long_tpl_header) = self.long_tpl_header {
DataRecord::try_from((remaining, long_tpl_header))
} else {
DataRecord::try_from(remaining)
};
return Some(record);
}
data_information::SpecialFunctions::Reserved => {
self.offset += 1;
}
}
} else {
let record = if let Some(long_tpl_header) = self.long_tpl_header {
DataRecord::try_from((self.data.get(self.offset..)?, long_tpl_header))
} else {
DataRecord::try_from(self.data.get(self.offset..)?)
};
if let Ok(record) = record {
self.offset += record.get_size();
return Some(Ok(record));
} else {
self.offset = self.data.len();
}
}
}
None
Expand Down Expand Up @@ -1485,4 +1505,12 @@ mod tests {
}
}
}

#[test]
fn global_readout_request_does_not_consume_following_records() {
// 0x7F followed by a valid 8-bit integer record: DIF=0x01, VIF=0x13, data=0x05
let data: &[u8] = &[0x7F, 0x01, 0x13, 0x05];
let records: Vec<_> = DataRecords::new(data, None).flatten().collect();
assert_eq!(records.len(), 2);
}
}
Loading
Loading