From 7f322f9ff9dc105ff3567b4f31879f74072dc357 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Mon, 13 Apr 2026 18:23:27 -0400 Subject: [PATCH] feat: add option to restart monitoring after event download in AchSession --- bridges/ach_server.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/bridges/ach_server.py b/bridges/ach_server.py index f3fc6e4..d7363cf 100644 --- a/bridges/ach_server.py +++ b/bridges/ach_server.py @@ -139,6 +139,7 @@ class AchSession: state_path: Path, db: "SeismoDb", clear_after_download: bool = False, + restart_monitoring: bool = False, ) -> None: self.sock = sock self.peer = peer @@ -149,6 +150,7 @@ class AchSession: self.state_path = state_path self.db = db self.clear_after_download = clear_after_download + self.restart_monitoring = restart_monitoring def run(self) -> None: ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -319,6 +321,38 @@ class AchSession: "peer": self.peer, } _save_state(self.state_path, state) + + # ── Erase even when no new events (if requested) ────────── + # Blastware ACH always erases after every session — even when + # nothing new was downloaded. Without the erase the device + # still sees stored events in its memory and immediately + # retries the call-home, causing the looping we observed. + # Only erase when device actually has events stored; skip + # the erase if device_keys is empty (nothing to erase). + if self.clear_after_download and device_keys: + log.info( + " Clearing device memory (--clear-after-download, " + "no new events but device has %d stored)...", + len(device_keys), + ) + try: + client.delete_all_events() + log.info(" [OK] Device memory cleared") + # Reset state so the next session starts fresh. + state[unit_key] = { + "downloaded_keys": [], + "max_downloaded_key": "00000000", + "last_seen": datetime.datetime.now().isoformat(), + "serial": serial, + "peer": self.peer, + } + _save_state(self.state_path, state) + except Exception as exc: + log.error( + " [WARN] Event deletion failed: %s -- events NOT cleared", + exc, + ) + log.info("Session complete (no new events) -> %s", session_dir) return else: @@ -489,6 +523,15 @@ class AchSession: except Exception as exc: log.error(" [FAIL] Event download failed: %s", exc, exc_info=True) + # ── Optional: restart monitoring after successful download ───────── + if self.restart_monitoring: + log.info(" Restarting monitoring on device (--restart-monitoring)...") + try: + client.start_monitoring() + log.info(" [OK] Monitoring restarted") + except Exception as exc: + log.warning(" [WARN] Failed to restart monitoring: %s", exc) + finally: raw_fh.close() client.close() # closes transport / socket cleanly @@ -593,6 +636,7 @@ def serve(args: argparse.Namespace) -> None: print(f" State file: {state_path}") print(f" Max events per session: {max_ev if max_ev else 'unlimited'}") print(f" Clear device after download: {'YES' if args.clear_after_download else 'no'}") + print(f" Restart monitoring after download: {'YES' if args.restart_monitoring else 'no'}") print(f"{'='*60}") print(f"\n Point your test unit's ACEmanager call-home settings to:") print(f" Remote Host: ") @@ -631,6 +675,7 @@ def serve(args: argparse.Namespace) -> None: state_path=state_path, db=db, clear_after_download=args.clear_after_download, + restart_monitoring=args.restart_monitoring, ) t = threading.Thread(target=session.run, daemon=True, name=f"ach-{peer}") t.start() @@ -694,6 +739,16 @@ def parse_args() -> argparse.Namespace: "If not specified, all IPs are accepted (not recommended for public servers)." ), ) + p.add_argument( + "--restart-monitoring", + action="store_true", + default=False, + help=( + "After downloading events, send SUB 0x96 (start monitoring) before " + "disconnecting. Required for RV55 units whose firmware does not assert " + "DCD on disconnect — without this the unit stays idle after a call-home." + ), + ) p.add_argument( "--clear-after-download", action="store_true",