MQtt gui for pub
This commit is contained in:
parent
fbde79a57b
commit
aa74ffb9d9
|
@ -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()
|
Loading…
Reference in New Issue