diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md
index c440fee26..6447194dd 100644
--- a/docs/radio_profiles.md
+++ b/docs/radio_profiles.md
@@ -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
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
index 5c6bd4284..dc8f53814 100644
--- a/examples/companion_radio/MyMesh.cpp
+++ b/examples/companion_radio/MyMesh.cpp
@@ -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
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
index d28e4838f..005ac4458 100644
--- a/examples/companion_radio/MyMesh.h
+++ b/examples/companion_radio/MyMesh.h
@@ -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 offline_queue;
diff --git a/test/fixtures/companion_lost_reply.cpp b/test/fixtures/companion_lost_reply.cpp
index 5487a9c29..f1a05b1cf 100644
--- a/test/fixtures/companion_lost_reply.cpp
+++ b/test/fixtures/companion_lost_reply.cpp
@@ -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 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&);
diff --git a/test/fixtures/companion_meshtext.cpp b/test/fixtures/companion_meshtext.cpp
index c3fec8ecf..d1f1f3ba8 100644
--- a/test/fixtures/companion_meshtext.cpp
+++ b/test/fixtures/companion_meshtext.cpp
@@ -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> 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::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{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");
diff --git a/test/test_companion_channel_alias.py b/test/test_companion_channel_alias.py
index c6b16c977..d16b9ef9c 100644
--- a/test/test_companion_channel_alias.py
+++ b/test/test_companion_channel_alias.py
@@ -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(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
+#include
+#include
+#include
+#include
+// 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(×tamp,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;i0 && 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= 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__":
diff --git a/test/test_companion_meshtext.py b/test/test_companion_meshtext.py
index d82a1a2b1..b54aa7819 100644
--- a/test/test_companion_meshtext.py
+++ b/test/test_companion_meshtext.py
@@ -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")