mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-18 10:29:44 -07:00
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:
committed by
Cooper Quintin
parent
ed2781a4be
commit
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 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user