diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 /plugins/ardop | |
#2
Diffstat (limited to 'plugins/ardop')
| -rw-r--r-- | plugins/ardop/CMakeLists.txt | 57 | ||||
| -rw-r--r-- | plugins/ardop/ardop.c | 49 | ||||
| -rw-r--r-- | plugins/ardop/ardop_config.c | 186 | ||||
| -rw-r--r-- | plugins/ardop/ardop_crc.c | 69 | ||||
| -rw-r--r-- | plugins/ardop/ardop_crc.h | 15 | ||||
| -rw-r--r-- | plugins/ardop/ardop_host.c | 650 | ||||
| -rw-r--r-- | plugins/ardop/ardop_host.h | 47 | ||||
| -rw-r--r-- | plugins/ardop/ardop_link.c | 446 | ||||
| -rw-r--r-- | plugins/ardop/ardop_link.h | 37 |
9 files changed, 1556 insertions, 0 deletions
diff --git a/plugins/ardop/CMakeLists.txt b/plugins/ardop/CMakeLists.txt new file mode 100644 index 0000000..c39ab91 --- /dev/null +++ b/plugins/ardop/CMakeLists.txt @@ -0,0 +1,57 @@ +add_library(hybbx_ardop_common STATIC + ardop_link.c + ardop_host.c + ardop_crc.c + ardop_config.c +) + +target_include_directories(hybbx_ardop_common + PUBLIC + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_ardop_common + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_ardop_common PUBLIC hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_ardop_common PUBLIC amiga) +endif() +hybbx_apply_hardening(hybbx_ardop_common) + +if(HYBBX_PLUGIN_ARDOP) + add_library(hybbx_plugin_ardop STATIC + ardop.c + ) + + target_include_directories(hybbx_plugin_ardop + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ) + + target_compile_definitions(hybbx_plugin_ardop + PRIVATE HYBBX_PLUGIN_BUILD + ) + + target_link_libraries(hybbx_plugin_ardop PRIVATE hybbx_ardop_common) + hybbx_link_plugin_instances(hybbx_plugin_ardop HYBBX_HAVE_PLUGIN_ARDOP) + hybbx_apply_hardening(hybbx_plugin_ardop) + + if(HYBBX_INSTALL_DEV) + install(TARGETS hybbx_plugin_ardop + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + ) + endif() +endif() + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_ardop_common + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/ardop/ardop.c b/plugins/ardop/ardop.c new file mode 100644 index 0000000..e07a158 --- /dev/null +++ b/plugins/ardop/ardop.c @@ -0,0 +1,49 @@ +/* + * ardop — ARDOP host-client link adapter (external ARDOPC / ardopcf). + * INI: [transport.ardop] or [transport.ardopN]. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/ardop.h" +#include "ardop_link.h" + +static const ardop_link_plugin_t g_ardop_link = { + .name = "ardop", + .kind = HYBBX_TRANSPORT_ARDOP, + .default_link_id = "ardop-link", + .modem_hint = "ARDOPC or ardopcf", + .parse_config = (ardop_link_config_fn)hybbx_ardop_config_parse, + .config_free = (void (*)(void *))hybbx_ardop_config_free, + .config_size = sizeof(hybbx_ardop_config_t), +}; + +static hybbx_result_t ardop_init(hybbx_service_t *service) +{ + return ardop_link_init(service, &g_ardop_link); +} + +static void ardop_shutdown(void) +{ + ardop_link_shutdown(&g_ardop_link); +} + +static hybbx_result_t ardop_start(const char *config) +{ + return ardop_link_start(&g_ardop_link, config); +} + +static hybbx_result_t ardop_stop(void) +{ + return ardop_link_stop(&g_ardop_link); +} + +const hybbx_transport_plugin_t hybbx_plugin_ardop = { + .name = "ardop", + .kind = HYBBX_TRANSPORT_ARDOP, + .version = 1, + .init = ardop_init, + .shutdown = ardop_shutdown, + .start = ardop_start, + .stop = ardop_stop, + .write = NULL, +}; diff --git a/plugins/ardop/ardop_config.c b/plugins/ardop/ardop_config.c new file mode 100644 index 0000000..71b40dc --- /dev/null +++ b/plugins/ardop/ardop_config.c @@ -0,0 +1,186 @@ +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static char *ardop_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_ardop_config_free(hybbx_ardop_config_t *config) +{ + if (config == NULL) { + return; + } + + free(config->ardop_host); + free(config->mycall); + free(config->arq_bandwidth); + free(config->peer_call); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + memset(config, 0, sizeof(*config)); +} + +hybbx_result_t hybbx_ardop_config_parse(const char *config, + hybbx_ardop_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ardop_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_kv(config, "ardop_host", scratch, sizeof(scratch)); + out->ardop_host = ardop_strdup(value != NULL ? value : "127.0.0.1"); + + out->ardop_port = (unsigned)strtoul( + find_kv(config, "ardop_port", scratch, sizeof(scratch)) != NULL ? + scratch : "8515", NULL, 10); + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + out->mycall = ardop_strdup(value != NULL ? value : "NOCALL"); + + value = find_kv(config, "radio_profile", scratch, sizeof(scratch)); + out->radio_profile = hybbx_crdop_profile_parse(value); + + value = find_kv(config, "arq_bandwidth", scratch, sizeof(scratch)); + out->arq_bandwidth = ardop_strdup( + value != NULL ? value : + hybbx_crdop_default_arq_bandwidth(out->radio_profile)); + + value = find_kv(config, "peer_call", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->peer_call = ardop_strdup(value); + } + + value = find_kv(config, "listen", scratch, sizeof(scratch)); + out->listen = value == NULL || str_ieq(value, "yes") || str_ieq(value, "1") || + str_ieq(value, "true"); + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = ardop_strdup(value != NULL ? value : "127.0.0.1"); + + out->circuit_port = (unsigned)strtoul( + find_kv(config, "circuit_port", scratch, sizeof(scratch)) != NULL ? + scratch : "7323", NULL, 10); + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = ardop_strdup(value != NULL ? value : "ardop-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = ardop_strdup(value); + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = ardop_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = strtod(value, NULL); + } + + if (out->radio_profile == HYBBX_CRDOP_PROFILE_CB && + hybbx_crdop_bandwidth_exceeds_cb(out->arq_bandwidth)) { + hybbx_log_warn("[ardop] warning: arq_bandwidth=%s high for CB profile " + "(CRDOP Level 2 experimental)", + out->arq_bandwidth); + } + + if (out->ardop_host == NULL || out->mycall == NULL || + out->arq_bandwidth == NULL || out->circuit_host == NULL || + out->link_id == NULL || out->link_role == NULL) { + hybbx_ardop_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/ardop/ardop_crc.c b/plugins/ardop/ardop_crc.c new file mode 100644 index 0000000..4cf205a --- /dev/null +++ b/plugins/ardop/ardop_crc.c @@ -0,0 +1,69 @@ +#include "ardop_crc.h" + +/* Algorithm from ARDOPC (KN6KB / G8BPQ) — CRC-16-CCITT variant, poly 0x8810. */ + +unsigned int ardop_crc16(const unsigned char *data, unsigned short length) +{ + int int_register = 0xffff; + int i; + int j; + int bit; + const int int_poly = 0x8810; + + if (data == NULL || length == 0) { + return 0xffffu; + } + + for (j = 0; j < (int)length; j++) { + int mask = 0x80; + + for (i = 0; i < 8; i++) { + bit = data[j] & mask; + mask >>= 1; + + if (int_register & 0x8000) { + if (bit) { + int_register = 0xffff & (1 + (int_register << 1)); + } else { + int_register = 0xffff & (int_register << 1); + } + int_register = int_register ^ int_poly; + } else { + if (bit) { + int_register = 0xffff & (1 + (int_register << 1)); + } else { + int_register = 0xffff & (int_register << 1); + } + } + } + } + + return (unsigned int)int_register; +} + +int ardop_crc16_valid(const unsigned char *data, unsigned short length) +{ + unsigned int crc; + + if (data == NULL || length < 3) { + return 0; + } + + crc = ardop_crc16(data, (unsigned short)(length - 2)); + return data[length - 2] == (unsigned char)(crc >> 8) && + data[length - 1] == (unsigned char)(crc & 0xff); +} + +size_t ardop_crc16_append(unsigned char *data, size_t length, size_t cap) +{ + unsigned int crc; + + if (data == NULL || length + 2 > cap) { + return 0; + } + + crc = ardop_crc16(data, (unsigned short)length); + data[length] = (unsigned char)(crc >> 8); + data[length + 1] = (unsigned char)(crc & 0xff); + return length + 2; +} diff --git a/plugins/ardop/ardop_crc.h b/plugins/ardop/ardop_crc.h new file mode 100644 index 0000000..69c2f07 --- /dev/null +++ b/plugins/ardop/ardop_crc.h @@ -0,0 +1,15 @@ +#ifndef ARDOP_CRC_H +#define ARDOP_CRC_H + +#include <stddef.h> + +/** ARDOP host/TNC CRC-16 (poly 0x8810, init 0xFFFF) per ARDOPC reference. */ +unsigned int ardop_crc16(const unsigned char *data, unsigned short length); + +/** Non-zero when @p length includes trailing 2-byte CRC that validates. */ +int ardop_crc16_valid(const unsigned char *data, unsigned short length); + +/** Append CRC16 (MSB, LSB) after @p length bytes; returns new length. */ +size_t ardop_crc16_append(unsigned char *data, size_t length, size_t cap); + +#endif /* ARDOP_CRC_H */ diff --git a/plugins/ardop/ardop_host.c b/plugins/ardop/ardop_host.c new file mode 100644 index 0000000..aeac403 --- /dev/null +++ b/plugins/ardop/ardop_host.c @@ -0,0 +1,650 @@ +#include "ardop_host.h" +#include "ardop_crc.h" + +#include "hybbx/socket.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define ARDOP_HOST_RX_MAX 8192u +#define ARDOP_HOST_CMD_MAX 256u +#define ARDOP_HOST_FRAME_MAX (HYBBX_ARDOP_DATA_MAX + 64u) + +struct ardop_host { + hybbx_ardop_config_t cfg; + ardop_host_data_fn on_data; + ardop_host_event_fn on_event; + void *userdata; + int ctrl_fd; + int data_fd; + int tnc_ready; + int link_up; + char peer_call[16]; + unsigned char ctrl_rx[ARDOP_HOST_RX_MAX]; + size_t ctrl_len; + unsigned char data_rx[ARDOP_HOST_RX_MAX]; + size_t data_len; + int init_sent; + const char *log_tag; +}; + +static const char *host_log_tag(const ardop_host_t *host) +{ + if (host != NULL && host->log_tag != NULL && host->log_tag[0] != '\0') { + return host->log_tag; + } + return "ardop"; +} + +static const char *host_modem_name(const ardop_host_t *host) +{ + if (host != NULL && host->log_tag != NULL && + strcmp(host->log_tag, "crdop") == 0) { + return "CRDOPC"; + } + return "ARDOPC"; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void ardop_host_fire_event(ardop_host_t *host, const char *event, + const char *detail) +{ + if (host != NULL && host->on_event != NULL) { + host->on_event(event, detail, host->userdata); + } +} + +static int ardop_host_set_socket_opts(int fd) +{ + int on = 1; + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + return 0; +} + +static int ardop_host_tcp_connect(const char *host, unsigned port) +{ + struct sockaddr_in addr; + int fd; + int rc; + + if (host == NULL || host[0] == '\0' || port == 0) { + return -1; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (ardop_host_set_socket_opts(fd) != 0) { + close(fd); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc != 0) { + close(fd); + return -1; + } + + return fd; +} + +static hybbx_result_t ardop_host_write_all(int fd, const unsigned char *buf, + size_t len) +{ + size_t off = 0; + + if (fd < 0 || buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + while (off < len) { + ssize_t n = send(fd, buf + off, len - off, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)n; + } + + return HYBBX_OK; +} + +static hybbx_result_t ardop_host_send_cmd_frame(ardop_host_t *host, + const char *cmd) +{ + unsigned char frame[ARDOP_HOST_CMD_MAX + 4]; + size_t len; + + if (host == NULL || cmd == NULL || host->ctrl_fd < 0) { + return HYBBX_ERR_INVALID; + } + + len = strlen(cmd); + if (len + 3 > sizeof(frame)) { + return HYBBX_ERR_INVALID; + } + + memcpy(frame, cmd, len); + frame[len++] = '\r'; + len = ardop_crc16_append(frame, len, sizeof(frame)); + if (len == 0) { + return HYBBX_ERR_IO; + } + + return ardop_host_write_all(host->ctrl_fd, frame, len); +} + +static hybbx_result_t ardop_host_send_rdy(ardop_host_t *host) +{ + return ardop_host_send_cmd_frame(host, "RDY"); +} + +static void ardop_host_strip_prefix(char *line) +{ + if (line == NULL) { + return; + } + + if (strncmp(line, "c:", 2) == 0 || strncmp(line, "C:", 2) == 0) { + memmove(line, line + 2, strlen(line + 2) + 1); + } +} + +static void ardop_host_handle_ctrl_line(ardop_host_t *host, char *line) +{ + char *sp; + + if (host == NULL || line == NULL) { + return; + } + + ardop_host_strip_prefix(line); + + if (str_ieq(line, "RDY")) { + host->tnc_ready = 1; + return; + } + + if (strncmp(line, "VERSION", 7) == 0) { + hybbx_log_debug("[%s] modem %s", host_log_tag(host), line); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "CONNECTED", 9) == 0) { + char *tok; + + sp = line + 9; + while (*sp == ' ') { + sp++; + } + hybbx_strlcpy(host->peer_call, sp, sizeof(host->peer_call)); + tok = strchr(host->peer_call, ' '); + if (tok != NULL) { + *tok = '\0'; + } + host->link_up = 1; + ardop_host_fire_event(host, "connected", host->peer_call); + (void)ardop_host_send_rdy(host); + return; + } + + if (str_ieq(line, "DISCONNECTED")) { + host->link_up = 0; + host->peer_call[0] = '\0'; + ardop_host_fire_event(host, "disconnected", NULL); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "FAULT", 5) == 0 || + strncmp(line, "CRCFAULT", 8) == 0) { + hybbx_log_warn("[%s] TNC fault: %s", host_log_tag(host), line); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "BUSY", 4) == 0 || + strncmp(line, "NEWSTATE", 8) == 0 || + strncmp(line, "BUFFER", 6) == 0 || + strncmp(line, "STATUS", 6) == 0 || + strncmp(line, "PENDING", 7) == 0 || + strncmp(line, "TARGET", 6) == 0) { + (void)ardop_host_send_rdy(host); + return; + } + + (void)ardop_host_send_rdy(host); +} + +static void ardop_host_process_ctrl_rx(ardop_host_t *host) +{ + unsigned char *cr; + unsigned char frame[ARDOP_HOST_CMD_MAX + 8]; + size_t msg_len; + char line[ARDOP_HOST_CMD_MAX]; + + if (host == NULL) { + return; + } + + for (;;) { + cr = memchr(host->ctrl_rx, '\r', host->ctrl_len); + if (cr == NULL) { + return; + } + + msg_len = (size_t)(cr - host->ctrl_rx) + 1u + 2u; + if (host->ctrl_len < msg_len) { + return; + } + + if (msg_len > sizeof(frame)) { + host->ctrl_len = 0; + return; + } + + memcpy(frame, host->ctrl_rx, msg_len); + if (!ardop_crc16_valid(frame, (unsigned short)msg_len)) { + hybbx_log_warn("[%s] control CRC fault", host_log_tag(host)); + host->ctrl_len = 0; + return; + } + + frame[msg_len - 3] = '\0'; + hybbx_strlcpy(line, (const char *)frame, sizeof(line)); + ardop_host_handle_ctrl_line(host, line); + + memmove(host->ctrl_rx, host->ctrl_rx + msg_len, host->ctrl_len - msg_len); + host->ctrl_len -= msg_len; + } +} + +static void ardop_host_process_data_rx(ardop_host_t *host) +{ + size_t off = 0; + + if (host == NULL) { + return; + } + + while (host->data_len >= off + 6u) { + const unsigned char *buf = host->data_rx + off; + size_t remain = host->data_len - off; + size_t payload_len; + size_t frame_len; + const uint8_t *payload; + const char *tag; + + if (buf[0] != 'd' && buf[0] != 'D') { + off++; + continue; + } + if (buf[1] != ':') { + off++; + continue; + } + + payload_len = ((size_t)buf[2] << 8) | (size_t)buf[3]; + if (payload_len < 3 || payload_len > HYBBX_ARDOP_DATA_MAX + 8u) { + off++; + continue; + } + + frame_len = 2u + 2u + payload_len + 2u; + if (remain < frame_len) { + break; + } + + if (!ardop_crc16_valid(buf, (unsigned short)frame_len)) { + off++; + continue; + } + + tag = (const char *)(buf + 4); + payload = buf + 7; + payload_len -= 3u; + + if (strncmp(tag, "ARQ", 3) == 0 && host->on_data != NULL && + payload_len > 0) { + host->on_data(payload, payload_len, host->userdata); + } + + (void)ardop_host_send_cmd_frame(host, "RDY"); + off += frame_len; + } + + if (off > 0) { + memmove(host->data_rx, host->data_rx + off, host->data_len - off); + host->data_len -= off; + } +} + +static hybbx_result_t ardop_host_send_init(ardop_host_t *host) +{ + char cmd[96]; + hybbx_result_t rc; + + if (host == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = ardop_host_send_cmd_frame(host, "INITIALIZE"); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(cmd, sizeof(cmd), "MYCALL %s", + host->cfg.mycall != NULL ? host->cfg.mycall : "NOCALL"); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + + rc = ardop_host_send_cmd_frame(host, "PROTOCOLMODE ARQ"); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(cmd, sizeof(cmd), "ARQBW %s", + host->cfg.arq_bandwidth != NULL ? host->cfg.arq_bandwidth : + "500MAX"); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + + rc = ardop_host_send_cmd_frame(host, + host->cfg.listen ? "LISTEN TRUE" : + "LISTEN FALSE"); + if (rc != HYBBX_OK) { + return rc; + } + + if (host->cfg.peer_call != NULL && host->cfg.peer_call[0] != '\0') { + snprintf(cmd, sizeof(cmd), "ARQCALL %s 5", host->cfg.peer_call); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + host->init_sent = 1; + hybbx_log_info("[%s] host init sent (external %s at %s:%u)", + host_log_tag(host), host_modem_name(host), + host->cfg.ardop_host != NULL ? host->cfg.ardop_host : "?", + host->cfg.ardop_port); + return HYBBX_OK; +} + +static void ardop_host_read_fd(int fd, unsigned char *buf, size_t *len, + size_t cap) +{ + ssize_t n; + + if (fd < 0 || buf == NULL || len == NULL || *len >= cap) { + return; + } + + n = recv(fd, buf + *len, cap - *len, 0); + if (n > 0) { + *len += (size_t)n; + } else if (n == 0) { + close(fd); + } +} + +ardop_host_t *ardop_host_create(const hybbx_ardop_config_t *cfg, + ardop_host_data_fn on_data, + ardop_host_event_fn on_event, + void *userdata, + const char *log_tag) +{ + ardop_host_t *host; + + if (cfg == NULL) { + return NULL; + } + + host = calloc(1, sizeof(*host)); + if (host == NULL) { + return NULL; + } + + host->cfg = *cfg; + host->on_data = on_data; + host->on_event = on_event; + host->userdata = userdata; + host->ctrl_fd = -1; + host->data_fd = -1; + host->log_tag = log_tag; + return host; +} + +void ardop_host_destroy(ardop_host_t *host) +{ + if (host == NULL) { + return; + } + + ardop_host_disconnect(host); + free(host); +} + +hybbx_result_t ardop_host_connect(ardop_host_t *host) +{ + const char *h; + unsigned port; + + if (host == NULL) { + return HYBBX_ERR_INVALID; + } + + ardop_host_disconnect(host); + + h = host->cfg.ardop_host; + if (h == NULL || h[0] == '\0') { + h = "127.0.0.1"; + } + port = host->cfg.ardop_port; + if (port == 0) { + port = HYBBX_ARDOP_DEFAULT_PORT; + } + + host->ctrl_fd = ardop_host_tcp_connect(h, port); + if (host->ctrl_fd < 0) { + hybbx_log_warn("[%s] cannot connect to %s control %s:%u", + host_log_tag(host), host_modem_name(host), h, port); + return HYBBX_ERR_IO; + } + + host->data_fd = ardop_host_tcp_connect(h, port + 1u); + if (host->data_fd < 0) { + hybbx_log_warn("[%s] cannot connect to %s data %s:%u", + host_log_tag(host), host_modem_name(host), h, port + 1u); + ardop_host_disconnect(host); + return HYBBX_ERR_IO; + } + + host->tnc_ready = 0; + host->init_sent = 0; + hybbx_log_info("[%s] connected to external %s %s:%u (data %u)", + host_log_tag(host), host_modem_name(host), h, port, port + 1u); + + (void)ardop_host_send_init(host); + { + unsigned spin; + + for (spin = 0; spin < 8u; spin++) { + ardop_host_poll(host); + if (host->tnc_ready) { + break; + } + } + } + return HYBBX_OK; +} + +void ardop_host_disconnect(ardop_host_t *host) +{ + if (host == NULL) { + return; + } + + if (host->ctrl_fd >= 0) { + close(host->ctrl_fd); + host->ctrl_fd = -1; + } + if (host->data_fd >= 0) { + close(host->data_fd); + host->data_fd = -1; + } + + host->ctrl_len = 0; + host->data_len = 0; + host->tnc_ready = 0; + host->link_up = 0; + host->init_sent = 0; + host->peer_call[0] = '\0'; +} + +void ardop_host_poll(ardop_host_t *host) +{ + struct pollfd pfds[2]; + int count = 0; + int pr; + + if (host == NULL || host->ctrl_fd < 0) { + return; + } + + pfds[0].fd = host->ctrl_fd; + pfds[0].events = POLLIN; + count = 1; + + if (host->data_fd >= 0) { + pfds[1].fd = host->data_fd; + pfds[1].events = POLLIN; + count = 2; + } + + pr = poll(pfds, (nfds_t)count, 0); + if (pr <= 0) { + if (host->tnc_ready && !host->init_sent) { + (void)ardop_host_send_init(host); + } + return; + } + + if ((pfds[0].revents & POLLIN) != 0) { + ardop_host_read_fd(host->ctrl_fd, host->ctrl_rx, &host->ctrl_len, + ARDOP_HOST_RX_MAX); + ardop_host_process_ctrl_rx(host); + } + + if (count > 1 && (pfds[1].revents & POLLIN) != 0) { + ardop_host_read_fd(host->data_fd, host->data_rx, &host->data_len, + ARDOP_HOST_RX_MAX); + ardop_host_process_data_rx(host); + } + + if (host->tnc_ready && !host->init_sent) { + (void)ardop_host_send_init(host); + } +} + +int ardop_host_link_up(const ardop_host_t *host) +{ + return host != NULL && host->link_up; +} + +int ardop_host_modem_connected(const ardop_host_t *host) +{ + return host != NULL && host->ctrl_fd >= 0; +} + +hybbx_result_t ardop_host_send_data(ardop_host_t *host, + const uint8_t *data, size_t len) +{ + unsigned char frame[ARDOP_HOST_FRAME_MAX]; + size_t frame_len; + + if (host == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (!host->link_up || host->data_fd < 0) { + return HYBBX_ERR_BUSY; + } + + if (len > HYBBX_ARDOP_DATA_MAX) { + return HYBBX_ERR_INVALID; + } + + if (len + 8 > sizeof(frame)) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 'D'; + frame[1] = ':'; + frame[2] = (unsigned char)((len >> 8) & 0xff); + frame[3] = (unsigned char)(len & 0xff); + memcpy(frame + 4, data, len); + + frame_len = 4u + len; + frame_len = ardop_crc16_append(frame, frame_len, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return ardop_host_write_all(host->data_fd, frame, frame_len); +} diff --git a/plugins/ardop/ardop_host.h b/plugins/ardop/ardop_host.h new file mode 100644 index 0000000..4aee53d --- /dev/null +++ b/plugins/ardop/ardop_host.h @@ -0,0 +1,47 @@ +#ifndef ARDOP_HOST_H +#define ARDOP_HOST_H + +/** + * Minimal ARDOP TNC Host Interface client (TCP control + data ports). + * Implements the subset HyBBX needs — see docs/MANUAL.md § ARDOP. + */ + +#include "hybbx/types.h" +#include "hybbx/ardop.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ardop_host ardop_host_t; + +typedef void (*ardop_host_data_fn)(const uint8_t *data, size_t len, void *ctx); +typedef void (*ardop_host_event_fn)(const char *event, const char *detail, + void *ctx); + +ardop_host_t *ardop_host_create(const hybbx_ardop_config_t *cfg, + ardop_host_data_fn on_data, + ardop_host_event_fn on_event, + void *userdata, + const char *log_tag); +void ardop_host_destroy(ardop_host_t *host); + +hybbx_result_t ardop_host_connect(ardop_host_t *host); +void ardop_host_disconnect(ardop_host_t *host); + +/** Drive I/O and state machine; call periodically from plugin poll thread. */ +void ardop_host_poll(ardop_host_t *host); + +int ardop_host_link_up(const ardop_host_t *host); + +/** Control TCP session to external ARDOPC is open. */ +int ardop_host_modem_connected(const ardop_host_t *host); + +hybbx_result_t ardop_host_send_data(ardop_host_t *host, + const uint8_t *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* ARDOP_HOST_H */ 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 <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#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; +} diff --git a/plugins/ardop/ardop_link.h b/plugins/ardop/ardop_link.h new file mode 100644 index 0000000..0f10dba --- /dev/null +++ b/plugins/ardop/ardop_link.h @@ -0,0 +1,37 @@ +#ifndef ARDOP_LINK_H +#define ARDOP_LINK_H + +#include "hybbx/plugin.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; + +typedef hybbx_result_t (*ardop_link_config_fn)(const char *config, + void *config_out); + +typedef struct ardop_link_plugin { + const char *name; + hybbx_transport_kind_t kind; + const char *default_link_id; + const char *modem_hint; + ardop_link_config_fn parse_config; + void (*config_free)(void *config_out); + size_t config_size; +} ardop_link_plugin_t; + +hybbx_result_t ardop_link_init(struct hybbx_service *service, + const ardop_link_plugin_t *plugin); +void ardop_link_shutdown(const ardop_link_plugin_t *plugin); +hybbx_result_t ardop_link_start(const ardop_link_plugin_t *plugin, + const char *config); +hybbx_result_t ardop_link_stop(const ardop_link_plugin_t *plugin); + +#ifdef __cplusplus +} +#endif + +#endif /* ARDOP_LINK_H */ |
