Files
ozone-tech_owl_prime/backend_control/gui.py
Даня Архипов 5cc27c6fc2 chore: add unique assets from other branches into dan_branch
Bring hardware/spec content from drho1y-mvp_1 (3d_models, arduino_code, backend_control, kicad, specification) so dan_branch holds the shared union of branch files without rewriting other branch tips.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 14:32:56 +00:00

460 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import customtkinter as ctk
import paho.mqtt.client as mqtt
# ================= НАСТРОЙКИ =================
MQTT_BROKER = "192.168.0.200"
MQTT_PORT = 1883
MQTT_USER = "test"
MQTT_PASSWORD = "1234"
MAX_SERVOS = 4
MAX_SENSORS = 8 # Количество каналов VL53L0X
# Цвета
COLOR_OK = "#28a745"
COLOR_ERR = "#dc3545"
COLOR_OFF = "#555555"
COLOR_ACTIVE = "#00d2ff"
COLOR_PENDING = "#ffaa00"
COLOR_WARN = "#ff9900" # Для out_of_range
class MotorSCADA:
def __init__(self):
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
self.root = ctk.CTk()
self.root.title("🚀 Motor, Servo & Sensor SCADA")
self.root.geometry("1200x900")
self.root.minsize(1100, 800)
# Флаги ожидания
self.driver_pending = False
self.tmc_pending = False
self.servo_pending = {i: False for i in range(MAX_SERVOS)}
self.sensor_pending = {i: False for i in range(MAX_SENSORS)}
self.setup_gui()
self.setup_mqtt()
def setup_gui(self):
# --- Шапка ---
header = ctk.CTkFrame(self.root, height=60)
header.pack(fill="x", padx=20, pady=(20, 10))
header.pack_propagate(False)
ctk.CTkLabel(header, text="Motor, Servo & Sensor SCADA", font=ctk.CTkFont(size=24, weight="bold")).pack(side="left", padx=20)
self.lbl_status = ctk.CTkLabel(header, text="● Отключено", text_color=COLOR_ERR, font=ctk.CTkFont(size=16, weight="bold"))
self.lbl_status.pack(side="right", padx=20)
# --- Вкладки ---
self.tabview = ctk.CTkTabview(self.root)
self.tabview.pack(fill="both", expand=True, padx=20, pady=10)
self.tab_motor = self.tabview.add("Шаговый двигатель (TMC2209)")
self.tab_servo = self.tabview.add(f"Сервоприводы (0-{MAX_SERVOS-1})")
self.tab_sensor = self.tabview.add(f"Датчики VL53L0X (0-{MAX_SENSORS-1})")
self.create_motor_tab(self.tab_motor)
self.create_servo_tab(self.tab_servo)
self.create_sensor_tab(self.tab_sensor)
# ================= ВКЛАДКА ШАГОВОГО ДВИГАТЕЛЯ =================
# (Код для мотора остался без изменений, чтобы не раздувать ответ,
# но в реальном файле он должен быть здесь полностью)
def create_motor_tab(self, parent):
grid = ctk.CTkFrame(parent, fg_color="transparent")
grid.pack(fill="both", expand=True)
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform="col")
grid.grid_rowconfigure(0, weight=1)
self.create_control_frame(grid)
self.create_modes_frame(grid)
self.create_telemetry_frame(grid)
def create_control_frame(self, parent):
frame = ctk.CTkFrame(parent)
frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
ctk.CTkLabel(frame, text="⚙️ Управление", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20))
ctk.CTkLabel(frame, text="Целевой RPM:").pack(anchor="w", padx=20)
rpm_frame = ctk.CTkFrame(frame, fg_color="transparent")
rpm_frame.pack(fill="x", padx=20, pady=5)
self.sld_rpm = ctk.CTkSlider(rpm_frame, from_=-1000, to=1000, command=self.on_rpm_slider_change)
self.sld_rpm.pack(side="left", fill="x", expand=True, padx=(0, 10))
self.ent_rpm = ctk.CTkEntry(rpm_frame, width=80, justify="right")
self.ent_rpm.insert(0, "0"); self.ent_rpm.pack(side="left", padx=(0, 10))
self.ent_rpm.bind("<Return>", self.on_rpm_entry_apply); self.ent_rpm.bind("<FocusOut>", self.on_rpm_entry_apply)
self.lbl_rpm_val = ctk.CTkLabel(rpm_frame, text="0", width=50); self.lbl_rpm_val.pack(side="right")
ctk.CTkLabel(frame, text="Ток (%):").pack(anchor="w", padx=20, pady=(15,0))
cur_frame = ctk.CTkFrame(frame, fg_color="transparent")
cur_frame.pack(fill="x", padx=20, pady=5)
self.sld_current = ctk.CTkSlider(cur_frame, from_=0, to=100, command=self.on_current_change)
self.sld_current.pack(side="left", fill="x", expand=True)
self.lbl_cur_val = ctk.CTkLabel(cur_frame, text="50", width=50); self.lbl_cur_val.pack(side="right", padx=(10, 0))
ctk.CTkLabel(frame, text="StallGuard (0-255):").pack(anchor="w", padx=20, pady=(15,0))
self.ent_sg = ctk.CTkEntry(frame, width=100); self.ent_sg.insert(0, "0")
self.ent_sg.pack(anchor="w", padx=20, pady=5)
ctk.CTkButton(frame, text="Применить SG", width=150, command=self.on_sg_apply).pack(pady=5)
ctk.CTkLabel(frame, text="Микрошаги:").pack(anchor="w", padx=20, pady=(15,0))
self.opt_msteps = ctk.CTkOptionMenu(frame, values=["1", "2", "4", "8", "16", "32", "64", "128", "256"], command=self.on_msteps_change)
self.opt_msteps.set("16"); self.opt_msteps.pack(anchor="w", padx=20, pady=5)
ctk.CTkButton(frame, text="Сбросить счетчик шагов", fg_color="#dc3545", hover_color="#b02a37", command=self.on_reset_steps).pack(pady=20)
def create_modes_frame(self, parent):
frame = ctk.CTkFrame(parent)
frame.grid(row=0, column=1, sticky="nsew", padx=10)
ctk.CTkLabel(frame, text="🔌 Режимы и Включение", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20))
self.sw_driver, self.led_driver_fb = self.create_switch_with_feedback(frame, "Аппаратное вкл. (Driver EN)", self.on_driver_change)
self.sw_tmc_enable, self.led_tmc_fb = self.create_switch_with_feedback(frame, "Программное вкл. (TMC Chip)", self.on_tmc_enable_change)
ctk.CTkFrame(frame, height=2, fg_color="#4a4a6a").pack(fill="x", padx=20, pady=15)
self.sw_stealth = self.create_switch(frame, "StealthChop (Тихий)", self.on_stealth_change)
self.sw_cool = self.create_switch(frame, "CoolStep (Энергосбер.)", self.on_cool_change)
def create_telemetry_frame(self, parent):
frame = ctk.CTkFrame(parent)
frame.grid(row=0, column=2, sticky="nsew", padx=(10, 0))
ctk.CTkLabel(frame, text="📊 Телеметрия и Статусы", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 10))
tel_frame = ctk.CTkFrame(frame); tel_frame.pack(fill="x", padx=10, pady=5)
self.lbl_fb_rpm = self.create_telemetry_row(tel_frame, "Текущий RPM:")
self.lbl_fb_steps = self.create_telemetry_row(tel_frame, "Всего шагов:")
self.lbl_fb_run = self.create_telemetry_row(tel_frame, "Вращение:")
self.lbl_fb_sg = self.create_telemetry_row(tel_frame, "SG Result:")
self.lbl_fb_interstep = self.create_telemetry_row(tel_frame, "Interstep:")
self.lbl_fb_cscale = self.create_telemetry_row(tel_frame, "Current Scaling:")
ctk.CTkLabel(frame, text="🚨 Ошибки и Флаги", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(15, 5))
stat_frame = ctk.CTkFrame(frame); stat_frame.pack(fill="x", padx=10, pady=5)
self.led_over_temp = self.create_led_row(stat_frame, "Перегрев:")
self.led_short_gnd = self.create_led_row(stat_frame, "КЗ на землю:")
self.led_open_load = self.create_led_row(stat_frame, "Обрыв нагрузки:")
self.led_stealth_act = self.create_led_row(stat_frame, "StealthChop активен:")
self.led_standstill = self.create_led_row(stat_frame, "Остановка:")
# ================= ВКЛАДКА СЕРВОПРИВОДОВ =================
def create_servo_tab(self, parent):
grid = ctk.CTkFrame(parent, fg_color="transparent")
grid.pack(fill="both", expand=True, padx=10, pady=10)
grid.grid_columnconfigure((0, 1), weight=1, uniform="col")
grid.grid_rowconfigure((0, 1), weight=1, uniform="row")
self.servo_ui = {}
for i in range(MAX_SERVOS):
row, col = divmod(i, 2)
frame = ctk.CTkFrame(grid)
frame.grid(row=row, column=col, sticky="nsew", padx=10, pady=10)
self.servo_ui[i] = self.create_servo_card(frame, i)
def create_servo_card(self, parent, channel):
ui = {}
ctk.CTkLabel(parent, text=f"🦾 Сервопривод #{channel}", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(10, 10))
ctk.CTkLabel(parent, text="Угол (0-180°):").pack(anchor="w", padx=20)
ang_frame = ctk.CTkFrame(parent, fg_color="transparent"); ang_frame.pack(fill="x", padx=20, pady=5)
sld = ctk.CTkSlider(ang_frame, from_=0, to=180, command=lambda val, ch=channel: self.on_servo_ang_slider(ch, val))
sld.pack(side="left", fill="x", expand=True, padx=(0, 10)); sld.set(90)
ent = ctk.CTkEntry(ang_frame, width=60, justify="right"); ent.insert(0, "90"); ent.pack(side="left", padx=(0, 10))
ent.bind("<Return>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
ent.bind("<FocusOut>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
lbl_val = ctk.CTkLabel(ang_frame, text="90", width=40); lbl_val.pack(side="right")
ui['slider_ang'], ui['entry_ang'], ui['lbl_ang_val'] = sld, ent, lbl_val
sw, led_fb = self.create_switch_with_feedback(parent, "Включить серво", lambda ch=channel: self.on_servo_enable_change(ch))
ui['switch_en'], ui['led_fb'] = sw, led_fb
tel_frame = ctk.CTkFrame(parent); tel_frame.pack(fill="x", padx=10, pady=15)
ui['lbl_fb_ang'] = self.create_telemetry_row(tel_frame, "Текущий угол:")
stat_frame = ctk.CTkFrame(parent); stat_frame.pack(fill="x", padx=10, pady=5)
ui['led_status'] = self.create_led_row(stat_frame, "Статус:")
return ui
# ================= ВКЛАДКА ДАТЧИКОВ VL53L0X =================
def create_sensor_tab(self, parent):
main_frame = ctk.CTkFrame(parent, fg_color="transparent")
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# --- Глобальное управление ---
global_frame = ctk.CTkFrame(main_frame)
global_frame.pack(fill="x", padx=10, pady=(0, 10))
ctk.CTkLabel(global_frame, text="📏 Глобальные настройки VL53L0X", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 5))
info_frame = ctk.CTkFrame(global_frame, fg_color="transparent")
info_frame.pack(fill="x", padx=20, pady=10)
self.lbl_sensor_mode = self.create_telemetry_row(info_frame, "Режим:")
self.lbl_sensor_mode_id = self.create_telemetry_row(info_frame, "ID режима:")
self.lbl_sensor_max_range = self.create_telemetry_row(info_frame, "Макс. дальность (мм):")
ctrl_frame = ctk.CTkFrame(global_frame, fg_color="transparent")
ctrl_frame.pack(fill="x", padx=20, pady=(0, 10))
ctk.CTkLabel(ctrl_frame, text="Выбрать режим:").pack(side="left", padx=(0, 10))
# Предполагаем, что режимов от 0 до 4 (Default, HighAccuracy, LongRange, HighSpeed)
self.opt_sensor_mode = ctk.CTkOptionMenu(ctrl_frame, values=["0", "1", "2", "3", "4"], width=100, command=self.on_sensor_mode_change)
self.opt_sensor_mode.set("0"); self.opt_sensor_mode.pack(side="left", padx=(0, 20))
ctk.CTkButton(ctrl_frame, text="🔄 Принудительно обновить все", fg_color="#007bff", hover_color="#0056b3", command=self.on_sensor_publish_all).pack(side="right")
# --- Сетка каналов (Scrollable) ---
scroll_frame = ctk.CTkScrollableFrame(main_frame)
scroll_frame.pack(fill="both", expand=True, padx=10, pady=10)
scroll_frame.grid_columnconfigure((0, 1, 2, 3), weight=1, uniform="col")
self.sensor_ui = {}
for i in range(MAX_SENSORS):
row, col = divmod(i, 4)
frame = ctk.CTkFrame(scroll_frame)
frame.grid(row=row, column=col, sticky="nsew", padx=5, pady=5)
self.sensor_ui[i] = self.create_sensor_card(frame, i)
def create_sensor_card(self, parent, channel):
ui = {}
ctk.CTkLabel(parent, text=f"📡 Канал #{channel}", font=ctk.CTkFont(size=14, weight="bold")).pack(pady=(5, 5))
# Включение и калибровка
sw, led_fb = self.create_switch_with_feedback(parent, "Включить", lambda ch=channel: self.on_sensor_enable_change(ch))
ui['switch_en'], ui['led_fb'] = sw, led_fb
cal_stat_frame = ctk.CTkFrame(parent, fg_color="transparent")
cal_stat_frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(cal_stat_frame, text="Калибровка:").pack(side="left")
ui['led_calibrated'] = ctk.CTkLabel(cal_stat_frame, text="●", font=ctk.CTkFont(size=16), text_color=COLOR_OFF)
ui['led_calibrated'].pack(side="right")
# Поля калибровки
cal_ctrl_frame = ctk.CTkFrame(parent, fg_color="transparent")
cal_ctrl_frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(cal_ctrl_frame, text="Ближняя (мм):").pack(anchor="w")
ent_near = ctk.CTkEntry(cal_ctrl_frame, width=60, justify="right"); ent_near.insert(0, "50"); ent_near.pack(side="left", padx=(0, 5))
btn_start = ctk.CTkButton(cal_ctrl_frame, text="Старт", width=60, height=28, command=lambda ch=channel, e=ent_near: self.on_sensor_cal_start(ch, e))
btn_start.pack(side="right")
ctk.CTkLabel(cal_ctrl_frame, text="Дальняя (мм):").pack(anchor="w", pady=(5,0))
ent_far = ctk.CTkEntry(cal_ctrl_frame, width=60, justify="right"); ent_far.insert(0, "500"); ent_far.pack(side="left", padx=(0, 5), pady=(5,0))
btn_finish = ctk.CTkButton(cal_ctrl_frame, text="Финиш", width=60, height=28, command=lambda ch=channel, e=ent_far: self.on_sensor_cal_finish(ch, e))
btn_finish.pack(side="right")
ctk.CTkButton(parent, text="Сбросить калибровку", fg_color="#6c757d", hover_color="#5a6268", height=28, command=lambda ch=channel: self.on_sensor_clear_cal(ch)).pack(pady=5)
ui['ent_near'], ui['ent_far'] = ent_near, ent_far
# Телеметрия
tel_frame = ctk.CTkFrame(parent)
tel_frame.pack(fill="x", padx=5, pady=5)
ui['lbl_dist'] = self.create_telemetry_row(tel_frame, "Дист. (мм):")
ui['lbl_raw'] = self.create_telemetry_row(tel_frame, "Сырое (мм):")
return ui
# ================= ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ GUI =================
def create_switch(self, parent, text, command):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(frame, text=text).pack(side="left")
switch = ctk.CTkSwitch(frame, text="", command=command); switch.pack(side="right")
return switch
def create_switch_with_feedback(self, parent, text, command):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(frame, text=text).pack(side="left")
feedback_led = ctk.CTkLabel(frame, text="●", font=ctk.CTkFont(size=18), text_color=COLOR_OFF)
feedback_led.pack(side="right", padx=(10, 0))
switch = ctk.CTkSwitch(frame, text="", command=command); switch.pack(side="right", padx=(10, 0))
return switch, feedback_led
def create_telemetry_row(self, parent, text):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", pady=2)
ctk.CTkLabel(frame, text=text, anchor="w", font=ctk.CTkFont(size=12)).pack(side="left")
val_lbl = ctk.CTkLabel(frame, text="-", font=ctk.CTkFont(weight="bold", size=12), text_color=COLOR_ACTIVE, anchor="e")
val_lbl.pack(side="right")
return val_lbl
def create_led_row(self, parent, text):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", pady=2)
ctk.CTkLabel(frame, text=text, anchor="w", font=ctk.CTkFont(size=12)).pack(side="left")
led_lbl = ctk.CTkLabel(frame, text="●", font=ctk.CTkFont(size=16), text_color=COLOR_OFF)
led_lbl.pack(side="right")
return led_lbl
# ================= MQTT =================
def setup_mqtt(self):
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="python_scada")
self.client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
self.client.on_connect = self.on_mqtt_connect
self.client.on_disconnect = self.on_mqtt_disconnect
self.client.on_message = self.on_mqtt_message
try:
self.client.connect(MQTT_BROKER, MQTT_PORT, 60)
self.client.loop_start()
except Exception as e:
print(f"Ошибка подключения: {e}")
self.update_status(False)
def on_mqtt_connect(self, client, userdata, flags, reason_code, properties):
if reason_code == 0:
self.root.after(0, self.update_status, True)
client.subscribe("motor/feedback/#")
client.subscribe("servo/+/feedback/#")
client.subscribe("sensor/feedback/#") # Подписка на датчики
else:
self.root.after(0, self.update_status, False)
def on_mqtt_disconnect(self, client, userdata, flags, reason_code, properties):
self.root.after(0, self.update_status, False)
def on_mqtt_message(self, client, userdata, msg):
self.root.after(0, self.process_feedback, msg.topic, msg.payload.decode('utf-8'))
def process_feedback(self, topic, val):
parts = topic.split('/')
# --- MOTOR ---
if parts[0] == 'motor' and parts[1] == 'feedback':
if topic == "motor/feedback/rpm": self.lbl_fb_rpm.configure(text=val)
elif topic == "motor/feedback/totalsteps": self.lbl_fb_steps.configure(text=val)
elif topic == "motor/feedback/is_run":
is_run = val == "true"
self.lbl_fb_run.configure(text="Да" if is_run else "Нет", text_color=COLOR_OK if is_run else COLOR_ERR)
elif topic == "motor/feedback/tmc/current_percent": self.lbl_cur_val.configure(text=val); self.sld_current.set(int(val))
elif topic == "motor/feedback/tmc/microsteps": self.opt_msteps.set(val)
elif topic == "motor/feedback/tmc/sg_result": self.lbl_fb_sg.configure(text=val)
elif topic == "motor/feedback/tmc/interstep_duration": self.lbl_fb_interstep.configure(text=val)
elif topic == "motor/feedback/tmc/status/current_scaling": self.lbl_fb_cscale.configure(text=val)
elif topic == "motor/feedback/tmc/status/over_temp": self.update_led(self.led_over_temp, val, True)
elif topic == "motor/feedback/tmc/status/short_to_ground": self.update_led(self.led_short_gnd, val, True)
elif topic == "motor/feedback/tmc/status/open_load": self.update_led(self.led_open_load, val, True)
elif topic == "motor/feedback/tmc/status/stealth_chop_active": self.update_led(self.led_stealth_act, val, False)
elif topic == "motor/feedback/tmc/status/standstill": self.update_led(self.led_standstill, val, False)
elif topic == "motor/feedback/driver/status":
is_on = val == "on"; self.driver_pending = False
if self.sw_driver.get() != is_on: self.sw_driver.select() if is_on else self.sw_driver.deselect()
self.led_driver_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF)
elif topic == "motor/feedback/tmc/status":
is_on = val == "on"; self.tmc_pending = False
if self.sw_tmc_enable.get() != is_on: self.sw_tmc_enable.select() if is_on else self.sw_tmc_enable.deselect()
self.led_tmc_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF)
# --- SERVO ---
elif parts[0] == 'servo' and len(parts) == 4 and parts[2] == 'feedback':
try:
ch = int(parts[1]); param = parts[3]
if ch in self.servo_ui:
ui = self.servo_ui[ch]
if param == 'angle': ui['lbl_fb_ang'].configure(text=val)
elif param == 'status':
is_on = val == "on"; self.servo_pending[ch] = False
if ui['switch_en'].get() != is_on: ui['switch_en'].select() if is_on else ui['switch_en'].deselect()
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
self.update_led(ui['led_status'], val, False)
except ValueError: pass
# --- SENSOR ---
elif parts[0] == 'sensor' and parts[1] == 'feedback':
if len(parts) == 3: # Глобальные sensor/feedback/mode...
param = parts[2]
if param == 'mode': self.lbl_sensor_mode.configure(text=val)
elif param == 'mode_id': self.lbl_sensor_mode_id.configure(text=val); self.opt_sensor_mode.set(val)
elif param == 'max_range': self.lbl_sensor_max_range.configure(text=val)
elif len(parts) == 4: # Канальные sensor/feedback/{ch}/...
try:
ch = int(parts[2]); param = parts[3]
if ch in self.sensor_ui:
ui = self.sensor_ui[ch]
if param == 'status':
is_on = val == "on"; self.sensor_pending[ch] = False
if ui['switch_en'].get() != is_on: ui['switch_en'].select() if is_on else ui['switch_en'].deselect()
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
elif param == 'calibrated':
is_cal = val == "true"
ui['led_calibrated'].configure(text_color=COLOR_OK if is_cal else COLOR_OFF)
elif param == 'distance':
if val == "out_of_range":
ui['lbl_dist'].configure(text="Вне диапазона", text_color=COLOR_WARN)
else:
ui['lbl_dist'].configure(text=val, text_color=COLOR_ACTIVE)
elif param == 'raw':
ui['lbl_raw'].configure(text=val, text_color=COLOR_ACTIVE)
except ValueError: pass
# ================= ОБРАБОТЧИКИ СОБЫТИЙ =================
def publish(self, topic, payload):
if self.client.is_connected(): self.client.publish(topic, str(payload), qos=1)
def update_status(self, is_online):
self.lbl_status.configure(text="● Подключено" if is_online else "● Отключено", text_color=COLOR_OK if is_online else COLOR_ERR)
def update_led(self, label, val, is_error):
is_true = val in ["true", "1", "on"]
label.configure(text_color=COLOR_ERR if (is_true and is_error) else (COLOR_OK if is_true else COLOR_OFF))
# --- Motor Handlers ---
def on_rpm_slider_change(self, value):
int_val = int(value); self.ent_rpm.delete(0, ctk.END); self.ent_rpm.insert(0, str(int_val))
self.lbl_rpm_val.configure(text=str(int_val)); self.publish("motor/control/rpm", int_val)
def on_rpm_entry_apply(self, event=None):
try:
int_val = max(-1000, min(1000, int(self.ent_rpm.get())))
self.sld_rpm.set(int_val); self.lbl_rpm_val.configure(text=str(int_val)); self.publish("motor/control/rpm", int_val)
except ValueError: self.ent_rpm.delete(0, ctk.END); self.ent_rpm.insert(0, str(int(self.sld_rpm.get())))
def on_current_change(self, value):
int_val = int(value); self.lbl_cur_val.configure(text=str(int_val)); self.publish("motor/control/tmc/current_percent", int_val)
def on_sg_apply(self):
val = self.ent_sg.get()
if val.isdigit() and 0 <= int(val) <= 255: self.publish("motor/control/tmc/stallguard", int(val))
def on_msteps_change(self, choice): self.publish("motor/control/tmc/microsteps", int(choice))
def on_reset_steps(self): self.publish("motor/control/totalsteps/reset", "1")
def on_driver_change(self):
is_on = self.sw_driver.get(); self.driver_pending = True; self.led_driver_fb.configure(text_color=COLOR_PENDING)
self.publish("motor/control/driver", "on" if is_on else "off")
def on_tmc_enable_change(self):
is_on = self.sw_tmc_enable.get(); self.tmc_pending = True; self.led_tmc_fb.configure(text_color=COLOR_PENDING)
self.publish("motor/control/tmc/enable", "on" if is_on else "off")
def on_stealth_change(self): self.publish("motor/control/tmc/stealthchop", "on" if self.sw_stealth.get() else "off")
def on_cool_change(self): self.publish("motor/control/tmc/coolstep", "on" if self.sw_cool.get() else "off")
# --- Servo Handlers ---
def on_servo_ang_slider(self, channel, value):
int_val = int(value); ui = self.servo_ui[channel]
ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int_val))
ui['lbl_ang_val'].configure(text=str(int_val)); self.publish(f"servo/control/{channel}/angle", int_val)
def on_servo_ang_entry(self, channel, event=None):
ui = self.servo_ui[channel]
try:
int_val = max(0, min(180, int(ui['entry_ang'].get())))
ui['slider_ang'].set(int_val); ui['lbl_ang_val'].configure(text=str(int_val)); self.publish(f"servo/control/{channel}/angle", int_val)
except ValueError: ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int(ui['slider_ang'].get())))
def on_servo_enable_change(self, channel):
ui = self.servo_ui[channel]; is_on = ui['switch_en'].get()
self.servo_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
self.publish(f"servo/control/{channel}/enable", "on" if is_on else "off")
# --- Sensor Handlers ---
def on_sensor_mode_change(self, choice):
self.publish("sensor/control/mode", int(choice))
def on_sensor_publish_all(self):
self.publish("sensor/control/publish_all", "1")
def on_sensor_enable_change(self, channel):
ui = self.sensor_ui[channel]; is_on = ui['switch_en'].get()
self.sensor_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
self.publish(f"sensor/control/enable/{channel}", "on" if is_on else "off")
def on_sensor_cal_start(self, channel, entry_widget):
val = entry_widget.get()
if val.isdigit():
self.publish(f"sensor/control/calibrate/start/{channel}", int(val))
def on_sensor_cal_finish(self, channel, entry_widget):
val = entry_widget.get()
if val.isdigit():
self.publish(f"sensor/control/calibrate/finish/{channel}", int(val))
def on_sensor_clear_cal(self, channel):
self.publish(f"sensor/control/clear_cal/{channel}", "1")
def run(self):
self.root.mainloop()
self.client.loop_stop()
self.client.disconnect()
if __name__ == "__main__":
app = MotorSCADA()
app.run()