Speed up T-Deck radio2 retunes with guarded command reuse
Reuse the confirmed LoRa modem only inside acknowledged warm continuous-RX hops and revoke that capability before modem initialization or after failures. Opt the tested T-Deck into IQ workaround reuse and an exact 8,888,888 Hz SPI setting. Keep the existing BUSY, packet-ownership and recovery checks; other boards retain their modem-cache default.
This commit is contained in:
1 parent
a944b1779d
commit
a782cc8413
6 files changed
+271
-13
No files matched your search
@@ -611,6 +611,7 @@ jobs:
|
||||
run: >-
|
||||
python3 -B -m unittest -v
|
||||
test_sx1262_profile_switch test_sx1262_batched_modulation test_sx1262_protocol_batching
|
||||
test_sx1262_modem_cache
|
||||
test_radio_chirp_math
|
||||
|
||||
- name: Verify radio lab collectors and compiled experiment contracts
|
||||
|
||||
@@ -31,8 +31,14 @@
|
||||
#define MC_SX1262_FAST_IQ_CACHE 0
|
||||
#endif
|
||||
|
||||
#if (MC_SX1262_FAST_FS || MC_SX1262_FAST_PACKET_CACHE || MC_SX1262_FAST_IQ_CACHE) && !MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#error "SX1262 SetFs/packet reuse requires the owned fast profile-switch path"
|
||||
#ifndef MC_SX1262_FAST_MODEM_CACHE
|
||||
// Reuse the LoRa modem confirmation only inside an acknowledged owned RX hop.
|
||||
// Keep this board-qualified optimization off until its hardware comparison.
|
||||
#define MC_SX1262_FAST_MODEM_CACHE 0
|
||||
#endif
|
||||
|
||||
#if (MC_SX1262_FAST_FS || MC_SX1262_FAST_PACKET_CACHE || MC_SX1262_FAST_IQ_CACHE || MC_SX1262_FAST_MODEM_CACHE) && !MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#error "SX1262 SetFs/packet/modem reuse requires the owned fast profile-switch path"
|
||||
#endif
|
||||
|
||||
#ifndef SX126X_TX_BUSY_TIMEOUT_MS
|
||||
@@ -69,6 +75,27 @@ class CustomSX1262 : public SX1262 {
|
||||
_coldStandby = true;
|
||||
_tcxoWakePending = false;
|
||||
}
|
||||
// These public initializers can change packet type before returning an
|
||||
// error. Revoke ownership before entering RadioLib, rather than waiting
|
||||
// for a later standby/reset to happen. Preserve the inherited float APIs.
|
||||
using SX1262::beginFSK;
|
||||
using SX1262::beginLRFHSS;
|
||||
int16_t begin(const ConfigLoRa_t& config) override {
|
||||
invalidateMorseModemCache();
|
||||
return SX1262::begin(config);
|
||||
}
|
||||
int16_t beginFSK(const ConfigFSK_t& config) override {
|
||||
invalidateMorseModemCache();
|
||||
return SX1262::beginFSK(config);
|
||||
}
|
||||
int16_t beginLRFHSS(const ConfigLRFHSS_t& config) override {
|
||||
invalidateMorseModemCache();
|
||||
return SX1262::beginLRFHSS(config);
|
||||
}
|
||||
int16_t setModem(ModemType_t modem) override {
|
||||
invalidateMorseModemCache();
|
||||
return SX1262::setModem(modem);
|
||||
}
|
||||
int16_t setMorseFastRssi() {
|
||||
return setSX126xMorseRssiWindow(this);
|
||||
}
|
||||
@@ -93,8 +120,10 @@ class CustomSX1262 : public SX1262 {
|
||||
bool hilUseFsOnRetune = false;
|
||||
bool hilSkipUnchangedPacket = false;
|
||||
bool hilSkipUnchangedIq = false;
|
||||
bool hilSkipConfirmedModemQuery = false;
|
||||
float hilFrequencyMHz() const { return freqMHz; }
|
||||
uint32_t hilFsEntries = 0, hilPacketSkips = 0, hilIqWorkaroundSkips = 0;
|
||||
uint32_t hilModemQuerySkips = 0;
|
||||
#endif
|
||||
|
||||
int16_t restoreTcxoAfterSleep() {
|
||||
@@ -263,8 +292,25 @@ class CustomSX1262 : public SX1262 {
|
||||
if (index == sizeof(bandwidths) / sizeof(bandwidths[0])) {
|
||||
return RADIOLIB_ERR_INVALID_BANDWIDTH;
|
||||
}
|
||||
if (getPacketType() != RADIOLIB_SX126X_PACKET_TYPE_LORA) {
|
||||
_profileSwitch.invalidateModulation();
|
||||
bool queryModem = true;
|
||||
#if MC_SX1262_FAST_MODEM_CACHE || defined(MC_SX1262_HIL)
|
||||
bool reuseModem = MC_SX1262_FAST_MODEM_CACHE != 0;
|
||||
#ifdef MC_SX1262_HIL
|
||||
reuseModem = hilSkipConfirmedModemQuery;
|
||||
#endif
|
||||
// rxValid is established only after full RX setup confirms LoRa, or by
|
||||
// resuming that same owned context. A successful owned standby, warm
|
||||
// oscillator and continuous RX are required. TX/CAD staging, ordinary
|
||||
// standby, sleep, reset, duty cycling and command errors revoke it.
|
||||
// A caller using base-qualified GODMODE/raw SPI to change packet type
|
||||
// must revoke ownership first, just as for the packet/IQ caches.
|
||||
queryModem = !reuseModem || !_profileSwitch.canResumeFast();
|
||||
#ifdef MC_SX1262_HIL
|
||||
if (!queryModem) ++hilModemQuerySkips;
|
||||
#endif
|
||||
#endif
|
||||
if (queryModem && getPacketType() != RADIOLIB_SX126X_PACKET_TYPE_LORA) {
|
||||
_profileSwitch.invalidate();
|
||||
return RADIOLIB_ERR_WRONG_MODEM;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,27 +7,31 @@ import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_modem_cache as modem_cache
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ProfileIqCacheTest(unittest.TestCase):
|
||||
def test_only_qualified_indicator_recipe_opts_in(self):
|
||||
def test_only_qualified_recipes_opt_in(self):
|
||||
opt_ins = []
|
||||
for recipe in (ROOT / "variants").glob("*/platformio.ini"):
|
||||
if re.search(r"^\s*-D\s+MC_SX1262_FAST_IQ_CACHE=1\s*$",
|
||||
recipe.read_text(), re.MULTILINE):
|
||||
opt_ins.append(recipe.relative_to(ROOT).as_posix())
|
||||
self.assertEqual(opt_ins, ["variants/sensecap_indicator-espnow/platformio.ini"])
|
||||
self.assertCountEqual(opt_ins, ["variants/sensecap_indicator-espnow/platformio.ini",
|
||||
"variants/lilygo_tdeck/platformio.ini"])
|
||||
|
||||
def compile_run(self, source):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
cpp, binary = Path(folder) / "test.cpp", Path(folder) / "test"
|
||||
cpp = Path(folder) / "test.cpp"
|
||||
binary = Path(folder) / ("test.exe" if os.name == "nt" else "test")
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
sanitizers = [] if os.name == "nt" else [
|
||||
"-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-no-pie"]
|
||||
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
||||
"-Wall", "-Wextra", "-fsanitize=address,undefined",
|
||||
"-fno-omit-frame-pointer", "-no-pie", str(cpp), "-o", str(binary)],
|
||||
"-Wall", "-Wextra", *sanitizers, str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
@@ -243,8 +247,11 @@ int main() {
|
||||
def test_unqualified_boards_keep_the_original_live_modem_query_and_iq_path(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
self.assertIn("#define MC_SX1262_FAST_IQ_CACHE 0", source)
|
||||
modulation = method(source, "int16_t setLoRaModulationParams(")
|
||||
self.assertIn("if (getPacketType() != RADIOLIB_SX126X_PACKET_TYPE_LORA)", modulation)
|
||||
self.assertIn("#define MC_SX1262_FAST_MODEM_CACHE 0", source)
|
||||
# Execute the shared production-method fixture: default-off builds must
|
||||
# query the modem, while opted-in/HIL reuse must retain all ownership,
|
||||
# initialization and failure gates. Do not depend on an if's spelling.
|
||||
modem_cache.ModemCacheTests.test_confirmed_modem_requires_owned_warm_rx_and_survives_no_failure(self)
|
||||
helper = method(source, "int16_t writeProfilePacketParams(")
|
||||
self.assertIn("return getMod()->SPIwriteStream(RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS,", helper)
|
||||
|
||||
|
||||
@@ -138,10 +138,10 @@ class RadioRegulatorTests(unittest.TestCase):
|
||||
initialize = method(source, 'bool std_init(')
|
||||
init_call = 'std_init() ? 0 : -1'
|
||||
else:
|
||||
initialize = method(source, 'int16_t begin(')
|
||||
initialize = method(source, 'int16_t begin(float')
|
||||
init_call = 'begin()'
|
||||
if chip == 'SX1262':
|
||||
morse = method(source, 'int16_t beginFSK(')
|
||||
morse = method(source, 'int16_t beginFSK(float')
|
||||
morse_call = 'beginFSK()'
|
||||
else:
|
||||
morse = method(source, 'int16_t beginMorseReceive(')
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Execute production modem-query reuse and initializer invalidation gates."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ModemCacheTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_confirmed_modem_requires_owned_warm_rx_and_survives_no_failure(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "")
|
||||
signatures = ("void invalidateMorseModemCache(", "void setProfileSwitchOptimization(",
|
||||
"int16_t begin(const ConfigLoRa_t&", "int16_t beginFSK(const ConfigFSK_t&",
|
||||
"int16_t beginLRFHSS(const ConfigLRFHSS_t&", "int16_t setModem(",
|
||||
"int16_t setLoRaModulationParams(")
|
||||
production = "\n".join(method(source, signature).replace(" override", "") for signature in signatures)
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@DEFINES@
|
||||
@STATE@
|
||||
using std::isfinite;
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_INVALID_SPREADING_FACTOR -1
|
||||
#define RADIOLIB_ERR_INVALID_CODING_RATE -2
|
||||
#define RADIOLIB_ERR_INVALID_BANDWIDTH -3
|
||||
#define RADIOLIB_ERR_WRONG_MODEM -4
|
||||
#define RADIOLIB_SX126X_PACKET_TYPE_LORA 1
|
||||
#define RADIOLIB_SX126X_LORA_BW_7_8 0
|
||||
#define RADIOLIB_SX126X_LORA_BW_10_4 8
|
||||
#define RADIOLIB_SX126X_LORA_BW_15_6 1
|
||||
#define RADIOLIB_SX126X_LORA_BW_20_8 9
|
||||
#define RADIOLIB_SX126X_LORA_BW_31_25 2
|
||||
#define RADIOLIB_SX126X_LORA_BW_41_7 10
|
||||
#define RADIOLIB_SX126X_LORA_BW_62_5 3
|
||||
#define RADIOLIB_SX126X_LORA_BW_125_0 4
|
||||
#define RADIOLIB_SX126X_LORA_BW_250_0 5
|
||||
#define RADIOLIB_SX126X_LORA_BW_500_0 6
|
||||
struct ConfigLoRa_t {};
|
||||
struct ConfigFSK_t {};
|
||||
struct ConfigLRFHSS_t {};
|
||||
using ModemType_t = int;
|
||||
struct SX1262 {
|
||||
SX1262ProfileSwitchState* cache = nullptr;
|
||||
int result = 0, initializations = 0;
|
||||
int init() {
|
||||
// The capability must be revoked BEFORE any possibly failing base call.
|
||||
assert(cache && !cache->rxValid && !cache->active);
|
||||
assert(!cache->modulationValid && !cache->packetValid);
|
||||
++initializations;
|
||||
return result;
|
||||
}
|
||||
int16_t begin(const ConfigLoRa_t&) { return init(); }
|
||||
virtual int16_t beginFSK(const ConfigFSK_t&) { return init(); }
|
||||
virtual int16_t beginLRFHSS(const ConfigLRFHSS_t&) { return init(); }
|
||||
int16_t beginFSK(float) { return beginFSK(ConfigFSK_t{}); }
|
||||
int16_t beginLRFHSS(float) { return beginLRFHSS(ConfigLRFHSS_t{}); }
|
||||
int16_t setModem(ModemType_t) { return init(); }
|
||||
};
|
||||
struct Radio : SX1262 {
|
||||
SX1262ProfileSwitchState _profileSwitch{true};
|
||||
bool _coldStandby = false, _tcxoWakePending = true;
|
||||
uint8_t spreadingFactor = 9, bandwidth = 4, codingRate = 3, ldrOptimize = 0;
|
||||
float bandwidthKhz = 125;
|
||||
bool ldroAuto = true;
|
||||
unsigned queries = 0, writes = 0;
|
||||
uint8_t packetType = 1;
|
||||
int16_t writeResult = 0;
|
||||
#ifdef MC_SX1262_HIL
|
||||
bool hilSkipConfirmedModemQuery = false;
|
||||
uint32_t hilModemQuerySkips = 0;
|
||||
#endif
|
||||
using SX1262::beginFSK;
|
||||
using SX1262::beginLRFHSS;
|
||||
Radio() { cache = &_profileSwitch; }
|
||||
uint8_t getPacketType() { ++queries; return packetType; }
|
||||
int16_t setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) {
|
||||
assert(spreadingFactor == sf && bandwidth == bw && codingRate == cr);
|
||||
ldrOptimize = ldroAuto ? ((1U << sf) / bandwidthKhz >= 16.0f) : ldro;
|
||||
++writes;
|
||||
return writeResult;
|
||||
}
|
||||
@PRODUCTION@
|
||||
};
|
||||
bool skipsModem(const Radio& r) {
|
||||
#ifdef MC_SX1262_HIL
|
||||
return r.hilSkipConfirmedModemQuery;
|
||||
#else
|
||||
(void)r;
|
||||
return MC_SX1262_FAST_MODEM_CACHE != 0;
|
||||
#endif
|
||||
}
|
||||
void selectCache(Radio& r, bool enabled) {
|
||||
#ifdef MC_SX1262_HIL
|
||||
r.hilSkipConfirmedModemQuery = enabled;
|
||||
#else
|
||||
(void)r; (void)enabled;
|
||||
#endif
|
||||
}
|
||||
void prime(Radio& r) {
|
||||
// Models the full RX path's successful live LoRa confirmation. The ordinary
|
||||
// lifecycle tests separately execute startReceive's real confirmation path.
|
||||
r._profileSwitch.end(true);
|
||||
r._profileSwitch.rxResult(0, true);
|
||||
r._profileSwitch.begin(true, true);
|
||||
r._profileSwitch.standbyResult(0);
|
||||
}
|
||||
int main() {
|
||||
for (int requested = 0; requested < 2; ++requested) {
|
||||
Radio r; selectCache(r, requested);
|
||||
// No existing RX context: even an opted-in image must perform the query.
|
||||
assert(r.setLoRaModulationParams(125, 9, 7) == 0 && r.queries == 1);
|
||||
prime(r);
|
||||
assert(r.setLoRaModulationParams(125, 9, 7) == 0 && r.writes == 1);
|
||||
assert(r.queries == (skipsModem(r) ? 1U : 2U));
|
||||
// A changed modulation tuple still writes the complete tuple. Only the
|
||||
// confirmed packet-type read may disappear.
|
||||
assert(r.setLoRaModulationParams(500, 8, 5) == 0 && r.writes == 2);
|
||||
assert(r.queries == (skipsModem(r) ? 1U : 3U));
|
||||
#ifdef MC_SX1262_HIL
|
||||
assert(r.hilModemQuerySkips == (requested ? 2U : 0U));
|
||||
#endif
|
||||
// Every ownership gate independently restores the live query.
|
||||
for (int gate = 0; gate < 9; ++gate) {
|
||||
Radio x; selectCache(x, requested); prime(x);
|
||||
switch (gate) {
|
||||
case 0: x._profileSwitch.end(true); break;
|
||||
case 1: x._profileSwitch.begin(true, true); break; // standby missing
|
||||
case 2: x._profileSwitch.begin(false, true); break; // discontinuous RX
|
||||
case 3: x._profileSwitch.begin(true, false); break; // cold oscillator
|
||||
case 4: x.setProfileSwitchOptimization(false); break;
|
||||
case 5: x._profileSwitch.rxResult(-707, true); break;
|
||||
case 6: x._profileSwitch.standbyResult(-707); break;
|
||||
case 7: x._profileSwitch.end(false); break;
|
||||
case 8: x.invalidateMorseModemCache(); break;
|
||||
}
|
||||
assert(x.setLoRaModulationParams(125, 9, 7) == 0 && x.queries == 1);
|
||||
#ifdef MC_SX1262_HIL
|
||||
assert(x.hilModemQuerySkips == 0);
|
||||
#endif
|
||||
}
|
||||
// Failed modulation must revoke query reuse before the restoration path.
|
||||
Radio failed; selectCache(failed, requested); prime(failed);
|
||||
failed.writeResult = -707;
|
||||
assert(failed.setLoRaModulationParams(500, 8, 5) == -707);
|
||||
assert(!failed._profileSwitch.rxValid && failed._profileSwitch.failed());
|
||||
const auto before = failed.queries;
|
||||
failed.writeResult = 0;
|
||||
assert(failed.setLoRaModulationParams(125, 9, 7) == 0);
|
||||
assert(failed.queries == before + 1);
|
||||
|
||||
// A live wrong-modem/error-byte observation revokes the WHOLE context.
|
||||
// Reopening it without a fresh full RX confirmation cannot restore reuse.
|
||||
Radio wrong; selectCache(wrong, requested); prime(wrong);
|
||||
wrong._profileSwitch.end(true); wrong.packetType = 255;
|
||||
assert(wrong.setLoRaModulationParams(125, 9, 7) == -4 && wrong.queries == 1);
|
||||
assert(!wrong._profileSwitch.rxValid && !wrong._profileSwitch.modulationValid);
|
||||
wrong._profileSwitch.begin(true, true); wrong._profileSwitch.standbyResult(0);
|
||||
wrong.packetType = 1;
|
||||
assert(wrong.setLoRaModulationParams(125, 9, 7) == 0 && wrong.queries == 2);
|
||||
}
|
||||
// Public modem initialization calls invalidate before success AND failure,
|
||||
// including inherited float overloads dispatching to the config overrides.
|
||||
for (int api = 0; api < 6; ++api) for (int failure = 0; failure < 2; ++failure) {
|
||||
Radio r; selectCache(r, true); prime(r);
|
||||
r._profileSwitch.modulationResult(0, 9, 4, 3, 0);
|
||||
r._profileSwitch.packetResult(0, 32, 1, 255, 0, 0);
|
||||
r.result = failure ? -707 : 0;
|
||||
int rc = 0;
|
||||
switch (api) {
|
||||
case 0: rc = r.begin(ConfigLoRa_t{}); break;
|
||||
case 1: rc = r.beginFSK(ConfigFSK_t{}); break;
|
||||
case 2: rc = r.beginLRFHSS(ConfigLRFHSS_t{}); break;
|
||||
case 3: rc = r.setModem(0); break;
|
||||
case 4: rc = r.beginFSK(910.0f); break;
|
||||
case 5: rc = r.beginLRFHSS(910.0f); break;
|
||||
}
|
||||
assert(rc == r.result && r.initializations == 1);
|
||||
assert(r._coldStandby && !r._tcxoWakePending);
|
||||
assert(r.setLoRaModulationParams(125, 9, 7) == 0 && r.queries == 1);
|
||||
}
|
||||
}
|
||||
'''
|
||||
for policy in ("#define MC_SX1262_FAST_MODEM_CACHE 0",
|
||||
"#define MC_SX1262_FAST_MODEM_CACHE 1",
|
||||
"#define MC_SX1262_FAST_MODEM_CACHE 0\n#define MC_SX1262_HIL 1"):
|
||||
with self.subTest(policy=policy):
|
||||
self.compile_run(fixture.replace("@DEFINES@", policy)
|
||||
.replace("@STATE@", state).replace("@PRODUCTION@", production))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,9 @@ build_flags =
|
||||
-D PIN_USER_BTN=0 ; Trackball button
|
||||
-D PIN_PERF_POWERON=10 ; Peripheral power pin
|
||||
-D USE_SX1262
|
||||
-D MC_SX1262_SPI_HZ=8888888
|
||||
-D MC_SX1262_FAST_MODEM_CACHE=1
|
||||
-D MC_SX1262_FAST_IQ_CACHE=1
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
|
||||
Reference in new issue
Block a user