From da0450e511ea90ecda539305d80a9185cfafa17b Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:29:45 +0200 Subject: [PATCH] upstream sync + wio tracker 1w --- build.sh | 1 + docs/Repeater_CLI_commands.md | 4 +- docs/supported_boards.md | 1 + zephcore/app/CompanionMesh.cpp | 10 +- zephcore/boards/example_board/README.md | 1 + .../Kconfig.wio_tracker_l1_1w | 7 + .../wio_tracker_l1_1w/battery_curve.c | 31 ++ .../nrf52840/wio_tracker_l1_1w/board.cmake | 14 + .../nrf52840/wio_tracker_l1_1w/board.conf | 34 ++ .../nrf52840/wio_tracker_l1_1w/board.overlay | 34 ++ .../nrf52840/wio_tracker_l1_1w/board.yml | 10 + .../wio_tracker_l1_1w_nrf52840-pinctrl.dtsi | 125 +++++ .../wio_tracker_l1_1w_nrf52840.dts | 426 ++++++++++++++++++ .../wio_tracker_l1_1w_nrf52840.yaml | 20 + .../wio_tracker_l1_1w_nrf52840_defconfig | 21 + zephcore/helpers/CommonCLI.cpp | 21 +- zephcore/src/StaticPoolPacketManager.cpp | 13 +- 17 files changed, 765 insertions(+), 8 deletions(-) create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/Kconfig.wio_tracker_l1_1w create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/battery_curve.c create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/board.cmake create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/board.conf create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/board.overlay create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/board.yml create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.dts create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.yaml create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840_defconfig diff --git a/build.sh b/build.sh index 78ed719..474cf55 100644 --- a/build.sh +++ b/build.sh @@ -9,6 +9,7 @@ nRF_boards=( rak4631 rak3401_1watt wio_tracker_l1 + wio_tracker_l1_1w t1000_e meshtracker_x1 thinknode_m1 diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md index b8fd21a..d996c8b 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -322,8 +322,8 @@ four radio parameters together, since they are one interop-critical set. | `set tx ` | −9 to board max (default 30) | Set TX power | | `set lat ` | | Set stored latitude | | `set lon ` | | Set stored longitude | -| `set dutycycle ` | 1–100 | Set duty cycle percentage (converted to airtime factor internally) | -| `set af ` | float | Set raw airtime factor directly | +| `set dutycycle ` | 10–100 | Set duty cycle percentage (converted to airtime factor internally as `(100/pct)-1`). The floor is 10%, not Arduino's 1%: the stored airtime factor is bounded to 0–9 on load, so a lower duty cycle would run until the next reboot and then silently become 10%. | +| `set af ` | float, 0–9 | Set raw airtime factor directly. Out-of-range values are rejected: the duty cycle is computed as `100/(af+1)`, so `af = -1` is a divide-by-zero and `af < -1` wraps negative. | | `set txdelay ` | | Accepted for prefs compatibility — **ignored** (txdelay is adaptive) | | `set rxdelay ` | | Accepted for prefs compatibility — **ignored** (rxdelay is adaptive) | | `set direct.txdelay ` | | Accepted for prefs compatibility — **ignored** (direct.txdelay is adaptive) | diff --git a/docs/supported_boards.md b/docs/supported_boards.md index f81a7b2..2b16f7b 100644 --- a/docs/supported_boards.md +++ b/docs/supported_boards.md @@ -8,6 +8,7 @@ Board strings for use with `west build -b zephcore`. rak4631 rak3401_1watt wio_tracker_l1 +wio_tracker_l1_1w t1000_e meshtracker_x1 thinknode_m1 diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 0bedc5f..de19d91 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -3709,8 +3709,14 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) sendPacketError(ERR_NOT_FOUND); } else if (data_type == DATA_TYPE_RESERVED) { sendPacketError(ERR_ILLEGAL_ARG); - } else if (payload_len > MAX_CHANNEL_DATA_LENGTH) { - LOG_WRN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH); + } else if (payload_len > MAX_GROUP_DATA_LENGTH) { + /* Radio-side bound (165), NOT the host-frame bound (167). These + * disagree, and sendGroupData() enforces the smaller one, so gating + * here on MAX_CHANNEL_DATA_LENGTH let a 166/167-byte payload pass the + * frame check, fail the radio check, and come back to the app as + * ERR_TABLE_FULL — the "outbound queue full, retry later" code — for a + * send that can never succeed at any queue depth. Upstream 0a82fcd2. */ + LOG_WRN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH); sendPacketError(ERR_ILLEGAL_ARG); } else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) { sendPacketOk(); diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md index 7dce460..eafd8b9 100644 --- a/zephcore/boards/example_board/README.md +++ b/zephcore/boards/example_board/README.md @@ -12,6 +12,7 @@ Supported Boards | RAK4631 | `west build -b rak4631 zephcore` | UF2 drag-drop or `west flash` | | RAK3401 1W | `west build -b rak3401_1watt zephcore` | UF2 drag-drop or `west flash` | | Wio Tracker L1 | `west build -b wio_tracker_l1 zephcore` | UF2 drag-drop or `west flash` | +| Wio Tracker L1 Pro 1W | `west build -b wio_tracker_l1_1w zephcore` | UF2 drag-drop or `west flash` | | SenseCAP Solar | `west build -b sensecap_solar zephcore` | UF2 drag-drop or `west flash` | | XIAO nRF52840 | `west build -b xiao_nrf52840 zephcore` | UF2 drag-drop or `west flash` | | ProMicro SX1262 | `west build -b promicro_sx1262 zephcore` | UF2 drag-drop or `west flash` | diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/Kconfig.wio_tracker_l1_1w b/zephcore/boards/nrf52840/wio_tracker_l1_1w/Kconfig.wio_tracker_l1_1w new file mode 100644 index 0000000..5329578 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/Kconfig.wio_tracker_l1_1w @@ -0,0 +1,7 @@ +# Copyright (c) 2025 MeshCore +# +# SPDX-License-Identifier: MIT + +config BOARD_WIO_TRACKER_L1_1W + select SOC_NRF52840_QIAA + select ZEPHCORE_UI_JOYSTICK diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/battery_curve.c b/zephcore/boards/nrf52840/wio_tracker_l1_1w/battery_curve.c new file mode 100644 index 0000000..01f5eeb --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/battery_curve.c @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: MIT + * + * Wio Tracker L1 Pro 1W battery curve — single-cell LiPo, 21 points at 5% steps. + * + * Inherited verbatim from boards/nrf52840/wio_tracker_l1. The divider and + * multiplier are identical on the Pro 1W (BAT_CTL P0.04, VBAT P0.31, + * ADC_MULTIPLIER 2.0 in both MeshCore's and Meshtastic's variants, matching + * vbat-mv-multiplier = 7236 here), so the same curve applies -- but the + * 4190 mV ceiling was OBSERVED on the stock L1, not on this board. The Pro 1W + * charges through a BQ25616 rather than the stock part, so re-measure the + * ceiling before treating the top of this table as this board's. + * + * The discharge shape below 4190 mV follows the generic LiPo profile. + */ + +#include "battery_curve.h" + +static const uint16_t ocv_wio_tracker_l1_1w[21] = { + 4190, 4120, 4050, 4020, 3990, /* 100 .. 80% */ + 3940, 3890, 3845, 3800, 3760, /* 75 .. 55% */ + 3720, 3675, 3630, 3580, 3530, /* 50 .. 30% */ + 3475, 3420, 3360, 3300, 3200, /* 25 .. 5% */ + 3100, /* 0% */ +}; + +const battery_curve_t battery_curve_default = { + .ocv_mv = ocv_wio_tracker_l1_1w, + .num_points = 21, + .num_cells = 1, +}; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.cmake b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.cmake new file mode 100644 index 0000000..a22c84a --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.cmake @@ -0,0 +1,14 @@ +# Copyright (c) 2025 ZephCore +# +# SPDX-License-Identifier: MIT + +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") +board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") + +set(OPENOCD_NRF5_SUBFAMILY "nrf52") + +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) +include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake) diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.conf b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.conf new file mode 100644 index 0000000..a3dcd64 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.conf @@ -0,0 +1,34 @@ +# Wio Tracker L1 Pro 1W (nRF52840 + SX1262 + 1 W external PA) +# Board-specific configuration - pins and unique features only +# +# Hardware: +# - SX1262 LoRa on SPI2 (P1.14 CS, P1.07 RST, P1.10 BUSY, P0.07 DIO1) +# NO external RXEN on this board - DIO2 drives the whole antenna switch +# - 1 W PA behind an LDO on P0.14 (LORA_PWR_EN), held on by a devicetree +# regulator; see the DTS for why it is never switched off +# - L76KB GPS on UART0 (P0.26/P0.27) +# - P25Q16H 2MB QSPI flash +# - Battery ADC on AIN7 (P0.31), enable on P0.04 +# - SH1106 OLED on I2C0 (P0.05/P0.06, addr 0x3D) +# - Grove I2C1 on P1.11/P1.12, 5 V boost on P0.13 (parked off) +# - Joystick + user button + buzzer (P1.00) + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1 Pro 1W" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker L1 Pro 1W" + +# SoftDevice firmware ID (s140 v7.3.0 - Seeed bootloader, same as the stock L1) +CONFIG_ZEPHCORE_SD_FWID=0x0123 + +# TX power is left at the 22 dBm default, i.e. the full chip output into the +# PA. Both upstreams do the same on this board and neither states a PA input +# spec: MeshCore's env sets LORA_TX_POWER=22, Meshtastic sets +# SX126X_MAX_POWER 22 (plus SX126X_NO_POWER_OPTIMIZATION_TABLE, which only +# turns off RadioLib's own current-limit table and has no ZephCore analogue). +# UNVERIFIED on hardware. If a datasheet or a measurement later shows the PA +# saturates below 22 - the shape the W12 measurement found, see +# memory/meshnology-w12-port.md - set CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM +# and CONFIG_ZEPHCORE_MAX_TX_POWER_DBM here rather than leaving the knob at +# the ceiling. diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.overlay b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.overlay new file mode 100644 index 0000000..b3d4d0e --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.overlay @@ -0,0 +1,34 @@ +/* + * Wio Tracker L1 Pro 1W overlay for ZephCore. + * SoftDevice + UF2 bootloader layout. + * Console: USB CDC ACM. + * Battery: AIN7 = P0.31, voltage divider 2:1. + */ + +/* GPS standby control — P1.09 = the L76K's WAKEUP pin (schematic net + * GNSS_WAKEUP). This is NOT a supply switch: per the L76K hardware design the + * pin only enters/exits Standby, where the RF is off but the core and I/O + * domain stay powered. VCC is never removed, so ephemeris/almanac/RTC are + * retained and each wake is a warm start. Active low with an internal + * pull-up, which is why the module runs even with this pin unconfigured. + * + * The luatos,air530z driver also manages this pin via on-off-gpios, but we + * need our own alias so ZephyrSensorManager can toggle it independently + * (driver PM suspend/resume hangs on modem_pipe ops). */ +/ { + gps_en: gps-enable { + compatible = "gpio-leds"; + gps_enable_pin: gps_enable { + gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; + label = "GPS Enable"; + }; + }; + + aliases { + gps-enable = &gps_enable_pin; + }; +}; + +/* Internal LFS mounted manually by ZephyrDataStore (block_size=128, custom erase) */ +/* External QSPI /ext auto-mounted via DTS (standard 4KB blocks) */ +#include "../../common/qspi-ext.dtsi" diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.yml b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.yml new file mode 100644 index 0000000..9504103 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/board.yml @@ -0,0 +1,10 @@ +# Copyright (c) 2025 MeshCore +# +# SPDX-License-Identifier: MIT + +board: + name: wio_tracker_l1_1w + full_name: Seeed Wio Tracker L1 Pro 1W + vendor: seeed + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi new file mode 100644 index 0000000..d97d742 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi @@ -0,0 +1,125 @@ +/* + * Seeed Wio Tracker L1 pin control definitions + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: MIT + * + * Pin mappings from Arduino variant.cpp: + * - SPI2 (LoRa): SCK=P0.30, MISO=P0.03, MOSI=P0.28, CS=P1.14 + * - I2C0 (OLED): SDA=P0.06, SCL=P0.05 + * - I2C1 (Grove): SDA=P1.12 (D18), SCL=P1.11 (D17) + * - UART (GPS): TX=P0.27, RX=P0.26 + * - QSPI: SCK=P0.21, CS=P0.25, IO0=P0.20, IO1=P0.24, IO2=P0.22, IO3=P0.23 + */ + +&pinctrl { + /* SPI2 (SPIM2) for LoRa SX1262 — dedicated SPI-only instance so it + * doesn't collide with I2C0 (instance 0) or the Grove I2C1 (instance 1). */ + spi2_default: spi2_default { + group1 { + psels = , + , + ; + }; + }; + + spi2_sleep: spi2_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + /* I2C0 for OLED display */ + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* I2C1 for Grove connector — SDA=P1.12 (D18), SCL=P1.11 (D17) + * to match the physical Grove pinout (Arduino Wire1 / Meshtastic). */ + i2c1_default: i2c1_default { + group1 { + psels = , + ; + }; + }; + + i2c1_sleep: i2c1_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* UART0 for GPS L76KB at 9600 baud + * P0.27 = GNSS_TX (GPS sends to MCU) + * P0.26 = GNSS_RX (MCU sends to GPS) + */ + uart0_default: uart0_default { + group1 { + psels = , + ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* PWM0 for buzzer on P1.00 */ + pwm0_default: pwm0_default { + group1 { + psels = ; + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + /* QSPI for external flash */ + qspi_default: qspi_default { + group1 { + psels = , + , + , + , + , + ; + }; + }; + + qspi_sleep: qspi_sleep { + group1 { + psels = , + , + , + , + , + ; + low-power-enable; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.dts b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.dts new file mode 100644 index 0000000..e5d8d63 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.dts @@ -0,0 +1,426 @@ +/* + * Seeed Wio Tracker L1 - nRF52840 + SX1262 + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: MIT + * + * Hardware: + * - nRF52840 SoC with BLE + * - SX1262 LoRa radio + 1 W external PA (DIO2 as RF switch, TCXO 1.8V) + * - P25Q16H 2MB QSPI flash + * - SH1106 OLED display (I2C 0x3D) + * - L76KB GPS module + * - Joystick + user button + * - Battery voltage ADC + * + * Differences from the stock wio_tracker_l1, all confirmed against BOTH + * MeshCore's variants/wio-tracker-l1-1w and Meshtastic's + * variants/nrf52840/seeed_wio_tracker_L1_Pro_1W pin maps: + * + * P0.14 LORA_PWR_EN new: LDO enable for the SX1262 + 1 W PA (hog, below) + * P0.13 BOOST_EN new: Grove 5 V boost enable (hog low, below) + * P1.15 BQ25616 STAT new: charge status, active LOW - see note below + * P0.29 LORA_VDET replaces the stock board's P1.08 LORA_SW - NOT an + * RF-switch line, see the &spi2 lora node + * (no EInk: the stock board's EInk header pins D31..D37 are reused above) + * + * The orange mesh LED stays on P1.01. MeshCore's variant.cpp annotates it + * "Pro 1W: P1.01 (not P1.15)", which reads like a move, but the pin-map VALUE + * is 33 in both files - only the net NAME changed (User LED -> Mesh_LED). + * Meshtastic has 33 as well. Nothing to change here. + * + * P1.15 (BQ25616 STAT) is deliberately not bound. ZephCore has no charge-state + * consumer: mesh::ZephyrBoard::isExternalPowered() reads the nRF52 POWER + * peripheral's VBUS detect directly, which works on this board without a pin. + * Same treatment as thinknode_m9's charge-DONE line. + */ + +/dts-v1/; +#include +#include +#include +#include +#include +#include +#include "wio_tracker_l1_1w_nrf52840-pinctrl.dtsi" + +/* Delete default partitions from nrf52840_partition.dtsi */ +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + model = "Seeed Wio Tracker L1 Pro 1W"; + compatible = "seeed,wio-tracker-l1-1w"; + + chosen { + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,display = &sh1106; + }; + + leds { + compatible = "gpio-leds"; + + user_led: led_0 { + gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; + label = "User LED"; + }; + }; + + buttons: buttons { + compatible = "gpio-keys"; + + user_button: button_0 { + gpios = <&gpio0 8 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "User Button"; + }; + + joystick_up: button_1 { + gpios = <&gpio1 4 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Up"; + }; + + joystick_down: button_2 { + gpios = <&gpio0 12 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Down"; + }; + + joystick_left: button_3 { + gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Left"; + }; + + joystick_right: button_4 { + gpios = <&gpio1 3 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Right"; + }; + + joystick_press: button_5 { + gpios = <&gpio1 5 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Press"; + }; + }; + + /* Input filter: user button long-press detection (1000ms) */ + user_btn_longpress { + compatible = "zephyr,input-longpress"; + input = <&buttons>; + input-codes = ; + short-codes = ; + long-codes = ; + long-delay-ms = <1000>; + }; + + /* Input filter: user button multi-tap detection + * 1 tap (400ms wait) → KEY_1 = page next on OLED + * 2 taps (400ms wait) → KEY_B = LED heartbeat toggle + * 3 taps (400ms wait) → KEY_D = buzzer mute toggle + * 4 taps (400ms wait) → KEY_C = GPS on/off + * 5 taps (immediate) → KEY_E = flood advert */ + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + /* No 'input' phandle — listen to ALL devices. + * KEY_A is emitted by the longpress pseudo-device, + * not by &buttons, so we must not filter by device. */ + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + + /* P0.14 LORA_PWR_EN - external LDO feeding the SX1262 AND the 1 W PA. + * The radio is dead without it, so it must be up before the LoRa driver + * probes: REGULATOR_FIXED_INIT_PRIORITY is 75, LORA_INIT_PRIORITY is 90. + * Same shape as rak3401_1watt's lora_pwr_enable, which gates a SKY66122 + * the same way. + * + * Never turned off. ZephCore keeps the radio in RX 24/7 and deliberately + * does not PM-suspend it, and there is no fem_txgain pref in this tree to + * expose the rail as a runtime knob (MeshCore's variant drives it from + * `set radio.fem.txgain`; ZephCore only has fem_rxgain, and that acts on + * lna-bypass-gpios, a different pin class entirely). */ + lora_pwr_enable: lora-pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "lora-pwr-enable"; + enable-gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>; + regulator-boot-on; + /* Documents the LDO + PA settle requirement but does not enforce + * it: regulator-boot-on takes regulator_common_init()'s + * refcount-only branch, so regulator_delay() never runs. The real + * settle time comes from the SX126x driver's own reset (5 ms + * NRESET pulse + 5 ms wait + BUSY poll) 15 init priorities later. + * See the identical note on rak3401_1watt. */ + startup-delay-us = <10000>; + }; + + /* Battery voltage enable and ADC - on-demand only to save power */ + vbat_enable: vbat-enable { + compatible = "regulator-fixed"; + regulator-name = "vbat-enable"; + enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>; + /* NOT regulator-boot-on - enabled on-demand in ZephyrBoard.cpp */ + }; + + /* Battery ADC channel reference for ZephyrBoard */ + zephyr,user { + io-channels = <&adc 7>; + vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */ + }; + + /* Buzzer on PWM0 channel 0 (P1.00) - no enable pin needed */ + pwmbuzzer { + compatible = "pwm-leds"; + buzzer: buzzer { + pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + aliases { + led0 = &user_led; + sw0 = &user_button; + lora0 = &lora; + watchdog0 = &wdt0; + buzzer = &buzzer; + }; +}; + +®0 { + status = "okay"; +}; + +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* Battery voltage on P0.31 (AIN7) */ + channel@7 { + reg = <7>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&uicr { + nfct-pins-as-gpios; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; + + /* P0.13 BOOST_EN - Grove 5 V boost converter. Parked OFF, matching + * Meshtastic's initVariant(); nothing in ZephCore drives 5 V Grove + * peripherals, and the reset state of an nRF GPIO is disconnected, so + * leaving it unbound would float the converter's enable input. The + * Grove I2C bus itself (&i2c1, P1.11/P1.12) is unaffected - it runs at + * 3V3 and does not need the boost. */ + grove_boost_off { + gpio-hog; + gpios = <13 GPIO_ACTIVE_HIGH>; + output-low; + line-name = "grove-boost-en"; + }; +}; + +&gpio1 { + status = "okay"; + + /* NOTE: buzzer-off gpio-hog REMOVED - buzzer pin is now managed by PWM + * driver. When PWM is not driving, the pin is low (no current draw). */ + + /* NOTE: GPS enable hog REMOVED — GPS works fine without it, and the + * reason is now confirmed rather than guessed: P1.09 is the L76K's + * WAKEUP pin, which the L76K hardware design specifies as active low + * "with pull-up internally". Left unconfigured it floats high and the + * module stays in Continuous mode. */ +}; + +/* PWM0 for buzzer */ +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* GPS L76KB module - NOTE: Feb 6 working version had no GPIO control nodes. + * The GPS just worked without any power control! Keeping it simple. */ + +/* Quectel L76KB GNSS on UART0 - 9600 baud (PCAS/CASIC protocol) + * Uses Zephyr luatos,air530z driver (same PCAS command set for + * constellation config via PCAS04, fix rate via PCAS02, etc.) + * on-off-gpios = P1.09, the module's WAKEUP/standby pin (see &gpio1 note and + * board.overlay) — driver drives it high for Continuous, low for Standby. */ +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <9600>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; + /* No hw-flow-control - GPS doesn't use RTS/CTS */ + + gnss: gnss { + compatible = "luatos,air530z"; + on-off-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; + }; +}; + +/* I2C0 for onboard OLED display only. + * External environment/power sensors live on the Grove bus (&i2c1). */ +&i2c0 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + /* SH1106 sends 1 cmd + 128 data = 129 bytes per page write. + * nRF TWIM needs an internal buffer >= this size. */ + zephyr,concat-buf-size = <256>; + zephyr,flash-buf-max-size = <256>; + + sh1106: sh1106@3d { + compatible = "sinowealth,sh1106"; + reg = <0x3d>; + width = <128>; + height = <64>; + segment-offset = <2>; /* SH1106 has 2-column offset */ + page-offset = <0>; + display-offset = <0>; + multiplex-ratio = <63>; + segment-remap; + com-invdir; + prechargep = <0x22>; + }; +}; + +/* I2C1 for Grove connector — external environment/power sensors. + * Auto-detected at runtime; absent sensors fail device_is_ready() and + * are skipped harmlessly by ZephyrEnvSensors. */ +&i2c1 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c1_default>; + pinctrl-1 = <&i2c1_sleep>; + pinctrl-names = "default", "sleep"; + + /* All supported environment & power sensors — auto-detected at runtime */ + #include "../../common/sensors-i2c.dtsi" +}; + +/* SPI2 (SPIM2) for LoRa SX1262. + * Must NOT use SPIM0 (shares instance 0 with I2C0/OLED) or SPIM1 (shares + * instance 1 with TWIM1, which drives the Grove I2C bus &i2c1). SPIM2 is a + * dedicated SPI-only instance, so the radio and both I2C buses coexist. */ +&spi2 { + compatible = "nordic,nrf-spim"; + status = "okay"; + cs-gpios = <&gpio1 14 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&spi2_default>; + pinctrl-1 = <&spi2_sleep>; + pinctrl-names = "default", "sleep"; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio0 7 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + + /* NO rx-enable-gpios on this board, unlike the stock Wio Tracker + * L1 where RXEN is P1.08. DIO2 drives the whole antenna switch + * here. The pin the stock board calls LORA_SW (P1.08) is replaced + * on the Pro 1W by LORA_VDET on P0.29 - a PA power detector on + * AIN5, an ANALOG INPUT, not a switch control. + * + * MeshCore's variants/wio-tracker-l1-1w/variant.h keeps + * `#define SX126X_RXEN (5)` from the stock variant while + * remapping D5 to P0.29, so it drives the detector line as an + * RXEN output. Meshtastic's variant for the same board sets + * SX126X_RXEN / SX126X_TXEN to RADIOLIB_NC and comments + * "DIO2 controls antenna switch (no external RXEN/TXEN)". + * Meshtastic is followed here. Binding P0.29 as rx-enable-gpios + * would drive an analog detector node from a push-pull output. */ + dio2-tx-enable; + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + spi-max-frequency = <8000000>; + rx-boosted; + }; +}; + +/* QSPI Flash - P25Q16H 2MB - single-bit mode at 16MHz (matches Arduino) */ +&qspi { + status = "okay"; + pinctrl-0 = <&qspi_default>; + pinctrl-1 = <&qspi_sleep>; + pinctrl-names = "default", "sleep"; + + qspi_flash: qspi_flash@0 { + compatible = "nordic,qspi-nor"; + reg = <0>; + writeoc = "pp"; /* Single-bit page program (universal) */ + readoc = "fastread"; /* Single-bit fast read (universal) */ + sck-frequency = <16000000>; /* 16 MHz (matches Arduino) */ + jedec-id = [85 60 15]; /* P25Q16H */ + sfdp-bfp = [ + e5 20 f1 ff ff ff ff 01 44 eb 08 6b 08 3b 04 bb + ee ff ff ff ff ff 00 ff ff ff 00 ff 0c 20 0f 52 + 10 d8 00 ff + ]; + size = <16777216>; /* 2MB = 16Mbit */ + has-dpd; + t-enter-dpd = <3000>; + t-exit-dpd = <50000>; /* 50ms for reliable wakeup */ + /* quad-enable not needed for single-bit mode */ + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +/* Arduino MeshCore compatible partition layout (SoftDevice v7) */ +#include "../../common/nrf52_partitions_sdv7.dtsi" + +/* External QSPI flash partitions */ +&qspi_flash { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* Full 2MB for LittleFS storage */ + qspi_storage_partition: partition@0 { + label = "qspi_storage"; + reg = <0x00000000 0x00200000>; + }; + }; +}; +#include "../../common/nrf52_wakeup.dtsi" diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.yaml b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.yaml new file mode 100644 index 0000000..c072ca8 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840.yaml @@ -0,0 +1,20 @@ +identifier: wio_tracker_l1_1w/nrf52840 +name: Seeed Wio Tracker L1 Pro 1W +type: mcu +arch: arm +toolchain: + - zephyr + - gnuarmemb + - xtools +ram: 256 +flash: 1024 +supported: + - adc + - ble + - gpio + - i2c + - spi + - usb_device + - qspi + - lora +vendor: seeed diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840_defconfig b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840_defconfig new file mode 100644 index 0000000..76a1501 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840_defconfig @@ -0,0 +1,21 @@ +# Copyright (c) 2025 MeshCore +# SPDX-License-Identifier: MIT + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y + +# Build UF2 by default +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_USE_DT_CODE_PARTITION=y diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index a08dab2..a0d1f4f 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -713,11 +713,18 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; if (memcmp(config, "dutycycle ", 10) == 0) { + /* Floor is 10%, not Arduino's 1%: this writes airtime_factor as + * (100/dc)-1, and sanitizeNodePrefs() clamps that field to 0..9. + * A 1% duty cycle stores af=99, survives until the next load, and + * is then silently rewritten to 9 (=10%) — the node would run at + * one duty cycle and boot into another. 10% is the lowest value + * that round-trips, and it matches the 0..9 bound `set af` now + * enforces directly. */ float dc; if (!cliFloat(&config[10], 100.0f / (cliDefaults()->airtime_factor + 1.0f), &dc)) { - strcpy(reply, "ERROR: dutycycle must be 1-100, or default"); - } else if (dc < 1 || dc > 100) { - strcpy(reply, "ERROR: dutycycle must be 1-100"); + strcpy(reply, "ERROR: dutycycle must be 10-100, or default"); + } else if (dc < 10 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 10-100"); } else { _prefs->airtime_factor = (100.0f / dc) - 1.0f; savePrefs(); @@ -727,9 +734,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch snprintf(reply, CLI_REPLY_SIZE, "OK - %d.%d%%", a_int, a_frac); } } else if (memcmp(config, "af ", 3) == 0) { + /* Range-checked (upstream f722794e), and not only for tidiness: + * every role computes its duty cycle as 100/(af+1), so af=-1 is a + * live divide-by-zero and af<-1 yields a negative percentage that + * wraps through uint8_t. sanitizeNodePrefs() bounds the field to + * 0..9 but only on load, so an out-of-range value set at runtime + * is in force until the next reboot. */ float af; if (!cliFloat(&config[3], cliDefaults()->airtime_factor, &af)) { strcpy(reply, "Error: expected a number or default"); + } else if (af < 0.0f || af > 9.0f) { + strcpy(reply, "Error: af must be 0-9"); } else { _prefs->airtime_factor = af; savePrefs(); diff --git a/zephcore/src/StaticPoolPacketManager.cpp b/zephcore/src/StaticPoolPacketManager.cpp index db5290d..2bf1ca1 100644 --- a/zephcore/src/StaticPoolPacketManager.cpp +++ b/zephcore/src/StaticPoolPacketManager.cpp @@ -131,11 +131,22 @@ Packet *StaticPoolPacketManager::allocNew() void StaticPoolPacketManager::free(Packet *packet) { - if (packet) _unused.add(packet, 0, 0); + if (packet == nullptr) return; + + /* _unused is sized to hold the whole pool, so the only way this add can + * fail is a slot being returned twice. That is silent corruption — the + * second free drops the packet on the floor and the pool shrinks by one + * for the rest of the run — so say so rather than ignoring the return. + * Upstream 1d5283de. */ + if (!_unused.add(packet, 0, 0)) { + LOG_WRN("free: unused queue full, possible double-free"); + } } void StaticPoolPacketManager::queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) { + if (packet == nullptr) return; + if (_send_queue.add(packet, priority, scheduled_for)) return; /* Queue full — evict the least-important entry to make room.