diff --git a/nucleares_monitor/control_board_monitor.py b/nucleares_monitor/control_board_monitor.py index 1b1e50e..1fd30d3 100644 --- a/nucleares_monitor/control_board_monitor.py +++ b/nucleares_monitor/control_board_monitor.py @@ -12,6 +12,7 @@ What’s new: """ import json +import logging import math import queue import re @@ -23,8 +24,6 @@ import urllib.request from dataclasses import dataclass, field from datetime import datetime from typing import Dict, List, Optional, Tuple, Deque -import math # NOWE: do decymacji i obliczeń -import traceback import signal import sys @@ -37,8 +36,8 @@ os.environ.setdefault("QT_AUTO_SCREEN_SCALE_FACTOR", "0") try: import faulthandler as _fh -except Exception: - _fh = None # brak modułu lub inny problem – pomijamy +except ImportError: + _fh = None # module not available - continue without faulthandler def _fh_install_signal_dump(sig_name: str): """Próbuje włączyć dump stacka pod danym sygnałem. @@ -56,85 +55,76 @@ def _fh_install_signal_dump(sig_name: str): try: _fh.register(sig, file=sys.stderr, all_threads=True) return - except Exception: - # Brak wsparcia / błąd rejestracji – spróbuj fallbacku - pass + except (AttributeError, OSError) as e: + logger.debug(f"Failed to register faulthandler for signal {sig_name}: {e}") # Fallback: zwykły handler sygnału, który zrzuci stack wszystkich wątków def _dump(_signo, _frame): try: _fh.dump_traceback(file=sys.stderr, all_threads=True) - except Exception: - raise RuntimeError("Faulthandler dump_traceback failed") + except (AttributeError, OSError) as e: + logger.debug(f"Failed to dump traceback for signal {sig_name}: {e}") + # Log and continue - diagnostics should not crash the app + print(f"Faulthandler dump_traceback failed: {e}", file=sys.stderr) try: signal.signal(sig, _dump) - except Exception: + except Exception as e: # Nie udało się – trudno, po prostu odpuszczamy ten sygnał - raise RuntimeError(f"Faulthandler signal registration failed for {sig_name}") + print(f"Faulthandler signal registration failed for {sig_name}: {e}", file=sys.stderr) + +# Configure logging first to support error reporting in other initialization +import logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s', + datefmt='%H:%M:%S', + handlers=[ + logging.FileHandler('cbm_debug.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger('ControlBoardMonitor') # Włącz faulthandler globalnie (o ile jest) if _fh is not None: try: _fh.enable(all_threads=True) - except Exception: + except (AttributeError, OSError) as e: # Nie blokuj uruchomienia aplikacji – to tylko narzędzie diagnostyczne - pass + logger.debug(f"Failed to enable faulthandler: {e}") # Spróbuj podczepić kilka sensownych sygnałów; ignoruj, jeśli ich nie ma na danej platformie for _sig_name in ("SIGBREAK", "SIGTERM", "SIGINT"): _fh_install_signal_dump(_sig_name) # --- koniec bezpiecznej inicjalizacji faulthandlera --- -DEBUG_LOG = True # możesz wyłączyć na False gdy już będzie stabilnie - +# Legacy compatibility for existing debug functions def DBGL(msg: str): - if not DEBUG_LOG: - return - try: - from datetime import datetime - with open("cbm_debug.log", "a", encoding="utf-8") as f: - f.write(f"{datetime.now().strftime('%H:%M:%S.%f')} {msg}\n") - except Exception: - DBGEX("DBGL") + """Legacy debug log function - use logger.debug() instead""" + logger.debug(msg) def DBGEX(where: str): - DBGL(f"[EXC] {where}\n{traceback.format_exc()}") + """Legacy debug exception function - use logger.exception() instead""" + logger.exception(f"Exception in {where}") # 3) global excepthook (main thread) def _global_excepthook(exctype, value, tb): - DBGL("[GLOBAL EXC] " + "".join(traceback.format_exception(exctype, value, tb))) + logger.critical("Unhandled exception in main thread", exc_info=(exctype, value, tb)) sys.__excepthook__(exctype, value, tb) sys.excepthook = _global_excepthook # 4) thread excepthook (Python 3.8+) def _thread_excepthook(args): - DBGL("[THREAD EXC] " + "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback))) + logger.critical("Unhandled exception in thread", exc_info=(args.exc_type, args.exc_value, args.exc_traceback)) threading.excepthook = _thread_excepthook # 5) Qt message handler (jeśli PySide6 / pyqtgraph dostępne) try: from PySide6 import QtCore def _qt_msg_handler(mode, ctx, message): - DBGL(f"[QT] {message}") + logger.debug(f"[QT] {message}") QtCore.qInstallMessageHandler(_qt_msg_handler) -except Exception: - DBGEX("Qt qInstallMessageHandler") - -SAFE_GLOBALS = { - "__builtins__": {}, - "abs": abs, "min": min, "max": max, "round": round, - "int": int, "float": float, "pow": pow, "math": math, -} - -def safe_eval(expr: str, symbols: dict): - """Bardzo ograniczony eval – tylko dopuszczone funkcje i przekazane symbole.""" - clean = {k: v for k, v in (symbols or {}).items() if v is not None} - return eval(expr, SAFE_GLOBALS, clean) - -def eval_user_expression(expr: str, x, y, z) -> float: - """Eval wyrażenia użytkownika (np. w schedulerze) na (x,y,z).""" - val = safe_eval(expr, {"x": x, "y": y, "z": z}) - return float(val) - +except ImportError as e: + logger.debug(f"Qt qInstallMessageHandler not available: {e}") # Spójna warstwa Qt z pyqtgraph (nie mieszamy bezpośrednio PyQt5/PySide6) _pyqtgraph_available = False @@ -143,28 +133,28 @@ try: import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtWidgets _pyqtgraph_available = True -except Exception as e: - DBGEX(f"pyqtgraph import {e}") +except ImportError as e: + logger.info(f"pyqtgraph not available: {e}") _pyqtgraph_unavailable_reason = str(e) try: import tkinter as tk from tkinter import ttk, messagebox -except Exception: - raise SystemExit("Tkinter is required to run this app.") +except ImportError as e: + logger.critical(f"Tkinter is required to run this app: {e}") + raise SystemExit("Tkinter is required to run this app.") from e # Optional plotting support try: - from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg - from matplotlib.figure import Figure HAS_MPL = True import matplotlib as mpl mpl.rcParams["path.simplify"] = True mpl.rcParams["agg.path.chunksize"] = 10000 PLOT_SCALE = -200 - -except Exception: - DBGEX("matplotlib import") + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + from matplotlib.figure import Figure +except ImportError as e: + logger.info(f"matplotlib not available: {e}") HAS_MPL = False # --- Daemon thread spawner ---------------------------------------------------- def spawn_daemon(name: str, target, *args, **kwargs): @@ -199,7 +189,7 @@ def build_base_url(host: str, port: int) -> str: return f"http://{host}:{port}/" def _request(req: urllib.request.Request) -> Tuple[int, str, Dict[str, str]]: - DBGL(f"[HTTP] {req}") + logger.debug(f"[HTTP] {req}") with _HTTP_LOCK: try: with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_S) as resp: @@ -208,39 +198,39 @@ def _request(req: urllib.request.Request) -> Tuple[int, str, Dict[str, str]]: body_bytes = resp.read() try: body = body_bytes.decode("utf-8", errors="replace") - except Exception: - DBGEX("_request decode utf-8") + except UnicodeDecodeError as e: + logger.warning(f"Failed to decode response as UTF-8: {e}") body = body_bytes.decode("latin-1", errors="replace") headers = {k.lower(): v for k, v in resp.getheaders()} return status, body, headers except urllib.error.HTTPError as e: try: body = e.read().decode("utf-8", errors="replace") - except Exception: - DBGEX("_request HTTPError decode utf-8") + except Exception as e: + logger.exception("Failed to decode HTTP error response as UTF-8") body = str(e) return e.code, body, dict(e.headers or {}) except Exception as e: - DBGEX("_request") + logger.exception("HTTP request failed") return 0, str(e), {} def http_get(base_url: str, params: Dict[str, str]) -> Tuple[int, str, Dict[str, str]]: url = base_url + "?" + urllib.parse.urlencode(params) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Connection": "close"}) - DBGL(f"[HTTP] GET {url}") + logger.debug(f"[HTTP] GET {url}") return _request(req) def http_post_query(base_url: str, params: Dict[str, str]) -> Tuple[int, str, Dict[str, str]]: # EXACT behavior requested: POST with query string, no body. url = base_url + "?" + urllib.parse.urlencode(params) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Connection": "close"}, method="POST") - DBGL(f"[HTTP] POST {url}") + logger.debug(f"[HTTP] POST {url}") return _request(req) def http_get_root(base_url: str) -> Tuple[int, str, Dict[str, str]]: req = urllib.request.Request(base_url, headers={"User-Agent": USER_AGENT, "Connection": "close"}) - DBGL(f"[HTTP] GET ROOT {base_url}") + logger.debug(f"[HTTP] GET ROOT {base_url}") return _request(req) def coerce_preview(value: str, maxlen: int = 80) -> str: @@ -253,10 +243,12 @@ def parse_function_names_from_html_index(html_text: str) -> List[str]: return [] post_html = html_text[start:] names = re.findall(r"([A-Z0-9_]+)", post_html) - seen = set(); uniq: List[str] = [] + seen = set() + uniq: List[str] = [] for n in names: if n not in seen: - seen.add(n); uniq.append(n) + seen.add(n) + uniq.append(n) return uniq def parse_variable_names_from_html_index(html_text: str) -> List[str]: @@ -269,10 +261,12 @@ def parse_variable_names_from_html_index(html_text: str) -> List[str]: tokens.append(v) for m in re.finditer(r'(?:Variable|variable)\s*=\s*["\']?([A-Za-z0-9_.:-]+)', html_text, flags=re.I): tokens.append(m.group(1)) - seen = set(); uniq: List[str] = [] + seen = set() + uniq: List[str] = [] for v in tokens: if v and v.upper() not in {"VARNAME", "VARIABLE", "NAME"} and v not in seen: - seen.add(v); uniq.append(v) + seen.add(v) + uniq.append(v) return uniq # ---- Parse WEBSERVER_LIST_VARIABLES_JSON ---- @@ -280,8 +274,8 @@ def parse_weblist_names(body: str) -> Tuple[List[str], List[str]]: """Return (get_list, post_list). Keep discovered case for function names.""" try: data = json.loads(body) - except Exception: - DBGEX("parse_weblist_names json.loads") + except (json.JSONDecodeError, ValueError) as e: + logger.warning(f"Failed to parse JSON response: {e}") return [], [] get_names: List[str] = [] post_names: List[str] = [] @@ -299,10 +293,12 @@ def parse_weblist_names(body: str) -> Tuple[List[str], List[str]]: get_names.extend(extract_names_preserve_case(data)) def dedup(seq): - seen=set(); out=[] + seen = set() + out = [] for s in seq: if s not in seen: - seen.add(s); out.append(s) + seen.add(s) + out.append(s) return out return dedup(get_names), dedup(post_names) @@ -335,8 +331,8 @@ def extract_names_preserve_case(value_obj) -> List[str]: def parse_batch_values(body: str) -> Dict[str, str]: try: data = json.loads(body) - except Exception: - DBGEX("parse_batch_values json.loads") + except (json.JSONDecodeError, ValueError) as e: + logger.warning(f"Failed to parse JSON batch values: {e}") return {} # Jeśli serwer owinął odpowiedź w {"values": {...}}, wyciągamy środek: if isinstance(data, dict) and isinstance(data.get("values"), dict): @@ -348,8 +344,8 @@ def parse_batch_values(body: str) -> Dict[str, str]: k = str(k) try: values[k] = v if isinstance(v, str) else json.dumps(v) - except Exception: - DBGEX("parse_batch_values json.dumps") + except (TypeError, ValueError) as e: + logger.warning(f"Failed to serialize batch value for key '{k}': {e}") values[k] = str(v) return values def detect_batch_payload_type(body: str) -> str: @@ -359,8 +355,8 @@ def detect_batch_payload_type(body: str) -> str: return "enveloped(values+errors)" if isinstance(data, dict): return "flat" - except Exception: - DBGEX("detect_batch_payload_type json.loads") + except (json.JSONDecodeError, ValueError) as e: + logger.debug(f"Failed to parse JSON for payload type detection: {e}") pass return "unknown" def eval_threshold_expr(expr: str, @@ -391,8 +387,8 @@ def eval_threshold_expr(expr: str, # jeżeli masz już safe_eval w kodzie – użyj go; inaczej zwykły eval bez builtins val = safe_eval(expr, env) if 'safe_eval' in globals() else eval(expr, {"__builtins__": {}}, env) return float(val) - except Exception: - DBGEX("eval_threshold_expr") + except (NameError, SyntaxError, ValueError, TypeError, ZeroDivisionError) as e: + logger.warning(f"Failed to evaluate threshold expression '{expr}': {e}") return None # ===================== @@ -541,8 +537,8 @@ class ActionScheduler(threading.Thread): def gv(n): try: return self.get_value_cb(n) if n else None - except Exception: - DBGEX("Scheduler.run_task_once->get_value_cb") + except Exception as e: + logger.exception("Scheduler.run_task_once->get_value_cb") return None # stara wersja: # x = gv(task.x_src); y = gv(task.y_src); z = gv(task.z_src) @@ -571,7 +567,7 @@ class ActionScheduler(threading.Thread): computed = eval_user_expression(task.expr, x, y, z) value_to_send = str(computed) except Exception as e: - DBGEX(f"Scheduler.run_task_once->eval_user_expression: {e}") + logger.exception(f"Scheduler.run_task_once->eval_user_expression: {e}") # Return synthetic error without posting return 0, f"Expression error: {e}", {} base = self.get_base_url_cb() @@ -613,9 +609,9 @@ class ActionScheduler(threading.Thread): t.enabled = False else: t.next_run = t.last_run + max(0.0, float(t.interval_s)) - except Exception: + except Exception as e: # nie blokujemy pętli scheduler’a na wyjątkach z pojedynczego taska - DBGEX("Scheduler.run->run_task_once") + logger.exception("Scheduler.run->run_task_once") # krótka, przerywalna drzemka # (nie używamy time.sleep; dzięki temu zamknięcie jest natychmiastowe) @@ -647,7 +643,7 @@ class Poller(threading.Thread): def run(self) -> None: base_url = build_base_url(self.host, self.port) - DBGL("[Poller] start") + logger.debug("[Poller] start") while not self.stop_event.is_set(): cycle_start = time.time() @@ -656,14 +652,14 @@ class Poller(threading.Thread): self.ui_queue.put(("cycle_start", datetime.now().strftime("%H:%M:%S"))) # główny odczyt (BATCH lub fallback na pojedyncze) status, body, _ = http_get(base_url, {"variable": "WEBSERVER_BATCH_GET"}) - except Exception: - DBGEX("Poller.run->http_get(BATCH_GET)") + except Exception as e: + logger.exception("Poller.run->http_get(BATCH_GET)") status, body, _ = None, None, None if status == 200: try: values_map = parse_batch_values(body) - except Exception: - DBGEX("Poller.run->parse_batch_values") + except Exception as e: + logger.exception("Poller.run->parse_batch_values") values_map = None if values_map: lower_map = {k.lower(): v for k, v in values_map.items()} @@ -681,8 +677,8 @@ class Poller(threading.Thread): self.ui_queue.put(("update", key, b, st)) else: self.ui_queue.put(("error", key, st, coerce_preview(b, 200))) - except Exception: - DBGEX(f"Poller.run->http_get({key})") + except Exception as e: + logger.exception(f"Poller.run->http_get({key})") else: for key in list(self.variables_keys): st, b, _h = http_get(base_url, {"variable": key}) @@ -690,9 +686,9 @@ class Poller(threading.Thread): self.ui_queue.put(("update", key, b, st)) else: self.ui_queue.put(("error", key, st, coerce_preview(b, 200))) - except Exception: + except Exception as e: # nigdy nie wywalamy wątku na zewnątrz - DBGEX("Poller.run(main loop)") + logger.exception("Poller.run(main loop)") # OBSŁUGA PAUZY – aktywnie czekamy, ale przerywalnie while self.paused_event.is_set() and not self.stop_event.is_set(): @@ -711,12 +707,12 @@ class Poller(threading.Thread): chunk = min(0.1, remaining - waited) self.stop_event.wait(chunk) waited += chunk - DBGL("[Poller] stop") + logger.debug("[Poller] stop") # sygnalizacja zakończenia try: self.ui_queue.put(("stopped", "")) - except Exception: - DBGEX("Poller.run(stop)") + except Exception as e: + logger.exception("Poller.run(stop)") # ===================== @@ -730,6 +726,11 @@ class App(tk.Tk): # Canonical registry: lower-case key -> VarInfo self.vars: Dict[str, VarInfo] = {} + # Thread-safe data protection and snapshots + self._data_lock = threading.Lock() + self._cached_avg_window = 60 # Cache avg window value for thread safety + self._cached_host = SERVER_HOST # Cache host value for thread safety + self._cached_port = SERVER_PORT # Cache port value for thread safety self.stop_event = threading.Event() self.paused_event = threading.Event() self.ui_queue: queue.Queue = queue.Queue() @@ -739,8 +740,12 @@ class App(tk.Tk): self._closing = False self._stop_event = threading.Event() + # Window tracking initialization + self._plot_windows: Dict[str, object] = {} # Track plotting windows + self._qt_windows: Dict[str, tuple] = {} # Track Qt windows for cleanup + self.scheduler = ActionScheduler( - lambda: build_base_url(self.host_var.get().strip(), int(self.port_var.get())), + self._get_base_url_threadsafe, self.get_stats_for ) self.scheduler.start() @@ -752,6 +757,28 @@ class App(tk.Tk): self.port_var = tk.IntVar(value=SERVER_PORT) self.refresh_var = tk.DoubleVar(value=REFRESH_INTERVAL_S) self.avg_window_var = tk.IntVar(value=AVERAGE_WINDOW_N) + # Initialize cached values + self._cached_avg_window = AVERAGE_WINDOW_N + + # Add thread-safe callbacks for host/port changes + def _update_host(*_): + try: + with self._data_lock: + self._cached_host = self.host_var.get().strip() + except Exception: + with self._data_lock: + self._cached_host = SERVER_HOST + + def _update_port(*_): + try: + with self._data_lock: + self._cached_port = int(self.port_var.get()) + except Exception: + with self._data_lock: + self._cached_port = SERVER_PORT + + self.host_var.trace_add("write", _update_host) + self.port_var.trace_add("write", _update_port) ttk.Label(controls, text="Host:").grid(row=0, column=0, sticky="w") ttk.Entry(controls, textvariable=self.host_var, width=16).grid(row=0, column=1, padx=(0, 10)) @@ -793,7 +820,18 @@ class App(tk.Tk): vsb.pack(side=tk.RIGHT, fill=tk.Y) self.tree.configure(yscrollcommand=vsb.set) self._bind_mousewheel(self.tree) - self.avg_window_var.trace_add("write", lambda *_: (self.tree.heading("avg", text=f"Avg ({self.avg_window_var.get()})"), self.tree.heading("davg", text=f"Δ avg ({self.avg_window_var.get()})"))) + # Thread-safe avg window update callback + def _update_avg_window(*_): + try: + with self._data_lock: + self._cached_avg_window = max(1, int(self.avg_window_var.get())) + except Exception: + with self._data_lock: + self._cached_avg_window = 60 + self.tree.heading("avg", text=f"Avg ({self.avg_window_var.get()})") + self.tree.heading("davg", text=f"Δ avg ({self.avg_window_var.get()})") + + self.avg_window_var.trace_add("write", _update_avg_window) self.tree.heading("#0", text="Variable") self.tree.heading("value", text="Value") @@ -812,8 +850,10 @@ class App(tk.Tk): self.tree.column("status", width=60, anchor="center") style = ttk.Style(self) - try: style.theme_use("clam") - except Exception: DBGEX("App.__init__ theme_use clam") + try: + style.theme_use("clam") + except Exception as e: + logger.warning(f"Failed to set tkinter theme: {e}") # Context menu self.tree_menu = tk.Menu(self, tearoff=False) @@ -838,7 +878,7 @@ class App(tk.Tk): columns2 = ("func", "value", "mode", "interval", "next", "enabled") self.actions_tree = ttk.Treeview(bottom, columns=columns2, show="headings", height=10, selectmode="browse") - for col, hdr, w in zip(columns2, ["Function", "Value", "Mode", "Interval(s)", "Next Run", "Enabled"], [280, 160, 100, 100, 180, 80]): + for col, hdr, w in zip(columns2, ["Function", "Value", "Mode", "Interval(s)", "Next Run", "Enabled"], [280, 160, 100, 100, 180, 80], strict=True): self.actions_tree.heading(col, text=hdr) self.actions_tree.column(col, width=w, anchor="center") @@ -946,8 +986,8 @@ class App(tk.Tk): """Utwórz (raz) QApplication – wspólną dla wszystkich okien Qt.""" try: from pyqtgraph.Qt import QtWidgets - except Exception: - DBGEX("Qt import in _qt_ensure_app") + except Exception as e: + logger.exception("Qt import in _qt_ensure_app") self._qt_app = None return if getattr(self, "_qt_app", None) is None: @@ -972,7 +1012,7 @@ class App(tk.Tk): if not has_visible_qt: self._qt_pump_on = False except Exception as e: - DBGEX(f"[qt] stop-pump check error: {e!r}") + logger.exception(f"[qt] stop-pump check error: {e!r}") # Na wszelki wypadek nie wyłączaj pompy na błędzie def _pump_qt_events(self): @@ -1001,8 +1041,8 @@ class App(tk.Tk): finally: self._qt_pumping = False - except Exception: - DBGEX("_pump_qt_events") + except Exception as e: + logger.exception("_pump_qt_events") finally: # Pętla cykliczna – ale tylko jeśli aplikacja nie zamyka się i wciąż mamy okna QT if not getattr(self, "_closing", False) and any( @@ -1014,16 +1054,22 @@ class App(tk.Tk): def _safe_bool(self, tkvar, default=False): try: return bool(tkvar.get()) - except Exception: - DBGEX("get_safe_bool") + except Exception as e: + logger.exception("get_safe_bool") return default + def _get_base_url_threadsafe(self) -> str: + """Thread-safe version of getting base URL using cached values.""" + with self._data_lock: + return build_base_url(self._cached_host, self._cached_port) def get_stats_for(self, name: Optional[str]) -> Optional[dict]: if not name: return None key = str(name).lower() - vi = self.vars.get(key) + + with self._data_lock: + vi = self.vars.get(key) def _avg(dq, n): if not dq or n <= 0 or len(dq) < n: @@ -1031,15 +1077,12 @@ class App(tk.Tk): lst = list(dq)[-n:] try: return sum(lst) / float(n) - except Exception: - DBGEX("get_stats_for avg") + except Exception as e: + logger.exception("get_stats_for avg") return None - try: - n = max(1, int(self.avg_window_var.get())) - except Exception: - DBGEX("get_stats_for avg_window_var") - n = 60 + # Use cached value instead of Tkinter variable to ensure thread safety + n = max(1, self._cached_avg_window) if vi: return { @@ -1080,7 +1123,8 @@ class App(tk.Tk): for k in values_map.keys(): kl = k.lower() if kl not in seen: - seen.add(kl); discovered_vars.append(k) + seen.add(kl) + discovered_vars.append(k) else: msgs.append(f"BATCH_GET HTTP {s2}: {coerce_preview(b2,120)}") @@ -1098,7 +1142,8 @@ class App(tk.Tk): # 4) embedded defaults used_defaults = False if not discovered_vars: - discovered_vars = list(DEFAULT_VARS); used_defaults = True + discovered_vars = list(DEFAULT_VARS) + used_defaults = True if not discovered_funcs: discovered_funcs = list(DEFAULT_FUNCTIONS) @@ -1419,8 +1464,8 @@ class App(tk.Tk): if getattr(self, "_plot_timer", None) is None and not getattr(self, "_closing", False): try: ms = int(max(100, float(self.refresh_var.get()) * 1000)) # ~10 FPS max (wg refresh) - except Exception: - DBGEX("_ensure_plot_timer refresh_var") + except Exception as e: + logger.exception("_ensure_plot_timer refresh_var") ms = 200 self._plot_timer = self.after(ms, self._plot_tick) @@ -1460,21 +1505,21 @@ class App(tk.Tk): if qt is not None: try: vis = bool(qt.isVisible()) - except Exception: - DBGEX("pyqtgraph isVisible") + except Exception as e: + logger.exception("pyqtgraph isVisible") vis = True if vis: alive.append((key, win)) else: # nieznane (pomijamy) pass - except Exception: - DBGEX("_plot_tick(check window alive)") + except Exception as e: + logger.exception("_plot_tick(check window alive)") # okno padło – usuwamy z rejestru try: self._plot_windows.pop(key, None) - except Exception: - DBGEX("_plot_tick(remove dead window)") + except Exception as e: + logger.exception("_plot_tick(remove dead window)") pass if not alive: @@ -1495,8 +1540,8 @@ class App(tk.Tk): # Tk if hasattr(win, "title"): win.title(new_title) - except Exception: - DBGEX("_plot_tick(update title Tk)") + except Exception as e: + logger.exception("_plot_tick(update title Tk)") pass try: # Qt @@ -1504,8 +1549,8 @@ class App(tk.Tk): qtw = getattr(win, "_qt_widget", None) if qtw is not None and hasattr(qtw, "setWindowTitle"): qtw.setWindowTitle(new_title) - except Exception: - DBGEX("_plot_tick(update title Qt)") + except Exception as e: + logger.exception("_plot_tick(update title Qt)") pass win._last_title = new_title @@ -1517,13 +1562,13 @@ class App(tk.Tk): # Pobranie danych (per-okno liczba próbek i max rys. punktów) try: n = int(getattr(win, "_sample_len_var", self.default_samples_var).get()) - except Exception: - DBGEX("_plot_tick(get sample length)") + except Exception as e: + logger.exception("_plot_tick(get sample length)") n = int(self.default_samples_var.get()) try: max_pts = int(getattr(win, "_max_draw_var", self.default_maxpts_var).get()) - except Exception: - DBGEX("_plot_tick(get max points)") + except Exception as e: + logger.exception("_plot_tick(get max points)") max_pts = int(self.default_maxpts_var.get()) vals = list(vi.history)[-n:] if hasattr(vi, "history") else [] @@ -1544,15 +1589,15 @@ class App(tk.Tk): self._plot_update_pyqtgraph(win, xs_v, vals_draw, xs_b, dels_draw, thr) win._last_seen_ver = getattr(vi, "hist_ver", win._last_seen_ver) - except Exception: - DBGEX("_plot_tick(one window)") + except Exception as e: + logger.exception("_plot_tick(one window)") continue # kolejny tick try: ms = int(max(100, float(self.refresh_var.get()) * 1000)) - except Exception: - DBGEX("_plot_tick(refresh_var)") + except Exception as e: + logger.exception("_plot_tick(refresh_var)") ms = 200 if not getattr(self, "_closing", False): self._plot_timer = self.after(ms, self._plot_tick) @@ -1621,41 +1666,43 @@ class App(tk.Tk): kind = item[0] if kind == "update": _, key, value, status = item - vi = self.vars.get(key) - if not vi: - vi = VarInfo(key=key, display_name=key) - prev_num = parse_first_float(vi.last_value) - curr_num = parse_first_float(value) - vi.last_value = value - vi.last_updated = time.time() - vi.last_status = status - vi.error = None - # analytics update - if prev_num is not None and curr_num is not None: - vi.delta_last = curr_num - prev_num - try: - vi.history_delta.append(vi.delta_last) - except Exception: - DBGEX("Scheduler.run_task_once->get_value_cb") - pass - else: - vi.delta_last = None - if curr_num is not None: - try: - vi.history.append(curr_num) - vi.hist_ver += 1 # NOWE: sygnał dla wykresów, że są nowe dane - except Exception: - DBGEX("Scheduler.run_task_once->get_value_cb") - pass - self.vars[key] = vi + with self._data_lock: + vi = self.vars.get(key) + if not vi: + vi = VarInfo(key=key, display_name=key) + prev_num = parse_first_float(vi.last_value) + curr_num = parse_first_float(value) + vi.last_value = value + vi.last_updated = time.time() + vi.last_status = status + vi.error = None + # analytics update + if prev_num is not None and curr_num is not None: + vi.delta_last = curr_num - prev_num + try: + vi.history_delta.append(vi.delta_last) + except Exception as e: + logger.exception("Scheduler.run_task_once->get_value_cb") + pass + else: + vi.delta_last = None + if curr_num is not None: + try: + vi.history.append(curr_num) + vi.hist_ver += 1 # NOWE: sygnał dla wykresów, że są nowe dane + except Exception as e: + logger.exception("Scheduler.run_task_once->get_value_cb") + pass + self.vars[key] = vi self.evaluate_thresholds(vi, value) elif kind == "error": _, key, status, msg = item - vi = self.vars.get(key) or VarInfo(key=key, display_name=key) - vi.last_status = status - vi.error = msg - vi.last_updated = time.time() - self.vars[key] = vi + with self._data_lock: + vi = self.vars.get(key) or VarInfo(key=key, display_name=key) + vi.last_status = status + vi.error = msg + vi.last_updated = time.time() + self.vars[key] = vi elif kind == "cycle_start": _, hhmmss = item self.cycle_lbl.configure(text=f"Last cycle start: {hhmmss}") @@ -1666,7 +1713,8 @@ class App(tk.Tk): # refresh known_variables list from batch keys (preserve case with existing where possible) keys = list(mapping.keys()) # prefer existing display names from self.vars, else use given keys - disp = [self.vars.get(k.lower()).display_name if self.vars.get(k.lower()) else k for k in keys] + with self._data_lock: + disp = [self.vars.get(k.lower()).display_name if self.vars.get(k.lower()) else k for k in keys] self.known_variables = sorted(set(list(self.known_variables) + disp), key=str.lower) elif kind == "stopped": self.status_lbl.configure(text="Stopped") @@ -1926,8 +1974,8 @@ class App(tk.Tk): return None, None try: return float(s), None - except Exception: - DBGEX("App.__init__ theme_use clam") + except Exception as e: + logger.exception("App.__init__ theme_use clam") # potraktuj jako wyrażenie (np. "x_avg - 5") return None, s @@ -1982,8 +2030,8 @@ class App(tk.Tk): try: cur_x = parse_first_float(vi.last_value) self.on_state_change(vi, vi.last_state, vi.last_state, cur_x) - except Exception: - DBGEX("App.open_thresholds_dialog->do_save on_state_change") + except Exception as e: + logger.exception("App.open_thresholds_dialog->do_save on_state_change") pass dlg.destroy() self._reschedule_state_tasks_for(vi) @@ -2063,16 +2111,16 @@ class App(tk.Tk): if tid is not None: try: self.scheduler.remove_task(tid) - except Exception: - DBGEX("App.on_state_change remove_task") + except Exception as e: + logger.exception("App.on_state_change remove_task") pass # stop any repeating expr task for previous state etid = self.expr_state_tasks.pop((vi.key, prev_state), None) if etid is not None: try: self.scheduler.remove_task(etid) - except Exception: - DBGEX("App.on_state_change remove_task expr") + except Exception as e: + logger.exception("App.on_state_change remove_task expr") pass # then handle enter actions for new_state self.on_enter_state(vi, new_state, x) @@ -2103,10 +2151,10 @@ class App(tk.Tk): freq = {"DEAD_LOW": 350, "LOW": 550, "HIGH": 800, "EXTREME_HIGH": 1000}.get(state, 600) dur = 400 if state in ("DEAD_LOW", "EXTREME_HIGH") else 300 winsound.Beep(freq, dur) - except Exception: - DBGEX("App.on_enter_state winsound.Beep") + except Exception as e: + logger.exception("App.on_enter_state winsound.Beep") try: self.bell() - except Exception: DBGEX("App.on_enter_state bell") + except Exception as e: logger.exception("App.on_enter_state bell") expr_key = { "DEAD_LOW": "expr_dead_low", "LOW": "expr_low", @@ -2259,8 +2307,8 @@ class App(tk.Tk): self._color_tags[hex_color] = tag try: self.tree.tag_configure(tag, background=hex_color) - except Exception: - DBGEX("App._get_color_tag") + except Exception as e: + logger.exception("App._get_color_tag") pass return tag def _effective_thresholds(self, vi:"VarInfo") -> dict: @@ -2270,7 +2318,7 @@ class App(tk.Tk): Liczy progi efektywne: bierzemy liczby z konfiguracji + nadpisujemy je, jeśli zdefiniowano formuły expr_thr_*. Zwracamy też 'mid'=(low+high)/2 jeżeli możliwe. """ - DBGL(f"[_thr] key={vi.key} start") + logger.debug(f"[_thr] key={vi.key} start") t = vi.thresholds stats_x = self.get_stats_for(vi.key) stats_y = self.get_stats_for(getattr(t, "y_source", None)) @@ -2279,13 +2327,13 @@ class App(tk.Tk): def eff(num, expr): if expr: try: - DBGL(f"[_thr] expr {expr} for {vi.key} -> try") + logger.debug(f"[_thr] expr {expr} for {vi.key} -> try") v = eval_threshold_expr(expr, stats_x, stats_y, stats_z) - DBGL(f"[_thr] expr {expr} for {vi.key} -> {v}") + logger.debug(f"[_thr] expr {expr} for {vi.key} -> {v}") if v is not None: return float(v) - except Exception: - DBGEX("eval_threshold_expr") + except Exception as e: + logger.exception("eval_threshold_expr") return float(num) if num is not None else None dead = eff(getattr(t, "dead_low", None), getattr(t, "expr_thr_dead_low", None)) @@ -2293,7 +2341,7 @@ class App(tk.Tk): high = eff(getattr(t, "high", None), getattr(t, "expr_thr_high", None)) ext = eff(getattr(t, "extreme_high", None), getattr(t, "expr_thr_extreme_high", None)) mid = (low + high) / 2.0 if (low is not None and high is not None) else None - DBGL(f"[_thr] key={vi.key} done -> dead={dead} low={low} high={high} ext={ext} mid={mid}") + logger.debug(f"[_thr] key={vi.key} done -> dead={dead} low={low} high={high} ext={ext} mid={mid}") return {"dead_low": dead, "low": low, "high": high, "extreme": ext, "mid": mid} def evaluate_thresholds(self, vi: VarInfo, value: str) -> None: @@ -2328,8 +2376,8 @@ class App(tk.Tk): self.on_state_change(vi, prev, state, x) vi.last_state = state - except Exception: - DBGEX("evaluate_thresholds") + except Exception as e: + logger.exception("evaluate_thresholds") def _state_color(self, vi, x_value=None) -> str: """Kolor tła w hex na bazie progów efektywnych; odporne na None.""" try: @@ -2341,8 +2389,8 @@ class App(tk.Tk): try: x = float(x_value) if x_value is not None else parse_first_float(vi.last_value) - except Exception: - DBGEX("_state_color parse_first_float") + except Exception as e: + logger.exception("_state_color parse_first_float") x = None if x is None: return "#E9ECEF" # brak danych @@ -2382,8 +2430,8 @@ class App(tk.Tk): denom = (thr_ext - thr_high) if (thr_ext > thr_high) else 1.0 t = (x - thr_high) / max(denom, 1e-9) return _hex(_lerp(R_LO, R_HI, t)) - except Exception: - DBGEX("_state_color") + except Exception as e: + logger.exception("_state_color") return "#E9ECEF" @@ -2424,15 +2472,15 @@ class App(tk.Tk): if vi.delta_last is not None: try: delta_str = f"{vi.delta_last:+.6g}" - except Exception: - DBGEX("_state_color delta_last") + except Exception as e: + logger.exception("_state_color delta_last") delta_str = str(vi.delta_last) # Δ avg(N) davg_str = "N/A" try: n = max(1, int(self.avg_window_var.get())) - except Exception: + except Exception as e: n = AVERAGE_WINDOW_N if hasattr(vi, "history_delta") and vi.history_delta: dvals = list(vi.history_delta)[-n:] @@ -2440,8 +2488,8 @@ class App(tk.Tk): try: davg_val = sum(dvals) / float(n) davg_str = f"{davg_val:.6g}" - except Exception: - DBGEX("_state_color davg_last") + except Exception as e: + logger.exception("_state_color davg_last") davg_str = "N/A" # value avg(N) @@ -2452,8 +2500,8 @@ class App(tk.Tk): try: avg_val = sum(vals) / float(n) avg_str = f"{avg_val:.6g}" - except Exception: - DBGEX("_state_color avg_last") + except Exception as e: + logger.exception("_state_color avg_last") avg_str = "N/A" x = parse_first_float(vi.last_value) @@ -2487,8 +2535,8 @@ class App(tk.Tk): def _watch_refresh_interval(self): try: val = float(self.refresh_var.get()) - except Exception: - DBGEX("_watch_refresh_interval parse float") + except Exception as e: + logger.exception("_watch_refresh_interval parse float") val = self._last_refresh_val if val != self._last_refresh_val: self._last_refresh_val = val @@ -2496,8 +2544,8 @@ class App(tk.Tk): try: self.poller.refresh_interval = max(0.1, val) self.status_lbl.configure(text=f"Running… (refresh {self.poller.refresh_interval}s)") - except Exception: - DBGEX("_watch_refresh_interval set refresh_interval") + except Exception as e: + logger.exception("_watch_refresh_interval set refresh_interval") pass self.after(2000, self._watch_refresh_interval) @@ -2644,23 +2692,23 @@ class App(tk.Tk): dd = cfg.get("display_defaults", {}) try: self.display_backend_var.set(dd.get("backend", "matplotlib")) - except Exception: - DBGEX("load_config display_backend_var") + except Exception as e: + logger.exception("load_config display_backend_var") pass try: self.default_samples_var.set(int(dd.get("samples", 200))) - except Exception: - DBGEX("load_config default_samples_var") + except Exception as e: + logger.exception("load_config default_samples_var") pass try: self.default_maxpts_var.set(int(dd.get("max_draw_pts", 400))) - except Exception: - DBGEX("load_config default_maxpts_var") + except Exception as e: + logger.exception("load_config default_maxpts_var") pass try: self.display_show_thresholds_var.set(bool(dd.get("show_thresholds", True))) - except Exception: - DBGEX("load_config display_show_thresholds_var") + except Exception as e: + logger.exception("load_config display_show_thresholds_var") pass # replace scheduled tasks @@ -2748,71 +2796,109 @@ class App(tk.Tk): x_mode=d.get("x_mode","raw")) def on_close(self) -> None: - # zatrzymaj Pollera + """Enhanced resource cleanup to prevent memory leaks and ensure clean shutdown.""" + # Set closing flag to prevent new operations self._closing = True - # zatrzymaj okna/timery Qt (jeśli są) - try: - for _k, tup in list(getattr(self, "_qt_windows", {}).items()): - try: - win, timer = tup - except Exception: - DBGEX("on_close get timer") - continue - try: - timer.stop(); timer.deleteLater() - except Exception: - DBGEX("on_close stop timer") - pass - try: - win.close() - except Exception: - DBGEX("on_close close win") - pass - self._qt_windows.clear() - except Exception: - DBGEX("on_close clear windows") - pass - - try: - if getattr(self, "poller", None) and self.poller.is_alive(): - self.stop_event.set() # natychmiast przerwie wait() w pętli - self.paused_event.clear() - self.poller.join(timeout=1.5) - except Exception: - DBGEX("on_close poller") - pass - - # zatrzymaj scheduler - try: - if getattr(self, "scheduler", None) and self.scheduler.is_alive(): - self.scheduler.stop() # ustawia wewnętrzny _stop Event - self.scheduler.join(timeout=1.5) - except Exception: - DBGEX("on_close scheduler") - pass - - # zatrzymaj timer do plotów, jeśli działa - try: - if getattr(self, "_plot_timer", None): - self.after_cancel(self._plot_timer) - except Exception: - DBGEX("on_close plot_timer") - pass - - # zamknij okna wykresów - try: - for win in list(self._plot_windows.values()): - try: - win.destroy() - except Exception: - DBGEX("on_close destroy plot win") - pass - except Exception: - DBGEX("on_close plot_windows") - pass - + + logger.info("Application shutdown initiated") + + # 1. Stop all Qt windows and timers first (most complex cleanup) + self._cleanup_qt_windows() + + # 2. Stop polling thread + self._cleanup_poller() + + # 3. Stop scheduler thread + self._cleanup_scheduler() + + # 4. Cancel plot timer + self._cleanup_plot_timer() + + # 5. Close all plot windows + self._cleanup_plot_windows() + + # 6. Clear all window references + self._clear_window_references() + + logger.info("Application cleanup completed") self.destroy() + def _cleanup_qt_windows(self) -> None: + """Clean up Qt windows with proper error isolation.""" + if not hasattr(self, '_qt_windows') or not self._qt_windows: + return + + logger.debug(f"Cleaning up {len(self._qt_windows)} Qt windows") + for key, tup in list(self._qt_windows.items()): + try: + if isinstance(tup, tuple) and len(tup) >= 2: + win, timer = tup[:2] + if timer: + timer.stop() + timer.deleteLater() + if win: + win.close() + logger.debug(f"Cleaned up Qt window: {key}") + else: + logger.warning(f"Invalid Qt window tuple for key {key}: {tup}") + except Exception: + logger.exception(f"Failed to cleanup Qt window: {key}") + + self._qt_windows.clear() + + def _cleanup_poller(self) -> None: + """Stop poller thread with timeout.""" + poller = getattr(self, 'poller', None) + if poller and poller.is_alive(): + logger.debug("Stopping poller thread") + self.stop_event.set() + self.paused_event.clear() + poller.join(timeout=2.0) + if poller.is_alive(): + logger.warning("Poller thread did not stop within timeout") + + def _cleanup_scheduler(self) -> None: + """Stop scheduler thread with timeout.""" + scheduler = getattr(self, 'scheduler', None) + if scheduler and scheduler.is_alive(): + logger.debug("Stopping scheduler thread") + scheduler.stop() + scheduler.join(timeout=2.0) + if scheduler.is_alive(): + logger.warning("Scheduler thread did not stop within timeout") + + def _cleanup_plot_timer(self) -> None: + """Cancel the plot update timer.""" + plot_timer = getattr(self, '_plot_timer', None) + if plot_timer: + try: + self.after_cancel(plot_timer) + logger.debug("Plot timer cancelled") + except Exception: + logger.exception("Failed to cancel plot timer") + + def _cleanup_plot_windows(self) -> None: + """Close all plot windows with error isolation.""" + if not hasattr(self, '_plot_windows') or not self._plot_windows: + return + + logger.debug(f"Cleaning up {len(self._plot_windows)} plot windows") + for key, win in list(self._plot_windows.items()): + try: + if win and hasattr(win, 'destroy'): + win.destroy() + logger.debug(f"Destroyed plot window: {key}") + except Exception: + logger.exception(f"Failed to destroy plot window: {key}") + + def _clear_window_references(self) -> None: + """Clear all window reference dictionaries.""" + if hasattr(self, '_plot_windows'): + self._plot_windows.clear() + if hasattr(self, '_qt_windows'): + self._qt_windows.clear() + logger.debug("Window references cleared") + # --- [DROP-IN] otwieranie okna Canvas (lite) -------------------------------- def _open_canvas_window(self, key: str): vi = self.vars[key] @@ -2843,8 +2929,6 @@ class App(tk.Tk): win._cv_ids = [] win._last_seen_ver = -1 - if not hasattr(self, "_plot_windows"): - self._plot_windows = {} self._plot_windows[key] = win self._ensure_plot_timer() @@ -2854,7 +2938,7 @@ class App(tk.Tk): cv = win._tk_canvas for iid in getattr(win, "_cv_ids", []): try: cv.delete(iid) - except Exception: DBGEX("_plot_update_canvas delete iid") + except Exception as e: logger.exception("_plot_update_canvas delete iid") win._cv_ids = [] W = max(10, cv.winfo_width()) @@ -2915,8 +2999,8 @@ class App(tk.Tk): win._cv_ids.append(cv.create_rectangle(X, Y, X+bw, H-10, width=0)) # wskaźnik – tylko strzałka (kolor jak wyżej), rysowana nad osią # (prosto: nie liczymy „najbliższego” drugi raz – to kosmetyka) - except Exception: - DBGEX("_plot_update_canvas") + except Exception as e: + logger.exception("_plot_update_canvas") # --- [DROP-IN] otwieranie okna PyQtGraph (bez QTimer) ----------------------- def _open_pyqtgraph_window(self, key: str): @@ -2992,8 +3076,6 @@ class App(tk.Tk): win._last_seen_ver = -1 win._last_title = None - if not hasattr(self, "_plot_windows"): - self._plot_windows = {} self._plot_windows[key] = win self._ensure_qt_pump() # włącz pompowanie zdarzeń Qt w pętli Tk @@ -3039,8 +3121,8 @@ class App(tk.Tk): win._bars_w = max(1.0, float(len(xs_b)) / n) # symboliczne – pyqtgraph skaluje z X try: win._bars.setOpts(width=1.0) # stała, bo mamy indeksy jako X - except Exception: - DBGEX("_plot_update_pyqtgraph set bar width") + except Exception as e: + logger.exception("_plot_update_pyqtgraph set bar width") # --- progi + wskaźnik --- for ln in win._thr_lines.values(): @@ -3075,17 +3157,17 @@ class App(tk.Tk): y1 = p2.viewRange()[1][1] win._tri_val.setPos(x0, y0) win._tri_delta.setPos(x0, y1) - except Exception: - DBGEX("_plot_update_pyqtgraph set tri pos") + except Exception as e: + logger.exception("_plot_update_pyqtgraph set tri pos") - except Exception: - DBGEX("_plot_update_pyqtgraph") + except Exception as e: + logger.exception("_plot_update_pyqtgraph") # --- [DROP-IN] otwieranie okna Matplotlib ----------------------------------- def _open_matplotlib_window(self, key: str): import matplotlib - matplotlib.use("Agg") + matplotlib.use("TkAgg") # Correct backend for Tk embedding from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure @@ -3146,8 +3228,6 @@ class App(tk.Tk): win._layout_dirty = True canvas.mpl_connect("resize_event", lambda _evt=None: setattr(win, "_layout_dirty", True)) - if not hasattr(self, "_plot_windows"): - self._plot_windows = {} self._plot_windows[key] = win self._ensure_plot_timer() @@ -3167,7 +3247,7 @@ class App(tk.Tk): if len(dels_draw) != getattr(win, "_bars_len", 0): for b in getattr(win, "_bars", []): try: b.remove() - except Exception: DBGEX("_plot_update_matplotlib remove bar") + except Exception as e: logger.exception("_plot_update_matplotlib remove bar") win._bars = win._ax_delta.bar(xs_b, dels_draw) win._bars_len = len(dels_draw) else: @@ -3211,12 +3291,12 @@ class App(tk.Tk): if getattr(win, "_layout_dirty", False): try: win._fig.tight_layout() - except Exception: DBGEX("_plot_update_matplotlib tight_layout") + except Exception as e: logger.exception("_plot_update_matplotlib tight_layout") win._layout_dirty = False win._canvas.draw_idle() - except Exception: - DBGEX("_plot_update_matplotlib") + except Exception as e: + logger.exception("_plot_update_matplotlib") def count_plot_windows(self) -> int: n_tk = len([w for w in getattr(self, "_plot_windows", {}).values() if getattr(w, "winfo_exists", lambda: False)()]) @@ -3245,9 +3325,9 @@ class App(tk.Tk): user32.EnumDisplayMonitors(0, 0, MONITORENUMPROC(_cb), 0) if not monitors: raise RuntimeError("No monitors from EnumDisplayMonitors") - except Exception: + except Exception as e: # Fallback: pojedynczy ekran wg Tk - DBGEX("_enum_monitors") + logger.exception("_enum_monitors") w = self.winfo_screenwidth() h = self.winfo_screenheight() monitors = [(0, 0, w, h)] @@ -3310,8 +3390,8 @@ class App(tk.Tk): # fallback: tytuł okna name = str(w.title() or "") tk_wins.append(("tk", w, x, y, ww, hh, name)) - except Exception: - DBGEX("arrange_plot_windows Tk MPL") + except Exception as e: + logger.exception("arrange_plot_windows Tk MPL") # Zbierz okna (Tk Canvas) for w in list(getattr(self, "_plot_windows_canvas", [])): @@ -3330,8 +3410,8 @@ class App(tk.Tk): if not name: name = str(w.title() or "") tk_wins.append(("tk", w, x, y, ww, hh, name)) - except Exception: - DBGEX("arrange_plot_windows Tk Canvas") + except Exception as e: + logger.exception("arrange_plot_windows Tk Canvas") # Zbierz okna (Qt pyqtgraph) qt_wins = [] @@ -3347,8 +3427,8 @@ class App(tk.Tk): if not name: name = str(qw.windowTitle() or "") qt_wins.append(("qt", qw, x, y, ww, hh, name)) - except Exception: - DBGEX("arrange_plot_windows Qt pyqtgraph") + except Exception as e: + logger.exception("arrange_plot_windows Qt pyqtgraph") all_wins = tk_wins + qt_wins if not all_wins: @@ -3437,8 +3517,8 @@ class App(tk.Tk): win.geometry(f"{cell_w_adj}x{cell_h_adj}+{x}+{y}") else: win.setGeometry(x, y, cell_w_adj, cell_h_adj) - except Exception: - DBGEX("arrange_plot_windows set geometry") + except Exception as e: + logger.exception("arrange_plot_windows set geometry") i += 1 @@ -3451,8 +3531,8 @@ def parse_first_float(value: str) -> Optional[float]: if m: try: return float(m.group(0)) - except Exception: - DBGEX("parse_first_float") + except Exception as e: + logger.exception("parse_first_float") return None return None @@ -3499,10 +3579,10 @@ def main() -> None: # jeżeli App jeszcze żyje – wywołaj on_close # (dopasuj, jeśli trzymasz referencję gdzie indziej) app.on_close() - except Exception: - DBGEX("graceful_shutdown") + except Exception as e: + logger.exception("graceful_shutdown") atexit.register(_graceful_shutdown) app.mainloop() if __name__ == "__main__": - main() \ No newline at end of file + main()