From 61818572cc7ea06f6a917aa153d516bedb8a901c Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 19:10:49 +0200 Subject: [PATCH] Verify speaker timeout and record completed radio indication --- doc/tasks.md | 8 +++++ tests/test_radio.c | 77 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/doc/tasks.md b/doc/tasks.md index 5feafec3..d3333f10 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -3,6 +3,14 @@ Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ. ## Текущая задача +[+] **Динамик участника показывает речь в рации ChatGUI** — 07.10.2026: + без волн — молчит, с волнами — говорит, красное перечёркивание — локально заглушён (приоритет над речью). + START/STOP обновляют строки через Qt bridge; собственная передача учитывает PTT, хоткеи и VAD. + Обновления списка/RTT сохраняют индикацию; смена канала, выключение рации и stop очищают её. + Исправлен сдвиг индексов строк при удалении участника. Добавлены переводы подсказок и логи radio. + Чистая сборка desktop, 4 Qt-теста и сетевой test_radio с FIN/таймаутом/отпиской — PASS; + полный check.sh — 138 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS. Формы значков проверены визуально. + [+] **Случайный зашифрованный padding ETCP** — 07.10.2026: keepalive UDP/TCP, UDP PING/PONG и STCP-handshake используют нулевой padding переменной длины. Каждый ETCP_CONN хранит свой диапазон и таймер; отдельный диапазон instance обслуживает пинги без соединения. diff --git a/tests/test_radio.c b/tests/test_radio.c index e86309ae..221d3b47 100644 --- a/tests/test_radio.c +++ b/tests/test_radio.c @@ -9,6 +9,7 @@ // 2. A: radio_talk_begin + 5 кадров + radio_talk_end → B и C (через relay B) получают // кадры в порядке seq 0..4 и FIN. // 3. C radio_off (обрезание) → A снова говорит → B получает, C не получает. +// 4. Без FIN: C снимает индикацию при radio_off, B — по таймауту источника. #include "chat_core.h" #include "chat_sync.h" @@ -63,6 +64,10 @@ enum rad_phase { P_WAIT_RX2, P_REENABLE_C, P_WAIT_REENABLE_C, + P_TALK_STALLED, + P_WAIT_STALLED_START, + P_WAIT_STALLED_STOP, + P_WAIT_RESTORED_C, P_WITHDRAW_C, P_DONE, }; @@ -97,9 +102,30 @@ static struct rx_evt g_rx[3]; static int g_member_radio[3][3]; static int g_member_lists[3]; static int g_member_event_bad; +static int g_talk_on[3], g_talk_starts[3], g_talk_stops[3]; /* Проверяем именно UI-события: self, прямой пир и пир за посредником. */ static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type == CHAT_EVT_RADIO_TALK) { + uint64_t group, source; + if (!data || len != 19 || data[18] > 1) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: invalid talk event bytes=%d", len); g_member_event_bad = 1; return; + } + memcpy(&group, data, 8); memcpy(&source, data + 8, 8); + int dst = -1; + for (int i = 0; i < 3; ++i) if (inst->node_id == g_sh.nid[i]) dst = i; + if (dst < 0 || group != strtoull(CH_ID, NULL, 10) || source != g_sh.nid[IDX_A]) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: unexpected talk dst=%d group=%016llx source=%016llx", + dst, (unsigned long long)group, (unsigned long long)source); + g_member_event_bad = 1; return; + } + g_talk_on[dst] = data[18]; + if (data[18]) ++g_talk_starts[dst]; + else ++g_talk_stops[dst]; + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "test: talk event dst=%d on=%d starts=%d stops=%d", + dst, data[18], g_talk_starts[dst], g_talk_stops[dst]); + return; + } if (type != CHAT_EVT_MEMBER_UPDATED && type != CHAT_EVT_MEMBER_LIST) return; if (!data || len < 3) { g_member_event_bad = 1; return; } int cl = data[0]; @@ -446,6 +472,48 @@ static void rad_tick(void* arg) { t->phase = P_WAIT_REENABLE_C; break; case P_WAIT_REENABLE_C: + if (node_radio(A, nc) && node_radio(B, nc)) t->phase = P_TALK_STALLED; + break; + case P_TALK_STALLED: { + const uint8_t opus[] = {0xf8, 0xff, 0xfe}; + memset(g_rx, 0, sizeof(g_rx)); + if (radio_talk_begin(A, gid) < 0 || radio_talk_send(A, gid, opus, sizeof(opus)) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: cannot start burst without FIN"); + t->result = 2; uasync_stop(t->ua); return; + } + t->phase = P_WAIT_STALLED_START; + break; + } + case P_WAIT_STALLED_START: + if (g_rx[IDX_B].frames && g_rx[IDX_C].frames) { + if (!g_talk_on[IDX_B] || !g_talk_on[IDX_C] || g_member_event_bad) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: missing talk START B=%d C=%d bad=%d", + g_talk_on[IDX_B], g_talk_on[IDX_C], g_member_event_bad); + t->result = 2; uasync_stop(t->ua); return; + } + radio_set_active(C, gid, 0); + if (g_talk_on[IDX_C] || !g_talk_on[IDX_B]) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: subscription stop did not clear only C: B=%d C=%d", + g_talk_on[IDX_B], g_talk_on[IDX_C]); + t->result = 2; uasync_stop(t->ua); return; + } + t->phase = P_WAIT_STALLED_STOP; + } + break; + case P_WAIT_STALLED_STOP: + if (!g_talk_on[IDX_B]) { + if (g_rx[IDX_B].fin_seen || g_rx[IDX_C].fin_seen || g_member_event_bad) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: stopped indication without FIN check failed B_fin=%d C_fin=%d bad=%d", + g_rx[IDX_B].fin_seen, g_rx[IDX_C].fin_seen, g_member_event_bad); + t->result = 2; uasync_stop(t->ua); return; + } + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test: talk STOP without FIN verified (C unsubscribe, B timeout)"); + radio_talk_end(A, gid); + radio_set_active(C, gid, 1); + t->phase = P_WAIT_RESTORED_C; + } + break; + case P_WAIT_RESTORED_C: if (node_radio(A, nc) && node_radio(B, nc)) t->phase = P_WITHDRAW_C; break; case P_WITHDRAW_C: { @@ -466,6 +534,13 @@ static void rad_tick(void* arg) { break; } case P_DONE: + if (g_member_event_bad || g_talk_starts[IDX_B] != 4 || g_talk_stops[IDX_B] != 4 || + g_talk_starts[IDX_C] != 3 || g_talk_stops[IDX_C] != 3 || g_talk_on[IDX_B] || g_talk_on[IDX_C]) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: talk lifecycle mismatch B=%d/%d C=%d/%d on=%d/%d bad=%d", + g_talk_starts[IDX_B], g_talk_stops[IDX_B], g_talk_starts[IDX_C], g_talk_stops[IDX_C], + g_talk_on[IDX_B], g_talk_on[IDX_C], g_member_event_bad); + t->result = 2; uasync_stop(t->ua); return; + } t->result = 1; uasync_stop(t->ua); return; @@ -508,7 +583,7 @@ int main(int argc, char** argv) { printf("=== test_radio ===\n"); fflush(stdout); - TEST("radio: member snapshots/events + local mute relay + off + withdraw"); { + TEST("radio: members + mute relay + speaker FIN/timeout/off + withdraw"); { struct UASYNC* ua = uasync_create(); if (!ua) { FAIL("uasync_create failed"); return 1; }