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/ardop/ardop_link.c | 446 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 plugins/ardop/ardop_link.c (limited to 'plugins/ardop/ardop_link.c') diff --git a/plugins/ardop/ardop_link.c b/plugins/ardop/ardop_link.c new file mode 100644 index 0000000..e0500b9 --- /dev/null +++ b/plugins/ardop/ardop_link.c @@ -0,0 +1,446 @@ +#include "ardop_link.h" +#include "ardop_host.h" + +#include "hybbx/ardop.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/crdop.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +static const ardop_link_plugin_t *g_plugin; +static hybbx_ardop_config_t g_active_config; +static ardop_host_t *g_host; +static int g_circuit_fd = -1; +static hybbx_circuit_decoder_t g_circuit_dec; +static pthread_t g_poll_thread; +static volatile int g_running; +static int g_poll_thread_started; +static volatile int g_circuit_flow_paused; +static volatile int g_circuit_flow_cancel; +static unsigned g_modem_reconnect_backoff_ms; + +static const char *link_tag(void) +{ + return g_plugin != NULL && g_plugin->name != NULL ? g_plugin->name : "ardop"; +} + +static int circuit_uplink_allowed(void) +{ + return !g_circuit_flow_paused && !g_circuit_flow_cancel; +} + +static hybbx_result_t circuit_uplink_terminal(const uint8_t *payload, size_t len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + if (g_circuit_fd < 0 || len == 0) { + return HYBBX_ERR_IO; + } + + if (!circuit_uplink_allowed()) { + return HYBBX_OK; + } + + hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_RX, + payload, len, hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(g_circuit_fd, hbx, hbx_len); +} + +static void on_modem_data(const uint8_t *data, size_t len, void *userdata) +{ + (void)userdata; + + if (len == 0 || g_circuit_fd < 0 || !circuit_uplink_allowed()) { + return; + } + + (void)circuit_uplink_terminal(data, len); +} + +static void on_modem_event(const char *event, const char *detail, void *userdata) +{ + (void)userdata; + + if (event == NULL) { + return; + } + + if (strcmp(event, "connected") == 0) { + if (detail != NULL && detail[0] != '\0') { + hybbx_log_stats("[%s] RF link up (%s)", link_tag(), detail); + } else { + hybbx_log_stats("[%s] RF link up", link_tag()); + } + g_modem_reconnect_backoff_ms = 1000u; + return; + } + + if (strcmp(event, "disconnected") == 0) { + hybbx_log_stats("[%s] RF link down", link_tag()); + } +} + +static void on_circuit_flow_ctrl(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + (void)proto; + (void)flags; + (void)userdata; + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + g_circuit_flow_paused = 1; + hybbx_log_stats("[%s] circuit flow pause (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + g_circuit_flow_paused = 0; + hybbx_circuit_decoder_init(&g_circuit_dec); + hybbx_log_stats("[%s] circuit flow break (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + g_circuit_flow_cancel = 1; + hybbx_log_warn("[%s] circuit flow cancel (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + g_circuit_flow_paused = 0; + hybbx_log_stats("[%s] circuit flow resume (%s)", link_tag(), 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) +{ + (void)flags; + (void)userdata; + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + on_circuit_flow_ctrl(proto, flags, payload, len, userdata); + return; + } + + if (g_host == NULL || len == 0 || g_circuit_flow_paused) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_TERMINAL) { + return; + } + + if (!ardop_host_link_up(g_host)) { + return; + } + + (void)ardop_host_send_data(g_host, payload, len); +} + +static void link_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 link_connect_circuit(void); + +static void link_maybe_reconnect_modem(void) +{ + static unsigned ticks; + + if (g_host == NULL) { + return; + } + + if (ardop_host_modem_connected(g_host)) { + ticks = 0; + return; + } + + ticks++; + if (ticks < (g_modem_reconnect_backoff_ms / 20u)) { + return; + } + + ticks = 0; + if (ardop_host_connect(g_host) != HYBBX_OK) { + if (g_modem_reconnect_backoff_ms < 30000u) { + g_modem_reconnect_backoff_ms += 1000u; + } + } +} + +static void *link_poll_thread(void *arg) +{ + uint8_t buf[512]; + + (void)arg; + + while (g_running) { + if (g_host != NULL) { + ardop_host_poll(g_host); + link_maybe_reconnect_modem(); + } + + if (g_circuit_fd >= 0) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(g_circuit_fd, buf, + sizeof(buf), &read_len); + if (rc == HYBBX_OK && read_len > 0) { + hybbx_circuit_decoder_feed(&g_circuit_dec, buf, read_len, + on_circuit_downlink, NULL); + } + } else { + static unsigned circuit_ticks; + + circuit_ticks++; + if (circuit_ticks >= 50u) { + circuit_ticks = 0; + (void)link_connect_circuit(); + } + } + + if (g_circuit_flow_cancel) { + g_circuit_flow_cancel = 0; + g_circuit_flow_paused = 0; + if (g_circuit_fd >= 0) { + close(g_circuit_fd); + g_circuit_fd = -1; + } + (void)link_connect_circuit(); + } + + link_poll_sleep_ms(20); + } + + return NULL; +} + +static hybbx_result_t link_connect_circuit(void) +{ + const char *host = g_active_config.circuit_host; + unsigned port = g_active_config.circuit_port; + const char *link_id = g_active_config.link_id; + unsigned attempt; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + if (link_id == NULL || link_id[0] == '\0') { + link_id = g_plugin != NULL && g_plugin->default_link_id != NULL ? + g_plugin->default_link_id : "ardop-link"; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &g_circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&g_circuit_dec); + if (g_active_config.link_password != NULL && + g_active_config.link_password[0] != '\0') { + const char *link_role = g_active_config.link_role; + hybbx_circuit_link_qos_t qos; + char freq_buf[16]; + const char *freq_ptr = NULL; + + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + memset(&qos, 0, sizeof(qos)); + qos.bandwidth = "low"; + qos.duplex = 1; + if (g_active_config.frequency_mhz > 0.0) { + snprintf(freq_buf, sizeof(freq_buf), "%.3f", + g_active_config.frequency_mhz); + freq_ptr = freq_buf; + } + qos.frequency_mhz = freq_ptr; + + rc = hybbx_circuit_link_authenticate_ex( + g_circuit_fd, g_active_config.link_password, + link_role, link_id, &qos); + if (rc != HYBBX_OK) { + close(g_circuit_fd); + g_circuit_fd = -1; + link_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[%s] linked to internal circuit %s:%u (HBX)", + link_tag(), host, port); + return HYBBX_OK; + } + link_poll_sleep_ms(100); + } + + hybbx_log_warn("[%s] could not connect to internal circuit %s:%u", + link_tag(), host, port); + return HYBBX_ERR_IO; +} + +static void link_shutdown(void) +{ + g_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + if (g_circuit_fd >= 0) { + close(g_circuit_fd); + g_circuit_fd = -1; + } + + if (g_host != NULL) { + ardop_host_destroy(g_host); + g_host = NULL; + } + + if (g_plugin != NULL && g_plugin->config_free != NULL) { + g_plugin->config_free(&g_active_config); + } +} + +hybbx_result_t ardop_link_init(struct hybbx_service *service, + const ardop_link_plugin_t *plugin) +{ + (void)service; + g_plugin = plugin; + g_host = NULL; + g_circuit_fd = -1; + g_running = 0; + g_circuit_flow_paused = 0; + g_circuit_flow_cancel = 0; + g_modem_reconnect_backoff_ms = 1000u; + return HYBBX_OK; +} + +void ardop_link_shutdown(const ardop_link_plugin_t *plugin) +{ + (void)plugin; + link_shutdown(); +} + +hybbx_result_t ardop_link_start(const ardop_link_plugin_t *plugin, + const char *config) +{ + hybbx_result_t rc; + const char *modem_hint; + + if (plugin == NULL || plugin->parse_config == NULL) { + return HYBBX_ERR_INVALID; + } + + g_plugin = plugin; + link_shutdown(); + memset(&g_active_config, 0, sizeof(g_active_config)); + g_poll_thread = (pthread_t)0; + + rc = plugin->parse_config(config, &g_active_config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] invalid configuration", link_tag()); + return rc; + } + + hybbx_log_info("[%s] external modem %s:%u mycall=%s arq=%s profile=%s circuit=%s:%u " + "link_id=%s", + link_tag(), + g_active_config.ardop_host, + g_active_config.ardop_port, + g_active_config.mycall, + g_active_config.arq_bandwidth, + hybbx_crdop_profile_name(g_active_config.radio_profile), + g_active_config.circuit_host, + g_active_config.circuit_port, + g_active_config.link_id != NULL ? g_active_config.link_id : "?"); + + if (g_active_config.radio_profile == HYBBX_CRDOP_PROFILE_CB) { + hybbx_log_info("[%s] CB half-duplex profile (experimental Level 2)", + link_tag()); + } + if (g_active_config.frequency_mhz > 0.0) { + hybbx_log_info("[%s] frequency_mhz=%.3f", link_tag(), + g_active_config.frequency_mhz); + } + + modem_hint = plugin->modem_hint != NULL ? plugin->modem_hint : "ARDOPC"; + hybbx_log_info("[%s] operator must run external %s separately (e.g. ardopc TCPIP %u)", + link_tag(), modem_hint, g_active_config.ardop_port); + + g_host = ardop_host_create(&g_active_config, on_modem_data, on_modem_event, + NULL, link_tag()); + if (g_host == NULL) { + link_shutdown(); + return HYBBX_ERR_NOMEM; + } + + rc = ardop_host_connect(g_host); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] external modem not reachable yet — will retry in poll loop", + link_tag()); + } + + rc = link_connect_circuit(); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] circuit hub not reachable — HBX uplink deferred", + link_tag()); + } + + g_running = 1; + if (pthread_create(&g_poll_thread, NULL, link_poll_thread, NULL) != 0) { + hybbx_log_warn("[%s] poll thread failed", link_tag()); + link_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + return HYBBX_OK; +} + +hybbx_result_t ardop_link_stop(const ardop_link_plugin_t *plugin) +{ + (void)plugin; + hybbx_log_info("[%s] stop", link_tag()); + link_shutdown(); + return HYBBX_OK; +} -- cgit v1.3.1