diff --git a/MQTT_project/mqtt_pub_gui_qt.py b/MQTT_project/mqtt_pub_gui_qt.py new file mode 100644 index 0000000..c71ecf9 --- /dev/null +++ b/MQTT_project/mqtt_pub_gui_qt.py @@ -0,0 +1,336 @@ +# mqtt_pub_gui_qt.py +# pip install PyQt6 paho-mqtt +import time +import json +from dataclasses import dataclass +from typing import Optional + +from PyQt6 import QtCore, QtGui, QtWidgets +from paho.mqtt import client as mqtt + + +# ----------------- 原函式(稍作參數擴充,保留你的流程與註解) ----------------- +def pub_to_plc( + BROKER_HOST: str, + BROKER_PORT: int, + TOPIC: str, + QOS: int, + RETAIN: bool, + move_forward: bool = True, + forward_velocity: float = 1.2, + move_updown: bool = True, + updown_velocity: float = 55.1, + gap_seconds: float = 3.0, + log_cb=None +): + """ + 依序發佈兩筆訊息,中間等待 gap_seconds 秒。 + 加上 log_cb(str) 讓 GUI 可收到狀態訊息。 + """ + def _log(msg: str): + if callable(log_cb): + log_cb(msg) + + client = mqtt.Client(client_id="py_pub_plc_test", protocol=mqtt.MQTTv311) + + # 若 Broker 有帳密、TLS 在這裡設定(目前不需要) + # client.username_pw_set("user", "pass") + # client.tls_set(ca_certs="ca.pem") # 若是 TLS + + _log(f"🔌 Connecting to {BROKER_HOST}:{BROKER_PORT} ...") + client.connect(BROKER_HOST, BROKER_PORT, keepalive=60) + client.loop_start() + time.sleep(0.3) # 等連線建立 + _log("✅ Connected") + + # ---- 第一組: ---- + payload_forward = { + "Move_Forward": bool(move_forward), # BOOL + "Move_Forward_Velocity": float(forward_velocity) # REAL (float) + } + msg1 = json.dumps(payload_forward, ensure_ascii=False) + _log(f"📤 Publish #1 to {TOPIC} (QoS={QOS}, retain={RETAIN}): {msg1}") + r1 = client.publish(TOPIC, msg1, qos=QOS, retain=RETAIN) + r1.wait_for_publish() + _log("✅ Published #1") + + # 中間延遲 + if gap_seconds and gap_seconds > 0: + _log(f"⏳ Waiting {gap_seconds} s before #2 ...") + time.sleep(gap_seconds) + + # ---- 第二組: ---- + payload_updown = { + "Move_UPDown": bool(move_updown), # BOOL + "Move_UPDown_Velocity": float(updown_velocity) # REAL (float) + } + msg2 = json.dumps(payload_updown, ensure_ascii=False) + _log(f"📤 Publish #2 to {TOPIC} (QoS={QOS}, retain={RETAIN}): {msg2}") + r2 = client.publish(TOPIC, msg2, qos=QOS, retain=RETAIN) + r2.wait_for_publish() + _log("✅ Published #2") + + time.sleep(0.3) + client.loop_stop() + client.disconnect() + _log("🔌 Disconnected") + + +# ----------------- GUI 部分 ----------------- +@dataclass +class PublishParams: + host: str + port: int + topic: str + qos: int + retain: bool + move_forward: bool + forward_velocity: float + move_updown: bool + updown_velocity: float + gap_seconds: float + + +class PubWorker(QtCore.QThread): + sig_log = QtCore.pyqtSignal(str) + sig_done = QtCore.pyqtSignal(bool, str) # success, message + + def __init__(self, params: PublishParams, parent=None): + super().__init__(parent) + self.params = params + + def _log(self, text: str): + self.sig_log.emit(text) + + def run(self): + try: + pub_to_plc( + self.params.host, + self.params.port, + self.params.topic, + self.params.qos, + self.params.retain, + self.params.move_forward, + self.params.forward_velocity, + self.params.move_updown, + self.params.updown_velocity, + self.params.gap_seconds, + log_cb=self._log + ) + self.sig_done.emit(True, "Publish sequence completed.") + except Exception as e: + self.sig_done.emit(False, f"Error: {e}") + + +class MainWindow(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("MQTT PLC Publisher (PyQt6)") + self.resize(920, 620) + + # 預設值 + self.default_host = "169.254.11.130" + self.default_port = 1886 + self.default_topic = "topic_plc_and_py_for_AXIS" + self.default_qos = 1 + self.default_retain = False + + self.default_move_forward = True + self.default_forward_vel = 1.2 + self.default_move_updown = True + self.default_updown_vel = 55.1 + self.default_gap_seconds = 3.0 + + self.worker: Optional[PubWorker] = None + + central = QtWidgets.QWidget() + self.setCentralWidget(central) + vbox = QtWidgets.QVBoxLayout(central) + vbox.setContentsMargins(12, 12, 12, 12) + vbox.setSpacing(10) + + # ---- Connection Panel ---- + conn_group = QtWidgets.QGroupBox("Connection") + grid = QtWidgets.QGridLayout(conn_group) + grid.setHorizontalSpacing(12) + grid.setVerticalSpacing(8) + + self.ed_host = QtWidgets.QLineEdit(self.default_host) + self.ed_port = QtWidgets.QSpinBox() + self.ed_port.setRange(1, 65535) + self.ed_port.setValue(self.default_port) + + self.ed_topic = QtWidgets.QLineEdit(self.default_topic) + + self.cb_qos = QtWidgets.QComboBox() + self.cb_qos.addItems(["0", "1", "2"]) + self.cb_qos.setCurrentIndex(self.default_qos) + + self.chk_retain = QtWidgets.QCheckBox("Retain") + self.chk_retain.setChecked(self.default_retain) + + grid.addWidget(QtWidgets.QLabel("Broker Host"), 0, 0) + grid.addWidget(self.ed_host, 0, 1) + grid.addWidget(QtWidgets.QLabel("Port"), 0, 2) + grid.addWidget(self.ed_port, 0, 3) + + grid.addWidget(QtWidgets.QLabel("Topic"), 1, 0) + grid.addWidget(self.ed_topic, 1, 1, 1, 3) + + grid.addWidget(QtWidgets.QLabel("QoS"), 2, 0) + grid.addWidget(self.cb_qos, 2, 1) + grid.addWidget(self.chk_retain, 2, 2) + + vbox.addWidget(conn_group) + + # ---- Payload Panel ---- + payload_group = QtWidgets.QGroupBox("Payloads") + pgrid = QtWidgets.QGridLayout(payload_group) + pgrid.setHorizontalSpacing(12) + pgrid.setVerticalSpacing(8) + + # 第一筆 + self.chk_forward = QtWidgets.QCheckBox("Move_Forward") + self.chk_forward.setChecked(self.default_move_forward) + self.ed_forward_vel = QtWidgets.QDoubleSpinBox() + self.ed_forward_vel.setRange(-1e6, 1e6) + self.ed_forward_vel.setDecimals(3) + self.ed_forward_vel.setValue(self.default_forward_vel) + self.ed_forward_vel.setSuffix(" (Velocity)") + + # 第二筆 + self.chk_updown = QtWidgets.QCheckBox("Move_UPDown") + self.chk_updown.setChecked(self.default_move_updown) + self.ed_updown_vel = QtWidgets.QDoubleSpinBox() + self.ed_updown_vel.setRange(-1e6, 1e6) + self.ed_updown_vel.setDecimals(3) + self.ed_updown_vel.setValue(self.default_updown_vel) + self.ed_updown_vel.setSuffix(" (Velocity)") + + # 延遲 + self.ed_gap = QtWidgets.QDoubleSpinBox() + self.ed_gap.setRange(0.0, 600.0) + self.ed_gap.setDecimals(2) + self.ed_gap.setValue(self.default_gap_seconds) + self.ed_gap.setSuffix(" s (delay between #1 and #2)") + + pgrid.addWidget(QtWidgets.QLabel("Payload #1:"), 0, 0) + pgrid.addWidget(self.chk_forward, 0, 1) + pgrid.addWidget(self.ed_forward_vel, 0, 2) + + pgrid.addWidget(QtWidgets.QLabel("Payload #2:"), 1, 0) + pgrid.addWidget(self.chk_updown, 1, 1) + pgrid.addWidget(self.ed_updown_vel, 1, 2) + + pgrid.addWidget(QtWidgets.QLabel("Gap:"), 2, 0) + pgrid.addWidget(self.ed_gap, 2, 1, 1, 2) + + vbox.addWidget(payload_group) + + # ---- Actions ---- + hbox = QtWidgets.QHBoxLayout() + self.btn_publish = QtWidgets.QPushButton("🚀 Publish Sequence") + self.btn_publish.clicked.connect(self.on_publish) + hbox.addWidget(self.btn_publish) + hbox.addStretch(1) + vbox.addLayout(hbox) + + # ---- Logs ---- + log_group = QtWidgets.QGroupBox("Logs") + lvbox = QtWidgets.QVBoxLayout(log_group) + self.txt_log = QtWidgets.QPlainTextEdit() + self.txt_log.setReadOnly(True) + self.txt_log.setMaximumBlockCount(2000) + lvbox.addWidget(self.txt_log) + vbox.addWidget(log_group, 1) + + # 美化 + self._apply_dark_fusion_palette() + + def on_publish(self): + if self.worker and self.worker.isRunning(): + self._log("⚠️ 正在發佈中,請稍候...") + return + + try: + params = PublishParams( + host=self.ed_host.text().strip(), + port=int(self.ed_port.value()), + topic=self.ed_topic.text().strip(), + qos=int(self.cb_qos.currentText()), + retain=bool(self.chk_retain.isChecked()), + move_forward=bool(self.chk_forward.isChecked()), + forward_velocity=float(self.ed_forward_vel.value()), + move_updown=bool(self.chk_updown.isChecked()), + updown_velocity=float(self.ed_updown_vel.value()), + gap_seconds=float(self.ed_gap.value()), + ) + except Exception as e: + self._log(f"❌ 參數錯誤:{e}") + return + + if not params.host or not params.topic: + QtWidgets.QMessageBox.warning(self, "Warning", "Host 與 Topic 不可為空") + return + + self.worker = PubWorker(params, self) + self.worker.sig_log.connect(self._log) + self.worker.sig_done.connect(self._on_done) + self._log("▶️ 開始發佈序列...") + self.btn_publish.setEnabled(False) + self.worker.start() + + @QtCore.pyqtSlot(bool, str) + def _on_done(self, success: bool, message: str): + self._log(("✅ " if success else "❌ ") + message) + self.btn_publish.setEnabled(True) + + def _log(self, text: str): + self.txt_log.appendPlainText(text) + sb = self.txt_log.verticalScrollBar() + sb.setValue(sb.maximum()) + + def _apply_dark_fusion_palette(self): + app = QtWidgets.QApplication.instance() + app.setStyle("Fusion") + pal = QtGui.QPalette() + + base = QtGui.QColor(30, 30, 30) + alt = QtGui.QColor(45, 45, 45) + text = QtGui.QColor(220, 220, 220) + btn = QtGui.QColor(53, 53, 53) + hl = QtGui.QColor(42, 130, 218) + + pal.setColor(QtGui.QPalette.ColorRole.Window, base) + pal.setColor(QtGui.QPalette.ColorRole.WindowText, text) + pal.setColor(QtGui.QPalette.ColorRole.Base, QtGui.QColor(25, 25, 25)) + pal.setColor(QtGui.QPalette.ColorRole.AlternateBase, alt) + pal.setColor(QtGui.QPalette.ColorRole.ToolTipBase, text) + pal.setColor(QtGui.QPalette.ColorRole.ToolTipText, text) + pal.setColor(QtGui.QPalette.ColorRole.Text, text) + pal.setColor(QtGui.QPalette.ColorRole.Button, btn) + pal.setColor(QtGui.QPalette.ColorRole.ButtonText, text) + pal.setColor(QtGui.QPalette.ColorRole.BrightText, QtGui.QColor(255, 0, 0)) + pal.setColor(QtGui.QPalette.ColorRole.Link, hl) + pal.setColor(QtGui.QPalette.ColorRole.Highlight, hl) + pal.setColor(QtGui.QPalette.ColorRole.HighlightedText, QtGui.QColor(255, 255, 255)) + + app.setPalette(pal) + + +def main(): + import sys + app = QtWidgets.QApplication(sys.argv) + # 高 DPI(Windows 可忽略失敗) + try: + import ctypes + ctypes.windll.shcore.SetProcessDpiAwareness(1) + except Exception: + pass + w = MainWindow() + w.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main()