@cheeko-ai/esp32-voice 2026.2.2-3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ # ESP32-Voice Plugin — npm Publishing Readiness Report
2
+
3
+ > Full analysis of what needs to change before this plugin can be published to npm
4
+ > and work out-of-the-box for other OpenClaw users.
5
+
6
+ ---
7
+
8
+ ## Current Status: NOT READY for npm publish
9
+
10
+ ---
11
+
12
+ ## 🔴 Section 1 — Security (Fix Before Anything Else)
13
+
14
+ ### 1.1 Real API keys exposed in `SETUP.md`
15
+
16
+ `SETUP.md` contains live, active credentials committed to the repository:
17
+
18
+ ```
19
+ GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE
20
+ ELEVENLABS_API_KEY=YOUR_ELEVENLABS_API_KEY_HERE
21
+ ```
22
+
23
+ **Action required:**
24
+ 1. Rotate both keys immediately (they are now public):
25
+ - ElevenLabs: https://elevenlabs.io/app/settings/api-keys → delete → create new
26
+ - Google AI Studio: https://aistudio.google.com/apikey → delete → create new
27
+ 2. Replace all real values in `SETUP.md` with placeholders like `<YOUR_ELEVENLABS_API_KEY>`
28
+
29
+ ---
30
+
31
+ ### 1.2 Hardcoded fallback gateway token in `ota-server.js`
32
+
33
+ ```js
34
+ // Line ~59 in ota-server.js
35
+ const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "YOUR_GATEWAY_TOKEN_HERE";
36
+ ```
37
+
38
+ This token is now public. Any user who forgets to set the env var silently uses this known token.
39
+
40
+ **Action required:**
41
+ Replace with a hard exit if the env var is not set:
42
+ ```js
43
+ const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN;
44
+ if (!GATEWAY_TOKEN) {
45
+ console.error("[ota-server] ERROR: GATEWAY_TOKEN env var is required.");
46
+ process.exit(1);
47
+ }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 🔴 Section 2 — package.json Fixes (Required for npm publish)
53
+
54
+ ### 2.1 Remove unused `@discordjs/opus` dependency
55
+
56
+ `@discordjs/opus` is listed in `dependencies` but is **never imported anywhere** in the source.
57
+ It was replaced by `opusscript` after macOS Gatekeeper rejected its prebuilt native binary.
58
+ Leaving it in `dependencies` means every user downloads and tries to install a native binary
59
+ they don't need — and it will fail on many systems.
60
+
61
+ ```bash
62
+ cd extensions/esp32-voice
63
+ npm uninstall @discordjs/opus
64
+ ```
65
+
66
+ **Why opusscript needs no install script:**
67
+ `opusscript` is pure JavaScript/WebAssembly. It requires:
68
+ - No native compilation
69
+ - No `node-gyp`
70
+ - No system libraries (no `libopus` to install)
71
+ - No pre/post-install scripts
72
+
73
+ It installs cleanly on macOS, Linux, and Windows via a plain `npm install`. No extra steps for users.
74
+
75
+ ---
76
+
77
+ ### 2.2 Update version to CalVer
78
+
79
+ All OpenClaw extensions use CalVer format (`YYYY.M.D`). This package has `"version": "1.0.0"`.
80
+
81
+ Change to: `"version": "2026.2.21"` (or the date of first publish)
82
+
83
+ ---
84
+
85
+ ### 2.3 Add `ota-server.js` to the `files` array
86
+
87
+ `ota-server.js` is not in the `files` array so it will be excluded from the npm package.
88
+ Users will install the plugin but have no OTA server.
89
+
90
+ Current `files` array:
91
+ ```json
92
+ "files": ["index.ts", "src/", "openclaw.plugin.json", "README.md"]
93
+ ```
94
+
95
+ Should be:
96
+ ```json
97
+ "files": ["index.ts", "src/", "openclaw.plugin.json", "README.md", "ota-server.js", ".env.example"]
98
+ ```
99
+
100
+ ---
101
+
102
+ ### 2.4 Add `.env.example` file (new file to create)
103
+
104
+ Users need to know what env vars to set. Create `extensions/esp32-voice/.env.example`:
105
+
106
+ ```bash
107
+ # Required — get free key at https://console.deepgram.com
108
+ DEEPGRAM_API_KEY=<your-deepgram-api-key>
109
+
110
+ # Required — get free key at https://elevenlabs.io
111
+ ELEVENLABS_API_KEY=<your-elevenlabs-api-key>
112
+
113
+ # Optional — find voice IDs at https://elevenlabs.io/voice-library
114
+ # Default: Rachel (21m00Tcm4TlvDq8ikWAM)
115
+ ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM
116
+
117
+ # Optional — default: eleven_turbo_v2_5
118
+ ELEVENLABS_MODEL_ID=eleven_turbo_v2_5
119
+
120
+ # Optional — default: nova-2
121
+ DEEPGRAM_MODEL=nova-2
122
+
123
+ # Optional — port for ESP32 WebSocket server (default: 8765)
124
+ ESP32_VOICE_PORT=8765
125
+
126
+ # Required for OTA server — copy from your openclaw.json or gateway setup
127
+ GATEWAY_TOKEN=<your-openclaw-gateway-token>
128
+
129
+ # Optional — default: ws://127.0.0.1:18789
130
+ OPENCLAW_GATEWAY_URL=ws://127.0.0.1:18789
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 🟡 Section 3 — Setup Experience for New Users
136
+
137
+ ### What a new user has to do today (too many steps)
138
+
139
+ 1. Install OpenClaw and the plugin
140
+ 2. Sign up for Deepgram (STT) — get API key
141
+ 3. Sign up for ElevenLabs (TTS) — get API key
142
+ 4. Add keys to `~/.openclaw/.env`
143
+ 5. Edit `~/.openclaw/openclaw.json` with a device token (generate manually)
144
+ 6. Start OpenClaw Gateway in one terminal
145
+ 7. Start `ota-server.js` in a **second terminal**
146
+ 8. Find their machine's LAN IP address
147
+ 9. Flash ESP32 with the OTA URL
148
+ 10. Reboot ESP32 and hope auto-detect worked
149
+
150
+ **Pain point:** Two separate processes, manual IP lookup, unclear token generation.
151
+
152
+ ---
153
+
154
+ ### 3.1 Fix hardcoded timezone in `ota-server.js`
155
+
156
+ The OTA server hardcodes IST (UTC+5:30 = 330 minutes). Users in other timezones get wrong device time on their ESP32.
157
+
158
+ Current:
159
+ ```js
160
+ timezone_offset: 330 // hardcoded IST
161
+ ```
162
+
163
+ Fix:
164
+ ```js
165
+ timezone_offset: -new Date().getTimezoneOffset() // reads system timezone
166
+ ```
167
+
168
+ > `getTimezoneOffset()` returns minutes west of UTC (negative for east of UTC),
169
+ > so negating it gives the standard "minutes east of UTC" that XiaoZhi expects.
170
+
171
+ ---
172
+
173
+ ### 3.2 Integrate OTA endpoint into the plugin HTTP handler (removes second terminal)
174
+
175
+ Currently `ota-server.js` must be run as a separate process. The plugin already has an
176
+ HTTP handler (`src/http-handler.ts`). Adding the OTA route there means users only run
177
+ one command — `openclaw gateway` — and everything works.
178
+
179
+ The OTA route should be served at:
180
+ ```
181
+ http://<your-lan-ip>:18789/__openclaw__/esp32-voice/ota/
182
+ ```
183
+
184
+ Payload returned (same as ota-server.js currently returns):
185
+ ```json
186
+ {
187
+ "websocket": {
188
+ "url": "ws://<LAN_IP>:8765/"
189
+ },
190
+ "openclaw": {
191
+ "url": "ws://127.0.0.1:18789",
192
+ "token": "<GATEWAY_TOKEN>"
193
+ },
194
+ "timezone_offset": -330
195
+ }
196
+ ```
197
+
198
+ Once integrated, update `README.md` to say "OTA is automatically available — no second server needed."
199
+
200
+ ---
201
+
202
+ ### 3.3 Rewrite `README.md` top section as a 3-step Quick Start
203
+
204
+ The current README has good detail but buries the getting-started path. New users need:
205
+
206
+ ```
207
+ ## Quick Start
208
+
209
+ ### Step 1 — Get API keys (both have free tiers)
210
+ - Deepgram: https://console.deepgram.com → API Keys → Create
211
+ - ElevenLabs: https://elevenlabs.io → Profile → API Keys
212
+
213
+ ### Step 2 — Configure
214
+ cp .env.example ~/.openclaw/.env
215
+ # Edit ~/.openclaw/.env with your keys
216
+
217
+ ### Step 3 — Flash your ESP32
218
+ Point your XiaoZhi firmware OTA URL to:
219
+ http://<your-machine-lan-ip>:18789/__openclaw__/esp32-voice/ota/
220
+ Reboot the device. Done.
221
+ ```
222
+
223
+ ---
224
+
225
+ ## 🟡 Section 4 — Reliability Improvements
226
+
227
+ ### 4.1 Persist OTP pairing across Gateway restarts
228
+
229
+ Currently paired devices are stored in memory only. If the Gateway restarts, all ESP32
230
+ devices need to be re-paired with a new OTP. This is very annoying for always-on devices.
231
+
232
+ Fix: persist the paired device map to `~/.openclaw/esp32-voice-devices.json` on each
233
+ successful pairing and reload it on startup.
234
+
235
+ ---
236
+
237
+ ### 4.2 Add rate limiting to OTP verification
238
+
239
+ The OTP is 6 digits (100,000 possible values). Without rate limiting, someone on the
240
+ same LAN could brute-force it in minutes. Add a per-IP attempt counter: lock out for
241
+ 15 minutes after 5 failed attempts.
242
+
243
+ ---
244
+
245
+ ### 4.3 Auto-reconnect to Gateway on disconnect
246
+
247
+ If the OpenClaw Gateway drops the connection (restart, timeout, network blip),
248
+ `openclawConnected` goes false and stays false until the ESP32 session is restarted.
249
+ Should reconnect automatically with exponential backoff (3s → 6s → 12s → 30s cap).
250
+
251
+ ---
252
+
253
+ ## 🟢 Section 5 — Pre-publish Checklist
254
+
255
+ Run through this before `npm publish`:
256
+
257
+ - [ ] Rotated the exposed API keys (ElevenLabs + Gemini)
258
+ - [ ] `SETUP.md` has no real credentials — only `<PLACEHOLDER>` values
259
+ - [ ] Hardcoded fallback token removed from `ota-server.js`
260
+ - [ ] `@discordjs/opus` removed from `package.json` dependencies
261
+ - [ ] Version updated to CalVer (e.g. `2026.2.21`)
262
+ - [ ] `ota-server.js` added to `files` array in `package.json`
263
+ - [ ] `.env.example` created and added to `files` array
264
+ - [ ] Timezone fix applied in `ota-server.js`
265
+ - [ ] README has a 3-step Quick Start at the very top
266
+ - [ ] Ran `npm pack --dry-run` and verified:
267
+ - No `node_modules/` in the package
268
+ - No `.env` file in the package
269
+ - No real credentials in any file
270
+ - [ ] Test clean install: `mkdir /tmp/test && cd /tmp/test && npm install @openclaw/esp32-voice`
271
+
272
+ Then publish:
273
+ ```bash
274
+ cd extensions/esp32-voice
275
+ npm login
276
+ npm publish --access public
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Summary Table
282
+
283
+ | # | Issue | Severity | Effort |
284
+ |---|---|---|---|
285
+ | 1.1 | Real API keys in SETUP.md | 🔴 Critical | 5 min |
286
+ | 1.2 | Hardcoded fallback token in ota-server.js | 🔴 Critical | 5 min |
287
+ | 2.1 | Remove unused @discordjs/opus | 🔴 Blocker | 2 min |
288
+ | 2.2 | Version to CalVer | 🔴 Blocker | 1 min |
289
+ | 2.3 | Add ota-server.js to files array | 🔴 Blocker | 2 min |
290
+ | 2.4 | Create .env.example | 🟡 Important | 10 min |
291
+ | 3.1 | Fix hardcoded IST timezone | 🟡 Important | 5 min |
292
+ | 3.2 | Integrate OTA into plugin HTTP handler | 🟡 Important | 2–3 hours |
293
+ | 3.3 | Rewrite README Quick Start | 🟡 Important | 30 min |
294
+ | 4.1 | Persist OTP pairing to disk | 🟡 Nice to have | 1 hour |
295
+ | 4.2 | Rate limit OTP attempts | 🟡 Nice to have | 1 hour |
296
+ | 4.3 | Auto-reconnect to Gateway | 🟡 Nice to have | 1 hour |
297
+
298
+ **Minimum to publish safely: items 1.1, 1.2, 2.1, 2.2, 2.3**
299
+ **Minimum for a good user experience: all of Section 2 + Section 3**
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # 🎤 ESP32 Voice — OpenClaw Extension
2
+
3
+ Turn a XiaoZhi ESP32 board into a voice AI assistant powered by OpenClaw.
4
+ Push to talk → speak → get a spoken response. Integrates with the Cheeko dashboard for device management.
5
+
6
+ ---
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @cheeko-ai/esp32-voice
12
+ ```
13
+
14
+ Or via OpenClaw plugin system:
15
+
16
+ ```bash
17
+ openclaw channels add
18
+ # Select "ESP32 Voice (plugin)" from the menu
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ### Step 1 — Run the setup wizard
26
+
27
+ ```bash
28
+ pnpm openclaw channels add
29
+ ```
30
+
31
+ Select **ESP32 Voice (plugin)** from the channel menu. The interactive wizard guides you through:
32
+
33
+ | Step | What happens |
34
+ |------|-------------|
35
+ | **1. Connect to Cheeko** | Opens dashboard link → you log in → paste the pairing token |
36
+ | **2. STT setup** | Enter your Deepgram API key |
37
+ | **3. TTS setup** | Enter your ElevenLabs API key + voice ID |
38
+ | **4. Add device** | Opens dashboard to add your ESP32 device |
39
+
40
+ All keys are saved to `~/.openclaw/.env` automatically — you only do this once.
41
+
42
+ > **Note:** Use Node.js 22. Run `nvm use 22` before any openclaw commands.
43
+
44
+ ---
45
+
46
+ ### Step 2 — Start the Gateway
47
+
48
+ ```bash
49
+ openclaw gateway
50
+ ```
51
+
52
+ On startup the plugin:
53
+ - Starts the voice WebSocket server on port **8765**
54
+ - Auto-registers your machine's WebSocket URL with the Cheeko dashboard (if `CHEEKO_PAIR` is set)
55
+
56
+ ---
57
+
58
+ ### Step 3 — Start the OTA server
59
+
60
+ The OTA server tells your ESP32 where to connect on boot:
61
+
62
+ ```bash
63
+ GATEWAY_TOKEN=<your-gateway-token> node $(openclaw plugins path @cheeko-ai/esp32-voice)/ota-server.js
64
+ ```
65
+
66
+ It prints your URLs:
67
+
68
+ ```
69
+ 🦞 ESP32 OTA Mock Server
70
+ Auto-detected MAC IP : 192.168.1.10
71
+ OTA Server : http://192.168.1.10:8080/xiaozhi/ota/
72
+ Voice WebSocket : ws://192.168.1.10:8765/
73
+ ```
74
+
75
+ > **Gateway token** — found in `~/.openclaw/openclaw.json` under `gateway.auth.token`.
76
+
77
+ ---
78
+
79
+ ### Step 4 — Flash your ESP32
80
+
81
+ In your XiaoZhi firmware settings, set the OTA URL to what the server printed:
82
+
83
+ ```
84
+ http://192.168.1.10:8080/xiaozhi/ota/
85
+ ```
86
+
87
+ Reboot the device. It fetches its config, connects to the voice server, and is ready.
88
+ **Hold the button → speak → release → hear the response.**
89
+
90
+ ---
91
+
92
+ ## How It Works
93
+
94
+ ```
95
+ ESP32 (XiaoZhi firmware)
96
+ │ Opus audio frames → WebSocket port 8765
97
+
98
+ [esp32-voice plugin]
99
+ │ STT: Deepgram → transcript text
100
+ │ LLM: OpenClaw Gateway (port 18789) → response text
101
+ │ TTS: ElevenLabs → Opus audio frames
102
+
103
+ ESP32 speaker
104
+ ```
105
+
106
+ The plugin runs its own WebSocket server on port **8765** — completely separate from the OpenClaw Gateway port (18789). No changes to OpenClaw core are needed.
107
+
108
+ ---
109
+
110
+ ## Cheeko Dashboard Pairing
111
+
112
+ The plugin auto-registers your machine's voice URL with the Cheeko dashboard when `CHEEKO_PAIR` is set.
113
+
114
+ **How the pairing works:**
115
+
116
+ 1. Log in to the Cheeko dashboard → **Settings → Connect OpenClaw**
117
+ 2. The dashboard generates a short pairing token (e.g. `XK9-2M4`)
118
+ 3. Paste it into the setup wizard (or set `CHEEKO_PAIR=XK9-2M4` in `~/.openclaw/.env`)
119
+ 4. On next gateway start, the plugin POSTs your voice URL to the dashboard automatically
120
+ 5. Your ESP32 devices in the dashboard now know where to connect
121
+
122
+ ```
123
+ Dashboard generates token → you paste it in wizard
124
+
125
+ Plugin saves CHEEKO_PAIR to ~/.openclaw/.env
126
+
127
+ openclaw gateway starts
128
+
129
+ Plugin POSTs ws://<your-ip>:8765/ to dashboard API
130
+
131
+ Dashboard stores your OpenClaw URL against your account
132
+
133
+ ESP32 devices fetch config → connect to your machine
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Configuration Reference
139
+
140
+ All config goes in `~/.openclaw/openclaw.json` under `channels.esp32voice`.
141
+ Keys can also be set in `~/.openclaw/.env`.
142
+
143
+ ### Minimal config
144
+
145
+ ```json5
146
+ {
147
+ channels: {
148
+ esp32voice: {
149
+ enabled: true,
150
+ },
151
+ },
152
+ }
153
+ ```
154
+
155
+ Keys are read from env vars automatically:
156
+
157
+ ```bash
158
+ # ~/.openclaw/.env
159
+ DEEPGRAM_API_KEY=your-deepgram-key
160
+ ELEVENLABS_API_KEY=your-elevenlabs-key
161
+ CHEEKO_PAIR=XK9-2M4
162
+ ```
163
+
164
+ ### Full config
165
+
166
+ ```json5
167
+ {
168
+ channels: {
169
+ esp32voice: {
170
+ enabled: true,
171
+ sttApiKey: "your-deepgram-key",
172
+ ttsApiKey: "your-elevenlabs-key",
173
+ ttsVoiceId: "21m00Tcm4TlvDq8ikWAM", // optional, defaults to Rachel
174
+ language: "en",
175
+ maxResponseLength: 500,
176
+ voiceOptimized: true,
177
+ },
178
+ },
179
+ }
180
+ ```
181
+
182
+ ### All options
183
+
184
+ | Key | Type | Default | Description |
185
+ |-----|------|---------|-------------|
186
+ | `enabled` | boolean | `true` | Enable/disable the channel |
187
+ | `sttProvider` | string | `"deepgram"` | STT provider ID |
188
+ | `sttApiKey` | string | — | Deepgram API key |
189
+ | `sttModel` | string | `"nova-2"` | Deepgram model |
190
+ | `ttsProvider` | string | `"elevenlabs"` | TTS provider ID |
191
+ | `ttsApiKey` | string | — | ElevenLabs API key |
192
+ | `ttsVoiceId` | string | Rachel | ElevenLabs voice ID |
193
+ | `ttsModel` | string | `"eleven_turbo_v2_5"` | ElevenLabs model |
194
+ | `language` | string | `"en"` | Language code (ISO 639-1) |
195
+ | `maxResponseLength` | number | `500` | Max response chars (keep short for voice) |
196
+ | `voiceOptimized` | boolean | `true` | Tells the AI to respond concisely without markdown |
197
+
198
+ ### Environment variables
199
+
200
+ | Variable | Description |
201
+ |----------|-------------|
202
+ | `DEEPGRAM_API_KEY` | Deepgram STT API key |
203
+ | `ELEVENLABS_API_KEY` | ElevenLabs TTS API key |
204
+ | `ELEVENLABS_VOICE_ID` | ElevenLabs voice ID (optional) |
205
+ | `ELEVENLABS_MODEL_ID` | ElevenLabs model (optional) |
206
+ | `DEEPGRAM_MODEL` | Deepgram model (optional) |
207
+ | `ESP32_VOICE_PORT` | Voice WebSocket server port (default: `8765`) |
208
+ | `GATEWAY_TOKEN` | Required for OTA server |
209
+ | `CHEEKO_PAIR` | Pairing token from Cheeko dashboard — enables auto-registration |
210
+ | `CHEEKO_DASHBOARD_URL` | Cheeko dashboard UI URL (default: `http://64.227.170.31:8001`) |
211
+ | `CHEEKO_API_URL` | Cheeko backend API URL (default: `http://64.227.170.31:8002/toy`) |
212
+ | `MAC_IP` | Override auto-detected LAN IP (useful if machine has multiple interfaces) |
213
+
214
+ ---
215
+
216
+ ## OTA Server
217
+
218
+ The OTA server (`ota-server.js`) is a small HTTP server the ESP32 calls on boot to get its config — WebSocket URL, auth token, and timezone.
219
+
220
+ ```bash
221
+ # Basic
222
+ GATEWAY_TOKEN=<token> node ota-server.js
223
+
224
+ # With overrides
225
+ MAC_IP=192.168.1.50 VOICE_PORT=8765 OTA_PORT=8080 GATEWAY_TOKEN=<token> node ota-server.js
226
+ ```
227
+
228
+ | Env var | Default | Description |
229
+ |---------|---------|-------------|
230
+ | `GATEWAY_TOKEN` | — | **Required.** Your OpenClaw gateway token |
231
+ | `MAC_IP` | auto-detected | Your machine's LAN IP |
232
+ | `VOICE_PORT` | `8765` | Voice WebSocket port |
233
+ | `OTA_PORT` | `8080` | OTA server port |
234
+ | `TZ_OFFSET` | system timezone | Minutes east of UTC (e.g. `330` for IST) |
235
+
236
+ ---
237
+
238
+ ## Gateway HTTP Endpoints
239
+
240
+ The plugin also registers utility endpoints on the OpenClaw Gateway port (18789):
241
+
242
+ | Endpoint | Description |
243
+ |----------|-------------|
244
+ | `GET /__openclaw__/esp32-voice/health` | Health check — shows configured STT/TTS status |
245
+ | `GET /__openclaw__/esp32-voice/otp` | Generate a one-time device pairing code |
246
+ | `GET /__openclaw__/esp32-voice/devices` | List currently paired devices |
247
+ | `GET /__openclaw__/esp32-voice/stream` | Info about the voice WebSocket URL |
248
+
249
+ ---
250
+
251
+ ## Troubleshooting
252
+
253
+ **ESP32 shows "connecting" but never "listening"**
254
+ - Check the OTA server is running and the ESP32 fetched its config (watch OTA server logs)
255
+ - Make sure firewall allows port 8765 inbound
256
+ - Confirm `GATEWAY_TOKEN` matches `gateway.auth.token` in `~/.openclaw/openclaw.json`
257
+
258
+ **Response is always "HEARTBEAT_OK"**
259
+ - This was a known bug — fixed in v2026.2.21. Update to latest.
260
+
261
+ **Dashboard pairing fails**
262
+ - Make sure you paste only the short token (e.g. `XK9-2M4`), not the full command string
263
+ - The token expires after 10 minutes — generate a new one from the dashboard if needed
264
+ - Confirm the backend API is reachable: `curl http://64.227.170.31:8002/toy/health`
265
+
266
+ **No audio from ESP32 speaker**
267
+ - Plugin outputs 24kHz 16-bit mono Opus at 60ms frames — confirm firmware matches
268
+ - Check ElevenLabs key is valid and has quota remaining
269
+
270
+ **STT timeout / empty transcript**
271
+ - Validate Deepgram key: `curl https://api.deepgram.com/v1/auth -H "Authorization: Token YOUR_KEY"`
272
+ - Check the ESP32 is actually sending audio (hold button while speaking)
273
+
274
+ **"device signature invalid" in gateway logs**
275
+ - Device identity at `~/.openclaw/identity/device.json` may be missing
276
+ - Run `openclaw onboard` to regenerate it
277
+
278
+ ---
279
+
280
+ ## Supported Hardware
281
+
282
+ Tested with:
283
+ - **Jiuchuan S3** (XiaoZhi ESP32-S3 board) — recommended
284
+ - Any ESP32 board running [XiaoZhi firmware](https://github.com/78/xiaozhi-esp32)
285
+
286
+ ---
287
+
288
+ ## License
289
+
290
+ MIT — Published under [@cheeko-ai](https://www.npmjs.com/org/cheeko-ai) on npm.