Skip to content

Conversation

@michielappelman
Copy link

@michielappelman michielappelman commented Jan 11, 2026

Problem

Repeater flood advertisements are a significant source of airtime consumption in MeshCore networks. Each repeater periodically broadcasts its identity across the entire mesh, which:

  • Consumes valuable airtime that could be used for actual messages
  • Creates congestion in dense networks with many repeaters
  • Provides diminishing returns as network size grows

Related discussions and PRs attempting to address repeater airtime:

Solution

This PR implements a pull-based (on-demand) advertisement system that allows clients to request advertisement data from specific nodes when needed, rather than relying solely on periodic broadcasts.

Key Benefits

  • Reduced airtime: Repeaters no longer need to flood-advertise; they respond only when asked
  • Extended metadata: Pull responses can include larger fields (GPS coordinates, node description) that would be too expensive to broadcast periodically
  • Backward compatible: Works alongside existing zero-hop adverts and discovery
  • Enhanced path discovery: Instead of waiting for an advert from a particular Prefix ID you see in a message path, you can directly request it.

Behavior Changes for Repeaters

This PR makes two significant changes to repeater behavior:

1. Flood Adverts Disabled

Repeaters no longer send automatic flood advertisements. The flood_advert_interval is now disabled by default for new installs. Repeaters will:

  • Still receive and forward flood adverts from other nodes
  • Still respond to discovery requests
  • Respond to pull-advert requests with full metadata

2. Incoming Flood Adverts from Repeaters Dropped

When a repeater receives a flood advert from another repeater, it is dropped and not forwarded. This prevents the "flood advert cascade" that consumes significant airtime in larger networks.

Flood adverts from companions, room servers, and sensors are still forwarded normally.

3. GPS Location Removed from Zero-Hop Adverts

Zero-hop advertisements no longer include GPS coordinates, regardless of the advert.loc.policy setting. This provides:

  • Smaller packets: Reduced airtime for periodic adverts
  • Privacy: Location is only shared on explicit request via pull-advert

GPS coordinates are now only shared via pull-advert responses when a client explicitly requests extended metadata.

Important

Zero-Hop Adverts must stay enabled

Repeaters still need zero-hop advertisements for direct neighbors to discover them. The default advert.interval is 60 minutes for new installs.

If you previously set advert.interval to 0 to reduce airtime, you must re-enable it:

set advert.interval 60

Without zero-hop adverts, direct neighbors won't know the repeater exists and won't be able to send pull-advert requests to it.

Summary of Advert Behavior

Advert Type Old Behavior New Behavior
Zero-hop (local broadcast) Every N minutes, with GPS No GPS - still enabled, default 60 min
Flood (mesh-wide) Every N hours Disabled - no longer sent
Incoming flood from repeater Forwarded Dropped - not forwarded
Incoming flood from companion/room Forwarded Unchanged - still forwarded
Pull-advert response N/A New - responds with GPS + description

How It Works

Multi-Hop Request (via intermediate node)

sequenceDiagram
    participant App as Companion App
    participant Companion as Companion Radio
    participant Stock as Intermediate Repeater<br/>(stock firmware)
    participant Target as Target Repeater<br/>(new firmware)

    App->>Companion: CMD_REQUEST_ADVERT (0x3A)<br/>prefix=0x1F, path=[0x23, 0x1F]
    activate Companion

    Companion->>Stock: [RF] ADVERT_REQUEST (0x20)<br/>path=[0x23, 0x1F]
    Note over Stock: Forwards packet normally<br/>(no code changes needed)
    Stock->>Target: [RF] ADVERT_REQUEST (0x20)<br/>path=[0x1F]

    activate Target
    Note over Target: Builds signed response<br/>with extended metadata
    Target->>Stock: [RF] ADVERT_RESPONSE (0x30)<br/>signed payload
    deactivate Target

    Stock->>Companion: [RF] ADVERT_RESPONSE (0x30)

    Note over Companion: Verifies signature,<br/>strips it, forwards to app
    Companion->>App: PUSH_ADVERT_RESPONSE (0x8F)
    deactivate Companion
Loading

Request Flow Summary

  1. App sends CMD_REQUEST_ADVERT (0x3A) to companion with target prefix and path
  2. Companion sends CTL_TYPE_ADVERT_REQUEST (0x20) over RF using DIRECT routing
  3. Intermediate nodes (even stock firmware) forward the packet normally
  4. Target repeater receives request, extracts return path from payload, builds signed response
  5. Response travels back via reversed path (using path embedded in request payload)
  6. Companion verifies signature, strips it, forwards to app as PUSH_CODE_ADVERT_RESPONSE (0x8F)

Response Contents

Field Size Description
sub_type 1 0x30
tag 4 Matches request (for correlation)
pubkey 32 Full public key
timestamp 4 Current time
signature 64 Ed25519 signature (verified, then stripped)
adv_type 1 Node type (repeater=2, room=3, etc.)
node_name 32 Node name
flags 1 Indicates optional fields present
latitude 4 Optional (flag 0x01)
longitude 4 Optional (flag 0x02)
node_desc 32 Optional (flag 0x04) - e.g., "Rooftop 25m, solar"

Total: 139-179 bytes depending on optional fields (fits within MAX_PACKET_PAYLOAD of 184 bytes)

Backward Compatibility

This implementation is fully backward compatible:

Scenario Behavior
New companion → New repeater Full pull-advert support
New companion → Stock repeater Request times out gracefully; repeater still visible via zero-hop adverts
Stock companion → New repeater No change; repeater still sends zero-hop adverts
Multi-hop via stock nodes Works - stock firmware forwards packets normally

Why it works: The control packet sub-types use values without the high bit set (0x20/0x30 instead of 0xA0/0xB0). In Mesh.cpp, only sub-types with (payload[0] & 0x80) != 0 are restricted to zero-hop. This allows pull-advert packets to traverse multiple hops through stock firmware nodes.

Testing

Hardware Test Environment

Device Role Firmware
LilyGo T-Echo Companion Radio Modified companion_radio_usb
Heltec T114 Test Repeater Modified simple_repeater
Heltec V3 Interop Test Stock firmware

All tests performed over real RF links, not simulated.

Summary of changes

Core (src/)

  • Packet.h: Added CTL_TYPE_ADVERT_REQUEST (0x20) and CTL_TYPE_ADVERT_RESPONSE (0x30)
  • Mesh.cpp: Added PAYLOAD_TYPE_CONTROL handling for multi-hop delivery

Repeater (examples/simple_repeater/)

  • MyMesh.cpp: Added handleAdvertRequest() to process requests and build signed responses

Companion (examples/companion_radio/)

  • MyMesh.cpp: Added CMD_REQUEST_ADVERT (0x3A) command handler and handleAdvertResponse()
  • MyMesh.h: Added pending_advert_request field

CLI (src/helpers/)

  • CommonCLI.h: Added node_desc[32] field to NodePrefs
  • CommonCLI.cpp: Added set node.desc / get node.desc commands

Python Client Support

The meshcore_py Python client has been updated with full support for pull-based adverts:

  • request_advert(prefix, path) command
  • ADVERT_RESPONSE event type and parser

PR link: meshcore-dev/meshcore_py#45

Usage Example

from meshcore import MeshCore, EventType

async def get_repeater_info():
    mc = await MeshCore.create_serial("/dev/ttyACM0")

    # Request advert from repeater with prefix 0x1F
    await mc.commands.request_advert(
        prefix=bytes([0x1F]),
        path=bytes([0x1F])  # Direct neighbor
    )

    # Wait for response
    response = await mc.wait_for_event(EventType.ADVERT_RESPONSE, timeout=30)

    if response:
        print(f"Name: {response.payload['node_name']}")
        print(f"Description: {response.payload.get('node_desc', 'N/A')}")
        if response.payload['flags'] & 0x01:
            print(f"Location: {response.payload['latitude']}, {response.payload['longitude']}")

@marcelverdult
Copy link
Contributor

hundreds of users requesting node infos will flood even more

@michielappelman
Copy link
Author

hundreds of users requesting node infos will flood even more

First, there are no flood messages sent or received here (although you might have meant 'flood' differently here). Second, there is rate limiting on the repeater side (discover_limiter). Combined, this should already result in less total airtime than periodic flood adverts. But this can be extended with additional rate-limiting on forwarding repeaters as well? Curious if you have any other suggestions?

@fschrempf
Copy link
Contributor

@michielappelman First of all, thanks for your efforts! Especially the way you present and summarize your changes is simply amazing. I've rarely seen such a good description and also the design and testing looks very neat. I haven't looked at the code so far but I assume it looks just as nice. ;)

Now to the actual idea: I think it could work. When I was reading the description I had the same hesitation as @marcelverdult expressed above, but on second thought: the user needs to know the full path to the repeater they want request an advert from (usually by receiving a message through it), so they can not use flood requests for "scraping".

I think this hast the potential to limit the amount of traffic tremendously because people would either not care about repeater details or just request them once in a while. Using ratelimiting to enforce this sounds appropriate and reasonable to me.

Additionally people interested in the mesh structure and performance can use the advert request as a simple form of traceroute/ping to see if they can reach a certain repeater. They don't need to run full traceroutes to see if the path works.

@marcelverdult
Copy link
Contributor

every option you give people will ne abused. HA plugin can receive repater data? great all local repeater have free guest access so they add all of them.

i can do bots? awesome lets spam the mesh!

i‘d prefer a fixed system that makes the mesh work and ignore human „but i want to know“

@fschrempf
Copy link
Contributor

@marcelverdult I see your point and I agree. The question is: can there be a compromise that is robust enough against abuse but still lets people gather information about the mesh and I think rate-limited, non-flood, requested adverts could be such a compromise.

@michielappelman michielappelman marked this pull request as draft January 12, 2026 08:36
@michielappelman
Copy link
Author

michielappelman commented Jan 12, 2026

Marked this as draft because I missed a crucial limitation in testing: the ADVERT_RESPONSE is always zero-hop since the ADVERT_REQUEST isn't able to build up a return path using the control packet as I assumed.

Since all my radios are within range of each other all tests passed and I didn't notice this before... Need to re-think this a bit...

When we do have the path built up, we could build a per-hop rate-limiter that looks at the pair of source node prefix + first repeater prefix and limits the amount of requests to address the concern @marcelverdult raised.

To be clear: we could fix this in a new firmware, but I want to make this compatible with intermediate repeaters that do not run this code (yet).

@michielappelman
Copy link
Author

michielappelman commented Jan 12, 2026

Fixed the multi-hop response routing issue. The path is now embedded in the ADVERT_REQUEST payload so the target can reverse it for the response (excluding itself as the last element). This works with stock firmware intermediate nodes since they only look at the header path for forwarding. Tested successfully over RF.

michielappelman and others added 13 commits January 12, 2026 13:55
- Change GPS coordinates from double (8 bytes) to int32 (4 bytes)
- Fix signature to sign pubkey+timestamp only (matches regular adverts)
- Fix signature verification to verify pubkey+timestamp
- Add tag to push notification for app matching
- Update timeout notification to include tag

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Move CTL_TYPE_NODE_DISCOVER_REQ/RESP defines to Packet.h
- Change CTL_TYPE_ADVERT_REQUEST/RESPONSE to 0xA0/0xA1 to avoid
  conflict with node discovery response (0x90-0x9F range)
- Remove duplicate defines from repeater and sensor examples

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace array of PendingAdvertRequest structs with simple variables:
- pending_advert_request (tag, 0 = none)
- pending_advert_request_time (millis when created)

This aligns with how other pending requests (login, status, etc.) work.
Only one advert request can be pending at a time now.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The app handles its own timeouts - no need for the companion radio
to track them.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The int32 values are sent directly to the app, no conversion needed.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
payloads.md:
- Add ADVERT_REQUEST (0xA0) control sub-type
- Add ADVERT_RESPONSE (0xA1) control sub-type with all fields and flags

protocol_guide.md:
- Add CMD_REQUEST_ADVERT (0x39) command
- Add PUSH_CODE_ADVERT_RESPONSE (0x8F) to packet types
- Add parsing pseudocode for advert response
- Update response matching table

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Change ADVERT_RESPONSE from 0xA1 to 0xB0 so each control subtype
has a unique upper nibble per the protocol convention. Update
handlers to use nibble matching consistently.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This reduces unnecessary network traffic since repeater adverts are
primarily useful for local neighbor discovery (zero-hop), not for
multi-hop propagation.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Restructured packet to match regular advert pattern:
- Signature now covers pubkey + timestamp + app_data
- app_data includes: adv_type, node_name, flags, and all optional fields
- This prevents intermediate nodes from tampering with any metadata

Co-Authored-By: Claude Opus 4.5 <[email protected]>
DIRECT routing consumes the header path at each hop, so the target
node couldn't reverse it for the response. This fix:

- Embeds the forward path in the ADVERT_REQUEST payload
- Target extracts path, excludes itself (last element), and reverses
- For direct neighbors (path=[target]), response sent as zero-hop
- Works with stock firmware intermediate nodes (they only use header)

Example: path [0x23, 0x1F] → response path [0x23]

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants