feat: Refactor roles to helper functions where possible. Fixed issues where found with reasioning.#9804
feat: Refactor roles to helper functions where possible. Fixed issues where found with reasioning.#9804h3lix1 wants to merge 34 commits intomeshtastic:developfrom
Conversation
Rebase with upstream
Documents our experimentation with ESP-IDF DFS and why it doesn't work well for Meshtastic (RTOS locks, BLE locks, USB issues). Proposes simpler alternative: manual setCpuFrequencyMhz() control with explicit triggers for when to go fast vs slow.
Add isRouterRole(), isRouterLikeRole(), and isTrackerRole() inline helpers to eliminate scattered multi-role comparisons across the codebase. Also fixes magic number 12 in CannedMessageModule and adds missing TAK_TRACKER exclusion in NodeInfoModule requestWantResponse. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents our experimentation with ESP-IDF DFS and why it doesn't work well for Meshtastic (RTOS locks, BLE locks, USB issues). Proposes simpler alternative: manual setCpuFrequencyMhz() control with explicit triggers for when to go fast vs slow.
Add isRouterRole(), isRouterLikeRole(), and isTrackerRole() inline helpers to eliminate scattered multi-role comparisons across the codebase. Also fixes magic number 12 in CannedMessageModule and adds missing TAK_TRACKER exclusion in NodeInfoModule requestWantResponse. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…re-sunl into refactor/role-helpers # Conflicts: # src/PowerFSM.cpp # src/main.cpp # src/mesh/Default.cpp # src/meshUtils.h # src/modules/CannedMessageModule.cpp # src/modules/NodeInfoModule.cpp
caveman99
left a comment
There was a problem hiding this comment.
My thoughts on this:
- i spent 20 minutes checking your code for logic errors. That's probably more than it took you to churn it out.
- We are replacing comparison operators with nested function calls that use stack space. Most of these simple things could have been macros to inline code.
Apart from the inconsistencies i found, this role management in the code is really fine grained. Replacing them with the helpers creates uncertainty what each helper includes or excludes. I would have rather replaced more of the calls with the IS_ONE_OF macro, so you still see what exactly is permitted and not permitted in place.
| nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio | ||
| bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; | ||
| bool isPreferredRebroadcaster = isRouterLikeRole(config.device.role); | ||
| if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && |
There was a problem hiding this comment.
The same macro is replaced with isRouterLikeRole() when Default.h uses isRouterRole()
There was a problem hiding this comment.
Because this code should also include ROUTER_LATE, but it doesn't. Why should ROUTER_LATE be treated any differently than ROUTER here?
| config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; | ||
| } | ||
| bool isSensorOrRouter() const { return isSensorOrRouterRole(config.device.role); } | ||
| }; |
There was a problem hiding this comment.
Wouldn't that move into the helpers as well?
There was a problem hiding this comment.
isSensorOrRouter? I should replace its instances with isSensorOrRouterRole, correct.
| isRouterRole(config.device.role)) { | ||
| config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; | ||
| const char *warning = "Rebroadcast mode can't be set to NONE for a router"; | ||
| LOG_WARN(warning); |
There was a problem hiding this comment.
Well, what is it now? just router or router_late as well?
There was a problem hiding this comment.
isRouterRole includes router_late as well, since there is only one or two lines of code that shouldn't include both, and those are very specific pieces of code.
| // If we're in event mode, nobody is a Router or Router Late | ||
| if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || | ||
| config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { | ||
| if (isRouterRole(config.device.role)) { |
There was a problem hiding this comment.
Saw above, and commented
| if (config.device.role != 12) { | ||
| // Only add as favorite if our role is not router-like. | ||
| if (!isRouterLikeRole(config.device.role)) { | ||
| LOG_INFO("Proactively adding %x as favorite node", dest); |
There was a problem hiding this comment.
This is a functional change, the original code only checks for CLIENT_BASE
There was a problem hiding this comment.
Yup, wouldn't you think this is a bug? Read #37 in the summary. Why would routers or router_late automatically like nodes messaging it when is_messagable is disabled by default for these nodes.
| (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, | ||
| meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR) && | ||
| config.power.is_power_saving == true)) { | ||
| if (msecToWake != portMAX_DELAY && (isTrackerOrSensorRole(config.device.role) && config.power.is_power_saving == true)) { |
There was a problem hiding this comment.
someticmes trackerOrSensor includes the TAK Tracker, sometimes not
There was a problem hiding this comment.
And in the instances it does not, there is a reason probably why it should. #20 above, for example. Why does TAK_TRACKER use want_response for position packets?
Same with nodeinfo, why does TAK_TRACKER use want_response here? (#13) it seems like it shouldn't, but willing to hear why it would. Seems unnecessary load.
|
@caveman99 I respectfully disagree about the time to create this diff, the time taken to review it before sublission, and what it is trying to accomplish. The use of macros might save a few bytes on the stack, but there are a few examples in the code where this is done both ways (macros and functions) and I like functions for any flexibility this may need in future. What I found is a lot of inconsistency in the code about how router, router_late, and client_base, tracker, and tak_tracker is handled. I'm trying to find a solution here, if only to shine light on the problem. Role management isn't fine grained, it's a mess. |
Summary
Centralizes scattered device role comparisons into reusable inline helpers in
meshUtils.h, replacing duplicated role-check patterns across multiple files. Fixes one magic number, multiple missing role variants, broadens several ROUTER-only checks to coverROUTER_LATE(and in some casesCLIENT_BASE) where the original restriction was an oversight, adds semantic grouping helpers for sensor/tracker/TAK roles, and adds single-value helpers for complete coverage. Also fixes a bug whereROUTER_LATEwas missing from early rebroadcast logic, and aligns Canned Message auto-favoriting with router-like role semantics.NOTE: This was assisted with the use of both Claude Opus 4.6 and GPT 5.3-Codex. The logic changes are all mine.
Helpers in
src/meshUtils.h:isRouterRole(role)ROUTER,ROUTER_LATEisRouterLikeRole(role)ROUTER,ROUTER_LATE,CLIENT_BASEisTrackerRole(role)TRACKER,TAK_TRACKERisSensorRole(role)SENSORisTrackerOrSensorRole(role)TRACKER,TAK_TRACKER,SENSORisTakLikeRole(role)TAK,TAK_TRACKERisSensorOrRouterRole(role)SENSOR,ROUTER,ROUTER_LATEisSensorOrRouter()class wrapper)isClientMuteRole(role)CLIENT_MUTEisClientHiddenRole(role)CLIENT_HIDDENisClientBaseRole(role)CLIENT_BASEisLostAndFoundRole(role)LOST_AND_FOUNDisClientRole(role)CLIENT,CLIENT_MUTE,CLIENT_HIDDEN,CLIENT_BASEChanges
src/meshUtils.hconfig.pb.hinclude and 3 inline helper functionssrc/mesh/Router.cpp:88IS_ONE_OF(role, ROUTER, ROUTER_LATE, CLIENT_BASE)→isRouterLikeRole(role)isRouterLikeRolesrc/mesh/Router.cpp:112!IS_ONE_OF(node->user.role, ROUTER, ROUTER_LATE, CLIENT_BASE)→!isRouterLikeRole(static_cast<>(node->user.role))isRouterLikeRolesrc/mesh/FloodingRouter.cpp:103→isRouterRole(role)`src/modules/AdminModule.cpp:315isOneOf(role, CLIENT_BASE, ROUTER, ROUTER_LATE)→isRouterLikeRole(role)isRouterLikeRolesrc/modules/AdminModule.cpp:680→isRouterRole(role)`src/modules/PositionModule.cpp:40role != TRACKER && role != TAK_TRACKER→!isTrackerRole(role)isTrackerRolesrc/modules/PositionModule.cpp:46→isTrackerRole(role)`src/modules/PositionModule.cpp:381→isTrackerRole(role)`src/PowerFSM.cpp:374→isTrackerOrSensorRole(role)`src/main.cpp:720→isTrackerOrSensorRole(role)`src/modules/CannedMessageModule.cpp:107612→meshtastic_Config_DeviceConfig_Role_CLIENT_BASEsrc/modules/NodeInfoModule.cpp:99role != TRACKER && role != SENSOR→!isTrackerOrSensorRole(role)isTrackerOrSensorRolesrc/PowerFSM.cpp:41,265role == ROUTER→isRouterRole(role)isRouterRolesrc/mesh/Default.h:44IF_ROUTERmacro:role == ROUTER→isRouterRole(role)isRouterRolesrc/mesh/Default.cpp:41,45role == ROUTER→isRouterRole(role);TRACKER, SENSOR→isTrackerOrSensorRole(role)isRouterRole,isTrackerOrSensorRolesrc/mesh/MeshService.cpp:299role == ROUTER→isRouterLikeRole(role)isRouterLikeRolesrc/modules/AdminModule.cpp:653role == ROUTER→isRouterRole(role)isRouterRolesrc/sleep.cpp:549role == ROUTER→isRouterRole(role)isRouterRolesrc/modules/PositionModule.cpp:380role == TRACKER→isTrackerRole(role)isTrackerRolesrc/meshUtils.hisSensorRole,isTrackerOrSensorRole,isTakLikeRole,isSensorOrRouterRolehelperssrc/modules/Telemetry/BaseTelemetryModule.hisSensorOrRouterRole()withisSensorOrRouter()delegating to free function in meshUtils.hisSensorOrRouterRoleROUTER_LATE)src/modules/Telemetry/DeviceTelemetry.cppisSensorOrRouterRole()→isSensorOrRouter()(5 telemetry files)isSensorOrRouterRoleROUTER_LATE)src/modules/Telemetry/{Health,Environment,Power,AirQuality}Telemetry.cpprole == SENSOR/role != SENSOR→isSensorRole(role)/!isSensorRole(role)isSensorRolesrc/modules/SerialModule.cpp:317role == SENSOR→isSensorRole(role)isSensorRolesrc/modules/Modules.cpp:152→isTakLikeRole(role)`src/modules/PositionModule.cpp:429role != TRACKER && role != TAK_TRACKER→!isTrackerRole(role)isTrackerRolesrc/platform/nrf52/main-nrf52.cpp:450IS_ONE_OF(TRACKER, TAK_TRACKER, SENSOR)→isTrackerOrSensorRole(role)isTrackerOrSensorRolesrc/mesh/NodeDB.cpp:670role != ROUTER→!isRouterRole(role)isRouterRolesrc/modules/StoreForwardModule.cpp:591role == ROUTER→isRouterRole(role)isRouterRolesrc/mesh/RadioInterface.cpp:612role == ROUTER(ROUTER_LATE intentionally excluded)src/meshUtils.hisClientMuteRole,isClientHiddenRole,isClientBaseRole,isLostAndFoundRole,isClientRolehelperssrc/mesh/FloodingRouter.cpp:139role != CLIENT_MUTE→!isClientMuteRole(role)isClientMuteRolesrc/mesh/FloodingRouter.cpp:109role == CLIENT_BASE→isClientBaseRole(role)isClientBaseRolesrc/mesh/FloodingRouter.cpp:131role == CLIENT_BASE→isClientBaseRole(role)isClientBaseRolesrc/modules/NodeInfoModule.cpp:207role != CLIENT_HIDDEN→!isClientHiddenRole(role)isClientHiddenRolesrc/modules/CannedMessageModule.cpp:1077role != CLIENT_BASE→!isRouterLikeRole(role)isRouterLikeRolesrc/modules/AdminModule.cpp:108role == CLIENT_BASE→isClientBaseRole(role)isClientBaseRolesrc/modules/Telemetry/DeviceTelemetry.cpp:31role != CLIENT_HIDDEN→!isClientHiddenRole(role)isClientHiddenRolesrc/modules/PositionModule.cpp:291role != LOST_AND_FOUND→!isLostAndFoundRole(role)isLostAndFoundRolesrc/modules/PositionModule.cpp:449role == LOST_AND_FOUND→isLostAndFoundRole(role)isLostAndFoundRolesrc/graphics/Screen.cpp:1854IS_ONE_OF(CLIENT, CLIENT_MUTE, CLIENT_HIDDEN, CLIENT_BASE)→isClientRole(role)isClientRolesrc/modules/PositionModule.cpp:392IS_ONE_OF(TRACKER, TAK_TRACKER)→isTrackerRole(role)isTrackerRolesrc/mesh/NodeDB.cpp:582IS_ONE_OF(ROUTER, ROUTER_LATE, LOST_AND_FOUND)→isRouterRole() || isLostAndFoundRole()isRouterRole,isLostAndFoundRolesrc/mesh/NodeDB.cpp:1785role == CLIENT_BASE→isClientBaseRole(role)isClientBaseRoleFunctional change details
#13 — NodeInfoModule.cpp
requestWantResponse(line 99)role != TRACKER && role != SENSOR!isTrackerOrSensorRole(role)TRACKER,SENSORTRACKER,TAK_TRACKER,SENSORTAK_TRACKERnodes would setwant_response = true, soliciting NodeInfo repliesTAK_TRACKERnodes now skipwant_response, matchingTRACKERbehaviorRationale:
TAK_TRACKERis a tracker variant (enum value 10) and should behave likeTRACKER(enum value 5) for NodeInfo response solicitation. The original code predates theTAK_TRACKERrole and was not updated when it was added. This is a bug fix — tracker-class devices are low-power and should not solicit unnecessary responses.#14 — PowerFSM.cpp
isPowered()(line 41) andPowerFSM_setup()(line 265)role == ROUTERisRouterRole(role)ROUTERonlyROUTER,ROUTER_LATEROUTER_LATEwas not treated as power-saving and would not wake to NB (no-bluetooth) stateROUTER_LATEnow assumed power-saving and wakes to NB likeROUTERRationale:
ROUTER_LATEis an infrastructure relay role with the same deployment profile asROUTER— typically on solar or always-on power sources.installRoleDefaultsalready setsis_unmessagable = trueandtelemetry.device_update_interval = ONE_DAYforROUTER_LATE, confirming it is an infrastructure node. There is no reason for it to behave differently for power-state assumptions or bluetooth wake transitions. The only intentional difference betweenROUTERandROUTER_LATEis rebroadcast timing, not power management.#15 — Default.h
IF_ROUTERmacro (line 44)role == ROUTERisRouterRole(role)ROUTERonlyROUTER,ROUTER_LATEROUTER_LATEused CLIENT-class defaults: GPS every 2 min, screen-on 10 min, bluetooth wait 60s, etc.ROUTER_LATEnow gets infrastructure defaults: GPS once/day, screen-on 1s, bluetooth wait 1s, etc.Rationale: The
IF_ROUTERmacro controls interval defaults for GPS updates, telemetry broadcast, bluetooth timeout, sleep durations, and screen-on time.ROUTER_LATEis an infrastructure node that should not be broadcasting GPS every 2 minutes, keeping its screen on for 10 minutes, or waiting 60 seconds for bluetooth connections. ItsinstallRoleDefaultsalready setsdevice_update_interval = ONE_DAY— theIF_ROUTERdefaults are consistent with that infrastructure profile. Without this change,ROUTER_LATEdevices waste battery and airtime on intervals designed for handheld client devices.#16 — Default.cpp
getConfiguredOrDefaultMsScaled()(lines 41, 45)role == ROUTERskips congestion scalingisRouterRole(role)— bothROUTERandROUTER_LATEskipTRACKER, SENSORskip congestion scalingisTrackerOrSensorRole(role)— addsTAK_TRACKERROUTER_LATEandTAK_TRACKERhad their intervals congestion-scaled unnecessarilyRationale: Congestion scaling multiplies broadcast intervals based on online node count. Infrastructure roles (
ROUTER,ROUTER_LATE) already have significantly higher base intervals and should not be further scaled — they need to maintain predictable relay timing. Similarly,TAK_TRACKERis a tracker variant with priority position sends; its intervals should not be dilated by congestion scaling any more thanTRACKER's are.#17 — MeshService.cpp
isPreferredRebroadcaster(line 299)role == ROUTERisRouterLikeRole(role)ROUTERonlyROUTER,ROUTER_LATE,CLIENT_BASEROUTER_LATEandCLIENT_BASEwould send unsolicited NodeInfo when hearing unknown nodesRationale: When
isPreferredRebroadcasteris true, the node skips sending its own NodeInfo in response to hearing a new unknown node. The intent is that high-traffic infrastructure nodes should not add NodeInfo chatter for every new node they overhear.ROUTER_LATEis equally high-traffic infrastructure — it isis_unmessagable = trueand has no reason to solicit NodeInfo exchanges.CLIENT_BASEacts as a relay for favorited nodes and also benefits from reduced unsolicited chatter on the channel. All three roles prioritize forwarding over chatting.#18 — AdminModule.cpp rebroadcast mode restriction (line 653)
role == ROUTERisRouterRole(role)ROUTERonlyROUTER,ROUTER_LATEROUTER_LATEcould be configured withrebroadcast_mode = NONEROUTER_LATEnow blocked fromNONErebroadcast mode, matchingROUTERRationale: A router-class node with rebroadcast mode set to
NONEis a contradiction — it exists to relay packets. The validation guard preventing this misconfiguration should apply equally toROUTER_LATE. Without this check, a user could accidentally disable relaying on aROUTER_LATEnode with no warning, silently breaking mesh connectivity through that node.#19 — sleep.cpp
shouldLoraWake()(line 549)role == ROUTERisRouterRole(role)ROUTERonlyROUTER,ROUTER_LATEROUTER_LATEwould not wake on LoRa interrupt from deep sleepROUTER_LATEnow wakes on LoRa activity likeROUTERRationale:
shouldLoraWakedetermines whether LoRa radio activity should wake the device from deep sleep. AROUTER_LATEthat sleeps through incoming packets cannot fulfill its relay purpose — it would miss packets entirely and create a dead zone in the mesh. This is arguably the most impactful fix: without LoRa wake, a sleepingROUTER_LATEis a broken relay node. The only intentional difference between the two roles is rebroadcast timing (early vs late), not whether they should wake up to relay at all.#20 — PositionModule.cpp
want_responseon position packets (line 380)role == TRACKERforceswant_response = falseisTrackerRole(role)forceswant_response = falseTRACKERonlyTRACKER,TAK_TRACKERTAK_TRACKERwould setwant_response = trueon position packets, soliciting repliesTAK_TRACKERnow suppresseswant_response, matchingTRACKERbehaviorRationale:
TAK_TRACKERis a low-power tracker variant. Settingwant_response = truemeans it solicits position replies from other nodes, which costs airtime and battery — exactly what tracker-class devices should avoid. The very next line (381) already usesisTrackerRole()for setting priority toRELIABLEfor both tracker types. This inconsistency (line 380 TRACKER-only, line 381 both) indicates thewant_responseline was not updated whenTAK_TRACKERwas introduced. Same class of bug as the NodeInfoModule fix (#13).#22–23 — BaseTelemetryModule.h
isSensorOrRouterRole()→isSensorOrRouterRole()free functionSENSOR,ROUTERonlySENSOR,ROUTER,ROUTER_LATEROUTER_LATEwas not treated as an "impolite" telemetry role in 5 telemetry modulesROUTER_LATEnow skips multi-hop broadcast request filtering and uses impolite send timing, matchingROUTERRationale: The
isSensorOrRouterRole()check controls two behaviors in telemetry modules: (1) whether to ignore multi-hop broadcast telemetry requests (impolite roles respond regardless), and (2) whether to use more aggressive send timing without channel utilization checks.ROUTER_LATEis an infrastructure node with the same telemetry profile asROUTER—installRoleDefaultssetsdevice_update_interval = ONE_DAYfor both. It should behave identically for telemetry sending policy. The method was also moved from a class method to a free function inmeshUtils.hfor consistency with the other role helpers.#29 — NodeDB.cpp
node_info_broadcast_secsdefault (line 670)role != ROUTER!isRouterRole(role)ROUTERonly kept its configured valueROUTERandROUTER_LATEboth keep their configured valueROUTER_LATEhadnode_info_broadcast_secsoverwritten to default during factory resetROUTER_LATEnow preserves its configurednode_info_broadcast_secslikeROUTERRationale: During
installDefaultConfig()(factory reset),node_info_broadcast_secsis overwritten to the default value for all roles exceptROUTER.ROUTER_LATEis an infrastructure role that setsdevice_update_interval = ONE_DAYininstallRoleDefaults— it should also preserve its node info broadcast configuration. Without this, a factory reset on aROUTER_LATEdevice would silently revert to client-class NodeInfo broadcast frequency, increasing unnecessary airtime.#30 — StoreForwardModule.cpp auto-enable S&F server (line 591)
role == ROUTERisRouterRole(role)ROUTERonly auto-enables S&F serverROUTERandROUTER_LATEboth auto-enable S&F serverROUTER_LATErequired explicitis_server = trueto act as S&F serverROUTER_LATEnow auto-enables S&F server mode when S&F is enabledRationale:
ROUTER_LATEis an infrastructure relay with the same deployment profile asROUTER— always-on, with PSRAM available for S&F history storage. If Store & Forward is enabled on aROUTER_LATEnode, it should auto-enter server mode just likeROUTERdoes. The|| moduleConfig.store_forward.is_serverfallback remains, so any other role can still explicitly opt in. This removes a confusing configuration asymmetry between two roles that differ only in rebroadcast timing.#37 — CannedMessageModule.cpp DM auto-favorite gate (line 1077)
!isClientBaseRole(role)!isRouterLikeRole(role)ROUTER,ROUTER_LATE) still auto-favorited DM peersRationale: Auto-favoriting on DM send is useful for client-style roles, but for infrastructure/relay roles it can create noisy and unintended favorite state. Using
!isRouterLikeRole(role)keeps this convenience for client-style devices while avoiding automatic favorite churn onROUTER,ROUTER_LATE, andCLIENT_BASE.Intentionally unchanged
These checks have specific reasons for their current role restrictions and should not be broadened or replaced with helpers:
RadioInterface.cppROUTER-only:shouldRebroadcastEarlyLikeRouter()must excludeROUTER_LATE— the entire purpose ofROUTER_LATEis to not rebroadcast early. Late rebroadcast timing is handled separately inFloodingRouter::perhapsCancelDupe().FloodingRouter.cppROUTER_LATE-only: always clamps to late rebroadcast window. This is the defining single-role behavior forROUTER_LATE— no other role shares it. No single-value helper exists forROUTER_LATEalone.PositionModule.cppTAK_TRACKER-only: sends position as TAK packet over ATAK port. This is TAK wire-format specific — no other role sends ATAK PLI. No single-value helper exists forTAK_TRACKERalone.NodeDB.cppinstallRoleDefaults()— per-role switch statement setting unique defaults (different intervals, flags, position settings) for each role. Not groupable — each case is role-specific.DisplayFormatters.cppAdminModule.cppc.payload_variant.device.role), notconfig.device.role. Checks forROUTER_CLIENTandREPEATER, which are deprecated roles with no helpers.🤝 Attestations
Heltec V4