Merge pull request #21 from cjfranko/add_date
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Failing after 16s

feat: Add system date display and setting via API
This commit is contained in:
Chris Frankland-Wright 2025-07-30 22:41:49 +01:00 committed by GitHub
commit 6bc1f5ddbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 0 deletions

View file

@ -17,6 +17,7 @@ This document describes the HTTP API for the NTP Timeturner application.
"ltc_timecode": "10:20:30:00", "ltc_timecode": "10:20:30:00",
"frame_rate": "25.00fps", "frame_rate": "25.00fps",
"system_clock": "10:20:30.005", "system_clock": "10:20:30.005",
"system_date": "2025-07-30",
"timecode_delta_ms": 5, "timecode_delta_ms": 5,
"timecode_delta_frames": 0, "timecode_delta_frames": 0,
"sync_status": "IN SYNC", "sync_status": "IN SYNC",
@ -58,6 +59,33 @@ This document describes the HTTP API for the NTP Timeturner application.
} }
``` ```
- **`POST /api/set_date`**
Sets the system date. This is useful as LTC does not contain date information. Requires `sudo` privileges.
**Example Request:**
```json
{
"date": "2025-07-30"
}
```
**Success Response:**
```json
{
"status": "success",
"message": "Date update command issued."
}
```
**Error Response:**
```json
{
"status": "error",
"message": "Date update command failed."
}
```
### Configuration ### Configuration
- **`GET /api/config`** - **`GET /api/config`**

View file

@ -19,6 +19,7 @@ struct ApiStatus {
ltc_timecode: String, ltc_timecode: String,
frame_rate: String, frame_rate: String,
system_clock: String, system_clock: String,
system_date: String,
timecode_delta_ms: i64, timecode_delta_ms: i64,
timecode_delta_frames: i64, timecode_delta_frames: i64,
sync_status: String, sync_status: String,
@ -58,6 +59,7 @@ async fn get_status(data: web::Data<AppState>) -> impl Responder {
now_local.second(), now_local.second(),
now_local.timestamp_subsec_millis(), now_local.timestamp_subsec_millis(),
); );
let system_date = now_local.format("%Y-%m-%d").to_string();
let avg_delta = state.get_ewma_clock_delta(); let avg_delta = state.get_ewma_clock_delta();
let mut delta_frames = 0; let mut delta_frames = 0;
@ -83,6 +85,7 @@ async fn get_status(data: web::Data<AppState>) -> impl Responder {
ltc_timecode, ltc_timecode,
frame_rate, frame_rate,
system_clock, system_clock,
system_date,
timecode_delta_ms: avg_delta, timecode_delta_ms: avg_delta,
timecode_delta_frames: delta_frames, timecode_delta_frames: delta_frames,
sync_status: sync_status.to_string(), sync_status: sync_status.to_string(),
@ -135,6 +138,22 @@ async fn nudge_clock(req: web::Json<NudgeRequest>) -> impl Responder {
} }
} }
#[derive(Deserialize)]
struct SetDateRequest {
date: String,
}
#[post("/api/set_date")]
async fn set_date(req: web::Json<SetDateRequest>) -> impl Responder {
if system::set_date(&req.date).is_ok() {
HttpResponse::Ok()
.json(serde_json::json!({ "status": "success", "message": "Date update command issued." }))
} else {
HttpResponse::InternalServerError()
.json(serde_json::json!({ "status": "error", "message": "Date update command failed." }))
}
}
#[post("/api/config")] #[post("/api/config")]
async fn update_config( async fn update_config(
data: web::Data<AppState>, data: web::Data<AppState>,
@ -192,6 +211,7 @@ pub async fn start_api_server(
.service(update_config) .service(update_config)
.service(get_logs) .service(get_logs)
.service(nudge_clock) .service(nudge_clock)
.service(set_date)
// 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"))
}) })

View file

@ -131,6 +131,33 @@ pub fn nudge_clock(microseconds: i64) -> Result<(), ()> {
} }
} }
pub fn set_date(date: &str) -> Result<(), ()> {
#[cfg(target_os = "linux")]
{
let success = Command::new("sudo")
.arg("date")
.arg("--set")
.arg(date)
.status()
.map(|s| s.success())
.unwrap_or(false);
if success {
log::info!("Set system date to {}", date);
Ok(())
} else {
log::error!("Failed to set system date");
Err(())
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = date;
log::warn!("Date setting is only supported on Linux.");
Err(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -23,6 +23,7 @@
<div class="card"> <div class="card">
<h2>System Clock</h2> <h2>System Clock</h2>
<p id="system-clock">--:--:--.---</p> <p id="system-clock">--:--:--.---</p>
<p>Date: <span id="system-date">---- -- --</span></p>
<p>NTP Service: <span id="ntp-active">--</span></p> <p>NTP Service: <span id="ntp-active">--</span></p>
<p>Sync Status: <span id="sync-status">--</span></p> <p>Sync Status: <span id="sync-status">--</span></p>
</div> </div>
@ -90,6 +91,12 @@
<button id="nudge-up">+</button> <button id="nudge-up">+</button>
<span id="nudge-message"></span> <span id="nudge-message"></span>
</div> </div>
<div class="control-group">
<label for="date-input">Set System Date:</label>
<input type="date" id="date-input">
<button id="set-date">Set Date</button>
<span id="date-message"></span>
</div>
</div> </div>
<!-- Logs --> <!-- Logs -->

View file

@ -8,6 +8,7 @@ document.addEventListener('DOMContentLoaded', () => {
frameRate: document.getElementById('frame-rate'), frameRate: document.getElementById('frame-rate'),
lockRatio: document.getElementById('lock-ratio'), lockRatio: document.getElementById('lock-ratio'),
systemClock: document.getElementById('system-clock'), systemClock: document.getElementById('system-clock'),
systemDate: document.getElementById('system-date'),
ntpActive: document.getElementById('ntp-active'), ntpActive: document.getElementById('ntp-active'),
syncStatus: document.getElementById('sync-status'), syncStatus: document.getElementById('sync-status'),
deltaMs: document.getElementById('delta-ms'), deltaMs: document.getElementById('delta-ms'),
@ -35,12 +36,17 @@ document.addEventListener('DOMContentLoaded', () => {
const nudgeValueInput = document.getElementById('nudge-value'); const nudgeValueInput = document.getElementById('nudge-value');
const nudgeMessage = document.getElementById('nudge-message'); const nudgeMessage = document.getElementById('nudge-message');
const dateInput = document.getElementById('date-input');
const setDateButton = document.getElementById('set-date');
const dateMessage = document.getElementById('date-message');
function updateStatus(data) { function updateStatus(data) {
statusElements.ltcStatus.textContent = data.ltc_status; statusElements.ltcStatus.textContent = data.ltc_status;
statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.ltcTimecode.textContent = data.ltc_timecode;
statusElements.frameRate.textContent = data.frame_rate; statusElements.frameRate.textContent = data.frame_rate;
statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2);
statusElements.systemClock.textContent = data.system_clock; statusElements.systemClock.textContent = data.system_clock;
statusElements.systemDate.textContent = data.system_date;
statusElements.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive'; statusElements.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive';
statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive'; statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive';
@ -238,6 +244,35 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => { nudgeMessage.textContent = ''; }, 3000); setTimeout(() => { nudgeMessage.textContent = ''; }, 3000);
} }
async function setDate() {
const date = dateInput.value;
if (!date) {
alert('Please select a date.');
return;
}
dateMessage.textContent = 'Setting date...';
try {
const response = await fetch('/api/set_date', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date: date }),
});
const data = await response.json();
if (response.ok) {
dateMessage.textContent = `Success: ${data.message}`;
// Fetch status again to update the displayed date immediately
fetchStatus();
} else {
dateMessage.textContent = `Error: ${data.message}`;
}
} catch (error) {
console.error('Error setting date:', error);
dateMessage.textContent = 'Failed to send date command.';
}
setTimeout(() => { dateMessage.textContent = ''; }, 5000);
}
saveConfigButton.addEventListener('click', saveConfig); saveConfigButton.addEventListener('click', saveConfig);
manualSyncButton.addEventListener('click', triggerManualSync); manualSyncButton.addEventListener('click', triggerManualSync);
nudgeDownButton.addEventListener('click', () => { nudgeDownButton.addEventListener('click', () => {
@ -248,6 +283,7 @@ document.addEventListener('DOMContentLoaded', () => {
const ms = parseInt(nudgeValueInput.value, 10) || 0; const ms = parseInt(nudgeValueInput.value, 10) || 0;
nudgeClock(ms); nudgeClock(ms);
}); });
setDateButton.addEventListener('click', setDate);
// Initial data load // Initial data load
fetchStatus(); fetchStatus();