PTP status: 2-min average offset, details split; uptime as d/h/m/s

- Firmware: status.ptp.offset_avg_ns = mean |offset| over the last 2 min
  (one bucket per second, cleared on a clock step).
- Main panel: Time Offset (2 min avg) shows only the Bolero bracket of the
  average. Details: the current offset (coloured by the same brackets),
  hops, time/frequency traceable, version and own clock class.
- Uptime shown as "1d 2h 3m 4s".
- Preset buttons renamed: "Riedel PTP defaults", "AES67 Media PTP defaults".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 08:32:15 +10:00
parent 5f27262b27
commit d4aa9c7be1
4 changed files with 71 additions and 19 deletions
+19 -9
View File
@@ -98,8 +98,8 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next
<!-- ===== PROJECT END ===== -->
<fieldset><legend>PTP</legend>
<button type="button" onclick="ptpPreset('riedel')">Riedel SIC defaults</button>
<button type="button" onclick="ptpPreset('aes67')">AES67 media profile defaults</button>
<button type="button" onclick="ptpPreset('riedel')">Riedel PTP defaults</button>
<button type="button" onclick="ptpPreset('aes67')">AES67 Media PTP defaults</button>
<label><span>Mode</span><select name="ptp.mode">
<option value="multicast">multicast</option><option value="hybrid">hybrid</option></select>
<small>hybrid: Delay_Req/Resp unicast</small></label>
@@ -458,7 +458,7 @@ function offsetBucket(ns) {
// Same brackets and colours as Riedel Bolero.
const [t, cls] = a < 100 ? ['< 100 ns','ok'] : a < 500 ? ['100-500 ns','ok'] : a < 1e3 ? ['500-1000 ns','warn']
: a < 1e4 ? ['1000-10000 ns','bad'] : ['> 10000 ns','bad'];
return {cls, text: `${t} (${n(ns)} ns)`};
return {cls, text: t};
}
function freqBucket(ppb) {
if (ppb == null) return '–';
@@ -468,6 +468,13 @@ function freqBucket(ppb) {
return `${t} (${n(ppb)} ppb)`;
}
const yn = v => v == null ? '–' : v ? 'Yes' : 'No';
// 93784 -> "1d 2h 3m 4s"; leading zero units left out.
function uptime(s) {
if (s == null) return '–';
const parts = [[Math.floor(s / 86400), 'd'], [Math.floor(s / 3600) % 24, 'h'], [Math.floor(s / 60) % 60, 'm'], [s % 60, 's']];
const i = parts.findIndex(([v]) => v > 0);
return parts.slice(i < 0 ? 3 : i).map(([v, u]) => v + u).join(' ');
}
function showPtp(p) {
if (!p) { table($('ptpmore'), {}); return table($('ptpstat'), {'PTP': 'no data'}); }
@@ -476,18 +483,21 @@ function showPtp(p) {
'PTP State': PTP_STATE[p.state] || p.state,
'Lock State': p.locked ? {cls:'ok', text:'Locked'} : {cls:'bad', text:'Unlocked'},
'TimeTransmitter': clockId(p.gm_id) + (gmSelf ? ' (this device)' : ''),
'Time Offset': gmSelf ? '–' : offsetBucket(p.offset_ns),
// Bracket from the mean |offset| over the last 2 min; the current value is in Details.
'Time Offset (2 min avg)': gmSelf ? '–' : offsetBucket(p.offset_avg_ns),
'Frequency Deviation': gmSelf ? '–' : freqBucket(p.freq_ppb),
'Network Delay': gmSelf ? '–' : `${n(p.path_delay_ns)} ± ${n(p.path_delay_sd_ns)} ns`,
'Hops': p.steps_removed,
'Time/Frequency Traceable': `${yn(p.gm_time_traceable)} / ${yn(p.gm_freq_traceable)}`,
'Version': p.version ?? 2,
'Own Clock Class': p.own_class
});
table($('ptpmore'), {
'This clock': clockId(p.clock_id),
// Same colours as the average's brackets, for the current value.
'Time offset now': gmSelf || p.offset_ns == null ? '–' : {cls: offsetBucket(p.offset_ns).cls, text: `${n(p.offset_ns)} ns`},
'TT class / accuracy': `${p.gm_class ?? '–'} / 0x${(p.gm_accuracy ?? 0).toString(16).toUpperCase()}`,
'TT priority 1 / 2': `${p.gm_p1 ?? '–'} / ${p.gm_p2 ?? '–'}`,
'Hops': p.steps_removed ?? '–',
'Time/Frequency Traceable': `${yn(p.gm_time_traceable)} / ${yn(p.gm_freq_traceable)}`,
'Version': p.version ?? 2,
'Own Clock Class': p.own_class ?? '–',
// Measured over the last p.window Sync messages (RX as receiver, TX as transmitter).
'Sync interval avg': `${n(p.sync_avg_ms, 3)} ms (min ${n(p.sync_min_ms, 3)} / max ${n(p.sync_max_ms, 3)}, jitter ${n(p.sync_jitter_us, 1)} µs)`,
'Announce interval avg': n(p.announce_avg_ms, 1) + ' ms',
@@ -521,7 +531,7 @@ async function poll() {
...(st.power ? {'Power source': st.power} : {}),
...PROJECT.statusRows?.(st),
'AES67 TX': `${st.tx_packets} pkts, ${st.underruns} underruns`,
'Uptime': st.uptime_s + ' s', 'Heap / PSRAM free': `${st.heap_free} / ${st.psram_free}`,
'Uptime': uptime(st.uptime_s), 'Heap / PSRAM free': `${st.heap_free} / ${st.psram_free}`,
...temps(st.temps)
});
showPtp(st.ptp);