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/CMakeLists.txt | 36 + plugins/packet_radio/ax25.c | 250 +++++++ plugins/packet_radio/kiss.c | 152 ++++ plugins/packet_radio/packet_radio.c | 1005 ++++++++++++++++++++++++++ plugins/packet_radio/packet_radio_config.c | 786 ++++++++++++++++++++ plugins/packet_radio/serial_port.c | 705 ++++++++++++++++++ plugins/packet_radio/serial_port.h | 52 ++ plugins/packet_radio/sixpack.c | 146 ++++ plugins/packet_radio/sixpack.h | 40 ++ plugins/packet_radio/tnc.c | 1080 ++++++++++++++++++++++++++++ plugins/packet_radio/tnc_host.c | 143 ++++ plugins/packet_radio/tnc_host.h | 51 ++ plugins/packet_radio/tnc_profiles.c | 399 ++++++++++ 13 files changed, 4845 insertions(+) create mode 100644 plugins/packet_radio/CMakeLists.txt create mode 100644 plugins/packet_radio/ax25.c create mode 100644 plugins/packet_radio/kiss.c create mode 100644 plugins/packet_radio/packet_radio.c create mode 100644 plugins/packet_radio/packet_radio_config.c create mode 100644 plugins/packet_radio/serial_port.c create mode 100644 plugins/packet_radio/serial_port.h create mode 100644 plugins/packet_radio/sixpack.c create mode 100644 plugins/packet_radio/sixpack.h create mode 100644 plugins/packet_radio/tnc.c create mode 100644 plugins/packet_radio/tnc_host.c create mode 100644 plugins/packet_radio/tnc_host.h create mode 100644 plugins/packet_radio/tnc_profiles.c (limited to 'plugins/packet_radio') diff --git a/plugins/packet_radio/CMakeLists.txt b/plugins/packet_radio/CMakeLists.txt new file mode 100644 index 0000000..1dc07a5 --- /dev/null +++ b/plugins/packet_radio/CMakeLists.txt @@ -0,0 +1,36 @@ +add_library(hybbx_plugin_packet_radio STATIC + packet_radio.c + packet_radio_config.c + ax25.c + kiss.c + serial_port.c + sixpack.c + tnc.c + tnc_host.c + tnc_profiles.c +) + +target_include_directories(hybbx_plugin_packet_radio + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_packet_radio + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_plugin_packet_radio PRIVATE hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_plugin_packet_radio PRIVATE amiga) +endif() +hybbx_link_plugin_instances(hybbx_plugin_packet_radio HYBBX_HAVE_PLUGIN_PACKET_RADIO) +hybbx_apply_hardening(hybbx_plugin_packet_radio) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_packet_radio + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/packet_radio/ax25.c b/plugins/packet_radio/ax25.c new file mode 100644 index 0000000..5cbcfd8 --- /dev/null +++ b/plugins/packet_radio/ax25.c @@ -0,0 +1,250 @@ +#include "hybbx/ax25.h" + +#include +#include +#include + +static void address_upper(char *call) +{ + size_t i; + + for (i = 0; call[i] != '\0'; i++) { + call[i] = (char)toupper((unsigned char)call[i]); + } +} + +hybbx_result_t hybbx_ax25_address_parse(const char *text, + hybbx_ax25_address_t *out) +{ + const char *dash; + size_t call_len; + char *end = NULL; + unsigned long ssid; + + if (text == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + dash = strchr(text, '-'); + if (dash != NULL) { + call_len = (size_t)(dash - text); + ssid = strtoul(dash + 1, &end, 10); + if (end == dash + 1 || ssid > HYBBX_AX25_SSID_MAX) { + return HYBBX_ERR_INVALID; + } + out->ssid = (unsigned)ssid; + } else { + call_len = strlen(text); + } + + if (call_len == 0 || call_len > HYBBX_AX25_CALL_MAX) { + return HYBBX_ERR_INVALID; + } + + memcpy(out->call, text, call_len); + out->call[call_len] = '\0'; + address_upper(out->call); + return HYBBX_OK; +} + +void hybbx_ax25_encode_address(uint8_t *out, const hybbx_ax25_address_t *addr, + int last) +{ + size_t i; + char padded[HYBBX_AX25_CALL_MAX]; + + memset(padded, ' ', sizeof(padded)); + for (i = 0; i < HYBBX_AX25_CALL_MAX && addr->call[i] != '\0'; i++) { + padded[i] = addr->call[i]; + } + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + out[i] = (uint8_t)((unsigned char)padded[i] << 1); + } + + out[6] = (uint8_t)(((addr->ssid & 0x0Fu) << 1) | 0x60u); + if (last) { + out[6] |= 0x01u; + } +} + +uint16_t hybbx_ax25_crc(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFFu; + size_t i; + int bit; + + for (i = 0; i < len; i++) { + crc ^= (uint16_t)data[i] << 8; + for (bit = 0; bit < 8; bit++) { + if (crc & 0x8000u) { + crc = (uint16_t)((crc << 1) ^ 0x1021u); + } else { + crc = (uint16_t)(crc << 1); + } + } + } + + return crc; +} + +static size_t append_addresses(const hybbx_ax25_path_t *path, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + unsigned i; + + if (out_cap < (size_t)(2 + path->digi_count) * HYBBX_AX25_ADDR_ENCODED) { + return 0; + } + + hybbx_ax25_encode_address(out + pos, &path->dest, 0); + pos += HYBBX_AX25_ADDR_ENCODED; + + for (i = 0; i < path->digi_count; i++) { + hybbx_ax25_encode_address(out + pos, &path->digi[i], 0); + pos += HYBBX_AX25_ADDR_ENCODED; + } + + hybbx_ax25_encode_address(out + pos, &path->source, 1); + pos += HYBBX_AX25_ADDR_ENCODED; + return pos; +} + +size_t hybbx_ax25_build_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos; + uint16_t crc; + + if (path == NULL || out == NULL || + (payload_len > 0 && payload == NULL)) { + return 0; + } + + if (payload_len + 32 > out_cap || payload_len > HYBBX_AX25_FRAME_MAX) { + return 0; + } + + pos = append_addresses(path, out, out_cap); + if (pos + 4 + payload_len > out_cap) { + return 0; + } + + out[pos++] = HYBBX_AX25_CTRL_UI; + out[pos++] = HYBBX_AX25_PID_NOL3; + + if (payload_len > 0) { + memcpy(out + pos, payload, payload_len); + pos += payload_len; + } + + crc = hybbx_ax25_crc(out, pos); + out[pos++] = (uint8_t)(crc & 0xFFu); + out[pos++] = (uint8_t)((crc >> 8) & 0xFFu); + return pos; +} + +static void decode_address(const uint8_t *in, hybbx_ax25_address_t *out) +{ + size_t i; + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + char ch = (char)((in[i] >> 1) & 0x7Fu); + out->call[i] = ch == ' ' ? '\0' : ch; + } + out->call[HYBBX_AX25_CALL_MAX] = '\0'; + + for (i = HYBBX_AX25_CALL_MAX; i > 0; i--) { + if (out->call[i - 1] != '\0') { + break; + } + } + out->call[i] = '\0'; + out->ssid = (unsigned)((in[6] >> 1) & 0x0Fu); +} + +size_t hybbx_ax25_parse_ui(const uint8_t *frame, size_t frame_len, + hybbx_ax25_path_t *path, + uint8_t *payload, size_t payload_cap) +{ + size_t pos = 0; + unsigned digi = 0; + uint16_t crc_rx; + uint16_t crc_calc; + + if (frame == NULL || frame_len < 16) { + return 0; + } + + if (path != NULL) { + memset(path, 0, sizeof(*path)); + } + + while (pos + HYBBX_AX25_ADDR_ENCODED <= frame_len) { + int last = frame[pos + 6] & 0x01; + hybbx_ax25_address_t tmp; + + decode_address(frame + pos, &tmp); + if (path != NULL) { + if (digi == 0) { + path->dest = tmp; + } else if (!last) { + if (digi - 1 < HYBBX_AX25_MAX_DIGI) { + path->digi[digi - 1] = tmp; + path->digi_count = digi; + } + } else { + path->source = tmp; + } + } + + pos += HYBBX_AX25_ADDR_ENCODED; + digi++; + + if (last) { + break; + } + + if (digi > HYBBX_AX25_MAX_DIGI + 1) { + return 0; + } + } + + if (pos + 4 > frame_len) { + return 0; + } + + if (frame[pos] != HYBBX_AX25_CTRL_UI) { + return 0; + } + + pos += 2; /* control + PID */ + + if (frame_len < pos + 2) { + return 0; + } + + crc_rx = (uint16_t)frame[frame_len - 2] | + ((uint16_t)frame[frame_len - 1] << 8); + crc_calc = hybbx_ax25_crc(frame, frame_len - 2); + if (crc_rx != crc_calc) { + return 0; + } + + { + size_t payload_len = frame_len - pos - 2; + + if (payload_len > payload_cap) { + return 0; + } + + if (payload_len > 0 && payload != NULL) { + memcpy(payload, frame + pos, payload_len); + } + + return payload_len; + } +} diff --git a/plugins/packet_radio/kiss.c b/plugins/packet_radio/kiss.c new file mode 100644 index 0000000..a075442 --- /dev/null +++ b/plugins/packet_radio/kiss.c @@ -0,0 +1,152 @@ +#include "hybbx/kiss.h" + +#include + +void hybbx_kiss_decoder_init(hybbx_kiss_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void deliver_frame(hybbx_kiss_decoder_t *dec, + hybbx_kiss_frame_cb cb, void *userdata) +{ + uint8_t port; + const uint8_t *payload; + size_t payload_len; + + if (dec->len < 1) { + return; + } + + port = (uint8_t)((dec->buf[0] >> 4) & 0x0Fu); + payload = dec->buf + 1; + payload_len = dec->len - 1; + + if (cb != NULL) { + cb(port, payload, payload_len, userdata); + } +} + +void hybbx_kiss_decoder_feed(hybbx_kiss_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_kiss_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + uint8_t byte = data[i]; + + if (byte == HYBBX_KISS_FEND) { + if (dec->in_frame && dec->len > 0) { + deliver_frame(dec, cb, userdata); + } + dec->in_frame = 1; + dec->escape = 0; + dec->len = 0; + continue; + } + + if (!dec->in_frame) { + continue; + } + + if (dec->escape) { + if (byte == HYBBX_KISS_TFEND) { + byte = HYBBX_KISS_FEND; + } else if (byte == HYBBX_KISS_TFESC) { + byte = HYBBX_KISS_FESC; + } + dec->escape = 0; + } else if (byte == HYBBX_KISS_FESC) { + dec->escape = 1; + continue; + } + + if (dec->len >= HYBBX_KISS_MAX_FRAME) { + dec->len = 0; + dec->in_frame = 0; + continue; + } + + dec->buf[dec->len++] = byte; + } +} + +static size_t kiss_write_byte(uint8_t byte, uint8_t *out, size_t out_cap, size_t pos) +{ + if (pos + 1 > out_cap) { + return 0; + } + + if (byte == HYBBX_KISS_FEND) { + if (pos + 2 > out_cap) { + return 0; + } + out[pos++] = HYBBX_KISS_FESC; + out[pos++] = HYBBX_KISS_TFEND; + return pos; + } + + if (byte == HYBBX_KISS_FESC) { + if (pos + 2 > out_cap) { + return 0; + } + out[pos++] = HYBBX_KISS_FESC; + out[pos++] = HYBBX_KISS_TFESC; + return pos; + } + + out[pos++] = byte; + return pos; +} + +size_t hybbx_kiss_encode(uint8_t port, hybbx_kiss_command_t cmd, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + uint8_t cmd_byte; + size_t i; + + if (out == NULL || out_cap < 3) { + return 0; + } + + if (payload_len > 0 && payload == NULL) { + return 0; + } + + if (payload_len + 4 > out_cap) { + return 0; + } + + out[pos++] = HYBBX_KISS_FEND; + + cmd_byte = (uint8_t)(((port & 0x0Fu) << 4) | ((uint8_t)cmd & 0x0Fu)); + pos = kiss_write_byte(cmd_byte, out, out_cap, pos); + if (pos == 0) { + return 0; + } + + for (i = 0; i < payload_len; i++) { + pos = kiss_write_byte(payload[i], out, out_cap, pos); + if (pos == 0) { + return 0; + } + } + + if (pos + 1 > out_cap) { + return 0; + } + + out[pos++] = HYBBX_KISS_FEND; + return pos; +} diff --git a/plugins/packet_radio/packet_radio.c b/plugins/packet_radio/packet_radio.c new file mode 100644 index 0000000..e8952ae --- /dev/null +++ b/plugins/packet_radio/packet_radio.c @@ -0,0 +1,1005 @@ +/* + * packet_radio — link/repeater edge adapter: serial TNC, KISS/AX.25 → HBX client. + * INI: [transport.packet_radio] or [transport.packet_radioN]. See share/hybbx.ini.example. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/broadcast.h" +#include "hybbx/packet_radio.h" +#include "hybbx/tnc.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/traffic.h" +#include "hybbx/limits.h" +#include "hybbx/security_ban.h" +#include "hybbx/ax25.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "hybbx/max25.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#include +#include +#endif + +#define PACKET_RADIO_POLL_MS 20u + +static hybbx_service_t *g_service; + +typedef struct packet_radio_instance { + hybbx_packet_radio_config_t config; + hybbx_tnc_t *tnc; + int circuit_fd; + hybbx_circuit_decoder_t circuit_dec; + volatile int flow_paused; + volatile int flow_cancel; + unsigned index; + unsigned circuit_reconnect_polls; +} packet_radio_instance_t; + +static packet_radio_instance_t g_instances[HYBBX_PACKET_RADIO_MAX_INSTANCES]; +static unsigned g_instance_count; +static pthread_t g_poll_thread; +static volatile int g_radio_running; +static int g_poll_thread_started; + +extern const hybbx_transport_plugin_t hybbx_plugin_packet_radio; + +static void instance_note_rf_activity(packet_radio_instance_t *inst); + +static unsigned packet_radio_count_local_edges(const char *config) +{ + char scratch[4096]; + const char *cursor = config; + unsigned count = 0; + + while (*cursor != '\0') { + const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP); + size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor); + + if (len == 0) { + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + if (len >= sizeof(scratch)) { + return count; + } + + memcpy(scratch, cursor, len); + scratch[len] = '\0'; + + if (hybbx_packet_radio_section_is_local_edge(scratch)) { + count++; + } + + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + } + + return count; +} + +static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst); +static void packet_radio_poll_sleep_ms(unsigned ms); + +static const char *device_type_name(hybbx_packet_radio_device_type_t type) +{ + return type == HYBBX_PACKET_RADIO_DEVICE_SERIAL ? "serial" : "usb"; +} + +static const char *protocol_name(hybbx_packet_radio_protocol_t proto) +{ + switch (proto) { + case HYBBX_PACKET_RADIO_PROTO_HOSTMODE: + return "hostmode"; + case HYBBX_PACKET_RADIO_PROTO_SIXPACK: + return "6pack"; + case HYBBX_PACKET_RADIO_PROTO_KISS: + default: + return "kiss"; + } +} + +static const char *tnc_name(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return "baycom"; + case HYBBX_PACKET_RADIO_TNC_PCCOM: + return "pccom"; + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return "generic"; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + return "tnc2c"; + } +} + +static int instance_circuit_uplink_allowed(const packet_radio_instance_t *inst) +{ + return inst != NULL && !inst->flow_paused && !inst->flow_cancel; +} + +static int packet_radio_source_banned(const hybbx_ax25_path_t *path) +{ + char raw[HYBBX_CALLID_MAX]; + char norm[HYBBX_CALLID_MAX]; + + if (path == NULL || path->source.call[0] == '\0') { + return 0; + } + + if (path->source.ssid > 0u) { + snprintf(raw, sizeof(raw), "%s-%u", path->source.call, + path->source.ssid); + } else { + hybbx_strlcpy(raw, path->source.call, sizeof(raw)); + } + + if (!hybbx_security_callid_normalize(raw, norm, sizeof(norm))) { + return 0; + } + + return hybbx_security_ban_callid_is_banned(norm); +} + +static int packet_radio_frame_source_banned(const uint8_t *frame, size_t len) +{ + hybbx_ax25_path_t path; + uint8_t scratch[HYBBX_AX25_FRAME_MAX]; + + if (frame == NULL || len == 0) { + return 0; + } + + if (hybbx_ax25_parse_ui(frame, len, &path, scratch, sizeof(scratch)) == 0) { + return 0; + } + + return packet_radio_source_banned(&path); +} + +static hybbx_result_t instance_circuit_uplink(packet_radio_instance_t *inst, + const uint8_t *frame, + size_t frame_len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + uint16_t flags = HYBBX_CIRCUIT_FLAG_RX; + + if (inst == NULL || inst->circuit_fd < 0) { + return HYBBX_ERR_IO; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return HYBBX_OK; + } + + if (hybbx_packet_radio_modulation_is_g3ruh(inst->config.params.modulation)) { + flags |= HYBBX_CIRCUIT_FLAG_G3RUH_FSK; + } + + hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, flags, + hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); +} + +static void on_tnc_ax25_frame(const uint8_t *frame, size_t len, void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + + if (len == 0 || inst == NULL) { + return; + } + + if (packet_radio_frame_source_banned(frame, len)) { + return; + } + + instance_note_rf_activity(inst); + (void)instance_circuit_uplink(inst, frame, len); +} + +static void on_tnc_host_ui(const uint8_t *payload, size_t len, + const hybbx_ax25_path_t *path, void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + (void)path; + + if (inst == NULL || inst->circuit_fd < 0 || len == 0) { + return; + } + + if (path != NULL && packet_radio_source_banned(path)) { + return; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return; + } + + hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_RX, + payload, len, hbx, sizeof(hbx)); + if (hbx_len > 0) { + (void)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_log_rf_tx(packet_radio_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("[packet_radio%u] RF TX %s>%s (%zu bytes)", + inst->index + 1, src, dst, payload_len); + } else { + hybbx_log_warn("[packet_radio%u] RF TX failed %s>%s (%zu bytes, rc=%d)", + inst->index + 1, src, dst, payload_len, (int)rc); + } +} + +static void instance_note_rf_activity(packet_radio_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 (link_id == NULL || link_id[0] == '\0') { + link_id = "packet-radio"; + } + + if (hub != NULL) { + hybbx_circuit_hub_note_rf_activity(hub, link_id); + } +} + +static hybbx_result_t instance_tx_ax25_ui(packet_radio_instance_t *inst, + const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_AX25_FRAME_MAX]; + size_t frame_len; + hybbx_result_t rc; + + if (inst == NULL || inst->tnc == NULL || path == NULL || + payload == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_ax25_build_ui(path, payload, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_tnc_send_frame(inst->tnc, frame, frame_len); + if (rc == HYBBX_OK) { + instance_note_rf_activity(inst); + } + instance_log_rf_tx(inst, rc, path, len); + return rc; +} + +static hybbx_result_t instance_send_bytes(packet_radio_instance_t *inst, + const uint8_t *data, size_t len) +{ + if (inst == NULL || inst->tnc == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + if (hybbx_tnc_host_connected(inst->tnc)) { + return hybbx_tnc_send_converse(inst->tnc, (const char *)data, len); + } + return HYBBX_ERR_BUSY; + } + + return hybbx_tnc_send_ui(inst->tnc, data, len); +} + +static void instance_circuit_disconnect(packet_radio_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(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + instance_circuit_disconnect(inst); + packet_radio_poll_sleep_ms(250); + if (instance_connect_circuit(inst) == HYBBX_OK) { + hybbx_log_info("[packet_radio%u] circuit reconnected", inst->index + 1); + } +} + +static void instance_on_circuit_flow_ctrl(packet_radio_instance_t *inst, + hybbx_circuit_proto_t proto, + uint16_t flags, + const uint8_t *payload, size_t len) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + (void)proto; + (void)flags; + + 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("[packet_radio%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("[packet_radio%u] circuit flow break (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + inst->flow_cancel = 1; + hybbx_log_warn("[packet_radio%u] circuit flow cancel (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + inst->flow_paused = 0; + hybbx_log_stats("[packet_radio%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) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + + if (inst == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + instance_on_circuit_flow_ctrl(inst, proto, flags, payload, len); + return; + } + + if (inst->tnc == NULL || len == 0) { + return; + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_TERMINAL: + (void)instance_send_bytes(inst, payload, len); + break; + case HYBBX_CIRCUIT_PROTO_AX25: { + hybbx_result_t rc = hybbx_tnc_send_frame(inst->tnc, payload, len); + if (rc == HYBBX_OK) { + 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)); + + instance_note_rf_activity(inst); + if (ui_len > 0) { + instance_log_rf_tx(inst, rc, &path, ui_len); + } + } else { + hybbx_log_warn("[packet_radio%u] RF TX frame failed (rc=%d, %zu bytes)", + inst->index + 1, (int)rc, len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_AX25_UI: { + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, + ui, sizeof(ui)); + if (ui_len > 0) { + (void)instance_tx_ax25_ui(inst, &path, ui, ui_len); + } else { + hybbx_log_warn("[packet_radio%u] AX25_UI unpack failed (%zu bytes)", + inst->index + 1, len); + } + break; + } + default: + break; + } +} + +static void packet_radio_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(packet_radio_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; + } + + if (inst->config.link_password == NULL || + inst->config.link_password[0] == '\0') { + hybbx_log_warn("[packet_radio%u] missing link_password for HBX LINK_AUTH; " + "set link_password in [transport.packet_radioN] " + "(hub logs link_auth_fail reason=timeout_no_link_auth)", + inst->index + 1); + return HYBBX_ERR_INVALID; + } + + 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); + { + const char *link_id = inst->config.link_id; + const char *link_role = inst->config.link_role; + + if (link_id == NULL || link_id[0] == '\0') { + link_id = "packet-radio"; + } + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + { + hybbx_circuit_link_qos_t qos; + int duplex = (int)inst->config.params.duplex; + + memset(&qos, 0, sizeof(qos)); + qos.baud = inst->config.params.radio_baud; + if (duplex == HYBBX_PACKET_RADIO_DUPLEX_HALF) { + qos.duplex = 1; + } else if (duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) { + qos.duplex = 2; + } + 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; + packet_radio_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[packet_radio%u] linked to internal circuit %s:%u (HBX)", + inst->index + 1, host, port); + return HYBBX_OK; + } + packet_radio_poll_sleep_ms(100); + } + + hybbx_log_warn("[packet_radio%u] could not connect to internal circuit %s:%u", + inst->index + 1, host, port); + return HYBBX_ERR_IO; +} + +static void instance_shutdown(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + + if (inst->tnc != NULL) { + hybbx_tnc_close(inst->tnc); + inst->tnc = NULL; + } + + hybbx_packet_radio_config_free(&inst->config); + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; +} + +typedef enum { + PR_POLL_CIRCUIT = 0, + PR_POLL_SERIAL = 1 +} packet_radio_poll_kind_t; + +typedef struct { + unsigned instance_index; + packet_radio_poll_kind_t kind; +} packet_radio_poll_map_t; + +static void instance_drain_circuit(packet_radio_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("[packet_radio%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); + } +} + +#if !defined(_WIN32) +static void packet_radio_poll_instances(void) +{ + struct pollfd pfds[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u]; + packet_radio_poll_map_t map[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u]; + nfds_t nfds = 0; + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + int circuit_fd = inst->circuit_fd; + int serial_fd = inst->tnc != NULL ? hybbx_tnc_serial_fd(inst->tnc) : -1; + + if (circuit_fd >= 0) { + pfds[nfds].fd = circuit_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + map[nfds].instance_index = i; + map[nfds].kind = PR_POLL_CIRCUIT; + nfds++; + } + + if (serial_fd >= 0) { + pfds[nfds].fd = serial_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + map[nfds].instance_index = i; + map[nfds].kind = PR_POLL_SERIAL; + nfds++; + } + } + + if (nfds > 0) { + int pr = poll(pfds, nfds, (int)PACKET_RADIO_POLL_MS); + + if (pr < 0) { + if (errno != EINTR) { + hybbx_log_warn("[packet_radio] poll failed"); + } + } else if (pr > 0) { + nfds_t j; + + for (j = 0; j < nfds; j++) { + short rev = pfds[j].revents; + + if (rev == 0) { + continue; + } + + if ((rev & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + if (map[j].kind == PR_POLL_CIRCUIT) { + packet_radio_instance_t *inst = + &g_instances[map[j].instance_index]; + + hybbx_log_warn("[packet_radio%u] circuit hangup — " + "reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + } + continue; + } + + if ((rev & POLLIN) == 0) { + continue; + } + + if (map[j].kind == PR_POLL_CIRCUIT) { + instance_drain_circuit(&g_instances[map[j].instance_index]); + } else { + packet_radio_instance_t *inst = + &g_instances[map[j].instance_index]; + + if (inst->tnc != NULL) { + (void)hybbx_tnc_poll(inst->tnc); + } + } + } + } + } else { + packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS); + } +} +#endif + +static void packet_radio_service_maintenance(void) +{ + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + + if (inst->circuit_fd < 0) { + inst->circuit_reconnect_polls++; + if (inst->circuit_reconnect_polls >= 50u) { + inst->circuit_reconnect_polls = 0; + (void)instance_connect_circuit(inst); + } + } + + if (inst->flow_cancel) { + inst->flow_cancel = 0; + inst->flow_paused = 0; + instance_circuit_reconnect(inst); + } + } +} + +#if defined(_WIN32) +static void packet_radio_poll_win32(void) +{ + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + + if (inst->tnc != NULL) { + (void)hybbx_tnc_poll(inst->tnc); + } + + if (inst->circuit_fd >= 0) { + instance_drain_circuit(inst); + } + } +} +#endif + +static void *packet_radio_poll_thread(void *arg) +{ + (void)arg; + + while (g_radio_running) { +#if !defined(_WIN32) + packet_radio_poll_instances(); +#else + packet_radio_poll_win32(); + packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS); +#endif + packet_radio_service_maintenance(); + } + + return NULL; +} + +static hybbx_result_t instance_start(packet_radio_instance_t *inst, + const char *config, + unsigned index) +{ + hybbx_result_t rc; + hybbx_tnc_frame_cb frame_cb = on_tnc_ax25_frame; + hybbx_tnc_ui_cb ui_cb = NULL; + + if (inst == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; + inst->index = index; + + rc = hybbx_packet_radio_config_parse(config, &inst->config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[packet_radio%u] invalid configuration", index + 1); + instance_shutdown(inst); + return rc; + } + + if (g_service != NULL) { + const hybbx_broadcast_config_t *bc = + hybbx_service_get_broadcast(g_service); + + if (bc != NULL && bc->ax25_mycall[0] != '\0') { + size_t n = strlen(bc->ax25_mycall) + 1; + char *mc = malloc(n); + + if (mc != NULL) { + memcpy(mc, bc->ax25_mycall, n); + free(inst->config.mycall); + inst->config.mycall = mc; + hybbx_log_info("[packet_radio%u] ax25 source=%s (from [broadcast] ax25_mycall)", + index + 1, inst->config.mycall); + } + } + } + + hybbx_log_info("[packet_radio%u] tnc=%s protocol=%s device_type=%s device=%s " + "band=%s duplex=%s modulation=%s circuit=%s:%u " + "(HBX over internal TCP)", + index + 1, + tnc_name(inst->config.tnc), + protocol_name(inst->config.protocol), + device_type_name(inst->config.device_type), + inst->config.device, + hybbx_packet_radio_band_name(inst->config.params.band), + hybbx_packet_radio_duplex_name(inst->config.params.duplex), + hybbx_packet_radio_modulation_name(inst->config.params.modulation), + inst->config.circuit_host, + inst->config.circuit_port); + if (inst->config.frequency_mhz != NULL && + inst->config.frequency_mhz[0] != '\0') { + hybbx_log_info("[packet_radio%u] frequency_mhz=%s", index + 1, + inst->config.frequency_mhz); + } + + rc = instance_connect_circuit(inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + frame_cb = NULL; + ui_cb = on_tnc_host_ui; + } + + rc = hybbx_tnc_open(&inst->tnc, &inst->config, frame_cb, ui_cb, inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t packet_radio_init(hybbx_service_t *service) +{ + g_service = service; + g_instance_count = 0; + g_radio_running = 0; + return HYBBX_OK; +} + +static void packet_radio_shutdown(void) +{ + unsigned i; + + g_radio_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 packet_radio_start(const char *config) +{ + char scratch[4096]; + const char *cursor; + hybbx_max25_config_t max25; + hybbx_max25_status_t max25_status; + hybbx_result_t rc = HYBBX_OK; + unsigned started = 0; + unsigned section_num = 0; + unsigned local_edges; + + (void)g_service; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + packet_radio_shutdown(); + g_poll_thread = (pthread_t)0; + + cursor = hybbx_max25_config_skip_prefix(config, &max25); + local_edges = packet_radio_count_local_edges(cursor); + if (local_edges > 0u) { + rc = hybbx_max25_wait_ready(&max25); + if (rc != HYBBX_OK) { + return rc; + } + } + + while (*cursor != '\0' && g_instance_count < HYBBX_PACKET_RADIO_MAX_INSTANCES) { + const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP); + size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor); + + if (len == 0) { + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + if (len >= sizeof(scratch)) { + packet_radio_shutdown(); + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, cursor, len); + scratch[len] = '\0'; + section_num++; + + if (!hybbx_packet_radio_section_is_local_edge(scratch)) { + hybbx_log_info("[packet_radio%u] bridge registry only — no local TNC", + section_num); + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + rc = instance_start(&g_instances[g_instance_count], scratch, + g_instance_count); + if (rc != HYBBX_OK) { + packet_radio_shutdown(); + return rc; + } + + g_instance_count++; + started++; + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + } + + if (started == 0) { + return HYBBX_OK; + } + + g_radio_running = 1; + if (pthread_create(&g_poll_thread, NULL, packet_radio_poll_thread, + NULL) != 0) { + hybbx_log_warn("[packet_radio] poll thread failed"); + packet_radio_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + hybbx_log_info("[packet_radio] %u TNC instance(s) active", started); + return HYBBX_OK; +} + +static hybbx_result_t packet_radio_stop(void) +{ + hybbx_log_info("[packet_radio] stop"); + packet_radio_shutdown(); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_packet_radio = { + .name = "packet_radio", + .kind = HYBBX_TRANSPORT_PACKET_RADIO, + .version = 1, + .init = packet_radio_init, + .shutdown = packet_radio_shutdown, + .start = packet_radio_start, + .stop = packet_radio_stop, + .write = NULL, +}; diff --git a/plugins/packet_radio/packet_radio_config.c b/plugins/packet_radio/packet_radio_config.c new file mode 100644 index 0000000..f8e2d65 --- /dev/null +++ b/plugins/packet_radio/packet_radio_config.c @@ -0,0 +1,786 @@ +#include "hybbx/packet_radio.h" +#include "hybbx/tnc.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +int hybbx_packet_radio_section_is_local_edge(const char *config) +{ + char scratch[64]; + + if (config == NULL) { + return 0; + } + + if (find_kv(config, "circuit_host", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "device", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "tnc", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "protocol", scratch, sizeof(scratch)) != NULL) { + return 1; + } + + return 0; +} + +static hybbx_packet_radio_device_type_t parse_device_type(const char *value) +{ + if (value == NULL) { + return HYBBX_PACKET_RADIO_DEVICE_USB; + } + + if (str_ieq(value, "usb") || str_ieq(value, "ax25") || + str_ieq(value, "ax.25")) { + return HYBBX_PACKET_RADIO_DEVICE_USB; + } + + if (str_ieq(value, "serial") || str_ieq(value, "com") || + str_ieq(value, "rs232") || str_ieq(value, "v24")) { + return HYBBX_PACKET_RADIO_DEVICE_SERIAL; + } + + return HYBBX_PACKET_RADIO_DEVICE_USB; +} + +static const char *default_device_for_type(hybbx_packet_radio_device_type_t type) +{ + if (type == HYBBX_PACKET_RADIO_DEVICE_SERIAL) { + return HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL; + } + return HYBBX_PACKET_RADIO_DEFAULT_DEVICE_USB; +} + +static hybbx_packet_radio_tnc_t parse_tnc(const char *value) +{ + if (value == NULL || str_ieq(value, "tnc2c") || str_ieq(value, "tnc2-c")) { + return HYBBX_PACKET_RADIO_TNC_TNC2C; + } + + if (str_ieq(value, "tnc2") || str_ieq(value, "tnc-2") || + str_ieq(value, "pktnc2") || str_ieq(value, "pk-tnc2") || + str_ieq(value, "tapr") || str_ieq(value, "thefirmware") || + str_ieq(value, "tf")) { + return HYBBX_PACKET_RADIO_TNC_TNC2; + } + + if (str_ieq(value, "pk232") || str_ieq(value, "pk-232") || + str_ieq(value, "aea") || str_ieq(value, "pk87") || + str_ieq(value, "pk-87")) { + return HYBBX_PACKET_RADIO_TNC_PK232; + } + + if (str_ieq(value, "mfj1278") || str_ieq(value, "mfj-1278") || + str_ieq(value, "mfj1278b") || str_ieq(value, "mfj")) { + return HYBBX_PACKET_RADIO_TNC_MFJ1278; + } + + if (str_ieq(value, "kantronics") || str_ieq(value, "kpc") || + str_ieq(value, "kpc3") || str_ieq(value, "kpc9612")) { + return HYBBX_PACKET_RADIO_TNC_KANTRONICS; + } + + if (str_ieq(value, "baycom") || str_ieq(value, "bay-com")) { + return HYBBX_PACKET_RADIO_TNC_BAYCOM; + } + + if (str_ieq(value, "pccom") || str_ieq(value, "pc-com") || + str_ieq(value, "albrecht")) { + return HYBBX_PACKET_RADIO_TNC_PCCOM; + } + + if (str_ieq(value, "generic") || str_ieq(value, "tnc")) { + return HYBBX_PACKET_RADIO_TNC_GENERIC; + } + + return HYBBX_PACKET_RADIO_TNC_TNC2C; +} + +static hybbx_packet_radio_protocol_t parse_protocol(const char *value) +{ + if (value == NULL || str_ieq(value, "kiss")) { + return HYBBX_PACKET_RADIO_PROTO_KISS; + } + + if (str_ieq(value, "hostmode") || str_ieq(value, "host") || + str_ieq(value, "tnc2")) { + return HYBBX_PACKET_RADIO_PROTO_HOSTMODE; + } + + if (str_ieq(value, "6pack") || str_ieq(value, "sixpack")) { + return HYBBX_PACKET_RADIO_PROTO_SIXPACK; + } + + return HYBBX_PACKET_RADIO_PROTO_KISS; +} + +static hybbx_tnc2c_modem_t parse_modem(const char *value) +{ + if (value == NULL || str_ieq(value, "tcm3105") || str_ieq(value, "1200")) { + return HYBBX_TNC2C_MODEM_TCM3105; + } + + if (str_ieq(value, "9600")) { + return HYBBX_TNC2C_MODEM_9600; + } + + if (str_ieq(value, "g3ruh") || str_ieq(value, "g3ruh_fsk") || + str_ieq(value, "fsk9600")) { + return HYBBX_TNC2C_MODEM_9600; + } + + if (str_ieq(value, "19200")) { + return HYBBX_TNC2C_MODEM_19200; + } + + return HYBBX_TNC2C_MODEM_TCM3105; +} + +static float parse_clock_mhz(const char *value) +{ + if (value == NULL) { + return HYBBX_TNC2C_CLOCK_4_9_MHZ; + } + + if (str_ieq(value, "9.8") || str_ieq(value, "9,8")) { + return HYBBX_TNC2C_CLOCK_9_8_MHZ; + } + + if (str_ieq(value, "10") || str_ieq(value, "10.0")) { + return HYBBX_TNC2C_CLOCK_10_MHZ; + } + + return HYBBX_TNC2C_CLOCK_4_9_MHZ; +} + +static unsigned int parse_uint(const char *value, unsigned int default_value) +{ + char *end = NULL; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return default_value; + } + + n = strtoul(value, &end, 10); + if (end == value || n == 0) { + return default_value; + } + + return (unsigned int)n; +} + +static hybbx_packet_radio_band_t parse_radio_band(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_BAND_UNSET; + } + + if (str_ieq(value, "cb") || str_ieq(value, "11m") || + str_ieq(value, "27mhz") || str_ieq(value, "citizen")) { + return HYBBX_PACKET_RADIO_BAND_CB; + } + + if (str_ieq(value, "amateur") || str_ieq(value, "ham") || + str_ieq(value, "amateurfunk") || str_ieq(value, "afu")) { + return HYBBX_PACKET_RADIO_BAND_AMATEUR; + } + + return HYBBX_PACKET_RADIO_BAND_UNSET; +} + +static hybbx_packet_radio_duplex_t parse_radio_duplex(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_DUPLEX_UNSET; + } + + if (str_ieq(value, "full") || str_ieq(value, "full-duplex") || + str_ieq(value, "fullduplex") || str_ieq(value, "fulldup")) { + return HYBBX_PACKET_RADIO_DUPLEX_FULL; + } + + if (str_ieq(value, "half") || str_ieq(value, "half-duplex") || + str_ieq(value, "duplex") || str_ieq(value, "simplex")) { + return HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + return HYBBX_PACKET_RADIO_DUPLEX_UNSET; +} + +static hybbx_packet_radio_modulation_t parse_modulation(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_MOD_UNSET; + } + + if (str_ieq(value, "g3ruh") || str_ieq(value, "g3ruh_fsk") || + str_ieq(value, "fsk9600") || str_ieq(value, "9600fsk")) { + return HYBBX_PACKET_RADIO_MOD_G3RUH_FSK; + } + + if (str_ieq(value, "afsk") || str_ieq(value, "bell202") || + str_ieq(value, "tcm3105") || str_ieq(value, "1200")) { + return HYBBX_PACKET_RADIO_MOD_AFSK; + } + + return HYBBX_PACKET_RADIO_MOD_UNSET; +} + +static hybbx_tnc_serial_parity_t parse_serial_parity(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_TNC_SERIAL_PARITY_UNSET; + } + + if (str_ieq(value, "even") || str_ieq(value, "e")) { + return HYBBX_TNC_SERIAL_PARITY_EVEN; + } + + if (str_ieq(value, "odd") || str_ieq(value, "o")) { + return HYBBX_TNC_SERIAL_PARITY_ODD; + } + + if (str_ieq(value, "none") || str_ieq(value, "n") || str_ieq(value, "off")) { + return HYBBX_TNC_SERIAL_PARITY_NONE; + } + + return HYBBX_TNC_SERIAL_PARITY_UNSET; +} + +static void apply_serial_line(hybbx_packet_radio_config_t *out, const char *value) +{ + if (value == NULL || value[0] == '\0') { + return; + } + + if (str_ieq(value, "7e1")) { + out->params.data_bits = 7; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_EVEN; + out->params.stop_bits = 1; + return; + } + + if (str_ieq(value, "8n1")) { + out->params.data_bits = 8; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_NONE; + out->params.stop_bits = 1; + return; + } + + if (str_ieq(value, "7o1")) { + out->params.data_bits = 7; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_ODD; + out->params.stop_bits = 1; + } +} + +static hybbx_kiss_entry_t parse_kiss_entry(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_KISS_ENTRY_UNSET; + } + + if (str_ieq(value, "none") || str_ieq(value, "skip") || + str_ieq(value, "off") || str_ieq(value, "no")) { + return HYBBX_KISS_ENTRY_NONE; + } + + if (str_ieq(value, "kiss_on") || str_ieq(value, "kiss-on") || + str_ieq(value, "command") || str_ieq(value, "on") || + str_ieq(value, "yes")) { + return HYBBX_KISS_ENTRY_KISS_ON; + } + + if (str_ieq(value, "esc_at_k") || str_ieq(value, "esc@k") || + str_ieq(value, "esc") || str_ieq(value, "@k")) { + return HYBBX_KISS_ENTRY_ESC_AT_K; + } + + if (str_ieq(value, "auto")) { + return HYBBX_KISS_ENTRY_AUTO; + } + + return HYBBX_KISS_ENTRY_UNSET; +} + +static hybbx_kiss_exit_t parse_kiss_exit(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_KISS_EXIT_UNSET; + } + + if (str_ieq(value, "none") || str_ieq(value, "skip") || + str_ieq(value, "off") || str_ieq(value, "no")) { + return HYBBX_KISS_EXIT_NONE; + } + + if (str_ieq(value, "kiss_off") || str_ieq(value, "kiss-off") || + str_ieq(value, "command")) { + return HYBBX_KISS_EXIT_KISS_OFF; + } + + if (str_ieq(value, "kiss_frame") || str_ieq(value, "frame") || + str_ieq(value, "fend")) { + return HYBBX_KISS_EXIT_KISS_FRAME; + } + + if (str_ieq(value, "auto")) { + return HYBBX_KISS_EXIT_AUTO; + } + + return HYBBX_KISS_EXIT_UNSET; +} + +static unsigned int default_radio_baud_for_modem(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 HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } +} + +static hybbx_result_t parse_via_list(hybbx_packet_radio_config_t *out, + const char *value) +{ + char scratch[256]; + char *cursor; + char *token; + + if (value == NULL || value[0] == '\0') { + return HYBBX_OK; + } + + if (strlen(value) >= sizeof(scratch)) { + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, value, strlen(value) + 1); + cursor = scratch; + + while ((token = strtok(cursor, ",")) != NULL && out->via_count < HYBBX_PACKET_RADIO_VIA_MAX) { + char *item = hybbx_strdup(token); + if (item == NULL) { + return HYBBX_ERR_NOMEM; + } + out->via[out->via_count++] = item; + cursor = NULL; + } + + return HYBBX_OK; +} + +int hybbx_packet_radio_baud_valid(unsigned int baud) +{ + return baud > 0; +} + +hybbx_result_t hybbx_packet_radio_config_parse(const char *config, + hybbx_packet_radio_config_t *out) +{ + char scratch[256]; + const char *value; + unsigned long baud; + hybbx_result_t rc; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + out->tnc = HYBBX_PACKET_RADIO_TNC_TNC2C; + out->protocol = 0; /* profile may choose */ + out->params.clock_mhz = HYBBX_TNC2C_CLOCK_4_9_MHZ; + out->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + out->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + out->params.kiss_port = 0; + out->params.txdelay = 50; + out->params.persist = 0; + out->params.slot = 10; + out->params.txtail = 0; + out->params.band = HYBBX_PACKET_RADIO_BAND_UNSET; + out->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_UNSET; + out->params.modulation = HYBBX_PACKET_RADIO_MOD_UNSET; + out->params.kiss_entry = HYBBX_KISS_ENTRY_UNSET; + out->params.kiss_exit = HYBBX_KISS_EXIT_UNSET; + out->params.host_connect_on_start = 0; + out->params.assert_modem_lines = -1; + + value = find_kv(config, "device_type", scratch, sizeof(scratch)); + out->device_type = parse_device_type(value); + + value = find_kv(config, "device", scratch, sizeof(scratch)); + if (value == NULL) { + out->device = hybbx_strdup(default_device_for_type(out->device_type)); + } else { + out->device = hybbx_strdup(value); + } + + if (out->device == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + + value = find_kv(config, "baud", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + out->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } else { + baud = strtoul(value, NULL, 10); + if (!hybbx_packet_radio_baud_valid((unsigned int)baud)) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + out->baud = (unsigned int)baud; + } + + value = find_kv(config, "tnc", scratch, sizeof(scratch)); + out->tnc = parse_tnc(value); + + if (out->tnc == HYBBX_PACKET_RADIO_TNC_BAYCOM || + out->tnc == HYBBX_PACKET_RADIO_TNC_PCCOM) { + hybbx_log_warn("[packet_radio] tnc=%s is not a packet_radio profile — " + "use tnc=tnc2c (or generic) with kiss_entry=none after " + "max25d prep; BayCom/based SER12 = MAX25 max25e0 (bcpr), " + "optional [networks] baycom=yes on non-UN1TME sites only", + hybbx_packet_radio_tnc_name(out->tnc)); + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + + value = find_kv(config, "protocol", scratch, sizeof(scratch)); + if (value != NULL) { + out->protocol = parse_protocol(value); + } + + value = find_kv(config, "clock_mhz", scratch, sizeof(scratch)); + out->params.clock_mhz = parse_clock_mhz(value); + + value = find_kv(config, "modem", scratch, sizeof(scratch)); + out->params.modem = parse_modem(value); + + value = find_kv(config, "radio_baud", scratch, sizeof(scratch)); + if (value == NULL) { + out->params.radio_baud = default_radio_baud_for_modem(out->params.modem); + } else { + unsigned int radio_baud = parse_uint(value, 0); + if (!hybbx_packet_radio_baud_valid(radio_baud)) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + out->params.radio_baud = radio_baud; + } + + value = find_kv(config, "kiss_port", scratch, sizeof(scratch)); + out->params.kiss_port = parse_uint(value, 0); + + value = find_kv(config, "txdelay", scratch, sizeof(scratch)); + out->params.txdelay = parse_uint(value, out->params.txdelay); + + value = find_kv(config, "persist", scratch, sizeof(scratch)); + out->params.persist = parse_uint(value, out->params.persist); + + value = find_kv(config, "slot", scratch, sizeof(scratch)); + out->params.slot = parse_uint(value, out->params.slot); + + value = find_kv(config, "txtail", scratch, sizeof(scratch)); + out->params.txtail = parse_uint(value, out->params.txtail); + + value = find_kv(config, "radio_band", scratch, sizeof(scratch)); + if (value != NULL) { + out->params.band = parse_radio_band(value); + } + + value = find_kv(config, "radio_duplex", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "duplex", scratch, sizeof(scratch)); + } + if (value != NULL) { + out->params.duplex = parse_radio_duplex(value); + } + + value = find_kv(config, "fullduplex", scratch, sizeof(scratch)); + if (value != NULL && + out->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + out->params.duplex = hybbx_parse_bool(value, 0) ? + HYBBX_PACKET_RADIO_DUPLEX_FULL : + HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + value = find_kv(config, "g3ruh_fsk", scratch, sizeof(scratch)); + if (value != NULL) { + out->params.modulation = hybbx_parse_bool(value, 0) ? + HYBBX_PACKET_RADIO_MOD_G3RUH_FSK : + HYBBX_PACKET_RADIO_MOD_AFSK; + } + + value = find_kv(config, "modulation", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_packet_radio_modulation_t mod = parse_modulation(value); + if (mod != HYBBX_PACKET_RADIO_MOD_UNSET) { + out->params.modulation = mod; + } + } + + value = find_kv(config, "kiss_entry", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_kiss_entry_t kiss_entry = parse_kiss_entry(value); + if (kiss_entry != HYBBX_KISS_ENTRY_UNSET) { + out->params.kiss_entry = kiss_entry; + } + } + + value = find_kv(config, "kiss_exit", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_kiss_exit_t kiss_exit = parse_kiss_exit(value); + if (kiss_exit != HYBBX_KISS_EXIT_UNSET) { + out->params.kiss_exit = kiss_exit; + } + } + + value = find_kv(config, "host_connect", scratch, sizeof(scratch)); + out->params.host_connect_on_start = hybbx_parse_bool(value, + out->params.host_connect_on_start); + + value = find_kv(config, "serial_line", scratch, sizeof(scratch)); + if (value != NULL) { + apply_serial_line(out, value); + } + + value = find_kv(config, "data_bits", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned int data_bits = parse_uint(value, 0); + if (data_bits == 7 || data_bits == 8) { + out->params.data_bits = data_bits; + } + } + + value = find_kv(config, "parity", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_tnc_serial_parity_t parity = parse_serial_parity(value); + if (parity != HYBBX_TNC_SERIAL_PARITY_UNSET) { + out->params.serial_parity = parity; + } + } + + value = find_kv(config, "stop_bits", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned int stop_bits = parse_uint(value, 0); + if (stop_bits == 1 || stop_bits == 2) { + out->params.stop_bits = stop_bits; + } + } + + value = find_kv(config, "assert_modem_lines", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "rts_dtr", scratch, sizeof(scratch)); + } + if (value != NULL) { + out->params.assert_modem_lines = hybbx_parse_bool(value, 0) ? 1 : 0; + } + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->mycall = hybbx_strdup(value); + if (out->mycall == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "dest", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "dest_call", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->dest_call = hybbx_strdup(value); + if (out->dest_call == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "via", scratch, sizeof(scratch)); + rc = parse_via_list(out, value); + if (rc != HYBBX_OK) { + hybbx_packet_radio_config_free(out); + return rc; + } + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->circuit_host = hybbx_strdup(value); + if (out->circuit_host == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "circuit_port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->circuit_port = parse_uint(value, HYBBX_CIRCUIT_DEFAULT_PORT); + } + + if (out->circuit_port == 0) { + out->circuit_port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + if (out->circuit_host == NULL) { + out->circuit_host = hybbx_strdup("127.0.0.1"); + if (out->circuit_host == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_id = hybbx_strdup(value); + if (out->link_id == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = hybbx_strdup(value); + if (out->link_password == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_role = hybbx_strdup(value); + if (out->link_role == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = hybbx_strdup(value); + if (out->frequency_mhz == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + (void)hybbx_tnc_profile_apply_defaults(out); + return HYBBX_OK; +} + +void hybbx_packet_radio_config_free(hybbx_packet_radio_config_t *config) +{ + unsigned i; + + if (config == NULL) { + return; + } + + free(config->device); + free(config->mycall); + free(config->dest_call); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + free(config->frequency_mhz); + for (i = 0; i < config->via_count; i++) { + free(config->via[i]); + config->via[i] = NULL; + } + + memset(config, 0, sizeof(*config)); +} diff --git a/plugins/packet_radio/serial_port.c b/plugins/packet_radio/serial_port.c new file mode 100644 index 0000000..70dd92c --- /dev/null +++ b/plugins/packet_radio/serial_port.c @@ -0,0 +1,705 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "serial_port.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include +#include +#include + +void hybbx_serial_params_default(hybbx_serial_params_t *params, + unsigned int baud) +{ + if (params == NULL) { + return; + } + + memset(params, 0, sizeof(*params)); + params->baud = baud; + params->data_bits = 8; + params->parity = HYBBX_SERIAL_PARITY_NONE; + params->stop_bits = 1; + params->assert_modem_lines = 0; +} + +#if defined(_WIN32) + +#define WIN32_LEAN_AND_MEAN +#include + +struct hybbx_serial_port { + HANDLE handle; + unsigned int baud; +}; + +static int win32_normalize_device(const char *device, char *out, size_t out_len) +{ + const char *name; + unsigned com_num = 0; + + if (device == NULL || out_len == 0) { + return 0; + } + + if (strncmp(device, "\\\\.\\", 4) == 0) { + return hybbx_strlcpy(out, device, out_len) < out_len; + } + + name = device; + if (name[0] == '/' && strncmp(name, "/dev/ttyS", 9) == 0 && + name[9] >= '0' && name[9] <= '9' && name[10] == '\0') { + com_num = (unsigned)(name[9] - '0') + 1u; + return (size_t)snprintf(out, out_len, "\\\\.\\COM%u", com_num) < out_len; + } + + if ((name[0] == 'C' || name[0] == 'c') && + (name[1] == 'O' || name[1] == 'o') && + (name[2] == 'M' || name[2] == 'm')) { + name += 3; + } + + if (*name == '\0') { + return 0; + } + + while (*name >= '0' && *name <= '9') { + com_num = com_num * 10u + (unsigned)(*name - '0'); + name++; + } + if (*name != '\0' || com_num == 0) { + return 0; + } + + return (size_t)snprintf(out, out_len, "\\\\.\\COM%u", com_num) < out_len; +} + +static hybbx_result_t win32_configure(HANDLE handle, + const hybbx_serial_params_t *params) +{ + DCB dcb; + COMMTIMEOUTS timeouts; + + memset(&dcb, 0, sizeof(dcb)); + dcb.DCBlength = sizeof(dcb); + if (!GetCommState(handle, &dcb)) { + return HYBBX_ERR_IO; + } + + dcb.BaudRate = (DWORD)params->baud; + dcb.ByteSize = (BYTE)(params->data_bits == 7 ? 7 : 8); + switch (params->parity) { + case HYBBX_SERIAL_PARITY_EVEN: + dcb.Parity = EVENPARITY; + dcb.fParity = TRUE; + break; + case HYBBX_SERIAL_PARITY_ODD: + dcb.Parity = ODDPARITY; + dcb.fParity = TRUE; + break; + case HYBBX_SERIAL_PARITY_NONE: + default: + dcb.Parity = NOPARITY; + dcb.fParity = FALSE; + break; + } + dcb.StopBits = params->stop_bits == 2 ? TWOSTOPBITS : ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = params->assert_modem_lines ? + DTR_CONTROL_ENABLE : DTR_CONTROL_DISABLE; + dcb.fRtsControl = params->assert_modem_lines ? + RTS_CONTROL_ENABLE : RTS_CONTROL_DISABLE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fNull = FALSE; + + if (!SetCommState(handle, &dcb)) { + return HYBBX_ERR_IO; + } + + memset(&timeouts, 0, sizeof(timeouts)); + timeouts.ReadIntervalTimeout = MAXDWORD; + timeouts.ReadTotalTimeoutConstant = 0; + timeouts.ReadTotalTimeoutMultiplier = 0; + timeouts.WriteTotalTimeoutConstant = 0; + timeouts.WriteTotalTimeoutMultiplier = 0; + if (!SetCommTimeouts(handle, &timeouts)) { + return HYBBX_ERR_IO; + } + + PurgeComm(handle, PURGE_RXCLEAR | PURGE_TXCLEAR); + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + char path[64]; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + if (!win32_normalize_device(device, path, sizeof(path))) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + port->handle = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (port->handle == INVALID_HANDLE_VALUE) { + free(port); + return HYBBX_ERR_IO; + } + + rc = win32_configure(port->handle, params); + if (rc != HYBBX_OK) { + CloseHandle(port->handle); + free(port); + return rc; + } + + port->baud = params->baud; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->handle != INVALID_HANDLE_VALUE) { + CloseHandle(port->handle); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + size_t written = 0; + + if (port == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + while (written < len) { + DWORD chunk = 0; + + if (!WriteFile(port->handle, data + written, + (DWORD)(len - written), &chunk, NULL)) { + return HYBBX_ERR_IO; + } + if (chunk == 0) { + return HYBBX_ERR_IO; + } + written += (size_t)chunk; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + DWORD n = 0; + + if (port == NULL || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!ReadFile(port->handle, buf, (DWORD)buf_len, &n, NULL)) { + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + (void)port; + return -1; +} + +#elif defined(__AMIGA__) + +#include +#include + +struct hybbx_serial_port { + struct IOExtSer *io; + struct MsgPort *port; + char device_name[32]; + unsigned long unit; + unsigned int baud; +}; + +static int amiga_parse_device(const char *device, char *name, size_t name_len, + unsigned long *unit) +{ + const char *colon; + char *end; + + if (unit == NULL || name == NULL || name_len == 0) { + return 0; + } + + *unit = 0; + if (device == NULL || device[0] == '\0') { + hybbx_strlcpy(name, SERIALNAME, name_len); + return 1; + } + + colon = strchr(device, ':'); + if (colon != NULL) { + size_t prefix = (size_t)(colon - device); + + if (prefix == 0 || prefix >= name_len) { + return 0; + } + memcpy(name, device, prefix); + name[prefix] = '\0'; + *unit = strtoul(colon + 1, &end, 10); + return end != colon + 1 && *end == '\0'; + } + + if (device[0] >= '0' && device[0] <= '9') { + *unit = strtoul(device, &end, 10); + if (end != device && *end == '\0') { + hybbx_strlcpy(name, SERIALNAME, name_len); + return 1; + } + } + + hybbx_strlcpy(name, device, name_len); + return 1; +} + +static hybbx_result_t amiga_configure(struct IOExtSer *io, + const hybbx_serial_params_t *params) +{ + io->IOSer.io_Command = SDCMD_SETPARAMS; + io->io_Baud = (ULONG)params->baud; + io->io_ReadLen = (UBYTE)(params->data_bits == 7 ? 7 : 8); + io->io_WriteLen = (UBYTE)(params->data_bits == 7 ? 7 : 8); + io->io_StopBits = (UBYTE)(params->stop_bits == 2 ? 2 : 1); + io->io_SerFlags = (UBYTE)(io->io_SerFlags & (UBYTE)~SERF_PARTY_ON); + if (params->parity == HYBBX_SERIAL_PARITY_EVEN || + params->parity == HYBBX_SERIAL_PARITY_ODD) { + io->io_SerFlags |= SERF_PARTY_ON; + } + io->io_SerFlags |= SERF_XDISABLED; + + if (DoIO((struct IORequest *)io) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + if (!amiga_parse_device(device, port->device_name, sizeof(port->device_name), + &port->unit)) { + free(port); + return HYBBX_ERR_INVALID; + } + + port->port = CreatePort(NULL, 0); + if (port->port == NULL) { + free(port); + return HYBBX_ERR_NOMEM; + } + + port->io = (struct IOExtSer *)CreateExtIO(port->port, + (LONG)sizeof(*port->io)); + if (port->io == NULL) { + DeletePort(port->port); + free(port); + return HYBBX_ERR_NOMEM; + } + + port->io->io_SerFlags = SERF_SHARED | SERF_XDISABLED; + if (OpenDevice(port->device_name, port->unit, + (struct IORequest *)port->io, 0) != 0) { + DeleteExtIO((struct IORequest *)port->io); + DeletePort(port->port); + free(port); + return HYBBX_ERR_IO; + } + + rc = amiga_configure(port->io, params); + if (rc != HYBBX_OK) { + CloseDevice((struct IORequest *)port->io); + DeleteExtIO((struct IORequest *)port->io); + DeletePort(port->port); + free(port); + return rc; + } + + port->baud = params->baud; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->io != NULL) { + CloseDevice((struct IORequest *)port->io); + DeleteExtIO((struct IORequest *)port->io); + } + if (port->port != NULL) { + DeletePort(port->port); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + if (port == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + port->io->IOSer.io_Command = CMD_WRITE; + port->io->IOSer.io_Data = (APTR)data; + port->io->IOSer.io_Length = (ULONG)len; + port->io->IOSer.io_Flags = 0; + + if (DoIO((struct IORequest *)port->io) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + if (port == NULL || buf == NULL || read_len == NULL || buf_len == 0) { + return HYBBX_ERR_INVALID; + } + + port->io->IOSer.io_Command = CMD_READ; + port->io->IOSer.io_Data = (APTR)buf; + port->io->IOSer.io_Length = (ULONG)buf_len; + port->io->IOSer.io_Flags = IOF_QUICK; + + if (DoIO((struct IORequest *)port->io) != 0) { + *read_len = 0; + return HYBBX_OK; + } + + *read_len = (size_t)port->io->IOSer.io_Actual; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + (void)port; + return -1; +} + +#else /* POSIX */ + +#include +#include +#include +#include +#include +#include + +struct hybbx_serial_port { + int fd; + unsigned int baud; + int assert_modem_lines; +}; + +#ifndef cfmakeraw +static void hybbx_cfmakeraw(struct termios *tty) +{ + tty->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | + IGNCR | ICRNL | IXON); + tty->c_oflag &= ~OPOST; + tty->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tty->c_cflag &= ~(CSIZE | PARENB); + tty->c_cflag |= CS8; +} +#define cfmakeraw hybbx_cfmakeraw +#endif + +static hybbx_result_t set_baud_rate(struct termios *tty, unsigned int baud) +{ +#if defined(__linux__) || defined(__GLIBC__) + if (cfsetspeed(tty, (speed_t)baud) == 0) { + return HYBBX_OK; + } +#endif + + { + speed_t speed = B0; + switch (baud) { + case 50: speed = B50; break; + case 75: speed = B75; break; + case 110: speed = B110; break; + case 134: speed = B134; break; + case 150: speed = B150; break; + case 200: speed = B200; break; + case 300: speed = B300; break; + case 600: speed = B600; break; + case 1200: speed = B1200; break; + case 1800: speed = B1800; break; + case 2400: speed = B2400; break; + case 4800: speed = B4800; break; + case 9600: speed = B9600; break; +#ifdef B19200 + case 19200: speed = B19200; break; +#endif +#ifdef B38400 + case 38400: speed = B38400; break; +#endif +#ifdef B57600 + case 57600: speed = B57600; break; +#endif +#ifdef B115200 + case 115200: speed = B115200; break; +#endif + default: + return HYBBX_ERR_UNSUPPORTED; + } + + cfsetispeed(tty, speed); + cfsetospeed(tty, speed); + } + + return HYBBX_OK; +} + +static hybbx_result_t posix_assert_modem_lines(int fd, int on) +{ +#ifdef TIOCMGET + int flags; + + /* BayCom KISS PTY (max25-bcpr): assert_modem_lines=0 — no UART modem bits. + * TIOCMGET on /dev/pts/* often fails; skip when not asserting. */ + if (!on) { + return HYBBX_OK; + } + + if (ioctl(fd, TIOCMGET, &flags) != 0) { + return HYBBX_ERR_IO; + } + flags |= TIOCM_RTS | TIOCM_DTR; + if (ioctl(fd, TIOCMSET, &flags) != 0) { + return HYBBX_ERR_IO; + } +#else + (void)fd; + (void)on; +#endif + return HYBBX_OK; +} + +static hybbx_result_t configure_port(int fd, const hybbx_serial_params_t *params) +{ + struct termios tty; + tcflag_t databits; + + if (tcgetattr(fd, &tty) != 0) { + return HYBBX_ERR_IO; + } + + cfmakeraw(&tty); + tty.c_cflag |= (CLOCAL | CREAD); + tty.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB); + if (params->data_bits == 7) { + databits = CS7; + } else { + databits = CS8; + } + tty.c_cflag |= databits; + if (params->parity == HYBBX_SERIAL_PARITY_EVEN) { + tty.c_cflag |= PARENB; + } else if (params->parity == HYBBX_SERIAL_PARITY_ODD) { + tty.c_cflag |= PARENB | PARODD; + } + if (params->stop_bits == 2) { + tty.c_cflag |= CSTOPB; + } + tty.c_cc[VMIN] = 0; + tty.c_cc[VTIME] = 1; + + if (set_baud_rate(&tty, params->baud) != HYBBX_OK) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (tcsetattr(fd, TCSANOW, &tty) != 0) { + return HYBBX_ERR_IO; + } + + tcflush(fd, TCIOFLUSH); + return posix_assert_modem_lines(fd, params->assert_modem_lines); +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + port->fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (port->fd < 0) { + hybbx_log_warn("[serial] open %s failed: %s", device, strerror(errno)); + free(port); + return HYBBX_ERR_IO; + } + + rc = configure_port(port->fd, params); + if (rc != HYBBX_OK) { + close(port->fd); + free(port); + return rc; + } + + port->baud = params->baud; + port->assert_modem_lines = params->assert_modem_lines; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->fd >= 0) { + close(port->fd); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + size_t written = 0; + + if (port == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (port->assert_modem_lines) { + hybbx_result_t rc = posix_assert_modem_lines(port->fd, 1); + + if (rc != HYBBX_OK) { + return rc; + } + } + + while (written < len) { + ssize_t n = write(port->fd, data + written, len - written); + + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + written += (size_t)n; + } + + if (tcdrain(port->fd) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + ssize_t n; + + if (port == NULL || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + n = read(port->fd, buf, buf_len); + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + *read_len = 0; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + if (port == NULL) { + return -1; + } + + return port->fd; +} + +#endif diff --git a/plugins/packet_radio/serial_port.h b/plugins/packet_radio/serial_port.h new file mode 100644 index 0000000..549d3c5 --- /dev/null +++ b/plugins/packet_radio/serial_port.h @@ -0,0 +1,52 @@ +#ifndef HYBBX_SERIAL_PORT_H +#define HYBBX_SERIAL_PORT_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_serial_port hybbx_serial_port_t; + +typedef enum hybbx_serial_parity { + HYBBX_SERIAL_PARITY_NONE = 0, + HYBBX_SERIAL_PARITY_EVEN = 1, + HYBBX_SERIAL_PARITY_ODD = 2 +} hybbx_serial_parity_t; + +typedef struct hybbx_serial_params { + unsigned int baud; + unsigned int data_bits; + hybbx_serial_parity_t parity; + unsigned int stop_bits; + int assert_modem_lines; +} hybbx_serial_params_t; + +void hybbx_serial_params_default(hybbx_serial_params_t *params, + unsigned int baud); + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params); + +void hybbx_serial_close(hybbx_serial_port_t *port); + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len); + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len); + +/** POSIX serial fd for poll/select; returns -1 when unavailable. */ +int hybbx_serial_fd(const hybbx_serial_port_t *port); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SERIAL_PORT_H */ diff --git a/plugins/packet_radio/sixpack.c b/plugins/packet_radio/sixpack.c new file mode 100644 index 0000000..28c0510 --- /dev/null +++ b/plugins/packet_radio/sixpack.c @@ -0,0 +1,146 @@ +#include "sixpack.h" + +#include + +void hybbx_sixpack_decoder_init(hybbx_sixpack_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void sixpack_reset(hybbx_sixpack_decoder_t *dec) +{ + dec->len = 0; + dec->count = 0; + dec->checksum = 0; + dec->in_frame = 0; +} + +static void sixpack_deliver(hybbx_sixpack_decoder_t *dec, + hybbx_sixpack_frame_cb cb, void *userdata) +{ + if (cb != NULL && dec->len > 0) { + cb(dec->buf, dec->len, userdata); + } + + sixpack_reset(dec); +} + +void hybbx_sixpack_decoder_feed(hybbx_sixpack_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_sixpack_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + uint8_t raw = data[i]; + uint8_t byte = raw; + + if (!dec->in_frame) { + if (raw == HYBBX_SIXPACK_LEAD) { + dec->in_frame = 1; + dec->len = 0; + dec->count = 0; + dec->checksum = 0; + } + continue; + } + + if (dec->count == 0) { + dec->count = raw; + dec->checksum = raw; + continue; + } + + dec->count--; + dec->checksum = (uint8_t)(dec->checksum + raw); + + if (raw < 0x30u) { + byte = (uint8_t)(raw + 0x40u); + } + + if (dec->count > 0) { + if (dec->len >= HYBBX_KISS_MAX_FRAME) { + sixpack_reset(dec); + continue; + } + dec->buf[dec->len++] = byte; + continue; + } + + if (dec->checksum == 0) { + sixpack_deliver(dec, cb, userdata); + } else { + sixpack_reset(dec); + } + } +} + +static size_t sixpack_write_encoded(uint8_t byte, uint8_t *out, size_t out_cap, + size_t pos) +{ + if (byte < 0x30u) { + if (pos + 1 > out_cap) { + return 0; + } + out[pos++] = (uint8_t)(byte + 0x40u); + return pos; + } + + if (pos + 1 > out_cap) { + return 0; + } + + out[pos++] = byte; + return pos; +} + +size_t hybbx_sixpack_encode(const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap) +{ + uint8_t scratch[HYBBX_KISS_MAX_FRAME + 4]; + size_t enc_len = 0; + size_t i; + uint8_t count; + uint8_t sum; + size_t pos = 0; + + if (frame == NULL || out == NULL || frame_len == 0 || + frame_len > HYBBX_KISS_MAX_FRAME) { + return 0; + } + + for (i = 0; i < frame_len; i++) { + enc_len = sixpack_write_encoded(frame[i], scratch, sizeof(scratch), + enc_len); + if (enc_len == 0) { + return 0; + } + } + + if (enc_len + 4 > out_cap || enc_len + 1 > 255) { + return 0; + } + + count = (uint8_t)(enc_len + 1); + sum = count; + for (i = 0; i < enc_len; i++) { + sum = (uint8_t)(sum + scratch[i]); + } + + out[pos++] = HYBBX_SIXPACK_LEAD; + out[pos++] = count; + for (i = 0; i < enc_len; i++) { + out[pos++] = scratch[i]; + } + out[pos++] = (uint8_t)(0x100u - (unsigned)sum); + + return pos; +} diff --git a/plugins/packet_radio/sixpack.h b/plugins/packet_radio/sixpack.h new file mode 100644 index 0000000..0d60e87 --- /dev/null +++ b/plugins/packet_radio/sixpack.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_SIXPACK_H +#define HYBBX_SIXPACK_H + +#include "hybbx/kiss.h" +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_SIXPACK_LEAD 0x53u /* 'S' */ + +typedef void (*hybbx_sixpack_frame_cb)(const uint8_t *frame, size_t len, + void *userdata); + +typedef struct hybbx_sixpack_decoder { + uint8_t buf[HYBBX_KISS_MAX_FRAME]; + size_t len; + int in_frame; + uint8_t count; + uint8_t checksum; +} hybbx_sixpack_decoder_t; + +void hybbx_sixpack_decoder_init(hybbx_sixpack_decoder_t *dec); + +void hybbx_sixpack_decoder_feed(hybbx_sixpack_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_sixpack_frame_cb cb, void *userdata); + +size_t hybbx_sixpack_encode(const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SIXPACK_H */ 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; +} diff --git a/plugins/packet_radio/tnc_host.c b/plugins/packet_radio/tnc_host.c new file mode 100644 index 0000000..8700e61 --- /dev/null +++ b/plugins/packet_radio/tnc_host.c @@ -0,0 +1,143 @@ +#include "tnc_host.h" + +#include +#include + +void hybbx_tnc_host_init(hybbx_tnc_host_t *host, + hybbx_tnc_host_text_cb text_cb, + hybbx_tnc_host_event_cb event_cb, + void *userdata) +{ + if (host == NULL) { + return; + } + + memset(host, 0, sizeof(*host)); + host->state = HYBBX_TNC_HOST_BOOT; + host->text_cb = text_cb; + host->event_cb = event_cb; + host->userdata = userdata; +} + +void hybbx_tnc_host_reset(hybbx_tnc_host_t *host) +{ + if (host == NULL) { + return; + } + + host->line_len = 0; + host->line[0] = '\0'; + host->state = HYBBX_TNC_HOST_BOOT; +} + +static int host_line_ieq(const char *line, const char *prefix) +{ + size_t i; + size_t plen = strlen(prefix); + + for (i = 0; i < plen; i++) { + char a = (char)tolower((unsigned char)line[i]); + char b = (char)tolower((unsigned char)prefix[i]); + + if (a != b) { + return 0; + } + } + + return 1; +} + +static void host_emit_event(hybbx_tnc_host_t *host, hybbx_tnc_host_state_t state) +{ + if (host->state == state) { + return; + } + + host->state = state; + if (host->event_cb != NULL) { + host->event_cb(state, host->userdata); + } +} + +static void host_process_line(hybbx_tnc_host_t *host) +{ + const char *line = host->line; + + if (host_line_ieq(line, "cmd:") || host_line_ieq(line, "cmd: ")) { + host_emit_event(host, HYBBX_TNC_HOST_CMD); + return; + } + + if (strstr(line, "***") != NULL && + (host_line_ieq(line, "*** connected") || + host_line_ieq(line, "*** connected ") || + strstr(line, "CONNECTED") != NULL)) { + host_emit_event(host, HYBBX_TNC_HOST_CONNECTED); + return; + } + + if (strstr(line, "***") != NULL && + (host_line_ieq(line, "*** disconnected") || + strstr(line, "DISCONNECTED") != NULL)) { + host_emit_event(host, HYBBX_TNC_HOST_CMD); + return; + } + + if (host->state == HYBBX_TNC_HOST_CONNECTED && host->text_cb != NULL && + line[0] != '\0') { + host->text_cb((const uint8_t *)line, strlen(line), host->userdata); + host->text_cb((const uint8_t *)"\r\n", 2, host->userdata); + } +} + +static void host_push_byte(hybbx_tnc_host_t *host, uint8_t byte) +{ + if (byte == '\r' || byte == '\n') { + if (host->line_len > 0) { + host->line[host->line_len] = '\0'; + host_process_line(host); + host->line_len = 0; + } + return; + } + + if (host->line_len + 1 >= sizeof(host->line)) { + host->line_len = 0; + return; + } + + host->line[host->line_len++] = (char)byte; +} + +void hybbx_tnc_host_feed(hybbx_tnc_host_t *host, + const uint8_t *data, size_t len) +{ + size_t i; + + if (host == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + host_push_byte(host, data[i]); + } +} + +size_t hybbx_tnc_host_format_cmd(const char *cmd, char *out, size_t out_cap) +{ + size_t len; + + if (cmd == NULL || out == NULL || out_cap < 3) { + return 0; + } + + len = strlen(cmd); + if (len + 2 >= out_cap) { + return 0; + } + + memcpy(out, cmd, len); + out[len++] = '\r'; + out[len] = '\0'; + return len; +} diff --git a/plugins/packet_radio/tnc_host.h b/plugins/packet_radio/tnc_host.h new file mode 100644 index 0000000..af55505 --- /dev/null +++ b/plugins/packet_radio/tnc_host.h @@ -0,0 +1,51 @@ +#ifndef HYBBX_TNC_HOST_H +#define HYBBX_TNC_HOST_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum hybbx_tnc_host_state { + HYBBX_TNC_HOST_BOOT = 0, + HYBBX_TNC_HOST_CMD = 1, + HYBBX_TNC_HOST_CONNECTED = 2 +} hybbx_tnc_host_state_t; + +typedef void (*hybbx_tnc_host_text_cb)(const uint8_t *data, size_t len, + void *userdata); + +typedef void (*hybbx_tnc_host_event_cb)(hybbx_tnc_host_state_t state, + void *userdata); + +typedef struct hybbx_tnc_host { + hybbx_tnc_host_state_t state; + char line[256]; + size_t line_len; + hybbx_tnc_host_text_cb text_cb; + hybbx_tnc_host_event_cb event_cb; + void *userdata; +} hybbx_tnc_host_t; + +void hybbx_tnc_host_init(hybbx_tnc_host_t *host, + hybbx_tnc_host_text_cb text_cb, + hybbx_tnc_host_event_cb event_cb, + void *userdata); + +void hybbx_tnc_host_reset(hybbx_tnc_host_t *host); + +void hybbx_tnc_host_feed(hybbx_tnc_host_t *host, + const uint8_t *data, size_t len); + +/** Build a TNC2 command line (adds CR). Returns length or 0. */ +size_t hybbx_tnc_host_format_cmd(const char *cmd, char *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TNC_HOST_H */ diff --git a/plugins/packet_radio/tnc_profiles.c b/plugins/packet_radio/tnc_profiles.c new file mode 100644 index 0000000..05a2397 --- /dev/null +++ b/plugins/packet_radio/tnc_profiles.c @@ -0,0 +1,399 @@ +#include "hybbx/tnc.h" +#include "hybbx/log.h" + +#include + +const char *hybbx_packet_radio_duplex_name(hybbx_packet_radio_duplex_t duplex) +{ + switch (duplex) { + case HYBBX_PACKET_RADIO_DUPLEX_FULL: + return "full"; + case HYBBX_PACKET_RADIO_DUPLEX_HALF: + return "half"; + case HYBBX_PACKET_RADIO_DUPLEX_UNSET: + default: + return "unset"; + } +} + +const char *hybbx_packet_radio_band_name(hybbx_packet_radio_band_t band) +{ + switch (band) { + case HYBBX_PACKET_RADIO_BAND_CB: + return "cb"; + case HYBBX_PACKET_RADIO_BAND_AMATEUR: + return "amateur"; + case HYBBX_PACKET_RADIO_BAND_UNSET: + default: + return "unset"; + } +} + +const char *hybbx_packet_radio_modulation_name( + hybbx_packet_radio_modulation_t modulation) +{ + switch (modulation) { + case HYBBX_PACKET_RADIO_MOD_G3RUH_FSK: + return "g3ruh_fsk"; + case HYBBX_PACKET_RADIO_MOD_AFSK: + return "afsk"; + case HYBBX_PACKET_RADIO_MOD_UNSET: + default: + return "unset"; + } +} + +int hybbx_packet_radio_modulation_is_g3ruh( + hybbx_packet_radio_modulation_t modulation) +{ + return modulation == HYBBX_PACKET_RADIO_MOD_G3RUH_FSK; +} + +hybbx_result_t hybbx_tnc_finalize_modulation(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.modulation == HYBBX_PACKET_RADIO_MOD_UNSET) { + cfg->params.modulation = HYBBX_PACKET_RADIO_MOD_AFSK; + } + + if (cfg->params.modulation == HYBBX_PACKET_RADIO_MOD_G3RUH_FSK) { + cfg->params.modem = HYBBX_TNC2C_MODEM_9600; + cfg->params.radio_baud = HYBBX_G3RUH_RADIO_BAUD; + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C && + cfg->params.clock_mhz <= HYBBX_TNC2C_CLOCK_4_9_MHZ) { + cfg->params.clock_mhz = HYBBX_TNC2C_CLOCK_9_8_MHZ; + } + cfg->params.txdelay = 15; + cfg->params.persist = 128; + cfg->params.slot = 3; + return HYBBX_OK; + } + + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_AFSK_RADIO_BAUD; + } + if (cfg->params.modem == 0) { + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + } + + return HYBBX_OK; +} + +static hybbx_packet_radio_band_t default_band_for_tnc(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_PCCOM: + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return HYBBX_PACKET_RADIO_BAND_CB; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + case HYBBX_PACKET_RADIO_TNC_TNC2: + case HYBBX_PACKET_RADIO_TNC_PK232: + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + case HYBBX_PACKET_RADIO_TNC_GENERIC: + default: + return HYBBX_PACKET_RADIO_BAND_AMATEUR; + } +} + +const char *hybbx_packet_radio_tnc_name(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return "baycom"; + case HYBBX_PACKET_RADIO_TNC_PCCOM: + return "pccom"; + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return "generic"; + case HYBBX_PACKET_RADIO_TNC_TNC2: + return "tnc2"; + case HYBBX_PACKET_RADIO_TNC_PK232: + return "pk232"; + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + return "mfj1278"; + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + return "kantronics"; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + return "tnc2c"; + } +} + +const char *hybbx_kiss_entry_name(hybbx_kiss_entry_t entry) +{ + switch (entry) { + case HYBBX_KISS_ENTRY_NONE: + return "none"; + case HYBBX_KISS_ENTRY_KISS_ON: + return "kiss_on"; + case HYBBX_KISS_ENTRY_ESC_AT_K: + return "esc_at_k"; + case HYBBX_KISS_ENTRY_AUTO: + return "auto"; + case HYBBX_KISS_ENTRY_UNSET: + default: + return "unset"; + } +} + +static hybbx_kiss_entry_t default_kiss_entry_for_tnc(hybbx_packet_radio_tnc_t tnc) +{ + (void)tnc; + return HYBBX_KISS_ENTRY_NONE; +} + +hybbx_result_t hybbx_tnc_finalize_kiss_entry(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.kiss_entry == HYBBX_KISS_ENTRY_UNSET) { + cfg->params.kiss_entry = default_kiss_entry_for_tnc(cfg->tnc); + } + + if (cfg->params.kiss_exit == HYBBX_KISS_EXIT_UNSET) { + if (cfg->params.kiss_entry == HYBBX_KISS_ENTRY_NONE) { + cfg->params.kiss_exit = HYBBX_KISS_EXIT_NONE; + } else { + cfg->params.kiss_exit = HYBBX_KISS_EXIT_AUTO; + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_radio_duplex(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = default_band_for_tnc(cfg->tnc); + } + + if (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB && + cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) { + hybbx_log_warn("[tnc] radio_band=cb allows half-duplex only; " + "ignoring radio_duplex=full"); + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + cfg->params.fullduplex = + (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) ? 1 : 0; + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_csma(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.persist == 0) { + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB) { + cfg->params.persist = 255; + } else { + cfg->params.persist = 63; + } + } else if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB && + cfg->params.persist < 255) { + hybbx_log_warn("[tnc] CB band persist=%u overridden to 255 for beacon TX", + cfg->params.persist); + cfg->params.persist = 255; + } + + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 50; + } + + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_serial_line(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.data_bits == 0) { + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C || + cfg->tnc == HYBBX_PACKET_RADIO_TNC_MFJ1278) { + cfg->params.data_bits = HYBBX_TNC2C_DATA_BITS; + } else { + cfg->params.data_bits = 8; + } + } + + if (cfg->params.serial_parity == HYBBX_TNC_SERIAL_PARITY_UNSET) { + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C || + cfg->tnc == HYBBX_PACKET_RADIO_TNC_MFJ1278) { + cfg->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_EVEN; + } else { + cfg->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_NONE; + } + } + + if (cfg->params.stop_bits == 0) { + cfg->params.stop_bits = 1; + } + + if (cfg->params.assert_modem_lines < 0) { + cfg->params.assert_modem_lines = + (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C) ? 1 : 0; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_profile_apply_defaults(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (cfg->tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = HYBBX_PACKET_RADIO_BAND_CB; + } + cfg->params.radio_baud = 1200; + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 30; + } + if (cfg->params.persist == 0) { + cfg->params.persist = 63; + } + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + break; + + case HYBBX_PACKET_RADIO_TNC_PCCOM: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = HYBBX_PACKET_RADIO_BAND_CB; + } + cfg->params.radio_baud = 1200; + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_HOSTMODE; + } + if (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 25; + } + if (cfg->params.persist == 0) { + cfg->params.persist = 128; + } + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + cfg->params.host_connect_on_start = 1; + break; + + case HYBBX_PACKET_RADIO_TNC_GENERIC: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_TNC2: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_PK232: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + if (cfg->baud == 0) { + cfg->baud = 4800; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->params.clock_mhz == 0.0f) { + cfg->params.clock_mhz = HYBBX_TNC2C_CLOCK_4_9_MHZ; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + } + + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + + (void)hybbx_tnc_finalize_modulation(cfg); + (void)hybbx_tnc_finalize_radio_duplex(cfg); + (void)hybbx_tnc_finalize_csma(cfg); + (void)hybbx_tnc_finalize_serial_line(cfg); + return hybbx_tnc_finalize_kiss_entry(cfg); +} -- cgit v1.3.1