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)
This commit is contained in:
TJ Jaymes
2026-01-23 22:02:10 -06:00
committed by Cooper Quintin
parent ed2781a4be
commit 728fbe0f4d
8 changed files with 529 additions and 3 deletions

View File

@@ -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<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rsrq_db: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rssi_dbm: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pci: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub earfcn: Option<u32>,
}
/// 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<CellSignalInfo> {
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<BatteryState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_info: Option<CellSignalInfo>,
}
impl SystemStats {
@@ -36,6 +81,7 @@ impl SystemStats {
None
}
},
cell_info: get_cell_info(device),
})
}
}

View File

@@ -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}`;
}
</script>
<div
@@ -116,6 +133,39 @@
</svg>
</td>
</tr>
{#if stats.cell_info}
<tr class="border-b">
<th class={table_cell_classes}> Cell Signal </th>
<td class={table_cell_classes}>
<div class="flex flex-col gap-1 text-sm">
<div class="flex items-center gap-2">
<span class="font-medium {signal_quality.color}">
{signal_quality.label}
</span>
{#if stats.cell_info.rsrp_dbm !== undefined}
<span class="text-gray-600">
(RSRP: {formatSignal(stats.cell_info.rsrp_dbm, 'dBm')})
</span>
{/if}
</div>
<div class="text-gray-600 text-xs">
{#if stats.cell_info.pci !== undefined}
<span>PCI: {stats.cell_info.pci}</span>
{/if}
{#if stats.cell_info.earfcn !== undefined}
<span class="ml-2">EARFCN: {stats.cell_info.earfcn}</span>
{/if}
{#if stats.cell_info.rsrq_db !== undefined}
<span class="ml-2">RSRQ: {formatSignal(stats.cell_info.rsrq_db, 'dB')}</span>
{/if}
{#if stats.cell_info.rssi_dbm !== undefined}
<span class="ml-2">RSSI: {formatSignal(stats.cell_info.rssi_dbm, 'dBm')}</span>
{/if}
</div>
</div>
</td>
</tr>
{/if}
</tbody>
</table>
</div>

View File

@@ -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;
}

View File

@@ -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('<H', payload[0:2])
# earfcn = struct.unpack('<H', payload[2:4]) # or <L for 32-bit
# rsrp_raw = struct.unpack('<L', payload[offset:offset+4])
```
### Mobile Insight
The [Mobile Insight project](https://github.com/mobile-insight/mobileinsight-core) from UCLA WiNG Lab provides comprehensive Qualcomm DIAG parsing with extensive version support.
Relevant file: `mobile_insight/analyzer/msg_logger.py` and related LTE analyzers
Mobile Insight documents subpacket versions 4, 7, 18, 19, 22, 24, 35, 36, and 40, with version-specific field layouts.
### QCSuper
The [QCSuper project](https://github.com/P1sec/QCSuper) by P1 Security provides another implementation of Qualcomm DIAG protocol handling.
### 3GPP Specifications
- **3GPP TS 36.214**: Physical layer measurements (defines RSRP, RSRQ, RSSI)
- **3GPP TS 36.133**: Requirements for support of radio resource management
## Example Output
When rayhunter captures a 0xB193 log, debug output shows:
```
ML1 0xB193 v18: RSRP=-94.8dBm, PCI=446, EARFCN=975
```
The corresponding GSMTAP packets in Wireshark will display `Signal dBm: -95` (rounded to i8).

View File

@@ -222,6 +222,11 @@ pub enum LogBody {
#[deku(count = "hdr_len")]
msg: Vec<u8>,
},
/// 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<u8>,
}
impl LteMl1ServingCellMeasData {
/// Helper to read a u16 from subpacket data at given offset
fn read_u16(&self, offset: usize) -> Option<u16> {
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<u32> {
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<u16> {
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<u32> {
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<f32> {
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<f32> {
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<f32> {
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<i8> {
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<u8> = 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);
}
}
}

View File

@@ -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;

View File

@@ -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<f32>,
/// Reference Signal Received Quality in dB (typical range: -20 to -3)
pub rsrq_db: Option<f32>,
/// Received Signal Strength Indicator in dBm
pub rssi_dbm: Option<f32>,
/// Physical Cell ID (0-503)
pub pci: Option<u16>,
/// E-UTRA Absolute Radio Frequency Channel Number
pub earfcn: Option<u32>,
}
/// 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<CellInfo> = 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<Option<GsmtapMessage>, 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<Option<GsmtapMessage>, 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)

View File

@@ -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;