dev -> master

- Fix double framing in queue_data
- check if TCP port is available before binding
- show headers in verbose output
- Add packet fragmentation/reassembly with magic byte 0xF3
- Fragmentation enabled via --frag flag or UI toggle
- Update utility `update.sh`


Fragment header: `[MAGIC][PKT_ID:2][SEQ][FLAGS]`
This commit is contained in:
Zenith 2026-01-09 19:46:41 -05:00 committed by GitHub
commit 2b67455f5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 678 additions and 61 deletions

View file

@ -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
```

View file

@ -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<uint8_t>& 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<uint8_t> 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<uint8_t>& 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<float> 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<uint8_t>& 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<std::mutex> 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());
}
if (config_.fragmentation_enabled && reassembler_.is_fragment(payload)) {
if (g_verbose) {
std::cerr << packet_visualize(payload.data(), payload.size(), false, true) << std::endl;
}
#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<std::mutex> lock(clients_mutex_);
for (auto& client : clients_) {
client->send(kiss_frame);
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<bool> tx_running_{false};
std::atomic<bool> 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<uint8_t>& 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;

View file

@ -3,10 +3,13 @@
#include <cstdint>
#include <vector>
#include <queue>
#include <map>
#include <mutex>
#include <atomic>
#include <chrono>
#include <functional>
#include <string>
#include <sstream>
#include <iomanip>
#include <iostream>
@ -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<uint8_t> frame_with_length(const std::vector<uint8_t>& data) {
@ -246,3 +378,171 @@ inline std::vector<uint8_t> unframe_length(const uint8_t* data, size_t total_len
}
return std::vector<uint8_t>(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<std::vector<uint8_t>> fragment(const std::vector<uint8_t>& data, size_t max_payload) {
std::vector<std::vector<uint8_t>> 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<uint8_t> 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<uint8_t>(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<uint16_t> next_packet_id_;
};
class Reassembler {
public:
Reassembler() = default;
std::vector<uint8_t> process(const std::vector<uint8_t>& 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<uint8_t> payload(fragment.begin() + Frag::HEADER_SIZE, fragment.end());
std::lock_guard<std::mutex> 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<uint8_t> 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<uint8_t>& data) const {
if (data.size() < Frag::HEADER_SIZE) return false;
return data[0] == Frag::MAGIC;
}
void reset() {
std::lock_guard<std::mutex> lock(mutex_);
pending_.clear();
}
private:
struct PendingPacket {
std::map<uint8_t, std::vector<uint8_t>> 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<std::chrono::milliseconds>(
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<uint16_t, PendingPacket> pending_;
mutable std::mutex mutex_;
};

View file

@ -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<float> 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);
}
}
@ -658,6 +672,7 @@ private:
FIELD_CSMA,
FIELD_THRESHOLD,
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<uint8_t> data(state_.mtu_bytes, 0x55);
std::vector<uint8_t> 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<uint8_t> data(state_.mtu_bytes);
std::vector<uint8_t> 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<uint8_t> data(ping.begin(), ping.end());

112
update.sh Executable file
View file

@ -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!"