feat(forward): SFM event forwarder (v1.5.0) #8
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.5.3] - 2026-05-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Forward log lines now distinguish histogram events from waveform-without-report.** Previously every event without a paired `.TXT` report logged "no report", which on machines running histogram-mode units (extensions ending in `H`, e.g. `H907L1R7.PG0H`) generated alarming-looking lines on every single event when the lack of report was actually completely normal — Blastware doesn't auto-export ASCII reports for histograms. Three log states now:
|
||||||
|
- **Waveform + paired TXT**: `+ <txt> attached`
|
||||||
|
- **Waveform without TXT**: `no report ⚠` (suggests checking BW's "Save Event Report" setting)
|
||||||
|
- **Histogram (any flavour)**: `(histogram, no report expected)`
|
||||||
|
- New `is_histogram_event(filename)` helper classifies BW filenames by extension (4-char ext ending in `H` = histogram; old-firmware 3-char extensions remain unclassifiable and default to non-histogram for safe defaults).
|
||||||
|
- 1 new unit test for histogram classification.
|
||||||
|
|
||||||
## [1.5.2] - 2026-05-10
|
## [1.5.2] - 2026-05-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Series 3 Watcher v1.5.2
|
# Series 3 Watcher v1.5.3
|
||||||
|
|
||||||
Monitors Instantel **Series 3 (Minimate)** call-in activity on a Blastware server. Runs as a **system tray app** that starts automatically on login, reports heartbeats to terra-view, and self-updates from Gitea.
|
Monitors Instantel **Series 3 (Minimate)** call-in activity on a Blastware server. Runs as a **system tray app** that starts automatically on login, reports heartbeats to terra-view, and self-updates from Gitea.
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ All settings live in `config.ini`. The Setup Wizard covers every field, but here
|
|||||||
| `UPDATE_SOURCE` | `gitea` (default) or `url` — where to check for updates |
|
| `UPDATE_SOURCE` | `gitea` (default) or `url` — where to check for updates |
|
||||||
| `UPDATE_URL` | Base URL of the update server when `UPDATE_SOURCE = url` (e.g. terra-view URL). The watcher fetches `/api/updates/series3-watcher/version.txt` and `/api/updates/series3-watcher/series3-watcher.exe` from this base. |
|
| `UPDATE_URL` | Base URL of the update server when `UPDATE_SOURCE = url` (e.g. terra-view URL). The watcher fetches `/api/updates/series3-watcher/version.txt` and `/api/updates/series3-watcher/series3-watcher.exe` from this base. |
|
||||||
|
|
||||||
### SFM Event Forwarder (v1.5.2+)
|
### SFM Event Forwarder (v1.5.3+)
|
||||||
|
|
||||||
Forwards each Blastware event binary (and its paired `<binary>.TXT` ASCII report when present) to an SFM server's `/db/import/blastware_file` endpoint, where the report is parsed and the rich per-channel stats (PPV, ZC Freq, Time of Peak, Peak Acceleration / Displacement, sensor self-check) land in a searchable database. **Default-off** — existing deployments keep their old behaviour after auto-updating until the operator opts in.
|
Forwards each Blastware event binary (and its paired `<binary>.TXT` ASCII report when present) to an SFM server's `/db/import/blastware_file` endpoint, where the report is parsed and the rich per-channel stats (PPV, ZC Freq, Time of Peak, Peak Acceleration / Displacement, sensor self-check) land in a searchable database. **Default-off** — existing deployments keep their old behaviour after auto-updating until the operator opts in.
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ To view connected watchers: **Settings → Developer → Watcher Manager**.
|
|||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
Follows **Semantic Versioning**. Current release: **v1.5.2**.
|
Follows **Semantic Versioning**. Current release: **v1.5.3**.
|
||||||
See `CHANGELOG.md` for full history.
|
See `CHANGELOG.md` for full history.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+37
-5
@@ -101,6 +101,23 @@ def report_path_for(binary_path: str) -> str:
|
|||||||
return binary_path + ".TXT"
|
return binary_path + ".TXT"
|
||||||
|
|
||||||
|
|
||||||
|
def is_histogram_event(filename: str) -> bool:
|
||||||
|
"""True if the filename's extension marks the file as a Full Histogram
|
||||||
|
event (BW filename scheme: 4-char extensions of the form ``AB0T`` where
|
||||||
|
``T = H``). Old-firmware events use 3-char extensions where waveform-vs-
|
||||||
|
histogram is not encoded in the name; we can't tell those apart and
|
||||||
|
return False (the conservative answer — we don't want to suppress
|
||||||
|
"no report" warnings on potentially-waveform old-firmware events).
|
||||||
|
|
||||||
|
Used purely for log clarity — histograms don't get auto-exported BW
|
||||||
|
ASCII reports, so "no report" on a histogram is not a problem to
|
||||||
|
flag. Forwarding logic itself doesn't depend on this check.
|
||||||
|
"""
|
||||||
|
name = os.path.basename(filename)
|
||||||
|
ext = os.path.splitext(name)[1].lstrip(".").upper()
|
||||||
|
return len(ext) == 4 and ext.endswith("H")
|
||||||
|
|
||||||
|
|
||||||
# ── State file ────────────────────────────────────────────────────────────────
|
# ── State file ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -513,12 +530,27 @@ def forward_pending(
|
|||||||
counts["forwarded"] += 1
|
counts["forwarded"] += 1
|
||||||
if txt_path:
|
if txt_path:
|
||||||
counts["with_report"] += 1
|
counts["with_report"] += 1
|
||||||
|
|
||||||
|
# Differentiate three cases in the log so "no report" is only
|
||||||
|
# noisy when something's actually unexpected:
|
||||||
|
# - waveform + TXT → "+ <txt> attached"
|
||||||
|
# - waveform without TXT → "no report ⚠" (BW maybe didn't auto-export)
|
||||||
|
# - histogram (any flavour) → "(histogram, no report expected)"
|
||||||
|
if txt_path:
|
||||||
|
report_token = "+ {} attached".format(os.path.basename(txt_path))
|
||||||
|
elif is_histogram_event(binary_path):
|
||||||
|
report_token = "(histogram, no report expected)"
|
||||||
|
else:
|
||||||
|
report_token = "no report ⚠"
|
||||||
|
|
||||||
_log(
|
_log(
|
||||||
f"[forward] OK {os.path.basename(binary_path)} "
|
"[forward] OK {} ({}B, {}, inserted={}, skipped={})".format(
|
||||||
f"({result.get('filesize', 0)}B, "
|
os.path.basename(binary_path),
|
||||||
f"{'with' if txt_path else 'no'} report, "
|
result.get("filesize", 0),
|
||||||
f"inserted={result.get('inserted', 0)}, "
|
report_token,
|
||||||
f"skipped={result.get('skipped', 0)})"
|
result.get("inserted", 0),
|
||||||
|
result.get("skipped", 0),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
counts["errors"] += 1
|
counts["errors"] += 1
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppName=Series 3 Watcher
|
AppName=Series 3 Watcher
|
||||||
AppVersion=1.5.2
|
AppVersion=1.5.3
|
||||||
AppPublisher=Terra-Mechanics Inc.
|
AppPublisher=Terra-Mechanics Inc.
|
||||||
DefaultDirName={pf}\Series3Watcher
|
DefaultDirName={pf}\Series3Watcher
|
||||||
DefaultGroupName=Series 3 Watcher
|
DefaultGroupName=Series 3 Watcher
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Series 3 Watcher — System Tray Launcher v1.5.2
|
Series 3 Watcher — System Tray Launcher v1.5.3
|
||||||
Requires: pystray, Pillow, tkinter (stdlib)
|
Requires: pystray, Pillow, tkinter (stdlib)
|
||||||
|
|
||||||
Run with: pythonw series3_tray.py (no console window)
|
Run with: pythonw series3_tray.py (no console window)
|
||||||
|
|||||||
+1
-1
@@ -247,7 +247,7 @@ def scan_latest(
|
|||||||
|
|
||||||
|
|
||||||
# --- API heartbeat / SFM telemetry helpers ---
|
# --- API heartbeat / SFM telemetry helpers ---
|
||||||
VERSION = "1.5.2"
|
VERSION = "1.5.3"
|
||||||
|
|
||||||
|
|
||||||
def _read_log_tail(log_file: str, n: int = 25) -> Optional[list]:
|
def _read_log_tail(log_file: str, n: int = 25) -> Optional[list]:
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Series 3 Watcher — Settings Dialog v1.5.2
|
Series 3 Watcher — Settings Dialog v1.5.3
|
||||||
|
|
||||||
Provides a Tkinter settings dialog that doubles as a first-run wizard.
|
Provides a Tkinter settings dialog that doubles as a first-run wizard.
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ class TestIsEventBinary(unittest.TestCase):
|
|||||||
"something.h5", "noise.json"]:
|
"something.h5", "noise.json"]:
|
||||||
self.assertFalse(ef.is_event_binary(name), name)
|
self.assertFalse(ef.is_event_binary(name), name)
|
||||||
|
|
||||||
|
def test_is_histogram_event(self):
|
||||||
|
# 4-char extension ending in H = histogram
|
||||||
|
for name in ["H907L1R7.PG0H", "S353L4H0.8S0H", "P036L318.C80H"]:
|
||||||
|
self.assertTrue(ef.is_histogram_event(name), name)
|
||||||
|
# 4-char extension ending in W = waveform
|
||||||
|
for name in ["S353L4H0.3M0W", "M529LKVQ.6S0W", "P036L318.C80W"]:
|
||||||
|
self.assertFalse(ef.is_histogram_event(name), name)
|
||||||
|
# 3-char old-firmware extensions can't be classified — return False
|
||||||
|
for name in ["M529LK44.AB0", "M529LIY6.N00", "M529LJ8V.490"]:
|
||||||
|
self.assertFalse(ef.is_histogram_event(name), name)
|
||||||
|
|
||||||
def test_rejects_non_matching_filenames(self):
|
def test_rejects_non_matching_filenames(self):
|
||||||
for name in ["", "no_extension",
|
for name in ["", "no_extension",
|
||||||
"TooShort.AB0", # stem must be 8 chars
|
"TooShort.AB0", # stem must be 8 chars
|
||||||
|
|||||||
Reference in New Issue
Block a user