Files
MeshCore/test/fixtures/morse_rx_timing.cpp
mikecarper af08c02a00
Run Unit Tests / nrf52-companion-smoke (push) Canceled after 0s
Run Unit Tests / nrf52-sensor-smoke (push) Canceled after 0s
Run Unit Tests / esp32-sensor-smoke (push) Canceled after 0s
Run Unit Tests / management-reports (push) Canceled after 0s
Run Unit Tests / stm32-companion-smoke (push) Canceled after 0s
Run Unit Tests / bootloader-version (push) Canceled after 0s
Run Unit Tests / test (push) Canceled after 0s
Run Unit Tests / hosted-doc-links (push) Canceled after 0s
Build and deploy Docs site to GitHub Pages / github-pages (push) Canceled after 0s
Correct Morse human spacing and harden speed qualification
2026-10-13 05:25:07 -07:00

176 lines
5.9 KiB
C++

#include <helpers/MorseTx.h>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <string>
#include <type_traits>
// Exercise the actual timing methods independently of RF threshold acquisition.
// Production gains no test-only public API or state.
#define private public
#include <helpers/MorseRx.h>
#undef private
using mesh::MorseRx;
static_assert(std::is_same<decltype(MorseRx::_unit_q5), uint16_t>::value,
"fractional timing must reuse the 16-bit timing field");
#if defined(MESH_MORSE_HIL) && MESH_MORSE_HIL
static_assert(sizeof(MorseRx) == 4168 + sizeof(void*), "HIL decoder size changed");
#else
static_assert(sizeof(MorseRx) == 4168, "production decoder must remain 4168 bytes");
#endif
static void begin(MorseRx& rx, uint8_t wpm, uint32_t now = 100) {
assert(rx.start(wpm, 300000, now) == mesh::MorseRxResult::Started);
assert(rx._unit_q5 == uint32_t(rx._nominal_unit) * 32);
assert(rx.unitMillis() == rx._nominal_unit);
}
static void mark(MorseRx& rx, uint32_t duration) {
rx.recordMark(duration);
assert(!rx._bad_pattern && rx._symbols == 1);
rx.flushLetter();
}
static void fractional_drift_and_integer_negative_control() {
for (uint8_t wpm : {1, 25, 60, 120, 200, 240}) {
for (int difference : {-1, 1}) {
for (bool dash : {false, true}) {
MorseRx rx;
begin(rx, wpm);
const uint16_t nominal = rx._nominal_unit;
const uint16_t observed = uint16_t(int(nominal) + difference);
uint16_t old_unit = nominal;
for (unsigned count = 0; count < 64; ++count) {
old_unit = uint16_t((uint32_t(old_unit) * 7 + observed + 4) / 8);
mark(rx, uint32_t(observed) * (dash ? 3 : 1));
}
assert(old_unit == nominal); // the former whole-ms update never moves
assert(rx.unitMillis() == observed);
const int error = int(rx._unit_q5) - int(observed) * 32;
assert(error >= -4 && error <= 4);
mesh::MorseRxDiagnostics stats;
rx.diagnostics(stats);
assert(stats.unit_ms == observed); // API still reports rounded ms
}
}
}
}
static void first_ambiguous_and_warmed_borderline_marks() {
MorseRx first;
begin(first, 240);
first.recordMark(10);
first.flushLetter();
assert(std::strcmp(first.text(), "T") == 0); // no history can identify this first ambiguous mark
MorseRx trained;
begin(trained, 240);
mark(trained, 6);
assert(trained._unit_q5 == 164 && trained.unitMillis() == 5); // 5.125 ms, not 5 ms
trained.clear(); // retain timing, clear only the already captured training E
trained.recordMark(10);
trained.flushLetter();
assert(std::strcmp(trained.text(), "E") == 0);
assert(trained._unit_q5 == 168); // observed 10 is still clamped to the existing 6-ms upper limit
}
static void unchanged_bounds_and_checked_duration_limits() {
for (uint8_t wpm : {1, 25, 60, 120, 200, 240}) {
MorseRx rx;
begin(rx, wpm);
const uint16_t nominal = rx._nominal_unit;
const uint32_t lower = uint32_t(nominal) * 3 / 4;
const uint32_t upper = uint32_t(nominal) * 5 / 4;
for (unsigned count = 0; count < 64; ++count) {
mark(rx, 3 * (upper + 1));
assert(rx._unit_q5 >= lower * 32 && rx._unit_q5 <= upper * 32);
}
assert(rx.unitMillis() == upper);
for (unsigned count = 0; count < 96; ++count) {
mark(rx, nominal / 2);
assert(rx._unit_q5 >= lower * 32 && rx._unit_q5 <= upper * 32);
}
assert(rx.unitMillis() == lower);
begin(rx, wpm);
const uint16_t before = rx._unit_q5;
rx.recordMark(uint32_t(nominal) * 7);
assert(rx._bad_pattern && rx._symbols == 0 && rx._unit_q5 == before);
rx.clear();
rx.recordMark(nominal / 4);
assert(rx._symbols == 0 && rx._unit_q5 == before); // rejected sub-third-dot must not train timing
}
}
static void fractional_gap_comparisons() {
MorseRx rx;
begin(rx, 240);
rx._unit_q5 = 132; // 4.125 ms: faster than the nominal five-ms unit
rx._last_edge = 100;
rx._code = 2;
rx._symbols = 1;
rx.flushGap(108);
assert(rx.length() == 0); // eight ms is still shorter than two fractional units
rx.flushGap(109);
assert(std::strcmp(rx.text(), "E") == 0);
rx.flushGap(120);
assert(std::strcmp(rx.text(), "E") == 0); // 20 ms is shorter than five fractional units
rx.flushGap(121);
assert(std::strcmp(rx.text(), "E ") == 0);
begin(rx, 240);
rx._unit_q5 = 192; // a slower learned unit must not inflate the gap thresholds
rx._last_edge = 100;
rx._code = 2;
rx._symbols = 1;
rx.flushGap(109);
assert(rx.length() == 0);
rx.flushGap(110);
assert(std::strcmp(rx.text(), "E") == 0);
rx.flushGap(124);
assert(std::strcmp(rx.text(), "E") == 0);
rx.flushGap(125);
assert(std::strcmp(rx.text(), "E ") == 0);
}
struct SampledReceiver {
MorseRx rx;
uint32_t now;
explicit SampledReceiver(uint32_t started) : now(started) {
begin(rx, 240, now);
level(-110, 520);
}
void level(float dbm, uint32_t duration) {
for (uint32_t sample = 0; sample < duration; ++sample) rx.sample(dbm, now++);
}
};
static void actual_envelope_and_rollover() {
for (uint32_t started : {100U, 0xffffff00U}) {
SampledReceiver first(started);
first.level(-80, 10);
first.level(-110, 40);
first.rx.stop(first.now);
assert(std::strcmp(first.rx.text(), "T ") == 0);
SampledReceiver trained(started);
trained.level(-80, 6);
trained.level(-110, 20);
assert(trained.rx._unit_q5 == 164);
trained.level(-80, 10);
trained.level(-110, 40);
trained.rx.stop(trained.now);
assert(std::strcmp(trained.rx.text(), "EE ") == 0);
assert(trained.rx._unit_q5 == 168 && !trained.rx.active());
}
}
int main() {
fractional_drift_and_integer_negative_control();
first_ambiguous_and_warmed_borderline_marks();
unchanged_bounds_and_checked_duration_limits();
fractional_gap_comparisons();
actual_envelope_and_rollover();
std::printf("Q5 timing checks passed; MorseRx size: %zu bytes\n", sizeof(MorseRx));
}