feat: enhance waveform viewer with unit info display and event selection functionality
This commit is contained in:
@@ -163,7 +163,7 @@ class MiniMateClient:
|
|||||||
log.info("connect: %s", device_info)
|
log.info("connect: %s", device_info)
|
||||||
return device_info
|
return device_info
|
||||||
|
|
||||||
def get_events(self, include_waveforms: bool = True, debug: bool = False) -> list[Event]:
|
def get_events(self, full_waveform: bool = False, debug: bool = False) -> list[Event]:
|
||||||
"""
|
"""
|
||||||
Download all stored events from the device using the confirmed
|
Download all stored events from the device using the confirmed
|
||||||
1E → 0A → 0C → 5A → 1F event-iterator protocol.
|
1E → 0A → 0C → 5A → 1F event-iterator protocol.
|
||||||
@@ -247,21 +247,39 @@ class MiniMateClient:
|
|||||||
"get_events: 0C failed for key=%s: %s", key4.hex(), exc
|
"get_events: 0C failed for key=%s: %s", key4.hex(), exc
|
||||||
)
|
)
|
||||||
|
|
||||||
# SUB 5A — bulk waveform stream: event-time metadata
|
# SUB 5A — bulk waveform stream.
|
||||||
# Stops early after "Project:" is found (typically in A5[7] of 9)
|
# By default (full_waveform=False): stop early after frame 7 ("Project:")
|
||||||
# so we fetch only ~8 frames rather than the full multi-MB stream.
|
# is found — fetches only ~8 frames for event-time metadata.
|
||||||
# This is the authoritative source for client/operator/seis_loc/notes.
|
# When full_waveform=True: fetch the complete stream (stop_after_metadata=False,
|
||||||
|
# max_chunks=128) and decode raw ADC samples into ev.raw_samples.
|
||||||
|
# The full waveform MUST be fetched here, inside the 1E→0A→0C→5A→1F loop.
|
||||||
|
# Issuing 5A after 1F has advanced the event context will time out.
|
||||||
try:
|
try:
|
||||||
a5_frames = proto.read_bulk_waveform_stream(
|
if full_waveform:
|
||||||
key4, stop_after_metadata=True
|
log.info(
|
||||||
)
|
"get_events: 5A full waveform download for key=%s", key4.hex()
|
||||||
if a5_frames:
|
|
||||||
_decode_a5_metadata_into(a5_frames, ev)
|
|
||||||
log.debug(
|
|
||||||
"get_events: 5A metadata client=%r operator=%r",
|
|
||||||
ev.project_info.client if ev.project_info else None,
|
|
||||||
ev.project_info.operator if ev.project_info else None,
|
|
||||||
)
|
)
|
||||||
|
a5_frames = proto.read_bulk_waveform_stream(
|
||||||
|
key4, stop_after_metadata=False, max_chunks=128
|
||||||
|
)
|
||||||
|
if a5_frames:
|
||||||
|
_decode_a5_metadata_into(a5_frames, ev)
|
||||||
|
_decode_a5_waveform(a5_frames, ev)
|
||||||
|
log.info(
|
||||||
|
"get_events: 5A decoded %d sample-sets",
|
||||||
|
len((ev.raw_samples or {}).get("Tran", [])),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
a5_frames = proto.read_bulk_waveform_stream(
|
||||||
|
key4, stop_after_metadata=True
|
||||||
|
)
|
||||||
|
if a5_frames:
|
||||||
|
_decode_a5_metadata_into(a5_frames, ev)
|
||||||
|
log.debug(
|
||||||
|
"get_events: 5A metadata client=%r operator=%r",
|
||||||
|
ev.project_info.client if ev.project_info else None,
|
||||||
|
ev.project_info.operator if ev.project_info else None,
|
||||||
|
)
|
||||||
except ProtocolError as exc:
|
except ProtocolError as exc:
|
||||||
log.warning(
|
log.warning(
|
||||||
"get_events: 5A failed for key=%s: %s — event-time metadata unavailable",
|
"get_events: 5A failed for key=%s: %s — event-time metadata unavailable",
|
||||||
|
|||||||
@@ -427,14 +427,12 @@ def device_event_waveform(
|
|||||||
def _do():
|
def _do():
|
||||||
with _build_client(port, baud, host, tcp_port) as client:
|
with _build_client(port, baud, host, tcp_port) as client:
|
||||||
info = client.connect()
|
info = client.connect()
|
||||||
events = client.get_events()
|
# full_waveform=True fetches the complete 5A stream inside the
|
||||||
|
# 1E→0A→0C→5A→1F loop. Issuing a second 5A after 1F times out.
|
||||||
|
events = client.get_events(full_waveform=True)
|
||||||
matching = [ev for ev in events if ev.index == index]
|
matching = [ev for ev in events if ev.index == index]
|
||||||
if not matching:
|
return matching[0] if matching else None, info
|
||||||
return None, None, info
|
ev, info = _run_with_retry(_do, is_tcp=_is_tcp(host))
|
||||||
ev = matching[0]
|
|
||||||
client.download_waveform(ev)
|
|
||||||
return ev, events, info
|
|
||||||
ev, events, info = _run_with_retry(_do, is_tcp=_is_tcp(host))
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except ProtocolError as exc:
|
except ProtocolError as exc:
|
||||||
|
|||||||
@@ -132,6 +132,49 @@
|
|||||||
.ch-vert { color: #3fb950; }
|
.ch-vert { color: #3fb950; }
|
||||||
.ch-long { color: #d29922; }
|
.ch-long { color: #d29922; }
|
||||||
.ch-mic { color: #bc8cff; }
|
.ch-mic { color: #bc8cff; }
|
||||||
|
|
||||||
|
#unit-bar {
|
||||||
|
background: #0d1117;
|
||||||
|
border-bottom: 1px solid #21262d;
|
||||||
|
padding: 8px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-field { display: flex; flex-direction: column; gap: 1px; }
|
||||||
|
.unit-field .uf-label { color: #484f58; font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.unit-field .uf-value { color: #c9d1d9; font-family: monospace; font-size: 13px; }
|
||||||
|
.unit-field .uf-value.highlight { color: #58a6ff; font-weight: 600; }
|
||||||
|
|
||||||
|
.event-chips {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-chip {
|
||||||
|
background: #21262d;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #8b949e;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
transition: all 0.12s;
|
||||||
|
}
|
||||||
|
.event-chip:hover { background: #1f6feb; border-color: #1f6feb; color: #fff; }
|
||||||
|
.event-chip.active { background: #1f6feb; border-color: #388bfd; color: #fff; font-weight: 600; }
|
||||||
|
|
||||||
|
#connect-btn {
|
||||||
|
background: #238636;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
#connect-btn:hover { background: #2ea043; }
|
||||||
|
#connect-btn:disabled { background: #21262d; color: #484f58; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -147,15 +190,35 @@
|
|||||||
<input type="text" id="dev-host" value="" placeholder="e.g. 10.0.0.5" />
|
<input type="text" id="dev-host" value="" placeholder="e.g. 10.0.0.5" />
|
||||||
<label>TCP port</label>
|
<label>TCP port</label>
|
||||||
<input type="number" id="dev-tcp-port" value="9034" />
|
<input type="number" id="dev-tcp-port" value="9034" />
|
||||||
<label>Event #</label>
|
|
||||||
<input type="number" id="event-index" value="0" min="0" style="width:55px" />
|
|
||||||
</div>
|
</div>
|
||||||
<button id="load-btn" onclick="loadWaveform()">Load Waveform</button>
|
<button id="connect-btn" onclick="connectUnit()">Connect</button>
|
||||||
|
<button id="load-btn" onclick="loadWaveform()" disabled>Load Waveform</button>
|
||||||
<button id="prev-btn" onclick="stepEvent(-1)" disabled>◀ Prev</button>
|
<button id="prev-btn" onclick="stepEvent(-1)" disabled>◀ Prev</button>
|
||||||
<button id="next-btn" onclick="stepEvent(+1)" disabled>Next ▶</button>
|
<button id="next-btn" onclick="stepEvent(+1)" disabled>Next ▶</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="status-bar">Ready — enter device host and click Load Waveform.</div>
|
<!-- Unit info bar — hidden until connected -->
|
||||||
|
<div id="unit-bar" style="display:none">
|
||||||
|
<div class="unit-field">
|
||||||
|
<span class="uf-label">Serial</span>
|
||||||
|
<span class="uf-value" id="u-serial">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="unit-field">
|
||||||
|
<span class="uf-label">Firmware</span>
|
||||||
|
<span class="uf-value" id="u-fw">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="unit-field">
|
||||||
|
<span class="uf-label">Sample rate</span>
|
||||||
|
<span class="uf-value" id="u-sr">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="unit-field">
|
||||||
|
<span class="uf-label">Events</span>
|
||||||
|
<span class="uf-value highlight" id="u-count">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-chips" id="event-chips"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status-bar">Ready — enter device host and click Connect.</div>
|
||||||
|
|
||||||
<div id="empty-state">
|
<div id="empty-state">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
@@ -176,6 +239,8 @@
|
|||||||
|
|
||||||
let charts = {};
|
let charts = {};
|
||||||
let lastData = null;
|
let lastData = null;
|
||||||
|
let unitInfo = null;
|
||||||
|
let currentEventIndex = 0;
|
||||||
|
|
||||||
function setStatus(msg, cls = '') {
|
function setStatus(msg, cls = '') {
|
||||||
const bar = document.getElementById('status-bar');
|
const bar = document.getElementById('status-bar');
|
||||||
@@ -191,11 +256,84 @@
|
|||||||
bar.appendChild(pill);
|
bar.appendChild(pill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function connectUnit() {
|
||||||
|
const apiBase = document.getElementById('api-base').value.replace(/\/$/, '');
|
||||||
|
const devHost = document.getElementById('dev-host').value.trim();
|
||||||
|
const tcpPort = document.getElementById('dev-tcp-port').value;
|
||||||
|
|
||||||
|
if (!devHost) { setStatus('Enter a device host first.', 'error'); return; }
|
||||||
|
|
||||||
|
const btn = document.getElementById('connect-btn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Connecting…';
|
||||||
|
setStatus('Connecting to unit…', 'loading');
|
||||||
|
|
||||||
|
const url = `${apiBase}/device/info?host=${encodeURIComponent(devHost)}&tcp_port=${tcpPort}`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
||||||
|
throw new Error(err.detail || resp.statusText);
|
||||||
|
}
|
||||||
|
unitInfo = await resp.json();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Error: ${e.message}`, 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Connect';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate unit bar
|
||||||
|
document.getElementById('u-serial').textContent = unitInfo.serial || '—';
|
||||||
|
document.getElementById('u-fw').textContent = unitInfo.firmware_version || '—';
|
||||||
|
const sr = unitInfo.compliance_config?.sample_rate;
|
||||||
|
document.getElementById('u-sr').textContent = sr ? `${sr} sps` : '—';
|
||||||
|
const count = unitInfo.event_count ?? 0;
|
||||||
|
document.getElementById('u-count').textContent = count;
|
||||||
|
|
||||||
|
// Build event chips
|
||||||
|
const chipsEl = document.getElementById('event-chips');
|
||||||
|
chipsEl.innerHTML = '';
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const chip = document.createElement('button');
|
||||||
|
chip.className = 'event-chip' + (i === 0 ? ' active' : '');
|
||||||
|
chip.textContent = `Event ${i}`;
|
||||||
|
chip.onclick = () => selectEvent(i);
|
||||||
|
chipsEl.appendChild(chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('unit-bar').style.display = 'flex';
|
||||||
|
document.getElementById('load-btn').disabled = count === 0;
|
||||||
|
document.getElementById('prev-btn').disabled = true;
|
||||||
|
document.getElementById('next-btn').disabled = count <= 1;
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Reconnect';
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
setStatus('Connected — no events stored on device.', 'ok');
|
||||||
|
} else {
|
||||||
|
setStatus(`Connected — ${count} event${count !== 1 ? 's' : ''} stored. Select an event or click Load Waveform.`, 'ok');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEvent(idx) {
|
||||||
|
currentEventIndex = idx;
|
||||||
|
// Update chip highlight
|
||||||
|
document.querySelectorAll('.event-chip').forEach((c, i) => {
|
||||||
|
c.classList.toggle('active', i === idx);
|
||||||
|
});
|
||||||
|
document.getElementById('prev-btn').disabled = idx <= 0;
|
||||||
|
const count = unitInfo?.event_count ?? 0;
|
||||||
|
document.getElementById('next-btn').disabled = idx >= count - 1;
|
||||||
|
loadWaveform();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWaveform() {
|
async function loadWaveform() {
|
||||||
const apiBase = document.getElementById('api-base').value.replace(/\/$/, '');
|
const apiBase = document.getElementById('api-base').value.replace(/\/$/, '');
|
||||||
const devHost = document.getElementById('dev-host').value.trim();
|
const devHost = document.getElementById('dev-host').value.trim();
|
||||||
const tcpPort = document.getElementById('dev-tcp-port').value;
|
const tcpPort = document.getElementById('dev-tcp-port').value;
|
||||||
const evIndex = parseInt(document.getElementById('event-index').value, 10);
|
const evIndex = currentEventIndex;
|
||||||
|
|
||||||
if (!devHost) { setStatus('Enter a device host first.', 'error'); return; }
|
if (!devHost) { setStatus('Enter a device host first.', 'error'); return; }
|
||||||
|
|
||||||
@@ -222,15 +360,12 @@
|
|||||||
lastData = data;
|
lastData = data;
|
||||||
renderWaveform(data);
|
renderWaveform(data);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
document.getElementById('prev-btn').disabled = evIndex <= 0;
|
|
||||||
document.getElementById('next-btn').disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepEvent(delta) {
|
function stepEvent(delta) {
|
||||||
const el = document.getElementById('event-index');
|
const count = unitInfo?.event_count ?? 0;
|
||||||
const next = Math.max(0, parseInt(el.value, 10) + delta);
|
const next = Math.max(0, Math.min(count - 1, currentEventIndex + delta));
|
||||||
el.value = next;
|
selectEvent(next);
|
||||||
loadWaveform();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWaveform(data) {
|
function renderWaveform(data) {
|
||||||
@@ -379,9 +514,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow Enter key on inputs to trigger load
|
// Allow Enter key on connection inputs to trigger connect
|
||||||
document.querySelectorAll('input').forEach(el => {
|
['api-base', 'dev-host', 'dev-tcp-port'].forEach(id => {
|
||||||
el.addEventListener('keydown', e => { if (e.key === 'Enter') loadWaveform(); });
|
document.getElementById(id).addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') connectUnit();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user