Files
banban/ql-application/threadx/interface/custom_app/ws_talkingq.c
T
chenjintao db6109172c feat: add device diagnostics for missing voice replies
Track microphone levels, short reads and PCM buffer drops. Report audio
queue admission separately from TLS writes, expose server replies and
ACKs before filtering, and capture per-round counters before timeouts.

Add diagnostic regression coverage and support POSIX hosts in WSS tests.
2026-10-26 18:35:24 +08:00

1937 lines
59 KiB
C

#include <stdlib.h>
#include <string.h>
#include <librws.h>
#include "cJSON.h"
#include "app_inc.h"
#include "talkingq_wss.h"
#include "ql_rtos.h"
#include "ql_application.h"
#include "ql_audio.h"
#include "ql_gpio.h"
#include "sockets.h"
#include "ql_uart.h"
#include "ql_func.h"
#include "netdb.h"
#include "ql_sim.h"
#include "ql_dev.h"
#include "ql_wtd.h"
#include "ql_fs.h"
#include "ql_fota.h"
#include "ql_sim.h"
#include "ql_ping.h"
#include "ql_nw.h"
#include "ql_atcmd.h"
#include "ql_platform.h"
#include "layer3.h"
#include "ql_uart.h"
#include "hal_api.h"
#include "fvad.h"
#define PCM_FRAME_LENGTH 1152
#define CLOUD_TTS_ERROR_URL_FORMAT \
"http://101.35.224.118:8080/assets/roles/banban/tts_error/tts_error%02u.mp3"
#define LOCAL_TTS_ERROR_PATH "U:/tts_error.mp3"
static int g_sim_ready = 0;
int g_mic_working = 0;
static ql_queue_t player_queue = NULL;
ql_sem_t sender_sem = NULL;
ql_sem_t vad_check_sem = NULL;
//ql_sem_t check_connect_sem = NULL;
struct ringbuffer *ring_sender = NULL;
static uint8_t sender_buf[PCM_FRAME_LENGTH]={0};
static int16 mp3_buff[2*PCM_FRAME_LENGTH];
static int16 *ppbuff=mp3_buff;
static int onmessage_status = -1;
volatile int tts_playing = 0;
/* Reply waits and no-speech prompts belong to one listening round. */
static ql_mutex_t conversation_mutex = NULL;
static volatile unsigned int conversation_round = 0;
static unsigned int wait_round = 0;
static unsigned int sender_round = 0;
static uint64_t reply_wait_start_ms = 0;
enum { WAIT_NONE, WAIT_NO_SPEECH, WAIT_REPLY, WAIT_SEND_FAILURE };
static int conversation_wait = WAIT_NONE;
static int timeout_action_busy = 0;
static int round_response_received = 0;
/* Queue admission is distinct from a completed TLS write and from server ACK. */
static struct {
unsigned int tx_packets, tx_bytes, tx_failed, rx_text, rx_bin;
unsigned int start_ack, end_ack;
uint64_t started_ms, last_rx_ms;
} voice_diag;
static unsigned int active_session_round = 0;
static int active_session_valid = 0;
static uint64_t startup_start_ms = 0;
#define ROLE_NOTICE_TYPE 5
#define ROLE_NOTICE_URL_SIZE 256
static char role_notice_url[ROLE_NOTICE_URL_SIZE];
static int role_notice_pending = 0;
static int role_notice_playing = 0;
static int role_notice_consumed = 0;
static unsigned int role_notice_round = 0;
static void conversation_lock(void)
{
ql_rtos_mutex_lock(conversation_mutex, 0xFFFFFFFF);
}
static void conversation_unlock(void)
{
ql_rtos_mutex_unlock(conversation_mutex);
}
/* Caller holds the mutex; ignored recording keys must not call this. */
static void conversation_begin_round(void)
{
conversation_round++;
memset(&voice_diag, 0, sizeof(voice_diag));
voice_diag.started_ms = hal_os_up_time_ms();
printf("[VOICE-DIAG] begin round=%u uptime_ms=%u\n",
conversation_round, (unsigned int)voice_diag.started_ms);
conversation_wait = WAIT_NONE;
round_response_received = 0;
active_session_valid = 0;
startup_start_ms = hal_os_up_time_ms();
/* Accepted restart cancels the preceding round's deferred notice. */
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
role_notice_url[0] = '\0';
}
void conversation_record_begin(void)
{
conversation_lock();
if (!g_mic_working) conversation_begin_round();
g_mic_working = 1;
conversation_unlock();
}
void conversation_cancel_wait(void)
{
conversation_lock();
conversation_wait = WAIT_NONE;
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
role_notice_url[0] = '\0';
conversation_unlock();
}
void conversation_no_speech(void)
{
conversation_lock();
wait_round = conversation_round;
conversation_wait = WAIT_NO_SPEECH;
conversation_unlock();
}
static volatile int player_mq_couter = 0;
/* Set while a barge-in prompt bridges the old and new sessions. The old
* server response can still deliver URLs after its player queue was cleared. */
static volatile int barge_in_transition = 0;
static volatile unsigned char audio_in_task_cancel = 0;
static unsigned char xiaoice_wss_connect_status = 0;
static unsigned char wss_connect_counter = 0;
int fota_start = 0;
int mqtt_task_run_flag = 0;
extern int sub_topic_status;
extern int mqtt_task_run_flag;
static unsigned char shine_init = 0;
extern int fisrt_net_start;
extern u8 power_down_status;
extern int aofei_mp3_player_get_result(void);
extern int vad_leave_message_is_active(void);
extern int vad_cancel_leave_message(void);
extern void vad_reset_conversation_flags(void);
extern system_state_t vad_audio_get_state(void);
void sendAuthentication(void);
void vad_start_func(void);
void xiaoice_close_all_audio(void);
int audio_packet_type = 0;
typedef struct ice_work_item_s
{
int type; //0-tts_out 1-session_continue 2-session_end 3-云端超时提示(失败时播放本地兜底) 4-本地超时兜底 5-角色切换提示
unsigned int timeout_round; /* Startup, timeout and role notices use this round token. */
int error;
int del_url;
void *data;
}ice_work_item;
static uint32_t gSequenceNumber = 0;
#define DEVICE_ID_SIZE 22
#define SESSION_ID_SIZE 33
typedef struct {
uint32_t sequenceNumber;
uint8_t packetType;
uint32_t dataSize;
uint8_t *data;
} AudioPacket;
char talkingq_host[64]={0};
char deviceId[DEVICE_ID_SIZE] = {0};
char sessionId[SESSION_ID_SIZE] = {0};
char serialNumber[SESSION_ID_SIZE] = {0};
struct librws_app
{
int init;
rws_socket *socket;
};
typedef struct librws_app* librws_app_t;
static struct librws_app _app = {0};
static librws_app_t app = &_app;
static ql_sem_t ws_connect_sem = NULL;
extern int fota_start;
#define OTA_AUDIO_STOP_TIMEOUT_MS 3000
#define OTA_AUDIO_STOP_POLL_MS 20
void xiaoice_clear_player_mq(void);
static int tts_play_status = 0;//1播放中 2播放完成
int xiaoice_get_tts_play_flag(void)
{
return tts_play_status;
}
int xiaoice_get_play_counter(void)
{
return player_mq_couter;
}
int tts_playing_ret(void)
{
return tts_playing;
}
unsigned char get_xiaoice_connected_status(void)
{
return xiaoice_wss_connect_status;
}
/* Called with conversation_mutex held. Ages use monotonic milliseconds. */
static void voice_diag_snapshot(const char *stage)
{
uint64_t now = hal_os_up_time_ms();
printf("[VOICE-DIAG] %s round=%u session=%s age_ms=%u queue_pkts=%u queue_bytes=%u queue_fail=%u rx_text=%u rx_bin=%u start_ack=%u end_ack=%u last_rx_age_ms=%d wait=%d wait_ms=%u response=%d\n",
stage, conversation_round, active_session_valid ? sessionId : "none",
(unsigned int)(now - voice_diag.started_ms), voice_diag.tx_packets,
voice_diag.tx_bytes, voice_diag.tx_failed, voice_diag.rx_text,
voice_diag.rx_bin, voice_diag.start_ack, voice_diag.end_ack,
voice_diag.rx_text || voice_diag.rx_bin ? (int)(now - voice_diag.last_rx_ms) : -1,
conversation_wait, conversation_wait == WAIT_REPLY ?
(unsigned int)(now - reply_wait_start_ms) : 0, round_response_received);
}
static void onopen(rws_socket socket)
{
if (socket != app->socket || !app->init) return;
wss_connect_counter=0;
LOG_D("websocket connected. ");
if(ws_connect_sem){
ql_rtos_semaphore_release(ws_connect_sem);
}
sendAuthentication();
}
static void onclose(rws_socket socket)
{
rws_error error = rws_socket_get_error(socket);
int error_code = rws_error_get_code(error);
if(error)
{
LOG_E("websocket disconnect, error: %i, %s ", error_code, rws_error_get_description(error));
}
else
{
LOG_D("websocket disconnect! ");
}
if (socket != app->socket) return;
conversation_lock();
voice_diag_snapshot("disconnected");
conversation_unlock();
xiaoice_wss_connect_status = 0;
app->socket = NULL;
/* The worker owns and releases this socket after the callback. */
RLOGD("onclose end\r\n");
app->init = 0;
if (ws_connect_sem) ql_rtos_semaphore_release(ws_connect_sem);
hal_led_b_off();
}
// 安全修复:添加最大长度限制防止越界
static void remove_underscores(char *str) {
if (str == NULL) return; // 安全检测
char *src = str; // 源指针(读位置)
char *dst = str; // 目标指针(写位置)
size_t max_len = 128; // 最大处理长度限制
size_t processed = 0;
while (*src && processed < max_len) {
if (*src != '_') {
*dst = *src; // 非下划线字符:复制并移动目标指针
dst++;
}
src++; // 始终移动源指针
processed++;
}
*dst = '\0'; // 添加字符串结束符
}
/* 生成 banban 角色的随机云端对话超时提示音 URL。 */
static int build_random_tts_error_url(char *url, size_t url_size)
{
static uint32_t random_sequence = 0;
static unsigned int last_random_index = 10;
uint32_t random_value;
unsigned int random_index;
int written;
if(url == NULL || url_size == 0){
return 0;
}
/*
* 某些设备上硬件随机接口可能持续返回 0,因此混合伪随机数、系统 tick
* 和递增序列;若仍与上一次相同,则向后偏移,避免连续重复播放。
*/
random_sequence++;
random_value = hal_os_get_rand();
random_value ^= ql_pseudo_random_number();
random_value ^= ql_rtos_get_systicks();
random_value ^= random_sequence * 0x9E3779B9U;
random_index = random_value % 10;
if(random_index == last_random_index){
random_index = (random_index + 1 + (random_sequence % 9)) % 10;
}
last_random_index = random_index;
written = snprintf(url, url_size, CLOUD_TTS_ERROR_URL_FORMAT, random_index);
if(written < 0 || (size_t)written >= url_size){
printf("random tts error url buffer too small\n");
return -1;
}
printf("random cloud tts error index:%02u, url:%s\n", random_index, url);
return 1;
}
/*
* 后台下发任意角色的对话超时 URL 时,统一替换成 banban 的随机云端资源。
* 非对话超时 URL 保持不变。
*/
static int randomize_tts_error_url(char *url, size_t url_size)
{
char *file_name;
if(url == NULL || url_size == 0){
return 0;
}
file_name = strrchr(url, '/');
file_name = (file_name == NULL) ? url : file_name + 1;
if(strncmp(file_name, "tts_error", strlen("tts_error")) != 0 &&
strncmp(file_name, "tts_err", strlen("tts_err")) != 0){
return 0;
}
return build_random_tts_error_url(url, url_size);
}
int add_file_to_play_player_queue(char *path, int type)
{
ice_work_item *item = NULL;
// 安全修复:检查path参数有效性
if(path == NULL || strlen(path) == 0){
printf("add_file_to_play_player_queue: invalid path\n");
return -1;
}
item = malloc(sizeof(ice_work_item));
if(item == NULL){
printf("add_file_to_play_player_queue: malloc item failed\n");
return -1;
}
item->data = malloc(strlen(path)+1);
if(item->data == NULL){
printf("add_file_to_play_player_queue: malloc data failed\n");
free(item);
return -1;
}
item->type = type;
item->timeout_round = conversation_round;
// 安全修复:使用strncpy替代sprintf防止溢出
snprintf((char *)item->data, strlen(path)+1, "%s", path);
printf("play file:%s\r\n", (char *)item->data);
if (ql_rtos_queue_release(player_queue, sizeof(item), &item, 0) != 0)
{
printf("player_mq rt_mq_send ERR\n");
free(item->data);
item->data=NULL;
free(item);
item = NULL;
return -1;
}else{
player_mq_couter++;
printf("player_mq_couter:%d\r\n", player_mq_couter);
return 0;
}
}
/* Only this resource is a role-change notice. Other NFC notices keep their
* existing immediate queue path, including binding and card audio. */
static int role_notice_is_url(const char *url)
{
const char *host, *path;
const char resource[] = "/assets/audio/switch_role.mp3";
size_t n = sizeof(resource) - 1;
if (!url) return 0;
host = strstr(url, "://");
if (!host) return 0;
path = strchr(host + 3, '/');
return path && strncmp(path, resource, n) == 0 &&
(path[n] == '\0' || path[n] == '?' || path[n] == '#');
}
/* Caller holds conversation_mutex. A full queue/failed download must not
* prevent the original conversation completion from proceeding. */
static int role_notice_start_locked(void)
{
role_notice_pending = 0;
role_notice_consumed = 1;
role_notice_playing = 1;
role_notice_round = conversation_round;
if (add_file_to_play_player_queue(role_notice_url, ROLE_NOTICE_TYPE) != 0) {
role_notice_playing = 0;
printf("role notice queue failed:round=%u\n", conversation_round);
return 0;
}
printf("role notice queued once:round=%u\n", conversation_round);
return 1;
}
/* Called only after the normal VAD playback-finished signal, or while idle.
* The original finished flag/no-speech flag stays untouched during the notice. */
static int role_notice_poll_locked(int finishing)
{
if (fota_start || power_down_status || get_sleep_mode()) {
role_notice_pending = role_notice_playing = 0;
return 0;
}
if (role_notice_playing) return 1;
if (!role_notice_pending) return 0;
if (role_notice_round != conversation_round) {
role_notice_pending = 0;
return 0;
}
if (vad_audio_is_recording() || g_mic_working || shine_init ||
timeout_action_busy || player_mq_couter > 0 || tts_playing != 0 ||
conversation_wait != WAIT_NONE) return 1;
if (!finishing && vad_audio_get_state() != STATE_IDLE) return 1;
return role_notice_start_locked();
}
int app_role_notice_handle_url(const char *url)
{
if (!role_notice_is_url(url)) return 0;
conversation_lock();
if (strlen(url) >= sizeof(role_notice_url)) {
printf("role notice URL too long\n");
} else if (role_notice_playing ||
(role_notice_consumed && vad_audio_get_state() != STATE_IDLE)) {
printf("role notice coalesced:round=%u\n", conversation_round);
} else {
/* Repeated commands before playback replace one pending slot. */
snprintf(role_notice_url, sizeof(role_notice_url), "%s", url);
role_notice_pending = 1;
role_notice_round = conversation_round;
printf("role notice pending:round=%u\n", conversation_round);
role_notice_poll_locked(0);
}
conversation_unlock();
return 1;
}
int app_role_notice_before_finish(void)
{
int wait;
conversation_lock();
wait = role_notice_poll_locked(1);
conversation_unlock();
return wait;
}
static void role_notice_finished(unsigned int round, int result)
{
conversation_lock();
if (role_notice_round == round && round == conversation_round) {
role_notice_playing = 0;
printf("role notice finished:round=%u result=%d\n", round, result);
}
conversation_unlock();
}
//如果录音后 超过10s 还没有收到信息 提示本轮对话结束
static void onmessage_text_locked(rws_socket socket, const char *text, const unsigned int len)
{
if (fota_start == 1) {
printf("ignore websocket audio during OTA\r\n");
return;
}
/* The new session id is generated only when recording starts. Until
* then, every TTS message on this socket belongs to the interrupted
* session and must not be allowed to refill the cleared player queue. */
if (barge_in_transition) {
printf("drop stale websocket message during barge-in\r\n");
return;
}
// printf("message(txt), %d(byte): %s ", len, text);
char url_buff[128]={0x00};
char url_buff_bak[128]={0x00};
if(strstr(text, "_START")){
printf("TTS_START\r\n");
//语音交互音频开始
tts_playing = 1;
tts_play_status = 1;
// aofei_mp3_player_init();
}else if(strstr(text, "TTS_END")){
printf("TTS_END\r\n");
//语音交互音频结束
// tts_playing = 0;
tts_play_status = 2;
}else if(strstr(text, "END_ACK")||strstr(text, "START_ACK")){
}else{//TTS_URL:audio_url
char *ret_str1 = strstr(text, "https://");
char *ret_str2 = NULL;
size_t url_len = 0;
int is_tts_error_url = 0;
int is_message_ok_url = 0;
if(ret_str1 == NULL){
ret_str1 = strstr(text, "http://");
}
if(ret_str1 != NULL){
ret_str2 = strstr(ret_str1, ".mp3");
}
if(ret_str1!=NULL && ret_str2!=NULL){
/* 提取完整的 HTTP/HTTPS MP3 URL,结尾额外包含“.mp3”四个字符 */
url_len = (size_t)(ret_str2 - ret_str1) + strlen(".mp3");
if(url_len >= sizeof(url_buff)){
printf("audio url too long:%d\n", (int)url_len);
return;
}
memcpy(url_buff, ret_str1, url_len);
url_buff[url_len] = '\0';
is_message_ok_url = strstr(url_buff, "message_ok") != NULL;
/* A cancelled NFC transaction may still have a response in flight.
* Never let that acknowledgement enter a normal conversation. */
if(is_message_ok_url && !vad_leave_message_is_active()){
printf("drop stale leave message ack\r\n");
return;
}
/* 两个角色统一使用 banban 的 HTTP 随机超时提示音 */
is_tts_error_url = randomize_tts_error_url(url_buff, sizeof(url_buff));
if(is_tts_error_url < 0){
return;
}
memcpy(url_buff_bak, url_buff, strlen(url_buff)+1);
//先把文件下载存入到60k的全局数组里面 然后启动mp3播放
remove_underscores(url_buff_bak);
//TARGET_DEVICE_REGISTERED_URL:{sound_url}
if(strstr(text, "PROMPT_SOUND_URL")==NULL&&strstr(text, "TARGET_DEVICE_REGISTERED_URL")==NULL \
&&strstr(text, "NFC_SOUND_URL")==NULL){//提示音不需要判断sessionID
if(strstr(url_buff_bak,sessionId)==NULL){
return;
}
}else{
tts_play_status = 2;
}
conversation_wait = WAIT_NONE;
round_response_received = 1;
if(is_tts_error_url == 1 || is_message_ok_url){
printf("tts_error or message_ok\n");
vad_set_speech_timeout_flag(1);
}
//如果打断了 就不去做播放
if(tts_playing==2) return;
/* 对话超时提示使用 type=3,云端播放失败时由播放线程切换到本地兜底。 */
add_file_to_play_player_queue(url_buff, is_tts_error_url == 1 ? 3 : 0);
}
}
}
/* Untagged error prompts cannot identify an old server request. At least
* reject them throughout local startup and before this round sends START. */
static int conversation_accept_voice_message(const char *text)
{
int voice = strstr(text, "TTS_") != NULL ||
strstr(text, "END_ACK") != NULL ||
strstr(text, "START_ACK") != NULL ||
strstr(text, "/tts_error") != NULL ||
strstr(text, "/tts_err") != NULL;
return !voice || (active_session_valid &&
active_session_round == conversation_round && !g_mic_working);
}
/* Recognize the device's 32-character session-id format, including URL
* underscores. This is a candidate check, not a generic protocol parser. */
static int conversation_find_session_id(const char *text, unsigned int len, char *id)
{
unsigned int i, j, n;
for (i = 0; i < len; i++) {
if (text[i] < '0' || text[i] > '9') continue;
n = 0;
for (j = i; j < len && n < 32; j++) {
char ch = text[j];
if (ch == '_') continue;
if (n < 14 ? (ch < '0' || ch > '9') :
!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z'))) break;
id[n++] = ch;
}
if (n == 32 && (j == len ||
!((text[j] >= '0' && text[j] <= '9') ||
(text[j] >= 'A' && text[j] <= 'Z') ||
(text[j] >= 'a' && text[j] <= 'z')))) {
id[32] = '\0';
return 1;
}
}
id[0] = '\0';
return 0;
}
static void conversation_log_error_message(const char *text, unsigned int len)
{
char candidate[33];
unsigned int offset;
int found = conversation_find_session_id(text, len, candidate);
int field = strstr(text, "sessionId") != NULL ||
strstr(text, "session_id") != NULL || strstr(text, "sessionID") != NULL;
printf("[WSS-ERR-ID] round=%u session_valid=%d last_session=%s field=%d candidate=%s match_last=%d len=%u\n",
conversation_round, active_session_valid, sessionId, field,
found ? candidate : "none", found && strcmp(candidate, sessionId) == 0, len);
/* Log before URL randomization and before rejecting stale replies. */
for (offset = 0; offset < len; offset += 128) {
unsigned int chunk = len - offset;
if (chunk > 128) chunk = 128;
printf("[WSS-ERR-RAW] offset=%u/%u %.*s\n", offset, len,
(int)chunk, text + offset);
}
}
static void onmessage_text(rws_socket socket, const char *text, const unsigned int len)
{
conversation_lock();
voice_diag.rx_text++;
voice_diag.last_rx_ms = hal_os_up_time_ms();
if (strstr(text, "START_ACK")) voice_diag.start_ack++;
if (strstr(text, "END_ACK")) voice_diag.end_ack++;
/* Bounded preview includes unrecognized replies and ACKs before filtering. */
printf("[VOICE-RX] round=%u session=%s uptime_ms=%u len=%u accepted=%d preview=%.*s%s\n",
conversation_round, active_session_valid ? sessionId : "none",
(unsigned int)voice_diag.last_rx_ms, len,
conversation_accept_voice_message(text), (int)(len > 192 ? 192 : len), text,
len > 192 ? " [truncated]" : "");
if (strstr(text, "tts_error") != NULL || strstr(text, "tts_err") != NULL ||
strstr(text, "PROMPT_SOUND_URL") != NULL) {
conversation_log_error_message(text, len);
}
if (!conversation_accept_voice_message(text)) {
printf("drop old voice reply:round=%u startup=%d\n",
conversation_round, g_mic_working);
} else {
onmessage_text_locked(socket, text, len);
}
conversation_unlock();
}
static void onmessage_bin(rws_socket socket, const void * data, const unsigned int len)
{
(void)socket;
(void)data;
conversation_lock();
voice_diag.rx_bin++;
voice_diag.last_rx_ms = hal_os_up_time_ms();
if (voice_diag.rx_bin == 1 || voice_diag.rx_bin % 50 == 0)
printf("[VOICE-RX] binary round=%u len=%u count=%u ignored=1\n",
conversation_round, len, voice_diag.rx_bin);
conversation_unlock();
return;
}
int talkingq_ws_connect()
{
int result;
int ret = -1;
char wss_path[TALKINGQ_PATH_CAPACITY]={0};
char ws_talkingq_url[TALKINGQ_URL_CAPACITY]={0};
int port;
// char session_id[56]={0};
// char ip_name[56]={0};
RLOGD("talkingq_ws_connect 1\r\n");
if(app->init == 1)
{
LOG_E("the websocket connection has been opened. ");
return (-1);
}
RLOGD("talkingq_ws_connect 2\r\n");
app->init = 1;//防止重复进入等待
xiaoice_wss_connect_status = 1;
if(ws_connect_sem==NULL){
ql_rtos_semaphore_create(&ws_connect_sem, 0);
}
if (ws_connect_sem) while (ql_rtos_semaphore_wait(ws_connect_sem, 0) == 0) {}
hal_os_get_talkingq_url(ws_talkingq_url);
printf("ws_talkingq_url %s",ws_talkingq_url);
if (talkingq_parse_wss_url(ws_talkingq_url, talkingq_host, sizeof(talkingq_host),
&port, wss_path, sizeof(wss_path)) != 0) {
LOG_E("Invalid WSS endpoint; cleartext fallback is disabled\n");
app->init = 0;
xiaoice_wss_connect_status = 0;
return -1;
}
app->socket = rws_socket_create();
if(app->socket == NULL)
{
app->init = 0;
xiaoice_wss_connect_status = 0;
LOG_E("librws socket create failed. ");
return (-1);
}
printf("talkingq host %s , port %d",talkingq_host,port);
/* Server-authenticated TLS; application authentication stays in onopen. */
rws_socket_set_url(app->socket, "wss", talkingq_host, port, wss_path);
rws_socket_set_server_cert(app->socket, talkingq_root_ca, talkingq_root_ca_size);
rws_socket_set_on_connected(app->socket, &onopen);
rws_socket_set_on_disconnected(app->socket, &onclose);
rws_socket_set_on_received_text(app->socket, &onmessage_text);
rws_socket_set_on_received_bin(app->socket, &onmessage_bin);
//rws_socket_set_custom_mode(app->socket);
ret = rws_socket_connect(app->socket);
if(ret == rws_false)
{
rws_socket_disconnect_and_release(app->socket);
app->socket = NULL;
app->init = 0;
LOG_E("rws_socket_connect failed. ");
ret = -1;
}
else
{
ret = -1;
LOG_D("try rws_socket_connect. ");
if(ws_connect_sem){
result = ql_rtos_semaphore_wait(ws_connect_sem, 45000);
if(result == 0 && app->init && app->socket && rws_socket_is_connected(app->socket)){
LOG_I("ws connect okay");
wss_connect_counter = 0;
ret = 0;
}else{
/* Stop the pending worker before allowing a new attempt. */
if (app->socket) rws_socket_disconnect_and_release(app->socket);
app->socket = NULL;
app->init = 0;
LOG_E("ws connect failed or wait timeout");
wss_connect_counter++;
}
}else{
app->init = 0;
LOG_E("ws_connect_sem null");
}
}
if(ret == 0 && app->init == 1){
xiaoice_wss_connect_status = 2;
hal_led_b_on();
}
else{
xiaoice_wss_connect_status = 0;
hal_led_b_off();
}
return ret;
}
int talkingq_ws_disconnect()
{
audio_packet_type = 0;
g_mic_working = 0;
hal_led_b_off();
if(app->init == 0)
{
LOG_W("no websocket connection. ");
return (-1);
}
rws_socket_disconnect_and_release(app->socket);
LOG_I("try disconnect websocket connection. ");
app->socket = NULL;
app->init = 0;
return 0;
}
int talkingq_ws_send_text(char *senddata)
{
if(app->init == 0)
{
LOG_W("no websocket connection. ");
return (-1);
}
LOG_D("talkingq_wss_send_text string = %s, len = %d", senddata, strlen(senddata));
{
int queued = rws_socket_send_text(app->socket, senddata);
printf("[VOICE-TX] auth/text queue uptime_ms=%u bytes=%u accepted=%d\n",
(unsigned int)hal_os_up_time_ms(), (unsigned int)strlen(senddata), queued != 0);
}
return 0;
}
int talkingq_ws_send_bin(uint8_t *senddata, int len)
{
if(app->init == 0)
{
LOG_W("no websocket connection. ");
return (-1);
}
// LOG_D("talkingq_wss_send_bin, len = %d", len);
return rws_socket_send_bin(app->socket, senddata, len,2,rws_true) ? 0 : -1;
}
int sendRecordingAudioPacket(AudioPacket *packet);
/*串口AT指令做设置*/
void generateDeviceId(void) {
char temp_DeviceId[32];
memset(temp_DeviceId, 0, sizeof(temp_DeviceId));
hal_os_get_device_id(temp_DeviceId);
snprintf(deviceId, strlen(temp_DeviceId)+1, temp_DeviceId);
printf("temp_DeviceId:%s\n",temp_DeviceId);
}
void generateSerialNumber(void) {
char temp_SerialNumber[32];
memset(temp_SerialNumber, 0, sizeof(temp_SerialNumber));
hal_os_get_device_sn(temp_SerialNumber);
snprintf(serialNumber, strlen(temp_SerialNumber)+1, temp_SerialNumber);
printf("temp_SerialNumber:%s\n",temp_SerialNumber);
}
#if 0
#define INT_MAX 2147483647
#define INT_MIN (-1 - INT_MAX)
void int_to_digits_str(int num, char *buf, int buf_size) {
if (buf_size < 2) { // 至少需要存储空字符
if (buf_size > 0) buf[0] = '\0';
return;
}
unsigned int n; // 用于存储绝对值
// 处理INT_MIN特殊情况(避免溢出)
if (num == INT_MIN) {
n = (unsigned int)INT_MAX + 1; // 2147483648
}
// 处理其他负数
else if (num < 0) {
n = (unsigned int)(-num); // 取绝对值
}
// 处理正数和0
else {
n = (unsigned int)num;
}
// 处理0特殊情况
if (n == 0) {
buf[0] = '0';
buf[1] = '\0';
return;
}
// 临时存储数字(逆序)
char temp[11]; // 足够存储10位数字
int temp_index = 0;
// 提取数字(逆序)
while (n > 0) {
temp[temp_index++] = '0' + (n % 10);
n /= 10;
}
temp[temp_index] = '\0';
// 计算所需空间:数字位数 + 结束符
int required_size = temp_index + 1;
// 检查缓冲区大小
if (buf_size < required_size) {
buf[0] = '\0';
return;
}
// 反转数字到缓冲区
for (int i = 0; i < temp_index; i++) {
buf[i] = temp[temp_index - 1 - i];
}
buf[temp_index] = '\0'; // 终止字符串
}
void generateSessionId(void) {
static int rand_num = 0;
int index = 0;
if(rand_num==0){
rand_num = hal_os_get_rand();
}else{
rand_num++;
}
if(rand_num<10000000) rand_num += 10000000;
if(rand_num>99999999) rand_num = 10000000;
char buffer[12]; // 足够存储任何int值
int_to_digits_str(rand_num, buffer, sizeof(buffer));
for(index=0;index<8;index++){
sessionId[index]=buffer[index];
}
const char charset[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const int charset_size = sizeof(charset) - 1;
int i = 0;
for(i = 8; i < 32; i++) {
index= (hal_os_get_rand()+i) % charset_size;
sessionId[i] = charset[index];
}
sessionId[32] = '\0';
RLOGE("sessionId:%s\n", sessionId);
}
#endif
/**
* @brief 基于本地时间生成32字节sessionId
*
* 格式说明(32字节):
* [0-1] : 年份后两位 (00-99)
* [2-3] : 月份 (01-12)
* [4-5] : 日期 (01-31)
* [6-7] : 小时 (00-23)
* [8-9] : 分钟 (00-59)
* [10-11]: 秒 (00-59)
* [12-13]: 毫秒级计数器(同一秒内递增,保证唯一性)
* [14-31]: 随机字符(数字+大写字母)
*
* 示例:26060914305201A3B7C9D2E4F6G8H0J1K
*/
void generateSessionId(void)
{
static unsigned short seq_counter = 0; // 同一秒内的递增计数器
char time_buf[6];
int i = 0;
int index = 0;
// 获取当前本地时间
hal_os_get_localtime(time_buf);
// 同一秒内计数器递增,超过99则回绕
seq_counter++;
if (seq_counter > 99) {
seq_counter = 0;
}
// ========== 前14位:时间戳部分(基于本地时间)==========
// 将时间数据转为ASCII字符存入sessionId
// 年份 (2位) - buf[0] 是年份后两位,如26表示2026年
sessionId[0] = '0' + (time_buf[0] / 10); // 年十位
sessionId[1] = '0' + (time_buf[0] % 10); // 年个位
// 月份 (2位)
sessionId[2] = '0' + (time_buf[1] / 10); // 月十位
sessionId[3] = '0' + (time_buf[1] % 10); // 月个位
// 日期 (2位)
sessionId[4] = '0' + (time_buf[2] / 10); // 日十位
sessionId[5] = '0' + (time_buf[2] % 10); // 日个位
// 小时 (2位)
sessionId[6] = '0' + (time_buf[3] / 10); // 时十位
sessionId[7] = '0' + (time_buf[3] % 10); // 时个位
// 分钟 (2位)
sessionId[8] = '0' + (time_buf[4] / 10); // 分十位
sessionId[9] = '0' + (time_buf[4] % 10); // 分个位
// 秒 (2位)
sessionId[10] = '0' + (time_buf[5] / 10); // 秒十位
sessionId[11] = '0' + (time_buf[5] % 10); // 秒个位
// 同一秒内序列号 (2位,00-99)
sessionId[12] = '0' + (seq_counter / 10);
sessionId[13] = '0' + (seq_counter % 10);
// ========== 后18位:随机字符部分 ==========
const char charset[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const int charset_size = sizeof(charset) - 1;
for (i = 14; i < 32; i++) {
index = (hal_os_get_rand() + i) % charset_size;
sessionId[i] = charset[index];
}
sessionId[32] = '\0';
RLOGE("sessionId:%s\n", sessionId);
}
#if 1
void sendStartPacket(void) {
conversation_lock();
generateSessionId();
active_session_round = conversation_round;
/* sessionId now identifies the new conversation. Late URLs from the old
* conversation will fail the existing session-id check below. */
barge_in_transition = 0;
generateDeviceId();
gSequenceNumber = 0;
AudioPacket startPacket = {0};
startPacket.sequenceNumber = gSequenceNumber++;
startPacket.packetType = 1;
startPacket.dataSize = 0;
active_session_valid = sendRecordingAudioPacket(&startPacket) == 0;
voice_diag_snapshot("start-queued");
conversation_unlock();
// rt_kprintf("sendStartPacket, seq=%u\r\n", startPacket.sequenceNumber);
}
void sendEndPacket(void) {
AudioPacket endPacket = {0};
endPacket.sequenceNumber = gSequenceNumber++;
endPacket.packetType = 2;
endPacket.dataSize = 0;
conversation_lock();
if (sender_round == conversation_round) {
int end_ret = sendRecordingAudioPacket(&endPacket);
if (!round_response_received) {
wait_round = sender_round;
reply_wait_start_ms = hal_os_up_time_ms();
conversation_wait = end_ret == 0 ? WAIT_REPLY : WAIT_SEND_FAILURE;
}
voice_diag_snapshot("end-queued");
}
conversation_unlock();
// rt_kprintf("sendEndPacket, seq=%u\r\n", endPacket.sequenceNumber);
}
void sendAudioPacket(const uint8_t* data, size_t dataSize) {
AudioPacket audioPacket = {0};
audioPacket.sequenceNumber = gSequenceNumber++;
audioPacket.packetType = audio_packet_type;
//audioPacket.packetType = 4;
audioPacket.dataSize = dataSize;
if(audioPacket.dataSize>0){
audioPacket.data = malloc(audioPacket.dataSize);
if(audioPacket.data==NULL){
RLOGE("audioPacket.data NULL");
conversation_lock();
voice_diag.tx_failed++;
printf("[VOICE-TX] alloc_failed round=%u seq=%u bytes=%u\n",
conversation_round, audioPacket.sequenceNumber, (unsigned int)dataSize);
conversation_unlock();
return;
}
memcpy(audioPacket.data, data, dataSize);
}
conversation_lock();
sendRecordingAudioPacket(&audioPacket);
conversation_unlock();
// rt_kprintf("sendAudioPacket, seq=%u\r\n", audioPacket.sequenceNumber);
if(audioPacket.data){
free(audioPacket.data);
}
}
void sendInterruptPacket(void) {
AudioPacket interruptPacket = {0};
interruptPacket.sequenceNumber = gSequenceNumber++;
interruptPacket.packetType = 3;
interruptPacket.dataSize = 0;
conversation_lock();
sendRecordingAudioPacket(&interruptPacket);
conversation_unlock();
}
void sendAuthentication()
{
cJSON *sendAuth = cJSON_CreateObject();
generateDeviceId();
generateSerialNumber();
cJSON_AddItemToObject(sendAuth,"device_id",cJSON_CreateString(deviceId));
cJSON_AddItemToObject(sendAuth,"serial_number",cJSON_CreateString(serialNumber));
// cJSON_AddItemToObject(sendAuth,"deviceId",cJSON_CreateString(deviceId));
//cJSON_AddItemToObject(sendAuth,"serialNumber",cJSON_CreateString(serialNumber));
char *jstr = cJSON_PrintUnformatted(sendAuth);
if(jstr){
talkingq_ws_send_text(jstr);
free(jstr);
}
cJSON_Delete(sendAuth);
}
int sendRecordingAudioPacket(AudioPacket *packet) {
uint8_t *sendBuffer=NULL;
sendBuffer = malloc(DEVICE_ID_SIZE + SESSION_ID_SIZE + sizeof(uint32_t) * 2 + sizeof(uint32_t) + packet->dataSize);
if(sendBuffer==NULL){
RLOGE("sendBuffer NULL");
voice_diag.tx_failed++;
printf("[VOICE-TX] alloc_failed seq=%u type=%u bytes=%u\n",
packet->sequenceNumber, packet->packetType, (unsigned int)packet->dataSize);
return -1;
}
size_t headerSize = DEVICE_ID_SIZE + SESSION_ID_SIZE + sizeof(packet->sequenceNumber)
+ sizeof(packet->packetType) + sizeof(packet->dataSize);
size_t totalSize = headerSize + packet->dataSize;
size_t offset = 0;
memcpy(sendBuffer + offset, deviceId, DEVICE_ID_SIZE);
offset += DEVICE_ID_SIZE;
memcpy(sendBuffer + offset, sessionId, SESSION_ID_SIZE);
offset += SESSION_ID_SIZE;
memcpy(sendBuffer + offset, &packet->sequenceNumber, sizeof(packet->sequenceNumber));
offset += sizeof(packet->sequenceNumber);
memcpy(sendBuffer + offset, &packet->packetType, sizeof(packet->packetType));
offset += sizeof(packet->packetType);
memcpy(sendBuffer + offset, &packet->dataSize, sizeof(packet->dataSize));
offset += sizeof(packet->dataSize);
memcpy(sendBuffer + offset, packet->data, packet->dataSize);
// recorder->save_len = totalSize;
// memcpy(recorder->save_buf, (void *)sendBuffer, recorder->save_len);
//record_msg_send(recorder, RECORD_MSG_DATA, recorder->save_len);
uint64_t queue_started = hal_os_up_time_ms();
int send_ret = talkingq_ws_send_bin((uint8_t*)sendBuffer, totalSize);
if (send_ret == 0) {
voice_diag.tx_packets++;
voice_diag.tx_bytes += (unsigned int)packet->dataSize;
} else {
voice_diag.tx_failed++;
}
if (packet->packetType == 1 || packet->packetType == 2 || send_ret != 0 ||
voice_diag.tx_packets % 50 == 0) {
printf("[VOICE-TX] queue round=%u session=%s seq=%u type=%u bytes=%u ret=%d elapsed_ms=%u\n",
conversation_round, sessionId, packet->sequenceNumber, packet->packetType,
(unsigned int)packet->dataSize, send_ret,
(unsigned int)(hal_os_up_time_ms() - queue_started));
}
free(sendBuffer);
return send_ret;
// rt_kprintf("sendRecordingAudioPacket - Device ID: %s, Session ID: %s\n", deviceId, sessionId);
}
#endif
extern const char *mp3_file[];
int app_play_tips_file(int index)
{
int file_valid_check = 0;
int access_ret = 0;
printf("xiaoice_play_index:%d\n", index);
//正在录音过程中不允许播放
// if(xiaoice_get_tts_play_status() || get_start_mic_flag()==1) return -1;
if(power_down_status!=1){
if(vad_audio_is_recording() || tts_playing!=0 || fota_start==1 || power_down_status==2 || get_sleep_mode()==1) {
printf("tips blocked:index=%d vad=%d tts=%d fota=%d power=%d sleep=%d\r\n",
index, vad_audio_is_recording(), tts_playing, fota_start,
power_down_status, get_sleep_mode());
return -1;
}
}
char filename_buff[64]={0x00};
sprintf(filename_buff, "U:/%s", mp3_file[index]);
if(index==0) {
file_valid_check = 0;
}else{
access_ret = ql_access(filename_buff, 0);
if(access_ret==0){
file_valid_check = 1;
}else{
printf("tips file missing:index=%d path=%s access=%d\r\n",
index, filename_buff, access_ret);
file_valid_check = 2;
}
}
if(file_valid_check!=2){
if(index==18 || index==19){
return add_file_to_play_player_queue(filename_buff, 2);
}else{
return add_file_to_play_player_queue(filename_buff, 0);
}
}else{
return -2;
}
return 0;
}
int app_play_ota_tips_file(void)
{
conversation_lock();
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
conversation_unlock();
char filename_buff[64] = {0};
uint32_t timeout;
/* OTA owns the audio path from this point on. Stop the active conversation
* before queuing the prompt, otherwise recording/TTS rejects or masks it. */
if (vad_audio_is_recording()) {
vad_audio_stop_record();
}
if (get_xiaoice_connected_status() == 2) {
talkingq_ws_disconnect();
}
tts_playing = 2;
xiaoice_clear_player_mq();
timeout = hal_os_up_time_ms();
while (player_mq_couter > 0) {
ql_rtos_task_sleep_ms(OTA_AUDIO_STOP_POLL_MS);
if (hal_os_up_time_ms() - timeout > OTA_AUDIO_STOP_TIMEOUT_MS) {
printf("OTA wait previous audio timeout, counter:%d\r\n",
player_mq_couter);
return -3;
}
}
tts_play_status = 0;
tts_playing = 0;
/* Normal prompts are blocked while fota_start is set. Queue the dedicated
* OTA prompt directly after the old conversation has fully stopped. */
snprintf(filename_buff, sizeof(filename_buff), "U:/%s", mp3_file[3]);
printf("xiaoice_play_index:3\n");
if (ql_access(filename_buff, 0) != 0) {
return -2;
}
add_file_to_play_player_queue(filename_buff, 0);
return player_mq_couter > 0 ? 0 : -1;
}
static void conversation_finish_startup_prompt(unsigned int round, int result)
{
int start;
conversation_lock();
start = round == conversation_round && g_mic_working && tts_playing != 2;
if (start) startup_start_ms = hal_os_up_time_ms();
conversation_unlock();
if (start) {
printf("voice startup prompt finished:round=%u result=%d\n", round, result);
/* Queue fullness must not suppress this prompt's startup action. */
vad_start_func();
}
}
void app_player_thread(void *arvg)
{
ice_work_item *player_item=NULL;
int play_ret;
while(1){
ql_rtos_queue_wait(player_queue, (u8*)(&player_item), sizeof(ice_work_item *), 0xFFFFFFFF);
if(player_item!=NULL){
int stale_timeout;
conversation_lock();
stale_timeout = ((player_item->type == 2 || player_item->type == 3 || player_item->type == 4 ||
player_item->type == ROLE_NOTICE_TYPE) &&
player_item->timeout_round != conversation_round) ||
(player_item->type == ROLE_NOTICE_TYPE && !role_notice_playing);
conversation_unlock();
if (stale_timeout) {
if (player_mq_couter > 0) player_mq_couter--;
free(player_item->data);
free(player_item);
continue;
}
// if(player_item->type==0){//local mp3 file
if(player_item->data){
// printf("xiaoice player! %s\n",player_item->data);
app_set_app_operation(1);
aofei_mp3_player(player_item->data);
play_ret = aofei_mp3_player_get_result();
app_set_app_operation(1);
if(player_mq_couter>0) player_mq_couter--;
/*
* type=3 只用于对话超时提示。云端连接、HTTP、读取或播放器初始化
* 失败时才排入本地兜底;如果用户已经开始下一轮录音,则丢弃旧提示。
*/
conversation_lock();
if(play_ret != 0 && player_item->type == 3 &&
!vad_audio_is_recording() && !g_mic_working &&
player_item->timeout_round == conversation_round &&
tts_playing_ret() != 2){
printf("cloud tts error play failed:%d, fallback:%s\r\n",
play_ret, LOCAL_TTS_ERROR_PATH);
if(ql_access(LOCAL_TTS_ERROR_PATH, 0) == 0){
add_file_to_play_player_queue(LOCAL_TTS_ERROR_PATH, 4);
}else{
printf("local tts error fallback missing:%s\r\n", LOCAL_TTS_ERROR_PATH);
}
}
conversation_unlock();
if(player_mq_couter==0 && player_item->type != ROLE_NOTICE_TYPE){
printf("player_mq_couter==0\r\n");
if(tts_play_status==2){
tts_play_status = 0;
if(tts_playing!=2){
vad_tts_playback_finished();
tts_playing = 0;
}
printf("tts_play_status0==0\r\n");
}
if(tts_playing==2){
tts_play_status = 0;
printf("tts_play_status1==0\r\n");
}
if(player_item->type==1){
printf("vad_leave_message\r\n");
vad_leave_message();
}
}
if (player_item->type == ROLE_NOTICE_TYPE) {
role_notice_finished(player_item->timeout_round, play_ret);
}
if (player_item->type == 2) {
conversation_finish_startup_prompt(player_item->timeout_round, play_ret);
}
// }
}
if(player_item->data){
free(player_item->data);
player_item->data = NULL;
}
free(player_item);
player_item = NULL;
}
}
}
static int conversation_try_begin(void)
{
conversation_lock();
if(vad_audio_is_recording() || g_mic_working || shine_init || timeout_action_busy) {
printf("voice key ignored:round=%u vad=%d startup=%d sender=%d timeout=%d\n",
conversation_round, vad_audio_is_recording(), g_mic_working,
shine_init, timeout_action_busy);
conversation_unlock();
return -1;
}
conversation_begin_round();
vad_reset_conversation_flags();
g_mic_working = 1;
conversation_unlock();
return 0;
}
int tts_play_interrupt_func(void)
{
int playback_interrupted = 0;
int leave_message_cancelled = 0;
/* A short press explicitly starts a normal conversation. Cancel an NFC
* leave-message before the generic "already recording" guard, otherwise a
* pending/recording leave-message can leak packetType=4 into this session. */
leave_message_cancelled = vad_cancel_leave_message();
if(leave_message_cancelled){
uint64_t cancel_start;
/* Let the sender task discard buffered PCM and its encoder without
* emitting END. Only then close the old leave-message socket. */
audio_in_task_cancel = 1;
ql_rtos_semaphore_release(sender_sem);
cancel_start = hal_os_up_time_ms();
while(audio_in_task_cancel &&
hal_os_up_time_ms() - cancel_start < OTA_AUDIO_STOP_TIMEOUT_MS){
ql_rtos_task_sleep_ms(20);
}
if(audio_in_task_cancel){
printf("wait leave message sender cancel timeout\r\n");
}
talkingq_ws_disconnect();
}
//如果在vad状态,则直接返回
if (conversation_try_begin() != 0) return -1;
/* g_mic_working reserves startup until VAD enters recording. */
/* Remember whether this key press is interrupting TTS. tts_playing == 2
* is only a cancellation signal for the old player; it must not leak into
* the next conversation, otherwise prompts and error audio are discarded. */
playback_interrupted = (tts_playing != 0 || player_mq_couter > 0);
barge_in_transition = playback_interrupted;
xiaoice_close_all_audio();
talkingq_ws_connect();
if(get_xiaoice_connected_status()!=2) {
/* Let the caller play its local network-error prompt. */
tts_play_status = 0;
tts_playing = 0;
barge_in_transition = 0;
g_mic_working = 0;
return -2;
}
if(playback_interrupted){
printf("barge_in_prompt\n");
/* The old player has finished consuming the cancellation state. */
tts_play_status = 0;
tts_playing = 0;
/* Use the same local-prompt flow as ai_active_zh.mp3. Queue type 2
* starts VAD after playback; fall back to VAD if the file is absent. */
if(app_play_tips_file(19) != 0){
vad_start_func();
}
}else{
if (app_play_tips_file(18) != 0) {
conversation_lock();
g_mic_working = 0;
conversation_wait = WAIT_NONE;
conversation_unlock();
return -2;
}
}
return 0;
}
void stop_player_func(void)
{
vad_audio_stop_record();
xiaoice_close_all_audio();
tts_play_status = 0;
tts_playing = 0;
}
void vad_start_func(void)
{
set_state(STATE_IDLE);
//ql_rtos_task_sleep_ms(50);
vad_audio_trigger_record();
}
static void conversation_check_timeout(void)
{
int expired = WAIT_NONE;
unsigned int expired_round = 0;
conversation_lock();
/* A lost prompt/trigger must not leave all future keys permanently blocked.
* Never recover during an active player, upload or recording. */
if (g_mic_working && !vad_audio_is_recording() && !shine_init &&
player_mq_couter == 0 && !timeout_action_busy &&
hal_os_up_time_ms() - startup_start_ms > 30000) {
printf("recover stalled voice startup:round=%u\n", conversation_round);
g_mic_working = 0;
conversation_wait = WAIT_NONE;
active_session_valid = 0;
vad_reset_conversation_flags();
}
if (conversation_wait != WAIT_NONE &&
wait_round == conversation_round &&
!vad_audio_is_recording() && !g_mic_working &&
!timeout_action_busy && fota_start != 1 &&
(conversation_wait == WAIT_NO_SPEECH ||
conversation_wait == WAIT_SEND_FAILURE ||
hal_os_up_time_ms() - reply_wait_start_ms > 5000)) {
voice_diag_snapshot("timeout-before-disconnect");
expired = conversation_wait;
expired_round = wait_round;
conversation_wait = WAIT_NONE;
timeout_action_busy = 1;
}
conversation_unlock();
if (expired != WAIT_NONE) {
char cloud_tts_error_url[128] = {0};
/* Reserve the transition, without holding a mutex while
* disconnect joins a socket thread/calls callbacks. */
if (expired != WAIT_NO_SPEECH) talkingq_ws_disconnect();
conversation_lock();
if (expired_round == conversation_round) {
printf("conversation timeout:round=%u reason=%s\n",
expired_round,
expired == WAIT_NO_SPEECH ? "no_speech" :
(expired == WAIT_REPLY ? "reply" : "send_failure"));
tts_playing = 0;
tts_play_status = 2;
audio_packet_type = 0;
vad_set_speech_timeout_flag(1);
if (build_random_tts_error_url(cloud_tts_error_url,
sizeof(cloud_tts_error_url)) == 1) {
add_file_to_play_player_queue(cloud_tts_error_url, 3);
} else {
app_play_tips_file(9);
}
}
timeout_action_busy = 0;
conversation_unlock();
}
}
static void check_connect_thread_entry(void *parameter)
{
uint32 cfun_recorder = 0;
int sim_status = 0;
int per_socket_status = 0;
int ret = 0;
QL_NW_REG_STATUS_INFO_T ql_nw_reg_status;
while(1)
{
#if 0
if(app_get_data_call_net_status() && per_net_status==0){
//app_play_tips_file(1);
per_net_status = 1;
hal_led_b_on();
}else if(!app_get_data_call_net_status() && per_net_status==1){
app_play_tips_file(2);
per_net_status = 0;
hal_led_b_off();
}
#endif
//判断网络状态
//a.如果连续3次连不上后台
//b.查询注网状态 正常则进行重新拨号
//c.注网状态异常,则cfun0/1一次,查询卡状态,如果卡状态异常,则后续不再做cfun0/1操作
//d.卡状态正常,注册网络异常,则每2个小时cfun0/1一次
if(wss_connect_counter>3 && fisrt_net_start==0){
wss_connect_counter = 0;
ql_sim_get_card_status(&sim_status);
if(sim_status == QL_SIM_STATUS_READY){
g_sim_ready = 1;
}else{
g_sim_ready = 0;
}
ql_nw_get_reg_status(&ql_nw_reg_status);
printf("net.state:%d, g_sim_ready:%d\n", ql_nw_reg_status.data_reg.state, g_sim_ready);
if(ql_nw_reg_status.data_reg.state==1 || ql_nw_reg_status.data_reg.state==5){
if(g_sim_ready){
//触发拨号
printf("app_data_recall\r\n");
app_data_recall();
}
}else{
//两个小时 CFUN01一次
if(cfun_recorder==0 || ql_rtos_get_systicks()-cfun_recorder>2*60*60000/5)
{
if(g_sim_ready){
printf("ql_dev_set_modem_fun\r\n");
ql_dev_set_modem_fun(QL_DEV_MODEM_MIN_FUN,0);
ql_rtos_task_sleep_ms(10000);
ql_dev_set_modem_fun(QL_DEV_MODEM_FULL_FUN,0);
ql_rtos_task_sleep_ms(10000);
}
cfun_recorder = ql_rtos_get_systicks();
}
}
}
#if 0
//if(app_get_app_sleep_flag()!=0){
//判断网络状态ok在进行
if(!get_rws_socket_thread_status()&&app_get_data_call_net_status()&&fota_start!=1&&sub_topic_status==1){
if(xiaoice_wss_connect_status!=1){//如果没有正在连接状态
hal_led_b_off();
wss_connect_counter++;
talkingq_ws_disconnect();
printf("reconnect wss socket:%d\r\n", wss_connect_counter);
ret = talkingq_ws_connect();
if(ret!=0){
/*播报网络异常*/
per_socket_status = 0;
app_play_tips_file(2);
hal_led_b_off();
}
}
}else {//如果网络正常
if(app_get_data_call_net_status()){
if(per_socket_status==0){
per_socket_status = 1;
app_play_tips_file(11);
hal_led_b_on();
}
}
}
//}else{
// ql_rtos_semaphore_wait(check_connect_sem, 0xFFFFFFFF);
//}
#endif
#if 1
conversation_check_timeout();
conversation_lock();
role_notice_poll_locked(0);
conversation_unlock();
#endif
ql_rtos_task_sleep_ms(3000);
}
}
#if 1
void xiaoice_clear_player_mq(void)
{
ice_work_item *player_item=NULL;
while(ql_rtos_queue_wait(player_queue, (u8*)(&player_item), sizeof(ice_work_item *), 0)==0){
if(player_item!=NULL){
if(player_item->data){
free(player_item->data);
player_item->data = NULL;
}
if(player_mq_couter>0) player_mq_couter--;
free(player_item);
player_item = NULL;
}
}
}
void xiaoice_close_all_audio(void)
{
int timeout=0;
app_codec_pa_close();//防止突然关掉的滋滋声
#if 1
if(tts_playing){
sendInterruptPacket();
}
if (tts_playing || player_mq_couter > 0) tts_playing = 2;
if(player_mq_couter>0){
xiaoice_clear_player_mq();
timeout = hal_os_up_time_ms();
/* The queue counter also includes the item currently owned by the
* player task. Wait for that item to observe tts_playing == 2 and
* finish cleanup before the cancellation state is reused/reset. */
while(player_mq_couter>0){
ql_rtos_task_sleep_ms(20);
if(hal_os_up_time_ms()-timeout>OTA_AUDIO_STOP_TIMEOUT_MS){
printf("wait player stop timeout, counter:%d\r\n",
player_mq_couter);
break;
}
}
}
#endif
}
#endif
void app_sender_thread(void *arvg)
{
int index = 0, cnt = 0;
uint8 *data_out = NULL;
shine_t sh_shine;
shine_config_t sh_config;
int left = 0;
unsigned char value = 0;
while(1)
{
ql_rtos_semaphore_wait(sender_sem, 0xFFFFFFFF);
sender_loop:
if(audio_in_task_cancel){
left = rb_get_space_used(ring_sender);
while(left > 0){
int discard_len = left > PCM_FRAME_LENGTH ? PCM_FRAME_LENGTH : left;
hal_os_rb_read(ring_sender, sender_buf, discard_len, 0);
left = rb_get_space_used(ring_sender);
}
if(shine_init != 0){
shine_close(sh_shine);
sh_shine = NULL;
shine_init = 0;
}
audio_packet_type = 0;
audio_in_task_cancel = 0;
printf("leave message sender cancelled\r\n");
continue;
}
if(!app_get_data_call_net_status()) continue;
/* One recording can release the semaphore many times while the sender
* drains the ring buffer. A stale token must never create an empty
* session after the previous encoder has already been closed. */
left = rb_get_space_used(ring_sender);
if(shine_init == 0 && left <= 0){
continue;
}
if(shine_init==0){
shine_set_config_mpeg_defaults(&(sh_config.mpeg)); //--init sh_config struct
sh_config.wave.channels=PCM_MONO; //PCM_STEREO;//PCM_MONO;
sh_config.wave.samplerate=16000;//valid samplerates: 44100,48000,32000,22050,24000,16000,11025,12000,8000
sh_config.mpeg.copyright=1;
sh_config.mpeg.original=1;
sh_config.mpeg.mode=MONO;//STEREO;//MONO;
sh_config.mpeg.bitr=16; //64kbps for 16k //32kbps for 8k samplerate
sh_shine=shine_initialise(&sh_config);
shine_init = 1;
conversation_lock();
sender_round = conversation_round;
conversation_unlock();
talkingq_ws_connect();
sendStartPacket();
}
left = rb_get_space_used(ring_sender);
if(left<PCM_FRAME_LENGTH){
if(!vad_audio_is_recording()){//录音结束
hal_os_rb_read(ring_sender,sender_buf, left, 5);
memset(sender_buf+left, 0, PCM_FRAME_LENGTH-left);
}else{
continue;
}
}else{
hal_os_rb_read(ring_sender,sender_buf, PCM_FRAME_LENGTH, 5);
}
cnt = 0;
for(index=0; index<576; index++)
{
mp3_buff[index] = sender_buf[cnt] + (sender_buf[cnt+1]<<8);
cnt=cnt+2;
}
data_out = shine_encode_buffer(sh_shine,&ppbuff,&cnt);
sendAudioPacket(data_out, cnt);
//quec_usbcdc_write(data_out, cnt);
//vad 结束 进来发送最后一个包
if(!vad_audio_is_recording()){
left = rb_get_space_used(ring_sender);
if(left>0) goto sender_loop;
printf("sendEndPacket\n");
data_out=shine_flush(sh_shine,&index);
sendAudioPacket(data_out, index);
sendEndPacket();
if(sh_shine!=NULL){
shine_close(sh_shine);
sh_shine = NULL;
}
shine_init = 0;
audio_packet_type = 0;
/* Discard semaphore tokens already represented by the PCM that this
* session drained. Recording has stopped, so no new frames race here. */
while(ql_rtos_semaphore_wait(sender_sem, 0) == 0) {}
}
}
}
void app_nfc_sender(int8 *uuid)
{
int8 uuid_buff[128];
memset(uuid_buff, 0, sizeof(uuid_buff));
sprintf(uuid_buff, "REGISTER_TARGET_DEVICE:%s", uuid);
printf("uuid_buff:%s\r\n", uuid_buff);
talkingq_ws_connect();
talkingq_ws_send_text(uuid_buff);
audio_packet_type = 4;
return 0;
}
void app_vad_check_thread(void *arvg)
{
while(1)
{
if(app_get_app_sleep_flag()!=0){
vad_audio_process();
ql_rtos_task_sleep_ms(20);
}else{
ql_rtos_semaphore_wait(vad_check_sem, 0xFFFFFFFF);
}
}
}
void app_vad_check_sem_release(void)
{
ql_rtos_semaphore_release(vad_check_sem);
}
#if 0
void app_check_connect_sem_release(void)
{
ql_rtos_semaphore_release(check_connect_sem);
}
#endif
int app_ws_talkingq_init()
{
if (ql_rtos_mutex_create(&conversation_mutex) != 0) return -1;
//one second = 640*50 = 32000
ring_sender = hal_os_rb_create(32000*5);
ql_rtos_semaphore_create(&sender_sem, 0);
ql_rtos_semaphore_create(&vad_check_sem, 0);
//ql_rtos_semaphore_create(&check_connect_sem, 0);
ql_rtos_queue_create(&player_queue, sizeof(ice_work_item *), 20);
ql_task_t playerTaskRef = NULL;
// 安全修复:增加栈大小从2KB到4KB,防止音频处理时栈溢出
ql_rtos_task_create(&playerTaskRef, 1024*4, 100, "app_player", app_player_thread, NULL);
ql_task_t checkTaskRef = NULL;
ql_rtos_task_create(&checkTaskRef, 1024*3, 102, "check_connect", check_connect_thread_entry, NULL);
ql_task_t senderTaskRef = NULL;
ql_rtos_task_create(&senderTaskRef, 1024*8, 101, "app_sender", app_sender_thread, NULL);
ql_task_t vadTaskRef = NULL;
// 安全修复:减小栈大小从20KB到8KB(VAD不需要那么大),节约内存
ql_rtos_task_create(&vadTaskRef, 1024*8, 101, "vad_check", app_vad_check_thread, NULL);
return 0;
}