diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 9ae019a82..f50701ee2 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -197,6 +197,9 @@ jobs: python3 -B test/test_room_web_service.py -v python3 -B test/test_contact_clock_bootstrap.py -v python3 -B test/test_rtc_discovery.py -v + python3 -B test/test_upstream_routing_merge.py -v + python3 -B test/test_upstream_power_policy_merge.py -v + python3 -B test/test_lr1121_power_merge.py -v python3 -B test/test_radio_interrupt_recovery.py -v python3 -B test/test_bridge_recovery.py -v diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 9d72858ed..a4d2f3ba7 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -19,9 +19,11 @@ Once you have a working app/project, you need to be able to demonstrate it exist | Data-Type range | App name | Contact | |-----------------|-----------------------------|-------------------------------------------------------------------| | 0000 - 00FF | -reserved for internal use- | | -| 0100 | MeshCore Open | zsylvester@monitormx.com - https://github.com/zjs81/meshcore-open | -| 0110 - 011F | Ripple | ripple_biz@protonmail.com - https://buymeacoffee.com/ripplebiz | -| 0120 | MCO Advanced | most.original.address@gmail.com - https://github.com/HDDen/meshcore-open/tree/rename-mco-advanced | +| 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | +| 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | +| 0120 | MCO Advanced | most.original.address@gmail.com — https://hdden.ru/MCOa/ | +| 0130 - 013F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | +| 0140 | Nelos | support@nelos.app — https://nelos.app | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index d92e3c2f8..cfddf1d83 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -10,11 +10,11 @@ #define FIRMWARE_VER_CODE 14 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "14 Aug 2026" +#define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.17.1" +#define FIRMWARE_VERSION "v1.18.0" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 5a7c38d5e..b63bd7ecd 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -151,14 +151,14 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "14 Aug 2026" + #define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_BUILD_EPOCH #define FIRMWARE_BUILD_EPOCH 0UL #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.18.0" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 69b4a5a39..6b93f4ab4 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -3836,6 +3836,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { snprintf(reply, MAX_POST_TEXT_LEN, addSystemPost(msg) ? "OK" : "Err - post not retained"); } + } else if (strcmp(command, "set repeat on") == 0) { + strcpy(reply, "ERR - repeat now deprecated"); } #if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE else if (flood_rules.handleCommand(command, reply)) { diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index c94af4742..a9ef5f996 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -90,11 +90,11 @@ namespace mesh { class RoomJsonWriter; } /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "14 Aug 2026" + #define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.18.0" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index a73c4012e..e5c9e04ec 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -50,11 +50,11 @@ #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "14 Aug 2026" + #define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.18.0" #endif #define FIRMWARE_ROLE "sensor" diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 7fdf438ca..e822d4c4f 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -1145,13 +1145,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { removePathPrefix(pkt, pkt->getPathHashCount()); } else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { if (allowPacketForward(pkt)) { + const uint32_t d = getDirectRetransmitDelay(pkt); if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { - return forwardMultipartDirect(pkt); + return forwardMultipartDirect(pkt, d); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { if (!_tables->wasSeen(pkt)) { // don't retransmit! _tables->markSeen(pkt); removePathPrefix(pkt, 1); - routeDirectRecvAcks(pkt, 0); + routeDirectRecvAcks(pkt, d); } return ACTION_RELEASE; } @@ -1163,7 +1164,6 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { && hasValidEncryptedPayloadLength(pkt->payload_len, 2); removePathPrefix(pkt, 1); - uint32_t d = getDirectRetransmitDelay(pkt); maybeScheduleDirectRetry(pkt, 0, final_hop_retry); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } @@ -1563,7 +1563,7 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { return ACTION_RELEASE; } -DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { +DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt, uint32_t delay_millis) { if (pkt == NULL || pkt->payload_len < 1) { return ACTION_RELEASE; } @@ -1580,7 +1580,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); removePathPrefix(&tmp, 1); - routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) + routeDirectRecvAcks(&tmp, delay_millis + ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) } } return ACTION_RELEASE; @@ -1590,7 +1590,6 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { if (!packet->isMarkedDoNotRetransmit()) { uint8_t extra = getExtraAckTransmitCount(); while (extra > 0) { - delay_millis += getDirectRetransmitDelay(packet) + 300; auto a1 = createMultiAck(packet->payload, packet->payload_len, extra); if (a1) { a1->radio_reply = false; // relayed ACK, not a local response @@ -1599,6 +1598,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { a1->header |= ROUTE_TYPE_DIRECT; maybeScheduleDirectRetry(a1, 0); sendPacket(a1, 0, delay_millis); + delay_millis += 300; } extra--; } diff --git a/src/Mesh.h b/src/Mesh.h index 39dd82e19..ea293cce1 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -216,7 +216,7 @@ class Mesh : public Dispatcher { void decayOtaRelayBackoff(); void serviceLoopMaintenance(); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); - DispatcherAction forwardMultipartDirect(Packet* pkt); + DispatcherAction forwardMultipartDirect(Packet* pkt, uint32_t delay_millis); protected: // Roles with lazily persisted state can veto an OTA reset until that state diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 998e14259..9edc776f0 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -288,6 +288,7 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], } RegionEntry* RegionMap::findMatch(const mesh::Packet* packet, uint8_t mask) { + if (packet->transport_codes[0] == 0) return &wildcard; for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index b17b11061..73106cb0f 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -12,6 +12,8 @@ class CustomLR1121 : public LR1121 { bool _headerSeen = false; bool _rx_boosted = false; LR11x0MorseRfSwitch _morse_rf_switch; + int8_t _txPower = 0; + bool _txPowerSet = false; public: CustomLR1121(Module *mod) : LR1121(mod) { } @@ -70,6 +72,33 @@ class CustomLR1121 : public LR1121 { return len; } + using LR1121::setOutputPower; + using LR1121::setFrequency; + + int16_t setOutputPower(int8_t power) override { +#ifdef LR1121_FORCE_HP_PA + const bool force_hp = true; // boards with only RFO_HP_LF wired: never select the LP PA +#else + const bool force_hp = false; +#endif + // clip to the active PA's range (HF PA tops out at 13 dBm) rather than fail and keep the other band's PA + int8_t clipped = power; + checkOutputPower(power, &clipped, force_hp); + int16_t state = LR1121::setOutputPower(clipped, force_hp); + if (state == RADIOLIB_ERR_NONE) { + _txPower = power; + _txPowerSet = true; + } + return state; + } + + int16_t setFrequency(float freq) override { + int16_t state = LR1121::setFrequency(freq); + // PA selection depends on the band, and set radio / tempradio don't call setTxPower + if (state == RADIOLIB_ERR_NONE && _txPowerSet) state = setOutputPower(_txPower); + return state; + } + float getFreqMHz() const { return freqMHz; } int16_t setRxBoostedGainMode(bool en) { diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h index af6526e15..dd1294a17 100644 --- a/src/helpers/radiolib/CustomLR1121Wrapper.h +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -54,8 +54,17 @@ protected: if (!_board->prepareRadioFrequency(frequency_mhz)) return RADIOLIB_ERR_INVALID_FREQUENCY; auto* radio = static_cast(_radio); const int16_t status = radio->setFrequency(frequency_mhz); - return status == RADIOLIB_ERR_NONE - ? radio->setOutputPower(_dbm_valid ? _cur_dbm : LORA_TX_POWER, false, 304) : status; + if (status != RADIOLIB_ERR_NONE) return status; +#ifdef LR1121_FORCE_HP_PA + const bool force_hp = true; +#else + const bool force_hp = false; +#endif + // Preserve the Morse ramp while honoring HP-only wiring and the HF PA's + // 13 dBm ceiling, just like the normal packet-power path. + int8_t clipped = _dbm_valid ? _cur_dbm : LORA_TX_POWER; + radio->checkOutputPower(clipped, &clipped, force_hp); + return radio->setOutputPower(clipped, force_hp, 304); } bool restoreCarrierWaveModem() override { if (_morse_rx_fsk_mode) { diff --git a/test/test_carrier_wave.py b/test/test_carrier_wave.py index d6d906f2a..d51965eb6 100644 --- a/test/test_carrier_wave.py +++ b/test/test_carrier_wave.py @@ -87,6 +87,12 @@ struct Chip { standby(); if (power_result) return power_result; power=value; rfo=force_rfo; ramp_time=4; return 0; } + int checkOutputPower(int8_t value, int8_t* clipped, bool force_hp) { + const int minimum=frequency>1000 ? -18 : force_hp || value>14 ? -9 : -17; + const int maximum=frequency>1000 ? 13 : force_hp || value>14 ? 22 : 14; + if (clipped) *clipped=valuemaximum ? maximum : value; + return valuemaximum ? -1 : 0; + } int setOutputPower(int8_t value, bool force_rfo, uint16_t ramp) { ++ramp_power_calls; const int result=setOutputPower(value,force_rfo); diff --git a/test/test_contact_import/test_contact_import.cpp b/test/test_contact_import/test_contact_import.cpp index fb50248e9..9b70f6033 100644 --- a/test/test_contact_import/test_contact_import.cpp +++ b/test/test_contact_import/test_contact_import.cpp @@ -76,6 +76,7 @@ public: bool forward_packets = false, reject_primary_groups = false; bool bind_secondary_groups = false, packet_group_search = false; uint32_t direct_forward_delay = 0; + uint8_t extra_acks = 0; unsigned group_admissions = 0, group_searches = 0, received_groups = 0; uint8_t last_search_origin = 0xFF; mesh::GroupChannel primary_group, secondary_group, received_group; @@ -88,6 +89,7 @@ public: bool canMutateContacts() const override { return writable; } bool allowPacketForward(const mesh::Packet*) override { return forward_packets; } uint32_t getDirectRetransmitDelay(const mesh::Packet*) override { return direct_forward_delay; } + uint8_t getExtraAckTransmitCount() const override { return extra_acks; } bool allowGroupPacketOnRadio(mesh::Packet* packet) override { ++group_admissions; // Stub only the application's policy, not the core admission/order path. @@ -500,6 +502,72 @@ TEST_F(ContactImport, GroupAdmissionBindsDirectRelayBeforeCrossProfileFanout) { EXPECT_EQ(relayed->path[0], 0x93); EXPECT_EQ(pool.getFreeCount(), 7); } +TEST_F(ContactImport, RelayedAckHonorsInitialDirectForwardDelay) { + chat.forward_packets = true; + chat.direct_forward_delay = 175; + mesh::Packet packet; + packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); + packet.setPathHashSizeAndCount(1, 2); + packet.path[0] = chat.self_id.pub_key[0]; + packet.path[1] = 0x93; + packet.payload_len = 4; + memset(packet.payload, 0x37, packet.payload_len); + chat.receive(packet); + ASSERT_EQ(pool.getOutboundTotal(), 1); + EXPECT_EQ(pool.getOutboundCount(174), 0); + EXPECT_EQ(pool.getOutboundCount(175), 1); + EXPECT_EQ(pool.getOutboundByIdx(0)->getPathHashCount(), 1); + EXPECT_EQ(pool.getOutboundByIdx(0)->path[0], 0x93); + EXPECT_FALSE(pool.getOutboundByIdx(0)->radio_reply); +} + +TEST_F(ContactImport, ExtraForwardedAcksKeepInitialDelayAndThreeHundredMsSpacing) { + chat.forward_packets = true; + chat.direct_forward_delay = 175; + chat.extra_acks = 3; + mesh::Packet packet; + packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); + packet.setPathHashSizeAndCount(1, 1); + packet.path[0] = chat.self_id.pub_key[0]; + packet.payload_len = 4; + memset(packet.payload, 0x47, packet.payload_len); + chat.receive(packet); + ASSERT_EQ(pool.getOutboundTotal(), 4); + for (uint32_t index = 0; index < 4; ++index) { + const uint32_t due = 175 + 300 * index; + EXPECT_EQ(pool.getNextOutbound(due - 1), nullptr); + auto* ack = pool.getNextOutbound(due); + ASSERT_NE(ack, nullptr); + EXPECT_EQ(ack->getPayloadType(), index < 3 ? PAYLOAD_TYPE_MULTIPART : PAYLOAD_TYPE_ACK); + EXPECT_EQ(ack->getPathHashCount(), 0); + EXPECT_FALSE(ack->radio_reply); + pool.free(ack); + } + EXPECT_EQ(pool.getFreeCount(), 8); +} + +TEST_F(ContactImport, MultipartAckAddsInitialDelayWithoutLosingDuplicateProtection) { + chat.forward_packets = true; + chat.direct_forward_delay = 175; + tables.deduplicate = true; + mesh::Packet packet; + packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); + packet.setPathHashSizeAndCount(1, 2); + packet.path[0] = chat.self_id.pub_key[0]; + packet.path[1] = 0x93; + packet.payload_len = 5; + packet.payload[0] = (2 << 4) | PAYLOAD_TYPE_ACK; + memset(packet.payload + 1, 0x57, packet.payload_len - 1); + const auto duplicate = packet; + chat.receive(packet); + ASSERT_EQ(pool.getOutboundTotal(), 1); + EXPECT_EQ(pool.getOutboundCount(1074), 0); + EXPECT_EQ(pool.getOutboundCount(1075), 1); + auto second = duplicate; + chat.receive(second); + EXPECT_EQ(pool.getOutboundTotal(), 1); + EXPECT_EQ(tables.seen_marks, 1u); +} } // namespace int main(int argc, char** argv) { diff --git a/test/test_lr1121_power_merge.py b/test/test_lr1121_power_merge.py new file mode 100644 index 000000000..9c956a598 --- /dev/null +++ b/test/test_lr1121_power_merge.py @@ -0,0 +1,182 @@ +"""Execute merged LR1121 band/power and Morse PA selection without hardware.""" + +from pathlib import Path +import os +import shutil +import subprocess +import tempfile +import unittest + +from test_radio_receive_contract import method + + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include +#include +constexpr int16_t RADIOLIB_ERR_NONE=0; +constexpr int16_t RADIOLIB_ERR_INVALID_FREQUENCY=-705; +constexpr int8_t LORA_TX_POWER=22; +class LR1121 { + public: + float frequency=909.5f; + int power_calls=0,frequency_calls=0,checks=0,overload_calls=0; + int16_t power_result=0,frequency_result=0; + int8_t applied_power=0; + bool forced_hp=false; + uint32_t applied_ramp=0; + virtual ~LR1121()=default; + virtual int16_t setOutputPower(int8_t power) { return setOutputPower(power,false); } + int16_t setOutputPower(int8_t power,bool hp,uint32_t ramp=48) { + ++power_calls;applied_power=power;forced_hp=hp;applied_ramp=ramp;return power_result; + } + int16_t checkOutputPower(int8_t power,int8_t* clipped,bool hp) { + ++checks; + const int minimum=frequency>1000 ? -18 : hp || power>14 ? -9 : -17; + const int maximum=frequency>1000 ? 13 : hp || power>14 ? 22 : 14; + if(clipped) *clipped=std::max(minimum,std::min(maximum,int(power))); + return powermaximum ? -1 : 0; + } + virtual int16_t setFrequency(float value) { + ++frequency_calls;if(frequency_result==0)frequency=value;return frequency_result; + } + int16_t setFrequency(float value,bool,float=4) { + ++overload_calls;return LR1121::setFrequency(value); + } +}; +class CustomLR1121:public LR1121 { + int8_t _txPower=0; + bool _txPowerSet=false; + public: + using LR1121::setOutputPower; + using LR1121::setFrequency; + @POWER@ + @FREQUENCY@ +}; +struct Board { + bool frequency_ok=true; + bool prepareRadioFrequency(float) { return frequency_ok; } +}; +struct CarrierBase { + virtual ~CarrierBase()=default; + virtual int16_t prepareMorseCarrier(float)=0; + virtual bool restoreCarrierWaveModem()=0; +}; +class MorseWrapper:public CarrierBase { + public: + LR1121* _radio; + Board* _board; + bool _dbm_valid=true,_morse_rx_fsk_mode=false; + int8_t _cur_dbm=22; + float _cur_freq=909.5f; + MorseWrapper(LR1121* radio,Board* board):_radio(radio),_board(board) {} + bool restoreAfterDeepInit() { return true; } + @CARRIER@ + @RESTORE@ +}; +int main() { +#ifdef LR1121_FORCE_HP_PA + const bool expected_hp=true; +#else + const bool expected_hp=false; +#endif + CustomLR1121 radio; + assert(radio.setFrequency(909.5f)==0 && radio.power_calls==0); + assert(radio.setOutputPower(22)==0 && radio.applied_power==22); + assert(radio.forced_hp==expected_hp); + assert(radio.setFrequency(2400.0f)==0 && radio.applied_power==13); + assert(radio.setFrequency(909.5f)==0 && radio.applied_power==22); + // A failed explicit power write must not replace the last successful intent. + radio.power_result=-77; + assert(radio.setOutputPower(7)==-77); + radio.power_result=0; + assert(radio.setFrequency(2400.0f)==0 && radio.applied_power==13); + assert(radio.setFrequency(909.5f)==0 && radio.applied_power==22); + // A failed tune must not touch the PA. A failed PA reapply must propagate. + const int calls=radio.power_calls; + radio.frequency_result=-78; + assert(radio.setFrequency(2400.0f)==-78 && radio.power_calls==calls); + radio.frequency_result=0;radio.power_result=-79; + assert(radio.setFrequency(2400.0f)==-79); + radio.power_result=0; + assert(radio.setFrequency(909.5f)==0 && radio.applied_power==22); + assert(radio.setOutputPower(2)==0 && radio.forced_hp==expected_hp); + // The existing explicit calibration/ramp overloads used by Morse stay visible. + assert(radio.setFrequency(902.1f,true)==0 && radio.overload_calls==1); + assert(radio.setOutputPower(2,false,304)==0); + Board board; + MorseWrapper morse(&radio,&board); + morse._cur_dbm=2; + assert(morse.prepareMorseCarrier(909.5f)==0); + assert(radio.applied_power==2 && radio.forced_hp==expected_hp && radio.applied_ramp==304); + morse._cur_dbm=22; + assert(morse.prepareMorseCarrier(2400.0f)==0); + assert(radio.applied_power==13 && radio.applied_ramp==304); + assert(morse.restoreCarrierWaveModem()); + assert(radio.frequency==909.5f && radio.applied_power==22 && radio.forced_hp==expected_hp); + morse._dbm_valid=false; + assert(morse.prepareMorseCarrier(2400.0f)==0 && radio.applied_power==13); + assert(morse.restoreCarrierWaveModem() && radio.applied_power==22); + board.frequency_ok=false; + const int tunes=radio.frequency_calls; + assert(morse.prepareMorseCarrier(2400.0f)==RADIOLIB_ERR_INVALID_FREQUENCY); + assert(radio.frequency_calls==tunes); + board.frequency_ok=true;radio.frequency_result=-80; + assert(morse.prepareMorseCarrier(2400.0f)==-80); + assert(!morse.restoreCarrierWaveModem()); + radio.frequency_result=0;radio.power_result=-81; + assert(morse.prepareMorseCarrier(2400.0f)==-81); +} +''' + + +class UpstreamLR1121Test(unittest.TestCase): + def compile(self, *flags): + wrapper = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text() + source = HARNESS.replace("@POWER@", method(wrapper, "int16_t setOutputPower(int8_t power)")) + source = source.replace("@FREQUENCY@", method(wrapper, "int16_t setFrequency(float freq)")) + carrier = (ROOT / "src/helpers/radiolib/CustomLR1121Wrapper.h").read_text() + source = source.replace("@CARRIER@", method(carrier, "int16_t prepareMorseCarrier(")) + source = source.replace("@RESTORE@", method(carrier, "bool restoreCarrierWaveModem(")) + compiler = os.environ.get("CXX") or shutil.which("g++") + self.assertIsNotNone(compiler) + with tempfile.TemporaryDirectory(prefix="upstream-lr1121-") as directory: + folder = Path(directory) + source_file = folder / "test.cpp" + source_file.write_text(source) + binary = folder / ("test.exe" if os.name == "nt" else "test") + sanitizers = [] if os.name == "nt" else [ + "-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] + built = subprocess.run([ + compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", *sanitizers, + *flags, str(source_file), "-o", str(binary) + ], capture_output=True, text=True, timeout=60) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15) + self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + def test_band_changes_restore_original_power_and_propagate_failures(self): + self.compile() + + def test_hp_only_boards_keep_force_high_power(self): + self.compile("-DLR1121_FORCE_HP_PA=1") + + def test_merge_preserves_morse_switch_and_bounded_companion_ui(self): + radio = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text() + self.assertIn("LR11x0MorseRfSwitch _morse_rf_switch;", radio) + self.assertIn("_morse_rf_switch.capture", method(radio, "void setRfSwitchTable(")) + self.assertIn("_morse_rf_switch.restore", method(radio, "int16_t beginMorseReceive(")) + self.assertIn("using LR1121::setOutputPower;", radio) + self.assertIn("using LR1121::setFrequency;", radio) + ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text() + self.assertIn("makeLargeCompanionHomeLayout", ui) + self.assertIn("drawTextCenteredEllipsized", ui) + self.assertIn("isCompanionWiFiConnected()", ui) + self.assertIn("shouldDisplayBluetoothPairingPin", ui) + self.assertNotIn('display.drawTextCentered(display.width() / 2, 32, tmp)', ui) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_upstream_power_policy_merge.py b/test/test_upstream_power_policy_merge.py new file mode 100644 index 000000000..abd9c472f --- /dev/null +++ b/test/test_upstream_power_policy_merge.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Keep chemistry-aware nRF52 safety through upstream bootlock-stopgap merges. + +This parses the actual recipes without PlatformIO and executes the production +voltage rules on the host. It does not claim physical ADC/low-voltage testing. +""" +import configparser +from pathlib import Path +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +BOARDS = { + "gat562_30s_mesh_kit": "GAT562_30S_Mesh_Kit", + "gat562_mesh_evb_pro": "GAT562_Mesh_EVB_Pro", + "gat562_mesh_tracker_pro": "GAT562_Mesh_Tracker_Pro", + "gat562_mesh_watch13": "GAT562_Mesh_Watch13", + "heltec_t096": "Heltec_t096", + "heltec_t114": "Heltec_t114", + "muziworks_r1_neo": "R1Neo", + "rak3401": "rak3401", + "rak4631": "rak4631", + "sensecap_solar": "SenseCap_Solar", + "xiao_nrf52": "Xiao_nrf52", +} + + +class Recipes: + def __init__(self): + self.config = configparser.ConfigParser(interpolation=None, + inline_comment_prefixes=(";",)) + self.config.read([ROOT / "platformio.ini", + *sorted(ROOT.glob("variants/*/platformio.ini"))]) + + def value(self, section, option, seen=()): + pair = (section, option) + if pair in seen: + raise AssertionError(f"recipe interpolation cycle: {pair}") + seen = (*seen, pair) + if not self.config.has_option(section, option): + for parent in self.config.get(section, "extends", fallback="").split(","): + if parent.strip(): + value = self.value(parent.strip(), option, seen) + if value: + return value + return "" + raw = self.config.get(section, option) + return re.sub(r"\$\{([^}.]+)\.([^}]+)\}", + lambda match: self.value(match[1], match[2], seen), raw) + + +class UpstreamPowerPolicyMergeTests(unittest.TestCase): + def test_all_eleven_board_bases_keep_chemistry_power_policy(self): + recipes = Recipes() + for directory, section in BOARDS.items(): + with self.subTest(board=directory): + self.assertTrue(recipes.config.has_section(section)) + flags = recipes.value(section, "build_flags") + directives = re.findall( + r"(?:^|\s)-([DU])\s*NRF52_POWER_MANAGEMENT(?=$|\s|=)", flags) + self.assertTrue(directives, f"{directory}: voltage policy compiled out") + self.assertEqual(directives[-1], "D", f"{directory}: policy later undefined") + self.assertNotIn("NRF52_POWER_MANAGEMENT", + recipes.value(section, "build_unflags")) + + def test_affected_board_roles_inherit_chemistry_policy(self): + recipes = Recipes() + roles = 0 + for directory in BOARDS: + local = configparser.ConfigParser(interpolation=None) + local.read(ROOT / "variants" / directory / "platformio.ini") + for section in local.sections(): + if not section.startswith("env:"): + continue + with self.subTest(role=section): + flags = recipes.value(section, "build_flags") + directives = re.findall( + r"(?:^|\s)-([DU])\s*NRF52_POWER_MANAGEMENT(?=$|\s|=)", flags) + self.assertTrue(directives) + self.assertEqual(directives[-1], "D") + self.assertNotIn("NRF52_POWER_MANAGEMENT", + recipes.value(section, "build_unflags")) + roles += 1 + self.assertGreater(roles, 30) + + def test_voltage_policy_still_loads_saved_chemistry_and_uses_safe_adc(self): + board = (ROOT / "src/helpers/NRF52Board.cpp").read_text() + policy = (ROOT / "src/helpers/NRF52VoltagePolicy.cpp").read_text() + header = (ROOT / "src/helpers/NRF52VoltagePolicy.h").read_text() + self.assertIn("defined(NRF52_POWER_MANAGEMENT)", header) + self.assertIn("handleVoltagePolicyCommand", header) + self.assertIn("loadVoltagePolicyAtBoot(config->voltage_bootlock)", board) + self.assertIn("medianVoltage(samples[0], samples[1], samples[2])", board) + self.assertIn("if (threshold == 0 || external) return true;", board) + self.assertIn("lifepo4", policy) + self.assertIn("configuredBootlock()", policy) + self.assertIn("configuredCutoff()", policy) + self.assertIn("AtomicFileWriter", policy) + + def test_native_voltage_rules_preserve_lifepo4_cutoff_and_boot_validation(self): + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if not compiler: + self.skipTest("a host C++17 compiler is required") + source = r''' +#include +#include +#include +#include +int main() { + using namespace mesh::power; + static_assert(std::is_same::value, "boot API"); + static_assert(std::is_same::value, "cutoff API"); + assert(validThresholdPair(3300, 0, false)); + assert(validThresholdPair(3000, 2700, false)); + assert(!validThresholdPair(0, 0, false)); + assert(validThresholdPair(0, 0, true)); + assert(!validThresholdPair(3000, 3100, false)); + assert(batteryPercent(3550, 2700, 3550) == 100); + assert(batteryPercent(2700, 2700, 3550) == 0); + assert(medianVoltage(999, 3500, 3510) == 3500); + assert(!shouldBootLock(999, 3300, false)); + assert(!shouldBootLock(3000, 3300, true)); + assert(shouldBootLock(3000, 3300, false)); + assert(nextLowVoltageCount(2, 2600, 2700, false) == 3); + assert(nextLowVoltageCount(2, 2600, 2700, true) == 0); +} +''' + with tempfile.TemporaryDirectory(prefix="meshcore-power-merge-") as directory: + cpp = Path(directory) / "test.cpp" + binary = Path(directory) / "test.exe" + cpp.write_text(source) + sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", + "-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []) + built = subprocess.run( + [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", *sanitizers, + "-DNRF52_PLATFORM=1", "-DNRF52_POWER_MANAGEMENT=1", + f"-I{ROOT / 'src'}", str(cpp), "-o", str(binary)], + capture_output=True, text=True) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + run = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(run.returncode, 0, run.stdout + run.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_upstream_routing_merge.py b/test/test_upstream_routing_merge.py new file mode 100644 index 000000000..e3c1ee08f --- /dev/null +++ b/test/test_upstream_routing_merge.py @@ -0,0 +1,77 @@ +"""Check upstream zero-scope semantics without dropping fork safety rules.""" +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from test_replay_reset_integration import extract_braced + +ROOT = Path(__file__).resolve().parents[1] + + +class UpstreamRoutingMergeTests(unittest.TestCase): + def test_zero_transport_code_matches_wildcard_without_key_lookup(self): + method = extract_braced((ROOT / "src/helpers/RegionMap.cpp").read_text(), + "RegionEntry* RegionMap::findMatch(") + compiler = shutil.which("g++") or shutil.which("c++") + if not compiler: + self.skipTest("native C++ compiler required") + source = r''' +#include +#include +#include +namespace mesh { struct Packet { uint16_t transport_codes[2] = {}; }; } +struct RegionEntry { uint8_t flags; }; +struct TransportKey { + uint16_t code = 0x1234; + uint16_t calcTransportCode(const mesh::Packet*) const { return code; } +}; +class RegionMap { +public: + RegionEntry wildcard{0}, regions[1]{{0}}; + int num_regions = 1, lookups = 0; + int getTransportKeysFor(const RegionEntry&, TransportKey keys[], int) { + ++lookups; keys[0].code = 0x1234; return 1; + } + RegionEntry* findMatch(const mesh::Packet*, uint8_t); +}; +@METHOD@ +int main() { + RegionMap regions; + mesh::Packet packet; + const mesh::Packet* input = &packet; + assert(regions.findMatch(input, 1) == ®ions.wildcard); + assert(regions.lookups == 0); + packet.transport_codes[1] = 0x9999; + assert(regions.findMatch(input, 1) == ®ions.wildcard); + packet.transport_codes[0] = 0x1234; + assert(regions.findMatch(input, 1) == ®ions.regions[0]); + regions.regions[0].flags = 1; + assert(regions.findMatch(input, 1) == nullptr); + regions.regions[0].flags = 0; + packet.transport_codes[0] = 0x9999; + assert(regions.findMatch(input, 1) == nullptr); +} +'''.replace("@METHOD@", method) + with tempfile.TemporaryDirectory(prefix="mesh-upstream-routing-") as directory: + cpp = Path(directory) / "routing.cpp" + exe = Path(directory) / "routing" + cpp.write_text(source) + subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", + str(cpp), "-o", str(exe)], check=True, timeout=30, + capture_output=True) + subprocess.run([str(exe)], check=True, timeout=10, capture_output=True) + + def test_room_repeat_deprecation_keeps_full_rules_and_clock_sync(self): + source = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text() + handler = extract_braced(source, "void MyMesh::handleCommand(") + self.assertIn('strcmp(command, "set repeat on") == 0', handler) + self.assertIn('"ERR - repeat now deprecated"', handler) + self.assertIn("flood_rules.handleCommand(command, reply)", handler) + self.assertIn("_clock_sync.handleCommand(command, reply)", handler) + self.assertIn("_local_cli_output.owns", handler) + + +if __name__ == "__main__": + unittest.main() diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 1b27dac2c..6e396a06a 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -313,12 +313,14 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -517,12 +519,14 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE +; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index f12ef764c..8d33a0ae2 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -487,12 +487,14 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=512 + -D ENABLE_WIFI_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index bb8e210de..0953b9973 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -243,10 +243,12 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=512 + -D ENABLE_WIFI_INTERFACE build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 32c74eb37..951946a68 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -52,11 +52,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=E213Display -D BLE_PIN_CODE=123456 ; dynamic, random PIN - -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE +; -D BLE_DEBUG_LOGGING=1 build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 30bbc3a5c..f7ef536ba 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - TImpulsePlusBoard() : NRF52Board("T-Impulse-Plus OTA") {} + TImpulsePlusBoard() : NRF52Board("T-Impulse-Plus_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h index ba42d6736..e3f5d4dbe 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - MeshtinyBoard() : NRF52Board("Meshtiny OTA") {} + MeshtinyBoard() : NRF52Board("Meshtiny_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 02f01a64d..8341aa083 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -266,12 +266,14 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; dynamic, random PIN -D AUTO_SHUTDOWN_MILLIVOLTS=3400 - -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE +; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h b/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h new file mode 100644 index 000000000..8307ba505 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class WaveshareS3LR1121Board : public ESP32Board { +public: + const char* getManufacturerName() const override { + return "Waveshare ESP32-S3-LR1121"; + } +}; diff --git a/variants/waveshare_esp32s3_lr1121/platformio.ini b/variants/waveshare_esp32s3_lr1121/platformio.ini new file mode 100644 index 000000000..8e7b52d4f --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/platformio.ini @@ -0,0 +1,102 @@ +; Waveshare ESP32-S3-LR1121 (HF: 850-930 MHz + 2.4 GHz) +; ESP32-S3FH4R2 (4MB flash, 2MB PSRAM), native USB-CDC via the Tiny-Adapter FPC. +; No user LED, battery sense or user button (BOOT is GPIO0). +[Waveshare_S3_LR1121] +extends = esp32_base +board = esp32-s3-zero +; min_spiffs: 1.9MB OTA app slots; one layout for every role so switching roles keeps the filesystem +board_build.partitions = min_spiffs.csv +build_flags = ${esp32_base.build_flags} + -I variants/waveshare_esp32s3_lr1121 + -D BOARD_HAS_PSRAM + -D P_LORA_DIO_1=38 + -D P_LORA_NSS=42 + -D P_LORA_RESET=39 + -D P_LORA_BUSY=41 + -D P_LORA_SCLK=40 + -D P_LORA_MISO=46 + -D P_LORA_MOSI=45 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D LR11X0_DIO3_TCXO_VOLTAGE=1.8 + -D LR1121_FORCE_HP_PA ; RFO_LP_LF is not fitted (R4 NC) + -D RX_BOOSTED_GAIN=true + -D LORA_TX_POWER=22 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/waveshare_esp32s3_lr1121> +lib_deps = + ${esp32_base.lib_deps} + +[env:Waveshare_S3_LR1121_repeater] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D ADVERT_NAME='"Waveshare LR1121 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + ${esp32_ota.lib_deps} + +[env:Waveshare_S3_LR1121_room_server] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D ADVERT_NAME='"Waveshare LR1121 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + ${esp32_ota.lib_deps} + +[env:Waveshare_S3_LR1121_companion_radio_usb] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Waveshare_S3_LR1121_companion_radio_ble] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Waveshare_S3_LR1121_kiss_modem] +extends = Waveshare_S3_LR1121 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/waveshare_esp32s3_lr1121/target.cpp b/variants/waveshare_esp32s3_lr1121/target.cpp new file mode 100644 index 000000000..56775c4ce --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/target.cpp @@ -0,0 +1,63 @@ +#include +#include "target.h" + +WaveshareS3LR1121Board board; + +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +// DIO5/DIO6 drive the RTC6603SP sub-GHz T/R switch; the 2.4 GHz port (RFIO_HF) is unswitched +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, +}; + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, + LORA_TX_POWER, 16, LR11X0_DIO3_TCXO_VOLTAGE); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; + } + + radio.setCRC(2); + radio.explicitHeader(); + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/waveshare_esp32s3_lr1121/target.h b/variants/waveshare_esp32s3_lr1121/target.h new file mode 100644 index 000000000..461509e51 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/target.h @@ -0,0 +1,17 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include "WaveshareS3LR1121Board.h" + +extern WaveshareS3LR1121Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index 400d01782..d8a17829c 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -11,7 +11,7 @@ protected: KeyValueStore* _prefs = NULL; public: - WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + WioTrackerL1Board() : NRF52Board("WioTrackerL1_OTA") {} void begin(); void attachDynamicPrefs(KeyValueStore* prefs); bool setLoRaFemPaGainEnabled(bool enable) override; diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h index 1b1f8040b..9dbe85642 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + WioTrackerL1Board() : NRF52Board("WioTrackerL1_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index 232bc1693..0dc9ba618 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -100,12 +100,14 @@ build_flags = -D BLE_PIN_CODE=123456 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 1be7f0318..ee6b7f376 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -333,12 +333,14 @@ build_flags = -D BLE_PIN_CODE=123456 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp>