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
-43
View File
@@ -162,49 +162,6 @@ fn resolve_bin(name: &str) -> Option<String> {
None 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> pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
+7 -2
View File
@@ -56,6 +56,7 @@ pub struct DiagTask {
notification_channel: tokio::sync::mpsc::Sender<Notification>, notification_channel: tokio::sync::mpsc::Sender<Notification>,
min_space_to_start_mb: u64, min_space_to_start_mb: u64,
min_space_to_continue_mb: u64, min_space_to_continue_mb: u64,
gps_mode: u8,
state: DiagState, state: DiagState,
max_type_seen: EventType, max_type_seen: EventType,
bytes_since_space_check: usize, bytes_since_space_check: usize,
@@ -104,6 +105,7 @@ impl DiagTask {
notification_channel: tokio::sync::mpsc::Sender<Notification>, notification_channel: tokio::sync::mpsc::Sender<Notification>,
min_space_to_start_mb: u64, min_space_to_start_mb: u64,
min_space_to_continue_mb: u64, min_space_to_continue_mb: u64,
gps_mode: u8,
) -> Self { ) -> Self {
Self { Self {
ui_update_sender, ui_update_sender,
@@ -112,6 +114,7 @@ impl DiagTask {
notification_channel, notification_channel,
min_space_to_start_mb, min_space_to_start_mb,
min_space_to_continue_mb, min_space_to_continue_mb,
gps_mode,
state: DiagState::Stopped, state: DiagState::Stopped,
max_type_seen: EventType::Informational, max_type_seen: EventType::Informational,
bytes_since_space_check: 0, bytes_since_space_check: 0,
@@ -144,7 +147,7 @@ impl DiagTask {
DiskSpaceCheck::Failed => {} 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, Ok(files) => files,
Err(e) => { Err(e) => {
let msg = format!("failed creating QMDL file entry: {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>, notification_channel: tokio::sync::mpsc::Sender<Notification>,
min_space_to_start_mb: u64, min_space_to_start_mb: u64,
min_space_to_continue_mb: u64, min_space_to_continue_mb: u64,
gps_mode: u8,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
info!("Using configuration for device: {0:?}", device); info!("Using configuration for device: {0:?}", device);
@@ -396,7 +400,8 @@ pub fn run_diag_read_thread(
analyzer_config, analyzer_config,
notification_channel, notification_channel,
min_space_to_start_mb, min_space_to_start_mb,
min_space_to_continue_mb min_space_to_continue_mb,
gps_mode,
); );
qmdl_file_tx qmdl_file_tx
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None }) .send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
+22 -2
View File
@@ -1,17 +1,37 @@
use axum::Json; use axum::Json;
use axum::extract::State; use axum::extract::State;
use axum::http::StatusCode; use axum::http::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::sync::Arc; use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use crate::server::ServerState; 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GpsData { pub struct GpsData {
pub latitude: f64, pub latitude: f64,
pub longitude: f64, pub longitude: f64,
pub timestamp: String, #[serde(deserialize_with = "deserialize_unix_ts")]
pub timestamp: i64,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
+2 -1
View File
@@ -232,6 +232,7 @@ async fn run_with_config(
notification_service.new_handler(), notification_service.new_handler(),
config.min_space_to_start_recording_mb, config.min_space_to_start_recording_mb,
config.min_space_to_continue_recording_mb, config.min_space_to_continue_recording_mb,
config.gps_mode,
); );
info!("Starting UI"); info!("Starting UI");
@@ -305,7 +306,7 @@ async fn run_with_config(
(Some(lat), Some(lon)) => Some(gps::GpsData { (Some(lat), Some(lon)) => Some(gps::GpsData {
latitude: lat, latitude: lat,
longitude: lon, longitude: lon,
timestamp: "fixed".to_string(), timestamp: 0,
}), }),
_ => None, _ => None,
} }
+12 -8
View File
@@ -70,10 +70,12 @@ pub struct ManifestEntry {
/// When the manifest was uploaded to a WebDAV server /// When the manifest was uploaded to a WebDAV server
#[cfg_attr(feature = "apidocs", schema(value_type = String))] #[cfg_attr(feature = "apidocs", schema(value_type = String))]
pub upload_time: Option<DateTime<Local>>, pub upload_time: Option<DateTime<Local>>,
#[serde(default)]
pub gps_mode: Option<u8>,
} }
impl ManifestEntry { impl ManifestEntry {
fn new() -> Self { fn new(gps_mode: u8) -> Self {
let now = rayhunter::clock::get_adjusted_now(); let now = rayhunter::clock::get_adjusted_now();
let metadata = RuntimeMetadata::new(); let metadata = RuntimeMetadata::new();
ManifestEntry { ManifestEntry {
@@ -86,6 +88,7 @@ impl ManifestEntry {
arch: Some(metadata.arch), arch: Some(metadata.arch),
stop_reason: None, stop_reason: None,
upload_time: None, upload_time: None,
gps_mode: Some(gps_mode),
} }
} }
@@ -223,6 +226,7 @@ impl RecordingStore {
arch: None, arch: None,
stop_reason: None, stop_reason: None,
upload_time: 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 // 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 // current time, and updates the manifest. Returns a tuple of the entry's
// newly created QMDL file and analysis file. // 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 we've already got an entry open, close it
if self.current_entry.is_some() { if self.current_entry.is_some() {
self.close_current_entry().await?; 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_filepath = new_entry.get_qmdl_filepath(&self.path);
let qmdl_file = File::create(&qmdl_filepath) let qmdl_file = File::create(&qmdl_filepath)
.await .await
@@ -545,7 +549,7 @@ mod tests {
async fn test_creating_updating_and_closing_entries() { async fn test_creating_updating_and_closing_entries() {
let dir = make_temp_dir(); let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap(); 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 entry_index = store.current_entry.unwrap();
assert_eq!( assert_eq!(
RecordingStore::read_manifest(dir.path()).await.unwrap(), RecordingStore::read_manifest(dir.path()).await.unwrap(),
@@ -582,7 +586,7 @@ mod tests {
async fn test_create_on_existing_store() { async fn test_create_on_existing_store() {
let dir = make_temp_dir(); let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap(); 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 entry_index = store.current_entry.unwrap();
store store
.update_entry_qmdl_size(entry_index, 1000) .update_entry_qmdl_size(entry_index, 1000)
@@ -596,9 +600,9 @@ mod tests {
async fn test_repeated_new_entries() { async fn test_repeated_new_entries() {
let dir = make_temp_dir(); let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap(); 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 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(); let new_entry_index = store.current_entry.unwrap();
assert_ne!(entry_index, new_entry_index); assert_ne!(entry_index, new_entry_index);
assert_eq!(store.manifest.entries.len(), 2); assert_eq!(store.manifest.entries.len(), 2);
@@ -608,7 +612,7 @@ mod tests {
async fn test_delete_all_entries() { async fn test_delete_all_entries() {
let dir = make_temp_dir(); let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap(); 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()); assert!(store.current_entry.is_some());
store.delete_all_entries().await.unwrap(); store.delete_all_entries().await.unwrap();
+1 -1
View File
@@ -523,7 +523,7 @@ mod tests {
) -> String { ) -> String {
let entry_name = { let entry_name = {
let mut store = store_lock.write().await; 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() { if !test_data.is_empty() {
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
@@ -70,6 +70,12 @@
> >
</p> </p>
{/if} {/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> </div>
{#if metadata && metadata.analyzers} {#if metadata && metadata.analyzers}
<div> <div>
@@ -86,6 +86,11 @@
{entry.stop_reason} {entry.stop_reason}
</div> </div>
{/if} {/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"> <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_pcap_url()} text="pcap" full_button />
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button /> <DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
+5
View File
@@ -13,6 +13,7 @@ interface JsonManifestEntry {
qmdl_size_bytes: number; qmdl_size_bytes: number;
stop_reason: string | null; stop_reason: string | null;
upload_time: string | null; upload_time: string | null;
gps_mode: number | null;
} }
export class Manifest { export class Manifest {
@@ -61,6 +62,7 @@ export class ManifestEntry {
public analysis_report: AnalysisReport | string | undefined = $state(undefined); public analysis_report: AnalysisReport | string | undefined = $state(undefined);
public stop_reason: string | undefined = $state(undefined); public stop_reason: string | undefined = $state(undefined);
public upload_time: Date | undefined = $state(undefined); public upload_time: Date | undefined = $state(undefined);
public gps_mode: number | undefined = $state(undefined);
constructor(json: JsonManifestEntry) { constructor(json: JsonManifestEntry) {
this.name = json.name; this.name = json.name;
@@ -75,6 +77,9 @@ export class ManifestEntry {
if (json.upload_time) { if (json.upload_time) {
this.upload_time = new Date(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 { get_readable_qmdl_size(): string {
+2 -1
View File
@@ -160,7 +160,8 @@ export async function get_daemon_time(): Promise<TimeResponse> {
export interface GpsData { export interface GpsData {
latitude: number; latitude: number;
longitude: number; longitude: number;
timestamp: string; /** Unix timestamp in seconds (0 = fixed/no real time). */
timestamp: number;
} }
export async function get_gps(): Promise<GpsData | null> { export async function get_gps(): Promise<GpsData | null> {
+6 -1
View File
@@ -299,6 +299,7 @@
GPS Status GPS Status
</span> </span>
{#if gps_data} {#if gps_data}
{@const gps_date_formatter = new Intl.DateTimeFormat(undefined, { timeStyle: 'long', dateStyle: 'short' })}
<table class="w-full text-sm"> <table class="w-full text-sm">
<tbody> <tbody>
<tr class="border-b border-gray-100"> <tr class="border-b border-gray-100">
@@ -311,7 +312,11 @@
</tr> </tr>
<tr> <tr>
<td class="py-1 pr-4 text-gray-500 font-medium">GPS Timestamp</td> <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> </tr>
</tbody> </tbody>
</table> </table>