Preserve KISS ownership and wait for initial radio2 readiness
This commit is contained in:
1 parent
d836e661ba
commit
264a5a8b34
7 files changed
+306
-26
No files matched your search
@@ -32,6 +32,9 @@ runtime radio2 protocol lease without changing the saved radio2 tuple.
|
||||
Temporary radio settings and Meshtext/Ethernet radio2 owners block admission.
|
||||
The service never chooses a frequency, takes another service's radio2 lease,
|
||||
crosses raw packets onto radio1, or feeds them into MeshCore parsing/bridges.
|
||||
If the initial radio2 change is still pending, the connection waits and sends
|
||||
READY when radio2 becomes available; no reconnect is needed. DATA received
|
||||
while waiting is discarded, so hosts should wait for the initial READY.
|
||||
|
||||
USB remains the normal CLI until an initial KISS FEND (`C0`). During the raw
|
||||
session, USB Companion framing and text/debug output are parked. Send the
|
||||
|
||||
@@ -323,6 +323,17 @@ bool MyMesh::validateMeshtextChannelChange(int index, const ChannelDetails& cand
|
||||
}
|
||||
|
||||
void MyMesh::refreshMeshtextProfile() {
|
||||
const auto* current_profiles = _radio->profiles();
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
// Raw host traffic owns this running protocol until its session releases it.
|
||||
// Meshtext refreshes must not clear that lease or publish another overlay.
|
||||
if (current_profiles && current_profiles->secondary.params.protocol == mesh::RadioProfileProtocol::Kiss) {
|
||||
_mt_active = false;
|
||||
_mt_status = "radio2 reserved for KISS";
|
||||
_mt_profile_dirty = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
// This owner must never revoke an Ethernet lease simply because it has
|
||||
// no Meshtext channels. Explicit channel conflicts suspend Ethernet too.
|
||||
@@ -333,7 +344,6 @@ void MyMesh::refreshMeshtextProfile() {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
const auto* current_profiles = _radio->profiles();
|
||||
const uint32_t primary_generation = current_profiles ? current_profiles->generation[0] : 0;
|
||||
const uint32_t secondary_generation = current_profiles ? current_profiles->generation[1] : 0;
|
||||
const uint32_t intent_generation = _radio_profiles.secondaryIntentGeneration();
|
||||
|
||||
+113
-23
@@ -25,6 +25,7 @@ RadioProfileCLI* profiles = nullptr;
|
||||
Dispatcher* dispatcher = nullptr;
|
||||
Codec* codec = nullptr; // All frame buffers are absent while the setting is off.
|
||||
bool usb_session = false, suspended = false, leased = false, transmitting = false;
|
||||
bool initial_waiting = false, initial_fresh = false, established = false, ready_started = false;
|
||||
bool ready_pending = false, completion_pending = false, completion_success = false;
|
||||
uint8_t quarantine_return = 0;
|
||||
uint32_t profile_generation = 0, intent_generation = 0;
|
||||
@@ -65,6 +66,7 @@ void resetFrames() {
|
||||
if (codec) codec->reset();
|
||||
ready_pending = completion_pending = false;
|
||||
quarantine_return = 0;
|
||||
initial_waiting = initial_fresh = established = ready_started = false;
|
||||
}
|
||||
|
||||
void closeSession() {
|
||||
@@ -102,17 +104,55 @@ bool leaseRadio() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool current() {
|
||||
bool validLease() {
|
||||
if (!leased || !active() || suspended || admission()) return false;
|
||||
const auto* p = radio->profiles();
|
||||
return p && p->generation[1] == profile_generation
|
||||
&& profiles->secondaryIntentGeneration() == intent_generation;
|
||||
}
|
||||
bool current() { return established && validLease(); }
|
||||
|
||||
// Before the first READY, a pending settings handoff or another RF owner is a
|
||||
// wait, not a failed connection. All pre-READY DATA is discarded; admission
|
||||
// requires no buffered input and a complete KISS frame boundary.
|
||||
bool startInitialLease() {
|
||||
if (!leaseRadio()) return false;
|
||||
resetFrames();
|
||||
codec->feed(Codec::Fend);
|
||||
ready_pending = true;
|
||||
return true;
|
||||
}
|
||||
void admitInitial(bool input_empty) {
|
||||
if (initial_waiting && codec && input_empty && codec->atFrameBoundary()
|
||||
&& (!usb_session || isUsbBinaryStreamReady())) startInitialLease();
|
||||
}
|
||||
|
||||
bool inputByte(uint8_t byte) {
|
||||
codec->feed(byte);
|
||||
if (codec->returned()) { closeSession(); return false; }
|
||||
if (initial_waiting || suspended || !leased) {
|
||||
const uint8_t* bytes;
|
||||
size_t length;
|
||||
while (codec->peekTransmit(bytes, length)) {
|
||||
codec->consumeTransmit();
|
||||
++discarded;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void outputProgress() {
|
||||
// No RX or TX output is admitted before this initial READY completes.
|
||||
if (leased && !initial_waiting && !established && !ready_pending
|
||||
&& !codec->queuedOutput()) established = true;
|
||||
}
|
||||
|
||||
const char* state(const Selection& selected, const char* reason) {
|
||||
if (!selected.enabled) return "off";
|
||||
if (suspended) return "suspended";
|
||||
if (!codec) return "unavailable";
|
||||
if (active() && initial_waiting) return "waiting-radio";
|
||||
if (active() && !established) return "starting";
|
||||
if (active()) return "connected";
|
||||
if (reason) return "blocked";
|
||||
#if defined(ESP32)
|
||||
@@ -142,6 +182,17 @@ void invalidateSession() {
|
||||
}
|
||||
}
|
||||
|
||||
void checkLease() {
|
||||
if (!leased || validLease()) return;
|
||||
// A partial READY cannot be withdrawn: the peer may already have its bytes.
|
||||
if (established || ready_started) invalidateSession();
|
||||
else {
|
||||
releaseRadio();
|
||||
resetFrames();
|
||||
initial_waiting = true;
|
||||
}
|
||||
}
|
||||
|
||||
void queueControls() {
|
||||
if (!codec) return;
|
||||
// Completion must survive a full RX queue, rather than being silently lost.
|
||||
@@ -231,38 +282,61 @@ void loopTcp() {
|
||||
if (tcp->listening) {
|
||||
WiFiClient incoming = tcp->server.available();
|
||||
if (incoming) {
|
||||
if (tcp->connected || admission()) incoming.stop();
|
||||
if (tcp->connected) incoming.stop();
|
||||
else {
|
||||
resetFrames();
|
||||
if (!leaseRadio()) incoming.stop();
|
||||
else {
|
||||
tcp->client = incoming;
|
||||
tcp->client.setNoDelay(true);
|
||||
tcp->connected = true;
|
||||
suspended = false;
|
||||
ready_pending = true;
|
||||
}
|
||||
tcp->client = incoming;
|
||||
tcp->client.setNoDelay(true);
|
||||
tcp->connected = true;
|
||||
suspended = false;
|
||||
initial_waiting = true;
|
||||
// A stable tuple accepts the first DATA even from a generic KISS
|
||||
// client with flow control disabled. Only a genuinely blocked initial
|
||||
// handoff needs to discard pre-READY input.
|
||||
startInitialLease();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!tcp->connected) return;
|
||||
const bool was_ready = established;
|
||||
// Drain invalid/pre-READY host bytes before admitting the final RF tuple.
|
||||
// recv's EAGAIN proves the socket input was empty on this bounded poll.
|
||||
bool input_empty = false;
|
||||
if (!established) {
|
||||
for (size_t i = 0; i < ByteBudget && codec->canAcceptByte(); ++i) {
|
||||
uint8_t byte;
|
||||
const int got = ::recv(tcp->client.fd(), &byte, 1, MSG_DONTWAIT);
|
||||
if (got == 1) { if (!inputByte(byte)) return; }
|
||||
else {
|
||||
if (!got || (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) {
|
||||
closeSession(); return;
|
||||
}
|
||||
input_empty = errno == EAGAIN || errno == EWOULDBLOCK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
admitInitial(input_empty);
|
||||
}
|
||||
queueControls();
|
||||
const uint8_t* bytes;
|
||||
size_t length;
|
||||
if (codec->peekOutput(bytes, length)) {
|
||||
if (length > ByteBudget) length = ByteBudget;
|
||||
const int written = ::send(tcp->client.fd(), bytes, length, MSG_DONTWAIT);
|
||||
if (written > 0) codec->consumeOutput(size_t(written));
|
||||
if (written > 0) {
|
||||
ready_started = true;
|
||||
codec->consumeOutput(size_t(written)); outputProgress();
|
||||
}
|
||||
else if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
||||
closeSession(); return;
|
||||
}
|
||||
}
|
||||
if (!was_ready) return;
|
||||
for (size_t i = 0; i < ByteBudget && codec->canAcceptByte(); ++i) {
|
||||
uint8_t byte;
|
||||
const int got = ::recv(tcp->client.fd(), &byte, 1, MSG_DONTWAIT);
|
||||
if (got == 1) {
|
||||
codec->feed(byte);
|
||||
if (codec->returned()) { closeSession(); break; }
|
||||
if (!inputByte(byte)) break;
|
||||
} else {
|
||||
if (!got || (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) closeSession();
|
||||
break;
|
||||
@@ -318,8 +392,8 @@ bool handleCommand(const char* command, char* reply, size_t capacity, bool local
|
||||
(unsigned long)received, (unsigned long)transmitted, (unsigned long)failed,
|
||||
(unsigned long)(discarded + (codec ? codec->droppedFrames() : 0)),
|
||||
(unsigned long)(codec ? codec->malformedFrames() : 0),
|
||||
selected.enabled && !active() && reason ? " reason=" : "",
|
||||
selected.enabled && !active() && reason ? reason : "");
|
||||
selected.enabled && (!active() || initial_waiting) && reason ? " reason=" : "",
|
||||
selected.enabled && (!active() || initial_waiting) && reason ? reason : "");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -375,7 +449,7 @@ bool handleCommand(const char* command, char* reply, size_t capacity, bool local
|
||||
|
||||
void loop() {
|
||||
if (!settings.selection().enabled || !codec) return;
|
||||
if (active() && !suspended && !current()) invalidateSession();
|
||||
if (active() && !suspended) checkLease();
|
||||
#if defined(ESP32)
|
||||
if (settings.selection().transport == Transport::Tcp) loopTcp();
|
||||
#endif
|
||||
@@ -389,9 +463,10 @@ bool serviceUsb() {
|
||||
if (!port.available() || port.peek() != Codec::Fend) return false;
|
||||
resetFrames();
|
||||
usb_session = true;
|
||||
suspended = !codec || !leaseRadio();
|
||||
suspended = false;
|
||||
initial_waiting = true;
|
||||
initial_fresh = true;
|
||||
setUsbBinaryStreamOwned(true);
|
||||
ready_pending = !suspended;
|
||||
}
|
||||
// USB text writers may have been admitted just before the binary claim.
|
||||
// Their nonblocking retirement must finish before READY or host bytes move.
|
||||
@@ -410,22 +485,37 @@ bool serviceUsb() {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!suspended && !current()) invalidateSession();
|
||||
if (initial_fresh) {
|
||||
initial_fresh = false;
|
||||
startInitialLease();
|
||||
}
|
||||
if (!suspended) checkLease();
|
||||
const bool was_ready = established;
|
||||
if (!established) {
|
||||
for (size_t i = 0; i < ByteBudget && port.available() && codec->canAcceptByte(); ++i) {
|
||||
const int value = port.read();
|
||||
if (value < 0) break;
|
||||
if (!inputByte(uint8_t(value))) return true;
|
||||
}
|
||||
if (!suspended) admitInitial(!port.available());
|
||||
}
|
||||
if (!suspended) {
|
||||
queueControls();
|
||||
const uint8_t* bytes;
|
||||
size_t length;
|
||||
if (codec->peekOutput(bytes, length)) {
|
||||
if (length > ByteBudget) length = ByteBudget;
|
||||
codec->consumeOutput(port.write(bytes, length));
|
||||
const size_t written = port.write(bytes, length);
|
||||
if (written) ready_started = true;
|
||||
codec->consumeOutput(written);
|
||||
outputProgress();
|
||||
}
|
||||
}
|
||||
if (!was_ready) { pumpTransmit(); return true; }
|
||||
for (size_t i = 0; i < ByteBudget && port.available() && codec->canAcceptByte(); ++i) {
|
||||
const int value = port.read();
|
||||
if (value < 0) break;
|
||||
codec->feed(uint8_t(value));
|
||||
if (codec->returned()) { closeSession(); return true; }
|
||||
if (suspended) codec->consumeTransmit(); // Never queue TX while suspended.
|
||||
if (!inputByte(uint8_t(value))) return true;
|
||||
}
|
||||
pumpTransmit();
|
||||
return true;
|
||||
|
||||
@@ -100,6 +100,11 @@ class Codec {
|
||||
return true;
|
||||
}
|
||||
bool returned() const { return return_; }
|
||||
// A pristine decoder or a closing FEND is a safe place to announce READY.
|
||||
// A host's partial pre-READY frame must finish (and be discarded) first.
|
||||
bool atFrameBoundary() const {
|
||||
return !has_command_ && !escaped_ && !discard_ && input_size_ == 0;
|
||||
}
|
||||
bool peekTransmit(const uint8_t*& bytes, size_t& length) const {
|
||||
if (!tx_count_) { bytes = nullptr; length = 0; return false; }
|
||||
bytes = tx_[tx_head_].bytes;
|
||||
|
||||
@@ -50,7 +50,11 @@ bool Settings::save(const Selection& selection) {
|
||||
if (!healthy_ || !fs_ || uint8_t(selection.transport) > 1) return false;
|
||||
uint8_t image[ImageSize];
|
||||
encode(image, selection);
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
const auto outcome = storage::writeCheckedSettingsFile(fs_, Path, image, sizeof(image));
|
||||
if (outcome == AtomicFileWriter::Outcome::Indeterminate) healthy_ = false;
|
||||
if (outcome != AtomicFileWriter::Outcome::Committed) return false;
|
||||
#elif defined(STM32_PLATFORM)
|
||||
AtomicFileWriter writer(fs_, Path);
|
||||
const bool wrote = writer && writer.write(image, sizeof(image)) == sizeof(image);
|
||||
const bool saved = writer.commit(wrote);
|
||||
@@ -65,10 +69,12 @@ bool Settings::save(const Selection& selection) {
|
||||
writer.commit();
|
||||
if (writer.outcome() == ContactFileTransaction::Outcome::NotCommitted) return false;
|
||||
#endif
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
uint8_t verified[ImageSize];
|
||||
healthy_ = storage::readFixedSettingsFile(fs_, Path, verified, sizeof(verified))
|
||||
&& memcmp(image, verified, sizeof(image)) == 0;
|
||||
if (!healthy_) return false;
|
||||
#endif
|
||||
selection_ = selection;
|
||||
return true;
|
||||
}
|
||||
|
||||
+66
-1
@@ -294,6 +294,35 @@ int main(){
|
||||
const int third=newSocket();accepts.push_back(third);hostkiss::loop();assert(hostkiss::radioActive());
|
||||
WiFi.mode=WIFI_AP;hostkiss::loop();assert(!hostkiss::radioActive()&&!sockets.at(third)->connected);
|
||||
command("set kiss off");assert(strstr(reply,"OK"));
|
||||
WiFi.mode=WIFI_STA;command("set kiss on");hostkiss::loop();
|
||||
// A single connection survives the two-second settings handoff. No
|
||||
// pre-READY DATA, including a partial escaped frame, can reach the radio.
|
||||
profiles.busy=true;const int waiting=newSocket();accepts.push_back(waiting);
|
||||
input(waiting,{0xc0,0,91,0xdb});hostkiss::loop();
|
||||
assert(sockets.at(waiting)->connected&&!hostkiss::radioActive());
|
||||
assert(sockets.at(waiting)->output.empty());command("get kiss");assert(strstr(reply,"waiting-radio"));
|
||||
fake_millis=1999;hostkiss::loop();assert(sockets.at(waiting)->output.empty());
|
||||
profiles.busy=false;fake_millis=2000;hostkiss::loop();assert(sockets.at(waiting)->output.empty());
|
||||
input(waiting,{0xdc,0xc0});hostkiss::loop();
|
||||
assert(sockets.at(waiting)->connected&&hostkiss::radioActive());
|
||||
assert(sockets.at(waiting)->output==std::vector<uint8_t>({0xc0,0x0f,1,0xc0}));
|
||||
assert(dispatcher.submitted==before);
|
||||
input(waiting,{0,71,0xc0});hostkiss::loop();assert(dispatcher.submitted==before+1&&dispatcher.packet[0]==71);
|
||||
command("set kiss off");assert(!sockets.at(waiting)->connected&&!dispatcher.pending);
|
||||
// Initial conflicts wait automatically, but RETURN can cancel that wait.
|
||||
command("set kiss on");profiles.manual=false;hostkiss::loop();
|
||||
const int conflict=newSocket();accepts.push_back(conflict);hostkiss::loop();
|
||||
assert(sockets.at(conflict)->connected&&!hostkiss::radioActive());
|
||||
input(conflict,{0xc0,0xff,0xc0});hostkiss::loop();assert(!sockets.at(conflict)->connected);
|
||||
const int resolves=newSocket();accepts.push_back(resolves);hostkiss::loop();
|
||||
profiles.manual=true;hostkiss::loop();assert(sockets.at(resolves)->connected&&hostkiss::radioActive());
|
||||
command("set kiss off");
|
||||
// A stable, fresh connection preserves immediate first DATA from a generic
|
||||
// KISS client which does not wait for the READY extension.
|
||||
command("set kiss on");hostkiss::loop();const int immediate=newSocket();
|
||||
input(immediate,{0xc0,0,72,0xc0});accepts.push_back(immediate);hostkiss::loop();
|
||||
assert(dispatcher.submitted==before+2&&dispatcher.packet==std::vector<uint8_t>({72}));
|
||||
command("set kiss off");
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -321,6 +350,27 @@ int main() {
|
||||
hostkiss::Settings saved;assert(saved.begin(&fs)&&saved.selection().enabled);
|
||||
input({'g','e','t',' '});assert(!hostkiss::serviceUsb()&&serial.available()==4);
|
||||
serial.input.clear();
|
||||
// First FEND can arrive during the saved radio2 two-second handoff. The
|
||||
// same connection becomes ready automatically once the tuple is stable.
|
||||
profiles.busy=true;input({0xc0});poll();
|
||||
assert(hostkiss::usbActive()&&!hostkiss::radioActive()&&serial.output.empty());
|
||||
command("get kiss");assert(strstr(reply,"waiting-radio")&&strstr(reply,"pending radio settings"));
|
||||
fake_millis=1999;poll();assert(serial.output.empty());
|
||||
profiles.busy=false;fake_millis=2000;poll();
|
||||
assert(hostkiss::radioActive()&&serial.output==std::vector<uint8_t>({0xc0,0x0f,1,0xc0}));
|
||||
hostkiss::resetUsbSession();serial.output.clear();
|
||||
// Drain a complete pre-READY frame and wait for the remainder of a partial
|
||||
// escaped frame before announcing READY. Only fresh post-READY DATA sends.
|
||||
profiles.busy=true;input({0xc0,0,90,0xc0,0,91,0xdb});poll();
|
||||
profiles.busy=false;poll();assert(serial.output.empty()&&!hostkiss::radioActive());
|
||||
input({0xdc,0xc0});poll();assert(hostkiss::radioActive());
|
||||
assert(dispatcher.submitted==0&&serial.output==std::vector<uint8_t>({0xc0,0x0f,1,0xc0}));
|
||||
hostkiss::resetUsbSession();serial.output.clear();
|
||||
// Another initial owner can release radio2 without a reconnect.
|
||||
radio.p.secondary.params.protocol=RadioProfileProtocol::Meshtext;input({0xc0});poll();
|
||||
assert(!hostkiss::radioActive()&&serial.output.empty());
|
||||
radio.p.secondary.params.protocol=RadioProfileProtocol::MeshCore;++radio.p.generation[1];poll();
|
||||
assert(hostkiss::radioActive());hostkiss::resetUsbSession();serial.output.clear();
|
||||
profiles.manual=false;input({0xc0});assert(hostkiss::serviceUsb()&&hostkiss::usbActive()&&binary_owned);
|
||||
assert(!hostkiss::radioActive());input({0xff,0xc0});assert(hostkiss::serviceUsb()&&!hostkiss::usbActive());
|
||||
profiles.manual=true;input({0xc0});binary_ready=false;
|
||||
@@ -352,13 +402,28 @@ int main() {
|
||||
poll();assert(hostkiss::usbActive()&&!hostkiss::radioActive());
|
||||
assert(profiles.clear==cleared&&radio.p.secondary.params.protocol==RadioProfileProtocol::Meshtext);
|
||||
const unsigned submitted=dispatcher.submitted;
|
||||
radio.p.secondary.params.protocol=RadioProfileProtocol::MeshCore;++radio.p.generation[1];poll();
|
||||
assert(!hostkiss::radioActive()); // An established invalidated session cannot reacquire.
|
||||
input({0xc0,0,99,0xc0});poll();assert(dispatcher.submitted==submitted);
|
||||
input({0xc0,0xff,0xc0});assert(hostkiss::serviceUsb());
|
||||
assert(!hostkiss::usbActive()&&!binary_owned&&!dispatcher.pending);
|
||||
hostkiss::sendComplete(true); // late completion cannot enter a new session
|
||||
profiles.overlay=false;radio.p.secondary=profiles.saved;++radio.p.generation[1];
|
||||
input({0xc0,0,17,0xc0});poll();assert(dispatcher.submitted==submitted+1);
|
||||
input({0xc0});poll();input({0,17,0xc0});poll();assert(dispatcher.submitted==submitted+1);
|
||||
hostkiss::resetUsbSession();assert(!hostkiss::usbActive()&&!binary_owned&&!dispatcher.pending);
|
||||
// A partially written READY is already observable; a later profile change
|
||||
// quarantines it rather than manufacturing READY on another generation.
|
||||
serial.output.clear();serial.write_budget=1;input({0xc0});poll();
|
||||
assert(serial.output.size()==1&&!hostkiss::radioActive());
|
||||
++radio.p.generation[1];poll();serial.write_budget=128;poll();
|
||||
assert(!hostkiss::radioActive()&&hostkiss::usbActive());
|
||||
input({0xc0,0xff,0xc0});poll();assert(!hostkiss::usbActive());
|
||||
// Stable radio2 also preserves a complete first DATA before the peer reads
|
||||
// READY, as ordinary KISS hosts with flow control disabled require.
|
||||
const unsigned before_immediate=dispatcher.submitted;
|
||||
input({0xc0,0,33,0xc0});poll();
|
||||
assert(dispatcher.submitted==before_immediate+1&&dispatcher.packet==std::vector<uint8_t>({33}));
|
||||
hostkiss::resetUsbSession();
|
||||
// Off must be durable, releases the overlay and cannot consume binary input.
|
||||
command("set kiss off");assert(strstr(reply,"OK")&&!hostkiss::radioActive());
|
||||
assert(saved.begin(&fs)&&!saved.selection().enabled);
|
||||
|
||||
@@ -227,6 +227,107 @@ int main() {
|
||||
"-DMESH_ENABLE_ETHERNET_TUNNEL=" + str(foreign)],
|
||||
["src/helpers/RadioProfileCLI.cpp"], ["test/fixtures/radio_profiles/mocks"])
|
||||
|
||||
def test_actual_meshtext_refresh_never_revokes_host_kiss_protocol(self):
|
||||
source = (ROOT / "examples/companion_radio/MeshtextCompanion.cpp").read_text()
|
||||
refresh = body(source, "void MyMesh::refreshMeshtextProfile()")
|
||||
code = r'''
|
||||
#define main existing_cli_main
|
||||
#include "test/fixtures/radio_profiles/cli_test.cpp"
|
||||
#undef main
|
||||
#include <helpers/CompanionChannelAlias.h>
|
||||
#define MAX_GROUP_CHANNELS 4
|
||||
struct ChannelDetails {char name[32]={};};
|
||||
class BaseChatMesh {
|
||||
public:
|
||||
ChannelDetails channels[MAX_GROUP_CHANNELS];unsigned channel_reads=0;
|
||||
bool getChannel(int n,ChannelDetails& out){++channel_reads;out=channels[n];return out.name[0];}
|
||||
};
|
||||
namespace mesh {namespace mt {
|
||||
enum class Region{Unset,US};enum class Preset{LongFast,ShortFast};struct Channel{};
|
||||
bool parsePreset(const char* name,Preset& preset){
|
||||
if(!strcmp(name,"longfast")){preset=Preset::LongFast;return true;}
|
||||
if(!strcmp(name,"shortfast")){preset=Preset::ShortFast;return true;}
|
||||
return false;
|
||||
}
|
||||
bool modemConfig(Region,Preset,RadioProfileParams& out,uint32_t){
|
||||
out={910.5f,500,16,8,5};out.protocol=RadioProfileProtocol::Meshtext;return true;
|
||||
}
|
||||
bool frequencyInRegion(Region region,const RadioProfileParams&){return region==Region::US;}
|
||||
bool sameModulation(const RadioProfileParams&a,const RadioProfileParams&b){return a.bw==b.bw&&a.sf==b.sf&&a.cr==b.cr;}
|
||||
}}
|
||||
bool describe(const ChannelDetails&,mesh::mt::Channel&){return true;}
|
||||
class MyMesh:public BaseChatMesh {
|
||||
public:
|
||||
struct RadioView {
|
||||
Radio& radio;
|
||||
mesh::RadioProfiles* profiles(){return radio.profiles();}
|
||||
bool supportsMeshtext()const{return true;}
|
||||
bool validateProfile(const mesh::RadioProfileParams& p)const{return radio.validateProfile(p);}
|
||||
} view;
|
||||
RadioView* _radio;mesh::RadioProfileCLI& _radio_profiles;
|
||||
bool _mt_active=true,_mt_profile_dirty=true,_mt_last_radio_ready=false,_mt_last_secondary_ready=false;
|
||||
bool _radio_available=true,_mt_settings_valid=true;
|
||||
uint32_t _mt_primary_generation=0,_mt_secondary_generation=0,_mt_intent_generation=0,_mt_slot=0;
|
||||
float _mt_primary_frequency=0;const char* _mt_status="";
|
||||
struct {float freq=909.5f;} _prefs;
|
||||
struct {bool enabled=false;} _ethernet_settings;
|
||||
mesh::mt::Region region=mesh::mt::Region::US;
|
||||
MyMesh(Radio& radio,mesh::RadioProfileCLI& profiles):view{radio},_radio(&view),_radio_profiles(profiles){}
|
||||
mesh::mt::Region meshtextRegion()const{return region;}
|
||||
void refreshMeshtextProfile();
|
||||
};
|
||||
@REFRESH@
|
||||
int main(){
|
||||
using P=mesh::RadioProfileProtocol;
|
||||
for(unsigned scenario=0;scenario<10;++scenario){
|
||||
Fixture f;f.cmd("set radio2 910.5,500,8,5,rxtx,80");f.advance(2000);
|
||||
const auto saved=f.fs.files;const auto intent=f.cli.secondaryIntentGeneration();
|
||||
assert(f.cli.useSecondaryProtocol(P::Kiss));
|
||||
const auto generation=f.radio.p.generation[1];const auto params=f.radio.p.secondary.params;
|
||||
MyMesh mesh(f.radio,f.cli);
|
||||
if(scenario!=0)strcpy(mesh.channels[0].name,"#mt-longfast");
|
||||
if(scenario==2)strcpy(mesh.channels[0].name,"#mt-custom");
|
||||
if(scenario==3)strcpy(mesh.channels[1].name,"#mt-shortfast");
|
||||
if(scenario==4)strcpy(mesh.channels[1].name,"#r2-public");
|
||||
if(scenario==5)mesh._mt_settings_valid=false;
|
||||
if(scenario==6)mesh._radio_available=false;
|
||||
if(scenario==7)mesh.region=mesh::mt::Region::Unset;
|
||||
if(scenario==8)mesh._prefs.freq=915;
|
||||
if(scenario==9)mesh._ethernet_settings.enabled=true;
|
||||
mesh.refreshMeshtextProfile();mesh.refreshMeshtextProfile();
|
||||
assert(f.radio.p.secondary.params.protocol==P::Kiss && f.radio.p.secondary.params==params);
|
||||
assert(f.radio.p.generation[1]==generation && f.cli.secondaryIntentGeneration()==intent && f.fs.files==saved);
|
||||
assert(!mesh._mt_active && mesh._mt_profile_dirty && !strcmp(mesh._mt_status,"radio2 reserved for KISS"));
|
||||
assert(!mesh.channel_reads);
|
||||
if(scenario==1){
|
||||
f.cli.clearSecondaryProtocol();mesh.refreshMeshtextProfile();
|
||||
assert(mesh._mt_active && f.radio.p.secondary.params.protocol==P::Meshtext);
|
||||
assert(f.fs.files==saved);
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
runner = ethernet_test.EthernetRadioTest()
|
||||
runner.setUpClass()
|
||||
guard_start = refresh.index("#if MESH_ENABLE_HOST_KISS")
|
||||
guard_end = refresh.index("#endif", guard_start) + len("#endif")
|
||||
negative = refresh[:guard_start] + refresh[guard_end:]
|
||||
for ethernet, remove_guard in ((0, False), (1, False), (0, True)):
|
||||
with self.subTest(ethernet=ethernet, remove_guard=remove_guard):
|
||||
candidate = negative if remove_guard else refresh
|
||||
if remove_guard:
|
||||
with self.assertRaises(AssertionError) as failure:
|
||||
runner.compile_run(code.replace("@REFRESH@", candidate),
|
||||
["-DMESH_ENABLE_HOST_KISS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=1",
|
||||
"-DMESH_ENABLE_ETHERNET_TUNNEL=" + str(ethernet)],
|
||||
["src/helpers/RadioProfileCLI.cpp"], ["test/fixtures/radio_profiles/mocks"])
|
||||
self.assertIn("f.radio.p.secondary.params.protocol==P::Kiss", str(failure.exception))
|
||||
else:
|
||||
runner.compile_run(code.replace("@REFRESH@", candidate),
|
||||
["-DMESH_ENABLE_HOST_KISS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=1",
|
||||
"-DMESH_ENABLE_ETHERNET_TUNNEL=" + str(ethernet)],
|
||||
["src/helpers/RadioProfileCLI.cpp"], ["test/fixtures/radio_profiles/mocks"])
|
||||
|
||||
def test_feature_layout_is_consistent_with_platform_and_explicit_overrides(self):
|
||||
checker = feature_config.MeshtextConfigTest()
|
||||
checker.setUp()
|
||||
|
||||
Reference in new issue
Block a user