mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 01:53:53 +08:00
162 lines
6.7 KiB
Markdown
162 lines
6.7 KiB
Markdown
# Native MQTT/UDP integration
|
|||
|
|
|
||
|
|
The server can accept device MQTT control messages and encrypted UDP audio
|
||
|
|
without running `xiaozhi-mqtt-gateway`. Native MQTT is optional. The existing
|
||
|
|
Gateway path and direct WebSocket path remain supported.
|
||
|
|
|
||
|
|
## Compatibility modes
|
||
|
|
|
||
|
|
| Mode | Device control | Device audio | Server business runtime |
|
||
|
|
| --- | --- | --- | --- |
|
||
|
|
| Direct WebSocket | WebSocket JSON | WebSocket binary | Shared runtime |
|
||
|
|
| MQTT Gateway | MQTT via Gateway WebSocket | UDP via Gateway WebSocket | Shared runtime |
|
||
|
|
| Native MQTT | MQTT 3.1.1 QoS 0 | AES-128-CTR UDP | Shared runtime |
|
||
|
|
|
||
|
|
Native MQTT keeps the physical MQTT connection alive between conversations.
|
||
|
|
Each device `hello` creates a new logical conversation and UDP encryption
|
||
|
|
session. `goodbye` ends only that logical conversation; MQTT is closed only by
|
||
|
|
MQTT disconnect, duplicate client takeover, keepAlive expiry, server shutdown,
|
||
|
|
or a protocol/authentication error.
|
||
|
|
|
||
|
|
## Enable Native MQTT
|
||
|
|
|
||
|
|
Native MQTT is disabled by default. Both the protocol and server switches must
|
||
|
|
be enabled:
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
protocols:
|
||
|
|
enabled_protocols: [websocket, mqtt]
|
||
|
|
websocket_enabled: true
|
||
|
|
mqtt_enabled: true
|
||
|
|
|
||
|
|
mqtt_server:
|
||
|
|
enabled: true
|
||
|
|
host: 0.0.0.0
|
||
|
|
port: 1883
|
||
|
|
udp_port: 1883
|
||
|
|
udp_bind_host: ""
|
||
|
|
public_endpoint: mqtt.example.com
|
||
|
|
signature_key: replace-with-a-strong-secret
|
||
|
|
max_connections: 1000
|
||
|
|
max_pending_connections: 128
|
||
|
|
heartbeat_interval: 30
|
||
|
|
max_payload_size: 8192
|
||
|
|
message_queue_size: 128
|
||
|
|
business_ready_timeout: 30
|
||
|
|
goodbye_timeout: 1
|
||
|
|
close_timeout: 2
|
||
|
|
|
||
|
|
# Includes the common model and any Agent-specific local ASR variants.
|
||
|
|
shared_asr_max_models: 3
|
||
|
|
```
|
||
|
|
|
||
|
|
`host` is the local MQTT listen address. `public_endpoint` must be reachable by
|
||
|
|
the device and may be either `host` or `host:port`; manager-api normalizes it
|
||
|
|
and does not append the port twice. When `host` is a wildcard and
|
||
|
|
`public_endpoint` is a local IPv4 address, the UDP socket binds that address so
|
||
|
|
reply datagrams use the same source IP advertised to the device. Set
|
||
|
|
`udp_bind_host` only when the local UDP bind address must differ from the
|
||
|
|
advertised endpoint (for example, behind NAT); leaving it empty enables the
|
||
|
|
automatic behavior. Open the configured MQTT TCP and UDP ports in the firewall.
|
||
|
|
|
||
|
|
When configuration is read from manager-api, configure the equivalent
|
||
|
|
`protocols.*` and `mqtt_server.*` system parameters. The OTA response prefers a
|
||
|
|
valid Native endpoint when Native is enabled. If Native is disabled or invalid,
|
||
|
|
manager-api falls back to `server.mqtt_gateway`. If neither is configured, OTA
|
||
|
|
returns only WebSocket information.
|
||
|
|
|
||
|
|
The current firmware persists the OTA `mqtt` object. The UDP server, key and
|
||
|
|
nonce used by Native mode are negotiated in the MQTT `hello` response, so
|
||
|
|
manager-api does not send a separate top-level OTA `udp` object.
|
||
|
|
|
||
|
|
Long-lived Native connections refresh Agent-private components at logical
|
||
|
|
`hello` boundaries. Local ASR variants are shared by effective configuration
|
||
|
|
instead of loaded once per device. `shared_asr_max_models` bounds resident
|
||
|
|
models; when capacity is exhausted, the replacement runtime is rejected and
|
||
|
|
the previous healthy runtime remains active.
|
||
|
|
The default capacity reserves one transition slot so a single Agent can replace
|
||
|
|
an active private local model without dropping its healthy runtime first. Idle
|
||
|
|
variants are cached and evicted before another model is loaded; resident models
|
||
|
|
never exceed this limit.
|
||
|
|
|
||
|
|
MQTT application packets are dispatched in order outside the socket read loop,
|
||
|
|
so PINGREQ remains responsive while a logical Hello waits for private runtime
|
||
|
|
refresh. `business_ready_timeout` bounds that wait and closes an unusable
|
||
|
|
connection instead of leaving the device in a half-negotiated session.
|
||
|
|
`max_connections` limits authenticated client identities, while
|
||
|
|
`max_pending_connections` separately bounds sockets that have not completed
|
||
|
|
CONNECT. A connection using an existing client id may therefore replace its
|
||
|
|
previous owner even when active capacity is full. The server sends the success
|
||
|
|
CONNACK only after the previous owner has been reclaimed. `goodbye_timeout` and
|
||
|
|
`close_timeout` bound best-effort device reset and physical resource cleanup so
|
||
|
|
a stalled peer cannot block heartbeat or shutdown indefinitely.
|
||
|
|
|
||
|
|
## Authentication and topics
|
||
|
|
|
||
|
|
manager-api generates the device credentials and topics:
|
||
|
|
|
||
|
|
- client id: `<group>@@@<mac_with_underscores>@@@<mac_with_underscores>`
|
||
|
|
- publish topic: `device-server`
|
||
|
|
- subscribe topic: `devices/p2p/<mac_with_underscores>`
|
||
|
|
- username: Base64-encoded JSON metadata
|
||
|
|
- password: Base64(HMAC-SHA256(`client_id + "|" + username`, signature key))
|
||
|
|
|
||
|
|
Use the same secret for manager-api `mqtt_server.signature_key` and
|
||
|
|
xiaozhi-server `mqtt_server.signature_key`. The legacy
|
||
|
|
`server.mqtt_signature_key` remains a fallback for Gateway compatibility.
|
||
|
|
|
||
|
|
Native MQTT accepts MQTT 3.1.1 CONNECT, SUBSCRIBE, PUBLISH QoS 0, PING and
|
||
|
|
DISCONNECT. Unsupported protocol versions, QoS levels, malformed packets, or
|
||
|
|
invalid credentials close the physical connection.
|
||
|
|
|
||
|
|
## Session and UDP flow
|
||
|
|
|
||
|
|
1. Device connects and subscribes over MQTT.
|
||
|
|
2. Server validates CONNECT credentials and replaces any older connection with
|
||
|
|
the same client id.
|
||
|
|
3. Device publishes protocol version 3 `hello`.
|
||
|
|
4. Server waits until private configuration and business components are ready.
|
||
|
|
5. Server creates a logical session and returns the UDP server, AES key, nonce,
|
||
|
|
output audio parameters and session id.
|
||
|
|
6. The first valid UDP packet from the MQTT peer binds the UDP source tuple for
|
||
|
|
that logical session. Source changes are rejected until the next `hello`.
|
||
|
|
7. MQTT JSON and decrypted Opus frames enter the same message processors used
|
||
|
|
by WebSocket and Gateway connections.
|
||
|
|
8. Server `goodbye` returns the device to Idle and finalizes conversation tasks
|
||
|
|
while preserving the MQTT connection.
|
||
|
|
|
||
|
|
The 16-byte UDP header is also the AES-CTR nonce:
|
||
|
|
|
||
|
|
| Bytes | Field |
|
||
|
|
| --- | --- |
|
||
|
|
| 0 | packet type (`1`) |
|
||
|
|
| 1 | reserved |
|
||
|
|
| 2..3 | encrypted payload length, big endian |
|
||
|
|
| 4..7 | connection id, big endian |
|
||
|
|
| 8..11 | timestamp, big endian |
|
||
|
|
| 12..15 | sequence, big endian |
|
||
|
|
|
||
|
|
Datagrams with an invalid type, header, payload length, connection id, source
|
||
|
|
address or stale sequence are discarded. Sequence gaps are tolerated because
|
||
|
|
UDP can lose packets; late and duplicate packets are discarded. AES-CTR
|
||
|
|
provides confidentiality but not integrity, so Native MQTT and UDP should be
|
||
|
|
exposed only with a strong signature key and appropriate network controls.
|
||
|
|
|
||
|
|
## Verification
|
||
|
|
|
||
|
|
From `main/xiaozhi-server`:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python -m compileall -q .
|
||
|
|
```
|
||
|
|
|
||
|
|
From `main/manager-api` with JDK 21:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
mvn clean package
|
||
|
|
```
|
||
|
|
|
||
|
|
Before deployment, verify Native and Gateway separately with real hardware:
|
||
|
|
multiple conversations, TTS interruption, exit to Idle, duplicate reconnect,
|
||
|
|
server restart, short network loss and a 30-60 minute keepAlive soak.
|