[REFACTOR] 移除 MQTT $SYS 監聽機制並優化 CI/CD 流程
1. 移除 MQTT Gateway 對 $SYS 系統主題的訂閱與解析邏輯,改由遺囑 (Last Will) 統一處理上下線狀態。 2. 更新 EMQX ACL 設定,移除 Gateway 的系統主題存取權限。 3. 優化 CI/CD 流程 (deploy-demo.yaml):將 EMQX 重啟改為熱重載 (Reload),並新增 MQTT Worker 自動重啟。
This commit is contained in:
parent
b21ea670bf
commit
4b8436e6c2
@ -38,6 +38,8 @@ jobs:
|
|||||||
- name: Step 1 - Build & Deploy Containers
|
- name: Step 1 - Build & Deploy Containers
|
||||||
run: |
|
run: |
|
||||||
WWWGROUP=1000 WWWUSER=1000 docker compose -f compose.yaml up -d --build --wait
|
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
|
- name: Step 2 - Deploy Code
|
||||||
run: |
|
run: |
|
||||||
@ -75,5 +77,10 @@ jobs:
|
|||||||
"
|
"
|
||||||
docker exec star-cloud-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
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
|
run: docker image prune -f
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
{allow, {user, "star-cloud-gateway"}, subscribe, ["$SYS/brokers/+/clients/+/connected", "$SYS/brokers/+/clients/+/disconnected"]}.
|
|
||||||
{allow, {username, "dashboard"}, subscribe, ["$SYS/#"]}.
|
{allow, {username, "dashboard"}, subscribe, ["$SYS/#"]}.
|
||||||
{allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}.
|
{allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}.
|
||||||
{allow, {username, "star-cloud-gateway"}, subscribe, ["$SYS/#"]}.
|
|
||||||
{deny, all, subscribe, ["$SYS/#", {eq, "#"}, {eq, "+/#"}]}.
|
{deny, all, subscribe, ["$SYS/#", {eq, "#"}, {eq, "+/#"}]}.
|
||||||
{allow, all}.
|
{allow, all}.
|
||||||
|
|||||||
@ -23,96 +23,34 @@ func (h *MqttHandler) HandleMessage(client mqtt.Client, msg mqtt.Message) {
|
|||||||
|
|
||||||
log.Printf("Received message on topic: %s", topic)
|
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{}
|
var jsonPayload interface{}
|
||||||
|
err := json.Unmarshal(payload, &jsonPayload)
|
||||||
// 檢查是否為系統主題 ($SYS)
|
if err != nil {
|
||||||
if strings.HasPrefix(topic, "$SYS/") {
|
// 如果不是有效的 JSON,則視為純文字/數值 (Raw Value)
|
||||||
parts := strings.Split(topic, "/")
|
jsonPayload = string(payload)
|
||||||
|
|
||||||
// 尋找事件類型 (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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bridgePayload := bridge.BridgePayload{
|
bridgePayload := bridge.BridgePayload{
|
||||||
Type: msgType,
|
Type: msgType,
|
||||||
SerialNo: cleanSerialNo(serialNo),
|
SerialNo: serialNo,
|
||||||
Payload: jsonPayload,
|
Payload: jsonPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.pusher.Push(bridgePayload)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -63,16 +63,10 @@ func main() {
|
|||||||
ackToken := c.Subscribe("machine/+/command/ack", 1, mqttHandler.HandleMessage)
|
ackToken := c.Subscribe("machine/+/command/ack", 1, mqttHandler.HandleMessage)
|
||||||
ackToken.Wait()
|
ackToken.Wait()
|
||||||
|
|
||||||
// 訂閱 EMQX 5 系統主題 (連線與斷線事件)
|
if token.Error() != nil || ackToken.Error() != nil {
|
||||||
sysToken := c.Subscribe("$SYS/brokers/+/clients/+/connected", 1, mqttHandler.HandleMessage)
|
log.Printf("Failed to subscribe to MQTT topics: %v %v", token.Error(), ackToken.Error())
|
||||||
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())
|
|
||||||
} else {
|
} else {
|
||||||
log.Println("Successfully subscribed to machine/+/+, machine/+/command/ack and $SYS topics")
|
log.Println("Successfully subscribed to machine/+/+ and machine/+/command/ack")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
BIN
star-cloud-demo-day.pptx
Normal file
BIN
star-cloud-demo-day.pptx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user