Let the mesh timed OTA retry guard own byte-identical requests instead of the permanent bridge FIFO. Bind bridge ingress to its primary profile generation and preserve UART reflection/liveness protections. Verified a LoRa to ESP-NOW to LoRa update with outage recovery and install.
117 lines
5.8 KiB
Python
117 lines
5.8 KiB
Python
"""Execute real bridge admission and dispatcher/OTA lease guards without RF."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_radio_receive_contract import method
|
|
from test_transport_filter_wiring import HARNESS, PACKET
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class BridgeIngressTests(unittest.TestCase):
|
|
def test_primary_affinity_stale_queue_and_no_secondary_lease_appropriation(self):
|
|
base = (ROOT / "src/helpers/bridges/BridgeBase.cpp").read_text()
|
|
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
|
|
mesh = (ROOT / "src/Mesh.cpp").read_text()
|
|
receive = method(dispatcher, "void Dispatcher::processRecvPacket(")
|
|
guard = receive[receive.index(" const auto* profiles ="):receive.index(" const uint8_t previous_profile")]
|
|
ota_start = mesh.index(" case PAYLOAD_TYPE_OTA: {")
|
|
ota_guard = mesh[mesh.index(" if (!isAnyTempRadioActive()", ota_start):
|
|
mesh.index(" observeOtaRequestPressure(pkt);", ota_start)]
|
|
ota_guard = ota_guard.replace(" break;", " return false;")
|
|
code = HARNESS[:HARNESS.index("@METHODS@")]
|
|
code += method(base, "void BridgeBase::handleReceivedPacket(")
|
|
code += r'''
|
|
using mesh::RadioCrossMode;
|
|
using mesh::RadioProfileProtocol;
|
|
struct Radio {
|
|
mesh::RadioProfiles* state;
|
|
const mesh::RadioProfiles* profiles() const {return state;}
|
|
};
|
|
struct Dispatcher {
|
|
Radio* _radio; unsigned accepted=0,released=0;
|
|
void releasePacket(mesh::Packet*) {++released;}
|
|
void receive(mesh::Packet* pkt) {
|
|
@DISPATCH_GUARD@
|
|
++accepted;
|
|
}
|
|
bool isAnyTempRadioActive() const {
|
|
return _radio->state && (_radio->state->primary_temporary || _radio->state->secondary_temporary);
|
|
}
|
|
bool otaEligible(mesh::Packet* pkt) {
|
|
@OTA_GUARD@
|
|
return true;
|
|
}
|
|
};
|
|
int main() {
|
|
mesh::RadioProfiles profiles;
|
|
profiles.generation[0]=12;profiles.generation[1]=93;
|
|
profiles.primary_temporary=false;profiles.secondary_temporary=true;
|
|
profiles.cross=mesh::RadioCrossMode::Off;
|
|
BridgeBase bridge;bridge.setIngressProfiles(&profiles);
|
|
mesh::Packet packet;
|
|
packet.header=(PAYLOAD_TYPE_OTA<<2)|1;
|
|
packet.radio_profile=packet.radio_origin=1;
|
|
packet.radio_generation=packet.radio_origin_generation=93;
|
|
packet.radio_bound=true;packet.radio_local=true;packet.tx_radio=mesh::RADIO_TX_BOTH;
|
|
packet.radio_reply=packet.radio_reply_force=true;
|
|
bridge.handleReceivedPacket(&packet);
|
|
assert(bridge.manager.queued==1&&bridge.manager.freed==0);
|
|
assert(packet.radio_profile==0&&packet.radio_origin==0);
|
|
assert(packet.radio_generation==12&&packet.radio_origin_generation==12);
|
|
assert(!packet.radio_bound&&!packet.radio_local&&packet.tx_radio==mesh::RADIO_TX_AUTO);
|
|
assert(!packet.radio_reply&&!packet.radio_reply_force);
|
|
Radio radio{&profiles};Dispatcher dispatch{&radio};
|
|
dispatch.receive(&packet);assert(dispatch.accepted==1&&dispatch.released==0);
|
|
// A secondary-only update window is not permission to steal that lease.
|
|
assert(!dispatch.otaEligible(&packet));
|
|
profiles.primary_temporary=true;assert(dispatch.otaEligible(&packet));
|
|
// Frames already admitted with bridge.delay retain the old primary lease.
|
|
profiles.generation[0]=13;
|
|
dispatch.receive(&packet);assert(dispatch.accepted==1&&dispatch.released==1);
|
|
assert(packet.radio_generation==12&&packet.radio_origin_generation==12);
|
|
mesh::Packet fresh;fresh.header=packet.header;bridge.handleReceivedPacket(&fresh);
|
|
assert(fresh.radio_generation==13&&fresh.radio_profile==0);
|
|
dispatch.receive(&fresh);assert(dispatch.accepted==2&&dispatch.released==1);
|
|
// An explicit cross policy remains the sole opt-in to the other temp tuple.
|
|
profiles.primary_temporary=false;profiles.cross=mesh::RadioCrossMode::On;
|
|
assert(dispatch.otaEligible(&fresh));profiles.cross=mesh::RadioCrossMode::Off;
|
|
// A foreign primary tuple cannot reinterpret bridge MeshCore bytes.
|
|
profiles.primary.protocol=mesh::RadioProfileProtocol::Meshtext;
|
|
const auto queued=bridge.manager.queued;
|
|
bridge.handleReceivedPacket(&fresh);
|
|
assert(bridge.manager.queued==queued&&bridge.manager.freed==1);
|
|
// Legacy single-profile radios have no generation table; preserve that path.
|
|
bridge.setIngressProfiles(nullptr);radio.state=nullptr;
|
|
bridge.handleReceivedPacket(&fresh);assert(fresh.radio_generation==0&&!fresh.radio_local);
|
|
dispatch.receive(&fresh);assert(dispatch.accepted==3&&dispatch.released==1);
|
|
}
|
|
'''
|
|
code = code.replace("@DISPATCH_GUARD@", guard).replace("@OTA_GUARD@", ota_guard)
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-bridge-ingress-") as directory:
|
|
work = Path(directory)
|
|
(work / "Mesh.h").write_text(PACKET)
|
|
source, binary = work / "main.cpp", work / "test"
|
|
source.write_text(code)
|
|
built = subprocess.run([
|
|
os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
|
|
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
|
"-I", str(work), "-I", str(ROOT / "src"), str(source), "-o", str(binary),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
|
|
def test_every_role_bridge_configuration_binds_the_primary_profiles(self):
|
|
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
|
|
configure = method(header, "void configureBridgeFilter(")
|
|
self.assertIn("active_bridge->setIngressProfiles(_radio->profiles());", configure)
|
|
self.assertLess(configure.index("setIngressProfiles"), configure.index("#if MESH_ENABLE_FLOOD_RULE_ENGINE"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|