85 lines
2.4 KiB
Python
85 lines
2.4 KiB
Python
"""Append-only structured log for host-side BLE events.
|
|
|
|
One JSON line per event, in `config/.ble_debug.log`. Written from the BLE
|
|
thread; reads happen from anywhere. Trimmed to the most recent N lines on
|
|
each write so the file doesn't grow forever.
|
|
|
|
This is independent of Python's `logging` module so it survives across
|
|
restarts and gets written even if the GUI crashes.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from utils.constants import APPDATA_DIR
|
|
|
|
_LOG_PATH = Path(APPDATA_DIR) / ".ble_debug.log"
|
|
_MAX_LINES = 2000 # trimmed when exceeded
|
|
|
|
_lock = threading.Lock()
|
|
|
|
|
|
def _trim_locked() -> None:
|
|
try:
|
|
with open(_LOG_PATH, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
except FileNotFoundError:
|
|
return
|
|
if len(lines) <= _MAX_LINES:
|
|
return
|
|
keep = lines[-_MAX_LINES:]
|
|
tmp = _LOG_PATH.with_suffix(".tmp")
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
f.writelines(keep)
|
|
os.replace(tmp, _LOG_PATH)
|
|
|
|
|
|
def event(_event_name: str, **fields) -> None:
|
|
"""Append a structured event line.
|
|
|
|
First positional arg is the event name (renamed to avoid collision with
|
|
a `kind` keyword that some call sites legitimately want to log).
|
|
`fields` are arbitrary JSON-serializable extras.
|
|
"""
|
|
rec = {
|
|
"t": time.time(),
|
|
"ts": time.strftime("%H:%M:%S", time.localtime()),
|
|
"kind": _event_name,
|
|
}
|
|
rec.update(fields)
|
|
line = json.dumps(rec, default=str) + "\n"
|
|
with _lock:
|
|
try:
|
|
_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(_LOG_PATH, "a", encoding="utf-8") as f:
|
|
f.write(line)
|
|
# Cheap probabilistic trim: only check size every ~50 lines.
|
|
if int(rec["t"] * 1000) % 50 == 0:
|
|
_trim_locked()
|
|
except Exception:
|
|
# Logging failures must not break BLE.
|
|
pass
|
|
# Mirror to stdout for live debugging. The variable was previously
|
|
# named `kind` which raised NameError and silently suppressed every
|
|
# stdout mirror via the broad except — fixed to use the local arg.
|
|
try:
|
|
print(f"[BLE.dbg {rec['ts']}] {_event_name} "
|
|
f"{json.dumps(fields, default=str)}")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def path() -> str:
|
|
return str(_LOG_PATH)
|
|
|
|
|
|
def clear() -> None:
|
|
with _lock:
|
|
try:
|
|
_LOG_PATH.unlink()
|
|
except FileNotFoundError:
|
|
pass
|