diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 9c935c78fa..63ef0e74ed 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -22,7 +22,7 @@ from .helpers import ( map_section_name, parse_symbol_line, ) -from .toolchain import find_tool, run_tool +from .toolchain import find_tool, resolve_tool_path, run_tool if TYPE_CHECKING: from esphome.platformio_api import IDEData @@ -132,6 +132,12 @@ class MemoryAnalyzer: readelf_path = readelf_path or idedata.readelf_path _LOGGER.debug("Using toolchain paths from PlatformIO idedata") + # Validate paths exist, fall back to find_tool if they don't + # This handles cases like Zephyr where cc_path doesn't include full path + # and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-) + objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path) + readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path) + self.objdump_path = objdump_path or "objdump" self.readelf_path = readelf_path or "readelf" self.external_components = external_components or set() diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index aadc6a231c..83547b1eb5 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -15,6 +15,7 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") # - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM) # - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors) # - LibreTiny LN882X: .flash_text, .flash_copy* (flash code) +# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots) SECTION_MAPPING = { ".text": frozenset( [ @@ -30,6 +31,9 @@ SECTION_MAPPING = { # LibreTiny LN882X flash code ".flash_text", ".flash_copy", + # Zephyr/nRF52 sections (no leading dots) + "text", + "rom_start", ] ), ".rodata": frozenset( @@ -37,6 +41,8 @@ SECTION_MAPPING = { ".rodata", # LibreTiny RTL87xx read-only data in RAM ".ram.code_rodata", + # Zephyr/nRF52 sections (no leading dots) + "rodata", ] ), # .bss patterns - must be before .data to catch ".dram0.bss" @@ -45,9 +51,19 @@ SECTION_MAPPING = { ".bss", # LibreTiny LN882X BSS ".bss_ram", + # Zephyr/nRF52 sections (no leading dots) + "bss", + "noinit", + ] + ), + ".data": frozenset( + [ + ".data", + ".dram", + # Zephyr/nRF52 sections (no leading dots) + "datas", ] ), - ".data": frozenset([".data", ".dram"]), } # Section to ComponentMemory attribute mapping diff --git a/esphome/analyze_memory/helpers.py b/esphome/analyze_memory/helpers.py index cb503b37c5..a6ca7e7f0d 100644 --- a/esphome/analyze_memory/helpers.py +++ b/esphome/analyze_memory/helpers.py @@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: return None # Find section, size, and name + # Try each part as a potential section name for i, part in enumerate(parts): - if not part.startswith("."): - continue - + # Skip parts that are clearly flags, addresses, or other metadata + # Sections start with '.' (standard ELF) or are known section names (Zephyr) section = map_section_name(part) if not section: - break + continue # Need at least size field after section if i + 1 >= len(parts): diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py index 23d85e9700..3a8a5f7be4 100644 --- a/esphome/analyze_memory/toolchain.py +++ b/esphome/analyze_memory/toolchain.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import os from pathlib import Path import subprocess from typing import TYPE_CHECKING @@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [ "xtensa-lx106-elf-", # ESP8266 "xtensa-esp32-elf-", # ESP32 "xtensa-esp-elf-", # ESP32 (newer IDF) + "arm-zephyr-eabi-", # nRF52/Zephyr SDK + "arm-none-eabi-", # Generic ARM (RP2040, etc.) "", # System default (no prefix) ] +def _find_in_platformio_packages(tool_name: str) -> str | None: + """Search for a tool in PlatformIO package directories. + + This handles cases like Zephyr SDK where tools are installed in nested + directories that aren't in PATH. + + Args: + tool_name: Name of the tool (e.g., "readelf", "objdump") + + Returns: + Full path to the tool or None if not found + """ + # Get PlatformIO packages directory + platformio_home = Path(os.path.expanduser("~/.platformio/packages")) + if not platformio_home.exists(): + return None + + # Search patterns for toolchains that might contain the tool + # Order matters - more specific patterns first + search_patterns = [ + # Zephyr SDK deeply nested structure (4 levels) + # e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump + f"toolchain-*/*/*/bin/*-{tool_name}", + # Zephyr SDK nested structure (3 levels) + f"toolchain-*/*/bin/*-{tool_name}", + f"toolchain-*/bin/*-{tool_name}", + # Standard PlatformIO toolchain structure + f"toolchain-*/bin/*{tool_name}", + ] + + for pattern in search_patterns: + matches = list(platformio_home.glob(pattern)) + if matches: + # Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi + matches.sort(key=lambda p: ("zephyr" not in str(p), str(p))) + tool_path = str(matches[0]) + _LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path) + return tool_path + + return None + + +def resolve_tool_path( + tool_name: str, + derived_path: str | None, + objdump_path: str | None = None, +) -> str | None: + """Resolve a tool path, falling back to find_tool if derived path doesn't exist. + + Args: + tool_name: Name of the tool (e.g., "objdump", "readelf") + derived_path: Path derived from idedata (may not exist for some platforms) + objdump_path: Path to objdump binary to derive other tool paths from + + Returns: + Resolved path to the tool, or the original derived_path if it exists + """ + if derived_path and not Path(derived_path).exists(): + found = find_tool(tool_name, objdump_path) + if found: + _LOGGER.debug( + "Derived %s path %s not found, using %s", + tool_name, + derived_path, + found, + ) + return found + return derived_path + + def find_tool( tool_name: str, objdump_path: str | None = None, @@ -28,7 +101,8 @@ def find_tool( """Find a toolchain tool by name. First tries to derive the tool path from objdump_path (if provided), - then falls back to searching for platform-specific tools. + then searches PlatformIO package directories (for cross-compile toolchains), + and finally falls back to searching for platform-specific tools in PATH. Args: tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") @@ -47,7 +121,13 @@ def find_tool( _LOGGER.debug("Found %s at: %s", tool_name, potential_path) return potential_path - # Try platform-specific tools + # Search in PlatformIO packages directory first (handles Zephyr SDK, etc.) + # This must come before PATH search because system tools (e.g., /usr/bin/objdump) + # are for the host architecture, not the target (ARM, Xtensa, etc.) + if found := _find_in_platformio_packages(tool_name): + return found + + # Try platform-specific tools in PATH (fallback for when tools are installed globally) for prefix in TOOLCHAIN_PREFIXES: cmd = f"{prefix}{tool_name}" try: diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a63d33f73b..ed97c3b9a2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -558,8 +558,10 @@ bool APIServer::clear_noise_psk(bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->flags_.remove && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) { client->send_time_request(); + return; // Only request from one client to avoid clock conflicts + } } } #endif diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a1b684abbf..13f2fa59bd 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() { this->send_command_(DALLAS_COMMAND_START_CONVERSION); - this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] { + this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] { if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) { this->publish_state(NAN); return; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 149fcc79d5..01f79156a9 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -193,10 +193,18 @@ void BLEClientBase::log_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } -void BLEClientBase::log_gattc_event_(const char *name) { +void BLEClientBase::log_gattc_lifecycle_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); } +void BLEClientBase::log_gattc_data_event_(const char *name) { + // Data transfer events are logged at VERBOSE level because logging to UART creates + // delays that cause timing issues during time-sensitive BLE operations. This is + // especially problematic during pairing or firmware updates which require rapid + // writes to many characteristics - the log spam can cause these operations to fail. + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); +} + void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); } @@ -280,7 +288,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_OPEN_EVT: { if (!this->check_addr(param->open.remote_bda)) return false; - this->log_gattc_event_("OPEN"); + this->log_gattc_lifecycle_event_("OPEN"); // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; @@ -331,7 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CONNECT_EVT: { if (!this->check_addr(param->connect.remote_bda)) return false; - this->log_gattc_event_("CONNECT"); + this->log_gattc_lifecycle_event_("CONNECT"); this->conn_id_ = param->connect.conn_id; // Start MTU negotiation immediately as recommended by ESP-IDF examples // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in @@ -376,7 +384,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CLOSE_EVT: { if (this->conn_id_ != param->close.conn_id) return false; - this->log_gattc_event_("CLOSE"); + this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); this->set_state(espbt::ClientState::IDLE); this->conn_id_ = UNSET_CONN_ID; @@ -404,7 +412,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_SEARCH_CMPL_EVT: { if (this->conn_id_ != param->search_cmpl.conn_id) return false; - this->log_gattc_event_("SEARCH_CMPL"); + this->log_gattc_lifecycle_event_("SEARCH_CMPL"); // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery // This balances performance with bandwidth usage after the critical discovery phase if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { @@ -431,35 +439,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_READ_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("READ_DESCR"); + this->log_gattc_data_event_("READ_DESCR"); break; } case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("WRITE_DESCR"); + this->log_gattc_data_event_("WRITE_DESCR"); break; } case ESP_GATTC_WRITE_CHAR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("WRITE_CHAR"); + this->log_gattc_data_event_("WRITE_CHAR"); break; } case ESP_GATTC_READ_CHAR_EVT: { if (this->conn_id_ != param->read.conn_id) return false; - this->log_gattc_event_("READ_CHAR"); + this->log_gattc_data_event_("READ_CHAR"); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; - this->log_gattc_event_("NOTIFY"); + this->log_gattc_data_event_("NOTIFY"); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->log_gattc_event_("REG_FOR_NOTIFY"); + this->log_gattc_data_event_("REG_FOR_NOTIFY"); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value @@ -491,7 +499,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_err_t status = esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); - ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); + ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); if (status) { this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status); } @@ -499,13 +507,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - this->log_gattc_event_("UNREG_FOR_NOTIFY"); + this->log_gattc_data_event_("UNREG_FOR_NOTIFY"); break; } default: - // ideally would check all other events for matching conn_id - ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); + // Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations + ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 92c7444ee1..c52f0e5d2d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -127,7 +127,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { // 6 bytes used, 2 bytes padding void log_event_(const char *name); - void log_gattc_event_(const char *name); + void log_gattc_lifecycle_event_(const char *name); + void log_gattc_data_event_(const char *name); void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 639af4457f..f217d14c55 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -31,6 +31,18 @@ void RealTimeClock::dump_config() { void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); + // Skip if time is already synchronized to avoid unnecessary writes, log spam, + // and prevent clock jumping backwards due to network latency + constexpr time_t min_valid_epoch = 1546300800; // January 1, 2019 + time_t current_time = this->timestamp_now(); + // Check if time is valid (year >= 2019) before comparing + if (current_time >= min_valid_epoch) { + // Unsigned subtraction handles wraparound correctly, then cast to signed + int32_t diff = static_cast(epoch - static_cast(current_time)); + if (diff >= -1 && diff <= 1) { + return; + } + } // Update UTC epoch time. #ifdef USE_ZEPHYR struct timespec ts; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 309407fbec..b5bf849c30 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -617,6 +617,55 @@ std::vector base64_decode(const std::string &encoded_string) { return ret; } +/// Encode int32 to 5 base85 characters + null terminator +/// Standard ASCII85 alphabet: '!' (33) = 0 through 'u' (117) = 84 +inline void base85_encode_int32(int32_t value, std::span output) { + uint32_t v = static_cast(value); + // Encode least significant digit first, then reverse + for (int i = 4; i >= 0; i--) { + output[i] = static_cast('!' + (v % 85)); + v /= 85; + } + output[5] = '\0'; +} + +/// Decode 5 base85 characters to int32 +inline bool base85_decode_int32(const char *input, int32_t &out) { + uint8_t c0 = static_cast(input[0] - '!'); + uint8_t c1 = static_cast(input[1] - '!'); + uint8_t c2 = static_cast(input[2] - '!'); + uint8_t c3 = static_cast(input[3] - '!'); + uint8_t c4 = static_cast(input[4] - '!'); + + // Each digit must be 0-84. Since uint8_t wraps, chars below '!' become > 84 + if (c0 > 84 || c1 > 84 || c2 > 84 || c3 > 84 || c4 > 84) + return false; + + // 85^4 = 52200625, 85^3 = 614125, 85^2 = 7225, 85^1 = 85 + out = static_cast(c0 * 52200625u + c1 * 614125u + c2 * 7225u + c3 * 85u + c4); + return true; +} + +/// Decode base85 string directly into vector (no intermediate buffer) +bool base85_decode_int32_vector(const std::string &base85, std::vector &out) { + size_t len = base85.size(); + if (len % 5 != 0) + return false; + + out.clear(); + const char *ptr = base85.data(); + const char *end = ptr + len; + + while (ptr < end) { + int32_t value; + if (!base85_decode_int32(ptr, value)) + return false; + out.push_back(value); + ptr += 5; + } + return true; +} + // Colors float gamma_correct(float value, float gamma) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 4decc19657..9dc289c743 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -21,6 +21,7 @@ #ifdef USE_ESP8266 #include +#include #endif #ifdef USE_RP2040 @@ -1136,6 +1137,14 @@ std::vector base64_decode(const std::string &encoded_string); size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); +/// Size of buffer needed for base85 encoded int32 (5 chars + null terminator) +static constexpr size_t BASE85_INT32_ENCODED_SIZE = 6; + +void base85_encode_int32(int32_t value, std::span output); + +bool base85_decode_int32(const char *input, int32_t &out); +bool base85_decode_int32_vector(const std::string &base85, std::vector &out); + ///@} /// @name Colors diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 7ecbfb225e..318ac04a7d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -93,6 +93,7 @@ class Platform(StrEnum): RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x LN882X_ARD = "ln882x-ard" # LibreTiny LN882x RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico + NRF52_ZEPHYR = "nrf52-adafruit" # Nordic nRF52 (Zephyr) # Memory impact analysis constants @@ -112,7 +113,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( "rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny) "ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny) "host", # Host platform (for testing on development machine) - "nrf52", # Nordic nRF52 platform implementation + "nrf52", # Nordic nRF52 platform implementation (uses Zephyr) } ) @@ -126,6 +127,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( # 4-6. Other ESP32 variants - Less commonly used but still supported # 7-9. LibreTiny platforms (BK72XX, RTL87XX, LN882X) - good for detecting LibreTiny-specific changes # 10. RP2040 - Raspberry Pi Pico platform +# 11. nRF52 - Nordic nRF52 with Zephyr (good for detecting Zephyr-specific changes) MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee) Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds) @@ -137,6 +139,7 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.RTL87XX_ARD, # LibreTiny RTL8720x Platform.LN882X_ARD, # LibreTiny LN882x Platform.RP2040_ARD, # Raspberry Pi Pico + Platform.NRF52_ZEPHYR, # Nordic nRF52 (Zephyr) ] @@ -463,6 +466,10 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: if "pico" in filename_lower or "rp2040" in filename_lower: return Platform.RP2040_ARD + # nRF52 / Zephyr + if "nrf52" in filename_lower or "zephyr" in filename_lower: + return Platform.NRF52_ZEPHYR + return None diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 52025513a8..61ef8985df 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1499,6 +1499,23 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> "tests/components/rp2040/test.rp2040-ard.yaml", determine_jobs.Platform.RP2040_ARD, ), + # nRF52 / Zephyr detection + ( + "tests/components/logger/test.nrf52-adafruit.yaml", + determine_jobs.Platform.NRF52_ZEPHYR, + ), + ( + "esphome/components/nrf52/gpio.cpp", + determine_jobs.Platform.NRF52_ZEPHYR, + ), + ( + "esphome/components/zephyr/core.cpp", + determine_jobs.Platform.NRF52_ZEPHYR, + ), + ( + "esphome/components/zephyr_ble_server/ble_server.cpp", + determine_jobs.Platform.NRF52_ZEPHYR, + ), # No platform hint (generic files) ("esphome/components/wifi/wifi.cpp", None), ("esphome/components/sensor/sensor.h", None), @@ -1528,6 +1545,10 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> "pico_i2c", "pico_spi", "rp2040_test_yaml", + "nrf52_test_yaml", + "nrf52_gpio", + "zephyr_core", + "zephyr_ble_server", "generic_wifi_no_hint", "generic_sensor_no_hint", "core_helpers_no_hint", @@ -1554,6 +1575,11 @@ def test_detect_platform_hint_from_filename( ("file_ESP8266.cpp", determine_jobs.Platform.ESP8266_ARD), # ESP32 with different cases ("file_ESP32.cpp", determine_jobs.Platform.ESP32_IDF), + # nRF52/Zephyr with different cases + ("file_NRF52.cpp", determine_jobs.Platform.NRF52_ZEPHYR), + ("file_Nrf52.cpp", determine_jobs.Platform.NRF52_ZEPHYR), + ("file_ZEPHYR.cpp", determine_jobs.Platform.NRF52_ZEPHYR), + ("file_Zephyr.cpp", determine_jobs.Platform.NRF52_ZEPHYR), ], ids=[ "rp2040_uppercase", @@ -1562,6 +1588,10 @@ def test_detect_platform_hint_from_filename( "pico_titlecase", "esp8266_uppercase", "esp32_uppercase", + "nrf52_uppercase", + "nrf52_mixedcase", + "zephyr_uppercase", + "zephyr_titlecase", ], ) def test_detect_platform_hint_from_filename_case_insensitive(