MeshTools is a Rich+Questionary interactive / Typer-scriptable toolkit for tuning a MeshCore mesh over a serial companion device, with SQLite-logged runs and Plotly visualizations. Includes the skeleton (trace, info, history), TX-power optimization, and a MockDevice simulator (--mock). Device discovery & selection: - core/discovery.py: enumerate serial ports (pyserial), flag/sort likely LoRa hardware by USB vendor id, stable_id for replug-safe identity. - core/device_store.py: remember the last successfully connected device in <config_dir>/devices.json (independent of --db). - core/selection.py: non-interactive resolver (--port > profile > remembered > sole device > guidance error) for the CLI. - ui/device_picker.py + menu startup: interactive picker preselecting the remembered default; banner reflects the choice. - tools/devices.py: `meshtools devices` lists/marks attached devices. - AppContext resolves the port and records last-known-good on connect success. Fixes: - theme.py: force UTF-8 on stdout/stderr so glyphs don't crash legacy consoles. - connection.py: friendly message when meshcore is missing; pin meshcore>=2.3. - trace/tx-optimize: fall back to free-text target entry when the device reports no contacts (autocomplete requires non-empty choices). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
2.6 KiB
Python
94 lines
2.6 KiB
Python
"""Structured logging setup.
|
|
|
|
Two sinks are configured: a Rich-formatted console handler for human-readable output,
|
|
and a JSON-lines file handler that captures every record for later replay/analysis.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from rich.console import Console
|
|
from rich.logging import RichHandler
|
|
|
|
_LOGGER_NAME = "meshtools"
|
|
|
|
|
|
class JsonlFormatter(logging.Formatter):
|
|
"""Format log records as one JSON object per line."""
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
"""Serialize a log record to a compact JSON string.
|
|
|
|
Args:
|
|
record: The record to format.
|
|
|
|
Returns:
|
|
A single-line JSON document.
|
|
"""
|
|
payload = {
|
|
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
|
|
"level": record.levelname,
|
|
"logger": record.name,
|
|
"msg": record.getMessage(),
|
|
}
|
|
if record.exc_info:
|
|
payload["exc"] = self.formatException(record.exc_info)
|
|
return json.dumps(payload)
|
|
|
|
|
|
def configure_logging(
|
|
console: Console,
|
|
log_dir: Path,
|
|
*,
|
|
level: int = logging.INFO,
|
|
quiet: bool = False,
|
|
) -> logging.Logger:
|
|
"""Configure and return the application logger.
|
|
|
|
Args:
|
|
console: The Rich console the handler should render to.
|
|
log_dir: Directory for the ``meshtools.log.jsonl`` event file.
|
|
level: Console log level.
|
|
quiet: When ``True``, suppress console output (file logging continues).
|
|
|
|
Returns:
|
|
The configured ``meshtools`` logger.
|
|
"""
|
|
logger = logging.getLogger(_LOGGER_NAME)
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.handlers.clear()
|
|
logger.propagate = False
|
|
|
|
if not quiet:
|
|
rich_handler = RichHandler(
|
|
console=console, rich_tracebacks=True, show_path=False, markup=True
|
|
)
|
|
rich_handler.setLevel(level)
|
|
logger.addHandler(rich_handler)
|
|
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
file_handler = logging.FileHandler(log_dir / "meshtools.log.jsonl", encoding="utf-8")
|
|
file_handler.setLevel(logging.DEBUG)
|
|
file_handler.setFormatter(JsonlFormatter())
|
|
logger.addHandler(file_handler)
|
|
|
|
return logger
|
|
|
|
|
|
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
"""Return a child of the application logger.
|
|
|
|
Args:
|
|
name: Optional child name; when ``None`` the root app logger is returned.
|
|
|
|
Returns:
|
|
The requested logger.
|
|
"""
|
|
if name is None:
|
|
return logging.getLogger(_LOGGER_NAME)
|
|
return logging.getLogger(f"{_LOGGER_NAME}.{name}")
|