Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
python3 -m unittest discover -s scripts/tests -p 'test_package_firmware.py' -v
python3 -m unittest discover -s scripts/tests -p 'test_idf_tab5_compat.py' -v
python3 -m unittest discover -s scripts/tests -p 'test_nvs_diagnostics.py' -v
python3 -m unittest discover -s scripts/tests -p 'test_tab5_camera_diagnostics.py' -v

- name: Build
uses: espressif/esp-idf-ci-action@v1
Expand All @@ -81,20 +82,29 @@ jobs:
command: |
nvs_diagnostics="" &&
sdio_diagnostics="" &&
camera_compat="" &&
if [ "${{ matrix.name }}" = "m5stack-tab5-room-node" ]; then
nvs_diagnostics="--nvs-diagnostics" &&
sdio_diagnostics="--sdio-diagnostics --sdio-psram-rx" &&
camera_compat="--camera-compat" &&
python3 ../../scripts/idf_tab5_compat.py --idf-path "$IDF_PATH" --nvs-diagnostics
fi &&
idf.py reconfigure &&
if [ "${{ matrix.name }}" = "m5stack-tab5-room-node" ]; then
python3 ../../scripts/tab5_sdio_diagnostics.py \
--project-path . --component-path managed_components/espressif__esp_hosted --build-path build --psram-rx &&
python3 ../../scripts/tab5_camera_compat.py --idf-path "$IDF_PATH" \
--project-path . --component-path managed_components/espressif__esp_video --build-path build &&
python3 -m unittest discover -s ../../scripts/tests -p test_tab5_sdio_rx_psram.py -v &&
python3 -m unittest discover -s ../../scripts/tests -p test_tab5_sdio_diagnostics.py -v &&
python3 -m unittest discover -s ../../scripts/tests -p test_tab5_camera_compat.py -v &&
python3 -m unittest discover -s ../../scripts/tests -p test_package_firmware.py -v
fi &&
ninja -C build -j2 &&
if [ "${{ matrix.name }}" = "m5stack-tab5-room-node" ]; then
python3 ../../scripts/tab5_camera_compat.py --idf-path "$IDF_PATH" \
--project-path . --component-path managed_components/espressif__esp_video --build-path build --verify-only
fi &&
if [ "${{ matrix.name }}" = "component-test-app-build" ]; then
python3 ../../../esp-openclaw-talk/tests/run_host_tests.py --sanitize &&
python3 ../../../esp-openclaw-talk/tests/run_threaded_tests.py &&
Expand All @@ -114,7 +124,7 @@ jobs:
--run-id "${{ github.run_id }}" \
--run-attempt "${{ github.run_attempt }}" \
--idf-image "espressif/idf:release-v5.5" \
$nvs_diagnostics $sdio_diagnostics
$nvs_diagnostics $sdio_diagnostics $camera_compat
fi

- name: Upload firmware
Expand Down
102 changes: 88 additions & 14 deletions components/esp-openclaw-room-node/esp_openclaw_room_node.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ static char *gateway_http_base;
* generation-checked facts under state_lock are the only work authority. */
typedef uint8_t talk_teardown_request_t;

static int talk_stage(uint32_t generation, const char *stage, const char *phase, int result)
{
ESP_LOGI(TAG, "room_talk_diag generation=%" PRIu32 " stage=%s phase=%s result=%d",
generation, stage, phase, result);
return result;
}

static void talk_audio_snapshot(uint32_t generation, const char *stage)
{
room_audio_diagnostics_snapshot_t audio = {0};
room_diagnostics_audio_get(&audio);
ESP_LOGI(TAG,
"room_talk_audio generation=%" PRIu32 " stage=%s capture_ok=%" PRIu64
" capture_err=%" PRIu64 " feed_ok=%" PRIu64 " feed_err=%" PRIu64
" fetch_ok=%" PRIu64 " fetch_err=%" PRIu64 " render_ok=%" PRIu64 " render_err=%" PRIu64,
generation, stage, audio.capture_read_successes, audio.capture_read_errors,
audio.feed_successes, audio.feed_errors, audio.fetch_successes, audio.fetch_errors,
audio.renderer_accepted, audio.renderer_errors);
}

static void refresh_home_facts(void)
{
xSemaphoreTake(state_lock, portMAX_DELAY);
Expand Down Expand Up @@ -313,8 +333,9 @@ static bool request_talk_stop_locked(uint32_t generation, const char *message)
static void request_talk_teardown(uint32_t generation, const char *message)
{
xSemaphoreTake(state_lock, portMAX_DELAY);
request_talk_stop_locked(generation, message);
bool accepted = request_talk_stop_locked(generation, message);
xSemaphoreGive(state_lock);
talk_stage(generation, "stop_request", accepted ? "accepted" : "ignored", 0);
}

static void talk_teardown_task(void *arg)
Expand All @@ -337,6 +358,7 @@ static void talk_teardown_task(void *arg)
esp_webrtc_handle_t session = webrtc;
esp_openclaw_talk_call_handle_t call = talk_call;
const char *message = talk_cancel_message;
uint32_t generation = talk_generation;
xSemaphoreGive(state_lock);
refresh_home_facts();
/* One worker serializes call/operator UI. A delayed callback never
Expand All @@ -353,18 +375,36 @@ static void talk_teardown_task(void *arg)

/* The owner remains reserved until every local resource and UI update
* is finished. Pending RPC refs may survive, but cannot dispatch. */
talk_stage(generation, "stop_request", "consumed", 0);
talk_audio_snapshot(generation, "teardown");
talk_stage(generation, "quiesce", "begin", 0);
esp_openclaw_talk_call_quiesce(call);
talk_stage(generation, "quiesce", "end", 0);
if (talk_timeout_timer != NULL) {
xTimerStop(talk_timeout_timer, portMAX_DELAY);
xTimerDelete(talk_timeout_timer, portMAX_DELAY);
talk_timeout_timer = NULL;
}
if (session != NULL) esp_webrtc_close(session);
if (talk_ambient_suspended && room_media_set_ambient_wake(true) != ESP_OK) {
ESP_LOGE(TAG, "failed to restore ambient WakeNet after Talk");
message = "Talk failed";
if (session != NULL) {
talk_stage(generation, "sdk_close", "begin", 0);
int result = esp_webrtc_close(session);
talk_stage(generation, "sdk_close", "end", result);
}
if (talk_ambient_suspended) {
talk_stage(generation, "ambient_restore", "begin", 0);
esp_err_t result = room_media_set_ambient_wake(true);
talk_stage(generation, "ambient_restore", "end", result);
if (result != ESP_OK) {
ESP_LOGE(TAG, "failed to restore ambient WakeNet after Talk");
message = "Talk failed";
}
}
if (talk_media_owned) {
talk_stage(generation, "media_release", "begin", 0);
room_media_end_talk(!talk_ambient_suspended);
talk_stage(generation, "media_release", "end", 0);
}
if (talk_media_owned) room_media_end_talk(!talk_ambient_suspended);
talk_audio_snapshot(generation, "released");
room_ui_set(message != NULL ? ROOM_UI_ERROR : ROOM_UI_IDLE, message);
xSemaphoreTake(state_lock, portMAX_DELAY);
webrtc = NULL;
Expand Down Expand Up @@ -415,6 +455,16 @@ static int webrtc_event(esp_webrtc_event_t *event, void *ctx)
event->type == ESP_WEBRTC_EVENT_CONNECT_FAILED ? "Talk failed" : NULL);
}
xSemaphoreGive(state_lock);
const char *stage = event->type == ESP_WEBRTC_EVENT_CONNECTING ? "peer_connecting" :
event->type == ESP_WEBRTC_EVENT_PAIRED ? "peer_paired" :
event->type == ESP_WEBRTC_EVENT_CONNECTED ? "peer_connected" :
event->type == ESP_WEBRTC_EVENT_CONNECT_FAILED ? "peer_failed" :
event->type == ESP_WEBRTC_EVENT_DISCONNECTED ? "peer_disconnected" : NULL;
if (stage != NULL) {
talk_stage(generation, stage, current ? "current" : "stale",
(int)event->type);
if (current) talk_audio_snapshot(generation, stage);
}
return 0;
}

Expand Down Expand Up @@ -465,8 +515,12 @@ static void start_talk_once(void)
bool admitted = call != NULL && talk_start_in_flight;
xSemaphoreGive(state_lock);
if (!admitted) return;
talk_stage(generation, "startup", "begin", 0);
if (cancelled) goto done;
talk_stage(generation, "media_acquire", "begin", 0);
room_media_begin_talk();
talk_stage(generation, "media_acquire", "end", 0);
talk_audio_snapshot(generation, "acquired");
xSemaphoreTake(state_lock, portMAX_DELAY);
talk_media_owned = true;
cancelled = talk_cancel_requested;
Expand All @@ -487,7 +541,16 @@ static void start_talk_once(void)
.signaling_impl = esp_openclaw_talk_call_signaling_impl(),
};
esp_webrtc_handle_t session = NULL;
if (esp_webrtc_open(&config, &session) != 0) {
/* This SDK tag logs outbound SDP at INFO. Set before any negotiation. */
esp_log_level_set("webrtc", ESP_LOG_WARN);
if (esp_log_level_get("webrtc") > ESP_LOG_WARN) {
talk_stage(generation, "sdk_log_policy", "end", ESP_FAIL);
request_talk_teardown(generation, "Talk setup failed");
goto done;
}
talk_stage(generation, "sdk_log_policy", "end", ESP_OK);
talk_stage(generation, "sdk_open", "begin", 0);
if (talk_stage(generation, "sdk_open", "end", esp_webrtc_open(&config, &session)) != 0) {
request_talk_teardown(generation, "WebRTC open");
goto done;
}
Expand All @@ -496,10 +559,11 @@ static void start_talk_once(void)
talk_dialing = true;
xSemaphoreGive(state_lock);
esp_webrtc_media_provider_t media = {0};
if (room_media_get_webrtc_provider(&media) != ESP_OK ||
esp_webrtc_set_media_provider(session, &media) != 0 ||
esp_webrtc_set_no_auto_capture(session, true) != 0 ||
esp_webrtc_set_event_handler(session, webrtc_event, (void *)(uintptr_t)generation) != 0) {
if (talk_stage(generation, "media_provider", "end", room_media_get_webrtc_provider(&media)) != ESP_OK ||
talk_stage(generation, "sdk_media_provider", "end", esp_webrtc_set_media_provider(session, &media)) != 0 ||
talk_stage(generation, "sdk_capture_policy", "end", esp_webrtc_set_no_auto_capture(session, true)) != 0 ||
talk_stage(generation, "sdk_event_handler", "end",
esp_webrtc_set_event_handler(session, webrtc_event, (void *)(uintptr_t)generation)) != 0) {
request_talk_teardown(generation, "WebRTC start");
goto done;
}
Expand All @@ -508,16 +572,24 @@ static void start_talk_once(void)
if (!cancelled) talk_ambient_suspended = true;
xSemaphoreGive(state_lock);
if (cancelled) goto done;
if (room_media_set_ambient_wake(false) != ESP_OK || esp_webrtc_start(session) != 0 ||
xTimerStart(talk_timeout_timer, 0) != pdPASS) {
talk_stage(generation, "ambient_suspend", "begin", 0);
if (talk_stage(generation, "ambient_suspend", "end", room_media_set_ambient_wake(false)) != ESP_OK) {
request_talk_teardown(generation, "Talk setup failed");
goto done;
}
talk_stage(generation, "sdk_start", "begin", 0);
if (talk_stage(generation, "sdk_start", "end", esp_webrtc_start(session)) != 0 ||
talk_stage(generation, "timer_start", "end", xTimerStart(talk_timeout_timer, 0)) != pdPASS) {
request_talk_teardown(generation, "Talk setup failed");
}

done:
xSemaphoreTake(state_lock, portMAX_DELAY);
talk_start_in_flight = false;
if (talk_cancel_requested) wake_talk_teardown();
cancelled = talk_cancel_requested;
xSemaphoreGive(state_lock);
talk_stage(generation, "startup", cancelled ? "canceled" : "end", 0);
}

static void on_wake(const char *wake_word, void *ctx)
Expand Down Expand Up @@ -969,8 +1041,10 @@ static esp_err_t handle_talk_stop(
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(state_lock, portMAX_DELAY);
bool active = request_talk_stop_locked(talk_generation, NULL);
uint32_t generation = talk_generation;
bool active = request_talk_stop_locked(generation, NULL);
xSemaphoreGive(state_lock);
talk_stage(generation, "stop_request", active ? "accepted" : "ignored", 0);
*out_payload_json = strdup(
active ? "{\"stopped\":true}" : "{\"stopped\":false}");
if (*out_payload_json == NULL) {
Expand Down
17 changes: 17 additions & 0 deletions components/esp-openclaw-room-node/tests/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Talk lifetime source proofs

## Media diagnostic regressions

The Talk pthread suite now captures the actual adapter's fixed-field records,
injects HTTP initialization/transport/status/body failures and callback return
errors, and checks first-failure retention, ignored-return behavior, late
cancellation and secret exclusion. The room lifecycle fixture checks actual
startup failure short circuits, ordered teardown stages, SDK log suppression
before negotiation, and existing audio-counter snapshots outside locks.

`python3 -m unittest discover -s scripts/tests -p test_tab5_camera_diagnostics.py -v`
compiles the actual Tab5 capture/cleanup functions with narrow synthetic V4L2
boundaries under ASan/UBSan. It covers immediate errno retention across logging
and failing cleanup, stale errno on validation, partial mappings, initialization
caching, RGB565 variants and bounded dequeue-loop records. This is not V4L2 ABI,
DMA, sensor or physical frame proof. `TAB5_CAMERA_SOURCE` can select a trusted
baseline source file for a red comparison without editing the worktree.

## Static home UI

The UI host runner compiles the real UI controller and board binding with LVGL 9,
Expand Down
3 changes: 3 additions & 0 deletions components/esp-openclaw-room-node/tests/host/esp_log.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#pragma once
typedef enum { ESP_LOG_NONE, ESP_LOG_ERROR, ESP_LOG_WARN, ESP_LOG_INFO } esp_log_level_t;
void esp_log_level_set(const char *tag, esp_log_level_t level);
esp_log_level_t esp_log_level_get(const char *tag);
void host_log(const char *tag, const char *format, ...)
__attribute__((format(printf, 2, 3)));
#define ESP_LOGE(...) host_log(__VA_ARGS__)
Expand Down
5 changes: 5 additions & 0 deletions components/esp-openclaw-room-node/tests/host/esp_rom_sys.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#pragma once
#ifdef __APPLE__
/* Device ELF placement is not meaningful in the Mach-O host fixture. */
#undef DRAM_STR
#define DRAM_STR(value) (value)
#endif
int esp_rom_printf(const char *format, ...)
__attribute__((format(printf, 1, 2)));
46 changes: 42 additions & 4 deletions components/esp-openclaw-room-node/tests/host/room_host_fakes.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,41 @@ void host_require(bool condition, const char *message)

void host_log(const char *tag, const char *format, ...)
{
char line[1024];
va_list args;
va_start(args, format);
fprintf(stderr, "[%s] ", tag);
vfprintf(stderr, format, args);
fputc('\n', stderr);
int length = vsnprintf(line, sizeof(line), format, args);
va_end(args);
host_require(length >= 0 && (size_t)length < sizeof(line), "bounded fixture log");
bool diagnostic = strncmp(line, "room_talk_", 10) == 0 || strncmp(line, "talk_rtc_", 9) == 0;
if (diagnostic) {
host_require(mutex_depth == 0 && host.critical_depth == 0, "media diagnostic outside locks");
host_require(strstr(line, "voice-a") == NULL && strstr(line, "synthetic") == NULL &&
strstr(line, "gateway.example") == NULL && strstr(line, "agent:fixture") == NULL,
"media diagnostics exclude secret canaries");
if (host.capture_diagnostics) {
size_t offset = strlen(host.diagnostics);
host_require(offset + (size_t)length + 2 < sizeof(host.diagnostics), "fixture diagnostic capacity");
memcpy(host.diagnostics + offset, line, (size_t)length);
host.diagnostics[offset + (size_t)length] = '\n';
host.diagnostics[offset + (size_t)length + 1] = '\0';
}
}
fprintf(stderr, "[%s] %s\n", tag, line);
}

void esp_log_level_set(const char *tag, esp_log_level_t level)
{
host_require(mutex_depth == 0 && host.critical_depth == 0, "log policy outside locks");
host_require(strcmp(tag, "webrtc") == 0 && level == ESP_LOG_WARN, "only unsafe SDK SDP INFO suppressed");
if (!host.fail_log_policy) host.sdk_log_suppressed = true;
}

esp_log_level_t esp_log_level_get(const char *tag)
{
host_require(mutex_depth == 0 && host.critical_depth == 0, "log policy readback outside locks");
host_require(strcmp(tag, "webrtc") == 0, "only SDK SDP tag read back");
return host.sdk_log_suppressed ? ESP_LOG_WARN : ESP_LOG_INFO;
}

int esp_rom_printf(const char *format, ...)
Expand Down Expand Up @@ -531,6 +560,7 @@ static int signal_message(esp_peer_signaling_msg_t *message, void *ctx)
{ (void)message; (void)ctx; host_require(false, "no SDP exchange in this suite"); return -1; }
int esp_webrtc_start(esp_webrtc_handle_t session)
{
host_require(host.sdk_log_suppressed, "SDK SDP INFO suppressed before negotiation");
if (host.fail_start) return -1;
fake_rtc_t *rtc = session;
esp_peer_signaling_cfg_t config = {
Expand Down Expand Up @@ -646,7 +676,15 @@ esp_err_t room_diagnostics_request_open(void)
esp_err_t room_diagnostics_request_close(void)
{ unsupported_boundary(__func__); }
void room_diagnostics_audio_get(room_audio_diagnostics_snapshot_t *snapshot)
{ (void)snapshot; unsupported_boundary(__func__); }
{
host_require(mutex_depth == 0 && host.critical_depth == 0, "audio snapshot outside locks");
++host.audio_snapshots;
*snapshot = (room_audio_diagnostics_snapshot_t){
.capture_read_successes = 11, .capture_read_errors = 2,
.feed_successes = 13, .feed_errors = 3, .fetch_successes = 17,
.fetch_errors = 5, .renderer_accepted = 19, .renderer_errors = 7,
};
}
esp_err_t room_media_request_test_tone(room_media_talk_busy_cb_t busy_cb, void *ctx)
{ (void)busy_cb; (void)ctx; unsupported_boundary(__func__); }
void room_media_get_tone_snapshot(room_media_tone_snapshot_t *snapshot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ typedef struct {
bool fail_config_submit, fail_create_submit, fail_open, fail_provider, fail_start, fail_timer;
const char *node_uri;
const char *create_voice;
bool sdk_log_suppressed, fail_log_policy, capture_diagnostics;
unsigned audio_snapshots;
char diagnostics[16384];
} room_host_observations_t;
extern room_host_observations_t host;

Expand Down
Loading
Loading