From 8eb4c6f2da89967488e42d4b97cb39c198d5af66 Mon Sep 17 00:00:00 2001 From: Chaos Rogers Date: Sun, 26 Oct 2025 12:56:03 +0000 Subject: [PATCH] feat: add PTP daemon and start/stop API; rename API server Co-authored-by: aider (openai/gpt-5) --- src/api.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/src/api.rs b/src/api.rs index 522da01..6aaf1a3 100644 --- a/src/api.rs +++ b/src/api.rs @@ -45,11 +45,12 @@ struct ApiStatus { ptp_offset_ns: Option, } -// AppState to hold shared data + // AppState to hold shared data pub struct AppState { pub ltc_state: Arc>, pub config: Arc>, pub log_buffer: Arc>>, + pub ptp_daemon: Arc>>, } #[get("/api/status")] @@ -115,14 +116,19 @@ async fn get_status(data: web::Data) -> impl Responder { let mut ptp_offset_ns: Option = None; if ptp_supported { - // Check a few common PHC device nodes + // Check managed daemon handle first (if any) + if let Ok(mut guard) = data.ptp_daemon.lock() { + if let Some(ref mut d) = *guard { + ptp_daemon_running = d.is_running(); + } + } + + // Probe PHC devices to read current offset for dev in ["/dev/ptp0", "/dev/ptp1", "/dev/ptp2"] { if std::path::Path::new(dev).exists() { if let Ok(clock) = ptp::PtpClock::open(dev) { if let Ok(offset) = clock.offset_from_system_ns() { ptp_offset_ns = Some(offset); - // Consider daemon "running" if we can read a PHC offset - ptp_daemon_running = true; break; } } @@ -209,6 +215,95 @@ async fn set_date(req: web::Json) -> impl Responder { } } +// PTP control endpoints +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct StartPtpRequest { + interface: Option, + phc_index: Option, + args: Option>, +} + +#[post("/api/ptp/start")] +async fn start_ptp( + data: web::Data, + req: web::Json, +) -> impl Responder { + if !ptp::is_supported() { + return HttpResponse::Ok().json(serde_json::json!({ + "status": "error", + "message": "PTP is unsupported on this platform" + })); + } + + let mut guard = data.ptp_daemon.lock().unwrap(); + // Stop existing daemon if running + if let Some(ref mut d) = *guard { + let _ = d.stop(); + *guard = None; + } + + let iface = req + .interface + .clone() + .or_else(|| std::env::var("PTP_INTERFACE").ok()) + .unwrap_or_else(|| "lo".to_string()); + + let mut opts = ptp::StatimeOptions::new(iface); + if let Some(idx) = req.phc_index { + opts = opts.with_phc_index(idx); + } + if let Some(ref extra) = req.args { + opts = opts.with_args(extra.clone()); + } + + match ptp::spawn_statime(opts) { + Ok(d) => { + *guard = Some(d); + HttpResponse::Ok().json(serde_json::json!({ + "status": "success", + "message": "PTP daemon started" + })) + } + Err(e) => HttpResponse::Ok().json(serde_json::json!({ + "status": "error", + "message": e.to_string() + })), + } +} + +#[post("/api/ptp/stop")] +async fn stop_ptp(data: web::Data) -> impl Responder { + if !ptp::is_supported() { + return HttpResponse::Ok().json(serde_json::json!({ + "status": "error", + "message": "PTP is unsupported on this platform" + })); + } + + let mut guard = data.ptp_daemon.lock().unwrap(); + if let Some(ref mut d) = *guard { + match d.stop() { + Ok(_) => { + *guard = None; + HttpResponse::Ok().json(serde_json::json!({ + "status": "success", + "message": "PTP daemon stopped" + })) + } + Err(e) => HttpResponse::Ok().json(serde_json::json!({ + "status": "error", + "message": e.to_string() + })), + } + } else { + HttpResponse::Ok().json(serde_json::json!({ + "status": "success", + "message": "PTP daemon was not running" + })) + } +} + #[post("/api/config")] async fn update_config( data: web::Data, @@ -253,9 +348,10 @@ pub async fn start_api_server( ltc_state: state, config: config, log_buffer: log_buffer, + ptp_daemon: Arc::new(Mutex::new(None)), }); - log::info!("🚀 Starting API server at http://0.0.0.0:8080"); + log::info!("🚀 Starting Hachi-TimeTransformer API server at http://0.0.0.0:8080"); HttpServer::new(move || { App::new() @@ -267,6 +363,8 @@ pub async fn start_api_server( .service(get_logs) .service(nudge_clock) .service(set_date) + .service(start_ptp) + .service(stop_ptp) // Serve frontend static files .service(fs::Files::new("/", "static/").index_file("index.html")) }) @@ -322,6 +420,7 @@ mod tests { ltc_state, config, log_buffer, + ptp_daemon: Arc::new(Mutex::new(None)), }) }