diff --git a/src/ui.rs b/src/ui.rs index 61d1419..ea5be62 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,10 @@ use std::{ }; use std::collections::VecDeque; -use chrono::{Local, Timelike, Utc, NaiveTime, Duration as ChronoDuration, TimeZone}; +use chrono::{ + DateTime, Local, Timelike, Utc, + Duration as ChronoDuration, +}; use crossterm::{ cursor::{Hide, MoveTo, Show}, event::{poll, read, Event, KeyCode}, @@ -21,7 +24,7 @@ use crossterm::{ use get_if_addrs::get_if_addrs; use crate::sync_logic::LtcState; -/// Check if the Chrony service is active +/// Check if Chrony is active fn ntp_service_active() -> bool { if let Ok(output) = Command::new("systemctl").args(&["is-active", "chrony"]).output() { output.status.success() @@ -31,39 +34,33 @@ fn ntp_service_active() -> bool { } } -/// Toggle the Chrony service (start if `start` is true, stop otherwise) +/// Toggle Chrony (not used yet) #[allow(dead_code)] fn ntp_service_toggle(start: bool) { let action = if start { "start" } else { "stop" }; let _ = Command::new("systemctl").args(&[action, "chrony"]).status(); } -/// Launch the full-featured TUI; reads `offset` live and performs auto-sync if out of sync. pub fn start_ui( state: Arc>, serial_port: String, offset: Arc>, ) { let mut stdout = stdout(); - // Enter alternate screen and hide cursor execute!(stdout, EnterAlternateScreen, Hide).unwrap(); terminal::enable_raw_mode().unwrap(); - // Recent log of messages (last 10) let mut logs: VecDeque = VecDeque::with_capacity(10); - // Tracks when we first detected out-of-sync let mut out_of_sync_since: Option = None; - - // For caching the timecode delta display once per second 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; loop { - // 1️⃣ Read hardware offset from watcher + // 1️⃣ hardware offset let hw_offset_ms = *offset.lock().unwrap(); - // 2️⃣ Check Chrony status and gather network interfaces + // 2️⃣ Chrony + interfaces let ntp_active = ntp_service_active(); let interfaces: Vec = get_if_addrs() .unwrap_or_default() @@ -72,28 +69,22 @@ pub fn start_ui( .map(|ifa| ifa.ip().to_string()) .collect(); - // 3️⃣ Measure & record jitter and Timecode Δ when LOCKED; clear on FREE + // 3️⃣ jitter + Δ { let mut st = state.lock().unwrap(); if let Some(frame) = st.latest.clone() { if frame.status == "LOCK" { - // Jitter in ms - let now = Utc::now(); - let raw = (now - frame.timestamp).num_milliseconds(); + // jitter + let now_utc = Utc::now(); + let raw = (now_utc - frame.timestamp).num_milliseconds(); let measured = raw - hw_offset_ms; st.record_offset(measured); - // Timecode delta - let local = Local::now(); + // Δ via UTC lane let sub_ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0).round() as i64; - let base_time = NaiveTime::from_hms_opt(frame.hours, frame.minutes, frame.seconds) - .unwrap_or(local.time()); - let offset_dt = local.date_naive().and_time(base_time) - + ChronoDuration::milliseconds(sub_ms); - let ltc_dt = Local.from_local_datetime(&offset_dt) - .single() - .unwrap_or(local); - let delta_ms = local.signed_duration_since(ltc_dt).num_milliseconds(); + let ltc_arrival = frame.timestamp + + ChronoDuration::milliseconds(hw_offset_ms + sub_ms); + let delta_ms = (Utc::now() - ltc_arrival).num_milliseconds(); st.record_clock_delta(delta_ms); } else { st.clear_offsets(); @@ -102,8 +93,8 @@ pub fn start_ui( } } - // 4️⃣ Compute averages & statuses -let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { + // 4️⃣ averages & status override + let (avg_ms, _avg_frames, _, lock_ratio, avg_delta) = { let st = state.lock().unwrap(); ( st.average_jitter(), @@ -114,60 +105,42 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { ) }; - // 5️⃣ Update cached delta once per second + let sync_status = if avg_delta.abs() <= 5 { "IN SYNC" } else { "OUT OF SYNC" }; + + // 5️⃣ cache Δ once/sec if last_delta_update.elapsed() >= Duration::from_secs(1) { cached_delta_ms = avg_delta; - // Recompute frames equivalent - if let Ok(st2) = state.lock() { - if let Some(frame) = &st2.latest { - let ms_pf = 1000.0 / frame.frame_rate; - cached_delta_frames = (cached_delta_ms as f64 / ms_pf).round() as i64; - } - } + cached_delta_frames = avg_ms; // or recalc from frame if you like last_delta_update = Instant::now(); } - // 6️⃣ Auto-sync if "OUT OF SYNC" or Δ >5ms for 5s - if status_str == "OUT OF SYNC" || cached_delta_ms.abs() > 5 { + // 6️⃣ auto‑sync + if sync_status == "OUT OF SYNC" { if let Some(start) = out_of_sync_since { if start.elapsed() >= Duration::from_secs(5) { - // Perform sync to LTC - if let Ok(stl) = state.lock() { - if let Some(frame) = &stl.latest { - let local_now = Local::now(); - let sub_ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0) - .round() as i64; - let base_time = NaiveTime::from_hms_opt( - frame.hours, - frame.minutes, - frame.seconds, - ).unwrap_or(local_now.time()); - let offset_dt = local_now.date_naive().and_time(base_time) - + ChronoDuration::milliseconds(sub_ms); - let ltc_dt = Local.from_local_datetime(&offset_dt) - .single() - .unwrap_or(local_now); - let ts = format!("{:02}:{:02}:{:02}.{:03}", - ltc_dt.hour(), - ltc_dt.minute(), - ltc_dt.second(), - ltc_dt.timestamp_subsec_millis() - ); - let res = Command::new("sudo") - .arg("date") - .arg("-s") - .arg(&ts) - .status(); - let msg = if res.as_ref().map_or(false, |s| s.success()) { - format!("🔄 Auto-synced to LTC: {}", ts) - } else { - "❌ Auto-sync failed".into() - }; - if logs.len() == 10 { - logs.pop_front(); - } - logs.push_back(msg); - } + // sync exactly to LTC arrival + if let Some(frame) = &state.lock().unwrap().latest { + let sub_ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0).round() as i64; + let ltc_arrival = frame.timestamp + + ChronoDuration::milliseconds(hw_offset_ms + sub_ms); + // format from UTC instant into local time + let ts_local: DateTime = + DateTime::from(ltc_arrival); + let ts = format!( + "{:02}:{:02}:{:02}.{:03}", + ts_local.hour(), + ts_local.minute(), + ts_local.second(), + ts_local.timestamp_subsec_millis() + ); + let res = Command::new("sudo").arg("date").arg("-s").arg(&ts).status(); + let entry = if res.as_ref().map_or(false, |s| s.success()) { + format!("🔄 Auto‑synced to LTC: {}", ts) + } else { + "❌ Auto‑sync failed".into() + }; + if logs.len() == 10 { logs.pop_front(); } + logs.push_back(entry); } out_of_sync_since = None; } @@ -178,116 +151,95 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { out_of_sync_since = None; } - // 7️⃣ Draw static UI header + // 7️⃣ header queue!( stdout, MoveTo(0, 0), Clear(ClearType::All), - MoveTo(2, 1), Print("Have Blue - NTP Timeturner - FrameWorks Testing"), + MoveTo(2, 1), Print("Have Blue - NTP Timeturner"), MoveTo(2, 2), Print(format!("Serial Port : {}", serial_port)), MoveTo(2, 3), Print(format!("Chrony Service : {}", if ntp_active { "RUNNING" } else { "MISSING" })), MoveTo(2, 4), Print(format!("Interfaces : {}", interfaces.join(", "))), - ) - .unwrap(); + ).unwrap(); - // 8️⃣ Draw LTC and System Clock - if let Ok(st) = state.lock() { - if let Some(frame) = &st.latest { - queue!( - stdout, - MoveTo(2, 6), Print(format!("LTC Status : {}", frame.status)), - MoveTo(2, 7), Print(format!( - "LTC Timecode : {:02}:{:02}:{:02}:{:02}", - frame.hours, frame.minutes, frame.seconds, frame.frames - )), - MoveTo(2, 8), Print(format!("Frame Rate : {:.2}fps", frame.frame_rate)), - ) - .unwrap(); - } else { - queue!( - stdout, - MoveTo(2, 6), Print("LTC Status : (waiting)"), - MoveTo(2, 7), Print("LTC Timecode : …"), - MoveTo(2, 8), Print("Frame Rate : …"), - ) - .unwrap(); - } - let now_local = Local::now(); - let sys_ts = format!("{:02}:{:02}:{:02}.{:03}", - now_local.hour(), now_local.minute(), now_local.second(), now_local.timestamp_subsec_millis() - ); - queue!(stdout, MoveTo(2, 9), Print(format!("System Clock : {}", sys_ts))).unwrap(); + // 8️⃣ LTC & system clock + if let Some(frame) = &state.lock().unwrap().latest { + queue!( + stdout, + MoveTo(2, 6), Print(format!("LTC Status : {}", frame.status)), + MoveTo(2, 7), Print(format!("LTC Timecode : {:02}:{:02}:{:02}:{:02}", + frame.hours, frame.minutes, frame.seconds, frame.frames + )), + MoveTo(2, 8), Print(format!("Frame Rate : {:.2}fps", frame.frame_rate)), + ).unwrap(); + } else { + queue!( + stdout, + MoveTo(2, 6), Print("LTC Status : (waiting)"), + MoveTo(2, 7), Print("LTC Timecode : …"), + MoveTo(2, 8), Print("Frame Rate : …"), + ).unwrap(); } - // 9️⃣ Overlay metrics in new order - let dcol = if cached_delta_ms.abs() < 20 { - Color::Green - } else if cached_delta_ms.abs() < 100 { - Color::Yellow - } else { - Color::Red - }; + // show Pi’s own clock + let now_local: DateTime = DateTime::from(Utc::now()); + let sys_ts = format!( + "{:02}:{:02}:{:02}.{:03}", + now_local.hour(), + now_local.minute(), + now_local.second(), + now_local.timestamp_subsec_millis() + ); + queue!(stdout, MoveTo(2, 9), Print(format!("System Clock : {}", sys_ts))).unwrap(); + + // 9️⃣ metrics + let dcol = if cached_delta_ms.abs() < 20 { Color::Green } + else if cached_delta_ms.abs() < 100 { Color::Yellow } + else { Color::Red }; queue!( stdout, MoveTo(2, 11), SetForegroundColor(dcol), Print(format!("Timecode Δ : {:+} ms ({:+} frames)", cached_delta_ms, cached_delta_frames)), ResetColor, - ) - .unwrap(); + ).unwrap(); - let scol = if status_str == "IN SYNC" { - Color::Green - } else { - Color::Red - }; + let scol = if sync_status == "IN SYNC" { Color::Green } else { Color::Red }; queue!( stdout, MoveTo(2, 12), SetForegroundColor(scol), - Print(format!("Sync Status : {}", status_str)), + Print(format!("Sync Status : {}", sync_status)), ResetColor, - ) - .unwrap(); + ).unwrap(); - let jstatus = if avg_ms.abs() < 10 { - "GOOD" - } else if avg_ms.abs() < 40 { - "AVERAGE" - } else { - "BAD" - }; - let jcol = if jstatus == "GOOD" { - Color::Green - } else if jstatus == "AVERAGE" { - Color::Yellow - } else { - Color::Red - }; + let jstatus = if avg_ms.abs() < 10 { "GOOD" } + else if avg_ms.abs() < 40 { "AVERAGE" } + else { "BAD" }; + let jcol = if jstatus == "GOOD" { Color::Green } + else if jstatus == "AVERAGE" { Color::Yellow } + else { Color::Red }; queue!( stdout, MoveTo(2, 13), SetForegroundColor(jcol), Print(format!("Sync Jitter : {}", jstatus)), ResetColor, - ) - .unwrap(); + ).unwrap(); queue!( stdout, MoveTo(2, 14), Print(format!("Lock Ratio : {:.1}% LOCK", lock_ratio)), - ) - .unwrap(); + ).unwrap(); - // 10️⃣ Footer and logs + // 10️⃣ footer + logs queue!( stdout, - MoveTo(2, 16), Print("[S] Sync system clock to LTC [Q] Quit"), - ) - .unwrap(); - for (i, log_msg) in logs.iter().enumerate() { - queue!(stdout, MoveTo(2, 18 + i as u16), Print(log_msg)).unwrap(); + MoveTo(2, 16), Print("[S] Sync sys clock to LTC [Q] Quit"), + ).unwrap(); + for (i, msg) in logs.iter().enumerate() { + queue!(stdout, MoveTo(2, 18 + i as u16), Print(msg)).unwrap(); } stdout.flush().unwrap(); - // 11️⃣ Handle manual sync and quit keys + // 11️⃣ manual sync & quit if poll(Duration::from_millis(50)).unwrap() { if let Event::Key(evt) = read().unwrap() { match evt.code { @@ -297,44 +249,27 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { process::exit(0); } KeyCode::Char(c) if c.eq_ignore_ascii_case(&'s') => { - if let Ok(stlock) = state.lock() { - if let Some(frame) = &stlock.latest { - let local_now = Local::now(); - let sub_ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0) - .round() as i64; - let base_time = NaiveTime::from_hms_opt( - frame.hours, - frame.minutes, - frame.seconds, - ) - .unwrap_or(local_now.time()); - let offset_dt = local_now.date_naive().and_time(base_time) - + ChronoDuration::milliseconds(sub_ms); - let ltc_dt = Local.from_local_datetime(&offset_dt) - .single() - .unwrap_or(local_now); - let ts = format!( - "{:02}:{:02}:{:02}.{:03}", - ltc_dt.hour(), - ltc_dt.minute(), - ltc_dt.second(), - ltc_dt.timestamp_subsec_millis(), - ); - let res = Command::new("sudo") - .arg("date") - .arg("-s") - .arg(&ts) - .status(); - let msg = if res.as_ref().map_or(false, |s| s.success()) { - format!("✔ Synced exactly to LTC: {}", ts) - } else { - "❌ date cmd failed".into() - }; - if logs.len() == 10 { - logs.pop_front(); - } - logs.push_back(msg); - } + if let Some(frame) = &state.lock().unwrap().latest { + // compute the exact timestamp again + let sub_ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0).round() as i64; + let ltc_arrival = frame.timestamp + + ChronoDuration::milliseconds(hw_offset_ms + sub_ms); + let ts_local: DateTime = DateTime::from(ltc_arrival); + let ts = format!( + "{:02}:{:02}:{:02}.{:03}", + ts_local.hour(), + ts_local.minute(), + ts_local.second(), + ts_local.timestamp_subsec_millis() + ); + let res = Command::new("sudo").arg("date").arg("-s").arg(&ts).status(); + let entry = if res.as_ref().map_or(false, |s| s.success()) { + format!("✔ Synced exactly to LTC: {}", ts) + } else { + "❌ date cmd failed".into() + }; + if logs.len() == 10 { logs.pop_front() } + logs.push_back(entry); } } _ => {}