Files
esp32-server/app/xiaozhi/protocol.py
kunthawat a582c1ff36 feat: firmware-compatible handshake (header auth, transport=websocket) + /ota onboarding endpoint
- auth from Authorization/Device-Id headers (firmware v2.5.0), frame fallback kept
- server hello carries transport=websocket + session_id (firmware ParseServerHello)
- /ota GET+POST returns {websocket:{url,token,version}, server_time:{timestamp,timezone_offset}, firmware:{version}}
- config.yaml: device registered by MAC, ota block added
- 21 tests pass (added OTA + header-auth coverage)
2026-10-03 16:35:41 +07:00

73 lines
2.4 KiB
Python

"""Xiaozhi WebSocket handshake frame models (PoC).
CON-004: the exact field set of the Xiaozhi ``hello`` frame is a firmware
detail. These models capture the fields the plan names (Device-Id,
Client-Id, Authorization, audio_params) plus the standard handshake fields.
Every constant that must match the firmware is marked ``# FIRMWARE``.
"""
from __future__ import annotations
import uuid
from typing import Any, Optional
from pydantic import BaseModel, Field
class AudioParams(BaseModel):
# FIRMWARE: Opus params the device reports/accepts.
format: str = "opus"
sample_rate: int = 16000
channels: int = 1
frame_duration: int = 20 # ms
class Hello(BaseModel):
"""Device -> Bridge greeting (first text frame)."""
type: str = "hello"
device_id: str = ""
client_id: Optional[str] = None
authorization: Optional[str] = None # FIRMWARE: token field name
protocol_version: int = 1 # FIRMWARE
audio_params: AudioParams = Field(default_factory=AudioParams)
class ServerHello(BaseModel):
"""Bridge -> Device acknowledgement, firmware v2.5.0-compatible.
FIRMWARE: ParseServerHello requires ``transport == "websocket"`` and
reads ``session_id`` + ``audio_params`` (sample_rate, frame_duration).
"""
type: str = "hello"
transport: str = "websocket"
session_id: str = ""
audio_params: AudioParams = Field(default_factory=AudioParams)
server_time: int = 0
def build_hello(payload: dict) -> Hello:
"""Parse a raw hello dict; tolerate firmware field-name drift.
Tolerant parsing is intentional: firmware builds differ, and the PoC must
connect, not reject, on a missing optional field.
"""
data: dict[str, Any] = dict(payload)
data.setdefault("type", "hello")
# Common firmware aliases.
for src, dst in (("deviceId", "device_id"), ("Device-Id", "device_id")):
if src in data and "device_id" not in data:
data["device_id"] = data[src]
if "audio" in data and "audio_params" not in data:
data["audio_params"] = data.pop("audio")
return Hello.model_validate(data)
def build_server_hello(device_hello: Hello) -> ServerHello:
"""Firmware-compatible server hello (transport=websocket + session_id)."""
import time
return ServerHello(
session_id=str(uuid.uuid4()),
audio_params=device_hello.audio_params,
server_time=int(time.time() * 1000),
)