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;
 }

 /*
-----
This commit is contained in:
Steve Gilberd 2025-07-01 11:29:59 +12:00
parent 4bd416413a
commit 7d3af3aa8e
No known key found for this signature in database
GPG Key ID: 3CA3675E3C90637F
5 changed files with 350 additions and 8 deletions

View File

@ -94,6 +94,13 @@ class Channels
bool ensureLicensedOperation();
/**
* Validate a channel, fixing any errors as needed
*/
meshtastic_Channel &fixupChannel(ChannelIndex chIndex);
int16_t getHash(ChannelIndex i) { return hashes[i]; }
private:
/** Given a channel index, change to use the crypto key specified by that index
*
@ -111,13 +118,6 @@ class Channels
*/
int16_t generateHash(ChannelIndex channelNum);
int16_t getHash(ChannelIndex i) { return hashes[i]; }
/**
* Validate a channel, fixing any errors as needed
*/
meshtastic_Channel &fixupChannel(ChannelIndex chIndex);
/**
* Writes the default lora config
*/

View File

@ -8,6 +8,9 @@
#include "error.h"
#include "main.h"
#include "mesh-pb-constants.h"
#if !MESHTASTIC_EXCLUDE_TIPS
#include "modules/MeshTipsModule.h"
#endif
#include <pb_decode.h>
#include <pb_encode.h>
@ -248,6 +251,9 @@ void RadioLibInterface::onNotify(uint32_t notification)
switch (notification) {
case ISR_TX:
handleTransmitInterrupt();
#if !MESHTASTIC_EXCLUDE_TIPS
MeshTipsModule::configureRadioForPacket(this, txQueue.getFront());
#endif
startReceive();
setTransmitDelay();
break;
@ -270,9 +276,16 @@ void RadioLibInterface::onNotify(uint32_t notification)
if (delay_remaining > 0) {
// There's still some delay pending on this packet, so resume waiting for it to elapse
notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false);
#if !MESHTASTIC_EXCLUDE_TIPS
} else if (MeshTipsModule::configureRadioForPacket(this, txp)) {
// We just switched radio config, so wait to ensure the new channel is available
setTransmitDelay();
#endif
} else {
if (isChannelActive()) { // check if there is currently a LoRa packet on the channel
startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again
if (!txp->nonstandard_radio_config) {
startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again
}
setTransmitDelay();
} else {
// Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and

View File

@ -0,0 +1,242 @@
#include "MeshTipsModule.h"
#include "Default.h"
#include "MeshService.h"
#include "RTC.h"
#include "RadioInterface.h"
#include "configuration.h"
#include "main.h"
#include <Throttle.h>
#define NODENUM_TIPS 0x00000004
static meshtastic_Config_LoRaConfig_ModemPreset originalModemPreset; // original modem preset
static uint16_t originalLoraChannel; // original frequency slot
char originalChannelName[sizeof(MeshTipsModule_TXSettings)]; // original channel name
MeshTipsModule::MeshTipsModule()
{
originalModemPreset = config.lora.modem_preset;
originalLoraChannel = config.lora.channel_num;
strncpy(originalChannelName, channels.getPrimary().name, sizeof(originalChannelName));
}
bool MeshTipsModule::configureRadioForPacket(RadioInterface *iface, meshtastic_MeshPacket *p)
{
meshtastic_ChannelSettings *c = (meshtastic_ChannelSettings *)&channels.getPrimary();
if (p && p->from == NODENUM_TIPS && p->nonstandard_radio_config &&
(p->modem_preset != config.lora.modem_preset || p->frequency_slot != config.lora.channel_num)) {
LOG_INFO("Reconfiguring for TX of packet %#08lx (from=%#08lx size=%lu)", p->id, p->from, p->decoded.payload.size);
config.lora.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)p->modem_preset;
config.lora.channel_num = p->frequency_slot;
memset(c->name, 0, sizeof(c->name));
switch (p->modem_preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
strncpy(c->name, "ShortTurbo", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
strncpy(c->name, "ShortFast", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
strncpy(c->name, "ShortSlow", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
strncpy(c->name, "MediumFast", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
strncpy(c->name, "MediumSlow", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
strncpy(c->name, "LongFast", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
strncpy(c->name, "LongMod", sizeof(c->name));
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
strncpy(c->name, "LongSlow", sizeof(c->name));
break;
}
channels.fixupChannel(channels.getPrimaryIndex());
p->channel = channels.getHash(channels.getPrimaryIndex());
iface->reconfigure();
return true;
} else if ((!p || !p->nonstandard_radio_config) &&
(config.lora.modem_preset != originalModemPreset || config.lora.channel_num != originalLoraChannel)) {
LOG_INFO("Reconfiguring for TX of packet %#08lx (from=%#08lx size=%lu)", p->id, p->from, p->decoded.payload.size);
config.lora.modem_preset = originalModemPreset;
config.lora.channel_num = originalLoraChannel;
memset(c->name, 0, sizeof(c->name));
strncpy(c->name, originalChannelName, sizeof(c->name));
channels.fixupChannel(channels.getPrimaryIndex());
iface->reconfigure();
return true;
}
return false;
}
MeshTipsModule_TXSettings MeshTipsModule::stripTargetRadioSettings(meshtastic_MeshPacket *p)
{
MeshTipsModule_TXSettings s = {
.preset = originalModemPreset,
.slot = originalLoraChannel,
};
// clamp final byte of payload, just in case, because I don't know if this is taken care of elsewhere
p->decoded.payload.bytes[p->decoded.payload.size] = 0;
if (!p || strlen((char *)p->decoded.payload.bytes) < 4 || p->decoded.payload.bytes[0] != '#')
return s;
char *msg = strchr((char *)p->decoded.payload.bytes, ' ');
if (!*msg)
return s;
*msg++ = 0;
const char presetString[3] = {p->decoded.payload.bytes[1], p->decoded.payload.bytes[2], 0};
char *slotString = (char *)&p->decoded.payload.bytes[3];
if (!*slotString)
return s;
for (char *c = slotString; *c; c++) {
if (!strchr("1234567890", *c))
return s;
}
uint8_t preset = 0;
if (!strncmp(presetString, "ST", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
else if (!strncmp(presetString, "SF", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
else if (!strncmp(presetString, "SS", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
else if (!strncmp(presetString, "MF", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
else if (!strncmp(presetString, "MS", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
else if (!strncmp(presetString, "LF", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
else if (!strncmp(presetString, "LM", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
else if (!strncmp(presetString, "LS", 2))
s.preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
else
return s;
s.slot = std::stoi(slotString);
p->decoded.payload.size = 1;
// don't use strcpy, because strcpy has undefined behaviour if src & dst overlap
for (char *a = msg, *b = (char *)p->decoded.payload.bytes; (*b++ = *a++);)
p->decoded.payload.size++;
return s;
}
MeshTipsNodeInfoModule *meshTipsNodeInfoModule;
bool MeshTipsNodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
{
// do nothing if we receive nodeinfo, because we only care about sending our own
return true;
}
void MeshTipsNodeInfoModule::sendTipsNodeInfo()
{
LOG_INFO("Send NodeInfo for mesh tips");
static meshtastic_User u = {
.hw_model = meshtastic_HardwareModel_PRIVATE_HW,
.is_licensed = false,
.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE,
.public_key =
{
.size = 32,
.bytes = {0x39, 0x37, 0x58, 0xe4, 0x05, 0x34, 0x7d, 0xe0, 0x49, 0x73, 0xec, 0xaf, 0xbc, 0x8e, 0x07, 0xe8,
0x66, 0x57, 0xe4, 0xa1, 0x2d, 0x53, 0x0e, 0x26, 0x51, 0x1f, 0x1a, 0x6c, 0xbf, 0xe8, 0x5e, 0x04},
},
.has_is_unmessagable = true,
.is_unmessagable = true,
};
strncpy(u.id, "!mesh_tips", sizeof(u.id));
strncpy(u.long_name, "WLG Mesh Tips Robot", sizeof(u.long_name));
strncpy(u.short_name, "TIPS", sizeof(u.short_name));
meshtastic_MeshPacket *p = allocDataProtobuf(u);
p->to = NODENUM_BROADCAST;
p->from = NODENUM_TIPS;
p->hop_limit = 0;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
meshtastic_MeshPacket *p_LF20 = packetPool.allocCopy(*p);
service->sendToMesh(p, RX_SRC_LOCAL, false);
p_LF20->frequency_slot = 20;
p_LF20->nonstandard_radio_config = true;
service->sendToMesh(p_LF20, RX_SRC_LOCAL, false);
}
MeshTipsNodeInfoModule::MeshTipsNodeInfoModule()
: ProtobufModule("nodeinfo_tips", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg),
concurrency::OSThread("MeshTipsNodeInfo")
{
MeshTipsModule();
setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds
// after we start (to give network time to setup)
}
int32_t MeshTipsNodeInfoModule::runOnce()
{
if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) {
sendTipsNodeInfo();
}
return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs);
}
MeshTipsMessageModule *meshTipsMessageModule;
ProcessMessage MeshTipsMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
#ifdef DEBUG_PORT
auto &d = mp.decoded;
#endif
meshtastic_MeshPacket *p = packetPool.allocCopy(mp);
MeshTipsModule_TXSettings s = stripTargetRadioSettings(p);
if (!p->decoded.payload.size || p->decoded.payload.bytes[0] == '#')
return ProcessMessage::STOP;
p->to = NODENUM_BROADCAST;
p->decoded.source = p->from;
p->from = NODENUM_TIPS;
p->channel = channels.getPrimaryIndex();
p->hop_limit = 0;
p->hop_start = 0;
p->rx_rssi = 0;
p->rx_snr = 0;
p->priority = meshtastic_MeshPacket_Priority_HIGH;
p->want_ack = false;
p->modem_preset = s.preset;
p->frequency_slot = s.slot;
if (s.preset != originalModemPreset || s.slot != originalLoraChannel) {
p->nonstandard_radio_config = true;
}
p->rx_time = getValidTime(RTCQualityFromNet);
service->sendToMesh(p, RX_SRC_LOCAL, false);
powerFSM.trigger(EVENT_RECEIVED_MSG);
notifyObservers(&mp);
return ProcessMessage::CONTINUE; // No other module should be caring about this message
}
bool MeshTipsMessageModule::wantPacket(const meshtastic_MeshPacket *p)
{
meshtastic_Channel *c = &channels.getByIndex(p->channel);
return c->role == meshtastic_Channel_Role_SECONDARY && strlen(c->settings.name) == strlen("Tips") &&
!strcmp(c->settings.name, "Tips") && p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP;
}

View File

@ -0,0 +1,80 @@
#pragma once
#include "Observer.h"
#include "ProtobufModule.h"
#include "RadioInterface.h"
#include "SinglePortModule.h"
typedef struct {
meshtastic_Config_LoRaConfig_ModemPreset preset;
uint16_t slot;
} MeshTipsModule_TXSettings;
/**
* Base class for the tips robot
*/
class MeshTipsModule
{
public:
/**
* Constructor
*/
MeshTipsModule();
/**
* Configure the radio to send the target packet, or return to default config if p is NULL
*/
static bool configureRadioForPacket(RadioInterface *iface, meshtastic_MeshPacket *p);
/**
* Get target modem settings
*/
MeshTipsModule_TXSettings stripTargetRadioSettings(meshtastic_MeshPacket *p);
};
/**
* Tips module for sending tips into the mesh
*/
class MeshTipsNodeInfoModule : private MeshTipsModule, public ProtobufModule<meshtastic_User>, private concurrency::OSThread
{
public:
/**
* Constructor
*/
MeshTipsNodeInfoModule();
protected:
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *p) override;
/**
* Send NodeInfo to the mesh
*/
void sendTipsNodeInfo();
/** Does our periodic broadcast */
virtual int32_t runOnce() override;
};
extern MeshTipsNodeInfoModule *meshTipsNodeInfoModule;
/**
* Text message handling for the tips robot
*/
class MeshTipsMessageModule : private MeshTipsModule, public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
{
public:
/**
* Constructor
*/
MeshTipsMessageModule() : MeshTipsModule(), SinglePortModule("tips", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
protected:
/**
* Called to handle a particular incoming message
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
/**
* Indicate whether this module wants to process the packet
*/
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
};
extern MeshTipsMessageModule *meshTipsMessageModule;

View File

@ -34,6 +34,9 @@
#if !MESHTASTIC_EXCLUDE_NODEINFO
#include "modules/NodeInfoModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_TIPS
#include "modules/MeshTipsModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_GPS
#include "modules/PositionModule.h"
#endif
@ -121,6 +124,10 @@ void setupModules()
#if !MESHTASTIC_EXCLUDE_NODEINFO
nodeInfoModule = new NodeInfoModule();
#endif
#if !MESHTASTIC_EXCLUDE_TIPS
meshTipsNodeInfoModule = new MeshTipsNodeInfoModule();
meshTipsMessageModule = new MeshTipsMessageModule();
#endif
#if !MESHTASTIC_EXCLUDE_GPS
positionModule = new PositionModule();
#endif