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:
"""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()
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]:
@@ -178,38 +186,54 @@ def parse_idf_report(text: Union[str, bytes]) -> Dict[str, Any]:
except ValueError:
pass
# Numeric scalars
for key in ("sample_rate",):
# Numeric scalars. For every field we typify here, we MUST drop the
# 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)
if v is not None:
iv = _parse_int(v)
if iv is not None:
out[key] = iv
if v is None:
continue
iv = _parse_int(v)
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",
"tran_zc_freq", "vert_zc_freq", "long_zc_freq",
"tran_peak_acceleration", "vert_peak_acceleration",
"long_peak_acceleration",
"tran_peak_displacement", "vert_peak_displacement",
"long_peak_displacement",
"tran_time_of_peak", "vert_time_of_peak", "long_time_of_peak",
"mic_time_of_peak", "mic_zc_freq"):
float_fields = (
"tran_ppv", "vert_ppv", "long_ppv", "peak_vector_sum",
"tran_zc_freq", "vert_zc_freq", "long_zc_freq",
"tran_peak_acceleration", "vert_peak_acceleration",
"long_peak_acceleration",
"tran_peak_displacement", "vert_peak_displacement",
"long_peak_displacement",
"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)
if v is not None:
fv = _parse_float(v)
if fv is not None:
out[key] = fv
if v is None:
continue
fv = _parse_float(v)
if fv is not None:
out[key] = fv
else:
out.pop(key, None)
# Microphone — Thor reports MicPSPL (dB(L)) which is the closest
# analogue to BW's mic_ppv. Stored as a float; units are in the
# original raw field (`mic_pspl` raw entry preserves "99.4 dB(L)").
# analogue to BW's mic_ppv. The raw "99.4 dB(L)" string stays in
# `out` under the original `mic_pspl` key for display; the parsed
# float goes in `mic_ppv`.
mic = raw.get("mic_pspl")
if mic is not None:
fv = _parse_float(mic)
if fv is not None:
out["mic_ppv"] = fv
# Record / pre-trigger duration
# Record / pre-trigger duration — same drop-on-failure discipline.
rt = raw.get("record_time")
if rt is not None:
fv = _parse_float(rt)