diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index cee337d..7f90ece 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -171,7 +171,7 @@ set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_com # Detect platform from board name and add platform-specific common config # For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD # string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840") -if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1") +if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") @@ -396,7 +396,6 @@ else() src/main_companion.cpp adapters/ble/ZephyrBLE.cpp adapters/datastore/ZephyrDataStore.cpp - adapters/datastore/lfs_128b_erase.c helpers/BaseChatMesh.cpp helpers/TransportKeyStore.cpp helpers/ui/ui_mesh_actions.cpp diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 4e64ce6..09ac161 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -443,14 +443,14 @@ config ZEPHCORE_UI_BUZZER config ZEPHCORE_UI_DISPLAY bool "Enable display (OLED, LCD, e-ink)" - default y - depends on DISPLAY - depends on CHARACTER_FRAMEBUFFER + default y if $(dt_chosen_enabled,zephyr/display) + select DISPLAY + select CHARACTER_FRAMEBUFFER help Enable page-based UI on any Zephyr-supported display. - Auto-detects display via "zephyr,display" chosen node or - legacy nodelabels (sh1106, ssd1306). Resolution and font - are queried at runtime — no hardcoded dimensions. + Auto-enabled when "zephyr,display" chosen node exists in DT. + Auto-detects display via chosen node or legacy nodelabels + (sh1106, ssd1306). Resolution and font are queried at runtime. Supports OLED (SSD1306, SH1106), LCD, e-ink, etc. config ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 1e306e5..e116239 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -4,14 +4,14 @@ * * Universal mount strategy: * nRF52: Manual mount /efs (ExtraFS, 100KB) + /ifs (InternalFS, 28KB) - * with block_size=128 and custom erase for Arduino MeshCore compat. + * using native Zephyr LittleFS (block_size=4096). * Others: DTS-automounted /lfs (single partition). * QSPI: /ext overrides contacts mount when available. * * Data format (all platforms): - * contacts3 — 152-byte records (Arduino-compatible) - * new_prefs — Arduino-compatible byte layout - * channels2 — 68-byte records (identical to Arduino) + * contacts3 — 152-byte records + * new_prefs — byte layout + * channels2 — 68-byte records * _main.id — identity blob * adv_blobs — fixed record array */ @@ -23,9 +23,6 @@ #include #include -/* Custom 128B-block erase — defined in lfs_128b_erase.c */ -extern "C" int lfs_128b_erase(const struct lfs_config *c, lfs_block_t block); - #include LOG_MODULE_REGISTER(zephcore_datastore, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); @@ -56,27 +53,14 @@ static bool is_mounted(const char *mount_point) return fs_statvfs(mount_point, &stat) == 0; } -/* ── nRF52 dual-mount: ExtraFS + InternalFS (block_size=128) ───────── */ +/* ── nRF52 dual-mount: ExtraFS + InternalFS ────────────────────────── */ #if FIXED_PARTITION_EXISTS(extrafs_partition) && FIXED_PARTITION_EXISTS(internalfs_partition) #define HAS_NRF52_DUAL_MOUNT 1 -/* Arduino-compatible LFS: block_size=128 */ -FS_LITTLEFS_DECLARE_CUSTOM_CONFIG(extrafs_data, - 4, /* alignment */ - 16, /* read_size */ - 16, /* prog_size */ - 128, /* cache_size = block_size */ - 128 /* lookahead_size */ -); - -FS_LITTLEFS_DECLARE_CUSTOM_CONFIG(internalfs_data, - 4, /* alignment */ - 16, /* read_size */ - 16, /* prog_size */ - 128, /* cache_size = block_size */ - 128 /* lookahead_size */ -); +/* Native Zephyr LFS — block_size derived from flash erase size (4096). */ +FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(extrafs_data); +FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(internalfs_data); static struct fs_mount_t extrafs_mnt = { .type = FS_LITTLEFS, @@ -94,53 +78,56 @@ static struct fs_mount_t internalfs_mnt = { .flags = 0, }; +/* Mount a LittleFS partition, format if mount fails. Returns the mount rc. */ +static int mount_or_format(struct fs_mount_t *mnt, int partition_id) +{ + int rc = fs_mount(mnt); + + if (rc < 0) { + LOG_WRN("%s: mount failed (%d) — formatting", mnt->mnt_point, rc); + + /* A failed fs_mount may leave fs->backend set (flash_area + * opened in littlefs_init_backend but lfs_mount failed). + * Clear it so the retry doesn't hit -EBUSY. */ + struct fs_littlefs *fsd = (struct fs_littlefs *)mnt->fs_data; + if (fsd->backend) { + flash_area_close((const struct flash_area *)fsd->backend); + fsd->backend = NULL; + } + + const struct flash_area *fap; + if (flash_area_open(partition_id, &fap) == 0) { + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } + rc = fs_mount(mnt); + if (rc < 0) { + LOG_ERR("%s: mount failed after format: %d", + mnt->mnt_point, rc); + } + } + return rc; +} + static bool try_mount_nrf52_dual() { - int rc; - - /* Set custom block_size and erase callback before mount. - * The Zephyr LFS patch (0007) preserves pre-set erase callbacks. */ - extrafs_data.cfg.block_size = 128; - extrafs_data.cfg.erase = lfs_128b_erase; - - internalfs_data.cfg.block_size = 128; - internalfs_data.cfg.erase = lfs_128b_erase; - /* Mount ExtraFS — contacts, channels, blobs */ - rc = fs_mount(&extrafs_mnt); - if (rc < 0) { - LOG_ERR("ExtraFS mount failed: %d — formatting", rc); - const struct flash_area *fap; - if (flash_area_open(FIXED_PARTITION_ID(extrafs_partition), &fap) == 0) { - flash_area_flatten(fap, 0, fap->fa_size); - flash_area_close(fap); - } - rc = fs_mount(&extrafs_mnt); - if (rc < 0) { - LOG_ERR("ExtraFS mount failed after format: %d", rc); - return false; - } + if (mount_or_format(&extrafs_mnt, + FIXED_PARTITION_ID(extrafs_partition)) < 0) { + return false; } efs_mounted = true; - LOG_INF("ExtraFS at /efs (100KB, 128B blocks)"); + LOG_INF("ExtraFS at /efs: blk_sz=%u blk_cnt=%u", + extrafs_data.cfg.block_size, extrafs_data.cfg.block_count); /* Mount InternalFS — prefs, identity, BLE settings */ - rc = fs_mount(&internalfs_mnt); - if (rc < 0) { - LOG_ERR("InternalFS mount failed: %d — formatting", rc); - const struct flash_area *fap; - if (flash_area_open(FIXED_PARTITION_ID(internalfs_partition), &fap) == 0) { - flash_area_flatten(fap, 0, fap->fa_size); - flash_area_close(fap); - } - rc = fs_mount(&internalfs_mnt); - if (rc < 0) { - LOG_ERR("InternalFS mount failed after format: %d", rc); - return false; - } + if (mount_or_format(&internalfs_mnt, + FIXED_PARTITION_ID(internalfs_partition)) < 0) { + return false; } ifs_mounted = true; - LOG_INF("InternalFS at /ifs (28KB, 128B blocks)"); + LOG_INF("InternalFS at /ifs: blk_sz=%u blk_cnt=%u", + internalfs_data.cfg.block_size, internalfs_data.cfg.block_count); return true; } @@ -158,7 +145,7 @@ bool ZephyrDataStore::mount() return true; /* already mounted */ } - /* 1. Try nRF52 dual-mount (manual, block_size=128) */ + /* 1. Try nRF52 dual-mount (manual, native block_size) */ if (try_mount_nrf52_dual()) { _contacts_mnt = "/efs"; _prefs_mnt = "/ifs"; diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index abb83b3..592aded 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -3,9 +3,9 @@ * Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash * * Universal across all platforms (nRF52, ESP32, MG24, nRF54L). - * On nRF52, uses Arduino MeshCore-compatible dual-mount layout: - * /efs (ExtraFS @ 0xD4000, 100KB, block_size=128) — contacts3, channels2, blobs - * /ifs (InternalFS @ 0xED000, 28KB, block_size=128) — new_prefs, _main.id + * On nRF52, uses dual-mount layout with native Zephyr LittleFS: + * /efs (ExtraFS @ 0xD4000, 100KB) — contacts3, channels2, blobs + * /ifs (InternalFS @ 0xED000, 28KB) — new_prefs, _main.id * On other platforms, uses DTS-automounted /lfs for everything. * QSPI /ext overrides contacts path when available (any platform). */ diff --git a/zephcore/adapters/datastore/lfs_128b_erase.c b/zephcore/adapters/datastore/lfs_128b_erase.c deleted file mode 100644 index 0958622..0000000 --- a/zephcore/adapters/datastore/lfs_128b_erase.c +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * Custom LittleFS erase callback for 128-byte blocks on 4KB-erase flash. - * - * Arduino MeshCore uses LFS with block_size=128 on nRF52840 which has - * 4096-byte erase granularity. The standard Zephyr erase callback calls - * flash_area_flatten(fa, offset, 128) which fails because the flash driver - * requires page-aligned (4096-byte) erases. - * - * This callback implements read-modify-erase-write: - * 1. Read the entire 4KB page containing the 128B block - * 2. Memset the 128B region to 0xFF (erased state) - * 3. Hardware-erase the 4KB page - * 4. Write back the modified page - */ - -#include -#include -#include -#include - -#define HW_ERASE_SIZE 4096 - -/* Static 4KB buffer for read-modify-erase-write. - * Both ExtraFS and InternalFS mounts are serialized by the Zephyr FS mutex, - * so this is never accessed concurrently. */ -static uint8_t page_buf[HW_ERASE_SIZE]; - -int lfs_128b_erase(const struct lfs_config *c, lfs_block_t block) -{ - const struct flash_area *fa = (const struct flash_area *)c->context; - int rc; - - /* Offset of this LFS block within the flash area */ - size_t block_offset = block * c->block_size; - - /* Align down to the 4KB page boundary (within flash area) */ - size_t page_offset = block_offset & ~((size_t)HW_ERASE_SIZE - 1); - size_t offset_in_page = block_offset - page_offset; - - /* 1. Read entire 4KB page */ - rc = flash_area_read(fa, page_offset, page_buf, HW_ERASE_SIZE); - if (rc < 0) { - return LFS_ERR_IO; - } - - /* 2. Set the 128B block region to erased state (0xFF) */ - memset(&page_buf[offset_in_page], 0xFF, c->block_size); - - /* 3. Hardware-erase the 4KB page */ - rc = flash_area_flatten(fa, page_offset, HW_ERASE_SIZE); - if (rc < 0) { - return LFS_ERR_IO; - } - - /* 4. Write back the modified page */ - rc = flash_area_write(fa, page_offset, page_buf, HW_ERASE_SIZE); - if (rc < 0) { - return LFS_ERR_IO; - } - - return LFS_ERR_OK; -} diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index a560b99..5302b3c 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -267,12 +267,18 @@ void CompanionMesh::loop() void CompanionMesh::markContactsDirty() { - _dirty_contacts_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; + /* Only set the timer on first dirty — don't keep pushing + * the deadline forward or a busy mesh never flushes. */ + if (!_dirty_contacts_expiry) { + _dirty_contacts_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; + } } void CompanionMesh::markChannelsDirty() { - _dirty_channels_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; + if (!_dirty_channels_expiry) { + _dirty_channels_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; + } } void CompanionMesh::flushDirtyContacts() diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index eee5a88..5a6dd7c 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -137,7 +137,7 @@ uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t memcpy(client->shared_secret, secret, PUB_KEY_SIZE); if (perms != PERM_ACL_GUEST) { - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + if (!dirty_contacts_expiry) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); } } @@ -1018,7 +1018,7 @@ void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* if (hex_len > PUB_KEY_SIZE * 2) hex_len = PUB_KEY_SIZE * 2; if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + if (!dirty_contacts_expiry) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); strcpy(reply, "OK"); } else { strcpy(reply, "Err - invalid params"); diff --git a/zephcore/boards/common/nrf52_common.conf b/zephcore/boards/common/nrf52_common.conf index b606a4e..5023680 100644 --- a/zephcore/boards/common/nrf52_common.conf +++ b/zephcore/boards/common/nrf52_common.conf @@ -35,10 +35,9 @@ CONFIG_SOC_FLASH_NRF_PARTIAL_ERASE=y # Boards with QSPI (Wio Tracker L1) override in board.conf. CONFIG_NORDIC_QSPI_NOR=n -# ========== Storage: Arduino MeshCore compatible layout ========== +# ========== Storage ========== # BLE bonds via file-based settings on InternalFS (replaces NVS). -# Required for Arduino MeshCore flash layout compatibility — the NVS region -# (0xD4000-0xD6000) is now part of ExtraFS. +# No NVS partition on nRF52 — ExtraFS occupies that region. CONFIG_NVS=n CONFIG_SETTINGS_NVS=n CONFIG_SETTINGS_FILE=y diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index 605dda4..e1b3d04 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -39,12 +39,11 @@ CONFIG_LORA_MODULE_BACKEND_NATIVE=y CONFIG_REGULATOR=y # ========== Display Subsystem ========== -# When no display is in devicetree, no driver is instantiated — zero overhead. -CONFIG_DISPLAY=y -CONFIG_CHARACTER_FRAMEBUFFER=y +# Display and CFB are auto-enabled by ZEPHCORE_UI_DISPLAY (Kconfig select) +# when "zephyr,display" chosen node exists in the board's devicetree. +# No need to set CONFIG_DISPLAY or CONFIG_CHARACTER_FRAMEBUFFER here. -# Heap for CFB framebuffer allocation (cfb_framebuffer_init uses k_malloc). -# 2KB baseline — platforms with larger BLE stacks override this. +# Heap baseline — display boards override for larger framebuffers. CONFIG_HEAP_MEM_POOL_SIZE=2048 # ========== Sensor Subsystem ========== @@ -156,7 +155,6 @@ CONFIG_FLASH_MAP=y CONFIG_NVS=y CONFIG_SETTINGS=y CONFIG_SETTINGS_NVS=y -CONFIG_SETTINGS_NVS_SECTOR_COUNT=2 CONFIG_FLASH_PAGE_LAYOUT=y # LittleFS for DataStore diff --git a/zephcore/boards/example_board/board.conf b/zephcore/boards/example_board/board.conf index 8b76e46..beb0a9c 100644 --- a/zephcore/boards/example_board/board.conf +++ b/zephcore/boards/example_board/board.conf @@ -105,11 +105,11 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="My Board v1" # CRITICAL: SoftDevice version determines the app start address! # Check your board's Arduino platformio.ini for which linker script it uses: # nrf52840_s140_v6*.ld → s140 v6.1.1, app@0x26000, FWID=0x00B6 -# nrf52840_s140_v7*.ld → s140 v7.3.0, app@0x27000, FWID=0x0100 +# nrf52840_s140_v7*.ld → s140 v7.3.0, app@0x27000, FWID=0x0123 # Your board.overlay partition table MUST match (see Section 4 in board.overlay). # Wrong address = bootloop / bricked device with no USB/BLE/logs. # CONFIG_ZEPHCORE_SD_FWID=0x00B6 # SoftDevice firmware ID (s140 v6.1.1) -# CONFIG_ZEPHCORE_SD_FWID=0x0100 # SoftDevice firmware ID (s140 v7.3.0) +# CONFIG_ZEPHCORE_SD_FWID=0x0123 # SoftDevice firmware ID (s140 v7.3.0) # # --- MG24 (Silicon Labs) --- # Requires: west blobs fetch hal_silabs (before first build) diff --git a/zephcore/boards/example_board/board.overlay b/zephcore/boards/example_board/board.overlay index 964ffbc..c2e0954 100644 --- a/zephcore/boards/example_board/board.overlay +++ b/zephcore/boards/example_board/board.overlay @@ -82,6 +82,13 @@ */ /* --- Display (uncomment if board has OLED/LCD/e-paper) --- + * Setting zephyr,display auto-enables CONFIG_DISPLAY, + * CONFIG_CHARACTER_FRAMEBUFFER, and the UI page system. + * No board.conf changes needed. + * HEAP: 128x64 OLEDs fit in the default 2KB heap. + * Larger displays need CONFIG_HEAP_MEM_POOL_SIZE + * override in board.conf (e.g. 6144 for 200x200 EPD). + * * zephyr,display = &oled; I2C OLED (SH1106, SSD1306) * zephyr,display = &epd; SPI e-paper (SSD1681, etc.) */ diff --git a/zephcore/boards/mg24/xiao_mg24/board.conf b/zephcore/boards/mg24/xiao_mg24/board.conf index 3af1c83..88b94fe 100644 --- a/zephcore/boards/mg24/xiao_mg24/board.conf +++ b/zephcore/boards/mg24/xiao_mg24/board.conf @@ -8,5 +8,4 @@ CONFIG_ZEPHCORE_BOARD_NAME="XIAO MG24" # Device Information Service model name CONFIG_BT_DIS_MODEL_NUMBER_STR="XIAO MG24" -# RAM-limited: Silabs BLE blob needs 32KB heap, override prod.conf MAX_CONTACTS -CONFIG_ZEPHCORE_MAX_CONTACTS=650 +# RAM-limited: Silabs BLE blob needs 32KB heap — use Kconfig default (350) diff --git a/zephcore/boards/nrf52840/rak4631/board.conf b/zephcore/boards/nrf52840/rak4631/board.conf index 79e58d9..651e936 100644 --- a/zephcore/boards/nrf52840/rak4631/board.conf +++ b/zephcore/boards/nrf52840/rak4631/board.conf @@ -19,3 +19,6 @@ CONFIG_ZEPHCORE_BOARD_NAME="RAK 4631" # Device Information Service model name CONFIG_BT_DIS_MODEL_NUMBER_STR="RAK4631" +# SoftDevice firmware ID (s140 v6.1.1) +CONFIG_ZEPHCORE_SD_FWID=0x00B6 + diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/Kconfig.rak_wismesh_tag b/zephcore/boards/nrf52840/rak_wismesh_tag/Kconfig.rak_wismesh_tag new file mode 100644 index 0000000..b94870b --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/Kconfig.rak_wismesh_tag @@ -0,0 +1,6 @@ +# RAK WisMesh Tag board configuration +# Copyright (c) 2025 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_RAK_WISMESH_TAG + select SOC_NRF52840_QIAA diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/board.cmake b/zephcore/boards/nrf52840/rak_wismesh_tag/board.cmake new file mode 100644 index 0000000..34bb176 --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/board.cmake @@ -0,0 +1,13 @@ +# Copyright (c) 2025 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +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/rak_wismesh_tag/board.conf b/zephcore/boards/nrf52840/rak_wismesh_tag/board.conf new file mode 100644 index 0000000..21ae488 --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/board.conf @@ -0,0 +1,31 @@ +# RAK WisMesh Tag (nRF52840 + SX1262 GPS Tracker) +# Board-specific configuration +# +# Hardware: +# - nRF52840 SoC, SX1262 LoRa (22dBm, TCXO 1.8V) +# - AT6558R GNSS on UART0 at 9600 baud +# - LIS2DH accelerometer on I2C0 +# - 1000 mAh battery, ADC on AIN3 (P0.05) +# - Buzzer on P0.21, user button on P0.09 +# - LEDs: GREEN=P1.03 (TX), BLUE=P1.04 (status) +# - No display, no QSPI flash +# - SoftDevice s140 v6.1.1, UF2 bootloader + +# Board identification (matches Arduino MeshCore variant name) +CONFIG_ZEPHCORE_BOARD_NAME="RAK WisMesh Tag" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="RAK WisMesh Tag" + +# SoftDevice firmware ID (s140 v6.1.1) +CONFIG_ZEPHCORE_SD_FWID=0x00B6 + +# No display — enable LED heartbeat instead +CONFIG_ZEPHCORE_UI_DISPLAY=n + +# PWM for buzzer +CONFIG_PWM=y + +# RX duty cycle — safe on SX1262, extends battery life on this +# card-sized tracker with 1000mAh battery +CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=y diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/board.yml b/zephcore/boards/nrf52840/rak_wismesh_tag/board.yml new file mode 100644 index 0000000..bf0132e --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/board.yml @@ -0,0 +1,9 @@ +# Copyright (c) 2025 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +board: + name: rak_wismesh_tag + full_name: RAK WisMesh Tag (nRF52840 + SX1262 GPS Tracker) + vendor: rak + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag-pinctrl.dtsi b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag-pinctrl.dtsi new file mode 100644 index 0000000..5f2b1f3 --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag-pinctrl.dtsi @@ -0,0 +1,82 @@ +/* + * RAK WisMesh Tag pin control definitions + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Pin mappings (from Arduino MeshCore variant.h): + * + * SPI2 (LoRa SX1262): SCK=P1.11, MOSI=P1.12, MISO=P1.13, CS=P1.10 + * I2C0 (sensors): SDA=P0.25, SCL=P0.24 + * UART0 (GPS 9600): TX=P0.16 (→GPS RX), RX=P0.15 (←GPS TX) + * PWM0 (buzzer): P0.21 + */ + +&pinctrl { + /* UART0 for GPS module at 9600 baud */ + uart0_default: uart0_default { + group1 { + psels = ; /* P0.16 → GPS RX */ + }; + group2 { + psels = ; /* P0.15 ← GPS TX */ + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* I2C0 for sensors (LIS2DH, environment sensors) */ + i2c0_default: i2c0_default { + group1 { + psels = , /* P0.25 */ + ; /* P0.24 */ + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI2 for LoRa SX1262 */ + spi2_default: spi2_default { + group1 { + psels = , /* P1.11 (Arduino 43) */ + , /* P1.12 (Arduino 44) */ + ; /* P1.13 (Arduino 45) */ + }; + }; + + spi2_sleep: spi2_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + /* PWM0 for buzzer on P0.21 */ + pwm0_default: pwm0_default { + group1 { + psels = ; + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts new file mode 100644 index 0000000..5e00886 --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts @@ -0,0 +1,277 @@ +/* + * RAK WisMesh Tag — nRF52840 + SX1262 (card-sized GPS tracker) + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Hardware: + * - nRF52840 SoC with BLE 5 + * - SX1262 LoRa radio (DIO2 as RF switch, TCXO 1.8V via DIO3) + * - AT6558R GNSS on UART0 at 9600 baud + * - LIS2DH 3-axis accelerometer on I2C0 + * - 1000 mAh battery with ADC on AIN3 (P0.05) + * - Buzzer on PWM0 (P0.21) + * - LEDs: GREEN=P1.03 (TX, active HIGH), BLUE=P1.04 (status, active HIGH) + * - User button on P0.09 (NFC1, active LOW) + * - Board power enable on P0.12 + * - No display, no QSPI flash + * - IP66 sealed, pogo-pin USB interface + * + * Pin mappings (from Arduino MeshCore variant.h): + * - LoRa SPI: SCK=P1.11, MOSI=P1.12, MISO=P1.13, CS=P1.10 + * - LoRa ctl: DIO1=P1.15, BUSY=P1.14, RESET=P1.06, PWR_EN=P1.05 + * - I2C: SDA=P0.25, SCL=P0.24 + * - GPS: TX=P0.16, RX=P0.15, EN=P1.02, PPS=P0.17 + */ + +/dts-v1/; +#include +#include "rak_wismesh_tag-pinctrl.dtsi" +#include +#include +#include +#include + +/ { + model = "RAK WisMesh Tag"; + compatible = "rak,wismesh-tag"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + }; + + aliases { + lora0 = &lora; + led0 = &led_green; + led1 = &led_blue; + lora-tx-led = &led_green; + sw0 = &user_button; + watchdog0 = &wdt0; + buzzer = &buzzer; + gps-enable = &gps_enable_pin; + }; + + /* ---- LEDs (active HIGH per Arduino LED_STATE_ON=HIGH) ---- */ + leds { + compatible = "gpio-leds"; + + led_green: led_0 { + gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; + label = "Green LED"; + }; + + led_blue: led_1 { + gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; + label = "Blue LED"; + }; + }; + + /* ---- Button (P0.09/NFC1, active LOW with pull-up) ---- */ + buttons: buttons { + compatible = "gpio-keys"; + + user_button: button_0 { + gpios = <&gpio0 9 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "User Button"; + }; + }; + + /* Input filter: long-press (1000ms) */ + user_btn_longpress { + compatible = "zephyr,input-longpress"; + input = <&buttons>; + input-codes = ; + short-codes = ; + long-codes = ; + long-delay-ms = <1000>; + }; + + /* Input filter: multi-tap + * 1 tap → KEY_1 = (reserved / next page if display added) + * 2 taps → KEY_B = flood advert + * 3 taps → KEY_D = buzzer mute toggle + * 4 taps → KEY_C = GPS on/off */ + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + + /* ---- Power control ---- */ + + /* Board power enable — must be HIGH for peripherals */ + pwr_enable: pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "pwr-enable"; + enable-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; + regulator-boot-on; + }; + + /* LoRa radio power enable — must be HIGH before SX1262 can communicate */ + lora_pwr_enable: lora-pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "lora-pwr-enable"; + enable-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; + regulator-boot-on; + startup-delay-us = <10000>; + }; + + /* GPS power control */ + gps_en: gps-enable { + compatible = "gpio-leds"; + gps_enable_pin: gps_enable { + gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>; + label = "GPS Enable"; + }; + }; + + /* ---- Battery ADC ---- */ + /* P0.05 (AIN3), same divider circuit as RAK4631 */ + zephyr,user { + io-channels = <&adc 3>; + vbat-mv-multiplier = <4216>; + }; + + /* ---- Buzzer on PWM0 ---- */ + pwmbuzzer { + compatible = "pwm-leds"; + buzzer: buzzer { + pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; + }; + }; +}; + +/* ---- SoC configuration ---- */ + +®0 { + status = "okay"; +}; + +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* Battery voltage on P0.05 (AIN3) */ + channel@3 { + reg = <3>; + 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"; +}; + +&gpio1 { + status = "okay"; +}; + +/* ---- PWM0 for buzzer ---- */ +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* ---- GPS on UART0 (AT6558R, 9600 baud) ---- + * Uses luatos,air530z driver for NMEA with power control. + * GPS power controlled via on-off-gpios (P1.02 = GPS_EN). */ +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <9600>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; + + gnss: gnss { + compatible = "luatos,air530z"; + on-off-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>; + }; +}; + +/* ---- I2C0 for sensors ---- */ +&i2c0 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + + /* All supported environment & power sensors — auto-detected at runtime */ + #include "../../common/sensors-i2c.dtsi" +}; + +/* ---- SPI2 for LoRa SX1262 ---- */ +&spi2 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi2_default>; + pinctrl-1 = <&spi2_sleep>; + pinctrl-names = "default", "sleep"; + cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; /* P1.10 (Arduino 42) LORA_CS */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins */ + reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; /* P1.06 (Arduino 38) */ + busy-gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>; /* P1.14 (Arduino 46) */ + dio1-gpios = <&gpio1 15 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* P1.15 (Arduino 47) */ + + /* DIO2 drives RF switch for TX */ + dio2-tx-enable; + + /* TCXO voltage 1.8V via DIO3 */ + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + + /* RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +/* ---- QSPI Flash — not present on WisMesh Tag ---- */ +&qspi { + status = "disabled"; +}; + +/* ---- USB CDC for console/CLI ---- */ +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +/* ---- Flash partitions (SoftDevice v6, app@0x26000) ---- */ +#include "../../common/nrf52_partitions_sdv6.dtsi" diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag_defconfig b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag_defconfig new file mode 100644 index 0000000..0af30f8 --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag_defconfig @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# RAK WisMesh Tag default configuration + +# Enable MPU +CONFIG_ARM_MPU=y +CONFIG_HW_STACK_PROTECTION=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable console on USB CDC +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y +CONFIG_SERIAL=y +CONFIG_UART_LINE_CTRL=y + +# Enable flash/NVS storage +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y + +# Pinctrl +CONFIG_PINCTRL=y + +# SPI for LoRa +CONFIG_SPI=y + +# ADC for battery +CONFIG_ADC=y diff --git a/zephcore/boards/nrf52840/t1000_e/board.conf b/zephcore/boards/nrf52840/t1000_e/board.conf index 7dd0a81..f778a33 100644 --- a/zephcore/boards/nrf52840/t1000_e/board.conf +++ b/zephcore/boards/nrf52840/t1000_e/board.conf @@ -13,6 +13,9 @@ CONFIG_ZEPHCORE_BOARD_NAME="T1000-E" # Device Information Service model name CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed T1000-E" +# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader) +CONFIG_ZEPHCORE_SD_FWID=0x0123 + # Radio type (TCXO, RF switch, PA config in DTS) CONFIG_ZEPHCORE_RADIO_LR1110=y diff --git a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts index 38deb4a..5a6dddf 100644 --- a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts +++ b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts @@ -209,7 +209,8 @@ reg = <0>; width = <200>; height = <200>; - rotation = <270>; + rotation = <0>; + gate-inverted; busy-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>; }; }; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf index 291ddf9..6d29d46 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf @@ -13,7 +13,10 @@ CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1" # Device Information Service model name -CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker 1110" +CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker L1" + +# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader) +CONFIG_ZEPHCORE_SD_FWID=0x0123 # QSPI flash present (overrides nrf52_common.conf default of =n) CONFIG_NORDIC_QSPI_NOR=y diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c index 26f0338..2cc49ed 100644 --- a/zephcore/helpers/ui/display.c +++ b/zephcore/helpers/ui/display.c @@ -73,9 +73,12 @@ static void auto_off_handler(struct k_work *work) return; } /* E-paper content persists without power — blanking wastes a full - * refresh cycle (~2s) for no benefit. Just turn off the backlight. */ + * refresh cycle (~2s) for no benefit. Just turn off the backlight + * and mark display "off" so the next button press triggers + * mc_display_on() → backlight restore. */ if (is_epd) { backlight_set(false); + disp_on = false; return; } if (disp_on) { @@ -232,7 +235,11 @@ void mc_display_on(void) } if (!disp_on) { - display_blanking_off(disp_dev); + /* EPD: content persists (bistable) — no need to unblank, + * just restore backlight. OLED: actually unblank. */ + if (!is_epd) { + display_blanking_off(disp_dev); + } disp_on = true; } backlight_set(true); diff --git a/zephcore/helpers/ui/ui_task.c b/zephcore/helpers/ui/ui_task.c index 05db8b3..f511a74 100644 --- a/zephcore/helpers/ui/ui_task.c +++ b/zephcore/helpers/ui/ui_task.c @@ -114,7 +114,7 @@ static struct ui_state local_ui_state; /* ========== Constants ========== */ #define SPLASH_DURATION_MS 3000 #define RENDER_DEBOUNCE_MS 50 -#define RENDER_DEBOUNCE_EPD_MS 2500 /* e-paper: coalesce updates across full refresh (~2s) */ +#define RENDER_DEBOUNCE_EPD_MS 200 /* e-paper: coalesce rapid state updates, then refresh */ /* ========== State ========== */ static bool ui_initialized; @@ -1038,6 +1038,7 @@ void ui_refresh_display(void) return; } +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY /* EPD displays: skip periodic housekeeping renders. * Each full e-paper refresh takes ~2s and causes visible flashing. * All meaningful events (messages, BLE, GPS fix, button presses) @@ -1049,4 +1050,5 @@ void ui_refresh_display(void) } schedule_render(); +#endif } diff --git a/zephcore/patches/zephyr/0007-littlefs-custom-erase.patch b/zephcore/patches/zephyr/0007-littlefs-custom-erase.patch deleted file mode 100644 index 66cb39c..0000000 --- a/zephcore/patches/zephyr/0007-littlefs-custom-erase.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/subsys/fs/littlefs_fs.c b/subsys/fs/littlefs_fs.c -index 6176d533e32..1ea405a3d1f 100644 ---- a/subsys/fs/littlefs_fs.c -+++ b/subsys/fs/littlefs_fs.c -@@ -901,7 +901,9 @@ static int littlefs_init_cfg(struct fs_littlefs *fs, int flags) - #ifdef CONFIG_FS_LITTLEFS_FMP_DEV - lcp->read = lfs_api_read; - lcp->prog = lfs_api_prog; -- lcp->erase = lfs_api_erase; -+ if (!lcp->erase) { -+ lcp->erase = lfs_api_erase; -+ } - #endif - - lcp->read_size = read_size; diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index b10b9b5..963cd11 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -485,6 +485,21 @@ int main(void) k_sleep(K_MSEC(100)); LOG_INF("=== ZephCore starting ==="); + /* Log reset reason so we can diagnose random reboots */ + { + uint32_t cause; + if (hwinfo_get_reset_cause(&cause) == 0) { + LOG_INF("Reset cause: 0x%08x%s%s%s%s%s%s", cause, + (cause & RESET_PIN) ? " PIN" : "", + (cause & RESET_SOFTWARE) ? " SOFTWARE" : "", + (cause & RESET_BROWNOUT) ? " BROWNOUT" : "", + (cause & RESET_POR) ? " POR" : "", + (cause & RESET_WATCHDOG) ? " WATCHDOG" : "", + (cause & RESET_CPU_LOCKUP)? " LOCKUP" : ""); + hwinfo_clear_reset_cause(); + } + } + if (!ZephyrDataStore::mount()) { LOG_ERR("LittleFS mount failed"); }