improved logging

This commit is contained in:
Ayzen
2026-06-06 00:52:52 +03:00
parent af6005d68f
commit aea49f6128
65 changed files with 1206 additions and 240 deletions
@@ -10,7 +10,6 @@ from .enums import (
SweepScale,
SyncMode,
)
from .logging_utils import DEFAULT_LOG_LEVEL, configure_logging
from .exceptions import (
CRCError,
DeviceDisconnectedError,
@@ -39,7 +38,6 @@ from .models import (
__all__ = [
"CRCError",
"DEFAULT_LOG_LEVEL",
"DeviceConfigVariant",
"DeviceDisconnectedError",
"DeviceInfo",
@@ -55,7 +53,6 @@ __all__ = [
"PacketType",
"ParseError",
"ProtocolVersionMismatch",
"configure_logging",
"SParameter",
"StreamHandle",
"SweepKind",
@@ -44,7 +44,18 @@ class GeneratorController:
timeout_s: float,
poll_interval_s: float,
) -> DeviceStatus:
"""Wait until available lock flags report the generator is ready."""
"""Poll device status until the source/LO lock flags report the generator
is ready, then return that status.
Polls every ``poll_interval_s`` seconds up to ``timeout_s`` total. Raises
``TimeoutError`` if the locks do not assert in time and ``RuntimeError`` if
the connected hardware family exposes no lock telemetry.
"""
logger.info(
"Waiting for generator lock (timeout=%.2fs, poll_interval=%.2fs)",
timeout_s,
poll_interval_s,
)
deadline = time.monotonic() + timeout_s
@@ -53,13 +64,19 @@ class GeneratorController:
lock_values = [value for value in (status.source_locked, status.lo_locked) if value is not None]
if lock_values:
if all(lock_values):
logger.info("Generator locked (family=%s)", status.family.name)
return status
else:
logger.error(
"Generator lock telemetry unavailable for hardware family %s",
status.family.name,
)
raise RuntimeError(
f"Generator lock telemetry is unavailable for hardware family {status.family.name}"
)
remaining = deadline - time.monotonic()
if remaining <= 0:
logger.warning("Timed out waiting for generator lock after %.2fs", timeout_s)
raise TimeoutError("Timed out waiting for LibreVNA generator lock")
time.sleep(min(poll_interval_s, remaining))
@@ -1,52 +0,0 @@
"""Logging helpers for applications embedding ``librevna_driver``.
The library uses standard ``logging`` module loggers under the
``librevna_driver`` namespace and never configures global logging implicitly.
Use :func:`configure_logging` in scripts/services when you want a convenient
default console setup.
"""
from __future__ import annotations
import logging
_LOGGER_NAMESPACE = "librevna_driver"
_DEFAULT_FORMAT = (
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
)
DEFAULT_LOG_LEVEL = "INFO"
def configure_logging(
level: int | str | None = None,
*,
fmt: str = _DEFAULT_FORMAT,
datefmt: str | None = "%Y-%m-%d %H:%M:%S",
) -> None:
"""Configure package logger with one stream handler.
This helper affects only the ``librevna_driver`` logger tree and is safe to
call repeatedly (previous handlers attached by this function are replaced).
When ``level`` is ``None``, :data:`DEFAULT_LOG_LEVEL` is used.
"""
effective_level = level if level is not None else DEFAULT_LOG_LEVEL
logger = logging.getLogger(_LOGGER_NAMESPACE)
logger.handlers.clear()
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt))
logger.addHandler(handler)
logger.setLevel(_parse_level(effective_level))
logger.propagate = False
def _parse_level(level: int | str) -> int:
"""Parse numeric or textual log level into logging constant."""
if isinstance(level, int):
return level
normalized = level.strip().upper()
if normalized in logging.getLevelNamesMapping():
return logging.getLevelNamesMapping()[normalized]
raise ValueError(f"Unknown logging level: {level!r}")
@@ -251,13 +251,14 @@ class SweepResult:
return self.trace(parameter).imag
def to_npz(self, path: str) -> None:
"""Save result as NumPy `.npz` archive."""
"""Save the axis and all traces to a NumPy `.npz` archive at ``path``."""
data: dict[str, np.ndarray] = {self.x_label: self.x}
data.update(self.traces)
np.savez(path, **data)
logger.debug("Saved SweepResult to NPZ: %s (traces=%d)", path, len(self.traces))
def to_csv(self, path: str) -> None:
"""Save result as CSV with `<trace>_real`/`<trace>_imag` columns."""
"""Save the axis and traces to CSV at ``path``, with `<trace>_real`/`<trace>_imag` columns."""
columns: list[np.ndarray] = [self.x]
headers: list[str] = [self.x_label]
for name, values in sorted(self.traces.items()):
@@ -267,6 +268,7 @@ class SweepResult:
headers.append(f"{name}_imag")
matrix = np.column_stack(columns)
np.savetxt(path, matrix, delimiter=",", header=",".join(headers), comments="")
logger.debug("Saved SweepResult to CSV: %s (traces=%d)", path, len(self.traces))
PacketPayload = Any