feat(voice): improve on-screen dialog text and wifi provisioning hints
This commit is contained in:
@@ -115,6 +115,10 @@ esp_err_t screen_preview_show_png(const uint8_t *png,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len);
|
||||
esp_err_t screen_preview_show_text(const char *text,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len);
|
||||
|
||||
// ---------- printer_protocol ----------
|
||||
typedef enum {
|
||||
|
||||
@@ -125,6 +125,13 @@ static void set_black(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) {
|
||||
buf[idx] |= (uint8_t)(1u << bit);
|
||||
}
|
||||
|
||||
static void set_black_bold(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) {
|
||||
set_black(buf, width, x, y);
|
||||
if ((uint16_t)(x + 1) < width) {
|
||||
set_black(buf, width, (uint16_t)(x + 1), y);
|
||||
}
|
||||
}
|
||||
|
||||
static size_t cn16_index_count(void) {
|
||||
size_t bytes = (size_t)(_binary_cn16_index_bin_end - _binary_cn16_index_bin_start);
|
||||
return bytes / CN16_INDEX_ENTRY_BYTES;
|
||||
@@ -211,7 +218,7 @@ static void draw_char(uint8_t *buf,
|
||||
uint16_t px = (uint16_t)(x + col * scale + sx);
|
||||
uint16_t py = (uint16_t)(y + row * scale + sy);
|
||||
if (py < height) {
|
||||
set_black(buf, width, px, py);
|
||||
set_black_bold(buf, width, px, py);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +248,7 @@ static void draw_cn16_glyph(uint8_t *buf,
|
||||
uint16_t px = (uint16_t)(x + col * scale + sx);
|
||||
uint16_t py = (uint16_t)(y + row * scale + sy);
|
||||
if (py < height && px < width) {
|
||||
set_black(buf, width, px, py);
|
||||
set_black_bold(buf, width, px, py);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +272,7 @@ static void draw_missing_box(uint8_t *buf,
|
||||
uint16_t px = (uint16_t)(x + c);
|
||||
uint16_t py = (uint16_t)(y + r);
|
||||
if (px < width && py < height) {
|
||||
set_black(buf, width, px, py);
|
||||
set_black_bold(buf, width, px, py);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#include "domain.h"
|
||||
#include "platform.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SCREEN_PREVIEW_TEXT_SCALE 2
|
||||
#define SCREEN_PREVIEW_TEXT_LINE_SPACING 1
|
||||
#define SCREEN_PREVIEW_TEXT_MAX_HEIGHT 3000
|
||||
|
||||
esp_err_t screen_preview_show_raster(const uint8_t *raster,
|
||||
size_t raster_len,
|
||||
uint16_t width,
|
||||
@@ -20,3 +27,53 @@ esp_err_t screen_preview_show_png(const uint8_t *png,
|
||||
{
|
||||
return platform_display_show_png(png, png_len, timeout_ms, err, err_len);
|
||||
}
|
||||
|
||||
esp_err_t screen_preview_show_text(const char *text,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len)
|
||||
{
|
||||
if (text == NULL || text[0] == '\0') {
|
||||
if (err != NULL && err_len > 0) {
|
||||
strlcpy(err, "invalid text", err_len);
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
uint8_t *raster = NULL;
|
||||
size_t raster_len = 0;
|
||||
char render_err[96] = {0};
|
||||
esp_err_t render_rc = raster_tools_render_text_384(text,
|
||||
SCREEN_PREVIEW_TEXT_SCALE,
|
||||
SCREEN_PREVIEW_TEXT_LINE_SPACING,
|
||||
SCREEN_PREVIEW_TEXT_MAX_HEIGHT,
|
||||
&width,
|
||||
&height,
|
||||
&raster,
|
||||
&raster_len,
|
||||
render_err,
|
||||
sizeof(render_err));
|
||||
if (render_rc != ESP_OK) {
|
||||
if (err != NULL && err_len > 0 && render_err[0] != '\0') {
|
||||
strlcpy(err, render_err, err_len);
|
||||
}
|
||||
return render_rc;
|
||||
}
|
||||
|
||||
char preview_err[96] = {0};
|
||||
esp_err_t preview_rc = screen_preview_show_raster(raster,
|
||||
raster_len,
|
||||
width,
|
||||
height,
|
||||
timeout_ms,
|
||||
preview_err,
|
||||
sizeof(preview_err));
|
||||
free(raster);
|
||||
|
||||
if (preview_rc != ESP_OK && err != NULL && err_len > 0 && preview_err[0] != '\0') {
|
||||
strlcpy(err, preview_err, err_len);
|
||||
}
|
||||
return preview_rc;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,69 @@
|
||||
#include "domain.h"
|
||||
#include "platform.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "system_runtime";
|
||||
|
||||
#define SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS 220
|
||||
|
||||
static bool s_wifi_hint_shown;
|
||||
|
||||
static void system_runtime_show_wifi_hint(void) {
|
||||
if (s_wifi_hint_shown) {
|
||||
return;
|
||||
}
|
||||
s_wifi_hint_shown = true;
|
||||
|
||||
const char *ap_ssid = CONFIG_TQ_WIFI_PROV_SOFTAP_SSID;
|
||||
if (ap_ssid == NULL || ap_ssid[0] == '\0') {
|
||||
ap_ssid = "TalkingQ-Setup";
|
||||
}
|
||||
|
||||
char hint[256] = {0};
|
||||
snprintf(hint,
|
||||
sizeof(hint),
|
||||
"未检测到Wi-Fi配置\n连接热点: %s\n打开: http://192.168.4.1",
|
||||
ap_ssid);
|
||||
|
||||
char preview_err[96] = {0};
|
||||
esp_err_t preview_rc = screen_preview_show_text(hint,
|
||||
SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS,
|
||||
preview_err,
|
||||
sizeof(preview_err));
|
||||
if (preview_rc != ESP_OK && preview_rc != ESP_ERR_NOT_SUPPORTED) {
|
||||
ESP_LOGW(TAG,
|
||||
"wifi_hint_preview_failed: rc=0x%x msg=%s",
|
||||
(unsigned)preview_rc,
|
||||
preview_err[0] != '\0' ? preview_err : "show text failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void system_runtime_try_show_wifi_hint_for_unprovisioned(void) {
|
||||
if (!wifi_manager_has_saved_credentials()) {
|
||||
system_runtime_show_wifi_hint();
|
||||
}
|
||||
}
|
||||
|
||||
static void system_runtime_wifi_event_handler(wifi_manager_event_t event, void *user_data) {
|
||||
(void)user_data;
|
||||
if (event == WIFI_MANAGER_EVENT_PROVISIONING_STARTED) {
|
||||
system_runtime_show_wifi_hint();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t system_runtime_start(void) {
|
||||
esp_err_t err = platform_bootstrap_init();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
s_wifi_hint_shown = false;
|
||||
(void)wifi_manager_set_event_callback(system_runtime_wifi_event_handler, NULL);
|
||||
system_runtime_try_show_wifi_hint_for_unprovisioned();
|
||||
|
||||
err = wifi_manager_start();
|
||||
if (err != ESP_OK) {
|
||||
// Reset platform state so lifecycle retry runs from a clean baseline.
|
||||
@@ -18,6 +75,7 @@ esp_err_t system_runtime_start(void) {
|
||||
}
|
||||
|
||||
esp_err_t system_runtime_shutdown(void) {
|
||||
(void)wifi_manager_set_event_callback(NULL, NULL);
|
||||
return wifi_manager_stop();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "voice_interaction_internal.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define cJSON_malloc voice_malloc_prefer_psram
|
||||
@@ -14,10 +16,99 @@ static const char *TAG = "voice_interaction";
|
||||
#define VOICE_RESPONSE_FINALIZE_TASK_STACK 6144
|
||||
#define VOICE_RESPONSE_FINALIZE_TASK_RETRY_COUNT 3
|
||||
#define VOICE_RESPONSE_FINALIZE_TASK_RETRY_DELAY_MS 120
|
||||
#define VOICE_SCREEN_PREVIEW_TIMEOUT_MS 180
|
||||
|
||||
static portMUX_TYPE s_response_finalize_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static bool s_response_finalize_running;
|
||||
|
||||
static bool voice_is_space_char(char c) {
|
||||
return isspace((unsigned char)c) != 0;
|
||||
}
|
||||
|
||||
static size_t voice_find_first_sentence_end(const char *text, size_t len) {
|
||||
if (text == NULL || len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
unsigned char c = (unsigned char)text[i];
|
||||
if (c == '.' || c == '!' || c == '?' || c == ':' || c == ';' ||
|
||||
c == '\n' || c == '\r') {
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
if (i + 2 < len) {
|
||||
unsigned char b1 = (unsigned char)text[i];
|
||||
unsigned char b2 = (unsigned char)text[i + 1];
|
||||
unsigned char b3 = (unsigned char)text[i + 2];
|
||||
|
||||
// UTF-8: 。 (E3 80 82)
|
||||
if (b1 == 0xE3 && b2 == 0x80 && b3 == 0x82) {
|
||||
return i + 3;
|
||||
}
|
||||
// UTF-8: !(EF BC 81) ?(EF BC 9F) ;(EF BC 9B) :(EF BC 9A)
|
||||
if (b1 == 0xEF && b2 == 0xBC &&
|
||||
(b3 == 0x81 || b3 == 0x9F || b3 == 0x9B || b3 == 0x9A)) {
|
||||
return i + 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static const char *voice_get_text_without_first_sentence(const char *text) {
|
||||
if (text == NULL || text[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t len = strlen(text);
|
||||
size_t begin = 0;
|
||||
while (begin < len && voice_is_space_char(text[begin])) {
|
||||
begin++;
|
||||
}
|
||||
if (begin >= len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t first_end = begin + voice_find_first_sentence_end(text + begin, len - begin);
|
||||
while (first_end < len && voice_is_space_char(text[first_end])) {
|
||||
first_end++;
|
||||
}
|
||||
|
||||
if (first_end < len) {
|
||||
return text + first_end;
|
||||
}
|
||||
|
||||
// Fallback: keep answer body after marker prefix when punctuation split
|
||||
// does not leave a second sentence.
|
||||
const char *marker_begin = strstr(text + begin, "/*");
|
||||
if (marker_begin == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *marker_end = strstr(marker_begin + 2, "*/");
|
||||
if (marker_end == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *cursor = marker_end + 2;
|
||||
while (*cursor != '\0') {
|
||||
unsigned char ch = (unsigned char)*cursor;
|
||||
if (!voice_is_space_char((char)ch) &&
|
||||
ch != '.' && ch != '!' && ch != '?' &&
|
||||
ch != ':' && ch != ';' && ch != ',') {
|
||||
break;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
|
||||
if (*cursor == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
static esp_err_t voice_send_directive_with_retry(const char *action,
|
||||
const char *directive,
|
||||
bool with_dialog_id) {
|
||||
@@ -136,6 +227,24 @@ static bool voice_get_json_bool(cJSON *obj, const char *name, bool *out_value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void voice_try_preview_final_text(const char *text) {
|
||||
if (text == NULL || text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
char preview_err[96] = {0};
|
||||
esp_err_t preview_rc = screen_preview_show_text(text,
|
||||
VOICE_SCREEN_PREVIEW_TIMEOUT_MS,
|
||||
preview_err,
|
||||
sizeof(preview_err));
|
||||
if (preview_rc != ESP_OK && preview_rc != ESP_ERR_NOT_SUPPORTED) {
|
||||
ESP_LOGW(TAG,
|
||||
"dialog_text_preview_failed: rc=0x%x msg=%s",
|
||||
(unsigned)preview_rc,
|
||||
preview_err[0] != '\0' ? preview_err : "show text failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void voice_handle_output_event(cJSON *output) {
|
||||
const char *event_name = voice_get_json_str(output, "event");
|
||||
if (event_name == NULL) {
|
||||
@@ -150,11 +259,14 @@ static void voice_handle_output_event(cJSON *output) {
|
||||
|
||||
bool finished = false;
|
||||
bool has_finished = voice_get_json_bool(output, "finished", &finished);
|
||||
const char *speech_final_text = NULL;
|
||||
const char *responding_final_text = NULL;
|
||||
bool should_check_marker_in_first_sentence = false;
|
||||
bool should_try_send_speech = false;
|
||||
if (has_finished && finished) {
|
||||
if (strcmp(event_name, "RespondingContent") == 0) {
|
||||
if (strcmp(event_name, "SpeechContent") == 0) {
|
||||
speech_final_text = voice_get_json_str(output, "text");
|
||||
} else if (strcmp(event_name, "RespondingContent") == 0) {
|
||||
const char *final_text = voice_get_json_str(output, "text");
|
||||
if (final_text == NULL || final_text[0] == '\0') {
|
||||
final_text = voice_get_json_str(output, "spoken");
|
||||
@@ -233,6 +345,17 @@ static void voice_handle_output_event(cJSON *output) {
|
||||
voice_close_audio_with_drain(VOICE_AUDIO_DRAIN_WAIT_MS, "server_event_close");
|
||||
}
|
||||
|
||||
if (speech_final_text != NULL && speech_final_text[0] != '\0') {
|
||||
voice_try_preview_final_text(speech_final_text);
|
||||
}
|
||||
|
||||
if (responding_final_text != NULL && responding_final_text[0] != '\0') {
|
||||
const char *preview_text = voice_get_text_without_first_sentence(responding_final_text);
|
||||
if (preview_text != NULL && preview_text[0] != '\0') {
|
||||
voice_try_preview_final_text(preview_text);
|
||||
}
|
||||
}
|
||||
|
||||
if (should_check_marker_in_first_sentence) {
|
||||
voice_marker_try_trigger_image_generation(dialog_id, responding_final_text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user