Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions src/gui/modem/streaming_ofdm_decode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1028,15 +1028,27 @@ void StreamingDecoder::decodeCurrentFrame() {

// Multi-candidate light-sync recovery (connected OFDM):
// If decode fails at the detected sync point, retry nearby timing candidates.
// detectDataSync() scans with coarse steps, and fading can shift the best
// decode point by a few samples even when correlation looks valid.
if (!result.success && result.codewords_ok == 0 && is_ofdm && connected_) {
// Keep this recovery path tight. Moderate-fading hardware traces showed
// low-confidence syncs can pass the LLR gate, then repeated full fixed-frame LDPC
// retries burn several seconds with zero recoveries and trigger ARQ
// timeouts. Nearby timing retry is still useful for clean, high-corr
// locks, but beyond +/-8 samples the candidate is usually a bad lock.
const int retry_deltas[] = {8, -8};
// detectDataSync() scans with coarse steps, and clean light-preamble locks can
// still land late enough to leave only part of a fixed frame decodable.
const int attempted_codewords = result.codewords_ok + result.codewords_failed;
const bool partial_fixed_ofdm_failure =
attempted_codewords >= 2 &&
attempted_codewords <= v2::kMaxFixedFrameCodewords &&
result.codewords_ok < attempted_codewords;
const bool d8psk_data_mode = (current_modulation_ == Modulation::D8PSK);
if (!result.success && is_ofdm && connected_ &&
(result.codewords_ok == 0 || (d8psk_data_mode && partial_fixed_ofdm_failure))) {
// Keep this recovery path gated by high sync correlation. Moderate-fading
// hardware traces showed low-confidence syncs can pass the LLR gate, then
// repeated full fixed-frame LDPC retries burn several seconds with zero
// recoveries and trigger ARQ timeouts. Prefer earlier candidates first:
// late light-sync locks show up as a positive LTS phase slope.
const int d8psk_retry_deltas[] = {-32, -24, -16, -8, 8, 16, 24, 32};
const int default_retry_deltas[] = {8, -8};
const int* retry_deltas = d8psk_data_mode ? d8psk_retry_deltas : default_retry_deltas;
const size_t retry_delta_count = d8psk_data_mode
? (sizeof(d8psk_retry_deltas) / sizeof(d8psk_retry_deltas[0]))
: (sizeof(default_retry_deltas) / sizeof(default_retry_deltas[0]));
bool recovered = false;
int recovered_delta = 0;
uint64_t recovery_attempts = 0;
Expand All @@ -1061,7 +1073,12 @@ void StreamingDecoder::decodeCurrentFrame() {
};

if (allow_sync_recovery) {
for (int delta : retry_deltas) {
for (size_t retry_idx = 0; retry_idx < retry_delta_count; ++retry_idx) {
const int delta = retry_deltas[retry_idx];
if (delta < 0 && total_fed_ < buffer_capacity_samples_ &&
sync_position_ < static_cast<size_t>(-delta)) {
continue;
}
recovery_attempts++;
size_t retry_sync = wrapRingIndexLocked(sync_position_ + buffer_capacity_samples_ + delta);

Expand Down Expand Up @@ -1107,7 +1124,11 @@ void StreamingDecoder::decodeCurrentFrame() {
}

auto retry_result = decodeFrame(retry_bits, sync_snr_, sync_cfo_);
if (!(retry_result.success || retry_result.codewords_ok > 0)) {
if (d8psk_data_mode) {
if (!retry_result.success) {
continue;
}
} else if (!(retry_result.success || retry_result.codewords_ok > 0)) {
continue;
}

Expand Down
9 changes: 7 additions & 2 deletions tools/cli_simulator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,13 @@ class LocalOtaServer {
if (error) *error = "failed to write OTASim token file";
return false;
}
out << kOtaAlphaToken << ":ALPHA:Alpha station\n";
out << kOtaBravoToken << ":BRAVO:Bravo station\n";
// cli_simulator calls SetChannel (admin-gated since PR #30) to
// configure the spawned OTASim's channel model. Both tokens get
// admin role here because the test harness fully owns its own
// sandbox; production servers should not hand out admin tokens
// this freely.
out << kOtaAlphaToken << ":ALPHA:Alpha station:admin\n";
out << kOtaBravoToken << ":BRAVO:Bravo station:admin\n";
}

const int log_fd = ::open(log_path_.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0600);
Expand Down
20 changes: 12 additions & 8 deletions tools/decode_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,11 @@ int runGen(const Args& a) {
enc.setMode(ultra::tools::cli::requireWaveformMode(a.waveform));
enc.setOFDMConfig(benchOFDMConfig());
enc.setDataMode(*modulation, code_rate);
// Bench targets the connected-mode 4-CW fixed-frame data path —
// that's the throughput hot path agents will be optimizing.
enc.setFixedFrameCodewords(4);
const int fixed_cw = (a.cw_count > 0)
? v2::sanitizeFixedFrameCodewords(a.cw_count)
: v2::kDefaultFixedFrameCodewords;
// Bench targets the connected-mode fixed-frame data path.
enc.setFixedFrameCodewords(fixed_cw);
// Channel interleave defaults to true on both encoder and decoder.
// Match the default so fixtures are decodable by anything that
// hasn't explicitly overridden — including the GUI in monitor mode
Expand All @@ -327,7 +329,7 @@ int runGen(const Args& a) {
// into multi-frame fragmentation. We want a deterministic single-
// frame burst per iteration.
const size_t cap = v2::getFixedFramePayloadCapacity(
code_rate, 4);
code_rate, fixed_cw);
const size_t payload_bytes = std::min(static_cast<size_t>(a.payload_bytes), cap);

std::cout << "[gen] waveform=" << a.waveform
Expand All @@ -338,6 +340,7 @@ int runGen(const Args& a) {
<< " wav_format=" << a.wav_format
<< " sample_rate=" << a.output_sample_rate
<< " frames=" << a.num_frames
<< " fixed_cw=" << fixed_cw
<< " payload=" << payload_bytes << " bytes/frame (capacity=" << cap << ")"
<< " seed=" << a.seed << "\n";

Expand All @@ -363,16 +366,17 @@ int runGen(const Args& a) {
}

// Use v2::makeFixedDataFrame so total_cw is explicitly set to
// 4. DataFrame::makeData() calls calculateCodewords() which for
// a 60-byte payload at R1/4 returns 5 CWs (continuation CWs
// the requested fixed-CW geometry. DataFrame::makeData() calls
// calculateCodewords() which for a 60-byte payload at R1/4 returns 5 CWs
// (continuation CWs
// reserve DATA_CW_HEADER_SIZE bytes). The OFDM encoder trusts
// byte 12 of the serialized frame and frame-interleaves over
// that count — if it's 5 while the decoder expects 4, the
// that count — if it disagrees with the decoder, the
// de-interleave permutation is wrong and LDPC fails on every
// CW with saturated-but-wrong-position bits. (Codex review.)
auto frame = v2::makeFixedDataFrame(
"BENCH1", "BENCH2", static_cast<uint16_t>(f), payload,
code_rate, /*cw_count=*/4);
code_rate, fixed_cw);
Bytes serialized = frame.serialize();

// Preamble selection:
Expand Down
35 changes: 28 additions & 7 deletions tools/ofdm_snr_probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct Args {
::ChannelType channel = ::ChannelType::AWGN;
CodeRate rate = CodeRate::R1_2;
Modulation mod = Modulation::DQPSK;
int cw_count = 4;
uint32_t seed = 42;
size_t payload_bytes = 32;
bool header = true;
Expand All @@ -39,7 +40,8 @@ struct Args {
void usage(const char* argv0) {
std::cout
<< "Usage: " << argv0 << " [--snr DB] [--channel awgn|good|moderate|poor|flutter]\n"
<< " [--rate r1_4|r1_2|r2_3|r3_4] [--seed N] [--payload BYTES]\n";
<< " [--rate r1_4|r1_2|r2_3|r3_4] [--mod dqpsk|d8psk]\n"
<< " [--cw-count N] [--seed N] [--payload BYTES]\n";
}

const char* channelName(::ChannelType channel) {
Expand Down Expand Up @@ -86,6 +88,20 @@ bool parseArgs(int argc, char** argv, Args& args) {
return false;
}
args.rate = *parsed;
} else if (arg == "--mod") {
const char* v = need("--mod");
if (!v) return false;
auto parsed = cli::parseModulation(
v, cli::AllowAuto::No, cli::AllowExperimentalModulation::Yes);
if (!parsed) {
std::cerr << "Unknown modulation: " << v << "\n";
return false;
}
args.mod = *parsed;
} else if (arg == "--cw-count") {
const char* v = need("--cw-count");
if (!v) return false;
args.cw_count = v2::sanitizeFixedFrameCodewords(std::stoi(v));
} else if (arg == "--seed") {
const char* v = need("--seed");
if (!v) return false;
Expand Down Expand Up @@ -146,15 +162,18 @@ TxFrame buildTxFrame(const Args& args, const ModemConfig& cfg) {
payload[i] = static_cast<uint8_t>((i * 37u + 11u) & 0xffu);
}

const auto frame = v2::DataFrame::makeData("ALPHA", "BRAVO", 1, payload, args.rate);
const auto frame = v2::makeFixedDataFrame("ALPHA", "BRAVO", 1, payload,
args.rate, args.cw_count);
const Bytes frame_data = frame.serialize();
const Bytes encoded = v2::encodeFixedFrame(frame_data, args.rate);
const Bytes encoded = v2::encodeFixedFrame(frame_data, args.rate,
args.cw_count, true,
static_cast<size_t>(bitsPerOFDMSymbol(cfg)));

TxFrame tx;
tx.serialized_frame = frame_data;
tx.signal_start = 48000;
tx.samples.reserve(48000 + waveform.getDataPreambleSamples() +
waveform.getMinSamplesForCWCount(4) + 48000);
waveform.getMinSamplesForCWCount(args.cw_count) + 48000);
tx.samples.resize(tx.signal_start, 0.0f);

Samples preamble = waveform.generateDataPreamble();
Expand Down Expand Up @@ -208,13 +227,13 @@ ProbeResult decodeProbe(const Args& args, const ModemConfig& cfg,
result.fading_index = rx_waveform.getFadingIndex();

std::vector<float> soft_bits = rx_waveform.getSoftBits();
if (soft_bits.size() < 4u * v2::LDPC_CODEWORD_BITS) {
if (soft_bits.size() < static_cast<size_t>(args.cw_count) * v2::LDPC_CODEWORD_BITS) {
result.got_result = false;
return result;
}

auto status = v2::decodeFixedFrame(
soft_bits, args.rate, 4, false,
soft_bits, args.rate, args.cw_count, true,
static_cast<size_t>(bitsPerOFDMSymbol(cfg)));
result.cw_failed = status.countFailures();
result.cw_ok = static_cast<int>(status.decoded.size()) - result.cw_failed;
Expand Down Expand Up @@ -243,13 +262,15 @@ int main(int argc, char** argv) {

const ProbeResult r = decodeProbe(args, cfg, tx, rx);
if (args.header) {
std::cout << "channel,configured_snr,rate,success,cw_ok,cw_failed,"
std::cout << "channel,configured_snr,mod,rate,cw_count,success,cw_ok,cw_failed,"
<< "sync_snr_db,pilot_snr_db,lts_snr_db,fading_index\n";
}
std::cout << channelName(args.channel) << ","
<< std::fixed << std::setprecision(2)
<< args.snr_db << ","
<< ultra::modulationToString(args.mod) << ","
<< ultra::codeRateToString(args.rate) << ","
<< args.cw_count << ","
<< (r.success ? 1 : 0) << ","
<< r.cw_ok << ","
<< r.cw_failed << ","
Expand Down
Loading