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
Generated
+1
View File
@@ -4881,6 +4881,7 @@ dependencies = [
"rustls-rustcrypto", "rustls-rustcrypto",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr",
"tempfile", "tempfile",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
+1
View File
@@ -24,6 +24,7 @@ rayhunter = { path = "../lib" }
wifi-station = "0.10.1" wifi-station = "0.10.1"
toml = "0.8.8" toml = "0.8.8"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
serde_repr = "0.1"
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] } tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] }
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] } axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
thiserror = "1.0.52" thiserror = "1.0.52"
+37 -7
View File
@@ -1,10 +1,40 @@
use log::warn; use log::warn;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use rayhunter::Device; use rayhunter::Device;
use rayhunter::analysis::analyzer::AnalyzerConfig; use rayhunter::analysis::analyzer::AnalyzerConfig;
use crate::error::RayhunterError; 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; use crate::notifications::NotificationType;
/// The structure of a valid rayhunter configuration /// The structure of a valid rayhunter configuration
@@ -21,11 +51,11 @@ pub struct Config {
/// Internal device name /// Internal device name
pub device: Device, pub device: Device,
/// UI level /// UI level
pub ui_level: u8, pub ui_level: UiLevel,
/// Colorblind mode /// Colorblind mode
pub colorblind_mode: bool, pub colorblind_mode: bool,
/// Key input mode /// Key input mode
pub key_input_mode: u8, pub key_input_mode: KeyInputMode,
/// ntfy.sh URL /// ntfy.sh URL
pub ntfy_url: Option<String>, pub ntfy_url: Option<String>,
/// Vector containing the types of enabled notifications /// Vector containing the types of enabled notifications
@@ -36,8 +66,8 @@ pub struct Config {
pub min_space_to_start_recording_mb: u64, pub min_space_to_start_recording_mb: u64,
/// Minimum disk space required to continue a recording /// Minimum disk space required to continue a recording
pub min_space_to_continue_recording_mb: u64, pub min_space_to_continue_recording_mb: u64,
/// GPS mode: 0=Disabled, 1=Fixed coordinates, 2=API endpoint /// GPS mode
pub gps_mode: u8, pub gps_mode: GpsMode,
/// Fixed latitude used when gps_mode=1 /// Fixed latitude used when gps_mode=1
pub gps_fixed_latitude: Option<f64>, pub gps_fixed_latitude: Option<f64>,
/// Fixed longitude used when gps_mode=1 /// Fixed longitude used when gps_mode=1
@@ -98,15 +128,15 @@ impl Default for Config {
port: 8080, port: 8080,
debug_mode: false, debug_mode: false,
device: Device::Orbic, device: Device::Orbic,
ui_level: 1, ui_level: UiLevel::Subtle,
colorblind_mode: false, colorblind_mode: false,
key_input_mode: 0, key_input_mode: KeyInputMode::Disabled,
analyzers: AnalyzerConfig::default(), analyzers: AnalyzerConfig::default(),
ntfy_url: None, ntfy_url: None,
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery], enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
min_space_to_start_recording_mb: 1, min_space_to_start_recording_mb: 1,
min_space_to_continue_recording_mb: 1, min_space_to_continue_recording_mb: 1,
gps_mode: 0, gps_mode: GpsMode::Disabled,
gps_fixed_latitude: None, gps_fixed_latitude: None,
gps_fixed_longitude: None, gps_fixed_longitude: None,
wifi_ssid: None, wifi_ssid: None,
+5 -4
View File
@@ -28,6 +28,7 @@ use rayhunter::diag_device::DiagDevice;
use rayhunter::qmdl::QmdlWriter; use rayhunter::qmdl::QmdlWriter;
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter}; use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
use crate::config::GpsMode;
use crate::display; use crate::display;
use crate::notifications::{Notification, NotificationType}; use crate::notifications::{Notification, NotificationType};
use crate::qmdl_store::{RecordingStore, RecordingStoreError}; use crate::qmdl_store::{RecordingStore, RecordingStoreError};
@@ -58,7 +59,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, gps_mode: GpsMode,
gps_fixed_coords: Option<(f64, f64)>, gps_fixed_coords: Option<(f64, f64)>,
state: DiagState, state: DiagState,
max_type_seen: EventType, max_type_seen: EventType,
@@ -108,7 +109,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, gps_mode: GpsMode,
gps_fixed_coords: Option<(f64, f64)>, gps_fixed_coords: Option<(f64, f64)>,
) -> Self { ) -> Self {
Self { Self {
@@ -163,7 +164,7 @@ impl DiagTask {
// For fixed-mode sessions, write the configured coordinates to the sidecar // For fixed-mode sessions, write the configured coordinates to the sidecar
// immediately so the per-session GPS is stored durably and isn't affected // immediately so the per-session GPS is stored durably and isn't affected
// by future config changes or GPS API calls. // 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((lat, lon)) = self.gps_fixed_coords {
if let Some((entry_idx, _)) = qmdl_store.get_current_entry() { 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 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>, 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, gps_mode: GpsMode,
gps_fixed_coords: Option<(f64, f64)>, gps_fixed_coords: Option<(f64, f64)>,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
+9 -10
View File
@@ -3,7 +3,7 @@ use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::F
use std::io::Cursor; use std::io::Cursor;
use std::time::Duration; use std::time::Duration;
use crate::config; use crate::config::{self, UiLevel};
use crate::display::DisplayState; use crate::display::DisplayState;
use rayhunter::analysis::analyzer::EventType; use rayhunter::analysis::analyzer::EventType;
@@ -176,7 +176,7 @@ pub fn update_ui(
) { ) {
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/"); static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/");
let display_level = config.ui_level; let display_level = config.ui_level;
if display_level == 0 { if display_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
return; return;
} }
@@ -187,14 +187,14 @@ pub fn update_ui(
task_tracker.spawn(async move { task_tracker.spawn(async move {
// this feels wrong, is there a more rusty way to do this? // this feels wrong, is there a more rusty way to do this?
let mut img: Option<&[u8]> = None; let mut img: Option<&[u8]> = None;
if display_level == 2 { if display_level == UiLevel::Demo {
img = Some( img = Some(
IMAGE_DIR IMAGE_DIR
.get_file("orca.gif") .get_file("orca.gif")
.expect("failed to read orca.gif") .expect("failed to read orca.gif")
.contents(), .contents(),
); );
} else if display_level == 3 { } else if display_level == UiLevel::EffLogo {
img = Some( img = Some(
IMAGE_DIR IMAGE_DIR
.get_file("eff.png") .get_file("eff.png")
@@ -217,20 +217,19 @@ pub fn update_ui(
let mut status_bar_height = 2; let mut status_bar_height = 2;
match display_level { match display_level {
2 => fb.draw_gif(img.unwrap()).await, UiLevel::Demo => fb.draw_gif(img.unwrap()).await,
3 => fb.draw_img(img.unwrap()).await, UiLevel::EffLogo => fb.draw_img(img.unwrap()).await,
4 => { UiLevel::HighVisibility => {
status_bar_height = fb.dimensions().height; status_bar_height = fb.dimensions().height;
} }
128 => { UiLevel::TransFlag => {
fb.draw_line(Color::Cyan, 128).await; fb.draw_line(Color::Cyan, 128).await;
fb.draw_line(Color::Pink, 102).await; fb.draw_line(Color::Pink, 102).await;
fb.draw_line(Color::White, 76).await; fb.draw_line(Color::White, 76).await;
fb.draw_line(Color::Pink, 50).await; fb.draw_line(Color::Pink, 50).await;
fb.draw_line(Color::Cyan, 25).await; fb.draw_line(Color::Cyan, 25).await;
} }
// this branch is for ui_level 1, which is also the default if an // UiLevel::Subtle (1) and anything else: just the status bar line
// unknown value is used
_ => {} _ => {}
}; };
let (color, pattern) = display_style; let (color, pattern) = display_style;
+2 -2
View File
@@ -9,7 +9,7 @@ use tokio_util::task::TaskTracker;
use std::time::Duration; use std::time::Duration;
use crate::config; use crate::config::{self, UiLevel};
use crate::display::DisplayState; use crate::display::DisplayState;
macro_rules! led { macro_rules! led {
@@ -31,7 +31,7 @@ pub fn update_ui(
mut ui_update_rx: mpsc::Receiver<DisplayState>, mut ui_update_rx: mpsc::Receiver<DisplayState>,
) { ) {
let mut invisible: bool = false; let mut invisible: bool = false;
if config.ui_level == 0 { if config.ui_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
invisible = true; invisible = true;
} }
+2 -2
View File
@@ -3,7 +3,7 @@ use tokio::sync::mpsc::Receiver;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use crate::config; use crate::config::{self, UiLevel};
use crate::display::{DisplayState, tplink_framebuffer, tplink_onebit}; use crate::display::{DisplayState, tplink_framebuffer, tplink_onebit};
use std::fs; use std::fs;
@@ -15,7 +15,7 @@ pub fn update_ui(
ui_update_rx: Receiver<DisplayState>, ui_update_rx: Receiver<DisplayState>,
) { ) {
let display_level = config.ui_level; let display_level = config.ui_level;
if display_level == 0 { if display_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
} }
+3 -3
View File
@@ -1,7 +1,7 @@
/// Display module for the TP-Link M7350 oled one-bit display. /// Display module for the TP-Link M7350 oled one-bit display.
/// ///
/// https://github.com/m0veax/tplink_m7350/tree/main/oled /// https://github.com/m0veax/tplink_m7350/tree/main/oled
use crate::config; use crate::config::{self, UiLevel};
use crate::display::DisplayState; use crate::display::DisplayState;
use log::{error, info}; use log::{error, info};
@@ -115,7 +115,7 @@ pub fn update_ui(
mut ui_update_rx: Receiver<DisplayState>, mut ui_update_rx: Receiver<DisplayState>,
) { ) {
let display_level = config.ui_level; let display_level = config.ui_level;
if display_level == 0 { if display_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI."); 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 // we write the status every second because it may have been overwritten through menu
// navigation. // navigation.
if display_level != 0 if display_level != UiLevel::Invisible
&& let Err(e) = tokio::fs::write(OLED_PATH, pixels).await && let Err(e) = tokio::fs::write(OLED_PATH, pixels).await
{ {
error!("failed to write to display: {e}"); error!("failed to write to display: {e}");
+2 -2
View File
@@ -9,7 +9,7 @@ use tokio_util::task::TaskTracker;
use std::time::Duration; use std::time::Duration;
use crate::config; use crate::config::{self, UiLevel};
use crate::display::DisplayState; use crate::display::DisplayState;
macro_rules! led { macro_rules! led {
@@ -31,7 +31,7 @@ pub fn update_ui(
mut ui_update_rx: mpsc::Receiver<DisplayState>, mut ui_update_rx: mpsc::Receiver<DisplayState>,
) { ) {
let mut invisible: bool = false; let mut invisible: bool = false;
if config.ui_level == 0 { if config.ui_level == UiLevel::Invisible {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
invisible = true; invisible = true;
} }
+2 -1
View File
@@ -7,6 +7,7 @@ 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::config::GpsMode;
use crate::server::ServerState; use crate::server::ServerState;
fn deserialize_unix_ts<'de, D>(deserializer: D) -> Result<i64, D::Error> 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>>, State(state): State<Arc<ServerState>>,
Json(gps_data): Json<GpsData>, Json(gps_data): Json<GpsData>,
) -> Result<StatusCode, (StatusCode, String)> { ) -> Result<StatusCode, (StatusCode, String)> {
if state.config.gps_mode != 2 { if state.config.gps_mode != GpsMode::Api {
return Err(( return Err((
StatusCode::FORBIDDEN, StatusCode::FORBIDDEN,
"GPS API endpoint is disabled. Set gps_mode to 2 in configuration.".to_string(), "GPS API endpoint is disabled. Set gps_mode to 2 in configuration.".to_string(),
+2 -2
View File
@@ -6,7 +6,7 @@ use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use crate::config; use crate::config::{self, KeyInputMode};
use crate::diag::DiagDeviceCtrlMessage; use crate::diag::DiagDeviceCtrlMessage;
#[derive(Debug)] #[derive(Debug)]
@@ -23,7 +23,7 @@ pub fn run_key_input_thread(
diag_tx: Sender<DiagDeviceCtrlMessage>, diag_tx: Sender<DiagDeviceCtrlMessage>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) { ) {
if config.key_input_mode == 0 { if config.key_input_mode == KeyInputMode::Disabled {
return; return;
} }
+2 -2
View File
@@ -18,7 +18,7 @@ use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use crate::battery::run_battery_notification_worker; 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::diag::run_diag_read_thread;
use crate::error::RayhunterError; use crate::error::RayhunterError;
use crate::gps::{get_gps, post_gps}; use crate::gps::{get_gps, post_gps};
@@ -306,7 +306,7 @@ async fn run_with_config(
config.webdav.clone().into(), 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) { match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
(Some(lat), Some(lon)) => Some(gps::GpsData { (Some(lat), Some(lon)) => Some(gps::GpsData {
latitude: lat, latitude: lat,
+2 -1
View File
@@ -1,3 +1,4 @@
use crate::config::GpsMode;
use crate::gps::{GpsRecord, load_gps_records}; use crate::gps::{GpsRecord, load_gps_records};
use crate::server::ServerState; 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 // 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 // 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. // 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)) = ( if let (Some(lat), Some(lon)) = (
state.config.gps_fixed_latitude, state.config.gps_fixed_latitude,
state.config.gps_fixed_longitude, state.config.gps_fixed_longitude,
+7 -3
View File
@@ -2,6 +2,7 @@ use std::io::{self, ErrorKind};
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::config::GpsMode;
use chrono::{DateTime, Local, TimeDelta}; use chrono::{DateTime, Local, TimeDelta};
use log::{info, warn}; use log::{info, warn};
use rayhunter::util::RuntimeMetadata; use rayhunter::util::RuntimeMetadata;
@@ -71,11 +72,11 @@ pub struct ManifestEntry {
#[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)] #[serde(default)]
pub gps_mode: Option<u8>, pub gps_mode: Option<GpsMode>,
} }
impl ManifestEntry { impl ManifestEntry {
fn new(gps_mode: u8) -> Self { fn new(gps_mode: GpsMode) -> 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 {
@@ -257,7 +258,10 @@ 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, 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 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?;
+10 -11
View File
@@ -169,11 +169,11 @@
bind:value={config.ui_level} 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" 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={0}>Invisible mode</option>
<option value={1}>1 - Subtle mode (colored line)</option> <option value={1}>Subtle mode (colored line)</option>
<option value={2}>2 - Demo mode (orca gif)</option> <option value={2}>Demo mode (orca gif)</option>
<option value={3}>3 - EFF logo</option> <option value={3}>EFF logo</option>
<option value={4}>4 - High visibility (full screen color)</option> <option value={4}>High visibility (full screen color)</option>
</select> </select>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
Note: Rayhunter draws over the device's native UI, so some flickering is Note: Rayhunter draws over the device's native UI, so some flickering is
@@ -193,9 +193,8 @@
bind:value={config.key_input_mode} 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" 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={0}>Disable button control</option>
<option value={1}>1 - Double-tap power button to start new recording</option <option value={1}>Double-tap power button to start new recording</option>
>
</select> </select>
</div> </div>
@@ -786,9 +785,9 @@
<div> <div>
<label for="gps_mode" class="block text-sm font-medium text-gray-700 mb-1">GPS Mode</label> <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"> <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={0}>Disabled</option>
<option value={1}>1 - Fixed coordinates</option> <option value={1}>Fixed coordinates</option>
<option value={2}>2 - API Endpoint</option> <option value={2}>API endpoint</option>
</select> </select>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
{#if config.gps_mode === 2} {#if config.gps_mode === 2}