summaryrefslogtreecommitdiff
path: root/plugins/baycom
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/baycom')
-rw-r--r--plugins/baycom/CMakeLists.txt36
-rw-r--r--plugins/baycom/baycom.c623
-rw-r--r--plugins/baycom/baycom_config.c395
-rw-r--r--plugins/baycom/baycom_kernel.c472
-rw-r--r--plugins/baycom/baycom_kernel.h26
-rw-r--r--plugins/baycom/baycom_kiss.c262
-rw-r--r--plugins/baycom/baycom_kiss.h26
-rw-r--r--plugins/baycom/baycom_modem.c94
-rw-r--r--plugins/baycom/baycom_modem.h31
9 files changed, 1965 insertions, 0 deletions
diff --git a/plugins/baycom/CMakeLists.txt b/plugins/baycom/CMakeLists.txt
new file mode 100644
index 0000000..8a2dc2d
--- /dev/null
+++ b/plugins/baycom/CMakeLists.txt
@@ -0,0 +1,36 @@
+add_library(hybbx_plugin_baycom STATIC
+ baycom.c
+ baycom_config.c
+ baycom_modem.c
+ baycom_kernel.c
+ baycom_kiss.c
+ ../packet_radio/ax25.c
+ ../packet_radio/kiss.c
+ ../packet_radio/serial_port.c
+)
+
+target_include_directories(hybbx_plugin_baycom
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_SOURCE_DIR}/plugins/packet_radio
+ ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+target_compile_definitions(hybbx_plugin_baycom
+ PRIVATE HYBBX_PLUGIN_BUILD
+)
+
+find_package(Threads REQUIRED)
+target_link_libraries(hybbx_plugin_baycom PRIVATE hybbx_core Threads::Threads)
+if(HYBBX_PLATFORM_AMIGAOS)
+ target_link_libraries(hybbx_plugin_baycom PRIVATE amiga)
+endif()
+hybbx_link_plugin_instances(hybbx_plugin_baycom HYBBX_HAVE_PLUGIN_BAYCOM)
+hybbx_apply_hardening(hybbx_plugin_baycom)
+
+if(HYBBX_INSTALL_DEV)
+install(TARGETS hybbx_plugin_baycom
+ ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR}
+ LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR}
+)
+endif()
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 <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#if defined(_WIN32)
+#include <windows.h>
+#else
+#include <sys/select.h>
+#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,
+};
diff --git a/plugins/baycom/baycom_config.c b/plugins/baycom/baycom_config.c
new file mode 100644
index 0000000..4e740f1
--- /dev/null
+++ b/plugins/baycom/baycom_config.c
@@ -0,0 +1,395 @@
+#include "hybbx/baycom.h"
+#include "hybbx/circuit.h"
+#include "hybbx/util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static char *baycom_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 char *baycom_normalize_kiss_path(const char *device)
+{
+ static const struct {
+ const char *from;
+ const char *to;
+ } map[] = {
+ {"/tmp/bcpr/", "/tmp/max25-bcpr/"},
+ {"/tmp/bcpr", "/tmp/max25-bcpr"},
+ {"/var/run/bcpr/", "/tmp/max25-bcpr/"},
+ {"/var/run/bcpr", "/tmp/max25-bcpr"},
+ };
+ size_t i;
+ size_t flen;
+ char *norm;
+
+ if (device == NULL || device[0] == '\0') {
+ return NULL;
+ }
+
+ for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) {
+ flen = strlen(map[i].from);
+ if (strncmp(device, map[i].from, flen) != 0) {
+ continue;
+ }
+ if (device[flen] != '\0' && device[flen] != '/') {
+ continue;
+ }
+ norm = malloc(strlen(map[i].to) + strlen(device + flen) + 1);
+ if (norm == NULL) {
+ return baycom_strdup(device);
+ }
+ snprintf(norm, strlen(map[i].to) + strlen(device + flen) + 1, "%s%s",
+ map[i].to, device + flen);
+ return norm;
+ }
+
+ return baycom_strdup(device);
+}
+
+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;
+}
+
+const char *hybbx_baycom_backend_name(hybbx_baycom_backend_t backend)
+{
+ switch (backend) {
+ case HYBBX_BAYCOM_BACKEND_KISS:
+ return "kiss";
+ case HYBBX_BAYCOM_BACKEND_KERNEL:
+ default:
+ return "kernel";
+ }
+}
+
+const char *hybbx_baycom_modem_mode_name(hybbx_baycom_modem_mode_t mode)
+{
+ switch (mode) {
+ case HYBBX_BAYCOM_MODE_SER12_SOFTDCD:
+ return "ser12*";
+ case HYBBX_BAYCOM_MODE_SER12_INV:
+ return "ser12+";
+ case HYBBX_BAYCOM_MODE_SER12HDX:
+ return "ser12hdx";
+ case HYBBX_BAYCOM_MODE_PAR96:
+ return "par96";
+ case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD:
+ return "par96*";
+ case HYBBX_BAYCOM_MODE_EPP:
+ return "epp";
+ case HYBBX_BAYCOM_MODE_SER12:
+ default:
+ return "ser12";
+ }
+}
+
+static hybbx_baycom_backend_t parse_backend(const char *value)
+{
+ if (value == NULL || str_ieq(value, "kernel") || str_ieq(value, "hdlc") ||
+ str_ieq(value, "prstack") || str_ieq(value, "l2")) {
+ return HYBBX_BAYCOM_BACKEND_KERNEL;
+ }
+
+ if (str_ieq(value, "kiss") || str_ieq(value, "serial")) {
+ return HYBBX_BAYCOM_BACKEND_KISS;
+ }
+
+ return HYBBX_BAYCOM_BACKEND_KERNEL;
+}
+
+static hybbx_baycom_modem_mode_t parse_mode(const char *value)
+{
+ if (value == NULL || str_ieq(value, "ser12")) {
+ return HYBBX_BAYCOM_MODE_SER12;
+ }
+ if (str_ieq(value, "ser12*") || str_ieq(value, "ser12_softdcd")) {
+ return HYBBX_BAYCOM_MODE_SER12_SOFTDCD;
+ }
+ if (str_ieq(value, "ser12+") || str_ieq(value, "ser12_inv")) {
+ return HYBBX_BAYCOM_MODE_SER12_INV;
+ }
+ if (str_ieq(value, "ser12hdx") || str_ieq(value, "ser12_hdx") ||
+ str_ieq(value, "hdx")) {
+ return HYBBX_BAYCOM_MODE_SER12HDX;
+ }
+ if (str_ieq(value, "par96")) {
+ return HYBBX_BAYCOM_MODE_PAR96;
+ }
+ if (str_ieq(value, "par96*") || str_ieq(value, "par96_softdcd")) {
+ return HYBBX_BAYCOM_MODE_PAR96_SOFTDCD;
+ }
+ if (str_ieq(value, "epp") || str_ieq(value, "par97") ||
+ str_ieq(value, "baycom_epp")) {
+ return HYBBX_BAYCOM_MODE_EPP;
+ }
+
+ return HYBBX_BAYCOM_MODE_SER12_SOFTDCD;
+}
+
+void hybbx_baycom_config_free(hybbx_baycom_config_t *config)
+{
+ if (config == NULL) {
+ return;
+ }
+
+ free(config->interface);
+ free(config->kernel_module);
+ free(config->device);
+ free(config->mycall);
+ free(config->circuit_host);
+ free(config->link_id);
+ free(config->link_password);
+ free(config->link_role);
+ free(config->frequency_mhz);
+ memset(config, 0, sizeof(*config));
+}
+
+static void apply_mode_defaults(hybbx_baycom_config_t *out)
+{
+ if (out->interface == NULL) {
+ switch (out->mode) {
+ case HYBBX_BAYCOM_MODE_SER12HDX:
+ out->interface = baycom_strdup("bcsh0");
+ break;
+ case HYBBX_BAYCOM_MODE_PAR96:
+ case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD:
+ out->interface = baycom_strdup("bcp0");
+ break;
+ case HYBBX_BAYCOM_MODE_EPP:
+ out->interface = baycom_strdup("bce0");
+ break;
+ case HYBBX_BAYCOM_MODE_SER12:
+ case HYBBX_BAYCOM_MODE_SER12_SOFTDCD:
+ case HYBBX_BAYCOM_MODE_SER12_INV:
+ default:
+ out->interface = baycom_strdup(HYBBX_BAYCOM_DEFAULT_INTERFACE);
+ break;
+ }
+ }
+
+ if (out->kernel_module == NULL) {
+ switch (out->mode) {
+ case HYBBX_BAYCOM_MODE_SER12HDX:
+ out->kernel_module = baycom_strdup("baycom_ser_hdx");
+ break;
+ case HYBBX_BAYCOM_MODE_PAR96:
+ case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD:
+ out->kernel_module = baycom_strdup("baycom_par");
+ break;
+ case HYBBX_BAYCOM_MODE_EPP:
+ out->kernel_module = baycom_strdup("baycom_epp");
+ break;
+ default:
+ out->kernel_module = baycom_strdup("baycom_ser_fdx");
+ break;
+ }
+ }
+
+ if (out->device == NULL) {
+ if (out->backend == HYBBX_BAYCOM_BACKEND_KISS) {
+ out->device = baycom_strdup("/tmp/max25-bcpr/kiss-bc0");
+ } else {
+ out->device = baycom_strdup("/dev/ttyS0");
+ }
+ }
+}
+
+hybbx_result_t hybbx_baycom_config_parse(const char *config,
+ hybbx_baycom_config_t *out)
+{
+ char scratch[128];
+ const char *value;
+
+ if (out == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_baycom_config_free(out);
+
+ if (config == NULL) {
+ config = "";
+ }
+
+ value = find_kv(config, "backend", scratch, sizeof(scratch));
+ out->backend = parse_backend(value);
+
+ value = find_kv(config, "mode", scratch, sizeof(scratch));
+ out->mode = parse_mode(value);
+
+ value = find_kv(config, "interface", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->interface = baycom_strdup(value);
+ }
+
+ value = find_kv(config, "kernel_module", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->kernel_module = baycom_strdup(value);
+ }
+
+ value = find_kv(config, "iobase", scratch, sizeof(scratch));
+ out->iobase = value != NULL ? (unsigned)strtoul(value, NULL, 0)
+ : HYBBX_BAYCOM_DEFAULT_IOBASE;
+
+ value = find_kv(config, "irq", scratch, sizeof(scratch));
+ out->irq = value != NULL ? (unsigned)strtoul(value, NULL, 0)
+ : HYBBX_BAYCOM_DEFAULT_IRQ;
+
+ value = find_kv(config, "radio_baud", scratch, sizeof(scratch));
+ if (value == NULL) {
+ value = find_kv(config, "baud", scratch, sizeof(scratch));
+ }
+ out->radio_baud = value != NULL ? (unsigned)strtoul(value, NULL, 0)
+ : HYBBX_BAYCOM_DEFAULT_RADIO_BAUD;
+
+ value = find_kv(config, "kernel_autoload", scratch, sizeof(scratch));
+ out->kernel_autoload = value != NULL ? hybbx_parse_bool(value, 0) : 0;
+
+ value = find_kv(config, "txdelay", scratch, sizeof(scratch));
+ out->txdelay = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 30u;
+
+ value = find_kv(config, "txtail", scratch, sizeof(scratch));
+ out->txtail = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 1u;
+
+ value = find_kv(config, "slot", scratch, sizeof(scratch));
+ if (value == NULL) {
+ value = find_kv(config, "slottime", scratch, sizeof(scratch));
+ }
+ out->slot = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 10u;
+
+ value = find_kv(config, "persist", scratch, sizeof(scratch));
+ if (value == NULL) {
+ value = find_kv(config, "ppersist", scratch, sizeof(scratch));
+ }
+ out->persist = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 128u;
+
+ value = find_kv(config, "full_duplex", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->full_duplex = hybbx_parse_bool(value, 0);
+ } else {
+ value = find_kv(config, "duplex", scratch, sizeof(scratch));
+ out->full_duplex = value != NULL && str_ieq(value, "full");
+ }
+
+ value = find_kv(config, "device", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->device = baycom_strdup(value);
+ }
+
+ value = find_kv(config, "serial_baud", scratch, sizeof(scratch));
+ if (value == NULL) {
+ value = find_kv(config, "host_baud", scratch, sizeof(scratch));
+ }
+ out->serial_baud = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 1200u;
+
+ value = find_kv(config, "mycall", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->mycall = baycom_strdup(value);
+ }
+
+ value = find_kv(config, "circuit_host", scratch, sizeof(scratch));
+ out->circuit_host = baycom_strdup(value != NULL ? value : "127.0.0.1");
+
+ value = find_kv(config, "circuit_port", scratch, sizeof(scratch));
+ out->circuit_port = value != NULL ? (unsigned)strtoul(value, NULL, 0)
+ : HYBBX_CIRCUIT_DEFAULT_PORT;
+
+ value = find_kv(config, "link_id", scratch, sizeof(scratch));
+ out->link_id = baycom_strdup(value != NULL ? value : "baycom-link");
+
+ value = find_kv(config, "link_password", scratch, sizeof(scratch));
+ out->link_password = baycom_strdup(value != NULL ? value : "");
+
+ value = find_kv(config, "link_role", scratch, sizeof(scratch));
+ out->link_role = baycom_strdup(value != NULL ? value : "link");
+
+ value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->frequency_mhz = baycom_strdup(value);
+ }
+
+ apply_mode_defaults(out);
+
+ if (out->backend == HYBBX_BAYCOM_BACKEND_KISS && out->device != NULL) {
+ char *norm = baycom_normalize_kiss_path(out->device);
+
+ if (norm != NULL) {
+ free(out->device);
+ out->device = norm;
+ }
+ }
+
+ if (out->circuit_host == NULL || out->link_id == NULL) {
+ hybbx_baycom_config_free(out);
+ return HYBBX_ERR_NOMEM;
+ }
+
+ return HYBBX_OK;
+}
diff --git a/plugins/baycom/baycom_kernel.c b/plugins/baycom/baycom_kernel.c
new file mode 100644
index 0000000..0babc08
--- /dev/null
+++ b/plugins/baycom/baycom_kernel.c
@@ -0,0 +1,472 @@
+#if defined(__linux__)
+#define _DEFAULT_SOURCE
+#endif
+
+#include "baycom_kernel.h"
+
+#include "hybbx/ax25.h"
+#include "hybbx/baycom.h"
+#include "hybbx/log.h"
+#include "hybbx/rf_tx_pace.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#if defined(__linux__)
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <linux/if_packet.h>
+#include <linux/if_ether.h>
+#include <linux/hdlcdrv.h>
+
+static char *baycom_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;
+}
+
+struct baycom_kernel_modem {
+ hybbx_baycom_config_t cfg;
+ baycom_modem_frame_cb frame_cb;
+ void *frame_userdata;
+ int ctl_fd;
+ int pkt_fd;
+ int ifindex;
+};
+
+static void baycom_kernel_mode_string(const hybbx_baycom_config_t *cfg,
+ char *out, size_t out_cap)
+{
+ unsigned rate;
+ char suffix = '\0';
+
+ if (out == NULL || out_cap == 0 || cfg == NULL) {
+ return;
+ }
+
+ switch (cfg->mode) {
+ case HYBBX_BAYCOM_MODE_SER12_SOFTDCD:
+ case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD:
+ suffix = '*';
+ break;
+ case HYBBX_BAYCOM_MODE_SER12_INV:
+ suffix = '+';
+ break;
+ default:
+ break;
+ }
+
+ if (cfg->mode == HYBBX_BAYCOM_MODE_PAR96 ||
+ cfg->mode == HYBBX_BAYCOM_MODE_PAR96_SOFTDCD) {
+ snprintf(out, out_cap, "par96%c", suffix);
+ return;
+ }
+
+ if (cfg->mode == HYBBX_BAYCOM_MODE_EPP) {
+ snprintf(out, out_cap, "epp");
+ return;
+ }
+
+ rate = cfg->radio_baud / 100u;
+ if (rate == 0) {
+ rate = 12;
+ }
+ snprintf(out, out_cap, "ser%u%c", rate, suffix);
+}
+
+static int baycom_kernel_hdlc_ioctl(baycom_kernel_modem_t *modem,
+ struct hdlcdrv_ioctl *hi)
+{
+ struct ifreq ifr;
+
+ if (modem == NULL || hi == NULL || modem->ctl_fd < 0 ||
+ modem->cfg.interface == NULL) {
+ return -1;
+ }
+
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1);
+ ifr.ifr_data = (void *)hi;
+
+ if (ioctl(modem->ctl_fd, SIOCDEVPRIVATE, &ifr) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int baycom_kernel_interface_flags(baycom_kernel_modem_t *modem,
+ unsigned short *flags_out)
+{
+ struct ifreq ifr;
+
+ if (modem == NULL || flags_out == NULL || modem->ctl_fd < 0) {
+ return -1;
+ }
+
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1);
+ if (ioctl(modem->ctl_fd, SIOCGIFFLAGS, &ifr) < 0) {
+ return -1;
+ }
+
+ *flags_out = ifr.ifr_flags;
+ return 0;
+}
+
+static int baycom_kernel_interface_up(baycom_kernel_modem_t *modem, int up)
+{
+ struct ifreq ifr;
+ unsigned short flags = 0;
+
+ if (baycom_kernel_interface_flags(modem, &flags) < 0) {
+ return -1;
+ }
+
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1);
+ ifr.ifr_flags = flags;
+ if (up) {
+ ifr.ifr_flags |= IFF_UP;
+ } else {
+ ifr.ifr_flags &= (unsigned short)~IFF_UP;
+ }
+
+ return ioctl(modem->ctl_fd, SIOCSIFFLAGS, &ifr);
+}
+
+static int baycom_kernel_set_modem_params(baycom_kernel_modem_t *modem)
+{
+ struct hdlcdrv_ioctl hi;
+
+ memset(&hi, 0, sizeof(hi));
+ hi.cmd = HDLCDRVCTL_SETMODEMPAR;
+ hi.data.mp.iobase = (int)modem->cfg.iobase;
+ hi.data.mp.irq = (int)modem->cfg.irq;
+
+ return baycom_kernel_hdlc_ioctl(modem, &hi);
+}
+
+static int baycom_kernel_set_mode(baycom_kernel_modem_t *modem)
+{
+ struct hdlcdrv_ioctl hi;
+ char modestr[32];
+
+ baycom_kernel_mode_string(&modem->cfg, modestr, sizeof(modestr));
+
+ memset(&hi, 0, sizeof(hi));
+ hi.cmd = HDLCDRVCTL_SETMODE;
+ strncpy(hi.data.modename, modestr, sizeof(hi.data.modename) - 1);
+
+ return baycom_kernel_hdlc_ioctl(modem, &hi);
+}
+
+static int baycom_kernel_set_channel_params(baycom_kernel_modem_t *modem)
+{
+ struct hdlcdrv_ioctl hi;
+
+ memset(&hi, 0, sizeof(hi));
+ hi.cmd = HDLCDRVCTL_SETCHANNELPAR;
+ hi.data.cp.tx_delay = (int)modem->cfg.txdelay;
+ hi.data.cp.tx_tail = (int)modem->cfg.txtail;
+ hi.data.cp.slottime = (int)modem->cfg.slot;
+ hi.data.cp.ppersist = (int)modem->cfg.persist;
+ hi.data.cp.fulldup = modem->cfg.full_duplex ? 1 : 0;
+
+ return baycom_kernel_hdlc_ioctl(modem, &hi);
+}
+
+static int baycom_kernel_modprobe(const hybbx_baycom_config_t *cfg)
+{
+ char cmd[384];
+ char modestr[32];
+ int rc;
+
+ if (cfg == NULL || cfg->kernel_module == NULL) {
+ return -1;
+ }
+
+ baycom_kernel_mode_string(cfg, modestr, sizeof(modestr));
+ snprintf(cmd, sizeof(cmd),
+ "modprobe %s mode=%s iobase=0x%x irq=%u baud=%u 2>/dev/null",
+ cfg->kernel_module, modestr, cfg->iobase, cfg->irq,
+ cfg->radio_baud);
+
+ rc = system(cmd);
+ if (rc != 0) {
+ hybbx_log_warn("[baycom] modprobe failed (%s)", cfg->kernel_module);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int baycom_kernel_ifindex(baycom_kernel_modem_t *modem)
+{
+ struct ifreq ifr;
+
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1);
+ if (ioctl(modem->ctl_fd, SIOCGIFINDEX, &ifr) < 0) {
+ return -1;
+ }
+
+ modem->ifindex = ifr.ifr_ifindex;
+ return 0;
+}
+
+static int baycom_kernel_open_packet(baycom_kernel_modem_t *modem)
+{
+ struct sockaddr_ll bind_addr;
+
+ modem->pkt_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_AX25));
+ if (modem->pkt_fd < 0) {
+ return -1;
+ }
+
+ if (baycom_kernel_ifindex(modem) < 0) {
+ return -1;
+ }
+
+ memset(&bind_addr, 0, sizeof(bind_addr));
+ bind_addr.sll_family = AF_PACKET;
+ bind_addr.sll_protocol = htons(ETH_P_AX25);
+ bind_addr.sll_ifindex = modem->ifindex;
+
+ if (bind(modem->pkt_fd, (struct sockaddr *)&bind_addr,
+ sizeof(bind_addr)) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata)
+{
+ baycom_kernel_modem_t *km;
+ unsigned short flags = 0;
+
+ if (out == NULL || cfg == NULL || cfg->interface == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ *out = NULL;
+ km = calloc(1, sizeof(*km));
+ if (km == NULL) {
+ return HYBBX_ERR_NOMEM;
+ }
+
+ /* Own string copies only — avoid double-free with caller config. */
+ memset(&km->cfg, 0, sizeof(km->cfg));
+ km->cfg.backend = cfg->backend;
+ km->cfg.mode = cfg->mode;
+ km->cfg.iobase = cfg->iobase;
+ km->cfg.irq = cfg->irq;
+ km->cfg.radio_baud = cfg->radio_baud;
+ km->cfg.kernel_autoload = cfg->kernel_autoload;
+ km->cfg.txdelay = cfg->txdelay;
+ km->cfg.txtail = cfg->txtail;
+ km->cfg.slot = cfg->slot;
+ km->cfg.persist = cfg->persist;
+ km->cfg.full_duplex = cfg->full_duplex;
+ km->cfg.serial_baud = cfg->serial_baud;
+ km->cfg.circuit_port = cfg->circuit_port;
+ km->cfg.interface = baycom_strdup(cfg->interface);
+ km->cfg.kernel_module = baycom_strdup(cfg->kernel_module);
+ km->cfg.device = baycom_strdup(cfg->device);
+ km->cfg.mycall = baycom_strdup(cfg->mycall);
+ km->cfg.circuit_host = baycom_strdup(cfg->circuit_host);
+ km->cfg.link_id = baycom_strdup(cfg->link_id);
+ km->cfg.link_password = baycom_strdup(cfg->link_password);
+ km->cfg.link_role = baycom_strdup(cfg->link_role);
+ km->cfg.frequency_mhz = baycom_strdup(cfg->frequency_mhz);
+ km->frame_cb = cb;
+ km->frame_userdata = userdata;
+ km->ctl_fd = -1;
+ km->pkt_fd = -1;
+
+ if (km->cfg.interface == NULL) {
+ free(km);
+ return HYBBX_ERR_NOMEM;
+ }
+
+ if (cfg->kernel_autoload) {
+ if (baycom_kernel_modprobe(cfg) < 0) {
+ hybbx_baycom_config_free(&km->cfg);
+ free(km);
+ return HYBBX_ERR_IO;
+ }
+ }
+
+ km->ctl_fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (km->ctl_fd < 0) {
+ hybbx_baycom_config_free(&km->cfg);
+ free(km);
+ return HYBBX_ERR_IO;
+ }
+
+ if (baycom_kernel_interface_flags(km, &flags) < 0) {
+ hybbx_log_warn("[baycom] interface %s not found (load %s first)",
+ km->cfg.interface,
+ km->cfg.kernel_module != NULL ? km->cfg.kernel_module
+ : "baycom module");
+ baycom_kernel_close(km);
+ return HYBBX_ERR_IO;
+ }
+
+ if (!(flags & IFF_UP)) {
+ if (baycom_kernel_set_modem_params(km) < 0) {
+ hybbx_log_warn("[baycom] SETMODEMPAR failed (need CAP_SYS_RAWIO, iface down)");
+ }
+ if (baycom_kernel_set_mode(km) < 0) {
+ hybbx_log_warn("[baycom] SETMODE failed (need CAP_NET_ADMIN, iface down)");
+ }
+ if (baycom_kernel_interface_up(km, 1) < 0) {
+ hybbx_log_warn("[baycom] failed to bring %s up", km->cfg.interface);
+ baycom_kernel_close(km);
+ return HYBBX_ERR_IO;
+ }
+ }
+
+ if (baycom_kernel_set_channel_params(km) < 0) {
+ hybbx_log_warn("[baycom] channel params ioctl failed");
+ }
+
+ if (baycom_kernel_open_packet(km) < 0) {
+ baycom_kernel_close(km);
+ return HYBBX_ERR_IO;
+ }
+
+ *out = km;
+ return HYBBX_OK;
+}
+
+void baycom_kernel_close(baycom_kernel_modem_t *km)
+{
+ if (km == NULL) {
+ return;
+ }
+
+ if (km->pkt_fd >= 0) {
+ close(km->pkt_fd);
+ }
+ if (km->ctl_fd >= 0) {
+ close(km->ctl_fd);
+ }
+
+ hybbx_baycom_config_free(&km->cfg);
+ free(km);
+}
+
+hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *km)
+{
+ uint8_t buf[HYBBX_AX25_FRAME_MAX + 64];
+ ssize_t n;
+
+ if (km == NULL || km->pkt_fd < 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ for (;;) {
+ n = recv(km->pkt_fd, buf, sizeof(buf), MSG_DONTWAIT);
+ if (n < 0) {
+ if (errno == EAGAIN
+#if EAGAIN != EWOULDBLOCK
+ || errno == EWOULDBLOCK
+#endif
+ ) {
+ return HYBBX_OK;
+ }
+ return HYBBX_ERR_IO;
+ }
+ if (n == 0) {
+ return HYBBX_OK;
+ }
+
+ if (km->frame_cb != NULL && (size_t)n > 0) {
+ km->frame_cb(buf, (size_t)n, km->frame_userdata);
+ }
+ }
+}
+
+hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *km,
+ const uint8_t *frame, size_t len)
+{
+ struct sockaddr_ll dest;
+ ssize_t n;
+
+ if (km == NULL || frame == NULL || len == 0 || km->pkt_fd < 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_rf_tx_pace();
+
+ memset(&dest, 0, sizeof(dest));
+ dest.sll_family = AF_PACKET;
+ dest.sll_protocol = htons(ETH_P_AX25);
+ dest.sll_ifindex = km->ifindex;
+ dest.sll_halen = 0;
+
+ n = sendto(km->pkt_fd, frame, len, 0, (struct sockaddr *)&dest,
+ sizeof(dest));
+ if (n < 0 || (size_t)n != len) {
+ return HYBBX_ERR_IO;
+ }
+
+ return HYBBX_OK;
+}
+
+#else /* !__linux__ */
+
+hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata)
+{
+ (void)out;
+ (void)cfg;
+ (void)cb;
+ (void)userdata;
+ return HYBBX_ERR_UNSUPPORTED;
+}
+
+void baycom_kernel_close(baycom_kernel_modem_t *modem)
+{
+ (void)modem;
+}
+
+hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *modem)
+{
+ (void)modem;
+ return HYBBX_ERR_UNSUPPORTED;
+}
+
+hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *modem,
+ const uint8_t *frame, size_t len)
+{
+ (void)modem;
+ (void)frame;
+ (void)len;
+ return HYBBX_ERR_UNSUPPORTED;
+}
+
+#endif /* __linux__ */
diff --git a/plugins/baycom/baycom_kernel.h b/plugins/baycom/baycom_kernel.h
new file mode 100644
index 0000000..b35e3b9
--- /dev/null
+++ b/plugins/baycom/baycom_kernel.h
@@ -0,0 +1,26 @@
+#ifndef BAYCOM_KERNEL_H
+#define BAYCOM_KERNEL_H
+
+#include "baycom_modem.h"
+#include "hybbx/baycom.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct baycom_kernel_modem baycom_kernel_modem_t;
+
+hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata);
+void baycom_kernel_close(baycom_kernel_modem_t *modem);
+hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *modem);
+hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *modem,
+ const uint8_t *frame, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BAYCOM_KERNEL_H */
diff --git a/plugins/baycom/baycom_kiss.c b/plugins/baycom/baycom_kiss.c
new file mode 100644
index 0000000..c66b220
--- /dev/null
+++ b/plugins/baycom/baycom_kiss.c
@@ -0,0 +1,262 @@
+#if defined(__linux__) || defined(__GLIBC__)
+#define _DEFAULT_SOURCE 1
+#endif
+
+#include "baycom_kiss.h"
+
+#include "hybbx/kiss.h"
+#include "hybbx/rf_tx_pace.h"
+#include "serial_port.h"
+#include "hybbx/log.h"
+#include "hybbx/limits.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static hybbx_result_t kiss_wait_device(const char *path)
+{
+ unsigned waited_ms = 0;
+ const unsigned step_ms = 500u;
+ const unsigned max_wait_ms = 30000u;
+
+ if (path == NULL || path[0] == '\0') {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (access(path, F_OK) == 0) {
+ return HYBBX_OK;
+ }
+
+ while (access(path, F_OK) != 0) {
+ if (waited_ms >= max_wait_ms) {
+ hybbx_log_warn("[baycom] kiss device not ready: %s", path);
+ return HYBBX_ERR_IO;
+ }
+ if (waited_ms == 0u) {
+ hybbx_log_info("[baycom] waiting for kiss device %s (up to %u s)",
+ path, max_wait_ms / 1000u);
+ }
+ usleep((useconds_t)step_ms * 1000u);
+ waited_ms += step_ms;
+ }
+
+ return HYBBX_OK;
+}
+
+struct baycom_kiss_modem {
+ hybbx_baycom_config_t cfg;
+ hybbx_serial_port_t *serial;
+ hybbx_kiss_decoder_t kiss_dec;
+ baycom_modem_frame_cb frame_cb;
+ void *frame_userdata;
+};
+
+static char *baycom_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 void on_kiss_frame(uint8_t port, const uint8_t *frame, size_t len,
+ void *userdata)
+{
+ baycom_kiss_modem_t *km = (baycom_kiss_modem_t *)userdata;
+
+ (void)port;
+
+ if (km == NULL || km->frame_cb == NULL || len == 0) {
+ return;
+ }
+
+ km->frame_cb(frame, len, km->frame_userdata);
+}
+
+static hybbx_result_t kiss_send_param(baycom_kiss_modem_t *km,
+ hybbx_kiss_command_t cmd, uint8_t value)
+{
+ uint8_t out[16];
+ size_t out_len;
+
+ out_len = hybbx_kiss_encode(0, cmd, &value, 1, out, sizeof(out));
+ if (out_len == 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ return hybbx_serial_write(km->serial, out, out_len);
+}
+
+static hybbx_result_t kiss_apply_channel_params(baycom_kiss_modem_t *km)
+{
+ hybbx_result_t rc;
+
+ rc = kiss_send_param(km, HYBBX_KISS_CMD_TXDELAY, (uint8_t)km->cfg.txdelay);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ rc = kiss_send_param(km, HYBBX_KISS_CMD_PERSIST, (uint8_t)km->cfg.persist);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ rc = kiss_send_param(km, HYBBX_KISS_CMD_SLOTTIME, (uint8_t)km->cfg.slot);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ rc = kiss_send_param(km, HYBBX_KISS_CMD_TXTAIL, (uint8_t)km->cfg.txtail);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ return kiss_send_param(km, HYBBX_KISS_CMD_FULLDUPLEX,
+ km->cfg.full_duplex ? 1u : 0u);
+}
+
+hybbx_result_t baycom_kiss_open(baycom_kiss_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata)
+{
+ baycom_kiss_modem_t *km;
+ hybbx_serial_params_t sparams;
+ hybbx_result_t rc;
+
+ if (out == NULL || cfg == NULL || cfg->device == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ *out = NULL;
+ km = calloc(1, sizeof(*km));
+ if (km == NULL) {
+ return HYBBX_ERR_NOMEM;
+ }
+
+ /* Own string copies only — never shallow-copy pointers from caller config
+ * (hybbx_baycom_config_free on close would double-free inst->config). */
+ memset(&km->cfg, 0, sizeof(km->cfg));
+ km->cfg.backend = cfg->backend;
+ km->cfg.mode = cfg->mode;
+ km->cfg.iobase = cfg->iobase;
+ km->cfg.irq = cfg->irq;
+ km->cfg.radio_baud = cfg->radio_baud;
+ km->cfg.kernel_autoload = cfg->kernel_autoload;
+ km->cfg.txdelay = cfg->txdelay;
+ km->cfg.txtail = cfg->txtail;
+ km->cfg.slot = cfg->slot;
+ km->cfg.persist = cfg->persist;
+ km->cfg.full_duplex = cfg->full_duplex;
+ km->cfg.serial_baud = cfg->serial_baud;
+ km->cfg.circuit_port = cfg->circuit_port;
+ km->cfg.device = baycom_strdup(cfg->device);
+ km->cfg.mycall = baycom_strdup(cfg->mycall);
+ km->cfg.circuit_host = baycom_strdup(cfg->circuit_host);
+ km->cfg.link_id = baycom_strdup(cfg->link_id);
+ km->cfg.link_password = baycom_strdup(cfg->link_password);
+ km->cfg.link_role = baycom_strdup(cfg->link_role);
+ km->cfg.frequency_mhz = baycom_strdup(cfg->frequency_mhz);
+ km->frame_cb = cb;
+ km->frame_userdata = userdata;
+ hybbx_kiss_decoder_init(&km->kiss_dec);
+
+ hybbx_serial_params_default(&sparams, cfg->serial_baud);
+ sparams.data_bits = 8;
+ sparams.parity = HYBBX_SERIAL_PARITY_NONE;
+ sparams.stop_bits = 1;
+ sparams.assert_modem_lines = 0;
+
+ rc = kiss_wait_device(km->cfg.device);
+ if (rc != HYBBX_OK) {
+ baycom_kiss_close(km);
+ return rc;
+ }
+
+ if (access(km->cfg.device, R_OK | W_OK) != 0) {
+ hybbx_log_warn("[baycom] kiss device not readable/writable: %s (%s; user in dialout?)",
+ km->cfg.device, strerror(errno));
+ baycom_kiss_close(km);
+ return HYBBX_ERR_IO;
+ }
+
+ rc = hybbx_serial_open(&km->serial, km->cfg.device, &sparams);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[baycom] kiss serial open failed: %s (see [serial] log)",
+ km->cfg.device);
+ baycom_kiss_close(km);
+ return rc;
+ }
+
+ rc = kiss_apply_channel_params(km);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[baycom] kiss channel params failed");
+ }
+
+ *out = km;
+ return HYBBX_OK;
+}
+
+void baycom_kiss_close(baycom_kiss_modem_t *km)
+{
+ if (km == NULL) {
+ return;
+ }
+
+ if (km->serial != NULL) {
+ hybbx_serial_close(km->serial);
+ }
+
+ hybbx_baycom_config_free(&km->cfg);
+ free(km);
+}
+
+hybbx_result_t baycom_kiss_poll(baycom_kiss_modem_t *km)
+{
+ uint8_t buf[256];
+ size_t read_len = 0;
+ hybbx_result_t rc;
+
+ if (km == NULL || km->serial == NULL) {
+ return HYBBX_ERR_IO;
+ }
+
+ rc = hybbx_serial_read(km->serial, buf, sizeof(buf), &read_len);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (read_len > 0) {
+ hybbx_kiss_decoder_feed(&km->kiss_dec, buf, read_len, on_kiss_frame, km);
+ }
+
+ return HYBBX_OK;
+}
+
+hybbx_result_t baycom_kiss_send_frame(baycom_kiss_modem_t *km,
+ const uint8_t *frame, size_t len)
+{
+ uint8_t out[HYBBX_KISS_MAX_FRAME + 8];
+ size_t out_len;
+
+ if (km == NULL || km->serial == NULL || frame == NULL || len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_rf_tx_burst_guard();
+
+ out_len = hybbx_kiss_encode(0, HYBBX_KISS_CMD_DATA, frame, len,
+ out, sizeof(out));
+ if (out_len == 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ return hybbx_serial_write(km->serial, out, out_len);
+}
diff --git a/plugins/baycom/baycom_kiss.h b/plugins/baycom/baycom_kiss.h
new file mode 100644
index 0000000..49c7b36
--- /dev/null
+++ b/plugins/baycom/baycom_kiss.h
@@ -0,0 +1,26 @@
+#ifndef BAYCOM_KISS_H
+#define BAYCOM_KISS_H
+
+#include "baycom_modem.h"
+#include "hybbx/baycom.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct baycom_kiss_modem baycom_kiss_modem_t;
+
+hybbx_result_t baycom_kiss_open(baycom_kiss_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata);
+void baycom_kiss_close(baycom_kiss_modem_t *modem);
+hybbx_result_t baycom_kiss_poll(baycom_kiss_modem_t *modem);
+hybbx_result_t baycom_kiss_send_frame(baycom_kiss_modem_t *modem,
+ const uint8_t *frame, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BAYCOM_KISS_H */
diff --git a/plugins/baycom/baycom_modem.c b/plugins/baycom/baycom_modem.c
new file mode 100644
index 0000000..e4e332f
--- /dev/null
+++ b/plugins/baycom/baycom_modem.c
@@ -0,0 +1,94 @@
+#include "baycom_modem.h"
+#include "baycom_kernel.h"
+#include "baycom_kiss.h"
+
+#include <stdlib.h>
+
+struct baycom_modem {
+ hybbx_baycom_backend_t backend;
+ baycom_kernel_modem_t *kernel;
+ baycom_kiss_modem_t *kiss;
+};
+
+hybbx_result_t baycom_modem_open(baycom_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata)
+{
+ baycom_modem_t *modem;
+ hybbx_result_t rc;
+
+ if (out == NULL || cfg == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ *out = NULL;
+ modem = calloc(1, sizeof(*modem));
+ if (modem == NULL) {
+ return HYBBX_ERR_NOMEM;
+ }
+
+ modem->backend = cfg->backend;
+
+ if (cfg->backend == HYBBX_BAYCOM_BACKEND_KISS) {
+ rc = baycom_kiss_open(&modem->kiss, cfg, cb, userdata);
+ } else {
+ rc = baycom_kernel_open(&modem->kernel, cfg, cb, userdata);
+ }
+
+ if (rc != HYBBX_OK) {
+ baycom_modem_close(modem);
+ return rc;
+ }
+
+ *out = modem;
+ return HYBBX_OK;
+}
+
+void baycom_modem_close(baycom_modem_t *modem)
+{
+ if (modem == NULL) {
+ return;
+ }
+
+ if (modem->kernel != NULL) {
+ baycom_kernel_close(modem->kernel);
+ }
+ if (modem->kiss != NULL) {
+ baycom_kiss_close(modem->kiss);
+ }
+
+ free(modem);
+}
+
+hybbx_result_t baycom_modem_poll(baycom_modem_t *modem)
+{
+ if (modem == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (modem->kiss != NULL) {
+ return baycom_kiss_poll(modem->kiss);
+ }
+ if (modem->kernel != NULL) {
+ return baycom_kernel_poll(modem->kernel);
+ }
+
+ return HYBBX_ERR_IO;
+}
+
+hybbx_result_t baycom_modem_send_frame(baycom_modem_t *modem,
+ const uint8_t *frame, size_t len)
+{
+ if (modem == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (modem->kiss != NULL) {
+ return baycom_kiss_send_frame(modem->kiss, frame, len);
+ }
+ if (modem->kernel != NULL) {
+ return baycom_kernel_send_frame(modem->kernel, frame, len);
+ }
+
+ return HYBBX_ERR_IO;
+}
diff --git a/plugins/baycom/baycom_modem.h b/plugins/baycom/baycom_modem.h
new file mode 100644
index 0000000..2365636
--- /dev/null
+++ b/plugins/baycom/baycom_modem.h
@@ -0,0 +1,31 @@
+#ifndef BAYCOM_MODEM_H
+#define BAYCOM_MODEM_H
+
+#include "hybbx/baycom.h"
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*baycom_modem_frame_cb)(const uint8_t *frame, size_t len,
+ void *userdata);
+
+typedef struct baycom_modem baycom_modem_t;
+
+hybbx_result_t baycom_modem_open(baycom_modem_t **out,
+ const hybbx_baycom_config_t *cfg,
+ baycom_modem_frame_cb cb, void *userdata);
+void baycom_modem_close(baycom_modem_t *modem);
+hybbx_result_t baycom_modem_poll(baycom_modem_t *modem);
+hybbx_result_t baycom_modem_send_frame(baycom_modem_t *modem,
+ const uint8_t *frame, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BAYCOM_MODEM_H */
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com