summaryrefslogtreecommitdiff
path: root/plugins/packet_radio/packet_radio.c
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/packet_radio/packet_radio.c')
-rw-r--r--plugins/packet_radio/packet_radio.c1005
1 files changed, 1005 insertions, 0 deletions
diff --git a/plugins/packet_radio/packet_radio.c b/plugins/packet_radio/packet_radio.c
new file mode 100644
index 0000000..e8952ae
--- /dev/null
+++ b/plugins/packet_radio/packet_radio.c
@@ -0,0 +1,1005 @@
+/*
+ * packet_radio — link/repeater edge adapter: serial TNC, KISS/AX.25 → HBX client.
+ * INI: [transport.packet_radio] or [transport.packet_radioN]. See share/hybbx.ini.example.
+ */
+#include "hybbx/plugin.h"
+#include "hybbx/service.h"
+#include "hybbx/broadcast.h"
+#include "hybbx/packet_radio.h"
+#include "hybbx/tnc.h"
+#include "hybbx/circuit.h"
+#include "hybbx/circuit_tcp.h"
+#include "hybbx/circuit_balance.h"
+#include "hybbx/traffic.h"
+#include "hybbx/limits.h"
+#include "hybbx/security_ban.h"
+#include "hybbx/ax25.h"
+#include "hybbx/util.h"
+#include "hybbx/log.h"
+#include "hybbx/max25.h"
+
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#if defined(_WIN32)
+#include <windows.h>
+#else
+#include <errno.h>
+#include <poll.h>
+#include <sys/select.h>
+#endif
+
+#define PACKET_RADIO_POLL_MS 20u
+
+static hybbx_service_t *g_service;
+
+typedef struct packet_radio_instance {
+ hybbx_packet_radio_config_t config;
+ hybbx_tnc_t *tnc;
+ int circuit_fd;
+ hybbx_circuit_decoder_t circuit_dec;
+ volatile int flow_paused;
+ volatile int flow_cancel;
+ unsigned index;
+ unsigned circuit_reconnect_polls;
+} packet_radio_instance_t;
+
+static packet_radio_instance_t g_instances[HYBBX_PACKET_RADIO_MAX_INSTANCES];
+static unsigned g_instance_count;
+static pthread_t g_poll_thread;
+static volatile int g_radio_running;
+static int g_poll_thread_started;
+
+extern const hybbx_transport_plugin_t hybbx_plugin_packet_radio;
+
+static void instance_note_rf_activity(packet_radio_instance_t *inst);
+
+static unsigned packet_radio_count_local_edges(const char *config)
+{
+ char scratch[4096];
+ const char *cursor = config;
+ unsigned count = 0;
+
+ while (*cursor != '\0') {
+ const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP);
+ size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor);
+
+ if (len == 0) {
+ cursor = sep != NULL ? sep + 1 : cursor + len;
+ if (sep == NULL) {
+ break;
+ }
+ continue;
+ }
+
+ if (len >= sizeof(scratch)) {
+ return count;
+ }
+
+ memcpy(scratch, cursor, len);
+ scratch[len] = '\0';
+
+ if (hybbx_packet_radio_section_is_local_edge(scratch)) {
+ count++;
+ }
+
+ cursor = sep != NULL ? sep + 1 : cursor + len;
+ if (sep == NULL) {
+ break;
+ }
+ }
+
+ return count;
+}
+
+static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst);
+static void packet_radio_poll_sleep_ms(unsigned ms);
+
+static const char *device_type_name(hybbx_packet_radio_device_type_t type)
+{
+ return type == HYBBX_PACKET_RADIO_DEVICE_SERIAL ? "serial" : "usb";
+}
+
+static const char *protocol_name(hybbx_packet_radio_protocol_t proto)
+{
+ switch (proto) {
+ case HYBBX_PACKET_RADIO_PROTO_HOSTMODE:
+ return "hostmode";
+ case HYBBX_PACKET_RADIO_PROTO_SIXPACK:
+ return "6pack";
+ case HYBBX_PACKET_RADIO_PROTO_KISS:
+ default:
+ return "kiss";
+ }
+}
+
+static const char *tnc_name(hybbx_packet_radio_tnc_t tnc)
+{
+ switch (tnc) {
+ case HYBBX_PACKET_RADIO_TNC_BAYCOM:
+ return "baycom";
+ case HYBBX_PACKET_RADIO_TNC_PCCOM:
+ return "pccom";
+ case HYBBX_PACKET_RADIO_TNC_GENERIC:
+ return "generic";
+ case HYBBX_PACKET_RADIO_TNC_TNC2C:
+ default:
+ return "tnc2c";
+ }
+}
+
+static int instance_circuit_uplink_allowed(const packet_radio_instance_t *inst)
+{
+ return inst != NULL && !inst->flow_paused && !inst->flow_cancel;
+}
+
+static int packet_radio_source_banned(const hybbx_ax25_path_t *path)
+{
+ char raw[HYBBX_CALLID_MAX];
+ char norm[HYBBX_CALLID_MAX];
+
+ if (path == NULL || path->source.call[0] == '\0') {
+ return 0;
+ }
+
+ if (path->source.ssid > 0u) {
+ snprintf(raw, sizeof(raw), "%s-%u", path->source.call,
+ path->source.ssid);
+ } else {
+ hybbx_strlcpy(raw, path->source.call, sizeof(raw));
+ }
+
+ if (!hybbx_security_callid_normalize(raw, norm, sizeof(norm))) {
+ return 0;
+ }
+
+ return hybbx_security_ban_callid_is_banned(norm);
+}
+
+static int packet_radio_frame_source_banned(const uint8_t *frame, size_t len)
+{
+ hybbx_ax25_path_t path;
+ uint8_t scratch[HYBBX_AX25_FRAME_MAX];
+
+ if (frame == NULL || len == 0) {
+ return 0;
+ }
+
+ if (hybbx_ax25_parse_ui(frame, len, &path, scratch, sizeof(scratch)) == 0) {
+ return 0;
+ }
+
+ return packet_radio_source_banned(&path);
+}
+
+static hybbx_result_t instance_circuit_uplink(packet_radio_instance_t *inst,
+ const uint8_t *frame,
+ size_t frame_len)
+{
+ uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME];
+ size_t hbx_len;
+ uint16_t flags = HYBBX_CIRCUIT_FLAG_RX;
+
+ if (inst == NULL || inst->circuit_fd < 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ if (!instance_circuit_uplink_allowed(inst)) {
+ return HYBBX_OK;
+ }
+
+ if (hybbx_packet_radio_modulation_is_g3ruh(inst->config.params.modulation)) {
+ flags |= HYBBX_CIRCUIT_FLAG_G3RUH_FSK;
+ }
+
+ hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, flags,
+ hbx, sizeof(hbx));
+ if (hbx_len == 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ return hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len);
+}
+
+static void on_tnc_ax25_frame(const uint8_t *frame, size_t len, void *userdata)
+{
+ packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata;
+
+ if (len == 0 || inst == NULL) {
+ return;
+ }
+
+ if (packet_radio_frame_source_banned(frame, len)) {
+ return;
+ }
+
+ instance_note_rf_activity(inst);
+ (void)instance_circuit_uplink(inst, frame, len);
+}
+
+static void on_tnc_host_ui(const uint8_t *payload, size_t len,
+ const hybbx_ax25_path_t *path, void *userdata)
+{
+ packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata;
+ uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME];
+ size_t hbx_len;
+
+ (void)path;
+
+ if (inst == NULL || inst->circuit_fd < 0 || len == 0) {
+ return;
+ }
+
+ if (path != NULL && packet_radio_source_banned(path)) {
+ return;
+ }
+
+ if (!instance_circuit_uplink_allowed(inst)) {
+ return;
+ }
+
+ hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL,
+ HYBBX_CIRCUIT_FLAG_RX,
+ payload, len, hbx, sizeof(hbx));
+ if (hbx_len > 0) {
+ (void)hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len);
+ }
+}
+
+static void instance_format_call(const hybbx_ax25_address_t *addr,
+ char *out, size_t out_len)
+{
+ if (out == NULL || out_len == 0) {
+ return;
+ }
+
+ out[0] = '\0';
+ if (addr == NULL || addr->call[0] == '\0') {
+ return;
+ }
+
+ if (addr->ssid > 0u) {
+ unsigned ssid = addr->ssid;
+
+ if (ssid > HYBBX_AX25_SSID_MAX) {
+ ssid = HYBBX_AX25_SSID_MAX;
+ }
+ snprintf(out, out_len, "%s-%u", addr->call, ssid);
+ } else {
+ hybbx_strlcpy(out, addr->call, out_len);
+ }
+}
+
+static void instance_log_rf_tx(packet_radio_instance_t *inst,
+ hybbx_result_t rc,
+ const hybbx_ax25_path_t *path,
+ size_t payload_len)
+{
+ char src[HYBBX_AX25_CALL_MAX + 8];
+ char dst[HYBBX_AX25_CALL_MAX + 8];
+
+ if (inst == NULL) {
+ return;
+ }
+
+ instance_format_call(path != NULL ? &path->source : NULL, src, sizeof(src));
+ instance_format_call(path != NULL ? &path->dest : NULL, dst, sizeof(dst));
+
+ if (rc == HYBBX_OK) {
+ hybbx_log_stats("[packet_radio%u] RF TX %s>%s (%zu bytes)",
+ inst->index + 1, src, dst, payload_len);
+ } else {
+ hybbx_log_warn("[packet_radio%u] RF TX failed %s>%s (%zu bytes, rc=%d)",
+ inst->index + 1, src, dst, payload_len, (int)rc);
+ }
+}
+
+static void instance_note_rf_activity(packet_radio_instance_t *inst)
+{
+ hybbx_circuit_hub_t *hub;
+ const char *link_id;
+
+ if (inst == NULL || g_service == NULL) {
+ return;
+ }
+
+ hub = hybbx_service_circuit_hub(g_service);
+ link_id = inst->config.link_id;
+ if (link_id == NULL || link_id[0] == '\0') {
+ link_id = "packet-radio";
+ }
+
+ if (hub != NULL) {
+ hybbx_circuit_hub_note_rf_activity(hub, link_id);
+ }
+}
+
+static hybbx_result_t instance_tx_ax25_ui(packet_radio_instance_t *inst,
+ const hybbx_ax25_path_t *path,
+ const uint8_t *payload, size_t len)
+{
+ uint8_t frame[HYBBX_AX25_FRAME_MAX];
+ size_t frame_len;
+ hybbx_result_t rc;
+
+ if (inst == NULL || inst->tnc == NULL || path == NULL ||
+ payload == NULL || len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ frame_len = hybbx_ax25_build_ui(path, payload, len, frame, sizeof(frame));
+ if (frame_len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_tnc_send_frame(inst->tnc, frame, frame_len);
+ if (rc == HYBBX_OK) {
+ instance_note_rf_activity(inst);
+ }
+ instance_log_rf_tx(inst, rc, path, len);
+ return rc;
+}
+
+static hybbx_result_t instance_send_bytes(packet_radio_instance_t *inst,
+ const uint8_t *data, size_t len)
+{
+ if (inst == NULL || inst->tnc == NULL || data == NULL || len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) {
+ if (hybbx_tnc_host_connected(inst->tnc)) {
+ return hybbx_tnc_send_converse(inst->tnc, (const char *)data, len);
+ }
+ return HYBBX_ERR_BUSY;
+ }
+
+ return hybbx_tnc_send_ui(inst->tnc, data, len);
+}
+
+static void instance_circuit_disconnect(packet_radio_instance_t *inst)
+{
+ if (inst == NULL) {
+ return;
+ }
+
+ if (inst->circuit_fd >= 0) {
+ close(inst->circuit_fd);
+ inst->circuit_fd = -1;
+ }
+ hybbx_circuit_decoder_init(&inst->circuit_dec);
+ inst->circuit_reconnect_polls = 0;
+}
+
+static void instance_circuit_reconnect(packet_radio_instance_t *inst)
+{
+ if (inst == NULL) {
+ return;
+ }
+
+ instance_circuit_disconnect(inst);
+ packet_radio_poll_sleep_ms(250);
+ if (instance_connect_circuit(inst) == HYBBX_OK) {
+ hybbx_log_info("[packet_radio%u] circuit reconnected", inst->index + 1);
+ }
+}
+
+static void instance_on_circuit_flow_ctrl(packet_radio_instance_t *inst,
+ hybbx_circuit_proto_t proto,
+ uint16_t flags,
+ const uint8_t *payload, size_t len)
+{
+ hybbx_circuit_balance_action_t action;
+ char reason[64];
+
+ (void)proto;
+ (void)flags;
+
+ if (inst == NULL) {
+ return;
+ }
+
+ if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action,
+ reason, sizeof(reason)) != HYBBX_OK) {
+ return;
+ }
+
+ switch (action) {
+ case HYBBX_CIRCUIT_BAL_PAUSE:
+ inst->flow_paused = 1;
+ hybbx_log_stats("[packet_radio%u] circuit flow pause (%s)",
+ inst->index + 1, reason);
+ break;
+ case HYBBX_CIRCUIT_BAL_BREAK:
+ inst->flow_paused = 0;
+ hybbx_circuit_decoder_init(&inst->circuit_dec);
+ hybbx_log_stats("[packet_radio%u] circuit flow break (%s)",
+ inst->index + 1, reason);
+ break;
+ case HYBBX_CIRCUIT_BAL_CANCEL:
+ inst->flow_cancel = 1;
+ hybbx_log_warn("[packet_radio%u] circuit flow cancel (%s)",
+ inst->index + 1, reason);
+ break;
+ case HYBBX_CIRCUIT_BAL_RESUME:
+ inst->flow_paused = 0;
+ hybbx_log_stats("[packet_radio%u] circuit flow resume (%s)",
+ inst->index + 1, reason);
+ break;
+ default:
+ break;
+ }
+}
+
+static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags,
+ const uint8_t *payload, size_t len,
+ void *userdata)
+{
+ packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata;
+
+ if (inst == NULL) {
+ return;
+ }
+
+ if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) {
+ instance_on_circuit_flow_ctrl(inst, proto, flags, payload, len);
+ return;
+ }
+
+ if (inst->tnc == NULL || len == 0) {
+ return;
+ }
+
+ switch (proto) {
+ case HYBBX_CIRCUIT_PROTO_TERMINAL:
+ (void)instance_send_bytes(inst, payload, len);
+ break;
+ case HYBBX_CIRCUIT_PROTO_AX25: {
+ hybbx_result_t rc = hybbx_tnc_send_frame(inst->tnc, payload, len);
+ if (rc == HYBBX_OK) {
+ hybbx_ax25_path_t path;
+ uint8_t ui[HYBBX_AX25_PAYLOAD_MAX];
+ size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path,
+ ui, sizeof(ui));
+
+ instance_note_rf_activity(inst);
+ if (ui_len > 0) {
+ instance_log_rf_tx(inst, rc, &path, ui_len);
+ }
+ } else {
+ hybbx_log_warn("[packet_radio%u] RF TX frame failed (rc=%d, %zu bytes)",
+ inst->index + 1, (int)rc, len);
+ }
+ break;
+ }
+ case HYBBX_CIRCUIT_PROTO_AX25_UI: {
+ uint8_t ui[HYBBX_AX25_PAYLOAD_MAX];
+ hybbx_ax25_path_t path;
+ size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path,
+ ui, sizeof(ui));
+ if (ui_len > 0) {
+ (void)instance_tx_ax25_ui(inst, &path, ui, ui_len);
+ } else {
+ hybbx_log_warn("[packet_radio%u] AX25_UI unpack failed (%zu bytes)",
+ inst->index + 1, len);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void packet_radio_poll_sleep_ms(unsigned ms)
+{
+#if defined(_WIN32)
+ Sleep(ms);
+#else
+ struct timeval tv;
+
+ tv.tv_sec = (time_t)(ms / 1000u);
+ tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u);
+ (void)select(0, NULL, NULL, NULL, &tv);
+#endif
+}
+
+static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst)
+{
+ const char *host;
+ unsigned port;
+ unsigned attempt;
+
+ if (inst == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ host = inst->config.circuit_host;
+ port = inst->config.circuit_port;
+
+ if (host == NULL || host[0] == '\0') {
+ host = "127.0.0.1";
+ }
+ if (port == 0) {
+ port = HYBBX_CIRCUIT_DEFAULT_PORT;
+ }
+
+ if (inst->config.link_password == NULL ||
+ inst->config.link_password[0] == '\0') {
+ hybbx_log_warn("[packet_radio%u] missing link_password for HBX LINK_AUTH; "
+ "set link_password in [transport.packet_radioN] "
+ "(hub logs link_auth_fail reason=timeout_no_link_auth)",
+ inst->index + 1);
+ return HYBBX_ERR_INVALID;
+ }
+
+ for (attempt = 0; attempt < 50; attempt++) {
+ hybbx_result_t rc = hybbx_circuit_link_connect(host, port,
+ &inst->circuit_fd);
+ if (rc == HYBBX_OK) {
+ hybbx_circuit_decoder_init(&inst->circuit_dec);
+ {
+ const char *link_id = inst->config.link_id;
+ const char *link_role = inst->config.link_role;
+
+ if (link_id == NULL || link_id[0] == '\0') {
+ link_id = "packet-radio";
+ }
+ if (link_role == NULL || link_role[0] == '\0') {
+ link_role = "link";
+ }
+
+ {
+ hybbx_circuit_link_qos_t qos;
+ int duplex = (int)inst->config.params.duplex;
+
+ memset(&qos, 0, sizeof(qos));
+ qos.baud = inst->config.params.radio_baud;
+ if (duplex == HYBBX_PACKET_RADIO_DUPLEX_HALF) {
+ qos.duplex = 1;
+ } else if (duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) {
+ qos.duplex = 2;
+ }
+ qos.bandwidth = "low";
+ qos.frequency_mhz = inst->config.frequency_mhz;
+
+ rc = hybbx_circuit_link_authenticate_ex(
+ inst->circuit_fd, inst->config.link_password,
+ link_role, link_id, &qos);
+ }
+ if (rc != HYBBX_OK) {
+ close(inst->circuit_fd);
+ inst->circuit_fd = -1;
+ packet_radio_poll_sleep_ms(100);
+ continue;
+ }
+ }
+ hybbx_log_info("[packet_radio%u] linked to internal circuit %s:%u (HBX)",
+ inst->index + 1, host, port);
+ return HYBBX_OK;
+ }
+ packet_radio_poll_sleep_ms(100);
+ }
+
+ hybbx_log_warn("[packet_radio%u] could not connect to internal circuit %s:%u",
+ inst->index + 1, host, port);
+ return HYBBX_ERR_IO;
+}
+
+static void instance_shutdown(packet_radio_instance_t *inst)
+{
+ if (inst == NULL) {
+ return;
+ }
+
+ if (inst->circuit_fd >= 0) {
+ close(inst->circuit_fd);
+ inst->circuit_fd = -1;
+ }
+
+ if (inst->tnc != NULL) {
+ hybbx_tnc_close(inst->tnc);
+ inst->tnc = NULL;
+ }
+
+ hybbx_packet_radio_config_free(&inst->config);
+ memset(inst, 0, sizeof(*inst));
+ inst->circuit_fd = -1;
+}
+
+typedef enum {
+ PR_POLL_CIRCUIT = 0,
+ PR_POLL_SERIAL = 1
+} packet_radio_poll_kind_t;
+
+typedef struct {
+ unsigned instance_index;
+ packet_radio_poll_kind_t kind;
+} packet_radio_poll_map_t;
+
+static void instance_drain_circuit(packet_radio_instance_t *inst)
+{
+ uint8_t buf[512];
+
+ if (inst == NULL || inst->circuit_fd < 0) {
+ return;
+ }
+
+ for (;;) {
+ size_t read_len = 0;
+ hybbx_result_t rc = hybbx_circuit_link_read(inst->circuit_fd, buf,
+ sizeof(buf), &read_len);
+
+ if (rc == HYBBX_ERR_IO) {
+ hybbx_log_warn("[packet_radio%u] circuit disconnected — reconnecting",
+ inst->index + 1);
+ instance_circuit_reconnect(inst);
+ return;
+ }
+ if (rc != HYBBX_OK || read_len == 0) {
+ break;
+ }
+
+ hybbx_circuit_decoder_feed(&inst->circuit_dec, buf, read_len,
+ on_circuit_downlink, inst);
+ }
+}
+
+#if !defined(_WIN32)
+static void packet_radio_poll_instances(void)
+{
+ struct pollfd pfds[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u];
+ packet_radio_poll_map_t map[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u];
+ nfds_t nfds = 0;
+ unsigned i;
+
+ for (i = 0; i < g_instance_count; i++) {
+ packet_radio_instance_t *inst = &g_instances[i];
+ int circuit_fd = inst->circuit_fd;
+ int serial_fd = inst->tnc != NULL ? hybbx_tnc_serial_fd(inst->tnc) : -1;
+
+ if (circuit_fd >= 0) {
+ pfds[nfds].fd = circuit_fd;
+ pfds[nfds].events = POLLIN;
+ pfds[nfds].revents = 0;
+ map[nfds].instance_index = i;
+ map[nfds].kind = PR_POLL_CIRCUIT;
+ nfds++;
+ }
+
+ if (serial_fd >= 0) {
+ pfds[nfds].fd = serial_fd;
+ pfds[nfds].events = POLLIN;
+ pfds[nfds].revents = 0;
+ map[nfds].instance_index = i;
+ map[nfds].kind = PR_POLL_SERIAL;
+ nfds++;
+ }
+ }
+
+ if (nfds > 0) {
+ int pr = poll(pfds, nfds, (int)PACKET_RADIO_POLL_MS);
+
+ if (pr < 0) {
+ if (errno != EINTR) {
+ hybbx_log_warn("[packet_radio] poll failed");
+ }
+ } else if (pr > 0) {
+ nfds_t j;
+
+ for (j = 0; j < nfds; j++) {
+ short rev = pfds[j].revents;
+
+ if (rev == 0) {
+ continue;
+ }
+
+ if ((rev & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
+ if (map[j].kind == PR_POLL_CIRCUIT) {
+ packet_radio_instance_t *inst =
+ &g_instances[map[j].instance_index];
+
+ hybbx_log_warn("[packet_radio%u] circuit hangup — "
+ "reconnecting",
+ inst->index + 1);
+ instance_circuit_reconnect(inst);
+ }
+ continue;
+ }
+
+ if ((rev & POLLIN) == 0) {
+ continue;
+ }
+
+ if (map[j].kind == PR_POLL_CIRCUIT) {
+ instance_drain_circuit(&g_instances[map[j].instance_index]);
+ } else {
+ packet_radio_instance_t *inst =
+ &g_instances[map[j].instance_index];
+
+ if (inst->tnc != NULL) {
+ (void)hybbx_tnc_poll(inst->tnc);
+ }
+ }
+ }
+ }
+ } else {
+ packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS);
+ }
+}
+#endif
+
+static void packet_radio_service_maintenance(void)
+{
+ unsigned i;
+
+ for (i = 0; i < g_instance_count; i++) {
+ packet_radio_instance_t *inst = &g_instances[i];
+
+ if (inst->circuit_fd < 0) {
+ inst->circuit_reconnect_polls++;
+ if (inst->circuit_reconnect_polls >= 50u) {
+ inst->circuit_reconnect_polls = 0;
+ (void)instance_connect_circuit(inst);
+ }
+ }
+
+ if (inst->flow_cancel) {
+ inst->flow_cancel = 0;
+ inst->flow_paused = 0;
+ instance_circuit_reconnect(inst);
+ }
+ }
+}
+
+#if defined(_WIN32)
+static void packet_radio_poll_win32(void)
+{
+ unsigned i;
+
+ for (i = 0; i < g_instance_count; i++) {
+ packet_radio_instance_t *inst = &g_instances[i];
+
+ if (inst->tnc != NULL) {
+ (void)hybbx_tnc_poll(inst->tnc);
+ }
+
+ if (inst->circuit_fd >= 0) {
+ instance_drain_circuit(inst);
+ }
+ }
+}
+#endif
+
+static void *packet_radio_poll_thread(void *arg)
+{
+ (void)arg;
+
+ while (g_radio_running) {
+#if !defined(_WIN32)
+ packet_radio_poll_instances();
+#else
+ packet_radio_poll_win32();
+ packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS);
+#endif
+ packet_radio_service_maintenance();
+ }
+
+ return NULL;
+}
+
+static hybbx_result_t instance_start(packet_radio_instance_t *inst,
+ const char *config,
+ unsigned index)
+{
+ hybbx_result_t rc;
+ hybbx_tnc_frame_cb frame_cb = on_tnc_ax25_frame;
+ hybbx_tnc_ui_cb ui_cb = NULL;
+
+ if (inst == NULL || config == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ memset(inst, 0, sizeof(*inst));
+ inst->circuit_fd = -1;
+ inst->index = index;
+
+ rc = hybbx_packet_radio_config_parse(config, &inst->config);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[packet_radio%u] invalid configuration", index + 1);
+ instance_shutdown(inst);
+ return rc;
+ }
+
+ if (g_service != NULL) {
+ const hybbx_broadcast_config_t *bc =
+ hybbx_service_get_broadcast(g_service);
+
+ if (bc != NULL && bc->ax25_mycall[0] != '\0') {
+ size_t n = strlen(bc->ax25_mycall) + 1;
+ char *mc = malloc(n);
+
+ if (mc != NULL) {
+ memcpy(mc, bc->ax25_mycall, n);
+ free(inst->config.mycall);
+ inst->config.mycall = mc;
+ hybbx_log_info("[packet_radio%u] ax25 source=%s (from [broadcast] ax25_mycall)",
+ index + 1, inst->config.mycall);
+ }
+ }
+ }
+
+ hybbx_log_info("[packet_radio%u] tnc=%s protocol=%s device_type=%s device=%s "
+ "band=%s duplex=%s modulation=%s circuit=%s:%u "
+ "(HBX over internal TCP)",
+ index + 1,
+ tnc_name(inst->config.tnc),
+ protocol_name(inst->config.protocol),
+ device_type_name(inst->config.device_type),
+ inst->config.device,
+ hybbx_packet_radio_band_name(inst->config.params.band),
+ hybbx_packet_radio_duplex_name(inst->config.params.duplex),
+ hybbx_packet_radio_modulation_name(inst->config.params.modulation),
+ inst->config.circuit_host,
+ inst->config.circuit_port);
+ if (inst->config.frequency_mhz != NULL &&
+ inst->config.frequency_mhz[0] != '\0') {
+ hybbx_log_info("[packet_radio%u] frequency_mhz=%s", index + 1,
+ inst->config.frequency_mhz);
+ }
+
+ rc = instance_connect_circuit(inst);
+ if (rc != HYBBX_OK) {
+ instance_shutdown(inst);
+ return rc;
+ }
+
+ if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) {
+ frame_cb = NULL;
+ ui_cb = on_tnc_host_ui;
+ }
+
+ rc = hybbx_tnc_open(&inst->tnc, &inst->config, frame_cb, ui_cb, inst);
+ if (rc != HYBBX_OK) {
+ instance_shutdown(inst);
+ return rc;
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t packet_radio_init(hybbx_service_t *service)
+{
+ g_service = service;
+ g_instance_count = 0;
+ g_radio_running = 0;
+ return HYBBX_OK;
+}
+
+static void packet_radio_shutdown(void)
+{
+ unsigned i;
+
+ g_radio_running = 0;
+ if (g_poll_thread_started) {
+ pthread_join(g_poll_thread, NULL);
+ g_poll_thread_started = 0;
+ }
+
+ for (i = 0; i < g_instance_count; i++) {
+ instance_shutdown(&g_instances[i]);
+ }
+ g_instance_count = 0;
+}
+
+static hybbx_result_t packet_radio_start(const char *config)
+{
+ char scratch[4096];
+ const char *cursor;
+ hybbx_max25_config_t max25;
+ hybbx_max25_status_t max25_status;
+ hybbx_result_t rc = HYBBX_OK;
+ unsigned started = 0;
+ unsigned section_num = 0;
+ unsigned local_edges;
+
+ (void)g_service;
+
+ if (config == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ packet_radio_shutdown();
+ g_poll_thread = (pthread_t)0;
+
+ cursor = hybbx_max25_config_skip_prefix(config, &max25);
+ local_edges = packet_radio_count_local_edges(cursor);
+ if (local_edges > 0u) {
+ rc = hybbx_max25_wait_ready(&max25);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ }
+
+ while (*cursor != '\0' && g_instance_count < HYBBX_PACKET_RADIO_MAX_INSTANCES) {
+ const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP);
+ size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor);
+
+ if (len == 0) {
+ cursor = sep != NULL ? sep + 1 : cursor + len;
+ if (sep == NULL) {
+ break;
+ }
+ continue;
+ }
+
+ if (len >= sizeof(scratch)) {
+ packet_radio_shutdown();
+ return HYBBX_ERR_INVALID;
+ }
+
+ memcpy(scratch, cursor, len);
+ scratch[len] = '\0';
+ section_num++;
+
+ if (!hybbx_packet_radio_section_is_local_edge(scratch)) {
+ hybbx_log_info("[packet_radio%u] bridge registry only — no local TNC",
+ section_num);
+ cursor = sep != NULL ? sep + 1 : cursor + len;
+ if (sep == NULL) {
+ break;
+ }
+ continue;
+ }
+
+ rc = instance_start(&g_instances[g_instance_count], scratch,
+ g_instance_count);
+ if (rc != HYBBX_OK) {
+ packet_radio_shutdown();
+ return rc;
+ }
+
+ g_instance_count++;
+ started++;
+ cursor = sep != NULL ? sep + 1 : cursor + len;
+ if (sep == NULL) {
+ break;
+ }
+ }
+
+ if (started == 0) {
+ return HYBBX_OK;
+ }
+
+ g_radio_running = 1;
+ if (pthread_create(&g_poll_thread, NULL, packet_radio_poll_thread,
+ NULL) != 0) {
+ hybbx_log_warn("[packet_radio] poll thread failed");
+ packet_radio_shutdown();
+ return HYBBX_ERR_IO;
+ }
+ g_poll_thread_started = 1;
+
+ hybbx_log_info("[packet_radio] %u TNC instance(s) active", started);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t packet_radio_stop(void)
+{
+ hybbx_log_info("[packet_radio] stop");
+ packet_radio_shutdown();
+ return HYBBX_OK;
+}
+
+const hybbx_transport_plugin_t hybbx_plugin_packet_radio = {
+ .name = "packet_radio",
+ .kind = HYBBX_TRANSPORT_PACKET_RADIO,
+ .version = 1,
+ .init = packet_radio_init,
+ .shutdown = packet_radio_shutdown,
+ .start = packet_radio_start,
+ .stop = packet_radio_stop,
+ .write = NULL,
+};
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com