mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-23 20:12:26 +00:00
ESP OTA first implementation
This commit is contained in:
1 parent
521955bc05
commit
b6bc46b921
26 files changed
+1462
-33
No files matched your search
+81
-14
@@ -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) ==========
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <zephyr/linker/iterable_sections.h>
|
||||
|
||||
ITERABLE_SECTION_ROM(http_resource_desc_ota_service, Z_LINK_ITERABLE_SUBALIGN)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
+199
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user