From 20cb29c2f8c5c87bc590896854a20b1473ceb358 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sat, 8 Aug 2026 03:54:55 +0000 Subject: #2 --- plugins/packet_radio/tnc.c | 1080 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1080 insertions(+) create mode 100644 plugins/packet_radio/tnc.c (limited to 'plugins/packet_radio/tnc.c') 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 +#include +#include +#include + +#if !defined(_WIN32) +#include +#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; +} -- cgit v1.3.1