117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
# pip install paho-mqtt
|
||
import json
|
||
import re
|
||
import time
|
||
from paho.mqtt import client as mqtt
|
||
|
||
# ========= 使用者設定 =========
|
||
BROKER_HOST = "169.254.11.130"
|
||
BROKER_PORT = 1886
|
||
TOPIC = "topic_plc_and_py_for_AXIS"
|
||
QOS = 1 # 建議測試用 1
|
||
# ============================
|
||
|
||
# 全域狀態(方便觀察最新值)
|
||
state = {
|
||
"BUTTON_Y": None,
|
||
"Forward_RPM": None,
|
||
"UPDown_RPM": None,
|
||
}
|
||
|
||
def _robust_json_parse(raw: str):
|
||
"""
|
||
將 PLC 端常見的非標準 JSON 嘗試修正後解析為 dict。
|
||
例:'{Forward_RPM:1500,UPDown_RPM:800}' -> {'Forward_RPM':1500,'UPDown_RPM':800}
|
||
也會把 TRUE/FALSE -> true/false
|
||
"""
|
||
s = raw.strip()
|
||
|
||
# 去掉最外層引號(若有)
|
||
if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
|
||
s = s[1:-1].strip()
|
||
|
||
# PLC 常見布林大寫 -> JSON 規範小寫
|
||
s = s.replace("TRUE", "true").replace("FALSE", "false")
|
||
|
||
# 自動補 key 的雙引號:{key: -> {"key":
|
||
s = re.sub(r'([{,]\s*)([A-Za-z_]\w*)\s*:', r'\1"\2":', s)
|
||
|
||
# 轉單引號為雙引號(若 payload 用了單引號包字串)
|
||
# 放在補 key 後,避免 key 的引號再被替換
|
||
s = s.replace("'", '"')
|
||
|
||
return json.loads(s)
|
||
|
||
def on_connect(client: mqtt.Client, userdata, flags, reason_code, properties=None):
|
||
"""
|
||
連上 Broker 後自動訂閱 Topic。
|
||
paho-mqtt 2.x 的 on_connect 第四個參數是 reason_code(非 rc),名稱略有不同。
|
||
"""
|
||
if reason_code == 0:
|
||
print(f"✅ 已連線到 MQTT Broker {BROKER_HOST}:{BROKER_PORT}")
|
||
client.subscribe(TOPIC, qos=QOS)
|
||
print(f"📡 已訂閱 Topic: {TOPIC}(QoS={QOS})")
|
||
else:
|
||
print(f"❌ 連線失敗,reason_code={reason_code}")
|
||
|
||
def on_message(client: mqtt.Client, userdata, msg: mqtt.MQTTMessage):
|
||
payload_raw = msg.payload.decode("utf-8", errors="replace")
|
||
print(f"\n📥 收到原始訊息:{payload_raw}")
|
||
|
||
# 先試標準 JSON,失敗再用容錯解析
|
||
try:
|
||
data = json.loads(payload_raw)
|
||
except json.JSONDecodeError:
|
||
try:
|
||
data = _robust_json_parse(payload_raw)
|
||
except Exception as e:
|
||
print(f"❌ 無法解析為 JSON:{payload_raw} ;錯誤:{e}")
|
||
return
|
||
|
||
# 只更新我們關心的欄位(有傳才改)
|
||
for key in ("BUTTON_Y", "Forward_RPM", "UPDown_RPM"):
|
||
if key in data:
|
||
state[key] = data[key]
|
||
|
||
print(
|
||
f"✅ 解析後資料:{data}\n"
|
||
f"📊 目前狀態:BUTTON_Y={state['BUTTON_Y']}, "
|
||
f"Forward_RPM={state['Forward_RPM']}, UPDown_RPM={state['UPDown_RPM']}"
|
||
)
|
||
|
||
def on_disconnect(client: mqtt.Client, userdata, reason_code, properties=None):
|
||
print(f"🔌 已斷線,reason_code={reason_code}")
|
||
|
||
def sub_to_plc(broker_host: str, broker_port: int, topic: str):
|
||
client = mqtt.Client(client_id="py_sub_plc_test", protocol=mqtt.MQTTv311)
|
||
|
||
# 如需帳密或 TLS,在這裡設定
|
||
# client.username_pw_set("user", "pass")
|
||
# client.tls_set(ca_certs="ca.pem")
|
||
|
||
# 自動重連延遲(paho-mqtt 2.x)
|
||
client.reconnect_delay_set(min_delay=1, max_delay=10)
|
||
|
||
client.on_connect = on_connect
|
||
client.on_message = on_message
|
||
client.on_disconnect = on_disconnect
|
||
|
||
# 先連線,再進入 loop
|
||
client.connect(broker_host, broker_port, keepalive=60)
|
||
|
||
try:
|
||
# loop_forever 會阻塞執行緒,內建自動重連
|
||
client.loop_forever()
|
||
except KeyboardInterrupt:
|
||
print("\n🛑 收到中斷訊號,準備斷線...")
|
||
finally:
|
||
try:
|
||
client.disconnect()
|
||
except Exception:
|
||
pass
|
||
|
||
if __name__ == "__main__":
|
||
print("🚀 啟動 MQTT 訂閱器...")
|
||
print(f"Broker={BROKER_HOST}:{BROKER_PORT}, Topic={TOPIC}, QoS={QOS}")
|
||
sub_to_plc(BROKER_HOST, BROKER_PORT, TOPIC)
|