import customtkinter as ctk import paho.mqtt.client as mqtt import threading import time # ================= НАСТРОЙКИ MQTT ================= MQTT_BROKER = "192.168.31.225" # Адрес MQTT брокера MQTT_PORT = 1883 # Порт MQTT MQTT_USER = "test" # Имя пользователя MQTT_PASSWORD = "1234" # Пароль # Цвета для индикаторов COLOR_OK = "#28a745" COLOR_ERR = "#dc3545" COLOR_OFF = "#555555" COLOR_ACTIVE = "#00d2ff" COLOR_PENDING = "#ffaa00" # Оранжевый для состояния "ожидание" class MotorSCADA: def __init__(self): # Инициализация GUI ctk.set_appearance_mode("Dark") ctk.set_default_color_theme("blue") self.root = ctk.CTk() self.root.title("🚀 Motor Control SCADA (TMC2209)") self.root.geometry("1100x800") self.root.minsize(900, 650) # Флаги состояния ожидания подтверждения self.driver_pending = False self.tmc_pending = False 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 Control SCADA (TMC2209)", 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) # --- Основной контейнер (Сетка) --- grid = ctk.CTkFrame(self.root, fg_color="transparent") grid.pack(fill="both", expand=True, padx=20, pady=10) grid.grid_columnconfigure((0, 1, 2), weight=1, uniform="col") grid.grid_rowconfigure(0, weight=1) # 1. Блок Управления self.create_control_frame(grid) # 2. Блок Режимов self.create_modes_frame(grid) # 3. Блок Телеметрии и Статусов 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)) # RPM (Слайдер + Точный ввод) 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("", self.on_rpm_entry_apply) self.ent_rpm.bind("", self.on_rpm_entry_apply) self.lbl_rpm_val = ctk.CTkLabel(rpm_frame, text="0", width=50) self.lbl_rpm_val.pack(side="right") # Current 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)) # StallGuard 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) # Microsteps 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) # Reset 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.create_switch_with_feedback(frame, "Аппаратное вкл. (Driver EN)", self.on_driver_change) # Программное включение с индикатором обратной связи self.sw_tmc_enable = 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 (ns):") self.lbl_fb_cscale = self.create_telemetry_row(tel_frame, "Current Scaling:") # Статусы (LEDs) 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, "Остановка (Standstill):") # --- Вспомогательные методы GUI --- def create_switch(self, parent, text, command): frame = ctk.CTkFrame(parent, fg_color="transparent") frame.pack(fill="x", padx=20, pady=10) 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=20, pady=10) ctk.CTkLabel(frame, text=text).pack(side="left") # Индикатор обратной связи (справа от переключателя) feedback_led = ctk.CTkLabel(frame, text="●", font=ctk.CTkFont(size=20), 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=4) ctk.CTkLabel(frame, text=text, anchor="w").pack(side="left") val_lbl = ctk.CTkLabel(frame, text="0", font=ctk.CTkFont(weight="bold"), 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=4) ctk.CTkLabel(frame, text=text, anchor="w").pack(side="left") led_lbl = ctk.CTkLabel(frame, text="●", font=ctk.CTkFont(size=20), 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"Ошибка подключения к MQTT: {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/#") else: print(f"Ошибка подключения MQTT. Код: {reason_code}") 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): topic = msg.topic val = msg.payload.decode('utf-8') self.root.after(0, self.process_feedback, topic, val) def process_feedback(self, topic, val): # Базовая телеметрия 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_running = val == "true" self.lbl_fb_run.configure(text="Вращается" if is_running else "Остановлен", text_color=COLOR_OK if is_running else COLOR_ERR) # Телеметрия TMC 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) # Статусы TMC (LEDs) elif topic == "motor/feedback/tmc/status/over_temp": self.update_led(self.led_over_temp, val, is_error=True) elif topic == "motor/feedback/tmc/status/short_to_ground": self.update_led(self.led_short_gnd, val, is_error=True) elif topic == "motor/feedback/tmc/status/open_load": self.update_led(self.led_open_load, val, is_error=True) elif topic == "motor/feedback/tmc/status/stealth_chop_active": self.update_led(self.led_stealth_act, val, is_error=False) elif topic == "motor/feedback/tmc/status/standstill": self.update_led(self.led_standstill, val, is_error=False) # Общие статусы системы (Синхронизация переключателей с обратной связью) elif topic == "motor/feedback/driver/status": is_on = val == "on" # Снимаем флаг ожидания self.driver_pending = False # Синхронизируем переключатель if self.sw_driver[0].get() != is_on: if is_on: self.sw_driver[0].select() else: self.sw_driver[0].deselect() # Обновляем индикатор обратной связи (зеленый = подтверждено) self.sw_driver[1].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[0].get() != is_on: if is_on: self.sw_tmc_enable[0].select() else: self.sw_tmc_enable[0].deselect() # Обновляем индикатор обратной связи (зеленый = подтверждено) self.sw_tmc_enable[1].configure(text_color=COLOR_OK if is_on else COLOR_OFF) # ================= Обработчики событий GUI ================= def publish(self, topic, payload): if self.client.is_connected(): self.client.publish(topic, str(payload), qos=1) def update_status(self, is_online): if is_online: self.lbl_status.configure(text="● Подключено", text_color=COLOR_OK) else: self.lbl_status.configure(text="● Отключено", text_color=COLOR_ERR) def update_led(self, label, val, is_error): is_true = val in ["true", "1", "on"] if is_true: label.configure(text_color=COLOR_ERR if is_error else COLOR_OK) else: label.configure(text_color=COLOR_OFF) # --- RPM --- 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): val_str = self.ent_rpm.get() try: int_val = int(val_str) int_val = max(-1000, min(1000, int_val)) 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): """Аппаратное управление пином EN с индикацией ожидания""" is_on = self.sw_driver[0].get() # Устанавливаем флаг ожидания и меняем цвет на оранжевый self.driver_pending = True self.sw_driver[1].configure(text_color=COLOR_PENDING) # Отправляем команду self.publish("motor/control/driver", "on" if is_on else "off") def on_tmc_enable_change(self): """Программное включение чипа TMC2209 с индикацией ожидания""" is_on = self.sw_tmc_enable[0].get() # Устанавливаем флаг ожидания и меняем цвет на оранжевый self.tmc_pending = True self.sw_tmc_enable[1].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") def run(self): self.root.mainloop() self.client.loop_stop() self.client.disconnect() if __name__ == "__main__": app = MotorSCADA() app.run()