diff --git a/CODEOWNERS b/CODEOWNERS index 2975080ba9..b5037a6f9f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/npi19/* @bakerkj +esphome/components/nrf52/* @tomaszduda23 esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages @@ -535,5 +536,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow +esphome/components/zephyr/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9ac2999696..c055facd6c 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( COMPONENT_LN882X, COMPONENT_RTL87XX, ) +from esphome.components.zephyr import ( + zephyr_add_cdc_acm, + zephyr_add_overlay, + zephyr_add_prj_conf, +) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -41,6 +46,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_NRF52, PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] +UART_SELECTION_NRF52 = [USB_CDC, UART0] + HARDWARE_UART_TO_UART_SELECTION = { UART0: logger_ns.UART_SELECTION_UART0, UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, @@ -167,6 +175,8 @@ def uart_selection(value): return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) if CORE.is_host: raise cv.Invalid("Uart selection not valid for host platform") + if CORE.is_nrf52: + return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value) raise NotImplementedError @@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_( automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), ) + CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( bk72xx=DEFAULT, ln882x=DEFAULT, rtl87xx=DEFAULT, + nrf52=USB_CDC, ): cv.All( cv.only_on( [ @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX, + PLATFORM_NRF52, ] ), uart_selection, @@ -358,6 +371,15 @@ async def to_code(config): except cv.Invalid: pass + if CORE.using_zephyr: + if config[CONF_HARDWARE_UART] == UART0: + zephyr_add_overlay("""&uart0 { status = "okay";};""") + if config[CONF_HARDWARE_UART] == UART1: + zephyr_add_overlay("""&uart1 { status = "okay";};""") + if config[CONF_HARDWARE_UART] == USB_CDC: + zephyr_add_prj_conf("UART_LINE_CTRL", True) + zephyr_add_cdc_acm(config, 0) + # Register at end for safe mode await cg.register_component(log, config) @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, "task_log_buffer.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index db807f7e53..01a7565699 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,9 +4,9 @@ #include // For unique_ptr #endif +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace logger { @@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); +#elif defined(USE_ZEPHYR) + this->main_task_ = k_current_get(); #endif } #ifdef USE_ESPHOME_TASK_LOG_BUFFER @@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) { } #endif +#ifndef USE_ZEPHYR #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) void Logger::loop() { #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) @@ -185,8 +188,13 @@ void Logger::loop() { } opened = !opened; } +#endif + this->process_messages_(); +} +#endif #endif +void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available if (this->log_buffer_->has_messages()) { @@ -227,12 +235,11 @@ void Logger::loop() { } #endif } -#endif void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } #endif diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fb68e75a51..6bd5bb66ed 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -29,6 +29,11 @@ #include #endif // USE_ESP_IDF +#ifdef USE_ZEPHYR +#include +struct device; +#endif + namespace esphome { namespace logger { @@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { "VV", // VERY_VERBOSE }; -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. @@ -82,7 +87,7 @@ enum UARTSelection : uint8_t { UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 }; -#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY +#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR /** * @brief Logger component for all ESPHome logging. @@ -107,7 +112,7 @@ class Logger : public Component { #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif -#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) +#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -122,7 +127,7 @@ class Logger : public Component { #ifdef USE_ESP32 void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; @@ -157,6 +162,7 @@ class Logger : public Component { #endif protected: + void process_messages_(); void write_msg_(const char *msg); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator @@ -164,7 +170,7 @@ class Logger : public Component { inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, va_list args, char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #else this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); @@ -231,7 +237,10 @@ class Logger : public Component { #ifdef USE_ARDUINO Stream *hw_serial_{nullptr}; #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ZEPHYR) + const device *uart_dev_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void *main_task_ = nullptr; // Only used for thread name identification #endif #ifdef USE_ESP32 @@ -256,7 +265,7 @@ class Logger : public Component { uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY @@ -268,9 +277,13 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) const char *HOT get_thread_name_() { +#ifdef USE_ZEPHYR + k_tid_t current_task = k_current_get(); +#else TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); +#endif if (current_task == main_task_) { return nullptr; // Main task } else { @@ -278,6 +291,8 @@ class Logger : public Component { return pcTaskGetName(current_task); #elif defined(USE_LIBRETINY) return pcTaskGetTaskName(current_task); +#elif defined(USE_ZEPHYR) + return k_thread_name_get(current_task); #endif } } @@ -319,7 +334,7 @@ class Logger : public Component { const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) if (thread_name != nullptr) { // Non-main task with thread name this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp new file mode 100644 index 0000000000..35ef2e9561 --- /dev/null +++ b/esphome/components/logger/logger_zephyr.cpp @@ -0,0 +1,88 @@ +#ifdef USE_ZEPHYR + +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "logger.h" + +#include +#include +#include + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::loop() { +#ifdef USE_LOGGER_USB_CDC + if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { + return; + } + static bool opened = false; + uint32_t dtr = 0; + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + + /* Poll if the DTR flag was set, optional */ + if (opened == dtr) { + return; + } + + if (!opened) { + App.schedule_dump_config(); + } + opened = !opened; +#endif + this->process_messages_(); +} + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + static const struct device *uart_dev = nullptr; + switch (this->uart_) { + case UART_SELECTION_UART0: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0)); + break; + case UART_SELECTION_UART1: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1)); + break; +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0)); + if (device_is_ready(uart_dev)) { + usb_enable(nullptr); + } + break; +#endif + } + if (!device_is_ready(uart_dev)) { + ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); + } else { + this->uart_dev_ = uart_dev; + } + } + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { +#ifdef CONFIG_PRINTK + printk("%s\n", msg); +#endif + if (nullptr == this->uart_dev_) { + return; + } + while (*msg) { + uart_poll_out(this->uart_dev_, *msg); + ++msg; + } + uart_poll_out(this->uart_dev_, '\n'); +} + +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome + +#endif diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py new file mode 100644 index 0000000000..c23298e38f --- /dev/null +++ b/esphome/components/nrf52/__init__.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +from pathlib import Path + +import esphome.codegen as cg +from esphome.components.zephyr import ( + copy_files as zephyr_copy_files, + zephyr_add_pm_static, + zephyr_set_core_data, + zephyr_to_code, +) +from esphome.components.zephyr.const import ( + BOOTLOADER_MCUBOOT, + KEY_BOOTLOADER, + KEY_ZEPHYR, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BOARD, + CONF_FRAMEWORK, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_NRF52, +) +from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.storage_json import StorageJSON +from esphome.types import ConfigType + +from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG +from .const import ( + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, +) + +# force import gpio to register pin schema +from .gpio import nrf52_pin_to_code # noqa + +CODEOWNERS = ["@tomaszduda23"] +AUTO_LOAD = ["zephyr"] +IS_TARGET_PLATFORM = True + + +def set_core_data(config: ConfigType) -> ConfigType: + zephyr_set_core_data(config) + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52 + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1) + + if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: + zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]) + + return config + + +BOOTLOADERS = [ + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + BOOTLOADER_MCUBOOT, +] + + +def _detect_bootloader(config: ConfigType) -> ConfigType: + """Detect the bootloader for the given board.""" + config = config.copy() + bootloaders: list[str] = [] + board = config[CONF_BOARD] + + if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]: + # this board have bootloaders config available + bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER] + + if KEY_BOOTLOADER not in config: + if bootloaders: + # there is no bootloader in config -> take first one + config[KEY_BOOTLOADER] = bootloaders[0] + else: + # make mcuboot as default if there is no configuration for that board + config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT + elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders: + raise cv.Invalid( + f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), + } + ), + _detect_bootloader, + set_core_data, +) + + +@coroutine_with_priority(1000) +async def to_code(config: ConfigType) -> None: + """Convert the configuration to code.""" + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_NRF52") + cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + cg.add_define("ESPHOME_VARIANT", "NRF52") + cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) + cg.add_platformio_option( + "platform", + "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip", + ) + cg.add_platformio_option( + "platform_packages", + [ + "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip", + "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip", + ], + ) + + if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT: + # make sure that firmware.zip is created + # for Adafruit_nRF52_Bootloader + cg.add_platformio_option("board_upload.protocol", "nrfutil") + cg.add_platformio_option("board_upload.use_1200bps_touch", "true") + cg.add_platformio_option("board_upload.require_upload_port", "true") + cg.add_platformio_option("board_upload.wait_for_upload_port", "true") + + zephyr_to_code(config) + + +def copy_files() -> None: + """Copy files to the build directory.""" + zephyr_copy_files() + + +def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: + """Get the download types for the firmware.""" + types = [] + UF2_PATH = "zephyr/zephyr.uf2" + DFU_PATH = "firmware.zip" + HEX_PATH = "zephyr/zephyr.hex" + HEX_MERGED_PATH = "zephyr/merged.hex" + APP_IMAGE_PATH = "zephyr/app_update.bin" + build_dir = Path(storage_json.firmware_bin_path).parent + if (build_dir / UF2_PATH).is_file(): + types = [ + { + "title": "UF2 package (recommended)", + "description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.", + "file": UF2_PATH, + "download": f"{storage_json.name}.uf2", + }, + { + "title": "DFU package", + "description": "For flashing via adafruit-nrfutil using USB CDC.", + "file": DFU_PATH, + "download": f"dfu-{storage_json.name}.zip", + }, + ] + else: + types = [ + { + "title": "HEX package", + "description": "For flashing via pyocd using SWD.", + "file": ( + HEX_MERGED_PATH + if (build_dir / HEX_MERGED_PATH).is_file() + else HEX_PATH + ), + "download": f"{storage_json.name}.hex", + }, + ] + if (build_dir / APP_IMAGE_PATH).is_file(): + types += [ + { + "title": "App update package", + "description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.", + "file": APP_IMAGE_PATH, + "download": f"app-{storage_json.name}.img", + }, + ] + + return types + + +def _upload_using_platformio( + config: ConfigType, port: str, upload_args: list[str] +) -> int | str: + from esphome import platformio_api + + if port is not None: + upload_args += ["--upload-port", port] + return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) + + +def upload_program(config: ConfigType, args, host: str) -> bool: + from esphome.__main__ import check_permissions, get_port_type + + result = 0 + handled = False + + if get_port_type(host) == "SERIAL": + check_permissions(host) + result = _upload_using_platformio(config, host, ["-t", "upload"]) + handled = True + + if host == "PYOCD": + result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) + handled = True + + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + + return handled diff --git a/esphome/components/nrf52/boards.py b/esphome/components/nrf52/boards.py new file mode 100644 index 0000000000..8e5fb2a23d --- /dev/null +++ b/esphome/components/nrf52/boards.py @@ -0,0 +1,34 @@ +from esphome.components.zephyr import Section +from esphome.components.zephyr.const import KEY_BOOTLOADER + +from .const import ( + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, +) + +BOARDS_ZEPHYR = { + "adafruit_itsybitsy_nrf52840": { + KEY_BOOTLOADER: [ + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + ] + }, +} + +# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go +# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map +BOOTLOADER_CONFIG = { + BOOTLOADER_ADAFRUIT_NRF52_SD132: [ + Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), + ], + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [ + Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), + ], + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [ + Section("empty_app_offset", 0x0, 0x27000, "flash_primary"), + ], +} diff --git a/esphome/components/nrf52/const.py b/esphome/components/nrf52/const.py new file mode 100644 index 0000000000..d827e5fb22 --- /dev/null +++ b/esphome/components/nrf52/const.py @@ -0,0 +1,4 @@ +BOOTLOADER_ADAFRUIT = "adafruit" +BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" +BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" +BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py new file mode 100644 index 0000000000..85230c1f57 --- /dev/null +++ b/esphome/components/nrf52/gpio.py @@ -0,0 +1,53 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components.zephyr.const import zephyr_ns +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52 + +ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin) + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + # e.g. P0.27 + if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".": + return cv.int_(value[len("P")].strip()) * 32 + cv.int_( + value[len("P0.") :].strip() + ) + raise cv.Invalid(f"Invalid pin: {value}") + + +def validate_gpio_pin(value): + value = _translate_pin(value) + if value < 0 or value > (32 + 16): + raise cv.Invalid(f"NRF52: Invalid pin number: {value}") + return value + + +NRF52_PIN_SCHEMA = cv.All( + pins.gpio_base_schema( + ZephyrGPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES, + ), +) + + +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA) +async def nrf52_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index ab821d457b..58d35c4baf 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -6,6 +6,7 @@ import tzlocal from esphome import automation from esphome.automation import Condition import esphome.codegen as cg +from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv from esphome.const import ( CONF_AT, @@ -25,7 +26,7 @@ from esphome.const import ( CONF_TIMEZONE, CONF_TRIGGER_ID, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority _LOGGER = logging.getLogger(__name__) @@ -341,6 +342,8 @@ async def register_time(time_var, config): @coroutine_with_priority(100.0) async def to_code(config): + if CORE.using_zephyr: + zephyr_add_prj_conf("POSIX_CLOCK", True) cg.add_define("USE_TIME") cg.add_global(time_ns.using) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 61391d2c6b..42c564659f 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -2,13 +2,15 @@ #include "esphome/core/log.h" #ifdef USE_HOST #include +#elif defined(USE_ZEPHYR) +#include #else #include "lwip/opt.h" #endif #ifdef USE_ESP8266 #include "sys/time.h" #endif -#ifdef USE_RP2040 +#if defined(USE_RP2040) || defined(USE_ZEPHYR) #include #endif #include @@ -22,11 +24,22 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::synchronize_epoch_(uint32_t epoch) { + ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); // Update UTC epoch time. +#ifdef USE_ZEPHYR + struct timespec ts; + ts.tv_nsec = 0; + ts.tv_sec = static_cast(epoch); + + int ret = clock_settime(CLOCK_REALTIME, &ts); + + if (ret != 0) { + ESP_LOGW(TAG, "clock_settime() failed with code %d", ret); + } +#else struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; - ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); if (ret == EINVAL) { @@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } - +#endif auto time = this->now(); ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second); diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py new file mode 100644 index 0000000000..2b542404a5 --- /dev/null +++ b/esphome/components/zephyr/__init__.py @@ -0,0 +1,231 @@ +import os +from typing import Final, TypedDict + +import esphome.codegen as cg +from esphome.const import CONF_BOARD +from esphome.core import CORE +from esphome.helpers import copy_file_if_changed, write_file_if_changed + +from .const import ( + BOOTLOADER_MCUBOOT, + KEY_BOOTLOADER, + KEY_EXTRA_BUILD_FILES, + KEY_OVERLAY, + KEY_PM_STATIC, + KEY_PRJ_CONF, + KEY_ZEPHYR, + zephyr_ns, +) + +CODEOWNERS = ["@tomaszduda23"] +AUTO_LOAD = ["preferences"] +KEY_BOARD: Final = "board" + +PrjConfValueType = bool | str | int + + +class Section: + def __init__(self, name, address, size, region): + self.name = name + self.address = address + self.size = size + self.region = region + self.end_address = self.address + self.size + + def __str__(self): + return ( + f"{self.name}:\n" + f" address: 0x{self.address:X}\n" + f" end_address: 0x{self.end_address:X}\n" + f" region: {self.region}\n" + f" size: 0x{self.size:X}" + ) + + +class ZephyrData(TypedDict): + board: str + bootloader: str + prj_conf: dict[str, tuple[PrjConfValueType, bool]] + overlay: str + extra_build_files: dict[str, str] + pm_static: list[Section] + + +def zephyr_set_core_data(config): + CORE.data[KEY_ZEPHYR] = ZephyrData( + board=config[CONF_BOARD], + bootloader=config[KEY_BOOTLOADER], + prj_conf={}, + overlay="", + extra_build_files={}, + pm_static=[], + ) + return config + + +def zephyr_data() -> ZephyrData: + return CORE.data[KEY_ZEPHYR] + + +def zephyr_add_prj_conf( + name: str, value: PrjConfValueType, required: bool = True +) -> None: + """Set an zephyr prj conf value.""" + if not name.startswith("CONFIG_"): + name = "CONFIG_" + name + prj_conf = zephyr_data()[KEY_PRJ_CONF] + if name not in prj_conf: + prj_conf[name] = (value, required) + return + old_value, old_required = prj_conf[name] + if old_value != value and old_required: + raise ValueError( + f"{name} already set with value '{old_value}', cannot set again to '{value}'" + ) + if required: + prj_conf[name] = (value, required) + + +def zephyr_add_overlay(content): + zephyr_data()[KEY_OVERLAY] += content + + +def add_extra_build_file(filename: str, path: str) -> bool: + """Add an extra build file to the project.""" + extra_build_files = zephyr_data()[KEY_EXTRA_BUILD_FILES] + if filename not in extra_build_files: + extra_build_files[filename] = path + return True + return False + + +def add_extra_script(stage: str, filename: str, path: str): + """Add an extra script to the project.""" + key = f"{stage}:{filename}" + if add_extra_build_file(filename, path): + cg.add_platformio_option("extra_scripts", [key]) + + +def zephyr_to_code(config): + cg.add(zephyr_ns.setup_preferences()) + cg.add_build_flag("-DUSE_ZEPHYR") + cg.set_cpp_standard("gnu++20") + # build is done by west so bypass board checking in platformio + cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) + + # c++ support + zephyr_add_prj_conf("NEWLIB_LIBC", True) + zephyr_add_prj_conf("CONFIG_FPU", True) + zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) + zephyr_add_prj_conf("CPLUSPLUS", True) + zephyr_add_prj_conf("CONFIG_STD_CPP20", True) + zephyr_add_prj_conf("LIB_CPLUSPLUS", True) + # preferences + zephyr_add_prj_conf("SETTINGS", True) + zephyr_add_prj_conf("NVS", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("CONFIG_FLASH", True) + # watchdog + zephyr_add_prj_conf("WATCHDOG", True) + zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) + # disable console + zephyr_add_prj_conf("UART_CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False, False) + # use NFC pins as GPIO + zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + + # os: ***** USAGE FAULT ***** + # os: Illegal load of EXC_RETURN into PC + zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048) + + add_extra_script( + "pre", + "pre_build.py", + os.path.join(os.path.dirname(__file__), "pre_build.py.script"), + ) + + +def _format_prj_conf_val(value: PrjConfValueType) -> str: + if isinstance(value, bool): + return "y" if value else "n" + if isinstance(value, int): + return str(value) + if isinstance(value, str): + return f'"{value}"' + raise ValueError + + +def zephyr_add_cdc_acm(config, id): + zephyr_add_prj_conf("USB_DEVICE_STACK", True) + zephyr_add_prj_conf("USB_CDC_ACM", True) + # prevent device to go to susspend, without this communication stop working in python + # there should be a way to solve it + zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False) + # prevent logging when buffer is full + zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True) + zephyr_add_overlay( + f""" +&zephyr_udc0 {{ + cdc_acm_uart{id}: cdc_acm_uart{id} {{ + compatible = "zephyr,cdc-acm-uart"; + }}; +}}; +""" + ) + + +def zephyr_add_pm_static(section: Section): + CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section) + + +def copy_files(): + want_opts = zephyr_data()[KEY_PRJ_CONF] + + prj_conf = ( + "\n".join( + f"{name}={_format_prj_conf_val(value[0])}" + for name, value in sorted(want_opts.items()) + ) + + "\n" + ) + + write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf) + + write_file_if_changed( + CORE.relative_build_path("zephyr/app.overlay"), + zephyr_data()[KEY_OVERLAY], + ) + + if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ + KEY_BOARD + ] in ["xiao_ble"]: + fake_board_manifest = """ +{ +"frameworks": [ + "zephyr" +], +"name": "esphome nrf52", +"upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104 +}, +"url": "https://esphome.io/", +"vendor": "esphome" +} +""" + write_file_if_changed( + CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), + fake_board_manifest, + ) + + for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items(): + copy_file_if_changed( + path, + CORE.relative_build_path(filename), + ) + + pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC]) + if pm_static: + write_file_if_changed( + CORE.relative_build_path("zephyr/pm_static.yml"), pm_static + ) diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py new file mode 100644 index 0000000000..f14a326344 --- /dev/null +++ b/esphome/components/zephyr/const.py @@ -0,0 +1,14 @@ +from typing import Final + +import esphome.codegen as cg + +BOOTLOADER_MCUBOOT = "mcuboot" + +KEY_BOOTLOADER: Final = "bootloader" +KEY_EXTRA_BUILD_FILES: Final = "extra_build_files" +KEY_OVERLAY: Final = "overlay" +KEY_PM_STATIC: Final = "pm_static" +KEY_PRJ_CONF: Final = "prj_conf" +KEY_ZEPHYR = "zephyr" + +zephyr_ns = cg.esphome_ns.namespace("zephyr") diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp new file mode 100644 index 0000000000..39b01f8abe --- /dev/null +++ b/esphome/components/zephyr/core.cpp @@ -0,0 +1,86 @@ +#ifdef USE_ZEPHYR + +#include +#include +#include +#include +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +namespace esphome { + +static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); + +void yield() { ::k_yield(); } +uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } +uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } +void delayMicroseconds(uint32_t us) { ::k_usleep(us); } +void delay(uint32_t ms) { ::k_msleep(ms); } + +void arch_init() { + if (device_is_ready(WDT)) { + static wdt_timeout_cfg wdt_config{}; + wdt_config.flags = WDT_FLAG_RESET_SOC; + wdt_config.window.max = 2000; + wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); + if (wdt_channel_id >= 0) { + wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP); + } + } +} + +void arch_feed_wdt() { + if (wdt_channel_id >= 0) { + wdt_feed(WDT, wdt_channel_id); + } +} + +void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } +uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } +uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } +uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } + +Mutex::Mutex() { + auto *mutex = new k_mutex(); + this->handle_ = mutex; + k_mutex_init(mutex); +} +Mutex::~Mutex() { delete static_cast(this->handle_); } +void Mutex::lock() { k_mutex_lock(static_cast(this->handle_), K_FOREVER); } +bool Mutex::try_lock() { return k_mutex_lock(static_cast(this->handle_), K_NO_WAIT) == 0; } +void Mutex::unlock() { k_mutex_unlock(static_cast(this->handle_)); } + +IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } +IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } + +uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) +bool random_bytes(uint8_t *data, size_t len) { + sys_rand_get(data, len); + return true; +} + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) + mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0; + mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF; + mac[2] = NRF_FICR->DEVICEADDR[0] >> 24; + mac[3] = NRF_FICR->DEVICEADDR[0] >> 16; + mac[4] = NRF_FICR->DEVICEADDR[0] >> 8; + mac[5] = NRF_FICR->DEVICEADDR[0]; +} + +} // namespace esphome + +void setup(); +void loop(); + +int main() { + setup(); + while (true) { + loop(); + esphome::yield(); + } + return 0; +} + +#endif diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp new file mode 100644 index 0000000000..4b84910368 --- /dev/null +++ b/esphome/components/zephyr/gpio.cpp @@ -0,0 +1,120 @@ +#ifdef USE_ZEPHYR +#include "gpio.h" +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace zephyr { + +static const char *const TAG = "zephyr"; + +static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { + int ret = 0; + if (flags & gpio::FLAG_INPUT) { + ret |= GPIO_INPUT; + } + if (flags & gpio::FLAG_OUTPUT) { + ret |= GPIO_OUTPUT; + if (value != inverted) { + ret |= GPIO_OUTPUT_INIT_HIGH; + } else { + ret |= GPIO_OUTPUT_INIT_LOW; + } + } + if (flags & gpio::FLAG_PULLUP) { + ret |= GPIO_PULL_UP; + } + if (flags & gpio::FLAG_PULLDOWN) { + ret |= GPIO_PULL_DOWN; + } + if (flags & gpio::FLAG_OPEN_DRAIN) { + ret |= GPIO_OPEN_DRAIN; + } + return ret; +} + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = this->pin_; + arg->inverted = this->inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + // TODO +} + +void ZephyrGPIOPin::setup() { + const struct device *gpio = nullptr; + if (this->pin_ < 32) { +#define GPIO0 DT_NODELABEL(gpio0) +#if DT_NODE_HAS_STATUS(GPIO0, okay) + gpio = DEVICE_DT_GET(GPIO0); +#else +#error "gpio0 is disabled" +#endif + } else { +#define GPIO1 DT_NODELABEL(gpio1) +#if DT_NODE_HAS_STATUS(GPIO1, okay) + gpio = DEVICE_DT_GET(GPIO1); +#else +#error "gpio1 is disabled" +#endif + } + if (device_is_ready(gpio)) { + this->gpio_ = gpio; + } else { + ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_); + return; + } + this->pin_mode(this->flags_); +} + +void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { + if (nullptr == this->gpio_) { + return; + } + gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); +} + +std::string ZephyrGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); + return buffer; +} + +bool ZephyrGPIOPin::digital_read() { + if (nullptr == this->gpio_) { + return false; + } + return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_); +} + +void ZephyrGPIOPin::digital_write(bool value) { + // make sure that value is not ignored since it can be inverted e.g. on switch side + // that way init state should be correct + this->value_ = value; + if (nullptr == this->gpio_) { + return; + } + gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0); +} +void ZephyrGPIOPin::detach_interrupt() const { + // TODO +} + +} // namespace zephyr + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + // TODO + return false; +} + +} // namespace esphome + +#endif diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h new file mode 100644 index 0000000000..f512ae4648 --- /dev/null +++ b/esphome/components/zephyr/gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ZEPHYR +#include "esphome/core/hal.h" +struct device; +namespace esphome { +namespace zephyr { + +class ZephyrGPIOPin : public InternalGPIOPin { + public: + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + void detach_interrupt() const override; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return this->pin_; } + bool is_inverted() const override { return this->inverted_; } + gpio::Flags get_flags() const override { return flags_; } + + protected: + void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; + const device *gpio_ = nullptr; + bool value_ = false; +}; + +} // namespace zephyr +} // namespace esphome + +#endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/pre_build.py.script b/esphome/components/zephyr/pre_build.py.script new file mode 100644 index 0000000000..3731fccf53 --- /dev/null +++ b/esphome/components/zephyr/pre_build.py.script @@ -0,0 +1,4 @@ +Import("env") + +board_config = env.BoardConfig() +board_config.update("frameworks", ["arduino", "zephyr"]) diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp new file mode 100644 index 0000000000..d702366044 --- /dev/null +++ b/esphome/components/zephyr/preferences.cpp @@ -0,0 +1,156 @@ +#ifdef USE_ZEPHYR + +#include +#include "esphome/core/preferences.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace zephyr { + +static const char *const TAG = "zephyr.preferences"; + +#define ESPHOME_SETTINGS_KEY "esphome" + +class ZephyrPreferenceBackend : public ESPPreferenceBackend { + public: + ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; } + ZephyrPreferenceBackend(uint32_t type, std::vector &&data) : data(std::move(data)) { this->type_ = type; } + + bool save(const uint8_t *data, size_t len) override { + this->data.resize(len); + std::memcpy(this->data.data(), data, len); + ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len); + return true; + } + + bool load(uint8_t *data, size_t len) override { + if (len != this->data.size()) { + ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len); + return false; + } + std::memcpy(data, this->data.data(), len); + ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len); + return true; + } + + uint32_t get_type() const { return this->type_; } + std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } + + std::vector data; + + protected: + uint32_t type_ = 0; +}; + +class ZephyrPreferences : public ESPPreferences { + public: + void open() { + int err = settings_subsys_init(); + if (err) { + ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); + return; + } + + static struct settings_handler settings_cb = { + .name = ESPHOME_SETTINGS_KEY, + .h_set = load_setting, + .h_export = export_settings, + }; + + err = settings_register(&settings_cb); + if (err) { + ESP_LOGE(TAG, "setting_register failed, err, %d", err); + return; + } + + err = settings_load_subtree(ESPHOME_SETTINGS_KEY); + if (err) { + ESP_LOGE(TAG, "Cannot load settings, err: %d", err); + return; + } + ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size()); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { + return make_preference(length, type); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { + for (auto *backend : this->backends_) { + if (backend->get_type() == type) { + return ESPPreferenceObject(backend); + } + } + printf("type %u size %u\n", type, this->backends_.size()); + auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory) + ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str()); + this->backends_.push_back(pref); + return ESPPreferenceObject(pref); + } + + bool sync() override { + ESP_LOGD(TAG, "Save settings"); + int err = settings_save(); + if (err) { + ESP_LOGE(TAG, "Cannot save settings, err: %d", err); + return false; + } + return true; + } + + bool reset() override { + ESP_LOGD(TAG, "Reset settings"); + for (auto *backend : this->backends_) { + // save empty delete data + backend->data.clear(); + } + sync(); + return true; + } + + protected: + std::vector backends_; + + static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { + auto type = parse_hex(name); + if (!type.has_value()) { + std::string full_name(ESPHOME_SETTINGS_KEY); + full_name += "/"; + full_name += name; + // Delete unusable keys. Otherwise it will stay in flash forever. + settings_delete(full_name.c_str()); + return 1; + } + std::vector data(len); + int err = read_cb(cb_arg, data.data(), len); + + ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err); + auto *pref = new ZephyrPreferenceBackend(*type, std::move(data)); // NOLINT(cppcoreguidelines-owning-memory) + static_cast(global_preferences)->backends_.push_back(pref); + return 0; + } + + static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { + for (auto *backend : static_cast(global_preferences)->backends_) { + auto name = backend->get_key(); + int err = cb(name.c_str(), backend->data.data(), backend->data.size()); + ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err); + } + return 0; + } +}; + +void setup_preferences() { + auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory) + global_preferences = prefs; + prefs->open(); +} + +} // namespace zephyr + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h new file mode 100644 index 0000000000..6a37e41b46 --- /dev/null +++ b/esphome/components/zephyr/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_ZEPHYR + +namespace esphome { +namespace zephyr { + +void setup_preferences(); + +} // namespace zephyr +} // namespace esphome + +#endif diff --git a/esphome/const.py b/esphome/const.py index a30df6ef35..333e822cfa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -21,6 +21,7 @@ class Platform(StrEnum): HOST = "host" LIBRETINY_OLDSTYLE = "libretiny" LN882X = "ln882x" + NRF52 = "nrf52" RP2040 = "rp2040" RTL87XX = "rtl87xx" @@ -31,6 +32,7 @@ class Framework(StrEnum): ARDUINO = "arduino" ESP_IDF = "esp-idf" NATIVE = "host" + ZEPHYR = "zephyr" class PlatformFramework(Enum): @@ -47,6 +49,9 @@ class PlatformFramework(Enum): RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) + # Zephyr framework platforms + NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR) + # Host platform (native) HOST_NATIVE = (Platform.HOST, Framework.NATIVE) @@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266 PLATFORM_HOST = Platform.HOST PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE PLATFORM_LN882X = Platform.LN882X +PLATFORM_NRF52 = Platform.NRF52 PLATFORM_RP2040 = Platform.RP2040 PLATFORM_RTL87XX = Platform.RTL87XX diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index e33bbcf726..5ce2ed5caf 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -21,6 +21,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_HOST, PLATFORM_LN882X, + PLATFORM_NRF52, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -670,6 +671,10 @@ class EsphomeCore: def is_libretiny(self): return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x + @property + def is_nrf52(self): + return self.target_platform == PLATFORM_NRF52 + @property def is_host(self): return self.target_platform == PLATFORM_HOST @@ -686,6 +691,10 @@ class EsphomeCore: def using_esp_idf(self): return self.target_framework == "esp-idf" + @property + def using_zephyr(self): + return self.target_framework == "zephyr" + def add_job(self, func, *args, **kwargs) -> None: self.event_loop.add_job(func, *args, **kwargs) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c3b404ae60..488ea3cdb3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -678,7 +679,7 @@ class InterruptLock { ~InterruptLock(); protected: -#if defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) uint32_t state_; #endif }; diff --git a/tests/components/gpio/test.nrf52-adafruit.yaml b/tests/components/gpio/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.nrf52-adafruit.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/gpio/test.nrf52-mcumgr.yaml b/tests/components/gpio/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.nrf52-mcumgr.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/logger/test.nrf52-mcumgr.yaml b/tests/components/logger/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/test.nrf52-mcumgr.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/time/test.nrf52-adafruit.yaml b/tests/components/time/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..a5502f8028 --- /dev/null +++ b/tests/components/time/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +time: diff --git a/tests/components/time/test.nrf52-mcumgr.yaml b/tests/components/time/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..a5502f8028 --- /dev/null +++ b/tests/components/time/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +time: diff --git a/tests/components/uptime/test.nrf52-adafruit.yaml b/tests/components/uptime/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..3c3c814813 --- /dev/null +++ b/tests/components/uptime/test.nrf52-adafruit.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: uptime + name: Uptime Sensor + - platform: uptime + name: Uptime Sensor Seconds + type: seconds + +text_sensor: + - platform: uptime + name: Uptime Text diff --git a/tests/components/uptime/test.nrf52-mcumgr.yaml b/tests/components/uptime/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..3c3c814813 --- /dev/null +++ b/tests/components/uptime/test.nrf52-mcumgr.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: uptime + name: Uptime Sensor + - platform: uptime + name: Uptime Sensor Seconds + type: seconds + +text_sensor: + - platform: uptime + name: Uptime Text diff --git a/tests/test_build_components/build_components_base.nrf52-adafruit.yaml b/tests/test_build_components/build_components_base.nrf52-adafruit.yaml new file mode 100644 index 0000000000..05e3a6387c --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-adafruit.yaml @@ -0,0 +1,16 @@ +esphome: + name: componenttestnrf52 + friendly_name: $component_name + +nrf52: + board: adafruit_itsybitsy_nrf52840 + bootloader: adafruit_nrf52_sd140_v6 + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.nrf52-mcumgr.yaml b/tests/test_build_components/build_components_base.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..04211ffdfe --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-mcumgr.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestnrf52 + friendly_name: $component_name + +nrf52: + board: adafruit_feather_nrf52840 + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file