diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-08-02 01:34:43 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-08-02 01:34:43 -0600 |
| commit | a3aa1df99df8f413cac2ba6020b7cd0dec6d2390 (patch) | |
| tree | a4c5feca6b0db326d9e54f417abc132c1270a7a3 /core/debug_log.py | |
| parent | f5066a8ca2fb50aa3dddf2c8847e52574cdde6ad (diff) | |
| parent | f1aaffbc3eb1e2c154315c556d2555803eea7997 (diff) | |
Merge origin/main: reconcile ConfigWindow consolidation with Debug window + protocol updates
origin/main (23 commits) added a Debug window/log system on top of the old
separate Devices/Channels/Plot windows, plus protocol fixes (cml, mark10,
modbus_rtu, scpi) and device/profile changes. Local main (3 commits)
replaced the separate windows with a unified ConfigWindow + dock panel.
Kept local's ConfigWindow/dock architecture and ported the Debug window
onto it: new toolbar button + _open_debug(), gated by developer_mode same
as origin's version. Deduped the two independent "developer mode" toggles
that had collided in SettingsWindow (origin's General-tab checkbox gating
Debug window + device sim-mode visibility, local's Advanced-tab checkbox
setting verbose logging) into one General-tab checkbox that does both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'core/debug_log.py')
| -rw-r--r-- | core/debug_log.py | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/core/debug_log.py b/core/debug_log.py new file mode 100644 index 0000000..86a8a59 --- /dev/null +++ b/core/debug_log.py @@ -0,0 +1,65 @@ +""" +core/debug_log.py + +Tees stdout/stderr into an in-memory ring buffer + Qt signal so the Debug +window can show everything the app has printed since startup — including +messages from background poll threads (e.g. "[CMLLayer] poll error: ...") — +not just whatever gets printed while the window happens to be open. + +install() should be called once, early, before anything prints. The real +streams are still written to, so running from a terminal is unaffected. +""" + +from __future__ import annotations + +import sys +from collections import deque +from typing import Optional + +from PyQt6.QtCore import QObject, pyqtSignal + + +class _Broadcaster(QObject): + line_written = pyqtSignal(str) + + +class _StreamTee: + def __init__(self, real_stream, lines: deque, broadcaster: _Broadcaster): + self._real = real_stream + self._lines = lines + self._broadcaster = broadcaster + + def write(self, text: str) -> None: + self._real.write(text) + if text: + self._lines.append(text) + self._broadcaster.line_written.emit(text) + + def flush(self) -> None: + self._real.flush() + + def isatty(self) -> bool: + return False + + +_lines: Optional[deque] = None +_broadcaster: Optional[_Broadcaster] = None + + +def install(max_lines: int = 2000) -> None: + """Redirect sys.stdout/sys.stderr through the tee. Safe to call once.""" + global _lines, _broadcaster + if _broadcaster is not None: + return + _lines = deque(maxlen=max_lines) + _broadcaster = _Broadcaster() + sys.stdout = _StreamTee(sys.stdout, _lines, _broadcaster) + sys.stderr = _StreamTee(sys.stderr, _lines, _broadcaster) + + +def get_broadcaster() -> Optional[_Broadcaster]: + return _broadcaster + + +def get_history() -> str: + return "".join(_lines) if _lines is not None else "" |
