From 552d9c7ef2e2cac1db3e99bc520a357c0efeb8f3 Mon Sep 17 00:00:00 2001 From: zenith Date: Fri, 9 Jan 2026 15:39:17 -0500 Subject: [PATCH 1/2] Double framing fix, check if port is bound , addl KISS cmds, fragmentation option with magic byte framing --- kiss_tnc.cc | 212 ++++++++++++++++++++++++++++------- kiss_tnc.hh | 312 +++++++++++++++++++++++++++++++++++++++++++++++++++- tnc_ui.hh | 96 +++++++++++++--- 3 files changed, 559 insertions(+), 61 deletions(-) diff --git a/kiss_tnc.cc b/kiss_tnc.cc index c3a5eea..bb96f51 100644 --- a/kiss_tnc.cc +++ b/kiss_tnc.cc @@ -67,6 +67,27 @@ inline void ui_log(const std::string& msg) { } } +bool check_port_available(const std::string& bind_address, int port) { + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + return false; + } + + int opt = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr(bind_address.c_str()); + addr.sin_port = htons(port); + + int result = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); + close(sock); + + return result == 0; +} + @@ -208,6 +229,8 @@ public: std::cerr << "CSMA: disabled" << std::endl; } + std::cerr << "Fragmentation: " << (config_.fragmentation_enabled ? "enabled" : "disabled") << std::endl; + // Show PTT status switch (config_.ptt_type) { case PTTType::NONE: @@ -321,47 +344,83 @@ private: void handle_kiss_frame(uint8_t /*port*/, uint8_t cmd, const std::vector& data) { if (cmd == KISS::CMD_DATA) { if (g_verbose) { - hex_dump("TX Queue", data.data(), data.size()); + std::cerr << kiss_frame_visualize(data.data(), data.size()) << std::endl; } - // Check size - if (data.size() > (size_t)payload_size_) { - std::cerr << "Warning: Frame too large (" << data.size() - << " > " << payload_size_ << "), truncating" << std::endl; - } + size_t max_payload = payload_size_ - 2; - tx_queue_.push(data); + if (config_.fragmentation_enabled && fragmenter_.needs_fragmentation(data.size(), max_payload)) { + auto fragments = fragmenter_.fragment(data, max_payload); + ui_log("TX: Fragmenting " + std::to_string(data.size()) + " bytes into " + + std::to_string(fragments.size()) + " fragments"); + for (auto& frag : fragments) { + if (g_verbose) { + std::cerr << packet_visualize(frag.data(), frag.size(), true, true) << std::endl; + } + tx_queue_.push(std::move(frag)); + } #ifdef WITH_UI - if (g_ui_state) { - g_ui_state->tx_queue_size = tx_queue_.size(); - } + if (g_ui_state) { + g_ui_state->tx_queue_size = tx_queue_.size(); + } #endif + } else { + std::vector frame_data = data; + if (frame_data.size() > max_payload) { + std::cerr << "Warning: Frame too large (" << frame_data.size() + << " > " << max_payload << "), truncating" << std::endl; + frame_data.resize(max_payload); + } + if (g_verbose) { + std::cerr << packet_visualize(frame_data.data(), frame_data.size(), true, config_.fragmentation_enabled) << std::endl; + } + tx_queue_.push(frame_data); +#ifdef WITH_UI + if (g_ui_state) { + g_ui_state->tx_queue_size = tx_queue_.size(); + } +#endif + } } else { - // Handle KISS control commands switch (cmd) { case KISS::CMD_TXDELAY: if (!data.empty()) { config_.tx_delay_ms = data[0] * 10; - std::cerr << "TXDelay set to " << config_.tx_delay_ms << " ms" << std::endl; + ui_log("TXDelay set to " + std::to_string(config_.tx_delay_ms) + " ms"); } break; case KISS::CMD_P: if (!data.empty()) { config_.p_persistence = data[0]; + ui_log("P-persistence set to " + std::to_string(config_.p_persistence)); } break; case KISS::CMD_SLOTTIME: if (!data.empty()) { config_.slot_time_ms = data[0] * 10; + ui_log("Slot time set to " + std::to_string(config_.slot_time_ms) + " ms"); + } + break; + case KISS::CMD_TXTAIL: + if (!data.empty()) { + config_.ptt_tail_ms = data[0] * 10; + ui_log("TXTail set to " + std::to_string(config_.ptt_tail_ms) + " ms"); } break; case KISS::CMD_FULLDUPLEX: if (!data.empty()) { config_.full_duplex = data[0] != 0; + ui_log(std::string("Full duplex ") + (config_.full_duplex ? "enabled" : "disabled")); } break; + case KISS::CMD_SETHW: + break; + case KISS::CMD_RETURN: + break; default: - std::cerr << "Unknown KISS command: " << (int)cmd << std::endl; + if (g_verbose) { + std::cerr << "Unknown KISS command: 0x" << std::hex << (int)cmd << std::dec << std::endl; + } } } } @@ -444,7 +503,7 @@ private: void transmit(const std::vector& data) { ui_log("TX: " + std::to_string(data.size()) + " bytes"); if (g_verbose) { - hex_dump("TX", data.data(), data.size()); + std::cerr << packet_visualize(data.data(), data.size(), true, config_.fragmentation_enabled) << std::endl; } #ifdef WITH_UI @@ -593,9 +652,30 @@ private: std::vector buffer(1024); int level_update_counter = 0; - const int LEVEL_UPDATE_INTERVAL = 5; // Update level every N reads + const int LEVEL_UPDATE_INTERVAL = 5; - auto frame_callback = [this](const uint8_t* data, size_t len) { + auto deliver_to_clients = [this](const std::vector& payload, float snr, bool was_reassembled) { + ui_log("RX: " + std::to_string(payload.size()) + " bytes, SNR=" + + std::to_string((int)snr) + "dB" + (was_reassembled ? " (reassembled)" : "")); + if (g_verbose) { + std::cerr << packet_visualize(payload.data(), payload.size(), false, false) << std::endl; + } + +#ifdef WITH_UI + if (g_ui_state) { + g_ui_state->add_packet(false, payload.size(), snr); + } +#endif + + auto kiss_frame = KISSParser::wrap(payload); + + std::lock_guard lock(clients_mutex_); + for (auto& client : clients_) { + client->send(kiss_frame); + } + }; + + auto frame_callback = [this, &deliver_to_clients](const uint8_t* data, size_t len) { set_tx_lockout(RX_LOCKOUT_SECONDS); float snr = decoder_->get_last_snr(); @@ -608,7 +688,6 @@ private: } #endif - // Strip length prefix framing auto payload = unframe_length(data, len); if (payload.empty()) { @@ -619,25 +698,18 @@ private: return; } - ui_log("RX: " + std::to_string(payload.size()) + " bytes, SNR=" + - std::to_string((int)snr) + "dB"); - if (g_verbose) { - hex_dump("RX", payload.data(), payload.size()); - } - -#ifdef WITH_UI - // Track packet for UI display - if (g_ui_state) { - g_ui_state->add_packet(false, payload.size(), snr); - } -#endif - - // Send to all clients - auto kiss_frame = KISSParser::wrap(payload); - - std::lock_guard lock(clients_mutex_); - for (auto& client : clients_) { - client->send(kiss_frame); + if (config_.fragmentation_enabled && reassembler_.is_fragment(payload)) { + if (g_verbose) { + std::cerr << packet_visualize(payload.data(), payload.size(), false, true) << std::endl; + } + + auto reassembled = reassembler_.process(payload); + if (!reassembled.empty()) { + ui_log("RX: Reassembled " + std::to_string(reassembled.size()) + " bytes from fragments"); + deliver_to_clients(reassembled, snr, true); + } + } else { + deliver_to_clients(payload, snr, false); } }; @@ -737,10 +809,13 @@ private: std::atomic tx_running_{false}; std::atomic rx_running_{false}; + Fragmenter fragmenter_; + Reassembler reassembler_; + // TX lockout - prevents TX while receiving std::mutex lockout_mutex_; std::chrono::steady_clock::time_point tx_lockout_until_; - static constexpr float RX_LOCKOUT_SECONDS = 0.5f; // Post-RX settling time + static constexpr float RX_LOCKOUT_SECONDS = 0.5f; public: // Update config at runtime (called from UI) @@ -810,10 +885,19 @@ public: return false; } - // Queue data for transmission void queue_data(const std::vector& data) { - auto framed = frame_with_length(data); - tx_queue_.push(framed); + size_t max_payload = payload_size_ - 2; + + if (config_.fragmentation_enabled && fragmenter_.needs_fragmentation(data.size(), max_payload)) { + auto fragments = fragmenter_.fragment(data, max_payload); + ui_log("TX: Fragmenting " + std::to_string(data.size()) + " bytes into " + + std::to_string(fragments.size()) + " fragments"); + for (auto& frag : fragments) { + tx_queue_.push(std::move(frag)); + } + } else { + tx_queue_.push(data); + } #ifdef WITH_UI if (g_ui_state) { g_ui_state->tx_queue_size = tx_queue_.size(); @@ -850,6 +934,9 @@ void print_help(const char* prog) { << " --csma-threshold DB Carrier sense threshold (default: -30)\n" << " --csma-slot MS Slot time in ms (default: 500)\n" << " --csma-persist N P-persistence 0-255 (default: 128 = 50%)\n" + << "\nFragmentation:\n" + << " --frag Enable packet fragmentation/reassembly\n" + << " --no-frag Disable fragmentation (default)\n" << "\n" #ifdef WITH_UI << " -h, --headless Run without TUI\n" @@ -948,6 +1035,10 @@ int main(int argc, char** argv) { config.slot_time_ms = std::atoi(argv[++i]); } else if (arg == "--csma-persist" && i + 1 < argc) { config.p_persistence = std::atoi(argv[++i]); + } else if (arg == "--frag") { + config.fragmentation_enabled = true; + } else if (arg == "--no-frag") { + config.fragmentation_enabled = false; } else { std::cerr << "Unknown option: " << arg << std::endl; print_help(argv[0]); @@ -1005,6 +1096,7 @@ int main(int argc, char** argv) { config.carrier_threshold_db = ui_state.carrier_threshold_db; config.slot_time_ms = ui_state.slot_time_ms; config.p_persistence = ui_state.p_persistence; + config.fragmentation_enabled = ui_state.fragmentation_enabled; // Audio devices config.audio_input_device = ui_state.audio_input_device; config.audio_output_device = ui_state.audio_output_device; @@ -1048,6 +1140,7 @@ int main(int argc, char** argv) { ui_state.slot_time_ms = config.slot_time_ms; ui_state.p_persistence = config.p_persistence; ui_state.short_frame = config.short_frame; + ui_state.fragmentation_enabled = config.fragmentation_enabled; // Audio devices ui_state.audio_input_device = config.audio_input_device; ui_state.audio_output_device = config.audio_output_device; @@ -1099,8 +1192,8 @@ int main(int argc, char** argv) { ui_state.load_presets(); - - + // Sync fragmentation setting from command line to UI + ui_state.fragmentation_enabled = config.fragmentation_enabled; ui_state.update_modem_info(); @@ -1111,6 +1204,40 @@ int main(int argc, char** argv) { } #endif + while (!check_port_available(config.bind_address, config.port)) { + std::cerr << "Error: Port " << config.port << " is already in use or cannot be bound" << std::endl; + std::cerr << "Another instance of modem73 may be running, or another application is using this port." << std::endl; + + if (!g_use_ui) { + std::cerr << "Use --port to specify a different port." << std::endl; + return 1; + } + + std::cerr << "\nEnter a different port number (or 'q' to quit): "; + std::string input; + if (!std::getline(std::cin, input) || input.empty() || input == "q" || input == "Q") { + std::cerr << "Exiting." << std::endl; + return 1; + } + + try { + int new_port = std::stoi(input); + if (new_port < 1 || new_port > 65535) { + std::cerr << "Invalid port number. Must be between 1 and 65535." << std::endl; + continue; + } + config.port = new_port; +#ifdef WITH_UI + if (g_use_ui) { + ui_state.port = new_port; + } +#endif + std::cerr << "Trying port " << config.port << "..." << std::endl; + } catch (const std::exception&) { + std::cerr << "Invalid input. Please enter a number." << std::endl; + } + } + try { KISSTNC tnc(config); @@ -1127,6 +1254,7 @@ int main(int argc, char** argv) { new_config.carrier_threshold_db = state.carrier_threshold_db; new_config.p_persistence = state.p_persistence; new_config.slot_time_ms = state.slot_time_ms; + new_config.fragmentation_enabled = state.fragmentation_enabled; // Audio devices new_config.audio_input_device = state.audio_input_device; new_config.audio_output_device = state.audio_output_device; diff --git a/kiss_tnc.hh b/kiss_tnc.hh index bb856da..8e02af1 100644 --- a/kiss_tnc.hh +++ b/kiss_tnc.hh @@ -3,10 +3,13 @@ #include #include #include +#include #include #include +#include #include #include +#include #include #include @@ -83,9 +86,12 @@ struct TNCConfig { // CSMA settings bool csma_enabled = true; - float carrier_threshold_db = -30.0f; // Carrier sense threshold - int carrier_sense_ms = 100; // How long to listen - int max_backoff_slots = 10; // Maximum backoff + float carrier_threshold_db = -30.0f; + int carrier_sense_ms = 100; + int max_backoff_slots = 10; + + // Fragmentation settings + bool fragmentation_enabled = false; // Settings file path std::string config_file = ""; @@ -129,7 +135,6 @@ private: void process_byte(uint8_t byte) { if (byte == KISS::FEND) { if (in_frame_ && buffer_.size() > 0) { - // frame complete uint8_t cmd_byte = buffer_[0]; uint8_t port = (cmd_byte >> 4) & 0x0F; uint8_t cmd = cmd_byte & 0x0F; @@ -206,7 +211,6 @@ inline void hex_dump(const char* prefix, const uint8_t* data, size_t len) { for (size_t i = 0; i < len; i += 16) { std::cerr << " " << std::hex << std::setfill('0') << std::setw(4) << i << ": "; - // Hex for (size_t j = 0; j < 16 && i + j < len; ++j) { std::cerr << std::setw(2) << (int)data[i + j] << " "; } @@ -214,7 +218,6 @@ inline void hex_dump(const char* prefix, const uint8_t* data, size_t len) { std::cerr << " "; } - // ASCII std::cerr << " |"; for (size_t j = 0; j < 16 && i + j < len; ++j) { char c = data[i + j]; @@ -225,6 +228,135 @@ inline void hex_dump(const char* prefix, const uint8_t* data, size_t len) { std::cerr << std::dec; } +inline std::string packet_visualize(const uint8_t* data, size_t len, bool is_tx, bool frag_enabled) { + std::ostringstream oss; + + if (len == 0) { + oss << " [EMPTY PACKET]"; + return oss.str(); + } + + oss << "\n ┌─────────────────────────────────────────────────────────────┐\n"; + oss << " │ " << (is_tx ? "TX" : "RX") << " PACKET: " << len << " bytes"; + oss << std::string(47 - std::to_string(len).length(), ' ') << "│\n"; + oss << " ├─────────────────────────────────────────────────────────────┤\n"; + + size_t offset = 0; + + // Check for fragment by magic byte + if (frag_enabled && len >= 5 && data[0] == 0xF3) { + uint16_t pkt_id = (data[1] << 8) | data[2]; + uint8_t seq = data[3]; + uint8_t flags = data[4]; + + oss << " │ FRAG HDR [5 bytes] Magic: 0xF3 │\n"; + oss << " │ Packet ID: " << std::setw(5) << pkt_id; + oss << " Seq: " << std::setw(3) << (int)seq; + oss << " Flags: "; + + std::string flag_str; + if (flags & 0x02) flag_str += "FIRST "; + if (flags & 0x01) flag_str += "MORE"; + if (flag_str.empty()) flag_str = "LAST"; + oss << std::left << std::setw(12) << flag_str << std::right << " │\n"; + + offset = 5; + oss << " ├─────────────────────────────────────────────────────────────┤\n"; + } + + size_t payload_len = len - offset; + oss << " │ PAYLOAD [" << payload_len << " bytes]"; + oss << std::string(49 - std::to_string(payload_len).length(), ' ') << "│\n"; + + size_t preview_len = std::min(payload_len, (size_t)32); + if (preview_len > 0) { + oss << " │ "; + for (size_t i = 0; i < preview_len; i++) { + oss << std::hex << std::setfill('0') << std::setw(2) << (int)data[offset + i]; + if (i < preview_len - 1) oss << " "; + } + if (payload_len > 32) oss << "..."; + size_t used = preview_len * 3 - 1 + (payload_len > 32 ? 3 : 0); + if (used < 57) oss << std::string(57 - used, ' '); + oss << std::dec << " │\n"; + + oss << " │ "; + for (size_t i = 0; i < preview_len; i++) { + char c = data[offset + i]; + oss << (c >= 32 && c < 127 ? c : '.'); + } + if (payload_len > 32) oss << "..."; + size_t ascii_used = preview_len + (payload_len > 32 ? 3 : 0); + if (ascii_used < 57) oss << std::string(57 - ascii_used, ' '); + oss << " │\n"; + } + + oss << " └─────────────────────────────────────────────────────────────┘"; + + return oss.str(); +} + +inline std::string kiss_frame_visualize(const uint8_t* data, size_t len) { + std::ostringstream oss; + + if (len == 0) { + oss << " [EMPTY KISS FRAME]"; + return oss.str(); + } + + oss << "\n ┌─────────────────────────────────────────────────────────────┐\n"; + oss << " │ KISS FRAME: " << len << " bytes"; + oss << std::string(45 - std::to_string(len).length(), ' ') << "│\n"; + oss << " ├─────────────────────────────────────────────────────────────┤\n"; + + if (len >= 1) { + uint8_t cmd_byte = data[0]; + uint8_t port = (cmd_byte >> 4) & 0x0F; + uint8_t cmd = cmd_byte & 0x0F; + + oss << " │ CMD BYTE: 0x" << std::hex << std::setfill('0') << std::setw(2) << (int)cmd_byte << std::dec; + oss << " Port: " << (int)port << " Cmd: "; + + std::string cmd_name; + switch (cmd) { + case 0x00: cmd_name = "DATA"; break; + case 0x01: cmd_name = "TXDELAY"; break; + case 0x02: cmd_name = "P"; break; + case 0x03: cmd_name = "SLOTTIME"; break; + case 0x04: cmd_name = "TXTAIL"; break; + case 0x05: cmd_name = "FULLDUPLEX"; break; + case 0x06: cmd_name = "SETHW"; break; + case 0x0F: cmd_name = "RETURN"; break; + default: cmd_name = "UNKNOWN"; break; + } + oss << std::left << std::setw(10) << cmd_name << std::right; + oss << " │\n"; + } + + if (len > 1) { + oss << " ├─────────────────────────────────────────────────────────────┤\n"; + size_t payload_len = len - 1; + oss << " │ PAYLOAD [" << payload_len << " bytes]"; + oss << std::string(49 - std::to_string(payload_len).length(), ' ') << "│\n"; + + size_t preview_len = std::min(payload_len, (size_t)24); + oss << " │ "; + for (size_t i = 0; i < preview_len; i++) { + oss << std::hex << std::setfill('0') << std::setw(2) << (int)data[1 + i]; + if (i < preview_len - 1) oss << " "; + } + if (payload_len > 24) oss << " ..."; + oss << std::dec; + size_t used = preview_len * 3 - 1 + (payload_len > 24 ? 4 : 0); + if (used < 57) oss << std::string(57 - used, ' '); + oss << " │\n"; + } + + oss << " └─────────────────────────────────────────────────────────────┘"; + + return oss.str(); +} + // Length-prefix framing // This handles OFDM frame padding where the payload is encoded within the 2 byte prefix inline std::vector frame_with_length(const std::vector& data) { @@ -246,3 +378,171 @@ inline std::vector unframe_length(const uint8_t* data, size_t total_len } return std::vector(data + 2, data + 2 + payload_len); } + +namespace Frag { + constexpr uint8_t MAGIC = 0xF3; + constexpr size_t HEADER_SIZE = 5; + constexpr uint8_t FLAG_MORE_FRAGMENTS = 0x01; + constexpr uint8_t FLAG_FIRST_FRAGMENT = 0x02; + constexpr int REASSEMBLY_TIMEOUT_MS = 30000; + constexpr size_t MAX_PENDING_PACKETS = 64; +} + +class Fragmenter { +public: + Fragmenter() : next_packet_id_(0) {} + + std::vector> fragment(const std::vector& data, size_t max_payload) { + std::vector> fragments; + + if (max_payload <= Frag::HEADER_SIZE) { + return fragments; + } + + size_t data_per_frag = max_payload - Frag::HEADER_SIZE; + size_t num_frags = (data.size() + data_per_frag - 1) / data_per_frag; + if (num_frags > 255) { + num_frags = 255; + } + + uint16_t packet_id = next_packet_id_++; + + for (size_t i = 0; i < num_frags; i++) { + size_t offset = i * data_per_frag; + size_t chunk_size = std::min(data_per_frag, data.size() - offset); + + std::vector frag; + frag.reserve(Frag::HEADER_SIZE + chunk_size); + + frag.push_back(Frag::MAGIC); + frag.push_back((packet_id >> 8) & 0xFF); + frag.push_back(packet_id & 0xFF); + frag.push_back(static_cast(i)); + + uint8_t flags = 0; + if (i == 0) flags |= Frag::FLAG_FIRST_FRAGMENT; + if (i < num_frags - 1) flags |= Frag::FLAG_MORE_FRAGMENTS; + frag.push_back(flags); + + frag.insert(frag.end(), data.begin() + offset, data.begin() + offset + chunk_size); + fragments.push_back(std::move(frag)); + } + + return fragments; + } + + bool needs_fragmentation(size_t data_size, size_t max_payload) const { + return data_size > (max_payload - Frag::HEADER_SIZE); + } + +private: + std::atomic next_packet_id_; +}; + +class Reassembler { +public: + Reassembler() = default; + + std::vector process(const std::vector& fragment) { + if (fragment.size() < Frag::HEADER_SIZE) { + return {}; + } + + if (fragment[0] != Frag::MAGIC) { + return {}; + } + + uint16_t packet_id = (fragment[1] << 8) | fragment[2]; + uint8_t seq = fragment[3]; + uint8_t flags = fragment[4]; + + std::vector payload(fragment.begin() + Frag::HEADER_SIZE, fragment.end()); + + std::lock_guard lock(mutex_); + + cleanup_stale(); + + auto& pkt = pending_[packet_id]; + if (pkt.fragments.empty()) { + pkt.first_seen = std::chrono::steady_clock::now(); + } + + pkt.fragments[seq] = std::move(payload); + + if (flags & Frag::FLAG_FIRST_FRAGMENT) { + pkt.has_first = true; + } + + if (!(flags & Frag::FLAG_MORE_FRAGMENTS)) { + pkt.last_seq = seq; + pkt.has_last = true; + } + + if (pkt.has_first && pkt.has_last) { + bool complete = true; + for (uint8_t i = 0; i <= pkt.last_seq; i++) { + if (pkt.fragments.find(i) == pkt.fragments.end()) { + complete = false; + break; + } + } + + if (complete) { + std::vector reassembled; + for (uint8_t i = 0; i <= pkt.last_seq; i++) { + auto& frag_data = pkt.fragments[i]; + reassembled.insert(reassembled.end(), frag_data.begin(), frag_data.end()); + } + pending_.erase(packet_id); + return reassembled; + } + } + + return {}; + } + + bool is_fragment(const std::vector& data) const { + if (data.size() < Frag::HEADER_SIZE) return false; + return data[0] == Frag::MAGIC; + } + + void reset() { + std::lock_guard lock(mutex_); + pending_.clear(); + } + +private: + struct PendingPacket { + std::map> fragments; + std::chrono::steady_clock::time_point first_seen; + uint8_t last_seq = 0; + bool has_first = false; + bool has_last = false; + }; + + void cleanup_stale() { + auto now = std::chrono::steady_clock::now(); + for (auto it = pending_.begin(); it != pending_.end();) { + auto age = std::chrono::duration_cast( + now - it->second.first_seen).count(); + if (age > Frag::REASSEMBLY_TIMEOUT_MS) { + it = pending_.erase(it); + } else { + ++it; + } + } + + while (pending_.size() > Frag::MAX_PENDING_PACKETS) { + auto oldest = pending_.begin(); + for (auto it = pending_.begin(); it != pending_.end(); ++it) { + if (it->second.first_seen < oldest->second.first_seen) { + oldest = it; + } + } + pending_.erase(oldest); + } + } + + std::map pending_; + mutable std::mutex mutex_; +}; diff --git a/tnc_ui.hh b/tnc_ui.hh index b2257a9..4c73378 100644 --- a/tnc_ui.hh +++ b/tnc_ui.hh @@ -91,6 +91,8 @@ struct TNCUIState { int mtu_bytes = 0; int bitrate_bps = 0; float airtime_seconds = 0.0f; + int random_data_size = 0; + bool fragmentation_enabled = false; // stats std::atomic total_tx_time{0.0f}; @@ -269,6 +271,13 @@ struct TNCUIState { bitrate_bps = bitrate_normal[mod][rate]; airtime_seconds = duration_normal[mod] / 1000.0f; } + + // Initialize random_data_size if not set, clamp to MTU only if fragmentation disabled + if (random_data_size == 0) { + random_data_size = mtu_bytes; + } else if (!fragmentation_enabled && random_data_size > mtu_bytes) { + random_data_size = mtu_bytes; + } } void update_level(float db) { @@ -345,6 +354,7 @@ struct TNCUIState { fprintf(f, "carrier_threshold_db=%.1f\n", carrier_threshold_db); fprintf(f, "slot_time_ms=%d\n", slot_time_ms); fprintf(f, "p_persistence=%d\n", p_persistence); + fprintf(f, "fragmentation_enabled=%d\n", fragmentation_enabled ? 1 : 0); fprintf(f, "# Audio/PTT\n"); fprintf(f, "audio_input=%s\n", audio_input_device.c_str()); fprintf(f, "audio_output=%s\n", audio_output_device.c_str()); @@ -359,6 +369,8 @@ struct TNCUIState { fprintf(f, "com_invert_rts=%d\n", com_invert_rts ? 1 : 0); fprintf(f, "# Network\n"); fprintf(f, "port=%d\n", port); + fprintf(f, "# Utils\n"); + fprintf(f, "random_data_size=%d\n", random_data_size); fclose(f); return true; @@ -386,6 +398,7 @@ struct TNCUIState { else if (strcmp(key, "carrier_threshold_db") == 0) carrier_threshold_db = atof(value); else if (strcmp(key, "slot_time_ms") == 0) slot_time_ms = atoi(value); else if (strcmp(key, "p_persistence") == 0) p_persistence = atoi(value); + else if (strcmp(key, "fragmentation_enabled") == 0) fragmentation_enabled = atoi(value) != 0; else if (strcmp(key, "audio_input") == 0) audio_input_device = value; else if (strcmp(key, "audio_output") == 0) audio_output_device = value; else if (strcmp(key, "audio_device") == 0) { @@ -401,6 +414,7 @@ struct TNCUIState { else if (strcmp(key, "com_invert_dtr") == 0) com_invert_dtr = atoi(value) != 0; else if (strcmp(key, "com_invert_rts") == 0) com_invert_rts = atoi(value) != 0; else if (strcmp(key, "port") == 0) port = atoi(value); + else if (strcmp(key, "random_data_size") == 0) random_data_size = atoi(value); } } @@ -657,7 +671,8 @@ private: FIELD_FREQ, FIELD_CSMA, FIELD_THRESHOLD, - FIELD_PERSISTENCE, + FIELD_PERSISTENCE, + FIELD_FRAGMENTATION, FIELD_AUDIO_INPUT, FIELD_AUDIO_OUTPUT, FIELD_PTT_TYPE, @@ -747,6 +762,11 @@ private: } else if (current_field_ >= FIELD_MODULATION && current_field_ != FIELD_PRESET) { adjust_field(-1); } + } else if (current_tab_ == 3 && (utils_selection_ == 0 || utils_selection_ == 1)) { + int step = 1; + if (state_.random_data_size >= 1000) step = 100; + else if (state_.random_data_size >= 100) step = 10; + state_.random_data_size = std::max(1, state_.random_data_size - step); } break; @@ -763,6 +783,12 @@ private: } else if (current_field_ >= FIELD_MODULATION && current_field_ != FIELD_PRESET) { adjust_field(1); } + } else if (current_tab_ == 3 && (utils_selection_ == 0 || utils_selection_ == 1)) { + int step = 1; + if (state_.random_data_size >= 1000) step = 100; + else if (state_.random_data_size >= 100) step = 10; + int max_size = state_.fragmentation_enabled ? 65535 : state_.mtu_bytes; + state_.random_data_size = std::min(max_size, state_.random_data_size + step); } break; @@ -1112,6 +1138,11 @@ private: state_.p_persistence += delta * 8; state_.p_persistence = std::max(0, std::min(255, state_.p_persistence)); break; + case FIELD_FRAGMENTATION: + state_.fragmentation_enabled = !state_.fragmentation_enabled; + state_.update_modem_info(); // Update random_data_size limits + state_.add_log("(!) Fragmentation changed, restart required"); + break; case FIELD_AUDIO_INPUT: break; case FIELD_AUDIO_OUTPUT: @@ -2115,6 +2146,20 @@ private: } row += 2; + // Fragmentation section + dy = visible_y(row); + if (dy >= 0) { + attron(A_DIM); + mvaddstr(dy, c1, "FRAGMENTATION"); + mvaddstr(dy, c1 + 14, "(restart)"); + attroff(A_DIM); + } + row++; + + dy = visible_y(row); + if (dy >= 0) draw_toggle_field(dy, c1, c2, "Enabled", FIELD_FRAGMENTATION, state_.fragmentation_enabled); + row += 2; + // Audio / ptt dy = visible_y(row); @@ -2625,28 +2670,56 @@ private: y++; - // Test info attron(COLOR_PAIR(4) | A_BOLD); mvaddstr(y, c1, "[ TEST INFO ]"); attroff(COLOR_PAIR(4) | A_BOLD); y++; attron(A_DIM); - mvaddstr(y, c1, "Payload"); + mvaddstr(y, c1, "MTU"); attroff(A_DIM); - mvprintw(y, c1 + 12, "%d bytes", state_.mtu_bytes); + mvprintw(y, c1 + 14, "%d bytes", state_.mtu_bytes); + if (state_.fragmentation_enabled) { + attron(COLOR_PAIR(4)); + printw(" [FRAG]"); + attroff(COLOR_PAIR(4)); + } + y++; + + bool size_selected = (utils_selection_ == 0 || utils_selection_ == 1); + if (size_selected) { + attron(A_BOLD | COLOR_PAIR(4)); + } else { + attron(A_DIM); + } + mvaddstr(y, c1, "Test Size"); + if (size_selected) { + attroff(A_BOLD | COLOR_PAIR(4)); + mvprintw(y, c1 + 14, "< %d bytes >", state_.random_data_size); + } else { + attroff(A_DIM); + mvprintw(y, c1 + 14, "%d bytes", state_.random_data_size); + } + + if (state_.fragmentation_enabled && state_.random_data_size > state_.mtu_bytes) { + int data_per_frag = state_.mtu_bytes - 5; // 5-byte fragment header + int num_frags = (state_.random_data_size + data_per_frag - 1) / data_per_frag; + attron(COLOR_PAIR(3)); + printw(" (%d frags)", num_frags); + attroff(COLOR_PAIR(3)); + } y++; attron(A_DIM); mvaddstr(y, c1, "Pattern"); attroff(A_DIM); - mvaddstr(y, c1 + 12, "0x55 (alternating)"); + mvaddstr(y, c1 + 14, "0x55 (alternating)"); y++; attron(A_DIM); mvaddstr(y, c1, "Frames Sent"); attroff(A_DIM); - mvprintw(y, c1 + 12, "%d", state_.tx_frame_count.load()); + mvprintw(y, c1 + 14, "%d", state_.tx_frame_count.load()); y++; int ry = 4; @@ -2704,29 +2777,26 @@ private: void handle_utils_action() { switch (utils_selection_) { case 0: { - // Send test pattern if (state_.on_send_data) { - std::vector data(state_.mtu_bytes, 0x55); + std::vector data(state_.random_data_size, 0x55); state_.on_send_data(data); - state_.add_log("Sent test pattern (" + std::to_string(state_.mtu_bytes) + " bytes)"); + state_.add_log("Sent test pattern (" + std::to_string(state_.random_data_size) + " bytes)"); } break; } case 1: { - // Send random data if (state_.on_send_data) { - std::vector data(state_.mtu_bytes); + std::vector data(state_.random_data_size); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 255); for (auto& b : data) b = dis(gen); state_.on_send_data(data); - state_.add_log("Sent random data (" + std::to_string(state_.mtu_bytes) + " bytes)"); + state_.add_log("Sent random data (" + std::to_string(state_.random_data_size) + " bytes)"); } break; } case 2: { - // Send ping if (state_.on_send_data) { std::string ping = "PING:" + state_.callsign; std::vector data(ping.begin(), ping.end()); From f82e2fe8e885ea981fcf8d432d09955e1fa3dbf0 Mon Sep 17 00:00:00 2001 From: zenith Date: Fri, 9 Jan 2026 15:56:29 -0500 Subject: [PATCH 2/2] Add update utility --- README.md | 7 ++++ update.sh | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100755 update.sh diff --git a/README.md b/README.md index 07571c6..2a75c23 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,11 @@ while running `rigctld` ``` +## Updating +modem73 comes included with a update utility `update.sh` + +To update to the latest version: +``` +./update.sh +``` \ No newline at end of file diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..303ac63 --- /dev/null +++ b/update.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# +# modem73 update script +# https://github.com/RFnexus/modem73 +# + +set -e + +REPO_URL="https://github.com/RFnexus/modem73.git" +INSTALL_DIR="${MODEM73_DIR:-$HOME/modem73}" + +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + NC='' +fi + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +command -v git >/dev/null 2>&1 || error "git is required but not installed" +command -v make >/dev/null 2>&1 || error "make is required but not installed" + + +if command -v g++ >/dev/null 2>&1; then + CXX="g++" +elif command -v clang++ >/dev/null 2>&1; then + CXX="clang++" +else + error "C++ compiler (g++ or clang++) is required but not installed" +fi + +info "modem73 updater" +echo " Repository: $REPO_URL" +echo " Install to: $INSTALL_DIR" +echo "" + +if [ -d "$INSTALL_DIR/.git" ]; then + info "Updating = installation..." + cd "$INSTALL_DIR" + + if ! git diff --quiet 2>/dev/null; then + warn "Local changes detected. Stashing..." + git stash + fi + + BRANCH=$(git rev-parse --abbrev-ref HEAD) + + git fetch origin + LOCAL=$(git rev-parse HEAD) + REMOTE=$(git rev-parse "origin/$BRANCH") + + if [ "$LOCAL" = "$REMOTE" ]; then + info "Already up to date" + NEEDS_BUILD=0 + else + info "Pulling updates..." + git pull --ff-only origin "$BRANCH" + NEEDS_BUILD=1 + fi +else + info "Cloning repository..." + mkdir -p "$(dirname "$INSTALL_DIR")" + git clone "$REPO_URL" "$INSTALL_DIR" + cd "$INSTALL_DIR" + NEEDS_BUILD=1 +fi + +# Build +if [ "${NEEDS_BUILD:-1}" = "1" ] || [ "$1" = "--force" ]; then + info "Building modem73..." + + + + make clean 2>/dev/null || true + + + + if make -j"$(nproc 2>/dev/null || echo 2)"; then + info "Build sucessful!" + + + + + echo "" + echo " Commit: $(git rev-parse --short HEAD)" + echo " Date: $(git log -1 --format=%ci)" + echo "" + + + + + if [ -f "$INSTALL_DIR/modem73" ]; then + info "Binary: $INSTALL_DIR/modem73" + fi + else + error "Build failed" + fi +else + info "No build needed (use --force to rebuild)" +fi + + + +info "Done!"