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
+46
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),
})
}
}
@@ -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>
+9
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;
}