fix: add thor specific ascii parser.

This commit is contained in:
2026-05-20 05:22:28 +00:00
parent cd20be2eff
commit 350f81f8b5
2 changed files with 59 additions and 28 deletions
+47 -23
View File
@@ -65,9 +65,17 @@ def _normalize_key(raw: str) -> str:
def _strip_unit_suffix(value: str) -> str: def _strip_unit_suffix(value: str) -> str:
"""Return the numeric part of values like "0.2119 in/s""0.2119".""" """Return the numeric part of values like "0.2119 in/s""0.2119".
Also strips Thor's below/above-threshold prefixes:
"<0.005 in/s""0.005" (below-noise-floor reading)
">100 Hz""100" (above-measurement-range reading)
"""
parts = value.strip().split() parts = value.strip().split()
return parts[0] if parts else value.strip() token = parts[0] if parts else value.strip()
if token.startswith("<") or token.startswith(">"):
token = token[1:]
return token
def _parse_float(value: str) -> Optional[float]: def _parse_float(value: str) -> Optional[float]:
@@ -178,38 +186,54 @@ def parse_idf_report(text: Union[str, bytes]) -> Dict[str, Any]:
except ValueError: except ValueError:
pass pass
# Numeric scalars # Numeric scalars. For every field we typify here, we MUST drop the
for key in ("sample_rate",): # raw string copy from `out` when parsing fails — Thor writes things
# like "<0.005 in/s" (below threshold) and "N/A" (not measured) that
# would otherwise linger in `out` as strings, sneak into SQLite REAL
# columns via permissive type affinity, and then crash the JS
# frontend on `.toFixed(...)`.
int_fields = ("sample_rate",)
for key in int_fields:
v = raw.get(key) v = raw.get(key)
if v is not None: if v is None:
iv = _parse_int(v) continue
if iv is not None: iv = _parse_int(v)
out[key] = iv if iv is not None:
out[key] = iv
else:
out.pop(key, None)
for key in ("tran_ppv", "vert_ppv", "long_ppv", "peak_vector_sum", float_fields = (
"tran_zc_freq", "vert_zc_freq", "long_zc_freq", "tran_ppv", "vert_ppv", "long_ppv", "peak_vector_sum",
"tran_peak_acceleration", "vert_peak_acceleration", "tran_zc_freq", "vert_zc_freq", "long_zc_freq",
"long_peak_acceleration", "tran_peak_acceleration", "vert_peak_acceleration",
"tran_peak_displacement", "vert_peak_displacement", "long_peak_acceleration",
"long_peak_displacement", "tran_peak_displacement", "vert_peak_displacement",
"tran_time_of_peak", "vert_time_of_peak", "long_time_of_peak", "long_peak_displacement",
"mic_time_of_peak", "mic_zc_freq"): "tran_time_of_peak", "vert_time_of_peak", "long_time_of_peak",
"mic_time_of_peak", "mic_zc_freq",
)
for key in float_fields:
v = raw.get(key) v = raw.get(key)
if v is not None: if v is None:
fv = _parse_float(v) continue
if fv is not None: fv = _parse_float(v)
out[key] = fv if fv is not None:
out[key] = fv
else:
out.pop(key, None)
# Microphone — Thor reports MicPSPL (dB(L)) which is the closest # Microphone — Thor reports MicPSPL (dB(L)) which is the closest
# analogue to BW's mic_ppv. Stored as a float; units are in the # analogue to BW's mic_ppv. The raw "99.4 dB(L)" string stays in
# original raw field (`mic_pspl` raw entry preserves "99.4 dB(L)"). # `out` under the original `mic_pspl` key for display; the parsed
# float goes in `mic_ppv`.
mic = raw.get("mic_pspl") mic = raw.get("mic_pspl")
if mic is not None: if mic is not None:
fv = _parse_float(mic) fv = _parse_float(mic)
if fv is not None: if fv is not None:
out["mic_ppv"] = fv out["mic_ppv"] = fv
# Record / pre-trigger duration # Record / pre-trigger duration — same drop-on-failure discipline.
rt = raw.get("record_time") rt = raw.get("record_time")
if rt is not None: if rt is not None:
fv = _parse_float(rt) fv = _parse_float(rt)
+12 -5
View File
@@ -2285,13 +2285,16 @@ let sessLoaded = false;
const _unitSerials = new Set(); const _unitSerials = new Set();
function _ppvClass(v) { function _ppvClass(v) {
if (v == null) return ''; const n = (v == null) ? null : Number(v);
if (v >= 2.0) return 'ppv-high'; if (n == null || !isFinite(n)) return '';
if (v >= 0.5) return 'ppv-warn'; if (n >= 2.0) return 'ppv-high';
if (n >= 0.5) return 'ppv-warn';
return 'ppv-ok'; return 'ppv-ok';
} }
function _ppvFmt(v) { function _ppvFmt(v) {
return v != null ? v.toFixed(5) : '—'; if (v == null) return '—';
const n = typeof v === 'number' ? v : Number(v);
return isFinite(n) ? n.toFixed(5) : String(v);
} }
function _fmtTs(ts) { function _fmtTs(ts) {
if (!ts) return '—'; if (!ts) return '—';
@@ -2386,7 +2389,11 @@ async function loadHistory() {
<td class="${_ppvClass(ev.vert_ppv)}">${_ppvFmt(ev.vert_ppv)}</td> <td class="${_ppvClass(ev.vert_ppv)}">${_ppvFmt(ev.vert_ppv)}</td>
<td class="${_ppvClass(ev.long_ppv)}">${_ppvFmt(ev.long_ppv)}</td> <td class="${_ppvClass(ev.long_ppv)}">${_ppvFmt(ev.long_ppv)}</td>
<td class="${_ppvClass(pvs)}">${_ppvFmt(pvs)}</td> <td class="${_ppvClass(pvs)}">${_ppvFmt(pvs)}</td>
<td class="td-dim">${ev.mic_ppv != null && ev.mic_ppv > 0 ? (20 * Math.log10(ev.mic_ppv / DBL_REF)).toFixed(1) + ' dBL' : '—'}</td> <td class="td-dim">${(() => {
const m = ev.mic_ppv == null ? null : Number(ev.mic_ppv);
if (m == null || !isFinite(m) || m <= 0) return '—';
return (20 * Math.log10(m / DBL_REF)).toFixed(1) + ' dBL';
})()}</td>
<td class="td-text">${ev.project ?? '—'}</td> <td class="td-text">${ev.project ?? '—'}</td>
<td class="td-text">${ev.client ?? '—'}</td> <td class="td-text">${ev.client ?? '—'}</td>
<td class="td-dim">${ev.record_type ?? '—'}</td> <td class="td-dim">${ev.record_type ?? '—'}</td>