From 8f03ad8f4ab177aff24013428f41f98b007a921a Mon Sep 17 00:00:00 2001 From: Carlos Guerra Date: Sun, 31 May 2026 14:46:05 +0200 Subject: [PATCH] Collect signal strength and timing advances. LTE serving cell measurements (0xb17f) and RACH Timing Advance (0xb062) --- lib/src/diag/diaglog/measurement.rs | 101 ++++++++++++++++++++++++ lib/src/diag/diaglog/mod.rs | 19 ++++- lib/src/diag_device.rs | 6 +- lib/src/gsmtap/parser.rs | 115 ++++++++++++++++++++++++++++ lib/src/log_codes.rs | 4 + 5 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 lib/src/diag/diaglog/measurement.rs diff --git a/lib/src/diag/diaglog/measurement.rs b/lib/src/diag/diaglog/measurement.rs new file mode 100644 index 0000000..9b0c494 --- /dev/null +++ b/lib/src/diag/diaglog/measurement.rs @@ -0,0 +1,101 @@ +use deku::prelude::*; + +// Qualcomm ML1 (physical layer) serving cell measurement log (0xb17f). +// Format from SCAT: https://github.com/fgsect/scat/blob/master/src/scat/parsers/qualcomm/diagltelogparser.py +// V5 format string (after version byte): ' u32 { + match self { + Self::V4 { earfcn, .. } => *earfcn as u32, + Self::V5 { earfcn, .. } => *earfcn, + } + } + + // Lower 9 bits are the Physical Cell ID (0–503); upper bits encode serving layer. + pub fn get_pci(&self) -> u16 { + let raw = match self { + Self::V4 { pci_serv_layer, .. } => *pci_serv_layer, + Self::V5 { pci_serv_layer, .. } => *pci_serv_layer, + }; + raw & 0x1FF + } + + // RSRP lower 12 bits, 1/16 dB steps, -180 dBm base. + // Returns whole dBm clamped to i8 for the GSMTAP signal_dbm header field. + pub fn get_rsrp_dbm(&self) -> i8 { + let raw = match self { + Self::V4 { meas_rsrp, .. } => *meas_rsrp, + Self::V5 { meas_rsrp, .. } => *meas_rsrp, + }; + let sixteenth_db = -2880_i32 + (raw & 0x0FFF) as i32; + (sixteenth_db / 16).clamp(i8::MIN as i32, i8::MAX as i32) as i8 + } +} + +#[cfg(test)] +mod test { + use crate::diag::{Message, diaglog::LogBody}; + use super::*; + + #[test] + fn test_lte_ml1_v5_rsrp() { + // Probe capture: full diag Message wrapping a 0xb17f log (Version 5, Band 3 / EARFCN 1849). + // Constructed as: opcode(1) + pending(1) + outer_len(2) + inner_len(2) + + // log_type(2=0xb17f LE) + timestamp(8) + body(40) = 56 bytes total + let mut msg_bytes: Vec = vec![ + 0x10, 0x00, // opcode=Log, pending=0 + 56, 0, 56, 0, // outer_length=56, inner_length=56 + 0x7f, 0xb1, // log_type = 0xb17f (LE) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // timestamp + ]; + msg_bytes.extend_from_slice(&[ + 0x05, // version=5 + 0x01, 0x00, 0x00, 0x39, 0x07, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, + 0xab, 0xb5, 0x5a, 0x00, 0xab, 0xb5, 0x5a, 0x00, + 0x1a, 0x69, 0xa4, 0x11, 0x1a, 0x45, 0x0d, 0x00, 0x86, 0xa7, 0xae, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x1c, 0x00, 0x00, + ]); + let msg = Message::from_bytes((&msg_bytes, 0)).expect("Message parse failed").1; + if let Message::Log { body: LogBody::LteMl1ServingCellMeas { packet, .. }, .. } = msg { + assert_eq!(packet.get_earfcn(), 1849); + let rsrp = packet.get_rsrp_dbm(); + assert!(rsrp <= -44 && rsrp >= -120, "RSRP {rsrp} dBm outside valid LTE range"); + } else { + panic!("unexpected message variant"); + } + } +} diff --git a/lib/src/diag/diaglog/mod.rs b/lib/src/diag/diaglog/mod.rs index 41f3701..86f4c2a 100644 --- a/lib/src/diag/diaglog/mod.rs +++ b/lib/src/diag/diaglog/mod.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, FixedOffset}; use deku::prelude::*; +pub mod measurement; pub mod rrc; #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] @@ -75,6 +76,18 @@ pub enum LogBody { #[deku(count = "hdr_len")] msg: Vec, }, + #[deku(id = "0xb17f")] + LteMl1ServingCellMeas { + version: u8, + #[deku(ctx = "*version")] + packet: measurement::LteMl1ServingCellMeasPacket, + }, + // Raw bytes; subpacket parsing happens in gsmtap_parser to extract Timing Advance + #[deku(id = "0xb062")] + LteMacRachResponse { + #[deku(count = "hdr_len")] + payload: Vec, + }, } #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] @@ -228,11 +241,11 @@ pub(crate) mod test { log_type, log_mask_bitsize: bitsize, log_mask: vec![ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xc, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, + 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, ], }) ); diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index fe42592..f9d2f31 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -40,7 +40,7 @@ pub enum DiagDeviceError { ParseMessagesContainerError(deku::DekuError), } -pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [ +pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 13] = [ // Layer 2: log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226 // Layer 3: @@ -56,6 +56,10 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [ log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed // User IP traffic: log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb + // LTE physical layer serving cell measurements: RSRP, RSRQ, RSSI + log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C, // 0xb17f + // LTE MAC Random Access Channel response: contains Timing Advance + log_codes::LOG_LTE_MAC_RACH_RESPONSE_C, // 0xb062 ]; const BUFFER_LEN: usize = 1024 * 1024 * 10; diff --git a/lib/src/gsmtap/parser.rs b/lib/src/gsmtap/parser.rs index 2c938d8..b45a12d 100644 --- a/lib/src/gsmtap/parser.rs +++ b/lib/src/gsmtap/parser.rs @@ -153,6 +153,19 @@ fn log_to_gsmtap(value: LogBody) -> Result, GsmtapParserEr payload: msg, })) } + LogBody::LteMl1ServingCellMeas { packet, .. } => { + // frame_number reused for PCI (normally SFN in RRC frames) so all three + // serving-cell fields are accessible in Wireshark as gsmtap.* columns. + let mut header = GsmtapHeader::new(GsmtapType::QcDiag); + header.signal_dbm = packet.get_rsrp_dbm(); + header.arfcn = packet.get_earfcn().try_into().unwrap_or(0); + header.frame_number = packet.get_pci() as u32; + Ok(Some(GsmtapMessage { + header, + payload: vec![], + })) + } + LogBody::LteMacRachResponse { payload } => Ok(parse_rach_response(&payload)), _ => { error!("gsmtap_sink: ignoring unhandled log type: {value:?}"); Ok(None) @@ -160,6 +173,108 @@ fn log_to_gsmtap(value: LogBody) -> Result, GsmtapParserEr } } +// Parses a 0xb062 RACH response log and reconstructs a 7-byte MAC RAR PDU for Wireshark. +// Returns None if the log contains no MSG2 (no Timing Advance was received). +fn parse_rach_response(payload: &[u8]) -> Option { + // Outer header: version(u8) + num_subpackets(u8) + reserved(u16) + if payload.len() < 4 || payload[0] != 0x01 { + return None; + } + let num_subpackets = payload[1] as usize; + let mut offset = 4; + + for _ in 0..num_subpackets { + // Subpacket header: id(u8) + version(u8) + size(u16 LE) + if offset + 4 > payload.len() { + break; + } + let sp_id = payload[offset]; + let sp_version = payload[offset + 1]; + let sp_size = u16::from_le_bytes([payload[offset + 2], payload[offset + 3]]) as usize; + if sp_size < 4 { + break; + } + let sp_end = offset + sp_size; + if sp_end > payload.len() { + break; + } + + if sp_id == 0x06 { + // RACH Attempt subpacket + if let Some(msg) = extract_rach_attempt_gsmtap(&payload[offset + 4..sp_end], sp_version) { + return Some(msg); + } + } + + offset = sp_end; + } + None +} + +fn extract_rach_attempt_gsmtap(body: &[u8], version: u8) -> Option { + // Per SCAT diagltelogparser.py, RACH Attempt subpacket layouts: + // v0x02: hdr=4B, msg1=4B(BBh), msg2=7B(HBHh) + // v0x03/0x31: hdr=6B, msg1=4B(BBh), msg2=7B(HBHh) + // v0x32: hdr=6B, msg1=7B(BBhHb), msg2=7B(HBHh) + // rapid_offset is the header byte holding preamble_index & 0x3F (the RAPID) + let (hdr_size, msg1_size, rapid_offset, bitmask_offset) = match version { + 0x02 => (4usize, 4usize, 0usize, 3usize), + 0x03 | 0x31 => (6, 4, 2, 5), + 0x32 => (6, 7, 2, 5), + _ => return None, + }; + + if body.len() < hdr_size { + return None; + } + let msg_bitmask = body[bitmask_offset]; + let rapid = body[rapid_offset] & 0x3F; + let msg1_present = msg_bitmask & 0x01 != 0; + let msg2_present = msg_bitmask & 0x02 != 0; + + if !msg2_present { + return None; + } + + // MSG2: backoff(u16) + result(u8) + tc_rnti(u16) + ta(u16) = 7 bytes + let msg2_offset = hdr_size + if msg1_present { msg1_size } else { 0 }; + if body.len() < msg2_offset + 7 { + return None; + } + let tc_rnti = u16::from_le_bytes([body[msg2_offset + 3], body[msg2_offset + 4]]); + let ta_raw = u16::from_le_bytes([body[msg2_offset + 5], body[msg2_offset + 6]]); + // 0xFFFF is a Qualcomm sentinel meaning the RAR was received but TA was not valid + if ta_raw == 0xFFFF { + return None; + } + let ta = ta_raw & 0x7FF; + + // Reconstruct 7-byte MAC RAR PDU (3GPP TS 36.321 §6.1.5): + // subheader: E=0, T=0, RAPID[5:0] + // payload: R(1)|TA[10:3](8) | TA[2:0](3)|ULGrant[19:15](5) | ULGrant[14:7](8) | + // ULGrant[6:0](7)|TC-RNTI[15](1) | TC-RNTI[14:7](8) | TC-RNTI[6:0](7)|0(1) + // + // Use LteMacFramed (0x0f) so Wireshark's mac-lte dissector knows the RNTI type is + // RA-RNTI (type=2) and applies the RAR PDU format. The 4-byte framing prefix is: + // [RadioType=1(FDD)][Direction=1(DL)][RNTIType=2(RA-RNTI)][0x01=payload-marker] + let payload = vec![ + 0x01u8, 0x01, 0x02, 0x01, // framing: FDD, DL, RA-RNTI, payload-marker + rapid & 0x3F, + ((ta >> 3) & 0xFF) as u8, + ((ta & 0x07) as u8) << 5, + 0u8, // UL grant zeroed; Wireshark only needs TA and TC-RNTI to decode the RAR + ((tc_rnti >> 15) & 0x01) as u8, + ((tc_rnti >> 7) & 0xFF) as u8, + ((tc_rnti & 0x7F) as u8) << 1, + ]; + + let mut header = GsmtapHeader::new(GsmtapType::LteMacFramed); + // Wireshark 4.x does not dispatch GSMTAP type 0x0f to its mac-lte dissector, so + // mac-lte.rar.ta is unavailable. TA is also stored in frame_number (gsmtap.frame_nr). + header.frame_number = ta as u32; + Some(GsmtapMessage { header, payload }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/lib/src/log_codes.rs b/lib/src/log_codes.rs index b337e99..65af35f 100644 --- a/lib/src/log_codes.rs +++ b/lib/src/log_codes.rs @@ -31,6 +31,10 @@ pub const LOG_NR_RRC_OTA_MSG_LOG_C: u32 = 0xb821; // These are 4G-related log types. pub const LOG_LTE_RRC_OTA_MSG_LOG_C: u32 = 0xb0c0; +// Qualcomm ML1 (physical layer) serving cell measurement report: RSRP, RSRQ, RSSI +pub const LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C: u32 = 0xb17f; +// Qualcomm MAC layer RACH response log: contains Timing Advance from Random Access Response +pub const LOG_LTE_MAC_RACH_RESPONSE_C: u32 = 0xb062; pub const LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C: u32 = 0xb0e2; pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3; pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec;