Files
story/firmware/echoear-platform-agent/tests/call-display-host.cjs
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

244 lines
15 KiB
JavaScript

// Emit a standalone host TU containing the actual production state machine,
// SDK event/status callbacks and conversation task. No device or cloud access.
const fs = require('node:fs'), path = require('node:path');
const root = path.resolve(__dirname, '..');
const inc = fs.readFileSync(path.join(root, 'components/application/service/conv_service/inc/call_display_state.h'), 'utf8');
const state = fs.readFileSync(path.join(root, 'components/application/service/conv_service/src/call_display_state.c'), 'utf8').replace('#include "call_display_state.h"', '');
const task = fs.readFileSync(path.join(root, 'components/application/service/conv_service/src/volc_conv.c'), 'utf8');
const voiceHeader = fs.readFileSync(path.join(root, 'components/application/service/conv_service/inc/call_voice_end.h'), 'utf8').replace('#pragma once', '');
const voiceState = fs.readFileSync(path.join(root, 'components/application/service/conv_service/src/call_voice_end.c'), 'utf8').replace('#include "call_voice_end.h"', '');
function section(first, next) {
const start = task.indexOf(first), end = task.indexOf(next, start + first.length);
if (start < 0 || end < 0) throw new Error(`production section missing: ${first}`);
return task.slice(start, end);
}
process.stdout.write(inc + '\n' + state + '\n' + voiceHeader + '\n' + voiceState + '\n' + String.raw`
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
typedef void *volc_engine_t;
typedef void *volc_hal_display_t;
typedef void *volc_hal_capture_t;
typedef struct {int code;} volc_event_t;
typedef enum {VOLC_CONV_STATUS_LISTENING,VOLC_CONV_STATUS_THINKING,VOLC_CONV_STATUS_ANSWERING,VOLC_CONV_STATUS_INTERRUPTED,VOLC_CONV_STATUS_ANSWER_FINISH} volc_conv_status_e;
typedef struct {void *display_handle,*capture_handle[1],*player_handle[1];} volc_hal_context_t;
typedef struct {int media_type;void (*data_cb)(void);void (*audio_wakeup_cb)(void*,void*);void *user_data;} volc_hal_capture_config_t;
typedef struct {int type;} esp_gmf_afe_evt_t;
#define ESP_GMF_AFE_EVT_VAD_START 1
#define ESP_GMF_AFE_EVT_VAD_END 2
typedef struct {int media_type;} volc_hal_player_config_t;
typedef struct {int mode;char *bot_id,*params;} volc_opt_t;
typedef struct {bool is_binary;} volc_message_info_t;
typedef struct {void *engine;int wait_time;} volc_conv_service_t;
#define VOLC_EV_CONNECTED 1
#define VOLC_EV_DISCONNECTED 2
#define VOLC_DISPLAY_OBJ_STATUS 0
#define VOLC_DISPLAY_OBJ_MAIN 1
#define VOLC_DISPLAY_OBJ_SUBTITLE 2
#define VOLC_DISPLAY_TEXT 0
#define VOLC_DISPLAY_IMAGE 1
#define MMAP_EAF_HAPPY_EAF 0
#define MMAP_EAF_EMOTION_BLINK_FAST_284_126_EAF 1
#define MMAP_EAF_EMOTION_DIZZY_284_126_EAF 2
#define VOLC_HAL_PLAYER_AUDIO 0
#define VOLC_HAL_CAPTURE_AUDIO 0
#define VOLC_MEDIA_TYPE_AUDIO 0
#define VOLC_AUDIO_MODE_CAPTURE 0
#define VOLC_AUDIO_MODE_WAKEUP 1
#define VOLC_MODE_RTC 1
#define VOLC_MODE_WS 2
#define VOLC_AGENT_TYPE_LLM 0
#define VOLC_SERVICE_AI_CONVERSATION_OVER 1
#define CONV_SERVICE_TIMEOUT 60
#define LOGI(...) do { if (0) printf(__VA_ARGS__); } while (0)
#define LOGE(...) do { if (0) printf(__VA_ARGS__); } while (0)
#define pdMS_TO_TICKS(value) (value)
enum {NORMAL,DELAYED_CONNECTION,START_FAILURE,BEGIN_FAILURE,REMOTE_DROP,SLOW_END_ACK,AUDIO_STOP_FAILURE,RESPONSE_FINISHED,TIMEOUT_WITHOUT_CONNECTION,IDLE_CONNECTED,ACTIVE_LONG_CALL,VOICE_GOODBYE,VOICE_RETRACT,VOICE_MISSING_REPLY};
static int scenario,polls,frames,ended_frames,interrupted_frames,failed_frames,ending_frames,talking_frames,over_count;
static uint64_t now_ms;
static char text[64];
static bool audio_stopped,output_stopped,sdk_stopped;
static atomic_bool is_stop=true,stop_requested=false,provider_disconnected=false,rtc_connected=false,display_connected=false,user_stop_requested=false;
static call_display_state_t call_display;
static volc_hal_display_t global_display;
static bool session_started_reported,diagnostic_say_pending;
static call_voice_end_t voice_end;
static atomic_uint voice_finish_serial,last_voice_received_ms;
typedef struct {double valuedouble;} cJSON;
static cJSON *cJSON_GetObjectItemCaseSensitive(const cJSON *j,const char *k){(void)j;(void)k;return NULL;}
static const char *cJSON_GetStringValue(const cJSON *j){(void)j;return NULL;}
static bool cJSON_IsNumber(const cJSON *j){return j!=NULL;}
static bool cJSON_IsTrue(const cJSON *j){(void)j;return false;}
static void *platform_report_queue;
static int xQueueReceive(void *q,cJSON **out,unsigned ticks){(void)q;(void)out;(void)ticks;return 0;}
static void cJSON_Delete(cJSON *root){(void)root;}
#define pdTRUE 1
enum { DIAGNOSTIC_NONE, DIAGNOSTIC_SAY, DIAGNOSTIC_TTS, DIAGNOSTIC_PENDING_MASK=3 };
static atomic_uint diagnostic_state;
static void diagnostic_prepare(void){}
static void diagnostic_close(void){}
static unsigned diagnostic_take_pending(void){return 0;}
static void diagnostic_send_tts(void){}
static unsigned uxTaskPriorityGet(void *task){(void)task;return 5;}
static int xTaskGetCoreID(void *task){(void)task;return 0;}
static volc_conv_service_t conv_service;
static volc_hal_context_t hal={(void*)1,{(void*)2},{(void*)3}};
static void __on_volc_event(volc_engine_t,volc_event_t*,void*);
static void __on_volc_conversation_status(volc_engine_t,volc_conv_status_e,void*);
void conv_ai_service_task_stop(void);
static uint64_t volc_osal_get_time_ms(void){return now_ms;}
` + section('/* CALL_IDLE_BEGIN:', '/* CALL_IDLE_END */') + String.raw`
static void vTaskDelay(unsigned ms){now_ms+=ms;}
static void sleep(unsigned seconds){now_ms+=seconds*1000;}
static void volc_osal_thread_exit(void *unused){(void)unused;}
static void *volc_osal_malloc(size_t size){return malloc(size);}
static void volc_osal_free(void *p){free(p);}
static volc_hal_context_t *volc_get_global_hal_context(void){return &hal;}
static int volc_hal_display_set_content(void *display,int obj,int type,const void *content){
(void)display;(void)type;
if(obj!=VOLC_DISPLAY_OBJ_STATUS)return 0;
snprintf(text,sizeof(text),"%s",(const char*)content);frames++;
if(!strcmp(text,"通话中")){assert(atomic_load(&display_connected));talking_frames++;}
if(!strcmp(text,"正在结束"))ending_frames++;
if(!strcmp(text,"通话结束")||!strcmp(text,"连接失败")||!strcmp(text,"连接中断")){
assert(audio_stopped&&output_stopped); // never claim ended before local shutdown
if(!strcmp(text,"通话结束"))ended_frames++;
if(!strcmp(text,"连接失败"))failed_frames++;
if(!strcmp(text,"连接中断"))interrupted_frames++;
}
return 0;
}
static void platform_conversation_observe(const char *type){(void)type;}
static bool platform_runtime_rtc(void){return true;}
static bool platform_conversation_audio_authorized(void){return true;}
static int platform_conversation_begin(void){return scenario==BEGIN_FAILURE?-1:0;}
static const char *platform_last_error_code(void){return "";}
static const char *call_client_start_error(void){return "";}
static const char *platform_conversation_update_json(void){return "{}";}
static const char *platform_conversation_bot_id(void){return "test";}
static const char *volc_current_user_id(void *engine,bool remote){(void)engine;(void)remote;return "test";}
static void process_platform_reports(void){}
static void platform_conversation_stop_result(bool started,int result){(void)started;(void)result;}
static void platform_conversation_finish(bool failed,const char *error){
(void)failed;(void)error;
if(scenario==AUDIO_STOP_FAILURE){assert(!strcmp(text,"正在结束"));return;}
assert(ended_frames+failed_frames+interrupted_frames>0); // visible before HTTP ACK
assert(sdk_stopped);
if(scenario==SLOW_END_ACK)now_ms+=5000;
}
static bool conversation_output_allowed(void){return !stop_requested;}
static bool volc_hal_player_voice_quiet(void *player,unsigned ms){(void)player;(void)ms;return true;}
` + section('/* Called only by the conversation task,', '/* HTTPS reporting') + String.raw`
static void __audio_capture_cb(void){}
static void *volc_hal_capture_create(const volc_hal_capture_config_t *cfg){(void)cfg;return (void*)2;}
static int volc_hal_capture_stop(void *capture){(void)capture;if(stop_requested&&scenario==AUDIO_STOP_FAILURE)return -1;audio_stopped=true;return 0;}
static int volc_hal_capture_update_config(void *capture,const volc_hal_capture_config_t *cfg){(void)capture;(void)cfg;return 0;}
static int volc_hal_capture_start(void *capture,int mode){(void)capture;if(mode==VOLC_AUDIO_MODE_CAPTURE)audio_stopped=false;return 0;}
static void *volc_hal_player_create(const volc_hal_player_config_t *cfg){(void)cfg;return (void*)3;}
static void volc_hal_player_set_authorization_gate(void *player,bool(*gate)(void)){(void)player;(void)gate;}
static void volc_hal_player_start(void *player){(void)player;output_stopped=false;}
static int volc_hal_player_stop(void *player){(void)player;output_stopped=true;return 0;}
static void volc_hal_player_discard_pending(void *player){(void)player;}
static void volc_hal_player_destroy(void *player){(void)player;}
static int volc_start(void *engine,const volc_opt_t *opt){
(void)engine;(void)opt;
if(scenario==START_FAILURE)return -1;
if(scenario!=DELAYED_CONNECTION&&scenario!=TIMEOUT_WITHOUT_CONNECTION){volc_event_t event={VOLC_EV_CONNECTED};__on_volc_event(NULL,&event,NULL);}
return 0;
}
static int volc_stop(void *engine){(void)engine;sdk_stopped=true;return 0;}
static int platform_conversation_poll(void){
polls++;
if(scenario==VOICE_GOODBYE||scenario==VOICE_RETRACT||scenario==VOICE_MISSING_REPLY){
if(polls==1)process_voice_end_transcript("subject","就聊到这吧",true);
if(scenario==VOICE_GOODBYE&&polls==2)__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_ANSWER_FINISH,NULL);
if(scenario==VOICE_RETRACT&&polls==2)process_voice_end_transcript("subject","等等,我还想聊",true);
if(scenario==VOICE_RETRACT&&polls==3){assert(!voice_end.pending);conv_ai_service_task_stop();}
assert(polls<15);return 0;
}
if(scenario==IDLE_CONNECTED){
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_LISTENING,NULL);
return 0; // Persistent listening + cloud renewals must still time out.
}
if(scenario==ACTIVE_LONG_CALL){
if(polls<=150&&polls%30==1)call_idle_activity();
return 0; // Activity keeps a call alive beyond 60s, then silence ends it.
}
if(scenario==TIMEOUT_WITHOUT_CONNECTION){assert(!talking_frames);return 0;}
if(scenario==DELAYED_CONNECTION){assert(!talking_frames);assert(!strcmp(text,"正在连接"));}
if(scenario==REMOTE_DROP){volc_event_t event={VOLC_EV_DISCONNECTED};__on_volc_event(NULL,&event,NULL);}
else if(scenario==RESPONSE_FINISHED&&polls==1){
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_ANSWER_FINISH,NULL);
assert(!stop_requested);assert(!strcmp(text,"通话中"));assert(!ended_frames);
}else conv_ai_service_task_stop();
return 0;
}
static int volc_send_text_to_agent(void *engine,const char *text,int type,int mode){(void)engine;(void)text;(void)type;(void)mode;return 0;}
static int volc_send_message(void *engine,const char *data,size_t len,const volc_message_info_t *info){(void)engine;(void)data;(void)len;(void)info;return 0;}
static void aios_event_pub(int type,void *a,void *b){
(void)a;(void)b;assert(type==VOLC_SERVICE_AI_CONVERSATION_OVER);over_count++;
if(scenario!=AUDIO_STOP_FAILURE)assert(!strcmp(text,"待机"));
}
` + section('static void publish_call_display(', '\nstatic bool conversation_output_allowed(')
+ section('static void __on_volc_event(', '\nstatic void __on_volc_audio_data(')
+ section('void conv_ai_service_prepare_task(', '\n#if __cplusplus') + String.raw`
static void reset(int next){
scenario=next;polls=frames=ended_frames=interrupted_frames=failed_frames=ending_frames=talking_frames=over_count=0;
now_ms=10000;text[0]=0;audio_stopped=output_stopped=true;sdk_stopped=false;
memset(&call_display,0,sizeof(call_display));memset(&conv_service,0,sizeof(conv_service));
hal.player_handle[0]=(void*)3;global_display=hal.display_handle;diagnostic_say_pending=false;
conv_ai_service_prepare_task();
}
int main(void){
// State transitions are fenced by generation, even if a former terminal
// deadline or delayed CONNECTED observation is delivered after a new begin.
call_display_state_t state={0};unsigned first=call_display_begin(&state);
call_display_connected(&state,first);call_display_stopping(&state,first);
call_display_finished(&state,first,CALL_DISPLAY_STOP_NORMAL,false,100);
assert(state.phase==CALL_DISPLAY_ENDING);
call_display_finished(&state,first,CALL_DISPLAY_STOP_NORMAL,true,100);
assert(!call_display_idle_if_due(&state,first,2099));
unsigned next=call_display_begin(&state);
assert(!call_display_idle_if_due(&state,first,9999));call_display_connected(&state,first);
assert(state.phase==CALL_DISPLAY_CONNECTING);call_display_connected(&state,next);assert(state.phase==CALL_DISPLAY_TALKING);
for(int test=NORMAL;test<=VOICE_MISSING_REPLY;test++){
reset(test);conv_ai_service_task(NULL);assert(over_count==1);assert(ending_frames);
if(test==START_FAILURE||test==BEGIN_FAILURE||test==TIMEOUT_WITHOUT_CONNECTION){assert(failed_frames&& !talking_frames && !ended_frames);}
else if(test==REMOTE_DROP){assert(interrupted_frames&&!ended_frames);}
else if(test==AUDIO_STOP_FAILURE){assert(!ended_frames&&!failed_frames&&!interrupted_frames);}
else{assert(ended_frames&&!failed_frames&&!interrupted_frames);}
if(test!=AUDIO_STOP_FAILURE){assert(call_display.phase==CALL_DISPLAY_IDLE);assert(now_ms>=12000);}
if(test==IDLE_CONNECTED||test==TIMEOUT_WITHOUT_CONNECTION)assert(polls==60);
if(test==ACTIVE_LONG_CALL)assert(polls==180);
if(test==VOICE_GOODBYE)assert(polls==2&&atomic_load(&user_stop_requested));
if(test==VOICE_RETRACT)assert(polls==3);
if(test==VOICE_MISSING_REPLY)assert(polls==12&&atomic_load(&user_stop_requested));
// Late supplier turn callbacks after teardown cannot overwrite idle/end.
char before[sizeof(text)];strcpy(before,text);
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_ANSWER_FINISH,NULL);
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_LISTENING,NULL);
assert(!strcmp(before,text));
}
// Actual VAD callback records both edges, and starts a new quiet window.
int16_t quiet[320]={0}, comfort[320], speech[320];
for(int i=0;i<320;i++){comfort[i]=(i&1)?-20:20;speech[i]=(i&1)?-500:500;}
assert(!call_idle_pcm_active(quiet,sizeof(quiet)));
assert(!call_idle_pcm_active(comfort,sizeof(comfort)));
assert(call_idle_pcm_active(speech,sizeof(speech)));
assert(!call_idle_pcm_active(NULL,640)&&!call_idle_pcm_active(speech,639));
reset(NORMAL);
esp_gmf_afe_evt_t vad={ESP_GMF_AFE_EVT_VAD_START};
now_ms+=59000;call_idle_capture_event(&vad,NULL);
assert(atomic_load(&local_speech_active)&&!call_idle_expired());
vad.type=ESP_GMF_AFE_EVT_VAD_END;now_ms+=90000;call_idle_capture_event(&vad,NULL);
assert(!atomic_load(&local_speech_active)&&!call_idle_expired());
now_ms+=59999;assert(!call_idle_expired());now_ms++;assert(call_idle_expired());
now_ms=UINT32_MAX-1000ull;call_idle_activity();
now_ms+=59999;assert(!call_idle_expired());now_ms++;assert(call_idle_expired());
puts("PASS call display + idle: actual task/callbacks, connected-only talking, local shutdown before terminal, start failure, provider drop, answer-finish continuity, slow end ACK, failed local stop, 60s quiet timeout, 180s activity call, VAD edges, silence/comfort PCM rejection, clock wrap, generation-fenced hold");
return 0;
}
`);