fix: Handle drop-frame timecode separator in API and UI

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
Chris Frankland-Wright 2025-08-03 15:44:35 +01:00
parent 22dc01e80f
commit 3ffb54e9aa
3 changed files with 38 additions and 6 deletions

View file

@ -8,13 +8,13 @@ This document describes the HTTP API for the NTP Timeturner application.
- **`GET /api/status`**
Retrieves the real-time status of the LTC reader and system clock synchronization.
Retrieves the real-time status of the LTC reader and system clock synchronization. The `ltc_timecode` field uses `:` as a separator for non-drop-frame timecode, and `;` for drop-frame timecode between seconds and frames (e.g., `10:20:30;00`).
**Example Response:**
```json
{
"ltc_status": "LOCK",
"ltc_timecode": "10:20:30:00",
"ltc_timecode": "10:20:30;00",
"frame_rate": "25.00fps",
"system_clock": "10:20:30.005",
"system_date": "2025-07-30",

View file

@ -47,7 +47,11 @@ async fn get_status(data: web::Data<AppState>) -> impl Responder {
let ltc_status = state.latest.as_ref().map_or("(waiting)".to_string(), |f| f.status.clone());
let ltc_timecode = state.latest.as_ref().map_or("".to_string(), |f| {
format!("{:02}:{:02}:{:02}:{:02}", f.hours, f.minutes, f.seconds, f.frames)
let sep = if f.is_drop_frame { ';' } else { ':' };
format!(
"{:02}:{:02}:{:02}{}{:02}",
f.hours, f.minutes, f.seconds, sep, f.frames
)
});
let frame_rate = state.latest.as_ref().map_or("".to_string(), |f| {
format!("{:.2}fps", f.frame_rate.to_f64().unwrap_or(0.0))
@ -291,6 +295,32 @@ mod tests {
assert_eq!(resp.hardware_offset_ms, 10);
}
#[actix_web::test]
async fn test_get_status_drop_frame() {
let app_state = get_test_app_state();
// Set state to drop frame
app_state
.ltc_state
.lock()
.unwrap()
.latest
.as_mut()
.unwrap()
.is_drop_frame = true;
let app = test::init_service(
App::new()
.app_data(app_state.clone())
.service(get_status),
)
.await;
let req = test::TestRequest::get().uri("/api/status").to_request();
let resp: ApiStatus = test::call_and_read_body_json(&app, req).await;
assert_eq!(resp.ltc_timecode, "01:02:03;04");
}
#[actix_web::test]
async fn test_get_config() {
let app_state = get_test_app_state();

View file

@ -98,9 +98,11 @@ document.addEventListener('DOMContentLoaded', () => {
}
// Animate LTC Timecode - only if status is LOCK
if (lastApiData.ltc_status === 'LOCK' && lastApiData.ltc_timecode && lastApiData.ltc_timecode.includes(':') && lastApiData.frame_rate) {
const tcParts = lastApiData.ltc_timecode.split(':');
if (lastApiData.ltc_status === 'LOCK' && lastApiData.ltc_timecode && lastApiData.ltc_timecode.match(/[:;]/) && lastApiData.frame_rate) {
const separator = lastApiData.ltc_timecode.includes(';') ? ';' : ':';
const tcParts = lastApiData.ltc_timecode.split(/[:;]/);
const frameRate = parseFloat(lastApiData.frame_rate);
if (tcParts.length === 4 && !isNaN(frameRate) && frameRate > 0) {
let h = parseInt(tcParts[0], 10);
let m = parseInt(tcParts[1], 10);
@ -126,7 +128,7 @@ document.addEventListener('DOMContentLoaded', () => {
h %= 24;
statusElements.ltcTimecode.textContent =
`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`;
`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${separator}${String(f).padStart(2, '0')}`;
}
}
}