Seed a quiet local emergency-channel notice on Companion joins

This commit is contained in:
mikecarper committed 2026-10-15 23:16:27 -07:00
1 parent 4b465f9966
commit 36ec9b8bd3
7 files changed
+384 -15

No files matched your search

+17
View File
@@ -416,6 +416,23 @@ Repeater/room/sensor hashtag behavior is unchanged; those nodes can join the
canonical MeshCore channel (`#emergency`). Older Companion firmware does not
recognize these aliases.
### Local emergency-channel notice
When a Companion app adds `#emergency` (or `#r2-emergency`), the firmware seeds
its local chat with this notice, using `#EMERGENCY` as the sender:
```text
#EMERGENCY: This is an emergency channel. Do not use for testing or small talk.
```
This is a local onboarding message, not an RF transmission or an emergency
alert. It does not change the node's name. It is added only after the channel
is successfully saved, not on every connection, reboot, key edit, or alias
rename. Deleting and adding the channel again adds a fresh notice. If the
receive queue is full, the notice waits for free space rather than replacing
an actual message. As with other pending received messages, it is RAM-backed
until downloaded by the app; a reboot before download can discard it.
### Meshtext channel chat
`MESH_ENABLE_MESHTEXT_CHAT` enables the text-only Meshtext backend in the
+52 -4
View File
@@ -2148,9 +2148,51 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
}
}
static bool isEmergencyNoticeChannelName(const char* name) {
const auto alias = parseCompanionChannelAlias(name);
return alias.hashtag && alias.kind != CompanionChannelKind::Invalid
&& alias.kind != CompanionChannelKind::Meshtext
&& strcmp(alias.tail, "emergency") == 0;
}
void MyMesh::seedEmergencyChannelNotice(uint8_t index, const ChannelDetails& previous) {
if (index >= MAX_GROUP_CHANNELS) return;
const uint8_t bit = 1U << (index % 8);
uint8_t& pending = pending_emergency_notices[index / 8];
ChannelDetails channel;
if (!getChannel(index, channel) || !isEmergencyNoticeChannelName(channel.name)) {
pending &= ~bit;
return;
}
if (!isEmergencyNoticeChannelName(previous.name)) pending |= bit;
if (!(pending & bit)) return; // Edits and reapplying a saved channel are not joins.
int capacity = getOfflineQueueCapacity();
#if ONE_KEY_DM_SHARED_OFFLINE_QUEUE
capacity -= held_dm_count;
#endif
// A welcome notice must never evict an actual message. Retry after sync
// frees queue space, including space temporarily reserved for held DMs.
if (offline_queue_len >= capacity) return;
if (queueChannelText(index, channel.channel.secret, 0, 0xFF,
getRTCClock()->getCurrentTime(),
"#EMERGENCY: This is an emergency channel. Do not use for testing or small talk.",
true, false)) pending &= ~bit;
}
void MyMesh::servicePendingChannelNotices() {
for (unsigned index = 0; index < MAX_GROUP_CHANNELS; ++index) {
if (!(pending_emergency_notices[index / 8] & (1U << (index % 8)))) continue;
ChannelDetails current{};
getChannel(index, current);
seedEmergencyChannelNotice(index, current);
return; // At most one local notice per loop, never a radio send.
}
}
bool MyMesh::queueChannelText(uint8_t channel_idx, const uint8_t secret[32], float snr,
uint8_t path_len, uint32_t timestamp, const char* text,
bool is_emergency_channel) {
bool is_emergency_channel, bool notify) {
if (!text || channel_idx >= MAX_GROUP_CHANNELS) return false;
int i = 0;
if (app_target_ver >= 3) {
@@ -2180,23 +2222,26 @@ bool MyMesh::queueChannelText(uint8_t channel_idx, const uint8_t secret[32], flo
if (!queued) return false;
#if COMPANION_FEATURE_NOTIFICATIONS
const bool notification_managed = _notifications.message(nullptr, secret, _serial && _serial->isConnected(), millis());
const bool notification_managed = notify && _notifications.message(nullptr, secret, _serial && _serial->isConnected(), millis());
if (_ui) _ui->setMessageNotificationOverride(notification_managed, _notifications.overridesScreen());
#else
const bool notification_managed = false;
#endif
// Older screen UIs can vibrate inside newMsg(), independently of notify().
// Treat local notices as managed/screen-suppressed while retaining history.
if (!notify && _ui) _ui->setMessageNotificationOverride(true, true);
if (_serial && _serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING;
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
if (_ui && !notification_managed) _ui->notify(UIEventType::channelMessage);
if (notify && _ui && !notification_managed) _ui->notify(UIEventType::channelMessage);
#endif
}
#if COMPANION_FEATURE_TEXT_TERMINAL
if (hasTerminalOutput()
if (notify && hasTerminalOutput()
&& _terminal_display.shouldShowChannel(is_emergency_channel)) {
ChannelDetails details;
const char* channel_name = getChannel(channel_idx, details) ? details.name : "Unknown";
@@ -6915,6 +6960,8 @@ void MyMesh::handleCmdFrame(size_t len) {
refreshMeshtextProfile();
#endif
writeOKFrame();
// Local history seed only after the durable save and its acknowledgement.
seedEmergencyChannelNotice(channel_idx, previous);
} else {
// AtomicFileWriter preserves the last complete channels file. Keep the
// live table consistent with it when a full or failing filesystem cannot
@@ -11381,6 +11428,7 @@ void MyMesh::loop() {
#endif
}
servicePendingRadioParamApply();
if (!mesh::morseTimingActive(_radio)) servicePendingChannelNotices();
#if MESH_ENABLE_ONE_KEY_DM
if (held_dm_count != 0 || verified_pending_count != 0) releaseHeldOneKeyDMs();
#endif
+5 -1
View File
@@ -442,7 +442,9 @@ protected:
#endif
bool queueChannelText(uint8_t index, const uint8_t secret[32], float snr,
uint8_t path_len, uint32_t timestamp, const char* text,
bool is_emergency_channel);
bool is_emergency_channel, bool notify = true);
void seedEmergencyChannelNotice(uint8_t index, const ChannelDetails& previous);
void servicePendingChannelNotices();
int findChannelIdxForPacket(const mesh::GroupChannel& channel, const mesh::Packet* packet);
bool allowFloodRetry(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
@@ -916,6 +918,8 @@ private:
void initializeOfflineQueue();
int offline_queue_len;
int offline_queue_head;
// Local onboarding only: RAM-backed like the receive queue, not RF traffic.
uint8_t pending_emergency_notices[(MAX_GROUP_CHANNELS + 7) / 8] = {};
#if defined(OTA_SHARED_COMPANION_QUEUE)
static_assert(OFFLINE_QUEUE_SIZE == 256, "Shared mOTA queue requires 256 normal slots");
mesh::BorrowableFrameBuffer<Frame, OFFLINE_QUEUE_SIZE, 128, mesh::ota::OtaContext> offline_queue;
+2 -2
View File
@@ -89,7 +89,7 @@ struct BaseSerialInterface {
size_t writeFrameToRoute(BaseSerialInterface*, const uint8_t*, size_t n) { ++writes; return n; }
size_t writeFrame(const uint8_t*, size_t n) { ++writes; return n; }
};
struct UI { void setMessageNotificationOverride(bool) {} };
struct UI { void setMessageNotificationOverride(bool, bool = false) {} };
struct Terminal {
void print(const char*) {}
template<typename... T> void printf(const char*, T...) {}
@@ -231,7 +231,7 @@ public:
void onCLICommandRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*, char*);
bool onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*);
void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*);
bool queueChannelText(uint8_t, const uint8_t[32], float, uint8_t, uint32_t, const char*, bool);
bool queueChannelText(uint8_t, const uint8_t[32], float, uint8_t, uint32_t, const char*, bool, bool = true);
bool handleDirectCommand(const char*, char*, size_t);
bool handleCommand(const char*, uint32_t, char*);
mesh::Packet* composeMsgPacket(const ContactInfo&, uint32_t, uint8_t, const char*, uint32_t&);
+26 -3
View File
@@ -69,7 +69,14 @@ struct Serial {
bool isConnected() { return connected; }
void writeFrame(const uint8_t* bytes,int size) { assert(size>=0); frames.emplace_back(bytes,bytes+size); }
};
struct UI { void setMessageNotificationOverride(bool,bool=false) {} };
struct UI {
bool managed=false,screen=false;
unsigned quiet_overrides=0;
void setMessageNotificationOverride(bool value,bool suppress_screen=false) {
managed=value;screen=value && suppress_screen;
if(managed && screen) ++quiet_overrides;
}
};
struct Notifications {
uint8_t key[32]={}; unsigned calls=0;
bool message(const void*,const uint8_t* secret,bool,uint32_t) { ++calls; memcpy(key,secret,32); return false; }
@@ -116,6 +123,8 @@ public:
unsigned foreign_admissions=0,meshcore_sends=0;
std::vector<std::vector<uint8_t>> foreign,offline;
size_t offline_capacity=4;
int offline_queue_len=0;
uint8_t pending_emergency_notices[(MAX_GROUP_CHANNELS+7)/8]={};
Serial serial; Serial* _serial=&serial; UI ui; UI* _ui=&ui; Notifications _notifications;
uint8_t out_frame[MAX_FRAME_SIZE]={}; uint8_t app_target_ver=3;
int error=-1;
@@ -145,7 +154,10 @@ public:
bool onForeignRadioFrame(const uint8_t*,size_t,uint8_t,uint32_t,float,float);
void onForeignRadioSendComplete(bool);
bool handleMeshtextCommand(const char*,char*,size_t);
bool queueChannelText(uint8_t,const uint8_t[32],float,uint8_t,uint32_t,const char*,bool);
bool queueChannelText(uint8_t,const uint8_t[32],float,uint8_t,uint32_t,const char*,bool,bool=true);
void seedEmergencyChannelNotice(uint8_t,const ChannelDetails&);
void servicePendingChannelNotices();
int getOfflineQueueCapacity() const { return offline_capacity; }
bool hasForeignRadioFrame() const { return foreign_pending; }
bool queueForeignRadioFrame(const uint8_t* bytes,size_t size,uint8_t profile,uint32_t generation) {
if(!admit_foreign || foreign_pending) return false;
@@ -173,7 +185,7 @@ public:
Random* getRNG() { return &random; }
bool addToOfflineQueue(const uint8_t* bytes,int size) {
assert(size>=0 && size<=MAX_FRAME_SIZE); if(offline.size()>=offline_capacity) return false;
offline.emplace_back(bytes,bytes+size); return true;
offline.emplace_back(bytes,bytes+size); offline_queue_len=offline.size(); return true;
}
bool saveChannels() { return save_result; }
bool savePrefs() { return save_result; }
@@ -396,6 +408,17 @@ static void receive() {
assert(node.offline.back()[1]==128 && node.offline.back()[5]==255);
assert(node.queueChannelText(0,ordinary_key,std::numeric_limits<float>::quiet_NaN(),0,124,"nan",false));
assert(node.offline.back()[1]==0);
node.offline_capacity=8;
const unsigned alerts=node._notifications.calls,quiet_overrides=node.ui.quiet_overrides;
const size_t queued=node.offline.size(),pushes=node.serial.frames.size();
assert(node.queueChannelText(0,ordinary_key,0,255,125,"#EMERGENCY: local notice",true,false));
assert(node.offline.size()==queued+1 && node.serial.frames.size()==pushes+1);
assert(node.serial.frames.back()==std::vector<uint8_t>{PUSH_CODE_MSG_WAITING});
assert(node._notifications.calls==alerts && node.ui.quiet_overrides==quiet_overrides+1);
assert(!node.ui.managed && !node.ui.screen); // Quiet suppression is scoped to this message.
assert(node.queueChannelText(0,ordinary_key,0,0,126,"ordinary message",false));
assert(node._notifications.calls==alerts+1 && node.ui.quiet_overrides==quiet_overrides+1);
assert(!node.ui.managed && !node.ui.screen);
uint8_t empty_secret[32]={}; assert(!node.queueChannelText(MAX_GROUP_CHANNELS,empty_secret,0,0,0,"x",false));
assert(!node.queueChannelText(0,empty_secret,0,0,0,nullptr,false));
puts("PASS receive");
+278 -4
View File
@@ -16,6 +16,7 @@ ROOT = Path(__file__).resolve().parents[1]
HASH_NAMES = (
"#emergency", "#r2-emergency", "#mt-emergency",
"#custom", "#r2-custom", "#mt-custom",
"#EMERGENCY", "#Emergency", "emergency", "Emergency", "#emergency-test",
)
HARNESS = r'''
@@ -378,6 +379,9 @@ struct StrHelper {
};
struct Serial {
uint8_t frame[128] = {}; int length = 0;
bool connected = true;
unsigned pushes = 0;
bool isConnected() const { return connected; }
void writeFrame(const uint8_t* data, int size) {
assert(size >= 0 && size <= static_cast<int>(sizeof(frame)));
memcpy(frame, data, size); length = size;
@@ -391,9 +395,211 @@ COMMAND_MEMBERS = r'''
bool saveChannels() { ++save_calls; return save_result; }
void writeErrFrame(uint8_t code) { error = code; }
void writeOKFrame() { ok = true; }
void seedEmergencyChannelNotice(uint8_t, const ChannelDetails&) {}
void command(uint8_t*, int);
'''
NOTICE_STUBS = r'''
#include <cmath>
#include <cstdlib>
#include <string>
#include <vector>
#include <helpers/CompanionFrameLimits.h>
// Expected failures in the negative controls must not open Windows CRT dialogs.
#undef assert
#define assert(condition) do { if(!(condition)) { \
std::fprintf(stderr,"notice fixture assertion: %s\n",#condition);std::exit(91); \
} } while(false)
using std::isfinite;
#define COMPANION_FEATURE_NOTIFICATIONS 1
#define COMPANION_FEATURE_TEXT_TERMINAL 1
#define DISPLAY_CLASS NoticeDisplay
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#define OFFLINE_QUEUE_SIZE 4
#define ONE_KEY_DM_SHARED_OFFLINE_QUEUE @HELD@
#define MESH_ENABLE_MESHTEXT_CHAT 1
static bool checking_ack_order=false,ack_written=false;
struct NoticeCommandScope {
NoticeCommandScope(){checking_ack_order=true;ack_written=false;}
~NoticeCommandScope(){checking_ack_order=false;}
};
uint32_t millis(){return 123;}
enum class UIEventType { channelMessage };
struct NoticeUI {
unsigned notifications=0,messages=0;
bool override_active=false,override_screen=false,last_quiet=false;
void syncMessageQueue(int,int=-1){assert(!checking_ack_order || ack_written);}
void setMessageNotificationOverride(bool active,bool screen=false){override_active=active;override_screen=screen;}
void notify(UIEventType){++notifications;}
void newMsg(uint8_t,const char*,const char*,int,int,const char*,int){
++messages;last_quiet=override_active && override_screen;
}
};
struct NoticeNotifications {
unsigned calls=0;
bool message(void*,const uint8_t*,bool,uint32_t){++calls;return false;}
bool overridesScreen()const{return false;}
};
struct NoticeClock {uint32_t epoch=1792012345;uint32_t getCurrentTime()const{return epoch;}};
struct NoticeTerminal {unsigned echoes=0;void printf(const char*,...){++echoes;}};
struct NoticeTerminalDisplay {bool shouldShowChannel(bool)const{return true;}};
'''
NOTICE_MEMBERS = r'''
NoticeUI ui; NoticeUI* _ui=&ui;
NoticeNotifications _notifications;
NoticeClock clock; NoticeClock* getRTCClock(){return &clock;}
NoticeTerminal terminal;NoticeTerminalDisplay _terminal_display;
bool hasTerminalOutput()const{return true;}NoticeTerminal& terminalOutput(){return terminal;}
uint8_t app_target_ver=3,out_frame[MAX_FRAME_SIZE]={};
uint8_t pending_emergency_notices[(MAX_GROUP_CHANNELS+7)/8]={};
unsigned held_dm_count=0;
struct Frame {uint8_t len=0,buf[MAX_FRAME_SIZE]={};bool isChannelMsg()const;};
Frame offline_queue[OFFLINE_QUEUE_SIZE];
int offline_queue_len=0,offline_queue_head=0;
int getOfflineQueueCapacity()const;
Frame& offlineQueueFrameAt(int);
bool addToOfflineQueue(const uint8_t*,int);
void popOfflineQueue();
int peekOfflineQueue(uint8_t*);
int getFromOfflineQueue(uint8_t*);
void seedEmergencyChannelNotice(uint8_t,const ChannelDetails&);
void servicePendingChannelNotices();
bool queueChannelText(uint8_t,const uint8_t[32],float,uint8_t,uint32_t,const char*,bool,bool=true);
bool _mt_delivery_config_dirty=false,_mt_profile_dirty=false,valid_transport=true;
struct Seen {void clear(){}} _mt_seen;
bool isMeshtextChannel(const ChannelDetails& value)const{
return parseCompanionChannelAlias(value.name).kind==CompanionChannelKind::Meshtext;
}
bool validateMeshtextChannelChange(int,const ChannelDetails&){return valid_transport;}
void refreshMeshtextProfile(){}
'''
NOTICE_CASES = r'''
static const char notice[]="#EMERGENCY: This is an emergency channel. Do not use for testing or small talk.";
static bool pending(const MyMesh& node,unsigned index){return node.pending_emergency_notices[index/8]&(1U<<(index%8));}
static unsigned effectiveCapacity(const MyMesh& node){
return node.getOfflineQueueCapacity()
#if ONE_KEY_DM_SHARED_OFFLINE_QUEUE
-node.held_dm_count
#endif
;
}
static void verifyNotice(MyMesh& node,unsigned index,int queue_position=-1){
if(queue_position<0) queue_position=node.offline_queue_len-1;
const auto& frame=node.offlineQueueFrameAt(queue_position);
const unsigned base=node.app_target_ver>=3 ? 4:1;
const unsigned text_at=base+7;
assert(frame.buf[0]==(node.app_target_ver>=3 ? RESP_CODE_CHANNEL_MSG_RECV_V3:RESP_CODE_CHANNEL_MSG_RECV));
if(base==4) assert(frame.buf[1]==0 && frame.buf[2]==0 && frame.buf[3]==0);
assert(frame.buf[base]==index && frame.buf[base+1]==0xff && frame.buf[base+2]==TXT_TYPE_PLAIN);
uint32_t timestamp=0;memcpy(&timestamp,frame.buf+base+3,sizeof(timestamp));
assert(timestamp==node.clock.epoch && frame.len==text_at+strlen(notice));
assert(!memcmp(frame.buf+text_at,notice,strlen(notice)));
assert(node.ui.last_quiet && !node.ui.override_active && !node.ui.override_screen);
assert(node.ui.notifications==0 && node._notifications.calls==0);
// An ASCII-origin mOTA owner can leave the terminal output registered.
// A quiet local seed must not inject ASCII into that binary folder stream.
assert(node.terminal.echoes==0);
assert(!pending(node,index));
}
static void fillRealMessages(MyMesh& node){
for(unsigned i=0;i<effectiveCapacity(node);++i){
uint8_t frame[]={RESP_CODE_CHANNEL_MSG_RECV,uint8_t(40+i)};
assert(node.addToOfflineQueue(frame,sizeof(frame)));
}
}
int main(){
for(uint8_t app:{uint8_t(1),uint8_t(3)}) for(bool connected:{false,true})
for(const char* name:{"#emergency","#r2-emergency"}){
MyMesh node;node.app_target_ver=app;node.serial.connected=connected;
ChannelDetails empty{};expectGet(node,9,empty);
auto emergency=channel(name);
applySet(node,9,emergency);
assert(node.ok && node.offline_queue_len==1 && node.ui.messages==1);
verifyNotice(node,9);
assert(node.serial.length==(connected ? 1:0));
if(connected) assert(node.serial.frame[0]==PUSH_CODE_MSG_WAITING);
// A label/key/routing edit and app replay are not new joins.
applySet(node,9,emergency);assert(node.offline_queue_len==1);
auto renamed=channel(!strcmp(name,"#emergency") ? "#r2-emergency":"#emergency");
applySet(node,9,renamed);assert(node.offline_queue_len==1);
for(unsigned i=0;i<16;++i) renamed.channel.secret[i]=uint8_t(i+1);
applySet(node,9,renamed);assert(node.offline_queue_len==1);
renamed.channel.tx_radio=mesh::RADIO_TX_PRIMARY;assert(node.setChannel(9,renamed));
applySet(node,9,renamed);node.servicePendingChannelNotices();
assert(node.offline_queue_len==1 && node.ui.messages==1);
// Later app download sees exactly one ordinary channel-text envelope.
uint8_t frame[MAX_FRAME_SIZE]={};
assert(node.getFromOfflineQueue(frame)>0 && node.offline_queue_len==0);
node.servicePendingChannelNotices();assert(node.offline_queue_len==0);
}
for(const char* name:{"#custom","#EMERGENCY","#Emergency","emergency","Emergency","#emergency-test","#mt-emergency"}){
MyMesh node;applySet(node,0,channel(name));
assert(node.ok && node.offline_queue_len==0 && !pending(node,0));
node.servicePendingChannelNotices();assert(node.offline_queue_len==0);
}
{
// The 32-byte SET remains Meshtext-only; it must not seed a MeshCore notice.
MyMesh node;auto mt=channel("#mt-emergency");
const auto alias=channel("#r2-emergency");memcpy(mt.channel.secret,alias.channel.secret,16);
for(unsigned i=16;i<32;++i) mt.channel.secret[i]=uint8_t(i+1);
applySet(node,0,mt,32);assert(node.ok && node.offline_queue_len==0 && !pending(node,0));
}
{
MyMesh node;auto original=channel("#custom");assert(node.setChannel(0,original));
node.save_result=false;applySet(node,0,channel("#emergency"));
assert(node.error==ERR_CODE_FILE_IO_ERROR && !node.ok && node.offline_queue_len==0 && !pending(node,0));
expectRaw(node,0,original);node.save_result=true;
applySet(node,MAX_GROUP_CHANNELS,channel("#emergency"));
assert(node.error==ERR_CODE_NOT_FOUND && node.offline_queue_len==0);
auto invalid=original;strcpy(invalid.name,"#r2-mt-emergency");applySet(node,0,invalid);
assert(node.error==ERR_CODE_ILLEGAL_ARG && node.offline_queue_len==0);
node.valid_transport=false;applySet(node,0,channel("#emergency"));
assert(node.error==ERR_CODE_BAD_STATE && node.offline_queue_len==0 && !pending(node,0));
}
for(bool cancel:{false,true}){
MyMesh node;node.held_dm_count=ONE_KEY_DM_SHARED_OFFLINE_QUEUE ? 1:0;
fillRealMessages(node);const unsigned full=node.offline_queue_len;
applySet(node,16,channel("#emergency"));
assert(node.ok && pending(node,16) && node.offline_queue_len==int(full));
node.servicePendingChannelNotices();node.servicePendingChannelNotices();
// Even a queue of evictable channel messages must remain untouched.
for(unsigned i=0;i<full;++i){const auto& f=node.offlineQueueFrameAt(i);assert(f.len==2 && f.buf[1]==40+i);}
if(cancel){ChannelDetails deleted{};applySet(node,16,deleted);assert(node.ok && !pending(node,16));}
node.popOfflineQueue();node.servicePendingChannelNotices();
if(cancel) assert(node.offline_queue_len==int(full-1) && node.ui.messages==0);
else {assert(node.offline_queue_len==int(full));verifyNotice(node,16);}
const int length=node.offline_queue_len;
node.servicePendingChannelNotices();assert(node.offline_queue_len==length);
}
{
MyMesh node;fillRealMessages(node);
applySet(node,8,channel("#emergency"));applySet(node,16,channel("#r2-emergency"));
assert(pending(node,8) && pending(node,16));node.popOfflineQueue();node.popOfflineQueue();
node.servicePendingChannelNotices();assert(!pending(node,8) && pending(node,16));verifyNotice(node,8);
node.servicePendingChannelNotices();assert(!pending(node,16));verifyNotice(node,16);
const int length=node.offline_queue_len;node.servicePendingChannelNotices();assert(node.offline_queue_len==length);
}
{
// Loading/reapplying saved channels during startup is not an app join.
MyMesh reboot;assert(reboot.setChannel(9,channel("#emergency")));
reboot.servicePendingChannelNotices();assert(reboot.offline_queue_len==0 && !pending(reboot,9));
ChannelDetails deleted{};applySet(reboot,9,deleted);applySet(reboot,9,channel("#emergency"));
assert(reboot.ok && reboot.offline_queue_len==1);verifyNotice(reboot,9);
}
{
// Optional suppression must not mute normal incoming channel messages.
MyMesh node;node.serial.connected=false;auto ordinary=channel("#emergency");
assert(node.setChannel(0,ordinary));
assert(node.queueChannelText(0,ordinary.channel.secret,0,0,123,"Alice: real traffic",true));
assert(node._notifications.calls==1 && node.ui.notifications==1 && !node.ui.last_quiet);
assert(node.terminal.echoes==1);
}
puts("PASS notice persisted join, legacy/V3 local history, quiet delivery, retry and cancellation");
}
'''
COMMAND_HARNESS = r'''
void MyMesh::command(uint8_t* cmd_frame, int len) {
uint8_t out_frame[128] = {};
@@ -632,7 +838,71 @@ class CompanionChannelAliasTest(unittest.TestCase):
"@METHODS@", methods).replace("@COMMAND_BRANCHES@", branches)
self.run_native_program(source_text, "command")
def run_native_program(self, source_text, expected):
def test_persisted_emergency_join_seeds_quiet_local_history_and_defers_without_eviction(self):
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text(encoding="utf-8")
signatures = (
"bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(",
"bool MyMesh::getChannel(", "bool MyMesh::setChannel(",
"static bool isEmergencyNoticeChannelName(",
"void MyMesh::seedEmergencyChannelNotice(", "void MyMesh::servicePendingChannelNotices(",
"bool MyMesh::queueChannelText(", "bool MyMesh::Frame::isChannelMsg() const",
"int MyMesh::getOfflineQueueCapacity() const", "MyMesh::Frame& MyMesh::offlineQueueFrameAt(",
"bool MyMesh::addToOfflineQueue(", "void MyMesh::popOfflineQueue(",
"int MyMesh::peekOfflineQueue(", "int MyMesh::getFromOfflineQueue(",
)
methods = "\n".join(body(companion, signature) for signature in signatures)
branches = " else ".join(body(companion, signature) for signature in (
"if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2)",
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32)",
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16)",
))
macros = (
"CMD_GET_CHANNEL", "CMD_SET_CHANNEL", "RESP_CODE_CHANNEL_INFO",
"RESP_CODE_CHANNEL_MSG_RECV", "RESP_CODE_CHANNEL_MSG_RECV_V3", "RESP_CODE_CHANNEL_DATA_RECV",
"PUSH_CODE_MSG_WAITING", "ERR_CODE_UNSUPPORTED_CMD", "ERR_CODE_NOT_FOUND",
"ERR_CODE_FILE_IO_ERROR", "ERR_CODE_ILLEGAL_ARG", "ERR_CODE_BAD_STATE",
)
constants = "\n".join(re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", companion,
flags=re.MULTILINE).group(0) for name in macros)
text_helpers = (ROOT / "src/helpers/TxtDataHelpers.h").read_text(encoding="utf-8")
constants += "\n" + re.search(r"^#define\s+TXT_TYPE_PLAIN\s+[^\r\n]+", text_helpers,
flags=re.MULTILINE).group(0)
save_getter = body(header, "bool getChannelForSave(uint8_t").replace("override", "")
members = COMMAND_MEMBERS.replace(
" void seedEmergencyChannelNotice(uint8_t, const ChannelDetails&) {}\n", "")
members = members.replace("void writeOKFrame() { ok = true; }",
"void writeOKFrame() { ok = true; ack_written = true; }")
fixture = RUNTIME_HARNESS[:RUNTIME_HARNESS.index("\nint main()")]
fixture = fixture.replace("#define MAX_GROUP_CHANNELS 5", "#define MAX_GROUP_CHANNELS 17")
fixture = fixture.replace("class MyMesh :", COMMAND_STUBS + "\nclass MyMesh :")
fixture = fixture.replace("@SAVE_GETTER@", save_getter + "\n" + members + NOTICE_MEMBERS)
command_helpers = COMMAND_HARNESS[:COMMAND_HARNESS.index("\nint main()")]
command_helpers = command_helpers.replace("uint8_t out_frame[128] = {};",
"NoticeCommandScope ack_scope; uint8_t out_frame[128] = {};")
shared = HARNESS[:HARNESS.index("static void assertAlias(")]
vectors, key_vectors = oracle_vectors()
program = (shared + constants + "\n" + NOTICE_STUBS + fixture + command_helpers + NOTICE_CASES)
program = program.replace("@VECTORS@", vectors).replace("@KEY_VECTORS@", key_vectors)
program = program.replace("@METHODS@", methods).replace("@COMMAND_BRANCHES@", branches)
for held in (0, 1):
with self.subTest(shared_held_dm_queue=held):
self.run_native_program(program.replace("@HELD@", str(held)), "notice",
extra_flags=("-Wno-unused-parameter",))
# Prove the executable fixture catches the two dangerous regressions:
# treating a local seed like a real audible message, or letting the
# general queue policy evict real traffic to make room for the seed.
self.assertIn("true, false)) pending", program)
self.run_native_program(program.replace("@HELD@", "0").replace(
"true, false)) pending", "true, true)) pending", 1), "notice",
extra_flags=("-Wno-unused-parameter",), expect_failure="node.ui.last_quiet")
self.assertIn("if (offline_queue_len >= capacity) return;", program)
self.run_native_program(program.replace("@HELD@", "1").replace(
"if (offline_queue_len >= capacity) return;",
"if (offline_queue_len > capacity) return;", 1), "notice",
extra_flags=("-Wno-unused-parameter",), expect_failure="pending(node,16)")
def run_native_program(self, source_text, expected, extra_flags=(), expect_failure=None):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
with tempfile.TemporaryDirectory(prefix="meshcore-channel-runtime-") as directory:
work = Path(directory)
@@ -642,14 +912,18 @@ class CompanionChannelAliasTest(unittest.TestCase):
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
built = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *sanitizers,
[compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *extra_flags, *sanitizers,
"-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=30)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("PASS " + expected, checked.stdout)
if expect_failure:
self.assertEqual(checked.returncode, 91, checked.stdout + checked.stderr)
self.assertIn(expect_failure, checked.stderr)
else:
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("PASS " + expected, checked.stdout)
if __name__ == "__main__":
+4 -1
View File
@@ -56,7 +56,10 @@ class CompanionMeshtextTest(unittest.TestCase):
mt = (ROOT / "examples/companion_radio/MeshtextCompanion.cpp").read_text(encoding="utf-8")
mt = mt[mt.index("namespace {"):].rsplit("#endif", 1)[0]
signatures = ("bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(",
"bool MyMesh::getChannel(", "bool MyMesh::setChannel(", "bool MyMesh::queueChannelText(",
"bool MyMesh::getChannel(", "bool MyMesh::setChannel(",
"static bool isEmergencyNoticeChannelName(",
"void MyMesh::seedEmergencyChannelNotice(",
"void MyMesh::servicePendingChannelNotices(", "bool MyMesh::queueChannelText(",
"bool MyMesh::saveAdvertName(")
methods = "\n".join(body(source, signature) for signature in signatures)
utils_source = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8")