Update: actual control program

This commit is contained in:
2026-07-15 23:45:56 +03:00
parent ff51eccf22
commit 31ca8bacd4

View File

@@ -1,18 +1,21 @@
import customtkinter as ctk
import paho.mqtt.client as mqtt
# ================= НАСТРОЙКИ MQTT =================
MQTT_BROKER = "192.168.31.225"
# ================= НАСТРОЙКИ =================
MQTT_BROKER = "192.168.0.200"
MQTT_PORT = 1883
MQTT_USER = "test"
MQTT_PASSWORD = "1234"
# Количество каналов сервоприводов (UI сгенерируется автоматически)
MAX_SERVOS = 4
# Цвета для индикаторов
COLOR_OK = "#28a745"
COLOR_ERR = "#dc3545"
COLOR_OFF = "#555555"
COLOR_ACTIVE = "#00d2ff"
COLOR_PENDING = "#ffaa00" # Оранжевый для ожидания
COLOR_PENDING = "#ffaa00"
class MotorSCADA:
def __init__(self):
@@ -21,13 +24,13 @@ class MotorSCADA:
self.root = ctk.CTk()
self.root.title("🚀 Motor & Servo Control SCADA")
self.root.geometry("1100x800")
self.root.minsize(900, 650)
self.root.geometry("1100x850")
self.root.minsize(1000, 750)
# Флаги ожидания подтверждения
self.driver_pending = False
self.tmc_pending = False
self.servo_pending = False
self.servo_pending = {i: False for i in range(MAX_SERVOS)}
self.setup_gui()
self.setup_mqtt()
@@ -47,7 +50,7 @@ class MotorSCADA:
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.tab_servo = self.tabview.add(f"Сервоприводы (0-{MAX_SERVOS-1})")
self.create_motor_tab(self.tab_motor)
self.create_servo_tab(self.tab_servo)
@@ -101,7 +104,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("1")
self.opt_msteps.set("16")
self.opt_msteps.pack(anchor="w", padx=20, pady=5)
# Reset
@@ -112,7 +115,6 @@ class MotorSCADA:
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.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)
@@ -144,68 +146,56 @@ class MotorSCADA:
self.led_stealth_act = self.create_led_row(stat_frame, "StealthChop активен:")
self.led_standstill = self.create_led_row(stat_frame, "Остановка (Standstill):")
# ================= ВКЛАДКА СЕРВОПРИВОДА =================
# ================= ВКЛАДКА СЕРВОПРИВОДОВ =================
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, "Текущий угол (Факт):")
grid.pack(fill="both", expand=True, padx=10, pady=10)
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, "Серво активно:")
# Настраиваем сетку 2x2 для карточек сервоприводов
grid.grid_columnconfigure((0, 1), weight=1, uniform="col")
grid.grid_rowconfigure((0, 1), weight=1, uniform="row")
self.servo_ui = {}
for i in range(MAX_SERVOS):
row = i // 2
col = i % 2
frame = ctk.CTkFrame(grid)
frame.grid(row=row, column=col, sticky="nsew", padx=10, pady=10)
self.servo_ui[i] = self.create_servo_card(frame, i)
def create_servo_card(self, parent, channel):
"""Создает карточку управления для одного канала сервопривода"""
ui_elements = {}
ctk.CTkLabel(parent, text=f"🦾 Сервопривод #{channel}", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(10, 10))
# Угол
ctk.CTkLabel(parent, text="Угол (0-180°):").pack(anchor="w", padx=20)
ang_frame = ctk.CTkFrame(parent, fg_color="transparent")
ang_frame.pack(fill="x", padx=20, pady=5)
sld = ctk.CTkSlider(ang_frame, from_=0, to=180, command=lambda val, ch=channel: self.on_servo_ang_slider(ch, val))
sld.pack(side="left", fill="x", expand=True, padx=(0, 10))
sld.set(90)
ent = ctk.CTkEntry(ang_frame, width=60, justify="right")
ent.insert(0, "90")
ent.pack(side="left", padx=(0, 10))
ent.bind("<Return>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
ent.bind("<FocusOut>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
lbl_val = ctk.CTkLabel(ang_frame, text="90", width=40)
lbl_val.pack(side="right")
ui_elements['slider_ang'] = sld
ui_elements['entry_ang'] = ent
ui_elements['lbl_ang_val'] = lbl_val
# Включение с обратной связью
sw, led_fb = self.create_switch_with_feedback(parent, "Включить серво", lambda ch=channel: self.on_servo_enable_change(ch))
ui_elements['switch_en'] = sw
ui_elements['led_fb'] = led_fb
return ui_elements
# ================= ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ GUI =================
def create_switch(self, parent, text, command):
@@ -259,8 +249,9 @@ class MotorSCADA:
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/#")
client.subscribe("servo/feedback/#")
client.subscribe("servo/+/feedback/#") # Динамический канал для серво
else:
print(f"Ошибка подключения MQTT. Код: {reason_code}")
self.root.after(0, self.update_status, False)
@@ -274,56 +265,62 @@ 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)
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)
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/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)
parts = topic.split('/')
# ИСПРАВЛЕНО: обращение к self.sw_driver напрямую, а не через [0]
elif topic == "motor/feedback/driver/status":
is_on = val == "on"
self.driver_pending = False
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.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)
# --- ШАГОВЫЙ ДВИГАТЕЛЬ ---
if parts[0] == 'motor' and parts[1] == 'feedback':
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)
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/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.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)
elif topic == "motor/feedback/tmc/status":
is_on = val == "on"
self.tmc_pending = False
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)
# --- СЕРВОПРИВОД ---
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)
# --- СЕРВОПРИВОДЫ ---
elif parts[0] == 'servo' and parts[2] == 'feedback':
try:
channel = int(parts[1])
if channel in self.servo_ui:
param = parts[3]
ui = self.servo_ui[channel]
if param == 'angle':
ui['lbl_fb_ang'].configure(text=val)
elif param == 'status':
is_on = val == "on"
self.servo_pending[channel] = False
if ui['switch_en'].get() != is_on:
if is_on: ui['switch_en'].select()
else: ui['switch_en'].deselect()
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
self.update_led(ui['led_status'], val, is_error=False)
except (IndexError, ValueError):
pass # Игнорируем некорректные или лишние топики
# ================= ОБРАБОТЧИКИ СОБЫТИЙ =================
def publish(self, topic, payload):
@@ -366,13 +363,11 @@ class MotorSCADA:
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")
# ИСПРАВЛЕНО: обращение к self.sw_driver напрямую
def on_driver_change(self):
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):
is_on = self.sw_tmc_enable.get()
self.tmc_pending = True; self.led_tmc_fb.configure(text_color=COLOR_PENDING)
@@ -381,43 +376,28 @@ class MotorSCADA:
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_servo_ang_slider(self, value):
# --- Сервоприводы ---
def on_servo_ang_slider(self, channel, 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)
ui = self.servo_ui[channel]
ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int_val))
ui['lbl_ang_val'].configure(text=str(int_val))
self.publish(f"servo/control/{channel}/angle", int_val)
def on_servo_ang_entry(self, event=None):
def on_servo_ang_entry(self, channel, event=None):
ui = self.servo_ui[channel]
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)
int_val = max(0, min(180, int(ui['entry_ang'].get())))
ui['slider_ang'].set(int_val); ui['lbl_ang_val'].configure(text=str(int_val))
self.publish(f"servo/control/{channel}/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())))
ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int(ui['slider_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 on_servo_enable_change(self, channel):
ui = self.servo_ui[channel]
is_on = ui['switch_en'].get()
self.servo_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
self.publish(f"servo/control/{channel}/enable", "on" if is_on else "off")
def run(self):
self.root.mainloop()