diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 2343887..f04f55e 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -76,6 +76,35 @@ list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) # 2. Create boards//board.overlay with hardware definitions # 3. Build: west build -b 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) ========== diff --git a/zephcore/Kconfig b/zephcore/Kconfig index fc4a3a1..ed0dd75 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -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 diff --git a/zephcore/adapters/ota/ota_page.h b/zephcore/adapters/ota/ota_page.h new file mode 100644 index 0000000..2ed67a4 --- /dev/null +++ b/zephcore/adapters/ota/ota_page.h @@ -0,0 +1,101 @@ +/* + * Auto-generated from ota_page.html � do not edit manually. + * Regenerate: python3 compress_html.py + */ + +#pragma once + +#include + +#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, +}; diff --git a/zephcore/adapters/ota/ota_page.html b/zephcore/adapters/ota/ota_page.html new file mode 100644 index 0000000..cdc68d1 --- /dev/null +++ b/zephcore/adapters/ota/ota_page.html @@ -0,0 +1,64 @@ + + + + + +MeshCore OTA + + + +
+

MeshCore OTA Update

+
+
+ + + +
+
+
+
+
+
+ + + diff --git a/zephcore/adapters/ota/sections-rom.ld b/zephcore/adapters/ota/sections-rom.ld new file mode 100644 index 0000000..02f198a --- /dev/null +++ b/zephcore/adapters/ota/sections-rom.ld @@ -0,0 +1,3 @@ +#include + +ITERABLE_SECTION_ROM(http_resource_desc_ota_service, Z_LINK_ITERABLE_SUBALIGN) diff --git a/zephcore/adapters/ota/wifi_ota.c b/zephcore/adapters/ota/wifi_ota.c new file mode 100644 index 0000000..ef83ecc --- /dev/null +++ b/zephcore/adapters/ota/wifi_ota.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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), + "" + "

MeshCore OTA: %s (%s)
" + "Go to Update Page

" + "", + 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 +} diff --git a/zephcore/adapters/ota/wifi_ota.h b/zephcore/adapters/ota/wifi_ota.h new file mode 100644 index 0000000..1344494 --- /dev/null +++ b/zephcore/adapters/ota/wifi_ota.h @@ -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 + +#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 diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index ff4def2..6969277 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -10,6 +10,7 @@ #include #include + #include 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) diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index c9f2247..feaa247 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -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, diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp index aca3a42..5d57b83 100644 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -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); } diff --git a/zephcore/boards/common/wifi_ota.conf b/zephcore/boards/common/wifi_ota.conf new file mode 100644 index 0000000..9f6660e --- /dev/null +++ b/zephcore/boards/common/wifi_ota.conf @@ -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 + diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index b874963..c188ed2 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -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). diff --git a/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi b/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi index 7b5c9cd..f16a300 100644 --- a/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi +++ b/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi @@ -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 = ; + pinmux = ; output-high; }; group2 { - pinmux = ; + pinmux = ; bias-pull-up; }; }; diff --git a/zephcore/boards/esp32/xiao_esp32c3/board.overlay b/zephcore/boards/esp32/xiao_esp32c3/board.overlay index 532123c..e44bb6a 100644 --- a/zephcore/boards/esp32/xiao_esp32c3/board.overlay +++ b/zephcore/boards/esp32/xiao_esp32c3/board.overlay @@ -77,3 +77,8 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" + +/* WiFi — required for OTA firmware updates */ +&wifi { + status = "okay"; +}; diff --git a/zephcore/boards/esp32/xiao_esp32c6/board.overlay b/zephcore/boards/esp32/xiao_esp32c6/board.overlay index 026f14b..cb7ef3a 100644 --- a/zephcore/boards/esp32/xiao_esp32c6/board.overlay +++ b/zephcore/boards/esp32/xiao_esp32c6/board.overlay @@ -77,3 +77,8 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" + +/* WiFi — required for OTA firmware updates */ +&wifi { + status = "okay"; +}; diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 297e584..a3afa58 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -13,6 +13,10 @@ #include #include +#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); } diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 78dee72..71e09cc 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -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 } diff --git a/zephcore/include/mesh/Dispatcher.h b/zephcore/include/mesh/Dispatcher.h index f947f36..78107c4 100644 --- a/zephcore/include/mesh/Dispatcher.h +++ b/zephcore/include/mesh/Dispatcher.h @@ -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; diff --git a/zephcore/lib/ed25519/sha512.h b/zephcore/lib/ed25519/sha512.h index a34dd5e..162a92c 100644 --- a/zephcore/lib/ed25519/sha512.h +++ b/zephcore/lib/ed25519/sha512.h @@ -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]; diff --git a/zephcore/patches/modules/hal/espressif/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h b/zephcore/patches/modules/hal/espressif/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h new file mode 100644 index 0000000..9cec9f7 --- /dev/null +++ b/zephcore/patches/modules/hal/espressif/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h @@ -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 +#include + +/* + * 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__ */ diff --git a/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c b/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c index 6ad7175..8518689 100644 --- a/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c +++ b/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c @@ -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; } diff --git a/zephcore/src/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp index ffb2d92..d01ed79 100644 --- a/zephcore/src/Dispatcher.cpp +++ b/zephcore/src/Dispatcher.cpp @@ -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); + } } } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 392a0f2..258d984 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -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); diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 2ab3c16..b4aa8f4 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -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 +#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 */ diff --git a/zephcore/sysbuild.conf b/zephcore/sysbuild.conf new file mode 100644 index 0000000..50b28bc --- /dev/null +++ b/zephcore/sysbuild.conf @@ -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 diff --git a/zephcore/sysbuild/mcuboot.conf b/zephcore/sysbuild/mcuboot.conf new file mode 100644 index 0000000..692620f --- /dev/null +++ b/zephcore/sysbuild/mcuboot.conf @@ -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.