1859 lines
55 KiB
C
1859 lines
55 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;
|
|
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++;
|
|
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;
|
|
}
|
|
|
|
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;
|
|
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();
|
|
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)
|
|
{
|
|
LOG_D("message(bin): %d ", len);
|
|
|
|
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));
|
|
|
|
rws_socket_send_text(app->socket, senddata);
|
|
|
|
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;
|
|
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;
|
|
}
|
|
}
|
|
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");
|
|
return;
|
|
}
|
|
memcpy(audioPacket.data, data, dataSize);
|
|
}
|
|
|
|
sendRecordingAudioPacket(&audioPacket);
|
|
// 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;
|
|
|
|
sendRecordingAudioPacket(&interruptPacket);
|
|
}
|
|
|
|
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");
|
|
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);
|
|
|
|
int send_ret = talkingq_ws_send_bin((uint8_t*)sendBuffer, totalSize);
|
|
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)) {
|
|
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;
|
|
}
|