Files
MeshCore/test/test_meshtext_transport.py

355 lines
18 KiB
Python

"""Run the actual Dispatcher foreign-frame ownership and isolation contract."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
from test_radio_profile_scan import HARNESS as SCAN_HARNESS
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <Dispatcher.h>
#include <helpers/StaticPoolPacketManager.h>
#include <cassert>
#include <cstring>
#include <vector>
using namespace mesh;
class Clock : public MillisecondClock {
public: unsigned long now=1000; unsigned long getMillis() override { return now; }
};
class TestRadio : public Radio {
public:
RadioProfiles p;
uint8_t active=0;
bool supported=true, receiving=false, complete=false, sending=false, start_ok=true, carrier=false;
bool incoming=false;
unsigned sends=0, finishes=0, recovery=0, processed=0;
RadioParamApplyResult prepare=RadioParamApplyResult::APPLIED;
std::vector<uint8_t> sent;
TestRadio() {
p.primary.freq=909.5f; p.primary.bw=500; p.primary.sf=5; p.primary.cr=5;
p.secondary.params=p.primary; p.secondary.params.protocol=RadioProfileProtocol::Meshtext;
p.secondary.params.preamble=16; p.secondary.mode=RadioProfileMode::RxTx;
}
RadioProfiles* profiles() override { return &p; }
const RadioProfiles* profiles() const override { return &p; }
bool supportsMeshtext() const override { return supported; }
uint8_t receiveProfile() const override { return active; }
int recvRaw(uint8_t* bytes,int) override {
if (!incoming) return 0;
incoming=false; bytes[0]=0x11;bytes[1]=0;bytes[2]=0x23;return 3;
}
void onReceiveProcessed() override { ++processed; }
uint32_t getEstAirtimeFor(int) override { return 100; }
uint32_t getProfileAirtime(uint8_t,int,uint8_t=0) override { return 100; }
float packetScore(float,int) override { return 1; }
RadioParamApplyResult prepareTransmitProfile(uint8_t n) override {
if (prepare==RadioParamApplyResult::APPLIED) active=n;
return prepare;
}
bool startSendRaw(const uint8_t* bytes,int len) override {
++sends;sent.assign(bytes,bytes+len);sending=start_ok;complete=false;return start_ok;
}
bool isSendComplete() override { return complete; }
void onSendFinished() override { ++finishes;sending=false; }
bool isInRecvMode() const override { return !sending; }
bool isReceiving() override { return receiving; }
bool isCarrierWaveActive() const override { return carrier; }
bool recoverRadio(bool) override { ++recovery;return true; }
};
class TestDispatcher : public Dispatcher {
public:
unsigned foreign_rx=0, mc_rx=0, raw_logs=0, mc_sent=0;
bool allow_foreign=true;
unsigned long duty_window=3600000;
std::vector<bool> done;
TestDispatcher(TestRadio& r, Clock& c, StaticPoolPacketManager& m):Dispatcher(r,c,m) {}
using Dispatcher::queueForeignRadioFrame;
using Dispatcher::hasForeignRadioFrame;
using Dispatcher::setRadioAvailable;
using Dispatcher::getTransmitProfileMask;
using Dispatcher::getNextQueueWakeDelay;
DispatcherAction onRecvPacket(Packet*) override { ++mc_rx;return ACTION_RELEASE; }
void logRxRaw(float,float,const uint8_t*,int) override { ++raw_logs; }
bool onForeignRadioFrame(const uint8_t* bytes,size_t length,uint8_t profile,uint32_t generation,float,float) override {
assert(length==3 && bytes[0]==0x11 && profile==1 && generation);++foreign_rx;return false;
}
void onForeignRadioSendComplete(bool ok) override { done.push_back(ok); }
bool allowForeignRadioTransmit() const override { return allow_foreign; }
unsigned long getDutyCycleWindowMs() const override { return duty_window; }
void onSendComplete(Packet*) override { ++mc_sent; }
bool queue(const uint8_t* b,size_t n=3) { return queueForeignRadioFrame(b,n,1,_radio->profiles()->generation[1]); }
};
struct Fixture {
Clock clock;TestRadio radio;StaticPoolPacketManager manager{8};TestDispatcher d{radio,clock,manager};
Fixture() { d.begin(); }
void tick(unsigned delta=1) { clock.now+=delta;d.loop(); }
};
static const uint8_t bytes[]={0x11,0,0x23};
static Packet* newPacket(TestDispatcher& d,uint8_t policy=RADIO_TX_BOTH) {
auto* p=d.obtainNewPacket();assert(p);p->header=ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT<<2);
p->tx_radio=policy;p->payload[0]=0x12;p->payload_len=1;return p;
}
int main() {
{ Fixture f;assert(f.d.queue(bytes));assert(f.d.hasOutbound());assert(!f.d.queue(bytes));
uint32_t delay=99;assert(f.d.getNextQueueWakeDelay(delay) && delay==0);
f.tick();assert(f.radio.sends==1 && f.radio.active==1 && f.radio.sent==std::vector<uint8_t>(bytes,bytes+3));
f.tick(20);assert(f.radio.sends==1 && f.d.done.empty());
f.radio.complete=true;f.tick();assert(f.d.done.size()==1 && f.d.done[0]);
assert(!f.d.hasOutbound() && f.radio.finishes==1 && f.radio.p.tx_packets[1]==1);
assert(f.d.getNumSentFlood()==0 && f.d.getNumSentDirect()==0 && f.d.getTotalAirTime()==21);
}
{ Fixture f;f.radio.active=1;f.radio.incoming=true;f.tick();
assert(f.d.foreign_rx==1 && !f.d.mc_rx && !f.d.raw_logs && f.d.getLastMeshCoreRecvMillis()==0);
assert(f.radio.processed==1 && f.d.getNumRecvFlood()==0 && f.radio.p.rx_packets[1]==1);
f.radio.p.secondary.mode=RadioProfileMode::Off;f.radio.incoming=true;f.tick();
assert(f.d.foreign_rx==1 && !f.d.raw_logs && !f.d.mc_rx);
}
{ Fixture f;assert(f.d.queue(bytes));++f.radio.p.generation[1];f.tick();
assert(f.radio.sends==0 && f.d.done.size()==1 && !f.d.done[0] && !f.d.hasOutbound());
}
{ Fixture f;f.radio.start_ok=false;assert(f.d.queue(bytes));f.tick();f.tick(2000);
assert(f.radio.sends==1 && f.d.done.size()==1 && !f.d.done[0] && !f.d.hasOutbound());
}
{ Fixture f;assert(f.d.queue(bytes));f.tick();f.tick(1151);
assert(f.radio.sends==1 && f.radio.finishes==1 && f.radio.recovery==1);
assert(f.d.done.size()==1 && !f.d.done[0] && !f.d.hasOutbound());f.tick(10000);assert(f.radio.sends==1);
}
{ Fixture f;assert(f.d.queue(bytes));f.tick();f.d.setRadioAvailable(false);f.tick(10000);
assert(f.radio.sends==1 && f.radio.finishes==1 && f.d.done.size()==1 && !f.d.done[0]);
assert(!f.d.queue(bytes) && !f.d.hasOutbound());
}
{ Fixture f;assert(f.d.queue(bytes));f.tick();++f.radio.p.generation[1];f.radio.complete=true;f.tick();
assert(f.d.done.size()==1 && !f.d.done[0] && f.radio.finishes==1);
}
{ Fixture f;f.radio.prepare=RadioParamApplyResult::FAILED;assert(f.d.queue(bytes));f.tick();
assert(f.radio.sends==0 && f.d.done.size()==1 && !f.d.done[0]);
}
{ Fixture f;assert(f.d.queue(bytes));f.d.allow_foreign=false;f.tick();
assert(f.radio.sends==0 && f.d.done.size()==1 && !f.d.done[0] && !f.d.queue(bytes));
}
{ Fixture f;assert(f.d.queue(bytes));f.tick();f.d.begin();
assert(f.radio.finishes==1 && f.d.done.size()==1 && !f.d.done[0] && !f.d.hasOutbound());
}
{ Fixture f;f.radio.prepare=RadioParamApplyResult::BUSY;assert(f.d.queue(bytes));f.tick();f.tick(8151);
assert(f.radio.sends==0 && f.d.done.size()==1 && !f.d.done[0] && f.radio.recovery==1);
}
{ Fixture f;f.radio.receiving=true;assert(f.d.queue(bytes));f.tick();f.tick(201);
assert(f.radio.sends==0 && f.d.hasOutbound());f.radio.receiving=false;f.tick(201);assert(f.radio.sends==1);
}
{ Fixture f;auto* p=newPacket(f.d);f.radio.p.cross=RadioCrossMode::On;
assert(!f.radio.p.canCross() && f.d.getTransmitProfileMask(p)==1);
assert(f.d.sendPacket(p,0));assert(f.manager.getOutboundTotal()==1);f.tick();
assert(f.radio.sends==1 && f.radio.active==0);f.radio.complete=true;f.tick();assert(f.d.mc_sent==1);
auto* denied=newPacket(f.d,RADIO_TX_SECONDARY);assert(!f.d.sendPacket(denied,0));
assert(f.manager.getOutboundTotal()==0 && f.radio.sends==1);
}
{ Fixture f;auto* p=newPacket(f.d,RADIO_TX_PRIMARY);assert(f.d.sendPacket(p,0));f.tick();
assert(f.d.queue(bytes));f.tick();assert(f.radio.sends==1);f.radio.complete=true;f.tick();
f.tick();assert(f.radio.sends==2 && f.radio.active==1);f.radio.complete=true;f.tick();
assert(f.d.done.size()==1 && f.d.done[0] && f.d.mc_sent==1);
}
{ Fixture f;f.d.duty_window=200;f.d.begin();assert(f.d.queue(bytes));f.tick();
f.clock.now+=100;f.radio.complete=true;f.d.loop();assert(f.d.getRemainingTxBudget()==0);
assert(f.d.queue(bytes));f.tick(100);assert(f.radio.sends==1);
f.tick(101);assert(f.radio.sends==2);
}
{ Fixture f;assert(f.d.queue(bytes));f.tick();f.radio.complete=true;f.tick();
assert(f.d.queue(bytes));auto* p=newPacket(f.d,RADIO_TX_PRIMARY);assert(f.d.sendPacket(p,0));f.tick();
assert(f.radio.sends==2 && f.radio.active==0);f.radio.complete=true;f.tick();f.tick();
assert(f.radio.sends==3 && f.radio.active==1);
}
{ Fixture f;f.radio.supported=false;assert(!f.d.queue(bytes));f.radio.supported=true;
assert(!f.d.queueForeignRadioFrame(bytes,3,0,f.radio.p.generation[0]));
assert(!f.d.queueForeignRadioFrame(bytes,3,1,f.radio.p.generation[1]+1));
assert(!f.d.queue(nullptr));assert(!f.d.queue(bytes,0));uint8_t full[256]={};assert(!f.d.queue(full,256));
f.radio.carrier=true;assert(!f.d.queue(bytes));f.radio.carrier=false;
f.radio.p.secondary.mode=RadioProfileMode::Rx;assert(!f.d.queue(bytes));
}
}
'''
class MeshtextTransportTest(unittest.TestCase):
def compile_and_run(self, text, extra_flags=()):
compiler = shutil.which('g++') or shutil.which('clang++')
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory() as folder:
cpp, exe = Path(folder)/'test.cpp', Path(folder)/'test.exe'
(Path(folder)/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cmath>\n')
cpp.write_text(text)
result = subprocess.run([compiler, '-std=c++17', '-Wall', '-Wextra',
*extra_flags, '-I', folder, '-I', str(ROOT/'src'), str(cpp), '-o', str(exe)],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr[-12000:])
run = subprocess.run([str(exe)], capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
def test_actual_dispatcher(self):
compiler = shutil.which('g++') or shutil.which('clang++')
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory() as folder:
cpp, exe = Path(folder)/'test.cpp', Path(folder)/'test.exe'
cpp.write_text(HARNESS)
flags = ['-std=c++17', '-Wall', '-Wextra', '-DMESH_ENABLE_MESHTEXT_CHAT=1']
if os.name != 'nt':
flags += ['-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-fno-pie', '-no-pie']
result = subprocess.run([compiler, *flags,
'-I', str(ROOT/'test/mocks'), '-I', str(ROOT/'src'),
str(ROOT/'src/Dispatcher.cpp'), str(ROOT/'src/Packet.cpp'),
str(ROOT/'src/helpers/StaticPoolPacketManager.cpp'), str(cpp), '-o', str(exe)],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr[-12000:])
env = os.environ.copy()
env['ASAN_OPTIONS'] = env.get('ASAN_OPTIONS', '') + ':detect_leaks=0'
run = subprocess.run([str(exe)], capture_output=True, text=True, env=env)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
def test_protocol_retune_and_rollback(self):
source = (ROOT/'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
names = [('uint8_t', 'beginReconfigure'), ('void', 'endReconfigure'),
('void', 'serviceProfileScan')] + [('mesh::RadioParamApplyResult', n) for n in
('tuneProfile', 'prepareTransmitProfile', 'tryRestoreCodingRate', 'trySetParams', 'trySetPrimaryParams')]
methods = '\n'.join(method(source, f'{kind} RadioLibWrapper::{name}(') for kind, name in names)
methods += '\n' + method(source, 'mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool')
wrapper = (ROOT/'src/helpers/radiolib/CustomSX1262Wrapper.h').read_text()
methods += '\n' + method(wrapper, 'void setProfileStandbyWarm(').replace(
'void setProfileStandbyWarm(bool enabled) override',
'void RadioLibWrapper::setProfileStandbyWarm(bool enabled)')
harness = SCAN_HARNESS.split('@METHODS@')[0]
harness = harness.replace('uint16_t _physical_preamble=32;', '''
uint16_t _physical_preamble=32;
mesh::RadioProfileProtocol _physical_protocol=mesh::RadioProfileProtocol::MeshCore;
mesh::RadioProfileProtocol applied_protocol=mesh::RadioProfileProtocol::MeshCore;
unsigned protocol_writes=0;
bool protocol_fail=false;
bool applyProtocol(mesh::RadioProfileProtocol p) {
++protocol_writes;
if (protocol_fail && p==mesh::RadioProfileProtocol::Meshtext) return false;
applied_protocol=p;return true;
}''')
cases = r'''
int main() {
using Protocol=mesh::RadioProfileProtocol;
using Result=mesh::RadioParamApplyResult;
{
RadioLibWrapper w;
mesh::RadioProfileConfig cfg;cfg.params=w._profiles.primary;
cfg.params.protocol=Protocol::Meshtext;cfg.mode=mesh::RadioProfileMode::RxTx;
w._profiles.setSecondary(cfg,false);
assert(w._profiles.preamble(1,128)==16 && w._profiles.automaticPreambleFits());
assert(w.tuneProfile(1)==Result::APPLIED);
assert(w._physical_protocol==Protocol::Meshtext && w.applied_protocol==Protocol::Meshtext);
assert(w.protocol_writes==1 && w._physical_preamble==16);
assert(w.tuneProfile(0)==Result::APPLIED);
assert(w._physical_protocol==Protocol::MeshCore && w.applied_protocol==Protocol::MeshCore);
assert(w.protocol_writes==2);
const auto generation=w._profiles.generation[1];
cfg.params.protocol=Protocol::MeshCore;w._profiles.setSecondary(cfg,false);
assert(w._profiles.generation[1]==generation+1);
}
{
RadioLibWrapper w;mesh::RadioProfileConfig cfg;cfg.params=w._profiles.primary;
cfg.params.protocol=Protocol::Meshtext;cfg.mode=mesh::RadioProfileMode::RxTx;
w._profiles.setSecondary(cfg,false);w.protocol_fail=true;
assert(w.tuneProfile(1)==Result::FAILED);
assert(w._physical_protocol==Protocol::MeshCore && w.applied_protocol==Protocol::MeshCore);
assert(w._active_profile==0 && w._profile_generation==1);
assert(w.protocol_writes==2);
}
{
RadioLibWrapper w;mesh::RadioProfileConfig cfg;cfg.params=w._profiles.primary;
cfg.params.protocol=Protocol::Meshtext;cfg.mode=mesh::RadioProfileMode::RxTx;
w._profiles.setSecondary(cfg,false);w.failRxStarts=1;
assert(w.tuneProfile(1)==Result::FAILED);
assert(w._physical_protocol==Protocol::MeshCore && w.applied_protocol==Protocol::MeshCore);
assert(w._active_profile==0 && w.isInRecvMode());
}
{
mesh::RadioProfiles p;p.primary.freq=909.5;p.primary.bw=500;p.primary.sf=5;p.primary.cr=5;
p.secondary.params=p.primary;p.secondary.params.protocol=Protocol::Meshtext;
p.secondary.mode=mesh::RadioProfileMode::RxTx;
for (auto cross: {mesh::RadioCrossMode::Off,mesh::RadioCrossMode::On,mesh::RadioCrossMode::Auto}) {
p.cross=cross;assert(!p.canCross());assert(p.transmitMask(0)==1 && p.transmitMask(1)==2);
}
}
}
'''
self.compile_and_run(harness + methods + cases, ['-DMESH_ENABLE_MESHTEXT_CHAT=1'])
def test_sx1262_wire_settings_and_deep_recovery(self):
wrapper = (ROOT/'src/helpers/radiolib/CustomSX1262Wrapper.h').read_text()
wire = method(wrapper, 'bool applyProtocol(').replace(' override', '')
chip_source = (ROOT/'src/helpers/radiolib/CustomSX1262.h').read_text()
protocol = method(chip_source, 'int16_t setProfileProtocol(')
source = (ROOT/'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
restore = method(source, 'bool RadioLibWrapper::restoreAfterDeepInit()')
harness = r'''
#include <cassert>
#include <cstdint>
#include <RadioProfiles.h>
#include <helpers/radiolib/SX1262ProfileSwitchState.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_SX126X_LORA_CRC_ON 1
#define RADIOLIB_SX126X_LORA_HEADER_EXPLICIT 0
#define RADIOLIB_SX126X_LORA_IQ_STANDARD 0
#define STATE_IDLE 0
struct SX1262 {
uint8_t sync=0x12;bool crc=false,explicit_header=false,iq=true;
size_t preambleLengthLoRa=32;
uint8_t crcTypeLoRa=1,implicitLen=255,headerType=0,invertIQEnabled=0;
unsigned step=0,fail_at=0;
int status() { return ++step==fail_at ? -1 : 0; }
int setSyncWord(uint8_t n) { sync=n;return status(); }
int setCRC(uint8_t n) { crc=n==1;return status(); }
int explicitHeader() { explicit_header=true;return status(); }
int invertIQ(bool n) { iq=n;return status(); }
int setPreambleLength(uint16_t) { return 0; }
};
struct CustomSX1262: SX1262 {
SX1262ProfileSwitchState _profileSwitch{false};
@PROTOCOL@
};
struct RadioLibWrapper {
CustomSX1262 chip;CustomSX1262* _radio=&chip;
bool _rx_ps_armed=true,_params_valid=true,_dbm_valid=false,_rx_boosted_gain_valid=false;
int state=1;uint8_t _cur_sf=11,_cur_cr=5;float _cur_freq=906.875,_cur_bw=250;
int8_t _cur_dbm=22;bool _cur_rx_boosted_gain=false;
mesh::RadioProfileProtocol _physical_protocol=mesh::RadioProfileProtocol::Meshtext;
bool radioDeepInit() { chip.sync=0x12;chip.step=0;return true; }
bool registerInterruptAction() { return true; }
bool applyParams(float,float,uint8_t,uint8_t) { return true; }
uint16_t currentPreambleLength() { return 16; }
int applyCachedTxPower(int8_t) { return 0; }
bool applyRxBoostedGainMode(bool) { return true; }
void recalibrateNoiseFloor() {}
@WIRE@
bool restoreAfterDeepInit();
};
@RESTORE@
int main() {
using Protocol=mesh::RadioProfileProtocol;
for (Protocol p: {Protocol::MeshCore,Protocol::Meshtext}) {
RadioLibWrapper w;assert(w.applyProtocol(p));
assert(w.chip.sync==(p==Protocol::MeshCore ? 0x12 : 0x2b));
assert(w.chip.crc && w.chip.explicit_header && !w.chip.iq);
}
for (unsigned fail=1;fail<=4;++fail) {
RadioLibWrapper w;w.chip.fail_at=fail;assert(!w.applyProtocol(Protocol::Meshtext));
}
RadioLibWrapper w;assert(w.restoreAfterDeepInit());
assert(w.chip.sync==0x2b && w.chip.crc && w.chip.explicit_header && !w.chip.iq);
assert(!w._rx_ps_armed && w.state==STATE_IDLE);
w._physical_protocol=Protocol::MeshCore;assert(w.restoreAfterDeepInit());assert(w.chip.sync==0x12);
}
'''
self.compile_and_run('#include <initializer_list>\n' + harness.replace('@WIRE@',wire)
.replace('@PROTOCOL@',protocol).replace('@RESTORE@',restore),
['-DMESH_ENABLE_MESHTEXT_CHAT=1'])
if __name__ == '__main__':
unittest.main()