// Template compiled by test_companion_meshtext.py. Markers insert actual // production method bodies; all boundary substitutes are visible below. @FIXTURE@ #include #include #include #include #include #include #include using std::isfinite; @CONSTANTS@ #define MAX_GROUP_CHANNELS 8 #define COMPANION_FEATURE_NOTIFICATIONS 1 #define COMPANION_FEATURE_TEXT_TERMINAL 0 #define TXT_TYPE_PLAIN 0 void mesh::Utils::sha256(uint8_t* out, size_t size, const uint8_t* data, int length) { assert(length >= 0 && size <= 32); SHA256 hash; hash.update(data, length); hash.finalize(out, size); } struct Preferences { static inline std::vector blob; static inline bool fail_begin=false, fail_write=false, fail_read=false; static inline unsigned writes=0; bool begin(const char* name, bool read_only) { assert(!strcmp(name,"mesh-mt") && !read_only); return !fail_begin; } size_t getBytesLength(const char* key) { assert(!strcmp(key,"settings")); return blob.size(); } size_t getBytes(const char*, void* out, size_t size) { if(fail_read) return 0; size=std::min(size,blob.size()); memcpy(out,blob.data(),size); return size; } size_t putBytes(const char*, const void* in, size_t size) { ++writes; if(fail_write) return 0; const auto* data=static_cast(in); blob.assign(data,data+size); return size; } void end() {} bool remove(const char* key) { assert(!strcmp(key,"settings")); if(fail_write) return false; blob.clear(); return true; } static void reset() { blob.clear(); fail_begin=fail_write=fail_read=false; writes=0; } }; static uint32_t test_random_seed=0x12345678; static uint32_t esp_random() { return test_random_seed; } namespace mesh { struct GroupChannel { uint8_t hash[1]={},secret[32]={},tx_radio=RADIO_TX_AUTO; }; } struct ChannelDetails { mesh::GroupChannel channel; char name[32]={}; }; struct BaseChatMesh { ChannelDetails stored[MAX_GROUP_CHANNELS]; bool getChannel(int index, ChannelDetails& out) { if(index<0 || index>=MAX_GROUP_CHANNELS) return false; out=stored[index]; return true; } bool setChannel(int index, const ChannelDetails& in) { if(index<0 || index>=MAX_GROUP_CHANNELS) return false; stored[index]=in; mesh::Utils::sha256(stored[index].channel.hash,1,in.channel.secret, mesh::Utils::isZeroes(in.channel.secret+16,16)?16:32); return true; } }; struct MtRadio : Radio { bool supported=true; bool physical_airtime=false; uint32_t fixed_airtime=10; std::vector> airtime_calls; mesh::RadioProfiles& _profiles=p; bool supportsMeshtext() const override { return supported; } @PROFILE_AIRTIME@ uint32_t getProfileAirtime(uint8_t profile,int length,uint8_t cr=0) override { airtime_calls.emplace_back(profile,length); return physical_airtime ? actualProfileAirtime(profile,length,cr):fixed_airtime; } }; struct Serial { bool connected=false; std::vector> frames; bool isConnected() { return connected; } void writeFrame(const uint8_t* bytes,int size) { assert(size>=0); frames.emplace_back(bytes,bytes+size); } }; 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; } bool overridesScreen() const { return false; } }; struct StrHelper { static void strncpy(char* out,const char* in,size_t size) { ::strncpy(out,in,size-1); out[size-1]=0; } }; class MyMesh : public BaseChatMesh { public: MemoryFS fs; Clock clock; MtRadio radio; mesh::Radio* _radio=&radio; mesh::RadioProfileCLI _radio_profiles; struct { float freq=909.5; char node_name[32]="Test"; uint8_t flood_retry_attempts=15,flood_retry_max_path=1; uint8_t flood_retry_group_max_path=0xff,flood_retry_advert_enabled=1; } _prefs; mesh::LocalIdentity self_id; mesh::mt::Region _mt_region=mesh::mt::Region::Unset; uint32_t _mt_slot=0,_mt_node=0,_mt_packet_id=0,_mt_sent=0,_mt_failed=0,_mt_received=0; uint32_t _mt_default_node=0,_mt_node_override=0; uint8_t _mt_hop_limit=7; bool _mt_rebroadcast=false; char _mt_short_name[17]={}; mesh::mt::SeenPackets _mt_seen; mesh::mt::PendingFrame _mt_delivery[mesh::mt::DeliverySlots]; int8_t _mt_tx_slot=-1; uint8_t _mt_nodeinfo_fingerprint[MAX_GROUP_CHANNELS][8]={}; bool _mt_nodeinfo_announced[MAX_GROUP_CHANNELS]={},_mt_nodeinfo_pending[MAX_GROUP_CHANNELS]={}; uint32_t _mt_nodeinfo_generation=0,_mt_retried=0,_mt_heard=0,_mt_exhausted=0; uint32_t _mt_last_tx_completed_at=0; struct Random { uint32_t jitter=0; uint32_t nextInt(uint32_t low,uint32_t high) { assert(low==0 && high==201 && jitter> 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; MyMesh() { g_mock_millis=0; self_id.pub_key[0]=42; _radio_profiles.begin(&fs,&radio,&clock); } bool isAliasRadio2Defined() const; bool resolveChannelAlias(ChannelDetails&) const; bool getChannel(int,ChannelDetails&); bool setChannel(int,const ChannelDetails&); bool getChannelForSave(uint8_t index,ChannelDetails& value) { return BaseChatMesh::getChannel(index,value); } mesh::mt::Region meshtextRegion() const; void refreshMeshtextIdentity(); void beginMeshtext(); bool saveMeshtextSettings(mesh::mt::Region,uint32_t,uint8_t hops=UINT8_MAX, const char* short_name=nullptr,uint32_t node_override=UINT32_MAX, uint8_t rebroadcast=UINT8_MAX); bool eraseMeshtextSettings(); bool isMeshtextChannel(const ChannelDetails&) const; bool validateMeshtextChannelChange(int,const ChannelDetails&); void refreshMeshtextProfile(); bool sendMeshtextText(const ChannelDetails&,const char*,size_t); bool sendMeshtextNodeInfo(const ChannelDetails&); bool queueMeshtextDelivery(const ChannelDetails&,const uint8_t*,size_t,bool); void maybeRebroadcastMeshtext(const ChannelDetails&,const uint8_t*,size_t); bool preemptMeshtextRelay(); int meshtextChannelIndex(const ChannelDetails&,uint8_t[8]); bool meshtextDeliveryCurrent(const mesh::mt::PendingFrame&); uint8_t meshtextRetryLimit(const mesh::mt::PendingFrame&) const; uint32_t meshtextRetryDelay(const mesh::mt::PendingFrame&); void loopMeshtextDelivery(); 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=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; ++foreign_admissions; foreign.emplace_back(bytes,bytes+size); foreign_profile=profile; foreign_generation=generation; foreign_pending=true; return true; } void completeForeign(bool success=true) { assert(foreign_pending); // The real Dispatcher releases queue ownership before the application // completion callback. Successful RF TX is not a delivery acknowledgement. foreign_pending=false; foreign_sending=false; onForeignRadioSendComplete(success); } bool discardQueuedForeignRadioFrame(mesh::RadioProfileProtocol protocol) { assert(protocol==mesh::RadioProfileProtocol::Meshtext); if(!foreign_pending || foreign_sending) return false; completeForeign(false); return true; } void releaseForeign() { if(foreign_pending) completeForeign(false); loopMeshtextDelivery(); } mesh::RTCClock* getRTCClock() { return &clock; } 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); offline_queue_len=offline.size(); return true; } bool saveChannels() { return save_result; } bool savePrefs() { return save_result; } bool saveAdvertName(const char*); void writeErrFrame(uint8_t value) { error=value; } void writeOKFrame() { ok=true; } bool sendGroupMessage(uint32_t,mesh::GroupChannel&,const char*,const char*,int) { ++meshcore_sends; return true; } void command(uint8_t* cmd_frame,int len); void commit(int index,const ChannelDetails& value) { assert(setChannel(index,value)); _mt_profile_dirty=true; refreshMeshtextProfile(); } const char* cli(const char* command,bool expected=true) { static char reply[256]; assert(_radio_profiles.handle(command,reply,sizeof(reply))); if(strncmp(command,"get ",4) && strncmp(command,"del ",4)) { if((!strncmp(reply,"OK",2))!=expected) { fprintf(stderr,"%s -> %s\n",command,reply); assert(false); } } return reply; } void advance(uint32_t ms) { g_mock_millis+=ms; clock.epoch+=ms/1000; _radio_profiles.loop(); refreshMeshtextProfile(); loopMeshtextDelivery(); } }; @METHODS@ void MyMesh::command(uint8_t* cmd_frame,int len) { error=-1; ok=false; serial.frames.clear(); @COMMANDS@ } static ChannelDetails channel(const char* name,int private_bytes=0) { ChannelDetails value; assert(strlen(name)(name),strlen(name)); return value; } static void active(MyMesh& node) { node.beginMeshtext(); node.commit(0,channel("#mt-longfast")); assert(node._mt_active && node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::Meshtext); } static void assertUntouched(const MyMesh& node) { assert(node.foreign.empty() && !node.meshcore_sends); } static void keys() { Preferences::reset(); MyMesh node; active(node); auto raw=channel("#mt-longfast"); ChannelDetails stored; assert(node.getChannelForSave(0,stored) && !memcmp(raw.channel.secret,stored.channel.secret,32)); mesh::mt::Channel wire; assert(describe(stored,wire)); assert(!strcmp(wire.name,"LongFast") && wire.named_preset && wire.key_length==16); assert(!memcmp(wire.key,mesh::mt::DefaultPsk,16)); ChannelDetails policy; assert(node.getChannel(0,policy)); assert(policy.channel.tx_radio==mesh::RADIO_TX_OFF); assert(memcmp(policy.channel.secret,raw.channel.secret,16)); for(const char* name:{"#mt-LoNgFaSt","#mt-shortturbo","#mt-longmoderate"}) { auto c=channel(name); const auto before=c; assert(describe(c,wire) && !memcmp(wire.key,mesh::mt::DefaultPsk,16)); assert(!memcmp(c.channel.secret,before.channel.secret,32)); } for(int size:{16,32}) { auto c=channel("#mt-longfast",size); const auto before=c; assert(describe(c,wire) && wire.key_length==size && !memcmp(wire.key,c.channel.secret,32)); assert(!memcmp(c.channel.secret,before.channel.secret,32)); node.releaseForeign(); node.commit(0,c); assert(node.sendMeshtextText(c,"private",7)); mesh::mt::DecodedText decoded; assert(mesh::mt::decodeText(node.foreign.back().data(),node.foreign.back().size(),wire.name,wire.key,size,1,decoded)); assert(!strcmp(decoded.text,"private")); } auto custom=channel("#mt-emergency"); assert(describe(custom,wire)); auto ordinary=channel("#emergency"); assert(!memcmp(wire.key,ordinary.channel.secret,16)); assert(!wire.named_preset && !strcmp(wire.name,"emergency")); auto invalid=channel("#mt-abcdefghijkl"); assert(!describe(invalid,wire)); puts("PASS keys"); } static void automatic() { Preferences::reset(); MyMesh node; assert(!node.radio.p.enabled()); active(node); assert(node.radio.p.secondary.mode==mesh::RadioProfileMode::RxTx); assert(node.radio.p.secondary.params.bw==250 && node.radio.p.secondary.params.sf==11); assert(node._radio_profiles.autoSecondaryActive() && node.fs.files.empty() && Preferences::writes==0); const auto generation=node.radio.p.generation[1]; node.refreshMeshtextProfile(); assert(node.radio.p.generation[1]==generation); assertUntouched(node); MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.fs=node.fs; reboot._radio_profiles.begin(&reboot.fs,&reboot.radio,&reboot.clock); reboot.beginMeshtext(); assert(reboot._mt_active && reboot._radio_profiles.autoSecondaryActive()); assertUntouched(reboot); node.commit(0,{}); assert(!node._mt_active && !node.radio.p.enabled()); assertUntouched(node); puts("PASS auto"); } static void conflicts() { Preferences::reset(); MyMesh node; active(node); assert(!node.validateMeshtextChannelChange(1,channel("#mt-longturbo"))); assert(!node.validateMeshtextChannelChange(1,channel("#r2-emergency"))); node.commit(1,channel("#mt-longturbo")); assert(!node._mt_active && !node.radio.p.enabled()); node.commit(1,{}); assert(node._mt_active); assert(node.validateMeshtextChannelChange(1,channel("#mt-emergency",32))); node.commit(1,channel("#mt-emergency",32)); node.commit(0,{}); assert(!node._mt_active && !node.radio.p.enabled()); assert(node.isMeshtextChannel(node.stored[1])); assert(!node.sendMeshtextText(node.stored[1],"x",1)); node.commit(1,{}); node._prefs.freq=915; node.commit(0,channel("#mt-longfast")); assert(!node._mt_active && strstr(node._mt_status,"ambiguous")); assert(!node.validateMeshtextChannelChange(0,channel("#mt-longfast"))); assert(node.saveMeshtextSettings(mesh::mt::Region::US,0) && node._mt_active); assert(node.saveMeshtextSettings(mesh::mt::Region::EU868,0) && node._mt_active); assert(!node.validateMeshtextChannelChange(0,channel("#mt-shortturbo"))); node.commit(0,channel("#mt-shortturbo")); assert(!node._mt_active); node.commit(0,channel("#mt-longfast")); assert(node._mt_active); assert(node.saveMeshtextSettings(mesh::mt::Region::EU868,2)); assert(!node._mt_active); assert(node.saveMeshtextSettings(mesh::mt::Region::EU868,1)); assert(node._mt_active); assert(std::fabs(node.radio.p.secondary.params.freq-869.525f)<0.0001f); node._radio_available=false; node.refreshMeshtextProfile(); assert(!node._mt_active); node._radio_available=true; node.refreshMeshtextProfile(); assert(node._mt_active); node.commit(0,channel("ordinary")); assert(!node._mt_active && !node.radio.p.enabled()); assertUntouched(node); puts("PASS conflicts"); } static void manual() { Preferences::reset(); MyMesh node; node.beginMeshtext(); node.cli("set radio2 906.875,250,11,5,rxtx,80"); node.advance(2000); const auto saved_files=node.fs.files; const auto saved=node._radio_profiles.savedSecondaryConfig(); node.commit(0,channel("#mt-longfast")); assert(node._mt_active); assert(node.radio.p.secondary.params.freq==906.875f && node.radio.p.secondary.params.preamble==16); assert(node._radio_profiles.savedSecondaryConfig().params==saved.params && node.fs.files==saved_files); node.cli("set tempradio2 909.5,62.5,7,5,rx,1,80"); node.advance(2000); assert(!node._mt_active && node.radio.p.secondary_temporary); assert(node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::MeshCore); node.advance(60000); assert(node._mt_active && !node.radio.p.secondary_temporary); node.commit(0,{}); assert(!node._mt_active); assert(node.radio.p.secondary.params==saved.params && node.fs.files==saved_files); node.cli("set radio2 off"); node.advance(2000); const auto off_files=node.fs.files; node.commit(0,channel("#mt-longfast")); assert(!node._mt_active && !node.radio.p.enabled()); node.commit(0,{}); assert(!node.radio.p.enabled() && node.fs.files==off_files); node.cli("set radio2 auto"); node.advance(2000); node.commit(0,channel("#mt-longfast")); assert(node._mt_active && node._radio_profiles.autoSecondaryActive()); // An external/temporary owner is not ours to disable when a channel vanishes. auto external=node.radio.p.secondary; external.params.freq=910.25; external.params.bw=125; external.params.sf=8; external.params.cr=5; external.params.preamble=80; external.params.protocol=mesh::RadioProfileProtocol::MeshCore; node.radio.p.setSecondary(external,false); node.refreshMeshtextProfile(); assert(!node._mt_active && node.radio.p.secondary.params==external.params); node.commit(0,{}); assert(node.radio.p.secondary.params==external.params); assertUntouched(node); puts("PASS manual"); } static void settings() { Preferences::reset(); MyMesh node; active(node); assert(node._mt_node && node._mt_packet_id); assert(node.saveMeshtextSettings(mesh::mt::Region::ANZ,1)); assert(Preferences::blob.size()==40 && Preferences::writes==1); const auto committed=Preferences::blob; Preferences::fail_write=true; assert(!node.saveMeshtextSettings(mesh::mt::Region::US,2)); assert(node._mt_region==mesh::mt::Region::ANZ && node._mt_slot==1 && Preferences::blob==committed); assert(!node._mt_settings_valid && !node._mt_active && !node.radio.p.enabled()); Preferences::fail_write=false; node.foreign_pending=true; const auto writes=Preferences::writes; assert(!node.saveMeshtextSettings(mesh::mt::Region::US,0)); assert(Preferences::writes==writes); node.foreign_pending=false; assert(!node.saveMeshtextSettings(mesh::mt::Region::US,1025)); assert(node.saveMeshtextSettings(mesh::mt::Region::ANZ,1) && node._mt_active); MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext(); assert(reboot._mt_region==mesh::mt::Region::ANZ && reboot._mt_slot==1 && reboot._mt_active); Preferences::blob[5]=1; MyMesh corrupt; corrupt.stored[0]=node.stored[0]; corrupt.beginMeshtext(); assert(!corrupt._mt_settings_valid && !corrupt._mt_active && !corrupt.radio.p.enabled()); char reply[160]; assert(corrupt.handleMeshtextCommand("set mt.region us",reply,sizeof(reply))); assert(!strncmp(reply,"OK",2) && corrupt._mt_active && corrupt._mt_settings_valid); assert(corrupt.handleMeshtextCommand("set mt.slot 1junk",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(corrupt.handleMeshtextCommand("get mt.status",reply,sizeof(reply)) && strstr(reply,"RX time-shared")); assert(!corrupt.handleMeshtextCommand("get unknown",reply,sizeof(reply))); // The atomic blob may have committed despite failed readback: suspend RAM, // then reconstruct the committed choice only after a successful new read. Preferences::fail_read=true; assert(!corrupt.saveMeshtextSettings(mesh::mt::Region::ANZ,2)); assert(!corrupt._mt_settings_valid && !corrupt._mt_active && !corrupt.radio.p.enabled()); Preferences::fail_read=false; MyMesh readback; readback.stored[0]=corrupt.stored[0]; readback.beginMeshtext(); assert(readback._mt_active && readback._mt_region==mesh::mt::Region::ANZ && readback._mt_slot==2); Preferences::fail_write=true; assert(!readback.eraseMeshtextSettings()); assert(!readback._mt_active && !readback._mt_settings_valid && !readback.radio.p.enabled()); Preferences::fail_write=false; assert(readback.eraseMeshtextSettings() && Preferences::blob.empty()); assert(readback._mt_region==mesh::mt::Region::Unset && readback._mt_slot==0); assert(readback._mt_hop_limit==7); readback.refreshMeshtextProfile(); assert(!readback._mt_active); Preferences::fail_begin=true; assert(!corrupt.eraseMeshtextSettings()); assert(!corrupt._mt_active && !corrupt._mt_settings_valid && !corrupt.radio.p.enabled()); Preferences::fail_begin=false; assertUntouched(node); assertUntouched(reboot); assertUntouched(corrupt); puts("PASS settings"); } static const uint8_t rx_frame[]=@RX_FRAME@,rx_retry[]=@RX_RETRY@,rx_dm[]=@RX_DM@, rx_ack[]=@RX_ACK@,rx_other[]=@RX_OTHER@; static bool receiveFrame(MyMesh& node,const uint8_t* bytes,size_t size,float snr=3) { return node.onForeignRadioFrame(bytes,size,1,node.radio.p.generation[1],snr,-90); } static bool validUtf8(const uint8_t* text,size_t size) { uint8_t frame[255]; return mesh::mt::encodeText(1,1,3,"LongFast",mesh::mt::DefaultPsk,16, reinterpret_cast(text),size,frame)!=0; } static void receive() { Preferences::reset(); MyMesh node; active(node); assert(!node.onForeignRadioFrame(rx_frame,sizeof(rx_frame),0,node.radio.p.generation[1],3,-90)); assert(!node.onForeignRadioFrame(rx_frame,sizeof(rx_frame),1,node.radio.p.generation[1]-1,3,-90)); assert(!receiveFrame(node,rx_dm,sizeof(rx_dm))); assert(!receiveFrame(node,rx_other,sizeof(rx_other))); assert(node.offline.empty()); assert(receiveFrame(node,rx_frame,sizeof(rx_frame),100)); assert(node.offline.size()==1 && node._mt_received==1 && node._notifications.calls==1); auto envelope=node.offline.back(); assert(envelope[0]==RESP_CODE_CHANNEL_MSG_RECV_V3); assert(envelope[1]==127 && envelope[2]==0 && envelope[3]==0 && envelope[4]==0); assert(envelope[5]==0 && envelope[6]==TXT_TYPE_PLAIN && envelope.size()<=MAX_FRAME_SIZE); uint32_t timestamp=0; memcpy(×tamp,envelope.data()+7,sizeof(timestamp)); assert(timestamp==node.clock.epoch); assert(validUtf8(envelope.data()+11,envelope.size()-11)); assert(!memcmp(envelope.data()+11,"!11223344: ",11)); assert(envelope.size()==175); // 153 ASCII bytes remain; no partial euro scalar. ChannelDetails policy; assert(node.getChannel(0,policy)); assert(!memcmp(node._notifications.key,policy.channel.secret,32)); assert(memcmp(node._notifications.key,mesh::mt::DefaultPsk,16)); assert(receiveFrame(node,rx_frame,sizeof(rx_frame)) && node.offline.size()==1); node.offline_capacity=1; assert(receiveFrame(node,rx_retry,sizeof(rx_retry))); assert(node._mt_received==1 && !node._mt_seen.seen(0x11223344,102)); node.offline_capacity=4; node.app_target_ver=1; node.serial.connected=true; assert(receiveFrame(node,rx_retry,sizeof(rx_retry)) && node._mt_received==2); assert(node.offline.back()[0]==RESP_CODE_CHANNEL_MSG_RECV && node.offline.back()[1]==0); assert(node.serial.frames.size()==1 && node.serial.frames[0][0]==PUSH_CODE_MSG_WAITING); assert(receiveFrame(node,rx_ack,sizeof(rx_ack)) && node._mt_received==3); assertUntouched(node); // request-ACK produces no radio response, no relay node.offline_capacity=6; uint8_t ordinary_key[32]={1}; node.app_target_ver=3; assert(node.queueChannelText(0,ordinary_key,-100,255,123,"negative",false)); 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"); } static void transmit() { Preferences::reset(); MyMesh node; active(node); assertUntouched(node); uint8_t request[32]={CMD_SEND_CHANNEL_TXT_MSG,TXT_TYPE_PLAIN,0}; memcpy(request+7,"hello",5); node.command(request,12); assert(node.ok && node.error==-1 && node.foreign.size()==1 && !node.meshcore_sends); assert(node.foreign_profile==1 && node.foreign_generation==node.radio.p.generation[1]); mesh::mt::DecodedText decoded; auto& frame=node.foreign.back(); assert(mesh::mt::decodeText(frame.data(),frame.size(),"LongFast",mesh::mt::DefaultPsk,16,1,decoded)); assert(!strcmp(decoded.text,"hello")); assert(decoded.hop_limit==7 && decoded.hop_start==7 && decoded.hops==0); assert(frame[12]==0xe7); // limit/start 7, no want-ACK or MQTT flag. assert(!mesh::mt::decodeText(frame.data(),frame.size(),"LongFast",mesh::mt::DefaultPsk,16,node._mt_node,decoded)); node.command(request,12); assert(!node.ok && node.error==ERR_CODE_BAD_STATE && node.foreign.size()==1); node.completeForeign(); node.onForeignRadioSendComplete(false); assert(node._mt_sent==1 && node._mt_failed==1); char oversized[229]; memset(oversized,'x',sizeof(oversized)); assert(!node.sendMeshtextText(node.stored[0],oversized,sizeof(oversized))); assert(!node.sendMeshtextText(node.stored[0],"x\0y",3)); node._mt_packet_id=UINT32_MAX; assert(node.sendMeshtextText(node.stored[0],"last",4)); node.foreign_pending=false; assert(node._mt_packet_id==0 && !node.sendMeshtextText(node.stored[0],"x",1)); assert(node.offline.empty() && node._notifications.calls==0); puts("PASS transmit"); } static void applySet(MyMesh& node,int index,const ChannelDetails& value,int key_size=16) { uint8_t frame[66]={CMD_SET_CHANNEL,static_cast(index)}; memcpy(frame+2,value.name,32); memcpy(frame+34,value.channel.secret,key_size); node.command(frame,34+key_size); } static void commands() { Preferences::reset(); MyMesh node; active(node); ChannelDetails raw; auto original=node.stored[0]; original.channel.tx_radio=mesh::RADIO_TX_BOTH; node.commit(0,original); uint8_t get[]={CMD_GET_CHANNEL,0}; node.command(get,sizeof(get)); assert(node.serial.frames.size()==1 && node.serial.frames[0].size()==50); assert(!memcmp(node.serial.frames[0].data()+34,original.channel.secret,16)); applySet(node,0,original); assert(node.ok && node.getChannelForSave(0,raw)); assert(raw.channel.tx_radio==mesh::RADIO_TX_BOTH && !memcmp(raw.channel.secret,original.channel.secret,32)); auto custom=channel("#mt-longfast",16); node.save_result=false; applySet(node,0,custom); assert(node.error==ERR_CODE_FILE_IO_ERROR && !node.ok && node.getChannelForSave(0,raw)); assert(!memcmp(raw.channel.secret,original.channel.secret,32)); node.save_result=true; applySet(node,0,custom); assert(node.ok && node.getChannelForSave(0,raw)); assert(!memcmp(raw.channel.secret,custom.channel.secret,32) && raw.channel.tx_radio==mesh::RADIO_TX_AUTO); applySet(node,1,channel("#mt-longturbo")); assert(node.error==ERR_CODE_BAD_STATE && !node.ok); applySet(node,1,channel("#mt-mt-room")); assert(node.error==ERR_CODE_ILLEGAL_ARG && !node.ok); auto aes256=channel("#mt-longfast",32); applySet(node,0,aes256,32); assert(node.ok && node.getChannelForSave(0,raw) && !memcmp(raw.channel.secret,aes256.channel.secret,32)); // Original two-byte GET is still a 16-byte response; opt-in GET exposes 32. node.command(get,sizeof(get)); assert(node.serial.frames[0].size()==50); uint8_t extended_get[]={CMD_GET_CHANNEL,0,32}; node.command(extended_get,sizeof(extended_get)); assert(node.serial.frames[0].size()==66 && !memcmp(node.serial.frames[0].data()+34,aes256.channel.secret,32)); auto rename=aes256; strcpy(rename.name,"#mt-LoNgFaSt"); applySet(node,0,rename); assert(node.ok && node.getChannelForSave(0,raw) && !memcmp(raw.channel.secret,aes256.channel.secret,32)); node.save_result=false; auto changed=channel("#mt-longfast",32); changed.channel.secret[20]^=0x80; applySet(node,0,changed,32); assert(node.error==ERR_CODE_FILE_IO_ERROR && !node.ok && node.getChannelForSave(0,raw)); assert(!memcmp(raw.channel.secret,aes256.channel.secret,32)); node.save_result=true; auto downgrade=channel("#mt-longfast",16); applySet(node,0,downgrade,32); assert(node.ok && node.getChannelForSave(0,raw) && mesh::Utils::isZeroes(raw.channel.secret+16,16)); auto mc=channel("private",32); applySet(node,1,mc,32); assert(node.error==ERR_CODE_UNSUPPORTED_CMD && !node.ok); node.commit(1,channel("ordinary")); uint8_t get_mc[]={CMD_GET_CHANNEL,1,32}; node.command(get_mc,sizeof(get_mc)); assert(node.serial.frames[0].size()==50); node.commit(1,{}); applySet(node,0,{}); assert(node.ok && !node._mt_active && !node.radio.p.enabled()); assertUntouched(node); puts("PASS commands"); } static void privateCLI() { Preferences::reset(); MyMesh node; node.beginMeshtext(); char reply[256]; const char* key128="0102030405060708090a0b0c0d0e0f10"; const char* key256="0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20"; char command[160]; snprintf(command,sizeof(command),"set mt.channel 0 mt-longfast %s",key256); assert(node.handleMeshtextCommand(command,reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_active && node._radio_profiles.autoSecondaryActive()); auto expected=channel("mt-longfast",32); assert(!memcmp(node.stored[0].channel.secret,expected.channel.secret,32)); assert(!strstr(reply,key256) && !strstr(reply,key128)); assert(node.handleMeshtextCommand("get mt.channel 0",reply,sizeof(reply))); assert(strstr(reply,"AES-256") && strstr(reply,"key not displayed") && !strstr(reply,key256)); const auto previous=node.stored[0]; node.save_result=false; snprintf(command,sizeof(command),"set mt.channel 0 mt-longfast %s",key128); assert(node.handleMeshtextCommand(command,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(!memcmp(node.stored[0].channel.secret,previous.channel.secret,32)); assert(node.handleMeshtextCommand("delete mt.channel 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(!memcmp(node.stored[0].channel.secret,previous.channel.secret,32)); node.save_result=true; for(const char* invalid:{"set mt.channel -1 mt-longfast AQ==", "set mt.channel 8 mt-longfast AQ==", "set mt.channel 999999999999999999999 mt-longfast AQ==", "set mt.channel 1 ordinary AQ==", "set mt.channel 1 #mt-mt-room AQ==", "set mt.channel 1 mt-longfast BADKEY", "set mt.channel 1 mt-longfast 0102030405060708090a0b0c0d0e0f1Z", "set mt.channel 1 mt-longfast 00000000000000000000000000000000", "set mt.channel 1 mt-longfast AQ== extra", "set mt.channel 1 mt-longfast", "delete mt.channel 0 extra", "get mt.channel 0 extra"}) { assert(node.handleMeshtextCommand(invalid,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(!node.stored[1].name[0] && !memcmp(node.stored[0].channel.secret,previous.channel.secret,32)); } assert(node.handleMeshtextCommand("set mt.channel 1 mt-team AQ==",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_active && !memcmp(node.stored[1].channel.secret,mesh::mt::DefaultPsk,16)); assert(node.handleMeshtextCommand("delete mt.channel 0",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(!node._mt_active && !node.radio.p.enabled()); assert(node.handleMeshtextCommand("set mt.preset longfast",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(!node._mt_active); node.advance(2000); assert(node._mt_active && node._radio_profiles.manualSecondaryConfigured()); const auto saved_files=node.fs.files; assert(node.handleMeshtextCommand("delete mt.channel 1",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(!node._mt_active && node.radio.p.enabled() && node.fs.files==saved_files); assert(node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::MeshCore); assert(node.handleMeshtextCommand("set mt.channel 1 mt-team AQ==",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_active); assert(node.handleMeshtextCommand("get mt.channel 1",reply,sizeof(reply)) && strstr(reply,"AES-128")); assert(node.handleMeshtextCommand("set mt.preset notapreset",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); for(const char* line:{"set mt.channel 0 mt-team ABCD", " SET\tMT.CHANNEL\t0\tmt-team\tABCD"}) { const auto* key=mesh::cli::terminalPasswordInput(line); assert(key && !strcmp(key,"ABCD")); assert(mesh::cli::shouldMaskTerminalInput(line)); } for(const char* line:{"set mt.channel 0 mt-team", "set mt.channel 0", "get mt.channel 0"}) { assert(!mesh::cli::shouldMaskTerminalInput(line)); } assertUntouched(node); puts("PASS cli"); } static void hops() { Preferences::reset(); MyMesh node; active(node); char reply[256]; assert(node._mt_hop_limit==7 && Preferences::writes==0); assert(node.handleMeshtextCommand("get mt.hops",reply,sizeof(reply)) && !strcmp(reply,"> 7")); for(uint8_t limit:{0,3,7}) { char command[32]; snprintf(command,sizeof(command),"set mt.hops %u",limit); assert(node.handleMeshtextCommand(command,reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_hop_limit==limit && node._mt_region==mesh::mt::Region::Unset && node._mt_slot==0); assert(Preferences::blob.size()==40 && !memcmp(Preferences::blob.data(),"MT04",4)); assert(Preferences::blob[5]==limit && Preferences::blob[6]==0 && Preferences::blob[7]==0); MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext(); assert(reboot._mt_hop_limit==limit && reboot._mt_active && reboot._mt_region==node._mt_region && reboot._mt_slot==node._mt_slot); assert(node.sendMeshtextText(node.stored[0],"hop",3)); const auto& frame=node.foreign.back(); mesh::mt::DecodedText decoded; assert(mesh::mt::decodeText(frame.data(),frame.size(),"LongFast",mesh::mt::DefaultPsk,16,1,decoded)); assert(decoded.hop_limit==limit && decoded.hop_start==limit && decoded.hops==0); assert(frame[12]==uint8_t(limit|(limit<<5))); node.foreign_pending=false; assertUntouched(reboot); } const auto writes=Preferences::writes; const auto committed=Preferences::blob; for(const char* invalid:{"set mt.hops 8","set mt.hops -1","set mt.hops 3junk", "set mt.hops 4294967296","set mt.hops 18446744073709551615","set mt.hops 3 extra"}) { assert(node.handleMeshtextCommand(invalid,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node._mt_hop_limit==7 && Preferences::writes==writes && Preferences::blob==committed); } assert(!node.saveMeshtextSettings(mesh::mt::Region::US,1,8)); assert(node.handleMeshtextCommand("set mt.hops 3",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.region us",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.slot 2",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_hop_limit==3 && node._mt_region==mesh::mt::Region::US && node._mt_slot==2); assert(node.saveMeshtextSettings(mesh::mt::Region::ANZ,4)); // Two arguments preserve the hop setting. assert(node._mt_hop_limit==3 && node._mt_slot==4 && node._mt_region==mesh::mt::Region::ANZ); assert(node.handleMeshtextCommand("set mt.hops 0",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_slot==4 && node._mt_region==mesh::mt::Region::ANZ && node._mt_hop_limit==0); const auto before_pending=Preferences::blob; const auto pending_writes=Preferences::writes; node.foreign_pending=true; assert(node.handleMeshtextCommand("set mt.hops 7",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node._mt_hop_limit==0 && node._mt_slot==4 && node._mt_region==mesh::mt::Region::ANZ); assert(Preferences::blob==before_pending && Preferences::writes==pending_writes); node.foreign_pending=false; Preferences::fail_write=true; assert(!node.saveMeshtextSettings(mesh::mt::Region::US,1,7)); assert(node._mt_hop_limit==0 && node._mt_slot==4 && node._mt_region==mesh::mt::Region::ANZ); assert(Preferences::blob==before_pending && !node._mt_active && !node._mt_settings_valid); Preferences::fail_write=false; assert(node.saveMeshtextSettings(mesh::mt::Region::ANZ,4) && node._mt_hop_limit==0); assert(node.eraseMeshtextSettings() && node._mt_hop_limit==7); // Python hashlib supplies this legacy MT01 image independently. Loading it // defaults hops to seven without silently rewriting persistent settings. const uint8_t legacy[]=@LEGACY_SETTINGS@; Preferences::reset(); Preferences::blob.assign(legacy,legacy+sizeof(legacy)); const auto legacy_blob=Preferences::blob; MyMesh migrated; migrated.stored[0]=channel("#mt-longfast"); migrated.beginMeshtext(); assert(migrated._mt_settings_valid && migrated._mt_active && migrated._mt_hop_limit==7); assert(migrated._mt_region==mesh::mt::Region::US && migrated._mt_slot==2); assert(Preferences::writes==0 && Preferences::blob==legacy_blob); assert(migrated.handleMeshtextCommand("set mt.hops 3",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(Preferences::writes==1 && !memcmp(Preferences::blob.data(),"MT04",4) && Preferences::blob[5]==3); assert(migrated._mt_region==mesh::mt::Region::US && migrated._mt_slot==2); MyMesh migrated_reboot; migrated_reboot.stored[0]=migrated.stored[0]; migrated_reboot.beginMeshtext(); assert(migrated_reboot._mt_active && migrated_reboot._mt_hop_limit==3); assert(migrated_reboot._mt_region==mesh::mt::Region::US && migrated_reboot._mt_slot==2); // Even a checksummed legacy reserved-byte change or MT02 out-of-range hop // value remains malformed, rather than becoming an undocumented option. Preferences::blob=legacy_blob; Preferences::blob[5]=1; mesh::Utils::sha256(Preferences::blob.data()+12,4,Preferences::blob.data(),12); MyMesh bad_legacy; bad_legacy.stored[0]=migrated.stored[0]; bad_legacy.beginMeshtext(); assert(!bad_legacy._mt_settings_valid && !bad_legacy._mt_active); Preferences::blob=legacy_blob; memcpy(Preferences::blob.data(),"MT02",4); Preferences::blob[5]=8; mesh::Utils::sha256(Preferences::blob.data()+12,4,Preferences::blob.data(),12); MyMesh bad_hops; bad_hops.stored[0]=migrated.stored[0]; bad_hops.beginMeshtext(); assert(!bad_hops._mt_settings_valid && !bad_hops._mt_active); assertUntouched(migrated); assertUntouched(migrated_reboot); puts("PASS hops"); } static mesh::mt::DecodedText decodeOwnText(MyMesh& node,const char* text,size_t length) { node.releaseForeign(); const uint32_t expected_packet=node._mt_packet_id; assert(node.sendMeshtextText(node.stored[0],text,length)); const auto& raw=node.foreign.back(); mesh::mt::DecodedText decoded; assert(mesh::mt::decodeText(raw.data(),raw.size(),"LongFast",mesh::mt::DefaultPsk,16,1,decoded)); assert(decoded.from==node._mt_node && decoded.id==expected_packet && node._mt_packet_id==expected_packet+1); node.releaseForeign(); return decoded; } static void shortname() { Preferences::reset(); MyMesh node; active(node); char reply[256]; uint8_t meshcore_identity[32]; memcpy(meshcore_identity,node.self_id.pub_key,32); const uint32_t default_node=@LOCAL_NODE@; assert(node._mt_node==default_node && node._mt_default_node==default_node && !node._mt_node_override); const uint32_t original_packet=node._mt_packet_id; assert(node.handleMeshtextCommand("set mt.shortname MIKE",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(!strcmp(node._mt_short_name,"MIKE") && node._mt_node==default_node && node._mt_packet_id==original_packet); assert(!memcmp(meshcore_identity,node.self_id.pub_key,32)); assert(Preferences::blob.size()==40 && !memcmp(Preferences::blob.data(),"MT04",4)); assert(!memcmp(Preferences::blob.data()+12,"MIKE\0",5)); for(unsigned i=17;i<36;++i) assert(Preferences::blob[i]==0); assert(node.handleMeshtextCommand("get mt.shortname",reply,sizeof(reply)) && strstr(reply,"MIKE")); char node_id[12]; snprintf(node_id,sizeof(node_id),"!%08lx",(unsigned long)default_node); assert(node.handleMeshtextCommand("get mt.node",reply,sizeof(reply)) && strstr(reply,node_id)); assert(node.handleMeshtextCommand("get mt.status",reply,sizeof(reply)) && strstr(reply,node_id)); auto decoded=decodeOwnText(node,"identity",8); assert(!strcmp(decoded.text,"identity") && !strstr(decoded.text,"MIKE") && !strstr(decoded.text,node_id)); strcpy(node._prefs.node_name,"Different MeshCore name"); decoded=decodeOwnText(node,"unchanged",9); assert(decoded.from==default_node && !strcmp(decoded.text,"unchanged")); assert(node.handleMeshtextCommand("set mt.hops 3",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.region us",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.slot 2",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(!strcmp(node._mt_short_name,"MIKE") && node._mt_node==default_node && !node._mt_node_override); assert(node._mt_region==mesh::mt::Region::US && node._mt_slot==2 && node._mt_hop_limit==3); const char* four_emoji="\xf0\x9f\x98\x80\xf0\x9f\x98\x80\xf0\x9f\x98\x80\xf0\x9f\x98\x80"; assert(node.saveMeshtextSettings(node._mt_region,node._mt_slot,UINT8_MAX,four_emoji)); assert(strlen(node._mt_short_name)==16 && !strcmp(node._mt_short_name,four_emoji)); assert(node._mt_hop_limit==3 && node._mt_node==default_node && !memcmp(meshcore_identity,node.self_id.pub_key,32)); std::string body(229,'x'); decoded=decodeOwnText(node,body.data(),228); assert(decoded.text_len==228); const auto admissions=node.foreign_admissions; assert(!node.sendMeshtextText(node.stored[0],body.data(),229) && node.foreign_admissions==admissions); auto persisted=Preferences::blob; const auto packet=node._mt_packet_id; for(const char* invalid:{"set mt.shortname ABCDE","set mt.shortname ", "set mt.shortname A:B","set mt.shortname A\nB","set mt.shortname \xc0\xaf", "set mt.shortname \xed\xa0\x80","set mt.shortname \xf4\x90\x80\x80", "set mt.shortname \xf0\x9f\x98", "set mt.shortname \xe2\x80\xae"}) { assert(node.handleMeshtextCommand(invalid,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(Preferences::blob==persisted && !strcmp(node._mt_short_name,four_emoji)); assert(node._mt_node==default_node && node._mt_packet_id==packet); } const auto writes=Preferences::writes; node.foreign_pending=true; assert(node.handleMeshtextCommand("set mt.shortname BOB",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(Preferences::writes==writes && Preferences::blob==persisted && !strcmp(node._mt_short_name,four_emoji)); node.foreign_pending=false; Preferences::fail_write=true; assert(!node.saveMeshtextSettings(mesh::mt::Region::ANZ,4,7,"BOB")); assert(!strcmp(node._mt_short_name,four_emoji) && Preferences::blob==persisted); assert(node._mt_region==mesh::mt::Region::US && node._mt_slot==2 && node._mt_hop_limit==3); assert(!node._mt_active && !node._mt_settings_valid && node._mt_node==default_node && node._mt_packet_id==packet); Preferences::fail_write=false; assert(node.saveMeshtextSettings(node._mt_region,node._mt_slot)); assert(!strcmp(node._mt_short_name,four_emoji)); test_random_seed=0x87654321; MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext(); assert(reboot._mt_node==default_node && reboot._mt_default_node==default_node && !reboot._mt_node_override); assert(reboot._mt_packet_id==test_random_seed && !strcmp(reboot._mt_short_name,four_emoji)); assert(reboot._mt_region==mesh::mt::Region::US && reboot._mt_slot==2 && reboot._mt_hop_limit==3); assertUntouched(reboot); assert(node.handleMeshtextCommand("set mt.shortname default",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(!node._mt_short_name[0] && node._mt_node==default_node && node._mt_packet_id==packet); memset(node._prefs.node_name,'A',31); node._prefs.node_name[31]=0; decoded=decodeOwnText(node,body.data(),228); assert(decoded.text_len==228 && !node.sendMeshtextText(node.stored[0],body.data(),229)); node._prefs.node_name[0]=0; decoded=decodeOwnText(node,body.data(),228); assert(decoded.text_len==228 && !strstr(decoded.text,node_id)); assert(!node.sendMeshtextText(node.stored[0],body.data(),229)); // Validate both independently supplied 16-byte predecessors without writes. const uint8_t mt01[]=@LEGACY_SETTINGS@,mt02[]=@LEGACY_HOPS_SETTINGS@; for(unsigned version=0;version<2;++version) { Preferences::reset(); const auto* old=version?mt02:mt01; Preferences::blob.assign(old,old+16); const auto before=Preferences::blob; MyMesh legacy; legacy.stored[0]=channel("#mt-longfast"); legacy.beginMeshtext(); assert(legacy._mt_active && legacy._mt_settings_valid && !legacy._mt_short_name[0] && !legacy._mt_node_override); assert(legacy._mt_node==default_node && legacy._mt_hop_limit==(version?3:7)); assert(Preferences::writes==0 && Preferences::blob==before); assert(legacy.handleMeshtextCommand("set mt.shortname TEST",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(Preferences::writes==1 && Preferences::blob.size()==40 && !memcmp(Preferences::blob.data(),"MT04",4)); assert(legacy._mt_region==mesh::mt::Region::US && legacy._mt_slot==2 && legacy._mt_hop_limit==(version?3:7)); assertUntouched(legacy); } const auto valid_image=Preferences::blob; for(unsigned fault=0;fault<7;++fault) { Preferences::blob=valid_image; switch(fault) { case 0: memset(Preferences::blob.data()+12,'X',17); break; // no NUL case 1: Preferences::blob[14]=0; Preferences::blob[15]='X'; break; // non-canonical tail case 2: Preferences::blob[29]=1; break; // reserved padding case 3: memset(Preferences::blob.data()+12,0,17); Preferences::blob[12]=0xc0; Preferences::blob[13]=0xaf; break; case 4: memset(Preferences::blob.data()+12,0,17); memcpy(Preferences::blob.data()+12,"ABCDE",5); break; case 5: Preferences::blob[32]=3; break; // reserved override case 6: memset(Preferences::blob.data()+32,0xff,4); break; } mesh::Utils::sha256(Preferences::blob.data()+36,4,Preferences::blob.data(),36); MyMesh invalid; invalid.stored[0]=channel("#mt-longfast"); invalid.beginMeshtext(); assert(!invalid._mt_settings_valid && !invalid._mt_active && !invalid.radio.p.enabled()); assertUntouched(invalid); } assert(!memcmp(meshcore_identity,node.self_id.pub_key,32)); puts("PASS shortname"); } static void nodeOverride() { Preferences::reset(); MyMesh node; active(node); char reply[256]; uint8_t identity[32]; memcpy(identity,node.self_id.pub_key,32); const uint32_t derived=@LOCAL_NODE@,packet=node._mt_packet_id; assert(node._mt_node==derived && node._mt_default_node==derived && !node._mt_node_override); for(const char* value:{"!12345678","ABCDEF01","00000004","fffffffe"}) { char command[64]; snprintf(command,sizeof(command),"set mt.node %s",value); const char* digits=value[0]=='!'?value+1:value; const uint32_t chosen=std::strtoul(digits,nullptr,16); const uint32_t next_packet=node._mt_packet_id; assert(node.handleMeshtextCommand(command,reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_node==chosen && node._mt_node_override==chosen && node._mt_default_node==derived); assert(node._mt_packet_id==next_packet && !memcmp(identity,node.self_id.pub_key,32)); const uint32_t image_id=uint32_t(Preferences::blob[32])|(uint32_t(Preferences::blob[33])<<8) |(uint32_t(Preferences::blob[34])<<16)|(uint32_t(Preferences::blob[35])<<24); assert(image_id==chosen); char label[12]; snprintf(label,sizeof(label),"!%08lx",(unsigned long)chosen); assert(node.handleMeshtextCommand("get mt.node",reply,sizeof(reply)) && strstr(reply,label)); auto decoded=decodeOwnText(node,"chosen",6); assert(decoded.from==chosen && !strcmp(decoded.text,"chosen")); MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext(); assert(reboot._mt_node==chosen && reboot._mt_node_override==chosen && reboot._mt_default_node==derived); assertUntouched(reboot); } assert(node._mt_packet_id==packet+4); assert(node.handleMeshtextCommand("set mt.shortname ID",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.region anz",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.slot 2",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.hops 3",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_node==0xfffffffe && node._mt_node_override==0xfffffffe && !strcmp(node._mt_short_name,"ID")); const auto blob=Preferences::blob; const auto next_packet=node._mt_packet_id,writes=Preferences::writes; for(const char* invalid:{"set mt.node 00000000","set mt.node !00000001","set mt.node 00000002", "set mt.node 00000003","set mt.node FFFFFFFF","set mt.node !ffffffff", "set mt.node 1234567","set mt.node 123456789","set mt.node 0x12345678", "set mt.node !1234567g","set mt.node !!12345678","set mt.node 12345678 extra"}) { assert(node.handleMeshtextCommand(invalid,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node._mt_node==0xfffffffe && node._mt_node_override==0xfffffffe && Preferences::blob==blob); assert(node._mt_packet_id==next_packet && Preferences::writes==writes); } node.foreign_pending=true; assert(node.handleMeshtextCommand("set mt.node !12345678",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node._mt_node==0xfffffffe && Preferences::blob==blob && Preferences::writes==writes); node.foreign_pending=false; Preferences::fail_write=true; assert(!node.saveMeshtextSettings(mesh::mt::Region::US,1,7,"BOB",0x12345678)); assert(node._mt_node==0xfffffffe && node._mt_node_override==0xfffffffe && !strcmp(node._mt_short_name,"ID")); assert(node._mt_region==mesh::mt::Region::ANZ && node._mt_slot==2 && node._mt_hop_limit==3); assert(node._mt_packet_id==next_packet && Preferences::blob==blob && !node._mt_active && !node._mt_settings_valid); Preferences::fail_write=false; assert(node.saveMeshtextSettings(node._mt_region,node._mt_slot)); assert(node._mt_node==0xfffffffe && node._mt_node_override==0xfffffffe && node._mt_packet_id==next_packet); assert(node.handleMeshtextCommand("set mt.node default",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_node==derived && !node._mt_node_override && node._mt_packet_id==next_packet); assert(!strcmp(node._mt_short_name,"ID") && node._mt_region==mesh::mt::Region::ANZ && node._mt_slot==2 && node._mt_hop_limit==3); auto decoded=decodeOwnText(node,"default",7); assert(decoded.from==derived); // If NVS commits but readback fails, RAM keeps its prior identity while the // backend suspends. A fresh boot may then adopt the committed override. Preferences::fail_read=true; const uint32_t before_uncertain=node._mt_packet_id; assert(!node.saveMeshtextSettings(node._mt_region,node._mt_slot,UINT8_MAX,nullptr,0x12345678)); assert(node._mt_node==derived && !node._mt_node_override && node._mt_packet_id==before_uncertain); assert(!node._mt_active && !node._mt_settings_valid); Preferences::fail_read=false; MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext(); assert(reboot._mt_node==0x12345678 && reboot._mt_node_override==0x12345678 && reboot._mt_default_node==derived); assert(!strcmp(reboot._mt_short_name,"ID") && reboot._mt_hop_limit==3); assert(!memcmp(identity,node.self_id.pub_key,32) && !memcmp(identity,reboot.self_id.pub_key,32)); assertUntouched(reboot); // Exercise the actual identity-refresh lifecycle after the durable-import // boundary, independently hashing the imported public keys in Python. Preferences::reset(); MyMesh imported; active(imported); const auto original_sequence=imported._mt_packet_id; assert(imported._mt_seen.remember(0x11223344,101)); const auto no_settings=Preferences::blob; const auto original_writes=Preferences::writes; imported.self_id.pub_key[0]=43; imported.refreshMeshtextIdentity(); assert(imported._mt_default_node==@IMPORTED_NODE_43@ && imported._mt_node==@IMPORTED_NODE_43@); assert(!imported._mt_node_override && imported._mt_packet_id==original_sequence); assert(!imported._mt_seen.seen(0x11223344,101)); assert(Preferences::blob==no_settings && Preferences::writes==original_writes); decoded=decodeOwnText(imported,"imported",8); assert(decoded.from==@IMPORTED_NODE_43@); assert(imported.handleMeshtextCommand("set mt.node !12345678",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); const auto overridden_sequence=imported._mt_packet_id; const auto overridden_settings=Preferences::blob; const auto overridden_writes=Preferences::writes; assert(imported._mt_seen.remember(0x11223344,102)); imported.self_id.pub_key[0]=44; imported.refreshMeshtextIdentity(); assert(imported._mt_default_node==@IMPORTED_NODE_44@ && imported._mt_node==0x12345678); assert(imported._mt_node_override==0x12345678 && imported._mt_packet_id==overridden_sequence); assert(!imported._mt_seen.seen(0x11223344,102)); assert(Preferences::blob==overridden_settings && Preferences::writes==overridden_writes); decoded=decodeOwnText(imported,"override",8); assert(decoded.from==0x12345678); const auto reset_sequence=imported._mt_packet_id; assert(imported.handleMeshtextCommand("set mt.node default",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(imported._mt_node==@IMPORTED_NODE_44@ && imported._mt_default_node==@IMPORTED_NODE_44@); assert(!imported._mt_node_override && imported._mt_packet_id==reset_sequence); decoded=decodeOwnText(imported,"reset",5); assert(decoded.from==@IMPORTED_NODE_44@); assert(imported._mt_packet_id==original_sequence+3 && imported.self_id.pub_key[0]==44); puts("PASS node"); } static void nodeinfo() { test_random_seed=0x12345678; Preferences::reset(); MyMesh node; active(node); char reply[256]; assertUntouched(node); assert(node.handleMeshtextCommand("get mt.shortname",reply,sizeof(reply))); char fallback[5]; snprintf(fallback,sizeof(fallback),"%04lx",(unsigned long)(node._mt_node&0xffff)); assert(strstr(reply,fallback) && strstr(reply,"first-chat/manual NodeInfo")); const uint8_t expected_default[]=@NODEINFO_DEFAULT@,expected_named[]=@NODEINFO_NAMED@, expected_utf8[]=@NODEINFO_UTF8@,expected_private[]=@NODEINFO_PRIVATE@; const auto original_settings=Preferences::blob; const auto original_writes=Preferences::writes; assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued")); assert(node.foreign.back()==std::vector(expected_default,expected_default+sizeof(expected_default))); assert(node.foreign_profile==1 && node.foreign_generation==node.radio.p.generation[1]); assert(Preferences::blob==original_settings && Preferences::writes==original_writes); const auto pending_id=node._mt_packet_id; const auto pending_count=node.foreign.size(); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node._mt_packet_id==pending_id && node.foreign.size()==pending_count); assert(!node.sendMeshtextText(node.stored[0],"busy",4)); // A fully encoded local attempt can lose TX admission, but its CTR nonce // must remain consumed rather than being reused by the following metadata. assert(node._mt_packet_id==pending_id+1 && node.foreign.size()==pending_count); assert(!node.saveMeshtextSettings(mesh::mt::Region::US,0)); node.releaseForeign(); assert(node.handleMeshtextCommand("set mt.node !12345678",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node.handleMeshtextCommand("set mt.shortname MIKE",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); strcpy(node._prefs.node_name,"Mike Carper"); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued")); assert(node.foreign.back()==std::vector(expected_named,expected_named+sizeof(expected_named))); node.releaseForeign(); auto text=decodeOwnText(node,"hello",5); assert(text.id==0x1234567b && !strcmp(text.text,"hello")); const char* emojis="\xf0\x9f\x98\x80\xf0\x9f\x98\x80\xf0\x9f\x98\x80\xf0\x9f\x98\x80"; assert(node.saveMeshtextSettings(node._mt_region,node._mt_slot,UINT8_MAX,emojis)); memset(node._prefs.node_name,'A',22); strcpy(node._prefs.node_name+22,"\xe2\x82\xac" "rest"); assert(node.handleMeshtextCommand("get mt.shortname",reply,sizeof(reply)) && strstr(reply,"effective=\xf0\x9f\x98\x80 (")); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued")); assert(node.foreign.back()==std::vector(expected_utf8,expected_utf8+sizeof(expected_utf8))); assert(strlen(node._prefs.node_name)==29 && strlen(node._mt_short_name)==16); // no mutation node.releaseForeign(); node.commit(1,channel("mt-Team",32)); assert(node.handleMeshtextCommand("send mt.nodeinfo 1",reply,sizeof(reply)) && strstr(reply,"queued")); assert(node.foreign.back()==std::vector(expected_private,expected_private+sizeof(expected_private))); node.releaseForeign(); const auto count=node.foreign.size(); const uint32_t packet=node._mt_packet_id; node.commit(2,channel("ordinary")); for(const char* bad:{"send mt.nodeinfo","send mt.nodeinfo ","send mt.nodeinfo -1", "send mt.nodeinfo 8","send mt.nodeinfo 4294967296","send mt.nodeinfo 999999999999999", "send mt.nodeinfo 0 extra","send mt.nodeinfo 2","send mt.nodeinfo 3"}) { assert(node.handleMeshtextCommand(bad,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node.foreign.size()==count && node._mt_packet_id==packet); } node.advance(3600000); assert(node.foreign.size()==count && node._mt_packet_id==packet); // no hourly/periodic advert node._radio_available=false; node.refreshMeshtextProfile(); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); node._radio_available=true; node.refreshMeshtextProfile(); strcpy(node._prefs.node_name,"\xc0\xaf"); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node.foreign.size()==count && node._mt_packet_id==packet); strcpy(node._prefs.node_name,"Test"); node.admit_foreign=false; assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node.foreign.size()==count && node._mt_packet_id==packet+1); // failed nonce never reused node.admit_foreign=true; node.cli("set radio2 906.875,250,11,5,rx,16"); node.advance(2000); assert(node._mt_active && !node.radio.p.canTransmit(1)); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node.foreign.size()==count && node._mt_packet_id==packet+1); node.cli("set radio2 off"); node.advance(2000); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); node.cli("set radio2 auto"); node.advance(2000); node._mt_packet_id=UINT32_MAX; assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued")); node.releaseForeign(); assert(node._mt_packet_id==0 && !node.sendMeshtextText(node.stored[0],"done",4)); assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext(); assertUntouched(reboot); assert(!node.meshcore_sends); puts("PASS nodeinfo"); } static mesh::mt::PendingFrame* pendingChat(MyMesh& node) { for(unsigned i=0;i(due-millis()); node.advance(delta>0?static_cast(delta):0); } static std::vector repeated(const std::vector& original) { auto echo=original; assert((echo[12]&7)>0); echo[12]=static_cast((echo[12]&0xf8)|((echo[12]&7)-1)); echo[14]=0x42; echo[15]=0x43; // Forwarder-controlled header fields. return echo; } static void announce(MyMesh& node) { node._prefs.flood_retry_attempts=0; assert(node.sendMeshtextNodeInfo(node.stored[0])); node.completeForeign(); node.advance(0); assert(!node.foreign_pending); node._prefs.flood_retry_attempts=15; } static void automaticNodeinfo() { const uint8_t expected[]=@NODEINFO_AFTER_CHAT@; Preferences::reset(); MyMesh node; active(node); node._prefs.flood_retry_attempts=0; node.advance(3600000); assertUntouched(node); assert(node.sendMeshtextText(node.stored[0],"hello",5)); const auto text=node.foreign.back(); node.advance(60000); assert(node.foreign.size()==1); // Admission is not RF success. node.completeForeign(); assert(node.foreign.size()==1); node.advance(250); assert(node.foreign.size()==2 && node.foreign.back()==std::vector(expected,expected+sizeof(expected))); // Exact independent protobuf/AES bytes prove only User/NodeInfo was sent: // no position packet, coordinates, MAC, public key or custom name envelope. node.completeForeign(); node.advance(3600000); assert(node.foreign.size()==2); assert(node.sendMeshtextText(node.stored[0],"again",5)); node.completeForeign(); node.advance(100000); assert(node.foreign.size()==3); node.commit(1,channel("mt-Team",32)); assert(node.sendMeshtextText(node.stored[1],"private",7)); node.completeForeign(); node.advance(250); assert(node.foreign.size()==5); auto private_info=node.foreign.back(); mesh::mt::DecodedText private_decoded; assert(private_info!=text && !mesh::mt::decodeText(private_info.data(),private_info.size(), "Team",node.stored[1].channel.secret,32,1,private_decoded)); node.completeForeign(); node.advance(3600000); assert(node.foreign.size()==5); assert(node.offline.empty() && !node.meshcore_sends && !node._notifications.calls); Preferences::reset(); MyMesh failure; active(failure); failure._prefs.flood_retry_attempts=0; failure.admit_foreign=false; assert(!failure.sendMeshtextText(failure.stored[0],"rejected",8)); failure.admit_foreign=true; failure.advance(1); assertUntouched(failure); assert(failure.sendMeshtextText(failure.stored[0],"failed",6)); failure.completeForeign(false); failure.advance(100000); assert(failure.foreign.size()==1); assert(failure.sendMeshtextText(failure.stored[0],"success",7)); failure.completeForeign(); failure.advance(250); assert(failure.foreign.size()==3); failure.completeForeign(); Preferences::reset(); MyMesh manual; active(manual); announce(manual); manual._prefs.flood_retry_attempts=0; assert(manual.sendMeshtextText(manual.stored[0],"already named",13)); manual.completeForeign(); manual.advance(100000); assert(manual.foreign.size()==2); puts("PASS auto-nodeinfo"); } static void automaticNodeinfoQuiet() { Preferences::reset(); MyMesh node; active(node); node._prefs.flood_retry_attempts=0; assert(node.sendMeshtextText(node.stored[0],"first",5)); node.completeForeign(); assert(node._mt_nodeinfo_pending[0] && !node._mt_last_tx_completed_at); node.advance(0); assert(node.foreign.size()==1 && !node.foreign_pending); node.advance(249); assert(node.foreign.size()==1 && node._mt_nodeinfo_pending[0]); node.advance(1); assert(node.foreign.size()==2 && node.foreign_pending); node.completeForeign(); // Retrying chats are not blocked by the metadata quiet gap. Every actual // completion restarts that gap, so the announcement follows the burst. Preferences::reset(); MyMesh retries; active(retries); retries._prefs.flood_retry_attempts=2; assert(retries.sendMeshtextText(retries.stored[0],"retry",5)); const auto original=retries.foreign.back(); retries.completeForeign(); auto* pending=pendingChat(retries); assert(pending && pending->due==210); for(unsigned attempt=0;attempt<2;++attempt) { advanceTo(retries,pending->due); assert(retries.foreign_pending && retries.foreign.back()==original && retries._mt_nodeinfo_pending[0]); const auto prior=retries._mt_last_tx_completed_at; retries.completeForeign(); assert(retries._mt_last_tx_completed_at==millis() && retries._mt_last_tx_completed_at!=prior); pending=pendingChat(retries); assert(pending); } const auto count=retries.foreign.size(); retries.advance(249); assert(retries.foreign.size()==count && !retries.foreign_pending); retries.advance(1); assert(retries.foreign.size()==count+1 && retries.foreign.back()!=original && retries.foreign_pending); retries.completeForeign(); Preferences::reset(); MyMesh wrap; active(wrap); wrap._prefs.flood_retry_attempts=0; g_mock_millis=UINT32_MAX-100; assert(wrap.sendMeshtextText(wrap.stored[0],"wrap",4)); wrap.completeForeign(); wrap.advance(249); assert(wrap.foreign.size()==1 && !wrap.foreign_pending); wrap.advance(1); assert(wrap.foreign.size()==2 && wrap.foreign_pending); wrap.completeForeign(); // A failed TX does not arm metadata or move the successful-completion // timestamp. Explicit manual NodeInfo still admits immediately, by choice. Preferences::reset(); MyMesh failure; active(failure); failure._prefs.flood_retry_attempts=0; failure.advance(1000); assert(failure.sendMeshtextText(failure.stored[0],"failed",6)); failure.completeForeign(false); assert(!failure._mt_last_tx_completed_at && !failure._mt_nodeinfo_pending[0]); failure.advance(1000); assert(failure.foreign.size()==1); assert(failure.sendMeshtextText(failure.stored[0],"success",7)); failure.completeForeign(); assert(failure._mt_last_tx_completed_at==millis()); assert(failure.sendMeshtextNodeInfo(failure.stored[0]) && failure.foreign.size()==3); failure.completeForeign(); failure.advance(250); assert(failure.foreign.size()==3); puts("PASS auto-nodeinfo-quiet"); } static void heardRetries() { Preferences::reset(); MyMesh node; active(node); announce(node); node._prefs.flood_retry_attempts=2; assert(node.sendMeshtextText(node.stored[0],"retry this",10)); const auto original=node.foreign.back(); const auto packet=node._mt_packet_id; const auto start=node.foreign.size(); node.completeForeign(); auto* pending=pendingChat(node); assert(pending); uint32_t due=pending->due; node.advance(static_cast(due-millis()-1)); assert(node.foreign.size()==start); node.advance(1); assert(node.foreign.size()==start+1 && node.foreign.back()==original); assert(node._mt_packet_id==packet && node._mt_retried==0); node.completeForeign(); assert(node._mt_retried==1); pending=pendingChat(node); assert(pending); advanceTo(node,pending->due); assert(node.foreign.size()==start+2 && node.foreign.back()==original && node._mt_retried==1); node.completeForeign(); assert(node._mt_retried==2); pending=pendingChat(node); assert(pending); // Final echo window remains. auto echo=repeated(original); assert(receiveFrame(node,echo.data(),echo.size())); assert(node._mt_heard==1 && !node._mt_exhausted && node.offline.empty()); node.advance(3600000); assert(node.foreign.size()==start+2 && !pendingChat(node)); Preferences::reset(); MyMesh capped; active(capped); announce(capped); capped._prefs.flood_retry_attempts=15; assert(capped.sendMeshtextText(capped.stored[0],"bounded",7)); const auto cap_original=capped.foreign.back(); capped.completeForeign(); for(unsigned i=0;i<15;++i) { auto* current=pendingChat(capped); assert(current); advanceTo(capped,current->due); assert(capped.foreign_pending && capped.foreign.back()==cap_original); capped.completeForeign(); } assert(capped._mt_retried==15 && capped.foreign.size()==17); auto* current=pendingChat(capped); assert(current); advanceTo(capped,current->due); assert(capped._mt_exhausted==1 && !pendingChat(capped)); capped.advance(3600000); assert(capped.foreign.size()==17); Preferences::reset(); MyMesh disabled; active(disabled); announce(disabled); disabled._prefs.flood_retry_attempts=0; assert(disabled.sendMeshtextText(disabled.stored[0],"once",4)); disabled.completeForeign(); disabled.advance(3600000); assert(disabled.foreign.size()==2 && !disabled._mt_retried && !pendingChat(disabled)); disabled._prefs.flood_retry_attempts=15; disabled._mt_hop_limit=0; assert(disabled.sendMeshtextText(disabled.stored[0],"no hops",7)); disabled.completeForeign(); disabled.advance(3600000); assert(disabled.foreign.size()==3 && !disabled._mt_retried && !pendingChat(disabled)); puts("PASS heard-retries"); } static void strictEcho() { Preferences::reset(); MyMesh node; active(node); announce(node); assert(node.sendMeshtextText(node.stored[0],"strict echo",11)); const auto original=node.foreign.back(); auto echo=repeated(original); node.completeForeign(); const auto count=node.foreign.size(); receiveFrame(node,original.data(),original.size()); assert(!node._mt_heard); for(unsigned position:{0U,4U,8U,12U,13U,16U,unsigned(echo.size()-1)}) { auto wrong=echo; wrong[position]^=position==12?0x20:1; receiveFrame(node,wrong.data(),wrong.size()); assert(!node._mt_heard); } receiveFrame(node,echo.data(),echo.size()-1); assert(!node._mt_heard); node.onForeignRadioFrame(echo.data(),echo.size(),0,node.radio.p.generation[1],1,-80); node.onForeignRadioFrame(echo.data(),echo.size(),1,node.radio.p.generation[1]-1,1,-80); assert(!node._mt_heard && pendingChat(node)); assert(receiveFrame(node,echo.data(),echo.size()) && node._mt_heard==1); receiveFrame(node,echo.data(),echo.size()); assert(node._mt_heard==1); node.advance(100000); assert(node.foreign.size()==count && node.offline.empty()); puts("PASS strict-echo"); } static void deliveryLifecycle() { for(unsigned change=0;change<7;++change) { Preferences::reset(); MyMesh node; active(node); announce(node); assert(node.sendMeshtextText(node.stored[0],"stale",5)); node.completeForeign(); assert(pendingChat(node)); const auto count=node.foreign.size(); char reply[256]; switch(change) { case 0: ++node.radio.p.generation[1]; break; case 1: node.cli("set radio2 off"); break; case 2: node.cli("set radio2 906.875,250,11,5,rx,16"); break; case 3: node.commit(0,channel("#mt-longfast",32)); break; case 4: assert(node.handleMeshtextCommand("set mt.node !12345678",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); break; case 5: node.commit(0,{}); break; case 6: node._radio_available=false; break; } node.advance(2000); node.advance(100000); assert(!pendingChat(node) && !node.foreign_pending && node.foreign.size()==count); assert(!node._mt_retried && !node._mt_exhausted); } // Configuration changes before RF completion cannot arm metadata intent. Preferences::reset(); MyMesh inflight; active(inflight); assert(inflight.sendMeshtextText(inflight.stored[0],"not sent",8)); inflight.commit(0,channel("#mt-longfast",32)); inflight.advance(1); assert(!inflight.foreign_pending && inflight.foreign.size()==1); inflight.advance(100000); assert(inflight.foreign.size()==1); // Config edits and RX by themselves remain completely silent. inflight.self_id.pub_key[0]=43; inflight.refreshMeshtextIdentity(); inflight.advance(1); assert(inflight.foreign.size()==1); puts("PASS delivery-lifecycle"); } static void deliveryContention() { Preferences::reset(); MyMesh node; active(node); announce(node); assert(node.sendMeshtextText(node.stored[0],"older",5)); const auto original=node.foreign.back(); node.completeForeign(); auto* pending=pendingChat(node); assert(pending); const auto due=pending->due; const auto count=node.foreign.size(); node.admit_foreign=false; advanceTo(node,due); assert(node.foreign.size()==count && !node._mt_retried && pending->retries==0); node.admit_foreign=true; assert(node.sendMeshtextText(node.stored[0],"newer",5)); // Don't block users for an echo window. node.completeForeign(); node.advance(0); assert(node.foreign_pending && node.foreign.back()==original && !node._mt_retried); const auto failed=node._mt_failed; auto echo=repeated(original); assert(receiveFrame(node,echo.data(),echo.size())); // Arrives while a retry is queued. assert(!node.foreign_pending && node._mt_heard==1 && node._mt_failed==failed); pending=pendingChat(node); assert(pending); advanceTo(node,pending->due); assert(node.foreign_pending); // Newer chat's own timer still works. const auto newer=node.foreign.back(); node.foreign_sending=true; auto newer_echo=repeated(newer); assert(receiveFrame(node,newer_echo.data(),newer_echo.size())); assert(node.foreign_pending && node._mt_heard==2); // In-flight RF cannot be discarded. node.completeForeign(); node.advance(100000); assert(!node.foreign_pending && !pendingChat(node) && node._mt_failed==failed && node._mt_retried==1); char reply[256]; assert(node.handleMeshtextCommand("get mt.retry",reply,sizeof(reply))); assert(strstr(reply,"heard=2") && strstr(reply,"pending=0")); Preferences::reset(); MyMesh full; active(full); announce(full); for(unsigned i=0;idue==109); const auto count=node.foreign.size(); node.advance(209); assert(node.foreign.size()==count); node.advance(1); assert(node.foreign.size()==count+1 && node.foreign.back()==original); node.completeForeign(); auto echo=repeated(original); assert(receiveFrame(node,echo.data(),echo.size()) && node._mt_heard==1); node.advance(100000); assert(!pendingChat(node)); Preferences::reset(); MyMesh metadata; active(metadata); metadata.random.jitter=200; assert(metadata.sendMeshtextNodeInfo(metadata.stored[0])); const auto info=metadata.foreign.back(); const auto packet=metadata._mt_packet_id; metadata.completeForeign(); auto& advert=metadata._mt_delivery[mesh::mt::ChatDeliverySlots]; assert(advert.length && advert.due-millis()==60230); metadata.advance(60229); assert(metadata.foreign.size()==1); metadata.advance(1); assert(metadata.foreign.size()==2 && metadata.foreign.back()==info); assert(metadata._mt_packet_id==packet && metadata._mt_retried==0); metadata.completeForeign(); assert(metadata._mt_retried==1); advanceTo(metadata,advert.due); assert(!advert.length && metadata._mt_exhausted==1); metadata.advance(3600000); assert(metadata.foreign.size()==2); // Metadata cap is one, not 15. Preferences::reset(); MyMesh no_advert; active(no_advert); no_advert._prefs.flood_retry_advert_enabled=0; assert(no_advert.sendMeshtextNodeInfo(no_advert.stored[0])); no_advert.completeForeign(); no_advert.advance(3600000); assert(no_advert.foreign.size()==1 && !no_advert._mt_retried); puts("PASS delivery-timing"); } static void announcementLifecycle() { Preferences::reset(); MyMesh node; active(node); announce(node); node._prefs.flood_retry_attempts=0; char reply[256]; for(const char* command:{"set mt.hops 7","set mt.hops 3"}) { assert(node.handleMeshtextCommand(command,reply,sizeof(reply)) && !strncmp(reply,"OK",2)); node.advance(1); const auto count=node.foreign.size(); assert(node.sendMeshtextText(node.stored[0],"no reannounce",13)); node.completeForeign(); node.advance(1); assert(node.foreign.size()==count+1); } node.commit(1,channel("#mt-LoNgFaSt")); const auto before_alias=node.foreign.size(); assert(node.sendMeshtextText(node.stored[1],"same wire",9)); node.completeForeign(); node.advance(1); assert(node.foreign.size()==before_alias+1); assert(node.handleMeshtextCommand("set mt.shortname NEW",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); node.advance(100000); assert(node.foreign.size()==before_alias+1); // Name changes do not send alone. assert(node.sendMeshtextText(node.stored[0],"new name",8)); node.completeForeign(); node.advance(250); assert(node.foreign.size()==before_alias+3); node.completeForeign(); auto changed=channel("#mt-longfast",32); changed.channel.secret[0]^=0x40; node.commit(0,changed); node.advance(1); const auto before_key=node.foreign.size(); assert(node.sendMeshtextText(node.stored[0],"new key",7)); node.completeForeign(); node.advance(250); assert(node.foreign.size()==before_key+2); node.completeForeign(); for(bool fail_admission:{false,true}) { Preferences::reset(); MyMesh failure; active(failure); failure._prefs.flood_retry_attempts=0; assert(failure.sendMeshtextText(failure.stored[0],"first",5)); failure.completeForeign(); const auto first_count=failure.foreign.size(); failure.admit_foreign=!fail_admission; failure.advance(250); if(!fail_admission) { assert(failure.foreign_pending); failure.completeForeign(false); } const auto attempts=failure.foreign_admissions,packet=failure._mt_packet_id; for(unsigned i=0;i<100;++i) failure.advance(1); assert(!failure.foreign_pending && failure.foreign_admissions==attempts && failure._mt_packet_id==packet); assert(failure.foreign.size()==first_count+(fail_admission?0:1)); failure.admit_foreign=true; assert(failure.sendMeshtextText(failure.stored[0],"another",7)); failure.completeForeign(); failure.advance(250); assert(failure.foreign_pending); failure.completeForeign(); } puts("PASS announcement-lifecycle"); } static void advertNameDelivery() { // A failed save or unchanged effective name must preserve the announced // state and the exact retained packet awaiting its one metadata retry. Preferences::reset(); MyMesh node; active(node); assert(node.sendMeshtextNodeInfo(node.stored[0])); const auto original=node.foreign.back(); node.completeForeign(); auto& metadata=node._mt_delivery[mesh::mt::ChatDeliverySlots]; assert(metadata.length && node._mt_nodeinfo_announced[0]); const auto old_due=metadata.due; node.save_result=false; assert(!node.saveAdvertName("Rejected") && !strcmp(node._prefs.node_name,"Test")); assert(metadata.length && !metadata.cancelled && node._mt_nodeinfo_announced[0]); node.save_result=true; assert(node.saveAdvertName("Test")); assert(metadata.length && !metadata.cancelled && metadata.due==old_due && node._mt_nodeinfo_announced[0]); assert(node.saveAdvertName("Renamed") && !strcmp(node._prefs.node_name,"Renamed")); assert(metadata.cancelled && !node._mt_nodeinfo_announced[0] && !node._mt_nodeinfo_pending[0]); advanceTo(node,old_due); node.advance(100000); assert(!metadata.length && node.foreign.size()==1 && !node.foreign_pending && !node._mt_retried && !node._mt_failed); // No transmission just from renaming. The next new chat advertises the // updated name with independently generated standard User/AES bytes. node._prefs.flood_retry_attempts=0; assert(node.sendMeshtextText(node.stored[0],"new name",8)); node.completeForeign(); node.advance(250); const uint8_t expected[]=@NODEINFO_RENAMED@; assert(node.foreign.size()==3 && node.foreign.back()!=original && node.foreign.back()==std::vector(expected,expected+sizeof(expected))); node.completeForeign(); node.advance(100000); assert(node.foreign.size()==3); // A queued old-name frame is cancelled. One which is already physically // transmitting may finish, but cannot re-arm metadata or its old retry. for(bool sending:{false,true}) { Preferences::reset(); MyMesh busy; active(busy); assert(busy.sendMeshtextNodeInfo(busy.stored[0])); busy.foreign_sending=sending; assert(busy.saveAdvertName("Renamed")); busy.advance(1); assert(busy.foreign_pending==sending); if(sending) busy.completeForeign(); busy.advance(100000); assert(busy.foreign.size()==1 && !busy._mt_nodeinfo_announced[0] && !busy._mt_nodeinfo_pending[0] && !busy._mt_failed && !busy._mt_retried); } // Clearing a deferred first-use intent does not turn the edit into TX, and // old text ciphertext remains independently eligible for its own retry. Preferences::reset(); MyMesh chat; active(chat); assert(chat.sendMeshtextText(chat.stored[0],"text",4)); const auto text=chat.foreign.back(); chat.completeForeign(); auto* pending=pendingChat(chat); assert(pending && chat._mt_nodeinfo_pending[0]); assert(chat.saveAdvertName("Renamed") && !pending->cancelled && !chat._mt_nodeinfo_pending[0]); chat.advance(1); assert(chat.foreign.size()==1); advanceTo(chat,pending->due); assert(chat.foreign.size()==2 && chat.foreign.back()==text); chat.completeForeign(); chat.advance(1); assert(!chat._mt_nodeinfo_pending[0] && chat.foreign.size()==2); auto echo=repeated(text); assert(receiveFrame(chat,echo.data(),echo.size())); chat.advance(100000); assert(chat.foreign.size()==2); puts("PASS advert-name-delivery"); } static void readinessCache() { // A delayed same-tuple publication releases its busy state without changing // either RF generation. An MT channel installed during that delay must not // remain permanently stuck behind a cached busy result. for(const char* command:{"set radio2 909.5,500,5,5,rxtx,32","set tempradio2 off"}) { Preferences::reset(); MyMesh node; node.beginMeshtext(); node.cli("set radio2 909.5,500,5,5,rxtx,32"); node.advance(2001); const auto generation=node.radio.p.generation[1]; const auto initial=node.radio.p.secondary; assert(initial.params.protocol==mesh::RadioProfileProtocol::MeshCore); node.cli(command); node.commit(0,channel("mt-labtest",16)); assert(!node._mt_active && !node._radio_profiles.secondaryRuntimeAvailable()); node.advance(1999); assert(!node._mt_active && node.radio.p.generation[1]==generation); node.advance(1); assert(node._radio_profiles.secondaryRuntimeAvailable() && node._mt_active && node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::Meshtext && node.radio.p.secondary.params.preamble==16); assertUntouched(node); // Readiness/configuration alone still never transmits. const auto active_generation=node.radio.p.generation[1]; node.advance(1000); assert(node._mt_active && node.radio.p.generation[1]==active_generation); // Readiness caching must remain read-only and preserve external ownership: // do not force the overlay back when another runtime owner changed radio2. auto external=initial; external.params.freq=910.5f; node.radio.p.setSecondary(external,false); const auto external_generation=node.radio.p.generation[1]; node.advance(1); assert(!node._mt_active && !node._radio_profiles.secondaryRuntimeAvailable()); node.commit(0,channel("mt-other",16)); node.advance(100000); assert(!node._mt_active && node.radio.p.generation[1]==external_generation && node.radio.p.secondary.params==external.params && node.radio.p.secondary.mode==external.mode); assertUntouched(node); } puts("PASS readiness-cache"); } static const uint8_t relay_short[]=@RELAY_SHORT@,relay_long[]=@RELAY_LONG@, relay_nodeinfo[]=@RELAY_NODEINFO@,relay_private128[]=@RELAY_PRIVATE128@, relay_private256[]=@RELAY_PRIVATE256@; static mesh::mt::PendingFrame* pendingRelay(MyMesh& node) { mesh::mt::PendingFrame* found=nullptr; for(auto& frame:node._mt_delivery) if(frame.length && frame.relayed) { assert(!found);found=&frame; // Retained forwarding is bounded to ONE copy. } return found; } static void enableRelay(MyMesh& node) { char reply[256];assert(node.handleMeshtextCommand("set mt.rebroadcast on",reply,sizeof(reply))); assert(!strncmp(reply,"OK",2) && node._mt_rebroadcast);node.advance(0); } static std::vector plannedRelay(MyMesh& node,const uint8_t* raw,size_t length) { std::vector expected(raw,raw+length); expected[12]=(raw[12]&0xf8)|((raw[12]&7)-1);expected[15]=uint8_t(node._mt_node); return expected; } static void assertRelayBytes(MyMesh& node,const uint8_t* raw,size_t length) { const auto* frame=pendingRelay(node);assert(frame && frame->length==length); const auto expected=plannedRelay(node,raw,length); assert(!memcmp(frame->bytes,expected.data(),length)); assert(!frame->nodeinfo && !frame->sent && !frame->retries && !frame->cancelled); assert(frame->generation==node.radio.p.generation[1]); } static void rebroadcastSettings() { Preferences::reset();MyMesh node;active(node);char reply[256]; assert(!node._mt_rebroadcast); for(const char* get:{"get mt.rebroadcast","get mt.maybe_rebroadcast"}) { assert(node.handleMeshtextCommand(get,reply,sizeof(reply)) && strstr(reply,"> off;")); } assert(node.handleMeshtextCommand("get mt.status",reply,sizeof(reply)) && strstr(reply,"rebroadcast=off")); const auto original=Preferences::blob;const auto packet=node._mt_packet_id; for(const char* invalid:{"set mt.rebroadcast ","set mt.rebroadcast yes","set mt.rebroadcast true", "set mt.rebroadcast 1","set mt.rebroadcast ON","set mt.rebroadcast on junk", "set mt.maybe_rebroadcast -1"}) { assert(node.handleMeshtextCommand(invalid,reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(!node._mt_rebroadcast && Preferences::blob==original); } assert(!node.saveMeshtextSettings(node._mt_region,node._mt_slot,UINT8_MAX,nullptr,UINT32_MAX,2)); assert(node.handleMeshtextCommand("set mt.maybe_rebroadcast on",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_rebroadcast && Preferences::blob.size()==40 && !memcmp(Preferences::blob.data(),"MT04",4)); assert(Preferences::blob[6]==1 && Preferences::blob[7]==0 && node._mt_packet_id==packet); uint8_t digest[4];mesh::Utils::sha256(digest,4,Preferences::blob.data(),36); assert(!memcmp(digest,Preferences::blob.data()+36,4)); assert(node.handleMeshtextCommand("set mt.hops 3",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(node._mt_rebroadcast && node._mt_hop_limit==3 && Preferences::blob[6]==1); MyMesh reboot;reboot.stored[0]=node.stored[0];reboot.beginMeshtext(); assert(reboot._mt_active && reboot._mt_rebroadcast && reboot._mt_hop_limit==3);assertUntouched(reboot); const auto valid=Preferences::blob; for(unsigned fault=0;fault<3;++fault) { Preferences::blob=valid; if(fault==0) Preferences::blob[6]=2; if(fault==1) Preferences::blob[7]=1; if(fault==2) {memcpy(Preferences::blob.data(),"MT03",4);Preferences::blob[6]=1;} mesh::Utils::sha256(Preferences::blob.data()+36,4,Preferences::blob.data(),36); MyMesh invalid;invalid.stored[0]=node.stored[0];invalid.beginMeshtext(); assert(!invalid._mt_settings_valid && !invalid._mt_active && !invalid._mt_rebroadcast);assertUntouched(invalid); } Preferences::blob=valid;Preferences::fail_write=true; assert(node.handleMeshtextCommand("set mt.rebroadcast off",reply,sizeof(reply)) && !strncmp(reply,"Error",5)); assert(node._mt_rebroadcast && Preferences::blob==valid && !node._mt_active && !node._mt_settings_valid); Preferences::fail_write=false;Preferences::fail_read=true; assert(!node.saveMeshtextSettings(node._mt_region,node._mt_slot,UINT8_MAX,nullptr,UINT32_MAX,0)); assert(node._mt_rebroadcast && !node._mt_active && !node._mt_settings_valid && Preferences::blob[6]==0); Preferences::fail_read=false;MyMesh recovered;recovered.stored[0]=node.stored[0];recovered.beginMeshtext(); assert(recovered._mt_active && recovered._mt_settings_valid && !recovered._mt_rebroadcast); assert(recovered.handleMeshtextCommand("set mt.rebroadcast on",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(recovered.eraseMeshtextSettings() && !recovered._mt_rebroadcast && Preferences::blob.empty()); const uint8_t mt01[]=@LEGACY_SETTINGS@,mt02[]=@LEGACY_HOPS_SETTINGS@,mt03[]=@LEGACY_MT03_SETTINGS@, mt03_named[]=@LEGACY_MT03_NAMED@; const std::vector> legacy={ {mt01,mt01+sizeof(mt01)},{mt02,mt02+sizeof(mt02)},{mt03,mt03+sizeof(mt03)}, {mt03_named,mt03_named+sizeof(mt03_named)}}; for(const auto& image:legacy) { Preferences::reset();Preferences::blob=image;MyMesh old;old.stored[0]=channel("#mt-longfast");old.beginMeshtext(); assert(old._mt_active && old._mt_settings_valid && !old._mt_rebroadcast && Preferences::writes==0); assert(Preferences::blob==image && receiveFrame(old,relay_short,sizeof(relay_short))); old.advance(100000);assertUntouched(old);assert(!pendingRelay(old)); if(image.size()==40 && image[12]) { assert(!strcmp(old._mt_short_name,"OLD") && old._mt_node_override==0x12345678 && old._mt_node==0x12345678); } const uint32_t old_node=old._mt_node,old_override=old._mt_node_override,old_slot=old._mt_slot; const uint8_t old_hops=old._mt_hop_limit;const std::string old_name=old._mt_short_name; assert(old.handleMeshtextCommand("set mt.rebroadcast on",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); assert(old._mt_rebroadcast && !memcmp(Preferences::blob.data(),"MT04",4) && Preferences::writes==1); assert(old._mt_node==old_node && old._mt_node_override==old_override && old._mt_slot==old_slot && old._mt_hop_limit==old_hops && old_name==old._mt_short_name);assertUntouched(old); } puts("PASS rebroadcast-settings"); } static void rebroadcastTiming() { Preferences::reset();MyMesh off;active(off); assert(receiveFrame(off,relay_short,sizeof(relay_short)));off.advance(100000); assert(!pendingRelay(off) && off.offline.size()==1);assertUntouched(off); for(uint32_t jitter:{0U,200U}) { Preferences::reset();MyMesh node;active(node);enableRelay(node);node.random.jitter=jitter; const auto packet=node._mt_packet_id; assert(receiveFrame(node,relay_short,sizeof(relay_short)));assertRelayBytes(node,relay_short,sizeof(relay_short)); const auto due=pendingRelay(node)->due;assert(uint32_t(due-millis())==50+jitter/10); assert(node.radio.airtime_calls.back()==std::make_pair(uint8_t(1),int(sizeof(relay_short)))); node.advance(uint32_t(due-millis())-1);assertUntouched(node); node.advance(1);assert(node.foreign_pending && node.foreign.size()==1); assert(node.foreign.back()==plannedRelay(node,relay_short,sizeof(relay_short))); assert(node.foreign_profile==1 && node.foreign_generation==node.radio.p.generation[1]); assert(node._mt_packet_id==packet && node._mt_received==1); node.completeForeign();assert(!pendingRelay(node) && node._mt_sent==1 && !node._mt_retried && !node._mt_exhausted); for(bool pending:node._mt_nodeinfo_pending) assert(!pending); for(bool announced:node._mt_nodeinfo_announced) assert(!announced); node.advance(3600000);assert(node.foreign.size()==1 && node._mt_packet_id==packet && !node.meshcore_sends); } Preferences::reset();MyMesh wrap;active(wrap);enableRelay(wrap);g_mock_millis=UINT32_MAX-20; assert(receiveFrame(wrap,relay_short,sizeof(relay_short)));assert(uint32_t(pendingRelay(wrap)->due-millis())==50); wrap.advance(49);assertUntouched(wrap);wrap.advance(1);assert(wrap.foreign_pending);wrap.completeForeign(); Preferences::reset();MyMesh zero;active(zero);enableRelay(zero);zero.radio.fixed_airtime=0; assert(receiveFrame(zero,relay_short,sizeof(relay_short)));zero.advance(100000);assert(!pendingRelay(zero));assertUntouched(zero); // A later explicit chat, rather than relaying by itself, announces the // standard CLIENT role with no position / direct-message capability. const uint8_t expected_info[]=@NODEINFO_REBROADCAST@; Preferences::reset();MyMesh named;active(named);enableRelay(named);named._prefs.flood_retry_attempts=0; assert(receiveFrame(named,relay_short,sizeof(relay_short)));advanceTo(named,pendingRelay(named)->due);named.completeForeign(); named.advance(1000);assert(named.foreign.size()==1); assert(named.sendMeshtextText(named.stored[0],"human",5));named.completeForeign();named.advance(250); assert(named.foreign.size()==3 && named.foreign.back()==std::vector(expected_info,expected_info+sizeof(expected_info))); puts("PASS rebroadcast-timing"); } static void rebroadcastDuplicates() { for(uint8_t remaining:{uint8_t(2),uint8_t(0)}) { Preferences::reset();MyMesh node;active(node);enableRelay(node); assert(receiveFrame(node,relay_short,sizeof(relay_short)));const auto due=pendingRelay(node)->due; assert(receiveFrame(node,relay_short,sizeof(relay_short))); // Original sender's retry is not a relay. assert(pendingRelay(node) && pendingRelay(node)->due==due && node.offline.size()==1); auto peer=plannedRelay(node,relay_short,sizeof(relay_short));peer[12]=(peer[12]&0xf8)|remaining; peer[14]=0x42;peer[15]=0x43;assert(receiveFrame(node,peer.data(),peer.size())); assert(!pendingRelay(node) && node.offline.size()==1 && !node._mt_heard); assert(receiveFrame(node,relay_short,sizeof(relay_short)));node.advance(100000); assert(!pendingRelay(node));assertUntouched(node); } Preferences::reset();MyMesh exact;active(exact);enableRelay(exact); assert(receiveFrame(exact,relay_short,sizeof(relay_short))); const auto expected=plannedRelay(exact,relay_short,sizeof(relay_short)); for(unsigned changed:{0U,4U,8U,12U,13U,16U}) { auto wrong=expected;wrong[changed]^=changed==12 ? 0x10:1; receiveFrame(exact,wrong.data(),wrong.size());assert(pendingRelay(exact)); } auto shorter=expected;shorter.pop_back();receiveFrame(exact,shorter.data(),shorter.size());assert(pendingRelay(exact)); assert(!exact.onForeignRadioFrame(expected.data(),expected.size(),0,exact.radio.p.generation[1],3,-90)); assert(!exact.onForeignRadioFrame(expected.data(),expected.size(),1,exact.radio.p.generation[1]-1,3,-90)); assert(pendingRelay(exact)); for(bool inflight:{false,true}) { Preferences::reset();MyMesh node;active(node);enableRelay(node); assert(receiveFrame(node,relay_short,sizeof(relay_short)));advanceTo(node,pendingRelay(node)->due); assert(node.foreign_pending);node.foreign_sending=inflight; auto peer=plannedRelay(node,relay_short,sizeof(relay_short));const auto failed=node._mt_failed; assert(receiveFrame(node,peer.data(),peer.size())); assert(node.foreign_pending==inflight); if(inflight) {assert(pendingRelay(node)->cancelled);node.completeForeign();} assert(!pendingRelay(node) && node._mt_failed==failed && !node._mt_retried); node.advance(100000);assert(node.foreign.size()==1); for(bool pending:node._mt_nodeinfo_pending) assert(!pending); } puts("PASS rebroadcast-duplicates"); } static void rebroadcastLifecycle() { for(unsigned change=0;change<7;++change) { Preferences::reset();MyMesh node;active(node);enableRelay(node); assert(receiveFrame(node,relay_short,sizeof(relay_short)));assert(pendingRelay(node)); char reply[256]; switch(change) { case 0:assert(node.handleMeshtextCommand("set mt.rebroadcast off",reply,sizeof(reply)) && !strncmp(reply,"OK",2));break; case 1:node.commit(0,{});break; case 2:node.commit(0,channel("#mt-longfast",32));break; case 3:node.self_id.pub_key[0]=43;node.refreshMeshtextIdentity();break; case 4:node._radio_available=false;break; case 5:node.cli("set radio2 off");break; case 6:node.cli("set tempradio2 909.5,500,5,5,rx,1,32");break; } node.advance(2000);node.advance(100000); assert(!pendingRelay(node) && !node.foreign_pending && !node._mt_retried && !node._mt_exhausted);assertUntouched(node); } Preferences::reset();MyMesh expired;active(expired);enableRelay(expired); assert(receiveFrame(expired,relay_short,sizeof(relay_short)));expired.admit_foreign=false; advanceTo(expired,pendingRelay(expired)->due);assert(pendingRelay(expired)); expired.advance(59999);assert(pendingRelay(expired));expired.advance(2);assert(!pendingRelay(expired)); expired.admit_foreign=true;expired.advance(100000);assertUntouched(expired); Preferences::reset();MyMesh priority;active(priority);enableRelay(priority); assert(receiveFrame(priority,relay_short,sizeof(relay_short)));const auto packet=priority._mt_packet_id; assert(priority.sendMeshtextText(priority.stored[0],"human",5)); assert(!pendingRelay(priority) && priority.foreign_pending && priority.foreign.size()==1 && priority._mt_packet_id==packet+1); mesh::mt::DecodedText decoded;const auto& raw=priority.foreign.back(); assert(mesh::mt::decodeText(raw.data(),raw.size(),"LongFast",mesh::mt::DefaultPsk,16,1,decoded)); assert(!strcmp(decoded.text,"human") && decoded.from==priority._mt_node); for(unsigned invalid=0;invalid<3;++invalid) { Preferences::reset();MyMesh node;active(node);enableRelay(node); assert(receiveFrame(node,relay_short,sizeof(relay_short))); const auto retained=*pendingRelay(node);std::string oversized(229,'x'); const char invalid_utf8[]={char(0xc0),char(0xaf)};const char embedded_nul[]={'x',0,'y'}; const char* text=invalid==0 ? oversized.data():invalid==1 ? invalid_utf8:embedded_nul; const size_t length=invalid==0 ? oversized.size():invalid==1 ? sizeof(invalid_utf8):sizeof(embedded_nul); assert(!node.sendMeshtextText(node.stored[0],text,length)); const auto* current=pendingRelay(node); assert(current && current->length==retained.length && current->due==retained.due && !current->cancelled); assert(!memcmp(current->bytes,retained.bytes,retained.length));assertUntouched(node); } for(bool inflight:{false,true}) { Preferences::reset();MyMesh node;active(node);enableRelay(node); assert(receiveFrame(node,relay_short,sizeof(relay_short)));advanceTo(node,pendingRelay(node)->due); node.foreign_sending=inflight;const auto packet=node._mt_packet_id,failed=node._mt_failed; const bool accepted=node.sendMeshtextText(node.stored[0],"priority",8); const uint32_t after_attempt=node._mt_packet_id; assert(after_attempt>=packet && after_attempt<=packet+1); assert(accepted==!inflight); if(inflight) { assert(node.foreign_pending && pendingRelay(node)->cancelled); node.completeForeign();assert(!pendingRelay(node)); assert(node.sendMeshtextText(node.stored[0],"priority",8)); } assert(node.foreign_pending && !pendingRelay(node) && node._mt_packet_id==after_attempt+unsigned(inflight) && node._mt_failed==failed); mesh::mt::DecodedText sent;const auto& bytes=node.foreign.back(); assert(mesh::mt::decodeText(bytes.data(),bytes.size(),"LongFast",mesh::mt::DefaultPsk,16,1,sent)); assert(!strcmp(sent.text,"priority") && sent.from==node._mt_node && sent.id==node._mt_packet_id-1); } Preferences::reset();MyMesh bounded;active(bounded);enableRelay(bounded); assert(receiveFrame(bounded,relay_short,sizeof(relay_short)));const auto retained=pendingRelay(bounded)->length; assert(receiveFrame(bounded,relay_long,sizeof(relay_long))); assert(pendingRelay(bounded)->length==retained && bounded.offline.size()==2); Preferences::reset();MyMesh full;active(full);enableRelay(full); for(unsigned i=0;i raw(relay_short,relay_short+sizeof(relay_short)); if(invalid==0) raw[12]&=0xf8; // No hops left. if(invalid==1) raw[12]=(1U<<5)|3; // Malformed hop_start < hop_limit. if(invalid==2) raw[14]=1; // Explicit next-hop routing is not a broadcast relay. if(invalid==3) raw[0]=1; // A DM / non-broadcast destination. if(invalid==4) raw.assign(relay_nodeinfo,relay_nodeinfo+sizeof(relay_nodeinfo)); if(invalid==5) raw.assign(rx_other,rx_other+sizeof(rx_other)); if(invalid==6) raw.resize(mesh::mt::HeaderSize); // Opaque/truncated ciphertext. receiveFrame(node,raw.data(),raw.size());node.advance(100000); assert(!pendingRelay(node));assertUntouched(node); } Preferences::reset();MyMesh capacity;active(capacity);enableRelay(capacity);capacity.offline_capacity=0; assert(receiveFrame(capacity,relay_short,sizeof(relay_short))); assert(!pendingRelay(capacity) && !capacity._mt_seen.seen(0x11223344,201)); capacity.offline_capacity=4;assert(receiveFrame(capacity,relay_short,sizeof(relay_short)));assert(pendingRelay(capacity)); for(unsigned key_bytes:{16U,32U}) { Preferences::reset();MyMesh node;node.beginMeshtext(); node.cli(key_bytes==16 ? "set radio2 909.5,500,7,5,rxtx,32":"set radio2 909.5,125,9,5,rxtx,32"); node.advance(2000);node.commit(0,channel("mt-Team",key_bytes));assert(node._mt_active);enableRelay(node); node.radio.physical_airtime=true;node.radio.airtime_calls.clear();node.random.jitter=key_bytes==16 ? 0:200; const auto* raw=key_bytes==16 ? relay_private128:relay_private256; const size_t length=key_bytes==16 ? sizeof(relay_private128):sizeof(relay_private256); const uint32_t airtime=node.radio.actualProfileAirtime(1,length); const auto packet=node._mt_packet_id; assert(receiveFrame(node,raw,length));assertRelayBytes(node,raw,length); assert((node.radio.airtime_calls==std::vector>({{1,int(length)}}))); const uint32_t delay=airtime*(key_bytes==16 ? 5:7); assert(uint32_t(pendingRelay(node)->due-millis())==delay && delay>0); node.advance(delay-1);assertUntouched(node);node.advance(1); assert(node.foreign_pending && node.foreign.back()==plannedRelay(node,raw,length)); node.completeForeign();node.advance(100000); assert(node.foreign.size()==1 && node._mt_packet_id==packet && !node._mt_retried && !node._mt_exhausted); } Preferences::reset();MyMesh wrong_key;active(wrong_key);enableRelay(wrong_key); assert(!receiveFrame(wrong_key,relay_private256,sizeof(relay_private256))); wrong_key.advance(100000);assert(!pendingRelay(wrong_key));assertUntouched(wrong_key); puts("PASS rebroadcast-guards-phy"); } int main(int argc,char** argv) { assert(argc==2); if(!strcmp(argv[1],"keys"))keys(); else if(!strcmp(argv[1],"auto"))automatic(); else if(!strcmp(argv[1],"conflicts"))conflicts(); else if(!strcmp(argv[1],"manual"))manual(); else if(!strcmp(argv[1],"settings"))settings(); else if(!strcmp(argv[1],"receive"))receive(); else if(!strcmp(argv[1],"transmit"))transmit(); else if(!strcmp(argv[1],"commands"))commands(); else if(!strcmp(argv[1],"cli"))privateCLI(); else if(!strcmp(argv[1],"hops"))hops(); else if(!strcmp(argv[1],"shortname"))shortname(); else if(!strcmp(argv[1],"node"))nodeOverride(); else if(!strcmp(argv[1],"nodeinfo"))nodeinfo(); else if(!strcmp(argv[1],"auto-nodeinfo"))automaticNodeinfo(); else if(!strcmp(argv[1],"auto-nodeinfo-quiet"))automaticNodeinfoQuiet(); else if(!strcmp(argv[1],"heard-retries"))heardRetries(); else if(!strcmp(argv[1],"strict-echo"))strictEcho(); else if(!strcmp(argv[1],"delivery-lifecycle"))deliveryLifecycle(); else if(!strcmp(argv[1],"delivery-contention"))deliveryContention(); else if(!strcmp(argv[1],"delivery-timing"))deliveryTiming(); else if(!strcmp(argv[1],"announcement-lifecycle"))announcementLifecycle(); else if(!strcmp(argv[1],"advert-name-delivery"))advertNameDelivery(); else if(!strcmp(argv[1],"readiness-cache"))readinessCache(); else if(!strcmp(argv[1],"rebroadcast-settings"))rebroadcastSettings(); else if(!strcmp(argv[1],"rebroadcast-timing"))rebroadcastTiming(); else if(!strcmp(argv[1],"rebroadcast-duplicates"))rebroadcastDuplicates(); else if(!strcmp(argv[1],"rebroadcast-lifecycle"))rebroadcastLifecycle(); else if(!strcmp(argv[1],"rebroadcast-guards-phy"))rebroadcastGuardsAndPhy(); else assert(!"unknown test case"); }