From 80a4fbe538de98d0493732be2f0fe99e547945bd Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright <85807217+cjfranko@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:38:29 +0100 Subject: [PATCH] force commit of ui.rs --- src/ui.rs | 341 ++++++++++++++++++++++++++---------------------------- 1 file changed, 166 insertions(+), 175 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index c1292c1..a8b0286 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,4 @@ -// src/ui.rs - -use std::{ +use std::{ io::{stdout, Write}, process::{self, Command}, sync::{Arc, Mutex}, @@ -9,7 +7,10 @@ use std::{ }; use std::collections::VecDeque; -use chrono::{Local, Timelike, Utc, NaiveTime, Duration as ChronoDuration, TimeZone}; +use chrono::{ + DateTime, Local, Timelike, Utc, + NaiveTime, TimeZone, +}; use crossterm::{ cursor::{Hide, MoveTo, Show}, event::{poll, read, Event, KeyCode}, @@ -21,7 +22,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 +32,33 @@ fn ntp_service_active() -> bool { } } -/// Toggle the Chrony service (start if `start` is true, stop otherwise) - -fn _ntp_service_toggle(start: bool) { +/// 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 +67,30 @@ 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(); - 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) + // Δ = system clock - LTC timecode (use LOCAL time) + let today_local = Local::now().date_naive(); + let ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0) + .round() as u32; + let tc_naive = NaiveTime::from_hms_milli_opt( + frame.hours, frame.minutes, frame.seconds, ms, + ).expect("Invalid LTC timecode"); + let naive_dt_local = today_local.and_time(tc_naive); + let dt_local = Local + .from_local_datetime(&naive_dt_local) .single() - .unwrap_or(local); - let delta_ms = local.signed_duration_since(ltc_dt).num_milliseconds(); + .expect("Invalid local time"); + let delta_ms = (Local::now() - dt_local).num_milliseconds(); st.record_clock_delta(delta_ms); } else { st.clear_offsets(); @@ -102,8 +99,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_jitter_ms, _avg_frames, _, lock_ratio, avg_delta) = { let st = state.lock().unwrap(); ( st.average_jitter(), @@ -114,60 +111,60 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { ) }; - // 5️⃣ Update cached delta once per second + // 5️⃣ cache Δ once/sec & Δ in frames 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; - } + if let Some(frame) = &state.lock().unwrap().latest { + let frame_ms = 1000.0 / frame.frame_rate; + cached_delta_frames = ((avg_delta as f64 / frame_ms).round()) as i64; + } else { + cached_delta_frames = 0; } 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️⃣ sync status wording + let sync_status = if cached_delta_ms.abs() <= 8 { + "IN SYNC" + } else if cached_delta_ms > 10 { + "CLOCK AHEAD" + } else { + "CLOCK BEHIND" + }; + + // 7️⃣ auto‑sync (same as manual but delayed) + if sync_status != "IN 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); - } + if let Some(frame) = &state.lock().unwrap().latest { + let today_local = Local::now().date_naive(); + let ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0) + .round() as u32; + let timecode = NaiveTime::from_hms_milli_opt( + frame.hours, frame.minutes, frame.seconds, ms, + ).expect("Invalid LTC timecode"); + let naive_dt = today_local.and_time(timecode); + let dt_local = Local + .from_local_datetime(&naive_dt) + .single() + .expect("Ambiguous or invalid local time"); + let ts = dt_local.format("%H:%M:%S.%3f").to_string(); + + let success = Command::new("sudo") + .arg("date") + .arg("-s") + .arg(&ts) + .status() + .map(|s| s.success()) + .unwrap_or(false); + + let entry = if 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,47 +175,52 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { out_of_sync_since = None; } - // 7️⃣ Draw static UI header - queue!( - stdout, - MoveTo(0, 0), Clear(ClearType::All), - MoveTo(2, 1), Print("Have Blue - NTP Timeturner - FrameWorks Testing"), - 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(); + // 8️⃣ header & LTC metrics display + { + let st = state.lock().unwrap(); + let opt = st.latest.as_ref(); + let status_str = opt.map(|f| f.status.as_str()).unwrap_or("(waiting)"); + let tc_str = match opt { + Some(f) => format!("LTC Timecode : {:02}:{:02}:{:02}:{:02}", + f.hours, f.minutes, f.seconds, f.frames), + None => "LTC Timecode : …".to_string(), + }; + let fr_str = match opt { + Some(f) => format!("Frame Rate : {:.2}fps", f.frame_rate), + None => "Frame Rate : …".to_string(), + }; - // 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(); + queue!( + stdout, + MoveTo(0, 0), Clear(ClearType::All), + 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(", "))), + MoveTo(2, 6), Print(format!("LTC Status : {}", status_str)), + MoveTo(2, 7), Print(tc_str), + MoveTo(2, 8), Print(fr_str), + ).unwrap(); } - // 9️⃣ Overlay metrics in new order + // system 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(); + + // Δ display let dcol = if cached_delta_ms.abs() < 20 { Color::Green } else if cached_delta_ms.abs() < 100 { @@ -231,10 +233,10 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { 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" { + // sync status + let scol = if sync_status == "IN SYNC" { Color::Green } else { Color::Red @@ -242,14 +244,14 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { 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 { + // jitter & lock ratio + let jstatus = if avg_jitter_ms.abs() < 10 { "GOOD" - } else if avg_ms.abs() < 40 { + } else if avg_jitter_ms.abs() < 40 { "AVERAGE" } else { "BAD" @@ -266,28 +268,26 @@ let (avg_ms, _avg_frames, status_str, lock_ratio, avg_delta) = { 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(); + MoveTo(2, 14), Print(format!("Lock Ratio : {:.1}% LOCK", + lock_ratio + )), + ).unwrap(); - // 10️⃣ Footer and logs + // 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 System 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 + // manual sync & quit if poll(Duration::from_millis(50)).unwrap() { if let Event::Key(evt) = read().unwrap() { match evt.code { @@ -297,44 +297,35 @@ 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 { + let today_local = Local::now().date_naive(); + let ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0) + .round() as u32; + let timecode = NaiveTime::from_hms_milli_opt( + frame.hours, frame.minutes, frame.seconds, ms, + ).expect("Invalid LTC timecode"); + let naive_dt = today_local.and_time(timecode); + let dt_local = Local + .from_local_datetime(&naive_dt) + .single() + .expect("Ambiguous or invalid local time"); + let ts = dt_local.format("%H:%M:%S.%3f").to_string(); + + let success = Command::new("sudo") + .arg("date") + .arg("-s") + .arg(&ts) + .status() + .map(|s| s.success()) + .unwrap_or(false); + + let entry = if success { + format!("✔ Synced exactly to LTC: {}", ts) + } else { + "❌ date cmd failed".into() + }; + if logs.len() == 10 { logs.pop_front(); } + logs.push_back(entry); } } _ => {}