NTP-Timeturner/src/ui.rs
Chris Frankland-Wright a1da396874 refactor: Use rational numbers for accurate frame rate calculations
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-02 12:26:17 +01:00

234 lines
7.9 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::{
io::{stdout, Write},
process::{self},
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
};
use std::collections::VecDeque;
use chrono::{
DateTime, Local, Timelike, Utc,
};
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{poll, read, Event, KeyCode},
execute, queue,
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use crate::config::Config;
use crate::sync_logic::{get_jitter_status, get_sync_status, LtcState};
use crate::system;
use get_if_addrs::get_if_addrs;
use num_rational::Ratio;
use num_traits::ToPrimitive;
pub fn start_ui(
state: Arc<Mutex<LtcState>>,
serial_port: String,
config: Arc<Mutex<Config>>,
) {
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, Hide).unwrap();
terminal::enable_raw_mode().unwrap();
let mut logs: VecDeque<String> = VecDeque::with_capacity(10);
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⃣ config
let cfg = config.lock().unwrap().clone();
let hw_offset_ms = cfg.hardware_offset_ms;
// 2⃣ Chrony + interfaces
let ntp_active = system::ntp_service_active();
let interfaces: Vec<String> = get_if_addrs()
.unwrap_or_default()
.into_iter()
.filter(|ifa| !ifa.is_loopback())
.map(|ifa| ifa.ip().to_string())
.collect();
// 3⃣ jitter
{
let mut st = state.lock().unwrap();
if let Some(frame) = st.latest.clone() {
if frame.status == "LOCK" {
// jitter
let now_utc = Utc::now();
let raw = (now_utc - frame.timestamp).num_milliseconds();
let measured = raw - hw_offset_ms;
st.record_offset(measured);
}
}
}
// 4⃣ averages & status override
let (avg_jitter_ms, _avg_frames, _, lock_ratio, avg_delta) = {
let st = state.lock().unwrap();
(
st.average_jitter(),
st.average_frames(),
st.timecode_match().to_string(),
st.lock_ratio(),
st.get_ewma_clock_delta(),
)
};
// 5⃣ cache Δ once/sec & Δ in frames
if last_delta_update.elapsed() >= Duration::from_secs(1) {
cached_delta_ms = avg_delta;
if let Some(frame) = &state.lock().unwrap().latest {
let delta_ms_ratio = Ratio::new(avg_delta, 1);
let frames_ratio = delta_ms_ratio * frame.frame_rate / Ratio::new(1000, 1);
cached_delta_frames = frames_ratio.round().to_integer();
} else {
cached_delta_frames = 0;
}
last_delta_update = Instant::now();
}
// 6⃣ sync status wording
let sync_status = get_sync_status(cached_delta_ms, &cfg);
// 7⃣ 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.to_f64().unwrap_or(0.0)),
None => "Frame Rate : …".to_string(),
};
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();
}
// system clock
let now_local: DateTime<Local> = 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 {
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();
// sync status
let scol = if sync_status == "IN SYNC" {
Color::Green
} else if sync_status == "TIMETURNING" {
Color::Cyan
} else {
Color::Red
};
queue!(
stdout,
MoveTo(2, 12), SetForegroundColor(scol),
Print(format!("Sync Status : {}", sync_status)),
ResetColor,
).unwrap();
// jitter & lock ratio
let jstatus = get_jitter_status(avg_jitter_ms);
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();
queue!(
stdout,
MoveTo(2, 14), Print(format!("Lock Ratio : {:.1}% LOCK",
lock_ratio
)),
).unwrap();
// footer + logs
queue!(
stdout,
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();
// manual sync & quit
if poll(Duration::from_millis(50)).unwrap() {
if let Event::Key(evt) = read().unwrap() {
match evt.code {
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'q') => {
execute!(stdout, Show, LeaveAlternateScreen).unwrap();
terminal::disable_raw_mode().unwrap();
process::exit(0);
}
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'s') => {
if let Some(frame) = &state.lock().unwrap().latest {
let entry = match system::trigger_sync(frame, &cfg) {
Ok(ts) => format!("✔ Synced exactly to LTC: {}", ts),
Err(_) => "❌ date cmd failed".into(),
};
if logs.len() == 10 { logs.pop_front(); }
logs.push_back(entry);
}
}
_ => {}
}
}
}
thread::sleep(Duration::from_millis(25));
}
}