diff --git a/minimateplus/client.py b/minimateplus/client.py index 554a44a..bb72858 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -163,7 +163,7 @@ class MiniMateClient: log.info("connect: %s", 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 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 ) - # SUB 5A — bulk waveform stream: event-time metadata - # Stops early after "Project:" is found (typically in A5[7] of 9) - # so we fetch only ~8 frames rather than the full multi-MB stream. - # This is the authoritative source for client/operator/seis_loc/notes. + # SUB 5A — bulk waveform stream. + # By default (full_waveform=False): stop early after frame 7 ("Project:") + # is found — fetches only ~8 frames for event-time metadata. + # 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: - 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, + if full_waveform: + log.info( + "get_events: 5A full waveform download for key=%s", key4.hex() ) + 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: log.warning( "get_events: 5A failed for key=%s: %s — event-time metadata unavailable", diff --git a/sfm/server.py b/sfm/server.py index 6263d16..c2f7240 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -427,14 +427,12 @@ def device_event_waveform( def _do(): with _build_client(port, baud, host, tcp_port) as client: 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] - if not matching: - return None, None, info - ev = matching[0] - client.download_waveform(ev) - return ev, events, info - ev, events, info = _run_with_retry(_do, is_tcp=_is_tcp(host)) + return matching[0] if matching else None, info + ev, info = _run_with_retry(_do, is_tcp=_is_tcp(host)) except HTTPException: raise except ProtocolError as exc: diff --git a/sfm/waveform_viewer.html b/sfm/waveform_viewer.html index fc9a181..5ec560c 100644 --- a/sfm/waveform_viewer.html +++ b/sfm/waveform_viewer.html @@ -132,6 +132,49 @@ .ch-vert { color: #3fb950; } .ch-long { color: #d29922; } .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; } @@ -147,15 +190,35 @@ - - - + + -
Ready — enter device host and click Load Waveform.
+ + + +
Ready — enter device host and click Connect.
@@ -176,6 +239,8 @@ let charts = {}; let lastData = null; + let unitInfo = null; + let currentEventIndex = 0; function setStatus(msg, cls = '') { const bar = document.getElementById('status-bar'); @@ -191,11 +256,84 @@ 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() { - const apiBase = document.getElementById('api-base').value.replace(/\/$/, ''); - const devHost = document.getElementById('dev-host').value.trim(); - const tcpPort = document.getElementById('dev-tcp-port').value; - const evIndex = parseInt(document.getElementById('event-index').value, 10); + const apiBase = document.getElementById('api-base').value.replace(/\/$/, ''); + const devHost = document.getElementById('dev-host').value.trim(); + const tcpPort = document.getElementById('dev-tcp-port').value; + const evIndex = currentEventIndex; if (!devHost) { setStatus('Enter a device host first.', 'error'); return; } @@ -222,15 +360,12 @@ lastData = data; renderWaveform(data); btn.disabled = false; - document.getElementById('prev-btn').disabled = evIndex <= 0; - document.getElementById('next-btn').disabled = false; } function stepEvent(delta) { - const el = document.getElementById('event-index'); - const next = Math.max(0, parseInt(el.value, 10) + delta); - el.value = next; - loadWaveform(); + const count = unitInfo?.event_count ?? 0; + const next = Math.max(0, Math.min(count - 1, currentEventIndex + delta)); + selectEvent(next); } function renderWaveform(data) { @@ -379,9 +514,11 @@ } } - // Allow Enter key on inputs to trigger load - document.querySelectorAll('input').forEach(el => { - el.addEventListener('keydown', e => { if (e.key === 'Enter') loadWaveform(); }); + // Allow Enter key on connection inputs to trigger connect + ['api-base', 'dev-host', 'dev-tcp-port'].forEach(id => { + document.getElementById(id).addEventListener('keydown', e => { + if (e.key === 'Enter') connectUnit(); + }); });