diff --git a/tests/test_radio.c b/tests/test_radio.c index 12f872f4..6914b464 100644 --- a/tests/test_radio.c +++ b/tests/test_radio.c @@ -20,6 +20,7 @@ #include "member_sync.h" #include "../radio/radio.h" #include "../radio/radio_call.h" +#include "../call/call.h" #include "../routing_layer/etcp_router.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_group.h" @@ -52,7 +53,7 @@ #define IDX_C 2 #define TICK_TB 100 /* 10 ms на такт */ -#define MAX_TICKS 12000 /* включает минуту ожидания решения по вызову */ +#define MAX_TICKS 24000 /* включает минутные deadline звонка и выбора продолжения */ #define N_FRAMES 5 enum rad_phase { @@ -111,8 +112,9 @@ 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]; -static struct { int state, reason, finished, incoming; uint64_t finished_tb; } g_calls[8][3]; -static int g_call_ready, g_drop_result, g_drop_ack; +static struct { int state, reason, finished, incoming; uint64_t finished_tb; } g_calls[20][3]; +static int g_call_ready, g_drop_result, g_drop_ack, g_drop_force; +static int g_member_phone[3][3]; static etcp_recv_fn g_call_recv[3]; /* Потеря приложения после router ACK: восстановить результат обязан сервис, не транспорт. */ @@ -122,7 +124,8 @@ static void call_packet_loss(struct ETCP_CONN* conn, struct ll_entry* entry) { if (idx < 0 || !entry) abort(); if (entry->len <= ROUTER_SVC_PAYLOAD_OFF) { g_call_recv[idx](conn, entry); return; } int cmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF]; - if ((idx == IDX_A && cmd == 3 && g_drop_result-- > 0) || (idx == IDX_C && cmd == 4 && g_drop_ack-- > 0)) { + if ((idx == IDX_A && cmd == 3 && g_drop_result-- > 0) || (idx == IDX_C && cmd == 4 && g_drop_ack-- > 0) || + (idx == IDX_C && cmd == 6 && g_drop_force-- > 0)) { DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test drops radio result/ACK dst=%d command=%d", idx, cmd); queue_dgram_free(entry); queue_entry_free(entry); return; } @@ -163,7 +166,7 @@ static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* da memcpy(&id, data, 8); memcpy(&group, data + 8, 8); memcpy(&peer, data + 16, 8); int idx = -1; for (int i = 0; i < 3; ++i) if (inst->node_id == g_sh.nid[i]) idx = i; - if (idx < 0 || id < 100 || id > 107 || group != strtoull(CH_ID, NULL, 10)) { g_member_event_bad = 1; return; } + if (idx < 0 || id < 100 || id > 119 || group != strtoull(CH_ID, NULL, 10)) { g_member_event_bad = 1; return; } g_calls[id - 100][idx].state = data[24]; g_calls[id - 100][idx].reason = data[25]; if (data[24] == RADIO_CALL_INCOMING) { ++g_calls[id - 100][idx].incoming; @@ -212,8 +215,10 @@ static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* da if (chat_core_deserialize_member(data + off + i * CHAT_MEMBER_DISPLAY_SIZE, &member) < 0) { g_member_event_bad = 1; return; } - for (int j = 0; j < 3; j++) - if (member.node_id == g_sh.nid[j]) g_member_radio[dst][j] = !!(member.flags & CHAT_MEMBER_FLAG_RADIO); + for (int j = 0; j < 3; j++) if (member.node_id == g_sh.nid[j]) { + g_member_radio[dst][j] = !!(member.flags & CHAT_MEMBER_FLAG_RADIO); + g_member_phone[dst][j] = member.flags & (CHAT_MEMBER_FLAG_PHONE_KNOWN | CHAT_MEMBER_FLAG_PHONE_BUSY); + } } } @@ -598,9 +603,69 @@ static void rad_tick(void* arg) { t->result = 2; uasync_stop(t->ua); return; } DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test radio calls passed: 2s/60s, accept/decline/cancel/busy, lost result/ACK, idle has no timer"); - t->phase = P_RADIO_ON; + call_set_phone_state(C, 2, CALL_POLICY_WARN_PHONE); + radio_action(A, 108, RADIO_CALL_START, nc); ++t->call_step; } break; + case 10: + if (g_calls[8][IDX_A].state != RADIO_CALL_WAIT_DECISION) break; + if (g_calls[8][IDX_C].incoming || g_member_phone[IDX_A][IDX_C]) goto phone_failed; + call_set_phone_state(C, 1, CALL_POLICY_WARN_PHONE); + t->call_started = now; ++t->call_step; break; + case 11: + if (now - t->call_started < 10000) break; + if (g_calls[8][IDX_C].incoming) goto phone_failed; // Освободившийся телефон сам не начинает вызов. + g_drop_force = 2; + radio_action(A, 108, RADIO_CALL_CONTINUE, 0); ++t->call_step; break; + case 12: + if (g_calls[8][IDX_A].state != RADIO_CALL_RINGING) break; + if (g_calls[8][IDX_C].incoming != 1 || g_drop_force > 0) goto phone_failed; + radio_action(C, 108, RADIO_CALL_DECLINE, 0); ++t->call_step; break; + case 13: + if (!g_calls[8][IDX_A].finished) break; + radio_action(A, 108, RADIO_CALL_CONTINUE, 0); // Запоздалое UI-подтверждение не оживляет попытку. + call_set_phone_state(C, 2, CALL_POLICY_PUBLISH_PHONE); + radio_action(A, 109, RADIO_CALL_START, nc); ++t->call_step; break; + case 14: + if (g_calls[9][IDX_A].state != RADIO_CALL_RINGING || g_member_phone[IDX_A][IDX_C] != 0xC0) break; + if (g_calls[9][IDX_C].incoming != 1 || g_calls[8][IDX_C].incoming != 1) goto phone_failed; + { + uint8_t member[CHAT_MEMBER_DISPLAY_SIZE]; + if (chat_core_get_single_member(A, CH_ID, nc, member) || (member[10] & 0xC0) != 0xC0) goto phone_failed; + } + radio_action(A, 109, RADIO_CALL_CANCEL, 0); ++t->call_step; break; + case 15: + if (!g_calls[9][IDX_C].finished) break; + call_set_phone_state(C, 2, CALL_POLICY_WARN_PHONE | CALL_POLICY_REJECT_PHONE); + radio_action(A, 110, RADIO_CALL_START, nc); ++t->call_step; break; + case 16: + if (!g_calls[10][IDX_A].finished || g_member_phone[IDX_A][IDX_C]) break; + if (g_calls[10][IDX_A].reason != RADIO_CALL_PHONE_BUSY || g_calls[10][IDX_C].incoming) goto phone_failed; + call_set_phone_state(C, 0, CALL_POLICY_BLOCK_CALLS); + radio_action(A, 111, RADIO_CALL_START, nc); ++t->call_step; break; + case 17: + if (g_calls[11][IDX_A].state != RADIO_CALL_RINGING) break; + radio_action(A, 111, RADIO_CALL_CANCEL, 0); ++t->call_step; break; + case 18: + if (!g_calls[11][IDX_C].finished) break; + call_set_phone_state(C, 2, CALL_POLICY_BLOCK_RADIO | CALL_POLICY_WARN_PHONE); + radio_action(A, 112, RADIO_CALL_START, nc); ++t->call_step; break; + case 19: + if (!g_calls[12][IDX_A].finished) break; + if (g_calls[12][IDX_A].reason != RADIO_CALL_DECLINED || g_calls[12][IDX_C].incoming) goto phone_failed; + call_set_phone_state(C, 2, CALL_POLICY_WARN_PHONE); + t->call_started = now; + radio_action(A, 113, RADIO_CALL_START, nc); ++t->call_step; break; + case 20: + if (!g_calls[13][IDX_A].finished) break; + if (g_calls[13][IDX_A].reason != RADIO_CALL_NO_ANSWER || g_calls[13][IDX_C].incoming || + now - t->call_started < RADIO_CALL_RING_TB || now - t->call_started > RADIO_CALL_RING_TB + 1000) goto phone_failed; + call_set_phone_state(C, 0, 0); + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "phone policy passed: hidden/published BGP, warn, override with loss, reject, independent blocks, 60s choice"); + t->phase = P_RADIO_ON; break; + phone_failed: + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "phone policy failed step=%d", t->call_step); + t->result = 2; uasync_stop(t->ua); return; } break; } diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 7b92fffc..6e876075 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -70,6 +70,7 @@ extern "C" { extern "C" { #include "chat/chat_core.h" #include "call/call.h" +#include "call/call_proto.h" #include "radio/radio_call.h" #include "dm/dm_core.h" #include "../../lib/mem.h" @@ -776,7 +777,7 @@ void MainWindow::onRadioCallRequested(const QString& channel, quint64 peer) { /* Один диалог на незавершённое приглашение; принятая рация живёт своим обычным циклом. */ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) { if (!data || len != 27 || data[24] < RADIO_CALL_OUTGOING || data[24] > RADIO_CALL_WAIT_DECISION || - data[25] > RADIO_CALL_AUDIO_ERROR || data[26] > 1) { + data[25] > RADIO_CALL_PHONE_BUSY || data[26] > 1) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "desktop invalid radio call event bytes=%d", len); return; } quint64 id, group, peer; @@ -798,7 +799,7 @@ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) { if (reason == RADIO_CALL_ACCEPTED) return; } if ((incoming && reason != RADIO_CALL_AUDIO_ERROR) || reason == RADIO_CALL_CANCELLED) return; - const char* text = reason == RADIO_CALL_DECLINED ? "Radio call declined" : + const char* text = reason == RADIO_CALL_PHONE_BUSY ? "Subscriber is on a phone call" : reason == RADIO_CALL_DECLINED ? "Radio call declined" : reason == RADIO_CALL_BUSY ? "Subscriber is busy" : reason == RADIO_CALL_NO_ANSWER ? "No answer" : reason == RADIO_CALL_AUDIO_ERROR ? "Could not start channel radio" : "Subscriber is unavailable"; auto* notice = new QMessageBox(QMessageBox::Information, QCoreApplication::translate("ChatGui", "Radio call"), @@ -953,11 +954,16 @@ void MainWindow::onCallAccepted(quint64 callId) { } void MainWindow::onCallEnded(quint64 callId, uint8_t reason) { - Q_UNUSED(reason); + const bool phoneBusy = reason == CALL_REASON_PHONE_BUSY && m_activeCalls.contains(callId); /* тон завершения (~450мс) → авто-teardown аудио-движка */ if (CallAudioEngine::instance()->callId() == callId) CallAudioEngine::instance()->beginEnd(callId); closeCallWindow(callId); + if (phoneBusy) { + auto* notice = new QMessageBox(QMessageBox::Information, QStringLiteral("vibechat"), + QCoreApplication::translate("ChatGui", "Subscriber is on a phone call"), QMessageBox::Ok, this); + notice->setAttribute(Qt::WA_DeleteOnClose); notice->show(); + } } void MainWindow::onCallError(quint64 callId, uint8_t err) {