You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
675 lines
31 KiB
675 lines
31 KiB
// radio.c — рация (PTT walkie-talkie): мультикаст голоса по CHAT-группе. |
|
// |
|
// Флудит Opus-кадры (ETCP_ID_RADIO) всем подписчикам канала — узлам с |
|
// TOPO_FLAG_RADIO в NODEINFO. Пакет несёт hop_list (узлы, через которые он уже |
|
// прошёл) для защиты от циклов и форвардится только в те ветви, за которыми есть |
|
// хотя бы один подписчик (RPF-обрезание по BGP-путям). Кадры нумеруются от 0, |
|
// последний кадр передачи помечается FIN. |
|
// |
|
// Приём: если мы подписаны (group->radio_active) — доставляем кадр в frame_fn |
|
// (аудио-движок radio_audio). Независимо от подписки форвардим дальше. |
|
// Все функции — в uasync-потоке. |
|
|
|
#include <stdlib.h> |
|
#include <stdio.h> |
|
#include <string.h> |
|
#include "../lib/platform_compat.h" |
|
#include "../lib/debug_config.h" |
|
#include "../lib/mem.h" |
|
#include "../lib/u_async.h" |
|
#include "../lib/ll_queue.h" |
|
#include "../transport_layer/etcp_api.h" |
|
#include "../transport_layer/etcp.h" |
|
#include "../routing_layer/topo_node.h" |
|
#include "../routing_layer/topo_group.h" |
|
#include "../routing_layer/topo_node_sqlite.h" |
|
#include "../chat/chat_event.h" |
|
#include "radio.h" |
|
#include "radio_proto.h" |
|
|
|
#define RADIO_ID "radio" |
|
|
|
#define RADIO_MAX_TALKERS 16 /* максимум одновременно говорящих источников */ |
|
|
|
#define RADIO_DEDUP_MAX 128 /* ёмкость кэша дедупликации (≈2.5с @ 50 кадров/с) */ |
|
#define RADIO_DEDUP_TTL_TB 20000 /* 2с — окно дедупликации (совпадает с таймаутом источника) */ |
|
#define RADIO_TALKER_TIMEOUT_TB 20000 /* 2с без кадров — считаем передачу завершённой (FIN потерян/пир отвалился) */ |
|
|
|
/* Запись кэша дедупликации: ключ — (src, stream, seq), tb — момент прихода. */ |
|
struct radio_seen { |
|
uint64_t src; |
|
uint16_t stream; |
|
uint16_t seq; |
|
uint64_t tb; |
|
}; |
|
|
|
struct radio_ctx { |
|
struct TOPO_GROUP* group; |
|
uint16_t stream_id; /* id текущей передачи (talk-burst) */ |
|
uint16_t seq; /* seq следующего исходящего кадра */ |
|
int talking; /* 1 = идёт передача (PTT зажата) */ |
|
uint32_t c_tx_frames; /* отправлено кадров */ |
|
uint32_t c_rx_frames; /* доставлено локально (мы подписаны) */ |
|
uint32_t c_fwd_frames; /* форварднуто кадров */ |
|
uint32_t c_loop_dropped; /* дропнуто из-за цикла (self в hop_list) */ |
|
uint32_t c_nosub_dropped; /* не отправлено: за соседом нет подписчиков */ |
|
uint32_t c_bad_dropped; /* дропнуто некорректных пакетов */ |
|
uint32_t c_dup_dropped; /* дропнуто дублей (src,stream,seq) пришедших по другому пути */ |
|
struct radio_talker { uint64_t src; uint16_t stream; uint64_t last_tb; } talkers[RADIO_MAX_TALKERS]; /* активные говорящие */ |
|
void* talker_timer; /* периодический sweep зависших говорящих (self-rearming) */ |
|
struct radio_seen seen[RADIO_DEDUP_MAX]; /* кэш дедупликации */ |
|
int seen_count; |
|
uint64_t* muted_members; /* локальный кэш, принадлежит uasync-потоку */ |
|
int muted_count; |
|
int muted_loaded; |
|
}; |
|
|
|
struct radio_instance { |
|
radio_frame_fn frame_fn; |
|
void* frame_arg; |
|
radio_mute_fn mute_fn; |
|
void* mute_arg; |
|
}; |
|
|
|
static void radio_talker_timer_cb(void* arg); |
|
|
|
static struct radio_ctx* radio_of(struct TOPO_GROUP* group) { |
|
return group ? group->radio : NULL; |
|
} |
|
|
|
static struct radio_instance* radio_inst(struct UTUN_INSTANCE* inst) { |
|
return inst ? inst->radio : NULL; |
|
} |
|
|
|
int radio_init(struct TOPO_GROUP* group) { |
|
if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: init null group", RADIO_ID); return -1; } |
|
if (group->radio) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: already initialized grp=%016llx", RADIO_ID, (unsigned long long)group->group_id); return -1; } |
|
|
|
struct radio_ctx* ctx = u_calloc(1, sizeof(struct radio_ctx)); |
|
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: alloc failed", RADIO_ID); return -1; } |
|
ctx->group = group; |
|
ctx->stream_id = (uint16_t)(get_time_tb() & 0xFFFF); |
|
group->radio = ctx; |
|
|
|
ctx->talker_timer = uasync_set_timeout(group->instance->ua, RADIO_TALKER_TIMEOUT_TB, ctx, radio_talker_timer_cb, "radio_talker"); |
|
if (!ctx->talker_timer) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: init talker_timer failed grp=%016llx", RADIO_ID, (unsigned long long)group->group_id); |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: init ok grp=%016llx", RADIO_ID, (unsigned long long)group->group_id); |
|
return 0; |
|
} |
|
|
|
void radio_destroy(struct TOPO_GROUP* group) { |
|
if (!group || !group->radio) return; |
|
struct radio_ctx* ctx = group->radio; |
|
if (ctx->talker_timer) { uasync_cancel_timeout(group->instance->ua, ctx->talker_timer); ctx->talker_timer = NULL; } |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroy grp=%016llx (tx=%u rx=%u fwd=%u loop=%u nosub=%u bad=%u dup=%u)", |
|
RADIO_ID, (unsigned long long)group->group_id, |
|
ctx->c_tx_frames, ctx->c_rx_frames, ctx->c_fwd_frames, |
|
ctx->c_loop_dropped, ctx->c_nosub_dropped, ctx->c_bad_dropped, ctx->c_dup_dropped); |
|
u_free(ctx->muted_members); |
|
u_free(ctx); |
|
group->radio = NULL; |
|
} |
|
|
|
/* ── сборка payload ── */ |
|
|
|
/* Пишет payload рации (начиная с group_id) в buf, возвращает длину. */ |
|
static size_t radio_build(uint8_t* buf, uint64_t group_id, uint64_t src, uint16_t stream, |
|
uint16_t seq, uint8_t flags, const uint64_t* hops, uint8_t hop_count, |
|
const uint8_t* opus, int opus_len) { |
|
size_t off = 0; |
|
memcpy(buf + off, &group_id, 8); off += 8; |
|
memcpy(buf + off, &src, 8); off += 8; |
|
memcpy(buf + off, &stream, 2); off += 2; |
|
memcpy(buf + off, &seq, 2); off += 2; |
|
buf[off++] = flags; |
|
buf[off++] = hop_count; |
|
if (hop_count) { memcpy(buf + off, hops, (size_t)hop_count * 8); off += (size_t)hop_count * 8; } |
|
if (opus_len > 0) { memcpy(buf + off, opus, (size_t)opus_len); off += (size_t)opus_len; } |
|
return off; |
|
} |
|
|
|
/* ── отправка ── */ |
|
|
|
/* статистика отправки: раз в секунду — сколько кадров реально ушло в etcp_send и сколько упало. */ |
|
static uint64_t s_send_stats_tb = 0; |
|
static uint32_t s_send_ok = 0; |
|
static uint32_t s_send_fail = 0; |
|
|
|
static void radio_send_to(struct TOPO_GROUP* group, struct ETCP_CONN* conn, |
|
const uint8_t* payload, size_t payload_len) { |
|
struct ll_entry* ee = queue_entry_new(0); |
|
if (!ee) { |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: send: queue_entry_new OOM", RADIO_ID); |
|
return; |
|
} |
|
uint8_t* pkt = u_malloc(1 + payload_len); |
|
if (!pkt) { |
|
queue_entry_free(ee); |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: send: u_malloc OOM len=%zu", RADIO_ID, payload_len); |
|
return; |
|
} |
|
pkt[0] = ETCP_ID_RADIO; |
|
memcpy(pkt + 1, payload, payload_len); |
|
ee->dgram = pkt; |
|
ee->len = (size_t)(1 + payload_len); |
|
if (etcp_send(conn, ee) != 0) { |
|
u_free(pkt); queue_entry_free(ee); |
|
s_send_fail++; |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: send: etcp_send FAILED conn=%s len=%zu", |
|
RADIO_ID, conn ? conn->log_name : "?", (size_t)(1 + payload_len)); |
|
} else { |
|
s_send_ok++; |
|
} |
|
|
|
uint64_t now = get_time_tb(); |
|
if (s_send_stats_tb == 0 || now - s_send_stats_tb >= 10000) { /* 1с (tb 0.1мс) */ |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: send stats ok=%u/s fail=%u", RADIO_ID, s_send_ok, s_send_fail); |
|
s_send_stats_tb = now; |
|
s_send_ok = 0; |
|
s_send_fail = 0; |
|
} |
|
} |
|
|
|
/* Есть ли за conn (не считая уже пройденных hop_list) хотя бы один подписчик рации. */ |
|
static int radio_neighbor_has_subscriber(struct TOPO_GROUP* group, struct ETCP_CONN* conn, |
|
const uint64_t* hop_list, uint8_t hop_count) { |
|
if (!group || !conn || !group->nodes) return 0; |
|
uint64_t self = group->instance ? group->instance->node_id : 0; |
|
struct ll_entry* ne = group->nodes->head; |
|
while (ne) { |
|
struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)ne; |
|
if (nq->radio && nq->node_id != self) { |
|
int visited = 0; |
|
for (int i = 0; i < hop_count; i++) if (hop_list[i] == nq->node_id) { visited = 1; break; } |
|
if (!visited) { |
|
struct ll_entry* pe = nq->paths ? nq->paths->head : NULL; |
|
while (pe) { |
|
struct TOPO_NODEPATH* p = (struct TOPO_NODEPATH*)pe; |
|
if (p->conn == conn) return 1; |
|
pe = pe->next; |
|
} |
|
} |
|
} |
|
ne = ne->next; |
|
} |
|
return 0; |
|
} |
|
|
|
/* Флудит payload (уже содержит group_id..hop_list..opus) всем соседям, кроме from_conn. */ |
|
static void radio_flood(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn, |
|
const uint8_t* payload, size_t payload_len, |
|
const uint64_t* hop_list, uint8_t hop_count) { |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx || !group->senders_list) return; |
|
struct ll_entry* se = group->senders_list->head; |
|
while (se) { |
|
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; |
|
if (item && item->conn && item->conn != from_conn) { |
|
if (radio_neighbor_has_subscriber(group, item->conn, hop_list, hop_count)) { |
|
radio_send_to(group, item->conn, payload, payload_len); |
|
ctx->c_fwd_frames++; |
|
} else { |
|
ctx->c_nosub_dropped++; |
|
} |
|
} |
|
se = se->next; |
|
} |
|
} |
|
|
|
/* ── индикация говорящих ── */ |
|
|
|
static void radio_talk_event(struct TOPO_GROUP* group, uint64_t src, uint16_t stream, uint8_t on) { |
|
uint8_t buf[19]; |
|
size_t off = 0; |
|
memcpy(buf + off, &group->group_id, 8); off += 8; |
|
memcpy(buf + off, &src, 8); off += 8; |
|
memcpy(buf + off, &stream, 2); off += 2; |
|
buf[off++] = on; |
|
chat_event_post(group->instance, CHAT_EVT_RADIO_TALK, buf, (int)off); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: talk %s src=%016llx stream=%u", |
|
RADIO_ID, on ? "START" : "STOP", (unsigned long long)src, stream); |
|
} |
|
|
|
/* зафиксировать начало передачи: пост on=1, если это новый поток источника */ |
|
static void radio_talker_on(struct radio_ctx* ctx, uint64_t src, uint16_t stream) { |
|
uint64_t now = get_time_tb(); |
|
for (int i = 0; i < RADIO_MAX_TALKERS; i++) { |
|
if (ctx->talkers[i].src == src) { |
|
ctx->talkers[i].last_tb = now; |
|
if (ctx->talkers[i].stream != stream) { |
|
ctx->talkers[i].stream = stream; |
|
radio_talk_event(ctx->group, src, stream, 1); |
|
} |
|
return; |
|
} |
|
} |
|
for (int i = 0; i < RADIO_MAX_TALKERS; i++) { |
|
if (ctx->talkers[i].src == 0) { |
|
ctx->talkers[i].src = src; |
|
ctx->talkers[i].stream = stream; |
|
ctx->talkers[i].last_tb = now; |
|
radio_talk_event(ctx->group, src, stream, 1); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
/* зафиксировать конец передачи: пост on=0, если источник был в таблице */ |
|
static void radio_talker_off(struct radio_ctx* ctx, uint64_t src) { |
|
for (int i = 0; i < RADIO_MAX_TALKERS; i++) { |
|
if (ctx->talkers[i].src == src) { |
|
radio_talk_event(ctx->group, src, ctx->talkers[i].stream, 0); |
|
ctx->talkers[i].src = 0; |
|
ctx->talkers[i].stream = 0; |
|
ctx->talkers[i].last_tb = 0; |
|
return; |
|
} |
|
} |
|
} |
|
|
|
/* Снять говорящих, от которых дольше RADIO_TALKER_TIMEOUT_TB нет кадров: |
|
* FIN потерян/не был отправлен (пир отвалился, выключил рацию во время разговора). |
|
* Аналог таймаута источника в radio_audio.c — гарантирует, что индикация «говорит» гаснет. */ |
|
static void radio_talkers_sweep(struct radio_ctx* ctx) { |
|
uint64_t now = get_time_tb(); |
|
for (int i = 0; i < RADIO_MAX_TALKERS; i++) { |
|
if (!ctx->talkers[i].src) continue; |
|
if (now - ctx->talkers[i].last_tb > RADIO_TALKER_TIMEOUT_TB) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: talker timeout src=%016llx (last frame %.0fms ago)", |
|
RADIO_ID, (unsigned long long)ctx->talkers[i].src, |
|
(double)(now - ctx->talkers[i].last_tb) / 10.0); |
|
radio_talker_off(ctx, ctx->talkers[i].src); |
|
} |
|
} |
|
} |
|
|
|
static void radio_talker_timer_cb(void* arg) { |
|
struct radio_ctx* ctx = (struct radio_ctx*)arg; |
|
if (!ctx || !ctx->group) return; |
|
radio_talkers_sweep(ctx); |
|
ctx->talker_timer = uasync_set_timeout(ctx->group->instance->ua, RADIO_TALKER_TIMEOUT_TB, ctx, radio_talker_timer_cb, "radio_talker"); |
|
} |
|
|
|
/* Дедупликация: вернуть 1, если (src,stream,seq) уже видели (дубль по другому пути), |
|
* иначе зафиксировать в кэше и вернуть 0. Лениво чистит просроченные записи. */ |
|
static int radio_seen_dup(struct radio_ctx* ctx, uint64_t src, uint16_t stream, uint16_t seq, uint64_t now) { |
|
int free_slot = -1, oldest = -1; |
|
uint64_t oldest_tb = 0; |
|
for (int i = 0; i < ctx->seen_count; i++) { |
|
struct radio_seen* s = &ctx->seen[i]; |
|
if (s->tb && now - s->tb > RADIO_DEDUP_TTL_TB) { s->tb = 0; s->src = 0; } |
|
if (!s->tb) { if (free_slot < 0) free_slot = i; continue; } |
|
if (s->src == src && s->stream == stream && s->seq == seq) return 1; |
|
if (oldest < 0 || s->tb < oldest_tb) { oldest = i; oldest_tb = s->tb; } |
|
} |
|
if (free_slot < 0) { |
|
if (ctx->seen_count < RADIO_DEDUP_MAX) free_slot = ctx->seen_count++; |
|
else free_slot = oldest; |
|
} |
|
ctx->seen[free_slot].src = src; |
|
ctx->seen[free_slot].stream = stream; |
|
ctx->seen[free_slot].seq = seq; |
|
ctx->seen[free_slot].tb = now; |
|
return 0; |
|
} |
|
|
|
/* ── приём ── */ |
|
|
|
static void radio_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn, |
|
const uint8_t* payload, size_t payload_len) { |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx || !from_conn || payload_len < RADIO_HDR_SIZE) return; |
|
|
|
const uint8_t* p = payload; |
|
uint64_t group_id, src; |
|
uint16_t stream, seq; |
|
uint8_t flags, hop_count; |
|
memcpy(&group_id, p + RADIO_OFF_GROUP, 8); |
|
memcpy(&src, p + RADIO_OFF_SRC, 8); |
|
memcpy(&stream, p + RADIO_OFF_STREAM, 2); |
|
memcpy(&seq, p + RADIO_OFF_SEQ, 2); |
|
flags = p[RADIO_OFF_FLAGS]; |
|
hop_count = p[RADIO_OFF_HOPS]; |
|
|
|
if (group_id != group->group_id || hop_count > RADIO_MAX_HOPS |
|
|| payload_len < RADIO_HDR_SIZE + (size_t)hop_count * 8) { |
|
ctx->c_bad_dropped++; |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: bad pkt grp=%016llx hops=%u len=%zu from=%s", |
|
RADIO_ID, (unsigned long long)group_id, hop_count, payload_len, from_conn->log_name); |
|
return; |
|
} |
|
const uint64_t* hops = (const uint64_t*)(p + RADIO_OFF_HOPLIST); |
|
const uint8_t* opus = p + RADIO_OFF_HOPLIST + (size_t)hop_count * 8; |
|
int opus_len = (int)(payload_len - RADIO_OFF_HOPLIST - (size_t)hop_count * 8); |
|
if (opus_len > RADIO_MAX_OPUS) { ctx->c_bad_dropped++; return; } |
|
|
|
uint64_t self = group->instance->node_id; |
|
|
|
/* цикл: мы уже проходили этот пакет */ |
|
for (int i = 0; i < hop_count; i++) { |
|
if (hops[i] == self) { |
|
ctx->c_loop_dropped++; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: loop drop src=%016llx stream=%u seq=%u", RADIO_ID, (unsigned long long)src, stream, seq); |
|
return; |
|
} |
|
} |
|
|
|
/* дедупликация: (src,stream,seq) уже приходил по другому пути (mesh) */ |
|
if (radio_seen_dup(ctx, src, stream, seq, get_time_tb())) { |
|
ctx->c_dup_dropped++; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: dup drop src=%016llx stream=%u seq=%u", RADIO_ID, (unsigned long long)src, stream, seq); |
|
return; |
|
} |
|
|
|
/* начало передачи (первый кадр burst): видно и на транзитных узлах (коло), не только у подписчиков */ |
|
if (seq == 0) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: recv burst src=%016llx stream=%u fin=%u len=%d sub=%d", |
|
RADIO_ID, (unsigned long long)src, stream, (flags & RADIO_FLAG_FIN) ? 1 : 0, |
|
opus_len, group->radio_active ? 1 : 0); |
|
} |
|
|
|
/* доставка локально, если мы подписаны */ |
|
if (group->radio_active && ctx->group) { |
|
struct radio_instance* ri = radio_inst(group->instance); |
|
if (ri && ri->frame_fn) { |
|
ri->frame_fn(group->instance, group_id, src, stream, seq, |
|
(flags & RADIO_FLAG_FIN) ? 1 : 0, opus, opus_len, ri->frame_arg); |
|
ctx->c_rx_frames++; |
|
} |
|
/* индикация говорящих: старт/стоп передачи */ |
|
if (flags & RADIO_FLAG_FIN) radio_talker_off(ctx, src); |
|
else radio_talker_on(ctx, src, stream); |
|
} |
|
|
|
/* форвардинг: новый hop_list = старый + self */ |
|
uint8_t new_count = hop_count + 1; |
|
if (new_count >= RADIO_MAX_HOPS) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: max hops reached, no fwd src=%016llx", RADIO_ID, (unsigned long long)src); |
|
return; |
|
} |
|
uint64_t new_hops[RADIO_MAX_HOPS]; |
|
memcpy(new_hops, hops, (size_t)hop_count * 8); |
|
new_hops[hop_count] = self; |
|
|
|
uint8_t out[RADIO_HDR_SIZE + RADIO_MAX_HOPS * 8 + RADIO_MAX_OPUS]; |
|
size_t out_len = radio_build(out, group_id, src, stream, seq, flags, new_hops, new_count, opus, opus_len); |
|
radio_flood(group, from_conn, out, out_len, new_hops, new_count); |
|
} |
|
|
|
static void radio_recv_dispatcher(struct ETCP_CONN* from_conn, struct ll_entry* entry) { |
|
if (!from_conn || !entry || entry->len < 1 + 8 + RADIO_HDR_SIZE) { |
|
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } |
|
return; |
|
} |
|
struct UTUN_INSTANCE* instance = from_conn->instance; |
|
if (!instance) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
|
|
|
uint8_t* dgram = entry->dgram; |
|
if (dgram[0] != ETCP_ID_RADIO) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
|
|
|
uint64_t group_id; |
|
memcpy(&group_id, dgram + 1, 8); |
|
if (instance->topo_groups) { |
|
struct TOPO_GROUP* group = topo_groups_find(instance->topo_groups, group_id); |
|
if (group) radio_recv(group, from_conn, dgram + 1, entry->len - 1); |
|
} |
|
queue_dgram_free(entry); queue_entry_free(entry); |
|
} |
|
|
|
int radio_init_instance(struct UTUN_INSTANCE* inst) { |
|
if (!inst) return -1; |
|
if (inst->radio) return 0; |
|
struct radio_instance* ri = u_calloc(1, sizeof(*ri)); |
|
if (!ri) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: alloc instance failed", RADIO_ID); return -1; } |
|
inst->radio = ri; |
|
if (etcp_bind(inst, ETCP_ID_RADIO, radio_recv_dispatcher) != 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: etcp_bind(ETCP_ID_RADIO) failed", RADIO_ID); |
|
u_free(ri); inst->radio = NULL; |
|
return -1; |
|
} |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: instance initialized (bound ETCP_ID_RADIO)", RADIO_ID); |
|
return 0; |
|
} |
|
|
|
void radio_destroy_instance(struct UTUN_INSTANCE* inst) { |
|
if (!inst || !inst->radio) return; |
|
etcp_unbind(inst, ETCP_ID_RADIO); |
|
u_free(inst->radio); |
|
inst->radio = NULL; |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: instance destroyed", RADIO_ID); |
|
} |
|
|
|
void radio_set_frame_cb(struct UTUN_INSTANCE* inst, radio_frame_fn fn, void* arg) { |
|
struct radio_instance* ri = radio_inst(inst); |
|
if (!ri) return; |
|
ri->frame_fn = fn; |
|
ri->frame_arg = arg; |
|
} |
|
|
|
void radio_set_mute_cb(struct UTUN_INSTANCE* inst, radio_mute_fn fn, void* arg) { |
|
struct radio_instance* ri = radio_inst(inst); |
|
if (!ri) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: mute callback without instance"); return; } |
|
ri->mute_fn = fn; ri->mute_arg = arg; |
|
} |
|
|
|
static int radio_load_muted_members(struct radio_ctx* ctx) { |
|
struct UTUN_INSTANCE* inst = ctx->group->instance; |
|
sqlite3* db = inst->topo_sqlite_db; |
|
char sql[160]; |
|
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"peers_%llu\" WHERE radio_muted<>0", |
|
(unsigned long long)ctx->group->group_id); |
|
sqlite3_stmt* st = NULL; |
|
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: load muted members grp=%016llx: %s", |
|
(unsigned long long)ctx->group->group_id, db ? sqlite3_errmsg(db) : "no database"); return -1; |
|
} |
|
uint64_t* members = NULL; |
|
int count = 0, rc; |
|
while ((rc = sqlite3_step(st)) == SQLITE_ROW) { |
|
uint64_t* next = u_realloc(members, (size_t)(count + 1) * sizeof(*members)); |
|
if (!next) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: muted member cache allocation failed"); |
|
u_free(members); sqlite3_finalize(st); return -1; |
|
} |
|
members = next; members[count++] = (uint64_t)sqlite3_column_int64(st, 0); |
|
} |
|
sqlite3_finalize(st); |
|
if (rc != SQLITE_DONE) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read muted members: %s", sqlite3_errmsg(db)); u_free(members); return -1; |
|
} |
|
u_free(ctx->muted_members); ctx->muted_members = members; ctx->muted_count = count; ctx->muted_loaded = 1; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: loaded local mutes grp=%016llx count=%d", |
|
(unsigned long long)ctx->group->group_id, count); |
|
return 0; |
|
} |
|
|
|
int radio_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id) { |
|
if (!inst || !inst->topo_groups) return 0; |
|
struct radio_ctx* ctx = radio_of(topo_groups_find(inst->topo_groups, group_id)); |
|
if (!ctx) return 0; |
|
for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) return 1; |
|
return 0; |
|
} |
|
|
|
void radio_forget_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id) { |
|
if (!inst || !inst->topo_groups) return; |
|
struct radio_ctx* ctx = radio_of(topo_groups_find(inst->topo_groups, group_id)); |
|
if (!ctx) return; |
|
for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) { |
|
ctx->muted_members[i] = ctx->muted_members[--ctx->muted_count]; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: forget removed member mute grp=%016llx node=%016llx", |
|
(unsigned long long)group_id, (unsigned long long)node_id); |
|
return; |
|
} |
|
} |
|
|
|
int radio_set_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted) { |
|
struct TOPO_GROUP* group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL; |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx || group->group_type != TOPO_GROUP_TYPE_CHAT || !node_id || node_id == inst->node_id || (muted != 0 && muted != 1)) { |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio: invalid local mute grp=%016llx node=%016llx muted=%d", |
|
(unsigned long long)group_id, (unsigned long long)node_id, muted); return -1; |
|
} |
|
if (!ctx->muted_loaded && radio_load_muted_members(ctx) < 0) return -1; |
|
int slot = -1; |
|
for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) { slot = i; break; } |
|
/* Выделить память до записи БД: после commit применение не может отказать. */ |
|
if (muted && slot < 0) { |
|
uint64_t* next = u_realloc(ctx->muted_members, (size_t)(ctx->muted_count + 1) * sizeof(*next)); |
|
if (!next) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: grow muted member cache failed"); return -1; } |
|
ctx->muted_members = next; |
|
} |
|
sqlite3* db = inst->topo_sqlite_db; |
|
char sql[160]; |
|
snprintf(sql, sizeof(sql), "UPDATE \"peers_%llu\" SET radio_muted=? WHERE node_id=?", (unsigned long long)group_id); |
|
sqlite3_stmt* st = NULL; |
|
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: prepare local mute: %s", db ? sqlite3_errmsg(db) : "no database"); return -1; |
|
} |
|
sqlite3_bind_int(st, 1, muted); sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id); |
|
int rc = sqlite3_step(st); sqlite3_finalize(st); |
|
if (rc != SQLITE_DONE || sqlite3_changes(db) != 1) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: save local mute grp=%016llx node=%016llx rc=%d changed=%d: %s", |
|
(unsigned long long)group_id, (unsigned long long)node_id, rc, sqlite3_changes(db), sqlite3_errmsg(db)); return -1; |
|
} |
|
if (muted && slot < 0) ctx->muted_members[ctx->muted_count++] = node_id; |
|
else if (!muted && slot >= 0) ctx->muted_members[slot] = ctx->muted_members[--ctx->muted_count]; |
|
struct radio_instance* ri = radio_inst(inst); |
|
if (ri && ri->mute_fn) ri->mute_fn(inst, group_id, node_id, muted, ri->mute_arg); |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: local member mute grp=%016llx node=%016llx muted=%d", |
|
(unsigned long long)group_id, (unsigned long long)node_id, muted); |
|
return 0; |
|
} |
|
|
|
/* ── подписка (вкл/выкл прослушивание) ── */ |
|
|
|
void radio_set_active(struct UTUN_INSTANCE* inst, uint64_t group_id, int on) { |
|
if (!inst || !inst->topo_groups) return; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
if (!group) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: set_active group 0x%016llx not found", RADIO_ID, (unsigned long long)group_id); return; } |
|
if (on && group->group_type == TOPO_GROUP_TYPE_CHAT) { |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx || radio_load_muted_members(ctx) < 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: subscription rejected because local mutes could not be loaded"); return; |
|
} |
|
} |
|
if (!on) { |
|
/* выключение прослушивания: снять всех активных говорящих — индикация должна погаснуть |
|
* сразу, а не дожидаться FIN/таймаута */ |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (ctx) { |
|
for (int i = 0; i < RADIO_MAX_TALKERS; i++) |
|
if (ctx->talkers[i].src) radio_talker_off(ctx, ctx->talkers[i].src); |
|
} |
|
} |
|
topo_group_set_radio(group, on); |
|
} |
|
|
|
int radio_is_active(struct UTUN_INSTANCE* inst, uint64_t group_id) { |
|
if (!inst || !inst->topo_groups) return 0; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
return group ? group->radio_active : 0; |
|
} |
|
|
|
int radio_subscribers(struct UTUN_INSTANCE* inst, uint64_t group_id) { |
|
if (!inst || !inst->topo_groups) return 0; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
return group ? topo_group_radio_subscribers(group) : 0; |
|
} |
|
|
|
/* Число дропнутых дублей (src,stream,seq) — для диагностики/тестов. */ |
|
uint32_t radio_dup_dropped(struct UTUN_INSTANCE* inst, uint64_t group_id) { |
|
if (!inst || !inst->topo_groups) return 0; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
struct radio_ctx* ctx = radio_of(group); |
|
return ctx ? ctx->c_dup_dropped : 0; |
|
} |
|
|
|
/* ── передача (PTT) ── */ |
|
|
|
int radio_talk_begin(struct UTUN_INSTANCE* inst, uint64_t group_id) { |
|
if (!inst || !inst->topo_groups) return -1; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
if (!group) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: talk_begin group 0x%016llx not found", RADIO_ID, (unsigned long long)group_id); return -1; } |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx) return -1; |
|
ctx->stream_id = (uint16_t)(ctx->stream_id + 1); |
|
ctx->seq = 0; |
|
ctx->talking = 1; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: talk_begin grp=%016llx stream=%u", RADIO_ID, (unsigned long long)group_id, ctx->stream_id); |
|
return 0; |
|
} |
|
|
|
int radio_talk_send(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t* opus, int len) { |
|
if (!inst || !inst->topo_groups || !opus || len < 0 || len > RADIO_MAX_OPUS) return -1; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
if (!group) return -1; |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx) return -1; |
|
if (!ctx->talking) { |
|
/* burst не открыт: хвостовой кадр, пришедший после radio_talk_end (гонка релиза PTT) — |
|
* не переоткрываем передачу, иначе получится burst без FIN и «говорит» зависнет у пиров. */ |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: send dropped: no open burst grp=%016llx", RADIO_ID, (unsigned long long)group_id); |
|
return 0; |
|
} |
|
|
|
uint64_t self = inst->node_id; |
|
uint64_t hops[1] = { self }; |
|
uint8_t out[RADIO_HDR_SIZE + 8 + RADIO_MAX_OPUS]; |
|
size_t out_len = radio_build(out, group_id, self, ctx->stream_id, ctx->seq, 0, hops, 1, opus, len); |
|
ctx->seq++; |
|
ctx->c_tx_frames++; |
|
|
|
radio_flood(group, NULL, out, out_len, hops, 1); |
|
return 0; |
|
} |
|
|
|
int radio_talk_end(struct UTUN_INSTANCE* inst, uint64_t group_id) { |
|
if (!inst || !inst->topo_groups) return -1; |
|
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); |
|
if (!group) return -1; |
|
struct radio_ctx* ctx = radio_of(group); |
|
if (!ctx) return -1; |
|
if (!ctx->talking) return 0; |
|
|
|
uint64_t self = inst->node_id; |
|
uint64_t hops[1] = { self }; |
|
uint8_t out[RADIO_HDR_SIZE + 8]; |
|
size_t out_len = radio_build(out, group_id, self, ctx->stream_id, ctx->seq, |
|
RADIO_FLAG_FIN, hops, 1, NULL, 0); |
|
ctx->seq++; |
|
ctx->c_tx_frames++; |
|
ctx->talking = 0; |
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: talk_end grp=%016llx stream=%u frames=%u", |
|
RADIO_ID, (unsigned long long)group_id, ctx->stream_id, ctx->c_tx_frames); |
|
radio_flood(group, NULL, out, out_len, hops, 1); |
|
return 0; |
|
} |
|
|
|
/* ── trampoline-функции (GUI/аудио-поток → uasync) ── */ |
|
|
|
void radio_talk_begin_trampoline(void* arg) { |
|
struct radio_talk_arg* a = (struct radio_talk_arg*)arg; |
|
radio_talk_begin(a->inst, a->group_id); |
|
u_free(a); |
|
} |
|
|
|
void radio_talk_end_trampoline(void* arg) { |
|
struct radio_talk_arg* a = (struct radio_talk_arg*)arg; |
|
radio_talk_end(a->inst, a->group_id); |
|
u_free(a); |
|
} |
|
|
|
void radio_talk_send_trampoline(void* arg) { |
|
struct radio_media_arg* a = (struct radio_media_arg*)arg; |
|
radio_talk_send(a->inst, a->group_id, a->opus, a->len); |
|
u_free(a); |
|
} |
|
|
|
void radio_set_active_trampoline(void* arg) { |
|
struct radio_set_active_arg* a = (struct radio_set_active_arg*)arg; |
|
radio_set_active(a->inst, a->group_id, a->on); |
|
u_free(a); |
|
}
|
|
|