refactor zephyr patches to use proper git apply

This commit is contained in:
liquidraver
2026-02-22 14:16:48 +01:00
parent 73222600b2
commit 51783c6177
23 changed files with 615 additions and 4041 deletions
+112 -40
View File
@@ -4,56 +4,128 @@
cmake_minimum_required(VERSION 3.20.0)
# ============================================================================
# Zephyr Patch Auto-Apply
# Zephyr Patch Auto-Apply (unified diffs via git apply)
# ============================================================================
# Patches stored in zephcore/patches/zephyr/ are copied to Zephyr source tree
# at configure time. This survives `west update` - patches are reapplied on
# next build.
# Patches are applied at configure time using `git apply`. This means:
# - If upstream changed in a conflicting area → build FAILS with clear error
# - If upstream changed in non-conflicting areas → changes are preserved
# - Idempotent: re-running cmake without west update detects already-applied patches
#
# Current patches:
# zephyr/:
# - drivers/lora/loramac-node/sx12xx_common.c: bandwidth enum→index mapping
# - drivers/lora/lr11xx/*: LR11xx Zephyr LoRa driver (for future upstream PR)
# - drivers/lora/CMakeLists.txt: adds lr11xx subdirectory
# - drivers/lora/Kconfig: adds lr11xx Kconfig
# - dts/bindings/lora/semtech,lr1110.yaml: LR1110 DTS binding
# - drivers/gnss/gnss_luatos_air530z.c: EASY ephemeris prediction (PMTK869)
# - drivers/gnss/Kconfig.luatos_air530z: CONFIG_GNSS_LUATOS_AIR530Z_EASY
# - drivers/lora/native/sx126x/sx126x.c: fix rx-enable-gpios not toggled
# when dio2-tx-enable is set (breaks E22-900M30S and similar PA modules)
# modules/:
# - lib/loramac-node/src/radio/sx126x/radio.c: restore full 10-element
# Bandwidths[] array + fix LDRO for sub-125kHz bandwidths
# Directory layout:
# patches/zephyr/*.patch - unified diffs applied to the Zephyr tree
# patches/zephyr-new/ - new files copied to the Zephyr tree (no upstream)
# patches/loramac-node/*.patch - unified diffs for modules/lib/loramac-node
# patches/espressif/*.patch - unified diffs for modules/hal/espressif
#
# Zephyr patches:
# 0001-lora-lr11xx-build - CMakeLists.txt + Kconfig (lr11xx subdirectory)
# 0002-lora-sx12xx-common - RX error callback, bandwidth mapping, fast TX/RX
# 0003-lora-sx126x-native - PA module RF switch fix (dio2-tx-enable)
# 0004-lora-sx126x-standalone - DIO1 IRQ error logging
# 0005-gnss-air530z-easy - EASY ephemeris prediction (PMTK869) + Kconfig
# 0006-blobs-py - west blobs command fix
# New files (copied, not patched):
# drivers/lora/lr11xx/* - LR11xx Zephyr LoRa driver
# dts/bindings/lora/semtech,lr1110.yaml - LR1110 DTS binding
# Module patches:
# loramac-node: restore 10-element Bandwidths[] + LDRO fix
# espressif: MCUboot config flexibility (mbedtls, validation, sector count)
#
# --- Helper: apply unified diff patches via git apply ---
function(zephcore_apply_patches PATCH_DIR TARGET_DIR LABEL)
file(GLOB PATCH_FILES "${PATCH_DIR}/*.patch")
list(SORT PATCH_FILES)
foreach(PATCH_FILE ${PATCH_FILES})
get_filename_component(PATCH_NAME ${PATCH_FILE} NAME)
# Check if already applied (idempotent for re-configure without west update)
execute_process(
COMMAND git apply --reverse --check "${PATCH_FILE}"
WORKING_DIRECTORY "${TARGET_DIR}"
RESULT_VARIABLE ALREADY_APPLIED
OUTPUT_QUIET ERROR_QUIET
)
if(ALREADY_APPLIED EQUAL 0)
message(STATUS " [${LABEL}] Already applied: ${PATCH_NAME}")
continue()
endif()
# Verify patch applies cleanly
execute_process(
COMMAND git apply --check "${PATCH_FILE}"
WORKING_DIRECTORY "${TARGET_DIR}"
RESULT_VARIABLE PATCH_CHECK
ERROR_VARIABLE PATCH_ERR
)
if(NOT PATCH_CHECK EQUAL 0)
message(FATAL_ERROR
"ZephCore patch FAILED to apply: ${PATCH_NAME}\n"
"Target tree: ${TARGET_DIR}\n"
"Error:\n${PATCH_ERR}\n"
"Upstream likely changed — rebase the patch:\n"
" cd ${TARGET_DIR}\n"
" git diff -- <file> # inspect current upstream\n"
" # Regenerate: git diff -- <file> > ${PATCH_FILE}\n"
)
endif()
# Apply the patch
execute_process(
COMMAND git apply "${PATCH_FILE}"
WORKING_DIRECTORY "${TARGET_DIR}"
RESULT_VARIABLE APPLY_RESULT
ERROR_VARIABLE APPLY_ERR
)
if(NOT APPLY_RESULT EQUAL 0)
message(FATAL_ERROR "git apply failed unexpectedly: ${PATCH_NAME}\n${APPLY_ERR}")
endif()
message(STATUS " [${LABEL}] Applied: ${PATCH_NAME}")
endforeach()
endfunction()
# Resolve target directories (before find_package(Zephyr) sets ZEPHYR_BASE)
get_filename_component(ZEPHYR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE)
get_filename_component(MODULES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../modules ABSOLUTE)
# Apply unified diff patches to Zephyr tree
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr)
message(STATUS "Applying ZephCore patches to Zephyr...")
file(GLOB_RECURSE ZEPHCORE_PATCH_FILES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr
${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr/*)
foreach(REL_PATH ${ZEPHCORE_PATCH_FILES})
set(SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr/${REL_PATH})
# ZEPHYR_BASE is set by find_package(Zephyr), but we're before that.
# Use the parent directory structure: zephcore/../zephyr
get_filename_component(ZEPHYR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE)
zephcore_apply_patches(
"${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr"
"${ZEPHYR_DIR}"
"zephyr"
)
endif()
# Copy new files (no upstream equivalent) to Zephyr tree
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new)
file(GLOB_RECURSE ZEPHCORE_NEW_FILES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new
${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new/*)
foreach(REL_PATH ${ZEPHCORE_NEW_FILES})
set(SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new/${REL_PATH})
set(DST_FILE ${ZEPHYR_DIR}/${REL_PATH})
message(STATUS " Patching: ${REL_PATH}")
configure_file(${SRC_FILE} ${DST_FILE} COPYONLY)
message(STATUS " [zephyr-new] ${REL_PATH}")
endforeach()
endif()
# Patch modules (loramac-node, etc.) - same mechanism, different target dir
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules)
message(STATUS "Applying ZephCore patches to modules...")
file(GLOB_RECURSE ZEPHCORE_MODULE_PATCH_FILES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules
${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/*)
foreach(REL_PATH ${ZEPHCORE_MODULE_PATCH_FILES})
set(SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/${REL_PATH})
get_filename_component(MODULES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../modules ABSOLUTE)
set(DST_FILE ${MODULES_DIR}/${REL_PATH})
message(STATUS " Patching: ${REL_PATH}")
configure_file(${SRC_FILE} ${DST_FILE} COPYONLY)
endforeach()
# Apply unified diff patches to loramac-node module
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/loramac-node)
message(STATUS "Applying ZephCore patches to loramac-node...")
zephcore_apply_patches(
"${CMAKE_CURRENT_SOURCE_DIR}/patches/loramac-node"
"${MODULES_DIR}/lib/loramac-node"
"loramac-node"
)
endif()
# Apply unified diff patches to espressif module
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/espressif)
message(STATUS "Applying ZephCore patches to espressif...")
zephcore_apply_patches(
"${CMAKE_CURRENT_SOURCE_DIR}/patches/espressif"
"${MODULES_DIR}/hal/espressif"
"espressif"
)
endif()
# Add custom boards directory (for Wio Tracker L1, etc.)
@@ -0,0 +1,51 @@
diff --git a/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h b/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h
index 472ebbc5ab..9cec9f7e12 100644
--- a/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h
+++ b/zephyr/port/include/boot/mcuboot_config/mcuboot_config.h
@@ -2,6 +2,10 @@
* 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__
@@ -76,8 +80,8 @@
* available.
*/
-/* Uncomment to use Mbed TLS cryptographic primitives */
-#if defined(CONFIG_ESP_USE_MBEDTLS)
+/* 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,
@@ -90,7 +94,11 @@
* 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
@@ -114,7 +122,12 @@
/* 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. */
@@ -0,0 +1,75 @@
diff --git a/src/radio/sx126x/radio.c b/src/radio/sx126x/radio.c
index c4a40dab..bf6b4c9b 100644
--- a/src/radio/sx126x/radio.c
+++ b/src/radio/sx126x/radio.c
@@ -421,7 +421,26 @@ const FskBandwidth_t FskBandwidths[] =
{ 500000, 0x00 }, // Invalid Bandwidth
};
-const RadioLoRaBandwidths_t Bandwidths[] = { LORA_BW_125, LORA_BW_250, LORA_BW_500 };
+/*
+ * ZephCore patch: restore full 10-element Bandwidths array.
+ * Upstream Zephyr reduced this to {125, 250, 500} but the Zephyr LoRa API
+ * now defines enums for all bandwidths (BW_7_KHZ through BW_500_KHZ).
+ * sx12xx_common.c maps these enums to indices into this array, so all 10
+ * entries must be present to avoid out-of-bounds access.
+ */
+const RadioLoRaBandwidths_t Bandwidths[] =
+{
+ LORA_BW_007,
+ LORA_BW_010,
+ LORA_BW_015,
+ LORA_BW_020,
+ LORA_BW_031,
+ LORA_BW_041,
+ LORA_BW_062,
+ LORA_BW_125,
+ LORA_BW_250,
+ LORA_BW_500,
+};
uint8_t MaxPayloadLength = 0xFF;
@@ -702,8 +721,14 @@ void RadioSetRxConfig( RadioModems_t modem, uint32_t bandwidth,
SX126x.ModulationParams.Params.LoRa.Bandwidth = Bandwidths[bandwidth];
SX126x.ModulationParams.Params.LoRa.CodingRate = ( RadioLoRaCodingRates_t )coderate;
- if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ /*
+ * ZephCore patch: LDRO check updated for 10-element Bandwidths[].
+ * Enable LDRO when symbol duration > 16ms.
+ * Old indices (0=125k,1=250k) → new (7=125k,8=250k,6=62.5k, etc.)
+ */
+ if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 8 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) )
{
SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01;
}
@@ -809,8 +834,10 @@ void RadioSetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev,
SX126x.ModulationParams.Params.LoRa.Bandwidth = Bandwidths[bandwidth];
SX126x.ModulationParams.Params.LoRa.CodingRate= ( RadioLoRaCodingRates_t )coderate;
- if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ /* ZephCore patch: LDRO check updated for 10-element Bandwidths[] */
+ if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 8 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) )
{
SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01;
}
@@ -946,8 +973,10 @@ static uint32_t RadioGetLoRaTimeOnAirNumerator( uint32_t bandwidth,
}
}
- if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ /* ZephCore patch: LDRO check updated for 10-element Bandwidths[] */
+ if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 8 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) )
{
lowDatareOptimize = true;
}
@@ -1,199 +0,0 @@
/*
* 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__ */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt
index 9cd035fcf2b..cda2f6dbaf3 100644
--- a/drivers/lora/CMakeLists.txt
+++ b/drivers/lora/CMakeLists.txt
@@ -1,7 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
+# ZephCore patch: adds lr11xx driver subdirectory.
zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c)
+add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx)
+
# zephyr-keep-sorted-start
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node)
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem)
diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig
index 657bdb9ce28..0ebee4246f3 100644
--- a/drivers/lora/Kconfig
+++ b/drivers/lora/Kconfig
@@ -5,6 +5,7 @@
#
# Top-level configuration file for LORA drivers.
+# ZephCore patch: adds lr11xx driver Kconfig.
menuconfig LORA
bool "LoRa drivers"
@@ -60,6 +61,7 @@ config LORA_INIT_PRIORITY
rsource "Kconfig.sx12xx"
rsource "Kconfig.rylrxxx"
+rsource "lr11xx/Kconfig"
rsource "lora-basics-modem/Kconfig"
rsource "native/Kconfig"
@@ -0,0 +1,173 @@
diff --git a/drivers/lora/loramac-node/sx12xx_common.c b/drivers/lora/loramac-node/sx12xx_common.c
index 17689720dd2..85186893fc8 100644
--- a/drivers/lora/loramac-node/sx12xx_common.c
+++ b/drivers/lora/loramac-node/sx12xx_common.c
@@ -3,6 +3,9 @@
* Copyright (c) 2020 Grinn
*
* SPDX-License-Identifier: Apache-2.0
+ *
+ * ZEPHCORE PATCH: Modified sx12xx_ev_rx_error() to notify async callback
+ * with NULL data on RX errors (CRC/header). This allows error counting.
*/
#include <zephyr/drivers/gpio.h>
@@ -38,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)
@@ -179,6 +188,13 @@ static void sx12xx_ev_rx_error(void)
if (dev_data.async_rx_cb) {
/* Start receiving again */
Radio.Rx(0);
+ /*
+ * ZEPHCORE PATCH: Notify callback with NULL data to indicate
+ * RX error (CRC mismatch, header error). This allows the
+ * application to count receive errors for diagnostics.
+ */
+ dev_data.async_rx_cb(dev_data.dev, NULL, 0, 0, 0,
+ dev_data.async_user_data);
/* Don't run the synchronous code */
return;
}
@@ -195,26 +211,24 @@ static void sx12xx_ev_rx_error(void)
/**
* @brief Convert Zephyr bandwidth enum to loramac-node bandwidth index
*
- * The loramac-node library expects bandwidth as an index (0, 1, 2) into its
- * internal Bandwidths[] array, not the actual kHz value.
- *
- * @param bandwidth Zephyr lora_signal_bandwidth enum value
- * @param bw_idx Pointer to store the resulting bandwidth index
- * @return 0 on success, -EINVAL if bandwidth is not supported
+ * The loramac-node library expects bandwidth as an index into its internal
+ * Bandwidths[] array: {BW_007, BW_010, BW_015, BW_020, BW_031,
+ * BW_041, BW_062, BW_125, BW_250, BW_500}
*/
static int sx12xx_get_bandwidth_idx(enum lora_signal_bandwidth bandwidth,
uint32_t *bw_idx)
{
switch (bandwidth) {
- case BW_125_KHZ:
- *bw_idx = 0;
- break;
- case BW_250_KHZ:
- *bw_idx = 1;
- break;
- case BW_500_KHZ:
- *bw_idx = 2;
- break;
+ case BW_7_KHZ: *bw_idx = 0; break;
+ case BW_10_KHZ: *bw_idx = 1; break;
+ case BW_15_KHZ: *bw_idx = 2; break;
+ case BW_20_KHZ: *bw_idx = 3; break;
+ case BW_31_KHZ: *bw_idx = 4; break;
+ case BW_41_KHZ: *bw_idx = 5; break;
+ case BW_62_KHZ: *bw_idx = 6; break;
+ case BW_125_KHZ: *bw_idx = 7; break;
+ case BW_250_KHZ: *bw_idx = 8; break;
+ case BW_500_KHZ: *bw_idx = 9; break;
default:
return -EINVAL;
}
@@ -225,7 +239,6 @@ uint32_t sx12xx_airtime(const struct device *dev, uint32_t data_len)
{
uint32_t bw_idx;
- /* Translate bandwidth to loramac-node index, default to 0 if invalid */
if (sx12xx_get_bandwidth_idx(dev_data.tx_cfg.bandwidth, &bw_idx) < 0) {
bw_idx = 0;
}
@@ -375,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)
{
@@ -388,6 +416,39 @@ int sx12xx_lora_config(const struct device *dev,
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);
+
/* Ensure available, decremented after configuration */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
@@ -403,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;
}
@@ -0,0 +1,20 @@
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
index 8e0ca45c271..828faaaf37f 100644
--- a/drivers/lora/native/sx126x/sx126x.c
+++ b/drivers/lora/native/sx126x/sx126x.c
@@ -451,7 +451,14 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx)
const struct sx126x_hal_config *config = dev->config;
sx126x_hal_set_antenna_enable(dev, enable);
- if (!config->dio2_tx_enable) {
+ if (config->dio2_tx_enable) {
+ /* DIO2 handles TX enable in hardware — but rx-enable-gpios
+ * (e.g. E22-900M30S RXEN) still needs explicit GPIO control.
+ * RXEN HIGH when receiving, LOW otherwise. */
+ if (config->rx_enable.port != NULL) {
+ gpio_pin_set_dt(&config->rx_enable, enable && !tx);
+ }
+ } else {
sx126x_hal_set_rf_switch(dev, enable && tx);
}
}
@@ -0,0 +1,17 @@
diff --git a/drivers/lora/loramac-node/sx126x_standalone.c b/drivers/lora/loramac-node/sx126x_standalone.c
index c9b16965bc7..3a2ff8b20c9 100644
--- a/drivers/lora/loramac-node/sx126x_standalone.c
+++ b/drivers/lora/loramac-node/sx126x_standalone.c
@@ -40,8 +40,11 @@ uint32_t sx126x_get_dio1_pin_state(struct sx126x_data *dev_data)
void sx126x_dio1_irq_enable(struct sx126x_data *dev_data)
{
- gpio_pin_interrupt_configure_dt(&sx126x_gpio_dio1,
+ int ret = gpio_pin_interrupt_configure_dt(&sx126x_gpio_dio1,
GPIO_INT_EDGE_TO_ACTIVE);
+ if (ret != 0) {
+ LOG_ERR("DIO1 irq enable FAILED: %d", ret);
+ }
}
void sx126x_dio1_irq_disable(struct sx126x_data *dev_data)
@@ -0,0 +1,119 @@
diff --git a/drivers/gnss/Kconfig.luatos_air530z b/drivers/gnss/Kconfig.luatos_air530z
index c5d09261da5..ffc218b22e2 100644
--- a/drivers/gnss/Kconfig.luatos_air530z
+++ b/drivers/gnss/Kconfig.luatos_air530z
@@ -28,4 +28,16 @@ config GNSS_LUATOS_AIR530Z_SATELLITES_COUNT
the device is actually tracking, just how many of those can
be reported in the satellites callback.
+config GNSS_LUATOS_AIR530Z_EASY
+ bool "Enable EASY (Embedded Assist System) for faster TTFF"
+ default y
+ help
+ Enable MediaTek EASY (Embedded Assist System) mode via PMTK869
+ command. EASY caches predicted ephemeris in the GNSS module's
+ internal flash, reducing Time-To-First-Fix from 15-45s (cold)
+ to 1-3s (warm). Sent on every driver init (boot and PM resume).
+ The setting persists in GNSS flash, so resending is a no-op.
+ Compatible with L76K/L76KB and other MediaTek-based GNSS chips
+ that accept PMTK commands alongside PCAS.
+
endif
diff --git a/drivers/gnss/gnss_luatos_air530z.c b/drivers/gnss/gnss_luatos_air530z.c
index 74708edbd62..7de70ad93c8 100644
--- a/drivers/gnss/gnss_luatos_air530z.c
+++ b/drivers/gnss/gnss_luatos_air530z.c
@@ -9,7 +9,6 @@
#include <zephyr/modem/chat.h>
#include <zephyr/modem/backend/uart.h>
#include <zephyr/kernel.h>
-#include <zephyr/pm/device.h>
#include <zephyr/drivers/gpio.h>
#include <string.h>
@@ -36,6 +35,13 @@ MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_script_cmds,
/* receive only GGA and RMC NMEA messages */
MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E", 10),
#endif
+#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_EASY)
+ /* Enable EASY (Embedded Assist System) — caches predicted ephemeris
+ * in GNSS internal flash for 1-3s warm start instead of 15-45s cold.
+ * PMTK869,1,1 = Set EASY, Enable. Persists across power cycles.
+ * L76K/L76KB accept PMTK alongside PCAS (both MediaTek MT33xx). */
+ MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PMTK869,1,1*36", 10),
+#endif
);
MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(init_script, init_script_cmds, NULL, 5);
@@ -217,54 +223,11 @@ static int gnss_luatos_air530z_init(const struct device *dev)
return 0;
}
-static int luatos_air530z_pm_resume(const struct device *dev)
-{
- struct gnss_luatos_air530z_data *data = dev->data;
- int ret;
-
- ret = modem_pipe_open(data->uart_pipe, K_SECONDS(10));
- if (ret < 0) {
- return ret;
- }
-
- ret = modem_chat_attach(&data->chat, data->uart_pipe);
- if (ret < 0) {
- modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- return ret;
- }
-
- ret = modem_chat_run_script(&data->chat, &init_script);
- if (ret < 0) {
- modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- return ret;
- }
-
- return 0;
-}
-
-static int luatos_air530z_pm_action(const struct device *dev, enum pm_device_action action)
-{
- struct gnss_luatos_air530z_data *data = dev->data;
- const struct gnss_luatos_air530z_config *config = dev->config;
- int ret = -ENOTSUP;
-
- switch (action) {
- case PM_DEVICE_ACTION_SUSPEND:
- gpio_pin_set_dt(&config->on_off_gpio, 0);
- ret = modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- break;
-
- case PM_DEVICE_ACTION_RESUME:
- gpio_pin_set_dt(&config->on_off_gpio, 1);
- ret = luatos_air530z_pm_resume(dev);
- break;
-
- default:
- break;
- }
-
- return ret;
-}
+/* PM intentionally removed — the L76K ignores $PMTK161,0 (AT6558-based,
+ * not genuine MediaTek). GPS standby uses GPIO FORCE_ON pin instead.
+ * Also, CONFIG_PM_DEVICE_SYSTEM_MANAGED can auto-suspend devices during
+ * idle, calling modem_chat_run_script() from an unexpected context and
+ * potentially deadlocking the system. */
static int luatos_air530z_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms)
{
@@ -356,10 +319,8 @@ static DEVICE_API(gnss, gnss_api) = {
.dynamic_separators_buf = {',', '*'}, \
}; \
\
- PM_DEVICE_DT_INST_DEFINE(inst, luatos_air530z_pm_action); \
- \
DEVICE_DT_INST_DEFINE(inst, gnss_luatos_air530z_init, \
- PM_DEVICE_DT_INST_GET(inst), \
+ NULL, \
&gnss_luatos_air530z_data_##inst, \
&gnss_luatos_air530z_cfg_##inst, \
POST_KERNEL, CONFIG_GNSS_INIT_PRIORITY, &gnss_api);
@@ -0,0 +1,13 @@
diff --git a/scripts/west_commands/blobs.py b/scripts/west_commands/blobs.py
index 14f0412cda5..615ba602a6b 100644
--- a/scripts/west_commands/blobs.py
+++ b/scripts/west_commands/blobs.py
@@ -288,7 +288,7 @@ class Blobs(WestCommand):
continue
self.inf(f"Fetching blob {blob['module']}: {blob['abspath']}")
- if blob['click-through'] and not args.auto_accept:
+ if blob.get('click-through') and not args.auto_accept:
while True:
user_input = input(
"For this blob, need to read and accept "
@@ -1,43 +0,0 @@
# Copyright 2024 Jerónimo Agulló
# SPDX-License-Identifier: Apache-2.0
config GNSS_LUATOS_AIR530Z
bool "Luatos Air530z GNSS device"
default y
depends on GNSS
depends on DT_HAS_LUATOS_AIR530Z_ENABLED
depends on GNSS_REFERENCE_FRAME_WGS84
select MODEM_MODULES
select MODEM_BACKEND_UART
select MODEM_CHAT
select GNSS_PARSE
select GNSS_NMEA0183
select GNSS_NMEA0183_MATCH
help
Enable Luatos Air530z GNSS driver.
if GNSS_LUATOS_AIR530Z
config GNSS_LUATOS_AIR530Z_SATELLITES_COUNT
int "Maximum satellite count"
depends on GNSS_SATELLITES
default 24
help
Maximum number of satellites that can be decoded from the
GNSS device. This does not affect the number of devices that
the device is actually tracking, just how many of those can
be reported in the satellites callback.
config GNSS_LUATOS_AIR530Z_EASY
bool "Enable EASY (Embedded Assist System) for faster TTFF"
default y
help
Enable MediaTek EASY (Embedded Assist System) mode via PMTK869
command. EASY caches predicted ephemeris in the GNSS module's
internal flash, reducing Time-To-First-Fix from 15-45s (cold)
to 1-3s (warm). Sent on every driver init (boot and PM resume).
The setting persists in GNSS flash, so resending is a no-op.
Compatible with L76K/L76KB and other MediaTek-based GNSS chips
that accept PMTK commands alongside PCAS.
endif
@@ -1,328 +0,0 @@
/*
* Copyright (c) 2024 Jerónimo Agulló
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/drivers/gnss.h>
#include <zephyr/drivers/gnss/gnss_publish.h>
#include <zephyr/modem/chat.h>
#include <zephyr/modem/backend/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <string.h>
#include "gnss_nmea0183.h"
#include "gnss_nmea0183_match.h"
#include "gnss_parse.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(luatos_air530z, CONFIG_GNSS_LOG_LEVEL);
#define DT_DRV_COMPAT luatos_air530z
#define UART_RECV_BUF_SZ 128
#define UART_TRANS_BUF_SZ 64
#define CHAT_RECV_BUF_SZ 256
#define CHAT_ARGV_SZ 32
MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_script_cmds,
#if CONFIG_GNSS_SATELLITES
/* receive only GGA, RMC and GSV NMEA messages */
MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F", 10),
#else
/* receive only GGA and RMC NMEA messages */
MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E", 10),
#endif
#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_EASY)
/* Enable EASY (Embedded Assist System) — caches predicted ephemeris
* in GNSS internal flash for 1-3s warm start instead of 15-45s cold.
* PMTK869,1,1 = Set EASY, Enable. Persists across power cycles.
* L76K/L76KB accept PMTK alongside PCAS (both MediaTek MT33xx). */
MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PMTK869,1,1*36", 10),
#endif
);
MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(init_script, init_script_cmds, NULL, 5);
struct gnss_luatos_air530z_config {
const struct device *uart;
const struct gpio_dt_spec on_off_gpio;
const int uart_baudrate;
};
struct gnss_luatos_air530z_data {
struct gnss_nmea0183_match_data match_data;
#if CONFIG_GNSS_SATELLITES
struct gnss_satellite satellites[CONFIG_GNSS_LUATOS_AIR530Z_SATELLITES_COUNT];
#endif
/* UART backend */
struct modem_pipe *uart_pipe;
struct modem_backend_uart uart_backend;
uint8_t uart_backend_receive_buf[UART_RECV_BUF_SZ];
uint8_t uart_backend_transmit_buf[UART_TRANS_BUF_SZ];
/* Modem chat */
struct modem_chat chat;
uint8_t chat_receive_buf[CHAT_RECV_BUF_SZ];
uint8_t chat_delimiter[2];
uint8_t *chat_argv[CHAT_ARGV_SZ];
/* Dynamic chat script */
uint8_t dynamic_separators_buf[2];
uint8_t dynamic_request_buf[32];
struct modem_chat_script_chat dynamic_script_chat;
struct modem_chat_script dynamic_script;
struct k_sem lock;
};
MODEM_CHAT_MATCHES_DEFINE(unsol_matches,
MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
#if CONFIG_GNSS_SATELLITES
MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback),
#endif
);
static void luatos_air530z_lock(const struct device *dev)
{
struct gnss_luatos_air530z_data *data = dev->data;
(void)k_sem_take(&data->lock, K_FOREVER);
}
static void luatos_air530z_unlock(const struct device *dev)
{
struct gnss_luatos_air530z_data *data = dev->data;
k_sem_give(&data->lock);
}
static int gnss_luatos_air530z_init_nmea0183_match(const struct device *dev)
{
struct gnss_luatos_air530z_data *data = dev->data;
const struct gnss_nmea0183_match_config match_config = {
.gnss = dev,
#if CONFIG_GNSS_SATELLITES
.satellites = data->satellites,
.satellites_size = ARRAY_SIZE(data->satellites),
#endif
};
return gnss_nmea0183_match_init(&data->match_data, &match_config);
}
static void gnss_luatos_air530z_init_pipe(const struct device *dev)
{
const struct gnss_luatos_air530z_config *config = dev->config;
struct gnss_luatos_air530z_data *data = dev->data;
const struct modem_backend_uart_config uart_backend_config = {
.uart = config->uart,
.receive_buf = data->uart_backend_receive_buf,
.receive_buf_size = sizeof(data->uart_backend_receive_buf),
.transmit_buf = data->uart_backend_transmit_buf,
.transmit_buf_size = ARRAY_SIZE(data->uart_backend_transmit_buf),
};
data->uart_pipe = modem_backend_uart_init(&data->uart_backend, &uart_backend_config);
}
static int gnss_luatos_air530z_init_chat(const struct device *dev)
{
struct gnss_luatos_air530z_data *data = dev->data;
const struct modem_chat_config chat_config = {
.user_data = data,
.receive_buf = data->chat_receive_buf,
.receive_buf_size = sizeof(data->chat_receive_buf),
.delimiter = data->chat_delimiter,
.delimiter_size = ARRAY_SIZE(data->chat_delimiter),
.filter = NULL,
.filter_size = 0,
.argv = data->chat_argv,
.argv_size = ARRAY_SIZE(data->chat_argv),
.unsol_matches = unsol_matches,
.unsol_matches_size = ARRAY_SIZE(unsol_matches),
};
return modem_chat_init(&data->chat, &chat_config);
}
static void luatos_air530z_init_dynamic_script(const struct device *dev)
{
struct gnss_luatos_air530z_data *data = dev->data;
/* Air530z doesn't respond to commands. Thus, response_matches_size = 0; */
data->dynamic_script_chat.request = data->dynamic_request_buf;
data->dynamic_script_chat.response_matches = NULL;
data->dynamic_script_chat.response_matches_size = 0;
data->dynamic_script_chat.timeout = 0;
data->dynamic_script.name = "PCAS";
data->dynamic_script.script_chats = &data->dynamic_script_chat;
data->dynamic_script.script_chats_size = 1;
data->dynamic_script.abort_matches = NULL;
data->dynamic_script.abort_matches_size = 0;
data->dynamic_script.callback = NULL;
data->dynamic_script.timeout = 5;
}
static int gnss_luatos_air530z_init(const struct device *dev)
{
struct gnss_luatos_air530z_data *data = dev->data;
const struct gnss_luatos_air530z_config *config = dev->config;
int ret;
k_sem_init(&data->lock, 1, 1);
ret = gnss_luatos_air530z_init_nmea0183_match(dev);
if (ret < 0) {
return ret;
}
gnss_luatos_air530z_init_pipe(dev);
ret = gnss_luatos_air530z_init_chat(dev);
if (ret < 0) {
return ret;
}
luatos_air530z_init_dynamic_script(dev);
ret = modem_pipe_open(data->uart_pipe, K_SECONDS(10));
if (ret < 0) {
return ret;
}
ret = modem_chat_attach(&data->chat, data->uart_pipe);
if (ret < 0) {
modem_pipe_close(data->uart_pipe, K_SECONDS(10));
return ret;
}
ret = modem_chat_run_script(&data->chat, &init_script);
if (ret < 0) {
LOG_ERR("Failed to run init_script");
modem_pipe_close(data->uart_pipe, K_SECONDS(10));
return ret;
}
/* setup on-off gpio for power management */
if (!gpio_is_ready_dt(&config->on_off_gpio)) {
LOG_ERR("on-off GPIO device not ready");
return -ENODEV;
}
gpio_pin_configure_dt(&config->on_off_gpio, GPIO_OUTPUT_HIGH);
return 0;
}
/* PM intentionally removed — the L76K ignores $PMTK161,0 (AT6558-based,
* not genuine MediaTek). GPS standby uses GPIO FORCE_ON pin instead.
* Also, CONFIG_PM_DEVICE_SYSTEM_MANAGED can auto-suspend devices during
* idle, calling modem_chat_run_script() from an unexpected context and
* potentially deadlocking the system. */
static int luatos_air530z_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms)
{
struct gnss_luatos_air530z_data *data = dev->data;
int ret;
if (fix_interval_ms < 100 || fix_interval_ms > 1000) {
return -EINVAL;
}
luatos_air530z_lock(dev);
ret = gnss_nmea0183_snprintk(data->dynamic_request_buf, sizeof(data->dynamic_request_buf),
"PCAS02,%u", fix_interval_ms);
if (ret < 0) {
goto unlock_return;
}
data->dynamic_script_chat.request_size = ret;
ret = modem_chat_run_script(&data->chat, &data->dynamic_script);
if (ret < 0) {
goto unlock_return;
}
unlock_return:
luatos_air530z_unlock(dev);
return ret;
}
static int luatos_air530z_set_enabled_systems(const struct device *dev, gnss_systems_t systems)
{
struct gnss_luatos_air530z_data *data = dev->data;
gnss_systems_t supported_systems;
uint8_t encoded_systems = 0;
int ret;
supported_systems = (GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU);
if ((~supported_systems) & systems) {
return -EINVAL;
}
luatos_air530z_lock(dev);
WRITE_BIT(encoded_systems, 0, systems & GNSS_SYSTEM_GPS);
WRITE_BIT(encoded_systems, 1, systems & GNSS_SYSTEM_GLONASS);
WRITE_BIT(encoded_systems, 2, systems & GNSS_SYSTEM_BEIDOU);
ret = gnss_nmea0183_snprintk(data->dynamic_request_buf, sizeof(data->dynamic_request_buf),
"PCAS04,%u", encoded_systems);
if (ret < 0) {
goto unlock_return;
}
data->dynamic_script_chat.request_size = ret;
ret = modem_chat_run_script(&data->chat, &data->dynamic_script);
if (ret < 0) {
goto unlock_return;
}
unlock_return:
luatos_air530z_unlock(dev);
return ret;
}
static int luatos_air530z_get_supported_systems(const struct device *dev, gnss_systems_t *systems)
{
*systems = (GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU);
return 0;
}
static DEVICE_API(gnss, gnss_api) = {
.set_fix_rate = luatos_air530z_set_fix_rate,
.set_enabled_systems = luatos_air530z_set_enabled_systems,
.get_supported_systems = luatos_air530z_get_supported_systems,
};
#define LUATOS_AIR530Z(inst) \
static const struct gnss_luatos_air530z_config gnss_luatos_air530z_cfg_##inst = { \
.uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \
.on_off_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, on_off_gpios, { 0 }), \
}; \
\
static struct gnss_luatos_air530z_data gnss_luatos_air530z_data_##inst = { \
.chat_delimiter = {'\r', '\n'}, \
.dynamic_separators_buf = {',', '*'}, \
}; \
\
DEVICE_DT_INST_DEFINE(inst, gnss_luatos_air530z_init, \
NULL, \
&gnss_luatos_air530z_data_##inst, \
&gnss_luatos_air530z_cfg_##inst, \
POST_KERNEL, CONFIG_GNSS_INIT_PRIORITY, &gnss_api);
DT_INST_FOREACH_STATUS_OKAY(LUATOS_AIR530Z)
@@ -1,12 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# ZephCore patch: adds lr11xx driver subdirectory.
zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c)
add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx)
# zephyr-keep-sorted-start
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node)
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem)
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_NATIVE native)
# zephyr-keep-sorted-stop
@@ -1,68 +0,0 @@
#
# Copyright (c) 2019 Manivannan Sadhasivam
#
# SPDX-License-Identifier: Apache-2.0
#
# Top-level configuration file for LORA drivers.
# ZephCore patch: adds lr11xx driver Kconfig.
menuconfig LORA
bool "LoRa drivers"
select POLL
help
Include LoRa drivers in the system configuration.
if LORA
choice LORA_MODULE_BACKEND
prompt "Low-level LoRa modem integration to use"
default LORA_MODULE_BACKEND_LORA_BASICS_MODEM if DT_HAS_SEMTECH_SX1268_ENABLED \
|| DT_HAS_SEMTECH_LLCC68_ENABLED || DT_HAS_SEMTECH_SX1278_ENABLED
config LORA_MODULE_BACKEND_LORAMAC_NODE
bool "loramac-node backend"
depends on ZEPHYR_LORAMAC_NODE_MODULE
config LORA_MODULE_BACKEND_LORA_BASICS_MODEM
bool "LoRa Basic modem backend"
depends on ZEPHYR_LORA_BASICS_MODEM_MODULE
depends on DT_HAS_SEMTECH_SX1262_ENABLED || DT_HAS_SEMTECH_SX1261_ENABLED \
|| DT_HAS_SEMTECH_SX1272_ENABLED || DT_HAS_SEMTECH_SX1276_ENABLED \
|| DT_HAS_SEMTECH_SX1268_ENABLED || DT_HAS_SEMTECH_LLCC68_ENABLED \
|| DT_HAS_SEMTECH_SX1278_ENABLED
select USE_LORA_BASICS_MODEM_DRIVERS
help
LoRa API support using the LoRa Basics Modem module.
config LORA_MODULE_BACKEND_NATIVE
bool "Native Zephyr backend"
help
Use Zephyr's built-in LoRa drivers without external modules.
Currently supports SX1261/SX1262 transceivers.
endchoice
module = LORA
module-str = lora
source "subsys/logging/Kconfig.template.log_config"
config LORA_SHELL
bool "LoRa Shell"
depends on SHELL
help
Enable LoRa Shell for testing.
config LORA_INIT_PRIORITY
int "LoRa initialization priority"
default 90
help
System initialization priority for LoRa drivers.
rsource "Kconfig.sx12xx"
rsource "Kconfig.rylrxxx"
rsource "lr11xx/Kconfig"
rsource "lora-basics-modem/Kconfig"
rsource "native/Kconfig"
endif # LORA
@@ -1,521 +0,0 @@
/*
* Copyright (c) 2019 Manivannan Sadhasivam
* Copyright (c) 2020 Grinn
*
* SPDX-License-Identifier: Apache-2.0
*
* ZEPHCORE PATCH: Modified sx12xx_ev_rx_error() to notify async callback
* with NULL data on RX errors (CRC/header). This allows error counting.
*/
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/lora.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/kernel.h>
/* LoRaMac-node specific includes */
#include <radio.h>
#include "sx12xx_common.h"
#define STATE_FREE 0
#define STATE_BUSY 1
#define STATE_CLEANUP 2
LOG_MODULE_REGISTER(sx12xx_common, CONFIG_LORA_LOG_LEVEL);
struct sx12xx_rx_params {
uint8_t *buf;
uint8_t *size;
int16_t *rssi;
int8_t *snr;
};
static struct sx12xx_data {
const struct device *dev;
struct k_poll_signal *operation_done;
lora_recv_cb async_rx_cb;
void *async_user_data;
RadioEvents_t events;
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)
{
int err;
if (!device_is_ready(gpio->port)) {
LOG_ERR("GPIO device not ready %s", gpio->port->name);
return -ENODEV;
}
err = gpio_pin_configure_dt(gpio, flags);
if (err) {
LOG_ERR("Cannot configure gpio %s %d: %d", gpio->port->name,
gpio->pin, err);
return err;
}
return 0;
}
/**
* @brief Attempt to acquire the modem for operations
*
* @param data common sx12xx data struct
*
* @retval true if modem was acquired
* @retval false otherwise
*/
static inline bool modem_acquire(struct sx12xx_data *data)
{
return atomic_cas(&data->modem_usage, STATE_FREE, STATE_BUSY);
}
/**
* @brief Safely release the modem from any context
*
* This function can be called from any context and guarantees that the
* release operations will only be run once.
*
* @param data common sx12xx data struct
*
* @retval true if modem was released by this function
* @retval false otherwise
*/
static bool modem_release(struct sx12xx_data *data)
{
/* Increment atomic so both acquire and release will fail */
if (!atomic_cas(&data->modem_usage, STATE_BUSY, STATE_CLEANUP)) {
return false;
}
/* Put radio back into sleep mode */
Radio.Sleep();
/* Completely release modem */
data->operation_done = NULL;
atomic_clear(&data->modem_usage);
return true;
}
static void sx12xx_ev_rx_done(uint8_t *payload, uint16_t size, int16_t rssi,
int8_t snr)
{
struct k_poll_signal *sig = dev_data.operation_done;
/* Receiving in asynchronous mode */
if (dev_data.async_rx_cb) {
/* Start receiving again */
Radio.Rx(0);
/* Run the callback */
dev_data.async_rx_cb(dev_data.dev, payload, size, rssi, snr,
dev_data.async_user_data);
/* Don't run the synchronous code */
return;
}
/* Manually release the modem instead of just calling modem_release
* as we need to perform cleanup operations while still ensuring
* others can't use the modem.
*/
if (!atomic_cas(&dev_data.modem_usage, STATE_BUSY, STATE_CLEANUP)) {
return;
}
/* We can make two observations here:
* 1. lora_recv hasn't already exited due to a timeout.
* (modem_release would have been successfully called)
* 2. If the k_poll in lora_recv times out before we raise the signal,
* but while this code is running, it will block on the
* signal again.
* This lets us guarantee that the operation_done signal and pointers
* in rx_params are always valid in this function.
*/
/* Store actual size */
if (size < *dev_data.rx_params.size) {
*dev_data.rx_params.size = size;
}
/* Copy received data to output buffer */
memcpy(dev_data.rx_params.buf, payload,
*dev_data.rx_params.size);
/* Output RSSI and SNR */
if (dev_data.rx_params.rssi) {
*dev_data.rx_params.rssi = rssi;
}
if (dev_data.rx_params.snr) {
*dev_data.rx_params.snr = snr;
}
/* Put radio back into sleep mode */
Radio.Sleep();
/* Completely release modem */
dev_data.operation_done = NULL;
atomic_clear(&dev_data.modem_usage);
/* Notify caller RX is complete */
k_poll_signal_raise(sig, 0);
}
static void sx12xx_ev_tx_done(void)
{
struct k_poll_signal *sig = dev_data.operation_done;
if (modem_release(&dev_data)) {
/* Raise signal if provided */
if (sig) {
k_poll_signal_raise(sig, 0);
}
}
}
static void sx12xx_ev_tx_timed_out(void)
{
/* Just release the modem */
modem_release(&dev_data);
}
static void sx12xx_ev_rx_error(void)
{
struct k_poll_signal *sig = dev_data.operation_done;
/* Receiving in asynchronous mode */
if (dev_data.async_rx_cb) {
/* Start receiving again */
Radio.Rx(0);
/*
* ZEPHCORE PATCH: Notify callback with NULL data to indicate
* RX error (CRC mismatch, header error). This allows the
* application to count receive errors for diagnostics.
*/
dev_data.async_rx_cb(dev_data.dev, NULL, 0, 0, 0,
dev_data.async_user_data);
/* Don't run the synchronous code */
return;
}
/* Finish synchronous receive with error */
if (modem_release(&dev_data)) {
/* Raise signal if provided */
if (sig) {
k_poll_signal_raise(sig, -EIO);
}
}
}
/**
* @brief Convert Zephyr bandwidth enum to loramac-node bandwidth index
*
* The loramac-node library expects bandwidth as an index into its internal
* Bandwidths[] array: {BW_007, BW_010, BW_015, BW_020, BW_031,
* BW_041, BW_062, BW_125, BW_250, BW_500}
*/
static int sx12xx_get_bandwidth_idx(enum lora_signal_bandwidth bandwidth,
uint32_t *bw_idx)
{
switch (bandwidth) {
case BW_7_KHZ: *bw_idx = 0; break;
case BW_10_KHZ: *bw_idx = 1; break;
case BW_15_KHZ: *bw_idx = 2; break;
case BW_20_KHZ: *bw_idx = 3; break;
case BW_31_KHZ: *bw_idx = 4; break;
case BW_41_KHZ: *bw_idx = 5; break;
case BW_62_KHZ: *bw_idx = 6; break;
case BW_125_KHZ: *bw_idx = 7; break;
case BW_250_KHZ: *bw_idx = 8; break;
case BW_500_KHZ: *bw_idx = 9; break;
default:
return -EINVAL;
}
return 0;
}
uint32_t sx12xx_airtime(const struct device *dev, uint32_t data_len)
{
uint32_t bw_idx;
if (sx12xx_get_bandwidth_idx(dev_data.tx_cfg.bandwidth, &bw_idx) < 0) {
bw_idx = 0;
}
return Radio.TimeOnAir(MODEM_LORA,
bw_idx,
dev_data.tx_cfg.datarate,
dev_data.tx_cfg.coding_rate,
dev_data.tx_cfg.preamble_len,
0, data_len, !dev_data.tx_cfg.packet_crc_disable);
}
int sx12xx_lora_send(const struct device *dev, uint8_t *data,
uint32_t data_len)
{
struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done);
struct k_poll_event evt = K_POLL_EVENT_INITIALIZER(
K_POLL_TYPE_SIGNAL,
K_POLL_MODE_NOTIFY_ONLY,
&done);
uint32_t air_time;
int ret;
/* Validate that we have a TX configuration */
if (!dev_data.tx_cfg.frequency) {
return -EINVAL;
}
ret = sx12xx_lora_send_async(dev, data, data_len, &done);
if (ret < 0) {
return ret;
}
/* Calculate expected airtime of the packet */
air_time = sx12xx_airtime(dev, data_len);
LOG_DBG("Expected air time of %u bytes = %u ms", data_len, air_time);
/* Wait for the packet to finish transmitting.
* Use twice the tx duration to ensure that we are actually detecting
* a failed transmission, and not some minor timing variation between
* modem and driver.
*/
ret = k_poll(&evt, 1, K_MSEC(2 * air_time));
if (ret < 0) {
LOG_ERR("Packet transmission failed!");
if (!modem_release(&dev_data)) {
/* TX done interrupt is currently running */
k_poll(&evt, 1, K_FOREVER);
}
}
return ret;
}
int sx12xx_lora_send_async(const struct device *dev, uint8_t *data,
uint32_t data_len, struct k_poll_signal *async)
{
/* Ensure available, freed by sx12xx_ev_tx_done */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
/* Store signal */
dev_data.operation_done = async;
Radio.SetMaxPayloadLength(MODEM_LORA, data_len);
Radio.Send(data, data_len);
return 0;
}
int sx12xx_lora_recv(const struct device *dev, uint8_t *data, uint8_t size,
k_timeout_t timeout, int16_t *rssi, int8_t *snr)
{
struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done);
struct k_poll_event evt = K_POLL_EVENT_INITIALIZER(
K_POLL_TYPE_SIGNAL,
K_POLL_MODE_NOTIFY_ONLY,
&done);
int ret;
/* Ensure available, decremented by sx12xx_ev_rx_done or on timeout */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
dev_data.async_rx_cb = NULL;
/* Store operation signal */
dev_data.operation_done = &done;
/* Set data output location */
dev_data.rx_params.buf = data;
dev_data.rx_params.size = &size;
dev_data.rx_params.rssi = rssi;
dev_data.rx_params.snr = snr;
Radio.SetMaxPayloadLength(MODEM_LORA, 255);
Radio.Rx(0);
ret = k_poll(&evt, 1, timeout);
if (ret < 0) {
if (!modem_release(&dev_data)) {
/* Releasing the modem failed, which means that
* the RX callback is currently running. Wait until
* the RX callback finishes and we get our packet.
*/
k_poll(&evt, 1, K_FOREVER);
/* We did receive a packet */
return size;
}
LOG_INF("Receive timeout");
return ret;
}
if (done.result < 0) {
LOG_ERR("Receive error");
return done.result;
}
return size;
}
int sx12xx_lora_recv_async(const struct device *dev, lora_recv_cb cb, void *user_data)
{
/* Cancel ongoing reception */
if (cb == NULL) {
if (!modem_release(&dev_data)) {
/* Not receiving or already being stopped */
return -EINVAL;
}
return 0;
}
/* Ensure available */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
/* Store parameters */
dev_data.async_rx_cb = cb;
dev_data.async_user_data = user_data;
/* Start reception */
Radio.SetMaxPayloadLength(MODEM_LORA, 255);
Radio.Rx(0);
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)
{
bool crc = !config->packet_crc_disable;
uint32_t bw_idx;
int ret;
ret = sx12xx_get_bandwidth_idx(config->bandwidth, &bw_idx);
if (ret < 0) {
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);
/* Ensure available, decremented after configuration */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
Radio.SetChannel(config->frequency);
if (config->tx) {
/* Store TX config locally for airtime calculations */
memcpy(&dev_data.tx_cfg, config, sizeof(dev_data.tx_cfg));
/* Configure radio driver */
Radio.SetTxConfig(MODEM_LORA, config->tx_power, 0,
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;
}
int sx12xx_lora_test_cw(const struct device *dev, uint32_t frequency,
int8_t tx_power,
uint16_t duration)
{
/* Ensure available, freed in sx12xx_ev_tx_done */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
}
Radio.SetTxContinuousWave(frequency, tx_power, duration);
return 0;
}
int sx12xx_init(const struct device *dev)
{
atomic_set(&dev_data.modem_usage, 0);
dev_data.dev = dev;
dev_data.events.TxDone = sx12xx_ev_tx_done;
dev_data.events.RxDone = sx12xx_ev_rx_done;
dev_data.events.RxError = sx12xx_ev_rx_error;
/* TX timeout event raises at the end of the test CW transmission */
dev_data.events.TxTimeout = sx12xx_ev_tx_timed_out;
Radio.Init(&dev_data.events);
/*
* Automatically place the radio into sleep mode upon boot.
* The required `lora_config` call before transmission or reception
* will bring the radio out of sleep mode before it is used. The radio
* is automatically placed back into sleep mode upon TX or RX
* completion.
*/
Radio.Sleep();
return 0;
}
File diff suppressed because it is too large Load Diff
@@ -1,346 +0,0 @@
# Copyright (c) 2022 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
import argparse
import os
import re
import shutil
import sys
import textwrap
from pathlib import Path
from urllib.parse import urlparse
from west.commands import WestCommand
from zephyr_ext_common import ZEPHYR_BASE
sys.path.append(os.fspath(Path(__file__).parent.parent))
import zephyr_module
class Blobs(WestCommand):
DEFAULT_LIST_FMT = '{module} {status} {path} {type} {abspath}'
def __init__(self):
super().__init__(
'blobs',
# Keep this in sync with the string in west-commands.yml.
'work with binary blobs',
'Work with binary blobs',
accepts_unknown_args=False,
)
def do_add_parser(self, parser_adder):
parser = parser_adder.add_parser(
self.name,
help=self.help,
formatter_class=argparse.RawDescriptionHelpFormatter,
description=self.description,
epilog=textwrap.dedent(f'''\
FORMAT STRINGS
--------------
Blobs are listed using a Python 3 format string. Arguments
to the format string are accessed by name.
The default format string is:
"{self.DEFAULT_LIST_FMT}"
The following arguments are available:
- module: name of the module that contains this blob
- abspath: blob absolute path
- status: short status (A: present, M: hash failure, D: not present)
- path: blob local path from <module>/zephyr/blobs/
- sha256: blob SHA256 hash in hex
- type: type of blob
- version: version string
- license_path: path to the license file for the blob
- license-abspath: absolute path to the license file for the blob
- click-through: need license click-through or not
- uri: URI to the remote location of the blob
- description: blob text description
- doc-url: URL to the documentation for this blob
'''),
)
# Remember to update west-completion.bash if you add or remove
# flags
parser.add_argument(
'subcmd', nargs=1, choices=['list', 'fetch', 'clean'], help='sub-command to execute'
)
parser.add_argument(
'modules',
metavar='MODULE',
nargs='*',
help='''zephyr modules to operate on;
all modules will be used if not given''',
)
group = parser.add_argument_group('west blob list options')
group.add_argument(
'-f',
'--format',
help='''format string to use to list each blob;
see FORMAT STRINGS below''',
)
group = parser.add_argument_group('west blobs fetch options')
group.add_argument(
'-l',
'--allow-regex',
help='''Regex pattern to apply to the blob local path.
Only local paths matching this regex will be fetched.
Note that local paths are relative to the module directory''',
)
group.add_argument(
'-a',
'--auto-accept',
action='store_true',
help='''auto accept license if the fetching needs click-through''',
)
group.add_argument(
'--cache-dirs',
help='''Semicolon-separated list of directories to search for cached
blobs before downloading. Cache files may use the original
filename or be suffixed with `.<sha256>`.''',
)
group.add_argument(
'--auto-cache',
help='''Path to a directory that is automatically populated when a blob
is downloaded. Cached blobs are stored using the original
filename suffixed with `.<sha256>`.''',
)
return parser
def get_blobs(self, args):
blobs = []
modules = args.modules
all_modules = zephyr_module.parse_modules(ZEPHYR_BASE, self.manifest)
all_names = [m.meta.get('name', None) for m in all_modules]
unknown = set(modules) - set(all_names)
if len(unknown):
self.die(f'Unknown module(s): {unknown}')
for module in all_modules:
# Filter by module
module_name = module.meta.get('name', None)
if len(modules) and module_name not in modules:
continue
blobs += zephyr_module.process_blobs(module.project, module.meta)
return blobs
def list(self, args):
blobs = self.get_blobs(args)
fmt = args.format or self.DEFAULT_LIST_FMT
for blob in blobs:
self.inf(fmt.format(**blob))
def ensure_folder(self, path):
path.parent.mkdir(parents=True, exist_ok=True)
def handle_auto_cache(self, blob, auto_cache_dir) -> Path:
"""
This function guarantees that a given blob exists in the auto-cache.
It first checks whether the blob is already present. If so, it
returns the path of this cached blob. If the blob is not yet cached,
the blob is downloaded into the auto-cache directory and the path of
the freshly cached blob is returned.
"""
cached_blob = self.get_cached_blob(blob, [auto_cache_dir])
if cached_blob:
return cached_blob
name = Path(blob['path']).name
sha256 = blob['sha256']
self.download_blob(blob, auto_cache_dir / f'{name}.{sha256}')
cached_blob = self.get_cached_blob(blob, [auto_cache_dir])
assert cached_blob, f'Blob {name} still not cached in auto-cache.'
return cached_blob
def get_cached_blob(self, blob, cache_dirs: list) -> Path | None:
"""
Look for a cached blob in the provided cache directories.
A blob may be stored using either its original name or suffixed with
its SHA256 hash (e.g. "<name>.<sha256>").
Return the first matching path, or None if not found.
"""
name = Path(blob['path']).name
sha256 = blob["sha256"]
candidate_names = [
f"{name}.{sha256}", # suffixed version
name, # original blob name
]
for cache_dir in cache_dirs:
if not cache_dir.exists():
continue
for name in candidate_names:
candidate_path = cache_dir / name
if (
zephyr_module.get_blob_status(candidate_path, sha256)
== zephyr_module.BLOB_PRESENT
):
return candidate_path
return None
def download_blob(self, blob, path):
'''Download a blob from its url to a given path.'''
url = blob['url']
scheme = urlparse(url).scheme
self.dbg(f'Fetching blob from url {url} with {scheme} to path: {path}')
import fetchers
fetcher = fetchers.get_fetcher_cls(scheme)
self.dbg(f'Found fetcher: {fetcher}')
inst = fetcher()
self.ensure_folder(path)
inst.fetch(url, path)
def fetch_blob(self, args, blob):
"""
Ensures that the specified blob is available at its path.
If caching is enabled and the blob exists in the cache, it is copied
from there. Otherwise, the blob is downloaded from its URL and placed
at the target path.
"""
path = Path(blob['abspath'])
# collect existing cache dirs specified as args, otherwise from west config
cache_dirs = args.cache_dirs
auto_cache_dir = args.auto_cache
if self.has_config:
if cache_dirs is None:
cache_dirs = self.config.get('blobs.cache-dirs')
if auto_cache_dir is None:
auto_cache_dir = self.config.get('blobs.auto-cache')
# expand user home for each cache directory
if auto_cache_dir is not None:
auto_cache_dir = Path(auto_cache_dir).expanduser()
if cache_dirs is not None:
cache_dirs = [Path(p).expanduser() for p in cache_dirs.split(';') if p]
# search for cached blob in the cache directories
cached_blob = self.get_cached_blob(blob, cache_dirs or [])
# If blob is not found in cache directories: Use auto-cache if enabled
if not cached_blob and auto_cache_dir:
cached_blob = self.handle_auto_cache(blob, auto_cache_dir)
# Copy blob if it is cached, otherwise download it
if cached_blob:
self.dbg(f'Copy cached blob: {cached_blob}')
self.ensure_folder(path)
shutil.copy(cached_blob, path)
else:
self.download_blob(blob, path)
# Compare the checksum of a file we've just downloaded
# to the digest in blob metadata, warn user if they differ.
def verify_blob(self, blob) -> bool:
self.dbg(f"Verifying blob {blob['module']}: {blob['abspath']}")
status = zephyr_module.get_blob_status(blob['abspath'], blob['sha256'])
if status == zephyr_module.BLOB_OUTDATED:
self.err(
textwrap.dedent(
f'''\
The checksum of the downloaded file does not match that
in the blob metadata:
- if it is not certain that the download was successful,
try running 'west blobs fetch {blob['module']}'
to re-download the file
- if the error persists, please consider contacting
the maintainers of the module so that they can check
the corresponding blob metadata
Module: {blob['module']}
Blob: {blob['path']}
URL: {blob['url']}
Info: {blob['description']}'''
)
)
return False
return True
def fetch(self, args):
bad_checksum_count = 0
blobs = self.get_blobs(args)
for blob in blobs:
if blob['status'] == zephyr_module.BLOB_PRESENT:
self.dbg(f"Blob {blob['module']}: {blob['abspath']} is up to date")
continue
# if args.allow_regex is set, use it to filter the blob by path
if args.allow_regex and not re.match(args.allow_regex, blob['path']):
self.dbg(
f"Blob {blob['module']}: {blob['abspath']} does not match regex "
f"'{args.allow_regex}', skipping"
)
continue
self.inf(f"Fetching blob {blob['module']}: {blob['abspath']}")
if blob.get('click-through') and not args.auto_accept:
while True:
user_input = input(
"For this blob, need to read and accept "
"license to continue. Read it?\n"
"Please type 'y' or 'n' and press enter to confirm: "
)
if user_input.upper() == "Y" or user_input.upper() == "N":
break
if user_input.upper() != "Y":
self.wrn('Skip fetching this blob.')
continue
with open(blob['license-abspath'], encoding="utf-8") as license_file:
license_content = license_file.read()
print(license_content)
while True:
user_input = input(
"Accept license to continue?\n"
"Please type 'y' or 'n' and press enter to confirm: "
)
if user_input.upper() == "Y" or user_input.upper() == "N":
break
if user_input.upper() != "Y":
self.wrn('Skip fetching this blob.')
continue
self.fetch_blob(args, blob)
if not self.verify_blob(blob):
bad_checksum_count += 1
if bad_checksum_count:
self.err(f"{bad_checksum_count} blobs have bad checksums")
sys.exit(os.EX_DATAERR)
def clean(self, args):
blobs = self.get_blobs(args)
for blob in blobs:
if blob['status'] == zephyr_module.BLOB_NOT_PRESENT:
self.dbg(f"Blob {blob['module']}: {blob['abspath']} not in filesystem")
continue
self.inf(f"Deleting blob {blob['module']}: {blob['status']} {blob['abspath']}")
blob['abspath'].unlink()
def do_run(self, args, _):
self.dbg(f"subcmd: '{args.subcmd[0]}' modules: {args.modules}")
subcmd = getattr(self, args.subcmd[0])
if args.subcmd[0] != 'list' and args.format is not None:
self.die('unexpected --format argument; this is a "west blobs list" option')
subcmd(args)