diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index a91d39b..12ff8e6 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -107,17 +107,17 @@ config TQ_VOICE_HEARTBEAT_SEC config TQ_VOICE_I2C_PORT int "Voice I2C port" range 0 1 - default 0 + default 1 config TQ_VOICE_I2C_SDA_PIN int "Voice I2C SDA GPIO" range 0 48 - default 17 + default 4 config TQ_VOICE_I2C_SCL_PIN int "Voice I2C SCL GPIO" range 0 48 - default 18 + default 5 config TQ_VOICE_CODEC_ADDR int "ES8311 I2C address" @@ -132,27 +132,27 @@ config TQ_VOICE_I2S_PORT config TQ_VOICE_I2S_MCLK_PIN int "Voice I2S MCLK GPIO" range -1 48 - default 16 + default 39 config TQ_VOICE_I2S_BCLK_PIN int "Voice I2S BCLK GPIO" range 0 48 - default 9 + default 40 config TQ_VOICE_I2S_WS_PIN int "Voice I2S WS GPIO" range 0 48 - default 45 + default 41 config TQ_VOICE_I2S_DOUT_PIN int "Voice I2S DOUT GPIO" range 0 48 - default 8 + default 38 config TQ_VOICE_I2S_DIN_PIN int "Voice I2S DIN GPIO" range 0 48 - default 10 + default 42 config TQ_VOICE_CODEC_PA_PIN int "ES8311 PA control GPIO" diff --git a/main/control_plane/src/rest_server.c b/main/control_plane/src/rest_server.c index afc4b99..b76b2e3 100644 --- a/main/control_plane/src/rest_server.c +++ b/main/control_plane/src/rest_server.c @@ -30,6 +30,8 @@ static const char *s_web_index = " button { border:0; border-radius:8px; padding:8px 12px; cursor:pointer; background:#0b66d0; color:#fff; }\n" " button.alt { background:#5f6f82; }\n" " button.warn { background:#cf2f2f; }\n" + " button.talk { background:#198754; touch-action:none; user-select:none; }\n" + " button.talk.active { background:#d68910; }\n" " pre { background:#0f1720; color:#d6e2f5; border-radius:8px; padding:10px; overflow:auto; min-height:120px; }\n" " .small { font-size: 12px; color:#6a778a; }\n" " label { font-size:13px; color:#44566c; display:block; margin: 10px 0 4px 0; }\n" @@ -102,6 +104,12 @@ static const char *s_web_index = " \n" "
Ready.\n" " \n" @@ -121,7 +129,10 @@ static const char *s_web_index = " const imgScaleToWidthEl = document.getElementById('imgScaleToWidth');\n" " const imgInvertEl = document.getElementById('imgInvert');\n" " const imgPromptExtendEl = document.getElementById('imgPromptExtend');\n" + " const btnVoiceTalkEl = document.getElementById('btnVoiceTalk');\n" " const thermalPromptSuffix = 'black and white line art, pure white background, high contrast, clean edges, minimal texture, no dark background, no heavy shadows, centered subject, thermal printer friendly';\n" + " let voiceTalkHolding = false;\n" + " let voiceTalkBusy = false;\n" "\n" " function log(obj) {\n" " if (typeof obj === 'string') {\n" @@ -147,6 +158,97 @@ static const char *s_web_index = " return n;\n" " }\n" "\n" + " function sleep(ms) {\n" + " return new Promise((resolve) => setTimeout(resolve, ms));\n" + " }\n" + "\n" + " function setVoiceTalkButtonState(active) {\n" + " if (active) {\n" + " btnVoiceTalkEl.classList.add('active');\n" + " btnVoiceTalkEl.textContent = '点击结束对话';\n" + " } else {\n" + " btnVoiceTalkEl.classList.remove('active');\n" + " btnVoiceTalkEl.textContent = '点击开始对话';\n" + " }\n" + " }\n" + "\n" + " function syncVoiceTalkUi(st) {\n" + " const active = !!(st && st.voice_tap_active);\n" + " voiceTalkHolding = active;\n" + " setVoiceTalkButtonState(active);\n" + " }\n" + "\n" + " function voiceReady(st) {\n" + " return !!(st && st.voice_session_active && st.voice_ws_connected && st.voice_started);\n" + " }\n" + "\n" + " async function ensureVoiceSessionReady() {\n" + " let st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (voiceReady(st)) {\n" + " return st;\n" + " }\n" + "\n" + " await callApi('/v1/voice/session/start', 'POST', {});\n" + " const deadline = Date.now() + 10000;\n" + " while (Date.now() < deadline) {\n" + " await sleep(250);\n" + " st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (voiceReady(st)) {\n" + " return st;\n" + " }\n" + " }\n" + "\n" + " throw new Error('voice session is not ready, please retry');\n" + " }\n" + "\n" + " async function beginVoiceTalk() {\n" + " if (voiceTalkBusy) {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " }\n" + "\n" + " voiceTalkBusy = true;\n" + " try {\n" + " let st = await ensureVoiceSessionReady();\n" + " if (st && st.voice_tap_active) {\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " }\n" + " await callApi('/v1/voice/tap/start', 'POST', {});\n" + " st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " } finally {\n" + " voiceTalkBusy = false;\n" + " }\n" + " }\n" + "\n" + " async function endVoiceTalk() {\n" + " if (voiceTalkBusy) {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " }\n" + "\n" + " voiceTalkBusy = true;\n" + " try {\n" + " let st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (!(st && st.voice_tap_active)) {\n" + " return st;\n" + " }\n" + " await callApi('/v1/voice/tap/cancel', 'POST', {});\n" + " st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " } finally {\n" + " voiceTalkBusy = false;\n" + " }\n" + " }\n" + "\n" " function buildImagePromptRequest() {\n" " const prompt = imagePromptEl.value.trim();\n" " if (!prompt) {\n" @@ -222,6 +324,30 @@ static const char *s_web_index = " const req = buildImagePromptRequest();\n" " return callApi('/v1/print/image', 'POST', req);\n" " });\n" + "\n" + " btnVoiceTalkEl.addEventListener('click', () => run(async () => {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (st && st.voice_tap_active) {\n" + " return endVoiceTalk();\n" + " }\n" + " return beginVoiceTalk();\n" + " }));\n" + " window.addEventListener('blur', () => run(async () => {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (st && st.voice_tap_active) {\n" + " return endVoiceTalk();\n" + " }\n" + " return st;\n" + " }));\n" + " setVoiceTalkButtonState(false);\n" + " (async () => {\n" + " try {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " } catch (_) {}\n" + " })();\n" " \n" "