Browse Source

Verify speaker timeout and record completed radio indication

master
evgeny 4 days ago
parent
commit
61818572cc
  1. 8
      doc/tasks.md
  2. 77
      tests/test_radio.c

8
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 обслуживает пинги без соединения.

77
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; }

Loading…
Cancel
Save