| title | Troubleshooting |
|---|---|
| description | Symptom-first lookup table for common and obscure failure modes. |
Symptom-first lookup table covering common and obscure failure modes. Pair with quickstart.md for happy-path commands and voice-pipeline.md for ASR/TTS internals.
Symptom: The robot appears to process the utterance (logs show ASR text and an LLM response), but no audio plays back. The TTS stage produces zero-length or near-zero-length audio.
Cause: Language mismatch between the TTS voice and the response text. EdgeTTS en-* voices return empty audio when given non-English text (Chinese, Japanese, etc.). This is not a throttle or rate limit — it's a silent failure in the EdgeTTS service.
Fix:
- Check the xiaozhi-server logs for the LLM response text. On PiVoiceLLM, non-English text means the model ignored the per-turn
build_turn_suffix()policy. - Confirm
custom-providers/pi_voice/pi_voice.pyappendsbuild_turn_suffix()to the prompt. PiVoiceLLM uses--no-context-filesand does not forward xiaozhi's system dialogue, so persona and.config.yamlprompt edits do not diagnose this provider. - Check
data/.config.yamlto confirm the TTS voice matches the expected response language (e.g.,en-AU-WilliamNeuralfor English). - If using Piper TTS instead of EdgeTTS, confirm the selected voice model matches the response language.
Symptom: The robot speaks, but in the wrong language. Logs show Chinese or Japanese text in the LLM response.
Cause: The LLM (Qwen3) is ignoring the per-turn language constraint. Qwen3 can leak Chinese despite the English-only suffix; English has no deterministic output validator.
Fix:
- Confirm the per-turn sandwich enforcement is active on the live
PiVoiceLLMpath. Static system prompts alone are not enough — every turn is wrapped with an explicit English+emoji suffix. This isbuild_turn_suffix()incustom-providers/textUtils.py, applied bycustom-providers/pi_voice/pi_voice.py(_wrap_with_sandwich). (The oldbridge.py::_build_sandwich_promptwas part of the retired ZeroClaw bridge and no longer exists — the bridge is not in the voice path.) - Inspect the prompt captured by PiVoiceLLM or its unit tests; do not use persona files or the top-level
.config.yamlprompt as evidence for this path. - Watch the actual voice path in xiaozhi-server logs.
docker logs dotty-piis normally empty because Pi runs as adocker execchild; PiClient owns that child's stdio. - As a last resort, the ASR may be mis-transcribing English as another language. Check the configured ASR language is
enrather thanauto.
Symptom: The robot responds but the audio is choppy, stuttery, or cuts off mid-sentence.
Possible causes:
- WiFi signal. The robot's ESP32-S3 is 2.4 GHz only. Check RSSI — anything below -70 dBm will cause packet loss on the WebSocket stream. Move the robot closer to the access point, or reduce 2.4 GHz interference.
- WebSocket abnormal close. Check xiaozhi-server logs for WS disconnect/reconnect events. The device will silently reconnect, but audio in flight is lost.
- TTS chunk timing. If using EdgeTTS (cloud), network jitter between the server and Microsoft's edge servers can cause uneven audio delivery. Switching to Piper (local) eliminates this variable entirely.
- Server CPU contention. If other containers are competing for CPU during the ASR or TTS stages, audio processing can stall. Check
docker statson the server.
Symptom: After flashing the firmware the screen is frozen or black. A serial monitor shows the bootloader looping on E boot: No bootable app partitions in the partition table, or Image length ... doesn't fit in partition length ....
Cause: The device was flashed without a partition table, so it kept the layout left by whatever firmware was on it before (the M5Burner StackChan demo, Home Assistant Voice, etc.). That layout doesn't match Dotty's images.
Fix: Re-flash with the full six-file command in Quickstart step 1. It writes bootloader.bin at 0x0 and partition-table.bin at 0x8000 — with those in place the partition offsets line up. If your downloaded release is missing either file, grab the latest fw-v release, which ships all six binaries.
Symptom: The robot boots and connects to WiFi, but never responds to voice. May show a face but no indication of listening.
Fix:
- Check the bridge health endpoint:
curl http://<XIAOZHI_HOST>:8081/health. If the bridge is down, restart it. - Check xiaozhi-server logs:
docker logs -f xiaozhi-esp32-serveron the server. Look for connection attempts from the robot. - Verify the robot's OTA URL hasn't changed. After a firmware update, re-enter the OTA URL (
http://<XIAOZHI_HOST>:8003/xiaozhi/ota/) in the robot's Advanced Options if needed. - Open the browser test page (
repo/main/xiaozhi-server/test/test_page.html) and point it atws://<XIAOZHI_HOST>:8000/xiaozhi/v1/. If the browser page works but the robot doesn't, it's a robot-side configuration issue.
Symptom: The xiaozhi-server container starts but immediately fails with a Python ModuleNotFoundError in the logs.
Cause: Custom providers are not mounted correctly into the container. The volume mounts in docker-compose.yml map host-side files into specific paths inside the container. If the host path is wrong, the file doesn't arrive and the import fails.
Fix:
- Check
docker logs xiaozhi-esp32-serverfor the exact missing module name. - Verify the volume mounts in
docker-compose.ymlmatch the expected paths. The custom providers must land at:custom-providers/pi_voice/->/opt/xiaozhi-esp32-server/core/providers/llm/pi_voice/custom-providers/edge_stream/->/opt/xiaozhi-esp32-server/core/providers/tts/edge_stream/custom-providers/asr/fun_local.py->/opt/xiaozhi-esp32-server/core/providers/asr/fun_local.py
- If the missing module is a Python dependency (e.g.,
pydub,edge-tts), it may not be in the base image. Add it via the compose file's environment or bake a custom image layer. - After fixing mounts, restart the container:
docker compose restart(notdocker compose down+up, which marks the container as stopped and changes reboot behavior).
Symptom: The robot speaks but its face stays neutral. No smile, laugh, or other expression.
Cause: The LLM response doesn't start with a supported emoji. The xiaozhi firmware parses the leading emoji to select a face animation. If the first character isn't a recognized emoji, no animation triggers.
Supported emoji map:
| Emoji | Expression |
|---|---|
😊 |
Smile |
😆 |
Laugh |
😢 |
Sad |
😮 |
Surprise |
🤔 |
Thinking |
😠 |
Angry |
😐 |
Neutral |
😍 |
Love |
😴 |
Sleepy |
Fix:
- Check the xiaozhi-server logs for the raw LLM response. PiVoiceLLM's per-turn suffix requests an allowed emoji and
_enforce_leading_emoji()prepends neutral😐when it is absent. A prefix-free response therefore indicates the deployed provider is stale or bypassed. - If the response has an emoji but the face doesn't change, it may be an unsupported emoji. Only the nine listed above are mapped to animations.
- Confirm the selected provider is PiVoiceLLM and the deployed
pi_voice.pycontains_enforce_leading_emoji(). The retired bridge fallback is unrelated.
Symptom: The robot's head jerks abruptly when changing position, instead of moving smoothly.
Cause: Known limitation. The current firmware does not implement a velocity or acceleration cap on servo commands. The feedback servos move at their maximum speed, which can be startling — especially in a household with kids.
Workaround: There is no software workaround at this time. This is tracked as a firmware-level fix. See hardware.md for context.
Symptom: The robot says something like "no response" or goes silent after you speak. xiaozhi-server logs show a failed HTTP POST or a failed docker exec call.
Fix:
- Check that the
bridge.pydashboard container and thedotty-pibrain container are running:docker ps | grep -E 'bridge|dotty-pi' - Test the bridge dashboard health endpoint:
curl http://<XIAOZHI_HOST>:8081/health - For
PiVoiceLLMfailures, check that thedotty-picontainer is healthy and the Docker socket is bind-mounted into the xiaozhi container.docker exec -i dotty-pi echo okshould returnok. - If the bridge container crashes on startup, check its logs:
docker logs bridge
Symptom: After pulling a new xiaozhi-esp32-server image, the container fails to start or behaves differently.
Fix:
- Pin the image tag in
docker-compose.ymlbefore upgrading. Theserver_latesttag is a moving target. - Check the upstream changelog for breaking config changes —
data/.config.yamlkeys may have been renamed or removed. - If custom providers fail after an upgrade, the upstream Python module structure may have changed. Check that the mount target paths still exist inside the new image.
- Roll back by specifying the previous image tag in
docker-compose.ymland runningdocker compose up -d.
- quickstart.md — happy-path setup + common ops + reboot survival.
- voice-pipeline.md — details on ASR, TTS, VAD tuning.
- protocols.md — WebSocket wire format for debugging.
- hardware.md — hardware specs and safety notes.
Last verified: 2026-05-17.