mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 10:13:35 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 369a1ba3af | |||
| d6d10ca3a4 | |||
| 728fbe0f4d |
@@ -9,10 +9,53 @@ use axum::Json;
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use rayhunter::gsmtap_parser::get_cached_cell_info;
|
||||||
use rayhunter::{Device, util::RuntimeMetadata};
|
use rayhunter::{Device, util::RuntimeMetadata};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::process::Command;
|
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)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct SystemStats {
|
pub struct SystemStats {
|
||||||
pub disk_stats: DiskStats,
|
pub disk_stats: DiskStats,
|
||||||
@@ -20,6 +63,8 @@ pub struct SystemStats {
|
|||||||
pub runtime_metadata: RuntimeMetadata,
|
pub runtime_metadata: RuntimeMetadata,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub battery_status: Option<BatteryState>,
|
pub battery_status: Option<BatteryState>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cell_info: Option<CellSignalInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemStats {
|
impl SystemStats {
|
||||||
@@ -36,6 +81,7 @@ impl SystemStats {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
cell_info: get_cell_info(device),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,23 @@
|
|||||||
}
|
}
|
||||||
return text;
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -116,6 +133,46 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface SystemStats {
|
|||||||
memory_stats: MemoryStats;
|
memory_stats: MemoryStats;
|
||||||
runtime_metadata: RuntimeMetadata;
|
runtime_metadata: RuntimeMetadata;
|
||||||
battery_status?: BatteryStatus;
|
battery_status?: BatteryStatus;
|
||||||
|
cell_info?: CellSignalInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeMetadata {
|
export interface RuntimeMetadata {
|
||||||
@@ -30,3 +31,11 @@ export interface BatteryStatus {
|
|||||||
level: number;
|
level: number;
|
||||||
is_plugged_in: boolean;
|
is_plugged_in: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CellSignalInfo {
|
||||||
|
rsrp_dbm?: number;
|
||||||
|
rsrq_db?: number;
|
||||||
|
rssi_dbm?: number;
|
||||||
|
pci?: number;
|
||||||
|
earfcn?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
+216
-1
@@ -222,6 +222,11 @@ pub enum LogBody {
|
|||||||
#[deku(count = "hdr_len")]
|
#[deku(count = "hdr_len")]
|
||||||
msg: Vec<u8>,
|
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)]
|
#[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)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
#[deku(endian = "little")]
|
#[deku(endian = "little")]
|
||||||
pub struct Timestamp {
|
pub struct Timestamp {
|
||||||
@@ -441,6 +572,10 @@ mod test {
|
|||||||
bitsize,
|
bitsize,
|
||||||
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
|
&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!(
|
assert_eq!(
|
||||||
req,
|
req,
|
||||||
Request::LogConfig(LogConfigRequest::SetMask {
|
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, 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, 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, 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,
|
0x0, 0x0,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -697,4 +832,84 @@ mod test {
|
|||||||
// Verify we consumed the expected number of bytes
|
// Verify we consumed the expected number of bytes
|
||||||
assert_eq!(rest.len(), 17);
|
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),
|
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:
|
// Layer 2:
|
||||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
||||||
// Layer 3:
|
// 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
|
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
||||||
// User IP traffic:
|
// User IP traffic:
|
||||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
|
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;
|
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||||
|
|||||||
@@ -1,9 +1,70 @@
|
|||||||
use crate::diag::*;
|
use crate::diag::*;
|
||||||
use crate::gsmtap::*;
|
use crate::gsmtap::*;
|
||||||
|
|
||||||
use log::error;
|
use log::{debug, error};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::sync::RwLock;
|
||||||
use thiserror::Error;
|
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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum GsmtapParserError {
|
pub enum GsmtapParserError {
|
||||||
#[error("Invalid LteRrcOtaMessage ext header version {0}")]
|
#[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.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
||||||
header.frame_number = packet.get_sfn();
|
header.frame_number = packet.get_sfn();
|
||||||
header.subslot = packet.get_subfn();
|
header.subslot = packet.get_subfn();
|
||||||
|
// Apply cached signal strength from ML1 measurements
|
||||||
|
header.signal_dbm = get_cached_signal_dbm();
|
||||||
Ok(Some(GsmtapMessage {
|
Ok(Some(GsmtapMessage {
|
||||||
header,
|
header,
|
||||||
payload: packet.take_payload(),
|
payload: packet.take_payload(),
|
||||||
@@ -152,6 +215,22 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
payload: msg,
|
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:?}");
|
error!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub const LOG_NR_RRC_OTA_MSG_LOG_C: u32 = 0xb821;
|
|||||||
// These are 4G-related log types.
|
// These are 4G-related log types.
|
||||||
|
|
||||||
pub const LOG_LTE_RRC_OTA_MSG_LOG_C: u32 = 0xb0c0;
|
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_IN_MSG_LOG_C: u32 = 0xb0e2;
|
||||||
pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3;
|
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;
|
pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec;
|
||||||
|
|||||||
Reference in New Issue
Block a user