From 871fd192b0638d55b63b470eeae760e3be76766c Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 21:58:45 +0100 Subject: [PATCH 1/4] docs: Correct README for time offset features Co-authored-by: aider (gemini/gemini-2.5-pro) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7ed822..3e438eb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Created by Chris Frankland-Wright and John Rogers - Reads SMPTE LTC from Audio Interface (3.5mm TRS but adaptable to BNC/XLR) - Converts LTC into NTP-synced time - Broadcasts time via local NTP server -- Supports configurable time offsets (hours, minutes, seconds, milliseconds) - NOT AVAILABLE +- Supports configurable time offsets (hours, minutes, seconds, frames, and milliseconds) - Systemd service support for headless operation --- From 0c6e1b0f436b8f8ca0a7463b00e2638dea075054 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:06:43 +0100 Subject: [PATCH 2/4] feat: Animate system and LTC clocks client-side for dynamic display Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/static/script.js b/static/script.js index 22c8dd7..0dce505 100644 --- a/static/script.js +++ b/static/script.js @@ -1,4 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { + let lastApiData = null; + let lastApiFetchTime = null; + const statusElements = { ltcStatus: document.getElementById('ltc-status'), ltcTimecode: document.getElementById('ltc-timecode'), @@ -65,14 +68,75 @@ document.addEventListener('DOMContentLoaded', () => { } } + function animateClocks() { + if (!lastApiData || !lastApiFetchTime) return; + + const elapsedMs = new Date() - lastApiFetchTime; + + // Animate System Clock + if (lastApiData.system_clock && lastApiData.system_clock.includes(':')) { + const parts = lastApiData.system_clock.split(/[:.]/); + if (parts.length === 4) { + const baseDate = new Date(); + baseDate.setHours(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)); + baseDate.setMilliseconds(parseInt(parts[3], 10)); + + const newDate = new Date(baseDate.getTime() + elapsedMs); + + const h = String(newDate.getHours()).padStart(2, '0'); + const m = String(newDate.getMinutes()).padStart(2, '0'); + const s = String(newDate.getSeconds()).padStart(2, '0'); + const ms = String(newDate.getMilliseconds()).padStart(3, '0'); + statusElements.systemClock.textContent = `${h}:${m}:${s}.${ms}`; + } + } + + // 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(':'); + 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); + let s = parseInt(tcParts[2], 10); + let f = parseInt(tcParts[3], 10); + + const msPerFrame = 1000.0 / frameRate; + const elapsedFrames = Math.floor(elapsedMs / msPerFrame); + + f += elapsedFrames; + + const frameRateInt = Math.round(frameRate); + + s += Math.floor(f / frameRateInt); + f %= frameRateInt; + + m += Math.floor(s / 60); + s %= 60; + + h += Math.floor(m / 60); + m %= 60; + + 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')}`; + } + } + } + async function fetchStatus() { try { const response = await fetch('/api/status'); if (!response.ok) throw new Error('Failed to fetch status'); const data = await response.json(); updateStatus(data); + lastApiData = data; + lastApiFetchTime = new Date(); } catch (error) { console.error('Error fetching status:', error); + lastApiData = null; + lastApiFetchTime = null; } } @@ -193,4 +257,5 @@ document.addEventListener('DOMContentLoaded', () => { // Refresh data every 2 seconds setInterval(fetchStatus, 2000); setInterval(fetchLogs, 2000); + setInterval(animateClocks, 50); // High-frequency clock animation }); From 0745883e0dcfb6fd67b4c2b6d50069deacca12b0 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:21:09 +0100 Subject: [PATCH 3/4] Revert "docs: Correct README for time offset features" This reverts commit 871fd192b0638d55b63b470eeae760e3be76766c. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e438eb..d7ed822 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Created by Chris Frankland-Wright and John Rogers - Reads SMPTE LTC from Audio Interface (3.5mm TRS but adaptable to BNC/XLR) - Converts LTC into NTP-synced time - Broadcasts time via local NTP server -- Supports configurable time offsets (hours, minutes, seconds, frames, and milliseconds) +- Supports configurable time offsets (hours, minutes, seconds, milliseconds) - NOT AVAILABLE - Systemd service support for headless operation --- From 3df94667549a272b7d5e0322643476b5d9541566 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:25:10 +0100 Subject: [PATCH 4/4] animate timecode --- static/script.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/static/script.js b/static/script.js index 0dce505..ba997a4 100644 --- a/static/script.js +++ b/static/script.js @@ -41,7 +41,7 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.frameRate.textContent = data.frame_rate; statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); statusElements.systemClock.textContent = data.system_clock; - + statusElements.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive'; statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive'; @@ -50,7 +50,7 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.deltaMs.textContent = data.timecode_delta_ms; statusElements.deltaFrames.textContent = data.timecode_delta_frames; - + statusElements.jitterStatus.textContent = data.jitter_status; statusElements.jitterStatus.className = data.jitter_status.toLowerCase(); @@ -80,9 +80,9 @@ document.addEventListener('DOMContentLoaded', () => { const baseDate = new Date(); baseDate.setHours(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)); baseDate.setMilliseconds(parseInt(parts[3], 10)); - + const newDate = new Date(baseDate.getTime() + elapsedMs); - + const h = String(newDate.getHours()).padStart(2, '0'); const m = String(newDate.getMinutes()).padStart(2, '0'); const s = String(newDate.getSeconds()).padStart(2, '0'); @@ -100,26 +100,26 @@ document.addEventListener('DOMContentLoaded', () => { let m = parseInt(tcParts[1], 10); let s = parseInt(tcParts[2], 10); let f = parseInt(tcParts[3], 10); - + const msPerFrame = 1000.0 / frameRate; const elapsedFrames = Math.floor(elapsedMs / msPerFrame); - + f += elapsedFrames; - + const frameRateInt = Math.round(frameRate); - + s += Math.floor(f / frameRateInt); f %= frameRateInt; - + m += Math.floor(s / 60); s %= 60; - + h += Math.floor(m / 60); m %= 60; - + h %= 24; - - statusElements.ltcTimecode.textContent = + + statusElements.ltcTimecode.textContent = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`; } } @@ -164,10 +164,10 @@ document.addEventListener('DOMContentLoaded', () => { autoSyncEnabled: autoSyncCheckbox.checked, defaultNudgeMs: parseInt(nudgeValueInput.value, 10) || 0, timeturnerOffset: { - hours: parseInt(offsetInputs.h.value, 10) || 0, + hours: parseInt(offsetInputs.h.value, 10) || 0, minutes: parseInt(offsetInputs.m.value, 10) || 0, seconds: parseInt(offsetInputs.s.value, 10) || 0, - frames: parseInt(offsetInputs.f.value, 10) || 0, + frames: parseInt(offsetInputs.f.value, 10) || 0, milliseconds: parseInt(offsetInputs.ms.value, 10) || 0, } };