main: full CLI config with network/serial/file sources and JSON project export
- --network NAME HOST PORT (repeatable) - --serial NAME DEVICE BAUD (repeatable) - --file PATH (repeatable, replayed via FileBackend) - --log-dir DIR (frame logging, default ./log) - --delay SECS (file replay inter-frame delay, default 1.0) - --project FILE load config from JSON project file - --save FILE export current config as JSON project file Multiple source types can be combined. Network sources share one NetworkBackend; serial sources each get their own SerialBackend; file sources share one FileBackend. The poll loop runs only for network sources; file-only mode joins the reader threads; serial-only mode blocks on signal.pause(). file_backend: rename _source_id -> source_id_from_path (public), add join() to wait for reader threads. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import re
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from backend.a_backend import ABackend
|
||||
from msg.container import MsgContainer
|
||||
|
||||
_FILENAME_RE = re.compile(r'\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}_(.+)\.log$')
|
||||
_CHUNK = 64
|
||||
|
||||
|
||||
def source_id_from_path(path: str) -> str | None:
|
||||
m = _FILENAME_RE.search(path)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
class FileBackend(ABackend):
|
||||
def __init__(self, files: list[str], delay: float = 1.0):
|
||||
self._delay = max(0.0, min(10.0, delay))
|
||||
self._entries = [{'path': p, 'source_id': source_id_from_path(p), 'xcvr': None, 'file': None, 'thread': None}
|
||||
for p in files]
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def delay(self) -> float:
|
||||
return self._delay
|
||||
|
||||
@delay.setter
|
||||
def delay(self, value: float):
|
||||
self._delay = max(0.0, min(10.0, value))
|
||||
|
||||
def register_xcvr(self, xcvr):
|
||||
for e in self._entries:
|
||||
if e['source_id'] == xcvr.name.strip():
|
||||
e['xcvr'] = xcvr
|
||||
xcvr.on_register(self)
|
||||
return
|
||||
raise Exception(f"No log file found for source \"{xcvr.name}\"")
|
||||
|
||||
def connect(self):
|
||||
for e in self._entries:
|
||||
e['file'] = open(e['path'], 'rb')
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
for e in self._entries:
|
||||
e['thread'] = Thread(target=self._run, args=(e,), daemon=True)
|
||||
e['thread'].start()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
for e in self._entries:
|
||||
if e['thread'] and e['thread'].is_alive():
|
||||
e['thread'].join(timeout=self._delay + 1.0)
|
||||
|
||||
def join(self):
|
||||
for e in self._entries:
|
||||
if e['thread']:
|
||||
e['thread'].join()
|
||||
|
||||
def disconnect(self):
|
||||
for e in self._entries:
|
||||
if e['file']:
|
||||
e['file'].close()
|
||||
e['file'] = None
|
||||
|
||||
def _run(self, entry: dict):
|
||||
xcvr = entry['xcvr']
|
||||
f = entry['file']
|
||||
while self._running:
|
||||
data = f.read(_CHUNK)
|
||||
if not data:
|
||||
break
|
||||
if xcvr:
|
||||
xcvr.on_recv(MsgContainer(data, xcvr))
|
||||
if self._delay > 0:
|
||||
time.sleep(self._delay)
|
||||
Reference in New Issue
Block a user