多模态
This commit is contained in:
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"idf.currentSetup": "/Users/moyyang/esp/v5.5.2/esp-idf",
|
||||
"idf.flashType": "UART",
|
||||
"idf.port": "/dev/tty.wchusbserial5B0B0251771"
|
||||
"idf.port": "/dev/tty.wchusbserial5B0B0251771",
|
||||
"idf.openOcdConfigs": [
|
||||
"board/esp32s3-builtin.cfg"
|
||||
],
|
||||
"idf.customExtraVars": {
|
||||
"IDF_TARGET": "esp32s3"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(lyf_printer_controller)
|
||||
project(TQ_printer_controller)
|
||||
|
||||
76
README.md
76
README.md
@@ -1,8 +1,8 @@
|
||||
# ESP32-S3 LYF Printer Controller (Wi-Fi REST API)
|
||||
# ESP32-S3 TQ Printer Controller (Wi-Fi REST API)
|
||||
|
||||
ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
|
||||
- REST client -> ESP32-S3 (`esp_http_server`)
|
||||
- ESP32-S3 -> BLE printer (`lyfPrinter`, `FFF2` write / `FFF1` notify)
|
||||
- ESP32-S3 -> BLE printer (`TQPrinter` / `lyfPrinter`, `FFF2` write / `FFF1` notify)
|
||||
- Command compatibility:
|
||||
- Print path: `0x00~0x07`
|
||||
- OTA path: `0xA0~0xA4`
|
||||
@@ -15,6 +15,10 @@ ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
|
||||
- Async print queue with jobs (`queued/running/success/failed/canceled`)
|
||||
- Printer precheck (paper / battery / temperature)
|
||||
- Built-in UTF-8 text rendering with Chinese support (GB2312 character set, 16x16 bitmap)
|
||||
- Tap2talk multimodal voice interaction over DashScope WebSocket:
|
||||
- ES8311 microphone/speaker via `esp_codec_dev`
|
||||
- raw-opus uplink/downlink via `esp_audio_codec`
|
||||
- WebSocket transport via `esp_websocket_client`
|
||||
- Android-compatible print flow:
|
||||
- `0x00` power on
|
||||
- `0x03` set print param
|
||||
@@ -50,6 +54,12 @@ ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
|
||||
- `POST /v1/ota/erase_page`
|
||||
- `POST /v1/ota/write_frame`
|
||||
- `POST /v1/ota/upgrade`
|
||||
- Voice:
|
||||
- `GET /v1/voice/status`
|
||||
- `POST /v1/voice/session/start`
|
||||
- `POST /v1/voice/session/stop`
|
||||
- `POST /v1/voice/tap/start`
|
||||
- `POST /v1/voice/tap/cancel`
|
||||
|
||||
## Build
|
||||
|
||||
@@ -66,13 +76,23 @@ idf.py build
|
||||
|
||||
## Config
|
||||
|
||||
In `menuconfig -> LYF Controller Config`:
|
||||
- `LYF_WIFI_SSID`
|
||||
- `LYF_WIFI_PASSWORD`
|
||||
- `LYF_HTTP_PORT`
|
||||
- `LYF_API_KEY` (optional)
|
||||
In `menuconfig -> TQ Controller Config`:
|
||||
- `TQ_WIFI_SSID`
|
||||
- `TQ_WIFI_PASSWORD`
|
||||
- `TQ_HTTP_PORT`
|
||||
- `TQ_API_KEY` (optional)
|
||||
- Voice WebSocket auth and app fields:
|
||||
- `TQ_VOICE_API_KEY`
|
||||
- `TQ_VOICE_WORKSPACE_ID`
|
||||
- `TQ_VOICE_APP_ID`
|
||||
- Voice audio and codec fields:
|
||||
- `TQ_VOICE_SAMPLE_RATE`
|
||||
- `TQ_VOICE_OPUS_FRAME_MS`
|
||||
- `TQ_VOICE_OPUS_BITRATE_KBPS`
|
||||
- `TQ_VOICE_I2C_*` / `TQ_VOICE_I2S_*`
|
||||
- `TQ_VOICE_CODEC_*`
|
||||
|
||||
When `LYF_API_KEY` is not empty, each request must include:
|
||||
When `TQ_API_KEY` is not empty, each request must include:
|
||||
|
||||
```text
|
||||
X-API-Key: <your-key>
|
||||
@@ -117,7 +137,9 @@ curl http://<esp-ip>/v1/health
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/printer/connect \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name":"lyfPrinter","timeout_ms":15000}'
|
||||
-d '{"name":"TQPrinter","timeout_ms":15000}'
|
||||
# Legacy device name is also supported:
|
||||
# -d '{"name":"lyfPrinter","timeout_ms":15000}'
|
||||
```
|
||||
|
||||
Auto-match a compatible printer (by BLE service `0xFFF0`):
|
||||
@@ -170,7 +192,7 @@ curl -X POST http://<esp-ip>/v1/print/qr \
|
||||
curl -X POST http://<esp-ip>/v1/print/text \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"text":"欢迎使用LYF打印机\\n订单号: A1024\\n谢谢惠顾",
|
||||
"text":"欢迎使用TQ打印机\\n订单号: A1024\\n谢谢惠顾",
|
||||
"density":"中等",
|
||||
"scale":2,
|
||||
"line_spacing":2,
|
||||
@@ -183,7 +205,7 @@ curl -X POST http://<esp-ip>/v1/print/text \
|
||||
curl -X POST http://<esp-ip>/v1/print/receipt \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"title":"LYF FOOD",
|
||||
"title":"TQ FOOD",
|
||||
"density":"中等",
|
||||
"footer":"Thanks!",
|
||||
"items":[
|
||||
@@ -248,6 +270,37 @@ curl -X POST http://<esp-ip>/v1/ota/upgrade \
|
||||
}'
|
||||
```
|
||||
|
||||
### Voice tap2talk
|
||||
Start session:
|
||||
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/voice/session/start
|
||||
```
|
||||
|
||||
Start one tap2talk round:
|
||||
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/voice/tap/start
|
||||
```
|
||||
|
||||
Cancel current tap2talk round:
|
||||
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/voice/tap/cancel
|
||||
```
|
||||
|
||||
Get voice status:
|
||||
|
||||
```bash
|
||||
curl http://<esp-ip>/v1/voice/status
|
||||
```
|
||||
|
||||
Stop session:
|
||||
|
||||
```bash
|
||||
curl -X POST http://<esp-ip>/v1/voice/session/stop
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- BLE side is central/client role, not printer peripheral role.
|
||||
@@ -257,6 +310,7 @@ curl -X POST http://<esp-ip>/v1/ota/upgrade \
|
||||
- `/v1/print/text` and `/v1/print/receipt` now support UTF-8 Chinese via embedded 16x16 GB2312 glyphs.
|
||||
- Characters outside embedded glyph set are rendered as square fallback boxes.
|
||||
- QR encoding uses embedded `qrcodegen` (Project Nayuki C implementation).
|
||||
- Tap2talk uses server-side VAD and keeps uploading 100ms raw-opus frames in `Listening` state (silence when user is not tapping), matching `docs/websocket.md` requirements.
|
||||
|
||||
## Font Assets
|
||||
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
dependencies:
|
||||
espressif/esp_audio_codec:
|
||||
component_hash: af4663830bc73eca58febcc34a976d003465e78ed57458aa00a14b231e392ca1
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.4'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 2.4.1
|
||||
espressif/esp_codec_dev:
|
||||
component_hash: 0d9e9bc288156eb55f79338d312e1ebf8c9dfbd5e7d13ef0f20ccb031b4e15cf
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.5.4
|
||||
espressif/esp_jpeg:
|
||||
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
|
||||
dependencies:
|
||||
@@ -9,13 +29,26 @@ dependencies:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.3.1
|
||||
espressif/esp_websocket_client:
|
||||
component_hash: c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.6.1
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.5.2
|
||||
direct_dependencies:
|
||||
- espressif/esp_audio_codec
|
||||
- espressif/esp_codec_dev
|
||||
- espressif/esp_jpeg
|
||||
- espressif/esp_websocket_client
|
||||
- idf
|
||||
manifest_hash: 819afd83349fcea09da843d8b32ebcda85f77da70a3c32f07fb7fb08134db918
|
||||
manifest_hash: f8a6610cac00e11ee3cf43d68dd430fb8e5b79dd269b8576ed6f8f010cabeaca
|
||||
target: esp32s3
|
||||
version: 2.0.0
|
||||
|
||||
1164
docs/websocket.md
Normal file
1164
docs/websocket.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,17 @@ idf_component_register(
|
||||
"control_plane/src/rest_server_ops.c"
|
||||
"control_plane/src/rest_server_print.c"
|
||||
"control_plane/src/rest_server_jobs.c"
|
||||
"control_plane/src/rest_server_voice.c"
|
||||
"domain/src/printer_protocol.c"
|
||||
"domain/src/printer_protocol_commands.c"
|
||||
"domain/src/raster_tools.c"
|
||||
"domain/src/raster_tools_image_qr.c"
|
||||
"domain/src/system_runtime.c"
|
||||
"domain/src/voice_interaction.c"
|
||||
"platform/src/platform_bootstrap.c"
|
||||
"platform/src/wifi_manager.c"
|
||||
"platform/src/ble_printer_client.c"
|
||||
"platform/src/voice_audio.c"
|
||||
"third_party/qrcodegen.c"
|
||||
INCLUDE_DIRS
|
||||
"app_composition/include"
|
||||
@@ -36,6 +39,10 @@ idf_component_register(
|
||||
nvs_flash
|
||||
json
|
||||
mbedtls
|
||||
esp_websocket_client
|
||||
esp_codec_dev
|
||||
esp_audio_codec
|
||||
driver
|
||||
EMBED_FILES
|
||||
"domain/assets/fonts/cn16_index.bin"
|
||||
"domain/assets/fonts/cn16_glyphs.bin"
|
||||
|
||||
@@ -1,23 +1,140 @@
|
||||
menu "LYF Controller Config"
|
||||
menu "TQ Controller Config"
|
||||
|
||||
config LYF_WIFI_SSID
|
||||
config TQ_WIFI_SSID
|
||||
string "Wi-Fi SSID"
|
||||
default ""
|
||||
|
||||
config LYF_WIFI_PASSWORD
|
||||
config TQ_WIFI_PASSWORD
|
||||
string "Wi-Fi Password"
|
||||
default ""
|
||||
|
||||
config LYF_WIFI_MAXIMUM_RETRY
|
||||
config TQ_WIFI_MAXIMUM_RETRY
|
||||
int "Wi-Fi Maximum Retry"
|
||||
default 10
|
||||
|
||||
config LYF_HTTP_PORT
|
||||
config TQ_HTTP_PORT
|
||||
int "REST HTTP Port"
|
||||
default 80
|
||||
|
||||
config LYF_API_KEY
|
||||
config TQ_API_KEY
|
||||
string "REST API Key (optional, empty means disabled)"
|
||||
default ""
|
||||
|
||||
config TQ_VOICE_API_KEY
|
||||
string "Voice API Key for DashScope WebSocket (optional)"
|
||||
default ""
|
||||
|
||||
config TQ_VOICE_WS_URI
|
||||
string "Voice WebSocket URI"
|
||||
default "wss://dashscope.aliyuncs.com/api-ws/v1/inference"
|
||||
|
||||
config TQ_VOICE_WORKSPACE_ID
|
||||
string "Voice workspace_id"
|
||||
default ""
|
||||
|
||||
config TQ_VOICE_APP_ID
|
||||
string "Voice app_id"
|
||||
default ""
|
||||
|
||||
config TQ_VOICE_USER_ID
|
||||
string "Voice client user_id"
|
||||
default "esp32-user"
|
||||
|
||||
config TQ_VOICE_DEVICE_UUID
|
||||
string "Voice client device uuid (optional)"
|
||||
default ""
|
||||
|
||||
config TQ_VOICE_TTS_VOICE
|
||||
string "Voice TTS voice (optional)"
|
||||
default ""
|
||||
|
||||
config TQ_VOICE_SAMPLE_RATE
|
||||
int "Voice sample rate"
|
||||
range 8000 48000
|
||||
default 16000
|
||||
|
||||
config TQ_VOICE_OPUS_FRAME_MS
|
||||
int "Voice raw-opus frame size (ms)"
|
||||
range 10 120
|
||||
default 100
|
||||
|
||||
config TQ_VOICE_OPUS_BITRATE_KBPS
|
||||
int "Voice raw-opus bitrate (kbps)"
|
||||
range 6 128
|
||||
default 32
|
||||
|
||||
config TQ_VOICE_HEARTBEAT_SEC
|
||||
int "Voice heartbeat interval seconds"
|
||||
range 10 55
|
||||
default 50
|
||||
|
||||
config TQ_VOICE_I2C_PORT
|
||||
int "Voice I2C port"
|
||||
range 0 1
|
||||
default 0
|
||||
|
||||
config TQ_VOICE_I2C_SDA_PIN
|
||||
int "Voice I2C SDA GPIO"
|
||||
range 0 48
|
||||
default 17
|
||||
|
||||
config TQ_VOICE_I2C_SCL_PIN
|
||||
int "Voice I2C SCL GPIO"
|
||||
range 0 48
|
||||
default 18
|
||||
|
||||
config TQ_VOICE_CODEC_ADDR
|
||||
int "ES8311 I2C address"
|
||||
range 1 126
|
||||
default 48
|
||||
|
||||
config TQ_VOICE_I2S_PORT
|
||||
int "Voice I2S port"
|
||||
range 0 1
|
||||
default 0
|
||||
|
||||
config TQ_VOICE_I2S_MCLK_PIN
|
||||
int "Voice I2S MCLK GPIO"
|
||||
range -1 48
|
||||
default 16
|
||||
|
||||
config TQ_VOICE_I2S_BCLK_PIN
|
||||
int "Voice I2S BCLK GPIO"
|
||||
range 0 48
|
||||
default 9
|
||||
|
||||
config TQ_VOICE_I2S_WS_PIN
|
||||
int "Voice I2S WS GPIO"
|
||||
range 0 48
|
||||
default 45
|
||||
|
||||
config TQ_VOICE_I2S_DOUT_PIN
|
||||
int "Voice I2S DOUT GPIO"
|
||||
range 0 48
|
||||
default 8
|
||||
|
||||
config TQ_VOICE_I2S_DIN_PIN
|
||||
int "Voice I2S DIN GPIO"
|
||||
range 0 48
|
||||
default 10
|
||||
|
||||
config TQ_VOICE_CODEC_PA_PIN
|
||||
int "ES8311 PA control GPIO"
|
||||
range -1 48
|
||||
default 48
|
||||
|
||||
config TQ_VOICE_CODEC_PA_PIN_REVERSED
|
||||
bool "ES8311 PA enable level is active-low"
|
||||
default n
|
||||
|
||||
config TQ_VOICE_CODEC_OUT_VOL
|
||||
int "ES8311 output volume (0-100)"
|
||||
range 0 100
|
||||
default 70
|
||||
|
||||
config TQ_VOICE_CODEC_MIC_GAIN_DB
|
||||
int "ES8311 mic gain dB"
|
||||
range 0 42
|
||||
default 24
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -8,5 +8,5 @@ static const char *TAG = "app_main";
|
||||
void app_main(void) {
|
||||
ESP_ERROR_CHECK(controller_lifecycle_start());
|
||||
|
||||
ESP_LOGI(TAG, "LYF controller started");
|
||||
ESP_LOGI(TAG, "TQ controller started");
|
||||
}
|
||||
|
||||
@@ -44,3 +44,9 @@ esp_err_t rest_server_ota_jump_app_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_ota_erase_page_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_ota_write_frame_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_ota_upgrade_post(httpd_req_t *req);
|
||||
|
||||
esp_err_t rest_server_voice_status_get(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_session_start_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_session_stop_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_tap_start_post(httpd_req_t *req);
|
||||
esp_err_t rest_server_voice_tap_cancel_post(httpd_req_t *req);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "printer_protocol.h"
|
||||
#include "rest_server.h"
|
||||
#include "system_runtime.h"
|
||||
#include "voice_interaction.h"
|
||||
|
||||
static const char *TAG = "controller_lifecycle";
|
||||
|
||||
@@ -14,6 +15,9 @@ esp_err_t controller_lifecycle_start(void) {
|
||||
ESP_LOGI(TAG, "Starting printer protocol");
|
||||
ESP_ERROR_CHECK(printer_protocol_init());
|
||||
|
||||
ESP_LOGI(TAG, "Starting voice interaction");
|
||||
ESP_ERROR_CHECK(voice_interaction_init());
|
||||
|
||||
ESP_LOGI(TAG, "Starting REST server");
|
||||
ESP_ERROR_CHECK(rest_server_start());
|
||||
|
||||
@@ -22,6 +26,7 @@ esp_err_t controller_lifecycle_start(void) {
|
||||
|
||||
void controller_lifecycle_stop(void) {
|
||||
rest_server_stop();
|
||||
(void)voice_interaction_stop(NULL, 0);
|
||||
printer_protocol_disconnect();
|
||||
ESP_LOGI(TAG, "Controller lifecycle stopped");
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ static const char *s_web_index =
|
||||
"<head>\n"
|
||||
" <meta charset='utf-8'>\n"
|
||||
" <meta name='viewport' content='width=device-width,initial-scale=1'>\n"
|
||||
" <title>LYF Printer Controller</title>\n"
|
||||
" <title>TQ Printer Controller</title>\n"
|
||||
" <style>\n"
|
||||
" :root { color-scheme: light; }\n"
|
||||
" body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 16px; background:#f7f8fb; color:#1b2430; }\n"
|
||||
@@ -37,7 +37,7 @@ static const char *s_web_index =
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
" <div class='box'>\n"
|
||||
" <h1>LYF Printer Controller</h1>\n"
|
||||
" <h1>TQ Printer Controller</h1>\n"
|
||||
" <p>ESP32-S3 local control page. This page calls the onboard REST API directly.</p>\n"
|
||||
"\n"
|
||||
" <label>API Key (optional)</label>\n"
|
||||
@@ -50,7 +50,7 @@ static const char *s_web_index =
|
||||
" </div>\n"
|
||||
"\n"
|
||||
" <label>Printer Name</label>\n"
|
||||
" <input id='printerName' type='text' value='lyfPrinter'>\n"
|
||||
" <input id='printerName' type='text' value='TQPrinter'>\n"
|
||||
" <div class='row'>\n"
|
||||
" <button id='btnConnect'>Connect Printer</button>\n"
|
||||
" <button id='btnAutoConnect' class='alt'>Auto Connect</button>\n"
|
||||
@@ -206,7 +206,7 @@ static const char *s_web_index =
|
||||
" document.getElementById('btnJobs').onclick = () => run(() => callApi('/v1/jobs', 'GET'));\n"
|
||||
"\n"
|
||||
" document.getElementById('btnConnect').onclick = () => run(() => callApi('/v1/printer/connect', 'POST', {\n"
|
||||
" name: printerNameEl.value.trim() || 'lyfPrinter',\n"
|
||||
" name: printerNameEl.value.trim() || 'TQPrinter',\n"
|
||||
" timeout_ms: 15000\n"
|
||||
" }));\n"
|
||||
"\n"
|
||||
@@ -249,7 +249,7 @@ static esp_err_t favicon_get(httpd_req_t *req) {
|
||||
|
||||
esp_err_t rest_server_start(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = CONFIG_LYF_HTTP_PORT;
|
||||
config.server_port = CONFIG_TQ_HTTP_PORT;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
config.max_uri_handlers = 32;
|
||||
|
||||
@@ -301,6 +301,41 @@ esp_err_t rest_server_start(void) {
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &status);
|
||||
|
||||
httpd_uri_t voice_status = {
|
||||
.uri = "/v1/voice/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = rest_server_voice_status_get,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_status);
|
||||
|
||||
httpd_uri_t voice_session_start = {
|
||||
.uri = "/v1/voice/session/start",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_session_start_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_session_start);
|
||||
|
||||
httpd_uri_t voice_session_stop = {
|
||||
.uri = "/v1/voice/session/stop",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_session_stop_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_session_stop);
|
||||
|
||||
httpd_uri_t voice_tap_start = {
|
||||
.uri = "/v1/voice/tap/start",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_tap_start_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_tap_start);
|
||||
|
||||
httpd_uri_t voice_tap_cancel = {
|
||||
.uri = "/v1/voice/tap/cancel",
|
||||
.method = HTTP_POST,
|
||||
.handler = rest_server_voice_tap_cancel_post,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &voice_tap_cancel);
|
||||
|
||||
httpd_uri_t print_raster = {
|
||||
.uri = "/v1/print/raster",
|
||||
.method = HTTP_POST,
|
||||
@@ -434,7 +469,7 @@ esp_err_t rest_server_start(void) {
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &ota_upgrade);
|
||||
|
||||
ESP_LOGI(TAG, "REST server started on port %d", CONFIG_LYF_HTTP_PORT);
|
||||
ESP_LOGI(TAG, "REST server started on port %d", CONFIG_TQ_HTTP_PORT);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ static void *calloc_prefer_psram(size_t n, size_t size) {
|
||||
return ptr;
|
||||
}
|
||||
bool rest_server_auth_ok(httpd_req_t *req) {
|
||||
if (strlen(CONFIG_LYF_API_KEY) == 0) {
|
||||
if (strlen(CONFIG_TQ_API_KEY) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ bool rest_server_auth_ok(httpd_req_t *req) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strcmp(buf, CONFIG_LYF_API_KEY) == 0;
|
||||
return strcmp(buf, CONFIG_TQ_API_KEY) == 0;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_send_json(httpd_req_t *req, const char *status, cJSON *root) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "printer_protocol.h"
|
||||
#include "system_runtime.h"
|
||||
#include "voice_interaction.h"
|
||||
static void fill_runtime_json(cJSON *root) {
|
||||
char ip[32] = {0};
|
||||
system_runtime_get_ip(ip, sizeof(ip));
|
||||
@@ -26,6 +27,23 @@ static void fill_runtime_json(cJSON *root) {
|
||||
cJSON_AddNumberToObject(root, "queue_depth", st.queue_depth);
|
||||
cJSON_AddNumberToObject(root, "mtu", st.mtu);
|
||||
cJSON_AddNumberToObject(root, "last_status_ms", (double)st.last_status_ms);
|
||||
|
||||
voice_interaction_status_t voice = {0};
|
||||
voice_interaction_get_status(&voice);
|
||||
cJSON_AddBoolToObject(root, "voice_ready", voice.ready);
|
||||
cJSON_AddBoolToObject(root, "voice_session_active", voice.session_active);
|
||||
cJSON_AddBoolToObject(root, "voice_ws_connected", voice.ws_connected);
|
||||
cJSON_AddBoolToObject(root, "voice_started", voice.started);
|
||||
cJSON_AddBoolToObject(root, "voice_tap_active", voice.tap_active);
|
||||
cJSON_AddStringToObject(root,
|
||||
"voice_dialog_state",
|
||||
voice_interaction_dialog_state_str(voice.dialog_state));
|
||||
cJSON_AddStringToObject(root, "voice_task_id", voice.task_id);
|
||||
cJSON_AddStringToObject(root, "voice_dialog_id", voice.dialog_id);
|
||||
cJSON_AddStringToObject(root, "voice_last_error", voice.last_error);
|
||||
cJSON_AddNumberToObject(root, "voice_last_event_ms", (double)voice.last_event_ms);
|
||||
cJSON_AddNumberToObject(root, "voice_upstream_packets", (double)voice.upstream_packets);
|
||||
cJSON_AddNumberToObject(root, "voice_downstream_packets", (double)voice.downstream_packets);
|
||||
}
|
||||
|
||||
esp_err_t rest_server_health_get(httpd_req_t *req) {
|
||||
@@ -48,7 +66,7 @@ esp_err_t rest_server_connect_post(httpd_req_t *req) {
|
||||
}
|
||||
|
||||
char *body = NULL;
|
||||
char name[32] = "lyfPrinter";
|
||||
char name[32] = "TQPrinter";
|
||||
uint32_t timeout_ms = 15000;
|
||||
|
||||
if (req->content_len > 0) {
|
||||
|
||||
@@ -688,7 +688,7 @@ esp_err_t rest_server_print_receipt_post(httpd_req_t *req) {
|
||||
|
||||
const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL)
|
||||
? jtitle->valuestring
|
||||
: "LYF RECEIPT";
|
||||
: "TQ RECEIPT";
|
||||
const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL)
|
||||
? jfooter->valuestring
|
||||
: "Thank you";
|
||||
|
||||
167
main/control_plane/src/rest_server_voice.c
Normal file
167
main/control_plane/src/rest_server_voice.c
Normal file
@@ -0,0 +1,167 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "voice_interaction.h"
|
||||
|
||||
static void rest_server_add_voice_status(cJSON *root) {
|
||||
voice_interaction_status_t st = {0};
|
||||
voice_interaction_get_status(&st);
|
||||
|
||||
cJSON_AddBoolToObject(root, "voice_ready", st.ready);
|
||||
cJSON_AddBoolToObject(root, "voice_session_active", st.session_active);
|
||||
cJSON_AddBoolToObject(root, "voice_ws_connected", st.ws_connected);
|
||||
cJSON_AddBoolToObject(root, "voice_started", st.started);
|
||||
cJSON_AddBoolToObject(root, "voice_tap_active", st.tap_active);
|
||||
cJSON_AddStringToObject(root,
|
||||
"voice_dialog_state",
|
||||
voice_interaction_dialog_state_str(st.dialog_state));
|
||||
cJSON_AddStringToObject(root, "voice_task_id", st.task_id);
|
||||
cJSON_AddStringToObject(root, "voice_dialog_id", st.dialog_id);
|
||||
cJSON_AddStringToObject(root, "voice_last_error", st.last_error);
|
||||
cJSON_AddNumberToObject(root, "voice_last_event_ms", (double)st.last_event_ms);
|
||||
cJSON_AddNumberToObject(root, "voice_upstream_packets", (double)st.upstream_packets);
|
||||
cJSON_AddNumberToObject(root, "voice_downstream_packets", (double)st.downstream_packets);
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_status_get(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_session_start_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
if (req->content_len > 0) {
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
char start_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_start(start_err, sizeof(start_err));
|
||||
if (rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
start_err[0] != '\0' ? start_err : "voice session start failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "voice session started");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_session_stop_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
if (req->content_len > 0) {
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
char stop_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_stop(stop_err, sizeof(stop_err));
|
||||
if (rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
stop_err[0] != '\0' ? stop_err : "voice session stop failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "voice session stopped");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_tap_start_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
if (req->content_len > 0) {
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
char tap_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_tap_start(tap_err, sizeof(tap_err));
|
||||
if (rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
tap_err[0] != '\0' ? tap_err : "tap start failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "tap started");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_tap_cancel_post(httpd_req_t *req) {
|
||||
if (!rest_server_auth_ok(req)) {
|
||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||
}
|
||||
|
||||
if (req->content_len > 0) {
|
||||
char *body = NULL;
|
||||
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||
if (body_err != ESP_OK) {
|
||||
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
char tap_err[128] = {0};
|
||||
esp_err_t rc = voice_interaction_tap_cancel(tap_err, sizeof(tap_err));
|
||||
if (rc != ESP_OK) {
|
||||
return rest_server_send_error(req,
|
||||
"409 Conflict",
|
||||
tap_err[0] != '\0' ? tap_err : "tap cancel failed");
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "tap canceled");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
cJSON_Delete(root);
|
||||
return err;
|
||||
}
|
||||
39
main/domain/include/voice_interaction.h
Normal file
39
main/domain/include/voice_interaction.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef enum {
|
||||
VOICE_DIALOG_STATE_IDLE = 0,
|
||||
VOICE_DIALOG_STATE_LISTENING,
|
||||
VOICE_DIALOG_STATE_THINKING,
|
||||
VOICE_DIALOG_STATE_RESPONDING,
|
||||
} voice_dialog_state_t;
|
||||
|
||||
typedef struct {
|
||||
bool ready;
|
||||
bool session_active;
|
||||
bool ws_connected;
|
||||
bool started;
|
||||
bool tap_active;
|
||||
voice_dialog_state_t dialog_state;
|
||||
char task_id[40];
|
||||
char dialog_id[40];
|
||||
char last_error[128];
|
||||
int64_t last_event_ms;
|
||||
uint32_t upstream_packets;
|
||||
uint32_t downstream_packets;
|
||||
} voice_interaction_status_t;
|
||||
|
||||
esp_err_t voice_interaction_init(void);
|
||||
esp_err_t voice_interaction_start(char *err, size_t err_len);
|
||||
esp_err_t voice_interaction_stop(char *err, size_t err_len);
|
||||
|
||||
esp_err_t voice_interaction_tap_start(char *err, size_t err_len);
|
||||
esp_err_t voice_interaction_tap_cancel(char *err, size_t err_len);
|
||||
|
||||
void voice_interaction_get_status(voice_interaction_status_t *out_status);
|
||||
const char *voice_interaction_dialog_state_str(voice_dialog_state_t state);
|
||||
1236
main/domain/src/voice_interaction.c
Normal file
1236
main/domain/src/voice_interaction.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,3 +15,6 @@ dependencies:
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
||||
espressif/esp_jpeg: ^1.3.1
|
||||
espressif/esp_websocket_client: "^1.6.1"
|
||||
espressif/esp_codec_dev: "^1.5.4"
|
||||
espressif/esp_audio_codec: ^2.4.1
|
||||
31
main/platform/include/voice_audio.h
Normal file
31
main/platform/include/voice_audio.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t sample_rate;
|
||||
uint8_t channels;
|
||||
uint8_t bits_per_sample;
|
||||
int output_volume;
|
||||
float input_gain_db;
|
||||
} voice_audio_open_config_t;
|
||||
|
||||
esp_err_t voice_audio_open(const voice_audio_open_config_t *cfg, char *err, size_t err_len);
|
||||
void voice_audio_close(void);
|
||||
|
||||
bool voice_audio_is_open(void);
|
||||
|
||||
esp_err_t voice_audio_read_pcm(int16_t *pcm,
|
||||
size_t samples,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len);
|
||||
esp_err_t voice_audio_write_pcm(const int16_t *pcm,
|
||||
size_t samples,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len);
|
||||
@@ -21,6 +21,8 @@
|
||||
#define PRINTER_NOTIFY_UUID16 0xFFF1
|
||||
#define PRINTER_WRITE_UUID16 0xFFF2
|
||||
#define CCCD_UUID16 0x2902
|
||||
#define PRINTER_NAME_PRIMARY "TQPrinter"
|
||||
#define PRINTER_NAME_LEGACY "lyfPrinter"
|
||||
|
||||
#define EVT_CONNECTED BIT0
|
||||
#define EVT_READY BIT1
|
||||
@@ -35,7 +37,7 @@ static ble_frame_rx_cb_t s_rx_cb;
|
||||
|
||||
static uint8_t s_addr_type;
|
||||
static ble_addr_t s_target_addr;
|
||||
static char s_target_name[32] = "lyfPrinter";
|
||||
static char s_target_name[32] = PRINTER_NAME_PRIMARY;
|
||||
static bool s_match_any_compatible;
|
||||
|
||||
static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
@@ -117,6 +119,21 @@ static bool normalized_name_match(const char *adv_name, const char *target_name)
|
||||
return (strstr(adv, target) != NULL) || (strstr(target, adv) != NULL);
|
||||
}
|
||||
|
||||
static bool is_known_printer_name(const char *name) {
|
||||
return normalized_name_match(name, PRINTER_NAME_PRIMARY) ||
|
||||
normalized_name_match(name, PRINTER_NAME_LEGACY);
|
||||
}
|
||||
|
||||
static bool target_name_match_with_alias(const char *adv_name, const char *target_name) {
|
||||
if (normalized_name_match(adv_name, target_name)) {
|
||||
return true;
|
||||
}
|
||||
if (!is_known_printer_name(target_name)) {
|
||||
return false;
|
||||
}
|
||||
return is_known_printer_name(adv_name);
|
||||
}
|
||||
|
||||
static void reset_discovery_state(void) {
|
||||
s_service_start = 0;
|
||||
s_service_end = 0;
|
||||
@@ -347,7 +364,7 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg) {
|
||||
|
||||
if (s_match_any_compatible) {
|
||||
bool uuid_match = adv_has_uuid16(&fields, PRINTER_SERVICE_UUID16);
|
||||
bool name_match = has_name && normalized_name_match(name, "lyfPrinter");
|
||||
bool name_match = has_name && is_known_printer_name(name);
|
||||
if (!uuid_match && !name_match) {
|
||||
return 0;
|
||||
}
|
||||
@@ -357,7 +374,7 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg) {
|
||||
event->disc.rssi,
|
||||
uuid_match ? 1 : 0);
|
||||
} else {
|
||||
if (!has_name || !normalized_name_match(name, s_target_name)) {
|
||||
if (!has_name || !target_name_match_with_alias(name, s_target_name)) {
|
||||
return 0;
|
||||
}
|
||||
ESP_LOGI(TAG, "Found target device '%s' RSSI=%d", name, event->disc.rssi);
|
||||
|
||||
362
main/platform/src/voice_audio.c
Normal file
362
main/platform/src/voice_audio.c
Normal file
@@ -0,0 +1,362 @@
|
||||
#include "voice_audio.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "audio_codec_if.h"
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_codec_dev.h"
|
||||
#include "esp_codec_dev_defaults.h"
|
||||
#include "esp_idf_version.h"
|
||||
#include "esp_log.h"
|
||||
#include "es8311_codec.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) && !defined(CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE)
|
||||
#include "driver/i2c_master.h"
|
||||
#define VOICE_USE_IDF_I2C_MASTER
|
||||
#else
|
||||
#include "driver/i2c.h"
|
||||
#endif
|
||||
|
||||
static const char *TAG = "voice_audio";
|
||||
|
||||
#ifdef CONFIG_TQ_VOICE_CODEC_PA_PIN_REVERSED
|
||||
#define VOICE_CODEC_PA_REVERSED true
|
||||
#else
|
||||
#define VOICE_CODEC_PA_REVERSED false
|
||||
#endif
|
||||
|
||||
static SemaphoreHandle_t s_lock;
|
||||
|
||||
#ifdef VOICE_USE_IDF_I2C_MASTER
|
||||
static i2c_master_bus_handle_t s_i2c_bus;
|
||||
#endif
|
||||
|
||||
static i2s_chan_handle_t s_i2s_tx;
|
||||
static i2s_chan_handle_t s_i2s_rx;
|
||||
|
||||
static const audio_codec_ctrl_if_t *s_codec_ctrl_if;
|
||||
static const audio_codec_gpio_if_t *s_codec_gpio_if;
|
||||
static const audio_codec_data_if_t *s_codec_data_if;
|
||||
static const audio_codec_if_t *s_codec_if;
|
||||
static esp_codec_dev_handle_t s_codec_dev;
|
||||
|
||||
static bool s_initialized;
|
||||
static bool s_opened;
|
||||
static uint32_t s_sample_rate;
|
||||
static uint8_t s_channels;
|
||||
static uint8_t s_bits_per_sample;
|
||||
|
||||
static void fill_err(char *err, size_t err_len, const char *msg) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "%s", msg != NULL ? msg : "unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t voice_audio_init_i2c(void) {
|
||||
#ifdef VOICE_USE_IDF_I2C_MASTER
|
||||
if (s_i2c_bus != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
i2c_master_bus_config_t bus_cfg = {
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.i2c_port = CONFIG_TQ_VOICE_I2C_PORT,
|
||||
.sda_io_num = CONFIG_TQ_VOICE_I2C_SDA_PIN,
|
||||
.scl_io_num = CONFIG_TQ_VOICE_I2C_SCL_PIN,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
|
||||
return i2c_new_master_bus(&bus_cfg, &s_i2c_bus);
|
||||
#else
|
||||
i2c_config_t i2c_cfg = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = CONFIG_TQ_VOICE_I2C_SDA_PIN,
|
||||
.scl_io_num = CONFIG_TQ_VOICE_I2C_SCL_PIN,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed = 100000,
|
||||
};
|
||||
|
||||
ESP_RETURN_ON_ERROR(i2c_param_config(CONFIG_TQ_VOICE_I2C_PORT, &i2c_cfg), TAG, "i2c_param_config failed");
|
||||
return i2c_driver_install(CONFIG_TQ_VOICE_I2C_PORT, i2c_cfg.mode, 0, 0, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
static esp_err_t voice_audio_init_i2s(uint32_t sample_rate) {
|
||||
if (s_i2s_tx != NULL && s_i2s_rx != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)CONFIG_TQ_VOICE_I2S_PORT,
|
||||
I2S_ROLE_MASTER);
|
||||
chan_cfg.dma_desc_num = 6;
|
||||
chan_cfg.dma_frame_num = 240;
|
||||
|
||||
ESP_RETURN_ON_ERROR(i2s_new_channel(&chan_cfg, &s_i2s_tx, &s_i2s_rx), TAG, "i2s_new_channel failed");
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_rate),
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
|
||||
.gpio_cfg = {
|
||||
.mclk = CONFIG_TQ_VOICE_I2S_MCLK_PIN,
|
||||
.bclk = CONFIG_TQ_VOICE_I2S_BCLK_PIN,
|
||||
.ws = CONFIG_TQ_VOICE_I2S_WS_PIN,
|
||||
.dout = CONFIG_TQ_VOICE_I2S_DOUT_PIN,
|
||||
.din = CONFIG_TQ_VOICE_I2S_DIN_PIN,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
esp_err_t err = i2s_channel_init_std_mode(s_i2s_tx, &std_cfg);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
err = i2s_channel_init_std_mode(s_i2s_rx, &std_cfg);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(i2s_channel_enable(s_i2s_tx), TAG, "i2s tx enable failed");
|
||||
ESP_RETURN_ON_ERROR(i2s_channel_enable(s_i2s_rx), TAG, "i2s rx enable failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t voice_audio_build_codec_dev(void) {
|
||||
if (s_codec_dev != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
audio_codec_i2c_cfg_t i2c_cfg = {
|
||||
.port = (uint8_t)CONFIG_TQ_VOICE_I2C_PORT,
|
||||
.addr = (uint8_t)CONFIG_TQ_VOICE_CODEC_ADDR,
|
||||
#ifdef VOICE_USE_IDF_I2C_MASTER
|
||||
.bus_handle = s_i2c_bus,
|
||||
#endif
|
||||
};
|
||||
|
||||
s_codec_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
|
||||
if (s_codec_ctrl_if == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
audio_codec_i2s_cfg_t i2s_cfg = {
|
||||
.port = (uint8_t)CONFIG_TQ_VOICE_I2S_PORT,
|
||||
.rx_handle = s_i2s_rx,
|
||||
.tx_handle = s_i2s_tx,
|
||||
};
|
||||
|
||||
s_codec_data_if = audio_codec_new_i2s_data(&i2s_cfg);
|
||||
if (s_codec_data_if == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_codec_gpio_if = audio_codec_new_gpio();
|
||||
if (s_codec_gpio_if == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
es8311_codec_cfg_t codec_cfg = {
|
||||
.ctrl_if = s_codec_ctrl_if,
|
||||
.gpio_if = s_codec_gpio_if,
|
||||
.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH,
|
||||
.pa_pin = CONFIG_TQ_VOICE_CODEC_PA_PIN,
|
||||
.pa_reverted = VOICE_CODEC_PA_REVERSED,
|
||||
.master_mode = false,
|
||||
.use_mclk = (CONFIG_TQ_VOICE_I2S_MCLK_PIN >= 0),
|
||||
.digital_mic = false,
|
||||
.invert_mclk = false,
|
||||
.invert_sclk = false,
|
||||
};
|
||||
|
||||
s_codec_if = es8311_codec_new(&codec_cfg);
|
||||
if (s_codec_if == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
esp_codec_dev_cfg_t dev_cfg = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
|
||||
.codec_if = s_codec_if,
|
||||
.data_if = s_codec_data_if,
|
||||
};
|
||||
|
||||
s_codec_dev = esp_codec_dev_new(&dev_cfg);
|
||||
if (s_codec_dev == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t voice_audio_prepare(const voice_audio_open_config_t *cfg) {
|
||||
ESP_RETURN_ON_ERROR(voice_audio_init_i2c(), TAG, "i2c init failed");
|
||||
ESP_RETURN_ON_ERROR(voice_audio_init_i2s(cfg->sample_rate), TAG, "i2s init failed");
|
||||
ESP_RETURN_ON_ERROR(voice_audio_build_codec_dev(), TAG, "codec dev init failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t voice_audio_open(const voice_audio_open_config_t *cfg, char *err, size_t err_len) {
|
||||
if (cfg == NULL || cfg->sample_rate == 0 || cfg->channels == 0 || cfg->bits_per_sample != 16) {
|
||||
fill_err(err, err_len, "invalid voice audio config");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (s_lock == NULL) {
|
||||
s_lock = xSemaphoreCreateMutex();
|
||||
if (s_lock == NULL) {
|
||||
fill_err(err, err_len, "create mutex failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
|
||||
fill_err(err, err_len, "voice audio lock timeout");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (s_opened) {
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t init_err = voice_audio_prepare(cfg);
|
||||
if (init_err != ESP_OK) {
|
||||
fill_err(err, err_len, "voice audio peripheral init failed");
|
||||
xSemaphoreGive(s_lock);
|
||||
return init_err;
|
||||
}
|
||||
|
||||
if (s_initialized && (s_sample_rate != cfg->sample_rate ||
|
||||
s_channels != cfg->channels ||
|
||||
s_bits_per_sample != cfg->bits_per_sample)) {
|
||||
fill_err(err, err_len, "voice audio format changed after init");
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.sample_rate = cfg->sample_rate,
|
||||
.channel = cfg->channels,
|
||||
.bits_per_sample = cfg->bits_per_sample,
|
||||
};
|
||||
|
||||
int rc = esp_codec_dev_open(s_codec_dev, &fs);
|
||||
if (rc != ESP_CODEC_DEV_OK) {
|
||||
fill_err(err, err_len, "esp_codec_dev_open failed");
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
(void)esp_codec_dev_set_out_vol(s_codec_dev, cfg->output_volume);
|
||||
(void)esp_codec_dev_set_in_gain(s_codec_dev, cfg->input_gain_db);
|
||||
(void)esp_codec_dev_set_out_mute(s_codec_dev, false);
|
||||
(void)esp_codec_dev_set_in_mute(s_codec_dev, false);
|
||||
|
||||
s_sample_rate = cfg->sample_rate;
|
||||
s_channels = cfg->channels;
|
||||
s_bits_per_sample = cfg->bits_per_sample;
|
||||
s_initialized = true;
|
||||
s_opened = true;
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"voice audio opened: sr=%u ch=%u bits=%u",
|
||||
(unsigned)s_sample_rate,
|
||||
(unsigned)s_channels,
|
||||
(unsigned)s_bits_per_sample);
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void voice_audio_close(void) {
|
||||
if (s_lock == NULL) {
|
||||
return;
|
||||
}
|
||||
if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_opened) {
|
||||
(void)esp_codec_dev_set_in_mute(s_codec_dev, true);
|
||||
(void)esp_codec_dev_set_out_mute(s_codec_dev, true);
|
||||
(void)esp_codec_dev_close(s_codec_dev);
|
||||
s_opened = false;
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
}
|
||||
|
||||
bool voice_audio_is_open(void) {
|
||||
return s_opened;
|
||||
}
|
||||
|
||||
esp_err_t voice_audio_read_pcm(int16_t *pcm,
|
||||
size_t samples,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
if (pcm == NULL || samples == 0) {
|
||||
fill_err(err, err_len, "invalid read args");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (s_lock == NULL || xSemaphoreTake(s_lock, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) {
|
||||
fill_err(err, err_len, "voice read lock timeout");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (!s_opened) {
|
||||
fill_err(err, err_len, "voice audio not opened");
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
int rc = esp_codec_dev_read(s_codec_dev, pcm, (int)(samples * sizeof(int16_t)));
|
||||
xSemaphoreGive(s_lock);
|
||||
if (rc != ESP_CODEC_DEV_OK) {
|
||||
fill_err(err, err_len, "codec read failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t voice_audio_write_pcm(const int16_t *pcm,
|
||||
size_t samples,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
if (pcm == NULL || samples == 0) {
|
||||
fill_err(err, err_len, "invalid write args");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (s_lock == NULL || xSemaphoreTake(s_lock, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) {
|
||||
fill_err(err, err_len, "voice write lock timeout");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (!s_opened) {
|
||||
fill_err(err, err_len, "voice audio not opened");
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
int rc = esp_codec_dev_write(s_codec_dev, (void *)pcm, (int)(samples * sizeof(int16_t)));
|
||||
xSemaphoreGive(s_lock);
|
||||
if (rc != ESP_CODEC_DEV_OK) {
|
||||
fill_err(err, err_len, "codec write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -32,10 +32,10 @@ static void wifi_event_handler(void *arg,
|
||||
}
|
||||
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
if (s_retry_num < CONFIG_LYF_WIFI_MAXIMUM_RETRY) {
|
||||
if (s_retry_num < CONFIG_TQ_WIFI_MAXIMUM_RETRY) {
|
||||
esp_wifi_connect();
|
||||
s_retry_num++;
|
||||
ESP_LOGW(TAG, "retry to connect to the AP (%d/%d)", s_retry_num, CONFIG_LYF_WIFI_MAXIMUM_RETRY);
|
||||
ESP_LOGW(TAG, "retry to connect to the AP (%d/%d)", s_retry_num, CONFIG_TQ_WIFI_MAXIMUM_RETRY);
|
||||
} else {
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
}
|
||||
@@ -53,13 +53,13 @@ static void wifi_event_handler(void *arg,
|
||||
|
||||
static esp_err_t start_sta_mode(void) {
|
||||
wifi_config_t wifi_config = {0};
|
||||
if (strlen(CONFIG_LYF_WIFI_SSID) == 0) {
|
||||
ESP_LOGE(TAG, "CONFIG_LYF_WIFI_SSID is empty; STA-only mode requires valid SSID");
|
||||
if (strlen(CONFIG_TQ_WIFI_SSID) == 0) {
|
||||
ESP_LOGE(TAG, "CONFIG_TQ_WIFI_SSID is empty; STA-only mode requires valid SSID");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
strlcpy((char *)wifi_config.sta.ssid, CONFIG_LYF_WIFI_SSID, sizeof(wifi_config.sta.ssid));
|
||||
strlcpy((char *)wifi_config.sta.password, CONFIG_LYF_WIFI_PASSWORD, sizeof(wifi_config.sta.password));
|
||||
strlcpy((char *)wifi_config.sta.ssid, CONFIG_TQ_WIFI_SSID, sizeof(wifi_config.sta.ssid));
|
||||
strlcpy((char *)wifi_config.sta.password, CONFIG_TQ_WIFI_PASSWORD, sizeof(wifi_config.sta.password));
|
||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
wifi_config.sta.pmf_cfg.capable = true;
|
||||
wifi_config.sta.pmf_cfg.required = false;
|
||||
@@ -77,13 +77,13 @@ static esp_err_t start_sta_mode(void) {
|
||||
pdMS_TO_TICKS(15000));
|
||||
|
||||
if (bits & WIFI_CONNECTED_BIT) {
|
||||
ESP_LOGI(TAG, "connected to AP SSID:%s", CONFIG_LYF_WIFI_SSID);
|
||||
ESP_LOGI(TAG, "connected to AP SSID:%s", CONFIG_TQ_WIFI_SSID);
|
||||
s_ready = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (bits & WIFI_FAIL_BIT) {
|
||||
ESP_LOGW(TAG, "Failed to connect to SSID:%s", CONFIG_LYF_WIFI_SSID);
|
||||
ESP_LOGW(TAG, "Failed to connect to SSID:%s", CONFIG_TQ_WIFI_SSID);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
|
||||
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247
|
||||
# CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set
|
||||
CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n
|
||||
CONFIG_LYF_WIFI_SSID="TalkingQ"
|
||||
CONFIG_LYF_WIFI_PASSWORD="TalkingQ123"
|
||||
CONFIG_TQ_WIFI_SSID="TalkingQ"
|
||||
CONFIG_TQ_WIFI_PASSWORD="TalkingQ123"
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
|
||||
@@ -20,3 +20,27 @@ CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_SPIRAM_BOOT_INIT=y
|
||||
CONFIG_SPIRAM_USE_MALLOC=y
|
||||
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
|
||||
CONFIG_TQ_VOICE_API_KEY=""
|
||||
CONFIG_TQ_VOICE_WS_URI="wss://dashscope.aliyuncs.com/api-ws/v1/inference"
|
||||
CONFIG_TQ_VOICE_WORKSPACE_ID=""
|
||||
CONFIG_TQ_VOICE_APP_ID=""
|
||||
CONFIG_TQ_VOICE_USER_ID="esp32-user"
|
||||
CONFIG_TQ_VOICE_DEVICE_UUID=""
|
||||
CONFIG_TQ_VOICE_TTS_VOICE=""
|
||||
CONFIG_TQ_VOICE_SAMPLE_RATE=16000
|
||||
CONFIG_TQ_VOICE_OPUS_FRAME_MS=100
|
||||
CONFIG_TQ_VOICE_OPUS_BITRATE_KBPS=32
|
||||
CONFIG_TQ_VOICE_HEARTBEAT_SEC=50
|
||||
CONFIG_TQ_VOICE_I2C_PORT=0
|
||||
CONFIG_TQ_VOICE_I2C_SDA_PIN=17
|
||||
CONFIG_TQ_VOICE_I2C_SCL_PIN=18
|
||||
CONFIG_TQ_VOICE_CODEC_ADDR=48
|
||||
CONFIG_TQ_VOICE_I2S_PORT=0
|
||||
CONFIG_TQ_VOICE_I2S_MCLK_PIN=16
|
||||
CONFIG_TQ_VOICE_I2S_BCLK_PIN=9
|
||||
CONFIG_TQ_VOICE_I2S_WS_PIN=45
|
||||
CONFIG_TQ_VOICE_I2S_DOUT_PIN=8
|
||||
CONFIG_TQ_VOICE_I2S_DIN_PIN=10
|
||||
CONFIG_TQ_VOICE_CODEC_PA_PIN=48
|
||||
CONFIG_TQ_VOICE_CODEC_OUT_VOL=70
|
||||
CONFIG_TQ_VOICE_CODEC_MIC_GAIN_DB=24
|
||||
|
||||
Reference in New Issue
Block a user