From aa28495a4356f44bdc18624601f047cd03b51125 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 19 Apr 2026 18:15:23 -0400 Subject: [PATCH] fix: rename max_geo_range to ADC scale, and make it so its not user configurable. fix: change max_geo_range_enum to geo_range with two options (normal and sensitive) --- bridges/ach_server.py | 4 ++-- minimateplus/client.py | 33 +++++++++------------------------ minimateplus/models.py | 6 +++--- sfm/server.py | 10 ++-------- sfm/sfm_webapp.html | 18 +++++------------- sfm/waveform_viewer.html | 6 +++--- 6 files changed, 24 insertions(+), 53 deletions(-) diff --git a/bridges/ach_server.py b/bridges/ach_server.py index 6cea729..65fb9e9 100644 --- a/bridges/ach_server.py +++ b/bridges/ach_server.py @@ -561,8 +561,8 @@ def _device_info_to_dict(d: DeviceInfo) -> dict: "record_time": cc.record_time if cc else None, "trigger_level_geo": cc.trigger_level_geo if cc else None, "alarm_level_geo": cc.alarm_level_geo if cc else None, - "max_range_geo": cc.max_range_geo if cc else None, # hw constant 6.206053 - "max_range_geo_enum": cc.max_range_geo_enum if cc else None, # 0x01=10in/s, 0x00=1.25in/s (unconfirmed) + "geo_adc_scale": cc.geo_adc_scale if cc else None, # hw scale factor (in/s)/V + "geo_range": cc.geo_range if cc else None, # 0x01=Normal 10in/s, 0x00=Sensitive 1.25in/s (unconfirmed) "project": cc.project if cc else None, "client": cc.client if cc else None, "operator": cc.operator if cc else None, diff --git a/minimateplus/client.py b/minimateplus/client.py index 92e317c..6cc5edd 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -852,7 +852,6 @@ class MiniMateClient: # Threshold parameters (geo channels, in/s) trigger_level_geo: Optional[float] = None, alarm_level_geo: Optional[float] = None, - max_range_geo: Optional[float] = None, # Project / operator strings project: Optional[str] = None, client_name: Optional[str] = None, @@ -876,10 +875,6 @@ class MiniMateClient: Trigger/alarm thresholds (geo channels, in/s): trigger_level_geo : float — trigger threshold (e.g. 0.5) alarm_level_geo : float — alarm threshold (e.g. 1.0) - max_range_geo : float — ADC-to-velocity scale factor (= 1/sensitivity, in/s per V) - e.g. 6.206053 for the standard Instantel geophone. - This is a hardware/firmware constant — do NOT write it. - Accepted for API compatibility but silently ignored. Project / operator strings (max 41 ASCII characters each): project : str @@ -923,7 +918,6 @@ class MiniMateClient: record_time=record_time, trigger_level_geo=trigger_level_geo, alarm_level_geo=alarm_level_geo, - max_range_geo=max_range_geo, project=project, client_name=client_name, operator=operator, @@ -1660,7 +1654,6 @@ def _encode_compliance_config( record_time: Optional[float] = None, trigger_level_geo: Optional[float] = None, alarm_level_geo: Optional[float] = None, - max_range_geo: Optional[float] = None, project: Optional[str] = None, client_name: Optional[str] = None, operator: Optional[str] = None, @@ -1687,7 +1680,7 @@ def _encode_compliance_config( (= 1/sensitivity, in/s per V) for the standard Instantel geophone. Confirmed from Interface Handbook §4.5: Range = 1.61133 V × 6.206053 = 10.000 in/s. This is a hardware/firmware constant common to all MiniMate Plus S3 units. - It must NOT be written. max_range_geo is accepted as a parameter but silently ignored. + It must NOT be written — do not add it back as a parameter. String field locations (64-byte slots, label+22 format): b"Project:" → value at label_pos + 22, max 41 chars + null @@ -1728,17 +1721,9 @@ def _encode_compliance_config( log.debug("_encode_compliance_config: record_time=%.3f -> offset %d", record_time, _anc + 6) # ── Numeric: channel block (Tran label + unit-string guard) ─────────────── - # NOTE: max_range_geo (float32 at tran_pos+28) is the ADC-to-velocity scale factor - # (1/sensitivity = 6.206053 (in/s)/V — confirmed: 1.61133 V × 6.206053 = 10.000 in/s). - # It is a hardware/firmware constant common to all MiniMate Plus S3 units. - # It must NOT be written. The parameter is accepted for API compatibility but silently ignored. - if max_range_geo is not None: - log.warning( - "_encode_compliance_config: max_range_geo=%s ignored — " - "tran_pos+28 is the ADC scale factor (6.206053 (in/s)/V), a hardware constant " - "that must not be overwritten", - max_range_geo, - ) + # NOTE: tran_pos+28 (float32 = 6.206053) is the ADC-to-velocity scale factor + # (1/sensitivity, (in/s)/V — Interface Handbook §4.5: 1.61133 V × 6.206053 = 10.000 in/s). + # Hardware/firmware constant — never written here. _needs_channel = any(v is not None for v in (trigger_level_geo, alarm_level_geo)) if _needs_channel: _tran = buf.find(b"Tran", 44) @@ -1905,7 +1890,7 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: except Exception as exc: log.warning("compliance_config: project string extraction failed: %s", exc) - # ── Channel block: trigger_level_geo, alarm_level_geo, max_range_geo ───── + # ── Channel block: trigger_level_geo, alarm_level_geo, geo_range, geo_adc_scale ── # The channel block is only present in the full cfg (frame D delivered, # ~2126 bytes). Layout confirmed 2026-04-02 from both E5 frame 78 of the # 3-11-26 compliance-config capture and A5 frame 77 of the 1-2-26 event @@ -1930,14 +1915,14 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: and data[tran_pos + 38 : tran_pos + 42] == b"in.\x00" and data[tran_pos + 46 : tran_pos + 50] == b"/s\x00\x00" ): - config.max_range_geo_enum = data[tran_pos + 20] # range selector enum (see above) - config.max_range_geo = struct.unpack_from(">f", data, tran_pos + 28)[0] # hw constant + config.geo_range = data[tran_pos + 20] # range selector (0x01=Normal 10in/s, 0x00=Sensitive 1.25in/s — unconfirmed) + config.geo_adc_scale = struct.unpack_from(">f", data, tran_pos + 28)[0] # hw scale factor (in/s)/V — do NOT write config.trigger_level_geo = struct.unpack_from(">f", data, tran_pos + 34)[0] config.alarm_level_geo = struct.unpack_from(">f", data, tran_pos + 42)[0] log.debug( - "compliance_config: trigger=%.4f alarm=%.4f range_enum=0x%02X hw_const=%.6f", + "compliance_config: trigger=%.4f alarm=%.4f geo_range=0x%02X geo_adc_scale=%.6f", config.trigger_level_geo, config.alarm_level_geo, - config.max_range_geo_enum, config.max_range_geo, + config.geo_range, config.geo_adc_scale, ) elif tran_pos >= 0: log.warning( diff --git a/minimateplus/models.py b/minimateplus/models.py index 664fff7..67c8b7b 100644 --- a/minimateplus/models.py +++ b/minimateplus/models.py @@ -346,14 +346,14 @@ class ComplianceConfig: # full per-channel data would require structured Channel objects. trigger_level_geo: Optional[float] = None # in/s (first geo channel) ✅ alarm_level_geo: Optional[float] = None # in/s (first geo channel) ✅ - max_range_geo: Optional[float] = None # ADC-to-velocity scale factor (float32 at Tran+28) ✅ + geo_adc_scale: Optional[float] = None # ADC-to-velocity scale factor (float32 at Tran+28) ✅ # = inverse sensitivity = 1/sensitivity (in/s per V) # Formula (Interface Handbook §4.5): Range = 1.61133 V × scale_factor # → 1.61133 × 6.206053 = 10.000 in/s (Normal range) ✅ # Firmware uses: PPV (in/s) = ADC_voltage (V) × 6.206053 # Identical on BE11529 and BE18189 — same Instantel geophone hardware. - # NOT a user-configurable setting. Must NOT be written (use _encode_compliance_config). - max_range_geo_enum: Optional[int] = None # max range selector: uint8 at Tran+20 + # NOT a user-configurable setting. Must NOT be written. + geo_range: Optional[int] = None # range selector: uint8 at Tran+20 # hypothesis: 0x01 = Normal 10.000 in/s, 0x00 = Sensitive 1.25 in/s # reads 0x01 on all tested units — UNCONFIRMED (need 1.25 in/s capture) diff --git a/sfm/server.py b/sfm/server.py index f88ee34..5fbaf0b 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -289,8 +289,8 @@ def _serialise_compliance_config(cc: Optional["ComplianceConfig"]) -> Optional[d "sample_rate": cc.sample_rate, "trigger_level_geo": cc.trigger_level_geo, "alarm_level_geo": cc.alarm_level_geo, - "max_range_geo": cc.max_range_geo, # hw constant 6.206053 — informational only, do not write - "max_range_geo_enum": cc.max_range_geo_enum, # 0x01=Normal 10in/s, 0x00=Extended 1.25in/s (unconfirmed) + "geo_adc_scale": cc.geo_adc_scale, # hw scale factor (in/s)/V — informational only, do not write + "geo_range": cc.geo_range, # range selector: 0x01=Normal 10in/s, 0x00=Sensitive 1.25in/s (unconfirmed) "setup_name": cc.setup_name, "project": cc.project, "client": cc.client, @@ -843,10 +843,6 @@ class DeviceConfigBody(BaseModel): ------------------------------------------------ trigger_level_geo : Trigger threshold in in/s (e.g. 0.5). alarm_level_geo : Alarm threshold in in/s (e.g. 1.0). - max_range_geo : DEPRECATED — was misidentified as the max range; it is actually a - hardware/firmware constant (6.206053) the same on all units and - must not be written. Passing this field is a no-op (ignored with warning). - Project / operator strings (max 41 ASCII characters each) ---------------------------- project : Project description. @@ -861,7 +857,6 @@ class DeviceConfigBody(BaseModel): # Threshold parameters trigger_level_geo: Optional[float] = None alarm_level_geo: Optional[float] = None - max_range_geo: Optional[float] = None # Project / operator strings project: Optional[str] = None client_name: Optional[str] = None @@ -920,7 +915,6 @@ def device_config( record_time=body.record_time, trigger_level_geo=body.trigger_level_geo, alarm_level_geo=body.alarm_level_geo, - max_range_geo=body.max_range_geo, project=body.project, client_name=body.client_name, operator=body.operator, diff --git a/sfm/sfm_webapp.html b/sfm/sfm_webapp.html index d59ea41..db3745b 100644 --- a/sfm/sfm_webapp.html +++ b/sfm/sfm_webapp.html @@ -831,11 +831,6 @@
Alarm flagged when any geo channel exceeds this level
-
- - -
Full-scale calibration constant — only change if you have a cal cert
-
@@ -1037,7 +1032,7 @@ let unitInfo = null; let eventList = []; let currentEvent = 0; let charts = {}; -let geoRange = 6.206; +let geoAdcScale = 6.206; const DBL_REF = 2.9e-9; // 20 µPa in psi — reference pressure for dBL const CHANNEL_COLORS = { Tran:'#58a6ff', Vert:'#3fb950', Long:'#d29922', Mic:'#bc8cff' }; @@ -1189,7 +1184,7 @@ function populateDeviceBar() { qs('di-project').textContent = cc.project || '—'; qs('di-client').textContent = cc.client || '—'; qs('di-operator').textContent = cc.operator || '—'; - geoRange = cc.max_range_geo ?? 6.206; + geoAdcScale = cc.geo_adc_scale ?? 6.206; } // ── Monitoring ───────────────────────────────────────────────────────────────── @@ -1331,7 +1326,7 @@ function populateDeviceTab() { ['Record Time', cc.record_time != null ? `${cc.record_time.toFixed(2)} s` : '—'], ['Trigger Level (geo)', cc.trigger_level_geo != null ? `${cc.trigger_level_geo.toFixed(4)} in/s` : '—'], ['Alarm Level (geo)', cc.alarm_level_geo != null ? `${cc.alarm_level_geo.toFixed(4)} in/s` : '—'], - ['Max Range (geo)', cc.max_range_geo != null ? `${cc.max_range_geo.toFixed(4)} in/s` : '—'], + ['ADC Scale Factor (geo)', cc.geo_adc_scale != null ? `${cc.geo_adc_scale.toFixed(4)} in/s` : '—'], ['Setup Name', cc.setup_name || '—'], ]; renderTable('compliance-table', complianceRows); @@ -1366,7 +1361,6 @@ function populateConfigFromDeviceInfo() { if (cc.record_time != null) qs('cfg-record-time', cc.record_time.toFixed(1)); if (cc.trigger_level_geo != null) qs('cfg-trigger', cc.trigger_level_geo.toFixed(4)); if (cc.alarm_level_geo != null) qs('cfg-alarm', cc.alarm_level_geo.toFixed(4)); - if (cc.max_range_geo != null) qs('cfg-max-range',cc.max_range_geo.toFixed(4)); if (cc.project) qs('cfg-project', cc.project); if (cc.client) qs('cfg-client', cc.client); if (cc.operator) qs('cfg-operator', cc.operator); @@ -1375,7 +1369,7 @@ function populateConfigFromDeviceInfo() { } function clearConfigForm() { - ['cfg-sample-rate','cfg-record-time','cfg-trigger','cfg-alarm','cfg-max-range', + ['cfg-sample-rate','cfg-record-time','cfg-trigger','cfg-alarm', 'cfg-project','cfg-client','cfg-operator','cfg-seis-loc','cfg-notes'] .forEach(id => { const el = qs(id); el.tagName === 'SELECT' ? el.selectedIndex = 0 : el.value = ''; }); setCfgStatus(''); @@ -1413,8 +1407,6 @@ async function writeConfig() { if (trig) body.trigger_level_geo = parseFloat(trig); const alarm = qs('cfg-alarm').value; if (alarm) body.alarm_level_geo = parseFloat(alarm); - const mr = qs('cfg-max-range').value; - if (mr) body.max_range_geo = parseFloat(mr); const proj = qs('cfg-project').value.trim(); if (proj) body.project = proj; const cli = qs('cfg-client').value.trim(); @@ -1565,7 +1557,7 @@ function renderWaveform(data) { let plotData, peakLabel, yUnit, ttFmt, tickFmt; if (isGeo) { - const scale = geoRange / 32767; + const scale = geoAdcScale / 32767; plotData = samples.map(s => s * scale); // Use the device-recorded peak from the 0C waveform record — authoritative // and matches Blastware. Computing from raw samples can catch rogue diff --git a/sfm/waveform_viewer.html b/sfm/waveform_viewer.html index 8770544..dc2d3ef 100644 --- a/sfm/waveform_viewer.html +++ b/sfm/waveform_viewer.html @@ -240,7 +240,7 @@ let charts = {}; let lastData = null; let unitInfo = null; - let geoRange = 10.0; // in/s full-scale for geo channels; updated on connect + let geoAdcScale = 10.0; // in/s full-scale for geo channels; updated on connect let eventList = []; // populated from /device/events after connect let currentEventIndex = 0; @@ -278,7 +278,7 @@ throw new Error(err.detail || resp.statusText); } unitInfo = await resp.json(); - geoRange = unitInfo.compliance_config?.max_range_geo ?? 10.0; + geoAdcScale = unitInfo.compliance_config?.geo_adc_scale ?? 10.0; } catch (e) { setStatus(`Error: ${e.message}`, 'error'); btn.disabled = false; @@ -457,7 +457,7 @@ if (isGeo) { // Geo channels: counts × (range / 32767) → in/s - const scale = geoRange / 32767; + const scale = geoAdcScale / 32767; plotSamples = samples.map(c => c * scale); const peakIns = Math.max(...plotSamples.map(Math.abs)); peakLabel = `${peakIns.toFixed(5)} in/s`;