From 728fbe0f4d8a6aa2ac0920215a53cf62b2b59208 Mon Sep 17 00:00:00 2001 From: TJ Jaymes Date: Fri, 23 Jan 2026 22:02:10 -0600 Subject: [PATCH] Add cell/signal info to system stats panel Display LTE signal measurements (RSRP, RSRQ, RSSI, PCI, EARFCN) from DIAG ML1 Serving Cell Measurement messages in the web UI. - Add CellInfo struct with RwLock cache in gsmtap_parser - Add CellSignalInfo to SystemStats API response - Add Cell Signal row to SystemStatsTable with quality indicator - Support Orbic, Tplink, Tmobile, Wingtech devices (graceful degradation for others) --- daemon/src/stats.rs | 46 ++++ .../lib/components/SystemStatsTable.svelte | 50 ++++ daemon/web/src/lib/systemStats.ts | 9 + doc/lte-ml1-serving-cell-measurement.md | 122 ++++++++++ lib/src/diag.rs | 217 +++++++++++++++++- lib/src/diag_device.rs | 4 +- lib/src/gsmtap_parser.rs | 81 ++++++- lib/src/log_codes.rs | 3 + 8 files changed, 529 insertions(+), 3 deletions(-) create mode 100644 doc/lte-ml1-serving-cell-measurement.md diff --git a/daemon/src/stats.rs b/daemon/src/stats.rs index 5f5842c..cd699f5 100644 --- a/daemon/src/stats.rs +++ b/daemon/src/stats.rs @@ -9,10 +9,53 @@ use axum::Json; use axum::extract::State; use axum::http::StatusCode; use log::error; +use rayhunter::gsmtap_parser::get_cached_cell_info; use rayhunter::{Device, util::RuntimeMetadata}; use serde::Serialize; use tokio::process::Command; +/// LTE cell/signal information from DIAG measurements. +/// All fields are optional since they may not be available on all devices +/// or may not have been received yet. +#[derive(Debug, Serialize)] +pub struct CellSignalInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub rsrp_dbm: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rsrq_db: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rssi_dbm: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub pci: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub earfcn: Option, +} + +/// Get cell/signal information for devices that support DIAG. +/// Returns None for devices without DIAG support or if no measurements available. +pub fn get_cell_info(device: &Device) -> Option { + match device { + // Devices with DIAG support + Device::Orbic | Device::Tplink | Device::Tmobile | Device::Wingtech => { + let info = get_cached_cell_info(); + // Only return if we have at least some data + if info.rsrp_dbm.is_some() || info.pci.is_some() { + Some(CellSignalInfo { + rsrp_dbm: info.rsrp_dbm, + rsrq_db: info.rsrq_db, + rssi_dbm: info.rssi_dbm, + pci: info.pci, + earfcn: info.earfcn, + }) + } else { + None + } + } + // Devices without DIAG support + Device::Pinephone | Device::Uz801 => None, + } +} + #[derive(Debug, Serialize)] pub struct SystemStats { pub disk_stats: DiskStats, @@ -20,6 +63,8 @@ pub struct SystemStats { pub runtime_metadata: RuntimeMetadata, #[serde(skip_serializing_if = "Option::is_none")] pub battery_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cell_info: Option, } impl SystemStats { @@ -36,6 +81,7 @@ impl SystemStats { None } }, + cell_info: get_cell_info(device), }) } } diff --git a/daemon/web/src/lib/components/SystemStatsTable.svelte b/daemon/web/src/lib/components/SystemStatsTable.svelte index 29903df..bda7c17 100644 --- a/daemon/web/src/lib/components/SystemStatsTable.svelte +++ b/daemon/web/src/lib/components/SystemStatsTable.svelte @@ -33,6 +33,23 @@ } return text; }); + + // Cell signal quality assessment based on RSRP + // Excellent: >= -80 dBm, Good: -80 to -90, Fair: -90 to -100, Poor: < -100 + let signal_quality = $derived.by(() => { + const rsrp = stats.cell_info?.rsrp_dbm; + if (rsrp === undefined) return { label: 'Unknown', color: 'text-gray-500' }; + if (rsrp >= -80) return { label: 'Excellent', color: 'text-green-600' }; + if (rsrp >= -90) return { label: 'Good', color: 'text-green-600' }; + if (rsrp >= -100) return { label: 'Fair', color: 'text-yellow-600' }; + return { label: 'Poor', color: 'text-red-600' }; + }); + + // Format signal value with unit + function formatSignal(value: number | undefined, unit: string): string { + if (value === undefined) return '—'; + return `${value.toFixed(1)} ${unit}`; + }
+ {#if stats.cell_info} + + Cell Signal + +
+
+ + {signal_quality.label} + + {#if stats.cell_info.rsrp_dbm !== undefined} + + (RSRP: {formatSignal(stats.cell_info.rsrp_dbm, 'dBm')}) + + {/if} +
+
+ {#if stats.cell_info.pci !== undefined} + PCI: {stats.cell_info.pci} + {/if} + {#if stats.cell_info.earfcn !== undefined} + EARFCN: {stats.cell_info.earfcn} + {/if} + {#if stats.cell_info.rsrq_db !== undefined} + RSRQ: {formatSignal(stats.cell_info.rsrq_db, 'dB')} + {/if} + {#if stats.cell_info.rssi_dbm !== undefined} + RSSI: {formatSignal(stats.cell_info.rssi_dbm, 'dBm')} + {/if} +
+
+ + + {/if}
diff --git a/daemon/web/src/lib/systemStats.ts b/daemon/web/src/lib/systemStats.ts index 97648f5..37beeae 100644 --- a/daemon/web/src/lib/systemStats.ts +++ b/daemon/web/src/lib/systemStats.ts @@ -3,6 +3,7 @@ export interface SystemStats { memory_stats: MemoryStats; runtime_metadata: RuntimeMetadata; battery_status?: BatteryStatus; + cell_info?: CellSignalInfo; } export interface RuntimeMetadata { @@ -30,3 +31,11 @@ export interface BatteryStatus { level: number; is_plugged_in: boolean; } + +export interface CellSignalInfo { + rsrp_dbm?: number; + rsrq_db?: number; + rssi_dbm?: number; + pci?: number; + earfcn?: number; +} diff --git a/doc/lte-ml1-serving-cell-measurement.md b/doc/lte-ml1-serving-cell-measurement.md new file mode 100644 index 0000000..e70c6ef --- /dev/null +++ b/doc/lte-ml1-serving-cell-measurement.md @@ -0,0 +1,122 @@ +# LTE ML1 Serving Cell Measurement (0xB193) + +This document describes the Qualcomm DIAG log code 0xB193 (LTE ML1 Serving Cell Measurement Response), which provides detailed LTE signal strength measurements including RSRP, RSRQ, and RSSI. + +## Overview + +Log code 0xB193 (`LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE`) is emitted by the Qualcomm modem's Layer 1 (ML1) component and contains periodic measurements of the serving cell's signal characteristics. Rayhunter captures these measurements and includes the RSRP value in GSMTAP headers for PCAP output. + +## Packet Structure + +The 0xB193 log uses a subpacket architecture common to many Qualcomm DIAG logs: + +``` ++------------------+ +| Main Header | 4 bytes ++------------------+ +| Subpacket Header | 4 bytes ++------------------+ +| Subpacket Data | Variable (version-dependent) ++------------------+ +``` + +### Main Header (4 bytes) + +| Offset | Size | Field | Description | +|--------|------|-----------------|---------------------------------------| +| 0 | 1 | main_version | Main packet version (observed: 1) | +| 1 | 1 | num_subpackets | Number of subpackets (typically 1) | +| 2 | 2 | reserved | Reserved/padding | + +### Subpacket Header (4 bytes) + +| Offset | Size | Field | Description | +|--------|------|-------------------|-------------------------------------| +| 0 | 1 | subpacket_id | Subpacket identifier | +| 1 | 1 | subpacket_version | Subpacket version (see below) | +| 2 | 2 | subpacket_size | Size of subpacket including header | + +### Known Subpacket Versions + +Different modem firmware versions emit different subpacket versions. The field offsets within the subpacket data vary by version: + +| Version | PCI Offset | EARFCN Offset | RSRP Offset | Notes | +|---------|------------|---------------|-------------|--------------------------| +| 4 | 0 | 2 | 12 | Early format (SCAT) | +| 7 | 0 | 4 | 14 | Intermediate format | +| 18-24 | 0 | 4 | 24 | Common on Orbic RC400L | +| 35-40 | 0 | 4 | 28 | Newer modems | + +The Orbic RC400L device used for development emits **subpacket version 18**. + +## Signal Measurement Fields + +### RSRP (Reference Signal Received Power) + +RSRP is the primary signal strength indicator for LTE. The raw 12-bit value is converted to dBm: + +``` +RSRP (dBm) = -180.0 + (raw_value & 0xFFF) * 0.0625 +``` + +Typical range: -140 dBm (very weak) to -44 dBm (very strong) + +### PCI (Physical Cell ID) + +The Physical Cell ID identifies the serving cell. Stored as a 16-bit little-endian value at the PCI offset. + +Range: 0-503 + +### EARFCN (E-UTRA Absolute Radio Frequency Channel Number) + +The EARFCN identifies the carrier frequency. Stored as a 32-bit little-endian value at the EARFCN offset. + +## Implementation Notes + +1. **Caching Strategy**: Since 0xB193 messages arrive independently from RRC OTA messages, rayhunter caches the most recent RSRP value and applies it to subsequent GSMTAP headers. + +2. **Signal Conversion**: The `signal_dbm` field in GSMTAP headers is an `i8`, so the RSRP value is clamped to the range -128 to 0 dBm. + +3. **Version Detection**: The subpacket version determines field offsets. Unknown versions fall back to the v7 layout. + +## References + +### SCAT (Signaling Collection and Analysis Tool) + +The [SCAT project](https://github.com/fgsect/scat) by the Firmware Security (fgsect) research group at TU Berlin provides Qualcomm DIAG log parsers. + +Relevant file: `parsers/qualcomm/diagltelogparser.py` + +```python +# SCAT v4/v5 parser structure (simplified) +# pci = struct.unpack(', }, + /// LTE ML1 Serving Cell Measurement Response (0xB193) + /// Contains RSRP, RSRQ, and RSSI measurements for the serving cell. + /// This is used to populate signal strength in GSMTAP headers. + #[deku(id = "0xb193")] + LteMl1ServingCellMeas { meas: LteMl1ServingCellMeasData }, } #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] @@ -344,6 +349,132 @@ impl LteRrcOtaPacket { } } +/// LTE ML1 Serving Cell Measurement (0xB193) packet structure. +/// Uses subpacket architecture per Mobile Insight / Qualcomm DIAG format. +/// +/// Packet layout: +/// - Main Header: version (1) + num_subpackets (1) + reserved (2) = 4 bytes +/// - SubPacket Header: id (1) + version (1) + size (2) = 4 bytes +/// - SubPacket Data: varies by subpacket version (v4, v7, v18, v19, v22, v24, v35, v36, v40) +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct LteMl1ServingCellMeasData { + pub main_version: u8, + pub num_subpackets: u8, + pub reserved: u16, + // SubPacket header + pub subpacket_id: u8, + pub subpacket_version: u8, + pub subpacket_size: u16, + // SubPacket data - we read enough to get RSRP/RSRQ/RSSI + // The actual layout depends on subpacket_version, but EARFCN and PCI are always first + #[deku(count = "subpacket_size.saturating_sub(4).min(128)")] + pub subpacket_data: Vec, +} + +impl LteMl1ServingCellMeasData { + /// Helper to read a u16 from subpacket data at given offset + fn read_u16(&self, offset: usize) -> Option { + if offset + 2 <= self.subpacket_data.len() { + Some(u16::from_le_bytes([ + self.subpacket_data[offset], + self.subpacket_data[offset + 1], + ])) + } else { + None + } + } + + /// Helper to read a u32 from subpacket data at given offset + fn read_u32(&self, offset: usize) -> Option { + if offset + 4 <= self.subpacket_data.len() { + Some(u32::from_le_bytes([ + self.subpacket_data[offset], + self.subpacket_data[offset + 1], + self.subpacket_data[offset + 2], + self.subpacket_data[offset + 3], + ])) + } else { + None + } + } + + /// Get the RSRP field offset based on subpacket version + /// Returns (earfcn_offset, earfcn_size, rsrp_offset) + fn get_offsets(&self) -> (usize, usize, usize) { + match self.subpacket_version { + // v4: EARFCN(2) + PCI(2) + SFN(2) + skip(6) = offset 12 for RSRP + 4 => (0, 2, 12), + // v7: EARFCN(4) + PCI(2) + SFN(2) + skip(6) = offset 14 for RSRP + 7 => (0, 4, 14), + // v18+: more complex, estimate based on structure + // EARFCN(4) + PCI(2) + ... + skip = ~24-34 for RSRP + 18..=24 => (0, 4, 24), + // v35+: 4-antenna support, larger structure + 35..=40 => (0, 4, 28), + // Unknown version, try v7 offsets + _ => (0, 4, 14), + } + } + + /// Get Physical Cell ID from measurement + pub fn get_pci(&self) -> Option { + let (earfcn_offset, earfcn_size, _) = self.get_offsets(); + let pci_offset = earfcn_offset + earfcn_size; + self.read_u16(pci_offset).map(|v| v & 0x1FF) + } + + /// Get EARFCN from measurement + pub fn get_earfcn(&self) -> Option { + let (earfcn_offset, earfcn_size, _) = self.get_offsets(); + if earfcn_size == 2 { + self.read_u16(earfcn_offset).map(|v| v as u32) + } else { + self.read_u32(earfcn_offset) + } + } + + /// Get RSRP (Reference Signal Received Power) in dBm. + /// Formula: -180 + raw_value * 0.0625 + pub fn get_rsrp_dbm(&self) -> Option { + let (_, _, rsrp_offset) = self.get_offsets(); + self.read_u32(rsrp_offset).map(|raw| { + let rsrp_raw = raw & 0xFFF; + -180.0 + (rsrp_raw as f32) * 0.0625 + }) + } + + /// Get RSSI (Received Signal Strength Indicator) in dBm. + /// Formula: -110 + raw_value * 0.0625 + /// RSSI is typically 12 bytes after RSRP (RSRP + avg_RSRP + RSRQ) + pub fn get_rssi_dbm(&self) -> Option { + let (_, _, rsrp_offset) = self.get_offsets(); + let rssi_offset = rsrp_offset + 12; // Skip RSRP(4) + avg_RSRP(4) + RSRQ(4) + self.read_u32(rssi_offset).map(|raw| { + let rssi_raw = (raw >> 10) & 0x7FF; + -110.0 + (rssi_raw as f32) * 0.0625 + }) + } + + /// Get RSRQ (Reference Signal Received Quality) in dB. + /// Formula: -30 + raw_value * 0.0625 + pub fn get_rsrq_db(&self) -> Option { + let (_, _, rsrp_offset) = self.get_offsets(); + let rsrq_offset = rsrp_offset + 8; // Skip RSRP(4) + avg_RSRP(4) + self.read_u32(rsrq_offset).map(|raw| { + let rsrq_raw = raw & 0x3FF; + -30.0 + (rsrq_raw as f32) * 0.0625 + }) + } + + /// Get signal strength as i8 for GSMTAP header (clamped to valid range). + /// Uses RSRP as the primary signal indicator. + pub fn get_signal_dbm_i8(&self) -> Option { + self.get_rsrp_dbm() + .map(|rsrp| rsrp.clamp(-128.0, 127.0) as i8) + } +} + #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(endian = "little")] pub struct Timestamp { @@ -441,6 +572,10 @@ mod test { bitsize, &crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING, ); + // Expected mask includes: + // - 0xB0C0 (LTE RRC): byte 24 = 0x01 + // - 0xB0E2, 0xB0E3, 0xB0EC, 0xB0ED (NAS): bytes 28-29 = 0x0C, 0x30 + // - 0xB193 (ML1 Serving Cell Meas): byte 50 = 0x08 (bit 3 for code 0x193 = 403) assert_eq!( req, Request::LogConfig(LogConfigRequest::SetMask { @@ -450,7 +585,7 @@ mod test { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 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, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ], }) @@ -697,4 +832,84 @@ mod test { // Verify we consumed the expected number of bytes assert_eq!(rest.len(), 17); } + + #[test] + fn test_lte_ml1_serving_cell_meas_parsing() { + // Test parsing of 0xB193 LTE ML1 Serving Cell Measurement log + // with subpacket version 18 (common on Orbic RC400L) + // + // Structure: + // - Log message header (type=16, log_type=0xB193) + // - LteMl1ServingCellMeasData with v18 subpacket containing RSRP=-95dBm + // + // RSRP calculation: -180 + (raw & 0xFFF) * 0.0625 + // For -95 dBm: raw = (-95 + 180) / 0.0625 = 1360 = 0x550 + + let mut msg_bytes: Vec = vec![ + // Log message header + 0x10, // Message type: Log (16) + 0x00, // pending_msgs + 0x38, 0x00, // outer_length: 56 + 0x34, 0x00, // inner_length: 52 + 0x93, 0xB1, // log_type: 0xB193 (LTE ML1 Serving Cell Meas) + // timestamp (8 bytes, arbitrary) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // LteMl1ServingCellMeasData + 0x01, // main_version + 0x01, // num_subpackets + 0x00, 0x00, // reserved + 0x00, // subpacket_id + 0x12, // subpacket_version: 18 + 0x28, 0x00, // subpacket_size: 40 bytes (including header) + ]; + + // Subpacket data (36 bytes = 40 - 4 for header) + // For v18: EARFCN at offset 0 (4 bytes), PCI at offset 4 (2 bytes), RSRP at offset 24 + let mut subpacket_data = vec![0u8; 36]; + // EARFCN = 975 at offset 0 (u32 LE) + subpacket_data[0..4].copy_from_slice(&975u32.to_le_bytes()); + // PCI = 446 at offset 4 (u16 LE, only lower 9 bits used) + subpacket_data[4..6].copy_from_slice(&446u16.to_le_bytes()); + // RSRP raw = 1360 (0x550) at offset 24 (u32 LE) + // This gives RSRP = -180 + 1360 * 0.0625 = -95 dBm + subpacket_data[24..28].copy_from_slice(&1360u32.to_le_bytes()); + + msg_bytes.extend(subpacket_data); + + let ((rest, _), msg) = Message::from_bytes((&msg_bytes, 0)).unwrap(); + + assert_eq!(rest.len(), 0, "Should consume all bytes"); + + if let Message::Log { + log_type, + body: LogBody::LteMl1ServingCellMeas { meas }, + .. + } = msg + { + assert_eq!(log_type, 0xB193); + assert_eq!(meas.subpacket_version, 18); + + // Verify RSRP extraction + let rsrp = meas.get_rsrp_dbm().expect("Should extract RSRP"); + assert!( + (rsrp - (-95.0)).abs() < 0.1, + "RSRP should be -95 dBm, got {}", + rsrp + ); + + // Verify PCI extraction + let pci = meas.get_pci().expect("Should extract PCI"); + assert_eq!(pci, 446); + + // Verify EARFCN extraction + let earfcn = meas.get_earfcn().expect("Should extract EARFCN"); + assert_eq!(earfcn, 975); + + // Verify i8 conversion for GSMTAP header + let signal_dbm = meas.get_signal_dbm_i8().expect("Should get signal_dbm"); + assert_eq!(signal_dbm, -95); + } else { + panic!("Expected LteMl1ServingCellMeas message, got {:?}", msg); + } + } } diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index 47aa496..b86620a 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; 12] = [ // Layer 2: log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226 // Layer 3: @@ -56,6 +56,8 @@ 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 + // Signal strength measurements: + log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE, // 0xb193 ]; const BUFFER_LEN: usize = 1024 * 1024 * 10; diff --git a/lib/src/gsmtap_parser.rs b/lib/src/gsmtap_parser.rs index b258626..2f847b5 100644 --- a/lib/src/gsmtap_parser.rs +++ b/lib/src/gsmtap_parser.rs @@ -1,9 +1,70 @@ use crate::diag::*; use crate::gsmtap::*; -use log::error; +use log::{debug, error}; +use serde::Serialize; +use std::sync::RwLock; use thiserror::Error; +/// Cached LTE cell information from ML1 measurements. +/// Contains signal strength and cell identity information. +#[derive(Debug, Clone, Default, Serialize)] +pub struct CellInfo { + /// Reference Signal Received Power in dBm (typical range: -140 to -44) + pub rsrp_dbm: Option, + /// Reference Signal Received Quality in dB (typical range: -20 to -3) + pub rsrq_db: Option, + /// Received Signal Strength Indicator in dBm + pub rssi_dbm: Option, + /// Physical Cell ID (0-503) + pub pci: Option, + /// E-UTRA Absolute Radio Frequency Channel Number + pub earfcn: Option, +} + +/// Global cache for the most recent cell/signal measurement. +/// This is populated by LteMl1ServingCellMeas messages and can be used +/// to add signal strength to GSMTAP headers and display in the UI. +/// +/// Uses RwLock for consistent multi-field updates. Reads >> writes so this is efficient. +static CACHED_CELL_INFO: RwLock = RwLock::new(CellInfo { + rsrp_dbm: None, + rsrq_db: None, + rssi_dbm: None, + pci: None, + earfcn: None, +}); + +/// Get the cached cell information. +/// Returns a clone of the current cell info state. +pub fn get_cached_cell_info() -> CellInfo { + CACHED_CELL_INFO + .read() + .expect("cell info lock poisoned") + .clone() +} + +/// Get the cached signal strength (RSRP) in dBm as i8 for GSMTAP header compatibility. +/// Returns 0 if no measurement has been received yet. +pub fn get_cached_signal_dbm() -> i8 { + CACHED_CELL_INFO + .read() + .expect("cell info lock poisoned") + .rsrp_dbm + .map(|rsrp| rsrp.clamp(-128.0, 127.0) as i8) + .unwrap_or(0) +} + +/// Update the cached cell info from a measurement. +fn update_cell_info_cache(meas: &LteMl1ServingCellMeasData) { + let mut cache = CACHED_CELL_INFO.write().expect("cell info lock poisoned"); + cache.rsrp_dbm = meas.get_rsrp_dbm(); + cache.rsrq_db = meas.get_rsrq_db(); + cache.rssi_dbm = meas.get_rssi_dbm(); + cache.pci = meas.get_pci(); + cache.earfcn = meas.get_earfcn(); +} + #[derive(Debug, Error)] pub enum GsmtapParserError { #[error("Invalid LteRrcOtaMessage ext header version {0}")] @@ -138,6 +199,8 @@ fn log_to_gsmtap(value: LogBody) -> Result, GsmtapParserEr header.arfcn = packet.get_earfcn().try_into().unwrap_or(0); header.frame_number = packet.get_sfn(); header.subslot = packet.get_subfn(); + // Apply cached signal strength from ML1 measurements + header.signal_dbm = get_cached_signal_dbm(); Ok(Some(GsmtapMessage { header, payload: packet.take_payload(), @@ -152,6 +215,22 @@ fn log_to_gsmtap(value: LogBody) -> Result, GsmtapParserEr payload: msg, })) } + LogBody::LteMl1ServingCellMeas { meas, .. } => { + // Update the cell info cache with measurement data + update_cell_info_cache(&meas); + debug!( + "ML1 0xB193 v{}: RSRP={:?}dBm, RSRQ={:?}dB, RSSI={:?}dBm, PCI={:?}, EARFCN={:?}", + meas.subpacket_version, + meas.get_rsrp_dbm(), + meas.get_rsrq_db(), + meas.get_rssi_dbm(), + meas.get_pci(), + meas.get_earfcn() + ); + // Measurement messages don't produce GSMTAP output themselves, + // they just update the cell info cache for subsequent messages. + Ok(None) + } _ => { error!("gsmtap_sink: ignoring unhandled log type: {value:?}"); Ok(None) diff --git a/lib/src/log_codes.rs b/lib/src/log_codes.rs index b337e99..84ed28e 100644 --- a/lib/src/log_codes.rs +++ b/lib/src/log_codes.rs @@ -31,6 +31,9 @@ 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; + +// LTE ML1 (Modem Layer 1) measurement logs - contain signal strength data +pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE: u32 = 0xb193; 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;