Compare commits

...

54 Commits

Author SHA1 Message Date
Matthias Wientapper
29aa1c3795 rak4631: set lockout voltage to 0V 2026-03-05 16:26:19 +01:00
Matthias Wientapper
38d78041f7 set default: disable flood adverts 2026-03-05 16:26:19 +01:00
Matthias Wientapper
aaa08cb330 Integration of upstrem PR #1810 2026-03-05 16:26:19 +01:00
Matthias Wientapper
15daafff22 Integration of upstrem PR #1338 2026-03-05 16:26:17 +01:00
Matthias Wientapper
8827988842 Integration of upstrem PR #1297 2026-03-05 16:26:16 +01:00
mattzzw
853ee62eb0 Update examples/simple_room_server/MyMesh.cpp
Fix pow() and rand() are calculated for every forwarded packet, but we only need it for flood advert. Also fixes 'paket' typo.

Co-authored-by: Wessel <wessel@weebl.me>
2026-03-05 11:32:47 +01:00
mattzzw
7af6941220 Update examples/simple_repeater/MyMesh.cpp
Fix pow() and rand() are calculated for every forwarded packet, but we only need it for flood advert. Also fixes 'paket' typo.

Co-authored-by: Wessel <wessel@weebl.me>
2026-03-05 11:32:47 +01:00
mattzzw
a678c4a9e6 Update src/helpers/CommonCLI.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-03-05 11:32:47 +01:00
Matthias Wientapper
e24b5ff263 Add cli config flood.advert.base
0 = forwarding flood adverts off
1 = forwarding flood adverts on (unrestricted)
0.308 (default) = prob. forwarding according to #1338
2026-03-05 11:32:47 +01:00
Matthias Wientapper
6209ad12ce Limit flood advert packet forwarding for roomservers as well 2026-03-05 11:32:47 +01:00
Matthias Wientapper
971b3f414f Limit flood advert packet forwarding, implements #1223 2026-03-05 11:31:43 +01:00
Scott Powell
5684b4f1b8 * LOOP_DETECT_MODERATE bug fix 2026-03-05 17:38:51 +11:00
Scott Powell
e233346bf0 * repeater: new "get/set loop.detect {off | minimal | moderate | strict }" 2026-03-05 16:26:09 +11:00
Liam Cottle
3031deb980 Merge pull request #1811 from robekl/docs_update_cli_commands
docs: correct CLI command forms and targeted typos
2026-03-05 15:31:32 +13:00
Liam Cottle
dc9e7abacc Merge pull request #1490 from mesher-de/feature-0hop-cli
Add CLI-command for zerohop advert
2026-03-05 15:24:48 +13:00
Liam Cottle
044c66e261 Merge pull request #1001 from kallanreed/fix_gps_debug_logging
Use correct macro for GPS logging test
2026-03-05 13:14:37 +13:00
Liam Cottle
3e5522fcde Merge pull request #1901 from weebl2000/fix-ikoka-handheld-build
Fix ikoka handheld build
2026-03-04 15:54:17 +13:00
Wessel Nieboer
c0c9c17cf5 and buildscript 2026-03-03 15:37:33 +01:00
Wessel Nieboer
780720c117 and board! 2026-03-03 15:37:33 +01:00
Wessel Nieboer
90fcd1043f Fix ikoka handheld build
The extends was wrong.
2026-03-03 15:37:32 +01:00
Liam Cottle
cdd3d5f34e Merge pull request #1841 from weebl2000/nit-prefs
prefs is 5 char length :nerd:
2026-03-04 01:39:55 +13:00
ripplebiz
7c594ebc50 Merge pull request #1743 from weebl2000/fixagcreset
fix agc reset on SX126x, SX1276 & LR11x0 chips
2026-03-03 22:19:34 +11:00
Liam Cottle
ba3d9e264e Merge pull request #1836 from weebl2000/fix-rak3401-sky66122-11-fem
Fix RAK3401 SKY66122-11 FEM control: enable CSD/CPS for proper PA/LNA operation
2026-03-03 23:51:42 +13:00
Liam Cottle
d7ad89046b Merge pull request #1633 from weebl2000/fix/gps-uart-power-leak
Fix GPS UART consuming +8mA when disabled (nRF52)
2026-03-03 23:26:58 +13:00
Liam Cottle
67779aded8 Merge pull request #1600 from weebl2000/heltec_deep_sleep_lna
Hold GC1109 PA_POWER during deep sleep for LNA RX wake
2026-03-03 23:20:22 +13:00
Liam Cottle
bbd621ba85 Merge pull request #1351 from weebl2000/heltec_wireless_tracker_gps
Support GPS properly on Heltec Wireless Tracker v1.x
2026-03-03 23:11:46 +13:00
ripplebiz
6431cd2d47 Merge pull request #1900 from wbijen/feature/contact-filter-by-hops
Add configurable max hops filter for auto-add contacts
2026-03-03 21:05:44 +11:00
Wouter Bijen
1d190ad944 Clamp max_hops to 64 to cover full protocol hop range (0-63) 2026-03-03 09:05:53 +01:00
Wouter Bijen
2cb08775c0 Clarify comment wording: 1 = direct (0 hops)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:40:17 +01:00
Wouter Bijen
c016db86d5 Address PR review: subtract-1 encoding and clamp max_hops
- Change > to >= so stored value 1 means direct/0-hop only (liamcottle)
- Clamp max_hops to 63 on write since getPathHashCount() caps at 63 (robekl)
- Update comments to reflect encoding: 0=no limit, 1=direct only, N=up to N-1 hops

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:37:22 +01:00
Wouter Bijen
00566741f6 Add configurable max hops filter for auto-add contacts
Filter auto-add of new contacts by hop count (issues #1533, #1546).
Setting is configurable from the companion app via extended
CMD_SET/GET_AUTOADD_CONFIG protocol (0 = no limit, 1-63 = max hops).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:41:41 +01:00
Wessel Nieboer
8a9a0dca5f Fix GPS +8mA power leak when disabled (nRF52)
On the T114, GPS_RESET (pin 38) is the same pin as PIN_3V3_EN.
MicroNMEALocationProvider::begin() sets pin 38 HIGH (powering the 3V3
rail) but stop() never set it back LOW, leaving the GPS module powered
even when disabled.

Assert reset pin in stop() to mirror begin(), and guard
_location->loop() behind gps_active check.

Fixes meshcore-dev/MeshCore#1628
2026-02-28 19:13:42 +01:00
Wessel Nieboer
59d9770ab9 Add GPS support Heltec Wireless Tracker v1.x
Pin mapping verified against HTIT-Tracker V0.5 schematic:
- GPIO35 (GPS_EN): N-ch MOSFET drives P-ch high-side switch, active HIGH
- GPIO36 (GPS_RST): hardware reset, active LOW
- GPIO33/34: UART TX/RX

Delegates power management to MicroNMEALocationProvider begin()/stop()
which independently controls GPS power via GPS_EN and shares VEXT with
the display through RefCountedDigitalPin.
2026-02-28 19:09:28 +01:00
Wessel Nieboer
9bae9d0ed2 fix comment, we know the band now after checking LR1110 user manual 2026-02-28 19:09:25 +01:00
Wessel Nieboer
85f764a114 Calibrate configured frequency for AGC reset 2026-02-28 19:09:25 +01:00
Wessel Nieboer
f54948e06d Also implement LR11x10 AGC reset
Similar to SX126x but simpler.
2026-02-28 19:09:25 +01:00
Wessel Nieboer
b2032e11b6 make it more dry 2026-02-28 19:09:25 +01:00
Wessel Nieboer
9106ab46e1 reset noise_floor sampling after agc reset 2026-02-28 19:09:25 +01:00
Wessel Nieboer
a2dc2eb50c when doing AGC reset, call Calibrate(0x7F)
1. warm sleep
2. wake to stdby
3. Calibrate(0x7F) to reset all internal blocks
4. re-apply DIO2 RF / boosted gain & register patch to make sure
everything is as it was
2026-02-28 19:09:25 +01:00
Wessel Nieboer
f81ec4b14c fix agc reset 2026-02-28 19:09:25 +01:00
Wessel Nieboer
49d8313501 Fix pin mapping & TX switch (it's DIO2) 2026-02-28 19:09:06 +01:00
Wessel Nieboer
5a5568ed56 Drive CTX low first 2026-02-28 19:09:06 +01:00
Wessel Nieboer
ac2aa03b09 Add SX126X_REGISTER_PATCH for RAK3401 2026-02-28 19:09:06 +01:00
Wessel Nieboer
70f1ad4aeb Fix RAK3401 SKY66122-11 FEM control: enable CSD/CPS for proper PA and LNA operation
The RAK13302 1W module uses a Skyworks SKY66122-11 front-end module with
three digital control pins (CSD, CTX, CPS) that must be actively driven
by the host MCU. The previous code only managed CTX (GPIO 31) — toggling
it for TX/RX — but never initialized CSD (GPIO 24) or CPS (GPIO 21),
leaving them floating with no pull-up/pull-down resistors on the PCB.

With floating CSD and CPS, the SKY66122 was in an undefined operating
mode:
- The 30 dB TX PA may not have been reliably engaging
- The 16 dB RX LNA was never reliably active, degrading receive
sensitivity
2026-02-28 19:09:06 +01:00
Wessel Nieboer
d9e67222f5 prefs is 5 char length :nerd: 2026-02-28 19:07:38 +01:00
Wessel Nieboer
2bb6f636a4 Add 1ms delay after powering PA (cold-boot) 2026-02-28 19:05:31 +01:00
Wessel Nieboer
329e408197 Hold GC1109 PA_POWER during deep sleep for LNA RX wake
The GC1109 FEM needs its VFEM_Ctrl pin held HIGH during deep sleep
to keep the LNA active, enabling proper RX sensitivity for
wake-on-packet. Without this, the LNA is unpowered during sleep
and RX wake sensitivity is degraded by ~17dB.

Release RTC holds in begin() after configuring GPIO registers
(not before) to ensure glitch-free pin transitions on wake.

Trade-off: ~6.5mA additional sleep current for significantly
improved wake-on-packet range.
2026-02-28 19:05:31 +01:00
Robert Ekl
b43319d1a4 docs: correct CLI command forms and targeted typos 2026-02-23 18:53:20 -06:00
Stefan Berthold
f864c5f547 allow direct message paths when denyf * is set 2026-02-23 23:33:41 +01:00
mesher-de
8e404e9aea add advert.zerohop command to CLI 2026-02-17 13:56:18 +01:00
ViezeVingertjes
519b97a90a Updated the Dispatcher logic to replace hardcoded values with defined constants for minimum TX budget reserve and airtime division. 2026-02-07 19:07:33 +01:00
ViezeVingertjes
30d6588792 Update logic in Dispatcher to ensure refill is only applied when greater than zero. 2026-02-07 18:26:39 +01:00
ViezeVingertjes
eb4fa032ff Implement token bucket duty cycle enforcement 2026-01-04 21:33:46 +01:00
kallanreed
9e61b56e70 Use correct macro for logging test 2025-10-22 16:44:20 -07:00
40 changed files with 413 additions and 81 deletions

View File

@@ -51,7 +51,7 @@
- `time <epoch_seconds>` - `time <epoch_seconds>`
**Parameters:** **Parameters:**
- `epoc_seconds`: Unix epoc time - `epoch_seconds`: Unix epoch time
--- ---
@@ -134,7 +134,7 @@
--- ---
### End capture of rx log to node sotrage ### End capture of rx log to node storage
**Usage:** `log stop` **Usage:** `log stop`
--- ---
@@ -198,7 +198,7 @@
**Default:** Varies by board **Default:** Varies by board
**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Referr to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.** **Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.**
--- ---
@@ -228,6 +228,7 @@
**Default:** `869.525` **Default:** `869.525`
**Note:** Requires reboot to apply **Note:** Requires reboot to apply
**Serial Only:** `set freq <frequency>`
### System ### System
@@ -293,17 +294,16 @@
#### View or change this node's admin password #### View or change this node's admin password
**Usage:** **Usage:**
- `get password` - `password <new_password>`
- `set password <password>`
**Parameters:** **Parameters:**
- `password`: Admin password - `new_password`: New admin password
**Set by build flag:** `ADMIN_PASSWORD` **Set by build flag:** `ADMIN_PASSWORD`
**Default:** `password` **Default:** `password`
**Note:** Echoed back for confirmation **Note:** Command reply echoes the updated password for confirmation.
**Note:** Any node using this password will be added to the admin ACL list. **Note:** Any node using this password will be added to the admin ACL list.
@@ -768,7 +768,7 @@ region save
- `gps advert <policy>` - `gps advert <policy>`
**Parameters:** **Parameters:**
- `policy`: `none`|`shared`|`prefs` - `policy`: `none`|`share`|`prefs`
- `none`: don't include location in adverts - `none`: don't include location in adverts
- `share`: share gps location (from SensorManager) - `share`: share gps location (from SensorManager)
- `prefs`: location stored in node's lat and lon settings - `prefs`: location stored in node's lat and lon settings

View File

@@ -229,6 +229,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.close(); file.close();
} }
@@ -265,6 +266,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.close(); file.close();
} }

View File

@@ -318,6 +318,10 @@ bool MyMesh::shouldOverwriteWhenFull() const {
return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0; return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
} }
uint8_t MyMesh::getAutoAddMaxHops() const {
return _prefs.autoadd_max_hops;
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) { void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
if (_serial->isConnected()) { if (_serial->isConnected()) {
@@ -807,7 +811,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs)); memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half _prefs.airtime_factor = 1.0;
strcpy(_prefs.node_name, "NONAME"); strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ; _prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF; _prefs.sf = LORA_SF;
@@ -1785,12 +1789,16 @@ void MyMesh::handleCmdFrame(size_t len) {
} }
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
_prefs.autoadd_config = cmd_frame[1]; _prefs.autoadd_config = cmd_frame[1];
if (len >= 3) {
_prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)64);
}
savePrefs(); savePrefs();
writeOKFrame(); writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) { } else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
int i = 0; int i = 0;
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
out_frame[i++] = _prefs.autoadd_config; out_frame[i++] = _prefs.autoadd_config;
out_frame[i++] = _prefs.autoadd_max_hops;
_serial->writeFrame(out_frame, i); _serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) { } else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) {
int i = 0; int i = 0;

View File

@@ -119,6 +119,7 @@ protected:
bool isAutoAddEnabled() const override; bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override; bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override; bool shouldOverwriteWhenFull() const override;
uint8_t getAutoAddMaxHops() const override;
void onContactsFull() override; void onContactsFull() override;
void onContactOverwrite(const uint8_t* pub_key) override; void onContactOverwrite(const uint8_t* pub_key) override;
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;

View File

@@ -30,4 +30,5 @@ struct NodePrefs { // persisted to file
uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t client_repeat; uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
}; };

View File

@@ -396,6 +396,23 @@ File MyMesh::openAppend(const char *fname) {
#endif #endif
} }
static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 };
static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 };
static uint8_t max_loop_strict[] = { 0, /* 1-byte */ 1, /* 2-byte */ 1, /* 3-byte */ 1 };
bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) {
uint8_t hash_size = packet->getPathHashSize();
uint8_t hash_count = packet->getPathHashCount();
uint8_t n = 0;
const uint8_t* path = packet->path;
while (hash_count > 0) { // count how many times this node is already in the path
if (self_id.isHashMatch(path, hash_size)) n++;
hash_count--;
path += hash_size;
}
return n >= max_counters[hash_size];
}
bool MyMesh::allowPacketForward(const mesh::Packet *packet) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false; if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
@@ -403,6 +420,29 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false; return false;
} }
if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) {
const uint8_t* maximums;
if (_prefs.loop_detect == LOOP_DETECT_MINIMAL) {
maximums = max_loop_minimal;
} else if (_prefs.loop_detect == LOOP_DETECT_MODERATE) {
maximums = max_loop_moderate;
} else {
maximums = max_loop_strict;
}
if (isLooped(packet, maximums)) {
MESH_DEBUG_PRINTLN("allowPacketForward: FLOOD packet loop detected!");
return false;
}
}
// Limit flood advert paket forwarding using a probabilistic reduction defined by P(h) = 0.308^(hops-1)
// https://github.com/meshcore-dev/MeshCore/issues/1223
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->isRouteFlood()) {
double roll_dice = (double)rand() / RAND_MAX;
double forw_prob = pow(_prefs.flood_advert_base, packet->path_len - 1);
if (roll_dice > forw_prob)
return false;
}
return true; return true;
} }
@@ -506,7 +546,10 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { if ((pkt->getPayloadType() == PAYLOAD_TYPE_GRP_TXT ||
pkt->getPayloadType() == PAYLOAD_TYPE_GRP_DATA ||
pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT) &&
region_map.getWildcard().flags & REGION_DENY_FLOOD) {
recv_pkt_region = NULL; recv_pkt_region = NULL;
} else { } else {
recv_pkt_region = &region_map.getWildcard(); recv_pkt_region = &region_map.getWildcard();
@@ -824,7 +867,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs)); memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_tx_delay_factor = 0.3f; // was 0.2
@@ -838,7 +881,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.cr = LORA_CR; _prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER; _prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 12; // 12 hours _prefs.flood_advert_interval = 0; // 12 hours
_prefs.flood_advert_base = 0.308f;
_prefs.flood_max = 64; _prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled

View File

@@ -128,6 +128,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
mesh::Packet* createSelfAdvert(); mesh::Packet* createSelfAdvert();
File openAppend(const char* fname); File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
protected: protected:
float getAirtimeBudgetFactor() const override { float getAirtimeBudgetFactor() const override {

View File

@@ -276,7 +276,17 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
bool MyMesh::allowPacketForward(const mesh::Packet *packet) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false; if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
// Limit flood advert packet forwarding using a probabilistic reduction defined by P(h) = base^(hops-1)
// https://github.com/meshcore-dev/MeshCore/issues/1223
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->isRouteFlood()) {
double roll_dice = (double)rand() / RAND_MAX;
double forw_prob = pow(_prefs.flood_advert_base, packet->path_len - 1);
if (roll_dice > forw_prob)
return false;
}
return true; return true;
} }
@@ -599,7 +609,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs)); memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0 _prefs.rx_delay_base = 0.0f; // off by default, was 10.0
_prefs.tx_delay_factor = 0.5f; // was 0.25f; _prefs.tx_delay_factor = 0.5f; // was 0.25f;
_prefs.direct_tx_delay_factor = 0.2f; // was zero _prefs.direct_tx_delay_factor = 0.2f; // was zero
@@ -615,6 +625,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.disable_fwd = 1; _prefs.disable_fwd = 1;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 12; // 12 hours _prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_advert_base = 0.308f;
_prefs.flood_max = 64; _prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled
#ifdef ROOM_PASSWORD #ifdef ROOM_PASSWORD

View File

@@ -281,7 +281,7 @@ public:
{ {
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs)); memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 2.0; // one third _prefs.airtime_factor = 1.0;
strcpy(_prefs.node_name, "NONAME"); strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ; _prefs.freq = LORA_FREQ;
_prefs.tx_power_dbm = LORA_TX_POWER; _prefs.tx_power_dbm = LORA_TX_POWER;

View File

@@ -706,7 +706,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs)); memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.2f; // was zero _prefs.direct_tx_delay_factor = 0.2f; // was zero

View File

@@ -8,7 +8,9 @@
namespace mesh { namespace mesh {
#define MAX_RX_DELAY_MILLIS 32000 // 32 seconds #define MAX_RX_DELAY_MILLIS 32000 // 32 seconds
#define MIN_TX_BUDGET_RESERVE_MS 100 // min budget (ms) required before allowing next TX
#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX
#ifndef NOISE_FLOOR_CALIB_INTERVAL #ifndef NOISE_FLOOR_CALIB_INTERVAL
#define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds #define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds
@@ -20,12 +22,34 @@ void Dispatcher::begin() {
_err_flags = 0; _err_flags = 0;
radio_nonrx_start = _ms->getMillis(); radio_nonrx_start = _ms->getMillis();
duty_cycle_window_ms = getDutyCycleWindowMs();
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
tx_budget_ms = (unsigned long)(duty_cycle_window_ms * duty_cycle);
last_budget_update = _ms->getMillis();
_radio->begin(); _radio->begin();
prev_isrecv_mode = _radio->isInRecvMode(); prev_isrecv_mode = _radio->isInRecvMode();
} }
float Dispatcher::getAirtimeBudgetFactor() const { float Dispatcher::getAirtimeBudgetFactor() const {
return 2.0; // default, 33.3% (1/3rd) return 1.0;
}
void Dispatcher::updateTxBudget() {
unsigned long now = _ms->getMillis();
unsigned long elapsed = now - last_budget_update;
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long max_budget = (unsigned long)(getDutyCycleWindowMs() * duty_cycle);
unsigned long refill = (unsigned long)(elapsed * duty_cycle);
if (refill > 0) {
tx_budget_ms += refill;
if (tx_budget_ms > max_budget) {
tx_budget_ms = max_budget;
}
last_budget_update = now;
}
} }
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
@@ -61,11 +85,24 @@ void Dispatcher::loop() {
if (outbound) { // waiting for outbound send to be completed if (outbound) { // waiting for outbound send to be completed
if (_radio->isSendComplete()) { if (_radio->isSendComplete()) {
long t = _ms->getMillis() - outbound_start; long t = _ms->getMillis() - outbound_start;
total_air_time += t; // keep track of how much air time we are using total_air_time += t;
//Serial.print(" airtime="); Serial.println(t); //Serial.print(" airtime="); Serial.println(t);
// will need radio silence up to next_tx_time updateTxBudget();
next_tx_time = futureMillis(t * getAirtimeBudgetFactor());
if (t > tx_budget_ms) {
tx_budget_ms = 0;
} else {
tx_budget_ms -= t;
}
if (tx_budget_ms < MIN_TX_BUDGET_RESERVE_MS) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = MIN_TX_BUDGET_RESERVE_MS - tx_budget_ms;
next_tx_time = futureMillis((unsigned long)(needed / duty_cycle));
} else {
next_tx_time = _ms->getMillis();
}
_radio->onSendFinished(); _radio->onSendFinished();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
@@ -235,9 +272,20 @@ void Dispatcher::processRecvPacket(Packet* pkt) {
} }
void Dispatcher::checkSend() { void Dispatcher::checkSend() {
if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return; // nothing waiting to send if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return;
if (!millisHasNowPassed(next_tx_time)) return; // still in 'radio silence' phase (from airtime budget setting)
if (_radio->isReceiving()) { // LBT - check if radio is currently mid-receive, or if channel activity updateTxBudget();
uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms;
next_tx_time = futureMillis((unsigned long)(needed / duty_cycle));
return;
}
if (!millisHasNowPassed(next_tx_time)) return;
if (_radio->isReceiving()) {
if (cad_busy_start == 0) { if (cad_busy_start == 0) {
cad_busy_start = _ms->getMillis(); // record when CAD busy state started cad_busy_start = _ms->getMillis(); // record when CAD busy state started
} }

View File

@@ -122,8 +122,12 @@ class Dispatcher {
bool prev_isrecv_mode; bool prev_isrecv_mode;
uint32_t n_sent_flood, n_sent_direct; uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct; uint32_t n_recv_flood, n_recv_direct;
unsigned long tx_budget_ms;
unsigned long last_budget_update;
unsigned long duty_cycle_window_ms;
void processRecvPacket(Packet* pkt); void processRecvPacket(Packet* pkt);
void updateTxBudget();
protected: protected:
PacketManager* _mgr; PacketManager* _mgr;
@@ -142,6 +146,9 @@ protected:
_err_flags = 0; _err_flags = 0;
radio_nonrx_start = 0; radio_nonrx_start = 0;
prev_isrecv_mode = true; prev_isrecv_mode = true;
tx_budget_ms = 0;
last_budget_update = 0;
duty_cycle_window_ms = 3600000;
} }
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
@@ -159,6 +166,7 @@ protected:
virtual uint32_t getCADFailMaxDuration() const; virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
public: public:
void begin(); void begin();
@@ -168,8 +176,9 @@ public:
void releasePacket(Packet* packet); void releasePacket(Packet* packet);
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds unsigned long getTotalAirTime() const { return total_air_time; }
unsigned long getReceiveAirTime() const {return rx_air_time; } unsigned long getReceiveAirTime() const {return rx_air_time; }
unsigned long getRemainingTxBudget() const { return tx_budget_ms; }
uint32_t getNumSentFlood() const { return n_sent_flood; } uint32_t getNumSentFlood() const { return n_sent_flood; }
uint32_t getNumSentDirect() const { return n_sent_direct; } uint32_t getNumSentDirect() const { return n_sent_direct; }
uint32_t getNumRecvFlood() const { return n_recv_flood; } uint32_t getNumRecvFlood() const { return n_recv_flood; }

View File

@@ -141,6 +141,15 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
return; return;
} }
// check hop limit for new contacts (0 = no limit, 1 = direct (0 hops), N = up to N-1 hops)
uint8_t max_hops = getAutoAddMaxHops();
if (max_hops > 0 && packet->getPathHashCount() >= max_hops) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
return;
}
from = allocateContactSlot(); from = allocateContactSlot();
if (from == NULL) { if (from == NULL) {
ContactInfo ci; ContactInfo ci;

View File

@@ -98,6 +98,7 @@ protected:
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; } virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {}; virtual void onContactsFull() {};
virtual bool shouldOverwriteWhenFull() const { return false; } virtual bool shouldOverwriteWhenFull() const { return false; }
virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops
virtual void onContactOverwrite(const uint8_t* pub_key) {}; virtual void onContactOverwrite(const uint8_t* pub_key) {};
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0; virtual ContactInfo* processAck(const uint8_t *data) = 0;

View File

@@ -64,7 +64,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
file.read(pad, 2); // 122 file.read((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
file.read(pad, 1); // 123
file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -82,7 +83,9 @@ 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->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->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290 file.read((uint8_t *)&_prefs->flood_advert_base, sizeof(_prefs->flood_advert_base)); // 290
// 294
// sanitise bad pref values // sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -110,6 +113,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
_prefs->flood_advert_base = constrain(_prefs->flood_advert_base, 0, 1);
file.close(); file.close();
} }
} }
@@ -150,7 +155,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
file.write(pad, 2); // 122 file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
file.write(pad, 1); // 123
file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -168,7 +174,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 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->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290 file.write((uint8_t *)&_prefs->flood_advert_base, sizeof(_prefs->flood_advert_base)); // 290
// 294
file.close(); file.close();
} }
@@ -203,6 +211,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
// Reset clock // Reset clock
getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm
_board->reboot(); // doesn't return _board->reboot(); // doesn't return
} else if (memcmp(command, "advert.zerohop", 14) == 0 && (command[14] == 0 || command[14] == ' ')) {
// send zerohop advert
_callbacks->sendSelfAdvertisement(1500, false); // longer delay, give CLI response time to be sent first
strcpy(reply, "OK - zerohop advert sent");
} else if (memcmp(command, "advert", 6) == 0) { } else if (memcmp(command, "advert", 6) == 0) {
// send flood advert // send flood advert
_callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first _callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first
@@ -330,6 +342,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
*reply = 0; // set null terminator *reply = 0; // set null terminator
} else if (memcmp(config, "path.hash.mode", 14) == 0) { } else if (memcmp(config, "path.hash.mode", 14) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
} else if (memcmp(config, "loop.detect", 11) == 0) {
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
strcpy(reply, "> off");
} else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
strcpy(reply, "> minimal");
} else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
strcpy(reply, "> moderate");
} else {
strcpy(reply, "> strict");
}
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) { } else if (memcmp(config, "freq", 4) == 0) {
@@ -385,6 +407,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else { } else {
sprintf(reply, "> %.3f", adc_mult); sprintf(reply, "> %.3f", adc_mult);
} }
} else if (memcmp(config, "flood.advert.base", 17) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->flood_advert_base));
// Power management commands // Power management commands
} else if (memcmp(config, "pwrmgt.support", 14) == 0) { } else if (memcmp(config, "pwrmgt.support", 14) == 0) {
#ifdef NRF52_POWER_MANAGEMENT #ifdef NRF52_POWER_MANAGEMENT
@@ -571,6 +595,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else { } else {
strcpy(reply, "Error, must be 0,1, or 2"); strcpy(reply, "Error, must be 0,1, or 2");
} }
} else if (memcmp(config, "loop.detect ", 12) == 0) {
config += 12;
uint8_t mode;
if (memcmp(config, "off", 3) == 0) {
mode = LOOP_DETECT_OFF;
} else if (memcmp(config, "minimal", 7) == 0) {
mode = LOOP_DETECT_MINIMAL;
} else if (memcmp(config, "moderate", 8) == 0) {
mode = LOOP_DETECT_MODERATE;
} else if (memcmp(config, "strict", 6) == 0) {
mode = LOOP_DETECT_STRICT;
} else {
mode = 0xFF;
strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
}
if (mode != 0xFF) {
_prefs->loop_detect = mode;
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "tx ", 3) == 0) { } else if (memcmp(config, "tx ", 3) == 0) {
_prefs->tx_power_dbm = atoi(&config[3]); _prefs->tx_power_dbm = atoi(&config[3]);
savePrefs(); savePrefs();
@@ -642,6 +686,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->adc_multiplier = 0.0f; _prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board"); strcpy(reply, "Error: unsupported by this board");
}; };
} else if (memcmp(config, "flood.advert.base ", 18) == 0) {
float f = atof(&config[18]);
if(f >= 0 && f <= 1) {
_prefs->flood_advert_base = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: base must be between 0 and 1");
}
} else { } else {
sprintf(reply, "unknown config: %s", config); sprintf(reply, "unknown config: %s", config);
} }
@@ -749,7 +802,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->advert_loc_policy = ADVERT_LOC_SHARE; _prefs->advert_loc_policy = ADVERT_LOC_SHARE;
savePrefs(); savePrefs();
strcpy(reply, "ok"); strcpy(reply, "ok");
} else if (memcmp(command+11, "prefs", 4) == 0) { } else if (memcmp(command+11, "prefs", 5) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_PREFS; _prefs->advert_loc_policy = ADVERT_LOC_PREFS;
savePrefs(); savePrefs();
strcpy(reply, "ok"); strcpy(reply, "ok");

View File

@@ -13,6 +13,11 @@
#define ADVERT_LOC_SHARE 1 #define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2 #define ADVERT_LOC_PREFS 2
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
struct NodePrefs { // persisted to file struct NodePrefs { // persisted to file
float airtime_factor; float airtime_factor;
char node_name[32]; char node_name[32];
@@ -36,6 +41,7 @@ struct NodePrefs { // persisted to file
uint8_t flood_max; uint8_t flood_max;
uint8_t interference_threshold; uint8_t interference_threshold;
uint8_t agc_reset_interval; // secs / 4 uint8_t agc_reset_interval; // secs / 4
float flood_advert_base;
// Bridge settings // Bridge settings
uint8_t bridge_enabled; // boolean uint8_t bridge_enabled; // boolean
uint16_t bridge_delay; // milliseconds (default 500 ms) uint16_t bridge_delay; // milliseconds (default 500 ms)
@@ -53,6 +59,7 @@ struct NodePrefs { // persisted to file
float adc_multiplier; float adc_multiplier;
char owner_info[120]; char owner_info[120];
uint8_t path_hash_mode; // which path mode to use when sending uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
}; };
class CommonCLICallbacks { class CommonCLICallbacks {

View File

@@ -2,6 +2,7 @@
#include "CustomLLCC68.h" #include "CustomLLCC68.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomLLCC68Wrapper : public RadioLibWrapper { class CustomLLCC68Wrapper : public RadioLibWrapper {
public: public:
@@ -19,4 +20,6 @@ public:
int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; int sf = ((CustomLLCC68 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len); return packetScoreInt(snr, sf, packet_len);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -20,6 +20,8 @@ class CustomLR1110 : public LR1110 {
return len; return len;
} }
float getFreqMHz() const { return freqMHz; }
bool isReceiving() { bool isReceiving() {
uint16_t irq = getIrqStatus(); uint16_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));

View File

@@ -2,11 +2,13 @@
#include "CustomLR1110.h" #include "CustomLR1110.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "LR11x0Reset.h"
class CustomLR1110Wrapper : public RadioLibWrapper { class CustomLR1110Wrapper : public RadioLibWrapper {
public: public:
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override { void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }
bool isReceivingPacket() override {
return ((CustomLR1110 *)_radio)->isReceiving(); return ((CustomLR1110 *)_radio)->isReceiving();
} }
float getCurrentRSSI() override { float getCurrentRSSI() override {

View File

@@ -2,6 +2,7 @@
#include "CustomSTM32WLx.h" #include "CustomSTM32WLx.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
#include <math.h> #include <math.h>
class CustomSTM32WLxWrapper : public RadioLibWrapper { class CustomSTM32WLxWrapper : public RadioLibWrapper {
@@ -20,4 +21,6 @@ public:
int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len); return packetScoreInt(snr, sf, packet_len);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -2,6 +2,7 @@
#include "CustomSX1262.h" #include "CustomSX1262.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomSX1262Wrapper : public RadioLibWrapper { class CustomSX1262Wrapper : public RadioLibWrapper {
public: public:
@@ -22,4 +23,6 @@ public:
virtual void powerOff() override { virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false); ((CustomSX1262 *)_radio)->sleep(false);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -2,6 +2,7 @@
#include "CustomSX1268.h" #include "CustomSX1268.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomSX1268Wrapper : public RadioLibWrapper { class CustomSX1268Wrapper : public RadioLibWrapper {
public: public:
@@ -19,4 +20,6 @@ public:
int sf = ((CustomSX1268 *)_radio)->spreadingFactor; int sf = ((CustomSX1268 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len); return packetScoreInt(snr, sf, packet_len);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -0,0 +1,21 @@
#pragma once
#include <RadioLib.h>
// Full receiver reset for LR11x0-family chips (LR1110, LR1120, LR1121).
// Warm sleep powers down analog, calibrate(0x3F) refreshes all calibration blocks,
// then re-applies RX settings that calibration may reset.
inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) {
radio->sleep(true, 0);
radio->standby(RADIOLIB_LR11X0_STANDBY_RC, true);
radio->calibrate(RADIOLIB_LR11X0_CALIBRATE_ALL);
// calibrate(0x3F) defaults image calibration to 902-928MHz band.
// Re-calibrate for the actual operating frequency (band=4MHz matches RadioLib default).
radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f);
#ifdef RX_BOOSTED_GAIN
radio->setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
}

View File

@@ -53,13 +53,24 @@ void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
} }
} }
void RadioLibWrapper::doResetAGC() {
_radio->sleep(); // warm sleep to reset analog frontend
}
void RadioLibWrapper::resetAGC() { void RadioLibWrapper::resetAGC() {
// make sure we're not mid-receive of packet! // make sure we're not mid-receive of packet!
if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return; if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return;
// NOTE: according to higher powers, just issuing RadioLib's startReceive() will reset the AGC. doResetAGC();
// revisit this if a better impl is discovered.
state = STATE_IDLE; // trigger a startReceive() state = STATE_IDLE; // trigger a startReceive()
// Reset noise floor sampling so it reconverges from scratch.
// Without this, a stuck _noise_floor of -120 makes the sampling threshold
// too low (-106) to accept normal samples (~-105), self-reinforcing the
// stuck value even after the receiver has recovered.
_noise_floor = 0;
_num_floor_samples = 0;
_floor_sample_sum = 0;
} }
void RadioLibWrapper::loop() { void RadioLibWrapper::loop() {

View File

@@ -16,6 +16,7 @@ protected:
void startRecv(); void startRecv();
float packetScoreInt(float snr, int sf, int packet_len); float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0; virtual bool isReceivingPacket() =0;
virtual void doResetAGC();
public: public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; } RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }

View File

@@ -0,0 +1,37 @@
#pragma once
#include <RadioLib.h>
// Full receiver reset for all SX126x-family chips (SX1262, SX1268, LLCC68, STM32WLx).
// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration,
// then re-applies RX settings that calibration may reset.
inline void sx126xResetAGC(SX126x* radio) {
radio->sleep(true);
radio->standby(RADIOLIB_SX126X_STANDBY_RC, true);
uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL;
radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false);
radio->mod->hal->delay(5);
uint32_t start = millis();
while (radio->mod->hal->digitalRead(radio->mod->getGpio())) {
if (millis() - start > 50) break;
radio->mod->hal->yield();
}
// Calibrate(0x7F) defaults image calibration to 902-928MHz band.
// Re-calibrate for the actual operating frequency.
radio->calibrateImage(radio->freqMHz);
#ifdef SX126X_DIO2_AS_RF_SWITCH
radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
radio->readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
radio->writeRegister(0x8B5, &r_data, 1);
#endif
}

View File

@@ -683,7 +683,7 @@ void EnvironmentSensorManager::start_gps() {
_location->begin(); _location->begin();
_location->reset(); _location->reset();
#ifndef PIN_GPS_RESET #ifndef PIN_GPS_EN
MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged"); MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged");
#endif #endif
} }
@@ -707,7 +707,9 @@ void EnvironmentSensorManager::loop() {
static long next_gps_update = 0; static long next_gps_update = 0;
#if ENV_INCLUDE_GPS #if ENV_INCLUDE_GPS
_location->loop(); if (gps_active) {
_location->loop();
}
if (millis() > next_gps_update) { if (millis() > next_gps_update) {
if(gps_active){ if(gps_active){

View File

@@ -79,7 +79,10 @@ public :
if (_pin_en != -1) { if (_pin_en != -1) {
digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
} }
if (_peripher_power) _peripher_power->release(); if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
}
if (_peripher_power) _peripher_power->release();
} }
bool isEnabled() override { bool isEnabled() override {

View File

@@ -32,6 +32,11 @@ build_flags =
-D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground) -D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground)
-D PIN_GPS_RX=33 -D PIN_GPS_RX=33
-D PIN_GPS_TX=34 -D PIN_GPS_TX=34
-D PIN_GPS_EN=35 ; N-ch MOSFET Q2 drives P-ch high-side switch → active HIGH (default)
-D PIN_GPS_RESET=36
-D PIN_GPS_RESET_ACTIVE=LOW
-D GPS_BAUD_RATE=115200
-D ENV_INCLUDE_GPS=1
-D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140 -D SX126X_CURRENT_LIMIT=140

View File

@@ -16,7 +16,8 @@ WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock; ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock); AutoDiscoverRTCClock rtc_clock(fallback_clock);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); // GPS_EN (GPIO35) drives N-ch MOSFET → P-ch high-side switch; GPS_RESET (GPIO36) active LOW
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power);
HWTSensorManager sensors = HWTSensorManager(nmea); HWTSensorManager sensors = HWTSensorManager(nmea);
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
@@ -58,18 +59,16 @@ mesh::LocalIdentity radio_new_identity() {
void HWTSensorManager::start_gps() { void HWTSensorManager::start_gps() {
if (!gps_active) { if (!gps_active) {
board.periph_power.claim(); _location->begin(); // Claims periph_power via RefCountedDigitalPin
gps_active = true; gps_active = true;
Serial1.println("$CFGSYS,h35155*68"); Serial1.println("$CFGSYS,h35155*68"); // Configure GPS for all constellations
} }
} }
void HWTSensorManager::stop_gps() { void HWTSensorManager::stop_gps() {
if (gps_active) { if (gps_active) {
gps_active = false; gps_active = false;
_location->stop(); // Releases periph_power via RefCountedDigitalPin
board.periph_power.release();
} }
} }

View File

@@ -28,6 +28,7 @@ public:
const char* getSettingName(int i) const override; const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override; const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override; bool setSettingValue(const char* name, const char* value) override;
LocationProvider* getLocationProvider() override { return _location; }
}; };
extern HeltecV3Board board; extern HeltecV3Board board;

View File

@@ -6,18 +6,26 @@ void HeltecTrackerV2Board::begin() {
pinMode(PIN_ADC_CTRL, OUTPUT); pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
// Set up digital GPIO registers before releasing RTC hold. The hold latches
// the pad state including function select, so register writes accumulate
// without affecting the pad. On hold release, all changes apply atomically
// (IO MUX switches to digital GPIO with output already HIGH — no glitch).
pinMode(P_LORA_PA_POWER, OUTPUT); pinMode(P_LORA_PA_POWER, OUTPUT);
digitalWrite(P_LORA_PA_POWER,HIGH); digitalWrite(P_LORA_PA_POWER,HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
pinMode(P_LORA_PA_EN, OUTPUT); pinMode(P_LORA_PA_EN, OUTPUT);
digitalWrite(P_LORA_PA_EN,HIGH); digitalWrite(P_LORA_PA_EN,HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
pinMode(P_LORA_PA_TX_EN, OUTPUT); pinMode(P_LORA_PA_TX_EN, OUTPUT);
digitalWrite(P_LORA_PA_TX_EN,LOW); digitalWrite(P_LORA_PA_TX_EN,LOW);
periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason(); esp_reset_reason_t reason = esp_reset_reason();
if (reason != ESP_RST_DEEPSLEEP) {
delay(1); // GC1109 startup time after cold power-on
}
periph_power.begin();
if (reason == ESP_RST_DEEPSLEEP) { if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status(); long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
@@ -48,7 +56,9 @@ void HeltecTrackerV2Board::begin() {
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); //It also needs to be enabled in receive mode // Hold GC1109 FEM pins during sleep to keep LNA active for RX wake
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER);
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN);
if (pin_wake_btn < 0) { if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet

View File

@@ -17,11 +17,11 @@ build_flags =
-D P_LORA_SCLK=9 -D P_LORA_SCLK=9
-D P_LORA_MISO=11 -D P_LORA_MISO=11
-D P_LORA_MOSI=10 -D P_LORA_MOSI=10
-D P_LORA_PA_POWER=7 ;power en -D P_LORA_PA_POWER=7 ; VFEM_Ctrl - GC1109 LDO power enable
-D P_LORA_PA_EN=4 -D P_LORA_PA_EN=4 ; CSD - GC1109 chip enable (HIGH=on)
-D P_LORA_PA_TX_EN=46 ;enable tx -D P_LORA_PA_TX_EN=46 ; CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass)
-D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm. -D LORA_TX_POWER=10 ; 10dBm + ~11dB GC1109 gain = ~21dBm output
-D MAX_LORA_TX_POWER=22 ;Max SX1262 output -D MAX_LORA_TX_POWER=22 ; Max SX1262 output -> ~28dBm at antenna
-D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140 -D SX126X_CURRENT_LIMIT=140

View File

@@ -7,19 +7,26 @@ void HeltecV4Board::begin() {
pinMode(PIN_ADC_CTRL, OUTPUT); pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
// Set up digital GPIO registers before releasing RTC hold. The hold latches
// the pad state including function select, so register writes accumulate
// without affecting the pad. On hold release, all changes apply atomically
// (IO MUX switches to digital GPIO with output already HIGH — no glitch).
pinMode(P_LORA_PA_POWER, OUTPUT); pinMode(P_LORA_PA_POWER, OUTPUT);
digitalWrite(P_LORA_PA_POWER,HIGH); digitalWrite(P_LORA_PA_POWER,HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
pinMode(P_LORA_PA_EN, OUTPUT); pinMode(P_LORA_PA_EN, OUTPUT);
digitalWrite(P_LORA_PA_EN,HIGH); digitalWrite(P_LORA_PA_EN,HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
pinMode(P_LORA_PA_TX_EN, OUTPUT); pinMode(P_LORA_PA_TX_EN, OUTPUT);
digitalWrite(P_LORA_PA_TX_EN,LOW); digitalWrite(P_LORA_PA_TX_EN,LOW);
esp_reset_reason_t reason = esp_reset_reason();
if (reason != ESP_RST_DEEPSLEEP) {
delay(1); // GC1109 startup time after cold power-on
}
periph_power.begin(); periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) { if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status(); long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
@@ -50,7 +57,9 @@ void HeltecV4Board::begin() {
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); //It also needs to be enabled in receive mode // Hold GC1109 FEM pins during sleep to keep LNA active for RX wake
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER);
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN);
if (pin_wake_btn < 0) { if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet

View File

@@ -1,5 +1,7 @@
[ikoka_handheld_nrf] [ikoka_handheld_nrf]
extends = nrf52_base extends = nrf52_base
board = seeed-xiao-afruitnrf52-nrf52840
board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52_base.build_flags} build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags} ${sensor_base.build_flags}
-I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include
@@ -48,7 +50,8 @@ build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
+<../examples/companion_radio/*.cpp> +<../examples/companion_radio/*.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_ble] [env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_ble]
extends = ikoka_nrf52 extends = ikoka_handheld_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D BLE_PIN_CODE=123456 -D BLE_PIN_CODE=123456
-D LORA_TX_POWER=20 -D LORA_TX_POWER=20
@@ -56,7 +59,8 @@ build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp> +<helpers/nrf52/SerialBLEInterface.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_ble] [env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_ble]
extends = ikoka_nrf52 extends = ikoka_handheld_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D BLE_PIN_CODE=123456 -D BLE_PIN_CODE=123456
-D LORA_TX_POWER=20 -D LORA_TX_POWER=20
@@ -65,20 +69,22 @@ build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp> +<helpers/nrf52/SerialBLEInterface.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_usb] [env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_usb]
extends = ikoka_nrf52 extends = ikoka_handheld_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D LORA_TX_POWER=20 -D LORA_TX_POWER=20
build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter} build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
[env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb] [env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb]
extends = ikoka_nrf52 extends = ikoka_handheld_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D LORA_TX_POWER=20 -D LORA_TX_POWER=20
-D DISPLAY_ROTATION=2 -D DISPLAY_ROTATION=2
build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter} build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
[env:ikoka_handheld_nrf_e22_30dbm_repeater] [env:ikoka_handheld_nrf_e22_30dbm_repeater]
extends = ikoka_nrf52 extends = ikoka_handheld_nrf
build_flags = build_flags =
${ikoka_handheld_nrf.build_flags} ${ikoka_handheld_nrf.build_flags}
-D ADVERT_NAME='"ikoka_handheld Repeater"' -D ADVERT_NAME='"ikoka_handheld Repeater"'
@@ -91,7 +97,7 @@ build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
+<../examples/simple_repeater/*.cpp> +<../examples/simple_repeater/*.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_room_server] [env:ikoka_handheld_nrf_e22_30dbm_room_server]
extends = ikoka_nrf52 extends = ikoka_handheld_nrf
build_flags = build_flags =
${ikoka_handheld_nrf.build_flags} ${ikoka_handheld_nrf.build_flags}
-D ADVERT_NAME='"ikoka_handheld Room"' -D ADVERT_NAME='"ikoka_handheld Room"'

View File

@@ -20,13 +20,29 @@ void RAK3401Board::begin() {
Wire.begin(); Wire.begin();
// PIN_3V3_EN (WB_IO2, P0.34) controls the 3V3_S switched peripheral rail
// AND the 5V boost regulator (U5) on the RAK13302 that powers the SKY66122 PA.
// Must stay HIGH during radio operation — do not toggle for power saving.
pinMode(PIN_3V3_EN, OUTPUT); pinMode(PIN_3V3_EN, OUTPUT);
digitalWrite(PIN_3V3_EN, HIGH); digitalWrite(PIN_3V3_EN, HIGH);
#ifdef P_LORA_PA_EN // Enable SKY66122-11 FEM on the RAK13302 module.
// Initialize RAK13302 1W LoRa transceiver module PA control pin // CSD and CPS are tied together on the RAK13302 PCB, routed to IO3 (P0.21).
pinMode(P_LORA_PA_EN, OUTPUT); // HIGH = FEM active (LNA for RX, PA path available for TX).
digitalWrite(P_LORA_PA_EN, LOW); // Start with PA disabled // TX/RX switching (CTX) is handled by SX1262 DIO2 via SetDIO2AsRfSwitchCtrl.
delay(10); // Allow PA module to initialize pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
delay(1); // SKY66122 turn-on settling time (tON = 3us typ)
}
#ifdef NRF52_POWER_MANAGEMENT
void RAK3401Board::initiateShutdown(uint8_t reason) {
// Disable SKY66122 FEM (CSD+CPS LOW = shutdown, <1 uA)
digitalWrite(SX126X_POWER_EN, LOW);
// Disable 3V3 switched peripherals and 5V boost
digitalWrite(PIN_3V3_EN, LOW);
enterSystemOff(reason);
}
#endif #endif
}

View File

@@ -38,13 +38,6 @@ public:
return "RAK 3401"; return "RAK 3401";
} }
#ifdef P_LORA_PA_EN // TX/RX switching is handled by SX1262 DIO2 -> SKY66122 CTX (hardware-timed).
void onBeforeTransmit() override { // No onBeforeTransmit/onAfterTransmit overrides needed.
digitalWrite(P_LORA_PA_EN, HIGH); // Enable PA before transmission
}
void onAfterTransmit() override {
digitalWrite(P_LORA_PA_EN, LOW); // Disable PA after transmission to save power
}
#endif
}; };

View File

@@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags}
-D LORA_TX_POWER=22 -D LORA_TX_POWER=22
-D SX126X_CURRENT_LIMIT=140 -D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM
build_src_filter = ${nrf52_base.build_src_filter} build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/rak3401> +<../variants/rak3401>
+<helpers/sensors> +<helpers/sensors>

View File

@@ -147,8 +147,15 @@ static const uint8_t AREF = PIN_AREF;
#define SX126X_BUSY (9) #define SX126X_BUSY (9)
#define SX126X_RESET (4) #define SX126X_RESET (4)
#define SX126X_POWER_EN (21) // SKY66122-11 FEM control on the RAK13302 module:
// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 // CSD + CPS are tied together on the PCB, routed to WisBlock IO3 (P0.21).
// Setting IO3 HIGH enables the FEM (LNA for RX, PA path for TX).
// CTX is connected to SX1262 DIO2 — the radio handles TX/RX switching
// in hardware via SetDIO2AsRfSwitchCtrl (microsecond-accurate, no GPIO needed).
// The 5V boost for the PA is enabled by WB_IO2 (P0.34 = PIN_3V3_EN).
#define SX126X_POWER_EN (21) // P0.21 = IO3 -> SKY66122 CSD+CPS (FEM enable)
// CTX is driven by SX1262 DIO2, not a GPIO
#define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define SX126X_DIO3_TCXO_VOLTAGE 1.8
@@ -159,7 +166,6 @@ static const uint8_t AREF = PIN_AREF;
#define P_LORA_DIO_1 SX126X_DIO1 #define P_LORA_DIO_1 SX126X_DIO1
#define P_LORA_BUSY SX126X_BUSY #define P_LORA_BUSY SX126X_BUSY
#define P_LORA_RESET SX126X_RESET #define P_LORA_RESET SX126X_RESET
#define P_LORA_PA_EN 31
// enables 3.3V periphery like GPS or IO Module // enables 3.3V periphery like GPS or IO Module
// Do not toggle this for GPS power savings // Do not toggle this for GPS power savings

View File

@@ -106,7 +106,7 @@ extern "C"
// Power management boot protection threshold (millivolts) // Power management boot protection threshold (millivolts)
// Set to 0 to disable boot protection // Set to 0 to disable boot protection
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_VOLTAGE_BOOTLOCK 0 // Won't boot below this voltage (mV)
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF) // LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
// AIN3 = P0.05 = PIN_A0 / PIN_VBAT_READ // AIN3 = P0.05 = PIN_A0 / PIN_VBAT_READ
#define PWRMGT_LPCOMP_AIN 3 #define PWRMGT_LPCOMP_AIN 3