mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 06:09:26 -07:00
Compare commits
3 Commits
4d54ea03e8
...
fix-81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369a1ba3af | ||
|
|
d6d10ca3a4 | ||
|
|
728fbe0f4d |
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 format_signal(value: number | undefined, unit: string): string {
|
||||
if (value === undefined) return '—';
|
||||
return `${value.toFixed(1)} ${unit}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -116,6 +133,46 @@
|
||||
</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: {format_signal(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: {format_signal(stats.cell_info.rsrq_db, 'dB')}</span
|
||||
>
|
||||
{/if}
|
||||
{#if stats.cell_info.rssi_dbm !== undefined}
|
||||
<span class="ml-2"
|
||||
>RSSI: {format_signal(
|
||||
stats.cell_info.rssi_dbm,
|
||||
'dBm'
|
||||
)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
122
doc/lte-ml1-serving-cell-measurement.md
Normal file
122
doc/lte-ml1-serving-cell-measurement.md
Normal 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).
|
||||
217
lib/src/diag.rs
217
lib/src/diag.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user