summaryrefslogtreecommitdiff
path: root/plugins/packet_radio/tnc.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /plugins/packet_radio/tnc.c
#2
Diffstat (limited to 'plugins/packet_radio/tnc.c')
-rw-r--r--plugins/packet_radio/tnc.c1080
1 files changed, 1080 insertions, 0 deletions
diff --git a/plugins/packet_radio/tnc.c b/plugins/packet_radio/tnc.c
new file mode 100644
index 0000000..a42480d
--- /dev/null
+++ b/plugins/packet_radio/tnc.c
@@ -0,0 +1,1080 @@
+#if defined(__linux__) || defined(__GLIBC__)
+#define _DEFAULT_SOURCE 1
+#endif
+
+#include "hybbx/tnc.h"
+#include "hybbx/kiss.h"
+#include "hybbx/rf_tx_pace.h"
+#include "hybbx/traffic.h"
+#include "serial_port.h"
+#include "sixpack.h"
+#include "tnc_host.h"
+
+#include "hybbx/log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#if !defined(_WIN32)
+#include <sys/select.h>
+#endif
+
+struct hybbx_tnc {
+ hybbx_packet_radio_config_t config;
+ hybbx_serial_port_t *serial;
+ hybbx_kiss_decoder_t kiss_dec;
+ hybbx_sixpack_decoder_t sixpack_dec;
+ hybbx_tnc_host_t host;
+ hybbx_ax25_path_t tx_path;
+ int kiss_active;
+ hybbx_kiss_entry_t kiss_entry_used;
+ int host_ready;
+ hybbx_tnc_frame_cb frame_cb;
+ hybbx_tnc_ui_cb ui_cb;
+ void *rx_userdata;
+ unsigned long long last_rx_ms;
+};
+
+static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc);
+
+static void config_copy(hybbx_packet_radio_config_t *dst,
+ const hybbx_packet_radio_config_t *src)
+{
+ unsigned i;
+
+ memset(dst, 0, sizeof(*dst));
+ dst->device_type = src->device_type;
+ dst->device = src->device;
+ dst->baud = src->baud;
+ dst->tnc = src->tnc;
+ dst->protocol = src->protocol;
+ dst->params = src->params;
+ dst->mycall = src->mycall;
+ dst->dest_call = src->dest_call;
+ dst->via_count = src->via_count;
+ for (i = 0; i < src->via_count; i++) {
+ dst->via[i] = src->via[i];
+ }
+}
+
+static unsigned long long tnc_monotonic_ms(void)
+{
+#if defined(CLOCK_MONOTONIC)
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
+ return (unsigned long long)ts.tv_sec * 1000ull +
+ (unsigned long long)ts.tv_nsec / 1000000ull;
+ }
+#endif
+ return (unsigned long long)time(NULL) * 1000ull;
+}
+
+static void tnc_note_rx(hybbx_tnc_t *tnc)
+{
+ if (tnc != NULL) {
+ tnc->last_rx_ms = tnc_monotonic_ms();
+ }
+}
+
+static hybbx_result_t tnc_wait_half_duplex_clear(hybbx_tnc_t *tnc)
+{
+ unsigned guard_ms;
+ unsigned long long now;
+ unsigned long long elapsed;
+
+ if (tnc == NULL || hybbx_tnc_duplex_is_full(tnc)) {
+ return HYBBX_OK;
+ }
+
+ guard_ms = tnc->config.params.slot * 10u;
+ if (guard_ms < 50u) {
+ guard_ms = 50u;
+ }
+
+ now = tnc_monotonic_ms();
+ if (tnc->last_rx_ms == 0 || now <= tnc->last_rx_ms) {
+ return HYBBX_OK;
+ }
+
+ elapsed = now - tnc->last_rx_ms;
+ if (elapsed >= guard_ms) {
+ return HYBBX_OK;
+ }
+
+#if defined(_WIN32)
+ Sleep((DWORD)(guard_ms - elapsed));
+#else
+ {
+ struct timeval tv;
+
+ tv.tv_sec = (time_t)((guard_ms - elapsed) / 1000u);
+ tv.tv_usec = (suseconds_t)(((guard_ms - elapsed) % 1000u) * 1000u);
+ (void)select(0, NULL, NULL, NULL, &tv);
+ }
+#endif
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t send_raw(hybbx_tnc_t *tnc, const uint8_t *data, size_t len)
+{
+ return hybbx_serial_write(tnc->serial, data, len);
+}
+
+static hybbx_result_t send_cmd(hybbx_tnc_t *tnc, const char *cmd)
+{
+ char line[128];
+ size_t len;
+
+ len = hybbx_tnc_host_format_cmd(cmd, line, sizeof(line));
+ if (len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ return send_raw(tnc, (const uint8_t *)line, len);
+}
+
+static hybbx_result_t drain_serial(hybbx_tnc_t *tnc, unsigned rounds)
+{
+ uint8_t buf[128];
+ size_t read_len;
+ unsigned i;
+
+ for (i = 0; i < rounds; i++) {
+ if (hybbx_serial_read(tnc->serial, buf, sizeof(buf),
+ &read_len) != HYBBX_OK) {
+ return HYBBX_ERR_IO;
+ }
+ if (read_len == 0) {
+ break;
+ }
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t send_kiss_param(hybbx_tnc_t *tnc,
+ hybbx_kiss_command_t cmd,
+ uint8_t value)
+{
+ uint8_t frame[16];
+ size_t len;
+
+ len = hybbx_kiss_encode((uint8_t)tnc->config.params.kiss_port, cmd,
+ &value, 1, frame, sizeof(frame));
+ if (len == 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ return send_raw(tnc, frame, len);
+}
+
+static hybbx_result_t tnc_apply_kiss_params(hybbx_tnc_t *tnc)
+{
+ hybbx_result_t rc;
+
+ rc = send_kiss_param(tnc, HYBBX_KISS_CMD_TXDELAY,
+ (uint8_t)tnc->config.params.txdelay);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ rc = send_kiss_param(tnc, HYBBX_KISS_CMD_PERSIST,
+ (uint8_t)tnc->config.params.persist);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ rc = send_kiss_param(tnc, HYBBX_KISS_CMD_SLOTTIME,
+ (uint8_t)tnc->config.params.slot);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ rc = send_kiss_param(tnc, HYBBX_KISS_CMD_TXTAIL,
+ (uint8_t)tnc->config.params.txtail);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ return send_kiss_param(tnc, HYBBX_KISS_CMD_FULLDUPLEX,
+ (uint8_t)(tnc->config.params.fullduplex ? 1 : 0));
+}
+
+static hybbx_result_t tnc_send_esc_at_k(hybbx_tnc_t *tnc)
+{
+ const uint8_t seq[] = { 0x1b, '@', 'K' };
+
+ return send_raw(tnc, seq, sizeof(seq));
+}
+
+static hybbx_result_t tnc_send_kiss_return_frame(hybbx_tnc_t *tnc)
+{
+ const uint8_t frame[] = { HYBBX_KISS_FEND, 0xFFu, HYBBX_KISS_FEND };
+
+ return send_raw(tnc, frame, sizeof(frame));
+}
+
+static void tnc_serial_pause_ms(unsigned ms)
+{
+#if defined(_WIN32)
+ Sleep(ms);
+#else
+ struct timespec ts;
+
+ ts.tv_sec = (time_t)(ms / 1000u);
+ ts.tv_nsec = (long)((ms % 1000u) * 1000000u);
+ (void)nanosleep(&ts, NULL);
+#endif
+}
+
+static hybbx_result_t tnc_send_jhost0(hybbx_tnc_t *tnc)
+{
+ static const uint8_t nuls[300] = { 0 };
+ static const uint8_t jhost0[] = {
+ 0x00, 0x01, 0x06, 'J', 'H', 'O', 'S', 'T', ' ', '0', '\r'
+ };
+ hybbx_result_t rc;
+
+ rc = send_raw(tnc, nuls, sizeof(nuls));
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ tnc_serial_pause_ms(150);
+ return send_raw(tnc, jhost0, sizeof(jhost0));
+}
+
+static hybbx_result_t tnc_send_restart_escape(hybbx_tnc_t *tnc)
+{
+ unsigned i;
+
+ for (i = 0; i < 3u; i++) {
+ if (send_raw(tnc, (const uint8_t *)"\x03", 1) != HYBBX_OK) {
+ return HYBBX_ERR_IO;
+ }
+ tnc_serial_pause_ms(80);
+ }
+ tnc_serial_pause_ms(300);
+ return send_cmd(tnc, "RESTART");
+}
+
+/** Leave KISS/host/transparency → terminal (TheFirmware ladder). */
+static hybbx_result_t tnc_recover_terminal_mode(hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ tnc->kiss_active = 0;
+ (void)tnc_send_kiss_return_frame(tnc);
+ tnc_serial_pause_ms(400);
+ drain_serial(tnc, 8);
+
+ (void)tnc_send_jhost0(tnc);
+ tnc_serial_pause_ms(800);
+ drain_serial(tnc, 8);
+
+ (void)send_cmd(tnc, "kiss off");
+ tnc_serial_pause_ms(400);
+ drain_serial(tnc, 6);
+ (void)send_cmd(tnc, "INFO");
+ drain_serial(tnc, 12);
+
+ (void)tnc_send_restart_escape(tnc);
+ tnc_serial_pause_ms(1200);
+ drain_serial(tnc, 8);
+ (void)send_cmd(tnc, "kiss off");
+ tnc_serial_pause_ms(300);
+ drain_serial(tnc, 4);
+
+ return HYBBX_OK;
+}
+
+/** Leave KISS (crash/kill) so host commands (MYCALL, kiss on) work again. */
+static hybbx_result_t tnc_kiss_wakeup_host(hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ return tnc_recover_terminal_mode(tnc);
+}
+
+static hybbx_result_t tnc_enter_kiss_by_entry(hybbx_tnc_t *tnc,
+ hybbx_kiss_entry_t entry)
+{
+ if (tnc == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ switch (entry) {
+ case HYBBX_KISS_ENTRY_NONE:
+ tnc->kiss_active = 1;
+ tnc->kiss_entry_used = HYBBX_KISS_ENTRY_NONE;
+ return HYBBX_OK;
+
+ case HYBBX_KISS_ENTRY_ESC_AT_K:
+ if (tnc_send_esc_at_k(tnc) != HYBBX_OK) {
+ return HYBBX_ERR_IO;
+ }
+ drain_serial(tnc, 8);
+ tnc->kiss_active = 1;
+ tnc->kiss_entry_used = HYBBX_KISS_ENTRY_ESC_AT_K;
+ return HYBBX_OK;
+
+ case HYBBX_KISS_ENTRY_KISS_ON:
+ if (send_cmd(tnc, "kiss on") != HYBBX_OK) {
+ return HYBBX_ERR_IO;
+ }
+ drain_serial(tnc, 8);
+ tnc->kiss_active = 1;
+ tnc->kiss_entry_used = HYBBX_KISS_ENTRY_KISS_ON;
+ return HYBBX_OK;
+
+ case HYBBX_KISS_ENTRY_AUTO:
+ if (send_cmd(tnc, "kiss on") == HYBBX_OK) {
+ drain_serial(tnc, 8);
+ tnc->kiss_active = 1;
+ tnc->kiss_entry_used = HYBBX_KISS_ENTRY_KISS_ON;
+ return HYBBX_OK;
+ }
+ if (tnc_send_esc_at_k(tnc) == HYBBX_OK) {
+ drain_serial(tnc, 8);
+ tnc->kiss_active = 1;
+ tnc->kiss_entry_used = HYBBX_KISS_ENTRY_ESC_AT_K;
+ return HYBBX_OK;
+ }
+ return HYBBX_ERR_IO;
+
+ case HYBBX_KISS_ENTRY_UNSET:
+ default:
+ return HYBBX_ERR_INVALID;
+ }
+}
+
+static void tnc_exit_kiss_mode(hybbx_tnc_t *tnc)
+{
+ hybbx_kiss_exit_t exit_mode;
+
+ if (tnc == NULL || !tnc->kiss_active ||
+ tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) {
+ return;
+ }
+
+ exit_mode = tnc->config.params.kiss_exit;
+ if (exit_mode == HYBBX_KISS_EXIT_UNSET) {
+ exit_mode = HYBBX_KISS_EXIT_AUTO;
+ }
+ if (exit_mode == HYBBX_KISS_EXIT_AUTO) {
+ if (tnc->kiss_entry_used == HYBBX_KISS_ENTRY_ESC_AT_K) {
+ exit_mode = HYBBX_KISS_EXIT_KISS_FRAME;
+ } else {
+ exit_mode = HYBBX_KISS_EXIT_KISS_OFF;
+ }
+ }
+
+ switch (exit_mode) {
+ case HYBBX_KISS_EXIT_KISS_FRAME:
+ (void)tnc_send_kiss_return_frame(tnc);
+ break;
+ case HYBBX_KISS_EXIT_KISS_OFF:
+ (void)send_cmd(tnc, "kiss off");
+ break;
+ case HYBBX_KISS_EXIT_NONE:
+ case HYBBX_KISS_EXIT_UNSET:
+ default:
+ break;
+ }
+}
+
+static hybbx_result_t tnc_enter_kiss_mode(hybbx_tnc_t *tnc)
+{
+ return tnc_enter_kiss_by_entry(tnc, tnc->config.params.kiss_entry);
+}
+
+static void tnc_deliver_ui(hybbx_tnc_t *tnc, const uint8_t *frame, size_t len)
+{
+ uint8_t payload[HYBBX_AX25_PAYLOAD_MAX];
+ hybbx_ax25_path_t path;
+ size_t payload_len;
+
+ payload_len = hybbx_ax25_parse_ui(frame, len, &path, payload,
+ sizeof(payload));
+ if (payload_len == 0) {
+ return;
+ }
+
+ if (tnc->ui_cb != NULL) {
+ tnc->ui_cb(payload, payload_len, &path, tnc->rx_userdata);
+ }
+
+ tnc_note_rx(tnc);
+}
+
+static void on_kiss_frame(uint8_t port, const uint8_t *frame, size_t len,
+ void *userdata)
+{
+ hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata;
+ uint8_t payload[HYBBX_AX25_PAYLOAD_MAX];
+ hybbx_ax25_path_t path;
+ size_t payload_len;
+
+ (void)port;
+
+ if (tnc->frame_cb != NULL) {
+ tnc->frame_cb(frame, len, tnc->rx_userdata);
+ }
+
+ if (tnc->ui_cb == NULL) {
+ return;
+ }
+
+ payload_len = hybbx_ax25_parse_ui(frame, len, &path, payload,
+ sizeof(payload));
+ if (payload_len > 0) {
+ tnc->ui_cb(payload, payload_len, &path, tnc->rx_userdata);
+ }
+}
+
+static void on_sixpack_frame(const uint8_t *frame, size_t len, void *userdata)
+{
+ hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata;
+
+ if (tnc->frame_cb != NULL) {
+ tnc->frame_cb(frame, len, tnc->rx_userdata);
+ }
+
+ if (tnc->ui_cb != NULL) {
+ tnc_deliver_ui(tnc, frame, len);
+ }
+}
+
+static void on_host_text(const uint8_t *data, size_t len, void *userdata)
+{
+ hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata;
+
+ if (tnc->ui_cb != NULL) {
+ tnc->ui_cb(data, len, NULL, tnc->rx_userdata);
+ }
+
+ tnc_note_rx(tnc);
+}
+
+static void on_host_event(hybbx_tnc_host_state_t state, void *userdata)
+{
+ hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata;
+
+ if (state == HYBBX_TNC_HOST_CMD) {
+ tnc->host_ready = 1;
+ }
+
+ hybbx_log_debug("[tnc] host state -> %d", (int)state);
+}
+
+static hybbx_result_t tnc_build_tx_path(hybbx_tnc_t *tnc)
+{
+ unsigned i;
+ hybbx_result_t rc;
+
+ memset(&tnc->tx_path, 0, sizeof(tnc->tx_path));
+
+ if (tnc->config.dest_call != NULL && tnc->config.dest_call[0] != '\0') {
+ rc = hybbx_ax25_address_parse(tnc->config.dest_call,
+ &tnc->tx_path.dest);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ } else {
+ (void)hybbx_ax25_address_parse("QST", &tnc->tx_path.dest);
+ }
+
+ if (tnc->config.mycall != NULL && tnc->config.mycall[0] != '\0') {
+ rc = hybbx_ax25_address_parse(tnc->config.mycall,
+ &tnc->tx_path.source);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ } else {
+ (void)hybbx_ax25_address_parse("HYBBX", &tnc->tx_path.source);
+ }
+
+ for (i = 0; i < tnc->config.via_count; i++) {
+ rc = hybbx_ax25_address_parse(tnc->config.via[i],
+ &tnc->tx_path.digi[i]);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ }
+ tnc->tx_path.digi_count = tnc->config.via_count;
+
+ return HYBBX_OK;
+}
+
+static const char *serial_parity_letter(hybbx_tnc_serial_parity_t parity)
+{
+ switch (parity) {
+ case HYBBX_TNC_SERIAL_PARITY_EVEN:
+ return "E";
+ case HYBBX_TNC_SERIAL_PARITY_ODD:
+ return "O";
+ case HYBBX_TNC_SERIAL_PARITY_NONE:
+ default:
+ return "N";
+ }
+}
+
+static void tnc_serial_params_from_config(const hybbx_packet_radio_config_t *config,
+ hybbx_serial_params_t *out)
+{
+ hybbx_serial_params_default(out, config->baud);
+ out->data_bits = config->params.data_bits;
+ out->stop_bits = config->params.stop_bits;
+ out->assert_modem_lines = config->params.assert_modem_lines;
+
+ switch (config->params.serial_parity) {
+ case HYBBX_TNC_SERIAL_PARITY_EVEN:
+ out->parity = HYBBX_SERIAL_PARITY_EVEN;
+ break;
+ case HYBBX_TNC_SERIAL_PARITY_ODD:
+ out->parity = HYBBX_SERIAL_PARITY_ODD;
+ break;
+ case HYBBX_TNC_SERIAL_PARITY_NONE:
+ default:
+ out->parity = HYBBX_SERIAL_PARITY_NONE;
+ break;
+ }
+}
+
+static const char *tnc_profile_name(hybbx_packet_radio_tnc_t tnc)
+{
+ return hybbx_packet_radio_tnc_name(tnc);
+}
+
+static const char *modem_name(hybbx_tnc2c_modem_t modem)
+{
+ switch (modem) {
+ case HYBBX_TNC2C_MODEM_9600:
+ return "9600";
+ case HYBBX_TNC2C_MODEM_19200:
+ return "19200";
+ case HYBBX_TNC2C_MODEM_TCM3105:
+ default:
+ return "tcm3105";
+ }
+}
+
+
+static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc);
+
+static hybbx_result_t tnc_profile_init(hybbx_tnc_t *tnc)
+{
+ hybbx_result_t rc;
+ char cmd[96];
+
+ rc = tnc_build_tx_path(tnc);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) {
+ if (tnc->config.params.kiss_entry == HYBBX_KISS_ENTRY_NONE) {
+ tnc->kiss_active = 1;
+ tnc->kiss_entry_used = HYBBX_KISS_ENTRY_NONE;
+ hybbx_log_info("[tnc] KISS attach (MAX25 prep assumed)");
+ return HYBBX_OK;
+ }
+
+ if (tnc_profile_thefirmware(tnc->config.tnc)) {
+ (void)tnc_recover_terminal_mode(tnc);
+ hybbx_log_info("[tnc] TheFirmware terminal recovery before KISS entry");
+ }
+
+ rc = tnc_enter_kiss_mode(tnc);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ return tnc_apply_kiss_params(tnc);
+ }
+
+ if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) {
+ tnc->kiss_active = 0;
+ return HYBBX_OK;
+ }
+
+ /* Host mode setup (firmware TNC profiles only; not BayCom/based SER12 / PC-COM modem). */
+ drain_serial(tnc, 4);
+ (void)send_cmd(tnc, "");
+
+ if (tnc->config.mycall != NULL && tnc->config.mycall[0] != '\0') {
+ snprintf(cmd, sizeof(cmd), "MYCALL %s", tnc->config.mycall);
+ rc = send_cmd(tnc, cmd);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ }
+
+ snprintf(cmd, sizeof(cmd), "TXDELAY %u", tnc->config.params.txdelay);
+ (void)send_cmd(tnc, cmd);
+ snprintf(cmd, sizeof(cmd), "Persist %u", tnc->config.params.persist);
+ (void)send_cmd(tnc, cmd);
+ snprintf(cmd, sizeof(cmd), "SlotTime %u", tnc->config.params.slot);
+ (void)send_cmd(tnc, cmd);
+ snprintf(cmd, sizeof(cmd), "TXTAIL %u", tnc->config.params.txtail);
+ (void)send_cmd(tnc, cmd);
+
+ if (tnc->config.params.fullduplex) {
+ (void)send_cmd(tnc, "FULLDUP ON");
+ } else {
+ (void)send_cmd(tnc, "FULLDUP OFF");
+ }
+
+ if (tnc->config.tnc == HYBBX_PACKET_RADIO_TNC_PCCOM) {
+ (void)send_cmd(tnc, "PACLEN 256");
+ (void)send_cmd(tnc, "MAXFRAME 4");
+ }
+
+ if (tnc->config.tnc == HYBBX_PACKET_RADIO_TNC_BAYCOM) {
+ (void)send_cmd(tnc, "PACLEN 256");
+ }
+
+ if (tnc->config.params.host_connect_on_start &&
+ tnc->config.dest_call != NULL && tnc->config.dest_call[0] != '\0') {
+ snprintf(cmd, sizeof(cmd), "C %s", tnc->config.dest_call);
+ rc = send_cmd(tnc, cmd);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ }
+
+ drain_serial(tnc, 6);
+ return HYBBX_OK;
+}
+
+hybbx_result_t hybbx_tnc_open(hybbx_tnc_t **out,
+ const hybbx_packet_radio_config_t *config,
+ hybbx_tnc_frame_cb frame_cb,
+ hybbx_tnc_ui_cb ui_cb,
+ void *rx_userdata)
+{
+ hybbx_tnc_t *tnc;
+ hybbx_result_t rc;
+
+ if (out == NULL || config == NULL || config->device == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (frame_cb == NULL && ui_cb == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ tnc = calloc(1, sizeof(*tnc));
+ if (tnc == NULL) {
+ return HYBBX_ERR_NOMEM;
+ }
+
+ config_copy(&tnc->config, config);
+ tnc->frame_cb = frame_cb;
+ tnc->ui_cb = ui_cb;
+ tnc->rx_userdata = rx_userdata;
+
+ hybbx_kiss_decoder_init(&tnc->kiss_dec);
+ hybbx_sixpack_decoder_init(&tnc->sixpack_dec);
+ hybbx_tnc_host_init(&tnc->host, on_host_text, on_host_event, tnc);
+
+ hybbx_serial_params_t serial_params;
+
+ tnc_serial_params_from_config(config, &serial_params);
+
+ rc = hybbx_serial_open(&tnc->serial, config->device, &serial_params);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[tnc] cannot open %s at %u baud (%u%c%u) — "
+ "device not available or permission denied",
+ config->device, config->baud, serial_params.data_bits,
+ *serial_parity_letter(config->params.serial_parity),
+ serial_params.stop_bits);
+ free(tnc);
+ return rc;
+ }
+
+ hybbx_log_info("[tnc] profile=%s protocol=%u device=%s host=%u %u%c%u rts_dtr=%s "
+ "kiss_entry=%s radio=%u modem=%s modulation=%s band=%s duplex=%s "
+ "txdelay=%u persist=%u",
+ tnc_profile_name(config->tnc), (unsigned)config->protocol,
+ config->device, config->baud, serial_params.data_bits,
+ *serial_parity_letter(config->params.serial_parity),
+ serial_params.stop_bits,
+ serial_params.assert_modem_lines ? "on" : "off",
+ hybbx_kiss_entry_name(config->params.kiss_entry),
+ config->params.radio_baud,
+ modem_name(config->params.modem),
+ hybbx_packet_radio_modulation_name(config->params.modulation),
+ hybbx_packet_radio_band_name(config->params.band),
+ hybbx_packet_radio_duplex_name(config->params.duplex),
+ config->params.txdelay,
+ config->params.persist);
+
+ if (config->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C &&
+ serial_params.data_bits == 8 &&
+ config->params.serial_parity == HYBBX_TNC_SERIAL_PARITY_NONE) {
+ hybbx_log_warn("[tnc] tnc2c %s: 8N1 host line — use serial_line=7E1 if KISS/UNPROTO TX fails",
+ config->device);
+ }
+
+ rc = tnc_profile_init(tnc);
+ if (rc != HYBBX_OK) {
+ hybbx_tnc_close(tnc);
+ return rc;
+ }
+
+ if (config->protocol == HYBBX_PACKET_RADIO_PROTO_KISS) {
+ hybbx_log_info("[tnc] KISS active (port %u)", config->params.kiss_port);
+ if (tnc_profile_thefirmware(config->tnc)) {
+ hybbx_log_info("[tnc] UI transmit=TheFirmware UNPROTO (KISS for RX)");
+ }
+ } else if (config->protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) {
+ hybbx_log_info("[tnc] 6PACK active");
+ } else {
+ hybbx_log_info("[tnc] TNC2 host mode active");
+ }
+
+ *out = tnc;
+ return HYBBX_OK;
+}
+
+void hybbx_tnc_close(hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL) {
+ return;
+ }
+
+ if (tnc->serial != NULL) {
+ tnc_exit_kiss_mode(tnc);
+ hybbx_serial_close(tnc->serial);
+ }
+
+ free(tnc);
+}
+
+int hybbx_tnc_serial_fd(const hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL || tnc->serial == NULL) {
+ return -1;
+ }
+
+ return hybbx_serial_fd(tnc->serial);
+}
+
+static hybbx_result_t tnc_ensure_kiss_tx(hybbx_tnc_t *tnc)
+{
+ hybbx_result_t rc;
+
+ if (tnc == NULL ||
+ tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) {
+ return HYBBX_OK;
+ }
+
+ if (!tnc->kiss_active) {
+ rc = tnc_enter_kiss_mode(tnc);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[tnc] KISS TX blocked — host mode (re-entry failed)");
+ return rc;
+ }
+ hybbx_log_info("[tnc] KISS re-entered before RF TX");
+ return tnc_apply_kiss_params(tnc);
+ }
+
+ return HYBBX_OK;
+}
+
+/** KISS DATA: host sends AX.25 body only; TheFirmware adds FCS on air. */
+static size_t tnc_kiss_ax25_len(const uint8_t *frame, size_t len)
+{
+ uint16_t crc_rx;
+ uint16_t crc_calc;
+
+ if (len < 16) {
+ return len;
+ }
+
+ crc_rx = (uint16_t)frame[len - 2] | ((uint16_t)frame[len - 1] << 8);
+ crc_calc = hybbx_ax25_crc(frame, len - 2);
+ if (crc_rx == crc_calc) {
+ return len - 2;
+ }
+
+ return len;
+}
+
+static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc)
+{
+ switch (tnc) {
+ case HYBBX_PACKET_RADIO_TNC_TNC2:
+ case HYBBX_PACKET_RADIO_TNC_TNC2C:
+ case HYBBX_PACKET_RADIO_TNC_GENERIC:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static void tnc_format_ax25_call(const hybbx_ax25_address_t *addr,
+ char *out, size_t out_len)
+{
+ if (addr == NULL || out == NULL || out_len == 0) {
+ return;
+ }
+
+ snprintf(out, out_len, "%s", addr->call);
+ if (addr->ssid > 0) {
+ size_t n = strlen(out);
+
+ if (n + 4 < out_len) {
+ snprintf(out + n, out_len - n, "-%u", addr->ssid);
+ }
+ }
+}
+
+/**
+ * TheFirmware (TNC-2 class) often ignores KISS DATA on hybrid host/KISS builds
+ * while UNPROTO from cmd: mode keys PTT reliably. Exit KISS, send, re-enter.
+ */
+static hybbx_result_t tnc_thefirmware_unproto_send(hybbx_tnc_t *tnc,
+ const hybbx_ax25_path_t *path,
+ const uint8_t *payload,
+ size_t payload_len)
+{
+ char cmd[320];
+ char dest[HYBBX_AX25_CALL_MAX + 8];
+ hybbx_result_t rc;
+
+ if (tnc == NULL || path == NULL || payload == NULL || payload_len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (payload_len + 64 >= sizeof(cmd)) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ tnc_format_ax25_call(&path->dest, dest, sizeof(dest));
+
+ rc = tnc_kiss_wakeup_host(tnc);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ (void)send_cmd(tnc, "MON OFF");
+
+ snprintf(cmd, sizeof(cmd), "UNPROTO %s 0 %.*s",
+ dest, (int)payload_len, (const char *)payload);
+ rc = send_cmd(tnc, cmd);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[tnc] UNPROTO %s host send failed", dest);
+ return rc;
+ }
+
+ drain_serial(tnc, 24);
+ tnc_serial_pause_ms(500);
+
+ hybbx_log_info("[tnc] TheFirmware UNPROTO %s (%zu bytes)", dest, payload_len);
+
+ rc = tnc_enter_kiss_mode(tnc);
+ if (rc != HYBBX_OK) {
+ hybbx_log_warn("[tnc] KISS re-entry after UNPROTO failed");
+ return rc;
+ }
+
+ return tnc_apply_kiss_params(tnc);
+}
+
+hybbx_result_t hybbx_tnc_send_frame(hybbx_tnc_t *tnc,
+ const uint8_t *frame, size_t len)
+{
+ uint8_t buf[HYBBX_KISS_MAX_FRAME + 8];
+ size_t encoded;
+ hybbx_result_t rc;
+
+ if (tnc == NULL || frame == NULL || len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_rf_tx_pace();
+
+ rc = tnc_wait_half_duplex_clear(tnc);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) {
+ encoded = hybbx_sixpack_encode(frame, len, buf, sizeof(buf));
+ if (encoded == 0) {
+ return HYBBX_ERR_IO;
+ }
+ return send_raw(tnc, buf, encoded);
+ }
+
+ if (!tnc->kiss_active &&
+ tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) {
+ return HYBBX_ERR_UNSUPPORTED;
+ }
+
+ if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS &&
+ tnc_profile_thefirmware(tnc->config.tnc)) {
+ hybbx_ax25_path_t path;
+ uint8_t ui[HYBBX_AX25_PAYLOAD_MAX];
+ size_t ui_len = hybbx_ax25_parse_ui(frame, len, &path, ui, sizeof(ui));
+
+ if (ui_len > 0) {
+ return tnc_thefirmware_unproto_send(tnc, &path, ui, ui_len);
+ }
+ }
+
+ if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) {
+ rc = tnc_ensure_kiss_tx(tnc);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+ }
+
+ {
+ const uint8_t *tx_frame = frame;
+ size_t tx_len = len;
+
+ if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) {
+ tx_len = tnc_kiss_ax25_len(frame, len);
+ tx_frame = frame;
+ (void)send_kiss_param(tnc, HYBBX_KISS_CMD_PERSIST,
+ (uint8_t)tnc->config.params.persist);
+ if (tnc_profile_thefirmware(tnc->config.tnc)) {
+ hybbx_log_warn("[tnc] UI parse failed (%zu bytes) — KISS DATA fallback",
+ len);
+ }
+ }
+
+ encoded = hybbx_kiss_encode((uint8_t)tnc->config.params.kiss_port,
+ HYBBX_KISS_CMD_DATA, tx_frame, tx_len,
+ buf, sizeof(buf));
+ if (encoded == 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ hybbx_log_debug("[tnc] KISS DATA ax25=%zu kiss=%zu%s",
+ tx_len, encoded,
+ tx_len + 2 == len ? " (fcs stripped)" : "");
+
+ return send_raw(tnc, buf, encoded);
+ }
+}
+
+hybbx_result_t hybbx_tnc_send_ui(hybbx_tnc_t *tnc,
+ const uint8_t *payload, size_t len)
+{
+ uint8_t frame[HYBBX_AX25_FRAME_MAX];
+ size_t frame_len;
+
+ if (tnc == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ frame_len = hybbx_ax25_build_ui(&tnc->tx_path, payload, len,
+ frame, sizeof(frame));
+ if (frame_len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ return hybbx_tnc_send_frame(tnc, frame, frame_len);
+}
+
+hybbx_result_t hybbx_tnc_send_converse(hybbx_tnc_t *tnc,
+ const char *data, size_t len)
+{
+ hybbx_result_t rc;
+
+ if (tnc == NULL || data == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_HOSTMODE) {
+ return HYBBX_ERR_UNSUPPORTED;
+ }
+
+ rc = tnc_wait_half_duplex_clear(tnc);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ return send_raw(tnc, (const uint8_t *)data, len);
+}
+
+hybbx_packet_radio_duplex_t hybbx_tnc_duplex(const hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL) {
+ return HYBBX_PACKET_RADIO_DUPLEX_HALF;
+ }
+
+ return tnc->config.params.duplex;
+}
+
+hybbx_packet_radio_band_t hybbx_tnc_band(const hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL) {
+ return HYBBX_PACKET_RADIO_BAND_UNSET;
+ }
+
+ return tnc->config.params.band;
+}
+
+int hybbx_tnc_duplex_is_full(const hybbx_tnc_t *tnc)
+{
+ return tnc != NULL &&
+ tnc->config.params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL;
+}
+
+int hybbx_tnc_host_connected(const hybbx_tnc_t *tnc)
+{
+ if (tnc == NULL) {
+ return 0;
+ }
+
+ return tnc->host.state == HYBBX_TNC_HOST_CONNECTED;
+}
+
+hybbx_result_t hybbx_tnc_poll(hybbx_tnc_t *tnc)
+{
+ uint8_t buf[256];
+ size_t read_len;
+ hybbx_result_t rc;
+
+ if (tnc == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_serial_read(tnc->serial, buf, sizeof(buf), &read_len);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (read_len == 0) {
+ return HYBBX_OK;
+ }
+
+ switch (tnc->config.protocol) {
+ case HYBBX_PACKET_RADIO_PROTO_KISS:
+ hybbx_kiss_decoder_feed(&tnc->kiss_dec, buf, read_len,
+ on_kiss_frame, tnc);
+ break;
+ case HYBBX_PACKET_RADIO_PROTO_SIXPACK:
+ hybbx_sixpack_decoder_feed(&tnc->sixpack_dec, buf, read_len,
+ on_sixpack_frame, tnc);
+ break;
+ case HYBBX_PACKET_RADIO_PROTO_HOSTMODE:
+ hybbx_tnc_host_feed(&tnc->host, buf, read_len);
+ break;
+ default:
+ break;
+ }
+
+ return HYBBX_OK;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com