Add channel radio invitation signaling and bounded result cache
This commit is contained in:
1 parent
a84b226b18
commit
639be7fcc2
2 files changed
+327
No files matched your search
@@ -0,0 +1,273 @@
|
||||
/* Короткоживущие приглашения в рацию; один pending, ограниченный кэш результатов.
|
||||
* Повторы нужны также после смены router epoch. Одна периодическая задача владеет
|
||||
* всеми deadline; callback не хранит указателей на освобождаемые сессии. */
|
||||
#include <string.h>
|
||||
#include "radio_call.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../chat/chat_event.h"
|
||||
#include "../chat/member_sync.h"
|
||||
#include "../call/call.h"
|
||||
#include "../ntp_time.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../routing_layer/etcp_router.h"
|
||||
#include "../routing_layer/route_crypto.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/u_async.h"
|
||||
|
||||
#define RC_SLOTS 64
|
||||
#define RC_TICK_TB 500 /* 50мс, deadline первого ответа проверяется без продления */
|
||||
enum { RC_INVITE = 1, RC_RINGING, RC_RESULT, RC_ACK };
|
||||
/* wire: command:1, id:8LE, expires_us:8LE, reason:1. Результат повторяется до ACK.
|
||||
* INVITE действителен 2с: застрявшая в роутере попытка не запускает поздний рингтон.
|
||||
* Часы общие с NTP ядра; deadline самой попытки всегда монотонный. */
|
||||
#define RC_PACKET_SIZE 18
|
||||
|
||||
struct radio_call_session {
|
||||
uint64_t id, group, peer; /* ключ — вся тройка, чужой пакет не меняет попытку */
|
||||
uint64_t started, last_peer, last_send, finished;
|
||||
uint8_t state, reason, incoming, ready, awaiting_ack;
|
||||
};
|
||||
struct radio_call_context {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
void* timer;
|
||||
struct radio_call_session sessions[RC_SLOTS];
|
||||
};
|
||||
|
||||
/* Формат события: id/group/peer:8LE, state/reason/incoming:1 (27 байт). */
|
||||
static void rc_event(struct radio_call_context* ctx, const struct radio_call_session* s) {
|
||||
uint8_t data[27];
|
||||
memcpy(data, &s->id, 8); memcpy(data + 8, &s->group, 8); memcpy(data + 16, &s->peer, 8);
|
||||
data[24] = s->state; data[25] = s->reason; data[26] = s->incoming;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio call id=%016llx group=%016llx peer=%016llx incoming=%u state=%u reason=%u elapsed_ms=%llu",
|
||||
(unsigned long long)s->id, (unsigned long long)s->group, (unsigned long long)s->peer,
|
||||
s->incoming, s->state, s->reason, (unsigned long long)((get_time_tb() - s->started) / 10));
|
||||
chat_event_post(ctx->inst, CHAT_EVT_RADIO_CALL, data, sizeof(data));
|
||||
}
|
||||
|
||||
/* Никакого отдельного соединения: используем живой BGP-путь канала и backpressure роутера. */
|
||||
static int rc_send(struct radio_call_context* ctx, struct radio_call_session* s, uint8_t command, uint8_t reason) {
|
||||
s->last_send = get_time_tb();
|
||||
if (!etcp_router_send_q_has_room(ctx->inst, s->group, s->peer, ETCP_RT_ID_RADIO_CALL)) return 0;
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call packet allocation failed"); return -1; }
|
||||
e->dgram = u_malloc(1 + RC_PACKET_SIZE);
|
||||
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call payload allocation failed"); queue_entry_free(e); return -1; }
|
||||
e->len = 1 + RC_PACKET_SIZE;
|
||||
e->dgram[0] = ETCP_RT_ID_RADIO_CALL; e->dgram[1] = command;
|
||||
memcpy(e->dgram + 2, &s->id, 8);
|
||||
int64_t expires = ntp_time_get_us(ctx->inst) + 2000000;
|
||||
memcpy(e->dgram + 10, &expires, 8); e->dgram[18] = reason;
|
||||
int rc = etcp_route_send(ctx->inst, s->group, s->peer, e, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
|
||||
if (rc) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call send failed id=%016llx command=%u rc=%d",
|
||||
(unsigned long long)s->id, command, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Кэш не вытесняет живые записи: даже при потоке новых запросов старый INVITE не оживает. */
|
||||
static struct radio_call_session* rc_slot(struct radio_call_context* ctx, uint64_t id, uint64_t group, uint64_t peer, int create) {
|
||||
struct radio_call_session* empty = NULL;
|
||||
uint64_t now = get_time_tb();
|
||||
for (int i = 0; i < RC_SLOTS; ++i) {
|
||||
struct radio_call_session* s = &ctx->sessions[i];
|
||||
if (s->id && s->state == RADIO_CALL_FINISHED && now - s->finished >= RADIO_CALL_CACHE_TB) memset(s, 0, sizeof(*s));
|
||||
if (s->id == id && s->group == group && s->peer == peer) return s;
|
||||
if (!s->id && !empty) empty = s;
|
||||
}
|
||||
if (!create || !empty) return NULL;
|
||||
empty->id = id; empty->group = group; empty->peer = peer; empty->started = empty->last_peer = now;
|
||||
return empty;
|
||||
}
|
||||
|
||||
int radio_call_busy(struct UTUN_INSTANCE* inst) {
|
||||
struct radio_call_context* ctx = inst ? inst->radio_call : NULL;
|
||||
for (int i = 0; ctx && i < RC_SLOTS; ++i)
|
||||
if (ctx->sessions[i].id && ctx->sessions[i].state != RADIO_CALL_FINISHED) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Один результат на попытку; последующие пакеты только подтверждают сохранённый итог. */
|
||||
static void rc_finish(struct radio_call_context* ctx, struct radio_call_session* s, uint8_t reason, int send) {
|
||||
if (s->state == RADIO_CALL_FINISHED) return;
|
||||
s->state = RADIO_CALL_FINISHED; s->reason = reason; s->finished = get_time_tb(); s->awaiting_ack = send;
|
||||
if (send) rc_send(ctx, s, RC_RESULT, reason);
|
||||
rc_event(ctx, s);
|
||||
}
|
||||
|
||||
/* Новый запрос допустим только в существующей CHAT-группе между её проверенными участниками. */
|
||||
static struct TOPO_GROUP* rc_group(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer) {
|
||||
struct TOPO_GROUP* g = inst->topo_groups ? topo_groups_find(inst->topo_groups, group) : NULL;
|
||||
uint8_t key[32];
|
||||
if (!g || g->group_type != TOPO_GROUP_TYPE_CHAT || peer == inst->node_id || !peer ||
|
||||
member_sync_verified_ed25519(inst, g->channel_id, peer, key) < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call invalid channel/member group=%016llx peer=%016llx",
|
||||
(unsigned long long)group, (unsigned long long)peer); return NULL;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
/* Единый монотонный deadline: OUTGOING=2с, RINGING=45с от начала, callee lease=2с. */
|
||||
static void rc_tick(void* arg) {
|
||||
struct radio_call_context* ctx = arg;
|
||||
ctx->timer = NULL;
|
||||
uint64_t now = get_time_tb();
|
||||
for (int i = 0; i < RC_SLOTS; ++i) {
|
||||
struct radio_call_session* s = &ctx->sessions[i];
|
||||
if (!s->id) continue;
|
||||
if (s->state == RADIO_CALL_FINISHED) {
|
||||
if (now - s->finished >= RADIO_CALL_CACHE_TB) { memset(s, 0, sizeof(*s)); continue; }
|
||||
if (s->awaiting_ack && now - s->finished < RADIO_CALL_RESPONSE_TB * 10 && now - s->last_send >= RADIO_CALL_RETRY_TB)
|
||||
rc_send(ctx, s, RC_RESULT, s->reason);
|
||||
continue;
|
||||
}
|
||||
struct TOPO_GROUP* g = topo_groups_find(ctx->inst->topo_groups, s->group);
|
||||
if (!g) { rc_finish(ctx, s, RADIO_CALL_UNAVAILABLE, 1); continue; }
|
||||
if (!s->incoming && s->state == RADIO_CALL_OUTGOING && now - s->started >= RADIO_CALL_RESPONSE_TB)
|
||||
rc_finish(ctx, s, RADIO_CALL_UNAVAILABLE, 1);
|
||||
else if (now - s->started >= RADIO_CALL_RING_TB) rc_finish(ctx, s, RADIO_CALL_NO_ANSWER, 1);
|
||||
else if (s->incoming && now - s->last_peer >= RADIO_CALL_RESPONSE_TB) rc_finish(ctx, s, RADIO_CALL_CANCELLED, 1);
|
||||
else if (!s->incoming && now - s->last_send >= RADIO_CALL_RETRY_TB) rc_send(ctx, s, RC_INVITE, 0);
|
||||
}
|
||||
ctx->timer = uasync_set_timeout(ctx->inst->ua, RC_TICK_TB, ctx, rc_tick, "radio_call");
|
||||
if (!ctx->timer) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call timer allocation failed");
|
||||
}
|
||||
|
||||
/* Получатель доверяет identity из подписанного router envelope, а не физическому соседу. */
|
||||
static void rc_receive(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
struct radio_call_context* ctx = conn && conn->instance ? conn->instance->radio_call : NULL;
|
||||
if (!ctx || !entry || !entry->dgram || entry->len != ROUTER_SVC_PAYLOAD_OFF + RC_PACKET_SIZE) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call invalid packet bytes=%u", entry ? entry->len : 0); goto done;
|
||||
}
|
||||
const uint8_t* d = entry->dgram;
|
||||
uint8_t flags = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED;
|
||||
if ((d[ROUTER_SVC_FLAGS_OFF] & flags) != flags) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call unsigned/unencrypted packet flags=%02x", d[ROUTER_SVC_FLAGS_OFF]); goto done;
|
||||
}
|
||||
uint64_t group, peer, id;
|
||||
memcpy(&group, d + ROUTER_SVC_GROUP_OFF, 8); memcpy(&peer, d + ROUTER_SVC_SRC_OFF, 8);
|
||||
const uint8_t* b = d + ROUTER_SVC_PAYLOAD_OFF;
|
||||
memcpy(&id, b + 1, 8);
|
||||
uint8_t command = b[0], reason = b[17];
|
||||
if (!id || command < RC_INVITE || command > RC_ACK || reason > RADIO_CALL_AUDIO_ERROR ||
|
||||
((command == RC_RESULT || command == RC_ACK) ? !reason : reason != 0)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call invalid command=%u reason=%u id=%llu", command, reason, (unsigned long long)id); goto done;
|
||||
}
|
||||
struct radio_call_session* s = rc_slot(ctx, id, group, peer, 0);
|
||||
if (command == RC_INVITE && !s) {
|
||||
int64_t expires; memcpy(&expires, b + 9, 8);
|
||||
int64_t remaining = expires - ntp_time_get_us(ctx->inst);
|
||||
if (remaining <= 0 || remaining > 4000000 || !rc_group(ctx->inst, group, peer)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call stale/invalid invite id=%016llx remaining_us=%lld",
|
||||
(unsigned long long)id, (long long)remaining); goto done;
|
||||
}
|
||||
int busy = radio_call_busy(ctx->inst) || call_busy(ctx->inst);
|
||||
s = rc_slot(ctx, id, group, peer, 1);
|
||||
if (!s) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call result cache full; reply BUSY");
|
||||
struct radio_call_session rejected = {.id = id, .group = group, .peer = peer};
|
||||
rc_send(ctx, &rejected, RC_RESULT, RADIO_CALL_BUSY); goto done;
|
||||
}
|
||||
s->incoming = 1; s->state = RADIO_CALL_INCOMING;
|
||||
if (busy) rc_finish(ctx, s, RADIO_CALL_BUSY, 1);
|
||||
else rc_event(ctx, s); /* приложение подтверждает показ через READY */
|
||||
goto done;
|
||||
}
|
||||
if (!s) {
|
||||
if (command == RC_RESULT) {
|
||||
/* CANCEL раньше INVITE также оставляет tombstone, не позволяя открыть старое окно. */
|
||||
s = rc_slot(ctx, id, group, peer, 1);
|
||||
if (s) { s->state = RADIO_CALL_FINISHED; s->reason = reason; s->finished = get_time_tb(); }
|
||||
struct radio_call_session ack = {.id = id, .group = group, .peer = peer};
|
||||
rc_send(ctx, &ack, RC_ACK, reason);
|
||||
} else DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio call late command ignored id=%016llx command=%u", (unsigned long long)id, command);
|
||||
goto done;
|
||||
}
|
||||
if (command == RC_ACK) {
|
||||
if (s->state == RADIO_CALL_FINISHED && s->reason == reason && s->awaiting_ack) {
|
||||
s->awaiting_ack = 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio call result acknowledged id=%016llx reason=%u", (unsigned long long)id, reason);
|
||||
}
|
||||
} else if (command == RC_RESULT) {
|
||||
if (s->incoming && (reason == RADIO_CALL_ACCEPTED || reason == RADIO_CALL_DECLINED || reason == RADIO_CALL_BUSY)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call result invalid for callee id=%016llx reason=%u", (unsigned long long)id, reason); goto done;
|
||||
}
|
||||
rc_send(ctx, s, RC_ACK, reason);
|
||||
rc_finish(ctx, s, reason, 0);
|
||||
} else if (s->state == RADIO_CALL_FINISHED) rc_send(ctx, s, RC_RESULT, s->reason);
|
||||
else if (command == RC_INVITE && s->incoming) {
|
||||
s->last_peer = get_time_tb();
|
||||
if (s->ready) rc_send(ctx, s, RC_RINGING, 0);
|
||||
} else if (command == RC_RINGING && !s->incoming && s->state == RADIO_CALL_OUTGOING) {
|
||||
if (get_time_tb() - s->started >= RADIO_CALL_RESPONSE_TB) rc_finish(ctx, s, RADIO_CALL_UNAVAILABLE, 1);
|
||||
else { s->state = RADIO_CALL_RINGING; rc_event(ctx, s); }
|
||||
}
|
||||
done:
|
||||
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
|
||||
}
|
||||
|
||||
/* UI-команды сериализованы с сетью; запоздалое нажатие не меняет завершённый вызов. */
|
||||
void radio_call_execute(const struct radio_call_request* req) {
|
||||
struct radio_call_context* ctx = req && req->inst ? req->inst->radio_call : NULL;
|
||||
if (!ctx || !req->id) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call command without context/id"); return; }
|
||||
if (req->action == RADIO_CALL_START) {
|
||||
struct radio_call_session* s = rc_slot(ctx, req->id, req->group, req->peer, 0);
|
||||
if (s) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call duplicate local start id=%016llx", (unsigned long long)req->id); return; }
|
||||
int busy = radio_call_busy(req->inst) || call_busy(req->inst);
|
||||
s = rc_slot(ctx, req->id, req->group, req->peer, 1);
|
||||
if (!s) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call local result cache full");
|
||||
struct radio_call_session failed = {.id = req->id, .group = req->group, .peer = req->peer,
|
||||
.started = get_time_tb(), .state = RADIO_CALL_FINISHED, .reason = RADIO_CALL_BUSY};
|
||||
rc_event(ctx, &failed); return;
|
||||
}
|
||||
s->state = RADIO_CALL_OUTGOING;
|
||||
struct TOPO_GROUP* g = rc_group(req->inst, req->group, req->peer);
|
||||
if (busy) { rc_finish(ctx, s, RADIO_CALL_BUSY, 0); return; }
|
||||
if (!g || !topo_group_find_conn_for_node(g, req->peer)) { rc_finish(ctx, s, RADIO_CALL_UNAVAILABLE, 0); return; }
|
||||
rc_event(ctx, s);
|
||||
rc_send(ctx, s, RC_INVITE, 0);
|
||||
return;
|
||||
}
|
||||
struct radio_call_session* s = NULL;
|
||||
for (int i = 0; i < RC_SLOTS; ++i) if (ctx->sessions[i].id == req->id && ctx->sessions[i].state != RADIO_CALL_FINISHED)
|
||||
s = &ctx->sessions[i];
|
||||
if (!s) { DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio call stale UI command id=%016llx action=%d", (unsigned long long)req->id, req->action); return; }
|
||||
if (req->action == RADIO_CALL_READY && s->incoming) { s->ready = 1; rc_send(ctx, s, RC_RINGING, 0); return; }
|
||||
uint8_t reason = 0;
|
||||
if (req->action == RADIO_CALL_ACCEPT && s->incoming) reason = RADIO_CALL_ACCEPTED;
|
||||
else if (req->action == RADIO_CALL_DECLINE && s->incoming) reason = RADIO_CALL_DECLINED;
|
||||
else if (req->action == RADIO_CALL_CANCEL) reason = RADIO_CALL_CANCELLED;
|
||||
else if (req->action == RADIO_CALL_FAIL) reason = RADIO_CALL_AUDIO_ERROR;
|
||||
else if (req->action == RADIO_CALL_REJECT_BUSY) reason = RADIO_CALL_BUSY;
|
||||
if (reason) rc_finish(ctx, s, reason, 1);
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call invalid UI action=%d incoming=%u", req->action, s->incoming);
|
||||
}
|
||||
|
||||
void radio_call_trampoline(void* arg) {
|
||||
radio_call_execute(arg); u_free(arg);
|
||||
}
|
||||
|
||||
int radio_call_init(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call init without instance"); return -1; }
|
||||
if (inst->radio_call) return 0;
|
||||
struct radio_call_context* ctx = u_calloc(1, sizeof(*ctx));
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call context allocation failed"); return -1; }
|
||||
ctx->inst = inst; inst->radio_call = ctx;
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_RADIO_CALL, rc_receive)) { radio_call_destroy(inst); return -1; }
|
||||
ctx->timer = uasync_set_timeout(inst->ua, RC_TICK_TB, ctx, rc_tick, "radio_call");
|
||||
if (!ctx->timer) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio call init timer failed"); radio_call_destroy(inst); return -1; }
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio call initialized first_response_ms=2000 ring_ms=45000 cache=%d", RC_SLOTS);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void radio_call_destroy(struct UTUN_INSTANCE* inst) {
|
||||
struct radio_call_context* ctx = inst ? inst->radio_call : NULL;
|
||||
if (!ctx) return;
|
||||
if (ctx->timer) uasync_cancel_timeout(inst->ua, ctx->timer);
|
||||
for (int i = 0; i < RC_SLOTS; ++i) if (ctx->sessions[i].id && ctx->sessions[i].state != RADIO_CALL_FINISHED)
|
||||
rc_finish(ctx, &ctx->sessions[i], RADIO_CALL_CANCELLED, 0);
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_RADIO_CALL);
|
||||
inst->radio_call = NULL; u_free(ctx);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio call destroyed; timer and pending invitations released");
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* Адресный вызов участника в общую рацию CHAT-канала.
|
||||
* Ядро владеет сигналингом, GUI — рингтоном и включением обычной рации.
|
||||
* После INCOMING приложение вызывает READY; первый ответ ожидается строго 2с.
|
||||
* ACCEPT отправляется после локального включения рации. FINISHED завершает только
|
||||
* приглашение, а не рацию. Все функции и события выполняются в uasync-потоке. */
|
||||
#ifndef RADIO_CALL_H
|
||||
#define RADIO_CALL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum radio_call_state { RADIO_CALL_OUTGOING = 1, RADIO_CALL_INCOMING, RADIO_CALL_RINGING, RADIO_CALL_FINISHED };
|
||||
enum radio_call_reason {
|
||||
RADIO_CALL_NONE, RADIO_CALL_ACCEPTED, RADIO_CALL_DECLINED, RADIO_CALL_BUSY,
|
||||
RADIO_CALL_CANCELLED, RADIO_CALL_UNAVAILABLE, RADIO_CALL_NO_ANSWER, RADIO_CALL_AUDIO_ERROR
|
||||
};
|
||||
enum radio_call_action {
|
||||
RADIO_CALL_START = 1, RADIO_CALL_READY, RADIO_CALL_ACCEPT, RADIO_CALL_DECLINE,
|
||||
RADIO_CALL_CANCEL, RADIO_CALL_FAIL, RADIO_CALL_REJECT_BUSY
|
||||
};
|
||||
|
||||
/* Таймауты в timebase 0.1мс; повторы не сдвигают первоначальный deadline. */
|
||||
#define RADIO_CALL_RESPONSE_TB 20000
|
||||
#define RADIO_CALL_RING_TB 450000
|
||||
#define RADIO_CALL_RETRY_TB 3000
|
||||
#define RADIO_CALL_CACHE_TB 700000
|
||||
|
||||
/* init: 0/-1; destroy отменяет таймер и удаляет контекст до остановки групп. */
|
||||
int radio_call_init(struct UTUN_INSTANCE* inst);
|
||||
void radio_call_destroy(struct UTUN_INSTANCE* inst);
|
||||
/* Есть незавершённое приглашение, независимо от роли. */
|
||||
int radio_call_busy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* START: ненулевые id/group/peer; остальные действия используют только id.
|
||||
* READY подтверждает показ входящего вызова; ACCEPT — готовность местной рации.
|
||||
* Ошибки и результаты приходят через CHAT_EVT_RADIO_CALL, по одному FINISHED на попытку. */
|
||||
struct radio_call_request {
|
||||
struct UTUN_INSTANCE* inst; /* заимствован, запрос не должен пережить остановку ядра */
|
||||
uint64_t id, group, peer; /* идентификатор попытки, CHAT-канал, удалённый участник */
|
||||
int action; /* radio_call_action */
|
||||
};
|
||||
void radio_call_execute(const struct radio_call_request* req);
|
||||
/* Выполняет запрос и освобождает выделенный через u_malloc аргумент. */
|
||||
void radio_call_trampoline(void* arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
Reference in new issue
Block a user