447 lines
18 KiB
Python
447 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import time
|
|
import logging
|
|
from typing import Any, Dict, Optional
|
|
|
|
from .base_widgets import BaseCommandFrame, ToolTip
|
|
from pymsc.utils.converters import (
|
|
RAD_TO_DEG, DEG_TO_RAD, NM_TO_METERS, METERS_TO_FEET,
|
|
FEET_TO_METERS, MS_TO_KNOTS, KNOTS_TO_MS
|
|
)
|
|
|
|
# Registry for finding widgets by their label (used for automation/scripts)
|
|
WIDGET_MAP: Dict[str, Any] = {}
|
|
|
|
def get_correct_value(info: Dict, idx: int, value: float) -> float:
|
|
"""
|
|
Applies LSB and Scale factors to convert a raw value to a readable value.
|
|
"""
|
|
suffix = str(idx) if idx != -1 else ""
|
|
lsb = info.get(f"lsb{suffix}", 1.0)
|
|
scale = info.get(f"scale{suffix}", 1.0)
|
|
result = value * (scale * lsb)
|
|
return result
|
|
|
|
def set_correct_value(info: Dict, idx: int, value: float) -> int:
|
|
"""
|
|
Applies LSB and Scale factors to convert a readable value to a raw integer.
|
|
"""
|
|
suffix = str(idx) if idx != -1 else ""
|
|
lsb = info.get(f"lsb{suffix}", 1.0)
|
|
scale = info.get(f"scale{suffix}", 1.0)
|
|
raw_val = value / (scale * lsb)
|
|
return int(raw_val)
|
|
|
|
class CommandFrameCheckBox(BaseCommandFrame):
|
|
"""
|
|
Widget containing a Label, a Command Checkbox, and a Tellback Checkbox.
|
|
"""
|
|
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
|
|
super().__init__(parent, command_info, **kwargs)
|
|
WIDGET_MAP[self.info['label']] = self
|
|
self._create_ui()
|
|
|
|
def _create_ui(self):
|
|
self.label = tk.Label(
|
|
self, text=self.info['label'], width=self.column_widths[0],
|
|
anchor="w", font=("Helvetica", 8)
|
|
)
|
|
self.label.grid(row=0, column=0, sticky="w", padx=5)
|
|
self.tooltip = ToolTip(self.label, self.info['description'])
|
|
|
|
if self.info.get('message'):
|
|
self.command_var = tk.BooleanVar()
|
|
self.command_check = tk.Checkbutton(
|
|
self, variable=self.command_var, command=self.on_toggle,
|
|
width=self.column_widths[1]
|
|
)
|
|
self.command_check.grid(row=0, column=1)
|
|
initial_val = self.info['message'].get_value_for_field(self.info['field'])
|
|
self.command_var.set(bool(initial_val))
|
|
|
|
if self.info.get('message_tb'):
|
|
self.tellback_var = tk.BooleanVar()
|
|
self.tellback_check = tk.Checkbutton(
|
|
self, variable=self.tellback_var, state='disabled',
|
|
width=self.column_widths[2]
|
|
)
|
|
self.tellback_check.grid(row=0, column=2)
|
|
|
|
def on_toggle(self):
|
|
self.start_time = time.time()
|
|
val = 1 if self.command_var.get() else 0
|
|
self.info['message'].set_value_for_field(self.info['field'], val)
|
|
if self.script_manager.is_recording:
|
|
action_val = "on" if val == 1 else "off"
|
|
self.script_manager.write_command(self.info['label'], "toggle", action_val)
|
|
if self.info.get('message_tb'):
|
|
self.monitor_tellback()
|
|
|
|
def monitor_tellback(self):
|
|
msg_tb = self.info['message_tb']
|
|
field_tb = self.info['field_tb']
|
|
current_tb = bool(msg_tb.get_value_for_field(field_tb))
|
|
self.tellback_var.set(current_tb)
|
|
elapsed = (time.time() - self.start_time) * 1000
|
|
if current_tb == self.command_var.get():
|
|
self.label.config(bg="green" if current_tb else "yellow")
|
|
return
|
|
if elapsed > self.threshold_ms:
|
|
self.label.config(bg="red")
|
|
return
|
|
self.after(self.update_interval_ms, self.monitor_tellback)
|
|
|
|
def check_updated_value(self):
|
|
if self.info.get('message'):
|
|
current_val = bool(self.info['message'].get_value_for_field(self.info['field']))
|
|
self.command_var.set(current_val)
|
|
if self.info.get('message_tb'):
|
|
current_tb = bool(self.info['message_tb'].get_value_for_field(self.info['field_tb']))
|
|
self.tellback_var.set(current_tb)
|
|
|
|
class CommandFrameComboBox(BaseCommandFrame):
|
|
"""
|
|
Widget containing a Label, a Command ComboBox, and a Tellback Label.
|
|
"""
|
|
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
|
|
super().__init__(parent, command_info, **kwargs)
|
|
WIDGET_MAP[self.info['label']] = self
|
|
self._create_ui()
|
|
|
|
def _create_ui(self):
|
|
self.label = tk.Label(
|
|
self, text=self.info['label'], width=self.column_widths[0],
|
|
anchor="w", font=("Helvetica", 8)
|
|
)
|
|
self.label.grid(row=0, column=0, sticky="w", padx=5)
|
|
self.tooltip = ToolTip(self.label, self.info['description'])
|
|
|
|
self.cmd_var = tk.StringVar()
|
|
self.combo = ttk.Combobox(
|
|
self, textvariable=self.cmd_var, values=self.info["values"],
|
|
width=self.column_widths[1], state="readonly"
|
|
)
|
|
self.combo.grid(row=0, column=1)
|
|
self.combo.bind("<<ComboboxSelected>>", self.on_select)
|
|
|
|
if self.info.get('message_tb'):
|
|
self.tb_var = tk.StringVar(value="---")
|
|
self.tb_label = tk.Label(
|
|
self, textvariable=self.tb_var, bg="white",
|
|
width=self.column_widths[2], relief="sunken"
|
|
)
|
|
self.tb_label.grid(row=0, column=2)
|
|
|
|
def on_select(self, event=None):
|
|
self.start_time = time.time()
|
|
idx = self.combo.current()
|
|
self.info['message'].set_value_for_field(self.info['field'], idx)
|
|
if self.script_manager.is_recording:
|
|
self.script_manager.write_command(self.info['label'], "set_value", self.combo.get())
|
|
if self.info.get('message_tb'):
|
|
self.monitor_tellback()
|
|
|
|
def monitor_tellback(self):
|
|
msg_tb = self.info['message_tb']
|
|
idx_tb = msg_tb.get_value_for_field(self.info['field_tb'])
|
|
try:
|
|
val_tb_text = self.info["values_tb"][idx_tb]
|
|
except (IndexError, KeyError):
|
|
val_tb_text = "ERR"
|
|
self.tb_var.set(val_tb_text)
|
|
elapsed = (time.time() - self.start_time) * 1000
|
|
if idx_tb == self.combo.current():
|
|
self.label.config(bg="green")
|
|
return
|
|
if elapsed > self.threshold_ms:
|
|
self.label.config(bg="red")
|
|
return
|
|
self.after(self.update_interval_ms, self.monitor_tellback)
|
|
|
|
def check_updated_value(self):
|
|
if self.info.get('message'):
|
|
current_idx = self.info['message'].get_value_for_field(self.info['field'])
|
|
try:
|
|
self.cmd_var.set(self.info['values'][current_idx])
|
|
except IndexError:
|
|
self.cmd_var.set("ERR")
|
|
if self.info.get('message_tb'):
|
|
current_tb_idx = self.info['message_tb'].get_value_for_field(self.info['field_tb'])
|
|
try:
|
|
self.tb_var.set(self.info['values_tb'][current_tb_idx])
|
|
except IndexError:
|
|
self.tb_var.set("ERR")
|
|
|
|
class CommandFrameSpinBox(BaseCommandFrame):
|
|
"""
|
|
Widget containing a Label, a Command SpinBox, and a Tellback Label.
|
|
Supports scaling (LSB/Scale) and numeric validation.
|
|
"""
|
|
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
|
|
super().__init__(parent, command_info, **kwargs)
|
|
WIDGET_MAP[self.info['label']] = self
|
|
self.is_programmatic_change = False
|
|
self._create_ui()
|
|
|
|
def _create_ui(self):
|
|
self.label = tk.Label(
|
|
self, text=self.info['label'], width=self.column_widths[0],
|
|
anchor="w", font=("Helvetica", 8)
|
|
)
|
|
self.label.grid(row=0, column=0, sticky="w", padx=5)
|
|
self.tooltip = ToolTip(self.label, self.info['description'])
|
|
|
|
is_int = self.info.get("number_format", "float") == "integer"
|
|
self.cmd_var = tk.IntVar() if is_int else tk.DoubleVar()
|
|
self.cmd_var.set(self.info.get('default_value', 0))
|
|
|
|
v_cmd = (self.register(self._validate), '%P')
|
|
self.spin = tk.Spinbox(
|
|
self, textvariable=self.cmd_var, from_=self.info["min_value"],
|
|
to=self.info["max_value"], increment=self.info.get("increment", 1),
|
|
width=self.column_widths[1], validate="key", validatecommand=v_cmd
|
|
)
|
|
self.spin.grid(row=0, column=1)
|
|
self.spin.bind("<Return>", self.on_change)
|
|
self.spin.bind("<FocusOut>", self.on_change)
|
|
|
|
if self.info.get('message_tb'):
|
|
self.tb_var = tk.StringVar(value="0")
|
|
self.tb_label = tk.Label(
|
|
self, textvariable=self.tb_var, bg="white",
|
|
width=self.column_widths[2], relief="sunken"
|
|
)
|
|
self.tb_label.grid(row=0, column=2)
|
|
|
|
def _validate(self, p):
|
|
if p == "" or p == "-":
|
|
return True
|
|
try:
|
|
float(p)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
def on_change(self, event=None):
|
|
if self.is_programmatic_change:
|
|
return
|
|
self.start_time = time.time()
|
|
try:
|
|
val = float(self.cmd_var.get())
|
|
except tk.TclError:
|
|
return
|
|
raw_val = set_correct_value(self.info, -1, val)
|
|
self.info['message'].set_value_for_field(self.info['field'], raw_val)
|
|
if self.script_manager.is_recording:
|
|
self.script_manager.write_command(self.info['label'], "set_value", val)
|
|
if self.info.get('message_tb'):
|
|
self.monitor_tellback()
|
|
|
|
def monitor_tellback(self):
|
|
msg_tb = self.info['message_tb']
|
|
raw_tb = msg_tb.get_value_for_field(self.info['field_tb'])
|
|
readable_tb = get_correct_value(self.info, -1, raw_tb)
|
|
fmt = "%.0f" if self.info.get("number_format") == "integer" else "%.4f"
|
|
self.tb_var.set(fmt % readable_tb)
|
|
elapsed = (time.time() - self.start_time) * 1000
|
|
target = float(self.cmd_var.get())
|
|
if abs(readable_tb - target) < 0.0001:
|
|
self.label.config(bg="green")
|
|
return
|
|
if elapsed > self.threshold_ms:
|
|
self.label.config(bg="red")
|
|
return
|
|
self.after(self.update_interval_ms, self.monitor_tellback)
|
|
|
|
def check_updated_value(self):
|
|
self.is_programmatic_change = True
|
|
raw_val = self.info['message'].get_value_for_field(self.info['field'])
|
|
readable = get_correct_value(self.info, -1, raw_val)
|
|
self.cmd_var.set(readable)
|
|
self.is_programmatic_change = False
|
|
if self.info.get('message_tb'):
|
|
raw_tb = self.info['message_tb'].get_value_for_field(self.info['field_tb'])
|
|
readable_tb = get_correct_value(self.info, -1, raw_tb)
|
|
fmt = "%.0f" if self.info.get("number_format") == "integer" else "%.4f"
|
|
self.tb_var.set(fmt % readable_tb)
|
|
|
|
class CommandFrameLabels(tk.Frame):
|
|
"""
|
|
Widget for displaying up to 3 read-only values from the bus.
|
|
"""
|
|
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
|
|
super().__init__(parent)
|
|
self.info = command_info
|
|
self.vars = {}
|
|
self.column_widths = [20, 14, 14, 14]
|
|
WIDGET_MAP[self.info['label']] = self
|
|
self._create_ui()
|
|
|
|
def _create_ui(self):
|
|
self.label = tk.Label(
|
|
self, text=self.info['label'], width=self.column_widths[0],
|
|
anchor="w", font=("Helvetica", 8)
|
|
)
|
|
self.label.grid(row=0, column=0, sticky="w", padx=5)
|
|
ToolTip(self.label, self.info['description'])
|
|
|
|
for i in range(1, 4):
|
|
msg = self.info.get(f"message{i}")
|
|
if msg:
|
|
field = self.info[f"field{i}"]
|
|
self.vars[field] = tk.StringVar(value="0")
|
|
lbl = tk.Label(
|
|
self, textvariable=self.vars[field], bg="#f0f0f0",
|
|
width=self.column_widths[i], relief="groove", font=("Helvetica", 8)
|
|
)
|
|
lbl.grid(row=0, column=i, padx=2)
|
|
ToolTip(lbl, self.info.get(f"tooltip{i}", ""))
|
|
|
|
def check_updated_value(self):
|
|
for i in range(1, 4):
|
|
msg = self.info.get(f"message{i}")
|
|
if msg:
|
|
field = self.info[f"field{i}"]
|
|
raw = msg.get_value_for_field(field)
|
|
val = get_correct_value(self.info, i, raw)
|
|
self.vars[field].set("%.4f" % val)
|
|
|
|
class CommandFrameControls(tk.Frame):
|
|
"""
|
|
Highly flexible widget that can combine up to 3 sub-controls.
|
|
"""
|
|
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
|
|
super().__init__(parent)
|
|
self.info = command_info
|
|
self.column_widths = [20, 14, 14, 14]
|
|
self.vars = {}
|
|
self.is_programmatic_change = False
|
|
WIDGET_MAP[self.info['label']] = self
|
|
self._create_ui()
|
|
|
|
def _create_ui(self):
|
|
self.title_label = tk.Label(
|
|
self, text=self.info['label'], width=self.column_widths[0],
|
|
anchor="w", font=("Helvetica", 8)
|
|
)
|
|
self.title_label.grid(row=0, column=0, sticky="w", padx=5)
|
|
ToolTip(self.title_label, self.info['description'])
|
|
|
|
for i in range(1, 4):
|
|
ctrl_type = self.info.get(f"type{i}")
|
|
if not ctrl_type:
|
|
continue
|
|
self._add_sub_control(i, ctrl_type)
|
|
|
|
def _add_sub_control(self, i: int, ctrl_type: str):
|
|
field = self.info[f"field{i}"]
|
|
msg = self.info.get(f"message{i}")
|
|
|
|
if ctrl_type == "checkbox":
|
|
self.vars[field] = tk.BooleanVar()
|
|
lbl_text = self.info.get(f"label{i}", "")
|
|
cb = tk.Checkbutton(
|
|
self, text=lbl_text, variable=self.vars[field], width=self.column_widths[i],
|
|
command=lambda f=field, m=msg: self._on_check(f, m)
|
|
)
|
|
cb.grid(row=0, column=i)
|
|
WIDGET_MAP[field] = {"widget": cb, "var": self.vars[field]}
|
|
|
|
elif ctrl_type == "combobox":
|
|
self.vars[field] = tk.StringVar()
|
|
vals = self.info.get(f"values{i}", [])
|
|
cmb = ttk.Combobox(
|
|
self, textvariable=self.vars[field], values=vals,
|
|
width=self.column_widths[i], state="readonly"
|
|
)
|
|
cmb.grid(row=0, column=i)
|
|
cmb.bind("<<ComboboxSelected>>", lambda e, f=field, m=msg, idx=i: self._on_combo(f, m, idx))
|
|
WIDGET_MAP[field] = {"widget": cmb, "var": self.vars[field]}
|
|
|
|
elif ctrl_type == "spinbox":
|
|
number_format = self.info.get(f"number_format{i}", "float")
|
|
is_int = number_format == "integer"
|
|
self.vars[field] = tk.IntVar() if is_int else tk.DoubleVar()
|
|
fmt = '%.0f' if is_int else '%.4f'
|
|
min_v = self.info.get(f"min_value{i}", -999999)
|
|
max_v = self.info.get(f"max_value{i}", 999999)
|
|
inc_v = self.info.get(f"increment{i}", 1)
|
|
|
|
sp = tk.Spinbox(
|
|
self, textvariable=self.vars[field], width=self.column_widths[i],
|
|
from_=min_v, to=max_v, increment=inc_v, format=fmt
|
|
)
|
|
sp.grid(row=0, column=i)
|
|
sp.bind("<Return>", lambda e, f=field, m=msg, idx=i: self._on_spin(f, m, idx))
|
|
WIDGET_MAP[field] = {"widget": sp, "var": self.vars[field]}
|
|
|
|
elif ctrl_type == "label":
|
|
self.vars[field] = tk.StringVar(value="0")
|
|
lbl = tk.Label(
|
|
self, textvariable=self.vars[field], relief="groove",
|
|
width=self.column_widths[i], anchor="w", font=("Helvetica", 8)
|
|
)
|
|
lbl.grid(row=0, column=i)
|
|
|
|
elif ctrl_type == "space":
|
|
lbl = tk.Label(self, text="", width=self.column_widths[i])
|
|
lbl.grid(row=0, column=i)
|
|
|
|
def _on_check(self, field: str, msg: Any):
|
|
val = 1 if self.vars[field].get() else 0
|
|
msg.set_value_for_field(field, val)
|
|
from pymsc.gui.script_manager import get_script_manager
|
|
sm = get_script_manager()
|
|
if sm.is_recording:
|
|
sm.write_command(field, "toggle", "on" if val else "off")
|
|
|
|
def _on_combo(self, field: str, msg: Any, idx: int):
|
|
cmb = WIDGET_MAP[field]["widget"]
|
|
val_idx = cmb.current()
|
|
msg.set_value_for_field(field, val_idx)
|
|
from pymsc.gui.script_manager import get_script_manager
|
|
sm = get_script_manager()
|
|
if sm.is_recording:
|
|
sm.write_command(field, "set_value", cmb.get())
|
|
|
|
def _on_spin(self, field: str, msg: Any, idx: int):
|
|
if self.is_programmatic_change:
|
|
return
|
|
val = float(self.vars[field].get())
|
|
raw = set_correct_value(self.info, idx, val)
|
|
msg.set_value_for_field(field, raw)
|
|
from pymsc.gui.script_manager import get_script_manager
|
|
sm = get_script_manager()
|
|
if sm.is_recording:
|
|
sm.write_command(field, "set_value", val)
|
|
|
|
def check_updated_value(self):
|
|
"""
|
|
Syncs sub-controls with their corresponding 1553 message fields.
|
|
Renamed from check_actual_value for consistency with MainWindow refresh loop.
|
|
"""
|
|
self.is_programmatic_change = True
|
|
for i in range(1, 4):
|
|
ctrl_type = self.info.get(f"type{i}")
|
|
if not ctrl_type or ctrl_type == "space":
|
|
continue
|
|
field = self.info.get(f"field{i}")
|
|
msg = self.info.get(f"message{i}")
|
|
if not msg or not field:
|
|
continue
|
|
raw = msg.get_value_for_field(field)
|
|
readable = get_correct_value(self.info, i, raw)
|
|
if ctrl_type == "checkbox":
|
|
self.vars[field].set(bool(raw))
|
|
elif ctrl_type == "combobox":
|
|
try:
|
|
self.vars[field].set(self.info[f"values{i}"][int(raw)])
|
|
except IndexError:
|
|
self.vars[field].set("ERR")
|
|
elif ctrl_type == "spinbox":
|
|
self.vars[field].set(readable)
|
|
elif ctrl_type == "label":
|
|
self.vars[field].set("%.4f" % readable)
|
|
self.is_programmatic_change = False |