added documentation and polishing UI around GPS mode

This commit is contained in:
Carlos Guerra
2026-03-29 17:44:39 +02:00
committed by Will Greenberg
parent 66f0c2a336
commit 5451e23293
11 changed files with 68 additions and 59 deletions

View File

@@ -162,49 +162,6 @@ fn resolve_bin(name: &str) -> Option<String> {
None
}
impl Config {
pub fn wifi_config(&self) -> wifi_station::WifiConfig {
let (wpa_bin, hostapd_conf, ctrl_interface) = match self.device {
Device::Tmobile | Device::Wingtech => (
Some("/usr/sbin/wpa_supplicant".into()),
Some("/data/configs/hostapd.conf".into()),
None,
),
Device::Uz801 => (
Some("/system/bin/wpa_supplicant".into()),
Some("/data/misc/wifi/hostapd.conf".into()),
Some("/data/misc/wifi/sockets".into()),
),
_ => (None, None, None),
};
wifi_station::WifiConfig {
wifi_enabled: self.wifi_enabled,
dns_servers: self.dns_servers.clone(),
wifi_ssid: self.wifi_ssid.clone(),
wifi_password: self.wifi_password.clone(),
security_type: self.wifi_security,
wpa_supplicant_bin: wpa_bin.or_else(|| resolve_bin("wpa_supplicant")),
hostapd_conf,
ctrl_interface,
udhcpc_hook_path: Some("/data/rayhunter/udhcpc-hook.sh".into()),
dhcp_lease_path: Some("/data/rayhunter/dhcp_lease".into()),
wpa_conf_path: Some("/data/rayhunter/wpa_sta.conf".into()),
iw_bin: resolve_bin("iw"),
udhcpc_bin: resolve_bin("udhcpc"),
crash_log_dir: Some("/data/rayhunter/crash-logs".into()),
wakelock_name: Some("rayhunter".into()),
}
}
}
fn resolve_bin(name: &str) -> Option<String> {
let local = format!("/data/rayhunter/bin/{name}");
if std::path::Path::new(&local).exists() {
return Some(local);
}
None
}
pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
where
P: AsRef<std::path::Path>,

View File

@@ -56,6 +56,7 @@ pub struct DiagTask {
notification_channel: tokio::sync::mpsc::Sender<Notification>,
min_space_to_start_mb: u64,
min_space_to_continue_mb: u64,
gps_mode: u8,
state: DiagState,
max_type_seen: EventType,
bytes_since_space_check: usize,
@@ -104,6 +105,7 @@ impl DiagTask {
notification_channel: tokio::sync::mpsc::Sender<Notification>,
min_space_to_start_mb: u64,
min_space_to_continue_mb: u64,
gps_mode: u8,
) -> Self {
Self {
ui_update_sender,
@@ -112,6 +114,7 @@ impl DiagTask {
notification_channel,
min_space_to_start_mb,
min_space_to_continue_mb,
gps_mode,
state: DiagState::Stopped,
max_type_seen: EventType::Informational,
bytes_since_space_check: 0,
@@ -144,7 +147,7 @@ impl DiagTask {
DiskSpaceCheck::Failed => {}
}
let (qmdl_file, analysis_file) = match qmdl_store.new_entry().await {
let (qmdl_file, analysis_file) = match qmdl_store.new_entry(self.gps_mode).await {
Ok(files) => files,
Err(e) => {
let msg = format!("failed creating QMDL file entry: {e}");
@@ -381,6 +384,7 @@ pub fn run_diag_read_thread(
notification_channel: tokio::sync::mpsc::Sender<Notification>,
min_space_to_start_mb: u64,
min_space_to_continue_mb: u64,
gps_mode: u8,
) {
task_tracker.spawn(async move {
info!("Using configuration for device: {0:?}", device);
@@ -396,7 +400,8 @@ pub fn run_diag_read_thread(
analyzer_config,
notification_channel,
min_space_to_start_mb,
min_space_to_continue_mb
min_space_to_continue_mb,
gps_mode,
);
qmdl_file_tx
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })

View File

@@ -1,17 +1,37 @@
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use crate::server::ServerState;
/// Accepts both a JSON number and a numeric string (e.g. `"1234567890"` or `1234567890`).
/// Truncates floats to seconds. Returns an error for non-numeric values.
fn deserialize_unix_ts<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
use serde_json::Value;
match Value::deserialize(deserializer)? {
Value::Number(n) => n.as_i64()
.or_else(|| n.as_f64().map(|f| f as i64))
.ok_or_else(|| de::Error::custom("timestamp out of range")),
Value::String(s) => s.trim().parse::<f64>()
.map(|f| f as i64)
.map_err(|_| de::Error::custom("timestamp must be a numeric value")),
_ => Err(de::Error::custom("timestamp must be a number or numeric string")),
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GpsData {
pub latitude: f64,
pub longitude: f64,
pub timestamp: String,
#[serde(deserialize_with = "deserialize_unix_ts")]
pub timestamp: i64,
}
#[derive(Serialize, Deserialize)]

View File

@@ -232,6 +232,7 @@ async fn run_with_config(
notification_service.new_handler(),
config.min_space_to_start_recording_mb,
config.min_space_to_continue_recording_mb,
config.gps_mode,
);
info!("Starting UI");
@@ -305,7 +306,7 @@ async fn run_with_config(
(Some(lat), Some(lon)) => Some(gps::GpsData {
latitude: lat,
longitude: lon,
timestamp: "fixed".to_string(),
timestamp: 0,
}),
_ => None,
}

View File

@@ -70,10 +70,12 @@ pub struct ManifestEntry {
/// When the manifest was uploaded to a WebDAV server
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
pub upload_time: Option<DateTime<Local>>,
#[serde(default)]
pub gps_mode: Option<u8>,
}
impl ManifestEntry {
fn new() -> Self {
fn new(gps_mode: u8) -> Self {
let now = rayhunter::clock::get_adjusted_now();
let metadata = RuntimeMetadata::new();
ManifestEntry {
@@ -86,6 +88,7 @@ impl ManifestEntry {
arch: Some(metadata.arch),
stop_reason: None,
upload_time: None,
gps_mode: Some(gps_mode),
}
}
@@ -223,6 +226,7 @@ impl RecordingStore {
arch: None,
stop_reason: None,
upload_time: None,
gps_mode: None,
});
}
@@ -255,12 +259,12 @@ impl RecordingStore {
// Closes the current entry (if needed), creates a new entry based on the
// current time, and updates the manifest. Returns a tuple of the entry's
// newly created QMDL file and analysis file.
pub async fn new_entry(&mut self) -> Result<(File, File), RecordingStoreError> {
pub async fn new_entry(&mut self, gps_mode: u8) -> Result<(File, File), RecordingStoreError> {
// if we've already got an entry open, close it
if self.current_entry.is_some() {
self.close_current_entry().await?;
}
let new_entry = ManifestEntry::new();
let new_entry = ManifestEntry::new(gps_mode);
let qmdl_filepath = new_entry.get_qmdl_filepath(&self.path);
let qmdl_file = File::create(&qmdl_filepath)
.await
@@ -545,7 +549,7 @@ mod tests {
async fn test_creating_updating_and_closing_entries() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let _ = store.new_entry(0).await.unwrap();
let entry_index = store.current_entry.unwrap();
assert_eq!(
RecordingStore::read_manifest(dir.path()).await.unwrap(),
@@ -582,7 +586,7 @@ mod tests {
async fn test_create_on_existing_store() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let _ = store.new_entry(0).await.unwrap();
let entry_index = store.current_entry.unwrap();
store
.update_entry_qmdl_size(entry_index, 1000)
@@ -596,9 +600,9 @@ mod tests {
async fn test_repeated_new_entries() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let _ = store.new_entry(0).await.unwrap();
let entry_index = store.current_entry.unwrap();
let _ = store.new_entry().await.unwrap();
let _ = store.new_entry(0).await.unwrap();
let new_entry_index = store.current_entry.unwrap();
assert_ne!(entry_index, new_entry_index);
assert_eq!(store.manifest.entries.len(), 2);
@@ -608,7 +612,7 @@ mod tests {
async fn test_delete_all_entries() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let _ = store.new_entry(0).await.unwrap();
assert!(store.current_entry.is_some());
store.delete_all_entries().await.unwrap();

View File

@@ -523,7 +523,7 @@ mod tests {
) -> String {
let entry_name = {
let mut store = store_lock.write().await;
let (mut qmdl_file, _analysis_file) = store.new_entry().await.unwrap();
let (mut qmdl_file, _analysis_file) = store.new_entry(0).await.unwrap();
if !test_data.is_empty() {
use tokio::io::AsyncWriteExt;

View File

@@ -70,6 +70,12 @@
>
</p>
{/if}
{#if entry.gps_mode !== undefined}
<p>
<b>GPS Mode:</b>
{entry.gps_mode === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'}
</p>
{/if}
</div>
{#if metadata && metadata.analyzers}
<div>

View File

@@ -86,6 +86,11 @@
{entry.stop_reason}
</div>
{/if}
{#if entry.gps_mode !== undefined}
<div class="text-sm text-gray-500">
GPS: {entry.gps_mode === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'}
</div>
{/if}
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-auto">
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />

View File

@@ -13,6 +13,7 @@ interface JsonManifestEntry {
qmdl_size_bytes: number;
stop_reason: string | null;
upload_time: string | null;
gps_mode: number | null;
}
export class Manifest {
@@ -61,6 +62,7 @@ export class ManifestEntry {
public analysis_report: AnalysisReport | string | undefined = $state(undefined);
public stop_reason: string | undefined = $state(undefined);
public upload_time: Date | undefined = $state(undefined);
public gps_mode: number | undefined = $state(undefined);
constructor(json: JsonManifestEntry) {
this.name = json.name;
@@ -75,6 +77,9 @@ export class ManifestEntry {
if (json.upload_time) {
this.upload_time = new Date(json.upload_time);
}
if (json.gps_mode !== null) {
this.gps_mode = json.gps_mode;
}
}
get_readable_qmdl_size(): string {

View File

@@ -160,7 +160,8 @@ export async function get_daemon_time(): Promise<TimeResponse> {
export interface GpsData {
latitude: number;
longitude: number;
timestamp: string;
/** Unix timestamp in seconds (0 = fixed/no real time). */
timestamp: number;
}
export async function get_gps(): Promise<GpsData | null> {

View File

@@ -299,6 +299,7 @@
GPS Status
</span>
{#if gps_data}
{@const gps_date_formatter = new Intl.DateTimeFormat(undefined, { timeStyle: 'long', dateStyle: 'short' })}
<table class="w-full text-sm">
<tbody>
<tr class="border-b border-gray-100">
@@ -311,7 +312,11 @@
</tr>
<tr>
<td class="py-1 pr-4 text-gray-500 font-medium">GPS Timestamp</td>
<td class="py-1 font-mono">{gps_data.timestamp}</td>
<td class="py-1 font-mono">
{gps_data.timestamp > 0
? gps_date_formatter.format(new Date(gps_data.timestamp * 1000))
: 'Fixed'}
</td>
</tr>
</tbody>
</table>