diff --git a/README.md b/README.md index 08e3986..f7f97a0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Other benefits: | **Heltec Wireless Tracker V2** | ESP32-S3FN8 | SX1262 + KCT8103L PA/FEM | ST7735R 160x80 TFT, UC6580 GNSS, battery ADC | | **ThinkNode M9** | ESP32-S3 | LR1110 | ST7789 320x240 TFT, CC1167Q GPS, PCF8563 RTC, buzzer, 16MB flash, 8MB PSRAM | | **LilyGo T-Beam v1.2** | ESP32 (PICO-D4) | SX1262 | AXP2101 PMU, GNSS, USB-UART CLI | -| **TTGO LoRa32** | ESP32 (PICO-D4) | **SX1276** (loramac-node backend) | SX127x reference board, USB-UART CLI | +| **TTGO LoRa32** | ESP32 (PICO-D4) | **SX1276** (loramac-node backend) | SX127x reference board — **source-only, no published firmware** | ### Other diff --git a/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md index 58b4aa5..2cfabff 100644 --- a/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md @@ -245,11 +245,15 @@ Proposed by **bisbille** ([@bisbille](https://github.com/bisbille)) — Housekeeping, listed for completeness — nothing here changes how a node behaves day to day. -- **The XIAO nRF54L15 now ships firmware with every release.** The board has been supported for a - while but was never part of the published build, so it had to be compiled by hand. Companion and - repeater images are now built and listed alongside every other board. Like the ME25LS02 it is a - `.hex` flashed over SWD — though the XIAO's own USB cable is enough, since it has a debug bridge - built into the board. +- **Three boards that were supported but never published now ship firmware with every release.** All + three have been buildable for a while but were missing from the published build, so they had to be + compiled by hand. Companion and repeater images are now built and listed alongside every other + board: + - **XIAO nRF54L15** — like the ME25LS02, a `.hex` flashed over SWD, though the XIAO's own USB cable + is enough since it has a debug bridge built into the board. + - **XIAO MG24** — also SWD-only, and the first Silicon Labs board to get published firmware. + - **LoRa-E5 mini** — SWD-only. No Bluetooth on this chip, so the companion talks to the app over + the USB serial port instead. - **The Zephyr operating system underneath was updated** to a newer snapshot, and our radio patches were reorganised on top of it: five separate SX126x patches are now one. Same behaviour, fewer things to go wrong the next time Zephyr moves. diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md index 0f783b5..e6a0591 100644 --- a/zephcore/ARCHITECTURE.md +++ b/zephcore/ARCHITECTURE.md @@ -1221,10 +1221,10 @@ Timeouts and deadlines that are watchdogs in everything but name: | Mechanism | Location | Bounds | |-----------|----------|--------| -| RX-latch payload deadline | all three custom radio paths: SX126x `patch 0013`, `lr11xx_lora.c`, `lr20xx_lora.c` (`header_seen_at_ms` + `*_max_payload_ms()`) | A `HEADER_VALID` whose packet never completes would pin the TX gate closed and silently mute the node — continuous RX has no symbol timer. Released at 255-byte airtime +25% +100 ms. See [5.2.1](#521-rx-busy-gate-tx-during-rx-prevention). | +| RX-latch payload deadline | all three custom radio paths: SX126x `patch 0003` ("Bound the lifetime of the RX-busy latch"), `lr11xx_lora.c`, `lr20xx_lora.c` (`header_seen_at_ms` + `*_max_payload_ms()`) | A `HEADER_VALID` whose packet never completes would pin the TX gate closed and silently mute the node — continuous RX has no symbol timer. Released at 255-byte airtime +25% +100 ms. See [5.2.1](#521-rx-busy-gate-tx-during-rx-prevention). | | Stuck-DIO1 counter | `lr11xx_lora.c` and `lr20xx_lora.c` | 5 empty DIO1 cycles → hardware reset. Counting rather than timing; on the LR11xx it complements the wedge watchdog rather than replacing it. | | CAD timeout | `Dispatcher::checkSend()` | 4 s (~20 retry attempts) → `ERR_EVENT_CAD_TIMEOUT` + `recoverRxState()`, rather than falling through to TX. See [5.2.2](#522-cad-timeout-recovery). | -| Chip-side TX timeout | SX126x `SetTx` deadline (`patch 0014`, airtime +25% +500 ms, floored at 10 s, clamped 262143 ms); LR2021 `TIMEOUT` IRQ handler | The chip stops the transmission when this fires, so a fixed value is a truncation, not a safeguard — at SF12/BW62.5 the old flat 10 s cut every packet from 76 bytes up. | +| Chip-side TX timeout | SX126x `SetTx` deadline (`patch 0003`, "Scale the chip-side Tx timeout from airtime instead of a fixed 10 s"; airtime +25% +500 ms, floored at 10 s, clamped 262143 ms); LR2021 `TIMEOUT` IRQ handler | The chip stops the transmission when this fires, so a fixed value is a truncation, not a safeguard — at SF12/BW62.5 the old flat 10 s cut every packet from 76 bytes up. | | Serial partial-frame resync | `SerialCompanionTransport.c` (`FRAME_PARTIAL_TIMEOUT_MS`) | 2 s. Parser-level only — deliberately **not** a session or idle timeout; an idle-but-connected companion sits in `RX_IDLE` indefinitely. | | TCP send timeout | `LinuxTCPTransport.c` | Native sim only. A peer that can't accept a frame in the window is wedged → close it, rather than hang the whole queue. | | Bounded RXTO wait | `patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch` | `uarte_pm_suspend()` busy-waits for RXTO with no timeout upstream. Landing in the STOPRX race with bytes in flight spins forever on the main thread and wedges the entire mesh (observed: RAK3401 1W repeater on 1.16.6, CLI answering only `-> busy`). Backstop for the GPS UART PM path in [7.3](#73-gps-adaptersgps). | diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index ce5f0e9..a503609 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -1170,8 +1170,15 @@ bool LoRaRadioBase::isReceiving() void LoRaRadioBase::agcMaintenance() { - /* Never mid-transmit: both operations sleep the chip. */ - if (atomic_get(&_tx_active)) { + /* Never mid-transmit or mid-receive: both operations warm-sleep the + * chip, which aborts a TX and destroys an in-flight packet. The RX + * half cannot be inferred from the activity counters below — those + * only move at RX_DONE/CRC_ERR, so a packet whose preamble is landing + * right now still reads as silence. isReceiving() is the latch that + * knows (HEADER_VALID promotion + preamble grace); it is the same gate + * checkSend() and the noise-floor sampler use before touching the chip. + * Bailing here just defers the work to the next maintenance pass. */ + if (atomic_get(&_tx_active) || isReceiving()) { return; } diff --git a/zephcore/adapters/sensors/t1000e_analog.c b/zephcore/adapters/sensors/t1000e_analog.c index 509b850..98f39c8 100644 --- a/zephcore/adapters/sensors/t1000e_analog.c +++ b/zephcore/adapters/sensors/t1000e_analog.c @@ -63,10 +63,12 @@ static const uint32_t ntc_res[NTC_TABLE_LEN] = { * Photocell * * Seeed maps the divider voltage onto 0-100 with a dead band at each end. - * LIGHT_SPAN_MV is deliberately not (LIGHT_MAX_MV - LIGHT_MIN_MV): the - * stock firmware divides the 80..2480 mV range by 2400 while subtracting - * only the 80 mV floor, and the top of the range is clamped rather than - * reached. Reproduced as-is so readings match. + * LIGHT_SPAN_MV happens to equal LIGHT_MAX_MV - LIGHT_MIN_MV today, but it is + * kept as its own constant rather than derived from them: the stock firmware + * treats the divisor as an independent number (subtract the 80 mV floor, + * divide by 2400, clamp at the top rather than reach it). Deriving it would + * let a future tweak to either endpoint silently move the scale factor away + * from what the stock firmware uses, and the readings would stop matching. * ================================================================ */ #define LIGHT_MIN_MV 80 @@ -198,6 +200,14 @@ static int t1000e_power(const struct t1000e_config *cfg, bool on) int rc = 0; if (on) { + /* The rail is shared with the battery divider (one devicetree node, + * two labels: sensor_power / vbat_enable). The regulator core + * refcounts, so if another holder already has it up there is no + * off->on transition to wait out — settling only costs time when we + * are the one turning it on. */ + bool already_on = (cfg->power_supply != NULL) && + regulator_is_enabled(cfg->power_supply); + if (cfg->power_supply != NULL) { rc = regulator_enable(cfg->power_supply); if (rc < 0) { @@ -210,7 +220,9 @@ static int t1000e_power(const struct t1000e_config *cfg, bool on) return rc; } } - k_msleep(cfg->settle_time_ms); + if (!already_on) { + k_msleep(cfg->settle_time_ms); + } return 0; } diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index a72bf92..fea453a 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -1117,6 +1117,13 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len) if (ack == 0) ack = 1; sendPacketSent(MSG_SEND_SENT_DIRECT, ack, 3000); + /* One slot, not a queue: a second v-contact message inside + * VCONTACT_CONFIRM_DELAY_MS would overwrite the first ack before + * its work ever ran, so that message stayed un-confirmed until the + * app hit est_timeout and resent it. Flush the pending one first — + * it is already past the sub-millisecond window that made deferring + * necessary, so emitting it now is safe. */ + vcontactFlushConfirm(); _vcontact_confirm_ack = ack; k_work_reschedule(&_vcontact_confirm_work.work, K_MSEC(VCONTACT_CONFIRM_DELAY_MS)); @@ -2169,6 +2176,11 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) _contact_iter_active = false; cancelSyncPending(); cleanupSignState(); + /* Drop a deferred v-contact confirm from the previous session: its ack + * matches an outbound message the new session never sent, so delivering + * it here would push an unmatched SEND_CONFIRMED into a fresh app. */ + _vcontact_confirm_ack = 0; + k_work_cancel_delayable(&_vcontact_confirm_work.work); /* New session: suppress v-contact notice MSG_WAITING until the initial * sync (contacts + messages) completes at PACKET_NO_MORE_MSGS. */ diff --git a/zephcore/boards/esp32/ttgo_lora32/board.conf b/zephcore/boards/esp32/ttgo_lora32/board.conf index a3430c7..22e8e8f 100644 --- a/zephcore/boards/esp32/ttgo_lora32/board.conf +++ b/zephcore/boards/esp32/ttgo_lora32/board.conf @@ -7,9 +7,15 @@ # # Include order: prj.conf → zephcore_common.conf → esp32_common.conf → board.conf -# Board identification -CONFIG_ZEPHCORE_BOARD_NAME="TTGO LoRa32" -CONFIG_BT_DIS_MODEL_NUMBER_STR="TTGO LoRa32" +# Board identification. +# "ttgo_lora32" is the upstream Zephyr board name and stays as-is, but the name +# the node reports follows Arduino MeshCore: LilyGoTLoraBoard::getManufacturerName() +# in variants/lilygo_tlora_v2_1 returns exactly this string, so a ZephCore node +# and a stock MeshCore node identify themselves identically on the mesh. +# (MeshCore's web flasher lists the same hardware under a third spelling, +# "LilyGo LoRa32 V2.1_1.6" — irrelevant here, we publish no firmware for it.) +CONFIG_ZEPHCORE_BOARD_NAME="LILYGO T-LoRa V2.1-1.6" +CONFIG_BT_DIS_MODEL_NUMBER_STR="LILYGO T-LoRa V2.1-1.6" # Radio: SX1276 via Zephyr loramac-node driver (not the native SX126x driver) # zephcore_common.conf sets CONFIG_LORA_MODULE_BACKEND_NATIVE=y for SX126x boards. @@ -26,14 +32,21 @@ CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=17 # lora_recv_async, but disable it to avoid the spurious attempt) CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=n -# Companion RAM budget — same constraint as the T-Beam: the classic ESP32's -# contiguous DRAM segment can't fit the default contact/queue arrays (350/256) -# alongside the BT controller heap. The T-Beam sizes (160/8/128) still -# overflow here by ~1.2 KB — the SX1276 loramac-node backend carries more -# static DRAM than the native SX126x driver — so contacts go to 150. -CONFIG_ZEPHCORE_MAX_CONTACTS=150 +# Companion RAM budget — the binding constraint on this board, and tighter than +# the T-Beam's. The classic ESP32's contiguous DRAM segment cannot fit the +# default contact/queue arrays (350/256) alongside the BT controller heap, and +# this board pays for an SSD1306 display and the SX1276 loramac-node backend on +# top, neither of which the T-Beam carries. +# +# Measured, not guessed (ttgo_lora32/esp32/procpu companion, dram0_0_seg is +# 140452 B): 150/128 overflows by 32016 B. A contact costs ~188 B and an +# offline-queue slot 173 B (QueuedFrame: 1 + 172), so the two arrays are the +# whole lever. 50/32 lands at ~97% with a few KB to spare; raising either one +# spends that margin at those rates. Do not restore 150/128 — it has never +# linked on this board. +CONFIG_ZEPHCORE_MAX_CONTACTS=50 CONFIG_ZEPHCORE_MAX_CHANNELS=8 -CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=128 +CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=32 # Flash mode: ESP32-PICO-D4 (rev 1.0) — use DIO, not QIO. # esp32_common.conf enables QIO for the ESP32-S3/C3/C6 boards in this repo. diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md index 5da8f1e..a4c8456 100644 --- a/zephcore/boards/example_board/README.md +++ b/zephcore/boards/example_board/README.md @@ -98,7 +98,9 @@ for `0x1000`, since they use simple-boot in the release build too. ### SX127x Boards (loramac-node backend) -ZephCore supports SX1272/SX1276/SX1278 via the loramac-node backend — a separate radio path from the native SX126x driver used by all other boards. The TTGO LoRa32 is the reference implementation: +ZephCore can drive SX1272/SX1276/SX1278 via the loramac-node backend — a separate radio path from the native SX126x driver used by all other boards. The TTGO LoRa32 is the only board exercising it. + +**This path is source-only and unsupported.** No release publishes firmware for it, it is not in `build.sh` or the Mesh America catalog, and it has no RX duty cycle and no RX gain boost. Treat it as a starting point to maintain yourself, not as a supported target. | Board | Build string | Flash | |----------------|------------------------------------------------|--------------| diff --git a/zephcore/boards/supported_boards.md b/zephcore/boards/supported_boards.md index 0c7f746..5b75236 100644 --- a/zephcore/boards/supported_boards.md +++ b/zephcore/boards/supported_boards.md @@ -49,7 +49,7 @@ heltec_wireless_tracker/esp32s3/procpu heltec_wireless_tracker_v2/esp32s3/procpu thinknode_m9/esp32s3/procpu ttgo_tbeam/esp32/procpu -ttgo_lora32/esp32/procpu +ttgo_lora32/esp32/procpu # source-only, no published firmware ``` > ESP32 boards require `west blobs fetch hal_espressif` before first build. @@ -76,8 +76,21 @@ ttgo_lora32/esp32/procpu > SX1262 in `board.overlay`. Console/CLI are on `uart0` (onboard USB-UART). > > **TTGO LoRa32** (`ttgo_lora32/esp32/procpu`): classic ESP32 (PICO-D4) with -> **SX1276** — the SX127x (loramac-node backend) reference board. Console/CLI -> on `uart0`. +> **SX1276**. Console/CLI on `uart0`. +> +> **Source-only — no published firmware, and not a supported configuration.** +> It is deliberately absent from `build.sh`, the release workflow and the Mesh +> America catalog, so no release ever carries a binary for it. It is kept in the +> tree as the one exercise of the SX127x loramac-node radio path, which is old, +> lacks RX duty cycle and RX gain boost, and gets none of the work the native +> SX126x/LR11xx/LR20xx drivers do. Build it yourself if you want it; expect to +> maintain it yourself too. +> +> If you do build the companion, note it is DRAM-bound (~97% of `dram0_0_seg`) +> and capped at **50 contacts / 32 queued offline messages** — lower than any +> other companion board. The repeater has no such limit. At the 150/128 this +> board's config used to declare, it did not link at all. See `board.conf` for +> the measured budget before raising either number. ## STM32WL diff --git a/zephcore/helpers/ui-button/ui_task.c b/zephcore/helpers/ui-button/ui_task.c index 407d9a6..4841936 100644 --- a/zephcore/helpers/ui-button/ui_task.c +++ b/zephcore/helpers/ui-button/ui_task.c @@ -550,10 +550,14 @@ static void action_deep_sleep(void) * because the multi-tap filter passes an unhandled raw KEY_A through per tap * before it resolves; swallowing that would leave a double tap still acting. * - * No expiry is needed: on every board using this UI each switch case below is - * fed by the longpress or multi-tap filter, so a wake is always followed by an - * action code that clears the flag. (The two boards with direct joystick codes - * select ZEPHCORE_UI_JOYSTICK and never reach this callback.) + * No expiry is needed, but the flag must only be armed when the wake press was + * a raw code that still has an action code coming. ZEPHCORE_UI_DESIGN_JOYSTICK + * depends on ZEPHCORE_ROLE_COMPANION, so the repeater build of a joystick board + * (wio_tracker_l1, gat562_30s — both shipped as repeater artifacts) falls back + * to ZEPHCORE_UI_DESIGN_BUTTON and *does* reach this callback, with its + * joystick GPIOs wired straight to INPUT_KEY_UP/DOWN/LEFT/RIGHT/ENTER and no + * filter in between. There the wake press is already an action code and is + * consumed at the wake, so the flag stays clear and the next press acts. */ static bool display_woken_pending; @@ -636,10 +640,17 @@ static void ui_input_cb(struct input_event *evt, void *user_data) #ifdef CONFIG_ZEPHCORE_UI_DISPLAY /* If display is off, wake it and consume the event. The action this * press resolves to arrives later from the longpress / multi-tap filter; - * display_woken_pending makes the switch below swallow it. */ + * display_woken_pending makes the switch below swallow it. + * + * Only arm that flag when this press really does resolve into a *later* + * action code, i.e. when the code we just consumed is a raw one. Boards + * that wire an action code straight to the GPIO (joystick lines on + * wio_tracker_l1 / gat562_30s in repeater builds) already consumed their + * action here — arming the flag would make the next, genuinely separate + * press get swallowed instead. */ if (!mc_display_is_on()) { mc_display_on(); - display_woken_pending = true; + display_woken_pending = !is_ui_action_code(evt->code); schedule_render(); return; }