[DOCS]:初始化 MQTT 架構實作計畫與相關技術規範
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m12s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m12s
1. 新增 MQTT 即時通訊與 Topic 規範文件 (.agents/skills/mqtt-communication-specs/SKILL.md)。 2. 建立 MQTT 基礎架構實作計畫文件 (docs/mqtt-implementation-plan.md)。 3. 更新全域開發框架規範 (framework.md),納入 Go Gateway 與 EMQX 架構說明。 4. 重構 IoT 通訊處理規範 (iot-communication/SKILL.md),支援 HTTP 與 MQTT 雙軌管線。 5. 更新背景 API 規範 (api-rules.md) 與技能觸發規則 (skill-trigger.md) 以符合新架構。
This commit is contained in:
191
docs/mqtt-implementation-plan.md
Normal file
191
docs/mqtt-implementation-plan.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Star Cloud MQTT 基礎架構實作計畫 (Phase 1)
|
||||
|
||||
本計畫旨在建立 Star Cloud 的高併發通訊基石,包含佈署 EMQX Broker、開發 Go MQTT Gateway,並建立與 Laravel 之間的 Redis **雙向**異步橋接機制。
|
||||
|
||||
---
|
||||
|
||||
## User Review Required
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **雙向通訊架構**:本計畫不只處理「機台 ➜ 雲端」的上報,也同時建立「雲端 ➜ 機台」的指令下發通道。後台管理員在按下「遠端出貨」按鈕時,Laravel 會將指令推入 Redis,由 Go Gateway 轉發至 EMQX,再即時送達機台 APP。
|
||||
|
||||
> [!WARNING]
|
||||
> **資源配額**:Go Gateway 雖然輕量,但在 Docker 環境中仍建議設定 `mem_limit` 避免極端情況下的資源爭搶。
|
||||
|
||||
---
|
||||
|
||||
## 系統架構總覽
|
||||
|
||||
```
|
||||
機台 Android APP
|
||||
│
|
||||
├─ [上行] Publish ──→ EMQX ──→ Go Gateway ──→ Redis List (mqtt_incoming_jobs) ──→ Laravel mqtt:listen ──→ Job ──→ MySQL
|
||||
│
|
||||
└─ [下行] Subscribe ←── EMQX ←── Go Gateway ←── Redis List (mqtt_outgoing_commands) ←── Laravel MqttCommandService
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. 基礎設施佈署 (Infrastructure)
|
||||
|
||||
#### [MODIFY] [compose.yaml](file:///home/mama/projects/star-cloud/compose.yaml)
|
||||
- **新增 `emqx` 服務**:
|
||||
- Image: `emqx/emqx:5.10.3`(開源版最終穩定版,Apache 2.0 授權)
|
||||
- Ports: `1883:1883` (MQTT), `8083:8083` (WebSocket), `18083:18083` (Dashboard)
|
||||
- 加入 `sail` 網路
|
||||
- 配置 Redis Auth 插件環境變數,指向 `star-cloud-redis`
|
||||
- **新增 `mqtt-gateway` 服務**:
|
||||
- 使用 `mqtt-gateway/Dockerfile` 進行 Multi-stage build
|
||||
- 連接至 `sail` 網路
|
||||
- 依賴 `emqx` 與 `redis`
|
||||
- 環境變數從 `.env` 讀取
|
||||
|
||||
#### [MODIFY] [.env](file:///home/mama/projects/star-cloud/.env)
|
||||
- 新增以下環境變數:
|
||||
```env
|
||||
# MQTT / EMQX
|
||||
MQTT_BROKER_HOST=emqx
|
||||
MQTT_BROKER_PORT=1883
|
||||
EMQX_DASHBOARD_PORT=18083
|
||||
|
||||
# Go Gateway
|
||||
MQTT_GATEWAY_CLIENT_ID=star-cloud-gateway
|
||||
MQTT_REDIS_HOST=star-cloud-redis
|
||||
MQTT_REDIS_PORT=6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Go MQTT Gateway 開發 (The Bridge)
|
||||
|
||||
#### [NEW] 完整目錄結構
|
||||
```
|
||||
mqtt-gateway/
|
||||
├── Dockerfile ← Multi-stage build (builder + alpine)
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── main.go ← 進入點:初始化、訊號監聽、graceful shutdown
|
||||
├── config/
|
||||
│ └── config.go ← 讀取環境變數 (EMQX_ADDR, REDIS_ADDR 等)
|
||||
├── internal/
|
||||
│ ├── handler/
|
||||
│ │ ├── heartbeat_handler.go ← 處理 machine/+/heartbeat
|
||||
│ │ ├── error_handler.go ← 處理 machine/+/error
|
||||
│ │ └── transaction_handler.go ← 處理 machine/+/transaction
|
||||
│ └── bridge/
|
||||
│ ├── redis_consumer.go ← [下行] BLPOP mqtt_outgoing_commands,轉發至 EMQX
|
||||
│ └── redis_pusher.go ← [上行] RPUSH mqtt_incoming_jobs
|
||||
```
|
||||
|
||||
#### [上行邏輯] 機台 ➜ 雲端
|
||||
1. 訂閱 `machine/+/heartbeat`, `machine/+/error`, `machine/+/transaction`。
|
||||
2. 從 Topic 路徑提取 `serial_no`。
|
||||
3. 包裝成 `BridgePayload`:
|
||||
```json
|
||||
{
|
||||
"type": "heartbeat",
|
||||
"serial_no": "M-001",
|
||||
"payload": { "current_page": 1, "temperature": 25.5 },
|
||||
"received_at": "2026-04-14T09:00:00+08:00"
|
||||
}
|
||||
```
|
||||
4. 執行 `RPUSH mqtt_incoming_jobs {json}`。
|
||||
|
||||
#### [下行邏輯] 雲端 ➜ 機台 (新增)
|
||||
1. Go Gateway 啟動一條 Goroutine,持續 `BLPOP mqtt_outgoing_commands`。
|
||||
2. 取得 JSON 後解析目標 `serial_no` 與 `command` 內容。
|
||||
3. Publish 至 `machine/{serial_no}/command` (QoS 1)。
|
||||
```json
|
||||
{
|
||||
"target": "M-001",
|
||||
"command": "dispense",
|
||||
"payload": { "slot_no": 5, "transaction_id": "T202604140001" },
|
||||
"message_id": "MSG_123456789"
|
||||
}
|
||||
```
|
||||
|
||||
#### [NEW] mqtt-gateway/Dockerfile
|
||||
```dockerfile
|
||||
# Stage 1: Build
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o gateway .
|
||||
|
||||
# Stage 2: Run
|
||||
FROM alpine:3.20
|
||||
COPY --from=builder /app/gateway /usr/local/bin/gateway
|
||||
CMD ["gateway"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Laravel 端實作
|
||||
|
||||
#### [NEW] app/Console/Commands/ListenMqttQueue.php
|
||||
- Artisan Command: `mqtt:listen`
|
||||
- 使用 `BLPOP mqtt_incoming_jobs` 阻塞式監聽
|
||||
- 根據 `type` 分派至對應的 Laravel Job:
|
||||
- `heartbeat` ➜ `ProcessHeartbeatJob`
|
||||
- `error` ➜ `ProcessMachineErrorJob`
|
||||
- `transaction` ➜ `ProcessTransactionJob`
|
||||
|
||||
#### [NEW] app/Services/Machine/MqttCommandService.php
|
||||
- 提供 `sendCommand(string $serialNo, string $command, array $payload)` 方法
|
||||
- 將指令 JSON 推入 Redis List `mqtt_outgoing_commands`
|
||||
- 供 Controller 呼叫(例如後台管理員按下「遠端出貨」按鈕)
|
||||
|
||||
#### [NEW] app/Jobs/Machine/ (三個 Job)
|
||||
- `ProcessHeartbeatJob.php`: 更新 `last_heard_at`、溫度、頁面碼
|
||||
- `ProcessMachineErrorJob.php`: 寫入 `machine_logs`,觸發告警通知
|
||||
- `ProcessTransactionJob.php`: 更新庫存、建立交易紀錄
|
||||
|
||||
#### [MODIFY] [MachineAuthController.php](file:///home/mama/projects/star-cloud/app/Http/Controllers/Api/V1/App/MachineAuthController.php)
|
||||
- 在 B014 核發 `api_token` 後,同步寫入 Redis:
|
||||
`Redis::set("machine_auth:{$serial_no}", hash('sha256', $token));`
|
||||
- Token 更新/撤銷時,同步刪除 Redis 中的對應 Key
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decisions (Confirmed)
|
||||
|
||||
### ✅ EMQX 版本策略
|
||||
- **統一鎖定**:開發與正式環境皆使用 `emqx/emqx:5.10.3`(開源版最後一個穩定版本)。
|
||||
- **選擇理由**:EMQX 從 5.9.0 起合併為統一版本,6.x 改用 BSL 商業授權。`5.10.3` 是純開源 (Apache 2.0) 的最終穩定版,功能完整且免授權費用。
|
||||
|
||||
### ✅ 下行指令安全性(無需額外 OTP)
|
||||
App 連線 MQTT 時已使用 `api_token` 作為密碼完成身份認證,因此 **MQTT 連線本身即為認證通道**。安全性由以下三層保障:
|
||||
1. **連線層**:App 使用 `serial_no` + `api_token` 連線 EMQX,未通過驗證的裝置無法訂閱任何 Topic。
|
||||
2. **ACL 層**:EMQX 存取控制確保只有 Go Gateway 能 Publish 到 `machine/{id}/command`,機台 App 無法偽造指令。
|
||||
3. **冪等性層**:每條下行指令的 Payload 包含唯一的 `message_id`,App 端應記錄已執行的 ID,防止重複執行同一條指令。
|
||||
|
||||
> [!NOTE]
|
||||
> **結論**:上行用 Token 當密碼、下行用 ACL 當門禁、`message_id` 當防重複鎖。三層防護已足夠,不需要額外的 OTP 機制。
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
### 1. 基礎設施驗證
|
||||
- 執行 `./vendor/bin/sail up -d`
|
||||
- 造訪 `http://localhost:18083` 確認 EMQX Dashboard 正常啟動
|
||||
- 確認 Go Gateway Container 的 logs 顯示 "Connected to EMQX" 與 "Connected to Redis"
|
||||
|
||||
### 2. 上行通訊測試 (機台 ➜ 雲端)
|
||||
- 使用 MQTTX 工具連接 `localhost:1883`
|
||||
- 發送心跳 JSON 至 `machine/TEST-001/heartbeat`
|
||||
- 檢查 Laravel 日誌確認 `mqtt:listen` 成功接收並分派 Job
|
||||
- 檢查 MySQL `machines` 表的 `last_heard_at` 是否更新
|
||||
|
||||
### 3. 下行通訊測試 (雲端 ➜ 機台)
|
||||
- 在 MQTTX 訂閱 `machine/TEST-001/command`
|
||||
- 透過 Laravel Tinker 呼叫 `MqttCommandService::sendCommand('TEST-001', 'reboot', [])`
|
||||
- 確認 MQTTX 收到 reboot 指令的 JSON
|
||||
|
||||
### 4. 壓力測試
|
||||
- 使用 Go Script 模擬 500 台機台同時發送心跳
|
||||
- 監控 Redis List 長度與 Laravel Worker 的處理速率
|
||||
Reference in New Issue
Block a user