mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-29 10:22:22 +00:00
Persist unchanged Companion channel route SETs before OK
Require a real raw-table save for same-value channel SETs. This repairs the false acknowledgement after successful constructor recovery of an older backup followed by a failed candidate write. Keep resolved alias keys, raw AUTO policy and runtime generation untouched. Preserve GET and unchanged contact behavior, changed-policy validation and indeterminate-publication quarantine. Cover real stale-backup creation, failure/retry, reboot and exact old-skip controls. Add actual alias/BaseChatMesh and raw app-key regression tests on all four backends. Verified 117 related host tests, 16 final caller rechecks, four firmware qualifications and linked nRF52 crypto checks.
This commit is contained in:
1 parent
24b1264a0e
commit
4b81a9d5e0
6 files changed
+340
-16
No files matched your search
@@ -323,6 +323,7 @@ jobs:
|
||||
python3 -B test/test_companion_channel_alias.py -v
|
||||
python3 -B test/test_companion_tx_routing.py -v
|
||||
python3 -B test/test_companion_channel_publication.py -v
|
||||
python3 -B test/test_companion_tx_raw_alias.py -v
|
||||
python3 -B test/test_companion_traffic_routing.py -v
|
||||
python3 -B test/test_ethernet_kiss.py -v
|
||||
python3 -B test/test_ethernet_radio.py -v
|
||||
|
||||
@@ -3744,13 +3744,17 @@ bool MyMesh::handleTxRoutingCommand(const char* command, char* reply, size_t rep
|
||||
return true;
|
||||
}
|
||||
uint8_t policy = parsed.channel ? channel.channel.tx_radio : contact->tx_radio;
|
||||
if (parsed.set && policy != parsed.policy) {
|
||||
bool saved;
|
||||
if (parsed.channel) {
|
||||
if (parsed.set) {
|
||||
bool saved = true;
|
||||
if (policy == parsed.policy) {
|
||||
// A prior recovery may have restored older disk data. Save the raw
|
||||
// table without mutating a resolved alias or its runtime generation.
|
||||
if (parsed.channel) saved = saveChannels();
|
||||
} else if (parsed.channel) {
|
||||
const auto alias = parseCompanionChannelAlias(channel.name);
|
||||
if (isAliasRadio2Defined()
|
||||
&& (alias.kind == CompanionChannelKind::Radio2 || alias.kind == CompanionChannelKind::Meshtext)) {
|
||||
snprintf(reply, reply_size, "Error: channel prefix controls routing; rename it to change routing");
|
||||
snprintf(reply, reply_size, "Error: prefix controls routing; rename channel");
|
||||
return true;
|
||||
}
|
||||
// Persist the app PSK, not the resolved wire-key copy used for reporting.
|
||||
|
||||
+107
-5
@@ -19,7 +19,7 @@ static void failPublicObservationAfterPublish(MyMesh& node) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static void publication_uncertainty_cannot_acknowledge_a_rolled_back_value() {
|
||||
static void publication_uncertainty_quarantines_further_channel_writes() {
|
||||
MyMesh node;
|
||||
seed(node);
|
||||
const auto old_image = node.store.fs.files.at("/channels2");
|
||||
@@ -34,8 +34,8 @@ static void publication_uncertainty_cannot_acknowledge_a_rolled_back_value() {
|
||||
assert(!node.store.hasIncompleteContactLoad());
|
||||
const unsigned writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio", reply, sizeof(reply)));
|
||||
// Exact regression invariant: without the publication latch this same-value
|
||||
// branch returns OK without saving, although a reboot restores BOTH.
|
||||
// Indeterminate publication must keep channel writes quarantined, including
|
||||
// a new request for the caller's rolled-back live value.
|
||||
assert(strstr(reply, "storage unavailable"));
|
||||
assert(node.writes == writes && node.store.hasIncompleteChannelLoad());
|
||||
assert(node.store.fs.files.at("/channels2") == published);
|
||||
@@ -62,12 +62,64 @@ static void prepublication_failures_remain_retryable() {
|
||||
node.store.fs.fail_write = false;
|
||||
const unsigned writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == writes);
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == writes + 1);
|
||||
assert(last_channel_outcome == 2);
|
||||
assert(node.store.fs.files.at("/channels2")[0] == mesh::encodeRadioTxPolicy(mesh::RADIO_TX_PRIMARY));
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 both", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && last_channel_outcome == 2);
|
||||
}
|
||||
|
||||
static void unchanged_channel_sets_require_durability_without_mutation() {
|
||||
MyMesh node;
|
||||
seed(node);
|
||||
const auto old_image = node.store.fs.files.at("/channels2");
|
||||
const unsigned sets = node.channel_sets, generation = node._ethernet_channel_generation;
|
||||
node._mt_delivery_config_dirty = false;
|
||||
char reply[160];
|
||||
node.store.fs.fail_write = true;
|
||||
const unsigned failed_writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio", reply, sizeof(reply)));
|
||||
assert(strstr(reply, "not saved") && node.writes == failed_writes + 1);
|
||||
assert(last_channel_outcome == 1 && !node.store.hasIncompleteChannelLoad());
|
||||
assert(node.store.fs.files.at("/channels2") == old_image);
|
||||
node.store.fs.fail_write = false;
|
||||
const unsigned retry_writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == retry_writes + 1);
|
||||
assert(last_channel_outcome == 2);
|
||||
assert(node.store.fs.files.at("/channels2") == old_image);
|
||||
assert(node.channel_sets == sets && node._ethernet_channel_generation == generation);
|
||||
assert(!node._mt_delivery_config_dirty);
|
||||
const unsigned read_writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("get tx.channel 0", reply, sizeof(reply)));
|
||||
assert(reply[0] == '>' && node.writes == read_writes);
|
||||
assert(node.handleTxRoutingCommand("set tx.user \"Healthy contact\" auto", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == read_writes);
|
||||
}
|
||||
|
||||
static void unchanged_alias_sets_save_the_raw_policy_and_key() {
|
||||
MyMesh node;
|
||||
seed(node);
|
||||
strcpy(node.channels[1].name, "#r2-emergency");
|
||||
memset(node.channels[1].channel.secret, 0x42, 32);
|
||||
node.channels[1].channel.tx_radio = mesh::RADIO_TX_AUTO;
|
||||
node.radio.config.secondary.mode = mesh::RadioProfileMode::RxTx;
|
||||
assert(node.store.saveChannels(&node));
|
||||
const auto image = node.store.fs.files.at("/channels2");
|
||||
const unsigned sets = node.channel_sets, generation = node._ethernet_channel_generation;
|
||||
node._mt_delivery_config_dirty = false;
|
||||
char reply[160];
|
||||
const unsigned writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 1 radio2", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == writes + 1);
|
||||
assert(strstr(reply, "active=radio2"));
|
||||
assert(node.store.fs.files.at("/channels2") == image);
|
||||
assert(image[68] == mesh::encodeRadioTxPolicy(mesh::RADIO_TX_AUTO));
|
||||
assert(image[68 + 36] == 0x42);
|
||||
assert(node.channel_sets == sets && node._ethernet_channel_generation == generation);
|
||||
assert(!node._mt_delivery_config_dirty);
|
||||
}
|
||||
|
||||
static void binary_psk_rollback_preserves_live_communications_but_blocks_writes() {
|
||||
MyMesh node;
|
||||
seed(node);
|
||||
@@ -97,6 +149,51 @@ static void binary_psk_rollback_preserves_live_communications_but_blocks_writes(
|
||||
}
|
||||
|
||||
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
static void restored_stale_backup_requires_a_new_commit_before_acknowledgement() {
|
||||
for (bool fail_again : {false, true}) {
|
||||
MyMesh node;
|
||||
seed(node);
|
||||
const auto old_image = node.store.fs.files.at("/channels2");
|
||||
char reply[160];
|
||||
node.store.fs.fail_remove = true; // Retain old backup after successful publish.
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 both", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && last_channel_outcome == 2);
|
||||
assert(node.store.fs.files.at("/channels2.bak") == old_image);
|
||||
node.store.fs.fail_remove = false;
|
||||
node.store.fs.files.erase("/channels2"); // Loss of just the active metadata entry.
|
||||
node.store.fs.fail_write = true;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 off", reply, sizeof(reply)));
|
||||
assert(strstr(reply, "not saved") && last_channel_outcome == 1);
|
||||
assert(!node.store.hasIncompleteChannelLoad());
|
||||
assert(node.channels[0].channel.tx_radio == mesh::RADIO_TX_BOTH);
|
||||
assert(node.store.fs.files.at("/channels2") == old_image);
|
||||
node.store.fs.fail_write = fail_again;
|
||||
const unsigned sets = node.channel_sets, generation = node._ethernet_channel_generation;
|
||||
node._mt_delivery_config_dirty = false;
|
||||
const unsigned writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 both", reply, sizeof(reply)));
|
||||
if (fail_again) {
|
||||
assert(strstr(reply, "not saved") && node.writes == writes + 1);
|
||||
assert(node.store.fs.files.at("/channels2") == old_image);
|
||||
assert(!node.store.hasIncompleteChannelLoad());
|
||||
node.store.fs.fail_write = false;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 both", reply, sizeof(reply)));
|
||||
}
|
||||
assert(!strncmp(reply, "OK", 2));
|
||||
// Exact negative-control invariant: old same-value SET skipped persistence
|
||||
// and falsely acknowledged BOTH while this public record still held RADIO.
|
||||
assert(mesh::decodeRadioTxPolicy(node.store.fs.files.at("/channels2")[0]) == mesh::RADIO_TX_BOTH);
|
||||
assert(node.writes == writes + (fail_again ? 2 : 1));
|
||||
assert(last_channel_outcome == 2);
|
||||
assert(node.channel_sets == sets && node._ethernet_channel_generation == generation);
|
||||
assert(!node._mt_delivery_config_dirty);
|
||||
MyMesh reboot;
|
||||
reboot.store.fs.files = node.store.fs.files;
|
||||
reboot.store.loadChannels(&reboot);
|
||||
assert(reboot.channels[0].channel.tx_radio == mesh::RADIO_TX_BOTH);
|
||||
}
|
||||
}
|
||||
|
||||
static void known_rollback_is_retryable_but_failed_rollback_is_quarantined() {
|
||||
for (bool rollback_fails : {false, true}) {
|
||||
MyMesh node;
|
||||
@@ -148,8 +245,13 @@ static void constructor_recovery_uncertainty_is_quarantined_without_a_commit() {
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
restored_stale_backup_requires_a_new_commit_before_acknowledgement();
|
||||
#endif
|
||||
prepublication_failures_remain_retryable();
|
||||
publication_uncertainty_cannot_acknowledge_a_rolled_back_value();
|
||||
unchanged_channel_sets_require_durability_without_mutation();
|
||||
unchanged_alias_sets_save_the_raw_policy_and_key();
|
||||
publication_uncertainty_quarantines_further_channel_writes();
|
||||
binary_psk_rollback_preserves_live_communications_but_blocks_writes();
|
||||
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
known_rollback_is_retryable_but_failed_rollback_is_quarantined();
|
||||
|
||||
@@ -12,7 +12,7 @@ from test_companion_preferences_transaction import esp_recovery_helpers
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def publication_fixture(work, missing_latch):
|
||||
def publication_fixture(work, missing_latch, skip_noop_save=False):
|
||||
store = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
|
||||
header = (ROOT / "examples/companion_radio/DataStore.h").read_text()
|
||||
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
@@ -74,7 +74,18 @@ def publication_fixture(work, missing_latch):
|
||||
errno = EIO;
|
||||
return -1; // ESP metadata errors remain distinguishable from absence.
|
||||
}""", 1)
|
||||
methods = recovery + "\n" + method(mesh, "bool MyMesh::handleTxRoutingCommand(")
|
||||
route = method(mesh, "bool MyMesh::handleTxRoutingCommand(")
|
||||
if skip_noop_save:
|
||||
# Restore exactly the pre-fix outer changed-policy condition and skip
|
||||
# the new unchanged-channel branch; all writer and latch code remains.
|
||||
opening = route.index(" if (parsed.set) {", route.index(" uint8_t policy ="))
|
||||
changed = " } else if (parsed.channel) {"
|
||||
changed_end = route.index(changed, opening) + len(changed)
|
||||
assert "if (parsed.channel) saved = saveChannels();" in route[opening:changed_end]
|
||||
route = (route[:opening]
|
||||
+ " if (parsed.set && policy != parsed.policy) {\n bool saved;\n if (parsed.channel) {"
|
||||
+ route[changed_end:])
|
||||
methods = recovery + "\n" + route
|
||||
methods += "\n" + method(store, "void DataStore::loadChannels(")
|
||||
methods += "\n" + save + "\n" + method(store, "bool DataStore::hasIncompleteContactLoad(")
|
||||
binary_start = mesh.index(" } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) {")
|
||||
@@ -131,10 +142,10 @@ static bool contactPathPresence(FILESYSTEM* fs, const char* path, bool& present,
|
||||
|
||||
|
||||
class CompanionChannelPublicationTest(unittest.TestCase):
|
||||
def run_platform(self, platform, missing_latch=False):
|
||||
def run_platform(self, platform, missing_latch=False, skip_noop_save=False):
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-channel-publication-") as directory:
|
||||
work = Path(directory)
|
||||
fixture = publication_fixture(work, missing_latch)
|
||||
fixture = publication_fixture(work, missing_latch, skip_noop_save)
|
||||
binary = work / "test"
|
||||
compiled = subprocess.run([
|
||||
"g++", "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-D" + platform + "=1",
|
||||
@@ -146,7 +157,10 @@ class CompanionChannelPublicationTest(unittest.TestCase):
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
if missing_latch:
|
||||
if skip_noop_save:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('mesh::decodeRadioTxPolicy(node.store.fs.files.at("/channels2")[0]) == mesh::RADIO_TX_BOTH', result.stderr)
|
||||
elif missing_latch:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('strstr(reply, "storage unavailable")', result.stderr)
|
||||
else:
|
||||
@@ -177,6 +191,12 @@ class CompanionChannelPublicationTest(unittest.TestCase):
|
||||
def test_missing_rp2040_publication_latch_is_detected(self):
|
||||
self.run_platform("RP2040_PLATFORM", missing_latch=True)
|
||||
|
||||
def test_esp32_old_same_value_skip_after_stale_backup_is_detected(self):
|
||||
self.run_platform("ESP32_PLATFORM", skip_noop_save=True)
|
||||
|
||||
def test_rp2040_old_same_value_skip_after_stale_backup_is_detected(self):
|
||||
self.run_platform("RP2040_PLATFORM", skip_noop_save=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Keep actual channel aliases raw across durable same-value TX route SETs."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_companion_channel_publication import publication_fixture
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def replace_once(source, before, after):
|
||||
assert source.count(before) == 1, before
|
||||
return source.replace(before, after, 1)
|
||||
|
||||
|
||||
def raw_alias_fixture(work):
|
||||
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="ascii")
|
||||
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text(encoding="ascii")
|
||||
base = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text(encoding="ascii")
|
||||
# The alias/key helpers execute unchanged. Supply independent SHA-256
|
||||
# vectors only at their crypto boundary, including cached channel hashes.
|
||||
vectors = [b"#r2-emergency", b"#emergency", bytes(16)]
|
||||
vectors += [hashlib.sha256(name).digest()[:16] for name in vectors[:2]]
|
||||
sha = "static void sha256(uint8_t* out, size_t size, const uint8_t* input, int length) {\n"
|
||||
for raw in vectors:
|
||||
data = ",".join(str(value) for value in raw)
|
||||
digest = ",".join(str(value) for value in hashlib.sha256(raw).digest())
|
||||
sha += (" { static const uint8_t in[] = {" + data + "};\n"
|
||||
" static const uint8_t hash[] = {" + digest + "};\n"
|
||||
" if (length == int(sizeof(in)) && !memcmp(in,input,sizeof(in))) "
|
||||
"{ assert(size <= sizeof(hash)); memcpy(out,hash,size); return; } }\n")
|
||||
sha += ' assert(!"input outside independent SHA256 oracle");\n}\n'
|
||||
|
||||
generated = publication_fixture(work, False)
|
||||
code = generated.read_text(encoding="ascii")
|
||||
code = code[:code.index("int main() {")]
|
||||
code = replace_once(code, "struct Utils {", "struct Utils {\n" + sha)
|
||||
# Replace the publication fixture's simplified channel boundary with the
|
||||
# real raw accessors, alias resolution and generation-changing setter.
|
||||
for signature, declaration in (
|
||||
(" bool getChannel(int i, ChannelDetails& c)", " bool getChannel(int, ChannelDetails&);"),
|
||||
(" bool setChannel(int i, const ChannelDetails& c)", " bool setChannel(int, const ChannelDetails&);"),
|
||||
(" bool isAliasRadio2Defined() const", " bool isAliasRadio2Defined() const;"),
|
||||
(" bool resolveChannelAlias(ChannelDetails&) const", " bool resolveChannelAlias(ChannelDetails&) const;"),
|
||||
):
|
||||
code = replace_once(code, method(code, signature), declaration)
|
||||
for signature, production in (
|
||||
(" bool getChannelForSave(uint8_t i,ChannelDetails& c)", " bool getChannelForSave(uint8_t channel_idx,"),
|
||||
(" bool onChannelLoaded(uint8_t i,const ChannelDetails& c)", " bool onChannelLoaded(uint8_t channel_idx,"),
|
||||
):
|
||||
code = replace_once(code, method(code, signature), method(header, production))
|
||||
code = replace_once(code, " ChannelDetails channels[MAX_GROUP_CHANNELS];\n", "")
|
||||
code = replace_once(code, "class MyMesh : public DataStoreHost {", """
|
||||
class BaseChatMesh {
|
||||
public:
|
||||
ChannelDetails channels[MAX_GROUP_CHANNELS];
|
||||
bool getChannel(int, ChannelDetails&);
|
||||
bool setChannel(int, const ChannelDetails&);
|
||||
};
|
||||
class MyMesh : public BaseChatMesh, public DataStoreHost {""")
|
||||
code = replace_once(code, " bool writable=true, writes_ok=true; unsigned writes=0;", """
|
||||
struct { bool enabled = false; } _ethernet_settings;
|
||||
bool writable=true, writes_ok=true; unsigned writes=0;""")
|
||||
assert "unsigned channel_sets=0, _ethernet_channel_generation=0;" in code
|
||||
code += "\n" + "\n".join(method(base, signature) for signature in (
|
||||
"bool BaseChatMesh::getChannel(", "bool BaseChatMesh::setChannel("))
|
||||
code += "\n" + "\n".join(method(mesh, signature) for signature in (
|
||||
"bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(",
|
||||
"bool MyMesh::getChannel(", "bool MyMesh::setChannel("))
|
||||
code += r'''
|
||||
int main() {
|
||||
MyMesh node;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
node.store.fs.rename_replaces = true;
|
||||
#endif
|
||||
strcpy(node.channels[0].name, "#r2-emergency");
|
||||
mesh::Utils::sha256(node.channels[0].channel.secret, 16,
|
||||
reinterpret_cast<const uint8_t*>(node.channels[0].name), strlen(node.channels[0].name));
|
||||
node.channels[0].channel.tx_radio = mesh::RADIO_TX_AUTO;
|
||||
node.radio.config.secondary.mode = mesh::RadioProfileMode::RxTx;
|
||||
node.radio.config.cross = mesh::RadioCrossMode::Off;
|
||||
strcpy(node.contacts[1].name, "Healthy contact");
|
||||
node.contacts[1].tx_radio = mesh::RADIO_TX_PRIMARY;
|
||||
assert(node.store.saveChannels(&node));
|
||||
const ChannelDetails raw = node.channels[0];
|
||||
const auto original = node.store.fs.files.at("/channels2");
|
||||
uint8_t canonical_key[16];
|
||||
mesh::Utils::sha256(canonical_key, sizeof(canonical_key),
|
||||
reinterpret_cast<const uint8_t*>("#emergency"), strlen("#emergency"));
|
||||
ChannelDetails effective;
|
||||
assert(node.getChannel(0, effective));
|
||||
assert(effective.channel.tx_radio == mesh::RADIO_TX_SECONDARY);
|
||||
assert(memcmp(effective.channel.secret, raw.channel.secret, 16));
|
||||
assert(!memcmp(effective.channel.secret, canonical_key, sizeof(canonical_key)));
|
||||
char reply[160] = {};
|
||||
const auto generation = node._ethernet_channel_generation;
|
||||
auto writes = node.writes;
|
||||
node.store.fs.fail_write = true;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio2", reply, sizeof(reply)));
|
||||
assert(strstr(reply, "not saved") && node.writes == writes + 1);
|
||||
assert(!memcmp(&node.channels[0], &raw, sizeof(raw)));
|
||||
assert(node._ethernet_channel_generation == generation);
|
||||
assert(node.store.fs.files.at("/channels2") == original);
|
||||
assert(!node.store.hasIncompleteChannelLoad());
|
||||
node.store.fs.fail_write = false;
|
||||
writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio2", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == writes + 1);
|
||||
assert(!memcmp(&node.channels[0], &raw, sizeof(raw)));
|
||||
assert(node._ethernet_channel_generation == generation);
|
||||
assert(node.store.fs.files.at("/channels2") == original);
|
||||
assert(original[0] == mesh::encodeRadioTxPolicy(mesh::RADIO_TX_AUTO));
|
||||
assert(!memcmp(original.data() + 36, raw.channel.secret, 32));
|
||||
writes = node.writes;
|
||||
assert(node.handleTxRoutingCommand("get tx.channel 0", reply, sizeof(reply)));
|
||||
assert(node.handleTxRoutingCommand("set tx.user \"Healthy contact\" radio", reply, sizeof(reply)));
|
||||
assert(!strncmp(reply, "OK", 2) && node.writes == writes);
|
||||
assert(node.handleTxRoutingCommand("set tx.channel 0 radio", reply, sizeof(reply)));
|
||||
assert(strstr(reply, "prefix controls routing") && node.writes == writes);
|
||||
MyMesh reboot;
|
||||
reboot.store.fs.files = node.store.fs.files;
|
||||
reboot.radio.config.secondary = node.radio.config.secondary;
|
||||
reboot.store.loadChannels(&reboot);
|
||||
assert(reboot.channels[0].channel.tx_radio == mesh::RADIO_TX_AUTO);
|
||||
assert(!memcmp(reboot.channels[0].channel.secret, raw.channel.secret, 32));
|
||||
assert(reboot.getChannel(0, effective));
|
||||
assert(effective.channel.tx_radio == mesh::RADIO_TX_SECONDARY);
|
||||
assert(!memcmp(effective.channel.secret, canonical_key, sizeof(canonical_key)));
|
||||
puts("PASS: actual channel SET retry, raw AUTO/app PSK and effective SECONDARY/wire key");
|
||||
}
|
||||
'''
|
||||
generated.write_text(code, encoding="ascii")
|
||||
return generated
|
||||
|
||||
|
||||
class CompanionTxRawAliasTest(unittest.TestCase):
|
||||
def run_platform(self, platform):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a C++ compiler is required")
|
||||
with tempfile.TemporaryDirectory(prefix="channel-samevalue-real-alias-") as directory:
|
||||
work = Path(directory)
|
||||
generated = raw_alias_fixture(work)
|
||||
binary = work / "test"
|
||||
compiled = subprocess.run([
|
||||
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-D" + platform + "=1",
|
||||
"-DMESH_CONTACT_CACHE=0", "-DMESH_ENABLE_MESHTEXT_CHAT=0", "-DMESH_ENABLE_ETHERNET_TUNNEL=1",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
"-I", str(work), "-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
|
||||
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"), "-I", str(ROOT / "src/helpers"),
|
||||
str(generated), "-o", str(binary),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
||||
self.assertIn("PASS: actual channel SET retry", tested.stdout)
|
||||
|
||||
def test_nrf52_raw_alias_failure_and_retry(self):
|
||||
self.run_platform("NRF52_PLATFORM")
|
||||
|
||||
def test_stm32_raw_alias_failure_and_retry(self):
|
||||
self.run_platform("STM32_PLATFORM")
|
||||
|
||||
def test_esp32_raw_alias_failure_and_retry(self):
|
||||
self.run_platform("ESP32_PLATFORM")
|
||||
|
||||
def test_rp2040_raw_alias_failure_and_retry(self):
|
||||
self.run_platform("RP2040_PLATFORM")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -54,6 +54,11 @@ struct DataStore {
|
||||
@CHANNEL_METHOD@
|
||||
const char* _channel_recovery_source=nullptr;
|
||||
bool hasIncompleteContactLoad() const;
|
||||
DataStore() {
|
||||
#if defined(STM32_PLATFORM)
|
||||
fs.rename_replaces=true;
|
||||
#endif
|
||||
}
|
||||
FILESYSTEM* _getContactsChannelsFS() { return &fs; }
|
||||
File openRead(FILESYSTEM* fs, const char* name) { return fs->open(name,"r"); }
|
||||
void loadChannels(DataStoreHost*);
|
||||
@@ -68,6 +73,8 @@ public:
|
||||
DataStore store;
|
||||
DataStore* _store=&store;
|
||||
bool writable=true, writes_ok=true; unsigned writes=0;
|
||||
unsigned channel_sets=0, _ethernet_channel_generation=0;
|
||||
bool _mt_delivery_config_dirty=false;
|
||||
bool canMutateContacts() const { return writable && !store.hasIncompleteContactLoad(); }
|
||||
int getTotalContactSlots() const { return contacts.size(); }
|
||||
ContactInfo* getContactPtrByIdx(int i) { return &contacts.at(i); }
|
||||
@@ -81,7 +88,11 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool setChannel(int i, const ChannelDetails& c) { if (i<0 || i>=MAX_GROUP_CHANNELS) return false; channels[i]=c; return true; }
|
||||
bool setChannel(int i, const ChannelDetails& c) {
|
||||
if (i<0 || i>=MAX_GROUP_CHANNELS) return false;
|
||||
++channel_sets; ++_ethernet_channel_generation; _mt_delivery_config_dirty=true;
|
||||
channels[i]=c; return true;
|
||||
}
|
||||
bool saveChannels() { ++writes; return writes_ok && store.saveChannels(this); }
|
||||
bool scheduleContactWrite(const ContactInfo&) { ++writes; return writes_ok; }
|
||||
bool flushContactsBeforeReboot() { return writes_ok; }
|
||||
@@ -126,6 +137,9 @@ int main() {
|
||||
cmd("set tx.user \"Alice Jones\" radio2"); assert(!strncmp(reply,"OK",2));
|
||||
assert(strstr(reply,"active=radio2") && node.contacts[1].tx_radio==mesh::RADIO_TX_SECONDARY);
|
||||
assert(node.contacts[2].tx_radio==mesh::RADIO_TX_AUTO);
|
||||
const unsigned unchanged_contact_writes=node.writes;
|
||||
cmd("set tx.user \"Alice Jones\" radio2"); assert(!strncmp(reply,"OK",2));
|
||||
assert(node.writes==unchanged_contact_writes);
|
||||
cmd("get tx.user Alice Jones"); assert(strstr(reply,"radio2"));
|
||||
cmd("set tx.user Bob off"); assert(strstr(reply,"ambiguous"));
|
||||
cmd("set tx.user key:111111111111 off"); assert(strstr(reply,"ambiguous"));
|
||||
@@ -291,9 +305,16 @@ int main() {
|
||||
aliased.radio.config.secondary.mode=mesh::RadioProfileMode::RxTx;
|
||||
auto alias_cmd=[&](const char* text) { assert(aliased.handleTxRoutingCommand(text,reply,sizeof(reply))); };
|
||||
alias_cmd("get tx.channel 1"); assert(strstr(reply,"active=radio2"));
|
||||
assert(aliased.writes==0);
|
||||
alias_cmd("set tx.channel 1 radio"); assert(strstr(reply,"prefix controls routing"));
|
||||
assert(aliased.writes==0 && aliased.channels[1].channel.tx_radio==mesh::RADIO_TX_AUTO);
|
||||
alias_cmd("set tx.channel 1 radio2"); assert(!strncmp(reply,"OK",2) && aliased.writes==0);
|
||||
alias_cmd("set tx.channel 1 radio2"); assert(!strncmp(reply,"OK",2) && aliased.writes==1);
|
||||
assert(aliased.channel_sets==0 && aliased._ethernet_channel_generation==0);
|
||||
assert(!aliased._mt_delivery_config_dirty);
|
||||
assert(aliased.channels[1].channel.tx_radio==mesh::RADIO_TX_AUTO);
|
||||
assert(aliased.store.fs.files["/channels2"][68]==mesh::encodeRadioTxPolicy(mesh::RADIO_TX_AUTO));
|
||||
assert(aliased.store.fs.files["/channels2"][68+36]==42);
|
||||
assert(!strcmp(reinterpret_cast<const char*>(aliased.store.fs.files["/channels2"].data()+68+4),"#r2-emergency"));
|
||||
aliased.radio.config.secondary.mode=mesh::RadioProfileMode::Off;
|
||||
alias_cmd("set tx.channel 1 radio"); assert(!strncmp(reply,"OK",2));
|
||||
assert(aliased.channels[1].channel.tx_radio==mesh::RADIO_TX_PRIMARY);
|
||||
|
||||
Reference in new issue
Block a user