sync with upstream dev

This commit is contained in:
liquidraver
2026-07-30 11:16:31 +02:00
parent cceb6606e1
commit 9354975acf
12 changed files with 347 additions and 33 deletions
+2 -2
View File
@@ -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
+10
View File
@@ -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
+20 -10
View File
@@ -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();
+6
View File
@@ -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) {
+28 -1
View File
@@ -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);
}
+4
View File
@@ -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(). */
+11 -4
View File
@@ -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;
+76
View File
@@ -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) */