feat: add development mode flag HACI_DEV and PTP status fields in API/UI

Co-authored-by: aider (openai/gpt-5) <aider@aider.chat>
This commit is contained in:
Chaos Rogers 2025-10-22 12:46:27 +01:00
parent ee4a5a3630
commit d8eb1f9824
6 changed files with 115 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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 "$@"

View file

@ -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<String>,
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<String>,
#[serde(default)]
ptp_offset_ns: Option<i128>,
}
// AppState to hold shared data
@ -87,6 +100,36 @@ async fn get_status(data: web::Data<AppState>) -> 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<i128> = 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<AppState>) -> impl Responder {
ntp_active,
interfaces,
hardware_offset_ms: hw_offset_ms,
dev_mode,
ptp_supported,
ptp_daemon_running,
ptp_interface,
ptp_offset_ns,
})
}

View file

@ -31,6 +31,9 @@ use tokio::task::{self, LocalSet};
struct Args {
#[command(subcommand)]
command: Option<Command>,
#[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 => {

View file

@ -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() {