fix: update gui interface
This commit is contained in:
61
backend_control/README.md
Normal file
61
backend_control/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Интерфейс управления
|
||||
|
||||

|
||||
|
||||
## Область Управления
|
||||
|
||||
`Целевой 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)`
|
||||
|
||||

|
||||
|
||||
## Область Управление Серво
|
||||
|
||||
Тут можно задать угол сервопривода и отключить сервопривод
|
||||
|
||||
## Облать Статус и телеметрия
|
||||
|
||||
Вывод информации о сервоприводе включен или выключен. Так же выведен текущий угол.
|
||||
@@ -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("<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")
|
||||
|
||||
@@ -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("<Return>", self.on_servo_ang_entry)
|
||||
self.ent_servo_ang.bind("<FocusOut>", 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("<Return>", self.on_servo_pls_entry)
|
||||
self.ent_servo_pls.bind("<FocusOut>", 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()
|
||||
|
||||
BIN
backend_control/img/gui_interface.png
Normal file
BIN
backend_control/img/gui_interface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
backend_control/img/servo_interface.png
Normal file
BIN
backend_control/img/servo_interface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Reference in New Issue
Block a user