GPS feature webapp side: GPS mode selector, fixed mode lat/lon, API endpoint. Merging with Wifi client and webdav features

This commit is contained in:
Carlos Guerra
2026-03-28 17:55:39 +01:00
committed by Will Greenberg
parent ac33ebaf53
commit c107314194
8 changed files with 255 additions and 1 deletions

View File

@@ -36,6 +36,12 @@ pub struct Config {
pub min_space_to_start_recording_mb: u64,
/// Minimum disk space required to continue a recording
pub min_space_to_continue_recording_mb: u64,
/// GPS mode: 0=Disabled, 1=Fixed coordinates, 2=API endpoint
pub gps_mode: u8,
/// Fixed latitude used when gps_mode=1
pub gps_fixed_latitude: Option<f64>,
/// Fixed longitude used when gps_mode=1
pub gps_fixed_longitude: Option<f64>,
/// Wifi client SSID
pub wifi_ssid: Option<String>,
/// Wifi client password
@@ -100,6 +106,9 @@ impl Default for Config {
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
min_space_to_start_recording_mb: 1,
min_space_to_continue_recording_mb: 1,
gps_mode: 0,
gps_fixed_latitude: None,
gps_fixed_longitude: None,
wifi_ssid: None,
wifi_password: None,
wifi_security: None,
@@ -153,6 +162,49 @@ 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>,

39
daemon/src/gps.rs Normal file
View File

@@ -0,0 +1,39 @@
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::server::ServerState;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GpsData {
pub latitude: f64,
pub longitude: f64,
pub timestamp: String,
}
pub async fn post_gps(
State(state): State<Arc<ServerState>>,
Json(gps_data): Json<GpsData>,
) -> Result<StatusCode, (StatusCode, String)> {
if state.config.gps_mode != 2 {
return Err((
StatusCode::FORBIDDEN,
"GPS API endpoint is disabled. Set gps_mode to 2 in configuration.".to_string(),
));
}
let mut gps = state.gps_state.write().await;
*gps = Some(gps_data);
Ok(StatusCode::OK)
}
pub async fn get_gps(
State(state): State<Arc<ServerState>>,
) -> Result<Json<GpsData>, StatusCode> {
let gps = state.gps_state.read().await;
match gps.as_ref() {
Some(data) => Ok(Json(data.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}

View File

@@ -5,6 +5,7 @@ pub mod crypto_provider;
pub mod diag;
pub mod display;
pub mod error;
pub mod gps;
pub mod key_input;
pub mod notifications;
pub mod pcap;

View File

@@ -5,6 +5,7 @@ mod crypto_provider;
mod diag;
mod display;
mod error;
mod gps;
mod key_input;
mod notifications;
mod pcap;
@@ -23,6 +24,7 @@ use crate::error::RayhunterError;
use crate::notifications::{NotificationService, run_notification_worker};
use crate::pcap::get_pcap;
use crate::qmdl_store::RecordingStore;
use crate::gps::{get_gps, post_gps};
use crate::server::{
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip,
scan_wifi, serve_static, set_config, set_time_offset, test_notification,
@@ -78,6 +80,8 @@ fn get_router() -> AppRouter {
.route("/api/time", get(get_time))
.route("/api/time-offset", post(set_time_offset))
.route("/api/debug/display-state", post(debug_set_display_state))
.route("/api/gps", get(get_gps))
.route("/api/gps", post(post_gps))
.route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/{*path}", get(serve_static))
}
@@ -296,6 +300,18 @@ async fn run_with_config(
config.webdav.clone().into(),
);
}
let initial_gps = if config.gps_mode == 1 {
match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
(Some(lat), Some(lon)) => Some(gps::GpsData {
latitude: lat,
longitude: lon,
timestamp: "fixed".to_string(),
}),
_ => None,
}
} else {
None
};
let state = Arc::new(ServerState {
config_path: args.config_path.clone(),
@@ -308,6 +324,7 @@ async fn run_with_config(
ui_update_sender: Some(ui_update_tx),
wifi_status,
wifi_scan_lock: tokio::sync::Mutex::new(()),
gps_state: Arc::new(tokio::sync::RwLock::new(initial_gps)),
});
run_server(&task_tracker, state, shutdown_token.clone()).await;

View File

@@ -26,6 +26,7 @@ use crate::config::Config;
use crate::diag::DiagDeviceCtrlMessage;
use crate::display::DisplayState;
use crate::notifications::DEFAULT_NOTIFICATION_TIMEOUT;
use crate::gps::GpsData;
use crate::pcap::generate_pcap_data;
use crate::qmdl_store::RecordingStore;
@@ -40,6 +41,7 @@ pub struct ServerState {
pub ui_update_sender: Option<Sender<DisplayState>>,
pub wifi_status: Arc<RwLock<wifi_station::WifiStatus>>,
pub wifi_scan_lock: tokio::sync::Mutex<()>,
pub gps_state: Arc<RwLock<Option<GpsData>>>,
}
#[cfg_attr(feature = "apidocs", utoipa::path(
@@ -566,6 +568,7 @@ mod tests {
ui_update_sender: None,
wifi_status: Arc::new(RwLock::new(wifi_station::WifiStatus::default())),
wifi_scan_lock: tokio::sync::Mutex::new(()),
gps_state: Arc::new(RwLock::new(None)),
})
}

View File

@@ -781,6 +781,79 @@
</div>
</div>
<div class="border-t pt-4 mt-6 space-y-3">
<h3 class="text-lg font-semibold text-gray-800 mb-4">GPS Settings</h3>
<div>
<label for="gps_mode" class="block text-sm font-medium text-gray-700 mb-1">
GPS Mode
</label>
<select
id="gps_mode"
bind:value={config.gps_mode}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
>
<option value={0}>0 - Disabled</option>
<option value={1}>1 - Fixed coordinates</option>
<option value={2}>2 - API Endpoint</option>
</select>
<p class="text-xs text-gray-500 mt-1">
{#if config.gps_mode === 2}
POST latitude, longitude, and timestamp to <code>/api/gps</code> from
any device on the network.
{:else if config.gps_mode === 1}
GPS coordinates are fixed to the values below.
{:else}
GPS is disabled; no coordinates will be tracked.
{/if}
</p>
</div>
{#if config.gps_mode === 1}
<div>
<label
for="gps_fixed_latitude"
class="block text-sm font-medium text-gray-700 mb-1"
>
Fixed Latitude
</label>
<input
id="gps_fixed_latitude"
type="number"
min="-90"
max="90"
step="any"
required
bind:value={config.gps_fixed_latitude}
placeholder="e.g. 37.7749"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">Decimal degrees, -90 to 90</p>
</div>
<div>
<label
for="gps_fixed_longitude"
class="block text-sm font-medium text-gray-700 mb-1"
>
Fixed Longitude
</label>
<input
id="gps_fixed_longitude"
type="number"
min="-180"
max="180"
step="any"
required
bind:value={config.gps_fixed_longitude}
placeholder="e.g. -122.4194"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">Decimal degrees, -180 to 180</p>
</div>
{/if}
</div>
<div class="flex gap-2 pt-4">
<button
type="submit"

View File

@@ -46,6 +46,9 @@ export interface Config {
firewall_restrict_outbound: boolean;
firewall_allowed_ports: number[] | null;
webdav: WebdavConfig;
gps_mode: number;
gps_fixed_latitude: number | null;
gps_fixed_longitude: number | null;
}
export interface WifiStatus {
@@ -153,3 +156,20 @@ export interface TimeResponse {
export async function get_daemon_time(): Promise<TimeResponse> {
return JSON.parse(await req('GET', '/api/time'));
}
export interface GpsData {
latitude: number;
longitude: number;
timestamp: string;
}
export async function get_gps(): Promise<GpsData | null> {
const response = await fetch('/api/gps');
if (response.status === 404) {
return null;
}
if (response.status >= 200 && response.status < 300) {
return response.json();
}
throw new Error(await response.text());
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { ManifestEntry } from '$lib/manifest.svelte';
import { get_manifest, get_system_stats } from '$lib/utils.svelte';
import { get_manifest, get_system_stats, get_gps, get_config, type GpsData } from '$lib/utils.svelte';
import ManifestTable from '$lib/components/ManifestTable.svelte';
import Card from '$lib/components/ManifestCard.svelte';
import type { SystemStats } from '$lib/systemStats';
@@ -22,7 +22,13 @@
let update_error: string | undefined = $state(undefined);
let logview_shown: boolean = $state(false);
let config_shown: boolean = $state(false);
let gps_data: GpsData | null = $state(null);
let gps_mode: number = $state(0);
$effect(() => {
get_config().then((c) => {
gps_mode = c.gps_mode;
});
const interval = setInterval(async () => {
try {
// Don't update UI if browser tab isn't visible
@@ -40,6 +46,7 @@
current_entry = new_manifest.current_entry;
system_stats = await get_system_stats();
gps_data = await get_gps();
update_error = undefined;
loaded = true;
} catch (error) {
@@ -283,6 +290,48 @@
{/if}
<SystemStatsTable stats={system_stats!} />
</div>
{#if gps_mode !== 0}
<div class="bg-white border border-gray-200 drop-shadow rounded-md p-4 flex flex-col gap-2">
<span class="text-lg font-semibold flex flex-row items-center gap-2">
<svg
class="w-5 h-5 text-rayhunter-blue"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M11.906 1.994a8.002 8.002 0 0 1 8.09 8.421 7.996 7.996 0 0 1-1.297 3.957.996.996 0 0 1-.133.204l-.108.129c-.178.243-.37.477-.573.699l-5.112 6.224a1 1 0 0 1-1.545 0L5.982 15.26l-.002-.002a18.146 18.146 0 0 1-.309-.38l-.133-.163a.999.999 0 0 1-.13-.202 7.995 7.995 0 0 1 6.498-12.518ZM15 9.997a3 3 0 1 1-5.999 0 3 3 0 0 1 5.999 0Z"
clip-rule="evenodd"
/>
</svg>
GPS Status
</span>
{#if gps_data}
<table class="w-full text-sm">
<tbody>
<tr class="border-b border-gray-100">
<td class="py-1 pr-4 text-gray-500 font-medium">Latitude</td>
<td class="py-1 font-mono">{gps_data.latitude.toFixed(6)}</td>
</tr>
<tr class="border-b border-gray-100">
<td class="py-1 pr-4 text-gray-500 font-medium">Longitude</td>
<td class="py-1 font-mono">{gps_data.longitude.toFixed(6)}</td>
</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>
</tr>
</tbody>
</table>
{:else}
<span class="text-gray-400 text-sm">Awaiting GPS data...</span>
{/if}
</div>
{/if}
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-2">
<div class="text-xl flex-1">History</div>