WebSocket 通訊協議

以下是一份基於程式碼實現整理的 WebSocket 通訊協議文件,概述裝置端與伺服器之間如何透過 WebSocket 進行互動。

該文件僅基於所提供的程式碼推斷,實際部署時可能需要結合伺服器端實現進行進一步確認或補充。


1. 總體流程概覽

  1. 裝置端初始化

    • 裝置上電、初始化 Application:
      • 初始化音訊編解碼器、顯示屏、LED 等
      • 連線網路
      • 建立並初始化實現 Protocol 介面的 WebSocket 協議例項(WebsocketProtocol)
    • 進入主迴圈等待事件(音訊輸入、音訊輸出、排程任務等)。
  2. 建立 WebSocket 連線

    • 當裝置需要開始語音會話時(例如使用者喚醒、手動按鍵觸發等),呼叫 OpenAudioChannel():
      • 根據配置獲取 WebSocket URL
      • 設定若干請求頭(Authorization, Protocol-Version, Device-Id, Client-Id)
      • 呼叫 Connect() 與伺服器建立 WebSocket 連線
  3. 裝置端傳送 “hello” 訊息

    • 連線成功後,裝置會傳送一條 JSON 訊息,示例結構如下:
    {
      "type": "hello",
      "version": 1,
      "features": {
        "mcp": true,
        "glyph_push": true
      },
      "text_font": {
        "bundle": "noto-v1",
        "charset": "common",
        "size": 20,
        "bpp": 4
      },
      "transport": "websocket",
      "audio_params": {
        "format": "opus",
        "sample_rate": 16000,
        "channels": 1,
        "frame_duration": 60
      }
    }
    • 其中 features 欄位為可選,內容根據裝置編譯配置自動生成。例如:"mcp": true 表示支援 MCP 協議。
    • "glyph_push": true 和 text_font 宣告可選的動態文字 glyph 擴充,詳見動態文字 Glyph Push 擴充。
    • frame_duration 的值對應 OPUS_FRAME_DURATION_MS(例如 60ms)。
  4. 伺服器回覆 “hello”

    • 裝置等待伺服器返回一條包含 "type": "hello" 的 JSON 訊息,並檢查 "transport": "websocket" 是否匹配。
    • 伺服器可選下發 session_id 欄位,裝置端收到後會自動記錄。
    • 示例:
    {
      "type": "hello",
      "transport": "websocket",
      "session_id": "xxx",
      "audio_params": {
        "format": "opus",
        "sample_rate": 24000,
        "channels": 1,
        "frame_duration": 60
      }
    }
    • 如果匹配,則認為伺服器已就緒,標記音訊通道開啟成功。
    • 如果在超時時間(預設 10 秒)內未收到正確回覆,認為連線失敗並觸發網路錯誤回撥。
  5. 後續訊息互動

    • 裝置端和伺服器端之間可傳送兩種主要型別的資料:

      1. 二進位制音訊資料(Opus 編碼)
      2. 文字 JSON 訊息(用於傳輸聊天狀態、TTS/STT 事件、MCP 協議訊息等)
    • 在程式碼裡,接收回撥主要分為:

      • OnData(...):
        • 當 binary 為 true 時,認為是音訊幀;裝置會將其當作 Opus 資料進行解碼。
        • 當 binary 為 false 時,認為是 JSON 文字,需要在裝置端用 cJSON 進行解析並做相應業務邏輯處理(如聊天、TTS、MCP 協議訊息等)。
    • 當伺服器或網路出現斷連,回撥 OnDisconnected() 被觸發:

      • 裝置會呼叫 on_audio_channel_closed_(),並最終回到空閒狀態。
  6. 關閉 WebSocket 連線

    • 裝置在需要結束語音會話時,會呼叫 CloseAudioChannel() 主動斷開連線,並回到空閒狀態。
    • 或者如果伺服器端主動斷開,也會引發同樣的回撥流程。

2. 通用請求頭

在建立 WebSocket 連線時,程式碼示例中設定了以下請求頭:

  • Authorization: 用於存放訪問令牌,形如 "Bearer <token>"
  • Protocol-Version: 協議版本號,與 hello 訊息體內的 version 欄位保持一致
  • Device-Id: 裝置物理網絡卡 MAC 地址
  • Client-Id: 軟體生成的 UUID(擦除 NVS 或重新燒錄完整韌體會重置)

這些頭會隨著 WebSocket 握手一起傳送到伺服器,伺服器可根據需求進行校驗、認證等。


3. 二進位制協議版本

裝置支援多種二進位制協議版本,透過配置中的 version 欄位指定:

3.1 版本1(預設)

直接傳送 Opus 音訊資料,無額外後設資料。Websocket 協議會區分 text 與 binary。

3.2 版本2

使用 BinaryProtocol2 結構:

struct BinaryProtocol2 {
    uint16_t version;        // 協議版本
    uint16_t type;           // 訊息型別 (0: OPUS, 1: JSON)
    uint32_t reserved;       // 保留欄位
    uint32_t timestamp;      // 時間戳(毫秒,用於伺服器端AEC)
    uint32_t payload_size;   // 負載大小(位元組)
    uint8_t payload[];       // 負載資料
} __attribute__((packed));

3.3 版本3

使用 BinaryProtocol3 結構:

struct BinaryProtocol3 {
    uint8_t type;            // 訊息型別
    uint8_t reserved;        // 保留欄位
    uint16_t payload_size;   // 負載大小
    uint8_t payload[];       // 負載資料
} __attribute__((packed));

4. JSON 訊息結構

WebSocket 文字幀以 JSON 方式傳輸,以下為常見的 "type" 欄位及其對應業務邏輯。若訊息裡包含未列出的欄位,可能為可選或特定實現細節。

4.1 裝置端→伺服器

  1. Hello

    • 連線成功後,由裝置端傳送,告知伺服器基本參數。
    • 例:
      {
        "type": "hello",
        "version": 1,
        "features": {
          "mcp": true
        },
        "transport": "websocket",
        "audio_params": {
          "format": "opus",
          "sample_rate": 16000,
          "channels": 1,
          "frame_duration": 60
        }
      }
  2. Listen

    • 表示裝置端開始或停止錄音監聽。
    • 常見欄位:
      • "session_id":會話標識
      • "type": "listen"
      • "state":"start", "stop", "detect"(喚醒檢測已觸發)
      • "mode":"auto", "manual" 或 "realtime",表示識別模式。
    • 例:開始監聽
      {
        "session_id": "xxx",
        "type": "listen",
        "state": "start",
        "mode": "manual"
      }
  3. Abort

    • 終止當前說話(TTS 播放)或語音通道。
    • 例:
      {
        "session_id": "xxx",
        "type": "abort",
        "reason": "wake_word_detected"
      }
    • reason 值可為 "wake_word_detected" 或其他。
  4. Wake Word Detected

    • 用於裝置端向伺服器告知檢測到喚醒詞。
    • 在傳送該訊息之前,可提前傳送喚醒詞的 Opus 音訊資料,用於伺服器進行聲紋檢測。
    • 例:
      {
        "session_id": "xxx",
        "type": "listen",
        "state": "detect",
        "text": "你好小明"
      }
  5. MCP

    • 推薦用於物聯網控制的新一代協議。所有裝置能力發現、工具呼叫等均透過 type: “mcp” 的訊息進行,payload 內部為標準 JSON-RPC 2.0(詳見 MCP 協議文件)。

    • 裝置端到伺服器傳送 result 的例子:

      {
        "session_id": "xxx",
        "type": "mcp",
        "payload": {
          "jsonrpc": "2.0",
          "id": 1,
          "result": {
            "content": [
              { "type": "text", "text": "true" }
            ],
            "isError": false
          }
        }
      }

4.2 伺服器→裝置端

  1. Hello

    • 伺服器端返回的握手確認訊息。
    • 必須包含 "type": "hello" 和 "transport": "websocket"。
    • 可能會帶有 audio_params,表示伺服器期望的音訊參數,或與裝置端對齊的配置。
    • 伺服器可選下發 session_id 欄位,裝置端收到後會自動記錄。
    • 成功接收後裝置端會設定事件標誌,表示 WebSocket 通道就緒。
  2. STT

    • {"session_id": "xxx", "type": "stt", "text": "..."}
    • 表示伺服器端識別到了使用者語音。(例如語音轉文字結果)
    • 裝置可能將此文字顯示到螢幕上,後續再進入回答等流程。
  3. LLM

    • {"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "😀"}
    • 伺服器指示裝置調整表情動畫 / UI 表達。
  4. TTS

    • {"session_id": "xxx", "type": "tts", "state": "start"}:伺服器準備下發 TTS 音訊,裝置端進入 “speaking” 播放狀態。
    • {"session_id": "xxx", "type": "tts", "state": "stop"}:表示本次 TTS 結束。
    • {"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}
      • 讓裝置在介面上顯示當前要播放或朗讀的文字片段(例如用於顯示給使用者)。
  5. MCP

    • 伺服器透過 type: “mcp” 的訊息下發物聯網相關的控制指令或返回撥用結果,payload 結構同上。

    • 伺服器到裝置端傳送 tools/call 的例子:

      {
        "session_id": "xxx",
        "type": "mcp",
        "payload": {
          "jsonrpc": "2.0",
          "method": "tools/call",
          "params": {
            "name": "self.light.set_rgb",
            "arguments": { "r": 255, "g": 0, "b": 0 }
          },
          "id": 1
        }
      }
  6. System

    • 系統控制命令,常用於遠端升級更新。
    • 例:
      {
        "session_id": "xxx",
        "type": "system",
        "command": "reboot"
      }
    • 支援的命令:
      • "reboot":重啟裝置
  7. Custom(可選)

    • 自定義訊息,當 CONFIG_RECEIVE_CUSTOM_MESSAGE 啟用時支援。
    • 例:
      {
        "session_id": "xxx",
        "type": "custom",
        "payload": {
          "message": "自定義內容"
        }
      }
  8. 音訊資料:二進位制幀

    • 當伺服器傳送音訊二進位制幀(Opus 編碼)時,裝置端解碼並播放。
    • 若裝置端正在處於 “listening” (錄音)狀態,收到的音訊幀會被忽略或清空以防衝突。

5. 音訊編解碼

  1. 裝置端傳送錄音資料

    • 音訊輸入經過可能的回聲消除、降噪或音量增益後,透過 Opus 編碼打包為二進位制幀傳送給伺服器。
    • 根據協議版本,可能直接傳送 Opus 資料(版本1)或使用帶後設資料的二進位制協議(版本2/3)。
  2. 裝置端播放收到的音訊

    • 收到伺服器的二進位制幀時,同樣認定是 Opus 資料。
    • 裝置端會進行解碼,然後交由音訊輸出介面播放。
    • 如果伺服器的音訊取樣率與裝置不一致,會在解碼後再進行重取樣。

6. 常見狀態流轉

以下為常見裝置端關鍵狀態流轉,與 WebSocket 訊息對應:

  1. Idle → Connecting

    • 使用者觸發或喚醒後,裝置呼叫 OpenAudioChannel() → 建立 WebSocket 連線 → 傳送 "type":"hello"。
  2. Connecting → Listening

    • 成功建立連線後,若繼續執行 SendStartListening(...),則進入錄音狀態。此時裝置會持續編碼麥克風資料併傳送到伺服器。
  3. Listening → Speaking

    • 收到伺服器 TTS Start 訊息 ({"type":"tts","state":"start"}) → 停止錄音並播放接收到的音訊。
  4. Speaking → Idle

    • 伺服器 TTS Stop ({"type":"tts","state":"stop"}) → 音訊播放結束。若未繼續進入自動監聽,則返回 Idle;如果配置了自動迴圈,則再度進入 Listening。
  5. Listening / Speaking → Idle(遇到異常或主動中斷)

    • 呼叫 SendAbortSpeaking(...) 或 CloseAudioChannel() → 中斷會話 → 關閉 WebSocket → 狀態回到 Idle。

自動模式狀態流轉圖

  stateDiagram
  direction TB
  [*] --> kDeviceStateUnknown
  kDeviceStateUnknown --> kDeviceStateStarting:初始化
  kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
  kDeviceStateStarting --> kDeviceStateActivating:啟用裝置
  kDeviceStateActivating --> kDeviceStateUpgrading:檢測到新版本
  kDeviceStateActivating --> kDeviceStateIdle:啟用完成
  kDeviceStateIdle --> kDeviceStateConnecting:開始連線
  kDeviceStateConnecting --> kDeviceStateIdle:連線失敗
  kDeviceStateConnecting --> kDeviceStateListening:連線成功
  kDeviceStateListening --> kDeviceStateSpeaking:開始說話
  kDeviceStateSpeaking --> kDeviceStateListening:結束說話
  kDeviceStateListening --> kDeviceStateIdle:手動終止
  kDeviceStateSpeaking --> kDeviceStateIdle:自動終止

手動模式狀態流轉圖

  stateDiagram
  direction TB
  [*] --> kDeviceStateUnknown
  kDeviceStateUnknown --> kDeviceStateStarting:初始化
  kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
  kDeviceStateStarting --> kDeviceStateActivating:啟用裝置
  kDeviceStateActivating --> kDeviceStateUpgrading:檢測到新版本
  kDeviceStateActivating --> kDeviceStateIdle:啟用完成
  kDeviceStateIdle --> kDeviceStateConnecting:開始連線
  kDeviceStateConnecting --> kDeviceStateIdle:連線失敗
  kDeviceStateConnecting --> kDeviceStateListening:連線成功
  kDeviceStateIdle --> kDeviceStateListening:開始監聽
  kDeviceStateListening --> kDeviceStateIdle:停止監聽
  kDeviceStateIdle --> kDeviceStateSpeaking:開始說話
  kDeviceStateSpeaking --> kDeviceStateIdle:結束說話

7. 錯誤處理

  1. 連線失敗

    • 如果 Connect(url) 返回失敗或在等待伺服器 “hello” 訊息時超時,觸發 on_network_error_() 回撥。裝置會提示"無法連線到服務"或類似錯誤資訊。
  2. 伺服器斷開

    • 如果 WebSocket 異常斷開,回撥 OnDisconnected():
      • 裝置回撥 on_audio_channel_closed_()
      • 切換到 Idle 或其他重試邏輯。

8. 其它注意事項

  1. 鑑權

    • 裝置透過設定 Authorization: Bearer <token> 提供鑑權,伺服器端需驗證是否有效。
    • 如果令牌過期或無效,伺服器可拒絕握手或在後續斷開。
  2. 會話控制

    • 程式碼中部分訊息包含 session_id,用於區分獨立的對話或操作。服務端可根據需要對不同會話做分離處理。
  3. 音訊負載

    • 程式碼裡預設使用 Opus 格式,並設定 sample_rate = 16000,單聲道。幀時長由 OPUS_FRAME_DURATION_MS 控制,一般為 60ms。可根據頻寬或效能做適當調整。為了獲得更好的音樂播放效果,伺服器下行音訊可能使用 24000 取樣率。
  4. 協議版本配置

    • 透過設定中的 version 欄位配置二進位制協議版本(1、2 或 3)
    • 版本1:直接傳送 Opus 資料
    • 版本2:使用帶時間戳的二進位制協議,適用於伺服器端 AEC
    • 版本3:使用簡化的二進位制協議
  5. 物聯網控制推薦 MCP 協議

    • 裝置與伺服器之間的物聯網能力發現、狀態同步、控制指令等,建議全部透過 MCP 協議(type: “mcp”)實現。原有的 type: “iot” 方案已廢棄。
    • MCP 協議可在 WebSocket、MQTT 等多種底層協議上傳輸,具備更好的擴充性和標準化能力。
    • 詳細用法請參考 MCP 協議文件 及 MCP 物聯網控制用法。
  6. 錯誤或異常 JSON

    • 當 JSON 中缺少必要欄位,例如 {"type": ...},裝置端會記錄錯誤日誌(ESP_LOGE(TAG, "Missing message type, data: %s", data);),不會執行任何業務。

9. 訊息示例

下面給出一個典型的雙向訊息示例(流程簡化示意):

  1. 裝置端 → 伺服器(握手)

    {
      "type": "hello",
      "version": 1,
      "features": {
        "mcp": true
      },
      "transport": "websocket",
      "audio_params": {
        "format": "opus",
        "sample_rate": 16000,
        "channels": 1,
        "frame_duration": 60
      }
    }
  2. 伺服器 → 裝置端(握手應答)

    {
      "type": "hello",
      "transport": "websocket",
      "session_id": "xxx",
      "audio_params": {
        "format": "opus",
        "sample_rate": 16000
      }
    }
  3. 裝置端 → 伺服器(開始監聽)

    {
      "session_id": "xxx",
      "type": "listen",
      "state": "start",
      "mode": "auto"
    }

    同時裝置端開始傳送二進位制幀(Opus 資料)。

  4. 伺服器 → 裝置端(ASR 結果)

    {
      "session_id": "xxx",
      "type": "stt",
      "text": "使用者說的話"
    }
  5. 伺服器 → 裝置端(TTS開始)

    {
      "session_id": "xxx",
      "type": "tts",
      "state": "start"
    }

    接著伺服器傳送二進位制音訊幀給裝置端播放。

  6. 伺服器 → 裝置端(TTS結束)

    {
      "session_id": "xxx",
      "type": "tts",
      "state": "stop"
    }

    裝置端停止播放音訊,若無更多指令,則回到空閒狀態。


10. 總結

本協議透過在 WebSocket 上層傳輸 JSON 文字與二進位制音訊幀,完成功能包括音訊流上傳、TTS 音訊播放、語音識別與狀態管理、MCP 指令下發等。其核心特徵:

  • 握手階段:傳送 "type":"hello",等待伺服器返回。
  • 音訊通道:採用 Opus 編碼的二進位制幀雙向傳輸語音流,支援多種協議版本。
  • JSON 訊息:使用 "type" 為核心欄位標識不同業務邏輯,包括 TTS、STT、MCP、WakeWord、System、Custom 等。
  • 擴充性:可根據實際需求在 JSON 訊息中新增欄位,或在 headers 裡進行額外鑑權。

伺服器與裝置端需提前約定各類訊息的欄位含義、時序邏輯以及錯誤處理規則,方能保證通訊順暢。上述資訊可作為基礎文件,便於後續對接、開發或擴充。

來源:78/xiaozhi-esp32 的 docs/websocket_zh.md