use crate::user_data::data_record::DataRecord;
use crate::MbusError;
use std::borrow::Cow;
use std::fmt;
use wired_mbus_link_layer::WiredFrame;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum SegmentKind {
StartByte,
Length,
CField,
AField,
Checksum,
StopByte,
CiField,
IdentificationNumber,
ManufacturerCode,
Version,
DeviceType,
AccessNumber,
Status,
ConfigurationField,
EncryptionConfigByte,
Dif,
Dife,
Vif,
Vife,
PlaintextVif,
DataPayload,
LField,
WirelessManufacturerId,
Crc,
ExtendedLinkLayer,
EncryptedPayload,
ManufacturerSpecific,
IdleFiller,
Unknown,
}
impl fmt::Display for SegmentKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StartByte => write!(f, "Start Byte"),
Self::Length => write!(f, "Length"),
Self::CField => write!(f, "C Field"),
Self::AField => write!(f, "A Field"),
Self::Checksum => write!(f, "Checksum"),
Self::StopByte => write!(f, "Stop Byte"),
Self::CiField => write!(f, "CI Field"),
Self::IdentificationNumber => write!(f, "Identification Number"),
Self::ManufacturerCode => write!(f, "Manufacturer Code"),
Self::Version => write!(f, "Version"),
Self::DeviceType => write!(f, "Device Type"),
Self::AccessNumber => write!(f, "Access Number"),
Self::Status => write!(f, "Status"),
Self::ConfigurationField => write!(f, "Configuration Field"),
Self::EncryptionConfigByte => write!(f, "Encryption Config Byte"),
Self::Dif => write!(f, "DIF"),
Self::Dife => write!(f, "DIFE"),
Self::Vif => write!(f, "VIF"),
Self::Vife => write!(f, "VIFE"),
Self::PlaintextVif => write!(f, "Plaintext VIF"),
Self::DataPayload => write!(f, "Data"),
Self::LField => write!(f, "L Field"),
Self::WirelessManufacturerId => write!(f, "Manufacturer ID"),
Self::Crc => write!(f, "CRC"),
Self::ExtendedLinkLayer => write!(f, "Extended Link Layer"),
Self::EncryptedPayload => write!(f, "Encrypted Payload"),
Self::ManufacturerSpecific => write!(f, "Manufacturer Specific"),
Self::IdleFiller => write!(f, "Idle Filler"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Layer {
Frame,
AppHeader,
RecordField,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ByteSegment {
pub start: usize,
pub end: usize,
pub kind: SegmentKind,
pub detail: Cow<'static, str>,
pub group: Option<usize>,
pub layer: Layer,
}
pub fn annotate_frame(data: &[u8]) -> Result<Vec<ByteSegment>, MbusError> {
if let Ok(segments) = annotate_wired(data) {
return Ok(segments);
}
let mut crc_buf = [0u8; 512];
if let Some(stripped) = wireless_mbus_link_layer::strip_format_a_crcs(data, &mut crc_buf) {
if let Ok(segments) = annotate_wireless_format_a(data, stripped) {
return Ok(segments);
}
}
if let Ok(segments) = annotate_wireless_inner(data) {
return Ok(segments);
}
Err(MbusError::FrameError(
m_bus_core::FrameError::InvalidStartByte,
))
}
fn annotate_wired(data: &[u8]) -> Result<Vec<ByteSegment>, MbusError> {
let frame = WiredFrame::try_from(data)?;
let mut segments = Vec::new();
match frame {
WiredFrame::SingleCharacter { character } => {
segments.push(ByteSegment {
start: 0,
end: 1,
kind: SegmentKind::StartByte,
detail: Cow::Owned(format!("Single Character: 0x{:02X}", character)),
group: None,
layer: Layer::Frame,
});
}
WiredFrame::ShortFrame { function, address } => {
segments.push(ByteSegment {
start: 0,
end: 1,
kind: SegmentKind::StartByte,
detail: Cow::Borrowed("Start: 0x10"),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 1,
end: 2,
kind: SegmentKind::CField,
detail: Cow::Owned(format!("C Field: {}", function)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 2,
end: 3,
kind: SegmentKind::AField,
detail: Cow::Owned(format!("Address: {}", address)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 3,
end: 4,
kind: SegmentKind::Checksum,
detail: Cow::Owned(format!("Checksum: 0x{:02X}", data.get(3).copied().unwrap_or(0))),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 4,
end: 5,
kind: SegmentKind::StopByte,
detail: Cow::Borrowed("Stop: 0x16"),
group: None,
layer: Layer::Frame,
});
}
WiredFrame::LongFrame {
function,
address,
data: user_data_slice,
} => {
let is_control = false;
annotate_long_or_control_frame(
&mut segments,
data,
&function,
&address,
user_data_slice,
is_control,
);
}
WiredFrame::ControlFrame {
function,
address,
data: user_data_slice,
} => {
let is_control = true;
annotate_long_or_control_frame(
&mut segments,
data,
&function,
&address,
user_data_slice,
is_control,
);
}
_ => {
segments.push(ByteSegment {
start: 0,
end: data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Unknown frame type"),
group: None,
layer: Layer::Frame,
});
}
}
Ok(segments)
}
fn annotate_long_or_control_frame(
segments: &mut Vec<ByteSegment>,
data: &[u8],
function: &m_bus_core::Function,
address: &wired_mbus_link_layer::Address,
user_data_slice: &[u8],
is_control: bool,
) {
let l = data.get(1).copied().unwrap_or(0) as usize;
segments.push(ByteSegment {
start: 0,
end: 1,
kind: SegmentKind::StartByte,
detail: Cow::Borrowed("Start: 0x68"),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 1,
end: 4,
kind: SegmentKind::Length,
detail: Cow::Owned(format!("Length: {} (repeated + start)", l)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 4,
end: 5,
kind: SegmentKind::CField,
detail: Cow::Owned(format!(
"C Field: {}{}",
function,
if is_control { " (Control)" } else { "" }
)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 5,
end: 6,
kind: SegmentKind::AField,
detail: Cow::Owned(format!("Address: {}", address)),
group: None,
layer: Layer::Frame,
});
let user_data_start = 6;
let user_data_end = user_data_start + user_data_slice.len();
if !is_control && !user_data_slice.is_empty() {
annotate_application_layer(segments, data, user_data_start, user_data_slice);
} else if !user_data_slice.is_empty() {
segments.push(ByteSegment {
start: user_data_start,
end: user_data_end,
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Control frame data"),
group: None,
layer: Layer::AppHeader,
});
}
let cs_offset = user_data_end;
if cs_offset < data.len() {
segments.push(ByteSegment {
start: cs_offset,
end: cs_offset + 1,
kind: SegmentKind::Checksum,
detail: Cow::Owned(format!(
"Checksum: 0x{:02X}",
data.get(cs_offset).copied().unwrap_or(0)
)),
group: None,
layer: Layer::Frame,
});
}
let stop_offset = cs_offset + 1;
if stop_offset < data.len() {
segments.push(ByteSegment {
start: stop_offset,
end: stop_offset + 1,
kind: SegmentKind::StopByte,
detail: Cow::Borrowed("Stop: 0x16"),
group: None,
layer: Layer::Frame,
});
}
}
fn annotate_application_layer(
segments: &mut Vec<ByteSegment>,
frame_data: &[u8],
base: usize,
app_data: &[u8],
) {
let Some(&ci) = app_data.first() else {
return;
};
match ci {
0x72 | 0x76 => {
if app_data.len() < 13 {
segments.push(ByteSegment {
start: base,
end: base + app_data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Incomplete long TPL header"),
group: None,
layer: Layer::AppHeader,
});
return;
}
annotate_long_tpl_header(segments, frame_data, base, app_data);
let records_start = base + 13;
let records_data = &app_data[13..];
let is_encrypted = is_long_tpl_encrypted(app_data);
if is_encrypted {
if !records_data.is_empty() {
segments.push(ByteSegment {
start: records_start,
end: records_start + records_data.len(),
kind: SegmentKind::EncryptedPayload,
detail: Cow::Borrowed("Encrypted variable data"),
group: None,
layer: Layer::RecordField,
});
}
} else {
annotate_data_records(segments, records_start, records_data);
}
}
0x7A | 0xA0..=0xAF => {
let has_enc_config = ci == 0xA0;
let skip_count: usize = if has_enc_config { 2 } else { 1 };
let data_block_offset: usize = if has_enc_config { 6 } else { 5 };
if app_data.len() < data_block_offset {
segments.push(ByteSegment {
start: base,
end: base + app_data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Incomplete short TPL header"),
group: None,
layer: Layer::AppHeader,
});
return;
}
segments.push(ByteSegment {
start: base,
end: base + 1,
kind: SegmentKind::CiField,
detail: Cow::Owned(format!("CI: 0x{:02X} (Short TPL)", ci)),
group: None,
layer: Layer::AppHeader,
});
let mut offset = base + 1;
if has_enc_config {
segments.push(ByteSegment {
start: offset,
end: offset + 1,
kind: SegmentKind::EncryptionConfigByte,
detail: Cow::Owned(format!(
"Encryption config: 0x{:02X}",
app_data.get(1).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
offset += 1;
}
segments.push(ByteSegment {
start: offset,
end: offset + 1,
kind: SegmentKind::AccessNumber,
detail: Cow::Owned(format!(
"Access Number: {}",
app_data.get(skip_count).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
offset += 1;
segments.push(ByteSegment {
start: offset,
end: offset + 1,
kind: SegmentKind::Status,
detail: Cow::Owned(format!(
"Status: 0x{:02X}",
app_data.get(skip_count + 1).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
offset += 1;
segments.push(ByteSegment {
start: offset,
end: offset + 2,
kind: SegmentKind::ConfigurationField,
detail: Cow::Owned(format!(
"Configuration: 0x{:02X}{:02X}",
app_data.get(skip_count + 2).copied().unwrap_or(0),
app_data.get(skip_count + 3).copied().unwrap_or(0),
)),
group: None,
layer: Layer::AppHeader,
});
offset += 2;
let records_data = &app_data[data_block_offset..];
let is_encrypted = is_short_tpl_encrypted(app_data, skip_count);
if is_encrypted {
if !records_data.is_empty() {
segments.push(ByteSegment {
start: offset,
end: offset + records_data.len(),
kind: SegmentKind::EncryptedPayload,
detail: Cow::Borrowed("Encrypted variable data"),
group: None,
layer: Layer::RecordField,
});
}
} else {
annotate_data_records(segments, offset, records_data);
}
}
0x8C => {
let ell_size = 2;
let total_header = 1 + ell_size; if app_data.len() < total_header {
segments.push(ByteSegment {
start: base,
end: base + app_data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Incomplete ELL I header"),
group: None,
layer: Layer::AppHeader,
});
return;
}
segments.push(ByteSegment {
start: base,
end: base + 1,
kind: SegmentKind::CiField,
detail: Cow::Borrowed("CI: 0x8C (ELL I)"),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 1,
end: base + total_header,
kind: SegmentKind::ExtendedLinkLayer,
detail: Cow::Borrowed("ELL I: CC + Access Number"),
group: None,
layer: Layer::AppHeader,
});
let inner_data = &app_data[total_header..];
let inner_base = base + total_header;
if !inner_data.is_empty() {
annotate_application_layer(segments, frame_data, inner_base, inner_data);
}
}
0x8D => {
let ell_size = 8;
let total_header = 1 + ell_size;
if app_data.len() < total_header {
segments.push(ByteSegment {
start: base,
end: base + app_data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Incomplete ELL II header"),
group: None,
layer: Layer::AppHeader,
});
return;
}
segments.push(ByteSegment {
start: base,
end: base + 1,
kind: SegmentKind::CiField,
detail: Cow::Borrowed("CI: 0x8D (ELL II)"),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 1,
end: base + total_header,
kind: SegmentKind::ExtendedLinkLayer,
detail: Cow::Borrowed("ELL II: CC + ACC + SN[4] + CRC[2]"),
group: None,
layer: Layer::AppHeader,
});
let remaining = &app_data[total_header..];
let remaining_base = base + total_header;
if !remaining.is_empty() {
annotate_data_records(segments, remaining_base, remaining);
}
}
0x8E => {
let ell_size = 16;
let total_header = 1 + ell_size;
if app_data.len() < total_header {
segments.push(ByteSegment {
start: base,
end: base + app_data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Incomplete ELL III header"),
group: None,
layer: Layer::AppHeader,
});
return;
}
segments.push(ByteSegment {
start: base,
end: base + 1,
kind: SegmentKind::CiField,
detail: Cow::Borrowed("CI: 0x8E (ELL III)"),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 1,
end: base + total_header,
kind: SegmentKind::ExtendedLinkLayer,
detail: Cow::Borrowed("ELL III: CC + ACC + MFR[2] + ADDR[6] + SN[4] + CRC[2]"),
group: None,
layer: Layer::AppHeader,
});
let remaining = &app_data[total_header..];
let remaining_base = base + total_header;
if !remaining.is_empty() {
annotate_data_records(segments, remaining_base, remaining);
}
}
_ => {
segments.push(ByteSegment {
start: base,
end: base + app_data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Owned(format!("Unknown CI: 0x{:02X}", ci)),
group: None,
layer: Layer::AppHeader,
});
}
}
}
fn annotate_long_tpl_header(
segments: &mut Vec<ByteSegment>,
_frame_data: &[u8],
base: usize,
app_data: &[u8],
) {
let ci = app_data.first().copied().unwrap_or(0);
segments.push(ByteSegment {
start: base,
end: base + 1,
kind: SegmentKind::CiField,
detail: Cow::Owned(format!(
"CI: 0x{:02X} ({})",
ci,
if ci == 0x72 {
"Variable Data, Long TPL"
} else {
"Variable Data, Long TPL, LSB"
}
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 1,
end: base + 5,
kind: SegmentKind::IdentificationNumber,
detail: Cow::Owned(format!(
"ID: {:02X}{:02X}{:02X}{:02X}",
app_data.get(1).copied().unwrap_or(0),
app_data.get(2).copied().unwrap_or(0),
app_data.get(3).copied().unwrap_or(0),
app_data.get(4).copied().unwrap_or(0),
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 5,
end: base + 7,
kind: SegmentKind::ManufacturerCode,
detail: Cow::Owned(format!(
"Manufacturer: 0x{:02X}{:02X}",
app_data.get(5).copied().unwrap_or(0),
app_data.get(6).copied().unwrap_or(0),
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 7,
end: base + 8,
kind: SegmentKind::Version,
detail: Cow::Owned(format!(
"Version: {}",
app_data.get(7).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 8,
end: base + 9,
kind: SegmentKind::DeviceType,
detail: Cow::Owned(format!(
"Device Type: 0x{:02X}",
app_data.get(8).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 9,
end: base + 10,
kind: SegmentKind::AccessNumber,
detail: Cow::Owned(format!(
"Access Number: {}",
app_data.get(9).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 10,
end: base + 11,
kind: SegmentKind::Status,
detail: Cow::Owned(format!(
"Status: 0x{:02X}",
app_data.get(10).copied().unwrap_or(0)
)),
group: None,
layer: Layer::AppHeader,
});
segments.push(ByteSegment {
start: base + 11,
end: base + 13,
kind: SegmentKind::ConfigurationField,
detail: Cow::Owned(format!(
"Configuration: 0x{:02X}{:02X}",
app_data.get(11).copied().unwrap_or(0),
app_data.get(12).copied().unwrap_or(0),
)),
group: None,
layer: Layer::AppHeader,
});
}
fn annotate_data_records(segments: &mut Vec<ByteSegment>, base: usize, data: &[u8]) {
let mut offset = 0usize;
let mut record_index = 0usize;
while offset < data.len() {
let Some(&dif_byte) = data.get(offset) else {
break;
};
if dif_byte & 0x0F == 0x0F {
match dif_byte {
0x2F => {
segments.push(ByteSegment {
start: base + offset,
end: base + offset + 1,
kind: SegmentKind::IdleFiller,
detail: Cow::Borrowed("Idle Filler: 0x2F"),
group: None,
layer: Layer::RecordField,
});
offset += 1;
continue;
}
0x0F | 0x1F => {
let kind = if dif_byte == 0x0F {
SegmentKind::ManufacturerSpecific
} else {
SegmentKind::ManufacturerSpecific };
segments.push(ByteSegment {
start: base + offset,
end: base + data.len(),
kind,
detail: Cow::Owned(format!(
"{}: 0x{:02X} ({} bytes)",
if dif_byte == 0x0F {
"Manufacturer Specific"
} else {
"More Records Follow"
},
dif_byte,
data.len() - offset
)),
group: Some(record_index),
layer: Layer::RecordField,
});
return;
}
_ => {
segments.push(ByteSegment {
start: base + offset,
end: base + offset + 1,
kind: SegmentKind::Unknown,
detail: Cow::Owned(format!("Special DIF: 0x{:02X}", dif_byte)),
group: None,
layer: Layer::RecordField,
});
offset += 1;
continue;
}
}
}
let remaining = &data[offset..];
let record_result = DataRecord::try_from(remaining);
match record_result {
Ok(record) => {
let header_size = record.data_record_header.get_size();
let total_size = record.get_size();
let dif = &record
.data_record_header
.raw_data_record_header
.data_information_block;
segments.push(ByteSegment {
start: base + offset,
end: base + offset + 1,
kind: SegmentKind::Dif,
detail: Cow::Owned(format!("DIF: 0x{:02X}", dif_byte)),
group: Some(record_index),
layer: Layer::RecordField,
});
offset += 1;
let dife_count = dif.get_size() - 1;
if dife_count > 0 {
segments.push(ByteSegment {
start: base + offset,
end: base + offset + dife_count,
kind: SegmentKind::Dife,
detail: Cow::Owned(format!("DIFE: {} byte(s)", dife_count)),
group: Some(record_index),
layer: Layer::RecordField,
});
offset += dife_count;
}
if let Some(vib) = &record
.data_record_header
.raw_data_record_header
.value_information_block
{
segments.push(ByteSegment {
start: base + offset,
end: base + offset + 1,
kind: SegmentKind::Vif,
detail: Cow::Owned(format!(
"VIF: 0x{:02X}",
data.get(offset).copied().unwrap_or(0)
)),
group: Some(record_index),
layer: Layer::RecordField,
});
offset += 1;
let vife_count = if let Some(ext) = &vib.value_information_extension {
ext.len()
} else {
0
};
if vife_count > 0 {
segments.push(ByteSegment {
start: base + offset,
end: base + offset + vife_count,
kind: SegmentKind::Vife,
detail: Cow::Owned(format!("VIFE: {} byte(s)", vife_count)),
group: Some(record_index),
layer: Layer::RecordField,
});
offset += vife_count;
}
if let Some(plaintext) = &vib.plaintext_vife {
let pt_size = plaintext.len() + 1; segments.push(ByteSegment {
start: base + offset,
end: base + offset + pt_size,
kind: SegmentKind::PlaintextVif,
detail: Cow::Owned(format!(
"Plaintext VIF: \"{}\"",
plaintext.iter().collect::<String>()
)),
group: Some(record_index),
layer: Layer::RecordField,
});
offset += pt_size;
}
}
let data_size = total_size.saturating_sub(header_size);
if data_size > 0 {
let available = data.len().saturating_sub(offset);
let emit_size = data_size.min(available);
if emit_size > 0 {
segments.push(ByteSegment {
start: base + offset,
end: base + offset + emit_size,
kind: SegmentKind::DataPayload,
detail: Cow::Owned(format!("{}", record.data)),
group: Some(record_index),
layer: Layer::RecordField,
});
}
offset += emit_size;
if emit_size < data_size {
return;
}
}
record_index += 1;
}
Err(_) => {
segments.push(ByteSegment {
start: base + offset,
end: base + data.len(),
kind: SegmentKind::Unknown,
detail: Cow::Borrowed("Unparseable data record bytes"),
group: None,
layer: Layer::RecordField,
});
return;
}
}
}
}
fn annotate_wireless_format_a(
original: &[u8],
stripped: &[u8],
) -> Result<Vec<ByteSegment>, MbusError> {
let offset_map = build_format_a_offset_map(original);
let stripped_segments = annotate_wireless_inner(stripped)?;
let mut segments = Vec::new();
for seg in &stripped_segments {
let orig_start = offset_map
.get(seg.start)
.copied()
.unwrap_or(seg.start);
let orig_end = if seg.end <= offset_map.len() {
if seg.end < offset_map.len() {
offset_map[seg.end]
} else {
offset_map
.last()
.map(|&o| o + 1)
.unwrap_or(seg.end)
}
} else {
offset_map
.last()
.map(|&o| o + 1)
.unwrap_or(seg.end)
};
segments.push(ByteSegment {
start: orig_start,
end: orig_end,
kind: seg.kind.clone(),
detail: seg.detail.clone(),
group: seg.group,
layer: seg.layer,
});
}
let mut crc_positions = Vec::new();
if original.len() >= 12 {
crc_positions.push((10, 12));
}
let mut pos = 12usize;
while pos < original.len() {
let remaining = original.len() - pos;
if remaining < 3 {
break;
}
let max_data_len = 16.min(remaining - 2);
let mut found = false;
for data_len in (1..=max_data_len).rev() {
let crc_start = pos + data_len;
if crc_start + 2 > original.len() {
continue;
}
let computed = crc16_en13757(&original[pos..crc_start]);
let stored = u16::from_be_bytes([original[crc_start], original[crc_start + 1]]);
if computed == stored {
crc_positions.push((crc_start, crc_start + 2));
pos = crc_start + 2;
found = true;
break;
}
}
if !found {
break;
}
}
for &(crc_start, crc_end) in &crc_positions {
let crc_seg = ByteSegment {
start: crc_start,
end: crc_end,
kind: SegmentKind::Crc,
detail: Cow::Owned(format!(
"CRC: 0x{:02X}{:02X}",
original.get(crc_start).copied().unwrap_or(0),
original.get(crc_start + 1).copied().unwrap_or(0),
)),
group: None,
layer: Layer::Frame,
};
let insert_pos = segments
.iter()
.position(|s| s.start >= crc_start)
.unwrap_or(segments.len());
if insert_pos > 0 {
let prev = &segments[insert_pos - 1];
if prev.end > crc_start {
let orig_end = prev.end;
let orig_kind = prev.kind.clone();
let orig_detail = prev.detail.clone();
let orig_group = prev.group;
let orig_layer = prev.layer;
segments[insert_pos - 1].end = crc_start;
segments.insert(insert_pos, crc_seg);
if crc_end < orig_end {
segments.insert(
insert_pos + 1,
ByteSegment {
start: crc_end,
end: orig_end,
kind: orig_kind,
detail: orig_detail,
group: orig_group,
layer: orig_layer,
},
);
}
continue;
}
}
segments.insert(insert_pos, crc_seg);
}
segments.retain(|s| s.start < s.end);
Ok(segments)
}
fn annotate_wireless_inner(data: &[u8]) -> Result<Vec<ByteSegment>, MbusError> {
let _frame = wireless_mbus_link_layer::WirelessFrame::try_from(data)?;
let mut segments = Vec::new();
segments.push(ByteSegment {
start: 0,
end: 1,
kind: SegmentKind::LField,
detail: Cow::Owned(format!("L Field: {}", data.first().copied().unwrap_or(0))),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 1,
end: 2,
kind: SegmentKind::CField,
detail: Cow::Owned(format!("C Field: 0x{:02X}", data.get(1).copied().unwrap_or(0))),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 2,
end: 4,
kind: SegmentKind::ManufacturerCode,
detail: Cow::Owned(format!(
"Manufacturer: 0x{:02X}{:02X}",
data.get(2).copied().unwrap_or(0),
data.get(3).copied().unwrap_or(0),
)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 4,
end: 8,
kind: SegmentKind::IdentificationNumber,
detail: Cow::Owned(format!(
"ID: {:02X}{:02X}{:02X}{:02X}",
data.get(4).copied().unwrap_or(0),
data.get(5).copied().unwrap_or(0),
data.get(6).copied().unwrap_or(0),
data.get(7).copied().unwrap_or(0),
)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 8,
end: 9,
kind: SegmentKind::Version,
detail: Cow::Owned(format!(
"Version: {}",
data.get(8).copied().unwrap_or(0)
)),
group: None,
layer: Layer::Frame,
});
segments.push(ByteSegment {
start: 9,
end: 10,
kind: SegmentKind::DeviceType,
detail: Cow::Owned(format!(
"Device Type: 0x{:02X}",
data.get(9).copied().unwrap_or(0)
)),
group: None,
layer: Layer::Frame,
});
if data.len() > 10 {
let app_data = &data[10..];
annotate_application_layer(&mut segments, data, 10, app_data);
}
Ok(segments)
}
fn build_format_a_offset_map(original: &[u8]) -> Vec<usize> {
let mut map = Vec::new();
for i in 0..10.min(original.len()) {
map.push(i);
}
if original.len() < 12 {
return map;
}
let mut pos = 12usize;
while pos < original.len() {
let remaining = original.len() - pos;
if remaining < 3 {
for i in 0..remaining {
map.push(pos + i);
}
break;
}
let max_data_len = 16.min(remaining - 2);
let mut found = false;
for data_len in (1..=max_data_len).rev() {
let crc_start = pos + data_len;
if crc_start + 2 > original.len() {
continue;
}
let computed = crc16_en13757(&original[pos..crc_start]);
let stored = u16::from_be_bytes([original[crc_start], original[crc_start + 1]]);
if computed == stored {
for i in 0..data_len {
map.push(pos + i);
}
pos = crc_start + 2;
found = true;
break;
}
}
if !found {
let remaining = original.len() - pos;
for i in 0..remaining {
map.push(pos + i);
}
break;
}
}
map
}
fn crc16_en13757(data: &[u8]) -> u16 {
let mut crc: u16 = 0x0000;
for &byte in data {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x3D65;
} else {
crc <<= 1;
}
}
}
crc ^ 0xFFFF
}
fn is_long_tpl_encrypted(app_data: &[u8]) -> bool {
if app_data.len() < 13 {
return false;
}
let config = m_bus_core::ConfigurationField::from_bytes(app_data[11], app_data[12]);
!matches!(
config.security_mode(),
m_bus_core::SecurityMode::NoEncryption
)
}
fn is_short_tpl_encrypted(app_data: &[u8], skip_count: usize) -> bool {
if app_data.len() < skip_count + 4 {
return false;
}
let config = m_bus_core::ConfigurationField::from_bytes(
app_data[skip_count + 2],
app_data[skip_count + 3],
);
!matches!(
config.security_mode(),
m_bus_core::SecurityMode::NoEncryption
)
}
pub fn render_annotations(segments: &[ByteSegment], data: &[u8]) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(out, "Hex Dump:");
let _ = writeln!(out, "────────────────────────────────────────────────────────────────────");
for (i, chunk) in data.chunks(16).enumerate() {
let offset = i * 16;
let _ = write!(out, " {:04X} ", offset);
for (j, byte) in chunk.iter().enumerate() {
if j == 8 {
let _ = write!(out, " ");
}
let _ = write!(out, "{:02X} ", byte);
}
let pad = 16 - chunk.len();
for _ in 0..pad {
let _ = write!(out, " ");
}
if chunk.len() <= 8 {
let _ = write!(out, " ");
}
let _ = write!(out, " ");
for byte in chunk {
let ch = if byte.is_ascii_graphic() || *byte == b' ' {
*byte as char
} else {
'·'
};
let _ = write!(out, "{}", ch);
}
let _ = writeln!(out);
}
let _ = writeln!(out);
let _ = writeln!(
out,
"{:<12} {:<24} {:<22} {}",
"Offset", "Hex", "Kind", "Detail"
);
let _ = writeln!(
out,
"{:<12} {:<24} {:<22} {}",
"────────────", "────────────────────────", "──────────────────────", "──────────────────────────"
);
let mut current_layer = None;
let mut current_group: Option<usize> = None;
for seg in segments {
let layer_changed = current_layer != Some(seg.layer);
let group_changed = seg.group != current_group;
if layer_changed {
let header = match seg.layer {
Layer::Frame => "Frame",
Layer::AppHeader => "Application Header",
Layer::RecordField => "Data Records",
};
let _ = writeln!(out, "┌─ {} ────────────────────────────────────────────────────────", header);
current_layer = Some(seg.layer);
current_group = None;
}
if seg.layer == Layer::RecordField && group_changed {
if let Some(g) = seg.group {
let _ = writeln!(out, "│ ── Record {} ──", g);
}
current_group = seg.group;
}
let offset_str = if seg.end - seg.start == 1 {
format!("[{:02X}]", seg.start)
} else {
format!("[{:02X}..{:02X}]", seg.start, seg.end)
};
let max_hex_bytes = 8;
let byte_count = seg.end - seg.start;
let hex_str = if seg.start < data.len() {
let end = seg.end.min(data.len());
let show = (end - seg.start).min(max_hex_bytes);
let mut h: String = data[seg.start..seg.start + show]
.iter()
.map(|b| format!("{:02X} ", b))
.collect();
if byte_count > max_hex_bytes {
h.push_str("...");
} else {
h.pop();
}
h
} else {
String::new()
};
let kind_str = format!("{}", seg.kind);
let _ = writeln!(
out,
"│ {:<10} {:<24} {:<22} {}",
offset_str, hex_str, kind_str, seg.detail
);
}
out
}
pub fn annotate_and_render(data: &[u8]) -> Result<String, MbusError> {
let segments = annotate_frame(data)?;
Ok(render_annotations(&segments, data))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_long_frame(c_field: u8, address: u8, user_data: &[u8]) -> Vec<u8> {
let l = (user_data.len() + 2) as u8; let mut frame = vec![0x68, l, l, 0x68, c_field, address];
frame.extend_from_slice(user_data);
let checksum: u8 = frame[4..].iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
frame.push(checksum);
frame.push(0x16);
frame
}
fn assert_contiguous(segments: &[ByteSegment], total_len: usize) {
assert!(!segments.is_empty(), "segments should not be empty");
assert_eq!(segments[0].start, 0, "first segment should start at 0");
assert_eq!(
segments.last().map(|s| s.end).unwrap_or(0),
total_len,
"last segment should end at total length"
);
for window in segments.windows(2) {
assert_eq!(
window[0].end, window[1].start,
"gap between segments at {}-{}",
window[0].end, window[1].start
);
}
for seg in segments {
assert!(
seg.start < seg.end,
"zero-width segment at {}",
seg.start
);
}
}
#[test]
fn test_long_frame() {
let data: Vec<u8> = vec![
0x68, 0x4D, 0x4D, 0x68, 0x08, 0x01, 0x72, 0x01, 0x00, 0x00, 0x00, 0x96, 0x15, 0x01,
0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x78, 0x56, 0x00, 0x00, 0x00, 0x01, 0xFD, 0x1B,
0x00, 0x02, 0xFC, 0x03, 0x48, 0x52, 0x25, 0x74, 0x44, 0x0D, 0x22, 0xFC, 0x03, 0x48,
0x52, 0x25, 0x74, 0xF1, 0x0C, 0x12, 0xFC, 0x03, 0x48, 0x52, 0x25, 0x74, 0x63, 0x11,
0x02, 0x65, 0xB4, 0x09, 0x22, 0x65, 0x86, 0x09, 0x12, 0x65, 0xB7, 0x09, 0x01, 0x72,
0x00, 0x72, 0x65, 0x00, 0x00, 0xB2, 0x01, 0x65, 0x00, 0x00, 0x1F, 0xB3, 0x16,
];
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
assert_eq!(segments[0].kind, SegmentKind::StartByte);
assert_eq!(segments[1].kind, SegmentKind::Length);
assert_eq!(segments[2].kind, SegmentKind::CField);
assert_eq!(segments[3].kind, SegmentKind::AField);
assert_eq!(segments[4].kind, SegmentKind::CiField);
assert_eq!(segments[4].start, 6);
let last = segments.last().expect("non-empty");
assert_eq!(last.kind, SegmentKind::StopByte);
let second_last = &segments[segments.len() - 2];
assert_eq!(second_last.kind, SegmentKind::Checksum);
}
#[test]
fn test_short_frame() {
let data: Vec<u8> = vec![0x10, 0x5B, 0x01, 0x5C, 0x16];
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
assert_eq!(segments.len(), 5);
assert_eq!(segments[0].kind, SegmentKind::StartByte);
assert_eq!(segments[1].kind, SegmentKind::CField);
assert_eq!(segments[2].kind, SegmentKind::AField);
assert_eq!(segments[3].kind, SegmentKind::Checksum);
assert_eq!(segments[4].kind, SegmentKind::StopByte);
}
#[test]
fn test_single_character_frame() {
let data: Vec<u8> = vec![0xE5];
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].kind, SegmentKind::StartByte);
}
#[test]
fn test_data_record_offsets() {
let user_data: Vec<u8> = vec![
0x72, 0x01, 0x00, 0x00, 0x00, 0x96, 0x15, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x00, ];
let data = make_long_frame(0x08, 0x01, &user_data);
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
let dif_seg = segments.iter().find(|s| s.kind == SegmentKind::Dif);
assert!(dif_seg.is_some());
let dif_seg = dif_seg.expect("DIF segment");
assert_eq!(dif_seg.group, Some(0));
assert_eq!(dif_seg.start, 19);
let vif_seg = segments.iter().find(|s| s.kind == SegmentKind::Vif);
assert!(vif_seg.is_some());
let vif_seg = vif_seg.expect("VIF segment");
assert_eq!(vif_seg.group, Some(0));
assert_eq!(vif_seg.start, 20);
let data_seg = segments.iter().find(|s| s.kind == SegmentKind::DataPayload);
assert!(data_seg.is_some());
let data_seg = data_seg.expect("Data segment");
assert_eq!(data_seg.group, Some(0));
assert_eq!(data_seg.start, 21);
assert_eq!(data_seg.end, 25);
}
#[test]
fn test_encrypted_long_tpl() {
let user_data: Vec<u8> = vec![
0x72, 0x01, 0x00, 0x00, 0x00, 0x96, 0x15, 0x01, 0x00, 0x18, 0x00, 0x00, 0x05, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22,
];
let data = make_long_frame(0x08, 0x01, &user_data);
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
let enc_seg = segments
.iter()
.find(|s| s.kind == SegmentKind::EncryptedPayload);
assert!(enc_seg.is_some(), "should have encrypted payload segment");
let enc_seg = enc_seg.expect("encrypted payload");
assert_eq!(enc_seg.start, 19); assert_eq!(enc_seg.end, 27);
assert!(
!segments.iter().any(|s| s.kind == SegmentKind::Dif),
"should not parse DIF in encrypted payload"
);
}
#[test]
fn test_manufacturer_specific_tail() {
let user_data: Vec<u8> = vec![
0x72, 0x01, 0x00, 0x00, 0x00, 0x96, 0x15, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x60, 0x00,
];
let data = make_long_frame(0x08, 0x01, &user_data);
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
let mfr_seg = segments
.iter()
.find(|s| s.kind == SegmentKind::ManufacturerSpecific);
assert!(mfr_seg.is_some(), "should have manufacturer specific segment");
let mfr_seg = mfr_seg.expect("manufacturer specific");
assert_eq!(mfr_seg.start, 19);
assert_eq!(mfr_seg.end, 22);
}
#[test]
fn test_idle_fillers() {
let user_data: Vec<u8> = vec![
0x72, 0x01, 0x00, 0x00, 0x00, 0x96, 0x15, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x2F, 0x2F, 0x2F, 0x2F,
];
let data = make_long_frame(0x08, 0x01, &user_data);
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
let filler_count = segments
.iter()
.filter(|s| s.kind == SegmentKind::IdleFiller)
.count();
assert_eq!(filler_count, 4);
}
#[cfg(feature = "plaintext-before-extension")]
#[test]
fn test_plaintext_vif() {
let data: Vec<u8> = vec![
0x68, 0x4D, 0x4D, 0x68, 0x08, 0x01, 0x72, 0x01, 0x00, 0x00, 0x00, 0x96, 0x15, 0x01,
0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x78, 0x56, 0x00, 0x00, 0x00, 0x01, 0xFD, 0x1B,
0x00, 0x02, 0xFC, 0x03, 0x48, 0x52, 0x25, 0x74, 0x44, 0x0D, 0x22, 0xFC, 0x03, 0x48,
0x52, 0x25, 0x74, 0xF1, 0x0C, 0x12, 0xFC, 0x03, 0x48, 0x52, 0x25, 0x74, 0x63, 0x11,
0x02, 0x65, 0xB4, 0x09, 0x22, 0x65, 0x86, 0x09, 0x12, 0x65, 0xB7, 0x09, 0x01, 0x72,
0x00, 0x72, 0x65, 0x00, 0x00, 0xB2, 0x01, 0x65, 0x00, 0x00, 0x1F, 0xB3, 0x16,
];
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
let pt_seg = segments
.iter()
.find(|s| s.kind == SegmentKind::PlaintextVif);
assert!(
pt_seg.is_some(),
"should have plaintext VIF segment; kinds: {:?}",
segments.iter().map(|s| &s.kind).collect::<Vec<_>>()
);
}
#[test]
fn test_wireless_parse_directly() {
let data: Vec<u8> = vec![
0x19, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00, 0x00,
];
let wf = wireless_mbus_link_layer::WirelessFrame::try_from(data.as_slice());
assert!(
wf.is_ok(),
"wireless parse should succeed: {:?}",
wf.err()
);
let segments = annotate_frame(&data).expect("should parse");
assert_contiguous(&segments, data.len());
assert_eq!(segments[0].kind, SegmentKind::LField);
assert_eq!(segments[1].kind, SegmentKind::CField);
assert_eq!(segments[2].kind, SegmentKind::ManufacturerCode);
assert_eq!(segments[3].kind, SegmentKind::IdentificationNumber);
assert_eq!(segments[4].kind, SegmentKind::Version);
assert_eq!(segments[5].kind, SegmentKind::DeviceType);
}
}