From 20cb29c2f8c5c87bc590896854a20b1473ceb358 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sat, 8 Aug 2026 03:54:55 +0000 Subject: #2 --- plugins/baycom/baycom.c | 623 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 plugins/baycom/baycom.c (limited to 'plugins/baycom/baycom.c') diff --git a/plugins/baycom/baycom.c b/plugins/baycom/baycom.c new file mode 100644 index 0000000..e2fb5c9 --- /dev/null +++ b/plugins/baycom/baycom.c @@ -0,0 +1,623 @@ +/* + * baycom — BayCom PR-Stack link adapter (Linux kernel SER12/PAR96/EPP or KISS serial). + * INI: [transport.baycom] or [transport.baycomN]. See docs/BAYCOM.md. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/baycom.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/limits.h" +#include "hybbx/max25.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "hybbx/ax25.h" + +#include "baycom_modem.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +typedef struct baycom_instance { + hybbx_baycom_config_t config; + baycom_modem_t *modem; + int circuit_fd; + hybbx_circuit_decoder_t circuit_dec; + volatile int flow_paused; + volatile int flow_cancel; + unsigned circuit_reconnect_polls; + unsigned index; +} baycom_instance_t; + +static hybbx_service_t *g_service; + +static baycom_instance_t g_instances[HYBBX_BAYCOM_MAX_INSTANCES]; +static unsigned g_instance_count; +static pthread_t g_poll_thread; +static volatile int g_running; +static int g_poll_thread_started; + +static void baycom_poll_sleep_ms(unsigned ms); +static hybbx_result_t instance_connect_circuit(baycom_instance_t *inst); +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata); + +static int instance_circuit_uplink_allowed(const baycom_instance_t *inst) +{ + return inst != NULL && !inst->flow_paused && !inst->flow_cancel; +} + +static hybbx_result_t instance_circuit_uplink(baycom_instance_t *inst, + const uint8_t *frame, + size_t frame_len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + if (inst == NULL || inst->circuit_fd < 0 || frame_len == 0) { + return HYBBX_ERR_IO; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return HYBBX_OK; + } + + hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, HYBBX_CIRCUIT_FLAG_RX, + hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return 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_note_rf_activity(const baycom_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 (hub != NULL && link_id != NULL && link_id[0] != '\0') { + hybbx_circuit_hub_note_rf_activity(hub, link_id); + } +} + +static void instance_log_rf_tx(const baycom_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("[baycom%u] RF TX %s>%s (%zu bytes)", + inst->index + 1, src, dst, payload_len); + } else { + hybbx_log_warn("[baycom%u] RF TX failed %s>%s (%zu bytes, rc=%d)", + inst->index + 1, src, dst, payload_len, (int)rc); + } +} + +static void instance_circuit_disconnect(baycom_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(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + instance_circuit_disconnect(inst); + baycom_poll_sleep_ms(250); + if (instance_connect_circuit(inst) == HYBBX_OK) { + hybbx_log_info("[baycom%u] circuit reconnected", inst->index + 1); + } +} + +static void instance_drain_circuit(baycom_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("[baycom%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); + } +} + +static void on_modem_ax25_frame(const uint8_t *frame, size_t len, void *userdata) +{ + baycom_instance_t *inst = (baycom_instance_t *)userdata; + + if (len == 0 || inst == NULL) { + return; + } + + (void)instance_circuit_uplink(inst, frame, len); +} + +static void baycom_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(baycom_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; + } + + 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); + if (inst->config.link_password != NULL && + inst->config.link_password[0] != '\0') { + const char *link_id = inst->config.link_id; + const char *link_role = inst->config.link_role; + hybbx_circuit_link_qos_t qos; + + if (link_id == NULL || link_id[0] == '\0') { + link_id = "baycom-link"; + } + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + memset(&qos, 0, sizeof(qos)); + qos.baud = inst->config.radio_baud; + qos.duplex = inst->config.full_duplex ? 2 : 1; + 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; + baycom_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[baycom%u] linked to internal circuit %s:%u (HBX)", + inst->index + 1, host, port); + return HYBBX_OK; + } + baycom_poll_sleep_ms(100); + } + + hybbx_log_warn("[baycom%u] could not connect to internal circuit %s:%u", + inst->index + 1, host, port); + return HYBBX_ERR_IO; +} + +static void instance_on_circuit_flow_ctrl(baycom_instance_t *inst, + const uint8_t *payload, size_t len) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + 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("[baycom%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("[baycom%u] circuit flow break (%s)", inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + inst->flow_cancel = 1; + hybbx_log_warn("[baycom%u] circuit flow cancel (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + inst->flow_paused = 0; + hybbx_log_stats("[baycom%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) +{ + baycom_instance_t *inst = (baycom_instance_t *)userdata; + + (void)flags; + + if (inst == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + instance_on_circuit_flow_ctrl(inst, payload, len); + return; + } + + if (inst->modem == NULL || len == 0 || inst->flow_paused) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_AX25) { + hybbx_result_t rc = baycom_modem_send_frame(inst->modem, payload, len); + 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)); + + if (rc == HYBBX_OK) { + instance_note_rf_activity(inst); + if (ui_len > 0) { + instance_log_rf_tx(inst, rc, &path, ui_len); + } + } else { + hybbx_log_warn("[baycom%u] RF TX frame failed (rc=%d, %zu bytes)", + inst->index + 1, (int)rc, len); + } + } +} + +static void instance_shutdown(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->modem != NULL) { + baycom_modem_close(inst->modem); + inst->modem = NULL; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + + hybbx_baycom_config_free(&inst->config); + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; +} + +static void *baycom_poll_thread(void *arg) +{ + unsigned i; + + (void)arg; + + while (g_running) { + for (i = 0; i < g_instance_count; i++) { + baycom_instance_t *inst = &g_instances[i]; + + if (inst->modem != NULL) { + (void)baycom_modem_poll(inst->modem); + } + + instance_drain_circuit(inst); + + if (inst->circuit_fd < 0) { + inst->circuit_reconnect_polls++; + if (inst->circuit_reconnect_polls >= 50u) { + inst->circuit_reconnect_polls = 0; + instance_circuit_reconnect(inst); + } + } else { + inst->circuit_reconnect_polls = 0; + } + + if (inst->flow_cancel) { + inst->flow_cancel = 0; + inst->flow_paused = 0; + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + (void)instance_connect_circuit(inst); + } + } + + baycom_poll_sleep_ms(20); + } + + return NULL; +} + +static hybbx_result_t instance_start(baycom_instance_t *inst, + const char *config, + unsigned index) +{ + hybbx_result_t rc; + + if (inst == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; + inst->index = index; + + rc = hybbx_baycom_config_parse(config, &inst->config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom%u] invalid configuration", index + 1); + instance_shutdown(inst); + return rc; + } + + { + char msg[512]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + "[baycom%u] backend=%s mode=%s", + index + 1, + hybbx_baycom_backend_name(inst->config.backend), + hybbx_baycom_modem_mode_name(inst->config.mode)); + + if (inst->config.backend == HYBBX_BAYCOM_BACKEND_KERNEL) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " interface=%s module=%s iobase=0x%x irq=%u radio_baud=%u", + inst->config.interface != NULL ? inst->config.interface : "?", + inst->config.kernel_module != NULL ? inst->config.kernel_module + : "?", + inst->config.iobase, inst->config.irq, inst->config.radio_baud); + } else { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " device=%s serial_baud=%u", + inst->config.device != NULL ? inst->config.device : "?", + inst->config.serial_baud); + } + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " circuit=%s:%u (HBX over internal TCP)", + inst->config.circuit_host, inst->config.circuit_port); + hybbx_log_info("%s", msg); + } + + if (inst->config.frequency_mhz != NULL && + inst->config.frequency_mhz[0] != '\0') { + hybbx_log_info("[baycom%u] frequency_mhz=%s", index + 1, + inst->config.frequency_mhz); + } + + rc = instance_connect_circuit(inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + rc = baycom_modem_open(&inst->modem, &inst->config, on_modem_ax25_frame, + inst); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom%u] modem open failed (%s)", index + 1, + hybbx_result_name(rc)); + instance_shutdown(inst); + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t baycom_init(hybbx_service_t *service) +{ + g_service = service; + g_instance_count = 0; + g_running = 0; + return HYBBX_OK; +} + +static void baycom_shutdown(void) +{ + unsigned i; + + g_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 baycom_start(const char *config) +{ + hybbx_max25_config_t max25; + hybbx_result_t rc = HYBBX_OK; + unsigned started = 0; + const char *cursor; + const char *cursor_after_max25; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + baycom_shutdown(); + g_poll_thread = (pthread_t)0; + + cursor_after_max25 = hybbx_max25_config_skip_prefix(config, &max25); + if (max25.check && cursor_after_max25 != config) { + rc = hybbx_max25_wait_ready(&max25); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] local RF requires max25d — continuing without BayCom"); + return HYBBX_OK; + } + } + + cursor = cursor_after_max25; + while (*cursor != '\0' && g_instance_count < HYBBX_BAYCOM_MAX_INSTANCES) { + char scratch[4096]; + const char *sep = strchr(cursor, HYBBX_BAYCOM_INSTANCE_SEP); + size_t chunk_len; + + if (sep != NULL) { + chunk_len = (size_t)(sep - cursor); + } else { + chunk_len = strlen(cursor); + } + + if (chunk_len >= sizeof(scratch)) { + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, cursor, chunk_len); + scratch[chunk_len] = '\0'; + + rc = instance_start(&g_instances[g_instance_count], scratch, + g_instance_count); + if (rc == HYBBX_OK) { + g_instance_count++; + started++; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + if (started == 0) { + /* Soft-fail: keep HyBBX up (TNCs/telnet/…) if BayCom/based open fails. */ + hybbx_log_warn("[baycom] no instances started (%s) — continuing without BayCom", + rc != HYBBX_OK ? hybbx_result_name(rc) : "invalid"); + return HYBBX_OK; + } + + g_running = 1; + if (pthread_create(&g_poll_thread, NULL, baycom_poll_thread, NULL) != 0) { + baycom_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + return HYBBX_OK; +} + +static hybbx_result_t baycom_stop(void) +{ + baycom_shutdown(); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_baycom = { + .name = "baycom", + .kind = HYBBX_TRANSPORT_BAYCOM, + .version = 1, + .init = baycom_init, + .shutdown = baycom_shutdown, + .start = baycom_start, + .stop = baycom_stop, + .write = NULL, +}; -- cgit v1.3.1