diff --git a/config.yml b/config.yml index bf892f4..470c6c9 100644 --- a/config.yml +++ b/config.yml @@ -1,19 +1,10 @@ # Hardware offset in milliseconds for correcting capture latency. -hardwareOffsetMs: 55 - -# Enable automatic clock synchronization. -# When enabled, the system will perform an initial full sync, then periodically -# nudge the clock to keep it aligned with the LTC source. -autoSyncEnabled: true - -# Default nudge in milliseconds for adjtimex control. -defaultNudgeMs: 2 +hardwareOffsetMs: 20 # Time-turning offsets. All values are added to the incoming LTC time. # These can be positive or negative. timeturnerOffset: - hours: 1 - minutes: 2 - seconds: 3 - frames: 4 - milliseconds: 5 + hours: 0 + minutes: 0 + seconds: 0 + frames: 0 diff --git a/src/api.rs b/src/api.rs index 93a883d..19e58a7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -238,7 +238,6 @@ mod tests { hardware_offset_ms: 10, timeturner_offset: TimeturnerOffset::default(), default_nudge_ms: 2, - auto_sync_enabled: false, })); let log_buffer = Arc::new(Mutex::new(VecDeque::new())); web::Data::new(AppState { @@ -304,8 +303,7 @@ mod tests { let new_config_json = serde_json::json!({ "hardwareOffsetMs": 55, "defaultNudgeMs": 2, - "autoSyncEnabled": true, - "timeturnerOffset": { "hours": 1, "minutes": 2, "seconds": 3, "frames": 4, "milliseconds": 5 } + "timeturnerOffset": { "hours": 1, "minutes": 2, "seconds": 3, "frames": 4 } }); let req = test::TestRequest::post() @@ -316,22 +314,16 @@ mod tests { let resp: Config = test::call_and_read_body_json(&app, req).await; assert_eq!(resp.hardware_offset_ms, 55); - assert_eq!(resp.auto_sync_enabled, true); assert_eq!(resp.timeturner_offset.hours, 1); - assert_eq!(resp.timeturner_offset.milliseconds, 5); let final_config = app_state.config.lock().unwrap(); assert_eq!(final_config.hardware_offset_ms, 55); - assert_eq!(final_config.auto_sync_enabled, true); assert_eq!(final_config.timeturner_offset.hours, 1); - assert_eq!(final_config.timeturner_offset.milliseconds, 5); // Test that the file was written assert!(fs::metadata(config_path).is_ok()); let contents = fs::read_to_string(config_path).unwrap(); assert!(contents.contains("hardwareOffsetMs: 55")); - assert!(contents.contains("autoSyncEnabled: true")); assert!(contents.contains("hours: 1")); - assert!(contents.contains("milliseconds: 5")); // Cleanup let _ = fs::remove_file(config_path); diff --git a/src/config.rs b/src/config.rs index 974d60b..9b2cb5d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,17 +19,11 @@ pub struct TimeturnerOffset { pub minutes: i64, pub seconds: i64, pub frames: i64, - #[serde(default)] - pub milliseconds: i64, } impl TimeturnerOffset { pub fn is_active(&self) -> bool { - self.hours != 0 - || self.minutes != 0 - || self.seconds != 0 - || self.frames != 0 - || self.milliseconds != 0 + self.hours != 0 || self.minutes != 0 || self.seconds != 0 || self.frames != 0 } } @@ -41,8 +35,6 @@ pub struct Config { pub timeturner_offset: TimeturnerOffset, #[serde(default = "default_nudge_ms")] pub default_nudge_ms: i64, - #[serde(default)] - pub auto_sync_enabled: bool, } fn default_nudge_ms() -> i64 { @@ -72,34 +64,13 @@ impl Default for Config { hardware_offset_ms: 0, timeturner_offset: TimeturnerOffset::default(), default_nudge_ms: default_nudge_ms(), - auto_sync_enabled: false, } } } pub fn save_config(path: &str, config: &Config) -> Result<(), Box> { - let mut s = String::new(); - s.push_str("# Hardware offset in milliseconds for correcting capture latency.\n"); - s.push_str(&format!("hardwareOffsetMs: {}\n\n", config.hardware_offset_ms)); - - s.push_str("# Enable automatic clock synchronization.\n"); - s.push_str("# When enabled, the system will perform an initial full sync, then periodically\n"); - s.push_str("# nudge the clock to keep it aligned with the LTC source.\n"); - s.push_str(&format!("autoSyncEnabled: {}\n\n", config.auto_sync_enabled)); - - s.push_str("# Default nudge in milliseconds for adjtimex control.\n"); - s.push_str(&format!("defaultNudgeMs: {}\n\n", config.default_nudge_ms)); - - s.push_str("# Time-turning offsets. All values are added to the incoming LTC time.\n"); - s.push_str("# These can be positive or negative.\n"); - s.push_str("timeturnerOffset:\n"); - s.push_str(&format!(" hours: {}\n", config.timeturner_offset.hours)); - s.push_str(&format!(" minutes: {}\n", config.timeturner_offset.minutes)); - s.push_str(&format!(" seconds: {}\n", config.timeturner_offset.seconds)); - s.push_str(&format!(" frames: {}\n", config.timeturner_offset.frames)); - s.push_str(&format!(" milliseconds: {}\n", config.timeturner_offset.milliseconds)); - - fs::write(path, s)?; + let contents = serde_yaml::to_string(config)?; + fs::write(path, contents)?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index e265210..698eabe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,11 +42,6 @@ const DEFAULT_CONFIG: &str = r#" # Hardware offset in milliseconds for correcting capture latency. hardwareOffsetMs: 20 -# Enable automatic clock synchronization. -# When enabled, the system will perform an initial full sync, then periodically -# nudge the clock to keep it aligned with the LTC source. -autoSyncEnabled: false - # Default nudge in milliseconds for adjtimex control. defaultNudgeMs: 2 @@ -57,7 +52,6 @@ timeturnerOffset: minutes: 0 seconds: 0 frames: 0 - milliseconds: 0 "#; /// If no `config.yml` exists alongside the binary, write out the default. @@ -142,79 +136,11 @@ async fn main() { log::info!("🚀 Starting TimeTurner daemon..."); } - // 6️⃣ Spawn the auto-sync thread - { - let sync_state = ltc_state.clone(); - let sync_config = config.clone(); - thread::spawn(move || { - // Wait for the first LTC frame to arrive - loop { - if sync_state.lock().unwrap().latest.is_some() { - log::info!("Auto-sync: Initial LTC frame detected."); - break; - } - thread::sleep(std::time::Duration::from_secs(1)); - } - - // Initial sync - { - let state = sync_state.lock().unwrap(); - let config = sync_config.lock().unwrap(); - if config.auto_sync_enabled { - if let Some(frame) = &state.latest { - log::info!("Auto-sync: Performing initial full sync."); - if system::trigger_sync(frame, &config).is_ok() { - log::info!("Auto-sync: Initial sync successful."); - } else { - log::error!("Auto-sync: Initial sync failed."); - } - } - } - } - - thread::sleep(std::time::Duration::from_secs(10)); - - // Main auto-sync loop - loop { - { - let state = sync_state.lock().unwrap(); - let config = sync_config.lock().unwrap(); - - if config.auto_sync_enabled && state.latest.is_some() { - let delta = state.get_ewma_clock_delta(); - let frame = state.latest.as_ref().unwrap(); - - if delta.abs() > 40 { - log::info!("Auto-sync: Delta > 40ms ({}ms), performing full sync.", delta); - if system::trigger_sync(frame, &config).is_ok() { - log::info!("Auto-sync: Full sync successful."); - } else { - log::error!("Auto-sync: Full sync failed."); - } - } else if delta.abs() >= 1 { - // nudge_clock takes microseconds. A positive delta means clock is - // ahead, so we need a negative nudge. - let nudge_us = -delta * 1000; - log::info!("Auto-sync: Delta is {}ms, nudging clock by {}us.", delta, nudge_us); - if system::nudge_clock(nudge_us).is_ok() { - log::info!("Auto-sync: Clock nudge successful."); - } else { - log::error!("Auto-sync: Clock nudge failed."); - } - } - } - } // locks released here - - thread::sleep(std::time::Duration::from_secs(10)); - } - }); - } - - // 7️⃣ Set up a LocalSet for the API server and main loop + // 6️⃣ Set up a LocalSet for the API server and main loop let local = LocalSet::new(); local .run_until(async move { - // 8️⃣ Spawn the API server thread + // 7️⃣ Spawn the API server thread { let api_state = ltc_state.clone(); let config_clone = config.clone(); @@ -228,7 +154,7 @@ async fn main() { }); } - // 9️⃣ Main logic loop: process frames from serial and update state + // 8️⃣ Main logic loop: process frames from serial and update state let loop_state = ltc_state.clone(); let loop_config = config.clone(); let logic_task = task::spawn_blocking(move || { @@ -249,7 +175,7 @@ async fn main() { } }); - // 1️⃣0️⃣ Keep main thread alive + // 9️⃣ Keep main thread alive if args.command.is_some() { // In daemon mode, wait forever. The logic_task runs in the background. std::future::pending::<()>().await; @@ -269,35 +195,18 @@ mod tests { use std::fs; use std::path::Path; - /// RAII guard to manage config file during tests. - /// It saves the original content of `config.yml` if it exists, - /// and restores it when the guard goes out of scope. - /// If the file didn't exist, it's removed. - struct ConfigGuard { - original_content: Option, - } - - impl ConfigGuard { - fn new() -> Self { - Self { - original_content: fs::read_to_string("config.yml").ok(), - } - } - } + /// RAII guard to ensure config file is cleaned up after test. + struct ConfigGuard; impl Drop for ConfigGuard { fn drop(&mut self) { - if let Some(content) = &self.original_content { - fs::write("config.yml", content).expect("Failed to restore config.yml"); - } else { - let _ = fs::remove_file("config.yml"); - } + let _ = fs::remove_file("config.yml"); } } #[test] fn test_ensure_config() { - let _guard = ConfigGuard::new(); // Cleanup when _guard goes out of scope. + let _guard = ConfigGuard; // Cleanup when _guard goes out of scope. // --- Test 1: File creation --- // Pre-condition: config.yml does not exist. diff --git a/src/sync_logic.rs b/src/sync_logic.rs index d54f2cc..4a7fd5c 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -348,7 +348,7 @@ mod tests { assert_eq!(get_sync_status(-100, &config), "CLOCK BEHIND"); // Test TIMETURNING status - config.timeturner_offset = TimeturnerOffset { hours: 1, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }; + config.timeturner_offset = TimeturnerOffset { hours: 1, minutes: 0, seconds: 0, frames: 0 }; assert_eq!(get_sync_status(0, &config), "TIMETURNING"); assert_eq!(get_sync_status(100, &config), "TIMETURNING"); } diff --git a/src/system.rs b/src/system.rs index c3918f6..1d2ed7d 100644 --- a/src/system.rs +++ b/src/system.rs @@ -57,7 +57,7 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime Result { @@ -177,7 +177,6 @@ mod tests { minutes: 5, seconds: 10, frames: 12, // 12 frames at 25fps is 480ms - milliseconds: 20, }; let target_time = calculate_target_time(&frame, &config); @@ -185,8 +184,8 @@ mod tests { assert_eq!(target_time.hour(), 11); assert_eq!(target_time.minute(), 25); assert_eq!(target_time.second(), 40); - // 480ms + 20ms = 500ms - assert_eq!(target_time.nanosecond(), 500_000_000); + // 480ms + assert_eq!(target_time.nanosecond(), 480_000_000); } #[test] @@ -198,15 +197,14 @@ mod tests { minutes: -5, seconds: -10, frames: -12, // -480ms - milliseconds: -80, }; let target_time = calculate_target_time(&frame, &config); assert_eq!(target_time.hour(), 9); assert_eq!(target_time.minute(), 15); - assert_eq!(target_time.second(), 19); - assert_eq!(target_time.nanosecond(), 920_000_000); + assert_eq!(target_time.second(), 20); + assert_eq!(target_time.nanosecond(), 0); } #[test] diff --git a/src/ui.rs b/src/ui.rs index b36e9e3..fd7d71b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -34,6 +34,7 @@ pub fn start_ui( terminal::enable_raw_mode().unwrap(); let mut logs: VecDeque = VecDeque::with_capacity(10); + let mut out_of_sync_since: Option = None; let mut last_delta_update = Instant::now() - Duration::from_secs(1); let mut cached_delta_ms: i64 = 0; let mut cached_delta_frames: i64 = 0; @@ -93,7 +94,28 @@ pub fn start_ui( // 6️⃣ sync status wording let sync_status = get_sync_status(cached_delta_ms, &cfg); - // 7️⃣ header & LTC metrics display + // 7️⃣ auto‑sync (same as manual but delayed) + if sync_status != "IN SYNC" && sync_status != "TIMETURNING" { + if let Some(start) = out_of_sync_since { + if start.elapsed() >= Duration::from_secs(5) { + if let Some(frame) = &state.lock().unwrap().latest { + let entry = match system::trigger_sync(frame, &cfg) { + Ok(ts) => format!("🔄 Auto‑synced to LTC: {}", ts), + Err(_) => "❌ Auto‑sync failed".into(), + }; + if logs.len() == 10 { logs.pop_front(); } + logs.push_back(entry); + } + out_of_sync_since = None; + } + } else { + out_of_sync_since = Some(Instant::now()); + } + } else { + out_of_sync_since = None; + } + + // 8️⃣ header & LTC metrics display { let st = state.lock().unwrap(); let opt = st.latest.as_ref(); diff --git a/static/index.html b/static/index.html index eb074af..79bfd80 100644 --- a/static/index.html +++ b/static/index.html @@ -50,33 +50,14 @@
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ + + + + + + +
diff --git a/static/script.js b/static/script.js index 22c8dd7..ad9178c 100644 --- a/static/script.js +++ b/static/script.js @@ -15,13 +15,11 @@ document.addEventListener('DOMContentLoaded', () => { }; const hwOffsetInput = document.getElementById('hw-offset'); - const autoSyncCheckbox = document.getElementById('auto-sync-enabled'); const offsetInputs = { h: document.getElementById('offset-h'), m: document.getElementById('offset-m'), s: document.getElementById('offset-s'), f: document.getElementById('offset-f'), - ms: document.getElementById('offset-ms'), }; const saveConfigButton = document.getElementById('save-config'); const manualSyncButton = document.getElementById('manual-sync'); @@ -82,12 +80,10 @@ document.addEventListener('DOMContentLoaded', () => { if (!response.ok) throw new Error('Failed to fetch config'); const data = await response.json(); hwOffsetInput.value = data.hardwareOffsetMs; - autoSyncCheckbox.checked = data.autoSyncEnabled; offsetInputs.h.value = data.timeturnerOffset.hours; offsetInputs.m.value = data.timeturnerOffset.minutes; offsetInputs.s.value = data.timeturnerOffset.seconds; offsetInputs.f.value = data.timeturnerOffset.frames; - offsetInputs.ms.value = data.timeturnerOffset.milliseconds || 0; nudgeValueInput.value = data.defaultNudgeMs; } catch (error) { console.error('Error fetching config:', error); @@ -97,14 +93,12 @@ document.addEventListener('DOMContentLoaded', () => { async function saveConfig() { const config = { hardwareOffsetMs: parseInt(hwOffsetInput.value, 10) || 0, - autoSyncEnabled: autoSyncCheckbox.checked, defaultNudgeMs: parseInt(nudgeValueInput.value, 10) || 0, timeturnerOffset: { hours: parseInt(offsetInputs.h.value, 10) || 0, minutes: parseInt(offsetInputs.m.value, 10) || 0, seconds: parseInt(offsetInputs.s.value, 10) || 0, frames: parseInt(offsetInputs.f.value, 10) || 0, - milliseconds: parseInt(offsetInputs.ms.value, 10) || 0, } };