From fc940d27bae14c54f4c24f29503aa10a4f72c36a Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:13:26 +0200 Subject: [PATCH] sync with dev --- zephcore/ARCHITECTURE.md | 2 +- zephcore/Repeater_CLI_commands.md | 2 +- zephcore/adapters/radio/LoRaRadioBase.cpp | 3 +- zephcore/adapters/radio/LoRaRadioBase.h | 3 +- zephcore/adapters/radio/SX127xRadio.h | 7 ++++ zephcore/app/RepeaterMesh.cpp | 4 +++ zephcore/app/RepeaterMesh.h | 1 + zephcore/app/RoomServerMesh.cpp | 4 +++ zephcore/app/RoomServerMesh.h | 1 + zephcore/helpers/CommonCLI.cpp | 8 ++++- zephcore/helpers/CommonCLI.h | 3 ++ zephcore/include/mesh/Mesh.h | 3 +- zephcore/include/mesh/SimpleMeshTables.h | 9 +++-- zephcore/src/Dispatcher.cpp | 6 ++++ zephcore/src/Mesh.cpp | 43 ++++++++++++++--------- zephcore/src/main_companion.cpp | 4 +++ 16 files changed, 79 insertions(+), 24 deletions(-) diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md index f3726f2..0c21438 100644 --- a/zephcore/ARCHITECTURE.md +++ b/zephcore/ARCHITECTURE.md @@ -203,7 +203,7 @@ Path_len byte: **Direct routing**: Packet carries a source-routed `path[]`. Each relay node checks if the first path hash matches its own identity, removes itself, and forwards. Path is built from previous flood packets' accumulated hashes. -**Deduplication**: `SimpleMeshTables` maintains a circular buffer of 128 packet hashes (8 bytes each, SHA-256 truncated) and 64 ACK CRCs. `hasSeen()` prevents duplicate processing and retransmission. +**Deduplication**: `SimpleMeshTables` maintains a circular buffer of 160 packet hashes (8 bytes each, SHA-256 truncated); ACKs are deduped through the same packet-hash path. `wasSeen()` is a pure query; call sites insert explicitly via `markSeen()` to prevent duplicate processing and retransmission. ### 4.5 Dispatcher Scheduling diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index 8662ce6..dc8d6c0 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -247,7 +247,7 @@ Changes are persisted immediately unless noted. Some require a reboot. | `set multi.acks <0\|1>` | | Enable extra ACK transmits | | `set path.hash.mode ` | 0, 1, or 2 | Path hashing algorithm | | `set loop.detect ` | `off`, `minimal`, `moderate`, `strict` | Loop detection sensitivity | -| `set radio.rxgain <0\|1\|on\|off>` | | RX gain boost *(reboot required)* | +| `set radio.rxgain <0\|1\|on\|off>` | | RX gain boost, applied live. Replies `Error: unsupported` on radios without RX boost (SX127x); the pref is still saved. | | `set rxduty <0\|1\|on\|off>` | | RX duty cycle mode *(reboot required)*. Window timing auto-sized per SF/BW/preamble from the SX126x datasheet constraints (boot log line `rxduty:` shows the result). Zero-loss guarantee assumes senders on preamble-32 firmware (current MeshCore at SF≤8); legacy preamble-16 senders are only caught ~50% worst-phase — keep off until the local mesh has converted. Presets with 16-symbol preambles (SF≥9) fall back to continuous RX automatically. | | `set adc.multiplier ` | (0 = use board default) | Battery voltage ADC calibration multiplier | | `set prv.key ` | 64-char hex (32-byte key) | Replace private key; derive new identity *(reboot to apply)* | diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index 3f2176c..ba63112 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -881,7 +881,7 @@ void LoRaRadioBase::enableRxDutyCycle(bool enable) } } -void LoRaRadioBase::setRxBoost(bool enable) +bool LoRaRadioBase::setRxBoost(bool enable) { _rx_boost_enabled = enable; LOG_INF("RX boost %s (+3dB sensitivity, +2mA)", @@ -889,6 +889,7 @@ void LoRaRadioBase::setRxBoost(bool enable) if (atomic_get(&_in_recv_mode)) { hwSetRxBoost(enable); } + return true; } } /* namespace mesh */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index 8fcdff3..7e235e0 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -78,7 +78,8 @@ public: /* Power saving */ void enableRxDutyCycle(bool enable); bool isRxDutyCycleEnabled() const { return _rx_duty_cycle_enabled; } - void setRxBoost(bool enable); + /* Returns false when the chip has no RX boost feature (SX127x). */ + virtual bool setRxBoost(bool enable); bool isRxBoostEnabled() const { return _rx_boost_enabled; } /* Duty-cycle preamble false-positive counter. diff --git a/zephcore/adapters/radio/SX127xRadio.h b/zephcore/adapters/radio/SX127xRadio.h index 70ebdfc..a8779bb 100644 --- a/zephcore/adapters/radio/SX127xRadio.h +++ b/zephcore/adapters/radio/SX127xRadio.h @@ -21,6 +21,13 @@ public: void begin() override; + /* SX127x has no RX boost feature — report unsupported so the + * radio.rxgain CLI can reply "Error: unsupported". */ + bool setRxBoost(bool enable) override { + (void)enable; + return false; + } + protected: /* Hardware primitives */ bool hwConfigure(const struct lora_modem_config &cfg) override; diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 338c2da..374bad9 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -1146,6 +1146,10 @@ void RepeaterMesh::setTxPower(int8_t power_dbm) { radio_set_tx_power(power_dbm); } +bool RepeaterMesh::setRxBoostedGain(bool enable) { + return getRadioDriver(_radio).setRxBoost(enable); +} + void RepeaterMesh::formatNeighborsReply(char* reply) { char* dp = reply; diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index a25e378..5628558 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -233,6 +233,7 @@ public: void eraseLogFile() override; void dumpLogFile() override; void setTxPower(int8_t power_dbm) override; + bool setRxBoostedGain(bool enable) override; void formatNeighborsReply(char* reply) override; void removeNeighbor(const uint8_t* pubkey, int key_len) override; void formatStatsReply(char* reply) override; diff --git a/zephcore/app/RoomServerMesh.cpp b/zephcore/app/RoomServerMesh.cpp index b437c51..9e97751 100644 --- a/zephcore/app/RoomServerMesh.cpp +++ b/zephcore/app/RoomServerMesh.cpp @@ -918,6 +918,10 @@ void RoomServerMesh::setTxPower(int8_t power_dbm) { radio_set_tx_power(power_dbm); } +bool RoomServerMesh::setRxBoostedGain(bool enable) { + return getRadioDriver(_radio).setRxBoost(enable); +} + /* A room server keeps no neighbour table (it is not a repeater). */ void RoomServerMesh::formatNeighborsReply(char* reply) { strcpy(reply, "not supported"); diff --git a/zephcore/app/RoomServerMesh.h b/zephcore/app/RoomServerMesh.h index 3dc21a1..b3d67fd 100644 --- a/zephcore/app/RoomServerMesh.h +++ b/zephcore/app/RoomServerMesh.h @@ -182,6 +182,7 @@ public: void eraseLogFile() override; void dumpLogFile() override; void setTxPower(int8_t power_dbm) override; + bool setRxBoostedGain(bool enable) override; void formatNeighborsReply(char* reply) override; void formatStatsReply(char* reply) override; void formatRadioStatsReply(char* reply) override; diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 02ce179..1372d4e 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -895,9 +895,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch else if (memcmp(arg, "off", 3) == 0) val = 0; else if (arg[0] == '0' || arg[0] == '1') val = atoi(arg); if (val == 0 || val == 1) { + /* Always save (upstream f3d4d8cd), then apply live and + * report when the radio has no RX boost feature. */ _prefs->rx_boost = (uint8_t)val; savePrefs(); - snprintf(reply, CLI_REPLY_SIZE, "OK - radio.rxgain=%d (reboot to apply)", _prefs->rx_boost); + if (_callbacks->setRxBoostedGain(val == 1)) { + snprintf(reply, CLI_REPLY_SIZE, "OK - radio.rxgain=%d", _prefs->rx_boost); + } else { + strcpy(reply, "Error: unsupported"); + } } else { strcpy(reply, "Error: must be 0, 1, on, or off"); } diff --git a/zephcore/helpers/CommonCLI.h b/zephcore/helpers/CommonCLI.h index 33b2e38..6892064 100644 --- a/zephcore/helpers/CommonCLI.h +++ b/zephcore/helpers/CommonCLI.h @@ -35,6 +35,9 @@ public: virtual void eraseLogFile() = 0; virtual void dumpLogFile() = 0; virtual void setTxPower(int8_t power_dbm) = 0; + /* Apply RX boosted gain live; returns false when the radio has no + * RX boost feature (upstream PR #2844 semantics). */ + virtual bool setRxBoostedGain(bool enable) { (void)enable; return false; } /* Repeater-specific — default replies keep companion builds clean. * Repeater overrides all four; companions get "not available". */ virtual void formatNeighborsReply(char* reply) { strcpy(reply, "not available"); } diff --git a/zephcore/include/mesh/Mesh.h b/zephcore/include/mesh/Mesh.h index 9e00f5e..edcbe20 100644 --- a/zephcore/include/mesh/Mesh.h +++ b/zephcore/include/mesh/Mesh.h @@ -21,7 +21,8 @@ struct GroupChannel { class MeshTables { public: - virtual bool hasSeen(const Packet *packet) = 0; + virtual bool wasSeen(const Packet *packet) = 0; /* pure query, no insert */ + virtual void markSeen(const Packet *packet) = 0; /* explicit insert */ virtual void clear(const Packet *packet) = 0; }; diff --git a/zephcore/include/mesh/SimpleMeshTables.h b/zephcore/include/mesh/SimpleMeshTables.h index a6e7536..70df91e 100644 --- a/zephcore/include/mesh/SimpleMeshTables.h +++ b/zephcore/include/mesh/SimpleMeshTables.h @@ -24,7 +24,7 @@ public: _direct_dups = _flood_dups = 0; } - bool hasSeen(const Packet *packet) override { + bool wasSeen(const Packet *packet) override { uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); const uint8_t *sp = _hashes; @@ -38,9 +38,14 @@ public: return true; } } + return false; + } + + void markSeen(const Packet *packet) override { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE); _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; - return false; } void clear(const Packet *packet) override { diff --git a/zephcore/src/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp index 0e4994e..1720338 100644 --- a/zephcore/src/Dispatcher.cpp +++ b/zephcore/src/Dispatcher.cpp @@ -431,6 +431,12 @@ void Dispatcher::checkSend() len += outbound->payload_len; uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2; + /* Short packets (ACKs) have est airtimes small enough that + * IRQ/work-queue latency alone can blow the watchdog and clip + * the TX mid-air (upstream 4f8cb8db: 200ms est floor, x1.5). */ + if (max_airtime < 300) { + max_airtime = 300; + } outbound_start = now; #if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) diff --git a/zephcore/src/Mesh.cpp b/zephcore/src/Mesh.cpp index 7227f5b..7813bf1 100644 --- a/zephcore/src/Mesh.cpp +++ b/zephcore/src/Mesh.cpp @@ -146,7 +146,8 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt) tmp.path_len = Packet::copyPath(tmp.path, pkt->path, MAX_PATH_SIZE, pkt->path_len); tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - if (!_tables->hasSeen(&tmp)) { + if (!_tables->wasSeen(&tmp)) { + _tables->markSeen(&tmp); removeSelfFromPath(&tmp); routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); } @@ -205,7 +206,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) uint16_t offset = (uint16_t)pkt->path_len << path_sz; if (offset >= len) { onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); pkt->path[pkt->path_len++] = (int8_t)(pkt->getSNR() * 4); uint32_t d = getDirectRetransmitDelay(pkt); return ACTION_RETRANSMIT_DELAYED(5, d); @@ -241,13 +243,15 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) return forwardMultipartDirect(pkt); } if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - if (!_tables->hasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); removeSelfFromPath(pkt); routeDirectRecvAcks(pkt, 0); } return ACTION_RELEASE; } - if (!_tables->hasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); removeSelfFromPath(pkt); return ACTION_RETRANSMIT_DELAYED(0, getDirectRetransmitDelay(pkt)); } @@ -282,7 +286,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) } else { uint32_t ack_crc; memcpy(&ack_crc, pkt->payload, 4); - if (!_tables->hasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); onAckRecv(pkt, ack_crc); action = routeRecvPacket(pkt); } @@ -300,7 +305,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) uint8_t *macAndData = &pkt->payload[i]; if (i + CIPHER_MAC_SIZE >= (int)pkt->payload_len) { LOG_WRN("onRecvPacket: incomplete packet (i=%d, payload_len=%d)", i, pkt->payload_len); - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); if (self_id.isHashMatch(&dest_hash)) { int num = searchPeersByHash(&src_hash); bool found = false; @@ -360,7 +366,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) uint8_t *macAndData = &pkt->payload[i]; if (i + 2 >= (int)pkt->payload_len) { // incomplete packet - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); if (self_id.isHashMatch(&dest_hash)) { Identity sender(sender_pub_key); uint8_t secret[PUB_KEY_SIZE]; @@ -385,7 +392,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) uint8_t *macAndData = &pkt->payload[i]; if (i + 2 >= (int)pkt->payload_len) { // incomplete packet - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); GroupChannel channels[4]; int num = searchChannelsByHash(&channel_hash, channels, 4); for (int j = 0; j < num; j++) { @@ -410,7 +418,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) i += 4; const uint8_t *signature = &pkt->payload[i]; i += SIGNATURE_SIZE; - if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->hasSeen(pkt)) { + if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); uint8_t *app_data = (uint8_t *)&pkt->payload[i]; size_t app_data_len = pkt->payload_len - (size_t)i; if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE; @@ -427,7 +436,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) break; } case PAYLOAD_TYPE_RAW_CUSTOM: - if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) { + if (pkt->isRouteDirect() && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); onRawDataRecv(pkt); } break; @@ -444,7 +454,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt) tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - if (!_tables->hasSeen(&tmp)) { + if (!_tables->wasSeen(&tmp)) { + _tables->markSeen(&tmp); uint32_t ack_crc; memcpy(&ack_crc, tmp.payload, 4); onAckRecv(&tmp, ack_crc); @@ -538,7 +549,7 @@ void Mesh::sendFlood(Packet *packet, uint32_t delay_millis, uint8_t path_hash_si packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_FLOOD; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); + _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ #ifdef CONFIG_ZEPHCORE_APC { uint32_t h = ContentionTracker::computePacketHash32(packet); @@ -573,7 +584,7 @@ void Mesh::sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_m packet->transport_codes[0] = transport_codes[0]; packet->transport_codes[1] = transport_codes[1]; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); + _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ #ifdef CONFIG_ZEPHCORE_APC { uint32_t h = ContentionTracker::computePacketHash32(packet); @@ -625,7 +636,7 @@ void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uin } } - _tables->hasSeen(packet); + _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ sendPacket(packet, pri, delay_millis); } @@ -634,7 +645,7 @@ void Mesh::sendZeroHop(Packet *packet, uint32_t delay_millis) packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_DIRECT; packet->path_len = 0; - _tables->hasSeen(packet); + _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ sendPacket(packet, 0, delay_millis); } @@ -645,7 +656,7 @@ void Mesh::sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay packet->transport_codes[0] = transport_codes[0]; packet->transport_codes[1] = transport_codes[1]; packet->path_len = 0; - _tables->hasSeen(packet); + _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ sendPacket(packet, 0, delay_millis); } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 73ab951..1314999 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -682,6 +682,10 @@ public: LOG_INF("TX power %d dBm requested (reboot to apply)", power_dbm); } + bool setRxBoostedGain(bool enable) override { + return lora_radio.setRxBoost(enable); + } + mesh::LocalIdentity& getSelfId() override { return companion_mesh.self_id; } void saveIdentity(const mesh::LocalIdentity& new_id) override {