Feat/mesh beacon (#10618)
* Tips robot virtual node / relayer to different LoRa modes & channels
Note that this commit has details hardcoded for the Wellington (NZ)
mesh, and also requires the following patch to the protobufs:
-----
diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto
index 03162d8..ec54c99 100644
--- a/meshtastic/mesh.proto
+++ b/meshtastic/mesh.proto
@@ -1393,6 +1393,21 @@ message MeshPacket {
* Set by the firmware internally, clients are not supposed to set this.
*/
uint32 tx_after = 20;
+
+ /*
+ * The modem preset to use fo rthis packet
+ */
+ uint32 modem_preset = 21;
+
+ /*
+ * The frequency slot to use for this packet
+ */
+ uint32 frequency_slot = 22;
+
+ /*
+ * Whether the packet has a nonstandard radio config
+ */
+ bool nonstandard_radio_config = 23;
}
/*
-----
* fix: repair mesh tips CI build
* feat: add MeshBeacon module (Phase 1 — proto + generated code + initial stub)
* feat(beacon): implement broadcaster + listener (phases 2-5)
* feat(beacon): wire RadioLibInterface hooks + admin validation (phases 6-7)
* fix(beacon): fix LocalModuleConfig flat access (no payload_variant), add localonly proto field
* feat(beacon): fix broadcaster inheritance, add preset/region validation + proto cache
- MeshBeaconBroadcastModule now inherits ProtobufModule<meshtastic_MeshBeacon>
(alongside private MeshBeaconModule + OSThread), giving it allocDataPacket()
and setStartDelay() without extra includes.
- Payload cache: rebuildCache() encodes the MeshBeacon protobuf once and stores
it in payloadCache[]/payloadCacheSize; sendBeacon() only calls rebuildCache()
when payloadCacheDirty==true. AdminModule calls invalidateCache() after saving
new config so the next broadcast picks up changes.
- Region/preset validation in handleSetModuleConfig (mesh_beacon_tag):
broadcast_on_preset is validated against the device's current region via
RadioInterface::validateConfigLora(); broadcast_offer_region is validated via
RadioInterface::validateConfigRegion(). Invalid values are zeroed with a
LOG_WARN before saving.
* feat(beacon): add unit tests for MeshBeaconModule and AdminModule configuration validation
* remove old meshtips
* more validation in NodeDB and AdminModule, and userprefs for baked in goodness
* copilot is my gravity
* mmmmm... beacon
* oops
* Enhance unit tests for MeshBeaconModule with detailed validation checks and output formatting
* new lines. Why not?
* finally
* legacy mode activate!
* Update protobufs (#17)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* better logic, fixed a test
* updated for packet signing
fixed a test
added guards for licensed/ham mode
* channel numbers
* beacon: encrypt on the beacon channel PSK; fix split note
When broadcast_on_channel overrides the primary channel's name/PSK, the
beacon was encrypted with the PRIMARY PSK: perhapsEncode keys encryption
off the primary slot, but the radio-thread channel switch happens only
after encryption. sendBeaconPacket() now installs the beacon channel into
the primary slot for the synchronous duration of send() (cooperative
threading => no interleaving) so encryption/hash use the beacon channel,
then restores it. A shared beaconChannelSettings() helper builds the
channel for both the encrypt-time swap and the RF-time swap so the
key+hash cannot drift.
Also: correct the legacy-split comments (both packets go out on the same
beacon radio settings, not the normal config) and merge the two
consecutive `if (hasText)` blocks in the listener (cppcheck
duplicateCondition).
Tests: add channelPskOverride_swapsBeaconChannelAndRestores and
noChannelOverride_doesNotSwapPrimary; MockRouter snapshots the primary
channel at send() time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test/beacon: drain toPhoneQueue in tearDown to fix LSan leak abort
The listener delivers received text via MeshService::sendToPhone(), which
enqueues the packet into toPhoneQueue and takes ownership. Nothing dequeues
it in tests, so the three listener tests carrying message text stranded a
MeshPacket each — 1272 bytes / 3 allocations that LeakSanitizer flagged at
process exit, aborting the coverage run (surfaced by pio as [ERRORED] /
SIGHUP even though all 40 assertions passed).
Drain the phone queue in tearDown (getForPhone()/releaseToPool) so the
packets return to packetPool. Suite is now GREEN with no sanitizer abort.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* legacy hop override for zero-hoppers
* ever more beacons
* beacon: comment out broadcast_send_as_node pending further review
Functionality preserved in comments with full signing/has_bitfield notes
for when it is re-enabled. Proto tag 3 retained on the wire.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test/beacon: fix fromIsCustomNodeWhenSet now that send-as-node is disabled
broadcast_send_as_node is commented out; from is always the local node.
Update the test assertion and doc comment to match current behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update protobufs (#21)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* flags for beacons
* beacon: do more with less — slot-index targets + validation
Multi-target beacons embedded a full ChannelSettings in every BroadcastTarget,
blowing ModuleConfig past the 512-byte BLE FromRadio budget so the firmware would
not compile. Targets now reference an existing channel-table slot by channel_index
and the broadcaster resolves it via channels.getByIndex() at TX time. Net effect:
the same multi-target capability for a fraction of the bytes —
FromRadio 609 -> 510 B, MeshBeaconConfig 596 -> 324 B, AdminMessage 615 -> 511 B.
- proto: BroadcastTarget.channel (embedded) -> channel_index (uint32 ref); regen all
generated headers (size constants propagate to admin/localonly/deviceonly/mesh).
- broadcaster: resolve channel_index from the channel table; an out-of-range or blank
slot falls back to the default channel for the target preset rather than borrowing
the primary's name/PSK.
- AdminModule: validate broadcast_targets entries on write (region/preset sanitised
like the single-target fields; channel_index range-checked).
- userPrefs: TARGET_<n>_CHANNEL_{NAME,NUM,PSK} collapse to a single CHANNEL_INDEX.
- docs: two-step (set_channel -> set_module_config) multi-target setup, inline-vs-
reference distinction, and single-/multi-target are equal (not "legacy") options.
- tests: target validation + channel-index resolution incl. blank-slot fallback
(47/47 green on `./bin/run-tests.sh -e native -f test_mesh_beacon`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRAF5csgsMn6p1zEcFL8Qz
* throttling after reboot
* address copilot review
* simplify
* fix(beacon): use 0x%08x for node/packet IDs in logs; register test suite
The %#08lx log specifiers passed uint32_t (NodeNum/PacketId) to a %lx
length modifier — undefined behaviour on 64-bit (native test) targets and
non-standard width. Switch to the project-standard 0x%08x. Also bump
test/native-suite-count to 25 for the added test_mesh_beacon suite.
clod helped too
* copilot & clarity
clod helped too
* refactor(beacon): use auto for the sanitized config copy
clod helped too
* fix(beacon): guard empty-payload sends; gate has_mesh_beacon on build flag; document ISR_TX pre-switch
clod helped too
---------
Co-authored-by: Steve Gilberd <steve@erayd.net>
Co-authored-by: Darafei Praliaskouski <me@komzpa.net>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
GitHub
Steve Gilberd
Darafei Praliaskouski
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Claude Opus 4.8
parent
ad132e9540
commit
ec5d230305
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
#include "MeshRadio.h"
|
||||
#include "Observer.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/generated/meshtastic/mesh_beacon.pb.h"
|
||||
#include "mesh/generated/meshtastic/module_config.pb.h"
|
||||
|
||||
// Short aliases for the MeshBeaconConfig.flags bitfield (see module_config.proto MeshBeaconConfig.Flags).
|
||||
#define MESH_BEACON_FLAG_LISTEN_ENABLED meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LISTEN_ENABLED
|
||||
#define MESH_BEACON_FLAG_BROADCAST_ENABLED meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_BROADCAST_ENABLED
|
||||
#define MESH_BEACON_FLAG_LEGACY_SPLIT meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LEGACY_SPLIT
|
||||
|
||||
// Sidecar entry pairing a packet ID with target radio settings for beacon TX.
|
||||
typedef struct {
|
||||
bool inUse;
|
||||
PacketId id;
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset;
|
||||
uint16_t slot;
|
||||
// When true, reconfigureForBeaconTX sets hop_start=1 so pre-2.7.20 firmware
|
||||
// (which drops hop_start==0 packets) accepts the zero-hop beacon.
|
||||
bool legacyHopOverride;
|
||||
// Per-target radio settings. UNSET region means use current lora.region.
|
||||
meshtastic_Config_LoRaConfig_RegionCode region;
|
||||
bool has_channel;
|
||||
meshtastic_ChannelSettings channel;
|
||||
} MeshBeaconModule_TargetRadioSettings;
|
||||
|
||||
/**
|
||||
* Base class: holds the radio-switching sidecar table and static helpers.
|
||||
* The sidecar avoids touching MeshPacket proto fields for per-packet radio state.
|
||||
*/
|
||||
class MeshBeaconModule
|
||||
{
|
||||
public:
|
||||
MeshBeaconModule();
|
||||
|
||||
/**
|
||||
* Reconfigure the radio for beacon TX, or restore to original config if p is NULL.
|
||||
* Returns true if the radio was reconfigured (caller must re-run transmit delay for CCA).
|
||||
* Driven by broadcast_on_preset / broadcast_on_channel from MeshBeaconConfig.
|
||||
*/
|
||||
static bool reconfigureForBeaconTX(RadioInterface *iface, meshtastic_MeshPacket *p);
|
||||
|
||||
/**
|
||||
* Associate target radio settings with an outgoing packet by its ID.
|
||||
* Sidecar holds 8 entries; evicts slot 0 on overflow.
|
||||
*/
|
||||
static void
|
||||
setTargetRadioSettings(const meshtastic_MeshPacket *p, meshtastic_Config_LoRaConfig_ModemPreset preset, uint16_t slot,
|
||||
bool legacyHopOverride = false,
|
||||
meshtastic_Config_LoRaConfig_RegionCode region = meshtastic_Config_LoRaConfig_RegionCode_UNSET,
|
||||
bool has_channel = false, const meshtastic_ChannelSettings *channel = nullptr);
|
||||
|
||||
/**
|
||||
* Returns true if the sidecar table contains an entry for this packet's ID.
|
||||
* Used by RadioLibInterface to gate the channel-active check.
|
||||
*/
|
||||
static bool hasTargetRadioSettings(const meshtastic_MeshPacket *p);
|
||||
|
||||
/**
|
||||
* Remove the sidecar entry for this packet after it has been sent.
|
||||
* Called from RadioLibInterface::completeSending().
|
||||
*/
|
||||
static void clearTargetRadioSettings(const meshtastic_MeshPacket *p);
|
||||
|
||||
/**
|
||||
* True if p is tagged for a beacon radio switch whose target config must NOT be transmitted:
|
||||
* preset invalid for the target region, or an unlicensed node would key up on a ham-only
|
||||
* (licensed-only) region. The radio driver drops such packets rather than sending them on the
|
||||
* current config. False for any packet without a sidecar entry (normal traffic is never affected).
|
||||
*/
|
||||
static bool beaconTxConfigInvalid(const meshtastic_MeshPacket *p);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Build the ChannelSettings the beacon transmits on: the base (primary) channel overlaid with
|
||||
* any broadcast_on_channel overrides, defaulting an empty name to the target preset's display
|
||||
* name. Shared by the encrypt-time channel swap and the radio-thread RF swap so the channel
|
||||
* key + hash are identical at both points.
|
||||
*/
|
||||
static meshtastic_ChannelSettings beaconChannelSettings(const meshtastic_ChannelSettings &base,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset,
|
||||
const meshtastic_ChannelSettings *overrideChannel = nullptr);
|
||||
|
||||
static meshtastic_Config_LoRaConfig_ModemPreset originalModemPreset;
|
||||
static uint16_t originalLoraChannel;
|
||||
static meshtastic_Config_LoRaConfig_RegionCode originalRegion;
|
||||
static meshtastic_ChannelSettings originalPrimaryChannel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Broadcaster: periodically sends MeshBeacon packets on the configured preset/channel.
|
||||
* Active only when the FLAG_BROADCAST_ENABLED bit is set in moduleConfig.mesh_beacon.flags.
|
||||
* Inherits ProtobufModule to access allocDataProtobuf + setStartDelay.
|
||||
*
|
||||
* Packet flow:
|
||||
* Normal (combined): one MESH_BEACON_APP carrying offer + message on the beacon radio config.
|
||||
* Legacy split: two packets when both text and offer are present and FLAG_LEGACY_SPLIT is set,
|
||||
* both sent on the same beacon radio settings:
|
||||
* A) MESH_BEACON_APP with offer only (no text).
|
||||
* B) TEXT_MESSAGE_APP with the text (for clients that only decode TEXT_MESSAGE_APP).
|
||||
*/
|
||||
class MeshBeaconBroadcastModule : private MeshBeaconModule,
|
||||
public ProtobufModule<meshtastic_MeshBeacon>,
|
||||
private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
MeshBeaconBroadcastModule();
|
||||
|
||||
// Mark the cached payload dirty (call after config change).
|
||||
void invalidateCache() { payloadCacheDirty = true; }
|
||||
|
||||
protected:
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &, meshtastic_MeshBeacon *) override { return false; }
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
protected:
|
||||
void sendBeacon();
|
||||
void rebuildCache();
|
||||
|
||||
// Send one beacon packet. When overrideChannel is set and has a name/PSK override,
|
||||
// the packet is encrypted with that channel's key (not the primary's).
|
||||
void sendBeaconPacket(meshtastic_MeshPacket *p, meshtastic_Config_LoRaConfig_ModemPreset targetPreset,
|
||||
bool has_channel = false, const meshtastic_ChannelSettings *overrideChannel = nullptr);
|
||||
|
||||
bool payloadCacheDirty = true;
|
||||
uint8_t payloadCache[meshtastic_MeshBeacon_size] = {};
|
||||
pb_size_t payloadCacheSize = 0;
|
||||
};
|
||||
extern MeshBeaconBroadcastModule *meshBeaconBroadcastModule;
|
||||
|
||||
/**
|
||||
* Listener: receives MESH_BEACON_APP packets and caches any offered channel/preset for the client
|
||||
* app to retrieve. It does NOT unwrap the text into a separate message — the original beacon packet
|
||||
* already reaches the client (handler returns CONTINUE), which reads `message` from it directly.
|
||||
* Does NOT auto-apply offered settings — client app must do so explicitly.
|
||||
* Active only when the FLAG_LISTEN_ENABLED bit is set in moduleConfig.mesh_beacon.flags.
|
||||
*/
|
||||
class MeshBeaconListenerModule : public ProtobufModule<meshtastic_MeshBeacon>, public Observable<const meshtastic_MeshPacket *>
|
||||
{
|
||||
public:
|
||||
MeshBeaconListenerModule();
|
||||
|
||||
struct BeaconOffer {
|
||||
bool valid;
|
||||
NodeNum sender;
|
||||
bool has_channel;
|
||||
meshtastic_ChannelSettings channel;
|
||||
meshtastic_Config_LoRaConfig_RegionCode region;
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset;
|
||||
uint32_t received_at;
|
||||
};
|
||||
|
||||
// Last received offer — accessible to admin/API for client app retrieval.
|
||||
static BeaconOffer lastReceivedOffer;
|
||||
|
||||
protected:
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MeshBeacon *b) override;
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
};
|
||||
extern MeshBeaconListenerModule *meshBeaconListenerModule;
|
||||
Reference in New Issue
Block a user