多模态

This commit is contained in:
admin
2026-02-24 21:36:14 +08:00
parent e1e0ac8929
commit 68840e6ea4
23 changed files with 3368 additions and 44 deletions

View File

@@ -1,5 +1,11 @@
{ {
"idf.currentSetup": "/Users/moyyang/esp/v5.5.2/esp-idf", "idf.currentSetup": "/Users/moyyang/esp/v5.5.2/esp-idf",
"idf.flashType": "UART", "idf.flashType": "UART",
"idf.port": "/dev/tty.wchusbserial5B0B0251771" "idf.port": "/dev/tty.wchusbserial5B0B0251771",
"idf.openOcdConfigs": [
"board/esp32s3-builtin.cfg"
],
"idf.customExtraVars": {
"IDF_TARGET": "esp32s3"
}
} }

View File

@@ -1,3 +1,3 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(lyf_printer_controller) project(TQ_printer_controller)

View File

@@ -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: ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
- REST client -> ESP32-S3 (`esp_http_server`) - 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: - Command compatibility:
- Print path: `0x00~0x07` - Print path: `0x00~0x07`
- OTA path: `0xA0~0xA4` - 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`) - Async print queue with jobs (`queued/running/success/failed/canceled`)
- Printer precheck (paper / battery / temperature) - Printer precheck (paper / battery / temperature)
- Built-in UTF-8 text rendering with Chinese support (GB2312 character set, 16x16 bitmap) - 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: - Android-compatible print flow:
- `0x00` power on - `0x00` power on
- `0x03` set print param - `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/erase_page`
- `POST /v1/ota/write_frame` - `POST /v1/ota/write_frame`
- `POST /v1/ota/upgrade` - `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 ## Build
@@ -66,13 +76,23 @@ idf.py build
## Config ## Config
In `menuconfig -> LYF Controller Config`: In `menuconfig -> TQ Controller Config`:
- `LYF_WIFI_SSID` - `TQ_WIFI_SSID`
- `LYF_WIFI_PASSWORD` - `TQ_WIFI_PASSWORD`
- `LYF_HTTP_PORT` - `TQ_HTTP_PORT`
- `LYF_API_KEY` (optional) - `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 ```text
X-API-Key: <your-key> X-API-Key: <your-key>
@@ -117,7 +137,9 @@ curl http://<esp-ip>/v1/health
```bash ```bash
curl -X POST http://<esp-ip>/v1/printer/connect \ curl -X POST http://<esp-ip>/v1/printer/connect \
-H 'Content-Type: application/json' \ -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`): 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 \ curl -X POST http://<esp-ip>/v1/print/text \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{ -d '{
"text":"欢迎使用LYF打印机\\n订单号: A1024\\n谢谢惠顾", "text":"欢迎使用TQ打印机\\n订单号: A1024\\n谢谢惠顾",
"density":"中等", "density":"中等",
"scale":2, "scale":2,
"line_spacing":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 \ curl -X POST http://<esp-ip>/v1/print/receipt \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{ -d '{
"title":"LYF FOOD", "title":"TQ FOOD",
"density":"中等", "density":"中等",
"footer":"Thanks!", "footer":"Thanks!",
"items":[ "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 ## Notes
- BLE side is central/client role, not printer peripheral role. - 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. - `/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. - Characters outside embedded glyph set are rendered as square fallback boxes.
- QR encoding uses embedded `qrcodegen` (Project Nayuki C implementation). - 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 ## Font Assets

View File

@@ -1,4 +1,24 @@
dependencies: 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: espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7 component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies: dependencies:
@@ -9,13 +29,26 @@ dependencies:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com/
type: service type: service
version: 1.3.1 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: idf:
source: source:
type: idf type: idf
version: 5.5.2 version: 5.5.2
direct_dependencies: direct_dependencies:
- espressif/esp_audio_codec
- espressif/esp_codec_dev
- espressif/esp_jpeg - espressif/esp_jpeg
- espressif/esp_websocket_client
- idf - idf
manifest_hash: 819afd83349fcea09da843d8b32ebcda85f77da70a3c32f07fb7fb08134db918 manifest_hash: f8a6610cac00e11ee3cf43d68dd430fb8e5b79dd269b8576ed6f8f010cabeaca
target: esp32s3 target: esp32s3
version: 2.0.0 version: 2.0.0

1164
docs/websocket.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,14 +7,17 @@ idf_component_register(
"control_plane/src/rest_server_ops.c" "control_plane/src/rest_server_ops.c"
"control_plane/src/rest_server_print.c" "control_plane/src/rest_server_print.c"
"control_plane/src/rest_server_jobs.c" "control_plane/src/rest_server_jobs.c"
"control_plane/src/rest_server_voice.c"
"domain/src/printer_protocol.c" "domain/src/printer_protocol.c"
"domain/src/printer_protocol_commands.c" "domain/src/printer_protocol_commands.c"
"domain/src/raster_tools.c" "domain/src/raster_tools.c"
"domain/src/raster_tools_image_qr.c" "domain/src/raster_tools_image_qr.c"
"domain/src/system_runtime.c" "domain/src/system_runtime.c"
"domain/src/voice_interaction.c"
"platform/src/platform_bootstrap.c" "platform/src/platform_bootstrap.c"
"platform/src/wifi_manager.c" "platform/src/wifi_manager.c"
"platform/src/ble_printer_client.c" "platform/src/ble_printer_client.c"
"platform/src/voice_audio.c"
"third_party/qrcodegen.c" "third_party/qrcodegen.c"
INCLUDE_DIRS INCLUDE_DIRS
"app_composition/include" "app_composition/include"
@@ -36,6 +39,10 @@ idf_component_register(
nvs_flash nvs_flash
json json
mbedtls mbedtls
esp_websocket_client
esp_codec_dev
esp_audio_codec
driver
EMBED_FILES EMBED_FILES
"domain/assets/fonts/cn16_index.bin" "domain/assets/fonts/cn16_index.bin"
"domain/assets/fonts/cn16_glyphs.bin" "domain/assets/fonts/cn16_glyphs.bin"

View File

@@ -1,23 +1,140 @@
menu "LYF Controller Config" menu "TQ Controller Config"
config LYF_WIFI_SSID config TQ_WIFI_SSID
string "Wi-Fi SSID" string "Wi-Fi SSID"
default "" default ""
config LYF_WIFI_PASSWORD config TQ_WIFI_PASSWORD
string "Wi-Fi Password" string "Wi-Fi Password"
default "" default ""
config LYF_WIFI_MAXIMUM_RETRY config TQ_WIFI_MAXIMUM_RETRY
int "Wi-Fi Maximum Retry" int "Wi-Fi Maximum Retry"
default 10 default 10
config LYF_HTTP_PORT config TQ_HTTP_PORT
int "REST HTTP Port" int "REST HTTP Port"
default 80 default 80
config LYF_API_KEY config TQ_API_KEY
string "REST API Key (optional, empty means disabled)" string "REST API Key (optional, empty means disabled)"
default "" 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 endmenu

View File

@@ -8,5 +8,5 @@ static const char *TAG = "app_main";
void app_main(void) { void app_main(void) {
ESP_ERROR_CHECK(controller_lifecycle_start()); ESP_ERROR_CHECK(controller_lifecycle_start());
ESP_LOGI(TAG, "LYF controller started"); ESP_LOGI(TAG, "TQ controller started");
} }

View File

@@ -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_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_write_frame_post(httpd_req_t *req);
esp_err_t rest_server_ota_upgrade_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);

View File

@@ -4,6 +4,7 @@
#include "printer_protocol.h" #include "printer_protocol.h"
#include "rest_server.h" #include "rest_server.h"
#include "system_runtime.h" #include "system_runtime.h"
#include "voice_interaction.h"
static const char *TAG = "controller_lifecycle"; static const char *TAG = "controller_lifecycle";
@@ -14,6 +15,9 @@ esp_err_t controller_lifecycle_start(void) {
ESP_LOGI(TAG, "Starting printer protocol"); ESP_LOGI(TAG, "Starting printer protocol");
ESP_ERROR_CHECK(printer_protocol_init()); 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_LOGI(TAG, "Starting REST server");
ESP_ERROR_CHECK(rest_server_start()); ESP_ERROR_CHECK(rest_server_start());
@@ -22,6 +26,7 @@ esp_err_t controller_lifecycle_start(void) {
void controller_lifecycle_stop(void) { void controller_lifecycle_stop(void) {
rest_server_stop(); rest_server_stop();
(void)voice_interaction_stop(NULL, 0);
printer_protocol_disconnect(); printer_protocol_disconnect();
ESP_LOGI(TAG, "Controller lifecycle stopped"); ESP_LOGI(TAG, "Controller lifecycle stopped");
} }

View File

@@ -16,7 +16,7 @@ static const char *s_web_index =
"<head>\n" "<head>\n"
" <meta charset='utf-8'>\n" " <meta charset='utf-8'>\n"
" <meta name='viewport' content='width=device-width,initial-scale=1'>\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" " <style>\n"
" :root { color-scheme: light; }\n" " :root { color-scheme: light; }\n"
" body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 16px; background:#f7f8fb; color:#1b2430; }\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" "</head>\n"
"<body>\n" "<body>\n"
" <div class='box'>\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" " <p>ESP32-S3 local control page. This page calls the onboard REST API directly.</p>\n"
"\n" "\n"
" <label>API Key (optional)</label>\n" " <label>API Key (optional)</label>\n"
@@ -50,7 +50,7 @@ static const char *s_web_index =
" </div>\n" " </div>\n"
"\n" "\n"
" <label>Printer Name</label>\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" " <div class='row'>\n"
" <button id='btnConnect'>Connect Printer</button>\n" " <button id='btnConnect'>Connect Printer</button>\n"
" <button id='btnAutoConnect' class='alt'>Auto Connect</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" " document.getElementById('btnJobs').onclick = () => run(() => callApi('/v1/jobs', 'GET'));\n"
"\n" "\n"
" document.getElementById('btnConnect').onclick = () => run(() => callApi('/v1/printer/connect', 'POST', {\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" " timeout_ms: 15000\n"
" }));\n" " }));\n"
"\n" "\n"
@@ -249,7 +249,7 @@ static esp_err_t favicon_get(httpd_req_t *req) {
esp_err_t rest_server_start(void) { esp_err_t rest_server_start(void) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 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.uri_match_fn = httpd_uri_match_wildcard;
config.max_uri_handlers = 32; config.max_uri_handlers = 32;
@@ -301,6 +301,41 @@ esp_err_t rest_server_start(void) {
}; };
httpd_register_uri_handler(s_server, &status); 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 = { httpd_uri_t print_raster = {
.uri = "/v1/print/raster", .uri = "/v1/print/raster",
.method = HTTP_POST, .method = HTTP_POST,
@@ -434,7 +469,7 @@ esp_err_t rest_server_start(void) {
}; };
httpd_register_uri_handler(s_server, &ota_upgrade); 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; return ESP_OK;
} }

View File

@@ -27,7 +27,7 @@ static void *calloc_prefer_psram(size_t n, size_t size) {
return ptr; return ptr;
} }
bool rest_server_auth_ok(httpd_req_t *req) { 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; return true;
} }
@@ -36,7 +36,7 @@ bool rest_server_auth_ok(httpd_req_t *req) {
return false; 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) { esp_err_t rest_server_send_json(httpd_req_t *req, const char *status, cJSON *root) {

View File

@@ -6,6 +6,7 @@
#include "printer_protocol.h" #include "printer_protocol.h"
#include "system_runtime.h" #include "system_runtime.h"
#include "voice_interaction.h"
static void fill_runtime_json(cJSON *root) { static void fill_runtime_json(cJSON *root) {
char ip[32] = {0}; char ip[32] = {0};
system_runtime_get_ip(ip, sizeof(ip)); 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, "queue_depth", st.queue_depth);
cJSON_AddNumberToObject(root, "mtu", st.mtu); cJSON_AddNumberToObject(root, "mtu", st.mtu);
cJSON_AddNumberToObject(root, "last_status_ms", (double)st.last_status_ms); 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) { 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 *body = NULL;
char name[32] = "lyfPrinter"; char name[32] = "TQPrinter";
uint32_t timeout_ms = 15000; uint32_t timeout_ms = 15000;
if (req->content_len > 0) { if (req->content_len > 0) {

View File

@@ -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) const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL)
? jtitle->valuestring ? jtitle->valuestring
: "LYF RECEIPT"; : "TQ RECEIPT";
const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL) const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL)
? jfooter->valuestring ? jfooter->valuestring
: "Thank you"; : "Thank you";

View 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;
}

View 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);

File diff suppressed because it is too large Load Diff

View File

@@ -15,3 +15,6 @@ dependencies:
# # All dependencies of `main` are public by default. # # All dependencies of `main` are public by default.
# public: true # public: true
espressif/esp_jpeg: ^1.3.1 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

View 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);

View File

@@ -21,6 +21,8 @@
#define PRINTER_NOTIFY_UUID16 0xFFF1 #define PRINTER_NOTIFY_UUID16 0xFFF1
#define PRINTER_WRITE_UUID16 0xFFF2 #define PRINTER_WRITE_UUID16 0xFFF2
#define CCCD_UUID16 0x2902 #define CCCD_UUID16 0x2902
#define PRINTER_NAME_PRIMARY "TQPrinter"
#define PRINTER_NAME_LEGACY "lyfPrinter"
#define EVT_CONNECTED BIT0 #define EVT_CONNECTED BIT0
#define EVT_READY BIT1 #define EVT_READY BIT1
@@ -35,7 +37,7 @@ static ble_frame_rx_cb_t s_rx_cb;
static uint8_t s_addr_type; static uint8_t s_addr_type;
static ble_addr_t s_target_addr; 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 bool s_match_any_compatible;
static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE; 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); 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) { static void reset_discovery_state(void) {
s_service_start = 0; s_service_start = 0;
s_service_end = 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) { if (s_match_any_compatible) {
bool uuid_match = adv_has_uuid16(&fields, PRINTER_SERVICE_UUID16); 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) { if (!uuid_match && !name_match) {
return 0; return 0;
} }
@@ -357,7 +374,7 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg) {
event->disc.rssi, event->disc.rssi,
uuid_match ? 1 : 0); uuid_match ? 1 : 0);
} else { } 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; return 0;
} }
ESP_LOGI(TAG, "Found target device '%s' RSSI=%d", name, event->disc.rssi); ESP_LOGI(TAG, "Found target device '%s' RSSI=%d", name, event->disc.rssi);

View 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;
}

View File

@@ -32,10 +32,10 @@ static void wifi_event_handler(void *arg,
} }
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { 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(); esp_wifi_connect();
s_retry_num++; 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 { } else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); 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) { static esp_err_t start_sta_mode(void) {
wifi_config_t wifi_config = {0}; wifi_config_t wifi_config = {0};
if (strlen(CONFIG_LYF_WIFI_SSID) == 0) { if (strlen(CONFIG_TQ_WIFI_SSID) == 0) {
ESP_LOGE(TAG, "CONFIG_LYF_WIFI_SSID is empty; STA-only mode requires valid SSID"); ESP_LOGE(TAG, "CONFIG_TQ_WIFI_SSID is empty; STA-only mode requires valid SSID");
return ESP_ERR_INVALID_STATE; 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.ssid, CONFIG_TQ_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.password, CONFIG_TQ_WIFI_PASSWORD, sizeof(wifi_config.sta.password));
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
wifi_config.sta.pmf_cfg.capable = true; wifi_config.sta.pmf_cfg.capable = true;
wifi_config.sta.pmf_cfg.required = false; wifi_config.sta.pmf_cfg.required = false;
@@ -77,13 +77,13 @@ static esp_err_t start_sta_mode(void) {
pdMS_TO_TICKS(15000)); pdMS_TO_TICKS(15000));
if (bits & WIFI_CONNECTED_BIT) { 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; s_ready = true;
return ESP_OK; return ESP_OK;
} }
if (bits & WIFI_FAIL_BIT) { 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; return ESP_FAIL;
} }

View File

@@ -6,8 +6,8 @@ CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247 CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247
# CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set # CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set
CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n
CONFIG_LYF_WIFI_SSID="TalkingQ" CONFIG_TQ_WIFI_SSID="TalkingQ"
CONFIG_LYF_WIFI_PASSWORD="TalkingQ123" CONFIG_TQ_WIFI_PASSWORD="TalkingQ123"
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB" CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
@@ -20,3 +20,27 @@ CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_BOOT_INIT=y CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=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