From 75d85dd71e5bee689aebdf292d2cd3cde174ce4f Mon Sep 17 00:00:00 2001 From: MuninnMesh Date: Tue, 26 May 2026 13:20:21 -0500 Subject: [PATCH] Add direct repeater telemetry reply controls --- examples/simple_repeater/MyMesh.cpp | 49 ++++++++++++++++++++++++- examples/simple_repeater/MyMesh.h | 1 + src/Dispatcher.cpp | 17 ++++++++- src/Dispatcher.h | 12 ++++++ src/Packet.cpp | 3 +- src/Packet.h | 3 ++ src/helpers/CommonCLI.cpp | 34 +++++++++++++++-- src/helpers/CommonCLI.h | 4 ++ src/helpers/StaticPoolPacketManager.cpp | 6 ++- variants/rak3401/platformio.ini | 1 + 10 files changed, 121 insertions(+), 9 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5c..ecb66c4dae 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -41,6 +41,10 @@ #define TXT_ACK_DELAY 200 #endif +#ifndef TELEM_REPLY_ZEROHOP_DEFAULT + #define TELEM_REPLY_ZEROHOP_DEFAULT 0 +#endif + #define FIRMWARE_VER_LEVEL 2 #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS @@ -60,6 +64,10 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +static void setRepeaterTxPower(int8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -426,6 +434,14 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui } } +void MyMesh::applyTelemReplyPower(mesh::Packet* packet) { +#ifdef TELEM_REPLY_PER_PACKET_POWER + if (packet && _prefs.telem_reply_power_dbm != TELEM_REPLY_POWER_UNSET) { + packet->_tx_power = _prefs.telem_reply_power_dbm; + } +#endif +} + bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; @@ -587,6 +603,15 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m if (reply_len == 0) return; // invalid request + if (_prefs.telem_reply_zerohop) { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) { + applyTelemReplyPower(reply); + sendZeroHop(reply, SERVER_RESPONSE_DELAY); + } + return; + } + if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, @@ -663,6 +688,16 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, client->last_timestamp = timestamp; client->last_activity = getRTCClock()->getCurrentTime(); + if (_prefs.telem_reply_zerohop) { + mesh::Packet *reply = + createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + if (reply) { + applyTelemReplyPower(reply); + sendZeroHop(reply, SERVER_RESPONSE_DELAY); + } + return; + } + if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, @@ -705,7 +740,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { + if (_prefs.telem_reply_zerohop) { + applyTelemReplyPower(ack); + sendZeroHop(ack, TXT_ACK_DELAY); + } else if (client->out_path_len == OUT_PATH_UNKNOWN) { sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); @@ -733,7 +771,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { + if (_prefs.telem_reply_zerohop) { + applyTelemReplyPower(reply); + sendZeroHop(reply, CLI_REPLY_DELAY_MILLIS); + } else if (client->out_path_len == OUT_PATH_UNKNOWN) { sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); @@ -883,6 +924,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.bw = LORA_BW; _prefs.cr = LORA_CR; _prefs.tx_power_dbm = LORA_TX_POWER; + _prefs.telem_reply_zerohop = TELEM_REPLY_ZEROHOP_DEFAULT; + _prefs.telem_reply_power_dbm = TELEM_REPLY_POWER_UNSET; _prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.flood_advert_interval = 12; // 12 hours _prefs.flood_max = 64; @@ -955,6 +998,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + setTxPowerControl(setRepeaterTxPower, _prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", @@ -1051,6 +1095,7 @@ void MyMesh::dumpLogFile() { void MyMesh::setTxPower(int8_t power_dbm) { radio_set_tx_power(power_dbm); + setNodeTxPower(power_dbm); } #if defined(USE_SX1262) || defined(USE_SX1268) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e69..090458b116 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -129,6 +129,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); + void applyTelemReplyPower(mesh::Packet* packet); protected: float getAirtimeBudgetFactor() const override { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..254325e13d 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -63,6 +63,13 @@ uint32_t Dispatcher::getCADFailMaxDuration() const { return 4000; // 4 seconds } +void Dispatcher::restoreTxPower() { + if (_tx_power_overridden && _set_tx_power_fn) { + _set_tx_power_fn(_node_tx_power); + } + _tx_power_overridden = false; +} + void Dispatcher::loop() { if (millisHasNowPassed(next_floor_calib_time)) { _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); @@ -111,6 +118,7 @@ void Dispatcher::loop() { } else { n_sent_direct++; } + restoreTxPower(); releasePacket(outbound); // return to pool outbound = NULL; } else if (millisHasNowPassed(outbound_expiry)) { @@ -119,6 +127,7 @@ void Dispatcher::loop() { _radio->onSendFinished(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + restoreTxPower(); releasePacket(outbound); // return to pool outbound = NULL; } else { @@ -323,6 +332,10 @@ void Dispatcher::checkSend() { } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; + if (_set_tx_power_fn && outbound->_tx_power != PACKET_TX_POWER_DEFAULT) { + _set_tx_power_fn(outbound->_tx_power); + _tx_power_overridden = true; + } uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); @@ -331,6 +344,7 @@ void Dispatcher::checkSend() { logTxFail(outbound, outbound->getRawLength()); + restoreTxPower(); releasePacket(outbound); // return to pool outbound = NULL; return; @@ -359,6 +373,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->_tx_power = PACKET_TX_POWER_DEFAULT; } return pkt; } @@ -386,4 +401,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..47ceeec17d 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -126,9 +126,13 @@ class Dispatcher { unsigned long tx_budget_ms; unsigned long last_budget_update; unsigned long duty_cycle_window_ms; + void (*_set_tx_power_fn)(int8_t); + int8_t _node_tx_power; + bool _tx_power_overridden; void processRecvPacket(Packet* pkt); void updateTxBudget(); + void restoreTxPower(); protected: PacketManager* _mgr; @@ -150,6 +154,9 @@ class Dispatcher { tx_budget_ms = 0; last_budget_update = 0; duty_cycle_window_ms = 3600000; + _set_tx_power_fn = NULL; + _node_tx_power = 0; + _tx_power_overridden = false; } virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; @@ -176,6 +183,11 @@ class Dispatcher { Packet* obtainNewPacket(); void releasePacket(Packet* packet); void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); + void setTxPowerControl(void (*fn)(int8_t), int8_t node_dbm) { + _set_tx_power_fn = fn; + _node_tx_power = node_dbm; + } + void setNodeTxPower(int8_t node_dbm) { _node_tx_power = node_dbm; } unsigned long getTotalAirTime() const { return total_air_time; } unsigned long getReceiveAirTime() const {return rx_air_time; } diff --git a/src/Packet.cpp b/src/Packet.cpp index aad3e2f48e..4d80caf6ac 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -8,6 +8,7 @@ Packet::Packet() { header = 0; path_len = 0; payload_len = 0; + _tx_power = PACKET_TX_POWER_DEFAULT; } bool Packet::isValidPathLen(uint8_t path_len) { @@ -84,4 +85,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { return true; // success } -} \ No newline at end of file +} diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..f83e0d0ef8 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -16,6 +16,8 @@ namespace mesh { #define ROUTE_TYPE_DIRECT 0x02 // direct route, 'path' is supplied #define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 // direct route + transport codes +#define PACKET_TX_POWER_DEFAULT ((int8_t)0x7F) + #define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) #define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) #define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) @@ -49,6 +51,7 @@ class Packet { uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + int8_t _tx_power; /** * \brief calculate the hash of payload + type diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index d495aada5f..a2fb3506d6 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -87,8 +87,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 + file.read((uint8_t *)&_prefs->telem_reply_zerohop, sizeof(_prefs->telem_reply_zerohop)); // 291 + file.read((uint8_t *)&_prefs->telem_reply_power_dbm, sizeof(_prefs->telem_reply_power_dbm)); // 292 + // next: 293 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -118,6 +120,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->telem_reply_zerohop = constrain(_prefs->telem_reply_zerohop, 0, 1); // boolean file.close(); } @@ -178,8 +181,10 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 + file.write((uint8_t *)&_prefs->telem_reply_zerohop, sizeof(_prefs->telem_reply_zerohop)); // 291 + file.write((uint8_t *)&_prefs->telem_reply_power_dbm, sizeof(_prefs->telem_reply_power_dbm)); // 292 + // next: 293 file.close(); } @@ -492,6 +497,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->multi_acks = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "telem.reply.zerohop ", 20) == 0) { + _prefs->telem_reply_zerohop = (memcmp(&config[20], "on", 2) == 0) ? 1 : 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "telem.reply.power ", 18) == 0) { + const char* v = &config[18]; + if (memcmp(v, "off", 3) == 0) { + _prefs->telem_reply_power_dbm = TELEM_REPLY_POWER_UNSET; + } else { + _prefs->telem_reply_power_dbm = (int8_t)atoi(v); + } + savePrefs(); + strcpy(reply, "OK"); } else if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); @@ -745,6 +763,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); + } else if (memcmp(config, "telem.reply.zerohop", 19) == 0) { + sprintf(reply, "> %s", _prefs->telem_reply_zerohop ? "on" : "off"); + } else if (memcmp(config, "telem.reply.power", 17) == 0) { + if (_prefs->telem_reply_power_dbm == TELEM_REPLY_POWER_UNSET) { + strcpy(reply, "> off"); + } else { + sprintf(reply, "> %d", (int32_t)_prefs->telem_reply_power_dbm); + } } else if (memcmp(config, "allow.read.only", 15) == 0) { sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); } else if (memcmp(config, "flood.advert.interval", 21) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffdc7c6536..9823daa496 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,8 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 +#define TELEM_REPLY_POWER_UNSET ((int8_t)-128) + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -61,6 +63,8 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t telem_reply_zerohop; + int8_t telem_reply_power_dbm; }; class CommonCLICallbacks { diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0cc..e20c3f687e 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -76,7 +76,11 @@ StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_siz } mesh::Packet* StaticPoolPacketManager::allocNew() { - return unused.removeByIdx(0); // just get first one (returns NULL if empty) + mesh::Packet* packet = unused.removeByIdx(0); // just get first one (returns NULL if empty) + if (packet) { + packet->_tx_power = PACKET_TX_POWER_DEFAULT; + } + return packet; } void StaticPoolPacketManager::free(mesh::Packet* packet) { diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 3d2d4a3ec5..53271ec283 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -34,6 +34,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D TELEM_REPLY_PER_PACKET_POWER ;-D MESH_PACKET_LOGGING=1 ;-D MESH_DEBUG=1 build_src_filter = ${rak3401.build_src_filter}