From 5451e23293ae3bd3133de7d9471baf1e312c7bfb Mon Sep 17 00:00:00 2001 From: Carlos Guerra Date: Sun, 29 Mar 2026 17:44:39 +0200 Subject: [PATCH] added documentation and polishing UI around GPS mode --- daemon/src/config.rs | 43 ------------------- daemon/src/diag.rs | 9 +++- daemon/src/gps.rs | 24 ++++++++++- daemon/src/main.rs | 3 +- daemon/src/qmdl_store.rs | 20 +++++---- daemon/src/server.rs | 2 +- .../src/lib/components/AnalysisView.svelte | 6 +++ .../src/lib/components/ManifestCard.svelte | 5 +++ daemon/web/src/lib/manifest.svelte.ts | 5 +++ daemon/web/src/lib/utils.svelte.ts | 3 +- daemon/web/src/routes/+page.svelte | 7 ++- 11 files changed, 68 insertions(+), 59 deletions(-) diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 38e11d2..3c988ba 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -162,49 +162,6 @@ fn resolve_bin(name: &str) -> Option { 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 { - let local = format!("/data/rayhunter/bin/{name}"); - if std::path::Path::new(&local).exists() { - return Some(local); - } - None -} - pub async fn parse_config

(path: P) -> Result where P: AsRef, diff --git a/daemon/src/diag.rs b/daemon/src/diag.rs index 3db7492..9a309c5 100644 --- a/daemon/src/diag.rs +++ b/daemon/src/diag.rs @@ -56,6 +56,7 @@ pub struct DiagTask { notification_channel: tokio::sync::mpsc::Sender, 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, 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, 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 }) diff --git a/daemon/src/gps.rs b/daemon/src/gps.rs index 2d09011..fd499df 100644 --- a/daemon/src/gps.rs +++ b/daemon/src/gps.rs @@ -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 +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::() + .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)] diff --git a/daemon/src/main.rs b/daemon/src/main.rs index c77de44..08c8cd0 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -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, } diff --git a/daemon/src/qmdl_store.rs b/daemon/src/qmdl_store.rs index 1eefb66..a66cb8f 100644 --- a/daemon/src/qmdl_store.rs +++ b/daemon/src/qmdl_store.rs @@ -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>, + #[serde(default)] + pub gps_mode: Option, } 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(); diff --git a/daemon/src/server.rs b/daemon/src/server.rs index 12fc72d..e4bc204 100644 --- a/daemon/src/server.rs +++ b/daemon/src/server.rs @@ -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; diff --git a/daemon/web/src/lib/components/AnalysisView.svelte b/daemon/web/src/lib/components/AnalysisView.svelte index bc6d1bb..82be121 100644 --- a/daemon/web/src/lib/components/AnalysisView.svelte +++ b/daemon/web/src/lib/components/AnalysisView.svelte @@ -70,6 +70,12 @@ >

{/if} + {#if entry.gps_mode !== undefined} +

+ GPS Mode: + {entry.gps_mode === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'} +

+ {/if} {#if metadata && metadata.analyzers}
diff --git a/daemon/web/src/lib/components/ManifestCard.svelte b/daemon/web/src/lib/components/ManifestCard.svelte index 1b5d084..9f1a2c9 100644 --- a/daemon/web/src/lib/components/ManifestCard.svelte +++ b/daemon/web/src/lib/components/ManifestCard.svelte @@ -86,6 +86,11 @@ {entry.stop_reason}
{/if} + {#if entry.gps_mode !== undefined} +
+ GPS: {entry.gps_mode === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'} +
+ {/if}
diff --git a/daemon/web/src/lib/manifest.svelte.ts b/daemon/web/src/lib/manifest.svelte.ts index fdb1b32..b34aeaa 100644 --- a/daemon/web/src/lib/manifest.svelte.ts +++ b/daemon/web/src/lib/manifest.svelte.ts @@ -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 { diff --git a/daemon/web/src/lib/utils.svelte.ts b/daemon/web/src/lib/utils.svelte.ts index 663d807..7aedb0e 100644 --- a/daemon/web/src/lib/utils.svelte.ts +++ b/daemon/web/src/lib/utils.svelte.ts @@ -160,7 +160,8 @@ export async function get_daemon_time(): Promise { 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 { diff --git a/daemon/web/src/routes/+page.svelte b/daemon/web/src/routes/+page.svelte index 0324db4..27b1e0f 100644 --- a/daemon/web/src/routes/+page.svelte +++ b/daemon/web/src/routes/+page.svelte @@ -299,6 +299,7 @@ GPS Status {#if gps_data} + {@const gps_date_formatter = new Intl.DateTimeFormat(undefined, { timeStyle: 'long', dateStyle: 'short' })} @@ -311,7 +312,11 @@ - +
GPS Timestamp{gps_data.timestamp} + {gps_data.timestamp > 0 + ? gps_date_formatter.format(new Date(gps_data.timestamp * 1000)) + : 'Fixed'} +