mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-14 15:55:34 +00:00
Implement radio2, tempradio2, radioat2 and tempradioat2 across Mesh roles, with RX-only/RX+TX operation, optional preambles and persistent cross-TX policy. Keep temporary OTA traffic on its profile by default and maintain independent retry ownership and backoff for each profile. Use slow-first receive scanning with 4.8-symbol visits and automatic preambles rounded up in steps of eight. Preserve pending RX, restore power saving on exit, and discard work bound to changed or expired profiles. Restore the infrastructure path.hash.mode setter and report unsupported extra.sf settings consistently. Add CLI, scheduling, scan and retry tests, setup documentation, and the V4/XIAO hardware validation results. Validation: 1,422 native tests, eight KISS tests, 63 final focused profile tests, 18 checks from the staged source, sanitizer-enabled OTA transfers, and builds for V4 Mesh roles, Full XIAO Companion and nRF52 T1000-E. Hardware checks cover reception, cross-TX policy, expiry, reboot and OTA discovery while receiving main-channel adverts.
344 lines
15 KiB
Python
344 lines
15 KiB
Python
"""Compile real Companion command dispatch with local and LoRa callers."""
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
PREAMBLE = r'''
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#include <helpers/CLICommandUtils.h>
|
|
#include <helpers/RadioProfileCommandUtils.h>
|
|
#define ESP32 1
|
|
#define WIFI_SSID "test"
|
|
#define WITH_MQTT_BRIDGE 1
|
|
#define PRV_KEY_SIZE 64
|
|
#define MAX_MQTT_SLOTS 2
|
|
#define MAX_LORA_TX_POWER 22
|
|
#define TXT_TYPE_CLI_COMMAND 1
|
|
namespace mesh {
|
|
struct RadioProfileCLI {
|
|
bool handle(const char*, char*, size_t) { return false; }
|
|
static bool parseSuffix(const char* input, unsigned fields, char* legacy, size_t size, uint16_t& preamble) {
|
|
return cli::parseRadioPreambleSuffix(input, fields, legacy, size, preamble);
|
|
}
|
|
bool acceptsPrimary(float, float, uint8_t, uint8_t, uint16_t) { return true; }
|
|
bool savePrimaryPreamble(uint16_t) { return true; }
|
|
uint16_t primaryPreamble() const { return 0; }
|
|
void appendSavedPreamble(char*, size_t, uint8_t, float) {}
|
|
};
|
|
struct Packet {};
|
|
void resetLazyPersistenceAfterSuccess(unsigned& when, uint8_t& failures) { when=0; failures=0; }
|
|
struct Utils {
|
|
static void toHex(char* out,const uint8_t* data,size_t len) {
|
|
for(size_t i=0;i<len;++i) sprintf(out+2*i,"%02X",data[i]);
|
|
}
|
|
};
|
|
}
|
|
struct ContactInfo {
|
|
bool allowed=true;
|
|
bool isRemoteCLIAllowed() const { return allowed; }
|
|
};
|
|
struct WiFiSetupPortal {
|
|
static bool loadStoredCredentials(char*,size_t,char* password,size_t size) {
|
|
snprintf(password,size,"wifi-secret");return true;
|
|
}
|
|
};
|
|
struct Identity {
|
|
int writeTo(uint8_t* out,size_t len) { memset(out,0xA5,len);return len; }
|
|
};
|
|
struct Store {
|
|
int erased=0; bool success=true;
|
|
bool formatFileSystem(){++erased;return success;}
|
|
};
|
|
struct Prefs {
|
|
float freq=910, bw=125, airtime_factor=1, rx_delay_base=1;
|
|
uint8_t sf=7, cr=5, path_hash_mode=0, multi_acks=0, rx_ps_level=0, rx_ps_preamble=16;
|
|
uint32_t rx_ps_rx_us=1, rx_ps_sleep_us=1;
|
|
int8_t tx_power_dbm=10;
|
|
char node_name[32]="test";
|
|
Prefs* getRadioPrefs(){return this;}
|
|
bool handleCommand(const char*,uint32_t,char*){return false;}
|
|
bool isDirty() const {return false;}
|
|
void clearDirty(){}
|
|
};
|
|
struct Board {
|
|
bool rebootToUf2Bootloader(){return false;}
|
|
bool handleCommand(const char*,uint32_t,char*){return false;}
|
|
} board;
|
|
struct Radio {bool setTxPower(int){return true;}} radio_driver;
|
|
struct AdvertDataParser {static bool isValidName(const char*){return true;}};
|
|
struct StrHelper {static void strncpy(char* out,const char* in,size_t len){::strncpy(out,in,len);}};
|
|
void recalcRxPowerSavingFromLevel(uint8_t,uint8_t,float,uint8_t,uint32_t*,uint32_t*){}
|
|
struct StatsFormatHelper {
|
|
template<class... T> static void formatCoreStats(char* out,T&&...){strcpy(out,"core");}
|
|
template<class... T> static void formatRadioStats(char* out,T&&...){strcpy(out,"radio");}
|
|
template<class... T> static void formatRadioDiag(char* out,T&&...){strcpy(out,"diag");}
|
|
template<class... T> static void formatPacketStats(char* out,T&&...){strcpy(out,"packets");}
|
|
};
|
|
struct WebConfigServer {
|
|
static bool formatWiFiPassword(char* out,size_t len){snprintf(out,len,"> wifi-secret");return true;}
|
|
};
|
|
const char* FIRMWARE_VERSION="test";
|
|
const char* FIRMWARE_BUILD_DATE="test";
|
|
struct MyMesh {
|
|
mesh::RadioProfileCLI _radio_profiles;
|
|
Identity self_id;
|
|
Store store; Store* _store=&store;
|
|
Prefs _prefs;
|
|
struct {char mqtt_slot_password[2][64]={"mqtt-secret","other"};
|
|
char mqtt_slot_token[2][64]={"mqtt-token","other-token"};} _mqtt_prefs;
|
|
unsigned dirty_contacts_expiry=1; uint8_t dirty_contacts_failures=1;
|
|
int value=0; int* _ms=&value; int* _radio=&value; int* _mgr=&value;
|
|
uint16_t _err_flags=0;
|
|
bool _radio_available=false, save_ok=true;
|
|
int resets=0, saves=0;
|
|
bool handleCommand(const char*,uint32_t,char*);
|
|
bool handleDirectCommand(const char*,char*,size_t);
|
|
void onCLICommandRecv(const ContactInfo&,mesh::Packet*,uint32_t,const char*,char*);
|
|
bool handleLocalControlCommand(const char* text,char* reply,size_t) {
|
|
if(!strcmp(text,"get mqtt1.password")||!strcmp(text,"get mqtt1.token")) {
|
|
strcpy(reply,"> ********");return true;
|
|
}
|
|
return false;
|
|
}
|
|
void stopContactsIterator(){}
|
|
void resetContacts(){++resets;}
|
|
bool savePrefs(){++saves;return save_ok;}
|
|
int getTotalAirTime(){return 0;}
|
|
int getReceiveAirTime(){return 0;}
|
|
int getNumSentFlood(){return 0;}
|
|
int getNumSentDirect(){return 0;}
|
|
int getNumRecvFlood(){return 0;}
|
|
int getNumRecvDirect(){return 0;}
|
|
bool hasOutbound(){return false;}
|
|
void markConnectionActive(const ContactInfo&){}
|
|
void queueMessage(const ContactInfo&,int,mesh::Packet*,uint32_t,const void*,int,const char*){}
|
|
};
|
|
'''
|
|
|
|
SCENARIOS = r'''
|
|
int main() {
|
|
MyMesh node;
|
|
char reply[160] = {};
|
|
const char* local[] = {"get password", "get prv.key", "get wifi.pwd", "get mqtt1.password",
|
|
"get mqtt1.token", "stats-core", "stats-radio", "stats-radio-diag", "stats-packets"};
|
|
for(const char* command:local) {
|
|
memset(reply,0,sizeof(reply));
|
|
assert(node.handleCommand(command,0,reply));
|
|
assert(strlen(reply)>0);
|
|
if(!strcmp(command,"get password")) assert(strstr(reply,"no admin password"));
|
|
if(!strcmp(command,"get prv.key")) {
|
|
#if ENABLE_PRIVATE_KEY_EXPORT
|
|
assert(strlen(reply)==130 && !strncmp(reply,"> A5A5",6));
|
|
#else
|
|
assert(strstr(reply,"disabled"));
|
|
#endif
|
|
}
|
|
if(!strcmp(command,"get wifi.pwd")) assert(!strcmp(reply,"> wifi-secret"));
|
|
if(!strcmp(command,"get mqtt1.password")) assert(!strcmp(reply,"> mqtt-secret"));
|
|
if(!strcmp(command,"get mqtt1.token")) assert(!strcmp(reply,"> mqtt-token"));
|
|
memset(reply,0,sizeof(reply));
|
|
node.handleCommand(command,100,reply);
|
|
assert(!strstr(reply,"secret") && !strstr(reply,"token") && !strstr(reply,"A5A5"));
|
|
// In particular, an authenticated LoRa packet carrying timestamp zero
|
|
// still enters the production receiver with remote restrictions.
|
|
memset(reply,0,sizeof(reply));
|
|
node.onCLICommandRecv(ContactInfo{},nullptr,0,command,reply);
|
|
assert(!strstr(reply,"secret") && !strstr(reply,"token") && !strstr(reply,"A5A5"));
|
|
}
|
|
assert(node.handleCommand("set freq 915.25",0,reply));
|
|
assert(node._prefs.freq==915.25f && strstr(reply,"reboot to apply"));
|
|
assert(node.handleCommand("set freq 920",100,reply));
|
|
assert(node._prefs.freq==920.0f && strstr(reply,"reboot to apply"));
|
|
for(uint32_t stamp:{0u,100u}) {
|
|
reply[0]=0;
|
|
node.onCLICommandRecv(ContactInfo{},nullptr,stamp,"A7|set freq 921.125",reply);
|
|
assert(node._prefs.freq==921.125f && !strcmp(reply,"A7|OK - reboot to apply"));
|
|
assert(node._prefs.bw==125.0f && node._prefs.sf==7 && node._prefs.cr==5);
|
|
for(const char* bad:{"set freq 149.9","set freq 2500.1","set freq 920oops",
|
|
"set freq NaN","set freq inf","set freq 1e3","set freq "}) {
|
|
const int saves=node.saves;
|
|
reply[0]=0;
|
|
node.onCLICommandRecv(ContactInfo{},nullptr,stamp,bad,reply);
|
|
assert(strstr(reply,"Error") && node._prefs.freq==921.125f && node.saves==saves);
|
|
}
|
|
}
|
|
const int saves=node.saves;
|
|
node.onCLICommandRecv(ContactInfo{false},nullptr,100,"set freq 930",reply);
|
|
assert(node._prefs.freq==921.125f && node.saves==saves);
|
|
node.save_ok=false;
|
|
for(uint32_t stamp:{0u,100u}) {
|
|
assert(node.handleCommand("set freq 920",stamp,reply));
|
|
assert(node._prefs.freq==921.125f && strstr(reply,"Error"));
|
|
}
|
|
assert(!node.handleCommand("erase",100,reply));
|
|
node.onCLICommandRecv(ContactInfo{},nullptr,0,"erase",reply);
|
|
assert(node.store.erased==0);
|
|
assert(node.handleCommand("erase",0,reply));
|
|
assert(node.store.erased==1 && node.resets==1 && node.dirty_contacts_expiry==0);
|
|
assert(strstr(reply,"OK"));
|
|
node.store.success=false;
|
|
assert(node.handleCommand("erase",0,reply));
|
|
assert(node.resets==1 && strstr(reply,"Err"));
|
|
}
|
|
'''
|
|
|
|
|
|
class LocalCliAccessTest(unittest.TestCase):
|
|
def test_infrastructure_frequency_accepts_local_and_lora_callers(self):
|
|
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
|
anchor = source.index('memcmp(config, "freq ", 5)')
|
|
start = source.rfind('} else if (', 0, anchor) + len('} else ')
|
|
setter = extract_braced(source[start:], "if (")
|
|
# The following optional else-if puts its preprocessor guard just
|
|
# before this branch's closing brace. It is not part of this handler.
|
|
setter = setter.replace("#ifdef WITH_BRIDGE\n", "")
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#define MAX_LORA_TX_POWER 22
|
|
#include <helpers/CLICommandUtils.h>
|
|
#include <helpers/radiolib/RadioPowerLimits.h>
|
|
struct Prefs {
|
|
float freq=910.525f,bw=62.5f;
|
|
uint8_t sf=7,cr=5;
|
|
int8_t tx_power_dbm=30;
|
|
} prefs;
|
|
Prefs* _prefs=&prefs;
|
|
int saves=0;
|
|
void savePrefs(){++saves;}
|
|
void set(const char* config,uint32_t sender_timestamp,char* reply) {
|
|
''' + setter + r'''
|
|
}
|
|
int main() {
|
|
char reply[160]={};
|
|
for(uint32_t stamp:{0u,100u,0xFFFFFFFFu}) {
|
|
prefs=Prefs{};
|
|
set("freq 915.25",stamp,reply);
|
|
assert(prefs.freq==915.25f && prefs.tx_power_dbm==22);
|
|
assert(strstr(reply,"reboot to apply") && strstr(reply,"TX power limited"));
|
|
assert(prefs.bw==62.5f && prefs.sf==7 && prefs.cr==5);
|
|
for(const char* bad:{"freq 149.9","freq 2500.1","freq 920oops",
|
|
"freq NaN","freq inf","freq 1e3","freq "}) {
|
|
const int writes=saves;
|
|
set(bad,stamp,reply);
|
|
assert(strstr(reply,"Error") && prefs.freq==915.25f && saves==writes);
|
|
}
|
|
for(const char* boundary:{"freq 150","freq 2500"}) {
|
|
set(boundary,stamp,reply);
|
|
assert(strstr(reply,"OK - reboot to apply"));
|
|
}
|
|
}
|
|
assert(saves==9);
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
binary = Path(temp) / "test"
|
|
result = subprocess.run([
|
|
"c++", "-std=c++17", "-x", "c++", "-", "-I"+str(ROOT / "src"),
|
|
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", "-o", str(binary),
|
|
], input=code, 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)
|
|
|
|
def test_infrastructure_password_read_and_browser_transport_limits(self):
|
|
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
|
getter = extract_braced(source, "void CommonCLI::handleGetCmd(")
|
|
password = extract_braced(getter, 'if (strcmp(config, "password") == 0)')
|
|
web = (ROOT / "src/helpers/esp32/WebConfigServer.cpp").read_text()
|
|
limits = extract_braced(web, "static const char* wcCliUnavailable(")
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
struct Prefs { char password[16] = "admin-secret"; } prefs;
|
|
Prefs* _prefs = &prefs;
|
|
void get(const char* config, uint32_t sender_timestamp, char* reply) {
|
|
''' + password + "\n}\n" + limits + r'''
|
|
int main() {
|
|
char reply[160] = {};
|
|
get("password",0,reply);
|
|
assert(!strcmp(reply,"> admin-secret"));
|
|
for(uint32_t stamp:{1u,100u,0xFFFFFFFFu}) {
|
|
get("password",stamp,reply);
|
|
assert(strstr(reply,"local connection") && !strstr(reply,"admin-secret"));
|
|
}
|
|
reply[0]=0;get("passwordx",0,reply);assert(!reply[0]);
|
|
assert(wcCliUnavailable("get password",false)==nullptr);
|
|
assert(wcCliUnavailable("get password",true)==nullptr);
|
|
assert(wcCliUnavailable("log",false)!=nullptr);
|
|
assert(wcCliUnavailable("log",true)==nullptr);
|
|
assert(wcCliUnavailable("get acl",true)==nullptr);
|
|
assert(wcCliUnavailable("start ota",true)!=nullptr);
|
|
assert(wcCliUnavailable("clock sync",true)!=nullptr);
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
binary = Path(temp) / "test"
|
|
result = subprocess.run([
|
|
"c++", "-std=c++17", "-include", "initializer_list", "-x", "c++", "-",
|
|
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", "-o", str(binary),
|
|
], input=code, 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)
|
|
|
|
def test_real_companion_dispatch_local_binary_and_radio(self):
|
|
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
implementation = "\n".join(extract_braced(source, signature) for signature in (
|
|
"static bool isCompanionRadioPrefsCommand(",
|
|
"bool MyMesh::handleDirectCommand(", "bool MyMesh::handleCommand(",
|
|
"void MyMesh::onCLICommandRecv("))
|
|
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"
|
|
result = subprocess.run([
|
|
"c++", "-std=c++17", "-x", "c++", "-", "-I"+str(ROOT / "src"),
|
|
"-DENABLE_PRIVATE_KEY_EXPORT="+str(export),
|
|
*(["-DWITH_WEBCONFIG=1"] if webconfig else []),
|
|
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", "-o", str(binary),
|
|
], 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)
|
|
|
|
def test_each_host_transport_uses_local_dispatch(self):
|
|
for role in ("simple_repeater", "simple_room_server"):
|
|
header = (ROOT / "examples" / role / "MyMesh.h").read_text()
|
|
web = extract_braced(header, "void execAdminCommand(")
|
|
self.assertIn("handleCommand(0, cmd, reply)", web)
|
|
local = extract_braced(header, "void handleLocalCommand(")
|
|
self.assertIn("_command_output = &output", local)
|
|
self.assertIn("handleCommand(0, command, reply)", local)
|
|
self.assertIn("_command_output = nullptr", local)
|
|
stream = extract_braced(header, "void runStreamTerminal(")
|
|
self.assertIn("handleLocalCommand(command, reply, *_web_terminal)", stream)
|
|
main = (ROOT / "examples" / role / "main.cpp").read_text()
|
|
self.assertIn("handleLocalCommand(ethernet_command, reply, ethernet_client)", main)
|
|
self.assertIn("cancelLocalOutput(ethernet_client)", main)
|
|
source = (ROOT / "examples" / role / "MyMesh.cpp").read_text()
|
|
guard = ("sender != nullptr" if role == "simple_repeater"
|
|
else "gpio_client_index >= 0")
|
|
self.assertIn("if ("+guard+" && sender_timestamp == 0) sender_timestamp = 1;", source)
|
|
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
framed = extract_braced(companion, "else if (mesh::companion::isRunCliFrame(")
|
|
self.assertIn("handleCommand(text, 0, reply_buf)", framed)
|
|
sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text()
|
|
self.assertIn("if (gpio_client_index >= 0 && sender_timestamp == 0) sender_timestamp = 1;", sensor)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|