Files
ozone-tech_owl_prime/backend_control/gui.py

377 lines
18 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
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()