| 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
build_turn_suffix()rules in pi's system prompt. - Confirm
custom-providers/pi_voice/pi_voice.pyputsbuild_turn_suffix()into pi's system prompt (_turn_rules). 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 English-only rule. Qwen3 can leak Chinese despite it; English has no deterministic output validator.
Fix:
- Confirm the voice rules are in place on the live
PiVoiceLLMpath. The English-only and emoji rules come frombuild_turn_suffix()incustom-providers/textUtils.py, whichcustom-providers/pi_voice/pi_voice.py(_turn_rules) puts in pi's system prompt when pi spawns; pi respawns when Kid Mode changes. (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: Build from source and re-flash with idf.py flash, as in SETUP.md §2. It writes the bootloader at 0x0 and the partition table at 0x8000 along with the app, so the partition offsets line up.
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 which OTA URL the firmware was built with. The StackChan's on-device Settings app has no OTA URL field. For another self-hosted server, rebuild with
CONFIG_OTA_URL="http://<XIAOZHI_HOST>:8003/xiaozhi/ota/"and re-flash using SETUP.md. If the device is currently exposing the optional Xiaozhi SoftAP captive portal, its browser-based Advanced tab can persist anota_urloverride instead. - 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. The rules in PiVoiceLLM's system prompt request 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.