From d8eb1f9824169647f8209cc1186f571036e1a39c Mon Sep 17 00:00:00 2001 From: Chaos Rogers Date: Wed, 22 Oct 2025 12:46:27 +0100 Subject: [PATCH] feat: add development mode flag HACI_DEV and PTP status fields in API/UI Co-authored-by: aider (openai/gpt-5) --- Dockerfile | 3 ++- docker-compose.yml | 7 +++++++ scripts/entrypoint.sh | 16 +++++++++++++++ src/api.rs | 48 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 16 +++++++++++++++ static/script.js | 26 +++++++++++++++++++++++ 6 files changed, 115 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 45abb50..9ae8f5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN rm -rf src # Copy full source and build project COPY . . -RUN cargo build --release +RUN cargo build --release && cargo install --locked statime # (Statime installation removed; container runs only haci) @@ -37,6 +37,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app COPY --from=builder /app/target/release/get-haci /usr/local/bin/get-haci +COPY --from=builder /usr/local/cargo/bin/statime /usr/local/bin/statime COPY static ./static COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh COPY scripts/ltc_gen.sh /usr/local/bin/ltc-gen.sh diff --git a/docker-compose.yml b/docker-compose.yml index 96071e4..68bfdb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,15 @@ services: - RUST_LOG=info - MOCK_TEENSY=1 - HACI_SERIAL_PORT=/dev/ttyACM0 + - HACI_DEV=1 + - RUN_STATIME=1 + - PTP_INTERFACE=eth0 ports: - "8080:8080" volumes: - ./static:/app/static:ro + cap_add: + - NET_ADMIN + - SYS_NICE + - SYS_TIME restart: unless-stopped diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index acdd5e7..ba6fdeb 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,5 +1,7 @@ #!/bin/sh set -eu +# Mark container as development unless explicitly disabled +export HACI_DEV="${HACI_DEV:-1}" # If enabled, start a mock Teensy that exposes a PTY at /dev/ttyACM0 and streams LTC-like lines. if [ "${MOCK_TEENSY:-0}" = "1" ]; then @@ -26,5 +28,19 @@ if [ "${MOCK_TEENSY:-0}" = "1" ]; then fi fi +# Optionally start the PTP daemon (statime) for development +if [ "${RUN_STATIME:-0}" = "1" ]; then + echo "[entrypoint] Starting statime PTP daemon..." >&2 + if command -v statime >/dev/null 2>&1; then + IFACE="${PTP_INTERFACE:-eth0}" + echo "[entrypoint] statime interface: ${IFACE}" >&2 + # Run statime in background; logs to container stderr/stdout + statime -i "${IFACE}" & + STATIME_PID=$! + else + echo "[entrypoint] statime not found on PATH" >&2 + fi +fi + # Run the requested command (default: get-haci) exec "$@" diff --git a/src/api.rs b/src/api.rs index 14b0da4..53f2034 100644 --- a/src/api.rs +++ b/src/api.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex}; use crate::config::{self, Config}; use crate::sync_logic::{self, LtcState}; use crate::system; +use crate::ptp; use num_rational::Ratio; use num_traits::ToPrimitive; @@ -30,6 +31,18 @@ struct ApiStatus { ntp_active: bool, interfaces: Vec, hardware_offset_ms: i64, + + // Development/PTP fields (optional; defaulted for backward compatibility) + #[serde(default)] + dev_mode: bool, + #[serde(default)] + ptp_supported: bool, + #[serde(default)] + ptp_daemon_running: bool, + #[serde(default)] + ptp_interface: Option, + #[serde(default)] + ptp_offset_ns: Option, } // AppState to hold shared data @@ -87,6 +100,36 @@ async fn get_status(data: web::Data) -> impl Responder { .map(|ifa| ifa.ip().to_string()) .collect(); + // Development mode flag + let dev_mode = std::env::var("HACI_DEV") + .map(|v| { + let v = v.to_lowercase(); + v == "1" || v == "true" || v == "yes" + }) + .unwrap_or(false); + + // PTP status (best-effort; safe on non-Linux) + let ptp_supported = ptp::is_supported(); + let ptp_interface = std::env::var("PTP_INTERFACE").ok(); + let mut ptp_daemon_running = false; + let mut ptp_offset_ns: Option = None; + + if ptp_supported { + // Check a few common PHC device nodes + 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; + } + } + } + } + } + HttpResponse::Ok().json(ApiStatus { ltc_status, ltc_timecode, @@ -101,6 +144,11 @@ async fn get_status(data: web::Data) -> impl Responder { ntp_active, interfaces, hardware_offset_ms: hw_offset_ms, + dev_mode, + ptp_supported, + ptp_daemon_running, + ptp_interface, + ptp_offset_ns, }) } diff --git a/src/main.rs b/src/main.rs index 963dc85..d07de9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,9 @@ use tokio::task::{self, LocalSet}; struct Args { #[command(subcommand)] command: Option, + + #[arg(long, help = "Enable development mode (also respected via HACI_DEV=1)")] + dev: bool, } #[derive(clap::Subcommand, Debug)] @@ -116,6 +119,19 @@ async fn main() { let log_buffer = logger::setup_logger(); let args = Args::parse(); + // Determine development mode early and announce + let dev_mode = args.dev + || std::env::var("HACI_DEV") + .map(|v| { + let v = v.to_lowercase(); + v == "1" || v == "true" || v == "yes" + }) + .unwrap_or(false); + if dev_mode { + std::env::set_var("HACI_DEV", "1"); + log::info!("🧪 Development mode enabled"); + } + if let Some(command) = &args.command { match command { Command::Daemon => { diff --git a/static/script.js b/static/script.js index 634ed33..2bbcabf 100644 --- a/static/script.js +++ b/static/script.js @@ -21,6 +21,11 @@ deltaText: document.getElementById('delta-text'), interfaces: document.getElementById('interfaces'), logs: document.getElementById('logs'), + ptpSupported: document.getElementById('ptp-supported'), + ptpDaemon: document.getElementById('ptp-daemon'), + ptpOffset: document.getElementById('ptp-offset'), + ptpInterface: document.getElementById('ptp-interface'), + devMode: document.getElementById('dev-mode'), }; const hwOffsetInput = document.getElementById('hw-offset'); @@ -150,6 +155,27 @@ } else { statusElements.interfaces.textContent = 'No active interfaces found.'; } + + // Optional: Development/PTP Status (only if elements exist in DOM) + if (statusElements.devMode) { + statusElements.devMode.textContent = data.dev_mode ? 'DEV' : ''; + } + if (statusElements.ptpSupported) { + statusElements.ptpSupported.textContent = data.ptp_supported ? 'PTP supported' : 'PTP not supported'; + } + if (statusElements.ptpDaemon) { + statusElements.ptpDaemon.textContent = data.ptp_daemon_running ? 'PTP daemon: running' : 'PTP daemon: not running'; + } + if (statusElements.ptpInterface) { + statusElements.ptpInterface.textContent = data.ptp_interface || ''; + } + if (statusElements.ptpOffset) { + if (data.ptp_offset_ns !== null && data.ptp_offset_ns !== undefined) { + statusElements.ptpOffset.textContent = `${data.ptp_offset_ns} ns`; + } else { + statusElements.ptpOffset.textContent = '—'; + } + } } function animateClocks() {