diff --git a/backend_control/README.md b/backend_control/README.md new file mode 100644 index 0000000..4ab4ef8 --- /dev/null +++ b/backend_control/README.md @@ -0,0 +1,61 @@ +# Интерфейс управления + +![](img/gui_interface.png) + +## Область Управления + +`Целевой RPM` - Задается уставка скорости в диапозоне от -1000 до 1000 оборотов в минуту + +`Ток` - Уставка тока в диапозоне 0 - 100% + +`StallGuard` - + +`Микрошаг` - Уставка микрошага + +`Сброс счетчика шагов` - Сброс счетчика шагов + +## Область Режимы + +`Аппаратное вкл. (Driver EN)` - Аппаратное Включение/Выключение драйвера tmc2209 + +`Программное вкл. (TMC Chip)` - Программное Включение/Выключение драйвера tmc2209 + +`StealthChop (Тихий)` - Включение/Выключение тихого режима работы шагового двигателя. + +`CoolStep (Энергосбер.)` - Включение/Выключение энергосберегающего режима работы шагового двигателя. + +## Область Телеметрия и Статусы + +`Текущий RPM (Факт)` + +`Всего шагов` + +`Вращение` + +`SG Result` - Для отслеживания нагрузки на вал или момента срыва шагов. Резкое падение значения sg_result при движении обычно означает столкновение или заклинивание механизма. + +`Interstep (ns)` + +`Current Scaling` + +## Область Ошибки и Флаги + +`Перегрев` + +`КЗ на землю` + +`Обрыв нагрузки` + +`StealthChop активен` + +`Остановка (Standstill)` + +![](img/servo_interface.png) + +## Область Управление Серво + +Тут можно задать угол сервопривода и отключить сервопривод + +## Облать Статус и телеметрия + +Вывод информации о сервоприводе включен или выключен. Так же выведен текущий угол. \ No newline at end of file diff --git a/backend_control/gui.py b/backend_control/gui.py index 188dbcc..a5fee27 100644 --- a/backend_control/gui.py +++ b/backend_control/gui.py @@ -1,35 +1,33 @@ 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" # Пароль +MQTT_BROKER = "192.168.31.225" +MQTT_PORT = 1883 +MQTT_USER = "test" +MQTT_PASSWORD = "1234" # Цвета для индикаторов COLOR_OK = "#28a745" COLOR_ERR = "#dc3545" COLOR_OFF = "#555555" COLOR_ACTIVE = "#00d2ff" -COLOR_PENDING = "#ffaa00" # Оранжевый для состояния "ожидание" +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.title("🚀 Motor & Servo Control SCADA") self.root.geometry("1100x800") self.root.minsize(900, 650) - # Флаги состояния ожидания подтверждения + # Флаги ожидания подтверждения self.driver_pending = False self.tmc_pending = False + self.servo_pending = False self.setup_gui() self.setup_mqtt() @@ -40,44 +38,47 @@ class MotorSCADA: 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) + ctk.CTkLabel(header, text="Motor & Servo Control 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) - # --- Основной контейнер (Сетка) --- - grid = ctk.CTkFrame(self.root, fg_color="transparent") - grid.pack(fill="both", expand=True, padx=20, pady=10) + # --- Вкладки --- + 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("Сервопривод") + + self.create_motor_tab(self.tab_motor) + self.create_servo_tab(self.tab_servo) + + # ================= ВКЛАДКА ШАГОВОГО ДВИГАТЕЛЯ ================= + 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) - # 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 (Слайдер + Точный ввод) + # 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") @@ -100,7 +101,7 @@ class MotorSCADA: # 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.set("1") self.opt_msteps.pack(anchor="w", padx=20, pady=5) # Reset @@ -109,16 +110,13 @@ class MotorSCADA: 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_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) - # Программное включение с индикатором обратной связи - 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) # Разделитель + 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) @@ -126,13 +124,10 @@ class MotorSCADA: 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, "Вращение:") @@ -140,18 +135,79 @@ class MotorSCADA: 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_servo_tab(self, parent): + grid = ctk.CTkFrame(parent, fg_color="transparent") + grid.pack(fill="both", expand=True) + grid.grid_columnconfigure((0, 1), weight=1, uniform="col") + grid.grid_rowconfigure(0, weight=1) + + # --- Колонка 1: Управление --- + frame_ctrl = ctk.CTkFrame(grid) + frame_ctrl.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) + ctk.CTkLabel(frame_ctrl, text="⚙️ Управление серво", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20)) + + # Угол + ctk.CTkLabel(frame_ctrl, text="Угол (0-180°):").pack(anchor="w", padx=20) + ang_frame = ctk.CTkFrame(frame_ctrl, fg_color="transparent") + ang_frame.pack(fill="x", padx=20, pady=5) + self.sld_servo_ang = ctk.CTkSlider(ang_frame, from_=0, to=180, command=self.on_servo_ang_slider) + self.sld_servo_ang.pack(side="left", fill="x", expand=True, padx=(0, 10)) + self.ent_servo_ang = ctk.CTkEntry(ang_frame, width=70, justify="right") + self.ent_servo_ang.insert(0, "90") + self.ent_servo_ang.pack(side="left", padx=(0, 10)) + self.ent_servo_ang.bind("", self.on_servo_ang_entry) + self.ent_servo_ang.bind("", self.on_servo_ang_entry) + self.lbl_servo_ang_val = ctk.CTkLabel(ang_frame, text="90", width=40) + self.lbl_servo_ang_val.pack(side="right") + + # Импульс + ctk.CTkLabel(frame_ctrl, text="Импульс (500-2500 мкс):").pack(anchor="w", padx=20, pady=(15,0)) + pls_frame = ctk.CTkFrame(frame_ctrl, fg_color="transparent") + pls_frame.pack(fill="x", padx=20, pady=5) + self.sld_servo_pls = ctk.CTkSlider(pls_frame, from_=500, to=2500, command=self.on_servo_pls_slider) + self.sld_servo_pls.pack(side="left", fill="x", expand=True, padx=(0, 10)) + self.ent_servo_pls = ctk.CTkEntry(pls_frame, width=70, justify="right") + self.ent_servo_pls.insert(0, "1500") + self.ent_servo_pls.pack(side="left", padx=(0, 10)) + self.ent_servo_pls.bind("", self.on_servo_pls_entry) + self.ent_servo_pls.bind("", self.on_servo_pls_entry) + self.lbl_servo_pls_val = ctk.CTkLabel(pls_frame, text="1500", width=40) + self.lbl_servo_pls_val.pack(side="right") + + # Detach + ctk.CTkButton(frame_ctrl, text="Отсоединить сигнал (Detach)", fg_color="#dc3545", hover_color="#b02a37", command=self.on_servo_detach).pack(pady=30) + + # --- Колонка 2: Статус и Телеметрия --- + frame_stat = ctk.CTkFrame(grid) + frame_stat.grid(row=0, column=1, sticky="nsew", padx=(10, 0)) + ctk.CTkLabel(frame_stat, text="🔌 Статус и Телеметрия", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20)) + + # ИСПРАВЛЕНО: распаковка кортежа + self.sw_servo_enable, self.led_servo_fb = self.create_switch_with_feedback(frame_stat, "Включить сервопривод", self.on_servo_enable_change) + + ctk.CTkFrame(frame_stat, height=2, fg_color="#4a4a6a").pack(fill="x", padx=20, pady=20) + + # Телеметрия + tel_frame = ctk.CTkFrame(frame_stat) + tel_frame.pack(fill="x", padx=10, pady=5) + self.lbl_fb_servo_ang = self.create_telemetry_row(tel_frame, "Текущий угол (Факт):") + + ctk.CTkLabel(frame_stat, text="🚨 Статус", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(15, 5)) + stat_frame = ctk.CTkFrame(frame_stat) + stat_frame.pack(fill="x", padx=10, pady=5) + self.led_servo_status = self.create_led_row(stat_frame, "Серво активно:") + + # ================= ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ GUI ================= def create_switch(self, parent, text, command): frame = ctk.CTkFrame(parent, fg_color="transparent") frame.pack(fill="x", padx=20, pady=10) @@ -161,19 +217,13 @@ class MotorSCADA: 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): @@ -196,11 +246,9 @@ class MotorSCADA: 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() @@ -212,6 +260,7 @@ class MotorSCADA: if reason_code == 0: self.root.after(0, self.update_status, True) client.subscribe("motor/feedback/#") + client.subscribe("servo/feedback/#") else: print(f"Ошибка подключения MQTT. Код: {reason_code}") self.root.after(0, self.update_status, False) @@ -225,107 +274,86 @@ class MotorSCADA: 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) + # --- ШАГОВЫЙ ДВИГАТЕЛЬ --- + 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 + self.lbl_fb_run.configure(text="Вращается" if is_running else "Остановлен", text_color=COLOR_OK if is_running 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/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, 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) - # Статусы 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) - - # Общие статусы системы (Синхронизация переключателей с обратной связью) + # ИСПРАВЛЕНО: обращение к self.sw_driver напрямую, а не через [0] 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) + if self.sw_driver.get() != is_on: + if is_on: self.sw_driver.select() + else: self.sw_driver.deselect() + self.led_driver_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF) + # ИСПРАВЛЕНО: обращение к self.sw_tmc_enable напрямую 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) + if self.sw_tmc_enable.get() != is_on: + if is_on: self.sw_tmc_enable.select() + else: self.sw_tmc_enable.deselect() + self.led_tmc_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF) - # ================= Обработчики событий GUI ================= + # --- СЕРВОПРИВОД --- + elif topic == "servo/feedback/angle": + self.lbl_fb_servo_ang.configure(text=val) + + # ИСПРАВЛЕНО: обращение к self.sw_servo_enable напрямую + elif topic == "servo/feedback/status": + is_on = val == "on" + self.servo_pending = False + if self.sw_servo_enable.get() != is_on: + if is_on: self.sw_servo_enable.select() + else: self.sw_servo_enable.deselect() + self.led_servo_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF) + self.update_led(self.led_servo_status, val, is_error=False) + + # ================= ОБРАБОТЧИКИ СОБЫТИЙ ================= 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) + 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) + 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.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)) + 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()))) + 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)) @@ -333,39 +361,63 @@ class MotorSCADA: 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)) + 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_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_reset_steps(self): - self.publish("motor/control/totalsteps/reset", "1") - - # --- Обработчики включения с обратной связью --- + # ИСПРАВЛЕНО: обращение к self.sw_driver напрямую 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) - # Отправляем команду + 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") + # ИСПРАВЛЕНО: обращение к self.sw_tmc_enable напрямую 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) - # Отправляем команду + 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_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 on_cool_change(self): - self.publish("motor/control/tmc/coolstep", "on" if self.sw_cool.get() else "off") + # --- Сервопривод --- + def on_servo_ang_slider(self, value): + int_val = int(value) + self.ent_servo_ang.delete(0, ctk.END); self.ent_servo_ang.insert(0, str(int_val)) + self.lbl_servo_ang_val.configure(text=str(int_val)) + self.publish("servo/control/angle", int_val) + + def on_servo_ang_entry(self, event=None): + try: + int_val = max(0, min(180, int(self.ent_servo_ang.get()))) + self.sld_servo_ang.set(int_val); self.lbl_servo_ang_val.configure(text=str(int_val)) + self.publish("servo/control/angle", int_val) + except ValueError: + self.ent_servo_ang.delete(0, ctk.END); self.ent_servo_ang.insert(0, str(int(self.sld_servo_ang.get()))) + + def on_servo_pls_slider(self, value): + int_val = int(value) + self.ent_servo_pls.delete(0, ctk.END); self.ent_servo_pls.insert(0, str(int_val)) + self.lbl_servo_pls_val.configure(text=str(int_val)) + self.publish("servo/control/pulse", int_val) + + def on_servo_pls_entry(self, event=None): + try: + int_val = max(500, min(2500, int(self.ent_servo_pls.get()))) + self.sld_servo_pls.set(int_val); self.lbl_servo_pls_val.configure(text=str(int_val)) + self.publish("servo/control/pulse", int_val) + except ValueError: + self.ent_servo_pls.delete(0, ctk.END); self.ent_servo_pls.insert(0, str(int(self.sld_servo_pls.get()))) + + def on_servo_detach(self): + self.publish("servo/control/detach", "1") + + # ИСПРАВЛЕНО: обращение к self.sw_servo_enable напрямую + def on_servo_enable_change(self): + is_on = self.sw_servo_enable.get() + self.servo_pending = True; self.led_servo_fb.configure(text_color=COLOR_PENDING) + self.publish("servo/control/enable", "on" if is_on else "off") def run(self): self.root.mainloop() diff --git a/backend_control/img/gui_interface.png b/backend_control/img/gui_interface.png new file mode 100644 index 0000000..edff0dc Binary files /dev/null and b/backend_control/img/gui_interface.png differ diff --git a/backend_control/img/servo_interface.png b/backend_control/img/servo_interface.png new file mode 100644 index 0000000..38a4737 Binary files /dev/null and b/backend_control/img/servo_interface.png differ