6464#define CMD_GET_RADIO_SETTINGS 61
6565#define CMD_SET_MAX_HOPS 62 // Adaptive forwarding control
6666
67+ // Custom TEAM extensions (kept out of stock command space)
68+ #define CMD_SET_FORWARD_LIST 74 // [count][6-byte pubkey prefix] * count
69+ #define CMD_GET_AUTONOMOUS_SETTINGS 75 // returns persisted autonomous settings
70+ #define CMD_SET_AUTONOMOUS_SETTINGS 76 // set persisted autonomous settings
71+
6772// Stats sub-types for CMD_GET_STATS
6873#define STATS_TYPE_CORE 0
6974#define STATS_TYPE_RADIO 1
96101#define RESP_CODE_STATS 24 // v8+, second byte is stats type
97102#define RESP_CODE_AUTOADD_CONFIG 25
98103#define RESP_ALLOWED_REPEAT_FREQ 26
104+ #define RESP_CODE_AUTONOMOUS_SETTINGS 27
99105
100106#define SEND_TIMEOUT_BASE_MILLIS 500
101107#define FLOOD_SEND_TIMEOUT_FACTOR 16 .0f
@@ -463,9 +469,122 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
463469 return false ;
464470}
465471
472+ bool MyMesh::extractSenderNameFromGroupPayload (const mesh::Packet* packet, char * sender_name, size_t max_len) {
473+ if (!packet || !sender_name || max_len < 2 || packet->payload_len <= 6 ) return false ;
474+
475+ // Group text payload format starts at payload[5], usually: "SenderName: message"
476+ const uint8_t * text_data = &packet->payload [5 ];
477+ size_t text_len = packet->payload_len - 5 ;
478+ size_t out_idx = 0 ;
479+
480+ for (size_t i = 0 ; i < text_len && out_idx < max_len - 1 ; i++) {
481+ char c = (char )text_data[i];
482+ if (c == ' \0 ' ) break ;
483+ if (c == ' :' ) {
484+ sender_name[out_idx] = 0 ;
485+ return out_idx > 0 ;
486+ }
487+ sender_name[out_idx++] = c;
488+ }
489+
490+ sender_name[out_idx] = 0 ;
491+ return false ;
492+ }
493+
494+ bool MyMesh::lookupContactPrefixByName (const char * sender_name, uint8_t out_pub_key_prefix[6 ]) {
495+ if (!sender_name || !sender_name[0 ] || !out_pub_key_prefix) return false ;
496+
497+ ContactInfo contact;
498+ const int total = getNumContacts ();
499+ for (int i = 0 ; i < total; i++) {
500+ if (getContactByIdx (i, contact) && strcmp (contact.name , sender_name) == 0 ) {
501+ memcpy (out_pub_key_prefix, contact.id .pub_key , 6 );
502+ return true ;
503+ }
504+ }
505+ return false ;
506+ }
507+
508+ bool MyMesh::isInForwardList (const uint8_t * pub_key_prefix) const {
509+ if (!pub_key_prefix) return false ;
510+ for (uint8_t i = 0 ; i < forward_list_count; i++) {
511+ if (memcmp (forward_list[i], pub_key_prefix, 6 ) == 0 ) {
512+ return true ;
513+ }
514+ }
515+ return false ;
516+ }
517+
518+ void MyMesh::updateForwardListPolicyState () {
519+ if (forward_list_updated_at == 0 ) return ;
520+
521+ const unsigned long now = millis ();
522+ const unsigned long age = now - forward_list_updated_at;
523+
524+ // 10 min: list expires (fallback to flood_max behavior)
525+ if (age >= 10UL * 60UL * 1000UL && forward_list_count > 0 ) {
526+ forward_list_count = 0 ;
527+ memset (forward_list, 0 , sizeof (forward_list));
528+ MESH_DEBUG_PRINTLN (" FORWARD: whitelist expired (10m) -> reverting to flood_max behavior" );
529+ }
530+
531+ // 60 min: disable forwarding entirely until app refreshes policy
532+ if (age >= 60UL * 60UL * 1000UL && !forwarding_hard_disabled) {
533+ forwarding_hard_disabled = true ;
534+ MESH_DEBUG_PRINTLN (" FORWARD: hard-disabled (60m stale policy)" );
535+ }
536+ }
537+
538+ bool MyMesh::shouldSendAutonomousUpdate () {
539+ if (_prefs.autonomous_enabled == 0 ) return false ;
540+ if (_serial && _serial->isConnected ()) return false ; // autonomous mode is for disconnected operation
541+
542+ unsigned long interval_ms = (unsigned long )_prefs.autonomous_interval_sec * 1000UL ;
543+ if (interval_ms < 10000UL ) interval_ms = 10000UL ;
544+
545+ const unsigned long now = millis ();
546+ if (autonomous_last_sent_at != 0 && (now - autonomous_last_sent_at) < interval_ms) {
547+ return false ;
548+ }
549+
550+ if (_prefs.autonomous_min_distance_m == 0 || !autonomous_has_last_fix) {
551+ return true ;
552+ }
553+
554+ const double dLat = (sensors.node_lat - autonomous_last_lat) * DEG_TO_RAD;
555+ const double dLon = (sensors.node_lon - autonomous_last_lon) * DEG_TO_RAD;
556+ const double a = sin (dLat / 2.0 ) * sin (dLat / 2.0 )
557+ + cos (autonomous_last_lat * DEG_TO_RAD) * cos (sensors.node_lat * DEG_TO_RAD)
558+ * sin (dLon / 2.0 ) * sin (dLon / 2.0 );
559+ const double c = 2.0 * atan2 (sqrt (a), sqrt (1.0 - a));
560+ const double distance_m = 6371000.0 * c;
561+
562+ return distance_m >= _prefs.autonomous_min_distance_m ;
563+ }
564+
565+ void MyMesh::runAutonomousMode () {
566+ if (!shouldSendAutonomousUpdate ()) return ;
567+
568+ if (advert ()) {
569+ autonomous_last_sent_at = millis ();
570+ autonomous_last_lat = sensors.node_lat ;
571+ autonomous_last_lon = sensors.node_lon ;
572+ autonomous_has_last_fix = true ;
573+ MESH_DEBUG_PRINTLN (" AUTO: autonomous advert sent (interval=%lus, min_dist=%um)" ,
574+ (unsigned long )_prefs.autonomous_interval_sec ,
575+ (unsigned int )_prefs.autonomous_min_distance_m );
576+ }
577+ }
578+
466579bool MyMesh::allowPacketForward (const mesh::Packet* packet) {
467580 // Adaptive forwarding control for companion radios
468581 // Respects flood_max set by app via CMD_SET_MAX_HOPS (default 0 = disabled)
582+ updateForwardListPolicyState ();
583+
584+ if (forwarding_hard_disabled) {
585+ MESH_DEBUG_PRINTLN (" FORWARD: Blocked - hard disabled by stale whitelist policy" );
586+ return false ;
587+ }
469588
470589 if (_prefs.flood_max == 0 ) {
471590 MESH_DEBUG_PRINTLN (" FORWARD: Blocked - forwarding disabled (flood_max=0)" );
@@ -493,6 +612,20 @@ bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
493612 return false ;
494613 }
495614 }
615+
616+ // If whitelist is active, only forward messages from senders in list (when resolvable).
617+ if (forward_list_count > 0 && payload_type == PAYLOAD_TYPE_GRP_TXT) {
618+ char sender_name[32 ];
619+ uint8_t sender_prefix[6 ];
620+
621+ if (extractSenderNameFromGroupPayload (packet, sender_name, sizeof (sender_name)) &&
622+ lookupContactPrefixByName (sender_name, sender_prefix)) {
623+ if (!isInForwardList (sender_prefix)) {
624+ MESH_DEBUG_PRINTLN (" FORWARD: Blocked by whitelist (sender=%s)" , sender_name);
625+ return false ;
626+ }
627+ }
628+ }
496629 }
497630
498631 // Allow forwarding for:
@@ -840,6 +973,14 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
840973 dirty_contacts_expiry = 0 ;
841974 memset (advert_paths, 0 , sizeof (advert_paths));
842975 memset (send_scope.key , 0 , sizeof (send_scope.key ));
976+ memset (forward_list, 0 , sizeof (forward_list));
977+ forward_list_count = 0 ;
978+ forward_list_updated_at = 0 ;
979+ forwarding_hard_disabled = false ;
980+ autonomous_last_sent_at = 0 ;
981+ autonomous_last_lat = 0.0 ;
982+ autonomous_last_lon = 0.0 ;
983+ autonomous_has_last_fix = false ;
843984
844985 // defaults
845986 memset (&_prefs, 0 , sizeof (_prefs));
@@ -852,6 +993,10 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
852993 _prefs.tx_power_dbm = LORA_TX_POWER;
853994 _prefs.gps_enabled = 0 ; // GPS disabled by default
854995 _prefs.gps_interval = 0 ; // No automatic GPS updates by default
996+ _prefs.autonomous_enabled = 0 ;
997+ _prefs.autonomous_channel_hash = 0 ;
998+ _prefs.autonomous_interval_sec = 30 ;
999+ _prefs.autonomous_min_distance_m = 0 ;
8551000 // _prefs.rx_delay_base = 10.0f; enable once new algo fixed
8561001}
8571002
@@ -891,6 +1036,9 @@ void MyMesh::begin(bool has_display) {
8911036 _prefs.tx_power_dbm = constrain (_prefs.tx_power_dbm , -9 , MAX_LORA_TX_POWER);
8921037 _prefs.gps_enabled = constrain (_prefs.gps_enabled , 0 , 1 ); // Ensure boolean 0 or 1
8931038 _prefs.gps_interval = constrain (_prefs.gps_interval , 0 , 86400 ); // Max 24 hours
1039+ _prefs.autonomous_enabled = constrain (_prefs.autonomous_enabled , 0 , 1 );
1040+ _prefs.autonomous_interval_sec = constrain (_prefs.autonomous_interval_sec , 10 , 3600 );
1041+ _prefs.autonomous_min_distance_m = constrain (_prefs.autonomous_min_distance_m , 0 , 5000 );
8941042
8951043#ifdef BLE_PIN_CODE // 123456 by default
8961044 if (_prefs.ble_pin == 0 ) {
@@ -1026,7 +1174,7 @@ void MyMesh::handleCmdFrame(size_t len) {
10261174 out_frame[i++] = 0 ; // Null terminator for device name
10271175
10281176 // Firmware capability flags - tells app what features this firmware supports
1029- out_frame[i++] = CAPABILITY_FORWARDING; // Currently supports adaptive forwarding
1177+ out_frame[i++] = CAPABILITY_FORWARDING | CAPABILITY_AUTONOMOUS;
10301178
10311179 _serial->writeFrame (out_frame, i);
10321180 } else if (cmd_frame[0 ] == CMD_SEND_TXT_MSG && len >= 14 ) {
@@ -1884,6 +2032,56 @@ void MyMesh::handleCmdFrame(size_t len) {
18842032 memcpy (&out_frame[i], &r->upper_freq , 4 ); i += 4 ;
18852033 }
18862034 _serial->writeFrame (out_frame, i);
2035+ } else if (cmd_frame[0 ] == CMD_SET_FORWARD_LIST && len >= 2 ) {
2036+ uint8_t count = cmd_frame[1 ];
2037+ if (count > FORWARD_LIST_MAX) count = FORWARD_LIST_MAX;
2038+
2039+ const int needed = 2 + (count * 6 );
2040+ if (len < needed) {
2041+ writeErrFrame (ERR_CODE_ILLEGAL_ARG);
2042+ } else {
2043+ for (uint8_t i = 0 ; i < count; i++) {
2044+ memcpy (forward_list[i], &cmd_frame[2 + (i * 6 )], 6 );
2045+ }
2046+ for (uint8_t i = count; i < FORWARD_LIST_MAX; i++) {
2047+ memset (forward_list[i], 0 , 6 );
2048+ }
2049+
2050+ forward_list_count = count;
2051+ forward_list_updated_at = millis ();
2052+ forwarding_hard_disabled = false ;
2053+
2054+ MESH_DEBUG_PRINTLN (" FORWARD: whitelist updated (%u entries)" , (unsigned int )forward_list_count);
2055+ writeOKFrame ();
2056+ }
2057+ } else if (cmd_frame[0 ] == CMD_GET_AUTONOMOUS_SETTINGS) {
2058+ int i = 0 ;
2059+ out_frame[i++] = RESP_CODE_AUTONOMOUS_SETTINGS;
2060+ out_frame[i++] = _prefs.autonomous_enabled ;
2061+ out_frame[i++] = _prefs.autonomous_channel_hash ;
2062+ memcpy (&out_frame[i], &_prefs.autonomous_interval_sec , sizeof (_prefs.autonomous_interval_sec ));
2063+ i += sizeof (_prefs.autonomous_interval_sec );
2064+ memcpy (&out_frame[i], &_prefs.autonomous_min_distance_m , sizeof (_prefs.autonomous_min_distance_m ));
2065+ i += sizeof (_prefs.autonomous_min_distance_m );
2066+ _serial->writeFrame (out_frame, i);
2067+ } else if (cmd_frame[0 ] == CMD_SET_AUTONOMOUS_SETTINGS && len >= 7 ) {
2068+ uint8_t enabled = constrain (cmd_frame[1 ], 0 , 1 );
2069+ uint8_t channel_hash = cmd_frame[2 ];
2070+ uint16_t interval_sec = 0 ;
2071+ uint16_t min_distance_m = 0 ;
2072+ memcpy (&interval_sec, &cmd_frame[3 ], sizeof (interval_sec));
2073+ memcpy (&min_distance_m, &cmd_frame[5 ], sizeof (min_distance_m));
2074+
2075+ interval_sec = constrain (interval_sec, 10 , 3600 );
2076+ min_distance_m = constrain (min_distance_m, 0 , 5000 );
2077+
2078+ _prefs.autonomous_enabled = enabled;
2079+ _prefs.autonomous_channel_hash = channel_hash;
2080+ _prefs.autonomous_interval_sec = interval_sec;
2081+ _prefs.autonomous_min_distance_m = min_distance_m;
2082+
2083+ savePrefs ();
2084+ writeOKFrame ();
18872085 } else {
18882086 writeErrFrame (ERR_CODE_UNSUPPORTED_CMD);
18892087 MESH_DEBUG_PRINTLN (" ERROR: unknown command: %02X" , cmd_frame[0 ]);
@@ -2098,6 +2296,9 @@ void MyMesh::checkSerialInterface() {
20982296void MyMesh::loop () {
20992297 BaseChatMesh::loop ();
21002298
2299+ updateForwardListPolicyState ();
2300+ runAutonomousMode ();
2301+
21012302 if (_cli_rescue) {
21022303 checkCLIRescueCmd ();
21032304 } else {
0 commit comments