feat: add timeturner for time offsets and migrate config to YAML

Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
This commit is contained in:
Chaos Rogers 2025-07-21 19:16:06 +01:00
parent 08577f5064
commit 777a202877
9 changed files with 245 additions and 160 deletions

View file

@ -7,7 +7,7 @@ mod serial_input;
mod ui;
use crate::api::start_api_server;
use crate::config::watch_config;
use crate::config::{watch_config, Config};
use crate::sync_logic::LtcState;
use crate::serial_input::start_serial_thread;
use crate::ui::start_ui;
@ -21,17 +21,26 @@ use std::{
use tokio::task::{self, LocalSet};
/// Default config content, embedded in the binary.
const DEFAULT_CONFIG: &str = r#"{
"hardware_offset_ms": 20
}"#;
const DEFAULT_CONFIG: &str = r#"
# Hardware offset in milliseconds for correcting capture latency.
hardwareOffsetMs: 20
/// If no `config.json` exists alongside the binary, write out the default.
# Time-turning offsets. All values are added to the incoming LTC time.
# These can be positive or negative.
timeturnerOffset:
hours: 0
minutes: 0
seconds: 0
frames: 0
"#;
/// If no `config.yml` exists alongside the binary, write out the default.
fn ensure_config() {
let p = Path::new("config.json");
let p = Path::new("config.yml");
if !p.exists() {
fs::write(p, DEFAULT_CONFIG)
.expect("Failed to write default config.json");
eprintln!("⚙️ Emitted default config.json");
fs::write(p, DEFAULT_CONFIG.trim())
.expect("Failed to write default config.yml");
eprintln!("⚙️ Emitted default config.yml");
}
}
@ -40,9 +49,9 @@ async fn main() {
// 🔄 Ensure there's always a config.json present
ensure_config();
// 1⃣ Start watching config.json for changes
let hw_offset = watch_config("config.json");
println!("🔧 Watching config.json (hardware_offset_ms)...");
// 1⃣ Start watching config.yml for changes
let config = watch_config("config.yml");
println!("🔧 Watching config.yml...");
// 2⃣ Channel for raw LTC frames
let (tx, rx) = mpsc::channel();
@ -68,14 +77,14 @@ async fn main() {
});
}
// 5⃣ Spawn the UI renderer thread, passing the live offset Arc
// 5⃣ Spawn the UI renderer thread, passing the live config Arc
{
let ui_state = ltc_state.clone();
let offset_clone = hw_offset.clone();
let config_clone = config.clone();
let port = "/dev/ttyACM0".to_string();
thread::spawn(move || {
println!("🖥️ UI thread launched");
start_ui(ui_state, port, offset_clone);
start_ui(ui_state, port, config_clone);
});
}
@ -86,9 +95,9 @@ async fn main() {
// 7⃣ Spawn the API server thread
{
let api_state = ltc_state.clone();
let offset_clone = hw_offset.clone();
let config_clone = config.clone();
task::spawn_local(async move {
if let Err(e) = start_api_server(api_state, offset_clone).await {
if let Err(e) = start_api_server(api_state, config_clone).await {
eprintln!("API server error: {}", e);
}
});
@ -118,7 +127,7 @@ mod tests {
impl Drop for ConfigGuard {
fn drop(&mut self) {
let _ = fs::remove_file("config.json");
let _ = fs::remove_file("config.yml");
}
}
@ -127,28 +136,28 @@ mod tests {
let _guard = ConfigGuard; // Cleanup when _guard goes out of scope.
// --- Test 1: File creation ---
// Pre-condition: config.json does not exist.
let _ = fs::remove_file("config.json");
// Pre-condition: config.yml does not exist.
let _ = fs::remove_file("config.yml");
ensure_config();
// Post-condition: config.json exists and has default content.
let p = Path::new("config.json");
assert!(p.exists(), "config.json should have been created");
let contents = fs::read_to_string(p).expect("Failed to read created config.json");
assert_eq!(contents, DEFAULT_CONFIG, "config.json content should match default");
// Post-condition: config.yml exists and has default content.
let p = Path::new("config.yml");
assert!(p.exists(), "config.yml should have been created");
let contents = fs::read_to_string(p).expect("Failed to read created config.yml");
assert_eq!(contents, DEFAULT_CONFIG.trim(), "config.yml content should match default");
// --- Test 2: File is not overwritten ---
// Pre-condition: config.json exists with different content.
let custom_content = "{\"hardware_offset_ms\": 999}";
fs::write("config.json", custom_content)
.expect("Failed to write custom config.json for test");
// Pre-condition: config.yml exists with different content.
let custom_content = "hardwareOffsetMs: 999";
fs::write("config.yml", custom_content)
.expect("Failed to write custom config.yml for test");
ensure_config();
// Post-condition: config.json still has the custom content.
let contents_after = fs::read_to_string("config.json")
.expect("Failed to read config.json after second ensure_config call");
assert_eq!(contents_after, custom_content, "config.json should not be overwritten");
// Post-condition: config.yml still has the custom content.
let contents_after = fs::read_to_string("config.yml")
.expect("Failed to read config.yml after second ensure_config call");
assert_eq!(contents_after, custom_content, "config.yml should not be overwritten");
}
}