ESP OTA first implementation

This commit is contained in:
liquidraver committed 2026-02-22 13:09:18 +01:00
1 parent 521955bc05
commit b6bc46b921
26 files changed
+1462 -33

No files matched your search

+81 -14
View File
@@ -76,6 +76,35 @@ list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
# 2. Create boards/<board>/board.overlay with hardware definitions
# 3. Build: west build -b <board> zephcore --pristine
# Under sysbuild, BOARD and EXTRA_CONF_FILE are stored in a cache file that
# find_package(Zephyr) loads later. We need them NOW for config hierarchy
# detection. Read them directly from the sysbuild cache file.
if(NOT BOARD AND DEFINED SYSBUILD_CACHE AND EXISTS "${SYSBUILD_CACHE}")
file(STRINGS "${SYSBUILD_CACHE}" _sysbuild_strings ENCODING UTF-8)
foreach(_str ${_sysbuild_strings})
if(_str MATCHES "^BOARD:STRING=(.+)")
set(BOARD "${CMAKE_MATCH_1}")
endif()
# User-specified EXTRA_CONF_FILE (e.g., repeater.conf) is passed to sysbuild,
# not the app. Recover it so our auto-include logic can detect it.
if(_str MATCHES "^EXTRA_CONF_FILE:UNINITIALIZED=(.+)")
set(_SYSBUILD_USER_CONF "${CMAKE_MATCH_1}")
endif()
endforeach()
if(BOARD)
message(STATUS "Sysbuild: BOARD=${BOARD}")
endif()
if(_SYSBUILD_USER_CONF)
# Merge user extras into EXTRA_CONF_FILE so downstream checks see them
if(EXTRA_CONF_FILE)
set(EXTRA_CONF_FILE "${EXTRA_CONF_FILE};${_SYSBUILD_USER_CONF}")
else()
set(EXTRA_CONF_FILE "${_SYSBUILD_USER_CONF}")
endif()
message(STATUS "Sysbuild: user EXTRA_CONF_FILE=${_SYSBUILD_USER_CONF}")
endif()
endif()
# Build common config file list based on detected platform
set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_common.conf")
@@ -108,8 +137,12 @@ endif()
# For custom boards (e.g., wio_tracker_l1/nrf52840), BOARD contains the base name
# For Zephyr boards with qualifiers (e.g., rak4631/nrf52840), BOARD contains base/qualifier
# We extract just the base board name for folder lookup
string(REPLACE "/" ";" BOARD_PARTS ${BOARD})
list(GET BOARD_PARTS 0 BOARD_BASE) # Get base name without qualifier
if(BOARD)
string(REPLACE "/" ";" BOARD_PARTS "${BOARD}")
list(GET BOARD_PARTS 0 BOARD_BASE)
else()
set(BOARD_BASE "")
endif()
# Search for board.conf in possible locations
set(ZEPHCORE_BOARD_CONF "")
@@ -145,6 +178,18 @@ else()
endif()
endif()
# Auto-include WiFi OTA for ESP32 repeater builds.
# Enables WiFi AP + HTTP server + MCUboot image management.
# Requires --sysbuild to build MCUboot alongside the app.
# Non-ESP32 builds and companion builds are unaffected.
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater")
set(ZEPHCORE_WIFI_OTA_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/wifi_ota.conf")
if(EXISTS ${ZEPHCORE_WIFI_OTA_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_WIFI_OTA_CONF})
message(STATUS " WiFi OTA: auto-enabled (ESP32 repeater)")
endif()
endif()
# Append to EXTRA_CONF_FILE — auto-generated configs first, user extras last.
# User-specified extras (repeater.conf, prod.conf) MUST come after the auto chain
# so they can override settings (e.g. CONFIG_BT=n in repeater.conf).
@@ -345,20 +390,42 @@ if(CONFIG_ZEPHCORE_EASTER_EGG_DOOM)
)
endif()
# ========== WiFi OTA (ESP32 repeater builds) ==========
if(CONFIG_ZEPHCORE_WIFI_OTA)
message(STATUS "ZephCore WiFi OTA: ENABLED")
target_sources(app PRIVATE
adapters/ota/wifi_ota.c
)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/ota
)
# Register per-service HTTP resource iterable section (required by Zephyr HTTP server).
# Without this, the linker can't find _http_resource_desc_ota_service_list_start/end.
zephyr_linker_sources(SECTIONS ${CMAKE_CURRENT_SOURCE_DIR}/adapters/ota/sections-rom.ld)
endif()
# ========== Post-build: ESP32 flash instructions ==========
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common")
# ESP32 simple boot: zephyr.bin is a self-contained image at 0x0.
# It includes embedded bootloader code — no separate bootloader needed.
# NVS (0x3B0000) and LittleFS (0x3B2000) are NOT touched by flashing.
message(STATUS "")
message(STATUS "ESP32 Flash Commands:")
message(STATUS " Normal update (preserves NVS + user data):")
message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin")
message(STATUS " Full flash (after erase_flash):")
message(STATUS " esptool --port COMx --baud 921600 erase_flash")
message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin")
message(STATUS " Or use: west flash --build-dir build_dir")
message(STATUS "")
if(CONFIG_ZEPHCORE_WIFI_OTA)
# MCUboot build (via --sysbuild): merged image or west flash
message(STATUS "")
message(STATUS "ESP32 Flash Commands (MCUboot + WiFi OTA):")
message(STATUS " Flash all (MCUboot + app): west flash --build-dir build_dir")
message(STATUS " OTA update binary: build_dir/zephcore/zephyr/zephyr.signed.bin")
message(STATUS " Full erase first: esptool --port COMx erase_flash")
message(STATUS "")
else()
# Simple boot: zephyr.bin is a self-contained image at 0x0.
message(STATUS "")
message(STATUS "ESP32 Flash Commands:")
message(STATUS " Normal update (preserves NVS + user data):")
message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin")
message(STATUS " Full flash (after erase_flash):")
message(STATUS " esptool --port COMx --baud 921600 erase_flash")
message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin")
message(STATUS " Or use: west flash --build-dir build_dir")
message(STATUS "")
endif()
endif()
# ========== Post-build: Generate DFU zip for OTA updates (nRF52 only) ==========
+33
View File
@@ -379,6 +379,39 @@ config ZEPHCORE_GPS_FIX_TIMEOUT_SEC
endmenu
menu "WiFi OTA Update"
config ZEPHCORE_WIFI_OTA
bool "WiFi OTA firmware update"
depends on ZEPHCORE_ROLE_REPEATER
help
Enable WiFi AP + HTTP server for OTA firmware updates.
Auto-enabled for ESP32 repeater builds via wifi_ota.conf.
When "start ota" is issued, starts WiFi AP "MeshCore-OTA"
and HTTP server. User uploads firmware via browser at
http://192.168.100.1/update. Requires MCUboot (--sysbuild).
if ZEPHCORE_WIFI_OTA
config ZEPHCORE_OTA_AP_SSID
string "WiFi AP SSID for OTA"
default "MeshCore-OTA"
help
SSID of the WiFi access point started for OTA updates.
Open network (no password), matching Arduino behavior.
config ZEPHCORE_OTA_AP_IP
string "WiFi AP static IP address"
default "192.168.100.1"
help
Static IP address assigned to the WiFi AP interface.
DHCP server hands out addresses starting at .10.
endif # ZEPHCORE_WIFI_OTA
endmenu
menu "UI Configuration"
config ZEPHCORE_UI_BUTTONS
+101
View File
@@ -0,0 +1,101 @@
/*
* Auto-generated from ota_page.html � do not edit manually.
* Regenerate: python3 compress_html.py
*/
#pragma once
#include <stdint.h>
#define OTA_PAGE_GZ_SIZE 1405
static const uint8_t ota_page_gz[OTA_PAGE_GZ_SIZE] = {
0x1f, 0x8b, 0x08, 0x00, 0x5b, 0x12, 0x9a, 0x69, 0x02, 0xff, 0x95, 0x57, 0xe1, 0x6e, 0xdb, 0x36,
0x10, 0xfe, 0xef, 0xa7, 0x50, 0x55, 0x04, 0xb2, 0x9b, 0x48, 0x96, 0x1d, 0x27, 0x75, 0xa4, 0xc8,
0xc0, 0xda, 0x75, 0xd8, 0xb0, 0x75, 0x2d, 0xda, 0x14, 0xd8, 0x50, 0xf4, 0x07, 0x25, 0x9d, 0x2c,
0x36, 0x32, 0xa9, 0x92, 0x54, 0x62, 0x4f, 0x30, 0xb0, 0x87, 0xd8, 0x13, 0xee, 0x49, 0x76, 0xa4,
0xa4, 0x58, 0x76, 0xe3, 0xa1, 0xfd, 0x13, 0xc9, 0xe4, 0xf1, 0xbb, 0xbb, 0xef, 0xbe, 0x3b, 0x2a,
0xd7, 0x4f, 0x7e, 0x7c, 0xf3, 0xf2, 0xe6, 0xcf, 0xb7, 0xaf, 0xac, 0x5c, 0xad, 0x8a, 0xc5, 0xe0,
0xba, 0x7b, 0x00, 0x49, 0xf1, 0xb1, 0x02, 0x45, 0xac, 0x24, 0x27, 0x42, 0x82, 0x8a, 0xec, 0x4a,
0x65, 0xee, 0xdc, 0xee, 0x96, 0x19, 0x59, 0x41, 0x64, 0xdf, 0x51, 0xb8, 0x2f, 0xb9, 0x50, 0xb6,
0x95, 0x70, 0xa6, 0x80, 0xa1, 0xd9, 0x3d, 0x4d, 0x55, 0x1e, 0xa5, 0x70, 0x47, 0x13, 0x70, 0xcd,
0x8f, 0x33, 0xca, 0xa8, 0xa2, 0xa4, 0x70, 0x65, 0x42, 0x0a, 0x88, 0x26, 0x1a, 0x43, 0x51, 0x55,
0xc0, 0xe2, 0x35, 0xc8, 0xfc, 0x25, 0x17, 0x60, 0xbd, 0xb9, 0xf9, 0xe1, 0x7a, 0xdc, 0xac, 0x0d,
0xae, 0xa5, 0xda, 0xe8, 0xe7, 0xb3, 0x3a, 0xe6, 0x6b, 0x57, 0xd2, 0xbf, 0x28, 0x5b, 0x06, 0x31,
0x17, 0x29, 0x08, 0x17, 0x57, 0xc2, 0x15, 0x11, 0x4b, 0xca, 0x02, 0x3f, 0x2c, 0x49, 0x9a, 0xea,
0x3d, 0x7f, 0x3b, 0x88, 0x79, 0xba, 0xa9, 0x33, 0x0c, 0xc1, 0xcd, 0xc8, 0x8a, 0x16, 0x9b, 0x40,
0x6e, 0xa4, 0x82, 0x95, 0x5b, 0xd1, 0x33, 0x49, 0x98, 0x74, 0x25, 0x08, 0x9a, 0x85, 0x31, 0x49,
0x6e, 0x97, 0x82, 0x57, 0x2c, 0x0d, 0x9e, 0x4e, 0xc8, 0x84, 0x4c, 0x21, 0x4c, 0x78, 0xc1, 0x45,
0xf0, 0x14, 0x00, 0xc2, 0x94, 0xca, 0xb2, 0x20, 0x9b, 0x20, 0x2b, 0x60, 0x1d, 0x7e, 0xae, 0xa4,
0xa2, 0xd9, 0xc6, 0x6d, 0xd3, 0x0a, 0x12, 0xfc, 0x03, 0x22, 0x24, 0x05, 0x5d, 0x32, 0x97, 0x22,
0xb4, 0xec, 0x96, 0x56, 0x94, 0xb9, 0x39, 0xd0, 0x65, 0xae, 0x82, 0x89, 0xef, 0xdf, 0xe5, 0xdb,
0x81, 0x97, 0xd4, 0x7b, 0x9e, 0x2e, 0xa7, 0x93, 0x73, 0x08, 0xdb, 0x0c, 0x04, 0x49, 0x69, 0x25,
0x83, 0xc9, 0xb4, 0x5c, 0x3f, 0x24, 0x30, 0x85, 0x15, 0x66, 0xb5, 0x6e, 0xd8, 0x0a, 0x66, 0x53,
0x1f, 0xf7, 0x9a, 0xf7, 0x2b, 0xff, 0x24, 0x54, 0xb0, 0x56, 0xae, 0x71, 0xdc, 0xba, 0xdc, 0x0e,
0xf2, 0x69, 0xdd, 0xb0, 0x80, 0x84, 0x28, 0xc5, 0x57, 0x81, 0x77, 0x81, 0x10, 0x6d, 0x2e, 0x7e,
0x96, 0x61, 0x0c, 0x34, 0xad, 0xdb, 0xdf, 0xf3, 0xf9, 0x3c, 0x34, 0xd4, 0x20, 0x95, 0x10, 0x78,
0xf3, 0x0b, 0xe3, 0xad, 0x7f, 0x7a, 0xa2, 0x8f, 0x6f, 0x07, 0x94, 0x95, 0x95, 0xfa, 0xa8, 0x36,
0x25, 0x44, 0x19, 0x2d, 0xe0, 0x53, 0xdd, 0x31, 0xc2, 0x38, 0x03, 0x84, 0x8c, 0x15, 0x7b, 0x58,
0xa2, 0xac, 0xa0, 0x0c, 0xdc, 0xb8, 0xe0, 0xc9, 0xed, 0x43, 0x1e, 0xde, 0x73, 0x58, 0x59, 0x06,
0xec, 0x20, 0xdb, 0x39, 0x26, 0xd4, 0xac, 0x18, 0xac, 0x30, 0xa9, 0x84, 0xc4, 0xd0, 0x4a, 0x4e,
0x0d, 0x85, 0xbb, 0xe8, 0x26, 0x78, 0xd4, 0xfc, 0xba, 0x6f, 0x18, 0xbd, 0xf4, 0xfd, 0x50, 0x09,
0xac, 0x20, 0x0a, 0x88, 0xb3, 0x60, 0x47, 0xab, 0xe5, 0x4d, 0x25, 0x86, 0x24, 0xa1, 0xd8, 0xe3,
0xda, 0xcf, 0xce, 0x67, 0x97, 0x7e, 0xaf, 0xaa, 0x8d, 0x4d, 0x90, 0xf3, 0x3b, 0x10, 0xfb, 0x55,
0x21, 0x17, 0xd3, 0xe7, 0x97, 0xb8, 0x5d, 0x95, 0x07, 0x10, 0x7e, 0x3c, 0xbf, 0x9a, 0x75, 0x10,
0x59, 0x96, 0x75, 0x64, 0x15, 0x90, 0xa9, 0xa0, 0x61, 0x4a, 0x1f, 0x7a, 0x04, 0xd3, 0xf7, 0xc9,
0xf9, 0x7c, 0xd2, 0x6e, 0x23, 0x53, 0x24, 0x2e, 0x20, 0xdd, 0xb3, 0xb8, 0xb8, 0xb8, 0xe8, 0x92,
0x4f, 0x21, 0x23, 0x55, 0xa1, 0x34, 0xaf, 0x44, 0xd4, 0x4d, 0xb9, 0x51, 0x40, 0x27, 0x61, 0x2b,
0xa6, 0xe9, 0x4c, 0x93, 0xd6, 0x47, 0x27, 0x3e, 0x6a, 0xf6, 0x31, 0x1d, 0xb5, 0x1d, 0x31, 0xf1,
0x50, 0x48, 0x96, 0x1f, 0xea, 0xb8, 0xb2, 0x82, 0xdf, 0x07, 0x39, 0x4d, 0x53, 0x60, 0xe1, 0x41,
0x19, 0xb1, 0xb6, 0x45, 0xbd, 0x53, 0xec, 0x49, 0xdf, 0x89, 0x2e, 0x2a, 0x11, 0xee, 0x52, 0xa3,
0xa3, 0xd6, 0x86, 0x57, 0x7e, 0x0a, 0xcb, 0x33, 0x2d, 0xa9, 0xb3, 0x96, 0x98, 0xd1, 0x63, 0x01,
0xf4, 0x4a, 0x64, 0x12, 0xb1, 0xbc, 0x73, 0xd9, 0x2a, 0x18, 0x9b, 0xd3, 0x2b, 0x13, 0x55, 0x1f,
0x51, 0xa0, 0xe2, 0x65, 0xe0, 0x9d, 0x1b, 0x4e, 0xa5, 0xaa, 0x7b, 0x8b, 0x0f, 0x4a, 0x68, 0xce,
0x5c, 0xe9, 0x23, 0xbd, 0x4e, 0xf3, 0x66, 0xe6, 0x0c, 0xbf, 0xed, 0x54, 0xde, 0x44, 0x87, 0x4b,
0x58, 0x93, 0xae, 0xfc, 0xcf, 0x67, 0xc9, 0x79, 0xb2, 0x1d, 0x5c, 0x8f, 0xdb, 0x89, 0x72, 0x3d,
0x6e, 0x07, 0x9b, 0x1e, 0x17, 0xf8, 0x48, 0xe9, 0x9d, 0x95, 0x14, 0x44, 0xca, 0xc8, 0x4e, 0xf4,
0x50, 0xca, 0xa7, 0x7b, 0x13, 0xc9, 0xfa, 0x50, 0xa6, 0x44, 0x01, 0x9e, 0x9a, 0xee, 0x1b, 0xd3,
0xd4, 0xb6, 0x68, 0x1a, 0xd9, 0x7a, 0xc8, 0xa5, 0xf6, 0xe2, 0x7a, 0x8c, 0x7b, 0x8d, 0x85, 0xc6,
0xae, 0xb0, 0xa7, 0x58, 0x67, 0x8a, 0x3d, 0x63, 0xa1, 0x00, 0x6d, 0x8b, 0xb3, 0xa4, 0xa0, 0xc9,
0x2d, 0x1e, 0xe2, 0x49, 0xb5, 0x42, 0x6e, 0xbd, 0x25, 0xa8, 0x57, 0x05, 0xe8, 0xd7, 0x17, 0x9b,
0x5f, 0xd2, 0xa1, 0x93, 0x39, 0x23, 0xcf, 0xd8, 0x0c, 0x47, 0xf6, 0xe2, 0x3d, 0x14, 0x90, 0x28,
0xeb, 0x27, 0x2a, 0x56, 0xf7, 0x44, 0x60, 0x0c, 0x0d, 0x2c, 0xe2, 0x9b, 0x2e, 0xb5, 0x4c, 0x97,
0xda, 0xba, 0x4d, 0x9b, 0x50, 0x32, 0xdb, 0x22, 0x49, 0x02, 0x25, 0x0e, 0x60, 0x2f, 0xa6, 0xcc,
0xf8, 0xcb, 0x09, 0x5b, 0x6a, 0x23, 0x39, 0x54, 0x39, 0x95, 0x23, 0xfb, 0xd1, 0xe0, 0x50, 0xaa,
0x0d, 0x42, 0x15, 0xdb, 0x56, 0xa7, 0xd9, 0x5d, 0xb4, 0x55, 0xa9, 0xa3, 0xf9, 0x50, 0x16, 0x9c,
0xa4, 0xbd, 0x20, 0x76, 0x19, 0x37, 0xde, 0xd1, 0xa1, 0xe1, 0x38, 0xb2, 0xfb, 0x75, 0x9d, 0xef,
0xd7, 0x70, 0xde, 0x1b, 0x52, 0x38, 0x94, 0xf6, 0x88, 0x7b, 0x08, 0x89, 0x88, 0x26, 0x1c, 0xfd,
0xb2, 0xe8, 0xef, 0x68, 0xd9, 0xb6, 0xb9, 0xea, 0xb7, 0xf6, 0xf0, 0x23, 0x10, 0xa8, 0xb6, 0xc6,
0x4e, 0xbf, 0x3c, 0x66, 0x20, 0xdb, 0x7d, 0xd9, 0xdb, 0x6e, 0x1f, 0x32, 0x11, 0xb4, 0x54, 0x8b,
0xc1, 0x1d, 0x11, 0x96, 0x26, 0x37, 0x1c, 0x64, 0x15, 0x4b, 0xb4, 0xb0, 0x2d, 0xa4, 0x91, 0x8e,
0x6a, 0xbd, 0x18, 0x51, 0xdd, 0x43, 0x20, 0x3f, 0xfa, 0x9f, 0x42, 0x9a, 0x0d, 0xf5, 0xfb, 0xa8,
0x3e, 0x5e, 0x56, 0x86, 0x75, 0xd5, 0x33, 0xfc, 0x65, 0x7b, 0x43, 0x6a, 0x7b, 0x4f, 0x5f, 0x9f,
0xa7, 0x8e, 0x35, 0x74, 0x4e, 0x5f, 0x13, 0x95, 0x7b, 0xa6, 0xfb, 0x0c, 0x92, 0xa7, 0xb9, 0x1a,
0x4f, 0xfc, 0xe9, 0x6c, 0x74, 0xea, 0xfc, 0xfa, 0x62, 0xe4, 0x84, 0x47, 0x91, 0xab, 0x18, 0x91,
0xbb, 0x92, 0x45, 0x19, 0x29, 0x24, 0x1c, 0x37, 0x96, 0xea, 0x20, 0x0c, 0xc7, 0x09, 0xb7, 0xdb,
0x5d, 0x7a, 0xba, 0xd0, 0xf5, 0x00, 0xd3, 0x79, 0x62, 0xf2, 0x11, 0xa0, 0x2a, 0xc1, 0x42, 0x43,
0xc4, 0x3a, 0x62, 0x70, 0x6f, 0xfd, 0xf1, 0xfa, 0xb7, 0x9f, 0x95, 0x2a, 0xdf, 0xc1, 0x97, 0x0a,
0xa4, 0x1a, 0x8e, 0x9a, 0xbd, 0x38, 0x3a, 0xea, 0x11, 0x2b, 0xe8, 0xb4, 0x56, 0xd9, 0x71, 0x2b,
0x5d, 0xcc, 0xce, 0xac, 0x3c, 0x6e, 0x86, 0xb5, 0xec, 0xac, 0x64, 0xf4, 0xbf, 0x49, 0x86, 0x83,
0xd8, 0x33, 0x5a, 0xf4, 0xda, 0xb9, 0x17, 0x39, 0xe6, 0x96, 0x72, 0xc2, 0xc1, 0x37, 0x32, 0xa9,
0x44, 0x85, 0x85, 0x97, 0x9e, 0x91, 0xcb, 0xef, 0xfa, 0x3b, 0x47, 0x03, 0x87, 0x72, 0x9f, 0xbe,
0xa6, 0x27, 0xf0, 0xda, 0xf3, 0x3c, 0x0f, 0xb1, 0xd7, 0x7a, 0xe8, 0xe3, 0x82, 0xc7, 0x59, 0x29,
0xf8, 0x52, 0x00, 0x2a, 0xad, 0x23, 0x77, 0x88, 0xfa, 0x40, 0x66, 0xc1, 0x2b, 0x80, 0x2d, 0x15,
0xce, 0x97, 0x15, 0xb6, 0xb0, 0xf6, 0x35, 0xaa, 0x4d, 0xda, 0x49, 0xd4, 0x13, 0x01, 0x5a, 0x21,
0x0c, 0xa4, 0x63, 0xf0, 0x14, 0x57, 0xa4, 0x78, 0x86, 0x33, 0x7a, 0x14, 0x66, 0x6d, 0x4e, 0xcd,
0xd7, 0x55, 0x99, 0x9c, 0x3a, 0x27, 0x4e, 0x58, 0xee, 0x45, 0x64, 0x16, 0x0f, 0x14, 0xf5, 0x00,
0xd6, 0x0a, 0x6a, 0x7c, 0xb0, 0x6b, 0x3c, 0xec, 0xa9, 0x6d, 0xbb, 0xd5, 0xb9, 0x70, 0xa6, 0xcf,
0xed, 0x12, 0x30, 0xf1, 0xaf, 0x31, 0x06, 0xa2, 0x2a, 0x19, 0x45, 0x53, 0x0c, 0xa9, 0x3e, 0x20,
0xc8, 0xe2, 0xb7, 0x5f, 0x71, 0xf4, 0xbe, 0xc2, 0x99, 0x24, 0xe5, 0x13, 0xeb, 0x1d, 0xc4, 0x9c,
0xab, 0x8e, 0xac, 0xfd, 0x64, 0x1c, 0x7d, 0x09, 0xa1, 0x63, 0x40, 0xfd, 0x7e, 0x05, 0x0a, 0xe2,
0x2b, 0xd0, 0x57, 0x42, 0xe0, 0x04, 0xb1, 0x9c, 0xd3, 0xb5, 0x87, 0x2c, 0x97, 0x9c, 0x49, 0xb8,
0xc1, 0xfd, 0xef, 0xeb, 0x93, 0x2e, 0x4d, 0xd0, 0x60, 0xfd, 0x3c, 0xbf, 0x21, 0x80, 0xa6, 0xf2,
0x56, 0x46, 0xa8, 0x1e, 0x95, 0xff, 0xfe, 0xfd, 0x8f, 0xfe, 0xf8, 0x65, 0xd0, 0xf4, 0x91, 0x01,
0xfc, 0xce, 0xa6, 0x6d, 0x62, 0x29, 0x81, 0x0d, 0x9d, 0xb7, 0x6f, 0xde, 0xdf, 0x38, 0x67, 0xce,
0xb8, 0x11, 0x93, 0x56, 0x33, 0xb2, 0x0e, 0xed, 0x6c, 0xc0, 0x5f, 0xd8, 0xb1, 0xa0, 0x92, 0x7c,
0xe8, 0x8c, 0x29, 0x5e, 0xed, 0xf8, 0xc5, 0xbc, 0xd1, 0x6d, 0x9d, 0xe3, 0x51, 0x11, 0x2d, 0x84,
0xf7, 0x59, 0xea, 0x24, 0xda, 0x95, 0x34, 0x5a, 0x1c, 0x1f, 0x4b, 0xe6, 0xee, 0x3a, 0x18, 0x09,
0x69, 0x6f, 0x2c, 0xa5, 0x5e, 0xcc, 0x89, 0x48, 0x4f, 0x1d, 0xad, 0x09, 0xbc, 0x98, 0x88, 0xf6,
0x3a, 0x1c, 0x21, 0xe4, 0x16, 0xc3, 0xc0, 0x2b, 0xb5, 0x9d, 0x94, 0x78, 0x29, 0x34, 0x97, 0xe9,
0xb8, 0xf9, 0xdf, 0xe1, 0x3f, 0x6e, 0x43, 0x4f, 0x78, 0x53, 0x0c, 0x00, 0x00,
};
+64
View File
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>MeshCore OTA</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh}
.c{background:#16213e;border-radius:12px;padding:2em;max-width:420px;width:90%;text-align:center}
h2{margin-bottom:.5em;color:#0ff}
.id{color:#888;font-size:.85em;margin-bottom:1.5em}
input[type=file]{display:none}
.btn{display:inline-block;padding:.7em 1.5em;border-radius:8px;border:none;cursor:pointer;font-size:1em;font-weight:600;transition:background .2s}
.sel{background:#0f3460;color:#eee}
.sel:hover{background:#1a5276}
.upl{background:#00b894;color:#fff;margin-left:.5em}
.upl:hover{background:#00a381}
.upl:disabled{background:#555;cursor:default}
.bar{width:100%;height:24px;background:#0a0a1a;border-radius:12px;margin:1.2em 0;overflow:hidden;display:none}
.fill{height:100%;background:linear-gradient(90deg,#0ff,#00b894);border-radius:12px;transition:width .3s;width:0}
.pct{font-size:.85em;margin-top:.3em}
.st{margin-top:1em;font-size:.9em;min-height:1.4em}
.ok{color:#00b894}
.er{color:#e74c3c}
</style>
</head>
<body>
<div class="c">
<h2>MeshCore OTA Update</h2>
<div class="id" id="devid"></div>
<div>
<button class="btn sel" onclick="document.getElementById('f').click()">Select Firmware</button>
<input type="file" id="f" accept=".bin" onchange="fs(this)">
<button class="btn upl" id="ub" disabled onclick="up()">Upload</button>
</div>
<div id="fn" style="margin-top:.8em;font-size:.85em;color:#888"></div>
<div class="bar" id="bar"><div class="fill" id="fill"></div></div>
<div class="pct" id="pct"></div>
<div class="st" id="st"></div>
</div>
<script>
var file;
function fs(i){file=i.files[0];if(file){document.getElementById('fn').textContent=file.name+' ('+Math.round(file.size/1024)+'KB)';document.getElementById('ub').disabled=false;document.getElementById('st').textContent='';}}
function up(){
if(!file)return;
var x=new XMLHttpRequest();
var b=document.getElementById('bar');
var f=document.getElementById('fill');
var p=document.getElementById('pct');
var s=document.getElementById('st');
b.style.display='block';
document.getElementById('ub').disabled=true;
s.className='st';s.textContent='Uploading...';
x.upload.onprogress=function(e){if(e.lengthComputable){var pc=Math.round(e.loaded/e.total*100);f.style.width=pc+'%';p.textContent=pc+'% ('+Math.round(e.loaded/1024)+'/'+Math.round(e.total/1024)+'KB)';}};
x.onload=function(){if(x.status==200){s.className='st ok';s.textContent='Success! Rebooting...';f.style.width='100%';}else{s.className='st er';s.textContent='Error: '+x.responseText;document.getElementById('ub').disabled=false;}};
x.onerror=function(){s.className='st er';s.textContent='Upload failed — connection error';document.getElementById('ub').disabled=false;};
x.open('POST','/upload');
x.send(file);
}
fetch('/identity').then(r=>r.json()).then(d=>{document.getElementById('devid').textContent=d.name+' ('+d.board+')';}).catch(()=>{});
</script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
#include <zephyr/linker/iterable_sections.h>
ITERABLE_SECTION_ROM(http_resource_desc_ota_service, Z_LINK_ITERABLE_SUBALIGN)
+506
View File
@@ -0,0 +1,506 @@
/*
* SPDX-License-Identifier: Apache-2.0
* WiFi OTA Firmware Update
*
* Starts a WiFi AP + HTTP server for browser-based firmware upload.
* Mirrors Arduino MeshCore's ElegantOTA:
* - WiFi SoftAP "MeshCore-OTA" at 192.168.100.1
* - DHCP server for client IP assignment
* - HTTP server with upload page at /update
* - Firmware written to MCUboot slot1 via flash_img API
* - Reboot after successful upload (MCUboot activates new image)
*/
#include "wifi_ota.h"
#include "ota_page.h"
#include <zephyr/kernel.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_mgmt.h>
#include <zephyr/net/dhcpv4_server.h>
#include <zephyr/net/http/service.h>
#include <zephyr/net/http/server.h>
#include <zephyr/dfu/flash_img.h>
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/logging/log.h>
#include <stdio.h>
#include <string.h>
LOG_MODULE_REGISTER(wifi_ota, LOG_LEVEL_DBG);
/* ========== Configuration ========== */
#define OTA_AP_SSID CONFIG_ZEPHCORE_OTA_AP_SSID
#define OTA_STATIC_IP CONFIG_ZEPHCORE_OTA_AP_IP
#define OTA_NETMASK "255.255.255.0"
#define OTA_DHCP_BASE "192.168.100.10"
/* ========== State ========== */
static bool ota_active;
static struct flash_img_context flash_ctx;
static size_t total_bytes_received;
static bool flash_ctx_initialized;
/* Identity strings for web page */
static char identity_json[128];
static char home_html[384];
/* ========== Reboot work (delayed to let HTTP response send) ========== */
static void ota_reboot_fn(struct k_work *work)
{
LOG_INF("OTA reboot");
sys_reboot(SYS_REBOOT_COLD);
}
static K_WORK_DELAYABLE_DEFINE(ota_reboot_work, ota_reboot_fn);
/* ========== HTTP Service ========== */
static uint16_t http_port = 80;
/* Forward declarations */
static int home_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data);
static int identity_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data);
static int update_page_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data);
static int upload_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data);
/* HTTP service on port 80 */
HTTP_SERVICE_DEFINE(ota_service, NULL, &http_port, 1, 5, NULL, NULL, NULL);
/* GET / — home page */
static struct http_resource_detail_dynamic home_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_DYNAMIC,
.bitmask_of_supported_http_methods = BIT(HTTP_GET),
},
.cb = home_handler,
};
HTTP_RESOURCE_DEFINE(home_res, ota_service, "/", &home_detail);
/* GET /identity — JSON device info (for JS fetch) */
static struct http_resource_detail_dynamic identity_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_DYNAMIC,
.bitmask_of_supported_http_methods = BIT(HTTP_GET),
},
.cb = identity_handler,
};
HTTP_RESOURCE_DEFINE(identity_res, ota_service, "/identity", &identity_detail);
/* GET /update — upload page (gzip-compressed HTML) */
static struct http_resource_detail_dynamic update_page_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_DYNAMIC,
.bitmask_of_supported_http_methods = BIT(HTTP_GET),
},
.cb = update_page_handler,
};
HTTP_RESOURCE_DEFINE(update_res, ota_service, "/update", &update_page_detail);
/* POST /upload — firmware receive */
static struct http_resource_detail_dynamic upload_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_DYNAMIC,
.bitmask_of_supported_http_methods = BIT(HTTP_POST),
},
.cb = upload_handler,
};
HTTP_RESOURCE_DEFINE(upload_res, ota_service, "/upload", &upload_detail);
/* ========== HTTP Handlers ========== */
static int home_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data)
{
LOG_DBG("home_handler status=%d", status);
if (status == HTTP_SERVER_REQUEST_DATA_FINAL) {
response_ctx->body = (const uint8_t *)home_html;
response_ctx->body_len = strlen(home_html);
response_ctx->final_chunk = true;
response_ctx->status = HTTP_200_OK;
LOG_INF("Served home page (%u bytes)", (unsigned)response_ctx->body_len);
}
return 0;
}
static int identity_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data)
{
static const char ct[] = "application/json";
LOG_DBG("identity_handler status=%d", status);
if (status == HTTP_SERVER_REQUEST_DATA_FINAL) {
response_ctx->body = (const uint8_t *)identity_json;
response_ctx->body_len = strlen(identity_json);
response_ctx->final_chunk = true;
response_ctx->status = HTTP_200_OK;
response_ctx->header_count = 1;
static struct http_header ct_hdr = {
.name = "Content-Type",
.value = ct,
};
response_ctx->headers = &ct_hdr;
}
return 0;
}
static int update_page_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data)
{
LOG_DBG("update_page_handler status=%d", status);
if (status == HTTP_SERVER_REQUEST_DATA_FINAL) {
response_ctx->body = ota_page_gz;
response_ctx->body_len = OTA_PAGE_GZ_SIZE;
response_ctx->final_chunk = true;
response_ctx->status = HTTP_200_OK;
/* Headers for gzip-compressed HTML */
static struct http_header hdrs[] = {
{ .name = "Content-Type", .value = "text/html" },
{ .name = "Content-Encoding", .value = "gzip" },
};
response_ctx->headers = hdrs;
response_ctx->header_count = 2;
}
return 0;
}
static int upload_handler(struct http_client_ctx *client,
enum http_transaction_status status,
const struct http_request_ctx *request_ctx,
struct http_response_ctx *response_ctx,
void *user_data)
{
static const char ok_resp[] = "OK";
static const char fail_resp[] = "FAIL";
int ret;
if (status == HTTP_SERVER_REQUEST_DATA_MORE ||
status == HTTP_SERVER_REQUEST_DATA_FINAL) {
/* First chunk: initialize flash context */
if (!flash_ctx_initialized) {
ret = flash_img_init(&flash_ctx);
if (ret) {
LOG_ERR("flash_img_init failed: %d", ret);
response_ctx->status = HTTP_500_INTERNAL_SERVER_ERROR;
response_ctx->body = (const uint8_t *)fail_resp;
response_ctx->body_len = sizeof(fail_resp) - 1;
response_ctx->final_chunk = true;
return 0;
}
flash_ctx_initialized = true;
total_bytes_received = 0;
LOG_INF("OTA upload started");
}
/* Write chunk to flash (slot1) */
bool is_final = (status == HTTP_SERVER_REQUEST_DATA_FINAL);
if (request_ctx->data_len > 0) {
ret = flash_img_buffered_write(&flash_ctx,
request_ctx->data,
request_ctx->data_len,
is_final);
if (ret) {
LOG_ERR("Flash write failed at %u bytes: %d",
(unsigned)total_bytes_received, ret);
flash_ctx_initialized = false;
response_ctx->status = HTTP_500_INTERNAL_SERVER_ERROR;
response_ctx->body = (const uint8_t *)fail_resp;
response_ctx->body_len = sizeof(fail_resp) - 1;
response_ctx->final_chunk = true;
return 0;
}
total_bytes_received += request_ctx->data_len;
}
if (is_final) {
LOG_INF("OTA upload complete: %u bytes",
(unsigned)total_bytes_received);
/* Mark new image for boot (overwrite-only: permanent) */
ret = boot_request_upgrade(BOOT_UPGRADE_PERMANENT);
if (ret) {
LOG_ERR("boot_request_upgrade failed: %d", ret);
flash_ctx_initialized = false;
response_ctx->status = HTTP_500_INTERNAL_SERVER_ERROR;
response_ctx->body = (const uint8_t *)fail_resp;
response_ctx->body_len = sizeof(fail_resp) - 1;
response_ctx->final_chunk = true;
return 0;
}
flash_ctx_initialized = false;
response_ctx->status = HTTP_200_OK;
response_ctx->body = (const uint8_t *)ok_resp;
response_ctx->body_len = sizeof(ok_resp) - 1;
response_ctx->final_chunk = true;
/* Reboot after 2s (let HTTP response send) */
k_work_schedule(&ota_reboot_work, K_SECONDS(2));
}
} else if (status == HTTP_SERVER_TRANSACTION_ABORTED) {
LOG_WRN("OTA upload aborted at %u bytes",
(unsigned)total_bytes_received);
flash_ctx_initialized = false;
total_bytes_received = 0;
}
return 0;
}
/* ========== WiFi Event Monitoring ========== */
static struct net_mgmt_event_callback wifi_mgmt_cb;
static void wifi_mgmt_event_handler(struct net_mgmt_event_callback *cb,
uint32_t mgmt_event,
struct net_if *iface)
{
switch (mgmt_event) {
case NET_EVENT_WIFI_AP_ENABLE_RESULT:
LOG_INF("WiFi AP enable result event received");
break;
case NET_EVENT_WIFI_AP_DISABLE_RESULT:
LOG_INF("WiFi AP disable result event");
break;
case NET_EVENT_WIFI_AP_STA_CONNECTED:
LOG_INF("WiFi client CONNECTED to AP");
break;
case NET_EVENT_WIFI_AP_STA_DISCONNECTED:
LOG_INF("WiFi client DISCONNECTED from AP");
break;
default:
LOG_DBG("WiFi mgmt event: 0x%08x", mgmt_event);
break;
}
}
/* ========== WiFi AP Setup ========== */
static int wifi_ap_start(void)
{
struct net_if *iface = net_if_get_default();
if (!iface) {
LOG_ERR("No network interface");
return -ENODEV;
}
LOG_INF("Network iface: %p, idx=%d", iface, net_if_get_by_iface(iface));
/* Register WiFi management event callback */
net_mgmt_init_event_callback(&wifi_mgmt_cb, wifi_mgmt_event_handler,
NET_EVENT_WIFI_AP_ENABLE_RESULT |
NET_EVENT_WIFI_AP_DISABLE_RESULT |
NET_EVENT_WIFI_AP_STA_CONNECTED |
NET_EVENT_WIFI_AP_STA_DISCONNECTED);
net_mgmt_add_event_callback(&wifi_mgmt_cb);
/* Set static IP */
struct in_addr addr, netmask;
if (net_addr_pton(AF_INET, OTA_STATIC_IP, &addr)) {
LOG_ERR("Invalid IP: %s", OTA_STATIC_IP);
return -EINVAL;
}
if (net_addr_pton(AF_INET, OTA_NETMASK, &netmask)) {
LOG_ERR("Invalid netmask");
return -EINVAL;
}
struct net_if_addr *ifaddr = net_if_ipv4_addr_add(iface, &addr, NET_ADDR_MANUAL, 0);
if (!ifaddr) {
LOG_ERR("Failed to set static IP %s", OTA_STATIC_IP);
return -ENOMEM;
}
net_if_ipv4_set_netmask_by_addr(iface, &addr, &netmask);
net_if_ipv4_set_gw(iface, &addr);
LOG_INF("Static IP set: %s/%s", OTA_STATIC_IP, OTA_NETMASK);
/* Enable WiFi AP */
struct wifi_connect_req_params ap_params = {0};
ap_params.ssid = (const uint8_t *)OTA_AP_SSID;
ap_params.ssid_length = strlen(OTA_AP_SSID);
ap_params.channel = WIFI_CHANNEL_ANY;
ap_params.security = WIFI_SECURITY_TYPE_NONE;
ap_params.band = WIFI_FREQ_BAND_2_4_GHZ;
LOG_INF("Enabling WiFi AP: SSID=%s, channel=any, security=open", OTA_AP_SSID);
int ret = net_mgmt(NET_REQUEST_WIFI_AP_ENABLE, iface,
&ap_params, sizeof(ap_params));
if (ret) {
LOG_ERR("WiFi AP enable failed: %d", ret);
return ret;
}
LOG_INF("WiFi AP started: SSID=%s", OTA_AP_SSID);
/* Bring the interface up explicitly */
if (!net_if_is_up(iface)) {
LOG_WRN("Interface not up, bringing up...");
ret = net_if_up(iface);
if (ret) {
LOG_ERR("net_if_up failed: %d", ret);
}
} else {
LOG_INF("Interface is up");
}
/* Start DHCP server */
struct in_addr dhcp_base;
if (net_addr_pton(AF_INET, OTA_DHCP_BASE, &dhcp_base) == 0) {
ret = net_dhcpv4_server_start(iface, &dhcp_base);
if (ret) {
LOG_WRN("DHCP server start failed: %d (non-fatal)", ret);
} else {
LOG_INF("DHCP server started (pool: %s+)", OTA_DHCP_BASE);
}
}
return 0;
}
static int wifi_ap_stop(void)
{
struct net_if *iface = net_if_get_default();
if (!iface) {
return -ENODEV;
}
net_dhcpv4_server_stop(iface);
int ret = net_mgmt(NET_REQUEST_WIFI_AP_DISABLE, iface, NULL, 0);
if (ret) {
LOG_WRN("WiFi AP disable failed: %d", ret);
}
/* Remove static IP */
struct in_addr addr;
if (net_addr_pton(AF_INET, OTA_STATIC_IP, &addr) == 0) {
net_if_ipv4_addr_rm(iface, &addr);
}
return ret;
}
/* ========== Public API ========== */
int wifi_ota_start(const char *node_name, const char *board_name)
{
if (ota_active) {
return -EALREADY;
}
/* Prepare identity strings for web page */
snprintf(identity_json, sizeof(identity_json),
"{\"name\":\"%s\",\"board\":\"%s\"}",
node_name ? node_name : "Unknown",
board_name ? board_name : "Unknown");
snprintf(home_html, sizeof(home_html),
"<html><body style=\"background:#1a1a2e;color:#eee;font-family:sans-serif;"
"display:flex;justify-content:center;align-items:center;height:100vh\">"
"<h2 style=\"color:#0ff\">MeshCore OTA: %s (%s)<br>"
"<a href=\"/update\" style=\"color:#00b894\">Go to Update Page</a></h2>"
"</body></html>",
node_name ? node_name : "Unknown",
board_name ? board_name : "Unknown");
/* Start WiFi AP */
int ret = wifi_ap_start();
if (ret) {
return ret;
}
/* Start HTTP server */
LOG_INF("Starting HTTP server on port %u...", http_port);
ret = http_server_start();
if (ret) {
LOG_ERR("HTTP server start failed: %d", ret);
wifi_ap_stop();
return ret;
}
ota_active = true;
flash_ctx_initialized = false;
total_bytes_received = 0;
LOG_INF("OTA server ready at http://%s/update", OTA_STATIC_IP);
LOG_INF("HTTP routes: / (home), /identity (json), /update (upload page), /upload (POST)");
return 0;
}
int wifi_ota_stop(void)
{
if (!ota_active) {
return 0;
}
http_server_stop();
wifi_ap_stop();
ota_active = false;
flash_ctx_initialized = false;
total_bytes_received = 0;
LOG_INF("OTA server stopped");
return 0;
}
bool wifi_ota_is_active(void)
{
return ota_active;
}
void wifi_ota_confirm_image(void)
{
#if IS_ENABLED(CONFIG_BOOTLOADER_MCUBOOT)
if (!boot_is_img_confirmed()) {
int ret = boot_write_img_confirmed();
if (ret) {
LOG_ERR("Failed to confirm MCUboot image: %d", ret);
} else {
LOG_INF("MCUboot image confirmed");
}
}
#endif
}
+53
View File
@@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: Apache-2.0
* WiFi OTA Firmware Update — Public API
*
* Starts a WiFi AP + HTTP server for browser-based firmware upload.
* Mirrors Arduino MeshCore's ElegantOTA behavior.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Start WiFi AP + HTTP OTA server.
*
* Creates WiFi AP "MeshCore-OTA" with static IP 192.168.100.1,
* starts DHCP server and HTTP server with upload page at /update.
* The mesh continues running — LoRa is independent of WiFi.
*
* @param node_name Device name (shown on OTA page)
* @param board_name Board/manufacturer name (shown on OTA page)
* @return 0 on success, -EALREADY if active, negative errno on failure
*/
int wifi_ota_start(const char *node_name, const char *board_name);
/**
* Stop WiFi AP + HTTP OTA server.
* Tears down HTTP server, DHCP server, and WiFi AP.
*
* @return 0 on success, negative errno on failure
*/
int wifi_ota_stop(void);
/**
* Check if OTA mode is currently active.
*/
bool wifi_ota_is_active(void);
/**
* Confirm the current MCUboot image at startup.
* Call early in main() before mesh init.
* If the running image is unconfirmed (first boot after OTA),
* marks it as confirmed so MCUboot keeps it on next reboot.
*/
void wifi_ota_confirm_image(void);
#ifdef __cplusplus
}
#endif
+50
View File
@@ -10,6 +10,7 @@
#include <string.h>
#include <math.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lora_radio_base, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
@@ -26,6 +27,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
_noise_floor(DEFAULT_NOISE_FLOOR), _calibration_threshold(0),
_rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)),
_rx_boost_enabled(true),
_config_cached(false),
_rx_cb(nullptr), _rx_cb_user_data(nullptr),
_tx_done_cb(nullptr), _tx_done_cb_user_data(nullptr),
_tx_thread_running(false),
@@ -141,6 +143,10 @@ void LoRaRadioBase::rxCallbackStatic(const struct device *dev, uint8_t *data,
self->_packets_recv_errors++;
LOG_DBG("RX error (CRC/header), total errors: %u",
self->_packets_recv_errors);
/* Driver restarted with Radio.Rx(0) — restore duty cycle */
if (self->_rx_duty_cycle_enabled) {
self->hwSetRxDutyCycle(true);
}
return;
}
@@ -165,6 +171,11 @@ void LoRaRadioBase::rxCallbackStatic(const struct device *dev, uint8_t *data,
self->_last_snr = (float)snr;
self->_packets_recv++;
/* Driver restarted with Radio.Rx(0) — restore duty cycle */
if (self->_rx_duty_cycle_enabled) {
self->hwSetRxDutyCycle(true);
}
if (self->_rx_cb) {
self->_rx_cb(self->_rx_cb_user_data);
}
@@ -197,23 +208,61 @@ void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx)
cfg.packet_crc_disable = false;
}
/**
* Compare radio-relevant fields of two modem configs.
* Ignores the tx flag — that only selects TX vs RX mode, the actual
* modem parameters (freq, SF, BW, CR, power) are what the driver
* programs into registers.
*/
static bool configParamsEqual(const struct lora_modem_config &a,
const struct lora_modem_config &b)
{
/* CRITICAL: a.tx == b.tx MUST be compared — without it, switching
* RX→TX skips RadioSetTxConfig(), leaving TxTimeout=0 which causes
* the loramac-node software timeout to fire after 1ms and break TX. */
return a.frequency == b.frequency &&
a.bandwidth == b.bandwidth &&
a.datarate == b.datarate &&
a.coding_rate == b.coding_rate &&
a.preamble_len == b.preamble_len &&
a.tx_power == b.tx_power &&
a.tx == b.tx &&
a.iq_inverted == b.iq_inverted &&
a.public_network == b.public_network;
}
void LoRaRadioBase::configureRx()
{
struct lora_modem_config cfg;
buildModemConfig(cfg, false);
if (_config_cached && configParamsEqual(cfg, _last_cfg)) {
LOG_DBG("configureRx: params unchanged, skipping hwConfigure");
return;
}
LOG_DBG("configureRx: freq=%u bw=%d sf=%d cr=%d pwr=%d",
cfg.frequency, (int)cfg.bandwidth, (int)cfg.datarate,
(int)cfg.coding_rate, cfg.tx_power);
hwConfigure(cfg);
_last_cfg = cfg;
_config_cached = true;
}
void LoRaRadioBase::configureTx()
{
struct lora_modem_config cfg;
buildModemConfig(cfg, true);
if (_config_cached && configParamsEqual(cfg, _last_cfg)) {
LOG_DBG("configureTx: params unchanged, skipping hwConfigure");
return;
}
hwConfigure(cfg);
_last_cfg = cfg;
_config_cached = true;
}
/* ── Lifecycle ────────────────────────────────────────────────────────── */
@@ -252,6 +301,7 @@ void LoRaRadioBase::reconfigure()
{
hwCancelReceive();
_in_recv_mode = false;
_config_cached = false; /* Force full reconfigure */
startReceive();
uint32_t freq = _prefs ? (uint32_t)(_prefs->freq * 1000000.0f)
+4
View File
@@ -145,6 +145,10 @@ protected:
bool _rx_duty_cycle_enabled;
bool _rx_boost_enabled;
/* Config cache — skip redundant hwConfigure() on TX↔RX transitions */
struct lora_modem_config _last_cfg;
bool _config_cached;
/* Static RX callback — passed to lora_recv_async() by subclass hwStartReceive() */
static void rxCallbackStatic(const struct device *dev, uint8_t *data,
uint16_t size, int16_t rssi, int8_t snr,
+2 -2
View File
@@ -228,9 +228,9 @@ void SX126xRadio::applyRxDutyCycleIfEnabled()
uint32_t bw_int = (uint32_t)bw_khz;
uint32_t bw_frac = (uint32_t)((bw_khz - bw_int) * 10);
LOG_INF("RX duty cycle: SF%d BW%u.%u sym=%uus preamble=%ums",
LOG_DBG("RX duty cycle: SF%d BW%u.%u sym=%uus preamble=%ums",
sf, bw_int, bw_frac, symbol_us, preamble_ms);
LOG_INF(" -> rx=%ums sleep=%ums period=%ums (minSym=%d sleepSym=%d)",
LOG_DBG(" -> rx=%ums sleep=%ums period=%ums (minSym=%d sleepSym=%d)",
rx_ms, sleep_ms, total_ms, min_symbols, sleep_symbols);
}
+57
View File
@@ -0,0 +1,57 @@
# WiFi OTA Configuration
# Auto-included by CMakeLists.txt for ESP32 repeater builds.
# Enables WiFi AP + HTTP server + MCUboot image management.
CONFIG_ZEPHCORE_WIFI_OTA=y
# ========== Networking Stack ==========
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_IPV6=n
CONFIG_NET_TCP=y
CONFIG_NET_UDP=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_MAX_CONTEXTS=4
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_PKT_TX_COUNT=16
CONFIG_NET_BUF_RX_COUNT=32
CONFIG_NET_BUF_TX_COUNT=32
CONFIG_NET_IF_MAX_IPV4_COUNT=1
CONFIG_NET_MGMT=y
CONFIG_NET_MGMT_EVENT=y
CONFIG_ZVFS_POLL_MAX=8
CONFIG_EVENTFD=y
CONFIG_ZVFS_EVENTFD_MAX=4
CONFIG_ZVFS_OPEN_MAX=16
# ========== WiFi AP ==========
CONFIG_WIFI=y
CONFIG_NET_L2_WIFI_MGMT=y
CONFIG_NET_L2_ETHERNET=y
# ========== DHCP Server ==========
CONFIG_NET_DHCPV4_SERVER=y
CONFIG_NET_DHCPV4_SERVER_ADDR_COUNT=4
CONFIG_NET_DHCPV4_SERVER_INSTANCES=1
# ========== HTTP Server ==========
CONFIG_HTTP_SERVER=y
CONFIG_HTTP_SERVER_NUM_SERVICES=1
CONFIG_HTTP_SERVER_MAX_CLIENTS=1
CONFIG_HTTP_SERVER_MAX_URL_LENGTH=64
CONFIG_HTTP_SERVER_CLIENT_BUFFER_SIZE=1024
CONFIG_HTTP_SERVER_STACK_SIZE=4096
# ========== MCUboot Image Management (ESP32 only) ==========
CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_IMG_MANAGER=y
CONFIG_STREAM_FLASH=y
CONFIG_IMG_BLOCK_BUF_SIZE=4096
CONFIG_IMG_ERASE_PROGRESSIVELY=y
# ========== Debug logging (temporary) ==========
CONFIG_NET_LOG=y
# ========== Heap (WiFi + TCP/IP + HTTP needs more than default) ==========
CONFIG_HEAP_MEM_POOL_SIZE=65536
+3 -2
View File
@@ -147,8 +147,9 @@ CONFIG_INPUT_MODE_SYNCHRONOUS=y
CONFIG_POWEROFF=y
# ========== LoRa Power Saving ==========
# RX duty cycle enabled by default for battery-powered devices
CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=y
# RX duty cycle OFF by default — causes marginal packet loss on repeaters.
# Enable per-board via board.conf, or at runtime via CLI "set rxduty on".
CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=n
# ========== ZephCore Log Levels ==========
# Defaults are INF for all modules (Kconfig default — no explicit =y needed).
@@ -5,7 +5,7 @@
* Pin mapping (from Arduino MeshCore variants/station_g2):
* LoRa SPI2: SCLK=GPIO12, MOSI=GPIO13, MISO=GPIO14, NSS=GPIO11
* I2C0: SDA=GPIO5, SCL=GPIO6 (SH1106 OLED + sensors)
* UART1: TX=GPIO7, RX=GPIO15 (GPS)
* UART1: TX=GPIO15, RX=GPIO7 (GPS — GPIO7 is GPS TX, GPIO15 is GPS RX)
* Console: USB Serial (native ESP32-S3 USB)
*/
@@ -37,14 +37,14 @@
};
};
/* UART1 — GPS module */
/* UART1 — GPS module (GPIO7 = GPS TX → CPU RX, GPIO15 = GPS RX → CPU TX) */
uart1_default: uart1_default {
group1 {
pinmux = <UART1_TX_GPIO7>;
pinmux = <UART1_TX_GPIO15>;
output-high;
};
group2 {
pinmux = <UART1_RX_GPIO15>;
pinmux = <UART1_RX_GPIO7>;
bias-pull-up;
};
};
@@ -77,3 +77,8 @@
/* LittleFS auto-mount — standard /lfs mount point */
#include "../../common/filesystem.dtsi"
/* WiFi — required for OTA firmware updates */
&wifi {
status = "okay";
};
@@ -77,3 +77,8 @@
/* LittleFS auto-mount — standard /lfs mount point */
#include "../../common/filesystem.dtsi"
/* WiFi — required for OTA firmware updates */
&wifi {
status = "okay";
};
+43 -1
View File
@@ -13,6 +13,10 @@
#include <string.h>
#include <stdio.h>
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
#include "wifi_ota.h"
#endif
LOG_MODULE_REGISTER(zephcore_cli, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
// Helper: robust atoi
@@ -94,6 +98,7 @@ void CommonCLI::loadPrefs(const char* path) {
fs_read(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
fs_read(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); // 170
fs_read(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); // 290
fs_read(&file, &_prefs->rx_duty_cycle, sizeof(_prefs->rx_duty_cycle)); // 291
fs_close(&file);
@@ -122,6 +127,7 @@ void CommonCLI::loadPrefs(const char* path) {
_prefs->gps_enabled = constrain(_prefs->gps_enabled, (uint8_t)0, (uint8_t)1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, (uint8_t)0, (uint8_t)2);
_prefs->rx_boost = constrain(_prefs->rx_boost, (uint8_t)0, (uint8_t)1);
_prefs->rx_duty_cycle = constrain(_prefs->rx_duty_cycle, (uint8_t)0, (uint8_t)1);
LOG_INF("Loaded prefs from %s", path);
}
@@ -182,6 +188,7 @@ void CommonCLI::savePrefs(const char* path) {
fs_write(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier));
fs_write(&file, _prefs->owner_info, sizeof(_prefs->owner_info));
fs_write(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost));
fs_write(&file, &_prefs->rx_duty_cycle, sizeof(_prefs->rx_duty_cycle));
fs_close(&file);
LOG_INF("Saved prefs to %s", path);
@@ -245,9 +252,33 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "OK - rebooting to UF2 DFU");
scheduleReboot(REBOOT_DFU);
} else if (memcmp(command, "start ota", 9) == 0) {
/* Reboot into Adafruit BLE OTA DFU mode */
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
/* ESP32: Start WiFi AP + HTTP OTA server (no reboot) */
int ota_ret = wifi_ota_start(_prefs->node_name, _board->getManufacturerName());
if (ota_ret == 0) {
snprintf(reply, CLI_REPLY_SIZE, "Started: http://%s/update",
CONFIG_ZEPHCORE_OTA_AP_IP);
} else if (ota_ret == -EALREADY) {
strcpy(reply, "OTA already active");
} else {
snprintf(reply, CLI_REPLY_SIZE, "Error starting OTA: %d", ota_ret);
}
#else
/* nRF52: Reboot into Adafruit BLE OTA DFU mode */
strcpy(reply, "OK - rebooting to BLE OTA DFU");
scheduleReboot(REBOOT_OTA);
#endif
} else if (memcmp(command, "stop ota", 8) == 0) {
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
if (wifi_ota_is_active()) {
wifi_ota_stop();
strcpy(reply, "OTA stopped");
} else {
strcpy(reply, "OTA not active");
}
#else
strcpy(reply, "Not supported");
#endif
} else if (memcmp(command, "reboot", 6) == 0) {
strcpy(reply, "OK - rebooting");
scheduleReboot(REBOOT_NORMAL);
@@ -577,6 +608,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
snprintf(reply, CLI_REPLY_SIZE, "OK - rxboost=%d (reboot to apply)", _prefs->rx_boost);
} else if (memcmp(config, "rxboost", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
snprintf(reply, CLI_REPLY_SIZE, "> %d", _prefs->rx_boost);
} else if (memcmp(config, "rxduty ", 7) == 0) {
if (memcmp(&config[7], "on", 2) == 0) {
_prefs->rx_duty_cycle = 1;
} else {
_prefs->rx_duty_cycle = 0;
}
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - rxduty=%s (reboot to apply)",
_prefs->rx_duty_cycle ? "on" : "off");
} else if (memcmp(config, "rxduty", 6) == 0 && (config[6] == 0 || config[6] == ' ')) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->rx_duty_cycle ? "on" : "off");
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: %s", config);
}
+2
View File
@@ -57,6 +57,7 @@ struct NodePrefs {
float adc_multiplier;
char owner_info[120];
uint8_t rx_boost; // 1 = boosted RX gain (+3dB, +2mA), 0 = power save
uint8_t rx_duty_cycle; // 1 = RX duty cycle (power save), 0 = continuous RX
/* ---- Companion-only fields (Zephyr additions, not in Arduino) ---- */
uint8_t manual_add_contacts;
@@ -112,4 +113,5 @@ static inline void initNodePrefs(NodePrefs* prefs) {
prefs->advert_loc_policy = ADVERT_LOC_NONE;
prefs->adc_multiplier = 0.0f;
prefs->rx_boost = 1; // Default to boosted RX for better sensitivity
prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability
}
+10
View File
@@ -65,6 +65,10 @@ public:
virtual Packet *getNextInbound(uint32_t now) = 0;
};
/* Callback fired when a packet is queued for transmission with a delay.
* Allows the event loop to schedule a precise wake at delay expiry. */
typedef void (*tx_queued_callback_t)(uint32_t delay_ms, void *user_data);
typedef uint32_t DispatcherAction;
#define ACTION_RELEASE (0)
@@ -87,6 +91,8 @@ class Dispatcher {
bool prev_isrecv_mode;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
tx_queued_callback_t _tx_queued_cb;
void *_tx_queued_user_data;
void processRecvPacket(Packet *pkt);
@@ -130,6 +136,10 @@ public:
n_recv_flood = n_recv_direct = 0;
_err_flags = 0;
}
void setTxQueuedCallback(tx_queued_callback_t cb, void *user_data) {
_tx_queued_cb = cb;
_tx_queued_user_data = user_data;
}
bool millisHasNowPassed(uint32_t timestamp) const;
uint32_t futureMillis(int millis_from_now) const;
+6
View File
@@ -5,6 +5,12 @@
#include "fixedint.h"
/* Prefix to avoid symbol collision with ESP32 WPA supplicant's sha512 */
#define sha512_init ed25519_sha512_init
#define sha512_final ed25519_sha512_final
#define sha512_update ed25519_sha512_update
#define sha512 ed25519_sha512
/* state */
typedef struct sha512_context_ {
uint64_t length, state[8];
@@ -0,0 +1,199 @@
/*
* Copyright (c) 2023 Espressif Systems (Shanghai) Co., Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*
* ZephCore patch: Added #ifndef guards for MCUBOOT_MAX_IMG_SECTORS,
* fixed mbedTLS detection (also check CONFIG_BOOT_USE_MBEDTLS), and
* made MCUBOOT_VALIDATE_PRIMARY_SLOT conditional on Kconfig.
*/
#ifndef __MCUBOOT_CONFIG_H__
#define __MCUBOOT_CONFIG_H__
#include <zephyr/devicetree.h>
#include <watchdog.h>
/*
* Signature types
*
* You must choose exactly one signature type - check bootloader.conf
* configuration file
*/
/* Uncomment for RSA signature support */
#if defined(CONFIG_ESP_SIGN_RSA)
#define MCUBOOT_SIGN_RSA
# if (CONFIG_ESP_SIGN_RSA_LEN != 2048 && \
CONFIG_ESP_SIGN_RSA_LEN != 3072)
# error "Invalid RSA key size (must be 2048 or 3072)"
# else
# define MCUBOOT_SIGN_RSA_LEN CONFIG_ESP_SIGN_RSA_LEN
# endif
#elif defined(CONFIG_ESP_SIGN_EC256)
#define MCUBOOT_SIGN_EC256
#elif defined(CONFIG_ESP_SIGN_ED25519)
#define MCUBOOT_SIGN_ED25519
#endif
/* This mcuboot_config.h is used only by Zephyr builds, such as MCUboot
* Zephyr Port for ESP chips or a Zephyr application with MCUboot
* compatibilty (that builds the bootutil lib).
* In these cases, MCUBOOT_BOOT_MAX_ALIGN value must be taken from DT if
* the write-block-size is greater than 8 */
#if DT_PROP(DT_CHOSEN(zephyr_flash), write_block_size) > 8
#define MCUBOOT_BOOT_MAX_ALIGN DT_PROP(DT_CHOSEN(zephyr_flash), write_block_size)
#endif
/*
* Upgrade mode
*
* The default is to support A/B image swapping with rollback. Other modes
* with simpler code path, which only supports overwriting the existing image
* with the update image or running the newest image directly from its flash
* partition, are also available.
*
* You can enable only one mode at a time from the list below to override
* the default upgrade mode.
*/
/* Uncomment to enable the overwrite-only code path. */
/* #define MCUBOOT_OVERWRITE_ONLY */
#ifdef MCUBOOT_OVERWRITE_ONLY
/* Uncomment to only erase and overwrite those primary slot sectors needed
* to install the new image, rather than the entire image slot. */
/* #define MCUBOOT_OVERWRITE_ONLY_FAST */
#endif
/* Uncomment to enable the direct-xip code path. */
/* #define MCUBOOT_DIRECT_XIP */
/* Uncomment to enable the ram-load code path. */
/* #define MCUBOOT_RAM_LOAD */
/*
* Cryptographic settings
*
* You must choose between Mbed TLS and Tinycrypt as source of
* cryptographic primitives. Other cryptographic settings are also
* available.
*/
/* ZephCore patch: also check CONFIG_BOOT_USE_MBEDTLS (Zephyr Kconfig) */
#if defined(CONFIG_ESP_USE_MBEDTLS) || defined(CONFIG_BOOT_USE_MBEDTLS)
#define MCUBOOT_USE_MBED_TLS
#else
/* MCUboot requires the definition of a crypto lib,
* using Tinycrypt as default */
#define MCUBOOT_USE_TINYCRYPT
#endif
/*
* Always check the signature of the image in the primary slot before booting,
* even if no upgrade was performed. This is recommended if the boot
* time penalty is acceptable.
*/
/* ZephCore patch: honor Kconfig CONFIG_BOOT_VALIDATE_SLOT0 instead of
* always validating. When unset, skip per-boot validation for faster boot. */
#if !defined(CONFIG_BOOT_VALIDATE_SLOT0) || CONFIG_BOOT_VALIDATE_SLOT0
#define MCUBOOT_VALIDATE_PRIMARY_SLOT
#endif
#ifdef CONFIG_ESP_DOWNGRADE_PREVENTION
#define MCUBOOT_DOWNGRADE_PREVENTION 1
/* MCUBOOT_DOWNGRADE_PREVENTION_SECURITY_COUNTER is used later as bool value so it is
* always defined, (unlike MCUBOOT_DOWNGRADE_PREVENTION which is only used in
* preprocessor condition and my be not defined) */
# ifdef CONFIG_ESP_DOWNGRADE_PREVENTION_SECURITY_COUNTER
# define MCUBOOT_DOWNGRADE_PREVENTION_SECURITY_COUNTER 1
# else
# define MCUBOOT_DOWNGRADE_PREVENTION_SECURITY_COUNTER 0
# endif
#endif
/*
* Flash abstraction
*/
/* Uncomment if your flash map API supports flash_area_get_sectors().
* See the flash APIs for more details. */
#define MCUBOOT_USE_FLASH_AREA_GET_SECTORS
/* Default maximum number of flash sectors per image slot; change
* as desirable. */
/* ZephCore patch: allow override via -D or MIN_SECTOR_COUNT from AUTO mode */
#if defined(MIN_SECTOR_COUNT)
#define MCUBOOT_MAX_IMG_SECTORS MIN_SECTOR_COUNT
#elif !defined(MCUBOOT_MAX_IMG_SECTORS)
#define MCUBOOT_MAX_IMG_SECTORS 512
#endif
/* Default number of separately updateable images; change in case of
* multiple images. */
#if defined(CONFIG_ESP_IMAGE_NUMBER)
#define MCUBOOT_IMAGE_NUMBER CONFIG_ESP_IMAGE_NUMBER
#else
#define MCUBOOT_IMAGE_NUMBER 1
#endif
/*
* Logging
*/
/*
* If logging is enabled the following functions must be defined by the
* platform:
*
* MCUBOOT_LOG_MODULE_REGISTER(domain)
* Register a new log module and add the current C file to it.
*
* MCUBOOT_LOG_MODULE_DECLARE(domain)
* Add the current C file to an existing log module.
*
* MCUBOOT_LOG_ERR(...)
* MCUBOOT_LOG_WRN(...)
* MCUBOOT_LOG_INF(...)
* MCUBOOT_LOG_DBG(...)
*
* The function priority is:
*
* MCUBOOT_LOG_ERR > MCUBOOT_LOG_WRN > MCUBOOT_LOG_INF > MCUBOOT_LOG_DBG
*/
#define MCUBOOT_HAVE_LOGGING 1
/* #define MCUBOOT_LOG_LEVEL MCUBOOT_LOG_LEVEL_INFO */
/*
* Assertions
*/
/* Uncomment if your platform has its own mcuboot_config/mcuboot_assert.h.
* If so, it must provide an ASSERT macro for use by bootutil. Otherwise,
* "assert" is used. */
#define MCUBOOT_HAVE_ASSERT_H 1
#ifdef CONFIG_ESP_MCUBOOT_SERIAL
#define CONFIG_MCUBOOT_SERIAL
#endif
/*
* When a serial recovery process is receiving the image data, this option
* enables it to erase flash progressively (by sectors) instead of the
* default behavior that is erasing whole image size of flash area after
* receiving first frame.
* Enabling this options prevents stalling the beginning of transfer
* for the time needed to erase large chunk of flash.
*/
#ifdef CONFIG_ESP_MCUBOOT_ERASE_PROGRESSIVELY
#define MCUBOOT_ERASE_PROGRESSIVELY
#endif
/* Serial extensions are not implemented
*/
#define MCUBOOT_PERUSER_MGMT_GROUP_ENABLED 0
#define MCUBOOT_CPU_IDLE() \
do { \
} while (0)
#endif /* __MCUBOOT_CONFIG_H__ */
@@ -41,6 +41,12 @@ static struct sx12xx_data {
struct lora_modem_config tx_cfg;
atomic_t modem_usage;
struct sx12xx_rx_params rx_params;
/* Fast TX↔RX switching: cache last full config to detect
* direction-only changes and skip redundant SPI reconfigure. */
struct lora_modem_config last_cfg;
bool last_cfg_valid;
bool tx_configured; /* RadioSetTxConfig has been called with current params */
bool rx_configured; /* RadioSetRxConfig has been called with current params */
} dev_data;
int __sx12xx_configure_pin(const struct gpio_dt_spec *gpio, gpio_flags_t flags)
@@ -382,6 +388,21 @@ int sx12xx_lora_recv_async(const struct device *dev, lora_recv_cb cb, void *user
return 0;
}
/* Check if only the TX/RX direction changed (all radio params identical). */
static bool sx12xx_only_direction_changed(const struct lora_modem_config *a,
const struct lora_modem_config *b)
{
return a->frequency == b->frequency &&
a->bandwidth == b->bandwidth &&
a->datarate == b->datarate &&
a->coding_rate == b->coding_rate &&
a->preamble_len == b->preamble_len &&
a->tx_power == b->tx_power &&
a->iq_inverted == b->iq_inverted &&
a->public_network == b->public_network &&
a->tx != b->tx;
}
int sx12xx_lora_config(const struct device *dev,
struct lora_modem_config *config)
{
@@ -394,6 +415,36 @@ int sx12xx_lora_config(const struct device *dev,
LOG_ERR("Unsupported bandwidth: %d", config->bandwidth);
return ret;
}
/* Fast path: if only TX↔RX direction changed and the target direction
* was already configured once with the same params, skip the full
* RadioSetTxConfig/RadioSetRxConfig (saves ~35ms of SPI traffic).
* RadioSetTxConfig MUST have been called at least once to set
* TxTimeout=4000; RadioSetRxConfig MUST have been called at least
* once to set PayloadLength/SymbTimeout/IQ polarity workaround. */
if (dev_data.last_cfg_valid &&
sx12xx_only_direction_changed(config, &dev_data.last_cfg)) {
if (config->tx && dev_data.tx_configured) {
LOG_DBG("lora_config: fast TX switch (skip full reconfig)");
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
memcpy(&dev_data.tx_cfg, config, sizeof(dev_data.tx_cfg));
dev_data.last_cfg = *config;
modem_release(&dev_data);
return 0;
}
if (!config->tx && dev_data.rx_configured) {
LOG_DBG("lora_config: fast RX switch (skip full reconfig)");
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
dev_data.last_cfg = *config;
modem_release(&dev_data);
return 0;
}
}
LOG_INF("lora_config: bw_enum=%d bw_idx=%u tx=%d freq=%u sf=%d",
config->bandwidth, bw_idx, config->tx, config->frequency,
config->datarate);
@@ -413,16 +464,21 @@ int sx12xx_lora_config(const struct device *dev,
bw_idx, config->datarate,
config->coding_rate, config->preamble_len,
false, crc, 0, 0, config->iq_inverted, 4000);
dev_data.tx_configured = true;
} else {
/* TODO: Get symbol timeout value from config parameters */
Radio.SetRxConfig(MODEM_LORA, bw_idx,
config->datarate, config->coding_rate,
0, config->preamble_len, 10, false, 0,
crc, false, 0, config->iq_inverted, true);
dev_data.rx_configured = true;
}
Radio.SetPublicNetwork(config->public_network);
dev_data.last_cfg = *config;
dev_data.last_cfg_valid = true;
modem_release(&dev_data);
return 0;
}
+8
View File
@@ -38,6 +38,8 @@ Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
prev_isrecv_mode = true;
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
_tx_queued_cb = nullptr;
_tx_queued_user_data = nullptr;
}
void Dispatcher::begin()
@@ -277,6 +279,9 @@ void Dispatcher::processRecvPacket(Packet *pkt)
uint8_t priority = (uint8_t)((action >> 24) - 1);
uint32_t delay = action & 0xFFFFFF;
_mgr->queueOutbound(pkt, priority, futureMillis((int)delay));
if (_tx_queued_cb && delay > 0) {
_tx_queued_cb(delay, _tx_queued_user_data);
}
}
}
@@ -410,6 +415,9 @@ void Dispatcher::sendPacket(Packet *packet, uint8_t priority, uint32_t delay_mil
} else {
_mgr->queueOutbound(packet, priority, futureMillis((int)delay_millis));
LOG_INF("sendPacket: queued outbound count=%d", _mgr->getOutboundCount((uint32_t)_ms->getMillis()));
if (_tx_queued_cb && delay_millis > 0) {
_tx_queued_cb(delay_millis, _tx_queued_user_data);
}
}
}
+22 -2
View File
@@ -71,7 +71,8 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */
#define MESH_EVENT_UI_ACTION BIT(4) /* Button action from UI (deferred to mesh thread) */
#define MESH_EVENT_GPS_ACTION BIT(5) /* GPS state change (must run on main thread!) */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | MESH_EVENT_GPS_ACTION)
#define MESH_EVENT_TX_DRAIN BIT(6) /* Outbound packet delay expired, run checkSend */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN)
/* Housekeeping interval - infrequent to preserve power savings */
#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS
@@ -308,6 +309,22 @@ static void lora_tx_done_callback(void *user_data)
/* Signal event to wake mesh loop for TX completion handling */
k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE);
}
/* TX drain — Dispatcher queued a packet with a delay.
* Schedule a precise wake so checkSend runs when the delay expires. */
static void tx_drain_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
k_event_post(&mesh_events, MESH_EVENT_TX_DRAIN);
}
static K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn);
static void tx_queued_callback(uint32_t delay_ms, void *user_data)
{
ARG_UNUSED(user_data);
k_work_reschedule(&tx_drain_work, K_MSEC(delay_ms));
}
#endif
static mesh::ZephyrRTCClock rtc_clock;
@@ -471,6 +488,7 @@ int main(void)
companion_mesh.prefs.tx_power_dbm = 22;
companion_mesh.prefs.rx_delay_base = 0.0f; /* Disabled for companion */
companion_mesh.prefs.airtime_factor = 10.0f; /* 10% duty cycle (EU 868 default) */
companion_mesh.prefs.rx_duty_cycle = 1; /* Companions: duty cycle ON by default (power save) */
/* Generate default node name from hardware device ID */
uint8_t dev_id[8];
@@ -537,6 +555,7 @@ int main(void)
/* Set LoRa callbacks for event-driven packet processing */
lora_radio.setRxCallback(lora_rx_callback, nullptr);
lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr);
companion_mesh.setTxQueuedCallback(tx_queued_callback, nullptr);
/* Start mesh */
companion_mesh.begin();
@@ -592,8 +611,9 @@ int main(void)
}
}
/* Apply RX boost setting from prefs (overrides radio default if user changed it) */
/* Apply RX boost and duty cycle settings from prefs */
lora_radio.setRxBoost(companion_mesh.prefs.rx_boost != 0);
lora_radio.enableRxDutyCycle(companion_mesh.prefs.rx_duty_cycle != 0);
/* Initialize mesh event object */
k_event_init(&mesh_events);
+111 -8
View File
@@ -49,6 +49,10 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
/* Radio + mesh includes (shared header selects LR1110 or SX126x) */
#include <mesh/RadioIncludes.h>
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
#include "wifi_ota.h"
#endif
/* LED configuration */
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
#define LED0_NODE DT_ALIAS(led0)
@@ -71,7 +75,9 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios);
#define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete */
#define MESH_EVENT_CLI_RX BIT(2) /* CLI command received */
#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING)
#define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change (must run on main thread!) */
#define MESH_EVENT_TX_DRAIN BIT(5) /* Outbound packet delay expired, run checkSend */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN)
/* Housekeeping interval - infrequent to preserve power savings */
#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS
@@ -90,7 +96,11 @@ static uint16_t cli_line_idx;
/* Work items for event-driven processing */
static void cli_rx_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
static void tx_drain_work_fn(struct k_work *work);
static void initial_advert_work_fn(struct k_work *work);
K_WORK_DEFINE(cli_rx_work, cli_rx_work_fn);
K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn);
K_WORK_DELAYABLE_DEFINE(initial_advert_work, initial_advert_work_fn);
/* Housekeeping timer for periodic tasks (noise floor calibration, etc.) */
K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL);
@@ -201,10 +211,53 @@ static void lora_tx_done_callback(void *user_data)
ARG_UNUSED(user_data);
k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE);
}
/* TX drain — Dispatcher queued a packet with a delay.
* Schedule a precise wake so checkSend runs when the delay expires. */
static void tx_drain_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
k_event_post(&mesh_events, MESH_EVENT_TX_DRAIN);
}
/* Deferred initial advertisement — gives GPS time to get a fix at boot */
static void initial_advert_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
#ifdef ZEPHCORE_LORA
if (repeater_mesh_ptr) {
LOG_INF("Sending deferred initial advertisement");
repeater_mesh_ptr->sendSelfAdvertisement(500, false);
}
#endif
}
static void tx_queued_callback(uint32_t delay_ms, void *user_data)
{
ARG_UNUSED(user_data);
k_work_reschedule(&tx_drain_work, K_MSEC(delay_ms));
}
#endif
/* Global instances */
static mesh::ZephyrRTCClock rtc_clock;
/* GPS event callback - called when GPS work handlers need the main thread
* to process a state transition (wake from standby, fix done, timeout).
* Runs from system work queue context — just posts an event, no blocking. */
static void gps_event_callback(void)
{
k_event_post(&mesh_events, MESH_EVENT_GPS_ACTION);
}
/* GPS fix callback - syncs RTC from GPS time (repeater mode: time sync only) */
static void gps_fix_callback(double lat, double lon, int64_t utc_time)
{
if (utc_time > 0) {
LOG_INF("GPS fix: RTC sync time=%lld", utc_time);
rtc_clock.setCurrentTime((uint32_t)utc_time);
}
}
static RepeaterDataStore data_store;
#ifdef ZEPHCORE_LORA
@@ -249,15 +302,43 @@ static void repeater_event_loop(void)
uint32_t events = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER);
k_event_clear(&mesh_events, events);
/* GPS state transitions must run on main thread (GNSS driver
* modem_chat blocks on system work queue semaphore). */
if (events & MESH_EVENT_GPS_ACTION) {
gps_process_event();
}
#ifdef ZEPHCORE_LORA
if (repeater_mesh_ptr) {
repeater_mesh_ptr->loop();
}
#endif
/* Update display clock on housekeeping tick (if display is on) */
/* Periodic housekeeping — update display with live data */
if (events & MESH_EVENT_HOUSEKEEPING) {
ui_set_clock(rtc_clock.getCurrentTime());
#ifdef ZEPHCORE_LORA
/* Refresh radio params (noise floor changes from calibration) */
if (repeater_mesh_ptr) {
NodePrefs *p = repeater_mesh_ptr->getNodePrefs();
ui_set_radio_params(
(uint32_t)(p->freq * 1000000.0f + 0.5f),
p->sf,
(uint16_t)(p->bw * 10.0f + 0.5f),
p->cr,
p->tx_power_dbm,
lora_radio.getNoiseFloor());
}
/* Battery every ~60s (12 × 5s housekeeping) */
static uint8_t batt_counter;
if (++batt_counter >= 12) {
batt_counter = 0;
ui_set_battery(zephyr_board.getBattMilliVolts(), 0);
}
#endif
ui_refresh_display();
}
}
@@ -269,10 +350,22 @@ int main(void)
initNodePrefs(&radio_prefs);
strcpy(radio_prefs.node_name, "Repeater");
#ifdef ZEPHCORE_LORA
/* Clear any stale bootloader magic from previous sessions.
* Prevents nRF52 boards from re-entering bootloader after reboot. */
zephyr_board.clearBootloaderMagic();
#endif
/* Wait for USB CDC to enumerate before any logging */
k_sleep(K_MSEC(2000));
LOG_INF("=== ZephCore Repeater starting ===");
#if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)
/* Confirm MCUboot image early — if we just booted after OTA,
* this marks the image as good so MCUboot keeps it. */
wifi_ota_confirm_image();
#endif
/* Configure LEDs */
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
if (gpio_is_ready_dt(&led0)) {
@@ -296,6 +389,8 @@ int main(void)
/* Set GPS to repeater mode: power off now, wake every 48h for time sync only.
* This prevents GPS from draining power on boards that have it (e.g., Wio Tracker). */
if (gps_is_available()) {
gps_set_fix_callback(gps_fix_callback);
gps_set_event_callback(gps_event_callback);
gps_set_repeater_mode(true);
}
@@ -303,6 +398,9 @@ int main(void)
* transitions to STATUS page. Display sleeps after auto-off timeout;
* user button is the only wake source for repeater. */
ui_init();
#if !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY)
oled_sleep();
#endif
/* Log environment sensor availability */
if (env_sensors_available()) {
@@ -318,6 +416,7 @@ int main(void)
/* Set LoRa callbacks for event-driven packet processing */
lora_radio.setRxCallback(lora_rx_callback, nullptr);
lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr);
repeater_mesh.setTxQueuedCallback(tx_queued_callback, nullptr);
/* Load or generate identity BEFORE begin() */
mesh::LocalIdentity self_identity;
@@ -356,22 +455,26 @@ int main(void)
}
}
/* Apply RX boost setting from prefs (overrides radio default if user changed it) */
/* Apply RX boost and duty cycle settings from prefs */
lora_radio.setRxBoost(prefs->rx_boost != 0);
lora_radio.enableRxDutyCycle(prefs->rx_duty_cycle != 0);
/* Feed initial UI state from loaded prefs */
ui_set_node_name(prefs->node_name);
ui_set_radio_params(
(uint32_t)(prefs->freq * 1000000.0f), /* MHz → Hz */
(uint32_t)(prefs->freq * 1000000.0f + 0.5f), /* MHz → Hz */
prefs->sf,
(uint16_t)(prefs->bw * 10.0f), /* kHz → 0.1 kHz */
(uint16_t)(prefs->bw * 10.0f + 0.5f), /* kHz → 0.1 kHz */
prefs->cr,
prefs->tx_power_dbm,
lora_radio.getNoiseFloor());
ui_set_battery(zephyr_board.getBattMilliVolts(), 0);
ui_set_gps_available(gps_is_available());
/* Send initial zero-hop advertisement after boot */
LOG_INF("Sending initial advertisement...");
repeater_mesh.sendSelfAdvertisement(500, false); /* 500ms delay, zero-hop */
/* Defer initial advertisement by 10s — gives GPS time for a quick fix.
* Advert payload (including coords) is built when the work fires. */
LOG_INF("Initial advertisement scheduled in 10s");
k_work_schedule(&initial_advert_work, K_SECONDS(10));
#endif
/* Initialize USB serial for CLI */
+7
View File
@@ -0,0 +1,7 @@
# ZephCore Sysbuild Configuration
# Enables MCUboot bootloader for ESP32 OTA firmware updates.
# Only used when building with --sysbuild flag (ESP32 repeater builds).
# nRF52/nRF54L/MG24 builds do NOT use --sysbuild and are unaffected.
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_MCUBOOT_MODE_OVERWRITE_ONLY=y
+27
View File
@@ -0,0 +1,27 @@
# MCUboot Configuration for ESP32
# Minimal footprint — OTA only, no serial recovery, no logging.
# Overwrite-only mode (no swap, no scratch partition needed)
# Matches Arduino ElegantOTA behavior (no rollback)
CONFIG_BOOT_UPGRADE_ONLY=y
# Let MCUboot auto-calculate sector count from partition size
CONFIG_BOOT_MAX_IMG_SECTORS_AUTO=y
# Minimize size — no serial recovery
CONFIG_BOOT_SERIAL_CDC_ACM=n
CONFIG_SIZE_OPTIMIZATIONS=y
# Temporary: enable logging for debug
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_CONSOLE=y
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
CONFIG_LOG_DEFAULT_LEVEL=3
# MCUboot doesn't need SPI — ESP32 uses internal flash controller.
# Disable to avoid spi_context build errors with minimal threading config.
CONFIG_SPI=n
# ESP32 HAL's mcuboot_config.h is patched (patches/modules/hal/espressif/...)
# to respect Zephyr Kconfig: mbedTLS detection, AUTO sector count, slot0 validation.