From fee082cde4196a0b187238d935ffccefd1e1c7a3 Mon Sep 17 00:00:00 2001 From: Carlos Guerra Date: Wed, 8 Apr 2026 19:29:45 +0200 Subject: [PATCH] replacing numbered options in config with rust enum implementation, unique commit to make easier to debug or rollback --- Cargo.lock | 1 + daemon/Cargo.toml | 1 + daemon/src/config.rs | 44 ++++++++++++++++--- daemon/src/diag.rs | 9 ++-- daemon/src/display/generic_framebuffer.rs | 19 ++++---- daemon/src/display/tmobile.rs | 4 +- daemon/src/display/tplink.rs | 4 +- daemon/src/display/tplink_onebit.rs | 6 +-- daemon/src/display/uz801.rs | 4 +- daemon/src/gps.rs | 3 +- daemon/src/key_input.rs | 4 +- daemon/src/main.rs | 4 +- daemon/src/pcap.rs | 3 +- daemon/src/qmdl_store.rs | 10 +++-- .../web/src/lib/components/ConfigForm.svelte | 21 +++++---- 15 files changed, 87 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a07501..c1b3e84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "rustls-rustcrypto", "serde", "serde_json", + "serde_repr", "tempfile", "thiserror 1.0.69", "tokio", diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 2d4c7c7..37eac20 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -24,6 +24,7 @@ rayhunter = { path = "../lib" } wifi-station = "0.10.1" toml = "0.8.8" serde = { version = "1.0.193", features = ["derive"] } +serde_repr = "0.1" tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] } axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] } thiserror = "1.0.52" diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 3c988ba..7085d50 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -1,10 +1,40 @@ use log::warn; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use rayhunter::Device; use rayhunter::analysis::analyzer::AnalyzerConfig; use crate::error::RayhunterError; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] +#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))] +pub enum GpsMode { + Disabled = 0, + Fixed = 1, + Api = 2, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] +#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))] +pub enum UiLevel { + Invisible = 0, + Subtle = 1, + Demo = 2, + EffLogo = 3, + HighVisibility = 4, + TransFlag = 128, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] +#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))] +pub enum KeyInputMode { + Disabled = 0, + DoubleTapPower = 1, +} use crate::notifications::NotificationType; /// The structure of a valid rayhunter configuration @@ -21,11 +51,11 @@ pub struct Config { /// Internal device name pub device: Device, /// UI level - pub ui_level: u8, + pub ui_level: UiLevel, /// Colorblind mode pub colorblind_mode: bool, /// Key input mode - pub key_input_mode: u8, + pub key_input_mode: KeyInputMode, /// ntfy.sh URL pub ntfy_url: Option, /// Vector containing the types of enabled notifications @@ -36,8 +66,8 @@ 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, + /// GPS mode + pub gps_mode: GpsMode, /// Fixed latitude used when gps_mode=1 pub gps_fixed_latitude: Option, /// Fixed longitude used when gps_mode=1 @@ -98,15 +128,15 @@ impl Default for Config { port: 8080, debug_mode: false, device: Device::Orbic, - ui_level: 1, + ui_level: UiLevel::Subtle, colorblind_mode: false, - key_input_mode: 0, + key_input_mode: KeyInputMode::Disabled, analyzers: AnalyzerConfig::default(), ntfy_url: None, 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_mode: GpsMode::Disabled, gps_fixed_latitude: None, gps_fixed_longitude: None, wifi_ssid: None, diff --git a/daemon/src/diag.rs b/daemon/src/diag.rs index 10d5e3a..6b7f3d4 100644 --- a/daemon/src/diag.rs +++ b/daemon/src/diag.rs @@ -28,6 +28,7 @@ use rayhunter::diag_device::DiagDevice; use rayhunter::qmdl::QmdlWriter; use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter}; +use crate::config::GpsMode; use crate::display; use crate::notifications::{Notification, NotificationType}; use crate::qmdl_store::{RecordingStore, RecordingStoreError}; @@ -58,7 +59,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, + gps_mode: GpsMode, gps_fixed_coords: Option<(f64, f64)>, state: DiagState, max_type_seen: EventType, @@ -108,7 +109,7 @@ impl DiagTask { notification_channel: tokio::sync::mpsc::Sender, min_space_to_start_mb: u64, min_space_to_continue_mb: u64, - gps_mode: u8, + gps_mode: GpsMode, gps_fixed_coords: Option<(f64, f64)>, ) -> Self { Self { @@ -163,7 +164,7 @@ impl DiagTask { // For fixed-mode sessions, write the configured coordinates to the sidecar // immediately so the per-session GPS is stored durably and isn't affected // by future config changes or GPS API calls. - if self.gps_mode == 1 { + if self.gps_mode == GpsMode::Fixed { if let Some((lat, lon)) = self.gps_fixed_coords { if let Some((entry_idx, _)) = qmdl_store.get_current_entry() { if let Ok(mut gps_file) = qmdl_store.open_entry_gps_for_append(entry_idx).await @@ -409,7 +410,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, + gps_mode: GpsMode, gps_fixed_coords: Option<(f64, f64)>, ) { task_tracker.spawn(async move { diff --git a/daemon/src/display/generic_framebuffer.rs b/daemon/src/display/generic_framebuffer.rs index a37b236..9fcfb4f 100644 --- a/daemon/src/display/generic_framebuffer.rs +++ b/daemon/src/display/generic_framebuffer.rs @@ -3,7 +3,7 @@ use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::F use std::io::Cursor; use std::time::Duration; -use crate::config; +use crate::config::{self, UiLevel}; use crate::display::DisplayState; use rayhunter::analysis::analyzer::EventType; @@ -176,7 +176,7 @@ pub fn update_ui( ) { static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/"); let display_level = config.ui_level; - if display_level == 0 { + if display_level == UiLevel::Invisible { info!("Invisible mode, not spawning UI."); return; } @@ -187,14 +187,14 @@ pub fn update_ui( task_tracker.spawn(async move { // this feels wrong, is there a more rusty way to do this? let mut img: Option<&[u8]> = None; - if display_level == 2 { + if display_level == UiLevel::Demo { img = Some( IMAGE_DIR .get_file("orca.gif") .expect("failed to read orca.gif") .contents(), ); - } else if display_level == 3 { + } else if display_level == UiLevel::EffLogo { img = Some( IMAGE_DIR .get_file("eff.png") @@ -217,20 +217,19 @@ pub fn update_ui( let mut status_bar_height = 2; match display_level { - 2 => fb.draw_gif(img.unwrap()).await, - 3 => fb.draw_img(img.unwrap()).await, - 4 => { + UiLevel::Demo => fb.draw_gif(img.unwrap()).await, + UiLevel::EffLogo => fb.draw_img(img.unwrap()).await, + UiLevel::HighVisibility => { status_bar_height = fb.dimensions().height; } - 128 => { + UiLevel::TransFlag => { fb.draw_line(Color::Cyan, 128).await; fb.draw_line(Color::Pink, 102).await; fb.draw_line(Color::White, 76).await; fb.draw_line(Color::Pink, 50).await; fb.draw_line(Color::Cyan, 25).await; } - // this branch is for ui_level 1, which is also the default if an - // unknown value is used + // UiLevel::Subtle (1) and anything else: just the status bar line _ => {} }; let (color, pattern) = display_style; diff --git a/daemon/src/display/tmobile.rs b/daemon/src/display/tmobile.rs index 1a39fc1..e50e6a8 100644 --- a/daemon/src/display/tmobile.rs +++ b/daemon/src/display/tmobile.rs @@ -9,7 +9,7 @@ use tokio_util::task::TaskTracker; use std::time::Duration; -use crate::config; +use crate::config::{self, UiLevel}; use crate::display::DisplayState; macro_rules! led { @@ -31,7 +31,7 @@ pub fn update_ui( mut ui_update_rx: mpsc::Receiver, ) { let mut invisible: bool = false; - if config.ui_level == 0 { + if config.ui_level == UiLevel::Invisible { info!("Invisible mode, not spawning UI."); invisible = true; } diff --git a/daemon/src/display/tplink.rs b/daemon/src/display/tplink.rs index fe51277..a3d515e 100644 --- a/daemon/src/display/tplink.rs +++ b/daemon/src/display/tplink.rs @@ -3,7 +3,7 @@ use tokio::sync::mpsc::Receiver; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; -use crate::config; +use crate::config::{self, UiLevel}; use crate::display::{DisplayState, tplink_framebuffer, tplink_onebit}; use std::fs; @@ -15,7 +15,7 @@ pub fn update_ui( ui_update_rx: Receiver, ) { let display_level = config.ui_level; - if display_level == 0 { + if display_level == UiLevel::Invisible { info!("Invisible mode, not spawning UI."); } diff --git a/daemon/src/display/tplink_onebit.rs b/daemon/src/display/tplink_onebit.rs index fbf3ded..06d6098 100644 --- a/daemon/src/display/tplink_onebit.rs +++ b/daemon/src/display/tplink_onebit.rs @@ -1,7 +1,7 @@ /// Display module for the TP-Link M7350 oled one-bit display. /// /// https://github.com/m0veax/tplink_m7350/tree/main/oled -use crate::config; +use crate::config::{self, UiLevel}; use crate::display::DisplayState; use log::{error, info}; @@ -115,7 +115,7 @@ pub fn update_ui( mut ui_update_rx: Receiver, ) { let display_level = config.ui_level; - if display_level == 0 { + if display_level == UiLevel::Invisible { info!("Invisible mode, not spawning UI."); } @@ -140,7 +140,7 @@ pub fn update_ui( // we write the status every second because it may have been overwritten through menu // navigation. - if display_level != 0 + if display_level != UiLevel::Invisible && let Err(e) = tokio::fs::write(OLED_PATH, pixels).await { error!("failed to write to display: {e}"); diff --git a/daemon/src/display/uz801.rs b/daemon/src/display/uz801.rs index 1f7bd59..df2cc7e 100644 --- a/daemon/src/display/uz801.rs +++ b/daemon/src/display/uz801.rs @@ -9,7 +9,7 @@ use tokio_util::task::TaskTracker; use std::time::Duration; -use crate::config; +use crate::config::{self, UiLevel}; use crate::display::DisplayState; macro_rules! led { @@ -31,7 +31,7 @@ pub fn update_ui( mut ui_update_rx: mpsc::Receiver, ) { let mut invisible: bool = false; - if config.ui_level == 0 { + if config.ui_level == UiLevel::Invisible { info!("Invisible mode, not spawning UI."); invisible = true; } diff --git a/daemon/src/gps.rs b/daemon/src/gps.rs index 44893af..7dc6419 100644 --- a/daemon/src/gps.rs +++ b/daemon/src/gps.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use crate::config::GpsMode; use crate::server::ServerState; fn deserialize_unix_ts<'de, D>(deserializer: D) -> Result @@ -79,7 +80,7 @@ pub async fn post_gps( State(state): State>, Json(gps_data): Json, ) -> Result { - if state.config.gps_mode != 2 { + if state.config.gps_mode != GpsMode::Api { return Err(( StatusCode::FORBIDDEN, "GPS API endpoint is disabled. Set gps_mode to 2 in configuration.".to_string(), diff --git a/daemon/src/key_input.rs b/daemon/src/key_input.rs index 7e75fbb..0cea6ab 100644 --- a/daemon/src/key_input.rs +++ b/daemon/src/key_input.rs @@ -6,7 +6,7 @@ use tokio::sync::mpsc::Sender; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; -use crate::config; +use crate::config::{self, KeyInputMode}; use crate::diag::DiagDeviceCtrlMessage; #[derive(Debug)] @@ -23,7 +23,7 @@ pub fn run_key_input_thread( diag_tx: Sender, cancellation_token: CancellationToken, ) { - if config.key_input_mode == 0 { + if config.key_input_mode == KeyInputMode::Disabled { return; } diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 549908b..2d232f8 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -18,7 +18,7 @@ use std::net::SocketAddr; use std::sync::Arc; use crate::battery::run_battery_notification_worker; -use crate::config::{parse_args, parse_config}; +use crate::config::{GpsMode, parse_args, parse_config}; use crate::diag::run_diag_read_thread; use crate::error::RayhunterError; use crate::gps::{get_gps, post_gps}; @@ -306,7 +306,7 @@ async fn run_with_config( config.webdav.clone().into(), ); } - let initial_gps = if config.gps_mode == 1 { + let initial_gps = if config.gps_mode == GpsMode::Fixed { match (config.gps_fixed_latitude, config.gps_fixed_longitude) { (Some(lat), Some(lon)) => Some(gps::GpsData { latitude: lat, diff --git a/daemon/src/pcap.rs b/daemon/src/pcap.rs index 603028f..9899178 100644 --- a/daemon/src/pcap.rs +++ b/daemon/src/pcap.rs @@ -1,3 +1,4 @@ +use crate::config::GpsMode; use crate::gps::{GpsRecord, load_gps_records}; use crate::server::ServerState; @@ -95,7 +96,7 @@ pub(crate) async fn load_gps_records_for_entry( // not the current config, so old fixed-mode sessions still get coordinates even // if the mode has since been changed. Use the configured fixed coords directly // rather than gps_state, which can be overwritten by API calls or be None. - if entry_gps_mode == Some(1) { + if entry_gps_mode == Some(GpsMode::Fixed) { if let (Some(lat), Some(lon)) = ( state.config.gps_fixed_latitude, state.config.gps_fixed_longitude, diff --git a/daemon/src/qmdl_store.rs b/daemon/src/qmdl_store.rs index 7a0ef7c..5999646 100644 --- a/daemon/src/qmdl_store.rs +++ b/daemon/src/qmdl_store.rs @@ -2,6 +2,7 @@ use std::io::{self, ErrorKind}; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; +use crate::config::GpsMode; use chrono::{DateTime, Local, TimeDelta}; use log::{info, warn}; use rayhunter::util::RuntimeMetadata; @@ -71,11 +72,11 @@ pub struct ManifestEntry { #[cfg_attr(feature = "apidocs", schema(value_type = String))] pub upload_time: Option>, #[serde(default)] - pub gps_mode: Option, + pub gps_mode: Option, } impl ManifestEntry { - fn new(gps_mode: u8) -> Self { + fn new(gps_mode: GpsMode) -> Self { let now = rayhunter::clock::get_adjusted_now(); let metadata = RuntimeMetadata::new(); ManifestEntry { @@ -257,7 +258,10 @@ 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, gps_mode: u8) -> Result<(File, File), RecordingStoreError> { + pub async fn new_entry( + &mut self, + gps_mode: GpsMode, + ) -> 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?; diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index 284df90..5c9d7c6 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -169,11 +169,11 @@ bind:value={config.ui_level} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue" > - - - - - + + + + +

Note: Rayhunter draws over the device's native UI, so some flickering is @@ -193,9 +193,8 @@ bind:value={config.key_input_mode} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue" > - - + + @@ -786,9 +785,9 @@

{#if config.gps_mode === 2}