mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 21:38:20 +00:00
sync with upstream dev
This commit is contained in:
@@ -395,12 +395,12 @@ isReceiving()
|
||||
```
|
||||
|
||||
For SX126x, `hwIsReceiving()` → `sx126x_is_receiving()` reads in this order:
|
||||
1. **`data->rx_packet_active`** latch (no SPI). Set by the work handler on `HEADER_VALID`; cleared on every terminal event and RX (re)start. Covers the full payload phase.
|
||||
1. **`data->rx_packet_active`** latch (no SPI). Set by the work handler on `HEADER_VALID`; cleared on every terminal event and RX (re)start. Covers the full payload phase. Bounded by a payload deadline: `header_seen_at_ms` is stamped when the latch is promoted, and once `sx126x_max_payload_ms()` (255-byte airtime at the current SF/BW, CR 4/8, LDRO on, +25% +100 ms) has elapsed the latch is released and the sticky PREAMBLE/SYNC/HEADER bits cleared. Continuous RX has no symbol timer, so without this a `HEADER_VALID` whose packet never completes would hold the TX gate closed until reboot; the DC parked-RX watchdog does not cover it (DC-only, and it treats the latch as a legitimate in-flight packet).
|
||||
2. **Mutex-busy conservative** — if the SPI mutex is contended and `state == RX`, return true (the work handler is likely mid-`RxDone`).
|
||||
3. **`HEADER_VALID` raw bit** — covers the microseconds between DIO1 firing and the work handler running.
|
||||
4. **`PREAMBLE_DETECTED` raw bit with SF-aware grace** — `PREAMBLE_DETECTED` is masked off DIO1 (fires on noise), but visible in the IRQ register. On first observation, `is_receiving` records `data->preamble_seen_at_ms`; subsequent calls return true until either `HEADER_VALID` promotes the latch (timestamp reset) or `(preamble_len + 8) × 2^SF / BW` ms elapses — at which point the bit is explicitly cleared and TX is allowed. Grace scales with SF: ~82 ms at SF8, ~786 ms at SF12.
|
||||
|
||||
The poll path is otherwise non-destructive — IRQ bits are cleared only by the work-handler bulk clear (on any DIO1 event), explicit `clear_irq_status(IRQ_ALL)` at every RX (re)start, and the grace-expiry one-bit clear for foreign preambles.
|
||||
The poll path is otherwise non-destructive — IRQ bits are cleared only by the work-handler bulk clear (on any DIO1 event), explicit `clear_irq_status(IRQ_ALL)` at every RX (re)start, the grace-expiry one-bit clear for foreign preambles, and the payload-deadline clear in step 1.
|
||||
|
||||
### 5.2.2 CAD-Timeout Recovery
|
||||
|
||||
|
||||
@@ -65,6 +65,16 @@ All commands are sent over USB serial (CDC-ACM). Commands sent remotely over the
|
||||
|
||||
> **Password length:** admin and guest passwords are capped at **15 characters** (16-byte storage incl. NUL; same limit as Arduino MeshCore). The login-send path silently truncates anything longer, so a password >15 chars will never authenticate. Applies to `set guest.password` as well.
|
||||
|
||||
> **Guest access is off unless a guest password is set.** An empty `guest.password` (the default) disables guest login rather than matching a blank submitted password. To run an open room-server, use `set allow.read.only on` — that grants read-only (`PERM_ACL_GUEST`), not post rights.
|
||||
|
||||
---
|
||||
|
||||
## Room Server
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `room.post <message>` | Post a message to the shared room as the server itself (system post). Pushed to clients like any other post. |
|
||||
|
||||
---
|
||||
|
||||
## Region Filtering
|
||||
|
||||
@@ -1486,23 +1486,23 @@ int CompanionMesh::appendSelfTelemetry(uint8_t *reply, uint8_t permissions)
|
||||
}
|
||||
}
|
||||
|
||||
// Environment sensors if authorized and available
|
||||
/* Sensors are read once here. External temp/humidity/pressure require
|
||||
* TELEM_PERM_ENVIRONMENT, but the MCU die temperature is reported under
|
||||
* base permission — matching Arduino MeshCore (15e259c5) and ZephCore's
|
||||
* own repeater/room-server telemetry, neither of which gates it. */
|
||||
struct env_data env;
|
||||
bool env_ok = (env_sensors_read(&env) == 0);
|
||||
bool temp_reported = false;
|
||||
|
||||
if (permissions & TELEM_PERM_ENVIRONMENT) {
|
||||
struct env_data env;
|
||||
if (env_sensors_read(&env) == 0) {
|
||||
if (env_ok) {
|
||||
if (env.has_temperature) {
|
||||
temp_reported = true;
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
} else if (env.has_mcu_temperature) {
|
||||
// MCU die temp as fallback when no external sensor
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.mcu_temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
}
|
||||
if (env.has_humidity) {
|
||||
reply[i++] = CH_SELF;
|
||||
@@ -1550,6 +1550,16 @@ int CompanionMesh::appendSelfTelemetry(uint8_t *reply, uint8_t permissions)
|
||||
}
|
||||
}
|
||||
|
||||
/* MCU die temperature — reported under base permission, but only when no
|
||||
* external sensor already supplied a CH_SELF temperature (never emit two). */
|
||||
if (!temp_reported && env_ok && env.has_mcu_temperature) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_TEMPERATURE;
|
||||
int16_t temp = (int16_t)(env.mcu_temperature_c * 10);
|
||||
reply[i++] = (temp >> 8) & 0xFF;
|
||||
reply[i++] = temp & 0xFF;
|
||||
}
|
||||
|
||||
// Trigger GPS wake for fresh fix on next request
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
gps_request_fresh_fix();
|
||||
|
||||
@@ -161,6 +161,12 @@ uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t
|
||||
guest_pw,
|
||||
sizeof(received));
|
||||
|
||||
/* An empty stored guest password disables guest access (as
|
||||
* CONFIG_ZEPHCORE_GUEST_PASSWORD documents) rather than matching an
|
||||
* empty submitted password and letting anyone in. Both compares above
|
||||
* still run unconditionally, so timing is unchanged. */
|
||||
if (_prefs.guest_password[0] == 0) guest_match = false;
|
||||
|
||||
if (admin_match) {
|
||||
perms = PERM_ACL_ADMIN;
|
||||
} else if (guest_match) {
|
||||
|
||||
@@ -234,7 +234,18 @@ mesh::Packet* RoomServerMesh::createSelfAdvert() {
|
||||
/* ---- Room server: shared-post buffer + push-to-client sync ---- */
|
||||
|
||||
void RoomServerMesh::addPost(ClientInfo* client, const char* postData) {
|
||||
posts[next_post_idx].author = client->id;
|
||||
storePost(client->id, postData);
|
||||
}
|
||||
|
||||
/* Post authored by the server itself (admin "room.post" command). */
|
||||
void RoomServerMesh::addSystemPost(const char* postData) {
|
||||
if (!postData || postData[0] == 0) return;
|
||||
|
||||
storePost(self_id, postData);
|
||||
}
|
||||
|
||||
void RoomServerMesh::storePost(const mesh::Identity& author, const char* postData) {
|
||||
posts[next_post_idx].author = author;
|
||||
strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
|
||||
posts[next_post_idx].text[MAX_POST_TEXT_LEN] = '\0';
|
||||
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
@@ -466,6 +477,13 @@ void RoomServerMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret,
|
||||
bool admin_match = mesh::Utils::constantTimeEqual(received, admin_pw, sizeof(received));
|
||||
bool guest_match = mesh::Utils::constantTimeEqual(received, guest_pw, sizeof(received));
|
||||
|
||||
/* An empty stored guest password disables guest access (as
|
||||
* CONFIG_ZEPHCORE_GUEST_PASSWORD documents) rather than matching an
|
||||
* empty submitted password and granting read+write to anyone. Both
|
||||
* compares above still run unconditionally, so timing is unchanged.
|
||||
* allow_read_only below remains the intended way to run an open room. */
|
||||
if (_prefs.guest_password[0] == 0) guest_match = false;
|
||||
|
||||
uint8_t perms;
|
||||
if (admin_match) {
|
||||
perms = PERM_ACL_ADMIN;
|
||||
@@ -1033,6 +1051,15 @@ void RoomServerMesh::handleCommand(uint32_t sender_timestamp, char* command, cha
|
||||
reply[0] = 0;
|
||||
} else if (memcmp(command, "region", 6) == 0) {
|
||||
handleRegionCommand(command, reply);
|
||||
} else if (memcmp(command, "room.post", 9) == 0) {
|
||||
char* msg = command + 9;
|
||||
while (*msg == ' ') msg++;
|
||||
if (*msg == 0) {
|
||||
strcpy(reply, "Err - empty message");
|
||||
} else {
|
||||
addSystemPost(msg);
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else {
|
||||
_cli.handleCommand(sender_timestamp, command, reply);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
|
||||
/* Room server: shared-post buffer + push-to-client sync */
|
||||
void addPost(ClientInfo* client, const char* postData);
|
||||
void storePost(const mesh::Identity& author, const char* postData);
|
||||
void pushPostToClient(ClientInfo* client, PostInfo& post);
|
||||
uint8_t getUnsyncedCount(ClientInfo* client);
|
||||
bool processAck(const uint8_t* data);
|
||||
@@ -174,6 +175,9 @@ public:
|
||||
|
||||
void begin(RepeaterDataStore* store);
|
||||
|
||||
/* Post authored by the server itself (admin "room.post <msg>" command). */
|
||||
void addSystemPost(const char* postData);
|
||||
|
||||
/* CommonCLICallbacks */
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Hardware:
|
||||
# - nRF52840 SoC with BLE
|
||||
# - LR1110 LoRa radio (TCXO 3.3V, DIO5/DIO6 RF switch)
|
||||
# - GPS module on UART0 (9600 baud, 3-pin control)
|
||||
# - GPS module on UART0 (9600 baud; power + enable control, reset left floating)
|
||||
# - Battery ADC on AIN3 (P0.05), 2:1 voltage divider
|
||||
# - LEDs: GREEN=P1.03, RED=P1.01, POWER=P0.29
|
||||
# - Button on P0.12 (active-low, pull-up)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* GPS control pins for the GPS module (generic NMEA at 9600 baud):
|
||||
* - GPS_POWER (P0.14) - Main power enable (always HIGH, gpio-hog)
|
||||
* - GPS_EN (P0.21) - Standby control (HIGH = awake, LOW = standby)
|
||||
* - GPS_RESET (P0.25) - Reset (GPS_RESET_ACTIVE = LOW, REINIT pin)
|
||||
* - GPS_RESET (P0.25) - REINIT pin, must stay FLOATING on this unit (see below)
|
||||
*
|
||||
* Power enable pins (always HIGH):
|
||||
* - PIN_PWR_EN (P0.16) - Main power domain
|
||||
@@ -24,20 +24,18 @@
|
||||
};
|
||||
};
|
||||
|
||||
/* GPS hardware reset - P0.25 (GPS_RESET, active-LOW)
|
||||
* Asserted (pin LOW) on standby entry to halt the module cleanly.
|
||||
* Released (pin HIGH) after GPS_EN on wake with a brief reset pulse. */
|
||||
gps_rst: gps-reset {
|
||||
compatible = "gpio-leds";
|
||||
gps_reset_pin: gps_reset {
|
||||
gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
|
||||
label = "GPS Reset";
|
||||
};
|
||||
};
|
||||
/* GPS hardware reset (P0.25, "REINIT") is deliberately NOT declared.
|
||||
*
|
||||
* On this unit the pin must be left floating: Arduino MeshCore drove it
|
||||
* and GPS did not work, and setting GPS_RESET = -1 (never drive it) is
|
||||
* what fixed NMEA output on real hardware — upstream 3af5ffe1, resolving
|
||||
* meshcore-dev/MeshCore#1864 and #2879. Without a `gps-reset` alias,
|
||||
* every reset path in ZephyrGPSManager compiles out
|
||||
* (#if DT_NODE_EXISTS(DT_ALIAS(gps_reset))), so the pin stays untouched.
|
||||
* GPS_EN (P0.21) alone handles wake/standby. */
|
||||
|
||||
aliases {
|
||||
gps-enable = &gps_enable_pin;
|
||||
gps-reset = &gps_reset_pin;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - nRF52840 SoC with BLE
|
||||
* - LR1110 LoRa radio on SPI1 (TCXO 3.3V, DIO5/DIO6 RF switch)
|
||||
* - GPS module on UART0 (9600 baud, generic NMEA)
|
||||
* Control: GPS_POWER=P0.14 (always on), GPS_EN=P0.21 (standby), GPS_RESET=P0.25 (active-LOW)
|
||||
* Control: GPS_POWER=P0.14 (always on), GPS_EN=P0.21 (standby); GPS_RESET=P0.25 unused/floating
|
||||
* - Battery ADC on AIN3 (P0.05), 2:1 voltage divider
|
||||
* - LEDs: GREEN=P1.03, RED=P1.01 (active-low), POWER=P0.29 (always on via overlay)
|
||||
* - Button on P0.12 (active-low, pull-up), single button with multi-tap
|
||||
@@ -178,8 +178,9 @@
|
||||
* Using gnss-nmea-generic (passive NMEA listener) — Arduino reference code
|
||||
* uses MicroNMEA (generic NMEA parser only, no proprietary commands), so
|
||||
* chip-specific drivers are neither needed nor safe.
|
||||
* GPS_POWER=P0.14 (always on hog), GPS_EN=P0.21 (wake/standby),
|
||||
* GPS_RESET=P0.25 (active-LOW, pulsed on wake, asserted during standby).
|
||||
* GPS_POWER=P0.14 (always on hog), GPS_EN=P0.21 (wake/standby).
|
||||
* GPS_RESET=P0.25 (REINIT) is intentionally left floating — driving it stops
|
||||
* this module from producing NMEA (see board.overlay, upstream 3af5ffe1).
|
||||
*
|
||||
* zephyr,deferred-init: GPS needs GPIO power-up (GPS_EN HIGH) before NMEA
|
||||
* output. ZephyrGPSManager handles power-up then calls device_init(). */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
#include "AdvertDataHelpers.h"
|
||||
#include "UTF8Helpers.h"
|
||||
#include <string.h>
|
||||
|
||||
uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[])
|
||||
@@ -25,10 +26,16 @@ uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[])
|
||||
memcpy(&app_data[i], &_extra2, 2); i += 2;
|
||||
}
|
||||
if (_name && *_name != 0) {
|
||||
app_data[0] |= ADV_NAME_MASK;
|
||||
const char *sp = _name;
|
||||
while (*sp && i < MAX_ADVERT_DATA_SIZE) {
|
||||
app_data[i++] = *sp++;
|
||||
/* Truncate on a UTF-8 code-point boundary: a byte-wise copy would
|
||||
* emit a partial multi-byte sequence when a code point straddles
|
||||
* MAX_ADVERT_DATA_SIZE, producing a mangled name on the wire.
|
||||
* ADV_NAME_MASK is only set when at least one whole code point
|
||||
* fits in the remaining space. */
|
||||
size_t name_len = mesh::validUtf8PrefixLength(_name, MAX_ADVERT_DATA_SIZE - i);
|
||||
if (name_len > 0) {
|
||||
app_data[0] |= ADV_NAME_MASK;
|
||||
memcpy(&app_data[i], _name, name_len);
|
||||
i += name_len;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore UTF8Helpers - UTF-8 aware truncation
|
||||
*
|
||||
* Ported from Arduino MeshCore 79dc1de6 ("fix: preserve UTF-8 advert names").
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
inline bool isUtf8Continuation(uint8_t byte)
|
||||
{
|
||||
return (byte & 0xC0) == 0x80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the longest prefix of `text` that is (a) valid UTF-8 and (b) no
|
||||
* longer than `max_bytes`. Never splits a multi-byte code point, and rejects
|
||||
* overlong encodings, surrogates and out-of-range 4-byte sequences.
|
||||
*
|
||||
* @returns byte count to copy; 0 if not even one code point fits
|
||||
*/
|
||||
inline size_t validUtf8PrefixLength(const char *text, size_t max_bytes)
|
||||
{
|
||||
if (text == nullptr) return 0;
|
||||
|
||||
size_t offset = 0;
|
||||
while (text[offset] != '\0') {
|
||||
const uint8_t first = (uint8_t)text[offset];
|
||||
size_t sequence_length = 0;
|
||||
|
||||
if (first <= 0x7F) {
|
||||
sequence_length = 1;
|
||||
} else if (first >= 0xC2 && first <= 0xDF) {
|
||||
sequence_length = 2;
|
||||
} else if (first >= 0xE0 && first <= 0xEF) {
|
||||
sequence_length = 3;
|
||||
} else if (first >= 0xF0 && first <= 0xF4) {
|
||||
sequence_length = 4;
|
||||
} else {
|
||||
break; /* continuation byte or overlong 2-byte lead */
|
||||
}
|
||||
|
||||
if (offset + sequence_length > max_bytes) break;
|
||||
|
||||
bool complete = true;
|
||||
for (size_t i = 1; i < sequence_length; i++) {
|
||||
if (text[offset + i] == '\0' ||
|
||||
!isUtf8Continuation((uint8_t)text[offset + i])) {
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!complete) break;
|
||||
|
||||
/* Reject overlong 3-byte forms and UTF-16 surrogates. */
|
||||
if (sequence_length == 3) {
|
||||
const uint8_t second = (uint8_t)text[offset + 1];
|
||||
if ((first == 0xE0 && second < 0xA0) ||
|
||||
(first == 0xED && second > 0x9F)) break;
|
||||
} else if (sequence_length == 4) {
|
||||
const uint8_t second = (uint8_t)text[offset + 1];
|
||||
if ((first == 0xF0 && second < 0x90) ||
|
||||
(first == 0xF4 && second > 0x8F)) break;
|
||||
}
|
||||
|
||||
offset += sequence_length;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,175 @@
|
||||
Bound the lifetime of the RX-busy latch (payload-phase deadline).
|
||||
|
||||
sx126x_is_receiving() reports "busy" for the whole payload phase from the
|
||||
rx_packet_active latch, which patch 0003 sets when HEADER_VALID fires and clears
|
||||
only on a terminal IRQ (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start.
|
||||
|
||||
In continuous RX (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so
|
||||
none of those events is guaranteed to arrive. A HEADER_VALID whose packet never
|
||||
completes therefore pins the TX gate true forever: the node keeps receiving but
|
||||
never transmits again until reboot, with nothing in the logs pointing at it. The
|
||||
duty-cycle parked-RX watchdog does not cover this case -- it is DC-only and
|
||||
deliberately treats rx_packet_active as a legitimate in-flight packet.
|
||||
|
||||
Add header_seen_at_ms, stamped when the latch is promoted, and release the latch
|
||||
(clearing the sticky PREAMBLE_DETECTED / SYNC_WORD_VALID / HEADER_VALID /
|
||||
HEADER_ERR bits) once sx126x_max_payload_ms() has elapsed. That bound is the
|
||||
airtime of a 255-byte explicit-header packet at the current SF/BW with CR 4/8
|
||||
and LDRO pinned on, plus 25% and 100 ms -- deliberately generous, since firing
|
||||
early would let TX start on top of a packet that is still arriving.
|
||||
|
||||
Arduino MeshCore added the equivalent deadline in 79ef74ea (SX1262) and
|
||||
0bd871cd (LR11X0); their preamble-phase timeout is already covered here by the
|
||||
SF-aware grace in patch 0003.
|
||||
|
||||
Kept separate from 0003 because that patch cannot be regenerated from the tree:
|
||||
0011 and 0012 also modify this driver and would be folded into it.
|
||||
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
|
||||
index 822d0f5..00faca6 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.c
|
||||
@@ -82,14 +82,15 @@ static uint32_t bandwidth_to_hz(enum lora_signal_bandwidth bw)
|
||||
}
|
||||
|
||||
/* Reset all software state that indicates "we are currently receiving":
|
||||
- * the rx_packet_active latch and the preamble-grace timestamp. Paired
|
||||
- * write so the two fields never drift out of sync. Called from every
|
||||
- * RX (re)start site, every terminal-event handler (RX_DONE / CRC_ERR /
|
||||
- * RX_TX_TIMEOUT), and on TX-state entry. */
|
||||
+ * the rx_packet_active latch (and its deadline timestamp) and the
|
||||
+ * preamble-grace timestamp. Paired write so the fields never drift out of
|
||||
+ * sync. Called from every RX (re)start site, every terminal-event handler
|
||||
+ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT), and on TX-state entry. */
|
||||
static inline void sx126x_reset_rx_busy_signals(struct sx126x_data *data)
|
||||
{
|
||||
data->rx_packet_active = false;
|
||||
atomic_set(&data->preamble_seen_at_ms, 0);
|
||||
+ atomic_set(&data->header_seen_at_ms, 0);
|
||||
}
|
||||
|
||||
/* Grace period for the PREAMBLE_DETECTED -> HEADER_VALID gap, SF/BW-aware.
|
||||
@@ -113,6 +114,53 @@ static uint32_t sx126x_preamble_grace_ms(struct sx126x_data *data)
|
||||
return (uint32_t)((us + 999U) / 1000U);
|
||||
}
|
||||
|
||||
+/* Upper bound on the payload phase: airtime of a maximum-length (255 byte)
|
||||
+ * explicit-header packet at the current SF/BW, worst-case coding rate 4/8,
|
||||
+ * plus margin. Used by sx126x_is_receiving() to bound the lifetime of the
|
||||
+ * rx_packet_active latch.
|
||||
+ *
|
||||
+ * Why the latch needs a deadline at all: it is cleared only by a terminal IRQ
|
||||
+ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start. In continuous RX
|
||||
+ * (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so none of those is
|
||||
+ * guaranteed to arrive -- a HEADER_VALID that never completes would pin the TX
|
||||
+ * gate true forever and silently mute the node until reboot. (The parked-RX
|
||||
+ * watchdog cannot cover this: it is duty-cycle only and deliberately treats
|
||||
+ * rx_packet_active as a legitimate in-flight packet.) Mirrors the payload
|
||||
+ * deadline Arduino MeshCore added in 79ef74ea / 0bd871cd.
|
||||
+ *
|
||||
+ * Deliberately generous: this is a stuck-state safety net, and clearing the
|
||||
+ * latch early would let TX start on top of a packet that is still arriving.
|
||||
+ * The low-data-rate-optimisation term is pinned at 1 (rather than read from
|
||||
+ * the modem config) because DE=1 yields the larger symbol count of the two,
|
||||
+ * i.e. the safer bound. */
|
||||
+static uint32_t sx126x_max_payload_ms(struct sx126x_data *data)
|
||||
+{
|
||||
+ uint8_t sf = (uint8_t)data->config.datarate;
|
||||
+ uint32_t bw_hz = bandwidth_to_hz(data->config.bandwidth);
|
||||
+
|
||||
+ if (bw_hz == 0 || sf < 5 || sf > 12) {
|
||||
+ return 30000; /* safe default if config is uninitialised */
|
||||
+ }
|
||||
+
|
||||
+ /* Semtech payload-symbol count with PL=255, CRC on, explicit header,
|
||||
+ * CR = 4/8 (coded_bits = 8), DE = 1:
|
||||
+ * n = 8 + ceil((8*PL - 4*SF + 28 + 16) / (4*(SF - DE))) * 8
|
||||
+ * SF5/SF6 use SF-1 >= 4 so the divisor is always non-zero. */
|
||||
+ uint32_t numer = 8U * 255U + 28U + 16U;
|
||||
+ uint32_t denom = 4U * (uint32_t)(sf - 1U);
|
||||
+
|
||||
+ if (numer > 4U * (uint32_t)sf) {
|
||||
+ numer -= 4U * (uint32_t)sf;
|
||||
+ }
|
||||
+ uint32_t n_sym = 8U + ((numer + denom - 1U) / denom) * 8U;
|
||||
+
|
||||
+ /* n_sym * 2^sf * 1000000 / bw_hz -> us, then +25% and +100 ms margin. */
|
||||
+ uint64_t us = ((uint64_t)n_sym << sf) * 1000000ULL / bw_hz;
|
||||
+ uint32_t ms = (uint32_t)((us + 999U) / 1000U);
|
||||
+
|
||||
+ return ms + (ms / 4U) + 100U;
|
||||
+}
|
||||
+
|
||||
static bool should_enable_ldro(enum lora_datarate sf, enum lora_signal_bandwidth bw,
|
||||
const struct sx126x_hal_config *config)
|
||||
{
|
||||
@@ -1225,6 +1273,14 @@ static void sx126x_irq_work_handler(struct k_work *work)
|
||||
* is stale and we don't want a future is_receiving() to treat
|
||||
* it as live when the latch eventually clears. */
|
||||
atomic_set(&data->preamble_seen_at_ms, 0);
|
||||
+ /* Start the payload deadline (see sx126x_max_payload_ms). Use 1
|
||||
+ * as the "set" sentinel if k_uptime is 0 right after boot. */
|
||||
+ {
|
||||
+ uint32_t now = k_uptime_get_32();
|
||||
+
|
||||
+ atomic_set(&data->header_seen_at_ms,
|
||||
+ (atomic_val_t)(now == 0 ? 1U : now));
|
||||
+ }
|
||||
}
|
||||
|
||||
/* Re-enable the DIO1 interrupt for the next event (unless sleeping) */
|
||||
@@ -1886,9 +1942,33 @@ bool sx126x_is_receiving(const struct device *dev)
|
||||
/* Primary source of truth: software latch set by the work handler on
|
||||
* HEADER_VALID, cleared by terminal events (RX_DONE / CRC_ERR /
|
||||
* RX_TX_TIMEOUT) and RX (re)start sites. Covers the entire payload
|
||||
- * phase without an SPI access. */
|
||||
+ * phase without an SPI access.
|
||||
+ *
|
||||
+ * Bounded by a payload deadline: in continuous RX no terminal IRQ is
|
||||
+ * guaranteed, so a HEADER_VALID that never completes would otherwise pin
|
||||
+ * this gate true forever and mute TX until reboot. */
|
||||
if (data->rx_packet_active) {
|
||||
- return true;
|
||||
+ uint32_t seen = (uint32_t)atomic_get(&data->header_seen_at_ms);
|
||||
+ uint32_t now = k_uptime_get_32();
|
||||
+
|
||||
+ if (seen == 0 || (now - seen) < sx126x_max_payload_ms(data)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) {
|
||||
+ /* Chip is busy elsewhere; re-check on the next poll. */
|
||||
+ return true;
|
||||
+ }
|
||||
+ LOG_WRN("RX latch stuck %u ms after HEADER_VALID, releasing TX gate",
|
||||
+ now - seen);
|
||||
+ /* Drop the sticky reception bits so the next poll starts clean. */
|
||||
+ sx126x_clear_irq_status(dev, SX126X_IRQ_PREAMBLE_DETECTED |
|
||||
+ SX126X_IRQ_SYNC_WORD_VALID |
|
||||
+ SX126X_IRQ_HEADER_VALID |
|
||||
+ SX126X_IRQ_HEADER_ERR);
|
||||
+ sx126x_reset_rx_busy_signals(data);
|
||||
+ k_mutex_unlock(&data->lock);
|
||||
+ return false;
|
||||
}
|
||||
|
||||
if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) {
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h
|
||||
index 02bbaac..762f8eb 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x.h
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.h
|
||||
@@ -98,6 +98,14 @@ struct sx126x_data {
|
||||
* terminal-event handler, and on TX-state entry. */
|
||||
atomic_t preamble_seen_at_ms;
|
||||
|
||||
+ /* Timestamp (k_uptime_get_32() units, ms) at which rx_packet_active was
|
||||
+ * promoted by HEADER_VALID. Zero means "no payload phase being timed".
|
||||
+ * Bounds the latch's lifetime: continuous RX has no symbol timer, so a
|
||||
+ * HEADER_VALID whose packet never completes produces no terminal IRQ and
|
||||
+ * would keep the TX gate closed forever. sx126x_is_receiving() releases
|
||||
+ * the latch once sx126x_max_payload_ms() has elapsed. */
|
||||
+ atomic_t header_seen_at_ms;
|
||||
+
|
||||
uint32_t dc_rx_time; /* stored duty cycle rx period (15.625us steps) */
|
||||
uint32_t dc_sleep_time; /* stored duty cycle sleep period (15.625us steps) */
|
||||
|
||||
Reference in New Issue
Block a user