#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; }