Compare commits
2 Commits
2342caeb0a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88d6568a0 | ||
|
|
361c2b68c0 |
@@ -690,6 +690,8 @@ class LevelMonitorGUI:
|
|||||||
self.power_rows: dict[str, dict] = {}
|
self.power_rows: dict[str, dict] = {}
|
||||||
self.power_history: dict[str, list[tuple[float, float, float]]] = {}
|
self.power_history: dict[str, list[tuple[float, float, float]]] = {}
|
||||||
self.power_max_hold_w: dict[str, float] = {}
|
self.power_max_hold_w: dict[str, float] = {}
|
||||||
|
self.latest_power_frames: dict[str, PowerFrame] = {}
|
||||||
|
self.latest_headroom_frames: dict[str, dict] = {}
|
||||||
self.headroom_curves: dict[str, WinISDLimitCurve] = {}
|
self.headroom_curves: dict[str, WinISDLimitCurve] = {}
|
||||||
self.headroom_smoothed_db: dict[str, float] = {}
|
self.headroom_smoothed_db: dict[str, float] = {}
|
||||||
self.power_history_start_s: float | None = None
|
self.power_history_start_s: float | None = None
|
||||||
@@ -899,15 +901,16 @@ class LevelMonitorGUI:
|
|||||||
self._build_power_config_row("LF", 4, self.lf_enabled_var, self.lf_channel_var, self.lf_impedance_var, self.lf_gain_var)
|
self._build_power_config_row("LF", 4, self.lf_enabled_var, self.lf_channel_var, self.lf_impedance_var, self.lf_gain_var)
|
||||||
self._build_power_config_row("HF", 5, self.hf_enabled_var, self.hf_channel_var, self.hf_impedance_var, self.hf_gain_var)
|
self._build_power_config_row("HF", 5, self.hf_enabled_var, self.hf_channel_var, self.hf_impedance_var, self.hf_gain_var)
|
||||||
|
|
||||||
plots_frame = ttk.Frame(outer)
|
plots_frame = ttk.LabelFrame(outer, text="Plot viewer", padding=10)
|
||||||
plots_frame.pack(fill="both", expand=True, pady=(12, 0))
|
plots_frame.pack(fill="both", expand=True, pady=(12, 0))
|
||||||
plots_frame.columnconfigure(0, weight=1)
|
plots_frame.columnconfigure(0, weight=1)
|
||||||
plots_frame.columnconfigure(1, weight=1)
|
|
||||||
plots_frame.rowconfigure(0, weight=1)
|
plots_frame.rowconfigure(0, weight=1)
|
||||||
plots_frame.rowconfigure(1, weight=1)
|
|
||||||
|
|
||||||
self.scope_frame = ttk.LabelFrame(plots_frame, text="Input waveform, calibrated voltage", padding=10)
|
self.plot_tabs = ttk.Notebook(plots_frame)
|
||||||
self.scope_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 6))
|
self.plot_tabs.grid(row=0, column=0, sticky="nsew")
|
||||||
|
self.plot_tabs.bind("<<NotebookTabChanged>>", lambda _event: self._redraw_active_plot())
|
||||||
|
|
||||||
|
self.scope_frame = ttk.Frame(self.plot_tabs, padding=10)
|
||||||
self.scope_frame.rowconfigure(0, weight=1)
|
self.scope_frame.rowconfigure(0, weight=1)
|
||||||
self.scope_frame.columnconfigure(0, weight=1)
|
self.scope_frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
@@ -933,9 +936,9 @@ class LevelMonitorGUI:
|
|||||||
self.waveform_y_limit_var.trace_add("write", lambda *_args: self.draw_scope_grid())
|
self.waveform_y_limit_var.trace_add("write", lambda *_args: self.draw_scope_grid())
|
||||||
self.scope_canvas.bind("<Configure>", lambda _event: self.draw_scope_grid())
|
self.scope_canvas.bind("<Configure>", lambda _event: self.draw_scope_grid())
|
||||||
self.draw_scope_grid()
|
self.draw_scope_grid()
|
||||||
|
self.plot_tabs.add(self.scope_frame, text="Waveform")
|
||||||
|
|
||||||
self.spectrum_frame = ttk.LabelFrame(plots_frame, text="Input PSD, Welch scaled", padding=10)
|
self.spectrum_frame = ttk.Frame(self.plot_tabs, padding=10)
|
||||||
self.spectrum_frame.grid(row=0, column=1, sticky="nsew", padx=(6, 0))
|
|
||||||
self.spectrum_frame.rowconfigure(0, weight=1)
|
self.spectrum_frame.rowconfigure(0, weight=1)
|
||||||
self.spectrum_frame.columnconfigure(0, weight=1)
|
self.spectrum_frame.columnconfigure(0, weight=1)
|
||||||
self.spectrum_canvas = self.tk.Canvas(
|
self.spectrum_canvas = self.tk.Canvas(
|
||||||
@@ -951,9 +954,9 @@ class LevelMonitorGUI:
|
|||||||
)
|
)
|
||||||
self.spectrum_canvas.bind("<Configure>", lambda _event: self.draw_spectrum_grid())
|
self.spectrum_canvas.bind("<Configure>", lambda _event: self.draw_spectrum_grid())
|
||||||
self.draw_spectrum_grid()
|
self.draw_spectrum_grid()
|
||||||
|
self.plot_tabs.add(self.spectrum_frame, text="Input PSD")
|
||||||
|
|
||||||
self.power_history_frame = ttk.LabelFrame(plots_frame, text="Output power history", padding=10)
|
self.power_history_frame = ttk.Frame(self.plot_tabs, padding=10)
|
||||||
self.power_history_frame.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(12, 0))
|
|
||||||
self.power_history_frame.rowconfigure(0, weight=1)
|
self.power_history_frame.rowconfigure(0, weight=1)
|
||||||
self.power_history_frame.columnconfigure(0, weight=1)
|
self.power_history_frame.columnconfigure(0, weight=1)
|
||||||
self.power_history_canvas = self.tk.Canvas(
|
self.power_history_canvas = self.tk.Canvas(
|
||||||
@@ -978,6 +981,43 @@ class LevelMonitorGUI:
|
|||||||
self.power_history_window_var.trace_add("write", lambda *_args: self.draw_power_history())
|
self.power_history_window_var.trace_add("write", lambda *_args: self.draw_power_history())
|
||||||
self.power_history_canvas.bind("<Configure>", lambda _event: self.draw_power_history_grid())
|
self.power_history_canvas.bind("<Configure>", lambda _event: self.draw_power_history_grid())
|
||||||
self.draw_power_history_grid()
|
self.draw_power_history_grid()
|
||||||
|
self.plot_tabs.add(self.power_history_frame, text="Power history")
|
||||||
|
|
||||||
|
self.power_limit_frame = ttk.Frame(self.plot_tabs, padding=10)
|
||||||
|
self.power_limit_frame.rowconfigure(0, weight=1)
|
||||||
|
self.power_limit_frame.columnconfigure(0, weight=1)
|
||||||
|
self.power_limit_canvas = self.tk.Canvas(
|
||||||
|
self.power_limit_frame,
|
||||||
|
background="#111111",
|
||||||
|
highlightthickness=0,
|
||||||
|
height=240,
|
||||||
|
)
|
||||||
|
self.power_limit_canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
|
self.power_limit_status_var = self.tk.StringVar(value="P_bin and WinISD Pmax over frequency.")
|
||||||
|
ttk.Label(self.power_limit_frame, textvariable=self.power_limit_status_var).grid(
|
||||||
|
row=1, column=0, sticky="w", pady=(8, 0)
|
||||||
|
)
|
||||||
|
self.power_limit_canvas.bind("<Configure>", lambda _event: self.draw_power_limit_grid())
|
||||||
|
self.draw_power_limit_grid()
|
||||||
|
self.plot_tabs.add(self.power_limit_frame, text="P vs WinISD")
|
||||||
|
|
||||||
|
self.headroom_frame = ttk.Frame(self.plot_tabs, padding=10)
|
||||||
|
self.headroom_frame.rowconfigure(0, weight=1)
|
||||||
|
self.headroom_frame.columnconfigure(0, weight=1)
|
||||||
|
self.headroom_canvas = self.tk.Canvas(
|
||||||
|
self.headroom_frame,
|
||||||
|
background="#111111",
|
||||||
|
highlightthickness=0,
|
||||||
|
height=240,
|
||||||
|
)
|
||||||
|
self.headroom_canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
|
self.headroom_status_var = self.tk.StringVar(value="H(f,t) = 10 log10(Pmax / P_bin).")
|
||||||
|
ttk.Label(self.headroom_frame, textvariable=self.headroom_status_var).grid(
|
||||||
|
row=1, column=0, sticky="w", pady=(8, 0)
|
||||||
|
)
|
||||||
|
self.headroom_canvas.bind("<Configure>", lambda _event: self.draw_headroom_grid())
|
||||||
|
self.draw_headroom_grid()
|
||||||
|
self.plot_tabs.add(self.headroom_frame, text="Headroom")
|
||||||
|
|
||||||
def _build_power_config_row(
|
def _build_power_config_row(
|
||||||
self,
|
self,
|
||||||
@@ -1030,6 +1070,23 @@ class LevelMonitorGUI:
|
|||||||
vars_by_metric["light_id"] = light_id
|
vars_by_metric["light_id"] = light_id
|
||||||
self.power_rows[name] = vars_by_metric
|
self.power_rows[name] = vars_by_metric
|
||||||
|
|
||||||
|
def _redraw_active_plot(self) -> None:
|
||||||
|
selected = self.plot_tabs.select()
|
||||||
|
if selected == str(self.scope_frame):
|
||||||
|
self.draw_scope_grid()
|
||||||
|
elif selected == str(self.spectrum_frame):
|
||||||
|
self.draw_spectrum_grid()
|
||||||
|
self.draw_spectrum()
|
||||||
|
elif selected == str(self.power_history_frame):
|
||||||
|
self.draw_power_history_grid()
|
||||||
|
self.draw_power_history()
|
||||||
|
elif selected == str(self.power_limit_frame):
|
||||||
|
self.draw_power_limit_grid()
|
||||||
|
self.draw_power_limit()
|
||||||
|
elif selected == str(self.headroom_frame):
|
||||||
|
self.draw_headroom_grid()
|
||||||
|
self.draw_headroom()
|
||||||
|
|
||||||
def _select_initial_device(self, requested_index: int | None) -> None:
|
def _select_initial_device(self, requested_index: int | None) -> None:
|
||||||
selected = 0
|
selected = 0
|
||||||
if requested_index is not None:
|
if requested_index is not None:
|
||||||
@@ -1137,6 +1194,8 @@ class LevelMonitorGUI:
|
|||||||
self.power_buffers = {}
|
self.power_buffers = {}
|
||||||
self.power_history = {}
|
self.power_history = {}
|
||||||
self.power_max_hold_w = {}
|
self.power_max_hold_w = {}
|
||||||
|
self.latest_power_frames = {}
|
||||||
|
self.latest_headroom_frames = {}
|
||||||
self.headroom_curves = {}
|
self.headroom_curves = {}
|
||||||
self.headroom_smoothed_db = {}
|
self.headroom_smoothed_db = {}
|
||||||
self.power_history_start_s = None
|
self.power_history_start_s = None
|
||||||
@@ -1255,6 +1314,7 @@ class LevelMonitorGUI:
|
|||||||
if latest is None:
|
if latest is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
self.latest_power_frames[name] = latest
|
||||||
row = self.power_rows.get(name)
|
row = self.power_rows.get(name)
|
||||||
if row is None:
|
if row is None:
|
||||||
continue
|
continue
|
||||||
@@ -1265,10 +1325,12 @@ class LevelMonitorGUI:
|
|||||||
row["s"].set(f"{latest.total_s_va:.1f} VA")
|
row["s"].set(f"{latest.total_s_va:.1f} VA")
|
||||||
headroom = self._compute_gui_headroom(name, latest)
|
headroom = self._compute_gui_headroom(name, latest)
|
||||||
if headroom is None:
|
if headroom is None:
|
||||||
|
self.latest_headroom_frames.pop(name, None)
|
||||||
row["headroom"].set("-")
|
row["headroom"].set("-")
|
||||||
row["crit_hz"].set("-")
|
row["crit_hz"].set("-")
|
||||||
row["limit"].set("-")
|
row["limit"].set("-")
|
||||||
else:
|
else:
|
||||||
|
self.latest_headroom_frames[name] = headroom
|
||||||
row["headroom"].set(f"{headroom['worst_headroom_db']:+.1f} dB")
|
row["headroom"].set(f"{headroom['worst_headroom_db']:+.1f} dB")
|
||||||
row["crit_hz"].set(f"{headroom['critical_frequency_hz']:.1f}")
|
row["crit_hz"].set(f"{headroom['critical_frequency_hz']:.1f}")
|
||||||
row["limit"].set(headroom["status"])
|
row["limit"].set(headroom["status"])
|
||||||
@@ -1276,6 +1338,8 @@ class LevelMonitorGUI:
|
|||||||
self._append_power_history(name, latest)
|
self._append_power_history(name, latest)
|
||||||
|
|
||||||
self.draw_power_history()
|
self.draw_power_history()
|
||||||
|
self.draw_power_limit()
|
||||||
|
self.draw_headroom()
|
||||||
|
|
||||||
def _compute_gui_headroom(self, name: str, frame: PowerFrame) -> dict | None:
|
def _compute_gui_headroom(self, name: str, frame: PowerFrame) -> dict | None:
|
||||||
curve = self.headroom_curves.get(name)
|
curve = self.headroom_curves.get(name)
|
||||||
@@ -1653,6 +1717,187 @@ class LevelMonitorGUI:
|
|||||||
f"Window {window_s:g} s, y {y_min:.1f} to {y_max:.1f} W / VA"
|
f"Window {window_s:g} s, y {y_min:.1f} to {y_max:.1f} W / VA"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _winisd_plot_data(self):
|
||||||
|
for name in sorted(self.latest_headroom_frames):
|
||||||
|
frame = self.latest_power_frames.get(name)
|
||||||
|
headroom = self.latest_headroom_frames.get(name)
|
||||||
|
curve = self.headroom_curves.get(name)
|
||||||
|
if frame is not None and headroom is not None and curve is not None:
|
||||||
|
return name, frame, headroom, curve
|
||||||
|
return None
|
||||||
|
|
||||||
|
def draw_power_limit_grid(self) -> None:
|
||||||
|
canvas = self.power_limit_canvas
|
||||||
|
canvas.delete("grid")
|
||||||
|
canvas.delete("limit")
|
||||||
|
width = max(canvas.winfo_width(), 2)
|
||||||
|
height = max(canvas.winfo_height(), 2)
|
||||||
|
|
||||||
|
for frac in (0.0, 0.25, 0.5, 0.75, 1.0):
|
||||||
|
y = int(frac * (height - 1))
|
||||||
|
color = "#555555" if frac == 0.5 else "#2a2a2a"
|
||||||
|
canvas.create_line(0, y, width, y, fill=color, tags="grid")
|
||||||
|
x = int(frac * (width - 1))
|
||||||
|
canvas.create_line(x, 0, x, height, fill="#242424", tags="grid")
|
||||||
|
|
||||||
|
canvas.create_text(6, 6, anchor="nw", text="Power [W], log scale", fill="#bbbbbb", tags="grid")
|
||||||
|
canvas.create_text(6, height - 20, anchor="nw", text="20 Hz", fill="#bbbbbb", tags="grid")
|
||||||
|
canvas.create_text(width - 6, height - 20, anchor="ne", text="Nyquist", fill="#bbbbbb", tags="grid")
|
||||||
|
|
||||||
|
def draw_power_limit(self) -> None:
|
||||||
|
data = self._winisd_plot_data()
|
||||||
|
if np is None or data is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
name, frame, headroom, curve = data
|
||||||
|
canvas = self.power_limit_canvas
|
||||||
|
canvas.delete("limit")
|
||||||
|
width = max(canvas.winfo_width(), 2)
|
||||||
|
height = max(canvas.winfo_height(), 2)
|
||||||
|
|
||||||
|
freq = frame.frequencies_hz
|
||||||
|
pmax = curve.interpolate(freq)
|
||||||
|
p_bin = np.maximum(frame.p_w_per_bin, 0.0)
|
||||||
|
mask = np.isfinite(freq) & np.isfinite(pmax) & (freq >= 20.0) & (pmax > HEADROOM_EPS_W)
|
||||||
|
if np.count_nonzero(mask) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
freq = freq[mask]
|
||||||
|
pmax = pmax[mask]
|
||||||
|
p_bin = p_bin[mask]
|
||||||
|
p_plot = np.maximum(p_bin, 1e-9)
|
||||||
|
|
||||||
|
f_min = max(20.0, float(freq[0]))
|
||||||
|
f_max = float(freq[-1])
|
||||||
|
if f_max <= f_min:
|
||||||
|
return
|
||||||
|
log_f_min = math.log10(f_min)
|
||||||
|
log_f_max = math.log10(f_max)
|
||||||
|
|
||||||
|
positive_values = np.concatenate((p_plot[np.isfinite(p_plot)], pmax[np.isfinite(pmax)]))
|
||||||
|
positive_values = positive_values[positive_values > 0.0]
|
||||||
|
if len(positive_values) == 0:
|
||||||
|
return
|
||||||
|
y_min = max(1e-6, 10.0 ** math.floor(math.log10(float(np.nanmin(positive_values)))))
|
||||||
|
y_max = 10.0 ** math.ceil(math.log10(float(np.nanmax(positive_values)) * 1.2))
|
||||||
|
if y_max <= y_min:
|
||||||
|
y_max = y_min * 10.0
|
||||||
|
log_y_min = math.log10(y_min)
|
||||||
|
log_y_max = math.log10(y_max)
|
||||||
|
|
||||||
|
def to_points(y_values: np.ndarray) -> list[float]:
|
||||||
|
x = (np.log10(freq) - log_f_min) / (log_f_max - log_f_min) * (width - 1)
|
||||||
|
y = (log_y_max - np.log10(np.maximum(y_values, y_min))) / (log_y_max - log_y_min) * (height - 1)
|
||||||
|
step = max(1, len(x) // max(width, 1))
|
||||||
|
points: list[float] = []
|
||||||
|
for px, py in zip(x[::step], y[::step]):
|
||||||
|
points.extend((float(np.clip(px, 0, width - 1)), float(np.clip(py, 0, height - 1))))
|
||||||
|
return points
|
||||||
|
|
||||||
|
p_points = to_points(p_plot)
|
||||||
|
pmax_points = to_points(pmax)
|
||||||
|
if len(p_points) >= 4:
|
||||||
|
canvas.create_line(p_points, fill="#1f77b4", width=1.4, smooth=False, tags="limit")
|
||||||
|
if len(pmax_points) >= 4:
|
||||||
|
canvas.create_line(pmax_points, fill="#d62728", width=1.8, smooth=False, tags="limit")
|
||||||
|
|
||||||
|
critical_f = float(headroom["critical_frequency_hz"])
|
||||||
|
if math.isfinite(critical_f) and f_min <= critical_f <= f_max:
|
||||||
|
x = (math.log10(critical_f) - log_f_min) / (log_f_max - log_f_min) * (width - 1)
|
||||||
|
canvas.create_line(x, 0, x, height, fill="#e6a400", width=1.4, dash=(4, 3), tags="limit")
|
||||||
|
|
||||||
|
canvas.create_text(6, 22, anchor="nw", text=f"{y_max:g} W", fill="#888888", tags="limit")
|
||||||
|
canvas.create_text(6, height - 38, anchor="nw", text=f"{y_min:g} W", fill="#888888", tags="limit")
|
||||||
|
canvas.create_text(width - 8, 8, anchor="ne", text=f"{name} P_bin", fill="#1f77b4", tags="limit")
|
||||||
|
canvas.create_text(width - 8, 24, anchor="ne", text="WinISD Pmax", fill="#d62728", tags="limit")
|
||||||
|
self.power_limit_status_var.set(
|
||||||
|
f"{name}: P_bin vs Pmax, critical {critical_f:.1f} Hz, "
|
||||||
|
f"worst {headroom['worst_headroom_db']:+.1f} dB"
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw_headroom_grid(self) -> None:
|
||||||
|
canvas = self.headroom_canvas
|
||||||
|
canvas.delete("grid")
|
||||||
|
canvas.delete("headroom")
|
||||||
|
width = max(canvas.winfo_width(), 2)
|
||||||
|
height = max(canvas.winfo_height(), 2)
|
||||||
|
|
||||||
|
for frac in (0.0, 0.25, 0.5, 0.75, 1.0):
|
||||||
|
y = int(frac * (height - 1))
|
||||||
|
color = "#555555" if frac == 0.5 else "#2a2a2a"
|
||||||
|
canvas.create_line(0, y, width, y, fill=color, tags="grid")
|
||||||
|
x = int(frac * (width - 1))
|
||||||
|
canvas.create_line(x, 0, x, height, fill="#242424", tags="grid")
|
||||||
|
|
||||||
|
canvas.create_text(6, 6, anchor="nw", text="Headroom [dB]", fill="#bbbbbb", tags="grid")
|
||||||
|
canvas.create_text(6, height - 20, anchor="nw", text="20 Hz", fill="#bbbbbb", tags="grid")
|
||||||
|
canvas.create_text(width - 6, height - 20, anchor="ne", text="Nyquist", fill="#bbbbbb", tags="grid")
|
||||||
|
|
||||||
|
def draw_headroom(self) -> None:
|
||||||
|
data = self._winisd_plot_data()
|
||||||
|
if np is None or data is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
name, frame, headroom, curve = data
|
||||||
|
canvas = self.headroom_canvas
|
||||||
|
canvas.delete("headroom")
|
||||||
|
width = max(canvas.winfo_width(), 2)
|
||||||
|
height = max(canvas.winfo_height(), 2)
|
||||||
|
|
||||||
|
freq = frame.frequencies_hz
|
||||||
|
pmax = curve.interpolate(freq)
|
||||||
|
h_db = np.asarray(headroom["headroom_db_bin"], dtype=np.float64)
|
||||||
|
mask = np.isfinite(freq) & np.isfinite(pmax) & np.isfinite(h_db) & (freq >= 20.0) & (pmax > HEADROOM_EPS_W)
|
||||||
|
if np.count_nonzero(mask) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
freq = freq[mask]
|
||||||
|
h_db = h_db[mask]
|
||||||
|
f_min = max(20.0, float(freq[0]))
|
||||||
|
f_max = float(freq[-1])
|
||||||
|
if f_max <= f_min:
|
||||||
|
return
|
||||||
|
log_f_min = math.log10(f_min)
|
||||||
|
log_f_max = math.log10(f_max)
|
||||||
|
|
||||||
|
y_min = min(-6.0, math.floor((float(np.nanmin(h_db)) - 3.0) / 3.0) * 3.0)
|
||||||
|
h_display_top = min(float(np.nanpercentile(h_db, 95.0)), 60.0)
|
||||||
|
y_max = max(12.0, math.ceil((h_display_top + 3.0) / 3.0) * 3.0)
|
||||||
|
if y_max <= y_min:
|
||||||
|
y_max = y_min + 18.0
|
||||||
|
h_plot = np.clip(h_db, y_min, y_max)
|
||||||
|
|
||||||
|
x = (np.log10(freq) - log_f_min) / (log_f_max - log_f_min) * (width - 1)
|
||||||
|
y = (y_max - h_plot) / (y_max - y_min) * (height - 1)
|
||||||
|
step = max(1, len(x) // max(width, 1))
|
||||||
|
points: list[float] = []
|
||||||
|
for px, py in zip(x[::step], y[::step]):
|
||||||
|
points.extend((float(np.clip(px, 0, width - 1)), float(np.clip(py, 0, height - 1))))
|
||||||
|
if len(points) >= 4:
|
||||||
|
canvas.create_line(points, fill="#1f77b4", width=1.5, smooth=False, tags="headroom")
|
||||||
|
|
||||||
|
def y_for_db(value: float) -> float:
|
||||||
|
return (y_max - value) / (y_max - y_min) * (height - 1)
|
||||||
|
|
||||||
|
for value, color, label in ((0.0, "#d62728", "0 dB"), (6.0, "#e6a400", "+6 dB")):
|
||||||
|
if y_min <= value <= y_max:
|
||||||
|
y_line = y_for_db(value)
|
||||||
|
canvas.create_line(0, y_line, width, y_line, fill=color, width=1.2, dash=(5, 3), tags="headroom")
|
||||||
|
canvas.create_text(width - 8, y_line - 2, anchor="se", text=label, fill=color, tags="headroom")
|
||||||
|
|
||||||
|
critical_f = float(headroom["critical_frequency_hz"])
|
||||||
|
if math.isfinite(critical_f) and f_min <= critical_f <= f_max:
|
||||||
|
x_line = (math.log10(critical_f) - log_f_min) / (log_f_max - log_f_min) * (width - 1)
|
||||||
|
canvas.create_line(x_line, 0, x_line, height, fill="#e6a400", width=1.4, dash=(4, 3), tags="headroom")
|
||||||
|
|
||||||
|
canvas.create_text(6, 22, anchor="nw", text=f"{y_max:g} dB", fill="#888888", tags="headroom")
|
||||||
|
canvas.create_text(6, height - 38, anchor="nw", text=f"{y_min:g} dB", fill="#888888", tags="headroom")
|
||||||
|
canvas.create_text(width - 8, 8, anchor="ne", text=f"{name} H(f,t)", fill="#1f77b4", tags="headroom")
|
||||||
|
self.headroom_status_var.set(
|
||||||
|
f"{name}: worst {headroom['worst_headroom_db']:+.1f} dB "
|
||||||
|
f"at {critical_f:.1f} Hz, {headroom['status']}"
|
||||||
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
if self.capture is not None:
|
if self.capture is not None:
|
||||||
return
|
return
|
||||||
@@ -1703,6 +1948,8 @@ class LevelMonitorGUI:
|
|||||||
self.power_buffers = {}
|
self.power_buffers = {}
|
||||||
self.power_history = {}
|
self.power_history = {}
|
||||||
self.power_max_hold_w = {}
|
self.power_max_hold_w = {}
|
||||||
|
self.latest_power_frames = {}
|
||||||
|
self.latest_headroom_frames = {}
|
||||||
self.headroom_curves = {}
|
self.headroom_curves = {}
|
||||||
self.headroom_smoothed_db = {}
|
self.headroom_smoothed_db = {}
|
||||||
self.power_history_start_s = None
|
self.power_history_start_s = None
|
||||||
@@ -1719,6 +1966,8 @@ class LevelMonitorGUI:
|
|||||||
row["light"].itemconfigure(row["light_id"], fill="#22a447")
|
row["light"].itemconfigure(row["light_id"], fill="#22a447")
|
||||||
self.draw_spectrum_grid()
|
self.draw_spectrum_grid()
|
||||||
self.draw_power_history_grid()
|
self.draw_power_history_grid()
|
||||||
|
self.draw_power_limit_grid()
|
||||||
|
self.draw_headroom_grid()
|
||||||
self.start_button.configure(state="normal")
|
self.start_button.configure(state="normal")
|
||||||
self.stop_button.configure(state="disabled")
|
self.stop_button.configure(state="disabled")
|
||||||
self.calibrate_button.configure(state="disabled")
|
self.calibrate_button.configure(state="disabled")
|
||||||
|
|||||||
Reference in New Issue
Block a user