Create gui interface for controll step-drive

This commit is contained in:
2026-07-07 17:23:07 +07:00
parent 15645f7d25
commit ff49b03be2
3 changed files with 380 additions and 1 deletions

View File

@@ -3,4 +3,4 @@
python -m venv testing
source testing/bin/activate
pip install paho-mqtt
pip install -r requirements.txt

377
backend_control/gui.py Normal file
View File

@@ -0,0 +1,377 @@
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("<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")
# 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()

View File

@@ -0,0 +1,2 @@
paho-mqtt
customtkinter