replacing numbered options in config with rust enum implementation, unique commit to make easier to debug or rollback

This commit is contained in:
Carlos Guerra
2026-04-08 19:29:45 +02:00
committed by Will Greenberg
parent 0b91a6e5d3
commit fee082cde4
15 changed files with 87 additions and 50 deletions

1
Cargo.lock generated
View File

@@ -4881,6 +4881,7 @@ dependencies = [
"rustls-rustcrypto",
"serde",
"serde_json",
"serde_repr",
"tempfile",
"thiserror 1.0.69",
"tokio",

View File

@@ -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"

View File

@@ -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<String>,
/// 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<f64>,
/// 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,

View File

@@ -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<Notification>,
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<Notification>,
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<Notification>,
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 {

View File

@@ -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;

View File

@@ -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<DisplayState>,
) {
let mut invisible: bool = false;
if config.ui_level == 0 {
if config.ui_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI.");
invisible = true;
}

View File

@@ -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<DisplayState>,
) {
let display_level = config.ui_level;
if display_level == 0 {
if display_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI.");
}

View File

@@ -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<DisplayState>,
) {
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}");

View File

@@ -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<DisplayState>,
) {
let mut invisible: bool = false;
if config.ui_level == 0 {
if config.ui_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI.");
invisible = true;
}

View File

@@ -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<i64, D::Error>
@@ -79,7 +80,7 @@ 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 {
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(),

View File

@@ -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<DiagDeviceCtrlMessage>,
cancellation_token: CancellationToken,
) {
if config.key_input_mode == 0 {
if config.key_input_mode == KeyInputMode::Disabled {
return;
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<DateTime<Local>>,
#[serde(default)]
pub gps_mode: Option<u8>,
pub gps_mode: Option<GpsMode>,
}
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?;

View File

@@ -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"
>
<option value={0}>0 - Invisible mode</option>
<option value={1}>1 - Subtle mode (colored line)</option>
<option value={2}>2 - Demo mode (orca gif)</option>
<option value={3}>3 - EFF logo</option>
<option value={4}>4 - High visibility (full screen color)</option>
<option value={0}>Invisible mode</option>
<option value={1}>Subtle mode (colored line)</option>
<option value={2}>Demo mode (orca gif)</option>
<option value={3}>EFF logo</option>
<option value={4}>High visibility (full screen color)</option>
</select>
<p class="text-xs text-gray-500 mt-1">
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"
>
<option value={0}>0 - Disable button control</option>
<option value={1}>1 - Double-tap power button to start new recording</option
>
<option value={0}>Disable button control</option>
<option value={1}>Double-tap power button to start new recording</option>
</select>
</div>
@@ -786,9 +785,9 @@
<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>
<option value={0}>Disabled</option>
<option value={1}>Fixed coordinates</option>
<option value={2}>API endpoint</option>
</select>
<p class="text-xs text-gray-500 mt-1">
{#if config.gps_mode === 2}