Compare commits

...

7 commits

Author SHA1 Message Date
Chris Frankland-Wright
1842419f10 added assets for static 2025-08-07 19:15:22 +01:00
Chris Frankland-Wright
82fbefce0c fix: Remove NDF timecode scaling for pre-compensated LTC source
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-05 21:05:46 +01:00
Chris Frankland-Wright
e4c49a1e78 fix: Fix NDF LTC wall-clock time calculation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-05 20:59:19 +01:00
Chris Frankland-Wright
ed48c1284d fix: Forcefully terminate daemon process group
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-05 20:43:20 +01:00
Chris Frankland-Wright
43a3fc7aad feat: Add kill subcommand to stop daemon process
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-05 20:20:46 +01:00
Chris Frankland-Wright
a4bf025fd0 feat: Implement configurable auto-sync pausing 2025-08-05 20:20:40 +01:00
Chris Frankland-Wright
c9c6320abb feat: Set system time to 10am when setting date
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-05 20:00:35 +01:00
22 changed files with 68 additions and 30 deletions

View file

@ -36,6 +36,8 @@ struct Args {
enum Command {
/// Run as a background daemon providing a web UI.
Daemon,
/// Stop the running daemon process.
Kill,
}
/// Default config content, embedded in the binary.
@ -91,12 +93,16 @@ async fn main() {
let log_buffer = logger::setup_logger();
let args = Args::parse();
if let Some(Command::Daemon) = &args.command {
if let Some(command) = &args.command {
match command {
Command::Daemon => {
log::info!("🚀 Starting daemon...");
// Create files for stdout and stderr in the current directory
let stdout = fs::File::create("daemon.out").expect("Could not create daemon.out");
let stderr = fs::File::create("daemon.err").expect("Could not create daemon.err");
let stdout =
fs::File::create("daemon.out").expect("Could not create daemon.out");
let stderr =
fs::File::create("daemon.err").expect("Could not create daemon.err");
let daemonize = Daemonize::new()
.pid_file("ntp_timeturner.pid") // Create a PID file
@ -112,6 +118,43 @@ async fn main() {
}
}
}
Command::Kill => {
log::info!("🛑 Stopping daemon...");
let pid_file = "ntp_timeturner.pid";
match fs::read_to_string(pid_file) {
Ok(pid_str) => {
let pid_str = pid_str.trim();
log::info!("Found daemon with PID: {}", pid_str);
match std::process::Command::new("kill").arg("-9").arg(format!("-{}", pid_str)).status() {
Ok(status) => {
if status.success() {
log::info!("✅ Daemon stopped successfully.");
if fs::remove_file(pid_file).is_err() {
log::warn!("Could not remove PID file '{}'. It may need to be removed manually.", pid_file);
}
} else {
log::error!("'kill' command failed with status: {}. The daemon may not be running, or you may not have permission to stop it.", status);
log::warn!("Attempting to remove stale PID file '{}'...", pid_file);
if fs::remove_file(pid_file).is_ok() {
log::info!("Removed stale PID file.");
} else {
log::warn!("Could not remove PID file.");
}
}
}
Err(e) => {
log::error!("Failed to execute 'kill' command. Is 'kill' in your PATH? Error: {}", e);
}
}
}
Err(_) => {
log::error!("Could not read PID file '{}'. Is the daemon running in this directory?", pid_file);
}
}
return;
}
}
}
// 🔄 Ensure there's always a config.yml present
ensure_config();
@ -205,7 +248,9 @@ async fn main() {
let state = sync_state.lock().unwrap();
let config = sync_config.lock().unwrap();
if config.auto_sync_enabled && state.latest.is_some() {
if config.is_auto_sync_paused() {
log::info!("Auto-sync is temporarily paused.");
} else if config.auto_sync_enabled && state.latest.is_some() {
let delta = state.get_ewma_clock_delta();
let frame = state.latest.as_ref().unwrap();

View file

@ -45,21 +45,13 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime<Loca
let timecode_secs =
frame.hours as i64 * 3600 + frame.minutes as i64 * 60 + frame.seconds as i64;
// Total duration in seconds as a rational number, including frames
// Timecode is always treated as wall-clock time. NDF scaling is not applied
// as the LTC source appears to be pre-compensated.
let total_duration_secs =
Ratio::new(timecode_secs, 1) + Ratio::new(frame.frames as i64, 1) / frame.frame_rate;
// For non-drop-frame fractional rates (23.98, 29.97), timecode runs slower than wall clock.
// We need to scale the timecode duration up to get wall clock time.
// The scaling factor is 1001/1000. For drop-frame, this isn't necessary.
let scaled_duration_secs = if *frame.frame_rate.denom() == 1001 && !frame.is_drop_frame {
total_duration_secs * Ratio::new(1001, 1000)
} else {
total_duration_secs
};
// Convert to milliseconds
let total_ms = (scaled_duration_secs * Ratio::new(1000, 1))
let total_ms = (total_duration_secs * Ratio::new(1000, 1))
.round()
.to_integer();
@ -157,19 +149,20 @@ pub fn nudge_clock(microseconds: i64) -> Result<(), ()> {
pub fn set_date(date: &str) -> Result<(), ()> {
#[cfg(target_os = "linux")]
{
let datetime_str = format!("{} 10:00:00", date);
let success = Command::new("sudo")
.arg("date")
.arg("--set")
.arg(date)
.arg(&datetime_str)
.status()
.map(|s| s.success())
.unwrap_or(false);
if success {
log::info!("Set system date to {}", date);
log::info!("Set system date and time to {}", datetime_str);
Ok(())
} else {
log::error!("Failed to set system date");
log::error!("Failed to set system date and time");
Err(())
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB