From 04923cc449f96e97dc76933b384ce7853ef4ee56 Mon Sep 17 00:00:00 2001 From: Michal Tuszowski Date: Mon, 20 Oct 2025 18:31:44 +0200 Subject: [PATCH] 1.13 --- nucleares_monitor/control_board_monitor.py | 142 +++++++++++++++++++-- 1 file changed, 131 insertions(+), 11 deletions(-) diff --git a/nucleares_monitor/control_board_monitor.py b/nucleares_monitor/control_board_monitor.py index d641fbc..561c997 100644 --- a/nucleares_monitor/control_board_monitor.py +++ b/nucleares_monitor/control_board_monitor.py @@ -265,6 +265,11 @@ class Thresholds: # sources for y and z y_source: Optional[str] = None z_source: Optional[str] = None + expr_x_source_dead_low: str = "raw" # "raw" | "x_avg" | "dx" | "dx_avg" + expr_x_source_low: str = "raw" + expr_x_source_operating: str = "raw" + expr_x_source_high: str = "raw" + expr_x_source_extreme_high: str = "raw" @dataclass class VarInfo: @@ -297,6 +302,7 @@ class ActionTask: x_src: Optional[str] = None y_src: Optional[str] = None z_src: Optional[str] = None + x_mode: str = "raw" # "raw" | "x_avg" | "dx" | "dx_avg" # ===================== # Scheduler thread @@ -353,9 +359,27 @@ class ActionScheduler(threading.Thread): return self.get_value_cb(n) if n else None except Exception: return None - x = gv(task.x_src) - y = gv(task.y_src) - z = gv(task.z_src) + # stara wersja: + # x = gv(task.x_src); y = gv(task.y_src); z = gv(task.z_src) + def pick(stats: Optional[dict], mode: str) -> Optional[float]: + if not stats: + return None + if mode == "x_avg": + return stats.get("x_avg") + if mode == "dx": + return stats.get("dx") + if mode == "dx_avg": + return stats.get("dx_avg") + return stats.get("x") + + xs = self.get_value_cb(task.x_src) + ys = self.get_value_cb(task.y_src) + zs = self.get_value_cb(task.z_src) + + x = pick(xs, getattr(task, "x_mode", "raw")) + y = pick(ys, getattr(task, "y_mode", "raw")) + z = pick(zs, getattr(task, "z_mode", "raw")) + if x is None: x = 0.0 try: @@ -486,7 +510,10 @@ class App(tk.Tk): self.variables_keys: List[str] = [] # list of lower-case keys in selection order self.functions_list: List[str] = [] # preserve original casing - self.scheduler = ActionScheduler(lambda: build_base_url(self.host_var.get().strip(), int(self.port_var.get())), self.get_current_value) + self.scheduler = ActionScheduler( + lambda: build_base_url(self.host_var.get().strip(), int(self.port_var.get())), + self.get_stats_for + ) self.scheduler.start() controls = ttk.Frame(self, padding=10) @@ -653,6 +680,36 @@ class App(tk.Tk): self.actions_menu.add_command(label="Run Now", command=self.run_selected_task_now) self.actions_menu.add_command(label="Remove", command=self.remove_selected_task) self.actions_tree.bind("", self.on_actions_right_click) + def get_stats_for(self, name: Optional[str]) -> Optional[dict]: + if not name: + return None + key = str(name).lower() + vi = self.vars.get(key) + + def _avg(dq, n): + if not dq or n <= 0 or len(dq) < n: + return None + lst = list(dq)[-n:] + try: + return sum(lst) / float(n) + except Exception: + return None + + try: + n = max(1, int(self.avg_window_var.get())) + except Exception: + n = 60 + + if vi: + return { + "x": parse_first_float(vi.last_value), + "x_avg": _avg(vi.history, n), + "dx": vi.delta_last, + "dx_avg": _avg(vi.history_delta, n), + } + # fallback dla zmiennych nie-monitorowanych + val = self.get_current_value(name) + return {"x": val, "x_avg": None, "dx": None, "dx_avg": None} # --- Discovery --- def reload_discovery(self) -> None: @@ -1297,6 +1354,7 @@ class App(tk.Tk): def open_thresholds_dialog(self): sel = self.tree.selection() + expr_xsrc_vars = {} if not sel: messagebox.showinfo("No selection", "Select a variable first.") return @@ -1418,6 +1476,20 @@ class App(tk.Tk): s.grid(row=r, column=3, sticky="w", padx=6) self._attach_search_filter_to_combobox(s, self.functions_list) expr_target_vars[tkey] = s + ttk.Label(grid, text="x from").grid(row=r, column=4, sticky="e") + src_combo = ttk.Combobox(grid, values=["raw", "x_avg", "dx", "dx_avg"], width=8) + # mapowanie nazwy wiersza na pole w Thresholds: + statekey = { + "Dead Low expr": "dead_low", + "Low expr": "low", + "Operating expr": "operating", + "High expr": "high", + "Extreme High expr": "extreme_high", + }[label] + field_name = f"expr_x_source_{statekey}" + src_combo.set(getattr(t, field_name, "raw")) + src_combo.grid(row=r, column=5, sticky="w") + expr_xsrc_vars[field_name] = src_combo # Bottom bar btns = ttk.Frame(dlg); btns.pack(fill=tk.X, padx=10, pady=10) @@ -1458,6 +1530,11 @@ class App(tk.Tk): expr_target_extreme_high=(expr_target_vars["expr_target_extreme_high"].get().strip() or None), y_source=(y_source_var.get().strip() or None), z_source=(z_source_var.get().strip() or None), + expr_x_source_dead_low = expr_xsrc_vars["expr_x_source_dead_low"].get(), + expr_x_source_low = expr_xsrc_vars["expr_x_source_low"].get(), + expr_x_source_operating = expr_xsrc_vars["expr_x_source_operating"].get(), + expr_x_source_high = expr_xsrc_vars["expr_x_source_high"].get(), + expr_x_source_extreme_high = expr_xsrc_vars["expr_x_source_extreme_high"].get(), ) vi.thresholds = nt # Re-enter current state to refresh scheduled actions/intervals @@ -1596,9 +1673,23 @@ class App(tk.Tk): val = parse_first_float(bd) if label == 'y': y_val = val else: z_val = val - if expr and x is not None: + x_variants = self.get_stats_for(vi.key) or {} + mode_map = { + "DEAD_LOW": t.expr_x_source_dead_low, + "LOW": t.expr_x_source_low, + "OPERATING": t.expr_x_source_operating, + "HIGH": t.expr_x_source_high, + "EXTREME_HIGH": t.expr_x_source_extreme_high, + } + x_for_expr = { + "raw": x_variants.get("x"), + "x_avg": x_variants.get("x_avg"), + "dx": x_variants.get("dx"), + "dx_avg": x_variants.get("dx_avg"), + }.get(mode_map.get(state, "raw"), x_variants.get("x")) + if expr and x_for_expr is not None: try: - computed = eval_user_expression(expr, x, y_val, z_val) + computed = eval_user_expression(expr, x_for_expr, y_val, z_val) if target_func: base = build_base_url(self.host_var.get().strip(), int(self.port_var.get())) params = {"variable": target_func, "value": str(computed)} @@ -1661,8 +1752,12 @@ class App(tk.Tk): self.scheduler.add_task(task) else: # schedule repeating expression task - task = ActionTask(name=expr_target, value="", interval_s=max(0.0, float(expr_interval)), expr=expr_str, - x_src=vi.key, y_src=t.y_source, z_src=t.z_source) + task = ActionTask( + name=expr_target, value="", interval_s=max(0.0, float(expr_interval)), + expr=expr_str, x_src=vi.key, y_src=t.y_source, z_src=t.z_source, + x_mode=mode_map.get(state, "raw") + ) + tid = self.scheduler.add_task(task) self.expr_state_tasks[(vi.key, state)] = tid @@ -1920,6 +2015,11 @@ class App(tk.Tk): z_combo = ttk.Combobox(src_row, values=self.known_variables, textvariable=z_src, width=18) self._attach_search_filter_to_combobox(z_combo, self.known_variables) z_combo.pack(side=tk.LEFT, padx=4) + xmode_row = ttk.Frame(top); xmode_row.pack(fill=tk.X, padx=10, pady=(4,2)) + ttk.Label(xmode_row, text="x from:").pack(side=tk.LEFT) + x_mode = tk.StringVar(value="raw") + xmode_combo = ttk.Combobox(xmode_row, values=["raw", "x_avg", "dx", "dx_avg"], textvariable=x_mode, width=10) + xmode_combo.pack(side=tk.LEFT, padx=6) # Interval input ttk.Label(top, text="Interval (seconds, can be < 1.0):").pack(pady=(6,2)) interval = tk.DoubleVar(value=1.0) @@ -1940,7 +2040,12 @@ class App(tk.Tk): if mode.get() == "fixed": task = ActionTask(name=fname, value=val.get(), interval_s=max(0.0, float(interval.get()))) else: - task = ActionTask(name=fname, value="", interval_s=max(0.0, float(interval.get())), expr=expr.get().strip() or "x", x_src=(x_src.get().strip() or None), y_src=(y_src.get().strip() or None), z_src=(z_src.get().strip() or None)) + task = ActionTask( + name=fname, value="", interval_s=max(0.0, float(interval.get())), + expr=expr.get().strip() or "x", + x_src=(x_src.get().strip() or None), y_src=(y_src.get().strip() or None), z_src=(z_src.get().strip() or None), + x_mode=x_mode.get() + ) tid = self.scheduler.add_task(task) self.refresh_actions_tree() top.destroy() @@ -2015,6 +2120,11 @@ class App(tk.Tk): "expr_operating_interval": getattr(t, "expr_operating_interval", 1.0), "y_source": getattr(t, "y_source", None), "z_source": getattr(t, "z_source", None), + "expr_x_source_dead_low": t.expr_x_source_dead_low, + "expr_x_source_low": t.expr_x_source_low, + "expr_x_source_operating": t.expr_x_source_operating, + "expr_x_source_high": t.expr_x_source_high, + "expr_x_source_extreme_high": t.expr_x_source_extreme_high, }, } def _deserialize_varinfo(self, key: str, data: dict) -> VarInfo: @@ -2036,14 +2146,24 @@ class App(tk.Tk): # nowoĊ›ci (wstecznie opcjonalne): expr_operating_interval=float(td.get("expr_operating_interval", 1.0)), y_source=td.get("y_source"), z_source=td.get("z_source"), + expr_x_source_dead_low=td.get("expr_x_source_dead_low","raw"), + expr_x_source_low=td.get("expr_x_source_low","raw"), + expr_x_source_operating=td.get("expr_x_source_operating","raw"), + expr_x_source_high=td.get("expr_x_source_high","raw"), + expr_x_source_extreme_high=td.get("expr_x_source_extreme_high","raw"), ) return VarInfo(key=key.lower(), display_name=name, thresholds=t) def _serialize_task(self, t: ActionTask): - return {"name": t.name, "value": t.value, "interval_s": t.interval_s, "enabled": t.enabled, "expr": t.expr, "x_src": t.x_src, "y_src": t.y_src, "z_src": t.z_src} + return {"name": t.name, "value": t.value, "interval_s": t.interval_s, "enabled": t.enabled, + "expr": t.expr, "x_src": t.x_src, "y_src": t.y_src, "z_src": t.z_src, "x_mode": getattr(t, "x_mode", "raw")} def _deserialize_task(self, d: dict) -> ActionTask: - return ActionTask(name=d.get("name",""), value=d.get("value","1"), interval_s=float(d.get("interval_s", 1.0)), enabled=d.get("enabled", True), expr=d.get("expr"), x_src=d.get("x_src"), y_src=d.get("y_src"), z_src=d.get("z_src")) + return ActionTask(name=d.get("name",""), value=d.get("value","1"), + interval_s=float(d.get("interval_s", 1.0)), enabled=d.get("enabled", True), + expr=d.get("expr"), x_src=d.get("x_src"), y_src=d.get("y_src"), z_src=d.get("z_src"), + x_mode=d.get("x_mode","raw")) + def on_close(self) -> None: try: if self.poller and self.poller.is_alive():