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

@ -45,11 +45,12 @@ struct ApiStatus {
ptp_offset_ns: Option<i128>,
}
// AppState to hold shared data
// AppState to hold shared data
pub struct AppState {
pub ltc_state: Arc<Mutex<LtcState>>,
pub config: Arc<Mutex<Config>>,
pub log_buffer: Arc<Mutex<VecDeque<String>>>,
pub ptp_daemon: Arc<Mutex<Option<ptp::StatimeDaemon>>>,
}
#[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;
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<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")]
async fn update_config(
data: web::Data<AppState>,
@ -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)),
})
}