feat: add PTP daemon and start/stop API; rename API server

Co-authored-by: aider (openai/gpt-5) <aider@aider.chat>
This commit is contained in:
Chaos Rogers 2025-10-26 12:56:03 +00:00
parent 252ea15486
commit 8eb4c6f2da

View file

@ -50,6 +50,7 @@ pub struct AppState {
pub ltc_state: Arc<Mutex<LtcState>>, pub ltc_state: Arc<Mutex<LtcState>>,
pub config: Arc<Mutex<Config>>, pub config: Arc<Mutex<Config>>,
pub log_buffer: Arc<Mutex<VecDeque<String>>>, pub log_buffer: Arc<Mutex<VecDeque<String>>>,
pub ptp_daemon: Arc<Mutex<Option<ptp::StatimeDaemon>>>,
} }
#[get("/api/status")] #[get("/api/status")]
@ -115,14 +116,19 @@ async fn get_status(data: web::Data<AppState>) -> impl Responder {
let mut ptp_offset_ns: Option<i128> = None; let mut ptp_offset_ns: Option<i128> = None;
if ptp_supported { 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"] { for dev in ["/dev/ptp0", "/dev/ptp1", "/dev/ptp2"] {
if std::path::Path::new(dev).exists() { if std::path::Path::new(dev).exists() {
if let Ok(clock) = ptp::PtpClock::open(dev) { if let Ok(clock) = ptp::PtpClock::open(dev) {
if let Ok(offset) = clock.offset_from_system_ns() { if let Ok(offset) = clock.offset_from_system_ns() {
ptp_offset_ns = Some(offset); ptp_offset_ns = Some(offset);
// Consider daemon "running" if we can read a PHC offset
ptp_daemon_running = true;
break; break;
} }
} }
@ -209,6 +215,95 @@ async fn set_date(req: web::Json<SetDateRequest>) -> impl Responder {
} }
} }
// PTP control endpoints
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct StartPtpRequest {
interface: Option<String>,
phc_index: Option<u32>,
args: Option<Vec<String>>,
}
#[post("/api/ptp/start")]
async fn start_ptp(
data: web::Data<AppState>,
req: web::Json<StartPtpRequest>,
) -> 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<AppState>) -> 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")] #[post("/api/config")]
async fn update_config( async fn update_config(
data: web::Data<AppState>, data: web::Data<AppState>,
@ -253,9 +348,10 @@ pub async fn start_api_server(
ltc_state: state, ltc_state: state,
config: config, config: config,
log_buffer: log_buffer, 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 || { HttpServer::new(move || {
App::new() App::new()
@ -267,6 +363,8 @@ pub async fn start_api_server(
.service(get_logs) .service(get_logs)
.service(nudge_clock) .service(nudge_clock)
.service(set_date) .service(set_date)
.service(start_ptp)
.service(stop_ptp)
// Serve frontend static files // Serve frontend static files
.service(fs::Files::new("/", "static/").index_file("index.html")) .service(fs::Files::new("/", "static/").index_file("index.html"))
}) })
@ -322,6 +420,7 @@ mod tests {
ltc_state, ltc_state,
config, config,
log_buffer, log_buffer,
ptp_daemon: Arc::new(Mutex::new(None)),
}) })
} }