Add native narrow-band CW capture, ACK-first remote sessions, retained LoRa-readable pages, checked restoration and transport-safe progress. Add stock-compatible negotiated heartbeat/autosleep and recovery. Include host regressions and identity-bound HIL runners; keep unverified faster tuning HIL-only and document outstanding hardware validation.
270 lines
12 KiB
C++
270 lines
12 KiB
C++
using MResult = mesh::MorseTxResult;
|
|
using Edge = mesh::MorseTx::Edge;
|
|
|
|
template <typename Driver> static void complete_message(Driver& w) {
|
|
unsigned edges = 0;
|
|
while (w.isMorseTxActive() && !w._cw_stopping) {
|
|
g_mock_millis = w._morse.nextDeadline();
|
|
assert(w.serviceCarrierWave());
|
|
assert(++edges < 1000);
|
|
}
|
|
}
|
|
|
|
static void fresh_receive(const RadioLibWrapper& w) {
|
|
assert(!w._cw_active && !w._morse_owned && !w._cw_stopping);
|
|
assert(!w.chip.carrier && !w.board.awake && w.chip.callback);
|
|
assert(w.isInRecvMode() && w._rx_ps_armed);
|
|
assert(w._profile_refresh_required && w.calibrations);
|
|
}
|
|
|
|
int main() {
|
|
{
|
|
RadioLibWrapper w;
|
|
const auto primary = w._profiles.primary;
|
|
const auto secondary = w._profiles.secondary.params;
|
|
assert(w.startMorseTx(902.100f, 25, "SOS") == MResult::Started);
|
|
assert(w.isMorseTxActive() && w.isCarrierWaveActive());
|
|
assert(w.chip.carrier && !w.chip.callback && w.board.awake);
|
|
assert(w.morseTxWpm() == 25 && w.morseTxFrequency() == 902.100f);
|
|
assert(w.chip.frequency == 902.100f && w._cur_freq == 909.5f);
|
|
assert(w.morseTxRemainingMillis() == 1296 && w.carrierWaveRemainingMillis() == 1296);
|
|
char reply[160];
|
|
assert(mesh::handleCarrierWaveCommand(&w, "get cw2", reply, sizeof(reply)));
|
|
assert(!strcmp(reply, "> Morse TX owns radio; use get morse"));
|
|
assert(!w.isInRecvMode() && !w._rx_ps_armed && !w._nf_calib_active);
|
|
assert(w._profiles.primary == primary && w._profiles.secondary.params == secondary);
|
|
assert(!w.setTxPower(10)); // power sweeps must not key through Morse gaps
|
|
assert(w.setCarrierWave(0, 100) == mesh::RadioParamApplyResult::BUSY);
|
|
assert(w.startMorseTx(903, 25, "E") == MResult::Busy);
|
|
advance(47); assert(w.serviceCarrierWave() && w.chip.carrier);
|
|
advance(1); assert(w.serviceCarrierWave() && !w.chip.carrier);
|
|
assert(w.isMorseTxActive() && w.board.awake && !w.isInRecvMode());
|
|
assert(w.board.before == 1 && w.board.after == 1 && !w.setTxPower(10));
|
|
complete_message(w);
|
|
fresh_receive(w);
|
|
assert(w.chip.starts == 9 && w.board.before == 9 && w.board.after == 9);
|
|
assert(w._cur_dbm == 17 && w.frequency == 909.5f);
|
|
assert(w._profiles.primary == primary && w._profiles.secondary.params == secondary);
|
|
assert(!w.serviceCarrierWave());
|
|
}
|
|
// Slow/fast bounds, explicit cancellation while keyed and during a gap,
|
|
// rollover and arbitrarily delayed service never synthesize a chirp burst.
|
|
for (uint8_t speed : {1, 25, 60}) {
|
|
RadioLibWrapper w;
|
|
g_mock_millis = 0xfffffff0;
|
|
assert(w.startMorseTx(902.1f, speed, "EE") == MResult::Started);
|
|
const auto first_deadline = w._morse.nextDeadline();
|
|
g_mock_millis = first_deadline;
|
|
w.serviceCarrierWave();
|
|
assert(!w.chip.carrier && w.board.after == 1);
|
|
const auto starts = w.chip.starts;
|
|
for (int i = 0; i < 20; ++i) w.serviceCarrierWave();
|
|
assert(w.chip.starts == starts);
|
|
assert(w.stopMorseTx() == MResult::Stopped);
|
|
fresh_receive(w);
|
|
assert(w.board.before == w.board.after);
|
|
assert(w.startMorseTx(902.1f, speed, "T") == MResult::Started);
|
|
assert(w.stopMorseTx() == MResult::Stopped);
|
|
fresh_receive(w);
|
|
}
|
|
{
|
|
RadioLibWrapper w;
|
|
w.startMorseTx(902.1f, 25, "SOS");
|
|
advance(10000); w.serviceCarrierWave();
|
|
assert(!w.chip.carrier && w.chip.starts == 1 && w.board.after == 1);
|
|
assert(w._morse.nextDeadline() == g_mock_millis + 48);
|
|
w.serviceCarrierWave(); assert(w.chip.starts == 1);
|
|
g_mock_millis = w._morse.hardDeadline(); w.serviceCarrierWave();
|
|
fresh_receive(w);
|
|
assert(w.chip.starts == 1 && w.board.before == w.board.after);
|
|
}
|
|
// Every rejection is before oscillator, receive, external-PA or modem changes.
|
|
for (int reason = 0; reason < 15; ++reason) {
|
|
RadioLibWrapper w;
|
|
float frequency = 902.1f;
|
|
uint8_t speed = 25;
|
|
const char* text = "E";
|
|
MResult wanted = MResult::Busy;
|
|
if (reason == 0) w.state = STATE_TX_WAIT;
|
|
if (reason == 1) w.events.record();
|
|
if (reason == 2) w.busy = true;
|
|
if (reason == 3) w.packet = true;
|
|
if (reason == 4) { w.supported = false; wanted = MResult::Unsupported; }
|
|
if (reason == 5) { frequency = NAN; wanted = MResult::InvalidFrequency; }
|
|
if (reason == 6) { frequency = INFINITY; wanted = MResult::InvalidFrequency; }
|
|
if (reason == 7) { frequency = 99; wanted = MResult::InvalidFrequency; }
|
|
if (reason == 8) { frequency = 1001; wanted = MResult::InvalidFrequency; }
|
|
if (reason == 9) { w._params_valid = false; wanted = MResult::InvalidFrequency; }
|
|
if (reason == 10) { speed = 0; wanted = MResult::InvalidSpeed; }
|
|
if (reason == 11) { speed = uint8_t(mesh::MorseTx::MaxWpm + 1); wanted = MResult::InvalidSpeed; }
|
|
if (reason == 12) { text = "#"; wanted = MResult::InvalidText; }
|
|
if (reason == 13) { w._profiles.primary.freq = 902.1f; wanted = MResult::Overlap; }
|
|
if (reason == 14) { w._profiles.secondary.params.freq = 902.35f; wanted = MResult::Overlap; }
|
|
assert(w.startMorseTx(frequency, speed, text) == wanted);
|
|
assert(!w._cw_active && !w.chip.starts && !w.chip.standby_calls && !w.board.before);
|
|
assert(!w.tunes && !w.board.frequency_prepares);
|
|
}
|
|
{
|
|
RadioLibWrapper w;
|
|
w._profiles.secondary.params.freq = 902.1f;
|
|
w._profiles.secondary.mode = mesh::RadioProfileMode::Off;
|
|
assert(w.startMorseTx(902.1f, 25, "E") == MResult::Overlap);
|
|
assert(!w.chip.starts); // saved radio2 is reserved even while inactive
|
|
}
|
|
for (float frequency : {150.0f, 960.0f}) {
|
|
RadioLibWrapper w;
|
|
w._profiles.primary.bw = 0; w._profiles.primary.sf = 0;
|
|
w._profiles.primary.protocol = mesh::RadioProfileProtocol::Meshtext;
|
|
assert(w.startMorseTx(frequency, 25, "E") == MResult::Started);
|
|
assert(w.stopMorseTx() == MResult::Stopped);
|
|
fresh_receive(w); // unmodulated TX validates band, not cached LoRa SF/BW/protocol
|
|
}
|
|
for (float frequency : {137.0f, 1020.0f}) {
|
|
CustomSX1276Wrapper w;
|
|
assert(w.startMorseTx(frequency, 25, "E") == MResult::Started);
|
|
assert(w.stopMorseTx() == MResult::Stopped); fresh_receive(w);
|
|
}
|
|
for (uint8_t changed_profile : {0, 1}) {
|
|
RadioLibWrapper w; w.startMorseTx(902.1f, 25, "SOS");
|
|
++w._profiles.generation[changed_profile];
|
|
w.serviceCarrierWave(); fresh_receive(w);
|
|
assert(w.chip.starts == 1);
|
|
}
|
|
{
|
|
RadioLibWrapper w; w.startMorseTx(902.1f, 25, "SOS");
|
|
w._profiles.primary.freq = 902.1f; // board integrations may change tuple directly
|
|
w.serviceCarrierWave(); fresh_receive(w);
|
|
assert(w.chip.starts == 1 && w.frequency == 902.1f);
|
|
}
|
|
{
|
|
RadioLibWrapper w;
|
|
w.chip.tx_latency = 7; w.chip.standby_latency = 8;
|
|
assert(w.startMorseTx(902.1f, 60, "EE") == MResult::Started);
|
|
assert(w._morse.nextDeadline() == g_mock_millis + 20);
|
|
g_mock_millis = w._morse.nextDeadline(); w.serviceCarrierWave();
|
|
assert(!w.chip.carrier && w._morse.nextDeadline() == g_mock_millis + 60);
|
|
g_mock_millis = w._morse.nextDeadline(); w.serviceCarrierWave();
|
|
assert(w.chip.carrier && w._morse.nextDeadline() == g_mock_millis + 20);
|
|
complete_message(w); fresh_receive(w);
|
|
assert(w.chip.starts == 2);
|
|
}
|
|
{
|
|
RadioLibWrapper w; w._active_profile = 1; w.frequency = w._cur_freq = 911.5f;
|
|
w.startMorseTx(902.1f, 25, "E"); complete_message(w); fresh_receive(w);
|
|
assert(w._active_profile == 1 && w.frequency == 911.5f);
|
|
}
|
|
// An unrelated carrier is neither re-keyed nor stopped by Morse operations.
|
|
{
|
|
RadioLibWrapper w;
|
|
w.setCarrierWave(0, 1000);
|
|
assert(w.startMorseTx(902.1f, 25, "E") == MResult::Busy);
|
|
assert(w.stopMorseTx() == MResult::Stopped && w.chip.carrier && w.chip.starts == 1);
|
|
assert(w.setCarrierWave(0, 0) == mesh::RadioParamApplyResult::APPLIED);
|
|
}
|
|
for (int reason = 0; reason < 5; ++reason) {
|
|
RadioLibWrapper w;
|
|
if (reason == 0) w.board.frequency_ok = false;
|
|
if (reason == 1) w.chip.frequency_result = -705;
|
|
if (reason == 2) w.chip.power_result = -705;
|
|
if (reason == 3) w.chip.standby_result = -705;
|
|
if (reason == 4) w.chip.irq_result = -705;
|
|
assert(w.startMorseTx(902.1f, 25, "E") == MResult::Failed);
|
|
assert(!w.chip.starts && !w.board.before);
|
|
if (w._cw_active) assert(w._cw_stopping && w.board.awake);
|
|
w.board.frequency_ok = true;
|
|
w.chip.frequency_result = w.chip.power_result = w.chip.standby_result = w.chip.irq_result = 0;
|
|
if (w._cw_active) { advance(100); w.serviceCarrierWave(); }
|
|
assert(!w._cw_active && !w.board.awake && w.isInRecvMode());
|
|
}
|
|
// Native and FSK chips share fail-closed stop/recovery ownership. Both SPI
|
|
// stop operations may fail, but external transmit is still deasserted.
|
|
{
|
|
RadioLibWrapper w;
|
|
w.startMorseTx(902.1f, 25, "SOS");
|
|
w.chip.stop_result = w.chip.standby_result = -705;
|
|
assert(w.stopMorseTx() == MResult::Failed);
|
|
assert(w._cw_active && w._cw_stopping && w.board.awake && w.board.after == 1);
|
|
assert(!w.setTxPower(10) && w.startMorseTx(903, 25, "E") == MResult::Busy);
|
|
const auto stops = w.chip.stops;
|
|
advance(99); w.serviceCarrierWave(); assert(w.chip.stops == stops);
|
|
w.reset_ok = true;
|
|
advance(1); w.serviceCarrierWave(); fresh_receive(w);
|
|
assert(w.board.after == 1);
|
|
}
|
|
for (int reason : {0, 1, 2}) {
|
|
RadioLibWrapper w;
|
|
w.startMorseTx(902.1f, 25, "EE");
|
|
if (reason == 0) w.chip.standby_result = -705;
|
|
advance(48); w.serviceCarrierWave();
|
|
if (reason == 0) {
|
|
assert(!w._cw_active && !w.board.awake && !w.chip.carrier);
|
|
} else {
|
|
w.chip.start_result = -705;
|
|
if (reason == 2) w.chip.stop_result = w.chip.standby_result = -705;
|
|
advance(144); w.serviceCarrierWave();
|
|
if (reason == 2) assert(w._cw_stopping && w._cw_active && w.board.awake);
|
|
w.chip.start_result = w.chip.stop_result = w.chip.standby_result = 0;
|
|
if (w._cw_active) { advance(100); w.serviceCarrierWave(); }
|
|
fresh_receive(w);
|
|
}
|
|
assert(w.board.before == w.board.after);
|
|
}
|
|
for (int reason : {0, 1}) {
|
|
RadioLibWrapper w; w.startMorseTx(902.1f, 25, "E");
|
|
if (reason == 0) w.fail_tune = true;
|
|
else w.fail_rx_starts = 3;
|
|
assert(w.stopMorseTx() == MResult::Failed);
|
|
assert(w._cw_active && w._cw_stopping && w.board.awake && !w.chip.carrier);
|
|
w.fail_tune = false; w.fail_rx_starts = 0;
|
|
advance(100); w.serviceCarrierWave(); fresh_receive(w);
|
|
}
|
|
{
|
|
RadioLibWrapper w; w.startMorseTx(902.1f, 25, "E");
|
|
w.register_failures = 1;
|
|
assert(w.stopMorseTx() == MResult::Failed);
|
|
assert(w._cw_active && w._cw_stopping && w.board.awake && !w.chip.carrier);
|
|
assert(!w.isInRecvMode());
|
|
advance(100); w.serviceCarrierWave(); fresh_receive(w);
|
|
}
|
|
// SX1276 initializes FSK exactly once for an entire message. PA mapping and
|
|
// LoRa restoration remain production virtual hooks, not fixture equivalents.
|
|
{
|
|
CustomSX1276Wrapper w; w._cur_dbm = 14;
|
|
assert(w.startMorseTx(902.1f, 25, "SOS") == MResult::Started);
|
|
assert(w.chip.fsk && w.chip.fsk_starts == 1 && w.chip.frequency == 902.1f);
|
|
assert(w.chip.deviation == 0 && !w.chip.ook && w.chip.power == 14);
|
|
complete_message(w); fresh_receive(w);
|
|
assert(w.chip.fsk_starts == 1 && w.chip.starts == 9 && !w.chip.fsk);
|
|
assert(w.chip.morse_ramp_calls == 1);
|
|
assert(w.resets == 1 && w.chip.power == 14);
|
|
}
|
|
{
|
|
DacCarrier w; w._cur_dbm = 30;
|
|
assert(w.startMorseTx(902.1f, 25, "SOS") == MResult::Started);
|
|
assert(w.chip.power == 2 && w.gain == 130 && w.chip.carrier);
|
|
complete_message(w); fresh_receive(w);
|
|
assert(w.chip.fsk_starts == 1 && w.chip.starts == 9 && !w.chip.fsk);
|
|
assert(w.chip.power == 2 && w.gain == 130);
|
|
}
|
|
for (int reason : {0, 1, 2, 3}) {
|
|
CustomSX1276Wrapper w;
|
|
if (reason == 0) w.chip.fsk_result = -705;
|
|
if (reason == 1) w.chip.limit_result = -705;
|
|
if (reason == 2) w.chip.power_result = -705;
|
|
if (reason == 3) w.chip.ramp_result = -705;
|
|
assert(w.startMorseTx(902.1f, 25, "SOS") == MResult::Failed);
|
|
assert(!w.chip.starts && !w.chip.carrier && !w.board.before);
|
|
w.chip.power_result = 0;
|
|
if (w._cw_active) { advance(100); w.serviceCarrierWave(); }
|
|
fresh_receive(w);
|
|
}
|
|
{
|
|
CustomSX1276Wrapper w; w.startMorseTx(902.1f, 25, "SOS");
|
|
w.reset_ok = false;
|
|
assert(w.stopMorseTx() == MResult::Failed);
|
|
assert(w._cw_active && w._cw_stopping && w.board.awake && !w.chip.carrier && w.chip.fsk);
|
|
w.reset_ok = true; advance(100); w.serviceCarrierWave(); fresh_receive(w);
|
|
assert(!w.chip.fsk && w.board.before == w.board.after);
|
|
}
|
|
}
|