Compare commits
7 commits
65dd107514
...
1842419f10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1842419f10 | ||
|
|
82fbefce0c | ||
|
|
e4c49a1e78 | ||
|
|
ed48c1284d | ||
|
|
43a3fc7aad | ||
|
|
a4bf025fd0 | ||
|
|
c9c6320abb |
77
src/main.rs
|
|
@ -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,24 +93,65 @@ async fn main() {
|
|||
let log_buffer = logger::setup_logger();
|
||||
let args = Args::parse();
|
||||
|
||||
if let Some(Command::Daemon) = &args.command {
|
||||
log::info!("🚀 Starting daemon...");
|
||||
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");
|
||||
// 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 daemonize = Daemonize::new()
|
||||
.pid_file("ntp_timeturner.pid") // Create a PID file
|
||||
.working_directory(".") // Keep the same working directory
|
||||
.stdout(stdout)
|
||||
.stderr(stderr);
|
||||
let daemonize = Daemonize::new()
|
||||
.pid_file("ntp_timeturner.pid") // Create a PID file
|
||||
.working_directory(".") // Keep the same working directory
|
||||
.stdout(stdout)
|
||||
.stderr(stderr);
|
||||
|
||||
match daemonize.start() {
|
||||
Ok(_) => { /* Process is now daemonized */ }
|
||||
Err(e) => {
|
||||
log::error!("Error daemonizing: {}", e);
|
||||
return; // Exit if daemonization fails
|
||||
match daemonize.start() {
|
||||
Ok(_) => { /* Process is now daemonized */ }
|
||||
Err(e) => {
|
||||
log::error!("Error daemonizing: {}", e);
|
||||
return; // Exit if daemonization fails
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
static/assets/FuturaStdHeavy.otf
Normal file
BIN
static/assets/quartz-ms-regular.ttf
Normal file
BIN
static/assets/timeturner_controls.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/assets/timeturner_delta_green.png
Normal file
|
After Width: | Height: | Size: 981 B |
BIN
static/assets/timeturner_delta_orange.png
Normal file
|
After Width: | Height: | Size: 955 B |
BIN
static/assets/timeturner_delta_red.png
Normal file
|
After Width: | Height: | Size: 913 B |
BIN
static/assets/timeturner_jitter_green.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/assets/timeturner_jitter_orange.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/assets/timeturner_jitter_red.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/assets/timeturner_logs.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/assets/timeturner_ltc_green.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/assets/timeturner_ltc_orange.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/assets/timeturner_ltc_red.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/assets/timeturner_network.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/assets/timeturner_ntp_green.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/assets/timeturner_ntp_orange.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/assets/timeturner_ntp_red.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/assets/timeturner_sync_green.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/assets/timeturner_sync_orange.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/assets/timeturner_sync_red.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |