Fit all STM32 Companions with checked fractional preferences

Stage STM32 preference prefixes and checked footers in one atomic image, reject declared tails that encounter premature EOF, and share the typed CLI rollback helper. Use the proven Wio Mini code-layout flags on Wio E5 and Tiny Relay without changing features or memory boundaries. All four STM32 targets and RAK4631 Full pass at the full 1.18.0.1 release version length.
This commit is contained in:
mikecarper committed 2026-10-18 09:11:51 -07:00
1 parent 59b0ab93d0
commit 6ce784bbd6
8 files changed
+149 -50

No files matched your search

+44 -2
View File
@@ -130,6 +130,10 @@ int main() {
assert(store.savePrefs(prefs, 47.125, -122.25));
assert(store.fs.files.at("/new_prefs") == golden(prefs, 47.125, -122.25));
assert(!store.fs.exists("/new_prefs.tmp"));
#if defined(STM32_PLATFORM)
assert(WRITE_CALLS == std::vector<size_t>{244});
assert(store.fs.files.at("/new_prefs").size() == 244);
#endif
#if defined(NRF52_PLATFORM)
const std::vector<size_t> streaming = {@WRITE_SIZES@
#if defined(RAK_4631)
@@ -364,6 +368,43 @@ int main() {
assert(lat == 42.25 && lon == -121.125);
#if defined(STM32_PLATFORM)
// The extended STM32 image is still one atomic write. Its old prefix and
// TXR bytes come from the independent wire definition; TZF is a literal
// binary32/CRC vector, not the production footer encoder.
{
CompanionNodePrefs fractional;
assert(fractional.copyPersistedValuesFrom(changed));
fractional.tz_offset = 5.75f;
auto expected = golden(fractional, 42.25, -121.125);
expected.insert(expected.end(), {'T', 'Z', 'F', 1, 0, 0, 0xb8, 0x40, 0x7e, 0x2e});
assert(expected.size() == 254 && expected[225] == 5);
DataStore writer;
writer.fs.files["/new_prefs"] = before;
WRITE_CALLS.clear();
assert(writer.savePrefs(fractional, 42.25, -121.125));
assert(WRITE_CALLS == std::vector<size_t>{254});
assert(writer.fs.files.at("/new_prefs") == expected);
for (size_t quota = 0; quota < expected.size(); ++quota) {
for (bool readback : {false, true}) {
DataStore failed;
failed.fs.files["/new_prefs"] = before;
if (readback) failed.fs.fail_read_after = static_cast<int>(quota);
else failed.fs.fail_write_after = static_cast<int>(quota);
assert(!failed.savePrefs(fractional, 42.25, -121.125));
unchanged(failed, before);
failed.fs.fail_read_after = failed.fs.fail_write_after = -1;
WRITE_CALLS.clear();
assert(failed.savePrefs(fractional, 42.25, -121.125));
assert(WRITE_CALLS == std::vector<size_t>{254});
assert(failed.fs.files.at("/new_prefs") == expected);
++comparisons;
}
}
CompanionNodePrefs reboot;
assert(writer.loadPrefsInt("/new_prefs", reboot, lat, lon));
assert(reboot.tz_offset == 5.75f && reboot.freq == fractional.freq);
assert(reboot.tx_media == fractional.tx_media && reboot.tx_ssh == fractional.tx_ssh);
}
// A future field or wrong staging capacity must fail closed, not overrun
// the stack or silently publish a truncated/extra-byte format.
for (bool undersized : {false, true}) {
@@ -429,8 +470,9 @@ class CompanionPrefsImageTests(unittest.TestCase):
) + '\n' + save + '\n' + legacy_writer
for suffix, capacity in (('TooSmall', 235), ('TooLarge', 237)):
variant = save.replace('DataStore::savePrefs(', 'DataStore::savePrefs' + suffix + '(', 1)
self.assertIn('uint8_t image[236];', variant)
variant = variant.replace('uint8_t image[236];', f'uint8_t image[{capacity}];', 1)
prefix = 'static constexpr size_t PREFS_PREFIX_SIZE = 236;'
self.assertIn(prefix, variant)
variant = variant.replace(prefix, f'static constexpr size_t PREFS_PREFIX_SIZE = {capacity};', 1)
methods += '\n#if defined(STM32_PLATFORM)\n' + variant + '\n#endif\n'
code = HARNESS.split('int main(')[0].replace('@METHODS@', methods)
code = code.replace(' bool savePrefs(const CompanionNodePrefs&, double, double);',
+4 -4
View File
@@ -132,9 +132,9 @@ int main() {
"/new_prefs", actual, lat, lon);
assert(got == expected);
assert(READ_CALLS == expected_reads);
// The common footer reader adds one EOF probe after any recognized
// historical image; no original field read or ordering changes.
assert(AVAILABLE_CALLS == expected_available+(expected_reads.empty()?0:1));
// Footer presence follows the already validated declared size; every
// historical field read and EOF probe still matches the old reader.
assert(AVAILABLE_CALLS == expected_available);
if (length == 84 && quota == 84) {
// The original mandatory prefix has 21 reads, ending at ble_pin.
// This order is an independent wire contract, not the new table.
@@ -154,7 +154,7 @@ int main() {
#endif
// Every absent optional field observes EOF, then the completed
// image receives the existing final trailing-byte check.
assert(AVAILABLE_CALLS == optional_fields + 2);
assert(AVAILABLE_CALLS == optional_fields + 1);
}
if(got)legacy.tx_media=legacy.tx_ssh=mesh::RADIO_TX_AUTO;
equalValues(actual, legacy);
+21
View File
@@ -189,6 +189,7 @@ public:
DataStore store;
unsigned saves = 0;
bool savePrefs() { ++saves; return store.savePrefs(_prefs, 47.125, -122.25); }
@SAVE_PREFERENCE@
bool command(const char* command, char* reply) {
@GETTER@
@SETTER@
@@ -303,6 +304,13 @@ int main() {
const std::vector<uint8_t> fraction = @FIVE_SEVENTY_FIVE@;
complete.insert(complete.end(), fraction.begin(), fraction.end());
const size_t extension = sizeof(LEGACY_IMAGE);
// A declared fractional image must not silently fall back to its legacy
// byte when a stream prematurely reports EOF before the checked footers.
for (size_t cutoff : {size_t(84), extension - size_t(8), extension}) {
FORCED_EOF_CURSOR = cutoff;
rejectImage(complete, static_cast<int>(cutoff));
FORCED_EOF_CURSOR = size_t(-1);
}
// Every damaged extension byte fails atomically. Removing the complete
// optional footer is a legitimate old-format integer image, tested above.
for (size_t i = 0; i < 10; ++i) {
@@ -459,7 +467,10 @@ int main() {
code = HARNESS.split("int main(")[0].replace("@METHODS@", methods)
getter = method(mesh, ' if (strcmp(command, "get tz.offset") == 0)')
setter = method(mesh, ' if (strncmp(command, "set tz.offset ", 14) == 0)')
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
save_preference = method(header, 'template <typename T> bool savePreference(')
tests = TESTS.replace("@GETTER@", getter).replace("@SETTER@", setter)
tests = tests.replace("@SAVE_PREFERENCE@", save_preference)
values = (5.5, 5.75, -3.5, -3.75, 12.75, 13.75, -11.75, .25, -.25)
tests = tests.replace("@FRACTIONS@", ",\n ".join(
"{" + str(value) + "f," + cpp_bytes(fractional_footer(value)) + "}" for value in values))
@@ -485,6 +496,16 @@ inline char* utoa(unsigned int value,char* output,int base){
if(base!=10)abort();sprintf(output,"%u",value);return output;
}
''')
# Only the transport boundary is fault-injected: size() retains
# the complete declared image while available() can report EOF.
mock = (ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h").read_text()
mock = mock.replace("class MemoryFS;", "static size_t FORCED_EOF_CURSOR = size_t(-1);\nclass MemoryFS;", 1)
available = "size_t available() const { return size() - cursor_; }"
self.assertEqual(mock.count(available), 1)
mock = mock.replace(available,
"size_t available() const { return cursor_ >= FORCED_EOF_CURSOR ? 0 : size() - cursor_; }")
(work / "helpers").mkdir()
(work / "helpers/IdentityStore.h").write_text(mock)
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text()
(work / "ContactFileTransaction.h").write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
+23 -1
View File
@@ -16,6 +16,7 @@ PREAMBLE = r'''
#include <cstring>
#include <cstdint>
#include <initializer_list>
#include <string>
#define TBEAM_SX1262 1
#include <helpers/CLICommandUtils.h>
#include <helpers/RadioProfileCommandUtils.h>
@@ -164,6 +165,7 @@ struct MyMesh : CompanionFleetFixture {
void cancelAllFloodRetries(){}
void resetContacts(){++resets;}
bool savePrefs(){++saves;return save_ok;}
@SAVE_PREFERENCE@
int getTotalAirTime(){return 0;}
int getReceiveAirTime(){return 0;}
int getNumSentFlood(){return 0;}
@@ -288,6 +290,23 @@ int main() {
assert(node.handleCommand(command,0,reply));
assert(!strcmp(reply,"OK") && board.power_offs==previous+1);
}
// Exercise the real timezone dispatch and typed transaction helper, not a
// fixture-only setter: a failed save must preserve the exact fraction.
node.save_ok=true;
assert(node.handleCommand("set tz.offset 5.75",0,reply) && !strcmp(reply,"OK"));
assert(node._prefs.tz_offset==5.75f);
assert(node.handleCommand("get tz.offset",0,reply) && !strcmp(reply,"> 5.75"));
node.save_ok=false;
const int timezone_saves=node.saves;
assert(node.handleCommand("set tz.offset -3.5",0,reply));
assert(strstr(reply,"Error") && node.saves==timezone_saves+1);
assert(node._prefs.tz_offset==5.75f);
for(const char* bad:{"nan","inf","5.75junk","15","-12.25"}) {
const std::string command=std::string("set tz.offset ")+bad;
assert(node.handleCommand(command.c_str(),0,reply));
assert(strstr(reply,"Error") && node.saves==timezone_saves+1);
assert(node._prefs.tz_offset==5.75f);
}
}
'''
@@ -405,6 +424,9 @@ int main() {
"void MyMesh::onContactCacheFlushed()",
"bool MyMesh::handleDirectCommand(", "bool MyMesh::handleCommand(",
"void MyMesh::onCLICommandRecv("))
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
preamble = PREAMBLE.replace("@SAVE_PREFERENCE@", extract_braced(
header, "template <typename T> bool savePreference("))
for export, webconfig in ((0, False), (1, False), (0, True), (1, True)):
with self.subTest(export=export, webconfig=webconfig), tempfile.TemporaryDirectory() as temp:
binary = Path(temp) / "test"
@@ -415,7 +437,7 @@ int main() {
"-DMESH_ENABLE_LOST_REPLY=1",
*(["-DWITH_WEBCONFIG=1"] if webconfig else []),
*SANITIZER_FLAGS, "-o", str(binary),
], input=PREAMBLE+implementation+SCENARIOS, text=True, capture_output=True)
], input=preamble+implementation+SCENARIOS, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout+result.stderr)