@ -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/event s + 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 ; }