Files
m5stack-automation-tool/ble_debug_log.py
T
2026-07-17 15:29:53 -04:00

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