utils.datalog_bridge

Bridge Python logging -> wpilib DataLogManager.

Call setup_logging() once at robot init to:

  1. Forward all Python logging output into .wpilog files
  2. Create a persistent NT entry at /robot/logLevel that allows the dashboard to change the log level at runtime

Accepted level strings (case-insensitive): debug, info, warn, warning, error, critical, fatal, off. WPILib-style k-prefixed names (kDebug, kInfo, etc.) are also accepted.

  1"""Bridge Python logging -> wpilib DataLogManager.
  2
  3Call ``setup_logging()`` once at robot init to:
  4  1. Forward all Python ``logging`` output into .wpilog files
  5  2. Create a persistent NT entry at ``/robot/logLevel`` that allows
  6     the dashboard to change the log level at runtime
  7
  8Accepted level strings (case-insensitive): debug, info, warn, warning,
  9error, critical, fatal, off.  WPILib-style k-prefixed names (kDebug,
 10kInfo, etc.) are also accepted.
 11"""
 12
 13import logging
 14
 15import ntcore
 16import wpilib
 17
 18
 19class _WPILogHandler(logging.Handler):
 20    """Forwards Python log records into the wpilog 'messages' entry."""
 21
 22    def emit(self, record: logging.LogRecord) -> None:
 23        try:
 24            msg = self.format(record)
 25            wpilib.DataLogManager.log(msg)
 26        except Exception:
 27            self.handleError(record)
 28
 29
 30# Normalized string -> Python logging level
 31_LOG_LEVEL_MAP: dict[str, int] = {
 32    "debug": logging.DEBUG,
 33    "kdebug": logging.DEBUG,
 34    "info": logging.INFO,
 35    "kinfo": logging.INFO,
 36    "warn": logging.WARNING,
 37    "warning": logging.WARNING,
 38    "kwarning": logging.WARNING,
 39    "error": logging.ERROR,
 40    "kerror": logging.ERROR,
 41    "critical": logging.CRITICAL,
 42    "kcritical": logging.CRITICAL,
 43    "fatal": logging.CRITICAL,
 44    "off": logging.CRITICAL + 10,
 45    "koff": logging.CRITICAL + 10,
 46}
 47
 48# References kept alive to prevent GC of NT subscribers/publishers
 49_nt_refs: list = []
 50_initialized: bool = False
 51
 52
 53def _on_log_level_changed(event: ntcore.Event) -> None:
 54    """NT listener callback — updates root logger level."""
 55    value = event.data.value.getString()
 56    level = _LOG_LEVEL_MAP.get(value.strip().lower())
 57    if level is not None:
 58        logging.getLogger().setLevel(level)
 59        # Use print to avoid feeding back through the WPILog handler
 60        print(f"Log level changed to {value.strip()} ({level})")
 61
 62
 63def setup_logging(default_level: int = logging.INFO) -> None:
 64    """Initialize the Python logging -> wpilog bridge.
 65
 66    - Attaches a handler to the root logger that forwards all records
 67      to ``DataLogManager.log()``.
 68    - Sets the root logger to *default_level*.
 69    - Creates a persistent NT string entry at ``/robot/logLevel``
 70      with a listener that updates the root level on change.
 71
 72    Safe to call multiple times — subsequent calls are no-ops.
 73    """
 74    global _initialized
 75    if _initialized:
 76        return
 77    _initialized = True
 78
 79    # Attach wpilog handler to root logger
 80    handler = _WPILogHandler()
 81    handler.setFormatter(logging.Formatter(
 82        "%(name)s [%(levelname)s] %(message)s"))
 83    root = logging.getLogger()
 84    root.addHandler(handler)
 85    root.setLevel(default_level)
 86
 87    # NT-controlled log level (persistent, callback-driven)
 88    inst = ntcore.NetworkTableInstance.getDefault()
 89    topic = inst.getStringTopic("/robot/logLevel")
 90
 91    publisher = topic.publish(
 92        ntcore.PubSubOptions(keepDuplicates=False))
 93    publisher.setDefault("INFO")
 94    inst.getTable("/robot").getEntry("logLevel").setPersistent()
 95
 96    subscriber = topic.subscribe("INFO")
 97    inst.addListener(
 98        subscriber,
 99        ntcore.EventFlags.kValueRemote | ntcore.EventFlags.kImmediate,
100        _on_log_level_changed,
101    )
102
103    # Prevent GC from collecting the publisher/subscriber
104    _nt_refs.extend([publisher, subscriber])
def setup_logging(default_level: int = 20) -> None:
 64def setup_logging(default_level: int = logging.INFO) -> None:
 65    """Initialize the Python logging -> wpilog bridge.
 66
 67    - Attaches a handler to the root logger that forwards all records
 68      to ``DataLogManager.log()``.
 69    - Sets the root logger to *default_level*.
 70    - Creates a persistent NT string entry at ``/robot/logLevel``
 71      with a listener that updates the root level on change.
 72
 73    Safe to call multiple times — subsequent calls are no-ops.
 74    """
 75    global _initialized
 76    if _initialized:
 77        return
 78    _initialized = True
 79
 80    # Attach wpilog handler to root logger
 81    handler = _WPILogHandler()
 82    handler.setFormatter(logging.Formatter(
 83        "%(name)s [%(levelname)s] %(message)s"))
 84    root = logging.getLogger()
 85    root.addHandler(handler)
 86    root.setLevel(default_level)
 87
 88    # NT-controlled log level (persistent, callback-driven)
 89    inst = ntcore.NetworkTableInstance.getDefault()
 90    topic = inst.getStringTopic("/robot/logLevel")
 91
 92    publisher = topic.publish(
 93        ntcore.PubSubOptions(keepDuplicates=False))
 94    publisher.setDefault("INFO")
 95    inst.getTable("/robot").getEntry("logLevel").setPersistent()
 96
 97    subscriber = topic.subscribe("INFO")
 98    inst.addListener(
 99        subscriber,
100        ntcore.EventFlags.kValueRemote | ntcore.EventFlags.kImmediate,
101        _on_log_level_changed,
102    )
103
104    # Prevent GC from collecting the publisher/subscriber
105    _nt_refs.extend([publisher, subscriber])

Initialize the Python logging -> wpilog bridge.

  • Attaches a handler to the root logger that forwards all records to DataLogManager.log().
  • Sets the root logger to default_level.
  • Creates a persistent NT string entry at /robot/logLevel with a listener that updates the root level on change.

Safe to call multiple times — subsequent calls are no-ops.