mirror of
https://github.com/OpenRTX/OpenRTX
synced 2026-08-08 12:29:06 -04:00
tests: add end-to-end demodulator lock test
Add a test that generates 10 consecutive RRC-shaped stream frames and asserts the demodulator stays locked throughout. Co-authored-by: GitHub Copilot <175728472+github-copilot[bot]@users.noreply.github.com> Signed-off-by: Ryan Turner <ryan@turnrye.com>
This commit is contained in:
parent
60e8b780df
commit
1b8a479390
1 changed files with 115 additions and 0 deletions
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include "protocols/M17/Correlator.hpp"
|
||||
#include "protocols/M17/Constants.hpp"
|
||||
#include "protocols/M17/Demodulator.hpp"
|
||||
#include "protocols/M17/DSP.hpp"
|
||||
#include "protocols/M17/Synchronizer.hpp"
|
||||
#include "core/fir.hpp"
|
||||
|
|
@ -174,3 +175,117 @@ TEST_CASE("RRC 24kHz filter has unity DC gain", "[m17][demodulator]")
|
|||
|
||||
REQUIRE(std::abs(sum - 1.0f) < 1e-3f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// End-to-end demodulator lock tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Stream syncword symbols: 0xFF5D → dibits 11 11 11 11 01 01 11 01
|
||||
// Mapped to ±3/±1: +3→11, +1→01, -1→10, -3→00 → -3,-3,-3,-3,+3,+3,-3,+3
|
||||
static constexpr std::array<int8_t, M17::SYNCWORD_SYMBOLS> STREAM_SW_SYMS = {
|
||||
-3, -3, -3, -3, +3, +3, -3, +3
|
||||
};
|
||||
|
||||
// Generate an RRC-shaped baseband signal from a sequence of M17 symbols.
|
||||
// Each symbol is placed at sample-rate position (one every SAMPLES_PER_SYM
|
||||
// samples) with zero-fill in between, then shaped by the RRC transmit filter.
|
||||
static std::vector<int16_t> rrcBaseband(const std::vector<int8_t> &symbols,
|
||||
float amplitude = 2000.0f)
|
||||
{
|
||||
static constexpr size_t SPS = SAMPLES_PER_SYM;
|
||||
static constexpr size_t NTAPS = M17::rrc_taps_24k.size();
|
||||
|
||||
Fir<NTAPS> txRrc(M17::rrc_taps_24k);
|
||||
|
||||
std::vector<int16_t> out;
|
||||
out.reserve(symbols.size() * SPS);
|
||||
|
||||
for (size_t i = 0; i < symbols.size(); i++) {
|
||||
// First sample of the symbol period carries the impulse
|
||||
float imp = static_cast<float>(symbols[i]) * amplitude;
|
||||
out.push_back(static_cast<int16_t>(txRrc(imp) * SPS));
|
||||
|
||||
// Remaining SPS-1 samples are zero (interpolation)
|
||||
for (size_t s = 1; s < SPS; s++)
|
||||
out.push_back(static_cast<int16_t>(txRrc(0.0f) * SPS));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Build the symbol sequence for one complete M17 stream frame (192 symbols):
|
||||
// 8 sync-word symbols + 184 payload symbols (alternating +1/-1 pattern).
|
||||
static std::vector<int8_t> makeStreamFrame()
|
||||
{
|
||||
std::vector<int8_t> syms(M17::FRAME_SYMBOLS);
|
||||
for (size_t i = 0; i < STREAM_SW_SYMS.size(); i++)
|
||||
syms[i] = STREAM_SW_SYMS[i];
|
||||
for (size_t i = STREAM_SW_SYMS.size(); i < syms.size(); i++)
|
||||
syms[i] = (i % 2 == 0) ? +1 : -1;
|
||||
return syms;
|
||||
}
|
||||
|
||||
TEST_CASE("Demodulator maintains lock across multiple consecutive stream frames",
|
||||
"[m17][demodulator]")
|
||||
{
|
||||
// --- Build the synthetic baseband signal ---
|
||||
// Preamble: silence long enough to pass the INIT state (480 samples)
|
||||
// plus RRC filter settling time.
|
||||
static constexpr size_t PREAMBLE_SYMS = 200; // 200 * 5 = 1000 samples
|
||||
static constexpr size_t NUM_FRAMES = 10;
|
||||
|
||||
std::vector<int8_t> allSyms;
|
||||
|
||||
// Preamble: alternating +3/-3 to build up correlator energy
|
||||
for (size_t i = 0; i < PREAMBLE_SYMS; i++)
|
||||
allSyms.push_back((i % 2 == 0) ? +3 : -3);
|
||||
|
||||
// Concatenate NUM_FRAMES complete stream frames
|
||||
auto oneFrame = makeStreamFrame();
|
||||
for (size_t f = 0; f < NUM_FRAMES; f++)
|
||||
allSyms.insert(allSyms.end(), oneFrame.begin(), oneFrame.end());
|
||||
|
||||
// Trailing silence so the last frame can finish processing
|
||||
for (size_t i = 0; i < PREAMBLE_SYMS; i++)
|
||||
allSyms.push_back(0);
|
||||
|
||||
std::vector<int16_t> baseband = rrcBaseband(allSyms);
|
||||
|
||||
// --- Feed samples through the demodulator ---
|
||||
M17::Demodulator demod;
|
||||
demod.init();
|
||||
|
||||
bool everLocked = false;
|
||||
size_t lockSample = 0;
|
||||
bool lostLock = false;
|
||||
size_t lostSample = 0;
|
||||
|
||||
for (size_t i = 0; i < baseband.size(); i++) {
|
||||
demod.sample(baseband[i]);
|
||||
|
||||
if (!everLocked && demod.isLocked()) {
|
||||
everLocked = true;
|
||||
lockSample = i;
|
||||
}
|
||||
|
||||
// Once locked, the demodulator must stay locked for at least the
|
||||
// duration of the remaining frames (until the signal ends).
|
||||
// Allow a grace zone at the very end where the trailing silence
|
||||
// causes a natural unlock (last ~3 frame-lengths).
|
||||
size_t endGrace = baseband.size()
|
||||
- 3 * M17::FRAME_SYMBOLS * SAMPLES_PER_SYM;
|
||||
if (everLocked && !demod.isLocked() && i < endGrace) {
|
||||
lostLock = true;
|
||||
lostSample = i;
|
||||
break; // No need to keep going
|
||||
}
|
||||
}
|
||||
|
||||
INFO("Lock first acquired at sample " << lockSample);
|
||||
REQUIRE(everLocked);
|
||||
|
||||
INFO("Lock lost at sample " << lostSample << " ("
|
||||
<< (lostSample - lockSample)
|
||||
<< " samples after lock)");
|
||||
REQUIRE_FALSE(lostLock);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue