From 4b8436e6c2a43eb8998aa726a0e3a1f36f0adc6e Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 22 Apr 2026 13:33:42 +0800 Subject: [PATCH] =?UTF-8?q?[REFACTOR]=20=E7=A7=BB=E9=99=A4=20MQTT=20$SYS?= =?UTF-8?q?=20=E7=9B=A3=E8=81=BD=E6=A9=9F=E5=88=B6=E4=B8=A6=E5=84=AA?= =?UTF-8?q?=E5=8C=96=20CI/CD=20=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 移除 MQTT Gateway 對 $SYS 系統主題的訂閱與解析邏輯,改由遺囑 (Last Will) 統一處理上下線狀態。 2. 更新 EMQX ACL 設定,移除 Gateway 的系統主題存取權限。 3. 優化 CI/CD 流程 (deploy-demo.yaml):將 EMQX 重啟改為熱重載 (Reload),並新增 MQTT Worker 自動重啟。 --- .gitea/workflows/deploy-demo.yaml | 9 +- docker/emqx/acl.conf | 2 - mqtt-gateway/internal/handler/mqtt_handler.go | 104 ++++-------------- mqtt-gateway/main.go | 12 +- star-cloud-demo-day.pptx | Bin 0 -> 122097 bytes 5 files changed, 32 insertions(+), 95 deletions(-) create mode 100644 star-cloud-demo-day.pptx diff --git a/.gitea/workflows/deploy-demo.yaml b/.gitea/workflows/deploy-demo.yaml index 7f57b12..48f80cc 100644 --- a/.gitea/workflows/deploy-demo.yaml +++ b/.gitea/workflows/deploy-demo.yaml @@ -38,6 +38,8 @@ jobs: - name: Step 1 - Build & Deploy Containers run: | WWWGROUP=1000 WWWUSER=1000 docker compose -f compose.yaml up -d --build --wait + # 使用熱重載套用 ACL 變更,不中斷現有連線 + docker exec star-cloud-emqx emqx_ctl authorization reload - name: Step 2 - Deploy Code run: | @@ -75,5 +77,10 @@ jobs: " docker exec star-cloud-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache - - name: Step 4 - Cleanup + - name: Step 4 - Restart Long-running Workers + run: | + # 重啟 mqtt:listen 常駐行程以載入最新 PHP 程式碼 + docker restart star-cloud-mqtt-worker + + - name: Step 5 - Cleanup run: docker image prune -f diff --git a/docker/emqx/acl.conf b/docker/emqx/acl.conf index 128fc31..18f6e3a 100644 --- a/docker/emqx/acl.conf +++ b/docker/emqx/acl.conf @@ -1,6 +1,4 @@ -{allow, {user, "star-cloud-gateway"}, subscribe, ["$SYS/brokers/+/clients/+/connected", "$SYS/brokers/+/clients/+/disconnected"]}. {allow, {username, "dashboard"}, subscribe, ["$SYS/#"]}. {allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}. -{allow, {username, "star-cloud-gateway"}, subscribe, ["$SYS/#"]}. {deny, all, subscribe, ["$SYS/#", {eq, "#"}, {eq, "+/#"}]}. {allow, all}. diff --git a/mqtt-gateway/internal/handler/mqtt_handler.go b/mqtt-gateway/internal/handler/mqtt_handler.go index cef03c4..792d92f 100644 --- a/mqtt-gateway/internal/handler/mqtt_handler.go +++ b/mqtt-gateway/internal/handler/mqtt_handler.go @@ -23,96 +23,34 @@ func (h *MqttHandler) HandleMessage(client mqtt.Client, msg mqtt.Message) { log.Printf("Received message on topic: %s", topic) - var serialNo, msgType string + // 業務主題格式: + // 3 層: machine/{serialNo}/{type} (如 heartbeat, error, transaction, status) + // 4 層: machine/{serialNo}/{type}/{subType} (如 command/ack) + parts := strings.Split(topic, "/") + if len(parts) < 3 { + return + } + + serialNo := parts[1] + msgType := parts[2] + + // 4 層 Topic:將 type/subType 組合為 type_subType (如 command/ack → command_ack) + if len(parts) >= 4 { + msgType = parts[2] + "_" + parts[3] + } + var jsonPayload interface{} - - // 檢查是否為系統主題 ($SYS) - if strings.HasPrefix(topic, "$SYS/") { - parts := strings.Split(topic, "/") - - // 尋找事件類型 (connected 或 disconnected) - var event string - for _, p := range parts { - if p == "connected" || p == "disconnected" { - event = p - break - } - } - - // 尋找 Client ID (通常位於 "clients" 的下一層) - for i, p := range parts { - if p == "clients" && i+1 < len(parts) { - serialNo = parts[i+1] - break - } - } - - if event != "" && serialNo != "" { - // 解析系統 JSON Payload (補強:有些版本會把 ClientID 放在 Payload 裡) - var sysData map[string]interface{} - if err := json.Unmarshal(payload, &sysData); err == nil { - if cid, ok := sysData["clientid"].(string); ok { - serialNo = cid - } - } - - msgType = "status" - statusValue := "online" - if event == "disconnected" { - statusValue = "offline" - } - jsonPayload = map[string]string{"status": statusValue} - - log.Printf("Detected system event: Client [%s] is now [%s]", serialNo, statusValue) - } else { - return - } - } else { - // 一般業務主題 - // 3 層: machine/{serialNo}/{type} (如 heartbeat, error, transaction) - // 4 層: machine/{serialNo}/{type}/{subType} (如 command/ack) - parts := strings.Split(topic, "/") - if len(parts) < 3 { - return - } - serialNo = parts[1] - msgType = parts[2] - - // 4 層 Topic:將 type/subType 組合為 type_subType (如 command/ack → command_ack) - if len(parts) >= 4 { - msgType = parts[2] + "_" + parts[3] - } - - err := json.Unmarshal(payload, &jsonPayload) - if err != nil { - // 如果不是有效的 JSON,則視為純文字/數值 (Raw Value) - jsonPayload = string(payload) - } + err := json.Unmarshal(payload, &jsonPayload) + if err != nil { + // 如果不是有效的 JSON,則視為純文字/數值 (Raw Value) + jsonPayload = string(payload) } bridgePayload := bridge.BridgePayload{ Type: msgType, - SerialNo: cleanSerialNo(serialNo), + SerialNo: serialNo, Payload: jsonPayload, } h.pusher.Push(bridgePayload) } - -// cleanSerialNo 處理 Client ID 格式,將 SC_M0408_pndk 轉為 M0408 -func cleanSerialNo(clientID string) string { - if clientID == "" { - return "" - } - - // 1. 移除已知前綴 SC_ - cleaned := strings.TrimPrefix(clientID, "SC_") - - // 2. 如果中間還有底線,通常代表隨機碼,只取前面部分 - // 例如 M0408_test -> M0408 - if idx := strings.Index(cleaned, "_"); idx != -1 { - return cleaned[:idx] - } - - return cleaned -} diff --git a/mqtt-gateway/main.go b/mqtt-gateway/main.go index 6e7d2f8..d6d4d35 100644 --- a/mqtt-gateway/main.go +++ b/mqtt-gateway/main.go @@ -63,16 +63,10 @@ func main() { ackToken := c.Subscribe("machine/+/command/ack", 1, mqttHandler.HandleMessage) ackToken.Wait() - // 訂閱 EMQX 5 系統主題 (連線與斷線事件) - sysToken := c.Subscribe("$SYS/brokers/+/clients/+/connected", 1, mqttHandler.HandleMessage) - sysToken.Wait() - sysToken2 := c.Subscribe("$SYS/brokers/+/clients/+/disconnected", 1, mqttHandler.HandleMessage) - sysToken2.Wait() - - if token.Error() != nil || ackToken.Error() != nil || sysToken.Error() != nil || sysToken2.Error() != nil { - log.Printf("Failed to subscribe to MQTT topics: %v %v %v %v", token.Error(), ackToken.Error(), sysToken.Error(), sysToken2.Error()) + if token.Error() != nil || ackToken.Error() != nil { + log.Printf("Failed to subscribe to MQTT topics: %v %v", token.Error(), ackToken.Error()) } else { - log.Println("Successfully subscribed to machine/+/+, machine/+/command/ack and $SYS topics") + log.Println("Successfully subscribed to machine/+/+ and machine/+/command/ack") } }) diff --git a/star-cloud-demo-day.pptx b/star-cloud-demo-day.pptx new file mode 100644 index 0000000000000000000000000000000000000000..e701233aae72565841201e2765052c1b2fe6edf4 GIT binary patch literal 122097 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 su#;)i-Z^%&qt)C8zU$B&!%(JCozQ