From 88cda9ff59d3ca4a8a227e7b6039597ccfa58d34 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Tue, 21 Jan 2025 17:14:16 +0200 Subject: [PATCH 001/102] feat(idf): Add initial support for IDF v5.5 and ESP32-C5 --- CMakeLists.txt | 2 +- boards.txt | 212 ++++++++++++++++++++++++++++++ cores/esp32/Esp.cpp | 3 + cores/esp32/HardwareSerial.h | 8 ++ cores/esp32/esp32-hal-cpu.c | 8 +- cores/esp32/esp32-hal-i2c-slave.c | 7 +- cores/esp32/esp32-hal-matrix.c | 2 + cores/esp32/esp32-hal-misc.c | 4 +- cores/esp32/esp32-hal-spi.c | 83 ++++++------ cores/esp32/esp32-hal-uart.c | 22 +++- idf_component.yml | 6 +- libraries/SPI/src/SPI.cpp | 2 +- platform.txt | 9 ++ variants/esp32c5/pins_arduino.h | 42 ++++++ 14 files changed, 357 insertions(+), 53 deletions(-) create mode 100644 variants/esp32c5/pins_arduino.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d2a9162a9ba..dd9544f13e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ # idf.py build set(min_supported_idf_version "5.3.0") -set(max_supported_idf_version "5.4.99") +set(max_supported_idf_version "5.5.99") set(idf_version "${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}.${IDF_VERSION_PATCH}") if ("${idf_version}" AND NOT "$ENV{ARDUINO_SKIP_IDF_VERSION_CHECK}") diff --git a/boards.txt b/boards.txt index 2afe45f50a6..ea520e60fde 100644 --- a/boards.txt +++ b/boards.txt @@ -161,6 +161,218 @@ esp32c2.menu.EraseFlash.all.upload.erase_cmd=-e ############################################################## +esp32c5.name=ESP32C5 Dev Module + +esp32c5.bootloader.tool=esptool_py +esp32c5.bootloader.tool.default=esptool_py + +esp32c5.upload.tool=esptool_py +esp32c5.upload.tool.default=esptool_py +esp32c5.upload.tool.network=esp_ota + +esp32c5.upload.maximum_size=1310720 +esp32c5.upload.maximum_data_size=327680 +esp32c5.upload.flags= +esp32c5.upload.extra_flags= +esp32c5.upload.use_1200bps_touch=false +esp32c5.upload.wait_for_upload_port=false + +esp32c5.serial.disableDTR=false +esp32c5.serial.disableRTS=false + +esp32c5.build.tarch=riscv32 +esp32c5.build.target=esp +esp32c5.build.mcu=esp32c5 +esp32c5.build.core=esp32 +esp32c5.build.variant=esp32c5 +esp32c5.build.board=ESP32C5_DEV +esp32c5.build.bootloader_addr=0x0 + +esp32c5.build.cdc_on_boot=0 +esp32c5.build.f_cpu=240000000L +esp32c5.build.flash_size=4MB +esp32c5.build.flash_freq=80m +esp32c5.build.flash_mode=qio +esp32c5.build.boot=qio +esp32c5.build.partitions=default +esp32c5.build.defines= + +## IDE 2.0 Seems to not update the value +esp32c5.menu.JTAGAdapter.default=Disabled +esp32c5.menu.JTAGAdapter.default.build.copy_jtag_files=0 +esp32c5.menu.JTAGAdapter.builtin=Integrated USB JTAG +esp32c5.menu.JTAGAdapter.builtin.build.openocdscript=esp32c5-builtin.cfg +esp32c5.menu.JTAGAdapter.builtin.build.copy_jtag_files=1 +esp32c5.menu.JTAGAdapter.external=FTDI Adapter +esp32c5.menu.JTAGAdapter.external.build.openocdscript=esp32c5-ftdi.cfg +esp32c5.menu.JTAGAdapter.external.build.copy_jtag_files=1 +esp32c5.menu.JTAGAdapter.bridge=ESP USB Bridge +esp32c5.menu.JTAGAdapter.bridge.build.openocdscript=esp32c5-bridge.cfg +esp32c5.menu.JTAGAdapter.bridge.build.copy_jtag_files=1 + +esp32c5.menu.CDCOnBoot.default=Disabled +esp32c5.menu.CDCOnBoot.default.build.cdc_on_boot=0 +esp32c5.menu.CDCOnBoot.cdc=Enabled +esp32c5.menu.CDCOnBoot.cdc.build.cdc_on_boot=1 + +esp32c5.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS) +esp32c5.menu.PartitionScheme.default.build.partitions=default +esp32c5.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS) +esp32c5.menu.PartitionScheme.defaultffat.build.partitions=default_ffat +esp32c5.menu.PartitionScheme.default_8MB=8M with spiffs (3MB APP/1.5MB SPIFFS) +esp32c5.menu.PartitionScheme.default_8MB.build.partitions=default_8MB +esp32c5.menu.PartitionScheme.default_8MB.upload.maximum_size=3342336 +esp32c5.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS) +esp32c5.menu.PartitionScheme.minimal.build.partitions=minimal +esp32c5.menu.PartitionScheme.no_fs=No FS 4MB (2MB APP x2) +esp32c5.menu.PartitionScheme.no_fs.build.partitions=no_fs +esp32c5.menu.PartitionScheme.no_fs.upload.maximum_size=2031616 +esp32c5.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS) +esp32c5.menu.PartitionScheme.no_ota.build.partitions=no_ota +esp32c5.menu.PartitionScheme.no_ota.upload.maximum_size=2097152 +esp32c5.menu.PartitionScheme.noota_3g=No OTA (1MB APP/3MB SPIFFS) +esp32c5.menu.PartitionScheme.noota_3g.build.partitions=noota_3g +esp32c5.menu.PartitionScheme.noota_3g.upload.maximum_size=1048576 +esp32c5.menu.PartitionScheme.noota_ffat=No OTA (2MB APP/2MB FATFS) +esp32c5.menu.PartitionScheme.noota_ffat.build.partitions=noota_ffat +esp32c5.menu.PartitionScheme.noota_ffat.upload.maximum_size=2097152 +esp32c5.menu.PartitionScheme.noota_3gffat=No OTA (1MB APP/3MB FATFS) +esp32c5.menu.PartitionScheme.noota_3gffat.build.partitions=noota_3gffat +esp32c5.menu.PartitionScheme.noota_3gffat.upload.maximum_size=1048576 +esp32c5.menu.PartitionScheme.huge_app=Huge APP (3MB No OTA/1MB SPIFFS) +esp32c5.menu.PartitionScheme.huge_app.build.partitions=huge_app +esp32c5.menu.PartitionScheme.huge_app.upload.maximum_size=3145728 +esp32c5.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS) +esp32c5.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs +esp32c5.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080 +esp32c5.menu.PartitionScheme.fatflash=16M Flash (2MB APP/12.5MB FATFS) +esp32c5.menu.PartitionScheme.fatflash.build.partitions=ffat +esp32c5.menu.PartitionScheme.fatflash.upload.maximum_size=2097152 +esp32c5.menu.PartitionScheme.app3M_fat9M_16MB=16M Flash (3MB APP/9.9MB FATFS) +esp32c5.menu.PartitionScheme.app3M_fat9M_16MB.build.partitions=app3M_fat9M_16MB +esp32c5.menu.PartitionScheme.app3M_fat9M_16MB.upload.maximum_size=3145728 +esp32c5.menu.PartitionScheme.rainmaker=RainMaker 4MB +esp32c5.menu.PartitionScheme.rainmaker.build.partitions=rainmaker +esp32c5.menu.PartitionScheme.rainmaker.upload.maximum_size=1966080 +esp32c5.menu.PartitionScheme.rainmaker_4MB=RainMaker 4MB No OTA +esp32c5.menu.PartitionScheme.rainmaker_4MB.build.partitions=rainmaker_4MB_no_ota +esp32c5.menu.PartitionScheme.rainmaker_4MB.upload.maximum_size=4038656 +esp32c5.menu.PartitionScheme.rainmaker_8MB=RainMaker 8MB +esp32c5.menu.PartitionScheme.rainmaker_8MB.build.partitions=rainmaker_8MB +esp32c5.menu.PartitionScheme.rainmaker_8MB.upload.maximum_size=4116480 +esp32c5.menu.PartitionScheme.zigbee_2MB=Zigbee 2MB with spiffs +esp32c5.menu.PartitionScheme.zigbee_2MB.build.partitions=zigbee_2MB +esp32c5.menu.PartitionScheme.zigbee_2MB.upload.maximum_size=1310720 +esp32c5.menu.PartitionScheme.zigbee=Zigbee 4MB with spiffs +esp32c5.menu.PartitionScheme.zigbee.build.partitions=zigbee +esp32c5.menu.PartitionScheme.zigbee.upload.maximum_size=1310720 +esp32c5.menu.PartitionScheme.zigbee_8MB=Zigbee 8MB with spiffs +esp32c5.menu.PartitionScheme.zigbee_8MB.build.partitions=zigbee_8MB +esp32c5.menu.PartitionScheme.zigbee_8MB.upload.maximum_size=3407872 +esp32c5.menu.PartitionScheme.zigbee_zczr_2MB=Zigbee ZCZR 2MB with spiffs +esp32c5.menu.PartitionScheme.zigbee_zczr_2MB.build.partitions=zigbee_zczr_2MB +esp32c5.menu.PartitionScheme.zigbee_zczr_2MB.upload.maximum_size=1310720 +esp32c5.menu.PartitionScheme.zigbee_zczr=Zigbee ZCZR 4MB with spiffs +esp32c5.menu.PartitionScheme.zigbee_zczr.build.partitions=zigbee_zczr +esp32c5.menu.PartitionScheme.zigbee_zczr.upload.maximum_size=1310720 +esp32c5.menu.PartitionScheme.zigbee_zczr_8MB=Zigbee ZCZR 8MB with spiffs +esp32c5.menu.PartitionScheme.zigbee_zczr_8MB.build.partitions=zigbee_zczr_8MB +esp32c5.menu.PartitionScheme.zigbee_zczr_8MB.upload.maximum_size=3407872 +esp32c5.menu.PartitionScheme.custom=Custom +esp32c5.menu.PartitionScheme.custom.build.partitions= +esp32c5.menu.PartitionScheme.custom.upload.maximum_size=16777216 + +esp32c5.menu.CPUFreq.240=240MHz (WiFi) +esp32c5.menu.CPUFreq.240.build.f_cpu=240000000L +esp32c5.menu.CPUFreq.120=120MHz (WiFi) +esp32c5.menu.CPUFreq.120.build.f_cpu=120000000L +esp32c5.menu.CPUFreq.80=80MHz (WiFi) +esp32c5.menu.CPUFreq.80.build.f_cpu=80000000L +esp32c5.menu.CPUFreq.40=40MHz +esp32c5.menu.CPUFreq.40.build.f_cpu=40000000L +esp32c5.menu.CPUFreq.20=20MHz +esp32c5.menu.CPUFreq.20.build.f_cpu=20000000L +esp32c5.menu.CPUFreq.10=10MHz +esp32c5.menu.CPUFreq.10.build.f_cpu=10000000L + +esp32c5.menu.FlashMode.qio=QIO +esp32c5.menu.FlashMode.qio.build.flash_mode=dio +esp32c5.menu.FlashMode.qio.build.boot=qio +esp32c5.menu.FlashMode.dio=DIO +esp32c5.menu.FlashMode.dio.build.flash_mode=dio +esp32c5.menu.FlashMode.dio.build.boot=dio + +esp32c5.menu.FlashFreq.80=80MHz +esp32c5.menu.FlashFreq.80.build.flash_freq=80m +esp32c5.menu.FlashFreq.40=40MHz +esp32c5.menu.FlashFreq.40.build.flash_freq=40m + +esp32c5.menu.FlashSize.4M=4MB (32Mb) +esp32c5.menu.FlashSize.4M.build.flash_size=4MB +esp32c5.menu.FlashSize.8M=8MB (64Mb) +esp32c5.menu.FlashSize.8M.build.flash_size=8MB +esp32c5.menu.FlashSize.2M=2MB (16Mb) +esp32c5.menu.FlashSize.2M.build.flash_size=2MB +esp32c5.menu.FlashSize.16M=16MB (128Mb) +esp32c5.menu.FlashSize.16M.build.flash_size=16MB + +esp32c5.menu.UploadSpeed.921600=921600 +esp32c5.menu.UploadSpeed.921600.upload.speed=921600 +esp32c5.menu.UploadSpeed.115200=115200 +esp32c5.menu.UploadSpeed.115200.upload.speed=115200 +esp32c5.menu.UploadSpeed.256000.windows=256000 +esp32c5.menu.UploadSpeed.256000.upload.speed=256000 +esp32c5.menu.UploadSpeed.230400.windows.upload.speed=256000 +esp32c5.menu.UploadSpeed.230400=230400 +esp32c5.menu.UploadSpeed.230400.upload.speed=230400 +esp32c5.menu.UploadSpeed.460800.linux=460800 +esp32c5.menu.UploadSpeed.460800.macosx=460800 +esp32c5.menu.UploadSpeed.460800.upload.speed=460800 +esp32c5.menu.UploadSpeed.512000.windows=512000 +esp32c5.menu.UploadSpeed.512000.upload.speed=512000 + +esp32c5.menu.DebugLevel.none=None +esp32c5.menu.DebugLevel.none.build.code_debug=0 +esp32c5.menu.DebugLevel.error=Error +esp32c5.menu.DebugLevel.error.build.code_debug=1 +esp32c5.menu.DebugLevel.warn=Warn +esp32c5.menu.DebugLevel.warn.build.code_debug=2 +esp32c5.menu.DebugLevel.info=Info +esp32c5.menu.DebugLevel.info.build.code_debug=3 +esp32c5.menu.DebugLevel.debug=Debug +esp32c5.menu.DebugLevel.debug.build.code_debug=4 +esp32c5.menu.DebugLevel.verbose=Verbose +esp32c5.menu.DebugLevel.verbose.build.code_debug=5 + +esp32c5.menu.EraseFlash.none=Disabled +esp32c5.menu.EraseFlash.none.upload.erase_cmd= +esp32c5.menu.EraseFlash.all=Enabled +esp32c5.menu.EraseFlash.all.upload.erase_cmd=-e + +esp32c5.menu.ZigbeeMode.default=Disabled +esp32c5.menu.ZigbeeMode.default.build.zigbee_mode= +esp32c5.menu.ZigbeeMode.default.build.zigbee_libs= +esp32c5.menu.ZigbeeMode.ed=Zigbee ED (end device) +esp32c5.menu.ZigbeeMode.ed.build.zigbee_mode=-DZIGBEE_MODE_ED +esp32c5.menu.ZigbeeMode.ed.build.zigbee_libs=-lesp_zb_api_ed -lesp_zb_cli_command -lzboss_stack.ed -lzboss_port +esp32c5.menu.ZigbeeMode.zczr=Zigbee ZCZR (coordinator/router) +esp32c5.menu.ZigbeeMode.zczr.build.zigbee_mode=-DZIGBEE_MODE_ZCZR +esp32c5.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api_zczr -lesp_zb_cli_command -lzboss_stack.zczr -lzboss_port +esp32c5.menu.ZigbeeMode.rcp=Zigbee RCP (radio co-processor) +esp32c5.menu.ZigbeeMode.rcp.build.zigbee_mode=-DZIGBEE_MODE_RCP +esp32c5.menu.ZigbeeMode.rcp.build.zigbee_libs=-lesp_zb_api_rcp -lesp_zb_cli_command -lzboss_stack.rcp -lzboss_port +esp32c5.menu.ZigbeeMode.ed_debug=Zigbee ED (end device) - Debug +esp32c5.menu.ZigbeeMode.ed_debug.build.zigbee_mode=-DZIGBEE_MODE_ED +esp32c5.menu.ZigbeeMode.ed_debug.build.zigbee_libs=-lesp_zb_api_ed.debug -lesp_zb_cli_command -lzboss_stack.ed.debug -lzboss_port.debug +esp32c5.menu.ZigbeeMode.zczr_debug=Zigbee ZCZR (coordinator/router) - Debug +esp32c5.menu.ZigbeeMode.zczr_debug.build.zigbee_mode=-DZIGBEE_MODE_ZCZR +esp32c5.menu.ZigbeeMode.zczr_debug.build.zigbee_libs=-lesp_zb_api_zczr.debug -lesp_zb_cli_command -lzboss_stack.zczr.debug -lzboss_port.debug +esp32c5.menu.ZigbeeMode.rcp_debug=Zigbee RCP (radio co-processor) - Debug +esp32c5.menu.ZigbeeMode.rcp_debug.build.zigbee_mode=-DZIGBEE_MODE_RCP +esp32c5.menu.ZigbeeMode.rcp_debug.build.zigbee_libs=-lesp_zb_api_rcp.debug -lesp_zb_cli_command -lzboss_stack.rcp.debug -lzboss_port.debug + +############################################################## + esp32p4.name=ESP32P4 Dev Module esp32p4.bootloader.tool=esptool_py diff --git a/cores/esp32/Esp.cpp b/cores/esp32/Esp.cpp index 9f90a828b25..3cb0e7351c6 100644 --- a/cores/esp32/Esp.cpp +++ b/cores/esp32/Esp.cpp @@ -63,6 +63,9 @@ extern "C" { #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/spi_flash.h" #define ESP_FLASH_IMAGE_BASE 0x2000 // Esp32p4 is located at 0x2000 +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/spi_flash.h" +#define ESP_FLASH_IMAGE_BASE 0x0000 // Esp32c5 is located at 0x0000 #else #error Target CONFIG_IDF_TARGET is not supported #endif diff --git a/cores/esp32/HardwareSerial.h b/cores/esp32/HardwareSerial.h index b1f6df17724..775b6ee2bcb 100644 --- a/cores/esp32/HardwareSerial.h +++ b/cores/esp32/HardwareSerial.h @@ -139,6 +139,8 @@ typedef enum { #define SOC_RX0 (gpio_num_t)23 #elif CONFIG_IDF_TARGET_ESP32P4 #define SOC_RX0 (gpio_num_t)38 +#elif CONFIG_IDF_TARGET_ESP32C5 +#define SOC_RX0 (gpio_num_t)12 #endif #endif @@ -157,6 +159,8 @@ typedef enum { #define SOC_TX0 (gpio_num_t)24 #elif CONFIG_IDF_TARGET_ESP32P4 #define SOC_TX0 (gpio_num_t)37 +#elif CONFIG_IDF_TARGET_ESP32C5 +#define SOC_TX0 (gpio_num_t)11 #endif #endif @@ -180,6 +184,8 @@ typedef enum { #define RX1 (gpio_num_t)0 #elif CONFIG_IDF_TARGET_ESP32P4 #define RX1 (gpio_num_t)11 +#elif CONFIG_IDF_TARGET_ESP32C5 +#define RX1 (gpio_num_t)4 #endif #endif @@ -200,6 +206,8 @@ typedef enum { #define TX1 (gpio_num_t)1 #elif CONFIG_IDF_TARGET_ESP32P4 #define TX1 (gpio_num_t)10 +#elif CONFIG_IDF_TARGET_ESP32C5 +#define TX1 (gpio_num_t)5 #endif #endif #endif /* SOC_UART_HP_NUM > 1 */ diff --git a/cores/esp32/esp32-hal-cpu.c b/cores/esp32/esp32-hal-cpu.c index 1ffde860792..8c13a4077fd 100644 --- a/cores/esp32/esp32-hal-cpu.c +++ b/cores/esp32/esp32-hal-cpu.c @@ -19,7 +19,7 @@ #include "esp_attr.h" #include "esp_log.h" #include "soc/rtc.h" -#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) +#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5) #include "soc/rtc_cntl_reg.h" #include "soc/syscon_reg.h" #endif @@ -48,6 +48,8 @@ #include "esp32h2/rom/rtc.h" #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/rtc.h" #else #error Target CONFIG_IDF_TARGET is not supported #endif @@ -179,7 +181,7 @@ bool setCpuFrequencyMhz(uint32_t cpu_freq_mhz) { rtc_cpu_freq_config_t conf, cconf; uint32_t capb, apb; //Get XTAL Frequency and calculate min CPU MHz -#if (!defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4)) +#if (!defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5)) rtc_xtal_freq_t xtal = rtc_clk_xtal_freq_get(); #endif #if CONFIG_IDF_TARGET_ESP32 @@ -195,7 +197,7 @@ bool setCpuFrequencyMhz(uint32_t cpu_freq_mhz) { } } #endif -#if (!defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4)) +#if (!defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5)) if (cpu_freq_mhz > xtal && cpu_freq_mhz != 240 && cpu_freq_mhz != 160 && cpu_freq_mhz != 120 && cpu_freq_mhz != 80) { if (xtal >= RTC_XTAL_FREQ_40M) { log_e("Bad frequency: %u MHz! Options are: 240, 160, 120, 80, %u, %u and %u MHz", cpu_freq_mhz, xtal, xtal / 2, xtal / 4); diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index 46c3a4d58c2..0e01259da61 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -43,7 +43,9 @@ #include "soc/i2c_struct.h" #include "soc/periph_defs.h" #include "hal/i2c_ll.h" +#ifndef CONFIG_IDF_TARGET_ESP32C5 #include "hal/clk_gate_ll.h" +#endif #include "esp32-hal-log.h" #include "esp32-hal-i2c-slave.h" #include "esp32-hal-periman.h" @@ -325,7 +327,7 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t frequency = 100000L; } frequency = (frequency * 5) / 4; -#if !defined(CONFIG_IDF_TARGET_ESP32P4) +#if !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5) if (i2c->num == 0) { periph_ll_enable_clk_clear_rst(PERIPH_I2C0_MODULE); #if SOC_HP_I2C_NUM > 1 @@ -556,6 +558,9 @@ static bool i2c_slave_set_frequency(i2c_slave_struct_t *i2c, uint32_t clk_speed) i2c_ll_set_source_clk(i2c->dev, SOC_MOD_CLK_APB); /*!< I2C source clock from APB, 80M*/ } #elif SOC_I2C_SUPPORT_XTAL +#ifndef XTAL_CLK_FREQ +#define XTAL_CLK_FREQ APB_CLK_FREQ +#endif i2c_ll_master_cal_bus_clk(XTAL_CLK_FREQ, clk_speed, &clk_cal); I2C_CLOCK_SRC_ATOMIC() { i2c_ll_set_source_clk(i2c->dev, SOC_MOD_CLK_XTAL); /*!< I2C source clock from XTAL, 40M */ diff --git a/cores/esp32/esp32-hal-matrix.c b/cores/esp32/esp32-hal-matrix.c index 7cddb4e04db..0d81e979f2b 100644 --- a/cores/esp32/esp32-hal-matrix.c +++ b/cores/esp32/esp32-hal-matrix.c @@ -34,6 +34,8 @@ #include "esp32h2/rom/gpio.h" #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/gpio.h" +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/gpio.h" #else #error Target CONFIG_IDF_TARGET is not supported #endif diff --git a/cores/esp32/esp32-hal-misc.c b/cores/esp32/esp32-hal-misc.c index 50e2973d27a..39f81d50ae5 100644 --- a/cores/esp32/esp32-hal-misc.c +++ b/cores/esp32/esp32-hal-misc.c @@ -30,7 +30,7 @@ #endif //CONFIG_BT_ENABLED #include #include "soc/rtc.h" -#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) +#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5) #include "soc/rtc_cntl_reg.h" #include "soc/syscon_reg.h" #endif @@ -56,6 +56,8 @@ #include "esp32h2/rom/rtc.h" #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/rtc.h" #else #error Target CONFIG_IDF_TARGET is not supported diff --git a/cores/esp32/esp32-hal-spi.c b/cores/esp32/esp32-hal-spi.c index 80928309670..4cff353d76c 100644 --- a/cores/esp32/esp32-hal-spi.c +++ b/cores/esp32/esp32-hal-spi.c @@ -26,7 +26,9 @@ #include "soc/io_mux_reg.h" #include "soc/gpio_sig_map.h" #include "soc/rtc.h" +#ifndef CONFIG_IDF_TARGET_ESP32C5 #include "hal/clk_gate_ll.h" +#endif #include "esp32-hal-periman.h" #include "esp_private/periph_ctrl.h" @@ -60,6 +62,9 @@ #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/ets_sys.h" #include "esp32p4/rom/gpio.h" +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/ets_sys.h" +#include "esp32c5/rom/gpio.h" #else #error Target CONFIG_IDF_TARGET is not supported #endif @@ -119,7 +124,7 @@ struct spi_struct_t { #define SPI_SS_IDX(p, n) ((p == 0) ? SPI_FSPI_SS_IDX(n) : ((p == 1) ? SPI_HSPI_SS_IDX(n) : 0)) -#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 +#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32C5 // ESP32C3 #define SPI_COUNT (1) @@ -161,7 +166,7 @@ static spi_t _spi_bus_array[] = { {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), 0, -1, -1, -1, -1} #elif CONFIG_IDF_TARGET_ESP32C3 {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), 0, -1, -1, -1, -1} -#elif CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 +#elif CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32C5 {(spi_dev_t *)(DR_REG_SPI2_BASE), 0, -1, -1, -1, -1} #else {(volatile spi_dev_t *)(DR_REG_SPI0_BASE), 0, -1, -1, -1, -1}, @@ -188,7 +193,7 @@ static spi_t _spi_bus_array[] = { {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), NULL, 0, -1, -1, -1, -1} #elif CONFIG_IDF_TARGET_ESP32C3 {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), NULL, 0, -1, -1, -1, -1} -#elif CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 +#elif CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32C5 {(spi_dev_t *)(DR_REG_SPI2_BASE), NULL, 0, -1, -1, -1, -1} #else {(volatile spi_dev_t *)(DR_REG_SPI0_BASE), NULL, 0, -1, -1, -1, -1}, @@ -685,7 +690,7 @@ spi_t *spiStartBus(uint8_t spi_num, uint32_t clockDiv, uint8_t dataMode, uint8_t spi->dev->user.doutdin = 1; int i; for (i = 0; i < 16; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = 0x00000000; #else spi->dev->data_buf[i] = 0x00000000; @@ -733,7 +738,7 @@ void spiWrite(spi_t *spi, const uint32_t *data, uint8_t len) { spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif for (i = 0; i < len; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = data[i]; #else spi->dev->data_buf[i] = data[i]; @@ -760,7 +765,7 @@ void spiTransfer(spi_t *spi, uint32_t *data, uint8_t len) { spi->dev->mosi_dlen.usr_mosi_dbitlen = (len * 32) - 1; spi->dev->miso_dlen.usr_miso_dbitlen = (len * 32) - 1; for (i = 0; i < len; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = data[i]; #else spi->dev->data_buf[i] = data[i]; @@ -773,7 +778,7 @@ void spiTransfer(spi_t *spi, uint32_t *data, uint8_t len) { spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); for (i = 0; i < len; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data[i] = spi->dev->data_buf[i].val; #else data[i] = spi->dev->data_buf[i]; @@ -791,7 +796,7 @@ void spiWriteByte(spi_t *spi, uint8_t data) { #if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32 spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -813,7 +818,7 @@ uint8_t spiTransferByte(spi_t *spi, uint8_t data) { SPI_MUTEX_LOCK(); spi->dev->mosi_dlen.usr_mosi_dbitlen = 7; spi->dev->miso_dlen.usr_miso_dbitlen = 7; -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -824,7 +829,7 @@ uint8_t spiTransferByte(spi_t *spi, uint8_t data) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val & 0xFF; #else data = spi->dev->data_buf[0] & 0xFF; @@ -854,7 +859,7 @@ void spiWriteWord(spi_t *spi, uint16_t data) { #if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32 spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -878,7 +883,7 @@ uint16_t spiTransferWord(spi_t *spi, uint16_t data) { SPI_MUTEX_LOCK(); spi->dev->mosi_dlen.usr_mosi_dbitlen = 15; spi->dev->miso_dlen.usr_miso_dbitlen = 15; -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -889,7 +894,7 @@ uint16_t spiTransferWord(spi_t *spi, uint16_t data) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val; #else data = spi->dev->data_buf[0]; @@ -913,7 +918,7 @@ void spiWriteLong(spi_t *spi, uint32_t data) { #if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32 spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -937,7 +942,7 @@ uint32_t spiTransferLong(spi_t *spi, uint32_t data) { SPI_MUTEX_LOCK(); spi->dev->mosi_dlen.usr_mosi_dbitlen = 31; spi->dev->miso_dlen.usr_miso_dbitlen = 31; -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -948,7 +953,7 @@ uint32_t spiTransferLong(spi_t *spi, uint32_t data) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val; #else data = spi->dev->data_buf[0]; @@ -987,7 +992,7 @@ static void __spiTransferBytes(spi_t *spi, const uint8_t *data, uint8_t *out, ui spi->dev->miso_dlen.usr_miso_dbitlen = ((bytes * 8) - 1); for (i = 0; i < words; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = wordsBuf[i]; //copy buffer to spi fifo #else spi->dev->data_buf[i] = wordsBuf[i]; //copy buffer to spi fifo @@ -1004,7 +1009,7 @@ static void __spiTransferBytes(spi_t *spi, const uint8_t *data, uint8_t *out, ui if (out) { for (i = 0; i < words; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 wordsBuf[i] = spi->dev->data_buf[i].val; //copy spi fifo to buffer #else wordsBuf[i] = spi->dev->data_buf[i]; //copy spi fifo to buffer @@ -1145,7 +1150,7 @@ void ARDUINO_ISR_ATTR spiWriteByteNL(spi_t *spi, uint8_t data) { #if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32 spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1164,7 +1169,7 @@ uint8_t spiTransferByteNL(spi_t *spi, uint8_t data) { } spi->dev->mosi_dlen.usr_mosi_dbitlen = 7; spi->dev->miso_dlen.usr_miso_dbitlen = 7; -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1175,7 +1180,7 @@ uint8_t spiTransferByteNL(spi_t *spi, uint8_t data) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val & 0xFF; #else data = spi->dev->data_buf[0] & 0xFF; @@ -1194,7 +1199,7 @@ void ARDUINO_ISR_ATTR spiWriteShortNL(spi_t *spi, uint16_t data) { #if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32 spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1216,7 +1221,7 @@ uint16_t spiTransferShortNL(spi_t *spi, uint16_t data) { } spi->dev->mosi_dlen.usr_mosi_dbitlen = 15; spi->dev->miso_dlen.usr_miso_dbitlen = 15; -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1227,7 +1232,7 @@ uint16_t spiTransferShortNL(spi_t *spi, uint16_t data) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val & 0xFFFF; #else data = spi->dev->data_buf[0] & 0xFFFF; @@ -1249,7 +1254,7 @@ void ARDUINO_ISR_ATTR spiWriteLongNL(spi_t *spi, uint32_t data) { #if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32 spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1271,7 +1276,7 @@ uint32_t spiTransferLongNL(spi_t *spi, uint32_t data) { } spi->dev->mosi_dlen.usr_mosi_dbitlen = 31; spi->dev->miso_dlen.usr_miso_dbitlen = 31; -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1282,7 +1287,7 @@ uint32_t spiTransferLongNL(spi_t *spi, uint32_t data) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val; #else data = spi->dev->data_buf[0]; @@ -1313,7 +1318,7 @@ void spiWriteNL(spi_t *spi, const void *data_in, uint32_t len) { spi->dev->miso_dlen.usr_miso_dbitlen = 0; #endif for (size_t i = 0; i < c_longs; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = data[i]; #else spi->dev->data_buf[i] = data[i]; @@ -1352,7 +1357,7 @@ void spiTransferBytesNL(spi_t *spi, const void *data_in, uint8_t *data_out, uint spi->dev->miso_dlen.usr_miso_dbitlen = (c_len * 8) - 1; if (data) { for (size_t i = 0; i < c_longs; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = data[i]; #else spi->dev->data_buf[i] = data[i]; @@ -1360,7 +1365,7 @@ void spiTransferBytesNL(spi_t *spi, const void *data_in, uint8_t *data_out, uint } } else { for (size_t i = 0; i < c_longs; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = 0xFFFFFFFF; #else spi->dev->data_buf[i] = 0xFFFFFFFF; @@ -1376,13 +1381,13 @@ void spiTransferBytesNL(spi_t *spi, const void *data_in, uint8_t *data_out, uint if (result) { if (c_len & 3) { for (size_t i = 0; i < (c_longs - 1); i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 result[i] = spi->dev->data_buf[i].val; #else result[i] = spi->dev->data_buf[i]; #endif } -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 uint32_t last_data = spi->dev->data_buf[c_longs - 1].val; #else uint32_t last_data = spi->dev->data_buf[c_longs - 1]; @@ -1394,7 +1399,7 @@ void spiTransferBytesNL(spi_t *spi, const void *data_in, uint8_t *data_out, uint } } else { for (size_t i = 0; i < c_longs; i++) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 result[i] = spi->dev->data_buf[i].val; #else result[i] = spi->dev->data_buf[i]; @@ -1436,7 +1441,7 @@ void spiTransferBitsNL(spi_t *spi, uint32_t data, uint32_t *out, uint8_t bits) { spi->dev->mosi_dlen.usr_mosi_dbitlen = (bits - 1); spi->dev->miso_dlen.usr_miso_dbitlen = (bits - 1); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[0].val = data; #else spi->dev->data_buf[0] = data; @@ -1447,7 +1452,7 @@ void spiTransferBitsNL(spi_t *spi, uint32_t data, uint32_t *out, uint8_t bits) { #endif spi->dev->cmd.usr = 1; while (spi->dev->cmd.usr); -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 data = spi->dev->data_buf[0].val; #else data = spi->dev->data_buf[0]; @@ -1488,27 +1493,27 @@ void ARDUINO_ISR_ATTR spiWritePixelsNL(spi_t *spi, const void *data_in, uint32_t if (msb) { if (l_bytes && i == (c_longs - 1)) { if (l_bytes == 2) { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 MSB_16_SET(spi->dev->data_buf[i].val, data[i]); #else MSB_16_SET(spi->dev->data_buf[i], data[i]); #endif } else { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = data[i] & 0xFF; #else spi->dev->data_buf[i] = data[i] & 0xFF; #endif } } else { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 MSB_PIX_SET(spi->dev->data_buf[i].val, data[i]); #else MSB_PIX_SET(spi->dev->data_buf[i], data[i]); #endif } } else { -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 spi->dev->data_buf[i].val = data[i]; #else spi->dev->data_buf[i] = data[i]; diff --git a/cores/esp32/esp32-hal-uart.c b/cores/esp32/esp32-hal-uart.c index 75e2da013ea..a928a8ede3a 100644 --- a/cores/esp32/esp32-hal-uart.c +++ b/cores/esp32/esp32-hal-uart.c @@ -206,6 +206,14 @@ static bool lpuartCheckPins(int8_t rxPin, int8_t txPin, int8_t ctsPin, int8_t rt } #endif // SOC_UART_LP_NUM >= 1 +#ifndef GPIO_FUNC_IN_LOW +#define GPIO_FUNC_IN_LOW GPIO_MATRIX_CONST_ZERO_INPUT +#endif + +#ifndef GPIO_FUNC_IN_HIGH +#define GPIO_FUNC_IN_HIGH GPIO_MATRIX_CONST_ONE_INPUT +#endif + // Negative Pin Number will keep it unmodified, thus this function can detach individual pins // This function will also unset the pins in the Peripheral Manager and set the pin to -1 after detaching static bool _uartDetachPins(uint8_t uart_num, int8_t rxPin, int8_t txPin, int8_t ctsPin, int8_t rtsPin) { @@ -221,7 +229,8 @@ static bool _uartDetachPins(uint8_t uart_num, int8_t rxPin, int8_t txPin, int8_t // detaches HP and LP pins and sets Peripheral Manager and UART information if (rxPin >= 0 && uart->_rxPin == rxPin && perimanGetPinBusType(rxPin) == ESP32_BUS_TYPE_UART_RX) { - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[rxPin], PIN_FUNC_GPIO); + //gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[rxPin], PIN_FUNC_GPIO); + esp_rom_gpio_pad_select_gpio(rxPin); // avoids causing BREAK in the UART line if (uart->_inverted) { esp_rom_gpio_connect_in_signal(GPIO_FUNC_IN_LOW, UART_PERIPH_SIGNAL(uart_num, SOC_UART_RX_PIN_IDX), false); @@ -235,7 +244,8 @@ static bool _uartDetachPins(uint8_t uart_num, int8_t rxPin, int8_t txPin, int8_t } } if (txPin >= 0 && uart->_txPin == txPin && perimanGetPinBusType(txPin) == ESP32_BUS_TYPE_UART_TX) { - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[txPin], PIN_FUNC_GPIO); + //gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[txPin], PIN_FUNC_GPIO); + esp_rom_gpio_pad_select_gpio(txPin); esp_rom_gpio_connect_out_signal(txPin, SIG_GPIO_OUT_IDX, false, false); uart->_txPin = -1; // -1 means unassigned/detached if (!perimanClearPinBus(txPin)) { @@ -244,7 +254,8 @@ static bool _uartDetachPins(uint8_t uart_num, int8_t rxPin, int8_t txPin, int8_t } } if (ctsPin >= 0 && uart->_ctsPin == ctsPin && perimanGetPinBusType(ctsPin) == ESP32_BUS_TYPE_UART_CTS) { - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[ctsPin], PIN_FUNC_GPIO); + //gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[ctsPin], PIN_FUNC_GPIO); + esp_rom_gpio_pad_select_gpio(ctsPin); esp_rom_gpio_connect_in_signal(GPIO_FUNC_IN_LOW, UART_PERIPH_SIGNAL(uart_num, SOC_UART_CTS_PIN_IDX), false); uart->_ctsPin = -1; // -1 means unassigned/detached if (!perimanClearPinBus(ctsPin)) { @@ -253,7 +264,8 @@ static bool _uartDetachPins(uint8_t uart_num, int8_t rxPin, int8_t txPin, int8_t } } if (rtsPin >= 0 && uart->_rtsPin == rtsPin && perimanGetPinBusType(rtsPin) == ESP32_BUS_TYPE_UART_RTS) { - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[rtsPin], PIN_FUNC_GPIO); + //gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[rtsPin], PIN_FUNC_GPIO); + esp_rom_gpio_pad_select_gpio(rtsPin); esp_rom_gpio_connect_out_signal(rtsPin, SIG_GPIO_OUT_IDX, false, false); uart->_rtsPin = -1; // -1 means unassigned/detached if (!perimanClearPinBus(rtsPin)) { @@ -808,7 +820,7 @@ void uartSetRxInvert(uart_t *uart, bool invert) { if (uart == NULL) { return; } -#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 // POTENTIAL ISSUE :: original code only set/reset rxd_inv bit // IDF or LL set/reset the whole inv_mask! // if (invert) diff --git a/idf_component.yml b/idf_component.yml index 967c4ecf0f6..c1fc614cae5 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -10,6 +10,7 @@ targets: - esp32c6 - esp32h2 - esp32p4 + - esp32c5 tags: - arduino files: @@ -22,6 +23,7 @@ files: - "variants/esp32c6/**/*" - "variants/esp32h2/**/*" - "variants/esp32p4/**/*" + - "variants/esp32c5/**/*" exclude: - "docs/" - "docs/**/*" @@ -44,7 +46,7 @@ files: - "platform.txt" - "programmers.txt" dependencies: - idf: ">=5.3,<5.5" + idf: ">=5.3,<5.6" # mdns 1.2.1 is necessary to build H2 with no WiFi espressif/mdns: version: "^1.2.3" @@ -107,7 +109,7 @@ dependencies: rules: - if: "target == esp32p4" espressif/esp_wifi_remote: - version: "^0.4.1" + version: "^0.5.4" rules: - if: "target == esp32p4" espressif/libsodium: diff --git a/libraries/SPI/src/SPI.cpp b/libraries/SPI/src/SPI.cpp index 35e52f43e4d..732128809c9 100644 --- a/libraries/SPI/src/SPI.cpp +++ b/libraries/SPI/src/SPI.cpp @@ -83,7 +83,7 @@ void SPIClass::begin(int8_t sck, int8_t miso, int8_t mosi, int8_t ss) { _miso = (_spi_num == FSPI) ? MISO : -1; _mosi = (_spi_num == FSPI) ? MOSI : -1; _ss = (_spi_num == FSPI) ? SS : -1; -#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 _sck = SCK; _miso = MISO; _mosi = MOSI; diff --git a/platform.txt b/platform.txt index 65be05b3bf4..f41a8b4a764 100644 --- a/platform.txt +++ b/platform.txt @@ -84,6 +84,7 @@ build.extra_flags.esp32c3=-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT={build. build.extra_flags.esp32c6=-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT={build.cdc_on_boot} build.extra_flags.esp32h2=-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT={build.cdc_on_boot} build.extra_flags.esp32p4=-DARDUINO_USB_MODE={build.usb_mode} -DARDUINO_USB_CDC_ON_BOOT={build.cdc_on_boot} -DARDUINO_USB_MSC_ON_BOOT={build.msc_on_boot} -DARDUINO_USB_DFU_ON_BOOT={build.dfu_on_boot} +build.extra_flags.esp32c5=-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT={build.cdc_on_boot} # This can be overriden in boards.txt build.zigbee_mode= @@ -260,6 +261,14 @@ debug_config.esp32c6= debug_script.esp32h2=esp32h2-builtin.cfg debug_config.esp32h2= +# ESP32-P4 debug configuration (TBD) +debug_script.esp32p4=esp32p4-builtin.cfg +debug_config.esp32p4= + +# ESP32-C5 debug configuration (TBD) +debug_script.esp32c5=esp32c5-builtin.cfg +debug_config.esp32c5= + # Debug API variable definitions debug.executable={build.path}/{build.project_name}.elf debug.toolchain=gcc diff --git a/variants/esp32c5/pins_arduino.h b/variants/esp32c5/pins_arduino.h new file mode 100644 index 00000000000..eeaf3e01ebb --- /dev/null +++ b/variants/esp32c5/pins_arduino.h @@ -0,0 +1,42 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include "soc/soc_caps.h" + +#define PIN_RGB_LED 8 +// BUILTIN_LED can be used in new Arduino API digitalWrite() like in Blink.ino +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + PIN_RGB_LED; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN +// RGB_BUILTIN and RGB_BRIGHTNESS can be used in new Arduino API rgbLedWrite() +#define RGB_BUILTIN LED_BUILTIN +#define RGB_BRIGHTNESS 64 + +static const uint8_t TX = 11; +static const uint8_t RX = 12; + +// static const uint8_t USB_DM = 13; +// static const uint8_t USB_DP = 14; + +static const uint8_t SDA = 23; +static const uint8_t SCL = 22; + +static const uint8_t SS = 18; +static const uint8_t MOSI = 19; +static const uint8_t MISO = 20; +static const uint8_t SCK = 21; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; + +// LP I2C Pins are fixed on ESP32-C6 +#define WIRE1_PIN_DEFINED +static const uint8_t SDA1 = 2; +static const uint8_t SCL1 = 3; + +#endif /* Pins_Arduino_h */ From bf90cbd18316603a717fa08e2ba4dfcb85541236 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Wed, 22 Jan 2025 01:34:44 +0200 Subject: [PATCH 002/102] fix(examples): Add changes required to some examples --- libraries/ESP32/examples/DeepSleep/TouchWakeUp/TouchWakeUp.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ESP32/examples/DeepSleep/TouchWakeUp/TouchWakeUp.ino b/libraries/ESP32/examples/DeepSleep/TouchWakeUp/TouchWakeUp.ino index 9d2b248ba44..5b1e0e9e115 100644 --- a/libraries/ESP32/examples/DeepSleep/TouchWakeUp/TouchWakeUp.ino +++ b/libraries/ESP32/examples/DeepSleep/TouchWakeUp/TouchWakeUp.ino @@ -48,7 +48,7 @@ Method to print the touchpad by which ESP32 has been awaken from sleep */ void print_wakeup_touchpad() { - touchPin = esp_sleep_get_touchpad_wakeup_status(); + touchPin = (touch_pad_t)esp_sleep_get_touchpad_wakeup_status(); #if CONFIG_IDF_TARGET_ESP32 switch (touchPin) { From af5abd5f6196ab4868b229f1e45f662961e531c7 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Wed, 22 Jan 2025 12:19:42 +0200 Subject: [PATCH 003/102] fix(c5): Update bootloader location --- boards.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boards.txt b/boards.txt index ea520e60fde..af3e217df9d 100644 --- a/boards.txt +++ b/boards.txt @@ -186,7 +186,7 @@ esp32c5.build.mcu=esp32c5 esp32c5.build.core=esp32 esp32c5.build.variant=esp32c5 esp32c5.build.board=ESP32C5_DEV -esp32c5.build.bootloader_addr=0x0 +esp32c5.build.bootloader_addr=0x2000 esp32c5.build.cdc_on_boot=0 esp32c5.build.f_cpu=240000000L From 0894d7db68562d4690c5b201df7f0c333c776bc1 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Wed, 22 Jan 2025 12:31:54 +0200 Subject: [PATCH 004/102] fix(report): Add missing chip names --- cores/esp32/Esp.cpp | 6 ++++++ cores/esp32/chip-debug-report.cpp | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/cores/esp32/Esp.cpp b/cores/esp32/Esp.cpp index 3cb0e7351c6..2b2e1280683 100644 --- a/cores/esp32/Esp.cpp +++ b/cores/esp32/Esp.cpp @@ -21,6 +21,7 @@ #include "Esp.h" #include "esp_sleep.h" #include "spi_flash_mmap.h" +#include "esp_idf_version.h" #include #include #include @@ -304,6 +305,11 @@ const char *EspClass::getChipModel(void) { case CHIP_ESP32C6: return "ESP32-C6"; case CHIP_ESP32H2: return "ESP32-H2"; case CHIP_ESP32P4: return "ESP32-P4"; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + case CHIP_ESP32C5: return "ESP32-C5"; + case CHIP_ESP32C61: return "ESP32-C61"; + case CHIP_ESP32H21: return "ESP32-H21"; +#endif default: return "UNKNOWN"; } #endif diff --git a/cores/esp32/chip-debug-report.cpp b/cores/esp32/chip-debug-report.cpp index daafef3cab9..e3f3f431f81 100644 --- a/cores/esp32/chip-debug-report.cpp +++ b/cores/esp32/chip-debug-report.cpp @@ -88,6 +88,11 @@ static void printChipInfo(void) { case CHIP_ESP32C6: chip_report_printf("ESP32-C6\n"); break; case CHIP_ESP32H2: chip_report_printf("ESP32-H2\n"); break; case CHIP_ESP32P4: chip_report_printf("ESP32-P4\n"); break; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + case CHIP_ESP32C5: chip_report_printf("ESP32-C5\n"); break; + case CHIP_ESP32C61: chip_report_printf("ESP32-C61\n"); break; + case CHIP_ESP32H21: chip_report_printf("ESP32-H21\n"); break; +#endif default: chip_report_printf("Unknown %d\n", info.model); break; } printPkgVersion(); From 100ed8e89524d5228ea2fc8d5220eba775462519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:56:41 +0100 Subject: [PATCH 005/102] fix(c5): Update debug log in setCpuFrequencyMhz --- cores/esp32/esp32-hal-cpu.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cores/esp32/esp32-hal-cpu.c b/cores/esp32/esp32-hal-cpu.c index 8c13a4077fd..30c7e3a2b0a 100644 --- a/cores/esp32/esp32-hal-cpu.c +++ b/cores/esp32/esp32-hal-cpu.c @@ -267,6 +267,12 @@ bool setCpuFrequencyMhz(uint32_t cpu_freq_mhz) { (conf.source == SOC_CPU_CLK_SRC_PLL) ? "PLL" : ((conf.source == SOC_CPU_CLK_SRC_APLL) ? "APLL" : ((conf.source == SOC_CPU_CLK_SRC_XTAL) ? "XTAL" : "8M")), conf.source_freq_mhz, conf.div, conf.freq_mhz, apb ); +#elif defined(CONFIG_IDF_TARGET_ESP32C5) + log_d( + "%s: %u / %u = %u Mhz, APB: %u Hz", + (conf.source == SOC_CPU_CLK_SRC_PLL_F240M || conf.source == SOC_CPU_CLK_SRC_PLL_F160M) ? "PLL" : ((conf.source == SOC_CPU_CLK_SRC_XTAL) ? "XTAL" : "8M"), + conf.source_freq_mhz, conf.div, conf.freq_mhz, apb + ); #else log_d( "%s: %u / %u = %u Mhz, APB: %u Hz", (conf.source == SOC_CPU_CLK_SRC_PLL) ? "PLL" : ((conf.source == SOC_CPU_CLK_SRC_XTAL) ? "XTAL" : "17.5M"), From d9d3bf48d3f25bd3bc8a2217c67034c5842eccb5 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:50:05 +0100 Subject: [PATCH 006/102] add c5 bootloader location to pioarduino script (#10889) * add c5 bootloader location to pioarduino script * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- tools/pioarduino-build.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/pioarduino-build.py b/tools/pioarduino-build.py index 3335a716888..47940b0d043 100644 --- a/tools/pioarduino-build.py +++ b/tools/pioarduino-build.py @@ -213,7 +213,11 @@ def add_tinyuf2_extra_image(): LIBSOURCE_DIRS=[join(FRAMEWORK_DIR, "libraries")], FLASH_EXTRA_IMAGES=[ ( - "0x1000" if build_mcu in ["esp32", "esp32s2"] else ("0x2000" if build_mcu in ["esp32p4"] else "0x0000"), + ( + "0x1000" + if build_mcu in ["esp32", "esp32s2"] + else ("0x2000" if build_mcu in ["esp32p4", "esp32c5"] else "0x0000") + ), get_bootloader_image(variants_dir), ), ("0x8000", join(env.subst("$BUILD_DIR"), "partitions.bin")), From 8af81cdf096f003255679485733566ffb23f3042 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Wed, 22 Jan 2025 15:52:13 +0200 Subject: [PATCH 007/102] fix(ci): Fix/stop some examples for C5 --- libraries/ESP32/examples/DeepSleep/ExternalWakeUp/ci.json | 3 ++- .../ESP32/examples/DeepSleep/SmoothBlink_ULP_Code/ci.json | 3 ++- libraries/ESP32/examples/DeepSleep/TouchWakeUp/ci.json | 3 ++- libraries/ESP32/examples/RMT/RMTLoopback/RMTLoopback.ino | 2 +- .../ESP32/examples/ResetReason/ResetReason/ResetReason.ino | 2 ++ libraries/ESP32/examples/TWAI/TWAIreceive/ci.json | 5 +++++ libraries/ESP32/examples/TWAI/TWAItransmit/ci.json | 5 +++++ libraries/ESP_SR/examples/Basic/ci.json | 3 ++- 8 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 libraries/ESP32/examples/TWAI/TWAIreceive/ci.json create mode 100644 libraries/ESP32/examples/TWAI/TWAItransmit/ci.json diff --git a/libraries/ESP32/examples/DeepSleep/ExternalWakeUp/ci.json b/libraries/ESP32/examples/DeepSleep/ExternalWakeUp/ci.json index cd679adefad..dfd49d94fe9 100644 --- a/libraries/ESP32/examples/DeepSleep/ExternalWakeUp/ci.json +++ b/libraries/ESP32/examples/DeepSleep/ExternalWakeUp/ci.json @@ -3,6 +3,7 @@ "esp32c3": false, "esp32c6": false, "esp32h2": false, - "esp32p4": false + "esp32p4": false, + "esp32c5": false } } diff --git a/libraries/ESP32/examples/DeepSleep/SmoothBlink_ULP_Code/ci.json b/libraries/ESP32/examples/DeepSleep/SmoothBlink_ULP_Code/ci.json index 6afa60f44c4..5fa2bd14e5d 100644 --- a/libraries/ESP32/examples/DeepSleep/SmoothBlink_ULP_Code/ci.json +++ b/libraries/ESP32/examples/DeepSleep/SmoothBlink_ULP_Code/ci.json @@ -5,6 +5,7 @@ "esp32h2": false, "esp32p4": false, "esp32s2": false, - "esp32s3": false + "esp32s3": false, + "esp32c5": false } } diff --git a/libraries/ESP32/examples/DeepSleep/TouchWakeUp/ci.json b/libraries/ESP32/examples/DeepSleep/TouchWakeUp/ci.json index 25c42144223..ae65fa0df74 100644 --- a/libraries/ESP32/examples/DeepSleep/TouchWakeUp/ci.json +++ b/libraries/ESP32/examples/DeepSleep/TouchWakeUp/ci.json @@ -2,6 +2,7 @@ "targets": { "esp32c3": false, "esp32c6": false, - "esp32h2": false + "esp32h2": false, + "esp32c5": false } } diff --git a/libraries/ESP32/examples/RMT/RMTLoopback/RMTLoopback.ino b/libraries/ESP32/examples/RMT/RMTLoopback/RMTLoopback.ino index 17e7af290bf..1b96d868ecb 100644 --- a/libraries/ESP32/examples/RMT/RMTLoopback/RMTLoopback.ino +++ b/libraries/ESP32/examples/RMT/RMTLoopback/RMTLoopback.ino @@ -21,7 +21,7 @@ * */ -#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 // ESP32 C3 has only 2 channels for RX and 2 for TX, thus MAX RMT_MEM is 128 #define RMT_TX_PIN 4 #define RMT_RX_PIN 5 diff --git a/libraries/ESP32/examples/ResetReason/ResetReason/ResetReason.ino b/libraries/ESP32/examples/ResetReason/ResetReason/ResetReason.ino index 0104c6422f2..ca7e15bf479 100644 --- a/libraries/ESP32/examples/ResetReason/ResetReason/ResetReason.ino +++ b/libraries/ESP32/examples/ResetReason/ResetReason/ResetReason.ino @@ -28,6 +28,8 @@ #include "esp32h2/rom/rtc.h" #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/rtc.h" #else #error Target CONFIG_IDF_TARGET is not supported #endif diff --git a/libraries/ESP32/examples/TWAI/TWAIreceive/ci.json b/libraries/ESP32/examples/TWAI/TWAIreceive/ci.json new file mode 100644 index 00000000000..7379dba8bb9 --- /dev/null +++ b/libraries/ESP32/examples/TWAI/TWAIreceive/ci.json @@ -0,0 +1,5 @@ +{ + "targets": { + "esp32c5": false + } +} diff --git a/libraries/ESP32/examples/TWAI/TWAItransmit/ci.json b/libraries/ESP32/examples/TWAI/TWAItransmit/ci.json new file mode 100644 index 00000000000..7379dba8bb9 --- /dev/null +++ b/libraries/ESP32/examples/TWAI/TWAItransmit/ci.json @@ -0,0 +1,5 @@ +{ + "targets": { + "esp32c5": false + } +} diff --git a/libraries/ESP_SR/examples/Basic/ci.json b/libraries/ESP_SR/examples/Basic/ci.json index c395378f45e..ec0e969a7d0 100644 --- a/libraries/ESP_SR/examples/Basic/ci.json +++ b/libraries/ESP_SR/examples/Basic/ci.json @@ -13,6 +13,7 @@ "esp32c6": false, "esp32h2": false, "esp32p4": false, - "esp32s2": false + "esp32s2": false, + "esp32c5": false } } From 9fbcb345b786e30de491e34b1cd4ea8d8ff4e2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Wed, 22 Jan 2025 18:44:49 +0100 Subject: [PATCH 008/102] fix(c5): Update PIN_RGB_LED in pins_arduino.h --- variants/esp32c5/pins_arduino.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32c5/pins_arduino.h b/variants/esp32c5/pins_arduino.h index eeaf3e01ebb..6ed7f0320fe 100644 --- a/variants/esp32c5/pins_arduino.h +++ b/variants/esp32c5/pins_arduino.h @@ -4,7 +4,7 @@ #include #include "soc/soc_caps.h" -#define PIN_RGB_LED 8 +#define PIN_RGB_LED 27 // BUILTIN_LED can be used in new Arduino API digitalWrite() like in Blink.ino static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + PIN_RGB_LED; #define BUILTIN_LED LED_BUILTIN // backward compatibility From f45cd7bf337316f4c6dde18bf23cd9c5b413863f Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Mon, 27 Jan 2025 14:06:54 +0200 Subject: [PATCH 009/102] fix(psram): Add support for ESP32-C5 PSRAM --- boards.txt | 5 +++++ cores/esp32/chip-debug-report.cpp | 3 +++ cores/esp32/esp32-hal-psram.c | 2 ++ 3 files changed, 10 insertions(+) diff --git a/boards.txt b/boards.txt index af3e217df9d..0f42aa18a05 100644 --- a/boards.txt +++ b/boards.txt @@ -210,6 +210,11 @@ esp32c5.menu.JTAGAdapter.bridge=ESP USB Bridge esp32c5.menu.JTAGAdapter.bridge.build.openocdscript=esp32c5-bridge.cfg esp32c5.menu.JTAGAdapter.bridge.build.copy_jtag_files=1 +esp32c5.menu.PSRAM.disabled=Disabled +esp32c5.menu.PSRAM.disabled.build.defines= +esp32c5.menu.PSRAM.enabled=Enabled +esp32c5.menu.PSRAM.enabled.build.defines=-DBOARD_HAS_PSRAM + esp32c5.menu.CDCOnBoot.default=Disabled esp32c5.menu.CDCOnBoot.default.build.cdc_on_boot=0 esp32c5.menu.CDCOnBoot.cdc=Enabled diff --git a/cores/esp32/chip-debug-report.cpp b/cores/esp32/chip-debug-report.cpp index e3f3f431f81..281c7bdb62d 100644 --- a/cores/esp32/chip-debug-report.cpp +++ b/cores/esp32/chip-debug-report.cpp @@ -67,6 +67,9 @@ static void printPkgVersion(void) { #elif CONFIG_IDF_TARGET_ESP32P4 uint32_t pkg_ver = REG_GET_FIELD(EFUSE_RD_MAC_SYS_2_REG, EFUSE_PKG_VERSION); chip_report_printf("%lu", pkg_ver); +#elif CONFIG_IDF_TARGET_ESP32C5 + uint32_t pkg_ver = REG_GET_FIELD(EFUSE_RD_MAC_SYS2_REG, EFUSE_PKG_VERSION); + chip_report_printf("%lu", pkg_ver); #else chip_report_printf("Unknown"); #endif diff --git a/cores/esp32/esp32-hal-psram.c b/cores/esp32/esp32-hal-psram.c index 3c7a51c3343..0d57a67ede4 100644 --- a/cores/esp32/esp32-hal-psram.c +++ b/cores/esp32/esp32-hal-psram.c @@ -29,6 +29,8 @@ #include "esp32s3/rom/cache.h" #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/cache.h" +#elif CONFIG_IDF_TARGET_ESP32C5 +#include "esp32c5/rom/cache.h" #else #error Target CONFIG_IDF_TARGET is not supported #endif From 75de09ef298c5bc9e26c7be2fd8af0446b4e4296 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Tue, 4 Feb 2025 00:14:19 +0200 Subject: [PATCH 010/102] fix(board): Update ESP32-C5 Dev Kit Pinout --- variants/esp32c5/pins_arduino.h | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/variants/esp32c5/pins_arduino.h b/variants/esp32c5/pins_arduino.h index 6ed7f0320fe..e3a7e57ef44 100644 --- a/variants/esp32c5/pins_arduino.h +++ b/variants/esp32c5/pins_arduino.h @@ -19,13 +19,13 @@ static const uint8_t RX = 12; // static const uint8_t USB_DM = 13; // static const uint8_t USB_DP = 14; -static const uint8_t SDA = 23; -static const uint8_t SCL = 22; +static const uint8_t SDA = 0; +static const uint8_t SCL = 1; -static const uint8_t SS = 18; -static const uint8_t MOSI = 19; -static const uint8_t MISO = 20; -static const uint8_t SCK = 21; +static const uint8_t SS = 6; +static const uint8_t MOSI = 8; +static const uint8_t MISO = 9; +static const uint8_t SCK = 10; static const uint8_t A0 = 1; static const uint8_t A1 = 2; @@ -34,9 +34,15 @@ static const uint8_t A3 = 4; static const uint8_t A4 = 5; static const uint8_t A5 = 6; -// LP I2C Pins are fixed on ESP32-C6 +// LP I2C Pins are fixed on ESP32-C5 +static const uint8_t LP_SDA = 2; +static const uint8_t LP_SCL = 3; #define WIRE1_PIN_DEFINED -static const uint8_t SDA1 = 2; -static const uint8_t SCL1 = 3; +#define SDA1 LP_SDA +#define SCL1 LP_SCL + +// LP UART Pins are fixed on ESP32-C5 +static const uint8_t LP_RX = 4; +static const uint8_t LP_TX = 5; #endif /* Pins_Arduino_h */ From 6283c15ae22da9ec731b2fc7a1f5539f9efca6cf Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 18 Feb 2025 14:24:37 +0200 Subject: [PATCH 011/102] IDF master (#10887) * feat(ci): Run sketches on ESP32-C5 * IDF master 1160a86b * fix(zigbee): Remove RCP mode from ESP32-C5 --- .github/scripts/on-push.sh | 1 + .github/scripts/sketch_utils.sh | 5 + .github/workflows/push.yml | 1 + boards.txt | 14 +- package/package_esp32_index.template.json | 204 +++++++++++----------- 5 files changed, 113 insertions(+), 112 deletions(-) diff --git a/.github/scripts/on-push.sh b/.github/scripts/on-push.sh index 6095f88e727..3eb020c09a3 100755 --- a/.github/scripts/on-push.sh +++ b/.github/scripts/on-push.sh @@ -90,6 +90,7 @@ if [ "$BUILD_LOG" -eq 1 ]; then fi #build sketches for different targets +build "esp32c5" "$CHUNK_INDEX" "$CHUNKS_CNT" "$BUILD_LOG" "$LOG_LEVEL" "$SKETCHES_FILE" "${SKETCHES_ESP32[@]}" build "esp32p4" "$CHUNK_INDEX" "$CHUNKS_CNT" "$BUILD_LOG" "$LOG_LEVEL" "$SKETCHES_FILE" "${SKETCHES_ESP32[@]}" build "esp32s3" "$CHUNK_INDEX" "$CHUNKS_CNT" "$BUILD_LOG" "$LOG_LEVEL" "$SKETCHES_FILE" "${SKETCHES_ESP32[@]}" build "esp32s2" "$CHUNK_INDEX" "$CHUNKS_CNT" "$BUILD_LOG" "$LOG_LEVEL" "$SKETCHES_FILE" "${SKETCHES_ESP32[@]}" diff --git a/.github/scripts/sketch_utils.sh b/.github/scripts/sketch_utils.sh index e536da50111..323f39b3afc 100755 --- a/.github/scripts/sketch_utils.sh +++ b/.github/scripts/sketch_utils.sh @@ -156,6 +156,7 @@ function build_sketch { # build_sketch [ext esp32c6_opts=$(echo "$debug_level,$fqbn_append" | sed 's/^,*//;s/,*$//;s/,\{2,\}/,/g') esp32h2_opts=$(echo "$debug_level,$fqbn_append" | sed 's/^,*//;s/,*$//;s/,\{2,\}/,/g') esp32p4_opts=$(echo "PSRAM=enabled,USBMode=default,$debug_level,$fqbn_append" | sed 's/^,*//;s/,*$//;s/,\{2,\}/,/g') + esp32c5_opts=$(echo "$debug_level,$fqbn_append" | sed 's/^,*//;s/,*$//;s/,\{2,\}/,/g') # Select the common part of the FQBN based on the target. The rest will be # appended depending on the passed options. @@ -191,6 +192,10 @@ function build_sketch { # build_sketch [ext [ -n "${options:-$esp32p4_opts}" ] && opt=":${options:-$esp32p4_opts}" fqbn="espressif:esp32:esp32p4$opt" ;; + "esp32c5") + [ -n "${options:-$esp32c5_opts}" ] && opt=":${options:-$esp32c5_opts}" + fqbn="espressif:esp32:esp32c5$opt" + ;; *) echo "ERROR: Invalid chip: $target" exit 1 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index feb32f95d03..428b89a8c21 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -47,6 +47,7 @@ on: - "variants/esp32/**/*" - "variants/esp32c2/**/*" - "variants/esp32c3/**/*" + - "variants/esp32c5/**/*" - "variants/esp32c6/**/*" - "variants/esp32h2/**/*" - "variants/esp32p4/**/*" diff --git a/boards.txt b/boards.txt index 0f42aa18a05..f7ad60d3eab 100644 --- a/boards.txt +++ b/boards.txt @@ -359,22 +359,16 @@ esp32c5.menu.ZigbeeMode.default.build.zigbee_mode= esp32c5.menu.ZigbeeMode.default.build.zigbee_libs= esp32c5.menu.ZigbeeMode.ed=Zigbee ED (end device) esp32c5.menu.ZigbeeMode.ed.build.zigbee_mode=-DZIGBEE_MODE_ED -esp32c5.menu.ZigbeeMode.ed.build.zigbee_libs=-lesp_zb_api_ed -lesp_zb_cli_command -lzboss_stack.ed -lzboss_port +esp32c5.menu.ZigbeeMode.ed.build.zigbee_libs=-lesp_zb_api.ed -lzboss_stack.ed -lzboss_port.native esp32c5.menu.ZigbeeMode.zczr=Zigbee ZCZR (coordinator/router) esp32c5.menu.ZigbeeMode.zczr.build.zigbee_mode=-DZIGBEE_MODE_ZCZR -esp32c5.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api_zczr -lesp_zb_cli_command -lzboss_stack.zczr -lzboss_port -esp32c5.menu.ZigbeeMode.rcp=Zigbee RCP (radio co-processor) -esp32c5.menu.ZigbeeMode.rcp.build.zigbee_mode=-DZIGBEE_MODE_RCP -esp32c5.menu.ZigbeeMode.rcp.build.zigbee_libs=-lesp_zb_api_rcp -lesp_zb_cli_command -lzboss_stack.rcp -lzboss_port +esp32c5.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api.zczr -lzboss_stack.zczr -lzboss_port.native esp32c5.menu.ZigbeeMode.ed_debug=Zigbee ED (end device) - Debug esp32c5.menu.ZigbeeMode.ed_debug.build.zigbee_mode=-DZIGBEE_MODE_ED -esp32c5.menu.ZigbeeMode.ed_debug.build.zigbee_libs=-lesp_zb_api_ed.debug -lesp_zb_cli_command -lzboss_stack.ed.debug -lzboss_port.debug +esp32c5.menu.ZigbeeMode.ed_debug.build.zigbee_libs=-lesp_zb_api.ed.debug -lzboss_stack.ed.debug -lzboss_port.native.debug esp32c5.menu.ZigbeeMode.zczr_debug=Zigbee ZCZR (coordinator/router) - Debug esp32c5.menu.ZigbeeMode.zczr_debug.build.zigbee_mode=-DZIGBEE_MODE_ZCZR -esp32c5.menu.ZigbeeMode.zczr_debug.build.zigbee_libs=-lesp_zb_api_zczr.debug -lesp_zb_cli_command -lzboss_stack.zczr.debug -lzboss_port.debug -esp32c5.menu.ZigbeeMode.rcp_debug=Zigbee RCP (radio co-processor) - Debug -esp32c5.menu.ZigbeeMode.rcp_debug.build.zigbee_mode=-DZIGBEE_MODE_RCP -esp32c5.menu.ZigbeeMode.rcp_debug.build.zigbee_libs=-lesp_zb_api_rcp.debug -lesp_zb_cli_command -lzboss_stack.rcp.debug -lzboss_port.debug +esp32c5.menu.ZigbeeMode.zczr_debug.build.zigbee_libs=-lesp_zb_api.zczr.debug -lzboss_stack.zczr.debug -lzboss_port.native.debug ############################################################## diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index b0d7c3200d1..8bbd2e6a4e9 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.4-d4aa25a3-v1" + "version": "idf-master-1160a86b-v1" }, { "packager": "esp32", @@ -61,7 +61,7 @@ { "packager": "esp32", "name": "xtensa-esp-elf-gdb", - "version": "14.2_20240403" + "version": "15.2_20241112" }, { "packager": "esp32", @@ -71,7 +71,7 @@ { "packager": "esp32", "name": "riscv32-esp-elf-gdb", - "version": "14.2_20240403" + "version": "15.2_20241112" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.4-d4aa25a3-v1", + "version": "idf-master-1160a86b-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-d4aa25a3-v1.zip", - "checksum": "SHA-256:81101d580ebafb78f71bd494f4f5162fd829279d18634282c0f8f95c9e928335", - "size": "350941396" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", + "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", + "size": "405894668" } ] }, @@ -228,63 +228,63 @@ }, { "name": "xtensa-esp-elf-gdb", - "version": "14.2_20240403", + "version": "15.2_20241112", "systems": [ { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-x86_64-linux-gnu.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-x86_64-linux-gnu.tar.gz", - "checksum": "SHA-256:9d68472d4cba5cf8c2b79d94f86f92c828e76a632bd1e6be5e7706e5b304d36e", - "size": "31010320" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", + "checksum": "SHA-256:18774349d2b1c7d7f5ba984563f7022aef4a3df4b706ed8821c53266f599343d", + "size": "35179121" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-aarch64-linux-gnu.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-aarch64-linux-gnu.tar.gz", - "checksum": "SHA-256:bdabc3217994815fc311c4e16e588b78f6596b5ad4ffa46c80b40e982cfb1e66", - "size": "30954580" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", + "checksum": "SHA-256:77cb3b2c85d6cfbb40b7f99eebbc2b1c3f4fe13eba20b3da798bdbbc6eb8e87c", + "size": "34295046" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-arm-linux-gnueabi.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-arm-linux-gnueabi.tar.gz", - "checksum": "SHA-256:d54b8d703ba897b28c627da3d27106a3906dd01ba298778a67064710bc33c76d", - "size": "28697281" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", + "checksum": "SHA-256:b87af0539de118eb9d43a4a89c8c1b0a6ab2557560015155104c44f91b5c4aa0", + "size": "30338727" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-i586-linux-gnu.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-i586-linux-gnu.tar.gz", - "checksum": "SHA-256:64d3bc992ed8fdec383d49e8b803ac494605a38117c8293db8da055037de96b0", - "size": "29890994" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", + "checksum": "SHA-256:2c7531bd390928fed479999ac9089b39a25f56ca4f4cc330db7d7a633a37405b", + "size": "33906121" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-x86_64-apple-darwin14.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-x86_64-apple-darwin14.tar.gz", - "checksum": "SHA-256:023e74b3fda793da4bc0509b02de776ee0dad6efaaac17bef5916fb7dc9c26b9", - "size": "44446611" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:ba2907be9a4c22c4e418f42ec84cf57401570a71dabbe69425227114ebf351a7", + "size": "52502302" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-aarch64-apple-darwin21.1.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-aarch64-apple-darwin21.1.tar.gz", - "checksum": "SHA-256:ea757c6bf8c25238f6d2fdcc6bbab25a1b00608a0f9e19b7ddd2f37ddbdc3fb1", - "size": "37021423" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:893500d6de354a6870820b9398531d8cd37d1cd85f3ae9b1f8a4c070b8048707", + "size": "41892363" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-i686-w64-mingw32.zip", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-i686-w64-mingw32.zip", - "checksum": "SHA-256:322e8d9b700dc32d8158e3dc55fb85ec55de48d0bb7789375ee39a28d5d655e2", - "size": "26302466" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", + "checksum": "SHA-256:328181380fccb252105c51a86071edbc5ef63e0114540edc4f8b2d62acf44916", + "size": "31307770" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-x86_64-w64-mingw32.zip", - "archiveFileName": "xtensa-esp-elf-gdb-14.2_20240403-x86_64-w64-mingw32.zip", - "checksum": "SHA-256:a27a2fe20f192f8e0a51b8936428b4e1cf8935cfe008ee445cc49f6fc7f6db2e", - "size": "28366035" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", + "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", + "checksum": "SHA-256:9c1949058d7aa1fa6f6f2d03173659b9f54e3c3940cbeebc1d56ae169c604ab2", + "size": "31420687" } ] }, @@ -352,63 +352,63 @@ }, { "name": "riscv32-esp-elf-gdb", - "version": "14.2_20240403", + "version": "15.2_20241112", "systems": [ { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-x86_64-linux-gnu.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-x86_64-linux-gnu.tar.gz", - "checksum": "SHA-256:ce004bc0bbd71b246800d2d13b239218b272a38bd528e316f21f1af2db8a4b13", - "size": "30707431" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", + "checksum": "SHA-256:bfca245b3d84244ad3b6156496728d916ac5ccc0d7f8e048194b7eba5cdbe047", + "size": "35297398" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-aarch64-linux-gnu.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-aarch64-linux-gnu.tar.gz", - "checksum": "SHA-256:ba10f2866c61410b88c65957274280b1a62e3bed05131654ed9b6758efe18e55", - "size": "30824065" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", + "checksum": "SHA-256:8536da9e3093b8f25e0b5204b04ed4afea432d1fd262f2abb466016d3d750ea6", + "size": "34455317" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-arm-linux-gnueabi.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-arm-linux-gnueabi.tar.gz", - "checksum": "SHA-256:88539db5d987f28827efac7e26080a2803b9b539342ccd2963ccfdd56d7f08f7", - "size": "29000575" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", + "checksum": "SHA-256:33a80d5e6604bb7d08b15492b8ec84c9176245e066d0bf8aad7570786ca44081", + "size": "31188203" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-i586-linux-gnu.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-i586-linux-gnu.tar.gz", - "checksum": "SHA-256:0e628ee37438ab6ba05eb889a76d09e50cb98e0020a16b8e2b935c5cf19b4ed2", - "size": "29947521" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", + "checksum": "SHA-256:ba448ecf2c80064013eaacb0e7be514a5ef17516f49ff3e50bc41c5578b8d88e", + "size": "34211289" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-x86_64-apple-darwin14.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-x86_64-apple-darwin14.tar.gz", - "checksum": "SHA-256:8f6bda832d70dad5860a639d55aba4237bd10cbac9f4822db1eece97357b34a9", - "size": "44196117" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:bdd07c54fe3216eb5c34f92cac514eb4af777c951573f186c33408c75f56bb4b", + "size": "52831957" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-aarch64-apple-darwin21.1.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-aarch64-apple-darwin21.1.tar.gz", - "checksum": "SHA-256:d88b6116e86456c8480ce9bc95aed375a35c0d091f1da0a53b86be0e6ef3d320", - "size": "36794404" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:9924f439ae77c3346e532a48a0d56330466c33ce1abd8d72b77f9f7f5c3b1196", + "size": "42227631" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-i686-w64-mingw32.zip", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-i686-w64-mingw32.zip", - "checksum": "SHA-256:d6e7ce05805b0d8d4dd138ad239b98a1adf8da98941867d60760eb1ae5361730", - "size": "26486295" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", + "checksum": "SHA-256:dcdafd30854b092671f555d844d94b0733e2ffcac8c026b4fb7b8a72ebef0016", + "size": "31971712" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/riscv32-esp-elf-gdb-14.2_20240403-x86_64-w64-mingw32.zip", - "archiveFileName": "riscv32-esp-elf-gdb-14.2_20240403-x86_64-w64-mingw32.zip", - "checksum": "SHA-256:5c9f211dc46daf6b96fad09d709284a0f0186fef8947d9f6edd6bca5b5ad4317", - "size": "27942579" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", + "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", + "checksum": "SHA-256:6ec8b3a073f2c5835321b4b560f8fc56180458204d071da139ff983436ea45e5", + "size": "31759225" } ] }, From 042015efc8930c3de0633ad930c8c05ed536aadd Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 18 Feb 2025 21:09:27 +0200 Subject: [PATCH 012/102] IDF master (#10981) * IDF master 1160a86b * fix(ci): Do not compile RainMaker examples on ESP32 --- .../RainMaker/examples/RMakerCustom/ci.json | 3 + .../RainMaker/examples/RMakerSwitch/ci.json | 3 + package/package_esp32_index.template.json | 68 +++++++++---------- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/libraries/RainMaker/examples/RMakerCustom/ci.json b/libraries/RainMaker/examples/RMakerCustom/ci.json index 1c80eda1d90..ce63fe9ccf0 100644 --- a/libraries/RainMaker/examples/RMakerCustom/ci.json +++ b/libraries/RainMaker/examples/RMakerCustom/ci.json @@ -1,4 +1,7 @@ { + "targets": { + "esp32": false + }, "fqbn_append": "PartitionScheme=rainmaker_4MB", "requires": [ "CONFIG_ESP_RMAKER_WORK_QUEUE_TASK_STACK=[1-9][0-9]*" diff --git a/libraries/RainMaker/examples/RMakerSwitch/ci.json b/libraries/RainMaker/examples/RMakerSwitch/ci.json index 1c80eda1d90..ce63fe9ccf0 100644 --- a/libraries/RainMaker/examples/RMakerSwitch/ci.json +++ b/libraries/RainMaker/examples/RMakerSwitch/ci.json @@ -1,4 +1,7 @@ { + "targets": { + "esp32": false + }, "fqbn_append": "PartitionScheme=rainmaker_4MB", "requires": [ "CONFIG_ESP_RMAKER_WORK_QUEUE_TASK_STACK=[1-9][0-9]*" diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 8bbd2e6a4e9..714328f9a17 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-1160a86b-v1" + "version": "idf-master-1160a86b-v2" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-1160a86b-v1", + "version": "idf-master-1160a86b-v2", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v1.zip", - "checksum": "SHA-256:ab71bb61b12ad109750d4d39200c2aeaa826395f65a2c95c2012d969eb5f76a2", - "size": "405894668" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", + "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", + "size": "406007754" } ] }, From e30e3c30f189aa87d69acb16b672f0ff2a94f1be Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 25 Feb 2025 10:28:07 +0200 Subject: [PATCH 013/102] IDF master (#10999) * IDF master c71d74e2 * IDF master 81e8b752 * IDF master 877057db --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 714328f9a17..46f0cd3418a 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-1160a86b-v2" + "version": "idf-master-877057db-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-1160a86b-v2", + "version": "idf-master-877057db-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-1160a86b-v2.zip", - "checksum": "SHA-256:2050bda30b1d3cc397683f38267744dc45d7e4c4dc857250477117f30db754db", - "size": "406007754" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", + "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", + "size": "411930767" } ] }, From 3fe2fe5311df6fc93cad8cb19e5a234b37605e3d Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Thu, 27 Feb 2025 00:56:51 +0200 Subject: [PATCH 014/102] IDF master 0461e2ff (#11018) --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 46f0cd3418a..f5bf9ed85cd 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-877057db-v1" + "version": "idf-master-0461e2ff-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-877057db-v1", + "version": "idf-master-0461e2ff-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-877057db-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-877057db-v1.zip", - "checksum": "SHA-256:fcded8de4107841bea5aaeb7bf7b53a336b3d132df29fb09ae6697b187d51920", - "size": "411930767" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", + "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", + "size": "411886500" } ] }, From d66eeb77547290eb5811fe6662ca2f709e747577 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:44:51 +0100 Subject: [PATCH 015/102] c5 flash base address is 0x2000 (#11037) * c5 flash base address is 0x2000 * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- cores/esp32/Esp.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cores/esp32/Esp.cpp b/cores/esp32/Esp.cpp index 2b2e1280683..f911b2d5f20 100644 --- a/cores/esp32/Esp.cpp +++ b/cores/esp32/Esp.cpp @@ -66,7 +66,7 @@ extern "C" { #define ESP_FLASH_IMAGE_BASE 0x2000 // Esp32p4 is located at 0x2000 #elif CONFIG_IDF_TARGET_ESP32C5 #include "esp32c5/rom/spi_flash.h" -#define ESP_FLASH_IMAGE_BASE 0x0000 // Esp32c5 is located at 0x0000 +#define ESP_FLASH_IMAGE_BASE 0x2000 // Esp32c5 is located at 0x2000 #else #error Target CONFIG_IDF_TARGET is not supported #endif @@ -306,11 +306,11 @@ const char *EspClass::getChipModel(void) { case CHIP_ESP32H2: return "ESP32-H2"; case CHIP_ESP32P4: return "ESP32-P4"; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) - case CHIP_ESP32C5: return "ESP32-C5"; + case CHIP_ESP32C5: return "ESP32-C5"; case CHIP_ESP32C61: return "ESP32-C61"; case CHIP_ESP32H21: return "ESP32-H21"; #endif - default: return "UNKNOWN"; + default: return "UNKNOWN"; } #endif } From 7c1ac1ae60672216f942b812dcebc0e97326e9e2 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Mon, 10 Mar 2025 22:19:40 +0200 Subject: [PATCH 016/102] feat(wifi): Add support for 2.4GHz and 5GHz band switching (#11045) * feat(wifi): Add support for 2.4GHz and 5GHz band switching * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- libraries/WiFi/examples/WiFiScan/WiFiScan.ino | 41 ++++++--- libraries/WiFi/src/WiFiGeneric.cpp | 88 +++++++++++++++++++ libraries/WiFi/src/WiFiGeneric.h | 4 + 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/libraries/WiFi/examples/WiFiScan/WiFiScan.ino b/libraries/WiFi/examples/WiFiScan/WiFiScan.ino index 15ce367c897..98733adb0bb 100644 --- a/libraries/WiFi/examples/WiFiScan/WiFiScan.ino +++ b/libraries/WiFi/examples/WiFiScan/WiFiScan.ino @@ -1,5 +1,5 @@ /* - * This sketch demonstrates how to scan WiFi networks. + * This sketch demonstrates how to scan WiFi networks. For chips that support 5GHz band, separate scans are done for all bands. * The API is based on the Arduino WiFi Shield library, but has significant changes as newer WiFi functions are supported. * E.g. the return value of `encryptionType()` different because more modern encryption is supported. */ @@ -7,18 +7,13 @@ void setup() { Serial.begin(115200); - - // Set WiFi to station mode and disconnect from an AP if it was previously connected. - WiFi.mode(WIFI_STA); - WiFi.disconnect(); - delay(100); - + // Enable Station Interface + WiFi.STA.begin(); Serial.println("Setup done"); } -void loop() { +void ScanWiFi() { Serial.println("Scan start"); - // WiFi.scanNetworks will return the number of networks found. int n = WiFi.scanNetworks(); Serial.println("Scan done"); @@ -54,11 +49,33 @@ void loop() { delay(10); } } - Serial.println(""); // Delete the scan result to free memory for code below. WiFi.scanDelete(); - + Serial.println("-------------------------------------"); +} +void loop() { + Serial.println("-------------------------------------"); + Serial.println("Default wifi band mode scan:"); + Serial.println("-------------------------------------"); + WiFi.setBandMode(WIFI_BAND_MODE_AUTO); + ScanWiFi(); +#if CONFIG_SOC_WIFI_SUPPORT_5G + // Wait a bit before scanning again. + delay(1000); + Serial.println("-------------------------------------"); + Serial.println("2.4 Ghz wifi band mode scan:"); + Serial.println("-------------------------------------"); + WiFi.setBandMode(WIFI_BAND_MODE_2G_ONLY); + ScanWiFi(); + // Wait a bit before scanning again. + delay(1000); + Serial.println("-------------------------------------"); + Serial.println("5 Ghz wifi band mode scan:"); + Serial.println("-------------------------------------"); + WiFi.setBandMode(WIFI_BAND_MODE_5G_ONLY); + ScanWiFi(); +#endif // Wait a bit before scanning again. - delay(5000); + delay(10000); } diff --git a/libraries/WiFi/src/WiFiGeneric.cpp b/libraries/WiFi/src/WiFiGeneric.cpp index aa994963514..6e38e19a29f 100644 --- a/libraries/WiFi/src/WiFiGeneric.cpp +++ b/libraries/WiFi/src/WiFiGeneric.cpp @@ -378,6 +378,10 @@ static bool espWiFiStart() { log_e("esp_wifi_start 0x%x: %s", err, esp_err_to_name(err)); return _esp_wifi_started; } +#if SOC_WIFI_SUPPORT_5G + log_v("Setting Band Mode to AUTO"); + esp_wifi_set_band_mode(WIFI_BAND_MODE_AUTO); +#endif return _esp_wifi_started; } @@ -729,6 +733,90 @@ wifi_ps_type_t WiFiGenericClass::getSleep() { return _sleepEnabled; } +/** + * control wifi band mode + * @param band_mode enum possible band modes + * @return ok + */ +bool WiFiGenericClass::setBandMode(wifi_band_mode_t band_mode) { +#if SOC_WIFI_SUPPORT_5G + if (!WiFi.STA.started() && !WiFi.AP.started()) { + log_e("You need to start WiFi first"); + return false; + } + wifi_band_mode_t bm = WIFI_BAND_MODE_AUTO; + esp_err_t err = esp_wifi_get_band_mode(&bm); + if (err != ESP_OK) { + log_e("Failed to get Current Band Mode: 0x%x: %s", err, esp_err_to_name(err)); + return false; + } else if (bm == band_mode) { + log_d("No change in Band Mode"); + return true; + } else { + log_d("Switching Band Mode from %d to %d", bm, band_mode); + } +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR + if (WiFi.STA.connected() || WiFi.AP.connected()) { + log_e("Your network will get disconnected!"); + } +#endif + err = esp_wifi_set_band_mode(band_mode); + if (err != ESP_OK) { + log_e("Failed to set Band Mode: 0x%x: %s", err, esp_err_to_name(err)); + return false; + } + delay(100); + return true; +#else + if (band_mode == WIFI_BAND_MODE_5G_ONLY) { + log_e("This chip supports only 2.4GHz WiFi"); + } + return band_mode != WIFI_BAND_MODE_5G_ONLY; +#endif +} + +/** + * get the current enabled wifi band mode + * @return enum band mode + */ +wifi_band_mode_t WiFiGenericClass::getBandMode() { +#if SOC_WIFI_SUPPORT_5G + wifi_band_mode_t band_mode = WIFI_BAND_MODE_AUTO; + if (!WiFi.STA.started() && !WiFi.AP.started()) { + log_e("You need to start WiFi first"); + return band_mode; + } + esp_err_t err = esp_wifi_get_band_mode(&band_mode); + if (err != ESP_OK) { + log_e("Failed to get Band Mode: 0x%x: %s", err, esp_err_to_name(err)); + } + return band_mode; +#else + return WIFI_BAND_MODE_2G_ONLY; +#endif +} + +/** + * get the current active wifi band + * @return enum band + */ +wifi_band_t WiFiGenericClass::getBand() { +#if SOC_WIFI_SUPPORT_5G + wifi_band_t band = WIFI_BAND_2G; + if (!WiFi.STA.started() && !WiFi.AP.started()) { + log_e("You need to start WiFi first"); + return band; + } + esp_err_t err = esp_wifi_get_band(&band); + if (err != ESP_OK) { + log_e("Failed to get Band: 0x%x: %s", err, esp_err_to_name(err)); + } + return band; +#else + return WIFI_BAND_2G; +#endif +} + /** * control wifi tx power * @param power enum maximum wifi tx power diff --git a/libraries/WiFi/src/WiFiGeneric.h b/libraries/WiFi/src/WiFiGeneric.h index ed216229ed4..8497b4b4f3d 100644 --- a/libraries/WiFi/src/WiFiGeneric.h +++ b/libraries/WiFi/src/WiFiGeneric.h @@ -111,6 +111,10 @@ class WiFiGenericClass { bool setTxPower(wifi_power_t power); wifi_power_t getTxPower(); + bool setBandMode(wifi_band_mode_t band_mode); + wifi_band_mode_t getBandMode(); + wifi_band_t getBand(); + bool initiateFTM(uint8_t frm_count = 16, uint16_t burst_period = 2, uint8_t channel = 1, const uint8_t *mac = NULL); static bool setDualAntennaConfig(uint8_t gpio_ant1, uint8_t gpio_ant2, wifi_rx_ant_t rx_mode, wifi_tx_ant_t tx_mode); From a61961d5c995106ba89e1361655471938dfc4502 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Fri, 28 Mar 2025 10:48:39 +0200 Subject: [PATCH 017/102] IDF master (#11150) * IDF master ee77c489 * IDF master ee77c489 * IDF master 50be9735 * IDF master 23c73cdc * IDF master a45d713b --- package/package_esp32_index.template.json | 128 +++++++++++----------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index f5bf9ed85cd..1b935fd8acf 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-0461e2ff-v1" + "version": "idf-master-a45d713b-v1" }, { "packager": "esp32", @@ -76,7 +76,7 @@ { "packager": "esp32", "name": "openocd-esp32", - "version": "v0.12.0-esp32-20241016" + "version": "v0.12.0-esp32-20250226" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-0461e2ff-v1", + "version": "idf-master-a45d713b-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-0461e2ff-v1.zip", - "checksum": "SHA-256:e1413b7d711cbdc4efb3064d6ac25e09068d7a01db3a706bd1426b0fd992e35b", - "size": "411886500" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", + "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", + "size": "420636315" } ] }, @@ -414,56 +414,56 @@ }, { "name": "openocd-esp32", - "version": "v0.12.0-esp32-20241016", + "version": "v0.12.0-esp32-20250226", "systems": [ { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-linux-amd64-0.12.0-esp32-20241016.tar.gz", - "archiveFileName": "openocd-esp32-linux-amd64-0.12.0-esp32-20241016.tar.gz", - "checksum": "SHA-256:e82b0f036dc99244bead5f09a86e91bb2365cbcd1122ac68261e5647942485df", - "size": "2398717" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-linux-amd64-0.12.0-esp32-20250226.tar.gz", + "archiveFileName": "openocd-esp32-linux-amd64-0.12.0-esp32-20250226.tar.gz", + "checksum": "SHA-256:914c726342ba5828e53f41aa454f01f317c42d8e6772d3d874593a6960fc4729", + "size": "2414924" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-linux-arm64-0.12.0-esp32-20241016.tar.gz", - "archiveFileName": "openocd-esp32-linux-arm64-0.12.0-esp32-20241016.tar.gz", - "checksum": "SHA-256:8f8daf5bd22ec5d2fa9257b0862ec33da18ee677e023fb9a9eb17f74ce208c76", - "size": "2271584" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-linux-arm64-0.12.0-esp32-20250226.tar.gz", + "archiveFileName": "openocd-esp32-linux-arm64-0.12.0-esp32-20250226.tar.gz", + "checksum": "SHA-256:c44ee99a9209c0234dbbcec86339fd685f5c61a763b29c33eba590bf62db2296", + "size": "2293923" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-linux-armel-0.12.0-esp32-20241016.tar.gz", - "archiveFileName": "openocd-esp32-linux-armel-0.12.0-esp32-20241016.tar.gz", - "checksum": "SHA-256:bc9c020ecf20e2000f76cffa44305fd5bc44d2e688ea78cce423399d33f19767", - "size": "2414206" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-linux-armel-0.12.0-esp32-20250226.tar.gz", + "archiveFileName": "openocd-esp32-linux-armel-0.12.0-esp32-20250226.tar.gz", + "checksum": "SHA-256:21ab6af3cf05f9290f4d59f1f381d5094dd2755fc528d3d2feb9334348fc0d8d", + "size": "2436071" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-macos-0.12.0-esp32-20241016.tar.gz", - "archiveFileName": "openocd-esp32-macos-0.12.0-esp32-20241016.tar.gz", - "checksum": "SHA-256:02a2dffe801a2d005fa9e614d80ff8173395b2cb0b5d3118d0229d094a9946a7", - "size": "2508089" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-macos-0.12.0-esp32-20250226.tar.gz", + "archiveFileName": "openocd-esp32-macos-0.12.0-esp32-20250226.tar.gz", + "checksum": "SHA-256:0b5751699e93b6d101381611c96216ddff8c7dfd16425c610993fa27993f9a0a", + "size": "2525387" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-macos-arm64-0.12.0-esp32-20241016.tar.gz", - "archiveFileName": "openocd-esp32-macos-arm64-0.12.0-esp32-20241016.tar.gz", - "checksum": "SHA-256:c382f9e884d6565cb6089bff5f200f4810994667d885f062c3d3c5625a0fa9d6", - "size": "2552569" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-macos-arm64-0.12.0-esp32-20250226.tar.gz", + "archiveFileName": "openocd-esp32-macos-arm64-0.12.0-esp32-20250226.tar.gz", + "checksum": "SHA-256:8bffbbb594b27a4971a3922792135f8c836fff26991f7f450094386920263531", + "size": "2568843" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-win32-0.12.0-esp32-20241016.zip", - "archiveFileName": "openocd-esp32-win32-0.12.0-esp32-20241016.zip", - "checksum": "SHA-256:3b5d615e0a72cc771a45dd469031312d5881c01d7b6bc9edb29b8b6bda8c2e90", - "size": "2946244" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-win32-0.12.0-esp32-20250226.zip", + "archiveFileName": "openocd-esp32-win32-0.12.0-esp32-20250226.zip", + "checksum": "SHA-256:aaf3c955bb4eb47805a1ba108dfd07a8a56ce720cb40194a354362b5f0961230", + "size": "2960226" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20241016/openocd-esp32-win64-0.12.0-esp32-20241016.zip", - "archiveFileName": "openocd-esp32-win64-0.12.0-esp32-20241016.zip", - "checksum": "SHA-256:5e7b2fd1947d3a8625f6a11db7a2340cf2f41ff4c61284c022c7d7c32b18780a", - "size": "2946244" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-win64-0.12.0-esp32-20250226.zip", + "archiveFileName": "openocd-esp32-win64-0.12.0-esp32-20250226.zip", + "checksum": "SHA-256:79baf35325117a53093b62f6b9bee677dd12275d7066e3f8a274d2a80e986b6e", + "size": "2960225" } ] }, From 42ae2426affacf7162770b9399b486eb7954203f Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:32:51 +0200 Subject: [PATCH 018/102] fix C5 compile (#11255) --- cores/esp32/esp32-hal-uart.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cores/esp32/esp32-hal-uart.c b/cores/esp32/esp32-hal-uart.c index ca88edbca27..21540b3e95a 100644 --- a/cores/esp32/esp32-hal-uart.c +++ b/cores/esp32/esp32-hal-uart.c @@ -1157,7 +1157,11 @@ bool uartSetClockSource(uint8_t uartNum, uart_sclk_t clkSrc) { if (uart->num >= SOC_UART_HP_NUM) { switch (clkSrc) { case UART_SCLK_XTAL: uart->_uart_clock_source = LP_UART_SCLK_XTAL_D2; break; +#if CONFIG_IDF_TARGET_ESP32C5 + case UART_SCLK_RTC: uart->_uart_clock_source = LP_UART_SCLK_RC_FAST; break; +#else case UART_SCLK_RTC: uart->_uart_clock_source = LP_UART_SCLK_LP_FAST; break; +#endif case UART_SCLK_DEFAULT: default: uart->_uart_clock_source = LP_UART_SCLK_DEFAULT; } From 03e9c45084c389436869f1ae649549e245866ade Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 23 Apr 2025 15:50:58 +0300 Subject: [PATCH 019/102] IDF master (#11204) * fix(esp-now): Update TX Callback * IDF master d930a386 --------- Co-authored-by: Jason2866 <24528715+Jason2866@users.noreply.github.com> --- libraries/ESP_NOW/src/ESP32_NOW.cpp | 5 + package/package_esp32_index.template.json | 204 +++++++++++----------- 2 files changed, 107 insertions(+), 102 deletions(-) diff --git a/libraries/ESP_NOW/src/ESP32_NOW.cpp b/libraries/ESP_NOW/src/ESP32_NOW.cpp index 6fd3ff0a0b1..83fec4c4529 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW.cpp +++ b/libraries/ESP_NOW/src/ESP32_NOW.cpp @@ -129,7 +129,12 @@ static void _esp_now_rx_cb(const esp_now_recv_info_t *info, const uint8_t *data, } } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +static void _esp_now_tx_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status) { + const uint8_t *mac_addr = tx_info->des_addr; +#else static void _esp_now_tx_cb(const uint8_t *mac_addr, esp_now_send_status_t status) { +#endif log_v(MACSTR " : %s", MAC2STR(mac_addr), (status == ESP_NOW_SEND_SUCCESS) ? "SUCCESS" : "FAILED"); //find the peer and call it's callback for (uint8_t i = 0; i < ESP_NOW_MAX_TOTAL_PEER_NUM; i++) { diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 1b935fd8acf..a0d78ebc47e 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-a45d713b-v1" + "version": "idf-master-d930a386-v1" }, { "packager": "esp32", @@ -61,7 +61,7 @@ { "packager": "esp32", "name": "xtensa-esp-elf-gdb", - "version": "15.2_20241112" + "version": "16.2_20250324" }, { "packager": "esp32", @@ -71,7 +71,7 @@ { "packager": "esp32", "name": "riscv32-esp-elf-gdb", - "version": "15.2_20241112" + "version": "16.2_20250324" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-a45d713b-v1", + "version": "idf-master-d930a386-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-a45d713b-v1.zip", - "checksum": "SHA-256:52aec557b9744e721770853c67c4ee6a391debd7bb779f654bea0e5113658786", - "size": "420636315" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", + "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", + "size": "422376381" } ] }, @@ -228,63 +228,63 @@ }, { "name": "xtensa-esp-elf-gdb", - "version": "15.2_20241112", + "version": "16.2_20250324", "systems": [ { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", - "checksum": "SHA-256:18774349d2b1c7d7f5ba984563f7022aef4a3df4b706ed8821c53266f599343d", - "size": "35179121" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-x86_64-linux-gnu.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-x86_64-linux-gnu.tar.gz", + "checksum": "SHA-256:27b58ab12248e04277c4fdc74038cf0a001d5142df091ab94939ad35053738fd", + "size": "36361058" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", - "checksum": "SHA-256:77cb3b2c85d6cfbb40b7f99eebbc2b1c3f4fe13eba20b3da798bdbbc6eb8e87c", - "size": "34295046" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-aarch64-linux-gnu.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-aarch64-linux-gnu.tar.gz", + "checksum": "SHA-256:24f85aa778e1605098a13ff7bd29d5760767faf012705c8915cb08b32cad0206", + "size": "35442104" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", - "checksum": "SHA-256:b87af0539de118eb9d43a4a89c8c1b0a6ab2557560015155104c44f91b5c4aa0", - "size": "30338727" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-arm-linux-gnueabi.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-arm-linux-gnueabi.tar.gz", + "checksum": "SHA-256:c73e43038b6d50374cd0ee714370ce748189e0b00404d581babd2bb0115c4785", + "size": "31260410" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", - "checksum": "SHA-256:2c7531bd390928fed479999ac9089b39a25f56ca4f4cc330db7d7a633a37405b", - "size": "33906121" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-i586-linux-gnu.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-i586-linux-gnu.tar.gz", + "checksum": "SHA-256:dc7b8aad0fb1c6a1abfdb8dff4f08221ea08a0f28fb837f181969ac1174d4dc6", + "size": "35067894" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", - "checksum": "SHA-256:ba2907be9a4c22c4e418f42ec84cf57401570a71dabbe69425227114ebf351a7", - "size": "52502302" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-x86_64-apple-darwin21.1.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-x86_64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:398c429cfe696bad01d636c5488cadc87b20471c1b5ed02c60eee5ef2a775c93", + "size": "54992785" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", - "checksum": "SHA-256:893500d6de354a6870820b9398531d8cd37d1cd85f3ae9b1f8a4c070b8048707", - "size": "41892363" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-aarch64-apple-darwin21.1.tar.gz", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-aarch64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:b6d85c0d76d653bb55f9d06b0cd509eab7e99db541c88b8c849c64827e9d74a9", + "size": "43538967" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", - "checksum": "SHA-256:328181380fccb252105c51a86071edbc5ef63e0114540edc4f8b2d62acf44916", - "size": "31307770" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-i686-w64-mingw32.zip", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-i686-w64-mingw32.zip", + "checksum": "SHA-256:f748d6b65fdf66733b82e12d0d85a05e3134122416280379df129cfebe2aa4b2", + "size": "32189419" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/xtensa-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", - "archiveFileName": "xtensa-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", - "checksum": "SHA-256:9c1949058d7aa1fa6f6f2d03173659b9f54e3c3940cbeebc1d56ae169c604ab2", - "size": "31420687" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/xtensa-esp-elf-gdb-16.2_20250324-x86_64-w64-mingw32.zip", + "archiveFileName": "xtensa-esp-elf-gdb-16.2_20250324-x86_64-w64-mingw32.zip", + "checksum": "SHA-256:e970fc3ec8a1d0acee2432e91e0a01b348613a0425aacfa981b2fc505fe920cc", + "size": "32290997" } ] }, @@ -352,63 +352,63 @@ }, { "name": "riscv32-esp-elf-gdb", - "version": "15.2_20241112", + "version": "16.2_20250324", "systems": [ { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-x86_64-linux-gnu.tar.gz", - "checksum": "SHA-256:bfca245b3d84244ad3b6156496728d916ac5ccc0d7f8e048194b7eba5cdbe047", - "size": "35297398" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-x86_64-linux-gnu.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-x86_64-linux-gnu.tar.gz", + "checksum": "SHA-256:f9b172d8d72d0a1e2b0b80127df29263a0cb0d0c4e998e09c27031bfac09f3ec", + "size": "36528201" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-aarch64-linux-gnu.tar.gz", - "checksum": "SHA-256:8536da9e3093b8f25e0b5204b04ed4afea432d1fd262f2abb466016d3d750ea6", - "size": "34455317" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-aarch64-linux-gnu.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-aarch64-linux-gnu.tar.gz", + "checksum": "SHA-256:68bb6a85fb58b8a738f799e8fb4fa1f56cfeffc4de803ceb03c8a33cb2cd919d", + "size": "35643464" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-arm-linux-gnueabi.tar.gz", - "checksum": "SHA-256:33a80d5e6604bb7d08b15492b8ec84c9176245e066d0bf8aad7570786ca44081", - "size": "31188203" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-arm-linux-gnueabi.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-arm-linux-gnueabi.tar.gz", + "checksum": "SHA-256:673038ab9fb2b7391ff9252824194e3b9e40668efe9ce54d1e582a9d6c51f04a", + "size": "32154574" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-i586-linux-gnu.tar.gz", - "checksum": "SHA-256:ba448ecf2c80064013eaacb0e7be514a5ef17516f49ff3e50bc41c5578b8d88e", - "size": "34211289" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-i586-linux-gnu.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-i586-linux-gnu.tar.gz", + "checksum": "SHA-256:62f05d5fe08145b25e423dd0b3f1ae260be99abf5462b8cfd918bf2231e26e30", + "size": "35410891" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-x86_64-apple-darwin21.1.tar.gz", - "checksum": "SHA-256:bdd07c54fe3216eb5c34f92cac514eb4af777c951573f186c33408c75f56bb4b", - "size": "52831957" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-x86_64-apple-darwin21.1.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-x86_64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:63ae12cfbab648e2d2ca7a700a0c615c4f36a6fbe6876c11ba108115ee0d60f2", + "size": "55359246" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-aarch64-apple-darwin21.1.tar.gz", - "checksum": "SHA-256:9924f439ae77c3346e532a48a0d56330466c33ce1abd8d72b77f9f7f5c3b1196", - "size": "42227631" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-aarch64-apple-darwin21.1.tar.gz", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-aarch64-apple-darwin21.1.tar.gz", + "checksum": "SHA-256:bfbe49774f839020cef988537da0a06896dfe4a382674c62285361ed9bd4aee3", + "size": "43926592" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-i686-w64-mingw32.zip", - "checksum": "SHA-256:dcdafd30854b092671f555d844d94b0733e2ffcac8c026b4fb7b8a72ebef0016", - "size": "31971712" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-i686-w64-mingw32.zip", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-i686-w64-mingw32.zip", + "checksum": "SHA-256:e8b84eec990ff514729b3770edf2b543f36670f43663ce0c3b624fb4884812ca", + "size": "32914955" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v15.2_20241112/riscv32-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", - "archiveFileName": "riscv32-esp-elf-gdb-15.2_20241112-x86_64-w64-mingw32.zip", - "checksum": "SHA-256:6ec8b3a073f2c5835321b4b560f8fc56180458204d071da139ff983436ea45e5", - "size": "31759225" + "url": "https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v16.2_20250324/riscv32-esp-elf-gdb-16.2_20250324-x86_64-w64-mingw32.zip", + "archiveFileName": "riscv32-esp-elf-gdb-16.2_20250324-x86_64-w64-mingw32.zip", + "checksum": "SHA-256:37c79178900c19ca7487c26af4b5ad6b0d3f34683bd0e9c2ddd39038c999e429", + "size": "32667353" } ] }, From b115acea40b2381bc00e07944d926fe726d05ef5 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Thu, 1 May 2025 02:15:03 +0300 Subject: [PATCH 020/102] IDF master (#11289) * fix(libs): Ensure compilation with ESP32-C5 * fix(i2c): Update I2C Slave init call * IDF master 465b159c * ci(simple_ble): Add check for BLE supported * IDF master 38628f98 --------- Co-authored-by: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> --- cores/esp32/esp32-hal-i2c-slave.c | 7 ++ idf_component.yml | 18 ++--- libraries/BluetoothSerial/src/BTAddress.cpp | 4 +- libraries/BluetoothSerial/src/BTAddress.h | 4 +- .../BluetoothSerial/src/BTAdvertisedDevice.h | 6 +- .../src/BTAdvertisedDeviceSet.cpp | 4 +- libraries/BluetoothSerial/src/BTScan.h | 7 +- .../BluetoothSerial/src/BTScanResultsSet.cpp | 4 +- .../BluetoothSerial/src/BluetoothSerial.cpp | 3 +- .../BluetoothSerial/src/BluetoothSerial.h | 3 +- .../examples/SimpleBleDevice/ci.json | 1 + libraries/SimpleBLE/src/SimpleBLE.cpp | 3 +- libraries/SimpleBLE/src/SimpleBLE.h | 3 +- package/package_esp32_index.template.json | 68 +++++++++---------- 14 files changed, 80 insertions(+), 55 deletions(-) diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index 0e01259da61..1d92a55ae15 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -43,6 +43,7 @@ #include "soc/i2c_struct.h" #include "soc/periph_defs.h" #include "hal/i2c_ll.h" +#include "hal/i2c_types.h" #ifndef CONFIG_IDF_TARGET_ESP32C5 #include "hal/clk_gate_ll.h" #endif @@ -337,7 +338,13 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t } #endif // !defined(CONFIG_IDF_TARGET_ESP32P4) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + i2c_ll_set_mode(i2c->dev, I2C_BUS_MODE_SLAVE); + i2c_ll_enable_pins_open_drain(i2c->dev, true); +#else i2c_ll_slave_init(i2c->dev); +#endif + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) i2c_ll_enable_fifo_mode(i2c->dev, true); #else diff --git a/idf_component.yml b/idf_component.yml index c1fc614cae5..f0e3f84b28d 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -57,12 +57,12 @@ dependencies: version: "==1.6.3" require: public rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/esp-zigbee-lib: version: "==1.6.3" require: public rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/esp-dsp: version: "^1.3.4" rules: @@ -73,32 +73,32 @@ dependencies: espressif/esp_rainmaker: version: "1.5.2" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/rmaker_common: version: "1.4.6" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/esp_insights: version: "1.2.2" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" # New version breaks esp_insights 1.0.1 espressif/esp_diag_data_store: version: "1.0.2" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/esp_diagnostics: version: "1.2.1" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/cbor: version: "0.6.0~1" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" espressif/qrcode: version: "0.1.0~2" rules: - - if: "target not in [esp32c2, esp32p4]" + - if: "target not in [esp32c2, esp32p4, esp32c5]" # RainMaker End espressif/esp-sr: version: "^1.4.2" diff --git a/libraries/BluetoothSerial/src/BTAddress.cpp b/libraries/BluetoothSerial/src/BTAddress.cpp index 6a6de6522bd..6dc05f3aafb 100644 --- a/libraries/BluetoothSerial/src/BTAddress.cpp +++ b/libraries/BluetoothSerial/src/BTAddress.cpp @@ -7,7 +7,9 @@ * Author: Thomas M. (ArcticSnowSky) */ #include "sdkconfig.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#include "soc/soc_caps.h" + +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include "BTAddress.h" #include diff --git a/libraries/BluetoothSerial/src/BTAddress.h b/libraries/BluetoothSerial/src/BTAddress.h index a2af9247fb0..ece3ae83525 100644 --- a/libraries/BluetoothSerial/src/BTAddress.h +++ b/libraries/BluetoothSerial/src/BTAddress.h @@ -10,7 +10,9 @@ #ifndef COMPONENTS_CPP_UTILS_BTADDRESS_H_ #define COMPONENTS_CPP_UTILS_BTADDRESS_H_ #include "sdkconfig.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#include "soc/soc_caps.h" + +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include // ESP32 BT #include diff --git a/libraries/BluetoothSerial/src/BTAdvertisedDevice.h b/libraries/BluetoothSerial/src/BTAdvertisedDevice.h index 63c19c908b6..53aa2629b56 100644 --- a/libraries/BluetoothSerial/src/BTAdvertisedDevice.h +++ b/libraries/BluetoothSerial/src/BTAdvertisedDevice.h @@ -5,9 +5,11 @@ * Author: Thomas M. (ArcticSnowSky) */ -#ifndef __BTADVERTISEDDEVICE_H__ -#define __BTADVERTISEDDEVICE_H__ +#pragma once +#include "sdkconfig.h" +#include "soc/soc_caps.h" +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include "BTAddress.h" #include diff --git a/libraries/BluetoothSerial/src/BTAdvertisedDeviceSet.cpp b/libraries/BluetoothSerial/src/BTAdvertisedDeviceSet.cpp index ed6076a3103..9afc28547e5 100644 --- a/libraries/BluetoothSerial/src/BTAdvertisedDeviceSet.cpp +++ b/libraries/BluetoothSerial/src/BTAdvertisedDeviceSet.cpp @@ -6,7 +6,9 @@ */ #include "sdkconfig.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#include "soc/soc_caps.h" + +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) //#include diff --git a/libraries/BluetoothSerial/src/BTScan.h b/libraries/BluetoothSerial/src/BTScan.h index a08f68cd7c2..6fd2daf9f6d 100644 --- a/libraries/BluetoothSerial/src/BTScan.h +++ b/libraries/BluetoothSerial/src/BTScan.h @@ -5,8 +5,11 @@ * Author: Thomas M. (ArcticSnowSky) */ -#ifndef __BTSCAN_H__ -#define __BTSCAN_H__ +#pragma once +#include "sdkconfig.h" +#include "soc/soc_caps.h" + +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include #include diff --git a/libraries/BluetoothSerial/src/BTScanResultsSet.cpp b/libraries/BluetoothSerial/src/BTScanResultsSet.cpp index 3633c010eae..02459b081ba 100644 --- a/libraries/BluetoothSerial/src/BTScanResultsSet.cpp +++ b/libraries/BluetoothSerial/src/BTScanResultsSet.cpp @@ -6,7 +6,9 @@ */ #include "sdkconfig.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#include "soc/soc_caps.h" + +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include diff --git a/libraries/BluetoothSerial/src/BluetoothSerial.cpp b/libraries/BluetoothSerial/src/BluetoothSerial.cpp index 3d00504c1b1..b7eede93ee6 100644 --- a/libraries/BluetoothSerial/src/BluetoothSerial.cpp +++ b/libraries/BluetoothSerial/src/BluetoothSerial.cpp @@ -19,8 +19,9 @@ #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "soc/soc_caps.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #ifdef ARDUINO_ARCH_ESP32 #include "esp32-hal-log.h" diff --git a/libraries/BluetoothSerial/src/BluetoothSerial.h b/libraries/BluetoothSerial/src/BluetoothSerial.h index d59fbf1f714..8cb6edb876c 100644 --- a/libraries/BluetoothSerial/src/BluetoothSerial.h +++ b/libraries/BluetoothSerial/src/BluetoothSerial.h @@ -16,8 +16,9 @@ #define _BLUETOOTH_SERIAL_H_ #include "sdkconfig.h" +#include "soc/soc_caps.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include "Arduino.h" #include "Stream.h" diff --git a/libraries/SimpleBLE/examples/SimpleBleDevice/ci.json b/libraries/SimpleBLE/examples/SimpleBleDevice/ci.json index d33a23f332e..3b6a150b31a 100644 --- a/libraries/SimpleBLE/examples/SimpleBleDevice/ci.json +++ b/libraries/SimpleBLE/examples/SimpleBleDevice/ci.json @@ -1,5 +1,6 @@ { "requires": [ + "CONFIG_SOC_BLE_SUPPORTED=y", "CONFIG_BT_ENABLED=y", "CONFIG_BLUEDROID_ENABLED=y" ] diff --git a/libraries/SimpleBLE/src/SimpleBLE.cpp b/libraries/SimpleBLE/src/SimpleBLE.cpp index 3c4ed915c05..3f1f2bbd1c1 100644 --- a/libraries/SimpleBLE/src/SimpleBLE.cpp +++ b/libraries/SimpleBLE/src/SimpleBLE.cpp @@ -13,8 +13,9 @@ // limitations under the License. #include "sdkconfig.h" +#include "soc/soc_caps.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include "SimpleBLE.h" #include "esp32-hal-log.h" diff --git a/libraries/SimpleBLE/src/SimpleBLE.h b/libraries/SimpleBLE/src/SimpleBLE.h index 23c1cdb593f..df1ee751f10 100644 --- a/libraries/SimpleBLE/src/SimpleBLE.h +++ b/libraries/SimpleBLE/src/SimpleBLE.h @@ -16,8 +16,9 @@ #define _SIMPLE_BLE_H_ #include "sdkconfig.h" +#include "soc/soc_caps.h" -#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#if SOC_BT_SUPPORTED && defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) #include #include diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index a0d78ebc47e..63027704d8a 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-d930a386-v1" + "version": "idf-master-38628f98-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-d930a386-v1", + "version": "idf-master-38628f98-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-d930a386-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-d930a386-v1.zip", - "checksum": "SHA-256:0310daa4f08f807f2bf3babd2587c2694df64c70e367863eadf5020636b717ae", - "size": "422376381" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", + "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", + "size": "398323971" } ] }, From 602f1f6e7fbc56e36068c9521272d64d40e4df30 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 7 May 2025 16:24:33 +0300 Subject: [PATCH 021/102] IDF master (#11342) * fix(ci): ESP32-P4 hosted compile fail (#11341) * fix(ci): Update changes for P4 and C5 builds with latest IDF * IDF master aaebc374 * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: Jason2866 <24528715+Jason2866@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CMakeLists.txt | 2 +- cores/esp32/chip-debug-report.cpp | 7 +- idf_component.yml | 2 +- libraries/WiFi/src/WiFiGeneric.cpp | 12 +- package/package_esp32_index.template.json | 128 +++++++++++----------- 5 files changed, 81 insertions(+), 70 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ba9c999d81..9e981130715 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -362,7 +362,7 @@ set(priv_requires fatfs nvs_flash app_update spiffs bootloader_support bt esp_hi if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_OpenThread) #if(CONFIG_SOC_IEEE802154_SUPPORTED) # Does not work! #if(CONFIG_OPENTHREAD_ENABLED) # Does not work! - if(IDF_TARGET STREQUAL "esp32c6" OR IDF_TARGET STREQUAL "esp32h2") # Sadly only this works + if(IDF_TARGET STREQUAL "esp32c6" OR IDF_TARGET STREQUAL "esp32h2" OR IDF_TARGET STREQUAL "esp32c5") # Sadly only this works list(APPEND requires openthread) endif() endif() diff --git a/cores/esp32/chip-debug-report.cpp b/cores/esp32/chip-debug-report.cpp index 281c7bdb62d..8592031ee3f 100644 --- a/cores/esp32/chip-debug-report.cpp +++ b/cores/esp32/chip-debug-report.cpp @@ -68,7 +68,8 @@ static void printPkgVersion(void) { uint32_t pkg_ver = REG_GET_FIELD(EFUSE_RD_MAC_SYS_2_REG, EFUSE_PKG_VERSION); chip_report_printf("%lu", pkg_ver); #elif CONFIG_IDF_TARGET_ESP32C5 - uint32_t pkg_ver = REG_GET_FIELD(EFUSE_RD_MAC_SYS2_REG, EFUSE_PKG_VERSION); + // ToDo: Update this line when EFUSE_PKG_VERSION is available again for ESP32-C5 + uint32_t pkg_ver = 0; //REG_GET_FIELD(EFUSE_RD_MAC_SYS2_REG, EFUSE_PKG_VERSION); chip_report_printf("%lu", pkg_ver); #else chip_report_printf("Unknown"); @@ -92,11 +93,11 @@ static void printChipInfo(void) { case CHIP_ESP32H2: chip_report_printf("ESP32-H2\n"); break; case CHIP_ESP32P4: chip_report_printf("ESP32-P4\n"); break; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) - case CHIP_ESP32C5: chip_report_printf("ESP32-C5\n"); break; + case CHIP_ESP32C5: chip_report_printf("ESP32-C5\n"); break; case CHIP_ESP32C61: chip_report_printf("ESP32-C61\n"); break; case CHIP_ESP32H21: chip_report_printf("ESP32-H21\n"); break; #endif - default: chip_report_printf("Unknown %d\n", info.model); break; + default: chip_report_printf("Unknown %d\n", info.model); break; } printPkgVersion(); chip_report_printf(" Revision : %.2f\n", (float)(info.revision) / 100.0); diff --git a/idf_component.yml b/idf_component.yml index f0e3f84b28d..642bc54143d 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -105,7 +105,7 @@ dependencies: rules: - if: "target in [esp32s3]" espressif/esp_hosted: - version: "^0.0.25" + version: "^2.0.0" rules: - if: "target == esp32p4" espressif/esp_wifi_remote: diff --git a/libraries/WiFi/src/WiFiGeneric.cpp b/libraries/WiFi/src/WiFiGeneric.cpp index e669ba81c12..6ae451d1cb7 100644 --- a/libraries/WiFi/src/WiFiGeneric.cpp +++ b/libraries/WiFi/src/WiFiGeneric.cpp @@ -252,13 +252,23 @@ static bool wifiHostedInit() { if (!hosted_initialized) { hosted_initialized = true; struct esp_hosted_sdio_config conf = INIT_DEFAULT_HOST_SDIO_CONFIG(); +#ifdef BOARD_HAS_SDIO_ESP_HOSTED + conf.pin_clk.pin = BOARD_SDIO_ESP_HOSTED_CLK; + conf.pin_cmd.pin = BOARD_SDIO_ESP_HOSTED_CMD; + conf.pin_d0.pin = BOARD_SDIO_ESP_HOSTED_D0; + conf.pin_d1.pin = BOARD_SDIO_ESP_HOSTED_D1; + conf.pin_d2.pin = BOARD_SDIO_ESP_HOSTED_D2; + conf.pin_d3.pin = BOARD_SDIO_ESP_HOSTED_D3; + conf.pin_reset.pin = BOARD_SDIO_ESP_HOSTED_RESET; +#else conf.pin_clk.pin = CONFIG_ESP_SDIO_PIN_CLK; conf.pin_cmd.pin = CONFIG_ESP_SDIO_PIN_CMD; conf.pin_d0.pin = CONFIG_ESP_SDIO_PIN_D0; conf.pin_d1.pin = CONFIG_ESP_SDIO_PIN_D1; conf.pin_d2.pin = CONFIG_ESP_SDIO_PIN_D2; conf.pin_d3.pin = CONFIG_ESP_SDIO_PIN_D3; - //conf.pin_rst.pin = CONFIG_ESP_SDIO_GPIO_RESET_SLAVE; + conf.pin_reset.pin = CONFIG_ESP_SDIO_GPIO_RESET_SLAVE; +#endif // esp_hosted_sdio_set_config() will fail on second attempt but here temporarily to not cause exception on reinit if (esp_hosted_sdio_set_config(&conf) != ESP_OK || esp_hosted_init() != ESP_OK) { log_e("esp_hosted_init failed!"); diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 63027704d8a..aa21a192509 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-38628f98-v1" + "version": "idf-master-aaebc374-v1" }, { "packager": "esp32", @@ -76,7 +76,7 @@ { "packager": "esp32", "name": "openocd-esp32", - "version": "v0.12.0-esp32-20250226" + "version": "v0.12.0-esp32-20250422" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-38628f98-v1", + "version": "idf-master-aaebc374-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-38628f98-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-38628f98-v1.zip", - "checksum": "SHA-256:efc30a38cccff38c36a86fd3db78aeb13594da60ccf49bc7971b7a9f849abcdf", - "size": "398323971" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", + "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", + "size": "404569749" } ] }, @@ -414,56 +414,56 @@ }, { "name": "openocd-esp32", - "version": "v0.12.0-esp32-20250226", + "version": "v0.12.0-esp32-20250422", "systems": [ { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-linux-amd64-0.12.0-esp32-20250226.tar.gz", - "archiveFileName": "openocd-esp32-linux-amd64-0.12.0-esp32-20250226.tar.gz", - "checksum": "SHA-256:914c726342ba5828e53f41aa454f01f317c42d8e6772d3d874593a6960fc4729", - "size": "2414924" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-linux-amd64-0.12.0-esp32-20250422.tar.gz", + "archiveFileName": "openocd-esp32-linux-amd64-0.12.0-esp32-20250422.tar.gz", + "checksum": "SHA-256:eb1fa9b21c65b45a2200af6dcc2914e32335d37b6dbbd181778dcc0dc025e70a", + "size": "2445546" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-linux-arm64-0.12.0-esp32-20250226.tar.gz", - "archiveFileName": "openocd-esp32-linux-arm64-0.12.0-esp32-20250226.tar.gz", - "checksum": "SHA-256:c44ee99a9209c0234dbbcec86339fd685f5c61a763b29c33eba590bf62db2296", - "size": "2293923" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-linux-arm64-0.12.0-esp32-20250422.tar.gz", + "archiveFileName": "openocd-esp32-linux-arm64-0.12.0-esp32-20250422.tar.gz", + "checksum": "SHA-256:f70334a9b12a75b4d943e09fa5db30973037c39dbb54d6fa9f1a7118228b3d1c", + "size": "2330926" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-linux-armel-0.12.0-esp32-20250226.tar.gz", - "archiveFileName": "openocd-esp32-linux-armel-0.12.0-esp32-20250226.tar.gz", - "checksum": "SHA-256:21ab6af3cf05f9290f4d59f1f381d5094dd2755fc528d3d2feb9334348fc0d8d", - "size": "2436071" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-linux-armel-0.12.0-esp32-20250422.tar.gz", + "archiveFileName": "openocd-esp32-linux-armel-0.12.0-esp32-20250422.tar.gz", + "checksum": "SHA-256:4ac34d6fd1af86aeda87c8318732f8d691c300c285c7fd2f5037c432c63fbbb3", + "size": "2470732" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-macos-0.12.0-esp32-20250226.tar.gz", - "archiveFileName": "openocd-esp32-macos-0.12.0-esp32-20250226.tar.gz", - "checksum": "SHA-256:0b5751699e93b6d101381611c96216ddff8c7dfd16425c610993fa27993f9a0a", - "size": "2525387" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-macos-0.12.0-esp32-20250422.tar.gz", + "archiveFileName": "openocd-esp32-macos-0.12.0-esp32-20250422.tar.gz", + "checksum": "SHA-256:9186a7a06304c6d9201cbce4ee3c7099b393bf8d329cda17a68874f92308f6ce", + "size": "2548730" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-macos-arm64-0.12.0-esp32-20250226.tar.gz", - "archiveFileName": "openocd-esp32-macos-arm64-0.12.0-esp32-20250226.tar.gz", - "checksum": "SHA-256:8bffbbb594b27a4971a3922792135f8c836fff26991f7f450094386920263531", - "size": "2568843" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-macos-arm64-0.12.0-esp32-20250422.tar.gz", + "archiveFileName": "openocd-esp32-macos-arm64-0.12.0-esp32-20250422.tar.gz", + "checksum": "SHA-256:2cc39318d52f393233ff1f777871aebe5b97b3fbad29556a238489263401b774", + "size": "2593819" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-win32-0.12.0-esp32-20250226.zip", - "archiveFileName": "openocd-esp32-win32-0.12.0-esp32-20250226.zip", - "checksum": "SHA-256:aaf3c955bb4eb47805a1ba108dfd07a8a56ce720cb40194a354362b5f0961230", - "size": "2960226" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-win32-0.12.0-esp32-20250422.zip", + "archiveFileName": "openocd-esp32-win32-0.12.0-esp32-20250422.zip", + "checksum": "SHA-256:ecb4f8533fa9098d10000f5f7e8b8eaa8591015b824b481078ddb2b37e7aa6f2", + "size": "2988859" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250226/openocd-esp32-win64-0.12.0-esp32-20250226.zip", - "archiveFileName": "openocd-esp32-win64-0.12.0-esp32-20250226.zip", - "checksum": "SHA-256:79baf35325117a53093b62f6b9bee677dd12275d7066e3f8a274d2a80e986b6e", - "size": "2960225" + "url": "https://github.com/espressif/openocd-esp32/releases/download/v0.12.0-esp32-20250422/openocd-esp32-win64-0.12.0-esp32-20250422.zip", + "archiveFileName": "openocd-esp32-win64-0.12.0-esp32-20250422.zip", + "checksum": "SHA-256:e9eae8e1a8d0e030cd81dcb08394a9137cb7338a6211dfabcdbdfb37b58c5a23", + "size": "2988858" } ] }, From bc08c49579d083503d8db77254d62dcb36f0057f Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 13 May 2025 12:25:39 +0200 Subject: [PATCH 022/102] Update version for wifi remote (#11344) --- idf_component.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idf_component.yml b/idf_component.yml index 642bc54143d..98dd5c9b9d9 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -109,7 +109,7 @@ dependencies: rules: - if: "target == esp32p4" espressif/esp_wifi_remote: - version: "^0.5.4" + version: "~0.9.2" rules: - if: "target == esp32p4" espressif/libsodium: From 69cd5a0a664e96afb7007efe061bc80fef6f72cd Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Thu, 15 May 2025 11:10:00 +0200 Subject: [PATCH 023/102] IDF master 7cf5dacd (#11358) --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index aa21a192509..e3b42431cbf 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-aaebc374-v1" + "version": "idf-master-7cf5dacd-v2" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-aaebc374-v1", + "version": "idf-master-7cf5dacd-v2", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-aaebc374-v1.zip", - "checksum": "SHA-256:5baa0bbeae58973fd6bd4004332eb554b723a1e67f1af389840fac8081c2e21e", - "size": "404569749" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", + "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", + "size": "405095217" } ] }, From 5540afa1ea9ab070d85e1d4fa3338e6c6ce12405 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Thu, 15 May 2025 11:13:32 +0200 Subject: [PATCH 024/102] feat(ci): Run push CI against IDF v5.5 --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 026e0b8fc0d..68148fbcff7 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -248,7 +248,7 @@ jobs: # See https://hub.docker.com/r/espressif/idf/tags and # https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-docker-image.html # for details. - idf_ver: ["release-v5.4"] + idf_ver: ["release-v5.5"] idf_target: [ "esp32", From 3f63a4929f6cb7165e78d47c4478f62a76e00e9e Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Mon, 19 May 2025 13:54:12 +0200 Subject: [PATCH 025/102] fix(build): Add dependency on esp_http_client --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e981130715..3288f16f39f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ endforeach() set(includedirs variants/${CONFIG_ARDUINO_VARIANT}/ cores/esp32/ ${ARDUINO_LIBRARIES_INCLUDEDIRS}) set(srcs ${CORE_SRCS} ${ARDUINO_LIBRARIES_SRCS}) set(priv_includes cores/esp32/libb64) -set(requires spi_flash esp_partition mbedtls wpa_supplicant esp_adc esp_eth http_parser esp_ringbuf esp_driver_gptimer esp_driver_usb_serial_jtag driver) +set(requires spi_flash esp_partition mbedtls wpa_supplicant esp_adc esp_eth http_parser esp_ringbuf esp_driver_gptimer esp_driver_usb_serial_jtag driver esp_http_client) set(priv_requires fatfs nvs_flash app_update spiffs bootloader_support bt esp_hid usb esp_psram ${ARDUINO_LIBRARIES_REQUIRES}) if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_OpenThread) From 6ce7e254c43604c4927ef1bcca0468e1d33db243 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Fri, 30 May 2025 17:18:32 +0300 Subject: [PATCH 026/102] IDF release/v5.5 (#11369) * IDF release/v5.5 719b1b2b * fix(build): Add dependency on esp_http_client * fix(build): Add dependency on esp_https_ota * IDF release/v5.5 28ac0243 --- CMakeLists.txt | 2 +- package/package_esp32_index.template.json | 68 +++++++++++------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cee7962a65..3cbb59dd3d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -359,7 +359,7 @@ endforeach() set(includedirs variants/${CONFIG_ARDUINO_VARIANT}/ cores/esp32/ ${ARDUINO_LIBRARIES_INCLUDEDIRS}) set(srcs ${CORE_SRCS} ${ARDUINO_LIBRARIES_SRCS}) set(priv_includes cores/esp32/libb64) -set(requires spi_flash esp_partition mbedtls wpa_supplicant esp_adc esp_eth http_parser esp_ringbuf esp_driver_gptimer esp_driver_usb_serial_jtag driver esp_http_client) +set(requires spi_flash esp_partition mbedtls wpa_supplicant esp_adc esp_eth http_parser esp_ringbuf esp_driver_gptimer esp_driver_usb_serial_jtag driver esp_http_client esp_https_ota) set(priv_requires fatfs nvs_flash app_update spiffs bootloader_support bt esp_hid usb esp_psram ${ARDUINO_LIBRARIES_REQUIRES}) if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_OpenThread) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index e3b42431cbf..eecc7c10788 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-master-7cf5dacd-v2" + "version": "idf-release_v5.5-28ac0243-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-master-7cf5dacd-v2", + "version": "idf-release_v5.5-28ac0243-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-master/esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "archiveFileName": "esp32-arduino-libs-idf-master-7cf5dacd-v2.zip", - "checksum": "SHA-256:91f74810c5bee0fa4463f7df73f1f6b5e4a08a6c2094014b9a4c0fac61eb43de", - "size": "405095217" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", + "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", + "size": "405149394" } ] }, From a6bba43d32ab65d73054a0a65b63a4716673975c Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Fri, 30 May 2025 19:12:34 +0300 Subject: [PATCH 027/102] fix(c5): Enable components for ESP32-C5 --- idf_component.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index 98dd5c9b9d9..dca97dc1655 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -57,12 +57,12 @@ dependencies: version: "==1.6.3" require: public rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/esp-zigbee-lib: version: "==1.6.3" require: public rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/esp-dsp: version: "^1.3.4" rules: @@ -73,32 +73,32 @@ dependencies: espressif/esp_rainmaker: version: "1.5.2" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/rmaker_common: version: "1.4.6" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/esp_insights: version: "1.2.2" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" # New version breaks esp_insights 1.0.1 espressif/esp_diag_data_store: version: "1.0.2" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/esp_diagnostics: version: "1.2.1" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/cbor: version: "0.6.0~1" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" espressif/qrcode: version: "0.1.0~2" rules: - - if: "target not in [esp32c2, esp32p4, esp32c5]" + - if: "target not in [esp32c2, esp32p4]" # RainMaker End espressif/esp-sr: version: "^1.4.2" From 0ab9a0fe6fb450f527cb277d09b70065905cf807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:48:48 +0200 Subject: [PATCH 028/102] feat(zigbee): Update to esp-zigbee-sdk 1.6.5 and fix ci.json files (#11436) * feat(zigbee): Update esp-zigbee-sdk and fix ci.json files * fix(ci): Check if LED_BUILTIN exist * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- idf_component.yml | 4 ++-- .../Zigbee/examples/Zigbee_On_Off_MultiSwitch/ci.json | 2 +- .../examples/Zigbee_Power_Outlet/Zigbee_Power_Outlet.ino | 8 +++++++- libraries/Zigbee/examples/Zigbee_Power_Outlet/ci.json | 1 - 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index dca97dc1655..766704176b0 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -54,12 +54,12 @@ dependencies: espressif/esp_modem: version: "^1.1.0" espressif/esp-zboss-lib: - version: "==1.6.3" + version: "==1.6.4" # compatible with esp-zigbee-lib 1.6.5 require: public rules: - if: "target not in [esp32c2, esp32p4]" espressif/esp-zigbee-lib: - version: "==1.6.3" + version: "==1.6.5" require: public rules: - if: "target not in [esp32c2, esp32p4]" diff --git a/libraries/Zigbee/examples/Zigbee_On_Off_MultiSwitch/ci.json b/libraries/Zigbee/examples/Zigbee_On_Off_MultiSwitch/ci.json index e79a477da11..15d6190e4ae 100644 --- a/libraries/Zigbee/examples/Zigbee_On_Off_MultiSwitch/ci.json +++ b/libraries/Zigbee/examples/Zigbee_On_Off_MultiSwitch/ci.json @@ -1,6 +1,6 @@ { "fqbn_append": "PartitionScheme=zigbee_zczr,ZigbeeMode=zczr", "requires": [ - "CONFIG_SOC_IEEE802154_SUPPORTED=y" + "CONFIG_ZB_ENABLED=y" ] } diff --git a/libraries/Zigbee/examples/Zigbee_Power_Outlet/Zigbee_Power_Outlet.ino b/libraries/Zigbee/examples/Zigbee_Power_Outlet/Zigbee_Power_Outlet.ino index d9fe1b7aefe..ba1474d6455 100644 --- a/libraries/Zigbee/examples/Zigbee_Power_Outlet/Zigbee_Power_Outlet.ino +++ b/libraries/Zigbee/examples/Zigbee_Power_Outlet/Zigbee_Power_Outlet.ino @@ -34,7 +34,13 @@ /* Zigbee power outlet configuration */ #define ZIGBEE_OUTLET_ENDPOINT 1 -uint8_t led = RGB_BUILTIN; + +#ifdef LED_BUILTIN // Use built-in LED if defined for the board +uint8_t led = LED_BUILTIN; +#else +uint8_t led = 2; // Use custom LED pin +#endif + uint8_t button = BOOT_PIN; ZigbeePowerOutlet zbOutlet = ZigbeePowerOutlet(ZIGBEE_OUTLET_ENDPOINT); diff --git a/libraries/Zigbee/examples/Zigbee_Power_Outlet/ci.json b/libraries/Zigbee/examples/Zigbee_Power_Outlet/ci.json index 23decd7127c..15d6190e4ae 100644 --- a/libraries/Zigbee/examples/Zigbee_Power_Outlet/ci.json +++ b/libraries/Zigbee/examples/Zigbee_Power_Outlet/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=zigbee_zczr,ZigbeeMode=zczr", "requires": [ - "CONFIG_SOC_IEEE802154_SUPPORTED=y", "CONFIG_ZB_ENABLED=y" ] } From 89ff4653286206be29fe49e3c333f14065801616 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Tue, 10 Jun 2025 05:19:32 -0300 Subject: [PATCH 029/102] feat(esptool): Upgrade to esptool v5 (#11433) * feat(esptool): Upgrade to esptool v5 * fix(script): Update script for better handling of esptool * fix(script): Get proper download url * fix(script): Apply copilot suggestions --- .github/scripts/update_esptool.py | 236 ++++++++++++++++++++++ package/package_esp32_index.template.json | 70 +++---- platform.txt | 12 +- 3 files changed, 277 insertions(+), 41 deletions(-) create mode 100644 .github/scripts/update_esptool.py diff --git a/.github/scripts/update_esptool.py b/.github/scripts/update_esptool.py new file mode 100644 index 00000000000..dd5de5526c3 --- /dev/null +++ b/.github/scripts/update_esptool.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 + +# This script is used to re-package the esptool if needed and update the JSON file +# for the Arduino ESP32 platform. +# +# The script has only been tested on macOS. +# +# For regular esptool releases, the generated packages already contain the correct permissions, +# extensions and are uploaded to the GitHub release assets. In this case, the script will only +# update the JSON file with the information from the GitHub release. +# +# The script can be used in two modes: +# 1. Local build: The build artifacts must be already downloaded and extracted in the base_folder. +# This is useful for esptool versions that are not yet released and that are grabbed from the +# GitHub build artifacts. +# 2. Release build: The script will get the release information from GitHub and update the JSON file. +# This is useful for esptool versions that are already released and that are uploaded to the +# GitHub release assets. +# +# For local build, the artifacts must be already downloaded and extracted in the base_folder +# set with the -l option. +# For example, a base folder "esptool" should contain the following folders extracted directly +# from the GitHub build artifacts: +# esptool/esptool-linux-aarch64 +# esptool/esptool-linux-amd64 +# esptool/esptool-linux-armv7 +# esptool/esptool-macos-amd64 +# esptool/esptool-macos-arm64 +# esptool/esptool-windows-amd64 + +import argparse +import json +import os +import shutil +import stat +import tarfile +import zipfile +import hashlib +import requests +from pathlib import Path + +def compute_sha256(filepath): + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for block in iter(lambda: f.read(4096), b""): + sha256.update(block) + return f"SHA-256:{sha256.hexdigest()}" + +def get_file_size(filepath): + return os.path.getsize(filepath) + +def update_json_for_host(tmp_json_path, version, host, url, archiveFileName, checksum, size): + with open(tmp_json_path) as f: + data = json.load(f) + + for pkg in data.get("packages", []): + for tool in pkg.get("tools", []): + if tool.get("name") == "esptool_py": + tool["version"] = version + + if url is None: + # If the URL is not set, we need to find the old URL and update it + for system in tool.get("systems", []): + if system.get("host") == host: + url = system.get("url").replace(system.get("archiveFileName"), archiveFileName) + break + else: + print(f"No old URL found for host {host}. Using empty URL.") + url = "" + + # Preserve existing systems order and update or append the new system + systems = tool.get("systems", []) + system_updated = False + for i, system in enumerate(systems): + if system.get("host") == host: + systems[i] = { + "host": host, + "url": url, + "archiveFileName": archiveFileName, + "checksum": checksum, + "size": str(size), + } + system_updated = True + break + + if not system_updated: + systems.append({ + "host": host, + "url": url, + "archiveFileName": archiveFileName, + "checksum": checksum, + "size": str(size), + }) + tool["systems"] = systems + + with open(tmp_json_path, "w") as f: + json.dump(data, f, indent=2, sort_keys=False, ensure_ascii=False) + f.write("\n") + +def update_tools_dependencies(tmp_json_path, version): + with open(tmp_json_path) as f: + data = json.load(f) + + for pkg in data.get("packages", []): + for platform in pkg.get("platforms", []): + for dep in platform.get("toolsDependencies", []): + if dep.get("name") == "esptool_py": + dep["version"] = version + + with open(tmp_json_path, "w") as f: + json.dump(data, f, indent=2, sort_keys=False, ensure_ascii=False) + f.write("\n") + +def create_archives(version, base_folder): + archive_files = [] + + for dirpath in Path(base_folder).glob("esptool-*"): + if not dirpath.is_dir(): + continue + + base = dirpath.name[len("esptool-"):] + + if "windows" in dirpath.name: + zipfile_name = f"esptool-v{version}-{base}.zip" + print(f"Creating {zipfile_name} from {dirpath} ...") + with zipfile.ZipFile(zipfile_name, "w", zipfile.ZIP_DEFLATED) as zipf: + for root, _, files in os.walk(dirpath): + for file in files: + full_path = os.path.join(root, file) + zipf.write(full_path, os.path.relpath(full_path, start=dirpath)) + archive_files.append(zipfile_name) + else: + tarfile_name = f"esptool-v{version}-{base}.tar.gz" + print(f"Creating {tarfile_name} from {dirpath} ...") + for root, dirs, files in os.walk(dirpath): + for name in dirs + files: + os.chmod(os.path.join(root, name), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | + stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH) + with tarfile.open(tarfile_name, "w:gz") as tar: + tar.add(dirpath, arcname=dirpath.name) + archive_files.append(tarfile_name) + + return archive_files + +def determine_hosts(archive_name): + if "linux-amd64" in archive_name: + return ["x86_64-pc-linux-gnu"] + elif "linux-armv7" in archive_name: + return ["arm-linux-gnueabihf"] + elif "linux-aarch64" in archive_name: + return ["aarch64-linux-gnu"] + elif "macos-amd64" in archive_name: + return ["x86_64-apple-darwin"] + elif "macos-arm64" in archive_name: + return ["arm64-apple-darwin"] + elif "windows-amd64" in archive_name: + return ["x86_64-mingw32", "i686-mingw32"] + else: + return [] + +def update_json_from_local_build(tmp_json_path, version, base_folder, archive_files): + for archive in archive_files: + print(f"Processing archive: {archive}") + hosts = determine_hosts(archive) + if not hosts: + print(f"Skipping unknown archive type: {archive}") + continue + + archive_path = Path(archive) + checksum = compute_sha256(archive_path) + size = get_file_size(archive_path) + + for host in hosts: + update_json_for_host(tmp_json_path, version, host, None, archive_path.name, checksum, size) + +def update_json_from_release(tmp_json_path, version, release_info): + assets = release_info.get("assets", []) + for asset in assets: + if (asset.get("name").endswith(".tar.gz") or asset.get("name").endswith(".zip")) and "esptool" in asset.get("name"): + asset_fname = asset.get("name") + print(f"Processing asset: {asset_fname}") + hosts = determine_hosts(asset_fname) + if not hosts: + print(f"Skipping unknown archive type: {asset_fname}") + continue + + asset_url = asset.get("browser_download_url") + asset_checksum = asset.get("digest") + asset_size = asset.get("size") + if asset_checksum is None: + asset_checksum = "" + print(f"Asset {asset_fname} has no checksum. Please set the checksum in the JSON file.") + + for host in hosts: + update_json_for_host(tmp_json_path, version, host, asset_url, asset_fname, asset_checksum, asset_size) + +def get_release_info(version): + url = f"https://api.github.com/repos/espressif/esptool/releases/tags/v{version}" + response = requests.get(url) + response.raise_for_status() + return response.json() + +def main(): + parser = argparse.ArgumentParser(description="Repack esptool and update JSON metadata.") + parser.add_argument("version", help="Version of the esptool (e.g. 5.0.dev1)") + parser.add_argument("-l", "--local", dest="base_folder", help="Enable local build mode and set the base folder with unpacked artifacts") + args = parser.parse_args() + + script_dir = Path(__file__).resolve().parent + json_path = (script_dir / "../../package/package_esp32_index.template.json").resolve() + tmp_json_path = Path(str(json_path) + ".tmp") + shutil.copy(json_path, tmp_json_path) + + local_build = args.base_folder is not None + + if local_build: + os.chdir(args.base_folder) + os.environ['COPYFILE_DISABLE'] = 'true' # this disables including resource forks in tar files on macOS + # Clear any existing archive files + for file in Path(args.base_folder).glob("esptool-*.*"): + file.unlink() + archive_files = create_archives(args.version, args.base_folder) + update_json_from_local_build(tmp_json_path, args.version, args.base_folder, archive_files) + else: + release_info = get_release_info(args.version) + update_json_from_release(tmp_json_path, args.version, release_info) + + print(f"Updating esptool version fields to {args.version}") + update_tools_dependencies(tmp_json_path, args.version) + + shutil.move(tmp_json_path, json_path) + print(f"Done. JSON updated at {json_path}") + +if __name__ == "__main__": + main() diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index eecc7c10788..3ae2ff09ee6 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -81,7 +81,7 @@ { "packager": "esp32", "name": "esptool_py", - "version": "4.9.dev3" + "version": "5.0.dev1" }, { "packager": "esp32", @@ -469,56 +469,56 @@ }, { "name": "esptool_py", - "version": "4.9.dev3", + "version": "5.0.dev1", "systems": [ { - "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-linux-amd64.tar.gz", - "archiveFileName": "esptool-v4.9.dev3-linux-amd64.tar.gz", - "checksum": "SHA-256:4ecaf51836cbf4ea3c19840018bfef3b0b8cd8fc3c95f6e1e043ca5bbeab9bf0", - "size": "64958202" + "host": "aarch64-linux-gnu", + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-aarch64.tar.gz", + "archiveFileName": "esptool-v5.0.dev1-linux-aarch64.tar.gz", + "checksum": "SHA-256:bfafa7a7723ebbabfd8b6e3ca5ae00bfead0331de923754aeddb43b2c116a078", + "size": "58241736" }, { - "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-linux-armv7.tar.gz", - "archiveFileName": "esptool-v4.9.dev3-linux-armv7.tar.gz", - "checksum": "SHA-256:fff818573bce483ee793ac83c8211f6abf764aa3350f198228859f696a0a0b36", - "size": "31530030" + "host": "x86_64-pc-linux-gnu", + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-amd64.tar.gz", + "archiveFileName": "esptool-v5.0.dev1-linux-amd64.tar.gz", + "checksum": "SHA-256:acd0486e96586b99d053a1479acbbbfcae8667227c831cdc53a171f9ccfa27ee", + "size": "100740042" }, { - "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-linux-aarch64.tar.gz", - "archiveFileName": "esptool-v4.9.dev3-linux-aarch64.tar.gz", - "checksum": "SHA-256:5b274bdff2f62e6a07c3c1dfa51b1128924621f661747eca3dbe0f77972f2f06", - "size": "33663882" + "host": "arm-linux-gnueabihf", + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-armv7.tar.gz", + "archiveFileName": "esptool-v5.0.dev1-linux-armv7.tar.gz", + "checksum": "SHA-256:ea77a38681506761bbb7b0b39c130811ed565667b67ebbdb4d6dcc6cb6e07368", + "size": "53451939" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-macos-amd64.tar.gz", - "archiveFileName": "esptool-v4.9.dev3-macos-amd64.tar.gz", - "checksum": "SHA-256:c733c83b58fcf5f642fbb2fddb8ff24640c2c785126cba0821fb70c4a5ceea7a", - "size": "32767836" + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-macos-amd64.tar.gz", + "archiveFileName": "esptool-v5.0.dev1-macos-amd64.tar.gz", + "checksum": "SHA-256:900a8e90731208bee96647e0e207a43612b9452c2120c4fdc0ff4c6be226257b", + "size": "59631998" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-macos-arm64.tar.gz", - "archiveFileName": "esptool-v4.9.dev3-macos-arm64.tar.gz", - "checksum": "SHA-256:83c195a15981e6a5e7a130db2ccfb21e2d8093912e5b003681f9a5abadd71af7", - "size": "30121441" + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-macos-arm64.tar.gz", + "archiveFileName": "esptool-v5.0.dev1-macos-arm64.tar.gz", + "checksum": "SHA-256:3653f4de73cb4fc6a25351eaf663708e91c65ae3265d75bd54ca4315a4350bb4", + "size": "56349992" }, { - "host": "i686-mingw32", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-win64.zip", - "archiveFileName": "esptool-v4.9.dev3-win64.zip", - "checksum": "SHA-256:890051a4fdc684ff6f4af18d0bb27d274ca940ee0eef716a9455f8c64b25b215", - "size": "36072564" + "host": "x86_64-mingw32", + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-win64.zip", + "archiveFileName": "esptool-v5.0.dev1-win64.zip", + "checksum": "SHA-256:1e8fd89645daf94f2d4406ec73c9004e617ea921079515f9fd749205eece4d6d", + "size": "59102658" }, { - "host": "x86_64-mingw32", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.1.0-RC3/esptool-v4.9.dev3-win64.zip", - "archiveFileName": "esptool-v4.9.dev3-win64.zip", - "checksum": "SHA-256:890051a4fdc684ff6f4af18d0bb27d274ca940ee0eef716a9455f8c64b25b215", - "size": "36072564" + "host": "i686-mingw32", + "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-win64.zip", + "archiveFileName": "esptool-v5.0.dev1-win64.zip", + "checksum": "SHA-256:1e8fd89645daf94f2d4406ec73c9004e617ea921079515f9fd749205eece4d6d", + "size": "59102658" } ] }, diff --git a/platform.txt b/platform.txt index f41a8b4a764..8bfa69a79e0 100644 --- a/platform.txt +++ b/platform.txt @@ -120,7 +120,7 @@ recipe.hooks.prebuild.2.pattern.windows=cmd /c if not exist "{build.path}\partit recipe.hooks.prebuild.3.pattern.windows=cmd /c if not exist "{build.path}\partitions.csv" COPY "{runtime.platform.path}\tools\partitions\{build.partitions}.csv" "{build.path}\partitions.csv" # Check if custom bootloader exist: source > variant > build.boot -recipe.hooks.prebuild.4.pattern_args=--chip {build.mcu} elf2image --flash_mode {build.flash_mode} --flash_freq {build.img_freq} --flash_size {build.flash_size} -o +recipe.hooks.prebuild.4.pattern_args=--chip {build.mcu} elf2image --flash-mode {build.flash_mode} --flash-freq {build.img_freq} --flash-size {build.flash_size} -o recipe.hooks.prebuild.4.pattern=/usr/bin/env bash -c "[ -f "{build.source.path}"/bootloader.bin ] && cp -f "{build.source.path}"/bootloader.bin "{build.path}"/{build.project_name}.bootloader.bin || ( [ -f "{build.variant.path}"/{build.custom_bootloader}.bin ] && cp "{build.variant.path}"/{build.custom_bootloader}.bin "{build.path}"/{build.project_name}.bootloader.bin || "{tools.esptool_py.path}"/{tools.esptool_py.cmd} {recipe.hooks.prebuild.4.pattern_args} "{build.path}"/{build.project_name}.bootloader.bin "{compiler.sdk.path}"/bin/bootloader_{build.boot}_{build.boot_freq}.elf )" recipe.hooks.prebuild.4.pattern.windows=cmd /c IF EXIST "{build.source.path}\bootloader.bin" ( COPY /y "{build.source.path}\bootloader.bin" "{build.path}\{build.project_name}.bootloader.bin" ) ELSE ( IF EXIST "{build.variant.path}\{build.custom_bootloader}.bin" ( COPY "{build.variant.path}\{build.custom_bootloader}.bin" "{build.path}\{build.project_name}.bootloader.bin" ) ELSE ( "{tools.esptool_py.path}\{tools.esptool_py.cmd}" {recipe.hooks.prebuild.4.pattern_args} "{build.path}\{build.project_name}.bootloader.bin" "{compiler.sdk.path}\bin\bootloader_{build.boot}_{build.boot_freq}.elf" ) ) @@ -164,7 +164,7 @@ recipe.c.combine.pattern="{compiler.path}{compiler.c.elf.cmd}" {compiler.c.elf.f recipe.objcopy.partitions.bin.pattern={tools.gen_esp32part.cmd} -q "{build.path}/partitions.csv" "{build.path}/{build.project_name}.partitions.bin" ## Create bin -recipe.objcopy.bin.pattern_args=--chip {build.mcu} elf2image --flash_mode "{build.flash_mode}" --flash_freq "{build.img_freq}" --flash_size "{build.flash_size}" --elf-sha256-offset 0xb0 -o "{build.path}/{build.project_name}.bin" "{build.path}/{build.project_name}.elf" +recipe.objcopy.bin.pattern_args=--chip {build.mcu} elf2image --flash-mode "{build.flash_mode}" --flash-freq "{build.img_freq}" --flash-size "{build.flash_size}" --elf-sha256-offset 0xb0 -o "{build.path}/{build.project_name}.bin" "{build.path}/{build.project_name}.elf" recipe.objcopy.bin.pattern="{tools.esptool_py.path}/{tools.esptool_py.cmd}" {recipe.objcopy.bin.pattern_args} ## Create Insights Firmware Package @@ -177,7 +177,7 @@ recipe.hooks.objcopy.postobjcopy.2.pattern=/usr/bin/env bash -c "[ ! -d "{build. recipe.hooks.objcopy.postobjcopy.2.pattern.windows=cmd /c if exist "{build.path}\libraries\ESP_SR" if exist "{compiler.sdk.path}\esp_sr\srmodels.bin" COPY /y "{compiler.sdk.path}\esp_sr\srmodels.bin" "{build.path}\srmodels.bin" # Create merged binary -recipe.hooks.objcopy.postobjcopy.3.pattern_args=--chip {build.mcu} merge_bin -o "{build.path}/{build.project_name}.merged.bin" --fill-flash-size {build.flash_size} --flash_mode keep --flash_freq keep --flash_size keep {build.bootloader_addr} "{build.path}/{build.project_name}.bootloader.bin" 0x8000 "{build.path}/{build.project_name}.partitions.bin" 0xe000 "{runtime.platform.path}/tools/partitions/boot_app0.bin" 0x10000 "{build.path}/{build.project_name}.bin" +recipe.hooks.objcopy.postobjcopy.3.pattern_args=--chip {build.mcu} merge-bin -o "{build.path}/{build.project_name}.merged.bin" --pad-to-size {build.flash_size} --flash-mode keep --flash-freq keep --flash-size keep {build.bootloader_addr} "{build.path}/{build.project_name}.bootloader.bin" 0x8000 "{build.path}/{build.project_name}.partitions.bin" 0xe000 "{runtime.platform.path}/tools/partitions/boot_app0.bin" 0x10000 "{build.path}/{build.project_name}.bin" recipe.hooks.objcopy.postobjcopy.3.pattern="{tools.esptool_py.path}/{tools.esptool_py.cmd}" {recipe.hooks.objcopy.postobjcopy.3.pattern_args} ## Save bin @@ -294,14 +294,14 @@ debug.additional_config=debug_config.{build.mcu} tools.esptool_py.upload.protocol=serial tools.esptool_py.upload.params.verbose= tools.esptool_py.upload.params.quiet= -tools.esptool_py.upload.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default_reset --after hard_reset write_flash {upload.erase_cmd} -z --flash_mode keep --flash_freq keep --flash_size keep {build.bootloader_addr} "{build.path}/{build.project_name}.bootloader.bin" 0x8000 "{build.path}/{build.project_name}.partitions.bin" 0xe000 "{runtime.platform.path}/tools/partitions/boot_app0.bin" 0x10000 "{build.path}/{build.project_name}.bin" {upload.extra_flags} +tools.esptool_py.upload.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default-reset --after hard-reset write-flash {upload.erase_cmd} -z --flash-mode keep --flash-freq keep --flash-size keep {build.bootloader_addr} "{build.path}/{build.project_name}.bootloader.bin" 0x8000 "{build.path}/{build.project_name}.partitions.bin" 0xe000 "{runtime.platform.path}/tools/partitions/boot_app0.bin" 0x10000 "{build.path}/{build.project_name}.bin" {upload.extra_flags} tools.esptool_py.upload.pattern="{path}/{cmd}" {upload.pattern_args} ## Program Application ## ------------------- tools.esptool_py.program.params.verbose= tools.esptool_py.program.params.quiet= -tools.esptool_py.program.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default_reset --after hard_reset write_flash -z --flash_mode keep --flash_freq keep --flash_size keep 0x10000 "{build.path}/{build.project_name}.bin" +tools.esptool_py.program.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default-reset --after hard-reset write-flash -z --flash-mode keep --flash-freq keep --flash-size keep 0x10000 "{build.path}/{build.project_name}.bin" tools.esptool_py.program.pattern="{path}/{cmd}" {program.pattern_args} ## Erase Chip (before burning the bootloader) @@ -309,7 +309,7 @@ tools.esptool_py.program.pattern="{path}/{cmd}" {program.pattern_args} tools.esptool_py.erase.protocol=serial tools.esptool_py.erase.params.verbose= tools.esptool_py.erase.params.quiet= -tools.esptool_py.erase.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default_reset --after hard_reset erase_flash +tools.esptool_py.erase.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default-reset --after hard-reset erase-flash tools.esptool_py.erase.pattern="{path}/{cmd}" {erase.pattern_args} ## Burn Bootloader From febca6b2f1d02b9252c2f5edc2aa312129c68720 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Mon, 23 Jun 2025 12:11:32 +0300 Subject: [PATCH 030/102] fix(p4): Update hosted and wifi_remote components --- idf_component.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index 257890257cc..82f14ea554a 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -107,11 +107,11 @@ dependencies: rules: - if: "target in [esp32s3]" espressif/esp_hosted: - version: "^2.0.0" + version: "^2.0.12" rules: - if: "target == esp32p4" espressif/esp_wifi_remote: - version: "~0.9.2" + version: "^0.13.0" rules: - if: "target == esp32p4" espressif/libsodium: From 1c79eb823cf4beb8465b987bacea1347574979db Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Mon, 23 Jun 2025 08:47:49 -0300 Subject: [PATCH 031/102] feat(NimBLE): Add support for NimBLE (#11424) * feat(NimBLE): Add support for NimBLE Co-authored-by: h2zero * ci(pre-commit): Apply automatic fixes * fix(nimble): Fix typo in BLEClient --------- Co-authored-by: h2zero Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- cores/esp32/esp32-hal-bt.c | 6 +- cores/esp32/esp32-hal-misc.c | 11 +- libraries/BLE/README.md | 8 +- .../BLE5_extended_scan/BLE5_extended_scan.ino | 6 +- .../BLE/examples/BLE5_extended_scan/ci.json | 3 +- .../BLE5_multi_advertising.ino | 4 +- .../examples/BLE5_multi_advertising/ci.json | 3 +- .../BLE5_periodic_advertising.ino | 4 +- .../BLE5_periodic_advertising/ci.json | 3 +- .../BLE5_periodic_sync/BLE5_periodic_sync.ino | 6 +- .../BLE/examples/BLE5_periodic_sync/ci.json | 3 +- .../Beacon_Scanner/Beacon_Scanner.ino | 42 +- libraries/BLE/examples/Client/Client.ino | 2 + libraries/BLE/examples/Notify/Notify.ino | 1 + .../Server_multiconnect.ino | 52 +- libraries/BLE/examples/UART/UART.ino | 13 +- libraries/BLE/library.properties | 2 +- libraries/BLE/src/BLE2901.cpp | 47 +- libraries/BLE/src/BLE2901.h | 39 +- libraries/BLE/src/BLE2902.cpp | 73 +- libraries/BLE/src/BLE2902.h | 46 +- libraries/BLE/src/BLE2904.cpp | 36 +- libraries/BLE/src/BLE2904.h | 39 +- libraries/BLE/src/BLEAddress.cpp | 184 ++- libraries/BLE/src/BLEAddress.h | 85 +- libraries/BLE/src/BLEAdvertisedDevice.cpp | 165 ++- libraries/BLE/src/BLEAdvertisedDevice.h | 154 ++- libraries/BLE/src/BLEAdvertising.cpp | 915 +++++++++++-- libraries/BLE/src/BLEAdvertising.h | 210 ++- libraries/BLE/src/BLEBeacon.cpp | 47 +- libraries/BLE/src/BLEBeacon.h | 20 + libraries/BLE/src/BLECharacteristic.cpp | 1198 +++++++++++------ libraries/BLE/src/BLECharacteristic.h | 261 +++- libraries/BLE/src/BLECharacteristicMap.cpp | 76 +- libraries/BLE/src/BLEClient.cpp | 1009 +++++++++++--- libraries/BLE/src/BLEClient.h | 191 ++- libraries/BLE/src/BLEConnInfo.h | 110 ++ libraries/BLE/src/BLEDescriptor.cpp | 297 ++-- libraries/BLE/src/BLEDescriptor.h | 127 +- libraries/BLE/src/BLEDescriptorMap.cpp | 102 +- libraries/BLE/src/BLEDevice.cpp | 869 ++++++++---- libraries/BLE/src/BLEDevice.h | 246 +++- libraries/BLE/src/BLEEddystoneTLM.cpp | 8 +- .../BLE/src/BLEEddystoneTLM.cppwithheadder | 202 --- libraries/BLE/src/BLEEddystoneTLM.h | 10 +- libraries/BLE/src/BLEEddystoneURL.cpp | 12 +- libraries/BLE/src/BLEEddystoneURL.h | 8 + libraries/BLE/src/BLEEddystoneURL.h.orig | 66 - libraries/BLE/src/BLEExceptions.cpp | 5 + libraries/BLE/src/BLEHIDDevice.cpp | 39 +- libraries/BLE/src/BLEHIDDevice.h | 8 +- libraries/BLE/src/BLERemoteCharacteristic.cpp | 975 ++++++++++---- libraries/BLE/src/BLERemoteCharacteristic.h | 140 +- libraries/BLE/src/BLERemoteDescriptor.cpp | 393 +++++- libraries/BLE/src/BLERemoteDescriptor.h | 76 +- libraries/BLE/src/BLERemoteService.cpp | 369 +++-- libraries/BLE/src/BLERemoteService.h | 87 +- libraries/BLE/src/BLEScan.cpp | 690 +++++++--- libraries/BLE/src/BLEScan.h | 148 +- libraries/BLE/src/BLESecurity.cpp | 140 +- libraries/BLE/src/BLESecurity.h | 144 +- libraries/BLE/src/BLEServer.cpp | 748 ++++++++-- libraries/BLE/src/BLEServer.h | 217 ++- libraries/BLE/src/BLEService.cpp | 429 ++++-- libraries/BLE/src/BLEService.h | 126 +- libraries/BLE/src/BLEServiceMap.cpp | 40 +- libraries/BLE/src/BLEUUID.cpp | 375 +++--- libraries/BLE/src/BLEUUID.h | 101 +- libraries/BLE/src/BLEUtils.cpp | 682 ++++++++-- libraries/BLE/src/BLEUtils.h | 113 +- libraries/BLE/src/BLEValue.cpp | 18 +- libraries/BLE/src/BLEValue.h | 25 +- 72 files changed, 9898 insertions(+), 3211 deletions(-) create mode 100644 libraries/BLE/src/BLEConnInfo.h delete mode 100644 libraries/BLE/src/BLEEddystoneTLM.cppwithheadder delete mode 100644 libraries/BLE/src/BLEEddystoneURL.h.orig diff --git a/cores/esp32/esp32-hal-bt.c b/cores/esp32/esp32-hal-bt.c index 5d512d448a3..a2a603bf5b9 100644 --- a/cores/esp32/esp32-hal-bt.c +++ b/cores/esp32/esp32-hal-bt.c @@ -15,7 +15,7 @@ #include "esp32-hal-bt.h" #if SOC_BT_SUPPORTED -#if defined(CONFIG_BT_BLUEDROID_ENABLED) && __has_include("esp_bt.h") +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") #if CONFIG_IDF_TARGET_ESP32 bool btInUse() { @@ -116,7 +116,7 @@ bool btStop() { return false; } -#else // CONFIG_BT_ENABLED +#else // !__has_include("esp_bt.h") || !(defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) bool btStarted() { return false; } @@ -129,6 +129,6 @@ bool btStop() { return false; } -#endif /* CONFIG_BT_ENABLED */ +#endif /* !__has_include("esp_bt.h") || !(defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) */ #endif /* SOC_BT_SUPPORTED */ diff --git a/cores/esp32/esp32-hal-misc.c b/cores/esp32/esp32-hal-misc.c index 9c1eaf6ba25..4c8a4e5beab 100644 --- a/cores/esp32/esp32-hal-misc.c +++ b/cores/esp32/esp32-hal-misc.c @@ -25,12 +25,13 @@ #include "esp_ota_ops.h" #endif //CONFIG_APP_ROLLBACK_ENABLE #include "esp_private/startup_internal.h" -#if defined(CONFIG_BT_BLUEDROID_ENABLED) && SOC_BT_SUPPORTED && __has_include("esp_bt.h") +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && SOC_BT_SUPPORTED && __has_include("esp_bt.h") #include "esp_bt.h" -#endif //CONFIG_BT_BLUEDROID_ENABLED +#endif #include #include "soc/rtc.h" -#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5) +#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) \ + && !defined(CONFIG_IDF_TARGET_ESP32C5) #include "soc/rtc_cntl_reg.h" #include "soc/syscon_reg.h" #endif @@ -245,7 +246,7 @@ bool verifyRollbackLater() { } #endif -#ifdef CONFIG_BT_BLUEDROID_ENABLED +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) #if CONFIG_IDF_TARGET_ESP32 //overwritten in esp32-hal-bt.c bool btInUse() __attribute__((weak)); @@ -307,7 +308,7 @@ void initArduino() { if (err) { log_e("Failed to initialize NVS! Error: %u", err); } -#if defined(CONFIG_BT_BLUEDROID_ENABLED) && SOC_BT_SUPPORTED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && SOC_BT_SUPPORTED if (!btInUse()) { esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); } diff --git a/libraries/BLE/README.md b/libraries/BLE/README.md index eb70ee9ff00..759c8526f0f 100644 --- a/libraries/BLE/README.md +++ b/libraries/BLE/README.md @@ -1,8 +1,12 @@ # ESP32 BLE for Arduino The Arduino IDE provides an excellent library package manager where versions of libraries can be downloaded and installed. This Github project provides the repository for the ESP32 BLE support for Arduino. -The original source of the project, **which is not maintained anymore**, can be found here: https://github.com/nkolban/esp32-snippets +The original source of the Bluedroid project, **which is not maintained anymore**, can be found here: https://github.com/nkolban/esp32-snippets -Issues and questions should be raised here: https://github.com/espressif/arduino-esp32/issues
(please don't use https://github.com/nkolban/esp32-snippets/issues!) +Some parts of the NimBLE implementation are based on the work of h2zero, which can be found here: https://github.com/h2zero/NimBLE-Arduino + +Issues and questions should be raised here: https://github.com/espressif/arduino-esp32/issues
(please don't use https://github.com/nkolban/esp32-snippets/issues or https://github.com/h2zero/NimBLE-Arduino/issues!) Documentation for using the library can be found here: https://github.com/nkolban/esp32-snippets/tree/master/Documentation + +For a more customizable and feature-rich implementation of the NimBLE stack, you can use the [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library. diff --git a/libraries/BLE/examples/BLE5_extended_scan/BLE5_extended_scan.ino b/libraries/BLE/examples/BLE5_extended_scan/BLE5_extended_scan.ino index 42daff86835..e50d7339df6 100644 --- a/libraries/BLE/examples/BLE5_extended_scan/BLE5_extended_scan.ino +++ b/libraries/BLE/examples/BLE5_extended_scan/BLE5_extended_scan.ino @@ -7,8 +7,10 @@ author: chegewara */ -#ifndef SOC_BLE_50_SUPPORTED -#warning "This SoC does not support BLE5. Try using ESP32-C3, or ESP32-S3" +#ifndef CONFIG_BLUEDROID_ENABLED +#error "NimBLE does not support extended scan yet. Try using Bluedroid." +#elif !defined(SOC_BLE_50_SUPPORTED) +#error "This SoC does not support BLE5. Try using ESP32-C3, or ESP32-S3" #else #include diff --git a/libraries/BLE/examples/BLE5_extended_scan/ci.json b/libraries/BLE/examples/BLE5_extended_scan/ci.json index 184cc25a2b0..8e938055cf2 100644 --- a/libraries/BLE/examples/BLE5_extended_scan/ci.json +++ b/libraries/BLE/examples/BLE5_extended_scan/ci.json @@ -1,6 +1,7 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_BLE_50_SUPPORTED=y" + "CONFIG_SOC_BLE_50_SUPPORTED=y", + "CONFIG_BLUEDROID_ENABLED=y" ] } diff --git a/libraries/BLE/examples/BLE5_multi_advertising/BLE5_multi_advertising.ino b/libraries/BLE/examples/BLE5_multi_advertising/BLE5_multi_advertising.ino index c4d614786b0..ee25f8e95ef 100644 --- a/libraries/BLE/examples/BLE5_multi_advertising/BLE5_multi_advertising.ino +++ b/libraries/BLE/examples/BLE5_multi_advertising/BLE5_multi_advertising.ino @@ -6,7 +6,9 @@ author: chegewara */ -#ifndef CONFIG_BT_BLE_50_FEATURES_SUPPORTED +#ifndef CONFIG_BLUEDROID_ENABLED +#error "NimBLE does not support multi advertising yet. Try using Bluedroid." +#elif !defined(CONFIG_BT_BLE_50_FEATURES_SUPPORTED) #error "This SoC does not support BLE5. Try using ESP32-C3, or ESP32-S3" #else diff --git a/libraries/BLE/examples/BLE5_multi_advertising/ci.json b/libraries/BLE/examples/BLE5_multi_advertising/ci.json index 184cc25a2b0..8e938055cf2 100644 --- a/libraries/BLE/examples/BLE5_multi_advertising/ci.json +++ b/libraries/BLE/examples/BLE5_multi_advertising/ci.json @@ -1,6 +1,7 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_BLE_50_SUPPORTED=y" + "CONFIG_SOC_BLE_50_SUPPORTED=y", + "CONFIG_BLUEDROID_ENABLED=y" ] } diff --git a/libraries/BLE/examples/BLE5_periodic_advertising/BLE5_periodic_advertising.ino b/libraries/BLE/examples/BLE5_periodic_advertising/BLE5_periodic_advertising.ino index 0b9d4f87630..effb7efd241 100644 --- a/libraries/BLE/examples/BLE5_periodic_advertising/BLE5_periodic_advertising.ino +++ b/libraries/BLE/examples/BLE5_periodic_advertising/BLE5_periodic_advertising.ino @@ -5,7 +5,9 @@ author: chegewara */ -#ifndef CONFIG_BT_BLE_50_FEATURES_SUPPORTED +#ifndef CONFIG_BLUEDROID_ENABLED +#error "NimBLE does not support periodic advertising yet. Try using Bluedroid." +#elif !defined(CONFIG_BT_BLE_50_FEATURES_SUPPORTED) #error "This SoC does not support BLE5. Try using ESP32-C3, or ESP32-S3" #else #include diff --git a/libraries/BLE/examples/BLE5_periodic_advertising/ci.json b/libraries/BLE/examples/BLE5_periodic_advertising/ci.json index 184cc25a2b0..8e938055cf2 100644 --- a/libraries/BLE/examples/BLE5_periodic_advertising/ci.json +++ b/libraries/BLE/examples/BLE5_periodic_advertising/ci.json @@ -1,6 +1,7 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_BLE_50_SUPPORTED=y" + "CONFIG_SOC_BLE_50_SUPPORTED=y", + "CONFIG_BLUEDROID_ENABLED=y" ] } diff --git a/libraries/BLE/examples/BLE5_periodic_sync/BLE5_periodic_sync.ino b/libraries/BLE/examples/BLE5_periodic_sync/BLE5_periodic_sync.ino index 9e976e6ca6a..a93ebcefec3 100644 --- a/libraries/BLE/examples/BLE5_periodic_sync/BLE5_periodic_sync.ino +++ b/libraries/BLE/examples/BLE5_periodic_sync/BLE5_periodic_sync.ino @@ -7,8 +7,10 @@ author: chegewara */ -#ifndef SOC_BLE_50_SUPPORTED -#warning "This SoC does not support BLE5. Try using ESP32-C3, or ESP32-S3" +#ifndef CONFIG_BLUEDROID_ENABLED +#error "NimBLE does not support periodic sync yet. Try using Bluedroid." +#elif !defined(SOC_BLE_50_SUPPORTED) +#error "This SoC does not support BLE5. Try using ESP32-C3, or ESP32-S3" #else #include #include diff --git a/libraries/BLE/examples/BLE5_periodic_sync/ci.json b/libraries/BLE/examples/BLE5_periodic_sync/ci.json index 184cc25a2b0..8e938055cf2 100644 --- a/libraries/BLE/examples/BLE5_periodic_sync/ci.json +++ b/libraries/BLE/examples/BLE5_periodic_sync/ci.json @@ -1,6 +1,7 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_BLE_50_SUPPORTED=y" + "CONFIG_SOC_BLE_50_SUPPORTED=y", + "CONFIG_BLUEDROID_ENABLED=y" ] } diff --git a/libraries/BLE/examples/Beacon_Scanner/Beacon_Scanner.ino b/libraries/BLE/examples/Beacon_Scanner/Beacon_Scanner.ino index fbbf89ad274..4b4a311a37d 100644 --- a/libraries/BLE/examples/Beacon_Scanner/Beacon_Scanner.ino +++ b/libraries/BLE/examples/Beacon_Scanner/Beacon_Scanner.ino @@ -36,25 +36,33 @@ class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { if (advertisedDevice.haveManufacturerData() == true) { String strManufacturerData = advertisedDevice.getManufacturerData(); - uint8_t cManufacturerData[100]; - memcpy(cManufacturerData, strManufacturerData.c_str(), strManufacturerData.length()); + // Buffer to store manufacturer data (BLE max is 255 bytes) + uint8_t cManufacturerData[255]; + size_t dataLength = strManufacturerData.length(); - if (strManufacturerData.length() == 25 && cManufacturerData[0] == 0x4C && cManufacturerData[1] == 0x00) { - Serial.println("Found an iBeacon!"); - BLEBeacon oBeacon = BLEBeacon(); - oBeacon.setData(strManufacturerData); - Serial.printf("iBeacon Frame\n"); - Serial.printf( - "ID: %04X Major: %d Minor: %d UUID: %s Power: %d\n", oBeacon.getManufacturerId(), ENDIAN_CHANGE_U16(oBeacon.getMajor()), - ENDIAN_CHANGE_U16(oBeacon.getMinor()), oBeacon.getProximityUUID().toString().c_str(), oBeacon.getSignalPower() - ); - } else { - Serial.println("Found another manufacturers beacon!"); - Serial.printf("strManufacturerData: %d ", strManufacturerData.length()); - for (int i = 0; i < strManufacturerData.length(); i++) { - Serial.printf("[%X]", cManufacturerData[i]); + // Bounds checking to prevent buffer overflow + if (dataLength <= sizeof(cManufacturerData)) { + memcpy(cManufacturerData, strManufacturerData.c_str(), dataLength); + + if (dataLength == 25 && cManufacturerData[0] == 0x4C && cManufacturerData[1] == 0x00) { + Serial.println("Found an iBeacon!"); + BLEBeacon oBeacon = BLEBeacon(); + oBeacon.setData(strManufacturerData); + Serial.printf("iBeacon Frame\n"); + Serial.printf( + "ID: %04X Major: %d Minor: %d UUID: %s Power: %d\n", oBeacon.getManufacturerId(), ENDIAN_CHANGE_U16(oBeacon.getMajor()), + ENDIAN_CHANGE_U16(oBeacon.getMinor()), oBeacon.getProximityUUID().toString().c_str(), oBeacon.getSignalPower() + ); + } else { + Serial.println("Found another manufacturers beacon!"); + Serial.printf("strManufacturerData: %d ", dataLength); + for (int i = 0; i < dataLength; i++) { + Serial.printf("[%X]", cManufacturerData[i]); + } + Serial.printf("\n"); } - Serial.printf("\n"); + } else { + Serial.printf("Manufacturer data too large (%d bytes), skipping\n", dataLength); } } diff --git a/libraries/BLE/examples/Client/Client.ino b/libraries/BLE/examples/Client/Client.ino index ce2eb2ff7d1..713bfda8e93 100644 --- a/libraries/BLE/examples/Client/Client.ino +++ b/libraries/BLE/examples/Client/Client.ino @@ -19,6 +19,7 @@ static boolean doScan = false; static BLERemoteCharacteristic *pRemoteCharacteristic; static BLEAdvertisedDevice *myDevice; +// Callback function to handle notifications static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) { Serial.print("Notify callback for characteristic "); Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str()); @@ -80,6 +81,7 @@ bool connectToServer() { } if (pRemoteCharacteristic->canNotify()) { + // Register/Subscribe for notifications pRemoteCharacteristic->registerForNotify(notifyCallback); } diff --git a/libraries/BLE/examples/Notify/Notify.ino b/libraries/BLE/examples/Notify/Notify.ino index 6b552b01d11..5f267685dfa 100644 --- a/libraries/BLE/examples/Notify/Notify.ino +++ b/libraries/BLE/examples/Notify/Notify.ino @@ -69,6 +69,7 @@ void setup() { ); // Creates BLE Descriptor 0x2902: Client Characteristic Configuration Descriptor (CCCD) + // Descriptor 2902 is not required when using NimBLE as it is automatically added based on the characteristic properties pCharacteristic->addDescriptor(new BLE2902()); // Adds also the Characteristic User Description - 0x2901 descriptor descriptor_2901 = new BLE2901(); diff --git a/libraries/BLE/examples/Server_multiconnect/Server_multiconnect.ino b/libraries/BLE/examples/Server_multiconnect/Server_multiconnect.ino index afd15e9ae4f..ff8712d8281 100644 --- a/libraries/BLE/examples/Server_multiconnect/Server_multiconnect.ino +++ b/libraries/BLE/examples/Server_multiconnect/Server_multiconnect.ino @@ -5,6 +5,9 @@ updated by chegewara Create a BLE server that, once we receive a connection, will send periodic notifications. + The server will continue advertising for more connections after the first one and will notify + the value of a counter to all connected clients. + The service advertises itself as: 4fafc201-1fb5-459e-8fcc-c5c9c331914b And has a characteristic of: beb5483e-36e1-4688-b7f5-ea07361b26a8 @@ -26,8 +29,8 @@ BLEServer *pServer = NULL; BLECharacteristic *pCharacteristic = NULL; +int connectedClients = 0; bool deviceConnected = false; -bool oldDeviceConnected = false; uint32_t value = 0; // See the following for generating UUIDs: @@ -38,12 +41,17 @@ uint32_t value = 0; class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer *pServer) { - deviceConnected = true; + connectedClients++; + Serial.print("Client connected. Total clients: "); + Serial.println(connectedClients); + // Continue advertising for more connections BLEDevice::startAdvertising(); }; void onDisconnect(BLEServer *pServer) { - deviceConnected = false; + connectedClients--; + Serial.print("Client disconnected. Total clients: "); + Serial.println(connectedClients); } }; @@ -66,8 +74,7 @@ void setup() { BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_INDICATE ); - // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml - // Create a BLE Descriptor + // Descriptor 2902 is not required when using NimBLE as it is automatically added based on the characteristic properties pCharacteristic->addDescriptor(new BLE2902()); // Start the service @@ -79,27 +86,38 @@ void setup() { pAdvertising->setScanResponse(false); pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter BLEDevice::startAdvertising(); - Serial.println("Waiting a client connection to notify..."); + Serial.println("Waiting for client connections to notify..."); } void loop() { - // notify changed value - if (deviceConnected) { + // Notify changed value to all connected clients + if (connectedClients > 0) { + Serial.print("Notifying value: "); + Serial.print(value); + Serial.print(" to "); + Serial.print(connectedClients); + Serial.println(" client(s)"); pCharacteristic->setValue((uint8_t *)&value, 4); pCharacteristic->notify(); value++; - delay(10); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms + // Bluetooth stack will go into congestion, if too many packets are sent. + // In 6 hours of testing, I was able to go as low as 3ms. + // When using core debug level "debug" or "verbose", the delay can be increased in + // order to reduce the number of debug messages in the serial monitor. + delay(100); } - // disconnecting - if (!deviceConnected && oldDeviceConnected) { + + // Disconnecting - restart advertising when no clients are connected + if (connectedClients == 0 && deviceConnected) { delay(500); // give the bluetooth stack the chance to get things ready pServer->startAdvertising(); // restart advertising - Serial.println("start advertising"); - oldDeviceConnected = deviceConnected; + Serial.println("No clients connected, restarting advertising"); + deviceConnected = false; } - // connecting - if (deviceConnected && !oldDeviceConnected) { - // do stuff here on connecting - oldDeviceConnected = deviceConnected; + + // Connecting - update state when first client connects + if (connectedClients > 0 && !deviceConnected) { + // do stuff here on first connecting + deviceConnected = true; } } diff --git a/libraries/BLE/examples/UART/UART.ino b/libraries/BLE/examples/UART/UART.ino index 71cc850db66..d17ab494eca 100644 --- a/libraries/BLE/examples/UART/UART.ino +++ b/libraries/BLE/examples/UART/UART.ino @@ -40,10 +40,12 @@ uint8_t txValue = 0; class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer *pServer) { deviceConnected = true; + Serial.println("Device connected"); }; void onDisconnect(BLEServer *pServer) { deviceConnected = false; + Serial.println("Device disconnected"); } }; @@ -80,6 +82,7 @@ void setup() { // Create a BLE Characteristic pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY); + // Descriptor 2902 is not required when using NimBLE as it is automatically added based on the characteristic properties pTxCharacteristic->addDescriptor(new BLE2902()); BLECharacteristic *pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); @@ -97,22 +100,24 @@ void setup() { void loop() { if (deviceConnected) { + Serial.print("Notifying Value: "); + Serial.println(txValue); pTxCharacteristic->setValue(&txValue, 1); pTxCharacteristic->notify(); txValue++; - delay(10); // bluetooth stack will go into congestion, if too many packets are sent + delay(1000); // Notifying every 1 second } // disconnecting if (!deviceConnected && oldDeviceConnected) { delay(500); // give the bluetooth stack the chance to get things ready pServer->startAdvertising(); // restart advertising - Serial.println("start advertising"); - oldDeviceConnected = deviceConnected; + Serial.println("Started advertising again..."); + oldDeviceConnected = false; } // connecting if (deviceConnected && !oldDeviceConnected) { // do stuff here on connecting - oldDeviceConnected = deviceConnected; + oldDeviceConnected = true; } } diff --git a/libraries/BLE/library.properties b/libraries/BLE/library.properties index 7ef636223ec..fe39f5093c4 100644 --- a/libraries/BLE/library.properties +++ b/libraries/BLE/library.properties @@ -1,7 +1,7 @@ name=BLE version=3.2.0 author=Neil Kolban -maintainer=Dariusz Krempa +maintainer=lucasssvaz sentence=BLE functions for ESP32 paragraph=This library provides an implementation Bluetooth Low Energy support for the ESP32 using the Arduino platform. category=Communication diff --git a/libraries/BLE/src/BLE2901.cpp b/libraries/BLE/src/BLE2901.cpp index e929262b023..1fa4857ac33 100644 --- a/libraries/BLE/src/BLE2901.cpp +++ b/libraries/BLE/src/BLE2901.cpp @@ -1,29 +1,48 @@ /* - BLE2901.h + BLE2901.h - GATT Descriptor 0x2901 Characteristic User Description + GATT Descriptor 0x2901 Characteristic User Description - The value of this description is a user-readable string - describing the characteristic. + The value of this description is a user-readable string + describing the characteristic. - The Characteristic User Description descriptor - provides a textual user description for a characteristic - value. - If the Writable Auxiliary bit of the Characteristics - Properties is set then this descriptor is written. Only one - User Description descriptor exists in a characteristic - definition. + The Characteristic User Description descriptor + provides a textual user description for a characteristic + value. + If the Writable Auxiliary bit of the Characteristics + Properties is set then this descriptor is written. Only one + User Description descriptor exists in a characteristic + definition. */ #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes and definitions * + ***************************************************************************/ #include "BLE2901.h" -BLE2901::BLE2901() : BLEDescriptor(BLEUUID((uint16_t)0x2901)) {} // BLE2901 +#define BLE2901_UUID 0x2901 + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#define ESP_GATT_MAX_ATTR_LEN BLE_ATT_ATTR_MAX_LEN +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + +BLE2901::BLE2901() : BLEDescriptor(BLEUUID((uint16_t)BLE2901_UUID)) {} /** * @brief Set the Characteristic User Description @@ -36,5 +55,5 @@ void BLE2901::setDescription(String userDesc) { setValue(userDesc); } -#endif +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLE2901.h b/libraries/BLE/src/BLE2901.h index f5ad7c94add..21e7cc9398c 100644 --- a/libraries/BLE/src/BLE2901.h +++ b/libraries/BLE/src/BLE2901.h @@ -1,37 +1,48 @@ /* - BLE2901.h + BLE2901.h - GATT Descriptor 0x2901 Characteristic User Description + GATT Descriptor 0x2901 Characteristic User Description - The value of this description is a user-readable string - describing the characteristic. - - The Characteristic User Description descriptor - provides a textual user description for a characteristic - value. - If the Writable Auxiliary bit of the Characteristics - Properties is set then this descriptor is written. Only one - User Description descriptor exists in a characteristic - definition. + The value of this description is a user-readable string + describing the characteristic. + The Characteristic User Description descriptor + provides a textual user description for a characteristic + value. + If the Writable Auxiliary bit of the Characteristics + Properties is set then this descriptor is written. Only one + User Description descriptor exists in a characteristic + definition. */ #ifndef COMPONENTS_CPP_UTILS_BLE2901_H_ #define COMPONENTS_CPP_UTILS_BLE2901_H_ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include "BLEDescriptor.h" +/** + * @brief GATT Descriptor 0x2901 Characteristic User Description + */ class BLE2901 : public BLEDescriptor { public: + /*************************************************************************** + * Common public functions * + ***************************************************************************/ + BLE2901(); void setDescription(String desc); }; // BLE2901 -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLE2901_H_ */ diff --git a/libraries/BLE/src/BLE2902.cpp b/libraries/BLE/src/BLE2902.cpp index 90cdf088ff2..3bac9281328 100644 --- a/libraries/BLE/src/BLE2902.cpp +++ b/libraries/BLE/src/BLE2902.cpp @@ -3,46 +3,86 @@ * * Created on: Jun 25, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ /* * See also: * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes and definitions * + ***************************************************************************/ #include "BLE2902.h" -BLE2902::BLE2902() : BLEDescriptor(BLEUUID((uint16_t)0x2902)) { +#define BLE2902_UUID 0x2902 + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + +BLE2902::BLE2902() : BLEDescriptor(BLEUUID((uint16_t)BLE2902_UUID)) { +#if defined(CONFIG_BLUEDROID_ENABLED) uint8_t data[2] = {0, 0}; setValue(data, 2); -} // BLE2902 +#endif +} /** * @brief Get the notifications value. * @return The notifications value. True if notifications are enabled and false if not. */ bool BLE2902::getNotifications() { +#if defined(CONFIG_BLUEDROID_ENABLED) return (getValue()[0] & (1 << 0)) != 0; -} // getNotifications +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_pCharacteristic != nullptr) { + return (m_pCharacteristic->getProperties() & BLECharacteristic::PROPERTY_NOTIFY) != 0; + } else { + log_w("BLE2902::getNotifications() called on an uninitialized descriptor"); + return false; + } +#endif +} /** * @brief Get the indications value. * @return The indications value. True if indications are enabled and false if not. */ bool BLE2902::getIndications() { +#if defined(CONFIG_BLUEDROID_ENABLED) return (getValue()[0] & (1 << 1)) != 0; -} // getIndications +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_pCharacteristic != nullptr) { + return (m_pCharacteristic->getProperties() & BLECharacteristic::PROPERTY_INDICATE) != 0; + } else { + log_w("BLE2902::getIndications() called on an uninitialized descriptor"); + return false; + } +#endif +} /** * @brief Set the indications flag. * @param [in] flag The indications flag. */ void BLE2902::setIndications(bool flag) { +#if defined(CONFIG_BLUEDROID_ENABLED) uint8_t *pValue = getValue(); if (flag) { pValue[0] |= 1 << 1; @@ -50,13 +90,23 @@ void BLE2902::setIndications(bool flag) { pValue[0] &= ~(1 << 1); } setValue(pValue, 2); -} // setIndications +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_pCharacteristic != nullptr) { + m_pCharacteristic->setIndicateProperty(flag); + } else { + log_w("BLE2902::setIndications() called on an uninitialized descriptor"); + } +#endif +} /** * @brief Set the notifications flag. * @param [in] flag The notifications flag. */ void BLE2902::setNotifications(bool flag) { +#if defined(CONFIG_BLUEDROID_ENABLED) uint8_t *pValue = getValue(); if (flag) { pValue[0] |= 1 << 0; @@ -64,7 +114,16 @@ void BLE2902::setNotifications(bool flag) { pValue[0] &= ~(1 << 0); } setValue(pValue, 2); -} // setNotifications +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_pCharacteristic != nullptr) { + m_pCharacteristic->setNotifyProperty(flag); + } else { + log_w("BLE2902::setNotifications() called on an uninitialized descriptor"); + } #endif +} + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLE2902.h b/libraries/BLE/src/BLE2902.h index 74a477f3151..5cbf911ea4a 100644 --- a/libraries/BLE/src/BLE2902.h +++ b/libraries/BLE/src/BLE2902.h @@ -3,15 +3,24 @@ * * Created on: Jun 25, 2017 * Author: kolban + * + * Modified on: Feb 28, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLE2902_H_ #define COMPONENTS_CPP_UTILS_BLE2902_H_ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include "BLEDescriptor.h" @@ -23,16 +32,35 @@ * See also: * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml */ + +// Class declaration for Bluedroid +#if defined(CONFIG_BLUEDROID_ENABLED) class BLE2902 : public BLEDescriptor { -public: - BLE2902(); - bool getNotifications(); - bool getIndications(); - void setNotifications(bool flag); - void setIndications(bool flag); +#endif + +// Class declaration for NimBLE (deprecated) +#if defined(CONFIG_NIMBLE_ENABLED) + class [[deprecated("NimBLE does not support manually adding 2902 descriptors as they \ +are automatically added when the characteristic has notifications or indications enabled. \ +Get/Set the notifications/indications properties of the characteristic instead. \ +This class will be removed in a future version.")]] BLE2902 : public BLEDescriptor { +#endif + + public: + /*************************************************************************** + * Common public functions * + ***************************************************************************/ + + BLE2902(); + bool getNotifications(); + bool getIndications(); + void setNotifications(bool flag); + void setIndications(bool flag); -}; // BLE2902 + private: + friend class BLECharacteristic; + }; // BLE2902 -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLE2902_H_ */ diff --git a/libraries/BLE/src/BLE2904.cpp b/libraries/BLE/src/BLE2904.cpp index aeed11ebad1..19658051120 100644 --- a/libraries/BLE/src/BLE2904.cpp +++ b/libraries/BLE/src/BLE2904.cpp @@ -3,6 +3,10 @@ * * Created on: Dec 23, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ /* @@ -13,18 +17,30 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes and definitions * + ***************************************************************************/ #include "BLE2904.h" -BLE2904::BLE2904() : BLEDescriptor(BLEUUID((uint16_t)0x2904)) { +#define BLE2904_UUID 0x2904 +#define BLE2904_DEFAULT_NAMESPACE 1 // 1 = Bluetooth SIG Assigned Numbers +#define BLE2904_DEFAULT_UNIT 0x2700 // 0x2700 = Unitless + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + +BLE2904::BLE2904() : BLEDescriptor(BLEUUID((uint16_t)BLE2904_UUID)) { m_data.m_format = 0; m_data.m_exponent = 0; - m_data.m_namespace = 1; // 1 = Bluetooth SIG Assigned Numbers - m_data.m_unit = 0; + m_data.m_namespace = BLE2904_DEFAULT_NAMESPACE; + m_data.m_unit = BLE2904_DEFAULT_UNIT; m_data.m_description = 0; setValue((uint8_t *)&m_data, sizeof(m_data)); -} // BLE2902 +} /** * @brief Set the description. @@ -40,7 +56,7 @@ void BLE2904::setDescription(uint16_t description) { void BLE2904::setExponent(int8_t exponent) { m_data.m_exponent = exponent; setValue((uint8_t *)&m_data, sizeof(m_data)); -} // setExponent +} /** * @brief Set the format. @@ -48,7 +64,7 @@ void BLE2904::setExponent(int8_t exponent) { void BLE2904::setFormat(uint8_t format) { m_data.m_format = format; setValue((uint8_t *)&m_data, sizeof(m_data)); -} // setFormat +} /** * @brief Set the namespace. @@ -56,7 +72,7 @@ void BLE2904::setFormat(uint8_t format) { void BLE2904::setNamespace(uint8_t namespace_value) { m_data.m_namespace = namespace_value; setValue((uint8_t *)&m_data, sizeof(m_data)); -} // setNamespace +} /** * @brief Set the units for this value. It should be one of the encoded values defined here: @@ -66,7 +82,7 @@ void BLE2904::setNamespace(uint8_t namespace_value) { void BLE2904::setUnit(uint16_t unit) { m_data.m_unit = unit; setValue((uint8_t *)&m_data, sizeof(m_data)); -} // setUnit +} -#endif +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLE2904.h b/libraries/BLE/src/BLE2904.h index 3ba66da0dc8..87e8c03c048 100644 --- a/libraries/BLE/src/BLE2904.h +++ b/libraries/BLE/src/BLE2904.h @@ -3,27 +3,43 @@ * * Created on: Dec 23, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLE2904_H_ #define COMPONENTS_CPP_UTILS_BLE2904_H_ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include "BLEDescriptor.h" +/*************************************************************************** + * Common types * + ***************************************************************************/ + struct BLE2904_Data { uint8_t m_format; int8_t m_exponent; uint16_t m_unit; // See https://www.bluetooth.com/specifications/assigned-numbers/units uint8_t m_namespace; uint16_t m_description; - } __attribute__((packed)); +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief Descriptor for Characteristic Presentation Format. * @@ -34,7 +50,10 @@ struct BLE2904_Data { */ class BLE2904 : public BLEDescriptor { public: - BLE2904(); + /*************************************************************************** + * Common public constants * + ***************************************************************************/ + static const uint8_t FORMAT_BOOLEAN = 1; static const uint8_t FORMAT_UINT2 = 2; static const uint8_t FORMAT_UINT4 = 3; @@ -62,7 +81,13 @@ class BLE2904 : public BLEDescriptor { static const uint8_t FORMAT_UTF8 = 25; static const uint8_t FORMAT_UTF16 = 26; static const uint8_t FORMAT_OPAQUE = 27; + static const uint8_t FORMAT_MEDASN1 = 28; + + /*************************************************************************** + * Common public functions * + ***************************************************************************/ + BLE2904(); void setDescription(uint16_t); void setExponent(int8_t exponent); void setFormat(uint8_t format); @@ -70,9 +95,15 @@ class BLE2904 : public BLEDescriptor { void setUnit(uint16_t unit); private: + friend class BLECharacteristic; + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + BLE2904_Data m_data; }; // BLE2904 -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLE2904_H_ */ diff --git a/libraries/BLE/src/BLEAddress.cpp b/libraries/BLE/src/BLEAddress.cpp index b91ef3cc4de..11c64850367 100644 --- a/libraries/BLE/src/BLEAddress.cpp +++ b/libraries/BLE/src/BLEAddress.cpp @@ -3,12 +3,21 @@ * * Created on: Jul 2, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include "BLEAddress.h" #include @@ -17,43 +26,33 @@ #include #include #include + #ifdef ARDUINO_ARCH_ESP32 #include "esp32-hal-log.h" #endif -/** - * @brief Create an address from the native ESP32 representation. - * @param [in] address The native representation. - */ -BLEAddress::BLEAddress(esp_bd_addr_t address) { - memcpy(m_address, address, ESP_BD_ADDR_LEN); -} // BLEAddress +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ -/** - * @brief Create an address from a hex string - * - * A hex string is of the format: - * ``` - * 00:00:00:00:00:00 - * ``` - * which is 17 characters in length. - * - * @param [in] stringAddress The hex representation of the address. - */ -BLEAddress::BLEAddress(String stringAddress) { - if (stringAddress.length() != 17) { - return; - } +#if defined(CONFIG_NIMBLE_ENABLED) +/************************************************* + * NOTE: NimBLE address bytes are in INVERSE ORDER! + * We will accommodate that fact in these methods. +*************************************************/ +#include +#endif - int data[6]; - sscanf(stringAddress.c_str(), "%x:%x:%x:%x:%x:%x", &data[0], &data[1], &data[2], &data[3], &data[4], &data[5]); - m_address[0] = (uint8_t)data[0]; - m_address[1] = (uint8_t)data[1]; - m_address[2] = (uint8_t)data[2]; - m_address[3] = (uint8_t)data[3]; - m_address[4] = (uint8_t)data[4]; - m_address[5] = (uint8_t)data[5]; -} // BLEAddress +/*************************************************************************** + * Common functions * + ***************************************************************************/ + +BLEAddress::BLEAddress() { + memset(m_address, 0, ESP_BD_ADDR_LEN); +#if defined(CONFIG_NIMBLE_ENABLED) + m_addrType = 0; +#endif +} /** * @brief Determine if this address equals another. @@ -61,10 +60,20 @@ BLEAddress::BLEAddress(String stringAddress) { * @return True if the addresses are equal. */ bool BLEAddress::equals(BLEAddress otherAddress) { +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_addrType != otherAddress.m_addrType) { + return false; + } +#endif return memcmp(otherAddress.getNative(), m_address, ESP_BD_ADDR_LEN) == 0; -} // equals +} bool BLEAddress::operator==(const BLEAddress &otherAddress) const { +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_addrType != otherAddress.m_addrType) { + return false; + } +#endif return memcmp(otherAddress.m_address, m_address, ESP_BD_ADDR_LEN) == 0; } @@ -92,9 +101,9 @@ bool BLEAddress::operator>(const BLEAddress &otherAddress) const { * @brief Return the native representation of the address. * @return The native representation of the address. */ -esp_bd_addr_t *BLEAddress::getNative() { - return &m_address; -} // getNative +uint8_t *BLEAddress::getNative() { + return m_address; +} /** * @brief Convert a BLE address to a string. @@ -110,11 +119,112 @@ esp_bd_addr_t *BLEAddress::getNative() { String BLEAddress::toString() { auto size = 18; char *res = (char *)malloc(size); + +#if defined(CONFIG_BLUEDROID_ENABLED) snprintf(res, size, "%02x:%02x:%02x:%02x:%02x:%02x", m_address[0], m_address[1], m_address[2], m_address[3], m_address[4], m_address[5]); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + snprintf(res, size, "%02x:%02x:%02x:%02x:%02x:%02x", m_address[5], m_address[4], m_address[3], m_address[2], m_address[1], m_address[0]); +#endif + String ret(res); free(res); return ret; -} // toString +} + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +/** + * @brief Create an address from the native ESP32 representation. + * @param [in] address The native representation. + */ +BLEAddress::BLEAddress(esp_bd_addr_t address) { + memcpy(m_address, address, ESP_BD_ADDR_LEN); +} + +/** + * @brief Create an address from a hex string + * + * A hex string is of the format: + * ``` + * 00:00:00:00:00:00 + * ``` + * which is 17 characters in length. + * + * @param [in] stringAddress The hex representation of the address. + */ +BLEAddress::BLEAddress(String stringAddress) { + if (stringAddress.length() != 17) { + return; + } + + int data[6]; + sscanf(stringAddress.c_str(), "%x:%x:%x:%x:%x:%x", &data[0], &data[1], &data[2], &data[3], &data[4], &data[5]); + + for (size_t index = 0; index < sizeof(m_address); index++) { + m_address[index] = (uint8_t)data[index]; + } +} + +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +/************************************************* + * NOTE: NimBLE address bytes are in INVERSE ORDER! + * We will accommodate that fact in these methods. +*************************************************/ + +BLEAddress::BLEAddress(uint8_t address[ESP_BD_ADDR_LEN], uint8_t type) { + std::reverse_copy(address, address + sizeof(m_address), m_address); + m_addrType = type; +} + +BLEAddress::BLEAddress(ble_addr_t address) { + memcpy(m_address, address.val, ESP_BD_ADDR_LEN); + m_addrType = address.type; +} + +uint8_t BLEAddress::getType() { + return m_addrType; +} + +/** + * @brief Create an address from a hex string + * + * A hex string is of the format: + * ``` + * 00:00:00:00:00:00 + * ``` + * which is 17 characters in length. + * + * @param [in] stringAddress The hex representation of the address. + * @param [in] type The address type. + */ +BLEAddress::BLEAddress(String stringAddress, uint8_t type) { + if (stringAddress.length() != 17) { + return; + } + + int data[6]; + m_addrType = type; + // NimBLE addresses are in INVERSE ORDER! + sscanf(stringAddress.c_str(), "%x:%x:%x:%x:%x:%x", &data[5], &data[4], &data[3], &data[2], &data[1], &data[0]); + + for (size_t index = 0; index < sizeof(m_address); index++) { + m_address[index] = (uint8_t)data[index]; + } +} #endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEAddress.h b/libraries/BLE/src/BLEAddress.h index f1c8aa9b632..71b3bdfce5f 100644 --- a/libraries/BLE/src/BLEAddress.h +++ b/libraries/BLE/src/BLEAddress.h @@ -3,19 +3,53 @@ * * Created on: Jul 2, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEADDRESS_H_ #define COMPONENTS_CPP_UTILS_BLEADDRESS_H_ #include "soc/soc_caps.h" -#include "WString.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include // ESP32 BLE +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + +#include "WString.h" +#include #include +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#define ESP_BD_ADDR_LEN BLE_DEV_ADDR_LEN +#endif + +/*************************************************************************** + * NimBLE types * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +typedef uint8_t esp_bd_addr_t[ESP_BD_ADDR_LEN]; +#endif + /** * @brief A %BLE device address. * @@ -23,8 +57,11 @@ */ class BLEAddress { public: - BLEAddress(esp_bd_addr_t address); - BLEAddress(String stringAddress); + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + + BLEAddress(); bool equals(BLEAddress otherAddress); bool operator==(const BLEAddress &otherAddress) const; bool operator!=(const BLEAddress &otherAddress) const; @@ -32,13 +69,45 @@ class BLEAddress { bool operator<=(const BLEAddress &otherAddress) const; bool operator>(const BLEAddress &otherAddress) const; bool operator>=(const BLEAddress &otherAddress) const; - esp_bd_addr_t *getNative(); + uint8_t *getNative(); String toString(); + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + BLEAddress(esp_bd_addr_t address); + BLEAddress(String stringAddress); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + BLEAddress(ble_addr_t address); + BLEAddress(String stringAddress, uint8_t type = BLE_ADDR_PUBLIC); + BLEAddress(uint8_t address[ESP_BD_ADDR_LEN], uint8_t type = BLE_ADDR_PUBLIC); + uint8_t getType(); +#endif + private: - esp_bd_addr_t m_address; + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + + uint8_t m_address[ESP_BD_ADDR_LEN]; + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + uint8_t m_addrType; +#endif }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEADDRESS_H_ */ diff --git a/libraries/BLE/src/BLEAdvertisedDevice.cpp b/libraries/BLE/src/BLEAdvertisedDevice.cpp index 8752d24a199..3ac1dfab5c8 100644 --- a/libraries/BLE/src/BLEAdvertisedDevice.cpp +++ b/libraries/BLE/src/BLEAdvertisedDevice.cpp @@ -4,22 +4,35 @@ * During the scanning procedure, we will be finding advertised BLE devices. This class * models a found device. * - * * See also: * https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile * * Created on: Jul 3, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include "BLEAdvertisedDevice.h" #include "BLEUtils.h" #include "esp32-hal-log.h" +/*************************************************************************** + * Common functions * + ***************************************************************************/ + BLEAdvertisedDevice::BLEAdvertisedDevice() { m_adFlag = 0; m_appearance = 0; @@ -32,15 +45,47 @@ BLEAdvertisedDevice::BLEAdvertisedDevice() { m_serviceDataUUIDs = {}; m_txPower = 0; m_pScan = nullptr; + m_advType = 0; + +#if defined(CONFIG_NIMBLE_ENABLED) + m_callbackSent = false; +#endif m_haveAppearance = false; m_haveManufacturerData = false; m_haveName = false; m_haveRSSI = false; m_haveTXPower = false; - + m_isLegacyAdv = true; } // BLEAdvertisedDevice +bool BLEAdvertisedDevice::isLegacyAdvertisement() { + return m_isLegacyAdv; +} + +bool BLEAdvertisedDevice::isScannable() { +#if defined(CONFIG_BLUEDROID_ENABLED) + return isLegacyAdvertisement() && (m_advType == ESP_BLE_EVT_CONN_ADV || m_advType == ESP_BLE_EVT_DISC_ADV); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + return isLegacyAdvertisement() && (m_advType == BLE_HCI_ADV_TYPE_ADV_IND || m_advType == BLE_HCI_ADV_TYPE_ADV_SCAN_IND); +#endif +} + +bool BLEAdvertisedDevice::isConnectable() { +#if defined(CONFIG_BLUEDROID_ENABLED) + return m_advType == ESP_BLE_EVT_CONN_ADV || m_advType == ESP_BLE_EVT_CONN_DIR_ADV; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_isLegacyAdv) { + return m_advType == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND || m_advType == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND; + } + return (m_advType & BLE_HCI_ADV_CONN_MASK) || (m_advType & BLE_HCI_ADV_DIRECT_MASK); +#endif +} + /** * @brief Get the address. * @@ -277,75 +322,75 @@ void BLEAdvertisedDevice::parseAdvertisement(uint8_t *payload, size_t total_len) length--; char *pHex = BLEUtils::buildHexData(nullptr, payload, length); - log_d("Type: 0x%.2x (%s), length: %d, data: %s", ad_type, BLEUtils::advTypeToString(ad_type), length, pHex); + log_d("Type: 0x%.2x (%s), length: %d, data: %s", ad_type, BLEUtils::advDataTypeToString(ad_type), length, pHex); free(pHex); switch (ad_type) { - case ESP_BLE_AD_TYPE_NAME_CMPL: - { // Adv Data Type: 0x09 + case ESP_BLE_AD_TYPE_NAME_CMPL: // 0x09 + { // Adv Data Type: ESP_BLE_AD_TYPE_NAME_CMPL setName(String(reinterpret_cast(payload), length)); break; - } // ESP_BLE_AD_TYPE_NAME_CMPL + } // 0x09 - case ESP_BLE_AD_TYPE_TX_PWR: - { // Adv Data Type: 0x0A + case ESP_BLE_AD_TYPE_TX_PWR: // 0x0A + { // Adv Data Type: ESP_BLE_AD_TYPE_TX_PWR setTXPower(*payload); break; - } // ESP_BLE_AD_TYPE_TX_PWR + } // 0x0A - case ESP_BLE_AD_TYPE_APPEARANCE: - { // Adv Data Type: 0x19 + case ESP_BLE_AD_TYPE_APPEARANCE: // 0x19 + { // Adv Data Type: ESP_BLE_AD_TYPE_APPEARANCE setAppearance(*reinterpret_cast(payload)); break; - } // ESP_BLE_AD_TYPE_APPEARANCE + } // 0x19 - case ESP_BLE_AD_TYPE_FLAG: - { // Adv Data Type: 0x01 + case ESP_BLE_AD_TYPE_FLAG: // 0x01 + { // Adv Data Type: ESP_BLE_AD_TYPE_FLAG setAdFlag(*payload); break; - } // ESP_BLE_AD_TYPE_FLAG + } // 0x01 - case ESP_BLE_AD_TYPE_16SRV_CMPL: - case ESP_BLE_AD_TYPE_16SRV_PART: - { // Adv Data Type: 0x02 + case ESP_BLE_AD_TYPE_16SRV_PART: // 0x02 + case ESP_BLE_AD_TYPE_16SRV_CMPL: // 0x03 + { // Adv Data Type: ESP_BLE_AD_TYPE_16SRV_PART/CMPL for (int var = 0; var < length / 2; ++var) { setServiceUUID(BLEUUID(*reinterpret_cast(payload + var * 2))); } break; - } // ESP_BLE_AD_TYPE_16SRV_PART + } // 0x02, 0x03 - case ESP_BLE_AD_TYPE_32SRV_CMPL: - case ESP_BLE_AD_TYPE_32SRV_PART: - { // Adv Data Type: 0x04 + case ESP_BLE_AD_TYPE_32SRV_PART: // 0x04 + case ESP_BLE_AD_TYPE_32SRV_CMPL: // 0x05 + { // Adv Data Type: ESP_BLE_AD_TYPE_32SRV_PART/CMPL for (int var = 0; var < length / 4; ++var) { setServiceUUID(BLEUUID(*reinterpret_cast(payload + var * 4))); } break; - } // ESP_BLE_AD_TYPE_32SRV_PART + } // 0x04, 0x05 - case ESP_BLE_AD_TYPE_128SRV_CMPL: - { // Adv Data Type: 0x07 + case ESP_BLE_AD_TYPE_128SRV_CMPL: // 0x07 + { // Adv Data Type: ESP_BLE_AD_TYPE_128SRV_CMPL setServiceUUID(BLEUUID(payload, 16, false)); break; - } // ESP_BLE_AD_TYPE_128SRV_CMPL + } // 0x07 - case ESP_BLE_AD_TYPE_128SRV_PART: - { // Adv Data Type: 0x06 + case ESP_BLE_AD_TYPE_128SRV_PART: // 0x06 + { // Adv Data Type: ESP_BLE_AD_TYPE_128SRV_PART setServiceUUID(BLEUUID(payload, 16, false)); break; - } // ESP_BLE_AD_TYPE_128SRV_PART + } // 0x06 // See CSS Part A 1.4 Manufacturer Specific Data - case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: + case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: // 0xFF { setManufacturerData(String(reinterpret_cast(payload), length)); break; - } // ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE + } // 0xFF - case ESP_BLE_AD_TYPE_SERVICE_DATA: - { // Adv Data Type: 0x16 (Service Data) - 2 byte UUID + case ESP_BLE_AD_TYPE_SERVICE_DATA: // 0x16 + { // Adv Data Type: ESP_BLE_AD_TYPE_SERVICE_DATA - 2 byte UUID if (length < 2) { - log_e("Length too small for ESP_BLE_AD_TYPE_SERVICE_DATA"); + log_e("Length too small for SERVICE_DATA"); break; } uint16_t uuid = *(uint16_t *)payload; @@ -354,12 +399,12 @@ void BLEAdvertisedDevice::parseAdvertisement(uint8_t *payload, size_t total_len) setServiceData(String(reinterpret_cast(payload + 2), length - 2)); } break; - } //ESP_BLE_AD_TYPE_SERVICE_DATA + } // 0x16 - case ESP_BLE_AD_TYPE_32SERVICE_DATA: - { // Adv Data Type: 0x20 (Service Data) - 4 byte UUID + case ESP_BLE_AD_TYPE_32SERVICE_DATA: // 0x20 + { // Adv Data Type: ESP_BLE_AD_TYPE_32SERVICE_DATA - 4 byte UUID if (length < 4) { - log_e("Length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA"); + log_e("Length too small for 32SERVICE_DATA"); break; } uint32_t uuid = *(uint32_t *)payload; @@ -368,12 +413,12 @@ void BLEAdvertisedDevice::parseAdvertisement(uint8_t *payload, size_t total_len) setServiceData(String(reinterpret_cast(payload + 4), length - 4)); } break; - } //ESP_BLE_AD_TYPE_32SERVICE_DATA + } // 0x20 - case ESP_BLE_AD_TYPE_128SERVICE_DATA: - { // Adv Data Type: 0x21 (Service Data) - 16 byte UUID + case ESP_BLE_AD_TYPE_128SERVICE_DATA: // 0x21 + { // Adv Data Type: ESP_BLE_AD_TYPE_128SERVICE_DATA - 16 byte UUID if (length < 16) { - log_e("Length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA"); + log_e("Length too small for 128SERVICE_DATA"); break; } @@ -382,13 +427,13 @@ void BLEAdvertisedDevice::parseAdvertisement(uint8_t *payload, size_t total_len) setServiceData(String(reinterpret_cast(payload + 16), length - 16)); } break; - } //ESP_BLE_AD_TYPE_32SERVICE_DATA + } // 0x21 default: { log_d("Unhandled type: adType: %d - 0x%.2x", ad_type, ad_type); break; - } + } // default } // switch payload += length; } // Length <> 0 @@ -405,9 +450,19 @@ void BLEAdvertisedDevice::parseAdvertisement(uint8_t *payload, size_t total_len) * @param [in] payload The payload of the advertised device. * @param [in] total_len The length of payload */ -void BLEAdvertisedDevice::setPayload(uint8_t *payload, size_t total_len) { - m_payload = payload; - m_payloadLength = total_len; +void BLEAdvertisedDevice::setPayload(uint8_t *payload, size_t total_len, bool append) { + if (m_payload == nullptr || m_payloadLength == 0) { + return; + } + + if (append) { + m_payload = (uint8_t *)realloc(m_payload, m_payloadLength + total_len); + memcpy(m_payload + m_payloadLength, payload, total_len); + m_payloadLength += total_len; + } else { + m_payload = payload; + m_payloadLength = total_len; + } } // setPayload /** @@ -567,7 +622,7 @@ uint8_t *BLEAdvertisedDevice::getPayload() { return m_payload; } -esp_ble_addr_type_t BLEAdvertisedDevice::getAddressType() { +uint8_t BLEAdvertisedDevice::getAddressType() { return m_addressType; } @@ -587,7 +642,7 @@ ble_frame_type_t BLEAdvertisedDevice::getFrameType() { return BLE_UNKNOWN_FRAME; } -void BLEAdvertisedDevice::setAddressType(esp_ble_addr_type_t type) { +void BLEAdvertisedDevice::setAddressType(uint8_t type) { m_addressType = type; } @@ -595,5 +650,13 @@ size_t BLEAdvertisedDevice::getPayloadLength() { return m_payloadLength; } -#endif /* CONFIG_BLUEDROID_ENABLED */ +void BLEAdvertisedDevice::setAdvType(uint8_t type) { + m_advType = type; +} + +uint8_t BLEAdvertisedDevice::getAdvType() { + return m_advType; +} + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEAdvertisedDevice.h b/libraries/BLE/src/BLEAdvertisedDevice.h index 700e5704034..2a2fc0c81aa 100644 --- a/libraries/BLE/src/BLEAdvertisedDevice.h +++ b/libraries/BLE/src/BLEAdvertisedDevice.h @@ -3,6 +3,10 @@ * * Created on: Jul 3, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEADVERTISEDDEVICE_H_ @@ -11,15 +15,38 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) -#include +/*************************************************************************** + * Common includes * + ***************************************************************************/ +#include #include "BLEAddress.h" #include "BLEScan.h" #include "BLEUUID.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#endif + +/*************************************************************************** + * Common types * + ***************************************************************************/ + typedef enum { BLE_UNKNOWN_FRAME, BLE_EDDYSTONE_UUID_FRAME, @@ -28,7 +55,12 @@ typedef enum { BLE_FRAME_MAX } ble_frame_type_t; +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ + class BLEScan; + /** * @brief A representation of a %BLE advertised device found by a scan. * @@ -37,8 +69,11 @@ class BLEScan; */ class BLEAdvertisedDevice { public: - BLEAdvertisedDevice(); + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEAdvertisedDevice(); BLEAddress getAddress(); uint16_t getAppearance(); String getManufacturerData(); @@ -57,10 +92,15 @@ class BLEAdvertisedDevice { int8_t getTXPower(); uint8_t *getPayload(); size_t getPayloadLength(); - esp_ble_addr_type_t getAddressType(); + uint8_t getAddressType(); ble_frame_type_t getFrameType(); - void setAddressType(esp_ble_addr_type_t type); + void setAddressType(uint8_t type); + void setAdvType(uint8_t type); + uint8_t getAdvType(); + bool isLegacyAdvertisement(); + bool isScannable(); + bool isConnectable(); bool isAdvertisingService(BLEUUID uuid); bool haveAppearance(); bool haveManufacturerData(); @@ -75,28 +115,15 @@ class BLEAdvertisedDevice { private: friend class BLEScan; - void parseAdvertisement(uint8_t *payload, size_t total_len = 62); - void setPayload(uint8_t *payload, size_t total_len = 62); - void setAddress(BLEAddress address); - void setAdFlag(uint8_t adFlag); - void setAdvertizementResult(uint8_t *payload); - void setAppearance(uint16_t appearance); - void setManufacturerData(String manufacturerData); - void setName(String name); - void setRSSI(int rssi); - void setScan(BLEScan *pScan); - void setServiceData(String data); - void setServiceDataUUID(BLEUUID uuid); - void setServiceUUID(const char *serviceUUID); - void setServiceUUID(BLEUUID serviceUUID); - void setTXPower(int8_t txPower); + /*************************************************************************** + * Common private properties * + ***************************************************************************/ bool m_haveAppearance; bool m_haveManufacturerData; bool m_haveName; bool m_haveRSSI; bool m_haveTXPower; - BLEAddress m_address = BLEAddress((uint8_t *)"\0\0\0\0\0\0"); uint8_t m_adFlag; uint16_t m_appearance; @@ -111,7 +138,37 @@ class BLEAdvertisedDevice { std::vector m_serviceDataUUIDs; uint8_t *m_payload; size_t m_payloadLength = 0; - esp_ble_addr_type_t m_addressType; + uint8_t m_addressType; + uint8_t m_advType; + bool m_isLegacyAdv; + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + bool m_callbackSent; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + void parseAdvertisement(uint8_t *payload, size_t total_len = 62); + void setPayload(uint8_t *payload, size_t total_len = 62, bool append = false); + void setAddress(BLEAddress address); + void setAdFlag(uint8_t adFlag); + void setAdvertizementResult(uint8_t *payload); + void setAppearance(uint16_t appearance); + void setManufacturerData(String manufacturerData); + void setName(String name); + void setRSSI(int rssi); + void setScan(BLEScan *pScan); + void setServiceData(String data); + void setServiceDataUUID(BLEUUID uuid); + void setServiceUUID(const char *serviceUUID); + void setServiceUUID(BLEUUID serviceUUID); + void setTXPower(int8_t txPower); }; /** @@ -123,30 +180,55 @@ class BLEAdvertisedDevice { */ class BLEAdvertisedDeviceCallbacks { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + virtual ~BLEAdvertisedDeviceCallbacks() {} /** - * @brief Called when a new scan result is detected. - * - * As we are scanning, we will find new devices. When found, this call back is invoked with a reference to the - * device that was found. During any individual scan, a device will only be detected one time. - */ + * @brief Called when a new scan result is detected. + * + * As we are scanning, we will find new devices. When found, this call back is invoked with a reference to the + * device that was found. During any individual scan, a device will only be detected one time. + */ virtual void onResult(BLEAdvertisedDevice advertisedDevice) = 0; }; -#ifdef SOC_BLE_50_SUPPORTED +#if defined(SOC_BLE_50_SUPPORTED) && defined(CONFIG_BLUEDROID_ENABLED) class BLEExtAdvertisingCallbacks { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + virtual ~BLEExtAdvertisingCallbacks() {} + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + /** - * @brief Called when a new scan result is detected. - * - * As we are scanning, we will find new devices. When found, this call back is invoked with a reference to the - * device that was found. During any individual scan, a device will only be detected one time. - */ + * @brief Called when a new scan result is detected. + * + * As we are scanning, we will find new devices. When found, this call back is invoked with a reference to the + * device that was found. During any individual scan, a device will only be detected one time. + */ + +#if defined(CONFIG_BLUEDROID_ENABLED) virtual void onResult(esp_ble_gap_ext_adv_report_t report) = 0; +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + + // Extended advertising for NimBLE is not supported yet. +#if defined(CONFIG_NIMBLE_ENABLED) + virtual void onResult(struct ble_gap_ext_disc_desc report) = 0; +#endif }; -#endif // SOC_BLE_50_SUPPORTED +#endif // SOC_BLE_50_SUPPORTED && CONFIG_BLUEDROID_ENABLED -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEADVERTISEDDEVICE_H_ */ diff --git a/libraries/BLE/src/BLEAdvertising.cpp b/libraries/BLE/src/BLEAdvertising.cpp index fe39a69c206..4e76873dcad 100644 --- a/libraries/BLE/src/BLEAdvertising.cpp +++ b/libraries/BLE/src/BLEAdvertising.cpp @@ -5,6 +5,10 @@ * Created on: Jun 21, 2017 * Author: kolban * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE + * * The ESP-IDF provides a framework for BLE advertising. It has determined that there are a common set * of properties that are advertised and has built a data structure that can be populated by the programmer. * This means that the programmer doesn't have to "mess with" the low level construction of a low level @@ -16,46 +20,33 @@ * set in the data will be advertised. * */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include "BLEAdvertising.h" #include #include "BLEUtils.h" +#include "BLEDevice.h" #include "GeneralUtils.h" #include "esp32-hal-log.h" +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief Construct a default advertising object. - * */ -BLEAdvertising::BLEAdvertising() : m_scanRespData{} { - m_advData.set_scan_rsp = false; - m_advData.include_name = true; - m_advData.include_txpower = true; - m_advData.min_interval = 0x20; - m_advData.max_interval = 0x40; - m_advData.appearance = 0x00; - m_advData.manufacturer_len = 0; - m_advData.p_manufacturer_data = nullptr; - m_advData.service_data_len = 0; - m_advData.p_service_data = nullptr; - m_advData.service_uuid_len = 0; - m_advData.p_service_uuid = nullptr; - m_advData.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT); - - m_advParams.adv_int_min = 0x20; - m_advParams.adv_int_max = 0x40; - m_advParams.adv_type = ADV_TYPE_IND; - m_advParams.own_addr_type = BLE_ADDR_TYPE_PUBLIC; - m_advParams.channel_map = ADV_CHNL_ALL; - m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY; - m_advParams.peer_addr_type = BLE_ADDR_TYPE_PUBLIC; - - m_customAdvData = false; // No custom advertising data - m_customScanResponseData = false; // No custom scan response data +BLEAdvertising::BLEAdvertising() { + reset(); } // BLEAdvertising /** @@ -64,6 +55,9 @@ BLEAdvertising::BLEAdvertising() : m_scanRespData{} { */ void BLEAdvertising::addServiceUUID(BLEUUID serviceUUID) { m_serviceUUIDs.push_back(serviceUUID); +#ifdef CONFIG_NIMBLE_ENABLED + m_advDataSet = false; +#endif } // addServiceUUID /** @@ -72,6 +66,9 @@ void BLEAdvertising::addServiceUUID(BLEUUID serviceUUID) { */ void BLEAdvertising::addServiceUUID(const char *serviceUUID) { addServiceUUID(BLEUUID(serviceUUID)); +#ifdef CONFIG_NIMBLE_ENABLED + m_advDataSet = false; +#endif } // addServiceUUID /** @@ -87,6 +84,9 @@ bool BLEAdvertising::removeServiceUUID(int index) { } m_serviceUUIDs.erase(m_serviceUUIDs.begin() + index); +#ifdef CONFIG_NIMBLE_ENABLED + m_advDataSet = false; +#endif return true; } @@ -120,34 +120,107 @@ bool BLEAdvertising::removeServiceUUID(const char *serviceUUID) { */ void BLEAdvertising::setAppearance(uint16_t appearance) { m_advData.appearance = appearance; +#ifdef CONFIG_NIMBLE_ENABLED + m_advData.appearance_is_present = 1; + m_advDataSet = false; +#endif } // setAppearance -void BLEAdvertising::setAdvertisementType(esp_ble_adv_type_t adv_type) { - m_advParams.adv_type = adv_type; -} // setAdvertisementType +void BLEAdvertising::setAdvertisementType(uint8_t adv_type) { +#ifdef CONFIG_BLUEDROID_ENABLED + m_advParams.adv_type = (esp_ble_adv_type_t)adv_type; +#endif -void BLEAdvertising::setAdvertisementChannelMap(esp_ble_adv_channel_t channel_map) { - m_advParams.channel_map = channel_map; -} // setAdvertisementChannelMap +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.conn_mode = adv_type; +#endif +} // setAdvertisementType void BLEAdvertising::setMinInterval(uint16_t mininterval) { +#ifdef CONFIG_BLUEDROID_ENABLED m_advParams.adv_int_min = mininterval; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.itvl_min = mininterval; +#endif } // setMinInterval void BLEAdvertising::setMaxInterval(uint16_t maxinterval) { +#ifdef CONFIG_BLUEDROID_ENABLED m_advParams.adv_int_max = maxinterval; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.itvl_max = maxinterval; +#endif } // setMaxInterval void BLEAdvertising::setMinPreferred(uint16_t mininterval) { +#ifdef CONFIG_BLUEDROID_ENABLED m_advData.min_interval = mininterval; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + // invalid parameters, set the slave interval to null + if (mininterval < 0x0006 || mininterval > 0x0C80) { + m_advData.slave_itvl_range = nullptr; + return; + } + + if (m_advData.slave_itvl_range == nullptr) { + m_advData.slave_itvl_range = m_slaveItvl; + } + + m_slaveItvl[0] = mininterval; + m_slaveItvl[1] = mininterval >> 8; + + uint16_t maxinterval = *(uint16_t *)(m_advData.slave_itvl_range + 2); + + // If mininterval is higher than the maxinterval make them the same + if (mininterval > maxinterval) { + m_slaveItvl[2] = m_slaveItvl[0]; + m_slaveItvl[3] = m_slaveItvl[1]; + } + + m_advDataSet = false; +#endif } // void BLEAdvertising::setMaxPreferred(uint16_t maxinterval) { +#ifdef CONFIG_BLUEDROID_ENABLED m_advData.max_interval = maxinterval; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + // invalid parameters, set the slave interval to null + if (maxinterval < 0x0006 || maxinterval > 0x0C80) { + m_advData.slave_itvl_range = nullptr; + return; + } + if (m_advData.slave_itvl_range == nullptr) { + m_advData.slave_itvl_range = m_slaveItvl; + } + m_slaveItvl[2] = maxinterval; + m_slaveItvl[3] = maxinterval >> 8; + + uint16_t mininterval = *(uint16_t *)(m_advData.slave_itvl_range); + + // If mininterval is higher than the maxinterval make them the same + if (mininterval > maxinterval) { + m_slaveItvl[0] = m_slaveItvl[2]; + m_slaveItvl[1] = m_slaveItvl[3]; + } + + m_advDataSet = false; +#endif } // void BLEAdvertising::setScanResponse(bool set) { m_scanResp = set; +#ifdef CONFIG_NIMBLE_ENABLED + m_advDataSet = false; +#endif } /** @@ -158,22 +231,54 @@ void BLEAdvertising::setScanResponse(bool set) { void BLEAdvertising::setScanFilter(bool scanRequestWhitelistOnly, bool connectWhitelistOnly) { log_v(">> setScanFilter: scanRequestWhitelistOnly: %d, connectWhitelistOnly: %d", scanRequestWhitelistOnly, connectWhitelistOnly); if (!scanRequestWhitelistOnly && !connectWhitelistOnly) { + +#ifdef CONFIG_BLUEDROID_ENABLED m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.filter_policy = BLE_HCI_ADV_FILT_NONE; +#endif + log_v("<< setScanFilter"); return; } if (scanRequestWhitelistOnly && !connectWhitelistOnly) { + +#ifdef CONFIG_BLUEDROID_ENABLED m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_WLST_CON_ANY; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.filter_policy = BLE_HCI_ADV_FILT_SCAN; +#endif + log_v("<< setScanFilter"); return; } if (!scanRequestWhitelistOnly && connectWhitelistOnly) { + +#ifdef CONFIG_BLUEDROID_ENABLED m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_WLST; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.filter_policy = BLE_HCI_ADV_FILT_CONN; +#endif + log_v("<< setScanFilter"); return; } if (scanRequestWhitelistOnly && connectWhitelistOnly) { + +#ifdef CONFIG_BLUEDROID_ENABLED m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_WLST_CON_WLST; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_advParams.filter_policy = BLE_HCI_ADV_FILT_BOTH; +#endif + log_v("<< setScanFilter"); return; } @@ -185,10 +290,21 @@ void BLEAdvertising::setScanFilter(bool scanRequestWhitelistOnly, bool connectWh */ bool BLEAdvertising::setAdvertisementData(BLEAdvertisementData &advertisementData) { log_v(">> setAdvertisementData"); + +#ifdef CONFIG_BLUEDROID_ENABLED esp_err_t errRc = ::esp_ble_gap_config_adv_data_raw((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length()); if (errRc != ESP_OK) { log_e("esp_ble_gap_config_adv_data_raw: %d %s", errRc, GeneralUtils::errorToString(errRc)); } +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + esp_err_t errRc = ble_gap_adv_set_data((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length()); + if (errRc != ESP_OK) { + log_e("ble_gap_adv_set_data: %d %s", errRc, BLEUtils::returnCodeToString(errRc)); + } +#endif + m_customAdvData = true; // Set the flag that indicates we are using custom advertising data. log_v("<< setAdvertisementData"); return ESP_OK == errRc; @@ -200,127 +316,25 @@ bool BLEAdvertising::setAdvertisementData(BLEAdvertisementData &advertisementDat */ bool BLEAdvertising::setScanResponseData(BLEAdvertisementData &advertisementData) { log_v(">> setScanResponseData"); + +#ifdef CONFIG_BLUEDROID_ENABLED esp_err_t errRc = ::esp_ble_gap_config_scan_rsp_data_raw((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length()); if (errRc != ESP_OK) { log_e("esp_ble_gap_config_scan_rsp_data_raw: %d %s", errRc, GeneralUtils::errorToString(errRc)); } - m_customScanResponseData = true; // Set the flag that indicates we are using custom scan response data. - log_v("<< setScanResponseData"); - return ESP_OK == errRc; -} // setScanResponseData +#endif -/** - * @brief Start advertising. - * Start advertising. - * @return N/A. - */ -bool BLEAdvertising::start() { - log_v(">> start: customAdvData: %d, customScanResponseData: %d", m_customAdvData, m_customScanResponseData); - - // We have a vector of service UUIDs that we wish to advertise. In order to use the - // ESP-IDF framework, these must be supplied in a contiguous array of their 128bit (16 byte) - // representations. If we have 1 or more services to advertise then we allocate enough - // storage to host them and then copy them in one at a time into the contiguous storage. - int numServices = m_serviceUUIDs.size(); - if (numServices > 0) { - m_advData.service_uuid_len = 16 * numServices; - m_advData.p_service_uuid = (uint8_t *)malloc(m_advData.service_uuid_len); - if (!m_advData.p_service_uuid) { - log_e(">> start failed: out of memory"); - return false; - } - - uint8_t *p = m_advData.p_service_uuid; - for (int i = 0; i < numServices; i++) { - log_d("- advertising service: %s", m_serviceUUIDs[i].toString().c_str()); - BLEUUID serviceUUID128 = m_serviceUUIDs[i].to128(); - memcpy(p, serviceUUID128.getNative()->uuid.uuid128, 16); - p += 16; - } - } else { - m_advData.service_uuid_len = 0; - log_d("- no services advertised"); - } - - esp_err_t errRc; - - if (!m_customAdvData) { - // Set the configuration for advertising. - m_advData.set_scan_rsp = false; - m_advData.include_name = !m_scanResp; - m_advData.include_txpower = !m_scanResp; - errRc = ::esp_ble_gap_config_adv_data(&m_advData); - if (errRc != ESP_OK) { - log_e("<< esp_ble_gap_config_adv_data: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return false; - } - } - - if (!m_customScanResponseData && m_scanResp) { - // Set the configuration for scan response. - memcpy(&m_scanRespData, &m_advData, sizeof(esp_ble_adv_data_t)); // Copy the content of m_advData. - m_scanRespData.set_scan_rsp = true; // Define this struct as scan response data - m_scanRespData.include_name = true; // Caution: This may lead to a crash if the device name has more than 29 characters - m_scanRespData.include_txpower = true; - m_scanRespData.appearance = 0; // If defined the 'Appearance' attribute is already included in the advertising data - m_scanRespData.flag = 0; // 'Flags' attribute should no be included in the scan response - - errRc = ::esp_ble_gap_config_adv_data(&m_scanRespData); - if (errRc != ESP_OK) { - log_e("<< esp_ble_gap_config_adv_data (Scan response): rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return false; - } - } - - // If we had services to advertise then we previously allocated some storage for them. - // Here we release that storage. - free(m_advData.p_service_uuid); //TODO change this variable to local scope? - m_advData.p_service_uuid = nullptr; - - // Start advertising. - errRc = ::esp_ble_gap_start_advertising(&m_advParams); +#if defined(CONFIG_NIMBLE_ENABLED) + esp_err_t errRc = ble_gap_adv_rsp_set_data((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length()); if (errRc != ESP_OK) { - log_e("<< esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - } else { - log_v("<< start"); + log_e("ble_gap_adv_rsp_set_data: %d %s", errRc, BLEUtils::returnCodeToString(errRc)); } - return ESP_OK == errRc; -} // start - -/** - * @brief Stop advertising. - * Stop advertising. - * @return N/A. - */ -bool BLEAdvertising::stop() { - log_v(">> stop"); - esp_err_t errRc = ::esp_ble_gap_stop_advertising(); - if (errRc != ESP_OK) { - log_e("esp_ble_gap_stop_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - } else { - log_v("<< stop"); - } - return ESP_OK == errRc; -} // stop - -/** - * @brief Set BLE address. - * @param [in] Bluetooth address. - * @param [in] Bluetooth address type. - * Set BLE address. - */ -bool BLEAdvertising::setDeviceAddress(esp_bd_addr_t addr, esp_ble_addr_type_t type) { - log_v(">> setPrivateAddress"); +#endif - m_advParams.own_addr_type = type; - esp_err_t errRc = esp_ble_gap_set_rand_addr((uint8_t *)addr); - if (errRc != ESP_OK) { - log_e("esp_ble_gap_set_rand_addr: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - } else { - log_v("<< setPrivateAddress"); - } + m_customScanResponseData = true; // Set the flag that indicates we are using custom scan response data. + log_v("<< setScanResponseData"); return ESP_OK == errRc; -} // setPrivateAddress +} // setScanResponseData /** * @brief Add data to the payload to be advertised. @@ -333,6 +347,13 @@ void BLEAdvertisementData::addData(String data) { m_payload.concat(data); } // addData +void BLEAdvertisementData::addData(char *data, size_t length) { + if ((m_payload.length() + length) > ESP_BLE_ADV_DATA_LEN_MAX) { + return; + } + m_payload.concat(String(data, length)); +} // addData + /** * @brief Set the appearance. * @param [in] appearance The appearance code value. @@ -359,7 +380,12 @@ void BLEAdvertisementData::setCompleteServices(BLEUUID uuid) { // [Len] [0x03] [LL] [HH] cdata[0] = 3; cdata[1] = ESP_BLE_AD_TYPE_16SRV_CMPL; // 0x03 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid16, 2)); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u16.value, 2)); +#endif break; } @@ -368,7 +394,12 @@ void BLEAdvertisementData::setCompleteServices(BLEUUID uuid) { // [Len] [0x05] [LL] [LL] [HH] [HH] cdata[0] = 5; cdata[1] = ESP_BLE_AD_TYPE_32SRV_CMPL; // 0x05 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid32, 4)); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u32.value, 4)); +#endif break; } @@ -377,7 +408,12 @@ void BLEAdvertisementData::setCompleteServices(BLEUUID uuid) { // [Len] [0x07] [0] [1] ... [15] cdata[0] = 17; cdata[1] = ESP_BLE_AD_TYPE_128SRV_CMPL; // 0x07 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)uuid.getNative()->uuid.uuid128, 16)); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)uuid.getNative()->u128.value, 16)); +#endif break; } @@ -442,7 +478,12 @@ void BLEAdvertisementData::setPartialServices(BLEUUID uuid) { // [Len] [0x02] [LL] [HH] cdata[0] = 3; cdata[1] = ESP_BLE_AD_TYPE_16SRV_PART; // 0x02 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid16, 2)); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u16.value, 2)); +#endif break; } @@ -451,7 +492,12 @@ void BLEAdvertisementData::setPartialServices(BLEUUID uuid) { // [Len] [0x04] [LL] [LL] [HH] [HH] cdata[0] = 5; cdata[1] = ESP_BLE_AD_TYPE_32SRV_PART; // 0x04 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid32, 4)); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u32.value, 4)); +#endif break; } @@ -460,7 +506,12 @@ void BLEAdvertisementData::setPartialServices(BLEUUID uuid) { // [Len] [0x06] [0] [1] ... [15] cdata[0] = 17; cdata[1] = ESP_BLE_AD_TYPE_128SRV_PART; // 0x06 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid128, 16)); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u128.value, 16)); +#endif break; } @@ -481,7 +532,12 @@ void BLEAdvertisementData::setServiceData(BLEUUID uuid, String data) { // [Len] [0x16] [UUID16] data cdata[0] = data.length() + 3; cdata[1] = ESP_BLE_AD_TYPE_SERVICE_DATA; // 0x16 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid16, 2) + data); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u16.value, 2) + data); +#endif break; } @@ -490,7 +546,12 @@ void BLEAdvertisementData::setServiceData(BLEUUID uuid, String data) { // [Len] [0x20] [UUID32] data cdata[0] = data.length() + 5; cdata[1] = ESP_BLE_AD_TYPE_32SERVICE_DATA; // 0x20 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid32, 4) + data); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u32.value, 4) + data); +#endif break; } @@ -499,7 +560,12 @@ void BLEAdvertisementData::setServiceData(BLEUUID uuid, String data) { // [Len] [0x21] [UUID128] data cdata[0] = data.length() + 17; cdata[1] = ESP_BLE_AD_TYPE_128SERVICE_DATA; // 0x21 +#if defined(CONFIG_BLUEDROID_ENABLED) addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid128, 16) + data); +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + addData(String(cdata, 2) + String((char *)&uuid.getNative()->u128.value, 16) + data); +#endif break; } @@ -520,6 +586,33 @@ void BLEAdvertisementData::setShortName(String name) { log_d("BLEAdvertisementData", "<< setShortName"); } // setShortName +/** + * @brief Adds Tx power level to the advertisement data. + */ +void BLEAdvertisementData::addTxPower() { + char cdata[3]; + cdata[0] = 2; // length + cdata[1] = ESP_BLE_AD_TYPE_TX_PWR; + cdata[2] = BLEDevice::getPower(); + addData(cdata, 3); +} // addTxPower + +/** + * @brief Set the preferred connection interval parameters. + * @param [in] min The minimum interval desired. + * @param [in] max The maximum interval desired. + */ +void BLEAdvertisementData::setPreferredParams(uint16_t min, uint16_t max) { + char cdata[6]; + cdata[0] = 5; // length + cdata[1] = ESP_BLE_AD_TYPE_INT_RANGE; + cdata[2] = min; + cdata[3] = min >> 8; + cdata[4] = max; + cdata[5] = max >> 8; + addData(cdata, 6); +} // setPreferredParams + /** * @brief Retrieve the payload that is to be advertised. * @return The payload that is to be advertised. @@ -528,8 +621,146 @@ String BLEAdvertisementData::getPayload() { return m_payload; } // getPayload -void BLEAdvertising::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +void BLEAdvertising::reset() { + if (BLEDevice::getInitialized()) { + stop(); + } + + memset(&m_scanRespData, 0, sizeof(esp_ble_adv_data_t)); + memset(&m_advData, 0, sizeof(esp_ble_adv_data_t)); + memset(&m_advParams, 0, sizeof(esp_ble_adv_params_t)); + + m_advData.set_scan_rsp = false; + m_advData.include_name = true; + m_advData.include_txpower = true; + m_advData.min_interval = 0x20; + m_advData.max_interval = 0x40; + m_advData.appearance = 0x00; + m_advData.manufacturer_len = 0; + m_advData.p_manufacturer_data = nullptr; + m_advData.service_data_len = 0; + m_advData.p_service_data = nullptr; + m_advData.service_uuid_len = 0; + m_advData.p_service_uuid = nullptr; + m_advData.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT); + + m_advParams.adv_int_min = 0x20; + m_advParams.adv_int_max = 0x40; + m_advParams.adv_type = ADV_TYPE_IND; + m_advParams.own_addr_type = BLE_ADDR_TYPE_PUBLIC; + m_advParams.channel_map = ADV_CHNL_ALL; + m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY; + m_advParams.peer_addr_type = BLE_ADDR_TYPE_PUBLIC; + m_customAdvData = false; // No custom advertising data + m_customScanResponseData = false; // No custom scan response data +} // BLEAdvertising + +void BLEAdvertising::setAdvertisementChannelMap(esp_ble_adv_channel_t channel_map) { + m_advParams.channel_map = channel_map; +} // setAdvertisementChannelMap + +/** + * @brief Start advertising. + * Start advertising. + * @return N/A. + */ +bool BLEAdvertising::start() { + log_v(">> start: customAdvData: %d, customScanResponseData: %d", m_customAdvData, m_customScanResponseData); + + // We have a vector of service UUIDs that we wish to advertise. In order to use the + // ESP-IDF framework, these must be supplied in a contiguous array of their 128bit (16 byte) + // representations. If we have 1 or more services to advertise then we allocate enough + // storage to host them and then copy them in one at a time into the contiguous storage. + int numServices = m_serviceUUIDs.size(); + if (numServices > 0) { + m_advData.service_uuid_len = 16 * numServices; + m_advData.p_service_uuid = (uint8_t *)malloc(m_advData.service_uuid_len); + if (!m_advData.p_service_uuid) { + log_e(">> start failed: out of memory"); + return false; + } + + uint8_t *p = m_advData.p_service_uuid; + for (int i = 0; i < numServices; i++) { + log_d("- advertising service: %s", m_serviceUUIDs[i].toString().c_str()); + BLEUUID serviceUUID128 = m_serviceUUIDs[i].to128(); + memcpy(p, serviceUUID128.getNative()->uuid.uuid128, 16); + p += 16; + } + } else { + m_advData.service_uuid_len = 0; + log_d("- no services advertised"); + } + + esp_err_t errRc; + + if (!m_customAdvData) { + // Set the configuration for advertising. + m_advData.set_scan_rsp = false; + m_advData.include_name = !m_scanResp; + m_advData.include_txpower = !m_scanResp; + errRc = ::esp_ble_gap_config_adv_data(&m_advData); + if (errRc != ESP_OK) { + log_e("<< esp_ble_gap_config_adv_data: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return false; + } + } + + if (!m_customScanResponseData && m_scanResp) { + // Set the configuration for scan response. + memcpy(&m_scanRespData, &m_advData, sizeof(esp_ble_adv_data_t)); // Copy the content of m_advData. + m_scanRespData.set_scan_rsp = true; // Define this struct as scan response data + m_scanRespData.include_name = true; // Caution: This may lead to a crash if the device name has more than 29 characters + m_scanRespData.include_txpower = true; + m_scanRespData.appearance = 0; // If defined the 'Appearance' attribute is already included in the advertising data + m_scanRespData.flag = 0; // 'Flags' attribute should no be included in the scan response + + errRc = ::esp_ble_gap_config_adv_data(&m_scanRespData); + if (errRc != ESP_OK) { + log_e("<< esp_ble_gap_config_adv_data (Scan response): rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return false; + } + } + + // If we had services to advertise then we previously allocated some storage for them. + // Here we release that storage. + free(m_advData.p_service_uuid); //TODO change this variable to local scope? + m_advData.p_service_uuid = nullptr; + + // Start advertising. + errRc = ::esp_ble_gap_start_advertising(&m_advParams); + if (errRc != ESP_OK) { + log_e("<< esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } else { + log_v("<< start"); + } + return ESP_OK == errRc; +} // start + +/** + * @brief Stop advertising. + * Stop advertising. + * @return N/A. + */ +bool BLEAdvertising::stop() { + log_v(">> stop"); + esp_err_t errRc = ::esp_ble_gap_stop_advertising(); + if (errRc != ESP_OK) { + log_e("esp_ble_gap_stop_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } else { + log_v("<< stop"); + } + return ESP_OK == errRc; +} // stop + +void BLEAdvertising::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { log_d("handleGAPEvent [event no: %d]", (int)event); switch (event) { @@ -558,7 +789,26 @@ void BLEAdvertising::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb } } -#ifdef SOC_BLE_50_SUPPORTED +/** + * @brief Set BLE address. + * @param [in] Bluetooth address. + * @param [in] Bluetooth address type. + * Set BLE address. + */ +bool BLEAdvertising::setDeviceAddress(esp_bd_addr_t addr, esp_ble_addr_type_t type) { + log_v(">> setPrivateAddress"); + + m_advParams.own_addr_type = type; + esp_err_t errRc = esp_ble_gap_set_rand_addr((uint8_t *)addr); + if (errRc != ESP_OK) { + log_e("esp_ble_gap_set_rand_addr: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } else { + log_v("<< setPrivateAddress"); + } + return ESP_OK == errRc; +} // setPrivateAddress + +#if defined(SOC_BLE_50_SUPPORTED) /** * @brief Creator @@ -803,7 +1053,376 @@ void BLEMultiAdvertising::setDuration(uint8_t instance, int duration, int max_ev ext_adv[instance] = {instance, duration, max_events}; } -#endif // SOC_BLE_50_SUPPORTED +#endif /* SOC_BLE_50_SUPPORTED */ #endif /* CONFIG_BLUEDROID_ENABLED */ + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +void BLEAdvertising::reset() { + if (BLEDevice::getInitialized() && isAdvertising()) { + stop(); + } + memset(&m_advData, 0, sizeof m_advData); + memset(&m_scanData, 0, sizeof m_scanData); + memset(&m_advParams, 0, sizeof m_advParams); + memset(&m_slaveItvl, 0, sizeof m_slaveItvl); + const char *name = ble_svc_gap_device_name(); + + m_advData.name = (uint8_t *)name; + m_advData.name_len = strlen(name); + m_advData.name_is_complete = 1; + m_advData.tx_pwr_lvl = BLEDevice::getPower(); + m_advData.flags = (BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP); + m_advParams.conn_mode = BLE_GAP_CONN_MODE_UND; + m_advParams.disc_mode = BLE_GAP_DISC_MODE_GEN; + m_customAdvData = false; + m_customScanResponseData = false; + m_scanResp = true; + m_advDataSet = false; + // Set this to non-zero to prevent auto start if host reset before started by app. + m_duration = BLE_HS_FOREVER; + m_advCompCB = nullptr; +} // BLEAdvertising + +void BLEAdvertising::setName(String name) { + m_name = name; + m_advData.name = (uint8_t *)m_name.c_str(); + m_advData.name_len = m_name.length(); + m_advDataSet = false; +} + +/** + * @brief Add the transmission power level to the advertisement packet. + */ +void BLEAdvertising::addTxPower() { + m_advData.tx_pwr_lvl_is_present = 1; + m_advDataSet = false; +} // addTxPower + +/** + * @brief Handles the callback when advertising stops. + */ +void BLEAdvertising::advCompleteCB() { + if (m_advCompCB != nullptr) { + m_advCompCB(this); + } +} // advCompleteCB + +/** + * @brief Check if currently advertising. + * @return true if advertising is active. + */ +bool BLEAdvertising::isAdvertising() { + return ble_gap_adv_active(); +} // isAdvertising + +/* + * Host reset seems to clear advertising data, + * we need clear the flag so it reloads it. + */ +void BLEAdvertising::onHostSync() { + log_v("Host re-synced"); + + m_advDataSet = false; + // If we were advertising forever, restart it now + if (m_duration == 0) { + start(m_duration, m_advCompCB); + } else { + // Otherwise we should tell the app that advertising stopped. + advCompleteCB(); + } +} // onHostSync + +/** + * @brief Handler for gap events when not using peripheral role. + * @param [in] event the event data. + * @param [in] arg pointer to the advertising instance. + */ +int BLEAdvertising::handleGAPEvent(struct ble_gap_event *event, void *arg) { + BLEAdvertising *pAdv = (BLEAdvertising *)arg; + + if (event->type == BLE_GAP_EVENT_ADV_COMPLETE) { + switch (event->adv_complete.reason) { + // Don't call the callback if host reset, we want to + // preserve the active flag until re-sync to restart advertising. + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: + log_e("host reset, rc=%d", event->adv_complete.reason); + BLEDevice::onReset(event->adv_complete.reason); + return 0; + default: break; + } + pAdv->advCompleteCB(); + } + return 0; +} + +/** + * @brief Start advertising. + * @param [in] duration The duration, in seconds, to advertise, 0 == advertise forever. + * @param [in] advCompleteCB A pointer to a callback to be invoked when advertising ends. + * @return True if advertising started successfully. + */ +bool BLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(BLEAdvertising *pAdv)) { + log_v(">> Advertising start: customAdvData: %d, customScanResponseData: %d", m_customAdvData, m_customScanResponseData); + + // If Host is not synced we cannot start advertising. + if (!BLEDevice::m_synced) { + log_e("Host reset, wait for sync."); + return false; + } + + // If already advertising just return + if (ble_gap_adv_active()) { + log_w("Advertising already active"); + return true; + } + + BLEServer *pServer = BLEDevice::getServer(); + if (pServer != nullptr) { + if (!pServer->m_gattsStarted) { + pServer->start(); + } else if (pServer->getConnectedCount() >= CONFIG_BT_NIMBLE_MAX_CONNECTIONS) { + log_e("Max connections reached - not advertising"); + return false; + } + } + + // Save the duration in case of host reset so we can restart with the same parameters + m_duration = duration; + + if (duration == 0) { + duration = BLE_HS_FOREVER; + } else { + duration = duration * 1000; // convert duration to milliseconds + } + + m_advCompCB = advCompleteCB; + + m_advParams.disc_mode = BLE_GAP_DISC_MODE_GEN; + m_advData.flags = (BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP); + if (m_advParams.conn_mode == BLE_GAP_CONN_MODE_NON) { + if (!m_scanResp) { + m_advParams.disc_mode = BLE_GAP_DISC_MODE_NON; + m_advData.flags = BLE_HS_ADV_F_BREDR_UNSUP; + } + } + + int rc = 0; + + if (!m_customAdvData && !m_advDataSet) { + //start with 3 bytes for the flags data + uint8_t payloadLen = (2 + 1); + if (m_advData.mfg_data_len > 0) { + payloadLen += (2 + m_advData.mfg_data_len); + } + + if (m_advData.svc_data_uuid16_len > 0) { + payloadLen += (2 + m_advData.svc_data_uuid16_len); + } + + if (m_advData.svc_data_uuid32_len > 0) { + payloadLen += (2 + m_advData.svc_data_uuid32_len); + } + + if (m_advData.svc_data_uuid128_len > 0) { + payloadLen += (2 + m_advData.svc_data_uuid128_len); + } + + if (m_advData.uri_len > 0) { + payloadLen += (2 + m_advData.uri_len); + } + + if (m_advData.appearance_is_present) { + payloadLen += (2 + BLE_HS_ADV_APPEARANCE_LEN); + } + + if (m_advData.tx_pwr_lvl_is_present) { + payloadLen += (2 + BLE_HS_ADV_TX_PWR_LVL_LEN); + } + + if (m_advData.slave_itvl_range != nullptr) { + payloadLen += (2 + BLE_HS_ADV_SLAVE_ITVL_RANGE_LEN); + } + + for (auto &it : m_serviceUUIDs) { + if (it.getNative()->u.type == BLE_UUID_TYPE_16) { + int add = (m_advData.num_uuids16 > 0) ? 2 : 4; + if ((payloadLen + add) > BLE_HS_ADV_MAX_SZ) { + m_advData.uuids16_is_complete = 0; + continue; + } + payloadLen += add; + + if (nullptr == (m_advData.uuids16 = (ble_uuid16_t *)realloc((void *)m_advData.uuids16, (m_advData.num_uuids16 + 1) * sizeof(ble_uuid16_t)))) { + log_e("Error, no mem"); + abort(); + } + memcpy((void *)&m_advData.uuids16[m_advData.num_uuids16], &it.getNative()->u16, sizeof(ble_uuid16_t)); + m_advData.uuids16_is_complete = 1; + m_advData.num_uuids16++; + } + if (it.getNative()->u.type == BLE_UUID_TYPE_32) { + int add = (m_advData.num_uuids32 > 0) ? 4 : 6; + if ((payloadLen + add) > BLE_HS_ADV_MAX_SZ) { + m_advData.uuids32_is_complete = 0; + continue; + } + payloadLen += add; + + if (nullptr == (m_advData.uuids32 = (ble_uuid32_t *)realloc((void *)m_advData.uuids32, (m_advData.num_uuids32 + 1) * sizeof(ble_uuid32_t)))) { + log_e("Error, no mem"); + abort(); + } + memcpy((void *)&m_advData.uuids32[m_advData.num_uuids32], &it.getNative()->u32, sizeof(ble_uuid32_t)); + m_advData.uuids32_is_complete = 1; + m_advData.num_uuids32++; + } + if (it.getNative()->u.type == BLE_UUID_TYPE_128) { + int add = (m_advData.num_uuids128 > 0) ? 16 : 18; + if ((payloadLen + add) > BLE_HS_ADV_MAX_SZ) { + m_advData.uuids128_is_complete = 0; + continue; + } + payloadLen += add; + + if (nullptr == (m_advData.uuids128 = (ble_uuid128_t *)realloc((void *)m_advData.uuids128, (m_advData.num_uuids128 + 1) * sizeof(ble_uuid128_t)))) { + log_e("Error, no mem"); + abort(); + } + memcpy((void *)&m_advData.uuids128[m_advData.num_uuids128], &it.getNative()->u128, sizeof(ble_uuid128_t)); + m_advData.uuids128_is_complete = 1; + m_advData.num_uuids128++; + } + } + + // check if there is room for the name, if not put it in scan data + if ((payloadLen + (2 + m_advData.name_len)) > BLE_HS_ADV_MAX_SZ) { + if (m_scanResp && !m_customScanResponseData) { + m_scanData.name = m_advData.name; + m_scanData.name_len = m_advData.name_len; + if (m_scanData.name_len > BLE_HS_ADV_MAX_SZ - 2) { + m_scanData.name_len = BLE_HS_ADV_MAX_SZ - 2; + m_scanData.name_is_complete = 0; + } else { + m_scanData.name_is_complete = 1; + } + m_advData.name = nullptr; + m_advData.name_len = 0; + m_advData.name_is_complete = 0; + } else { + if (m_advData.tx_pwr_lvl_is_present) { + m_advData.tx_pwr_lvl_is_present = 0; + payloadLen -= (2 + 1); + } + // if not using scan response just cut the name down + // leaving 2 bytes for the data specifier. + if (m_advData.name_len > (BLE_HS_ADV_MAX_SZ - payloadLen - 2)) { + m_advData.name_len = (BLE_HS_ADV_MAX_SZ - payloadLen - 2); + m_advData.name_is_complete = 0; + } + } + } + + if (m_scanResp && !m_customScanResponseData) { + rc = ble_gap_adv_rsp_set_fields(&m_scanData); + switch (rc) { + case 0: break; + + case BLE_HS_EBUSY: log_e("Already advertising"); break; + + case BLE_HS_EMSGSIZE: log_e("Scan data too long"); break; + + default: log_e("Error setting scan response data; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break; + } + } + + if (rc == 0) { + rc = ble_gap_adv_set_fields(&m_advData); + switch (rc) { + case 0: break; + + case BLE_HS_EBUSY: log_e("Already advertising"); break; + + case BLE_HS_EMSGSIZE: log_e("Advertisement data too long"); break; + + default: log_e("Error setting advertisement data; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break; + } + } + + if (m_advData.num_uuids128 > 0) { + free((void *)m_advData.uuids128); + m_advData.uuids128 = nullptr; + m_advData.num_uuids128 = 0; + } + + if (m_advData.num_uuids32 > 0) { + free((void *)m_advData.uuids32); + m_advData.uuids32 = nullptr; + m_advData.num_uuids32 = 0; + } + + if (m_advData.num_uuids16 > 0) { + free((void *)m_advData.uuids16); + m_advData.uuids16 = nullptr; + m_advData.num_uuids16 = 0; + } + + if (rc != 0) { + return false; + } + + m_advDataSet = true; + } + + rc = ble_gap_adv_start( + BLEDevice::m_ownAddrType, NULL, duration, &m_advParams, (pServer != nullptr) ? BLEServer::handleGATTServerEvent : BLEAdvertising::handleGAPEvent, + (pServer != nullptr) ? (void *)pServer : (void *)this + ); + + switch (rc) { + case 0: break; + + case BLE_HS_EINVAL: log_e("Unable to advertise - Duration too long"); break; + + case BLE_HS_EPREEMPTED: log_e("Unable to advertise - busy"); break; + + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: log_e("Unable to advertise - Host Reset"); break; + + default: log_e("Error enabling advertising; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break; + } + + log_d("<< Advertising start"); + return (rc == 0); +} // start + +/** + * @brief Stop advertising. + */ +bool BLEAdvertising::stop() { + log_d(">> stop"); + + int rc = ble_gap_adv_stop(); + if (rc != 0 && rc != BLE_HS_EALREADY) { + log_e("ble_gap_adv_stop rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + return false; + } + + log_d("<< stop"); + return true; +} // stop + +#endif /* CONFIG_NIMBLE_ENABLED */ + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEAdvertising.h b/libraries/BLE/src/BLEAdvertising.h index 1e573ac814f..cf68768ec91 100644 --- a/libraries/BLE/src/BLEAdvertising.h +++ b/libraries/BLE/src/BLEAdvertising.h @@ -3,6 +3,10 @@ * * Created on: Jun 21, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEADVERTISING_H_ @@ -11,20 +15,97 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include "BLEUUID.h" #include #include "RTOS.h" +#include "BLEUtils.h" + +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include + +#define ESP_BLE_ADV_DATA_LEN_MAX BLE_HS_ADV_MAX_SZ +#define ESP_BLE_ADV_FLAG_LIMIT_DISC (0x01 << 0) +#define ESP_BLE_ADV_FLAG_GEN_DISC (0x01 << 1) +#define ESP_BLE_ADV_FLAG_BREDR_NOT_SPT (0x01 << 2) +#define ESP_BLE_ADV_FLAG_DMT_CONTROLLER_SPT (0x01 << 3) +#define ESP_BLE_ADV_FLAG_DMT_HOST_SPT (0x01 << 4) +#define ESP_BLE_ADV_FLAG_NON_LIMIT_DISC (0x00) +#endif /* CONFIG_NIMBLE_ENABLED */ + +/*************************************************************************** + * NimBLE types * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +typedef enum { + ESP_BLE_AD_TYPE_FLAG = 0x01, + ESP_BLE_AD_TYPE_16SRV_PART = 0x02, + ESP_BLE_AD_TYPE_16SRV_CMPL = 0x03, + ESP_BLE_AD_TYPE_32SRV_PART = 0x04, + ESP_BLE_AD_TYPE_32SRV_CMPL = 0x05, + ESP_BLE_AD_TYPE_128SRV_PART = 0x06, + ESP_BLE_AD_TYPE_128SRV_CMPL = 0x07, + ESP_BLE_AD_TYPE_NAME_SHORT = 0x08, + ESP_BLE_AD_TYPE_NAME_CMPL = 0x09, + ESP_BLE_AD_TYPE_TX_PWR = 0x0A, + ESP_BLE_AD_TYPE_DEV_CLASS = 0x0D, + ESP_BLE_AD_TYPE_SM_TK = 0x10, + ESP_BLE_AD_TYPE_SM_OOB_FLAG = 0x11, + ESP_BLE_AD_TYPE_INT_RANGE = 0x12, + ESP_BLE_AD_TYPE_SOL_SRV_UUID = 0x14, + ESP_BLE_AD_TYPE_128SOL_SRV_UUID = 0x15, + ESP_BLE_AD_TYPE_SERVICE_DATA = 0x16, + ESP_BLE_AD_TYPE_PUBLIC_TARGET = 0x17, + ESP_BLE_AD_TYPE_RANDOM_TARGET = 0x18, + ESP_BLE_AD_TYPE_APPEARANCE = 0x19, + ESP_BLE_AD_TYPE_ADV_INT = 0x1A, + ESP_BLE_AD_TYPE_LE_DEV_ADDR = 0x1b, + ESP_BLE_AD_TYPE_LE_ROLE = 0x1c, + ESP_BLE_AD_TYPE_SPAIR_C256 = 0x1d, + ESP_BLE_AD_TYPE_SPAIR_R256 = 0x1e, + ESP_BLE_AD_TYPE_32SOL_SRV_UUID = 0x1f, + ESP_BLE_AD_TYPE_32SERVICE_DATA = 0x20, + ESP_BLE_AD_TYPE_128SERVICE_DATA = 0x21, + ESP_BLE_AD_TYPE_LE_SECURE_CONFIRM = 0x22, + ESP_BLE_AD_TYPE_LE_SECURE_RANDOM = 0x23, + ESP_BLE_AD_TYPE_URI = 0x24, + ESP_BLE_AD_TYPE_INDOOR_POSITION = 0x25, + ESP_BLE_AD_TYPE_TRANS_DISC_DATA = 0x26, + ESP_BLE_AD_TYPE_LE_SUPPORT_FEATURE = 0x27, + ESP_BLE_AD_TYPE_CHAN_MAP_UPDATE = 0x28, + ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE = 0xFF, +} esp_ble_adv_data_type; +#endif /** * @brief Advertisement data set by the programmer to be published by the %BLE server. */ class BLEAdvertisementData { - // Only a subset of the possible BLE architected advertisement fields are currently exposed. Others will - // be exposed on demand/request or as time permits. - // public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + void setAppearance(uint16_t appearance); void setCompleteServices(BLEUUID uuid); void setFlags(uint8_t); @@ -33,85 +114,144 @@ class BLEAdvertisementData { void setPartialServices(BLEUUID uuid); void setServiceData(BLEUUID uuid, String data); void setShortName(String name); - void addData(String data); // Add data to the payload. - String getPayload(); // Retrieve the current advert payload. + void setPreferredParams(uint16_t min, uint16_t max); + void addTxPower(); + void addData(String data); + void addData(char *data, size_t length); + String getPayload(); private: friend class BLEAdvertising; - String m_payload; // The payload of the advertisement. -}; // BLEAdvertisementData + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + String m_payload; +}; /** * @brief Perform and manage %BLE advertising. - * - * A %BLE server will want to perform advertising in order to make itself known to %BLE clients. */ class BLEAdvertising { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEAdvertising(); void addServiceUUID(BLEUUID serviceUUID); void addServiceUUID(const char *serviceUUID); bool removeServiceUUID(int index); bool removeServiceUUID(BLEUUID serviceUUID); bool removeServiceUUID(const char *serviceUUID); - bool start(); bool stop(); + void reset(); void setAppearance(uint16_t appearance); - void setAdvertisementType(esp_ble_adv_type_t adv_type); - void setAdvertisementChannelMap(esp_ble_adv_channel_t channel_map); + void setAdvertisementType(uint8_t adv_type); void setMaxInterval(uint16_t maxinterval); void setMinInterval(uint16_t mininterval); bool setAdvertisementData(BLEAdvertisementData &advertisementData); void setScanFilter(bool scanRequestWhitelistOnly, bool connectWhitelistOnly); bool setScanResponseData(BLEAdvertisementData &advertisementData); - void setPrivateAddress(esp_ble_addr_type_t type = BLE_ADDR_TYPE_RANDOM); - bool setDeviceAddress(esp_bd_addr_t addr, esp_ble_addr_type_t type = BLE_ADDR_TYPE_RANDOM); - - void handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); void setMinPreferred(uint16_t); void setMaxPreferred(uint16_t); void setScanResponse(bool); + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void setPrivateAddress(esp_ble_addr_type_t type = BLE_ADDR_TYPE_RANDOM); + bool setDeviceAddress(esp_bd_addr_t addr, esp_ble_addr_type_t type = BLE_ADDR_TYPE_RANDOM); + void setAdvertisementChannelMap(esp_ble_adv_channel_t channel_map); + void handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + bool start(); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void setName(String name); + void addTxPower(); + void advCompleteCB(); + bool isAdvertising(); + void onHostSync(); + bool start(uint32_t duration = 0, void (*advCompleteCB)(BLEAdvertising *pAdv) = nullptr); + static int handleGAPEvent(ble_gap_event *event, void *arg); +#endif + private: - esp_ble_adv_data_t m_advData; - esp_ble_adv_data_t m_scanRespData; // Used for configuration of scan response data when m_scanResp is true - esp_ble_adv_params_t m_advParams; + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + std::vector m_serviceUUIDs; - bool m_customAdvData = false; // Are we using custom advertising data? - bool m_customScanResponseData = false; // Are we using custom scan response data? + bool m_customAdvData = false; + bool m_customScanResponseData = false; FreeRTOS::Semaphore m_semaphoreSetAdv = FreeRTOS::Semaphore("startAdvert"); bool m_scanResp = true; + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_ble_adv_data_t m_advData; + esp_ble_adv_data_t m_scanRespData; + esp_ble_adv_params_t m_advParams; +#endif + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + ble_hs_adv_fields m_advData; + ble_hs_adv_fields m_scanData; + ble_gap_adv_params m_advParams; + bool m_advDataSet; + void (*m_advCompCB)(BLEAdvertising *pAdv); + uint8_t m_slaveItvl[4]; + uint32_t m_duration; + String m_name; +#endif }; -#ifdef SOC_BLE_50_SUPPORTED +/*************************************************************************** + * Bluedroid 5.0 specific classes * + ***************************************************************************/ +#if defined(SOC_BLE_50_SUPPORTED) && defined(CONFIG_BLUEDROID_ENABLED) class BLEMultiAdvertising { -private: - esp_ble_gap_ext_adv_params_t *params_arrays; - esp_ble_gap_ext_adv_t *ext_adv; - uint8_t count; - public: BLEMultiAdvertising(uint8_t num = 1); ~BLEMultiAdvertising() {} - bool setAdvertisingParams(uint8_t instance, const esp_ble_gap_ext_adv_params_t *params); bool setAdvertisingData(uint8_t instance, uint16_t length, const uint8_t *data); bool setScanRspData(uint8_t instance, uint16_t length, const uint8_t *data); bool start(); bool start(uint8_t num, uint8_t from); void setDuration(uint8_t instance, int duration = 0, int max_events = 0); - bool setInstanceAddress(uint8_t instance, esp_bd_addr_t rand_addr); + bool setInstanceAddress(uint8_t instance, uint8_t *rand_addr); bool stop(uint8_t num_adv, const uint8_t *ext_adv_inst); bool remove(uint8_t instance); bool clear(); - bool setPeriodicAdvertisingParams(uint8_t instance, const esp_ble_gap_periodic_adv_params_t *params); bool setPeriodicAdvertisingData(uint8_t instance, uint16_t length, const uint8_t *data); bool startPeriodicAdvertising(uint8_t instance); -}; + bool setAdvertisingParams(uint8_t instance, const esp_ble_gap_ext_adv_params_t *params); + bool setPeriodicAdvertisingParams(uint8_t instance, const esp_ble_gap_periodic_adv_params_t *params); -#endif // SOC_BLE_50_SUPPORTED +private: + esp_ble_gap_ext_adv_params_t *params_arrays; + esp_ble_gap_ext_adv_t *ext_adv; + uint8_t count; +}; +#endif -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEADVERTISING_H_ */ diff --git a/libraries/BLE/src/BLEBeacon.cpp b/libraries/BLE/src/BLEBeacon.cpp index 43366a7b2d9..0a6c6b05258 100644 --- a/libraries/BLE/src/BLEBeacon.cpp +++ b/libraries/BLE/src/BLEBeacon.cpp @@ -3,17 +3,31 @@ * * Created on: Jan 4, 2018 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes and definitions * + ***************************************************************************/ + #include "BLEBeacon.h" #include "esp32-hal-log.h" #define ENDIAN_CHANGE_U16(x) ((((x) & 0xFF00) >> 8) + (((x) & 0xFF) << 8)) +/*************************************************************************** + * Common functions * + ***************************************************************************/ + BLEBeacon::BLEBeacon() { m_beaconData.manufacturerId = 0x4c00; m_beaconData.subType = 0x02; @@ -22,11 +36,11 @@ BLEBeacon::BLEBeacon() { m_beaconData.minor = 0; m_beaconData.signalPower = 0; memset(m_beaconData.proximityUUID, 0, sizeof(m_beaconData.proximityUUID)); -} // BLEBeacon +} String BLEBeacon::getData() { return String((char *)&m_beaconData, sizeof(m_beaconData)); -} // getData +} uint16_t BLEBeacon::getMajor() { return m_beaconData.major; @@ -48,37 +62,38 @@ int8_t BLEBeacon::getSignalPower() { return m_beaconData.signalPower; } -/** - * Set the raw data for the beacon record. - */ void BLEBeacon::setData(String data) { if (data.length() != sizeof(m_beaconData)) { log_e("Unable to set the data ... length passed in was %d and expected %d", data.length(), sizeof(m_beaconData)); return; } memcpy(&m_beaconData, data.c_str(), sizeof(m_beaconData)); -} // setData +} void BLEBeacon::setMajor(uint16_t major) { m_beaconData.major = ENDIAN_CHANGE_U16(major); -} // setMajor +} void BLEBeacon::setManufacturerId(uint16_t manufacturerId) { m_beaconData.manufacturerId = ENDIAN_CHANGE_U16(manufacturerId); -} // setManufacturerId +} void BLEBeacon::setMinor(uint16_t minor) { m_beaconData.minor = ENDIAN_CHANGE_U16(minor); -} // setMinior - -void BLEBeacon::setProximityUUID(BLEUUID uuid) { - uuid = uuid.to128(); - memcpy(m_beaconData.proximityUUID, uuid.getNative()->uuid.uuid128, 16); -} // setProximityUUID +} void BLEBeacon::setSignalPower(int8_t signalPower) { m_beaconData.signalPower = signalPower; -} // setSignalPower +} +void BLEBeacon::setProximityUUID(BLEUUID uuid) { + uuid = uuid.to128(); +#if defined(CONFIG_BLUEDROID_ENABLED) + memcpy(m_beaconData.proximityUUID, uuid.getNative()->uuid.uuid128, 16); +#elif defined(CONFIG_NIMBLE_ENABLED) + memcpy(m_beaconData.proximityUUID, uuid.getNative()->u128.value, 16); #endif +} + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEBeacon.h b/libraries/BLE/src/BLEBeacon.h index dcc41aafeb4..e0eaddda6e4 100644 --- a/libraries/BLE/src/BLEBeacon.h +++ b/libraries/BLE/src/BLEBeacon.h @@ -3,6 +3,10 @@ * * Created on: Jan 4, 2018 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEBEACON_H_ @@ -10,6 +14,13 @@ #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED +#include "sdkconfig.h" +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include "BLEUUID.h" /** * @brief Representation of a beacon. @@ -18,6 +29,10 @@ */ class BLEBeacon { private: + /*************************************************************************** + * Common types * + ***************************************************************************/ + struct { uint16_t manufacturerId; uint8_t subType; @@ -29,6 +44,10 @@ class BLEBeacon { } __attribute__((packed)) m_beaconData; public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEBeacon(); String getData(); uint16_t getMajor(); @@ -44,5 +63,6 @@ class BLEBeacon { void setSignalPower(int8_t signalPower); }; // BLEBeacon +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEBEACON_H_ */ diff --git a/libraries/BLE/src/BLECharacteristic.cpp b/libraries/BLE/src/BLECharacteristic.cpp index b03d524a6a5..0234cd11cca 100644 --- a/libraries/BLE/src/BLECharacteristic.cpp +++ b/libraries/BLE/src/BLECharacteristic.cpp @@ -3,12 +3,22 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include #include @@ -23,10 +33,31 @@ #include "GeneralUtils.h" #include "esp32-hal-log.h" +/*************************************************************************** + * Common definitions * + ***************************************************************************/ + #define NULL_HANDLE (0xffff) +/*************************************************************************** + * NimBLE definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#define NIMBLE_SUB_NOTIFY 0x0001 +#define NIMBLE_SUB_INDICATE 0x0002 +#endif + +/*************************************************************************** + * Common global variables * + ***************************************************************************/ + static BLECharacteristicCallbacks defaultCallback; //null-object-pattern +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief Construct a characteristic * @param [in] uuid - UUID (const char*) for the characteristic. @@ -42,15 +73,23 @@ BLECharacteristic::BLECharacteristic(const char *uuid, uint32_t properties) : BL BLECharacteristic::BLECharacteristic(BLEUUID uuid, uint32_t properties) { m_bleUUID = uuid; m_handle = NULL_HANDLE; - m_properties = (esp_gatt_char_prop_t)0; m_pCallbacks = &defaultCallback; - setBroadcastProperty((properties & PROPERTY_BROADCAST) != 0); - setReadProperty((properties & PROPERTY_READ) != 0); - setWriteProperty((properties & PROPERTY_WRITE) != 0); - setNotifyProperty((properties & PROPERTY_NOTIFY) != 0); - setIndicateProperty((properties & PROPERTY_INDICATE) != 0); - setWriteNoResponseProperty((properties & PROPERTY_WRITE_NR) != 0); +#ifdef CONFIG_BLUEDROID_ENABLED + m_properties = 0; + setBroadcastProperty((properties & BLECharacteristic::PROPERTY_BROADCAST) != 0); + setReadProperty((properties & BLECharacteristic::PROPERTY_READ) != 0); + setWriteProperty((properties & BLECharacteristic::PROPERTY_WRITE) != 0); + setNotifyProperty((properties & BLECharacteristic::PROPERTY_NOTIFY) != 0); + setIndicateProperty((properties & BLECharacteristic::PROPERTY_INDICATE) != 0); + setWriteNoResponseProperty((properties & BLECharacteristic::PROPERTY_WRITE_NR) != 0); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_readMux = portMUX_INITIALIZER_UNLOCKED; + m_removed = 0; + m_properties = properties; +#endif } // BLECharacteristic /** @@ -66,51 +105,20 @@ BLECharacteristic::~BLECharacteristic() { * @return N/A. */ void BLECharacteristic::addDescriptor(BLEDescriptor *pDescriptor) { +#ifdef CONFIG_NIMBLE_ENABLED + if (pDescriptor->getUUID() == BLEUUID(uint16_t(0x2902))) { + log_i("NimBLE automatically creates the 0x2902 descriptor if a characteristic has a notification or indication property assigned to it.\n" + "You should check the characteristic properties for notification or indication rather than adding the descriptor manually.\n" + "This will be removed in a future version of the library."); + pDescriptor->executeCreate(this); + return; + } +#endif log_v(">> addDescriptor(): Adding %s to %s", pDescriptor->toString().c_str(), toString().c_str()); m_descriptorMap.setByUUID(pDescriptor->getUUID(), pDescriptor); log_v("<< addDescriptor()"); } // addDescriptor -/** - * @brief Register a new characteristic with the ESP runtime. - * @param [in] pService The service with which to associate this characteristic. - */ -void BLECharacteristic::executeCreate(BLEService *pService) { - log_v(">> executeCreate()"); - - if (m_handle != NULL_HANDLE) { - log_e("Characteristic already has a handle."); - return; - } - - m_pService = pService; // Save the service to which this characteristic belongs. - - log_d("Registering characteristic (esp_ble_gatts_add_char): uuid: %s, service: %s", getUUID().toString().c_str(), m_pService->toString().c_str()); - - esp_attr_control_t control; - control.auto_rsp = ESP_GATT_RSP_BY_APP; - - m_semaphoreCreateEvt.take("executeCreate"); - esp_err_t errRc = ::esp_ble_gatts_add_char( - m_pService->getHandle(), getUUID().getNative(), static_cast(m_permissions), getProperties(), nullptr, - &control - ); // Whether to auto respond or not. - - if (errRc != ESP_OK) { - log_e("<< esp_ble_gatts_add_char: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return; - } - m_semaphoreCreateEvt.wait("executeCreate"); - - BLEDescriptor *pDescriptor = m_descriptorMap.getFirst(); - while (pDescriptor != nullptr) { - pDescriptor->executeCreate(this); - pDescriptor = m_descriptorMap.getNext(); - } // End while - - log_v("<< executeCreate"); -} // executeCreate - /** * @brief Return the BLE Descriptor for the given UUID if associated with this characteristic. * @param [in] descriptorUUID The UUID of the descriptor that we wish to retrieve. @@ -137,8 +145,10 @@ uint16_t BLECharacteristic::getHandle() { return m_handle; } // getHandle -void BLECharacteristic::setAccessPermissions(esp_gatt_perm_t perm) { +void BLECharacteristic::setAccessPermissions(uint8_t perm) { +#ifdef CONFIG_BLUEDROID_ENABLED m_permissions = perm; +#endif } esp_gatt_char_prop_t BLECharacteristic::getProperties() { @@ -185,390 +195,76 @@ size_t BLECharacteristic::getLength() { } // getLength /** - * Handle a GATT server event. + * @brief Register a new characteristic with the ESP runtime. + * @param [in] pService The service with which to associate this characteristic. */ -void BLECharacteristic::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - log_v(">> handleGATTServerEvent: %s", BLEUtils::gattServerEventTypeToString(event).c_str()); +void BLECharacteristic::executeCreate(BLEService *pService) { + log_v(">> executeCreate()"); - switch (event) { - // Events handled: - // - // ESP_GATTS_ADD_CHAR_EVT - // ESP_GATTS_CONF_EVT - // ESP_GATTS_CONNECT_EVT - // ESP_GATTS_DISCONNECT_EVT - // ESP_GATTS_EXEC_WRITE_EVT - // ESP_GATTS_READ_EVT - // ESP_GATTS_WRITE_EVT + if (m_handle != NULL_HANDLE) { + log_e("Characteristic already has a handle."); + return; + } - // - // ESP_GATTS_EXEC_WRITE_EVT - // When we receive this event it is an indication that a previous write long needs to be committed. - // - // exec_write: - // - uint16_t conn_id - // - uint32_t trans_id - // - esp_bd_addr_t bda - // - uint8_t exec_write_flag - Either ESP_GATT_PREP_WRITE_EXEC or ESP_GATT_PREP_WRITE_CANCEL - // - case ESP_GATTS_EXEC_WRITE_EVT: - { - if (m_writeEvt) { - m_writeEvt = false; - if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { - m_value.commit(); - // Invoke the onWrite callback handler. - m_pCallbacks->onWrite(this, param); - } else { - m_value.cancel(); - } - // ??? - esp_err_t errRc = ::esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); - if (errRc != ESP_OK) { - log_e("esp_ble_gatts_send_response: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - } - } - break; - } // ESP_GATTS_EXEC_WRITE_EVT + m_pService = pService; // Save the service to which this characteristic belongs. - // ESP_GATTS_ADD_CHAR_EVT - Indicate that a characteristic was added to the service. - // add_char: - // - esp_gatt_status_t status - // - uint16_t attr_handle - // - uint16_t service_handle - // - esp_bt_uuid_t char_uuid - case ESP_GATTS_ADD_CHAR_EVT: - { - if (getHandle() == param->add_char.attr_handle) { - // we have created characteristic, now we can create descriptors - // BLEDescriptor* pDescriptor = m_descriptorMap.getFirst(); - // while (pDescriptor != nullptr) { - // pDescriptor->executeCreate(this); - // pDescriptor = m_descriptorMap.getNext(); - // } // End while - m_semaphoreCreateEvt.give(); - } - break; - } // ESP_GATTS_ADD_CHAR_EVT +#ifdef CONFIG_BLUEDROID_ENABLED + log_d("Registering characteristic (esp_ble_gatts_add_char): uuid: %s, service: %s", getUUID().toString().c_str(), m_pService->toString().c_str()); - // ESP_GATTS_WRITE_EVT - A request to write the value of a characteristic has arrived. - // - // write: - // - uint16_t conn_id - // - uint16_t trans_id - // - esp_bd_addr_t bda - // - uint16_t handle - // - uint16_t offset - // - bool need_rsp - // - bool is_prep - // - uint16_t len - // - uint8_t *value - // - case ESP_GATTS_WRITE_EVT: - { - // We check if this write request is for us by comparing the handles in the event. If it is for us - // we save the new value. Next we look at the need_rsp flag which indicates whether or not we need - // to send a response. If we do, then we formulate a response and send it. - if (param->write.handle == m_handle) { - if (param->write.is_prep) { - m_value.addPart(param->write.value, param->write.len); - m_writeEvt = true; - } else { - setValue(param->write.value, param->write.len); - } + esp_attr_control_t control; + control.auto_rsp = ESP_GATT_RSP_BY_APP; - log_d(" - Response to write event: New value: handle: %.2x, uuid: %s", getHandle(), getUUID().toString().c_str()); + m_semaphoreCreateEvt.take("executeCreate"); + esp_err_t errRc = ::esp_ble_gatts_add_char( + m_pService->getHandle(), getUUID().getNative(), static_cast(m_permissions), getProperties(), nullptr, + &control + ); // Whether to auto respond or not. + + if (errRc != ESP_OK) { + log_e("<< esp_ble_gatts_add_char: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return; + } + m_semaphoreCreateEvt.wait("executeCreate"); -// The call to BLEUtils::buildHexData() doesn't output anything if the log level is not -// "DEBUG". As it is quite CPU intensive, it is much better to not call it if not needed. -#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG - char *pHexData = BLEUtils::buildHexData(nullptr, param->write.value, param->write.len); - log_d(" - Data: length: %d, data: %s", param->write.len, pHexData); - free(pHexData); #endif - if (param->write.need_rsp) { - esp_gatt_rsp_t rsp; + BLEDescriptor *pDescriptor = m_descriptorMap.getFirst(); + while (pDescriptor != nullptr) { + pDescriptor->executeCreate(this); + pDescriptor = m_descriptorMap.getNext(); + } // End while - rsp.attr_value.len = param->write.len; - rsp.attr_value.handle = m_handle; - rsp.attr_value.offset = param->write.offset; - rsp.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; - memcpy(rsp.attr_value.value, param->write.value, param->write.len); + log_v("<< executeCreate"); +} // executeCreate - esp_err_t errRc = ::esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &rsp); - if (errRc != ESP_OK) { - log_e("esp_ble_gatts_send_response: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - } - } // Response needed +/** + * @brief Send an indication. + * An indication is a transmission of up to the first 20 bytes of the characteristic value. An indication + * will block waiting a positive confirmation from the client. + * @return N/A + */ +void BLECharacteristic::indicate() { - if (param->write.is_prep != true) { - // Invoke the onWrite callback handler. - m_pCallbacks->onWrite(this, param); - } - } // Match on handles. - break; - } // ESP_GATTS_WRITE_EVT + log_v(">> indicate: length: %d", m_value.getValue().length()); + notify(false); + log_v("<< indicate"); +} // indicate - // ESP_GATTS_READ_EVT - A request to read the value of a characteristic has arrived. - // - // read: - // - uint16_t conn_id - // - uint32_t trans_id - // - esp_bd_addr_t bda - // - uint16_t handle - // - uint16_t offset - // - bool is_long - // - bool need_rsp - // - case ESP_GATTS_READ_EVT: - { - if (param->read.handle == m_handle) { - - // Here's an interesting thing. The read request has the option of saying whether we need a response - // or not. What would it "mean" to receive a read request and NOT send a response back? That feels like - // a very strange read. - // - // We have to handle the case where the data we wish to send back to the client is greater than the maximum - // packet size of 22 bytes. In this case, we become responsible for chunking the data into units of 22 bytes. - // The apparent algorithm is as follows: - // - // If the is_long flag is set then this is a follow on from an original read and we will already have sent at least 22 bytes. - // If the is_long flag is not set then we need to check how much data we are going to send. If we are sending LESS than - // 22 bytes, then we "just" send it and that's the end of the story. - // If we are sending 22 bytes exactly, we just send it BUT we will get a follow on request. - // If we are sending more than 22 bytes, we send the first 22 bytes and we will get a follow on request. - // Because of follow on request processing, we need to maintain an offset of how much data we have already sent - // so that when a follow on request arrives, we know where to start in the data to send the next sequence. - // Note that the indication that the client will send a follow on request is that we sent exactly 22 bytes as a response. - // If our payload is divisible by 22 then the last response will be a response of 0 bytes in length. - // - // The following code has deliberately not been factored to make it fewer statements because this would cloud the - // the logic flow comprehension. - // - - // get mtu for peer device that we are sending read request to - uint16_t maxOffset = getService()->getServer()->getPeerMTU(param->read.conn_id) - 1; - log_d("mtu value: %d", maxOffset); - if (param->read.need_rsp) { - log_d("Sending a response (esp_ble_gatts_send_response)"); - esp_gatt_rsp_t rsp; - - if (param->read.is_long) { - String value = m_value.getValue(); - - if (value.length() - m_value.getReadOffset() < maxOffset) { - // This is the last in the chain - rsp.attr_value.len = value.length() - m_value.getReadOffset(); - rsp.attr_value.offset = m_value.getReadOffset(); - memcpy(rsp.attr_value.value, value.c_str() + rsp.attr_value.offset, rsp.attr_value.len); - m_value.setReadOffset(0); - } else { - // There will be more to come. - rsp.attr_value.len = maxOffset; - rsp.attr_value.offset = m_value.getReadOffset(); - memcpy(rsp.attr_value.value, value.c_str() + rsp.attr_value.offset, rsp.attr_value.len); - m_value.setReadOffset(rsp.attr_value.offset + maxOffset); - } - } else { // read.is_long == false - - // If is.long is false then this is the first (or only) request to read data, so invoke the callback - // Invoke the read callback. - m_pCallbacks->onRead(this, param); - - String value = m_value.getValue(); - - if (value.length() + 1 > maxOffset) { - // Too big for a single shot entry. - m_value.setReadOffset(maxOffset); - rsp.attr_value.len = maxOffset; - rsp.attr_value.offset = 0; - memcpy(rsp.attr_value.value, value.c_str(), rsp.attr_value.len); - } else { - // Will fit in a single packet with no callbacks required. - rsp.attr_value.len = value.length(); - rsp.attr_value.offset = 0; - memcpy(rsp.attr_value.value, value.c_str(), rsp.attr_value.len); - } - } - rsp.attr_value.handle = param->read.handle; - rsp.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; - -// The call to BLEUtils::buildHexData() doesn't output anything if the log level is not -// "DEBUG". As it is quite CPU intensive, it is much better to not call it if not needed. -#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG - char *pHexData = BLEUtils::buildHexData(nullptr, rsp.attr_value.value, rsp.attr_value.len); - log_d(" - Data: length=%d, data=%s, offset=%d", rsp.attr_value.len, pHexData, rsp.attr_value.offset); - free(pHexData); -#endif - - esp_err_t errRc = ::esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp); - if (errRc != ESP_OK) { - log_e("esp_ble_gatts_send_response: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - } - } // Response needed - } // Handle matches this characteristic. - break; - } // ESP_GATTS_READ_EVT - - // ESP_GATTS_CONF_EVT - // - // conf: - // - esp_gatt_status_t status – The status code. - // - uint16_t conn_id – The connection used. - // - case ESP_GATTS_CONF_EVT: - { - // log_d("m_handle = %d, conf->handle = %d", m_handle, param->conf.handle); - if (param->conf.conn_id - == getService()->getServer()->getConnId()) { // && param->conf.handle == m_handle) // bug in esp-idf and not implemented in arduino yet - m_semaphoreConfEvt.give(param->conf.status); - } - break; - } - - case ESP_GATTS_CONNECT_EVT: - { - break; - } - - case ESP_GATTS_DISCONNECT_EVT: - { - m_semaphoreConfEvt.give(); - break; - } - - default: - { - break; - } // default - - } // switch event - - // Give each of the descriptors associated with this characteristic the opportunity to handle the - // event. - - m_descriptorMap.handleGATTServerEvent(event, gatts_if, param); - log_v("<< handleGATTServerEvent"); -} // handleGATTServerEvent - -/** - * @brief Send an indication. - * An indication is a transmission of up to the first 20 bytes of the characteristic value. An indication - * will block waiting a positive confirmation from the client. - * @return N/A - */ -void BLECharacteristic::indicate() { - - log_v(">> indicate: length: %d", m_value.getValue().length()); - notify(false); - log_v("<< indicate"); -} // indicate - -/** - * @brief Send a notify. - * A notification is a transmission of up to the first 20 bytes of the characteristic value. An notification - * will not block; it is a fire and forget. - * @return N/A. - */ -void BLECharacteristic::notify(bool is_notification) { - log_v(">> notify: length: %d", m_value.getValue().length()); - - assert(getService() != nullptr); - assert(getService()->getServer() != nullptr); - - m_pCallbacks->onNotify(this); // Invoke the notify callback. - - // GeneralUtils::hexDump() doesn't output anything if the log level is not - // "VERBOSE". Additionally, it is very CPU intensive, even when it doesn't - // output anything! So it is much better to *not* call it at all if not needed. - // In a simple program which calls BLECharacteristic::notify() every 50 ms, - // the performance gain of this little optimization is 37% in release mode - // (-O3) and 57% in debug mode. - // Of course, the "#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE" guard - // could also be put inside the GeneralUtils::hexDump() function itself. But - // it's better to put it here also, as it is clearer (indicating a verbose log - // thing) and it allows to remove the "m_value.getValue().c_str()" call, which - // is, in itself, quite CPU intensive. -#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE - GeneralUtils::hexDump((uint8_t *)m_value.getValue().c_str(), m_value.getValue().length()); -#endif - - if (getService()->getServer()->getConnectedCount() == 0) { - log_v("<< notify: No connected clients."); - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NO_CLIENT, 0); - return; - } - - // Test to see if we have a 0x2902 descriptor. If we do, then check to see if notification is enabled - // and, if not, prevent the notification. - - BLE2902 *p2902 = (BLE2902 *)getDescriptorByUUID((uint16_t)0x2902); - if (is_notification) { - if (p2902 != nullptr && !p2902->getNotifications()) { - log_v("<< notifications disabled; ignoring"); - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NOTIFY_DISABLED, 0); // Invoke the notify callback. - return; - } - } else { - if (p2902 != nullptr && !p2902->getIndications()) { - log_v("<< indications disabled; ignoring"); - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_DISABLED, 0); // Invoke the notify callback. - return; - } - } - for (auto &myPair : getService()->getServer()->getPeerDevices(false)) { - uint16_t _mtu = (myPair.second.mtu); - if (m_value.getValue().length() > _mtu - 3) { - log_w("- Truncating to %d bytes (maximum notify size)", _mtu - 3); - } - - size_t length = m_value.getValue().length(); - if (!is_notification) { // is indication - m_semaphoreConfEvt.take("indicate"); - } - esp_err_t errRc = ::esp_ble_gatts_send_indicate( - getService()->getServer()->getGattsIf(), myPair.first, getHandle(), length, (uint8_t *)m_value.getValue().c_str(), !is_notification - ); // The need_confirm = false makes this a notify. - if (errRc != ESP_OK) { - log_e("<< esp_ble_gatts_send_ %s: rc=%d %s", is_notification ? "notify" : "indicate", errRc, GeneralUtils::errorToString(errRc)); - m_semaphoreConfEvt.give(); - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_GATT, errRc); // Invoke the notify callback. - return; - } - if (!is_notification) { // is indication - if (!m_semaphoreConfEvt.timedWait("indicate", indicationTimeout)) { - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_TIMEOUT, 0); // Invoke the notify callback. - } else { - auto code = (esp_gatt_status_t)m_semaphoreConfEvt.value(); - if (code == ESP_GATT_OK) { - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_INDICATE, code); // Invoke the notify callback. - } else { - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_FAILURE, code); - } - } - } else { - m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_NOTIFY, 0); // Invoke the notify callback. - } - } - log_v("<< notify"); -} // Notify - -/** - * @brief Set the permission to broadcast. - * A characteristics has properties associated with it which define what it is capable of doing. - * One of these is the broadcast flag. - * @param [in] value The flag value of the property. - * @return N/A - */ -void BLECharacteristic::setBroadcastProperty(bool value) { - //log_d("setBroadcastProperty(%d)", value); - if (value) { - m_properties = (esp_gatt_char_prop_t)(m_properties | ESP_GATT_CHAR_PROP_BIT_BROADCAST); - } else { - m_properties = (esp_gatt_char_prop_t)(m_properties & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); - } -} // setBroadcastProperty +/** + * @brief Set the permission to broadcast. + * A characteristics has properties associated with it which define what it is capable of doing. + * One of these is the broadcast flag. + * @param [in] value The flag value of the property. + * @return N/A + */ +void BLECharacteristic::setBroadcastProperty(bool value) { + //log_d("setBroadcastProperty(%d)", value); + if (value) { + m_properties = (esp_gatt_char_prop_t)(m_properties | ESP_GATT_CHAR_PROP_BIT_BROADCAST); + } else { + m_properties = (esp_gatt_char_prop_t)(m_properties & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); + } +} // setBroadcastProperty /** * @brief Set the callback handlers for this characteristic. @@ -595,9 +291,15 @@ void BLECharacteristic::setCallbacks(BLECharacteristicCallbacks *pCallbacks) { * @param [in] handle The handle associated with this characteristic. */ void BLECharacteristic::setHandle(uint16_t handle) { +#if defined(CONFIG_BLUEDROID_ENABLED) log_v(">> setHandle: handle=0x%.2x, characteristic uuid=%s", handle, getUUID().toString().c_str()); m_handle = handle; log_v("<< setHandle"); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + log_w("NimBLE does not support manually setting the handle of a characteristic. Ignoring request."); +#endif } // setHandle /** @@ -767,19 +469,12 @@ String BLECharacteristic::toString() { BLECharacteristicCallbacks::~BLECharacteristicCallbacks() {} -void BLECharacteristicCallbacks::onRead(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param) { - onRead(pCharacteristic); -} // onRead - +// Common callbacks void BLECharacteristicCallbacks::onRead(BLECharacteristic *pCharacteristic) { log_d(">> onRead: default"); log_d("<< onRead"); } // onRead -void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param) { - onWrite(pCharacteristic); -} // onWrite - void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic) { log_d(">> onWrite: default"); log_d("<< onWrite"); @@ -795,5 +490,650 @@ void BLECharacteristicCallbacks::onStatus(BLECharacteristic *pCharacteristic, St log_d("<< onStatus"); } // onStatus -#endif /* CONFIG_BLUEDROID_ENABLED */ +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +/** + * Handle a GATT server event. + */ +void BLECharacteristic::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { + log_v(">> handleGATTServerEvent: %s", BLEUtils::gattServerEventTypeToString(event).c_str()); + + switch (event) { + // Events handled: + // + // ESP_GATTS_ADD_CHAR_EVT + // ESP_GATTS_CONF_EVT + // ESP_GATTS_CONNECT_EVT + // ESP_GATTS_DISCONNECT_EVT + // ESP_GATTS_EXEC_WRITE_EVT + // ESP_GATTS_READ_EVT + // ESP_GATTS_WRITE_EVT + + // + // ESP_GATTS_EXEC_WRITE_EVT + // When we receive this event it is an indication that a previous write long needs to be committed. + // + // exec_write: + // - uint16_t conn_id + // - uint32_t trans_id + // - esp_bd_addr_t bda + // - uint8_t exec_write_flag - Either ESP_GATT_PREP_WRITE_EXEC or ESP_GATT_PREP_WRITE_CANCEL + // + case ESP_GATTS_EXEC_WRITE_EVT: + { + if (m_writeEvt) { + m_writeEvt = false; + if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { + m_value.commit(); + // Invoke the onWrite callback handler. + m_pCallbacks->onWrite(this, param); + } else { + m_value.cancel(); + } + // ??? + esp_err_t errRc = ::esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + if (errRc != ESP_OK) { + log_e("esp_ble_gatts_send_response: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } + } + break; + } // ESP_GATTS_EXEC_WRITE_EVT + + // ESP_GATTS_ADD_CHAR_EVT - Indicate that a characteristic was added to the service. + // add_char: + // - esp_gatt_status_t status + // - uint16_t attr_handle + // - uint16_t service_handle + // - esp_bt_uuid_t char_uuid + case ESP_GATTS_ADD_CHAR_EVT: + { + if (getHandle() == param->add_char.attr_handle) { + // we have created characteristic, now we can create descriptors + // BLEDescriptor* pDescriptor = m_descriptorMap.getFirst(); + // while (pDescriptor != nullptr) { + // pDescriptor->executeCreate(this); + // pDescriptor = m_descriptorMap.getNext(); + // } // End while + m_semaphoreCreateEvt.give(); + } + break; + } // ESP_GATTS_ADD_CHAR_EVT + + // ESP_GATTS_WRITE_EVT - A request to write the value of a characteristic has arrived. + // + // write: + // - uint16_t conn_id + // - uint16_t trans_id + // - esp_bd_addr_t bda + // - uint16_t handle + // - uint16_t offset + // - bool need_rsp + // - bool is_prep + // - uint16_t len + // - uint8_t *value + // + case ESP_GATTS_WRITE_EVT: + { + // We check if this write request is for us by comparing the handles in the event. If it is for us + // we save the new value. Next we look at the need_rsp flag which indicates whether or not we need + // to send a response. If we do, then we formulate a response and send it. + if (param->write.handle == m_handle) { + if (param->write.is_prep) { + m_value.addPart(param->write.value, param->write.len); + m_writeEvt = true; + } else { + setValue(param->write.value, param->write.len); + } + + log_d(" - Response to write event: New value: handle: %.2x, uuid: %s", getHandle(), getUUID().toString().c_str()); + +// The call to BLEUtils::buildHexData() doesn't output anything if the log level is not +// "DEBUG". As it is quite CPU intensive, it is much better to not call it if not needed. +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + char *pHexData = BLEUtils::buildHexData(nullptr, param->write.value, param->write.len); + log_d(" - Data: length: %d, data: %s", param->write.len, pHexData); + free(pHexData); +#endif + + if (param->write.need_rsp) { + esp_gatt_rsp_t rsp; + + rsp.attr_value.len = param->write.len; + rsp.attr_value.handle = m_handle; + rsp.attr_value.offset = param->write.offset; + rsp.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; + memcpy(rsp.attr_value.value, param->write.value, param->write.len); + + esp_err_t errRc = ::esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &rsp); + if (errRc != ESP_OK) { + log_e("esp_ble_gatts_send_response: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } + } // Response needed + + if (param->write.is_prep != true) { + // Invoke the onWrite callback handler. + m_pCallbacks->onWrite(this, param); + } + } // Match on handles. + break; + } // ESP_GATTS_WRITE_EVT + + // ESP_GATTS_READ_EVT - A request to read the value of a characteristic has arrived. + // + // read: + // - uint16_t conn_id + // - uint32_t trans_id + // - esp_bd_addr_t bda + // - uint16_t handle + // - uint16_t offset + // - bool is_long + // - bool need_rsp + // + case ESP_GATTS_READ_EVT: + { + if (param->read.handle == m_handle) { + + // Here's an interesting thing. The read request has the option of saying whether we need a response + // or not. What would it "mean" to receive a read request and NOT send a response back? That feels like + // a very strange read. + // + // We have to handle the case where the data we wish to send back to the client is greater than the maximum + // packet size of 22 bytes. In this case, we become responsible for chunking the data into units of 22 bytes. + // The apparent algorithm is as follows: + // + // If the is_long flag is set then this is a follow on from an original read and we will already have sent at least 22 bytes. + // If the is_long flag is not set then we need to check how much data we are going to send. If we are sending LESS than + // 22 bytes, then we "just" send it and that's the end of the story. + // If we are sending 22 bytes exactly, we just send it BUT we will get a follow on request. + // If we are sending more than 22 bytes, we send the first 22 bytes and we will get a follow on request. + // Because of follow on request processing, we need to maintain an offset of how much data we have already sent + // so that when a follow on request arrives, we know where to start in the data to send the next sequence. + // Note that the indication that the client will send a follow on request is that we sent exactly 22 bytes as a response. + // If our payload is divisible by 22 then the last response will be a response of 0 bytes in length. + // + // The following code has deliberately not been factored to make it fewer statements because this would cloud the + // the logic flow comprehension. + // + + // get mtu for peer device that we are sending read request to + uint16_t maxOffset = getService()->getServer()->getPeerMTU(param->read.conn_id) - 1; + log_d("mtu value: %d", maxOffset); + if (param->read.need_rsp) { + log_d("Sending a response (esp_ble_gatts_send_response)"); + esp_gatt_rsp_t rsp; + + if (param->read.is_long) { + String value = m_value.getValue(); + + if (value.length() - m_value.getReadOffset() < maxOffset) { + // This is the last in the chain + rsp.attr_value.len = value.length() - m_value.getReadOffset(); + rsp.attr_value.offset = m_value.getReadOffset(); + memcpy(rsp.attr_value.value, value.c_str() + rsp.attr_value.offset, rsp.attr_value.len); + m_value.setReadOffset(0); + } else { + // There will be more to come. + rsp.attr_value.len = maxOffset; + rsp.attr_value.offset = m_value.getReadOffset(); + memcpy(rsp.attr_value.value, value.c_str() + rsp.attr_value.offset, rsp.attr_value.len); + m_value.setReadOffset(rsp.attr_value.offset + maxOffset); + } + } else { // read.is_long == false + + // If is.long is false then this is the first (or only) request to read data, so invoke the callback + // Invoke the read callback. + m_pCallbacks->onRead(this, param); + + String value = m_value.getValue(); + + if (value.length() + 1 > maxOffset) { + // Too big for a single shot entry. + m_value.setReadOffset(maxOffset); + rsp.attr_value.len = maxOffset; + rsp.attr_value.offset = 0; + memcpy(rsp.attr_value.value, value.c_str(), rsp.attr_value.len); + } else { + // Will fit in a single packet with no callbacks required. + rsp.attr_value.len = value.length(); + rsp.attr_value.offset = 0; + memcpy(rsp.attr_value.value, value.c_str(), rsp.attr_value.len); + } + } + rsp.attr_value.handle = param->read.handle; + rsp.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; + +// The call to BLEUtils::buildHexData() doesn't output anything if the log level is not +// "DEBUG". As it is quite CPU intensive, it is much better to not call it if not needed. +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + char *pHexData = BLEUtils::buildHexData(nullptr, rsp.attr_value.value, rsp.attr_value.len); + log_d(" - Data: length=%d, data=%s, offset=%d", rsp.attr_value.len, pHexData, rsp.attr_value.offset); + free(pHexData); +#endif + + esp_err_t errRc = ::esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp); + if (errRc != ESP_OK) { + log_e("esp_ble_gatts_send_response: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } + } // Response needed + } // Handle matches this characteristic. + break; + } // ESP_GATTS_READ_EVT + + // ESP_GATTS_CONF_EVT + // + // conf: + // - esp_gatt_status_t status – The status code. + // - uint16_t conn_id – The connection used. + // + case ESP_GATTS_CONF_EVT: + { + // log_d("m_handle = %d, conf->handle = %d", m_handle, param->conf.handle); + if (param->conf.conn_id + == getService()->getServer()->getConnId()) { // && param->conf.handle == m_handle) // bug in esp-idf and not implemented in arduino yet + m_semaphoreConfEvt.give(param->conf.status); + } + break; + } + + case ESP_GATTS_CONNECT_EVT: + { + break; + } + + case ESP_GATTS_DISCONNECT_EVT: + { + m_semaphoreConfEvt.give(); + break; + } + + default: + { + break; + } // default + + } // switch event + + // Give each of the descriptors associated with this characteristic the opportunity to handle the + // event. + + m_descriptorMap.handleGATTServerEvent(event, gatts_if, param); + log_v("<< handleGATTServerEvent"); +} // handleGATTServerEvent + +/** + * @brief Send a notify. + * A notification is a transmission of up to the first 20 bytes of the characteristic value. An notification + * will not block; it is a fire and forget. + * @return N/A. + */ +void BLECharacteristic::notify(bool is_notification) { + log_v(">> notify: length: %d", m_value.getValue().length()); + + assert(getService() != nullptr); + assert(getService()->getServer() != nullptr); + + m_pCallbacks->onNotify(this); // Invoke the notify callback. + + // GeneralUtils::hexDump() doesn't output anything if the log level is not + // "VERBOSE". Additionally, it is very CPU intensive, even when it doesn't + // output anything! So it is much better to *not* call it at all if not needed. + // In a simple program which calls BLECharacteristic::notify() every 50 ms, + // the performance gain of this little optimization is 37% in release mode + // (-O3) and 57% in debug mode. + // Of course, the "#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE" guard + // could also be put inside the GeneralUtils::hexDump() function itself. But + // it's better to put it here also, as it is clearer (indicating a verbose log + // thing) and it allows to remove the "m_value.getValue().c_str()" call, which + // is, in itself, quite CPU intensive. +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + GeneralUtils::hexDump((uint8_t *)m_value.getValue().c_str(), m_value.getValue().length()); +#endif + + if (getService()->getServer()->getConnectedCount() == 0) { + log_v("<< notify: No connected clients."); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NO_CLIENT, 0); + return; + } + + // Test to see if we have a 0x2902 descriptor. If we do, then check to see if notification is enabled + // and, if not, prevent the notification. + + BLE2902 *p2902 = (BLE2902 *)getDescriptorByUUID((uint16_t)0x2902); + if (is_notification) { + if (p2902 != nullptr && !p2902->getNotifications()) { + log_v("<< notifications disabled; ignoring"); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NOTIFY_DISABLED, 0); // Invoke the notify callback. + return; + } + } else { + if (p2902 != nullptr && !p2902->getIndications()) { + log_v("<< indications disabled; ignoring"); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_DISABLED, 0); // Invoke the notify callback. + return; + } + } + for (auto &myPair : getService()->getServer()->getPeerDevices(false)) { + uint16_t _mtu = (myPair.second.mtu); + if (m_value.getValue().length() > _mtu - 3) { + log_w("- Truncating to %d bytes (maximum notify size)", _mtu - 3); + } + + size_t length = m_value.getValue().length(); + if (!is_notification) { // is indication + m_semaphoreConfEvt.take("indicate"); + } + esp_err_t errRc = ::esp_ble_gatts_send_indicate( + getService()->getServer()->getGattsIf(), myPair.first, getHandle(), length, (uint8_t *)m_value.getValue().c_str(), !is_notification + ); // The need_confirm = false makes this a notify. + if (errRc != ESP_OK) { + log_e("<< esp_ble_gatts_send_ %s: rc=%d %s", is_notification ? "notify" : "indicate", errRc, GeneralUtils::errorToString(errRc)); + m_semaphoreConfEvt.give(); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_GATT, errRc); // Invoke the notify callback. + return; + } + if (!is_notification) { // is indication + if (!m_semaphoreConfEvt.timedWait("indicate", indicationTimeout)) { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_TIMEOUT, 0); // Invoke the notify callback. + } else { + auto code = (esp_gatt_status_t)m_semaphoreConfEvt.value(); + if (code == ESP_GATT_OK) { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_INDICATE, code); // Invoke the notify callback. + } else { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_FAILURE, code); + } + } + } else { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_NOTIFY, 0); // Invoke the notify callback. + } + } + log_v("<< notify"); +} // Notify + +void BLECharacteristicCallbacks::onRead(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param) { + onRead(pCharacteristic); +} // onRead + +void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param) { + onWrite(pCharacteristic); +} // onWrite + +#endif /* CONFIG_BLUEDROID_ENABLED */ + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { + const ble_uuid_t *uuid; + int rc; + struct ble_gap_conn_desc desc; + BLECharacteristic *pCharacteristic = (BLECharacteristic *)arg; + + log_d("Characteristic %s %s event", pCharacteristic->getUUID().toString().c_str(), ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR ? "Read" : "Write"); + + uuid = ctxt->chr->uuid; + if (ble_uuid_cmp(uuid, &pCharacteristic->getUUID().getNative()->u) == 0) { + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_READ_CHR: + { + // If the packet header is only 8 bytes this is a follow up of a long read + // so we don't want to call the onRead() callback again. + if (ctxt->om->om_pkthdr_len > 8) { + rc = ble_gap_conn_find(conn_handle, &desc); + assert(rc == 0); + pCharacteristic->m_pCallbacks->onRead(pCharacteristic); + pCharacteristic->m_pCallbacks->onRead(pCharacteristic, &desc); + } + + portENTER_CRITICAL(&pCharacteristic->m_readMux); + rc = os_mbuf_append(ctxt->om, (uint8_t *)pCharacteristic->m_value.getValue().c_str(), pCharacteristic->m_value.getValue().length()); + portEXIT_CRITICAL(&pCharacteristic->m_readMux); + + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + case BLE_GATT_ACCESS_OP_WRITE_CHR: + { + if (ctxt->om->om_len > BLE_ATT_ATTR_MAX_LEN) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + uint8_t buf[BLE_ATT_ATTR_MAX_LEN]; + size_t len = ctxt->om->om_len; + memcpy(buf, ctxt->om->om_data, len); + + os_mbuf *next; + next = SLIST_NEXT(ctxt->om, om_next); + while (next != NULL) { + if ((len + next->om_len) > BLE_ATT_ATTR_MAX_LEN) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + memcpy(&buf[len], next->om_data, next->om_len); + len += next->om_len; + next = SLIST_NEXT(next, om_next); + } + rc = ble_gap_conn_find(conn_handle, &desc); + assert(rc == 0); + pCharacteristic->setValue(buf, len); + pCharacteristic->m_pCallbacks->onWrite(pCharacteristic); + pCharacteristic->m_pCallbacks->onWrite(pCharacteristic, &desc); + + return 0; + } + + default: break; + } + } + + return BLE_ATT_ERR_UNLIKELY; + + //m_descriptorMap.handleGATTServerEvent(conn_handle, attr_handle, ctxt, arg); +} + +/** + * @brief Set the subscribe status for this characteristic.\n + * This will maintain a vector of subscribed clients and their indicate/notify status. + */ +void BLECharacteristic::setSubscribe(struct ble_gap_event *event) { + ble_gap_conn_desc desc; + if (ble_gap_conn_find(event->subscribe.conn_handle, &desc) != 0) { + return; + } + + uint16_t subVal = 0; + if (event->subscribe.cur_notify > 0 && (m_properties & BLECharacteristic::PROPERTY_NOTIFY)) { + subVal |= NIMBLE_SUB_NOTIFY; + } + if (event->subscribe.cur_indicate && (m_properties & BLECharacteristic::PROPERTY_INDICATE)) { + subVal |= NIMBLE_SUB_INDICATE; + } + + log_i("New subscribe value for conn: %d val: %d", event->subscribe.conn_handle, subVal); + + if (!event->subscribe.cur_indicate && event->subscribe.prev_indicate) { + BLEDevice::getServer()->clearIndicateWait(event->subscribe.conn_handle); + } + + auto it = m_subscribedVec.begin(); + for (; it != m_subscribedVec.end(); ++it) { + if ((*it).first == event->subscribe.conn_handle) { + break; + } + } + + if (subVal > 0) { + if (it == m_subscribedVec.end()) { + m_subscribedVec.push_back({event->subscribe.conn_handle, subVal}); + } else { + (*it).second = subVal; + } + } else if (it != m_subscribedVec.end()) { + m_subscribedVec.erase(it); + } + + m_pCallbacks->onSubscribe(this, &desc, subVal); +} + +/** + * @brief Send a notify. + * A notification is a transmission of up to the first 20 bytes of the characteristic value. An notification + * will not block; it is a fire and forget. + * @return N/A. + */ +void BLECharacteristic::notify(bool is_notification) { + log_v(">> notify: length: %d", m_value.getValue().length()); + + assert(getService() != nullptr); + assert(getService()->getServer() != nullptr); + + int rc = 0; + m_pCallbacks->onNotify(this); // Invoke the notify callback. + + // GeneralUtils::hexDump() doesn't output anything if the log level is not + // "VERBOSE". Additionally, it is very CPU intensive, even when it doesn't + // output anything! So it is much better to *not* call it at all if not needed. + // In a simple program which calls BLECharacteristic::notify() every 50 ms, + // the performance gain of this little optimization is 37% in release mode + // (-O3) and 57% in debug mode. + // Of course, the "#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE" guard + // could also be put inside the GeneralUtils::hexDump() function itself. But + // it's better to put it here also, as it is clearer (indicating a verbose log + // thing) and it allows to remove the "m_value.getValue().c_str()" call, which + // is, in itself, quite CPU intensive. +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + GeneralUtils::hexDump((uint8_t *)m_value.getValue().c_str(), m_value.getValue().length()); +#endif + + if (getService()->getServer()->getConnectedCount() == 0) { + log_v("<< notify: No connected clients."); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NO_CLIENT, 0); + return; + } + + if (m_subscribedVec.size() == 0) { + log_v("<< notify: No clients subscribed."); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NO_SUBSCRIBER, 0); + return; + } + + if (is_notification) { + if (!(m_properties & BLECharacteristic::PROPERTY_NOTIFY)) { + log_v("<< notifications disabled; ignoring"); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NOTIFY_DISABLED, 0); // Invoke the notify callback. + return; + } + } else { + if (!(m_properties & BLECharacteristic::PROPERTY_INDICATE)) { + log_v("<< indications disabled; ignoring"); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_DISABLED, 0); // Invoke the notify callback. + return; + } + } + + bool reqSec = (m_properties & BLE_GATT_CHR_F_READ_AUTHEN) || (m_properties & BLE_GATT_CHR_F_READ_AUTHOR) || (m_properties & BLE_GATT_CHR_F_READ_ENC); + + for (auto &myPair : m_subscribedVec) { + uint16_t _mtu = getService()->getServer()->getPeerMTU(myPair.first); + + // check if connected and subscribed + if (_mtu == 0 || myPair.second == 0) { + continue; + } + + if (reqSec) { + struct ble_gap_conn_desc desc; + rc = ble_gap_conn_find(myPair.first, &desc); + if (rc != 0 || !desc.sec_state.encrypted) { + continue; + } + } + + String value = getValue(); + size_t length = value.length(); + + if (length > _mtu - 3) { + log_w("- Truncating to %d bytes (maximum notify size)", _mtu - 3); + } + + if (is_notification && (!(myPair.second & NIMBLE_SUB_NOTIFY))) { + log_w("Sending notification to client subscribed to indications, sending indication instead"); + is_notification = false; + } + + if (!is_notification && (!(myPair.second & NIMBLE_SUB_INDICATE))) { + log_w("Sending indication to client subscribed to notification, sending notification instead"); + is_notification = true; + } + + if (!is_notification) { // is indication + m_semaphoreConfEvt.take("indicate"); + } + + // don't create the m_buf until we are sure to send the data or else + // we could be allocating a buffer that doesn't get released. + // We also must create it in each loop iteration because it is consumed with each host call. + os_mbuf *om = ble_hs_mbuf_from_flat((uint8_t *)value.c_str(), length); + + if (!is_notification && (m_properties & BLECharacteristic::PROPERTY_INDICATE)) { + if (!BLEDevice::getServer()->setIndicateWait(myPair.first)) { + log_e("prior Indication in progress"); + os_mbuf_free_chain(om); + return; + } + + rc = ble_gatts_indicate_custom(myPair.first, m_handle, om); + if (rc != 0) { + BLEDevice::getServer()->clearIndicateWait(myPair.first); + } + } else { + rc = ble_gatts_notify_custom(myPair.first, m_handle, om); + } + + if (rc != 0) { + log_e("<< ble_gatts_%s_custom: rc=%d %s", is_notification ? "notify" : "indicate", rc, GeneralUtils::errorToString(rc)); + m_semaphoreConfEvt.give(); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_GATT, rc); // Invoke the notify callback. + return; + } + + if (!is_notification) { // is indication + if (!m_semaphoreConfEvt.timedWait("indicate", indicationTimeout)) { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_TIMEOUT, 0); // Invoke the notify callback. + } else { + auto code = m_semaphoreConfEvt.value(); + if (code == ESP_OK) { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_INDICATE, code); // Invoke the notify callback. + } else { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_FAILURE, code); + } + } + } else { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_NOTIFY, 0); // Invoke the notify callback. + } + } + log_v("<< notify"); +} // Notify + +void BLECharacteristicCallbacks::onRead(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc) { + onRead(pCharacteristic); +} // onRead + +void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc) { + onWrite(pCharacteristic); +} // onWrite + +void BLECharacteristicCallbacks::onSubscribe(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue) { + log_d(">> onSubscribe: default"); + log_d("<< onSubscribe"); +} // onSubscribe + +#endif /* CONFIG_NIMBLE_ENABLED */ + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLECharacteristic.h b/libraries/BLE/src/BLECharacteristic.h index 29f105868fd..27df5a30c3e 100644 --- a/libraries/BLE/src/BLECharacteristic.h +++ b/libraries/BLE/src/BLECharacteristic.h @@ -3,6 +3,10 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLECHARACTERISTIC_H_ @@ -11,15 +15,59 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include #include "BLEUUID.h" -#include -#include #include "BLEDescriptor.h" #include "BLEValue.h" #include "RTOS.h" +#include "BLEUtils.h" + +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#include +#endif + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#include "BLEConnInfo.h" +#define ESP_GATT_MAX_ATTR_LEN BLE_ATT_ATTR_MAX_LEN +#define ESP_GATT_CHAR_PROP_BIT_READ BLE_GATT_CHR_PROP_READ +#define ESP_GATT_CHAR_PROP_BIT_WRITE BLE_GATT_CHR_PROP_WRITE +#define ESP_GATT_CHAR_PROP_BIT_WRITE_NR BLE_GATT_CHR_PROP_WRITE_NO_RSP +#define ESP_GATT_CHAR_PROP_BIT_BROADCAST BLE_GATT_CHR_PROP_BROADCAST +#define ESP_GATT_CHAR_PROP_BIT_NOTIFY BLE_GATT_CHR_PROP_NOTIFY +#define ESP_GATT_CHAR_PROP_BIT_INDICATE BLE_GATT_CHR_PROP_INDICATE +#endif + +/*************************************************************************** + * NimBLE types * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +typedef uint16_t esp_gatt_char_prop_t; +typedef uint8_t esp_gatt_perm_t; +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ class BLEService; class BLEDescriptor; @@ -30,6 +78,10 @@ class BLECharacteristicCallbacks; */ class BLEDescriptorMap { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + void setByUUID(const char *uuid, BLEDescriptor *pDescriptor); void setByUUID(BLEUUID uuid, BLEDescriptor *pDescriptor); void setByHandle(uint16_t handle, BLEDescriptor *pDescriptor); @@ -37,11 +89,32 @@ class BLEDescriptorMap { BLEDescriptor *getByUUID(BLEUUID uuid); BLEDescriptor *getByHandle(uint16_t handle); String toString(); - void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); BLEDescriptor *getFirst(); BLEDescriptor *getNext(); + int getRegisteredDescriptorCount(); + void removeDescriptor(BLEDescriptor *pDescriptor); + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, ble_gatt_access_ctxt *ctxt, void *arg); +#endif private: + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + std::map m_uuidMap; std::map m_handleMap; std::map::iterator m_iterator; @@ -55,6 +128,48 @@ class BLEDescriptorMap { */ class BLECharacteristic { public: + /*************************************************************************** + * Common properties * + ***************************************************************************/ + + static const uint32_t indicationTimeout = 1000; + + /*************************************************************************** + * Bluedroid public properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + static const uint32_t PROPERTY_READ = 1 << 0; + static const uint32_t PROPERTY_WRITE = 1 << 1; + static const uint32_t PROPERTY_NOTIFY = 1 << 2; + static const uint32_t PROPERTY_BROADCAST = 1 << 3; + static const uint32_t PROPERTY_INDICATE = 1 << 4; + static const uint32_t PROPERTY_WRITE_NR = 1 << 5; +#endif + + /*************************************************************************** + * NimBLE public properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static const uint32_t PROPERTY_READ = BLE_GATT_CHR_F_READ; + static const uint32_t PROPERTY_READ_ENC = BLE_GATT_CHR_F_READ_ENC; + static const uint32_t PROPERTY_READ_AUTHEN = BLE_GATT_CHR_F_READ_AUTHEN; + static const uint32_t PROPERTY_READ_AUTHOR = BLE_GATT_CHR_F_READ_AUTHOR; + static const uint32_t PROPERTY_WRITE = BLE_GATT_CHR_F_WRITE; + static const uint32_t PROPERTY_WRITE_NR = BLE_GATT_CHR_F_WRITE_NO_RSP; + static const uint32_t PROPERTY_WRITE_ENC = BLE_GATT_CHR_F_WRITE_ENC; + static const uint32_t PROPERTY_WRITE_AUTHEN = BLE_GATT_CHR_F_WRITE_AUTHEN; + static const uint32_t PROPERTY_WRITE_AUTHOR = BLE_GATT_CHR_F_WRITE_AUTHOR; + static const uint32_t PROPERTY_BROADCAST = BLE_GATT_CHR_F_BROADCAST; + static const uint32_t PROPERTY_NOTIFY = BLE_GATT_CHR_F_NOTIFY; + static const uint32_t PROPERTY_INDICATE = BLE_GATT_CHR_F_INDICATE; +#endif + + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLECharacteristic(const char *uuid, uint32_t properties = 0); BLECharacteristic(BLEUUID uuid, uint32_t properties = 0); virtual ~BLECharacteristic(); @@ -66,14 +181,9 @@ class BLECharacteristic { String getValue(); uint8_t *getData(); size_t getLength(); - void indicate(); void notify(bool is_notification = true); - void setBroadcastProperty(bool value); void setCallbacks(BLECharacteristicCallbacks *pCallbacks); - void setIndicateProperty(bool value); - void setNotifyProperty(bool value); - void setReadProperty(bool value); void setValue(uint8_t *data, size_t size); void setValue(String value); void setValue(uint16_t &data16); @@ -81,20 +191,16 @@ class BLECharacteristic { void setValue(int &data32); void setValue(float &data32); void setValue(double &data64); - void setWriteProperty(bool value); - void setWriteNoResponseProperty(bool value); String toString(); uint16_t getHandle(); - void setAccessPermissions(esp_gatt_perm_t perm); - - static const uint32_t PROPERTY_READ = 1 << 0; - static const uint32_t PROPERTY_WRITE = 1 << 1; - static const uint32_t PROPERTY_NOTIFY = 1 << 2; - static const uint32_t PROPERTY_BROADCAST = 1 << 3; - static const uint32_t PROPERTY_INDICATE = 1 << 4; - static const uint32_t PROPERTY_WRITE_NR = 1 << 5; - - static const uint32_t indicationTimeout = 1000; + void setAccessPermissions(uint8_t perm); + esp_gatt_char_prop_t getProperties(); + void setReadProperty(bool value); + void setWriteProperty(bool value); + void setNotifyProperty(bool value); + void setBroadcastProperty(bool value); + void setIndicateProperty(bool value); + void setWriteNoResponseProperty(bool value); private: friend class BLEServer; @@ -102,6 +208,10 @@ class BLECharacteristic { friend class BLEDescriptor; friend class BLECharacteristicMap; + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + BLEUUID m_bleUUID; BLEDescriptorMap m_descriptorMap; uint16_t m_handle; @@ -109,18 +219,53 @@ class BLECharacteristic { BLECharacteristicCallbacks *m_pCallbacks; BLEService *m_pService; BLEValue m_value; + bool m_writeEvt = false; + FreeRTOS::Semaphore m_semaphoreConfEvt = FreeRTOS::Semaphore("ConfEvt"); + FreeRTOS::Semaphore m_semaphoreSetValue = FreeRTOS::Semaphore("SetValue"); + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + FreeRTOS::Semaphore m_semaphoreCreateEvt = FreeRTOS::Semaphore("CreateEvt"); esp_gatt_perm_t m_permissions = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; - bool m_writeEvt = false; // If we have started a long write, this tells the commit code that we were the target +#endif - void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + portMUX_TYPE m_readMux; + uint8_t m_removed; + std::vector> m_subscribedVec; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ void executeCreate(BLEService *pService); - esp_gatt_char_prop_t getProperties(); BLEService *getService(); void setHandle(uint16_t handle); - FreeRTOS::Semaphore m_semaphoreCreateEvt = FreeRTOS::Semaphore("CreateEvt"); - FreeRTOS::Semaphore m_semaphoreConfEvt = FreeRTOS::Semaphore("ConfEvt"); - FreeRTOS::Semaphore m_semaphoreSetValue = FreeRTOS::Semaphore("SetValue"); + + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE private declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void setSubscribe(struct ble_gap_event *event); + static int handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +#endif }; // BLECharacteristic /** @@ -132,6 +277,10 @@ class BLECharacteristic { */ class BLECharacteristicCallbacks { public: + /*************************************************************************** + * Common public types * + ***************************************************************************/ + typedef enum { SUCCESS_INDICATE, SUCCESS_NOTIFY, @@ -139,51 +288,41 @@ class BLECharacteristicCallbacks { ERROR_NOTIFY_DISABLED, ERROR_GATT, ERROR_NO_CLIENT, + ERROR_NO_SUBSCRIBER, ERROR_INDICATE_TIMEOUT, ERROR_INDICATE_FAILURE } Status; - virtual ~BLECharacteristicCallbacks(); + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ - /** - * @brief Callback function to support a read request. - * @param [in] pCharacteristic The characteristic that is the source of the event. - * @param [in] param The BLE GATTS param. Use param->read. - */ - virtual void onRead(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param); - /** - * @brief DEPRECATED! Callback function to support a read request. Called only if onRead(,) is not overridden - * @param [in] pCharacteristic The characteristic that is the source of the event. - */ + virtual ~BLECharacteristicCallbacks(); virtual void onRead(BLECharacteristic *pCharacteristic); + virtual void onWrite(BLECharacteristic *pCharacteristic); + virtual void onNotify(BLECharacteristic *pCharacteristic); + virtual void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code); - /** - * @brief Callback function to support a write request. - * @param [in] pCharacteristic The characteristic that is the source of the event. - * @param [in] param The BLE GATTS param. Use param->write. - */ + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + virtual void onRead(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param); virtual void onWrite(BLECharacteristic *pCharacteristic, esp_ble_gatts_cb_param_t *param); - /** - * @brief DEPRECATED! Callback function to support a write request. Called only if onWrite(,) is not overridden. - * @param [in] pCharacteristic The characteristic that is the source of the event. - */ - virtual void onWrite(BLECharacteristic *pCharacteristic); +#endif - /** - * @brief Callback function to support a Notify request. - * @param [in] pCharacteristic The characteristic that is the source of the event. - */ - virtual void onNotify(BLECharacteristic *pCharacteristic); + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ - /** - * @brief Callback function to support a Notify/Indicate Status report. - * @param [in] pCharacteristic The characteristic that is the source of the event. - * @param [in] s Status of the notification/indication - * @param [in] code Additional code of underlying errors - */ - virtual void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code); +#if defined(CONFIG_NIMBLE_ENABLED) + virtual void onRead(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc); + virtual void onWrite(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc); + virtual void onSubscribe(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue); +#endif }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLECHARACTERISTIC_H_ */ diff --git a/libraries/BLE/src/BLECharacteristicMap.cpp b/libraries/BLE/src/BLECharacteristicMap.cpp index 6f2c0bb1154..ec3588bcde4 100644 --- a/libraries/BLE/src/BLECharacteristicMap.cpp +++ b/libraries/BLE/src/BLECharacteristicMap.cpp @@ -3,19 +3,34 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include #include "BLEService.h" +#include "BLEUtils.h" #ifdef ARDUINO_ARCH_ESP32 #include "esp32-hal-log.h" #endif +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief Return the characteristic by handle. * @param [in] handle The handle to look up the characteristic. @@ -77,17 +92,22 @@ BLECharacteristic *BLECharacteristicMap::getNext() { } // getNext /** - * @brief Pass the GATT server event onwards to each of the characteristics found in the mapping - * @param [in] event - * @param [in] gatts_if - * @param [in] param + * @brief Get the number of registered characteristics. + * @return The number of registered characteristics. */ -void BLECharacteristicMap::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - // Invoke the handler for every Service we have. - for (auto &myPair : m_uuidMap) { - myPair.first->handleGATTServerEvent(event, gatts_if, param); - } -} // handleGATTServerEvent +int BLECharacteristicMap::getRegisteredCharacteristicCount() { + return m_uuidMap.size(); +} // getRegisteredCharacteristicCount + +/** + * @brief Removes characteristic from maps. + * @param [in] characteristic The characteristic to remove. + * @return N/A. + */ +void BLECharacteristicMap::removeCharacteristic(BLECharacteristic *characteristic) { + m_handleMap.erase(characteristic->getHandle()); + m_uuidMap.erase(characteristic); +} // removeCharacteristic /** * @brief Set the characteristic by handle. @@ -130,5 +150,37 @@ String BLECharacteristicMap::toString() { return res; } // toString -#endif /* CONFIG_BLUEDROID_ENABLED */ +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +/** + * @brief Pass the GATT server event onwards to each of the characteristics found in the mapping + * @param [in] event + * @param [in] gatts_if + * @param [in] param + */ +void BLECharacteristicMap::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { + // Invoke the handler for every Service we have. + for (auto &myPair : m_uuidMap) { + myPair.first->handleGATTServerEvent(event, gatts_if, param); + } +} // handleGATTServerEvent +#endif // CONFIG_BLUEDROID_ENABLED + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +void BLECharacteristicMap::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, ble_gatt_access_ctxt *ctxt, void *arg) { + // Invoke the handler for every Service we have. + for (auto &myPair : m_uuidMap) { + myPair.first->handleGATTServerEvent(conn_handle, attr_handle, ctxt, arg); + } +} +#endif // CONFIG_NIMBLE_ENABLED + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEClient.cpp b/libraries/BLE/src/BLEClient.cpp index 29fa0fbc140..c101993d002 100644 --- a/libraries/BLE/src/BLEClient.cpp +++ b/libraries/BLE/src/BLEClient.cpp @@ -3,17 +3,23 @@ * * Created on: Mar 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include -#include -#include -#include -#include // ESP32 BLE #include "BLEClient.h" #include "BLEUtils.h" #include "BLEService.h" @@ -24,6 +30,39 @@ #include "BLEDevice.h" #include "esp32-hal-log.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#include +#include +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#include +#include +#endif + +/*************************************************************************** + * Common global variables * + ***************************************************************************/ + +static BLEClientCallbacks defaultCallbacks; + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /* * Design * ------ @@ -46,11 +85,30 @@ */ BLEClient::BLEClient() { - m_pClientCallbacks = nullptr; + m_pClientCallbacks = &defaultCallbacks; m_conn_id = ESP_GATT_IF_NONE; - m_gattc_if = ESP_GATT_IF_NONE; m_haveServices = false; m_isConnected = false; // Initially, we are flagged as not connected. + +#if defined(CONFIG_BLUEDROID_ENABLED) + m_gattc_if = ESP_GATT_IF_NONE; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_connectTimeout = 30000; + m_pTaskData = nullptr; + m_lastErr = 0; + m_terminateFailCount = 0; + + m_pConnParams.scan_itvl = 16; // Scan interval in 0.625ms units (NimBLE Default) + m_pConnParams.scan_window = 16; // Scan window in 0.625ms units (NimBLE Default) + m_pConnParams.itvl_min = BLE_GAP_INITIAL_CONN_ITVL_MIN; // min_int = 0x10*1.25ms = 20ms + m_pConnParams.itvl_max = BLE_GAP_INITIAL_CONN_ITVL_MAX; // max_int = 0x20*1.25ms = 40ms + m_pConnParams.latency = BLE_GAP_INITIAL_CONN_LATENCY; // number of packets allowed to skip (extends max interval) + m_pConnParams.supervision_timeout = BLE_GAP_INITIAL_SUPERVISION_TIMEOUT; // timeout = 400*10ms = 4000ms + m_pConnParams.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN; // Minimum length of connection event in 0.625ms units + m_pConnParams.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN; // Maximum length of connection event in 0.625ms units +#endif } // BLEClient /** @@ -86,7 +144,7 @@ void BLEClient::clearServices() { */ bool BLEClient::connect(BLEAdvertisedDevice *device) { BLEAddress address = device->getAddress(); - esp_ble_addr_type_t type = device->getAddressType(); + uint8_t type = device->getAddressType(); return connect(address, type); } @@ -95,10 +153,262 @@ bool BLEClient::connect(BLEAdvertisedDevice *device) { */ bool BLEClient::connectTimeout(BLEAdvertisedDevice *device, uint32_t timeoutMs) { BLEAddress address = device->getAddress(); - esp_ble_addr_type_t type = device->getAddressType(); + uint8_t type = device->getAddressType(); return connect(address, type, timeoutMs); } +esp_gatt_if_t BLEClient::getGattcIf() { +#if defined(CONFIG_BLUEDROID_ENABLED) + return m_gattc_if; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + log_e("NimBLE does not support getGattcIf()"); + return ESP_GATT_IF_NONE; +#endif +} // getGattcIf + +/** + * @brief Initiate a secure connection (pair/bond) with the server.\n + * Called automatically when a characteristic or descriptor requires encryption or authentication to access it. + * @return True on success. + */ +bool BLEClient::secureConnection() { +#if defined(CONFIG_BLUEDROID_ENABLED) + log_i("secureConnection() does not need to be called for Bluedroid"); + return true; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + int retryCount = 1; + BLETaskData taskData(const_cast(this), BLE_HS_ENOTCONN); + m_pTaskData = &taskData; + + do { + if (BLESecurity::startSecurity(m_conn_id)) { + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + } + + } while (taskData.m_flags == (BLE_HS_ERR_HCI_BASE + BLE_ERR_PINKEY_MISSING) && retryCount--); + + m_pTaskData = nullptr; + + if (taskData.m_flags == 0) { + log_d("<< secureConnection: success"); + return true; + } + + m_lastErr = taskData.m_flags; + log_e("secureConnection: failed rc=%d", taskData.m_flags); + return false; +#endif +} // secureConnection + +uint16_t BLEClient::getConnId() { + return m_conn_id; +} // getConnId + +/** + * @brief Retrieve the address of the peer. + * + * Returns the Bluetooth device address of the %BLE peer to which this client is connected. + */ +BLEAddress BLEClient::getPeerAddress() { + return m_peerAddress; +} // getAddress + +/** + * @brief Ask the BLE server for the RSSI value. + * @return The RSSI value. + */ +int BLEClient::getRssi() { + log_v(">> getRssi()"); + if (!isConnected()) { + log_v("<< getRssi(): Not connected"); + return 0; + } + +#if defined(CONFIG_BLUEDROID_ENABLED) + // We make the API call to read the RSSI value which is an asynchronous operation. We expect to receive + // an ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT to indicate completion. + // + m_semaphoreRssiCmplEvt.take("getRssi"); + esp_err_t rc = ::esp_ble_gap_read_rssi(getPeerAddress().getNative()); + if (rc != ESP_OK) { + log_e("<< getRssi: esp_ble_gap_read_rssi: rc=%d %s", rc, GeneralUtils::errorToString(rc)); + return 0; + } + int rssiValue = m_semaphoreRssiCmplEvt.wait("getRssi"); +#endif // CONFIG_BLUEDROID_ENABLED + +#if defined(CONFIG_NIMBLE_ENABLED) + int8_t rssiValue = 0; + int rc = ble_gap_conn_rssi(m_conn_id, &rssiValue); + if (rc != 0) { + log_e("<< getRssi: ble_gap_conn_rssi: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + return 0; + } +#endif // CONFIG_BLUEDROID_ENABLED + log_v("<< getRssi(): %d", rssiValue); + return rssiValue; +} // getRssi + +/** + * @brief Get the service BLE Remote Service instance corresponding to the uuid. + * @param [in] uuid The UUID of the service being sought. + * @return A reference to the Service or nullptr if don't know about it. + */ +BLERemoteService *BLEClient::getService(const char *uuid) { + return getService(BLEUUID(uuid)); +} // getService + +/** + * @brief Get the service object corresponding to the uuid. + * @param [in] uuid The UUID of the service being sought. + * @return A reference to the Service or nullptr if don't know about it. + * @throws BLEUuidNotFound + */ +BLERemoteService *BLEClient::getService(BLEUUID uuid) { + log_v(">> getService: uuid: %s", uuid.toString().c_str()); + // Design + // ------ + // We wish to retrieve the service given its UUID. It is possible that we have not yet asked the + // device what services it has in which case we have nothing to match against. If we have not + // asked the device about its services, then we do that now. Once we get the results we can then + // examine the services map to see if it has the service we are looking for. + if (!m_haveServices) { + getServices(); + } + std::string uuidStr = uuid.toString().c_str(); + for (auto &myPair : m_servicesMap) { + if (myPair.first == uuidStr) { + log_v("<< getService: found the service with uuid: %s", uuid.toString().c_str()); + return myPair.second; + } + } // End of each of the services. + log_v("<< getService: not found"); + return nullptr; +} // getService + +/** + * @brief Get the value of a specific characteristic associated with a specific service. + * @param [in] serviceUUID The service that owns the characteristic. + * @param [in] characteristicUUID The characteristic whose value we wish to read. + * @throws BLEUuidNotFound + */ +String BLEClient::getValue(BLEUUID serviceUUID, BLEUUID characteristicUUID) { + log_v(">> getValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); + String ret = getService(serviceUUID)->getCharacteristic(characteristicUUID)->readValue(); + log_v("<> setValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); + getService(serviceUUID)->getCharacteristic(characteristicUUID)->writeValue(value); + log_v("<< setValue"); +} // setValue + +uint16_t BLEClient::getMTU() { +#ifdef CONFIG_BLUEDROID_ENABLED + return m_mtu; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + return ble_att_mtu(m_conn_id); +#endif +} + +/** + @brief Set the local and remote MTU size. + Should be called once after client connects if MTU size needs to be changed. + @return bool indicating if MTU was successfully set locally and on remote. +*/ +bool BLEClient::setMTU(uint16_t mtu) { + log_v(">> setMTU: %d", mtu); + esp_err_t err = ESP_OK; + +#ifdef CONFIG_BLUEDROID_ENABLED + err = esp_ble_gatt_set_local_mtu(mtu); //First must set local MTU value. +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + err = ble_att_set_preferred_mtu(mtu); +#endif + + if (err == ESP_OK) { +#ifdef CONFIG_BLUEDROID_ENABLED + err = esp_ble_gattc_send_mtu_req(m_gattc_if, m_conn_id); //Once local is set successfully set remote size +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + //err = ble_gattc_exchange_mtu(m_conn_id, nullptr, nullptr); +#endif + + if (err != ESP_OK) { + log_e("Error setting send MTU request MTU: %d err=%d", mtu, err); + return false; + } + } else { + log_e("can't set local mtu value: %d", mtu); + return false; + } + log_v("<< setMTU"); + + m_mtu = mtu; //successfully changed + + return true; +} + +/** + * @brief Return a string representation of this client. + * @return A string representation of this client. + */ +String BLEClient::toString() { + String res = "peer address: " + m_peerAddress.toString(); + res += "\nServices:\n"; + for (auto &myPair : m_servicesMap) { + res += myPair.second->toString() + "\n"; + // myPair.second is the value + } + return res; +} // toString + +void BLEClientCallbacks::onConnect(BLEClient *pClient) { + log_d("BLEClientCallbacks", "onConnect: default"); +} + +void BLEClientCallbacks::onDisconnect(BLEClient *pClient) { + log_d("BLEClientCallbacks", "onDisconnect: default"); +} + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + /** * @brief Connect to the partner (BLE Server). * @param [in] address The address of the partner. @@ -106,7 +416,7 @@ bool BLEClient::connectTimeout(BLEAdvertisedDevice *device, uint32_t timeoutMs) * @param [in] timeoutMs The number of milliseconds to wait for the connection to complete. * @return True on success. */ -bool BLEClient::connect(BLEAddress address, esp_ble_addr_type_t type, uint32_t timeoutMs) { +bool BLEClient::connect(BLEAddress address, uint8_t type, uint32_t timeoutMs) { log_v(">> connect(%s)", address.toString().c_str()); // We need the connection handle that we get from registering the application. We register the app @@ -141,9 +451,9 @@ bool BLEClient::connect(BLEAddress address, esp_ble_addr_type_t type, uint32_t t m_semaphoreOpenEvt.take("connect"); errRc = ::esp_ble_gattc_open( m_gattc_if, - *getPeerAddress().getNative(), // address - type, // Note: This was added on 2018-04-03 when the latest ESP-IDF was detected to have changed the signature. - 1 // direct connection <-- maybe needs to be changed in case of direct indirect connection??? + getPeerAddress().getNative(), // address + (esp_ble_addr_type_t)type, // Note: This was added on 2018-04-03 when the latest ESP-IDF was detected to have changed the signature. + 1 // direct connection <-- maybe needs to be changed in case of direct indirect connection??? ); if (errRc != ESP_OK) { log_e("esp_ble_gattc_open: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); @@ -165,16 +475,17 @@ bool BLEClient::connect(BLEAddress address, esp_ble_addr_type_t type, uint32_t t /** * @brief Disconnect from the peer. - * @return N/A. + * @return error code from bluedroid, 0 = success. */ -void BLEClient::disconnect() { +int BLEClient::disconnect(uint8_t reason) { log_v(">> disconnect()"); esp_err_t errRc = ::esp_ble_gattc_close(getGattcIf(), getConnId()); if (errRc != ESP_OK) { log_e("esp_ble_gattc_close: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return; + return errRc; } log_v("<< disconnect()"); + return ESP_OK; } // disconnect /** @@ -361,89 +672,11 @@ void BLEClient::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t } // gattClientEventHandler -uint16_t BLEClient::getConnId() { - return m_conn_id; -} // getConnId - -esp_gatt_if_t BLEClient::getGattcIf() { - return m_gattc_if; -} // getGattcIf - -/** - * @brief Retrieve the address of the peer. - * - * Returns the Bluetooth device address of the %BLE peer to which this client is connected. - */ -BLEAddress BLEClient::getPeerAddress() { - return m_peerAddress; -} // getAddress - /** - * @brief Ask the BLE server for the RSSI value. - * @return The RSSI value. - */ -int BLEClient::getRssi() { - log_v(">> getRssi()"); - if (!isConnected()) { - log_v("<< getRssi(): Not connected"); - return 0; - } - // We make the API call to read the RSSI value which is an asynchronous operation. We expect to receive - // an ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT to indicate completion. - // - m_semaphoreRssiCmplEvt.take("getRssi"); - esp_err_t rc = ::esp_ble_gap_read_rssi(*getPeerAddress().getNative()); - if (rc != ESP_OK) { - log_e("<< getRssi: esp_ble_gap_read_rssi: rc=%d %s", rc, GeneralUtils::errorToString(rc)); - return 0; - } - int rssiValue = m_semaphoreRssiCmplEvt.wait("getRssi"); - log_v("<< getRssi(): %d", rssiValue); - return rssiValue; -} // getRssi - -/** - * @brief Get the service BLE Remote Service instance corresponding to the uuid. - * @param [in] uuid The UUID of the service being sought. - * @return A reference to the Service or nullptr if don't know about it. - */ -BLERemoteService *BLEClient::getService(const char *uuid) { - return getService(BLEUUID(uuid)); -} // getService - -/** - * @brief Get the service object corresponding to the uuid. - * @param [in] uuid The UUID of the service being sought. - * @return A reference to the Service or nullptr if don't know about it. - * @throws BLEUuidNotFound - */ -BLERemoteService *BLEClient::getService(BLEUUID uuid) { - log_v(">> getService: uuid: %s", uuid.toString().c_str()); - // Design - // ------ - // We wish to retrieve the service given its UUID. It is possible that we have not yet asked the - // device what services it has in which case we have nothing to match against. If we have not - // asked the device about its services, then we do that now. Once we get the results we can then - // examine the services map to see if it has the service we are looking for. - if (!m_haveServices) { - getServices(); - } - std::string uuidStr = uuid.toString().c_str(); - for (auto &myPair : m_servicesMap) { - if (myPair.first == uuidStr) { - log_v("<< getService: found the service with uuid: %s", uuid.toString().c_str()); - return myPair.second; - } - } // End of each of the services. - log_v("<< getService: not found"); - return nullptr; -} // getService - -/** - * @brief Ask the remote %BLE server for its services. - * A %BLE Server exposes a set of services for its partners. Here we ask the server for its set of - * services and wait until we have received them all. - * @return N/A + * @brief Ask the remote %BLE server for its services. + * A %BLE Server exposes a set of services for its partners. Here we ask the server for its set of + * services and wait until we have received them all. + * @return N/A */ std::map *BLEClient::getServices() { /* @@ -473,19 +706,6 @@ std::map *BLEClient::getServices() { return &m_servicesMap; } // getServices -/** - * @brief Get the value of a specific characteristic associated with a specific service. - * @param [in] serviceUUID The service that owns the characteristic. - * @param [in] characteristicUUID The characteristic whose value we wish to read. - * @throws BLEUuidNotFound - */ -String BLEClient::getValue(BLEUUID serviceUUID, BLEUUID characteristicUUID) { - log_v(">> getValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); - String ret = getService(serviceUUID)->getCharacteristic(characteristicUUID)->readValue(); - log_v("<> connect(%s)", address.toString().c_str()); + + if (!BLEDevice::m_synced) { + log_d("BLEClient", "Host reset, wait for sync."); + return false; + } + + if (m_conn_id != BLE_HS_CONN_HANDLE_NONE || m_isConnected || m_pTaskData != nullptr) { + log_e("Client busy, connected to %s, id=%d", m_peerAddress.toString().c_str(), getConnId()); + return false; + } + + ble_addr_t peerAddr_t; + memcpy(&peerAddr_t.val, address.getNative(), 6); + peerAddr_t.type = address.getType(); + if (ble_gap_conn_find_by_addr(&peerAddr_t, NULL) == 0) { + log_e("A connection to %s already exists", address.toString().c_str()); + return false; + } + + if (address == BLEAddress("")) { + log_e("Invalid peer address (NULL)"); + return false; + } + + m_appId = BLEDevice::m_appId++; + m_peerAddress = address; + + int rc = 0; + + /* Try to connect the the advertiser. Allow 30 seconds (30000 ms) for + * timeout (default value of m_connectTimeout). + * Loop on BLE_HS_EBUSY if the scan hasn't stopped yet. + */ + do { + rc = ble_gap_connect(BLEDevice::m_ownAddrType, &peerAddr_t, m_connectTimeout, &m_pConnParams, BLEClient::handleGAPEvent, this); + switch (rc) { + case 0: break; + + case BLE_HS_EBUSY: + // Scan was still running, stop it and try again + if (!BLEDevice::getScan()->stop()) { + rc = BLE_HS_EUNKNOWN; + } + break; + + case BLE_HS_EDONE: + // A connection to this device already exists, do not connect twice. + log_e("Already connected to device; addr=%s", m_peerAddress.toString().c_str()); + break; + + case BLE_HS_EALREADY: + // Already attempting to connect to this device, cancel the previous + // attempt and report failure here so we don't get 2 connections. + log_e("Already attempting to connect to %s - canceling", m_peerAddress.toString().c_str()); + ble_gap_conn_cancel(); + break; + + default: log_e("Failed to connect to %s, rc=%d; %s", m_peerAddress.toString().c_str(), rc, BLEUtils::returnCodeToString(rc)); break; + } + + } while (rc == BLE_HS_EBUSY); + + if (rc != 0) { + m_lastErr = rc; + return false; + } + + BLETaskData taskData(this); + m_pTaskData = &taskData; + + // Wait for the connect timeout time +1 second for the connection to complete + if (!BLEUtils::taskWait(taskData, m_connectTimeout + 1000)) { + // If a connection was made but no response from MTU exchange proceed anyway + if (isConnected()) { + taskData.m_flags = 0; + } else { + // workaround; if the controller doesn't cancel the connection at the timeout, cancel it here. + log_e("Connect timeout - canceling"); + ble_gap_conn_cancel(); + taskData.m_flags = BLE_HS_ETIMEOUT; + } + } + + m_pTaskData = nullptr; + rc = taskData.m_flags; + + if (rc != 0) { + log_e("Connection failed; status=%d %s", rc, BLEUtils::returnCodeToString(rc)); + m_lastErr = rc; + return false; + } + + m_isConnected = true; + m_pClientCallbacks->onConnect(this); + + log_i("<< connect()"); + + BLEDevice::addPeerDevice(this, true, m_appId); + // Check if still connected before returning + return isConnected(); +} /** - * @brief Set the value of a specific characteristic associated with a specific service. - * @param [in] serviceUUID The service that owns the characteristic. - * @param [in] characteristicUUID The characteristic whose value we wish to write. - * @throws BLEUuidNotFound + * @brief STATIC Callback for the service discovery API function.\n + * When a service is found or there is none left or there was an error + * the API will call this and report findings. */ -void BLEClient::setValue(BLEUUID serviceUUID, BLEUUID characteristicUUID, String value) { - log_v(">> setValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); - getService(serviceUUID)->getCharacteristic(characteristicUUID)->writeValue(value); - log_v("<< setValue"); -} // setValue +int BLEClient::serviceDiscoveredCB(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_svc *service, void *arg) { + log_d("Service Discovered >> status: %d handle: %d", error->status, (error->status == 0) ? service->start_handle : -1); -uint16_t BLEClient::getMTU() { - return m_mtu; + BLETaskData *pTaskData = (BLETaskData *)arg; + BLEClient *client = (BLEClient *)pTaskData->m_pInstance; + + if (error->status == BLE_HS_ENOTCONN) { + log_e("<< Service Discovered; Disconnected"); + BLEUtils::taskRelease(*pTaskData, error->status); + return error->status; + } + + // Make sure the service discovery is for this device + if (client->getConnId() != conn_handle) { + return 0; + } + + if (error->status == 0) { + // Found a service - add it to the vector + BLERemoteService *pRemoteService = new BLERemoteService(client, service); + client->m_servicesMap.insert(std::pair(pRemoteService->getUUID().toString().c_str(), pRemoteService)); + client->m_servicesMapByInstID.insert(std::pair(pRemoteService, service->start_handle)); + return 0; + } + + BLEUtils::taskRelease(*pTaskData, error->status); + log_d("<< Service Discovered"); + return error->status; } +std::map *BLEClient::getServices() { + log_v(">> getServices"); + + clearServices(); // Clear any services that may exist. + + if (!isConnected()) { + log_e("Disconnected, could not retrieve services -aborting"); + return &m_servicesMap; + } + + int errRc = 0; + BLETaskData taskData(this); + + errRc = ble_gattc_disc_all_svcs(m_conn_id, BLEClient::serviceDiscoveredCB, &taskData); + + if (errRc != 0) { + log_e("ble_gattc_disc_all_svcs: rc=%d %s", errRc, BLEUtils::returnCodeToString(errRc)); + m_lastErr = errRc; + return &m_servicesMap; + } + + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + errRc = taskData.m_flags; + if (errRc == 0 || errRc == BLE_HS_EDONE) { + // If successful, remember that we now have services. + m_haveServices = m_servicesMap.size() > 0; + log_v("<< getServices"); + return &m_servicesMap; + } + + m_lastErr = errRc; + log_e("Could not retrieve services, rc=%d %s", errRc, BLEUtils::returnCodeToString(errRc)); + return &m_servicesMap; +} // getServices + +int BLEClient::handleGAPEvent(struct ble_gap_event *event, void *arg) { + BLEClient *client = (BLEClient *)arg; + int rc = 0; + BLETaskData *pTaskData = client->m_pTaskData; + + log_d("BLEClient", "Got Client event %s", BLEUtils::gapEventToString(event->type)); + + switch (event->type) { + case BLE_GAP_EVENT_DISCONNECT: + { + rc = event->disconnect.reason; + // If Host reset tell the device now before returning to prevent + // any errors caused by calling host functions before resyncing. + switch (rc) { + case BLE_HS_ECONTROLLER: + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_ENOTSYNCED: + case BLE_HS_EOS: + log_e("BLEClient", "Disconnect - host reset, rc=%d", rc); + BLEDevice::onReset(rc); + break; + default: + // Check that the event is for this client. + if (client->m_conn_id != event->disconnect.conn.conn_handle) { + return 0; + } + break; + } + + // Remove the device from ignore list so we will scan it again + // BLEDevice::removeIgnored(client->m_peerAddress); + + // No longer connected, clear the connection ID. + client->m_conn_id = BLE_HS_CONN_HANDLE_NONE; + client->m_terminateFailCount = 0; + + // If we received a connected event but did not get established (no PDU) + // then a disconnect event will be sent but we should not send it to the + // app for processing. Instead we will ensure the task is released + // and report the error. + if (!client->m_isConnected) { + break; + } + + log_i("BLEClient", "disconnect; reason=%d, %s", rc, BLEUtils::returnCodeToString(rc)); + + BLEDevice::removePeerDevice(client->m_appId, true); + client->m_isConnected = false; + if (client->m_pClientCallbacks != nullptr) { + client->m_pClientCallbacks->onDisconnect(client); + } + break; + } // BLE_GAP_EVENT_DISCONNECT + + case BLE_GAP_EVENT_CONNECT: + { + // If we aren't waiting for this connection response + // we should drop the connection immediately. + if (client->isConnected() || client->m_pTaskData == nullptr) { + ble_gap_terminate(event->connect.conn_handle, BLE_ERR_REM_USER_CONN_TERM); + return 0; + } + + rc = event->connect.status; + if (rc == 0) { + log_i("BLEClient: Connected event. Handle: %d", event->connect.conn_handle); + + client->m_conn_id = event->connect.conn_handle; + + rc = ble_gattc_exchange_mtu(client->m_conn_id, nullptr, nullptr); + if (rc != 0) { + log_e("BLEClient", "MTU exchange error; rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + break; + } + + // In the case of a multiconnecting device we ignore this device when + // scanning since we are already connected to it + // BLEDevice::addIgnored(client->m_peerAddress); + } else { + client->m_conn_id = BLE_HS_CONN_HANDLE_NONE; + break; + } + + return 0; + } // BLE_GAP_EVENT_CONNECT + + case BLE_GAP_EVENT_TERM_FAILURE: + { + if (client->m_conn_id != event->term_failure.conn_handle) { + return 0; + } + + log_e("Connection termination failure; rc=%d - retrying", event->term_failure.status); + if (++client->m_terminateFailCount > 2) { + ble_hs_sched_reset(BLE_HS_ECONTROLLER); + } else { + ble_gap_terminate(event->term_failure.conn_handle, BLE_ERR_REM_USER_CONN_TERM); + } + return 0; + } // BLE_GAP_EVENT_TERM_FAILURE + + case BLE_GAP_EVENT_NOTIFY_RX: + { + if (client->m_conn_id != event->notify_rx.conn_handle) { + return 0; + } + + // If a notification comes before this flag is set we might + // access a vector while it is being cleared in connect() + if (!client->m_isConnected) { + return 0; + } + + log_d("BLEClient", "Notify received for handle: %d", event->notify_rx.attr_handle); + + for (auto &myPair : client->m_servicesMap) { + // Dont waste cycles searching services without this handle in its range + if (myPair.second->getEndHandle() < event->notify_rx.attr_handle) { + continue; + } + + auto cMap = &myPair.second->m_characteristicMap; + log_d("BLEClient", "checking service %s for handle: %d", myPair.second->getUUID().toString().c_str(), event->notify_rx.attr_handle); + + auto characteristic = cMap->cbegin(); + for (; characteristic != cMap->cend(); ++characteristic) { + if (characteristic->second->m_handle == event->notify_rx.attr_handle) { + break; + } + } + + if (characteristic != cMap->cend()) { + log_d("BLEClient", "Got Notification for characteristic %s", characteristic->second->toString().c_str()); + + characteristic->second->m_semaphoreReadCharEvt.take(); + characteristic->second->m_value = String((char *)event->notify_rx.om->om_data, event->notify_rx.om->om_len); + characteristic->second->m_semaphoreReadCharEvt.give(); + + if (characteristic->second->m_notifyCallback != nullptr) { + log_d("Invoking callback for notification on characteristic %s", characteristic->second->toString().c_str()); + characteristic->second->m_notifyCallback( + characteristic->second, event->notify_rx.om->om_data, event->notify_rx.om->om_len, !event->notify_rx.indication + ); + } + break; + } + } + + return 0; + } // BLE_GAP_EVENT_NOTIFY_RX + + case BLE_GAP_EVENT_CONN_UPDATE_REQ: + case BLE_GAP_EVENT_L2CAP_UPDATE_REQ: + { + if (client->m_conn_id != event->conn_update_req.conn_handle) { + return 0; + } + log_d("Peer requesting to update connection parameters"); + log_d( + "MinInterval: %d, MaxInterval: %d, Latency: %d, Timeout: %d", event->conn_update_req.peer_params->itvl_min, + event->conn_update_req.peer_params->itvl_max, event->conn_update_req.peer_params->latency, event->conn_update_req.peer_params->supervision_timeout + ); + + rc = client->m_pClientCallbacks->onConnParamsUpdateRequest(client, event->conn_update_req.peer_params) ? 0 : BLE_ERR_CONN_PARMS; + + if (!rc && event->type == BLE_GAP_EVENT_CONN_UPDATE_REQ) { + event->conn_update_req.self_params->itvl_min = client->m_pConnParams.itvl_min; + event->conn_update_req.self_params->itvl_max = client->m_pConnParams.itvl_max; + event->conn_update_req.self_params->latency = client->m_pConnParams.latency; + event->conn_update_req.self_params->supervision_timeout = client->m_pConnParams.supervision_timeout; + } + + log_d("%s peer params", (rc == 0) ? "Accepted" : "Rejected"); + return rc; + } // BLE_GAP_EVENT_CONN_UPDATE_REQ, BLE_GAP_EVENT_L2CAP_UPDATE_REQ + + case BLE_GAP_EVENT_CONN_UPDATE: + { + if (client->m_conn_id != event->conn_update.conn_handle) { + return 0; + } + if (event->conn_update.status == 0) { + log_i("Connection parameters updated."); + } else { + log_e("Update connection parameters failed."); + } + return 0; + } // BLE_GAP_EVENT_CONN_UPDATE + + case BLE_GAP_EVENT_ENC_CHANGE: + { + if (client->m_conn_id != event->enc_change.conn_handle) { + return 0; + } + + if (event->enc_change.status == 0 || event->enc_change.status == (BLE_HS_ERR_HCI_BASE + BLE_ERR_PINKEY_MISSING)) { + struct ble_gap_conn_desc desc; + rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc); + assert(rc == 0); + + if (event->enc_change.status == (BLE_HS_ERR_HCI_BASE + BLE_ERR_PINKEY_MISSING)) { + // Key is missing, try deleting. + ble_store_util_delete_peer(&desc.peer_id_addr); + } else if (BLEDevice::m_securityCallbacks != nullptr) { + BLEDevice::m_securityCallbacks->onAuthenticationComplete(&desc); + } else { + client->m_pClientCallbacks->onAuthenticationComplete(&desc); + } + } + + rc = event->enc_change.status; + break; + } //BLE_GAP_EVENT_ENC_CHANGE + + case BLE_GAP_EVENT_MTU: + { + if (client->m_conn_id != event->mtu.conn_handle) { + return 0; + } + log_i("mtu update event; conn_handle=%d mtu=%d", event->mtu.conn_handle, event->mtu.value); + rc = 0; + break; + } // BLE_GAP_EVENT_MTU + + case BLE_GAP_EVENT_PASSKEY_ACTION: + { + struct ble_sm_io pkey = {0, 0}; + + if (client->m_conn_id != event->passkey.conn_handle) { + return 0; + } + + if (event->passkey.params.action == BLE_SM_IOACT_DISP) { + pkey.action = event->passkey.params.action; + pkey.passkey = BLESecurity::m_passkey; // This is the passkey to be entered on peer + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("ble_sm_inject_io result: %d", rc); + + } else if (event->passkey.params.action == BLE_SM_IOACT_NUMCMP) { + log_d("Passkey on device's display: %d", event->passkey.params.numcmp); + pkey.action = event->passkey.params.action; + // Compatibility only - Do not use, should be removed the in future + if (BLEDevice::m_securityCallbacks != nullptr) { + pkey.numcmp_accept = BLEDevice::m_securityCallbacks->onConfirmPIN(event->passkey.params.numcmp); + //////////////////////////////////////////////////// + } else { + pkey.numcmp_accept = client->m_pClientCallbacks->onConfirmPIN(event->passkey.params.numcmp); + } + + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("ble_sm_inject_io result: %d", rc); + + //TODO: Handle out of band pairing + } else if (event->passkey.params.action == BLE_SM_IOACT_OOB) { + static uint8_t tem_oob[16] = {0}; + pkey.action = event->passkey.params.action; + for (int i = 0; i < 16; i++) { + pkey.oob[i] = tem_oob[i]; + } + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("ble_sm_inject_io result: %d", rc); + //////// + } else if (event->passkey.params.action == BLE_SM_IOACT_INPUT) { + log_d("Enter the passkey"); + pkey.action = event->passkey.params.action; + + // Compatibility only - Do not use, should be removed the in future + if (BLEDevice::m_securityCallbacks != nullptr) { + pkey.passkey = BLEDevice::m_securityCallbacks->onPassKeyRequest(); + ///////////////////////////////////////////// + } else { + pkey.passkey = client->m_pClientCallbacks->onPassKeyRequest(); + } + + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("ble_sm_inject_io result: %d", rc); + + } else if (event->passkey.params.action == BLE_SM_IOACT_NONE) { + log_d("No passkey action required"); + } + + return 0; + } // BLE_GAP_EVENT_PASSKEY_ACTION + + default: + { + return 0; + } + } // Switch + + if (pTaskData != nullptr) { + BLEUtils::taskRelease(*pTaskData, rc); + } + + return 0; +} // handleGAPEvent + /** - @brief Set the local and remote MTU size. - Should be called once after client connects if MTU size needs to be changed. - @return bool indicating if MTU was successfully set locally and on remote. -*/ -bool BLEClient::setMTU(uint16_t mtu) { - esp_err_t err = esp_ble_gatt_set_local_mtu(mtu); //First must set local MTU value. - if (err == ESP_OK) { - err = esp_ble_gattc_send_mtu_req(m_gattc_if, m_conn_id); //Once local is set successfully set remote size - if (err != ESP_OK) { - log_e("Error setting send MTU request MTU: %d err=%d", mtu, err); - return false; + * @brief Disconnect from the peer. + * @return Error code from NimBLE stack, 0 = success. + */ +int BLEClient::disconnect(uint8_t reason) { + log_d(">> disconnect()"); + int rc = 0; + + if (isConnected()) { + rc = ble_gap_terminate(m_conn_id, reason); + if (rc != 0 && rc != BLE_HS_ENOTCONN && rc != BLE_HS_EALREADY) { + m_lastErr = rc; + log_e("ble_gap_terminate failed: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); } } else { - log_e("can't set local mtu value: %d", mtu); - return false; + log_d("Not connected to any peers"); } - log_v("<< setLocalMTU"); - m_mtu = mtu; //successfully changed + log_d("<< disconnect()"); + return rc; +} // disconnect +bool BLEClientCallbacks::onConnParamsUpdateRequest(BLEClient *pClient, const ble_gap_upd_params *params) { + log_d("BLEClientCallbacks", "onConnParamsUpdateRequest: default"); return true; } -/** - * @brief Return a string representation of this client. - * @return A string representation of this client. - */ -String BLEClient::toString() { - String res = "peer address: " + m_peerAddress.toString(); - res += "\nServices:\n"; - for (auto &myPair : m_servicesMap) { - res += myPair.second->toString() + "\n"; - // myPair.second is the value - } - return res; -} // toString +uint32_t BLEClientCallbacks::onPassKeyRequest() { + log_d("onPassKeyRequest: default: 123456"); + return 123456; +} + +void BLEClientCallbacks::onAuthenticationComplete(ble_gap_conn_desc *desc) { + log_d("onAuthenticationComplete: default"); +} + +bool BLEClientCallbacks::onConfirmPIN(uint32_t pin) { + log_d("onConfirmPIN: default: true"); + return true; +} + +#endif // CONFIG_NIMBLE_ENABLED -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEClient.h b/libraries/BLE/src/BLEClient.h index ddb932fcd95..97ef72f4917 100644 --- a/libraries/BLE/src/BLEClient.h +++ b/libraries/BLE/src/BLEClient.h @@ -1,66 +1,128 @@ /* - * BLEDevice.h + * BLEClient.h * * Created on: Mar 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ -#ifndef MAIN_BLEDEVICE_H_ -#define MAIN_BLEDEVICE_H_ +#ifndef MAIN_BLECLIENT_H_ +#define MAIN_BLECLIENT_H_ #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ -#include #include #include #include -//#include "BLEExceptions.h" #include "BLERemoteService.h" #include "BLEService.h" #include "BLEAddress.h" #include "BLEAdvertisedDevice.h" +#include "BLEUtils.h" + +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#ifndef BLE_ERR_REM_USER_CONN_TERM +#define BLE_ERR_REM_USER_CONN_TERM 0x13 +#endif +#endif + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#define ESP_GATT_IF_NONE BLE_HS_CONN_HANDLE_NONE +#endif + +/*************************************************************************** + * NimBLE types * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +typedef uint16_t esp_gatt_if_t; +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ class BLERemoteService; class BLEClientCallbacks; class BLEAdvertisedDevice; +struct BLETaskData; /** * @brief A model of a %BLE client. */ class BLEClient { public: + /*************************************************************************** + * Common public properties * + ***************************************************************************/ + + uint16_t m_appId; + + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEClient(); ~BLEClient(); - bool connect(BLEAdvertisedDevice *device); bool connectTimeout(BLEAdvertisedDevice *device, uint32_t timeoutMS = portMAX_DELAY); - bool connect(BLEAddress address, esp_ble_addr_type_t type = BLE_ADDR_TYPE_PUBLIC, uint32_t timeoutMS = portMAX_DELAY); // Connect to the remote BLE Server - void disconnect(); // Disconnect from the remote BLE Server - BLEAddress getPeerAddress(); // Get the address of the remote BLE Server - int getRssi(); // Get the RSSI of the remote BLE Server - std::map *getServices(); // Get a map of the services offered by the remote BLE Server - BLERemoteService *getService(const char *uuid); // Get a reference to a specified service offered by the remote BLE server. - BLERemoteService *getService(BLEUUID uuid); // Get a reference to a specified service offered by the remote BLE server. - String getValue(BLEUUID serviceUUID, BLEUUID characteristicUUID); // Get the value of a given characteristic at a given service. - - void handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - - bool isConnected(); // Return true if we are connected. - + bool connect(BLEAddress address, uint8_t type = 0, uint32_t timeoutMS = portMAX_DELAY); + bool secureConnection(); + int disconnect(uint8_t reason = BLE_ERR_REM_USER_CONN_TERM); + BLEAddress getPeerAddress(); + int getRssi(); + std::map *getServices(); + BLERemoteService *getService(const char *uuid); + BLERemoteService *getService(BLEUUID uuid); + String getValue(BLEUUID serviceUUID, BLEUUID characteristicUUID); + bool isConnected(); void setClientCallbacks(BLEClientCallbacks *pClientCallbacks); - void setValue(BLEUUID serviceUUID, BLEUUID characteristicUUID, String value); // Set the value of a given characteristic at a given service. - - String toString(); // Return a string representation of this client. + void setValue(BLEUUID serviceUUID, BLEUUID characteristicUUID, String value); + String toString(); uint16_t getConnId(); esp_gatt_if_t getGattcIf(); uint16_t getMTU(); bool setMTU(uint16_t mtu); - uint16_t m_appId; + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static int handleGAPEvent(struct ble_gap_event *event, void *arg); + static int serviceDiscoveredCB(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_svc *service, void *arg); +#endif private: friend class BLEDevice; @@ -68,15 +130,14 @@ class BLEClient { friend class BLERemoteCharacteristic; friend class BLERemoteDescriptor; - void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + /*************************************************************************** + * Common private properties * + ***************************************************************************/ - BLEAddress m_peerAddress = BLEAddress((uint8_t *)"\0\0\0\0\0\0"); // The BD address of the remote server. + BLEAddress m_peerAddress = BLEAddress((uint8_t *)"\0\0\0\0\0\0"); uint16_t m_conn_id; - // int m_deviceType; - esp_gatt_if_t m_gattc_if; - bool m_haveServices = false; // Have we previously obtain the set of services from the remote server. - bool m_isConnected = false; // Are we currently connected. - + bool m_haveServices = false; + bool m_isConnected = false; BLEClientCallbacks *m_pClientCallbacks; FreeRTOS::Semaphore m_semaphoreRegEvt = FreeRTOS::Semaphore("RegEvt"); FreeRTOS::Semaphore m_semaphoreOpenEvt = FreeRTOS::Semaphore("OpenEvt"); @@ -84,20 +145,76 @@ class BLEClient { FreeRTOS::Semaphore m_semaphoreRssiCmplEvt = FreeRTOS::Semaphore("RssiCmplEvt"); std::map m_servicesMap; std::map m_servicesMapByInstID; - void clearServices(); // Clear any existing services. uint16_t m_mtu = 23; -}; // class BLEDevice + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_gatt_if_t m_gattc_if; +#endif + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + int m_lastErr; + int32_t m_connectTimeout; + uint8_t m_terminateFailCount; + ble_gap_conn_params m_pConnParams; + mutable BLETaskData *m_pTaskData; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + void clearServices(); + + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE private declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static void dcTimerCb(ble_npl_event *event); +#endif +}; // class BLEClient /** * @brief Callbacks associated with a %BLE client. */ class BLEClientCallbacks { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + virtual ~BLEClientCallbacks(){}; - virtual void onConnect(BLEClient *pClient) = 0; - virtual void onDisconnect(BLEClient *pClient) = 0; + virtual void onConnect(BLEClient *pClient); + virtual void onDisconnect(BLEClient *pClient); + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + virtual bool onConnParamsUpdateRequest(BLEClient *pClient, const ble_gap_upd_params *params); + virtual uint32_t onPassKeyRequest(); + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc); + virtual bool onConfirmPIN(uint32_t pin); +#endif }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ -#endif /* MAIN_BLEDEVICE_H_ */ +#endif /* MAIN_BLECLIENT_H_ */ diff --git a/libraries/BLE/src/BLEConnInfo.h b/libraries/BLE/src/BLEConnInfo.h new file mode 100644 index 00000000000..b81ab93c28f --- /dev/null +++ b/libraries/BLE/src/BLEConnInfo.h @@ -0,0 +1,110 @@ +/* + * Copyright 2020-2025 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef BLECONNINFO_H_ +#define BLECONNINFO_H_ + +#if defined(CONFIG_NIMBLE_ENABLED) + +#include +#include "BLEAddress.h" + +/** + * @brief Connection information. + */ +class BLEConnInfo { +public: + /** @brief Gets the over-the-air address of the connected peer */ + BLEAddress getAddress() const { + return BLEAddress(m_desc.peer_ota_addr); + } + + /** @brief Gets the ID address of the connected peer */ + BLEAddress getIdAddress() const { + return BLEAddress(m_desc.peer_id_addr); + } + + /** @brief Gets the connection handle (also known as the connection id) of the connected peer */ + uint16_t getConnHandle() const { + return m_desc.conn_handle; + } + + /** @brief Gets the connection interval for this connection (in 1.25ms units) */ + uint16_t getConnInterval() const { + return m_desc.conn_itvl; + } + + /** @brief Gets the supervision timeout for this connection (in 10ms units) */ + uint16_t getConnTimeout() const { + return m_desc.supervision_timeout; + } + + /** @brief Gets the allowable latency for this connection (unit = number of intervals) */ + uint16_t getConnLatency() const { + return m_desc.conn_latency; + } + + /** @brief Gets the maximum transmission unit size for this connection (in bytes) */ + uint16_t getMTU() const { + return ble_att_mtu(m_desc.conn_handle); + } + + /** @brief Check if we are in the master role in this connection */ + bool isMaster() const { + return (m_desc.role == BLE_GAP_ROLE_MASTER); + } + + /** @brief Check if we are in the slave role in this connection */ + bool isSlave() const { + return (m_desc.role == BLE_GAP_ROLE_SLAVE); + } + + /** @brief Check if we are connected to a bonded peer */ + bool isBonded() const { + return (m_desc.sec_state.bonded == 1); + } + + /** @brief Check if the connection in encrypted */ + bool isEncrypted() const { + return (m_desc.sec_state.encrypted == 1); + } + + /** @brief Check if the the connection has been authenticated */ + bool isAuthenticated() const { + return (m_desc.sec_state.authenticated == 1); + } + + /** @brief Gets the key size used to encrypt the connection */ + uint8_t getSecKeySize() const { + return m_desc.sec_state.key_size; + } + +private: + friend class BLEServer; + friend class BLEClient; + friend class BLECharacteristic; + friend class BLEDescriptor; + + ble_gap_conn_desc m_desc{}; + BLEConnInfo(){}; + BLEConnInfo(ble_gap_conn_desc desc) { + m_desc = desc; + } +}; +#endif + +#endif // BLECONNINFO_H_ diff --git a/libraries/BLE/src/BLEDescriptor.cpp b/libraries/BLE/src/BLEDescriptor.cpp index 69a93e57201..51c77bb9b49 100644 --- a/libraries/BLE/src/BLEDescriptor.cpp +++ b/libraries/BLE/src/BLEDescriptor.cpp @@ -3,25 +3,50 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Apr 3, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include #include #include #include "sdkconfig.h" #include +#include "BLE2904.h" #include "BLEService.h" #include "BLEDescriptor.h" #include "GeneralUtils.h" #include "esp32-hal-log.h" +/*************************************************************************** + * Common definitions * + ***************************************************************************/ + #define NULL_HANDLE (0xffff) +/*************************************************************************** + * Common global variables * + ***************************************************************************/ + +static BLEDescriptorCallbacks defaultCallbacks; + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief BLEDescriptor constructor. */ @@ -32,13 +57,15 @@ BLEDescriptor::BLEDescriptor(const char *uuid, uint16_t len) : BLEDescriptor(BLE */ BLEDescriptor::BLEDescriptor(BLEUUID uuid, uint16_t max_len) { m_bleUUID = uuid; - m_value.attr_len = 0; // Initial length is 0. - m_value.attr_max_len = max_len; // Maximum length of the data. - m_handle = NULL_HANDLE; // Handle is initially unknown. - m_pCharacteristic = nullptr; // No initial characteristic. - m_pCallback = nullptr; // No initial callback. - + m_handle = NULL_HANDLE; // Handle is initially unknown. + m_pCharacteristic = nullptr; // No initial characteristic. + m_pCallback = nullptr; // No initial callback. + m_value.attr_len = 0; // Initial length is 0. + m_value.attr_max_len = max_len; // Maximum length of the data. m_value.attr_value = (uint8_t *)malloc(max_len); // Allocate storage for the value. +#if CONFIG_NIMBLE_ENABLED + m_removed = 0; +#endif } // BLEDescriptor /** @@ -62,6 +89,7 @@ void BLEDescriptor::executeCreate(BLECharacteristic *pCharacteristic) { m_pCharacteristic = pCharacteristic; // Save the characteristic associated with this service. +#if CONFIG_BLUEDROID_ENABLED esp_attr_control_t control; control.auto_rsp = ESP_GATT_AUTO_RSP; m_semaphoreCreateEvt.take("executeCreate"); @@ -73,6 +101,7 @@ void BLEDescriptor::executeCreate(BLECharacteristic *pCharacteristic) { } m_semaphoreCreateEvt.wait("executeCreate"); +#endif log_v("<< executeCreate"); } // executeCreate @@ -107,6 +136,118 @@ uint8_t *BLEDescriptor::getValue() { return m_value.attr_value; } // getValue +/** + * @brief Get the characteristic this descriptor belongs to. + * @return A pointer to the characteristic this descriptor belongs to. + */ +BLECharacteristic *BLEDescriptor::getCharacteristic() { + return m_pCharacteristic; +} // getCharacteristic + +/** + * @brief Set the callback handlers for this descriptor. + * @param [in] pCallbacks An instance of a callback structure used to define any callbacks for the descriptor. + */ +void BLEDescriptor::setCallbacks(BLEDescriptorCallbacks *pCallback) { + log_v(">> setCallbacks: 0x%x", (uint32_t)pCallback); + if (pCallback != nullptr) { + m_pCallback = pCallback; + } else { + m_pCallback = &defaultCallbacks; + } + log_v("<< setCallbacks"); +} // setCallbacks + +/** + * @brief Set the handle of this descriptor. + * Set the handle of this descriptor to be the supplied value. + * @param [in] handle The handle to be associated with this descriptor. + * @return N/A. + */ +void BLEDescriptor::setHandle(uint16_t handle) { +#if defined(CONFIG_BLUEDROID_ENABLED) + log_v(">> setHandle(0x%.2x): Setting descriptor handle to be 0x%.2x", handle, handle); + m_handle = handle; + log_v("<< setHandle()"); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + log_w("NimBLE does not support manually setting the handle of a descriptor. Ignoring request."); +#endif +} // setHandle + +/** + * @brief Set the value of the descriptor. + * @param [in] data The data to set for the descriptor. + * @param [in] length The length of the data in bytes. + */ +void BLEDescriptor::setValue(uint8_t *data, size_t length) { + if (length > m_value.attr_max_len) { + log_e("Size %d too large, must be no bigger than %d", length, m_value.attr_max_len); + return; + } + + m_semaphoreSetValue.take(); + m_value.attr_len = length; + memcpy(m_value.attr_value, data, length); +#if CONFIG_BLUEDROID_ENABLED + if (m_handle != NULL_HANDLE) { + esp_ble_gatts_set_attr_value(m_handle, length, (const uint8_t *)data); + log_d("Set the value in the GATTS database using handle 0x%x", m_handle); + } +#endif + m_semaphoreSetValue.give(); +} // setValue + +/** + * @brief Set the value of the descriptor. + * @param [in] value The value of the descriptor in string form. + */ +void BLEDescriptor::setValue(String value) { + setValue((uint8_t *)value.c_str(), value.length()); +} // setValue + +void BLEDescriptor::setAccessPermissions(uint8_t perm) { + m_permissions = perm; +} + +/** + * @brief Return a string representation of the descriptor. + * @return A string representation of the descriptor. + */ +String BLEDescriptor::toString() { + char hex[5]; + snprintf(hex, sizeof(hex), "%04x", m_handle); + String res = "UUID: " + m_bleUUID.toString() + ", handle: 0x" + hex; + return res; +} // toString + +BLEDescriptorCallbacks::~BLEDescriptorCallbacks() {} + +/** + * @brief Callback function to support a read request. + * @param [in] pDescriptor The descriptor that is the source of the event. + */ +void BLEDescriptorCallbacks::onRead(BLEDescriptor *pDescriptor) { + log_d("BLEDescriptorCallbacks", ">> onRead: default"); + log_d("BLEDescriptorCallbacks", "<< onRead"); +} // onRead + +/** + * @brief Callback function to support a write request. + * @param [in] pDescriptor The descriptor that is the source of the event. + */ +void BLEDescriptorCallbacks::onWrite(BLEDescriptor *pDescriptor) { + log_d("BLEDescriptorCallbacks", ">> onWrite: default"); + log_d("BLEDescriptorCallbacks", "<< onWrite"); +} // onWrite + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + /** * @brief Handle GATT server events for the descripttor. * @param [in] event @@ -185,88 +326,84 @@ void BLEDescriptor::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_i } // switch event } // handleGATTServerEvent -/** - * @brief Set the callback handlers for this descriptor. - * @param [in] pCallbacks An instance of a callback structure used to define any callbacks for the descriptor. - */ -void BLEDescriptor::setCallbacks(BLEDescriptorCallbacks *pCallback) { - log_v(">> setCallbacks: 0x%x", (uint32_t)pCallback); - m_pCallback = pCallback; - log_v("<< setCallbacks"); -} // setCallbacks +#endif -/** - * @brief Set the handle of this descriptor. - * Set the handle of this descriptor to be the supplied value. - * @param [in] handle The handle to be associated with this descriptor. - * @return N/A. - */ -void BLEDescriptor::setHandle(uint16_t handle) { - log_v(">> setHandle(0x%.2x): Setting descriptor handle to be 0x%.2x", handle, handle); - m_handle = handle; - log_v("<< setHandle()"); -} // setHandle +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ -/** - * @brief Set the value of the descriptor. - * @param [in] data The data to set for the descriptor. - * @param [in] length The length of the data in bytes. - */ -void BLEDescriptor::setValue(uint8_t *data, size_t length) { - if (length > ESP_GATT_MAX_ATTR_LEN) { - log_e("Size %d too large, must be no bigger than %d", length, ESP_GATT_MAX_ATTR_LEN); - return; - } - m_value.attr_len = length; - memcpy(m_value.attr_value, data, length); - if (m_handle != NULL_HANDLE) { - esp_ble_gatts_set_attr_value(m_handle, length, (const uint8_t *)data); - log_d("Set the value in the GATTS database using handle 0x%x", m_handle); - } -} // setValue +#if defined(CONFIG_NIMBLE_ENABLED) /** - * @brief Set the value of the descriptor. - * @param [in] value The value of the descriptor in string form. + * @brief Handle GATT server events for the descriptor. + * @param [in] conn_handle The connection handle. + * @param [in] attr_handle The attribute handle. + * @param [in] ctxt The GATT access context. + * @param [in] arg The argument. */ -void BLEDescriptor::setValue(String value) { - setValue((uint8_t *)value.c_str(), value.length()); -} // setValue +int BLEDescriptor::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { + const ble_uuid_t *uuid; + int rc; + struct ble_gap_conn_desc desc; + BLEDescriptor *pDescriptor = (BLEDescriptor *)arg; + + log_d("Descriptor %s %s event", pDescriptor->getUUID().toString().c_str(), ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC ? "Read" : "Write"); + + uuid = ctxt->chr->uuid; + if (ble_uuid_cmp(uuid, &pDescriptor->getUUID().getNative()->u) == 0) { + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_READ_DSC: + { + rc = ble_gap_conn_find(conn_handle, &desc); + assert(rc == 0); + + // If the packet header is only 8 bytes this is a follow up of a long read + // so we don't want to call the onRead() callback again. + if (ctxt->om->om_pkthdr_len > 8 || pDescriptor->m_value.attr_len <= (ble_att_mtu(desc.conn_handle) - 3)) { + pDescriptor->m_pCallback->onRead(pDescriptor); + } -void BLEDescriptor::setAccessPermissions(esp_gatt_perm_t perm) { - m_permissions = perm; -} + ble_npl_hw_enter_critical(); + rc = os_mbuf_append(ctxt->om, pDescriptor->m_value.attr_value, pDescriptor->m_value.attr_len); + ble_npl_hw_exit_critical(0); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } -/** - * @brief Return a string representation of the descriptor. - * @return A string representation of the descriptor. - */ -String BLEDescriptor::toString() { - char hex[5]; - snprintf(hex, sizeof(hex), "%04x", m_handle); - String res = "UUID: " + m_bleUUID.toString() + ", handle: 0x" + hex; - return res; -} // toString + case BLE_GATT_ACCESS_OP_WRITE_DSC: + { + uint16_t att_max_len = pDescriptor->m_value.attr_max_len; -BLEDescriptorCallbacks::~BLEDescriptorCallbacks() {} + if (ctxt->om->om_len > att_max_len) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } -/** - * @brief Callback function to support a read request. - * @param [in] pDescriptor The descriptor that is the source of the event. - */ -void BLEDescriptorCallbacks::onRead(BLEDescriptor *pDescriptor) { - log_d("BLEDescriptorCallbacks", ">> onRead: default"); - log_d("BLEDescriptorCallbacks", "<< onRead"); -} // onRead + uint8_t buf[att_max_len]; + size_t len = ctxt->om->om_len; + memcpy(buf, ctxt->om->om_data, len); + os_mbuf *next; + next = SLIST_NEXT(ctxt->om, om_next); + while (next != NULL) { + if ((len + next->om_len) > att_max_len) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + memcpy(&buf[len], next->om_data, next->om_len); + len += next->om_len; + next = SLIST_NEXT(next, om_next); + } -/** - * @brief Callback function to support a write request. - * @param [in] pDescriptor The descriptor that is the source of the event. - */ -void BLEDescriptorCallbacks::onWrite(BLEDescriptor *pDescriptor) { - log_d("BLEDescriptorCallbacks", ">> onWrite: default"); - log_d("BLEDescriptorCallbacks", "<< onWrite"); -} // onWrite + pDescriptor->setValue(buf, len); + pDescriptor->m_pCallback->onWrite(pDescriptor); + return 0; + } + + default: break; + } + } + + return BLE_ATT_ERR_UNLIKELY; +} + +#endif -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEDescriptor.h b/libraries/BLE/src/BLEDescriptor.h index e155a1f2971..d0d34b24388 100644 --- a/libraries/BLE/src/BLEDescriptor.h +++ b/libraries/BLE/src/BLEDescriptor.h @@ -3,6 +3,10 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEDESCRIPTOR_H_ @@ -11,13 +15,60 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include "BLEUUID.h" #include "BLECharacteristic.h" -#include #include "RTOS.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include "BLEConnInfo.h" + +#define ESP_GATT_PERM_READ BLE_ATT_F_READ +#define ESP_GATT_PERM_WRITE BLE_ATT_F_WRITE +#define ESP_GATT_PERM_READ_ENCRYPTED BLE_ATT_F_READ_ENC +#define ESP_GATT_PERM_WRITE_ENCRYPTED BLE_ATT_F_WRITE_ENC +#define ESP_GATT_PERM_READ_AUTHORIZATION BLE_ATT_F_READ_AUTHOR +#define ESP_GATT_PERM_WRITE_AUTHORIZATION BLE_ATT_F_WRITE_AUTHOR +#define ESP_GATT_PERM_READ_ENC_MITM BLE_ATT_F_READ_AUTHEN +#define ESP_GATT_PERM_WRITE_ENC_MITM BLE_ATT_F_WRITE_AUTHEN + +#endif + +/*************************************************************************** + * NimBLE types * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +typedef struct { + uint16_t attr_max_len; /*!< attribute max value length */ + uint16_t attr_len; /*!< attribute current value length */ + uint8_t *attr_value; /*!< the pointer to attribute value */ +} esp_attr_value_t; +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ + class BLEService; class BLECharacteristic; class BLEDescriptorCallbacks; @@ -27,33 +78,83 @@ class BLEDescriptorCallbacks; */ class BLEDescriptor { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEDescriptor(const char *uuid, uint16_t max_len = 100); BLEDescriptor(BLEUUID uuid, uint16_t max_len = 100); virtual ~BLEDescriptor(); - uint16_t getHandle(); // Get the handle of the descriptor. - size_t getLength(); // Get the length of the value of the descriptor. - BLEUUID getUUID(); // Get the UUID of the descriptor. - uint8_t *getValue(); // Get a pointer to the value of the descriptor. - void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + uint16_t getHandle(); // Get the handle of the descriptor. + size_t getLength(); // Get the length of the value of the descriptor. + BLEUUID getUUID(); // Get the UUID of the descriptor. + uint8_t *getValue(); // Get a pointer to the value of the descriptor. + BLECharacteristic *getCharacteristic(); // Get the characteristic that this descriptor belongs to. - void setAccessPermissions(esp_gatt_perm_t perm); // Set the permissions of the descriptor. + void setAccessPermissions(uint8_t perm); // Set the permissions of the descriptor. void setCallbacks(BLEDescriptorCallbacks *pCallbacks); // Set callbacks to be invoked for the descriptor. void setValue(uint8_t *data, size_t size); // Set the value of the descriptor as a pointer to data. void setValue(String value); // Set the value of the descriptor as a data buffer. String toString(); // Convert the descriptor to a string representation. + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static int handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +#endif + private: friend class BLEDescriptorMap; friend class BLECharacteristic; + friend class BLEService; + friend class BLE2901; + friend class BLE2902; + friend class BLE2904; + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + BLEUUID m_bleUUID; uint16_t m_handle; + esp_attr_value_t m_value; BLEDescriptorCallbacks *m_pCallback; BLECharacteristic *m_pCharacteristic; - esp_gatt_perm_t m_permissions = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; + FreeRTOS::Semaphore m_semaphoreSetValue = FreeRTOS::Semaphore("SetValue"); + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) FreeRTOS::Semaphore m_semaphoreCreateEvt = FreeRTOS::Semaphore("CreateEvt"); - esp_attr_value_t m_value; + uint8_t m_permissions = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; +#endif + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + uint8_t m_permissions = HA_FLAG_PERM_RW; + uint8_t m_removed = 0; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ void executeCreate(BLECharacteristic *pCharacteristic); void setHandle(uint16_t handle); @@ -68,11 +169,15 @@ class BLEDescriptor { */ class BLEDescriptorCallbacks { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + virtual ~BLEDescriptorCallbacks(); virtual void onRead(BLEDescriptor *pDescriptor); virtual void onWrite(BLEDescriptor *pDescriptor); }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEDESCRIPTOR_H_ */ diff --git a/libraries/BLE/src/BLEDescriptorMap.cpp b/libraries/BLE/src/BLEDescriptorMap.cpp index 732fb62cdcf..e0c8b3a7cc3 100644 --- a/libraries/BLE/src/BLEDescriptorMap.cpp +++ b/libraries/BLE/src/BLEDescriptorMap.cpp @@ -3,21 +3,50 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include #include "BLECharacteristic.h" #include "BLEDescriptor.h" -#include // ESP32 BLE #ifdef ARDUINO_ARCH_ESP32 #include "esp32-hal-log.h" #endif +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include // ESP32 BLE +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include "host/ble_gatt.h" +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief Return the descriptor by UUID. * @param [in] UUID The UUID to look up the descriptor. @@ -81,6 +110,24 @@ void BLEDescriptorMap::setByHandle(uint16_t handle, BLEDescriptor *pDescriptor) m_handleMap.insert(std::pair(handle, pDescriptor)); } // setByHandle +/** + * @brief Get the number of registered descriptors. + * @return The number of registered descriptors. + */ +int BLEDescriptorMap::getRegisteredDescriptorCount() { + return m_uuidMap.size(); +} + +/** + * @brief Remove a descriptor from the map. + * @param [in] pDescriptor The descriptor to remove. + * @return N/A. + */ +void BLEDescriptorMap::removeDescriptor(BLEDescriptor *pDescriptor) { + m_uuidMap.erase(pDescriptor); + m_handleMap.erase(pDescriptor->getHandle()); +} + /** * @brief Return a string representation of the descriptor map. * @return A string representation of the descriptor map. @@ -102,19 +149,6 @@ String BLEDescriptorMap::toString() { return res; } // toString -/** - * @brief Pass the GATT server event onwards to each of the descriptors found in the mapping - * @param [in] event - * @param [in] gatts_if - * @param [in] param - */ -void BLEDescriptorMap::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - // Invoke the handler for every descriptor we have. - for (auto &myPair : m_uuidMap) { - myPair.first->handleGATTServerEvent(event, gatts_if, param); - } -} // handleGATTServerEvent - /** * @brief Get the first descriptor in the map. * @return The first descriptor in the map. @@ -142,5 +176,41 @@ BLEDescriptor *BLEDescriptorMap::getNext() { return pRet; } // getNext -#endif /* CONFIG_BLUEDROID_ENABLED */ +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +/** + * @brief Pass the GATT server event onwards to each of the descriptors found in the mapping + * @param [in] event + * @param [in] gatts_if + * @param [in] param + */ +void BLEDescriptorMap::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { + // Invoke the handler for every descriptor we have. + for (auto &myPair : m_uuidMap) { + myPair.first->handleGATTServerEvent(event, gatts_if, param); + } +} // handleGATTServerEvent + +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +void BLEDescriptorMap::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, ble_gatt_access_ctxt *ctxt, void *arg) { + // Invoke the handler for every descriptor we have. + for (auto &myPair : m_uuidMap) { + myPair.first->handleGATTServerEvent(conn_handle, attr_handle, ctxt, arg); + } +} + +#endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEDevice.cpp b/libraries/BLE/src/BLEDevice.cpp index 186b36d6a33..014806cc32b 100644 --- a/libraries/BLE/src/BLEDevice.cpp +++ b/libraries/BLE/src/BLEDevice.cpp @@ -1,30 +1,35 @@ /* - * BLE.cpp + * BLEDevice.cpp * * Created on: Mar 16, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include #include #include #include #include -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 ESP-IDF -#include // Part of C++ Standard library -#include // Part of C++ Standard library -#include // Part of C++ Standard library +#include + +#include +#include +#include +#include #include "BLEDevice.h" #include "BLEClient.h" @@ -37,34 +42,102 @@ #include "esp32-hal-log.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#include +#include +#include +#include +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#ifdef CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE +#include +#endif +#include +#include +#include +#include +#include +#include "host/ble_hs_pvcy.h" +#include "host/util/util.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" +#endif + +/*************************************************************************** + * Common properties * + ***************************************************************************/ + /** * Singletons for the BLEDevice. */ BLEServer *BLEDevice::m_pServer = nullptr; BLEScan *BLEDevice::m_pScan = nullptr; BLEClient *BLEDevice::m_pClient = nullptr; -bool initialized = false; -esp_ble_sec_act_t BLEDevice::m_securityLevel = (esp_ble_sec_act_t)0; +static bool initialized = false; BLESecurityCallbacks *BLEDevice::m_securityCallbacks = nullptr; uint16_t BLEDevice::m_localMTU = 23; // not sure if this variable is useful BLEAdvertising *BLEDevice::m_bleAdvertising = nullptr; uint16_t BLEDevice::m_appId = 0; std::map BLEDevice::m_connectedClientsMap; gap_event_handler BLEDevice::m_customGapHandler = nullptr; + +/*************************************************************************** + * Bluedroid properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +esp_ble_sec_act_t BLEDevice::m_securityLevel = (esp_ble_sec_act_t)0; gattc_event_handler BLEDevice::m_customGattcHandler = nullptr; gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; +#endif + +/*************************************************************************** + * NimBLE properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +ble_gap_event_listener BLEDevice::m_listener; +BLEDeviceCallbacks BLEDevice::defaultDeviceCallbacks{}; +BLEDeviceCallbacks *BLEDevice::m_pDeviceCallbacks = &defaultDeviceCallbacks; +uint8_t BLEDevice::m_ownAddrType = BLE_OWN_ADDR_PUBLIC; +bool BLEDevice::m_synced = false; +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ /** * @brief Create a new instance of a client. * @return A new instance of the client. */ -/* STATIC */ BLEClient *BLEDevice::createClient() { +BLEClient *BLEDevice::createClient() { log_v(">> createClient"); -#ifndef CONFIG_GATTC_ENABLE // Check that BLE GATTC is enabled in make menuconfig - log_e("BLE GATTC is not enabled - CONFIG_GATTC_ENABLE not defined"); +#if !defined(CONFIG_GATTC_ENABLE) && !defined(CONFIG_BT_NIMBLE_ROLE_CENTRAL) + log_e("BLE Client not enabled. Check CONFIG_GATTC_ENABLE for BlueDroid or CONFIG_BT_NIMBLE_ROLE_CENTRAL for NimBLE"); abort(); #endif // CONFIG_GATTC_ENABLE - m_pClient = new BLEClient(); + +#ifdef CONFIG_NIMBLE_ENABLED + if (m_connectedClientsMap.size() >= CONFIG_BT_NIMBLE_MAX_CONNECTIONS) { + log_e("Unable to create client. Max connections reached. Cur=%d Max=%d", m_connectedClientsMap.size(), CONFIG_BT_NIMBLE_MAX_CONNECTIONS); + m_pClient = nullptr; + } else +#endif + { + m_pClient = new BLEClient(); + } log_v("<< createClient"); return m_pClient; } // createClient @@ -73,200 +146,45 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; * @brief Create a new instance of a server. * @return A new instance of the server. */ -/* STATIC */ BLEServer *BLEDevice::createServer() { +BLEServer *BLEDevice::createServer() { log_v(">> createServer"); -#ifndef CONFIG_GATTS_ENABLE // Check that BLE GATTS is enabled in make menuconfig - log_e("BLE GATTS is not enabled - CONFIG_GATTS_ENABLE not defined"); +#if !defined(CONFIG_GATTS_ENABLE) && !defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) + log_e("BLE Server not enabled. Check CONFIG_GATTS_ENABLE for BlueDroid or CONFIG_BT_NIMBLE_ROLE_PERIPHERAL for NimBLE"); abort(); #endif // CONFIG_GATTS_ENABLE - m_pServer = new BLEServer(); - m_pServer->createApp(m_appId++); + + if (m_pServer == nullptr) { + m_pServer = new BLEServer(); + m_pServer->createApp(m_appId++); +#if defined(CONFIG_NIMBLE_ENABLED) + ble_gatts_reset(); + ble_svc_gap_init(); + ble_svc_gatt_init(); +#endif + } log_v("<< createServer"); return m_pServer; } // createServer -/** - * @brief Handle GATT server events. - * - * @param [in] event The event that has been newly received. - * @param [in] gatts_if The connection to the GATT interface. - * @param [in] param Parameters for the event. - */ -/* STATIC */ void BLEDevice::gattServerEventHandler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - log_d("gattServerEventHandler [esp_gatt_if: %d] ... %s", gatts_if, BLEUtils::gattServerEventTypeToString(event).c_str()); - - BLEUtils::dumpGattServerEvent(event, gatts_if, param); - - switch (event) { - case ESP_GATTS_CONNECT_EVT: - { -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - if (BLEDevice::m_securityLevel) { - esp_ble_set_encryption(param->connect.remote_bda, BLEDevice::m_securityLevel); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - } // ESP_GATTS_CONNECT_EVT - - default: - { - break; - } - } // switch - - if (BLEDevice::m_pServer != nullptr) { - BLEDevice::m_pServer->handleGATTServerEvent(event, gatts_if, param); - } - - if (m_customGattsHandler != nullptr) { - m_customGattsHandler(event, gatts_if, param); - } - -} // gattServerEventHandler - -/** - * @brief Handle GATT client events. - * - * Handler for the GATT client events. - * - * @param [in] event - * @param [in] gattc_if - * @param [in] param - */ -/* STATIC */ void BLEDevice::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - - log_d("gattClientEventHandler [esp_gatt_if: %d] ... %s", gattc_if, BLEUtils::gattClientEventTypeToString(event).c_str()); - BLEUtils::dumpGattClientEvent(event, gattc_if, param); - - switch (event) { - case ESP_GATTC_CONNECT_EVT: - { -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - if (BLEDevice::m_securityLevel) { - esp_ble_set_encryption(param->connect.remote_bda, BLEDevice::m_securityLevel); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - } // ESP_GATTS_CONNECT_EVT - - default: break; - } // switch - for (auto &myPair : BLEDevice::getPeerDevices(true)) { - conn_status_t conn_status = (conn_status_t)myPair.second; - if (((BLEClient *)conn_status.peer_device)->getGattcIf() == gattc_if || ((BLEClient *)conn_status.peer_device)->getGattcIf() == ESP_GATT_IF_NONE - || gattc_if == ESP_GATT_IF_NONE) { - ((BLEClient *)conn_status.peer_device)->gattClientEventHandler(event, gattc_if, param); - } - } - - if (m_customGattcHandler != nullptr) { - m_customGattcHandler(event, gattc_if, param); - } - -} // gattClientEventHandler - -/** - * @brief Handle GAP events. - */ -/* STATIC */ void BLEDevice::gapEventHandler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - - BLEUtils::dumpGapEvent(event, param); - - switch (event) { - - case ESP_GAP_BLE_OOB_REQ_EVT: /* OOB request event */ log_i("ESP_GAP_BLE_OOB_REQ_EVT"); break; - case ESP_GAP_BLE_LOCAL_IR_EVT: /* BLE local IR event */ log_i("ESP_GAP_BLE_LOCAL_IR_EVT"); break; - case ESP_GAP_BLE_LOCAL_ER_EVT: /* BLE local ER event */ log_i("ESP_GAP_BLE_LOCAL_ER_EVT"); break; - case ESP_GAP_BLE_NC_REQ_EVT: /* NUMERIC CONFIRMATION */ log_i("ESP_GAP_BLE_NC_REQ_EVT"); -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - if (BLEDevice::m_securityCallbacks != nullptr) { - esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, BLEDevice::m_securityCallbacks->onConfirmPIN(param->ble_security.key_notif.passkey)); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - case ESP_GAP_BLE_PASSKEY_REQ_EVT: /* passkey request event */ - log_i("ESP_GAP_BLE_PASSKEY_REQ_EVT: "); - // esp_log_buffer_hex(m_remote_bda, sizeof(m_remote_bda)); -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - if (BLEDevice::m_securityCallbacks != nullptr) { - esp_ble_passkey_reply(param->ble_security.ble_req.bd_addr, true, BLEDevice::m_securityCallbacks->onPassKeyRequest()); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - /* - * TODO should we add white/black list comparison? - */ - case ESP_GAP_BLE_SEC_REQ_EVT: - /* send the positive(true) security response to the peer device to accept the security request. - If not accept the security request, should sent the security response with negative(false) accept value*/ - log_i("ESP_GAP_BLE_SEC_REQ_EVT"); -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - if (BLEDevice::m_securityCallbacks != nullptr) { - esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, BLEDevice::m_securityCallbacks->onSecurityRequest()); - } else { - esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - /* - * - */ - case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: //the app will receive this evt when the IO has Output capability and the peer device IO has Input capability. - //display the passkey number to the user to input it in the peer device within 30 seconds - log_i("ESP_GAP_BLE_PASSKEY_NOTIF_EVT"); -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - log_i("passKey = %d", param->ble_security.key_notif.passkey); - if (BLEDevice::m_securityCallbacks != nullptr) { - BLEDevice::m_securityCallbacks->onPassKeyNotify(param->ble_security.key_notif.passkey); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - case ESP_GAP_BLE_KEY_EVT: - //shows the ble key type info share with peer device to the user. - log_d("ESP_GAP_BLE_KEY_EVT"); -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - log_i("key type = %s", BLESecurity::esp_key_type_to_str(param->ble_security.ble_key.key_type)); -#endif // CONFIG_BLE_SMP_ENABLE - break; - case ESP_GAP_BLE_AUTH_CMPL_EVT: log_i("ESP_GAP_BLE_AUTH_CMPL_EVT"); -#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig - if (BLEDevice::m_securityCallbacks != nullptr) { - BLEDevice::m_securityCallbacks->onAuthenticationComplete(param->ble_security.auth_cmpl); - } -#endif // CONFIG_BLE_SMP_ENABLE - break; - default: - { - break; - } - } // switch - - if (BLEDevice::m_pClient != nullptr) { - BLEDevice::m_pClient->handleGAPEvent(event, param); - } - - if (BLEDevice::m_pScan != nullptr) { - BLEDevice::getScan()->handleGAPEvent(event, param); - } - - if (m_bleAdvertising != nullptr) { - BLEDevice::getAdvertising()->handleGAPEvent(event, param); - } - - if (m_customGapHandler != nullptr) { - BLEDevice::m_customGapHandler(event, param); - } - -} // gapEventHandler - /** * @brief Get the BLE device address. * @return The BLE device address. */ -/* STATIC*/ BLEAddress BLEDevice::getAddress() { +BLEAddress BLEDevice::getAddress() { +#if defined(CONFIG_BLUEDROID_ENABLED) const uint8_t *bdAddr = esp_bt_dev_get_address(); esp_bd_addr_t addr; memcpy(addr, bdAddr, sizeof(addr)); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + ble_addr_t addr; + int ret = ble_hs_id_copy_addr(m_ownAddrType, addr.val, NULL); + if (ret != 0) { + log_e("No BLE address found. rc=%d", ret); + return BLEAddress(); + } +#endif return BLEAddress(addr); } // getAddress @@ -275,7 +193,7 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; * @return The scanning object reference. This is a singleton object. The caller should not * try and release/delete it. */ -/* STATIC */ BLEScan *BLEDevice::getScan() { +BLEScan *BLEDevice::getScan() { //log_v(">> getScan"); if (m_pScan == nullptr) { m_pScan = new BLEScan(); @@ -285,13 +203,21 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; return m_pScan; } // getScan +/** + * @brief Retrieve the server object + * @return The server object + */ +BLEServer *BLEDevice::getServer() { + return m_pServer; +} // getServer + /** * @brief Get the value of a characteristic of a service on a remote device. * @param [in] bdAddress * @param [in] serviceUUID * @param [in] characteristicUUID */ -/* STATIC */ String BLEDevice::getValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID) { +String BLEDevice::getValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID) { log_v( ">> getValue: bdAddress: %s, serviceUUID: %s, characteristicUUID: %s", bdAddress.toString().c_str(), serviceUUID.toString().c_str(), characteristicUUID.toString().c_str() @@ -308,18 +234,25 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; * @brief Initialize the %BLE environment. * @param deviceName The device name of the device. */ -/* STATIC */ void BLEDevice::init(String deviceName) { +void BLEDevice::init(String deviceName) { if (!initialized) { - initialized = true; // Set the initialization flag to ensure we are only initialized once. - esp_err_t errRc = ESP_OK; -#ifdef ARDUINO_ARCH_ESP32 +#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(ARDUINO_ARCH_ESP32) if (!btStart()) { errRc = ESP_FAIL; return; } #else - errRc = ::nvs_flash_init(); + btStarted(); + errRc = nvs_flash_init(); + if (errRc == ESP_ERR_NVS_NO_FREE_PAGES || errRc == ESP_ERR_NVS_NEW_VERSION_FOUND) { + errRc = nvs_flash_erase(); + if (errRc == ESP_OK) { + errRc = nvs_flash_init(); + } + } + if (errRc != ESP_OK) { log_e("nvs_flash_init: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); return; @@ -328,6 +261,7 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; #ifndef CONFIG_BT_CLASSIC_ENABLED esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); #endif + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); errRc = esp_bt_controller_init(&bt_cfg); if (errRc != ESP_OK) { @@ -348,7 +282,7 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; return; } #endif -#endif +#endif // !ARDUINO_ARCH_ESP32 esp_bluedroid_status_t bt_state = esp_bluedroid_get_status(); if (bt_state == ESP_BLUEDROID_STATUS_UNINITIALIZED) { @@ -403,6 +337,45 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; return; }; #endif // CONFIG_BLE_SMP_ENABLE +#endif // CONFIG_BLUEDROID_ENABLED + +#if defined(CONFIG_NIMBLE_ENABLED) + errRc = nimble_port_init(); + if (errRc != ESP_OK) { + log_e("nimble_port_init: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return; + } + + // Global struct ble_hs_cfg from nimble/host/ble_hs.h needs to be initialized + ble_hs_cfg.reset_cb = BLEDevice::onReset; + ble_hs_cfg.sync_cb = BLEDevice::onSync; + ble_hs_cfg.store_status_cb = [](struct ble_store_status_event *event, void *arg) { + return m_pDeviceCallbacks->onStoreStatus(event, arg); + }; + ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT; + ble_hs_cfg.sm_bonding = 0; + ble_hs_cfg.sm_mitm = 0; + ble_hs_cfg.sm_sc = 1; + ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC; + ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC; +#if MYNEWT_VAL(BLE_LL_CFG_FEAT_LL_PRIVACY) + ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ID; +#endif + + errRc = ble_svc_gap_device_name_set(deviceName.c_str()); + if (errRc != ESP_OK) { + log_e("ble_svc_gap_device_name_set: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + } + + ble_store_config_init(); + nimble_port_freertos_init(BLEDevice::host_task); + + while (!m_synced) { + ble_npl_time_delay(1); + } +#endif // CONFIG_NIMBLE_ENABLED + initialized = true; // Set the initialization flag to ensure we are only initialized once. } vTaskDelay(200 / portTICK_PERIOD_MS); // Delay for 200 msecs as a workaround to an apparent Arduino environment issue. } // init @@ -435,7 +408,7 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; * @param [in] powerType. * @param [in] powerLevel. */ -/* STATIC */ void BLEDevice::setPower(esp_power_level_t powerLevel, esp_ble_power_type_t powerType) { +void BLEDevice::setPower(esp_power_level_t powerLevel, esp_ble_power_type_t powerType) { log_v(">> setPower: %d (type: %d)", powerLevel, powerType); esp_err_t errRc = ::esp_ble_tx_power_set(powerType, powerLevel); if (errRc != ESP_OK) { @@ -444,13 +417,45 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; log_v("<< setPower"); } // setPower +/** + * @brief Get the transmission power. + * @param [in] powerType The power level to set, can be one of: + * * ESP_BLE_PWR_TYPE_CONN_HDL0 = 0, For connection handle 0 + * * ESP_BLE_PWR_TYPE_CONN_HDL1 = 1, For connection handle 1 + * * ESP_BLE_PWR_TYPE_CONN_HDL2 = 2, For connection handle 2 + * * ESP_BLE_PWR_TYPE_CONN_HDL3 = 3, For connection handle 3 + * * ESP_BLE_PWR_TYPE_CONN_HDL4 = 4, For connection handle 4 + * * ESP_BLE_PWR_TYPE_CONN_HDL5 = 5, For connection handle 5 + * * ESP_BLE_PWR_TYPE_CONN_HDL6 = 6, For connection handle 6 + * * ESP_BLE_PWR_TYPE_CONN_HDL7 = 7, For connection handle 7 + * * ESP_BLE_PWR_TYPE_CONN_HDL8 = 8, For connection handle 8 + * * ESP_BLE_PWR_TYPE_ADV = 9, For advertising + * * ESP_BLE_PWR_TYPE_SCAN = 10, For scan + * * ESP_BLE_PWR_TYPE_DEFAULT = 11, For default, if not set other, it will use default value + * @return the power level currently used by the type specified. + */ + +int BLEDevice::getPower(esp_ble_power_type_t powerType) { + switch (esp_ble_tx_power_get(powerType)) { + case ESP_PWR_LVL_N12: return -12; + case ESP_PWR_LVL_N9: return -9; + case ESP_PWR_LVL_N6: return -6; + case ESP_PWR_LVL_N3: return -6; + case ESP_PWR_LVL_N0: return 0; + case ESP_PWR_LVL_P3: return 3; + case ESP_PWR_LVL_P6: return 6; + case ESP_PWR_LVL_P9: return 9; + default: return -128; + } +} // getPower + /** * @brief Set the value of a characteristic of a service on a remote device. * @param [in] bdAddress * @param [in] serviceUUID * @param [in] characteristicUUID */ -/* STATIC */ void BLEDevice::setValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID, String value) { +void BLEDevice::setValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID, String value) { log_v( ">> setValue: bdAddress: %s, serviceUUID: %s, characteristicUUID: %s", bdAddress.toString().c_str(), serviceUUID.toString().c_str(), characteristicUUID.toString().c_str() @@ -465,7 +470,7 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; * @brief Return a string representation of the nature of this device. * @return A string representation of the nature of this device. */ -/* STATIC */ String BLEDevice::toString() { +String BLEDevice::toString() { String res = "BD Address: " + getAddress().toString(); return res; } // toString @@ -476,14 +481,27 @@ gatts_event_handler BLEDevice::m_customGattsHandler = nullptr; */ void BLEDevice::whiteListAdd(BLEAddress address) { log_v(">> whiteListAdd: %s", address.toString().c_str()); +#ifdef CONFIG_BLUEDROID_ENABLED #ifdef ESP_IDF_VERSION_MAJOR - esp_err_t errRc = esp_ble_gap_update_whitelist(true, *address.getNative(), BLE_WL_ADDR_TYPE_PUBLIC); // HACK!!! True to add an entry. + esp_err_t errRc = esp_ble_gap_update_whitelist(true, address.getNative(), BLE_WL_ADDR_TYPE_PUBLIC); // HACK!!! True to add an entry. #else - esp_err_t errRc = esp_ble_gap_update_whitelist(true, *address.getNative()); // True to add an entry. + esp_err_t errRc = esp_ble_gap_update_whitelist(true, address.getNative()); // True to add an entry. #endif if (errRc != ESP_OK) { log_e("esp_ble_gap_update_whitelist: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); } +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + if (!BLEDevice::onWhiteList(address)) { + m_whiteList.push_back(address); + int errRc = ble_gap_wl_set(reinterpret_cast(&m_whiteList[0]), m_whiteList.size()); + if (errRc != 0) { + log_e("Failed adding to whitelist rc=%d", errRc); + m_whiteList.pop_back(); + } + } +#endif log_v("<< whiteListAdd"); } // whiteListAdd @@ -493,28 +511,36 @@ void BLEDevice::whiteListAdd(BLEAddress address) { */ void BLEDevice::whiteListRemove(BLEAddress address) { log_v(">> whiteListRemove: %s", address.toString().c_str()); +#ifdef CONFIG_BLUEDROID_ENABLED #ifdef ESP_IDF_VERSION_MAJOR - esp_err_t errRc = esp_ble_gap_update_whitelist(false, *address.getNative(), BLE_WL_ADDR_TYPE_PUBLIC); // HACK!!! False to remove an entry. + esp_err_t errRc = esp_ble_gap_update_whitelist(false, address.getNative(), BLE_WL_ADDR_TYPE_PUBLIC); // HACK!!! False to remove an entry. #else - esp_err_t errRc = esp_ble_gap_update_whitelist(false, *address.getNative()); // False to remove an entry. + esp_err_t errRc = esp_ble_gap_update_whitelist(false, address.getNative()); // False to remove an entry. #endif if (errRc != ESP_OK) { log_e("esp_ble_gap_update_whitelist: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); } +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + for (auto it = m_whiteList.begin(); it < m_whiteList.end(); ++it) { + if (*it == address) { + m_whiteList.erase(it); + int errRc = ble_gap_wl_set(reinterpret_cast(&m_whiteList[0]), m_whiteList.size()); + if (errRc != 0) { + m_whiteList.push_back(address); + log_e("Failed removing from whitelist rc=%d", errRc); + } + std::vector(m_whiteList).swap(m_whiteList); + } + } +#endif log_v("<< whiteListRemove"); } // whiteListRemove /* - * @brief Set encryption level that will be negotiated with peer device durng connection - * @param [in] level Requested encryption level - */ -void BLEDevice::setEncryptionLevel(esp_ble_sec_act_t level) { - BLEDevice::m_securityLevel = level; -} - -/* - * @brief Set callbacks that will be used to handle encryption negotiation events and authentication events - * @param [in] cllbacks Pointer to BLESecurityCallbacks class callback + * @brief Set callbacks that will be used to handle encryption negotiation events and authentication events + * @param [in] callbacks Pointer to BLESecurityCallbacks class callback */ void BLEDevice::setSecurityCallbacks(BLESecurityCallbacks *callbacks) { BLEDevice::m_securityCallbacks = callbacks; @@ -526,11 +552,19 @@ void BLEDevice::setSecurityCallbacks(BLESecurityCallbacks *callbacks) { */ esp_err_t BLEDevice::setMTU(uint16_t mtu) { log_v(">> setLocalMTU: %d", mtu); + +#ifdef CONFIG_BLUEDROID_ENABLED esp_err_t err = esp_ble_gatt_set_local_mtu(mtu); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + int err = ble_att_set_preferred_mtu(mtu); +#endif + if (err == ESP_OK) { m_localMTU = mtu; } else { - log_e("can't set local mtu value: %d", mtu); + log_e("can't set local mtu value: %d, rc=%d", mtu, err); } log_v("<< setLocalMTU"); return err; @@ -574,10 +608,23 @@ std::map BLEDevice::getPeerDevices(bool _client) { return m_connectedClientsMap; } +BLEClient *BLEDevice::getClientByID(uint16_t conn_id) { + return BLEDevice::getClientByGattIf(conn_id); +} + BLEClient *BLEDevice::getClientByGattIf(uint16_t conn_id) { return (BLEClient *)m_connectedClientsMap.find(conn_id)->second.peer_device; } +BLEClient *BLEDevice::getClientByAddress(BLEAddress address) { + for (auto &it : m_connectedClientsMap) { + if (((BLEClient *)it.second.peer_device)->getPeerAddress() == address) { + return (BLEClient *)it.second.peer_device; + } + } + return nullptr; +} + void BLEDevice::updatePeerDevice(void *peer, bool _client, uint16_t conn_id) { log_d("update conn_id: %d, GATT role: %s", conn_id, _client ? "client" : "server"); std::map::iterator it = m_connectedClientsMap.find(ESP_GATT_IF_NONE); @@ -619,20 +666,32 @@ void BLEDevice::removePeerDevice(uint16_t conn_id, bool _client) { * @brief de-Initialize the %BLE environment. * @param release_memory release the internal BT stack memory */ -/* STATIC */ void BLEDevice::deinit(bool release_memory) { +void BLEDevice::deinit(bool release_memory) { if (!initialized) { return; } +#ifdef CONFIG_BLUEDROID_ENABLED esp_bluedroid_disable(); esp_bluedroid_deinit(); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + nimble_port_stop(); + nimble_port_deinit(); +#endif + esp_bt_controller_disable(); esp_bt_controller_deinit(); + #ifdef ARDUINO_ARCH_ESP32 if (release_memory) { - esp_bt_controller_mem_release(ESP_BT_MODE_BTDM - ); // <-- require tests because we released classic BT memory and this can cause crash (most likely not, esp-idf takes care of it) + // Require tests because we released classic BT memory and this can cause crash (most likely not, esp-idf takes care of it) + esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); } else { +#ifdef CONFIG_NIMBLE_ENABLED + m_synced = false; +#endif initialized = false; } #endif @@ -640,8 +699,195 @@ void BLEDevice::removePeerDevice(uint16_t conn_id, bool _client) { void BLEDevice::setCustomGapHandler(gap_event_handler handler) { m_customGapHandler = handler; +#ifdef CONFIG_NIMBLE_ENABLED + int rc = ble_gap_event_listener_register(&m_listener, handler, NULL); + if (rc == BLE_HS_EALREADY) { + log_i("Already listening to GAP events."); + } else if (rc != 0) { + log_e("ble_gap_event_listener_register: rc=%d %s", rc, GeneralUtils::errorToString(rc)); + } +#endif } +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +/** + * @brief Handle GATT server events. + * + * @param [in] event The event that has been newly received. + * @param [in] gatts_if The connection to the GATT interface. + * @param [in] param Parameters for the event. + */ +void BLEDevice::gattServerEventHandler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { + log_d("gattServerEventHandler [esp_gatt_if: %d] ... %s", gatts_if, BLEUtils::gattServerEventTypeToString(event).c_str()); + + BLEUtils::dumpGattServerEvent(event, gatts_if, param); + + switch (event) { + case ESP_GATTS_CONNECT_EVT: + { +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + if (BLEDevice::m_securityLevel) { + esp_ble_set_encryption(param->connect.remote_bda, BLEDevice::m_securityLevel); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + } // ESP_GATTS_CONNECT_EVT + + default: + { + break; + } + } // switch + + if (BLEDevice::m_pServer != nullptr) { + BLEDevice::m_pServer->handleGATTServerEvent(event, gatts_if, param); + } + + if (m_customGattsHandler != nullptr) { + m_customGattsHandler(event, gatts_if, param); + } + +} // gattServerEventHandler + +/** + * @brief Handle GATT client events. + * + * Handler for the GATT client events. + * + * @param [in] event + * @param [in] gattc_if + * @param [in] param + */ +void BLEDevice::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + + log_d("gattClientEventHandler [esp_gatt_if: %d] ... %s", gattc_if, BLEUtils::gattClientEventTypeToString(event).c_str()); + BLEUtils::dumpGattClientEvent(event, gattc_if, param); + + switch (event) { + case ESP_GATTC_CONNECT_EVT: + { +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + if (BLEDevice::m_securityLevel) { + esp_ble_set_encryption(param->connect.remote_bda, BLEDevice::m_securityLevel); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + } // ESP_GATTS_CONNECT_EVT + + default: break; + } // switch + for (auto &myPair : BLEDevice::getPeerDevices(true)) { + conn_status_t conn_status = (conn_status_t)myPair.second; + if (((BLEClient *)conn_status.peer_device)->getGattcIf() == gattc_if || ((BLEClient *)conn_status.peer_device)->getGattcIf() == ESP_GATT_IF_NONE + || gattc_if == ESP_GATT_IF_NONE) { + ((BLEClient *)conn_status.peer_device)->gattClientEventHandler(event, gattc_if, param); + } + } + + if (m_customGattcHandler != nullptr) { + m_customGattcHandler(event, gattc_if, param); + } + +} // gattClientEventHandler + +/** + * @brief Handle GAP events. + */ +void BLEDevice::gapEventHandler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + + BLEUtils::dumpGapEvent(event, param); + + switch (event) { + + case ESP_GAP_BLE_OOB_REQ_EVT: /* OOB request event */ log_i("ESP_GAP_BLE_OOB_REQ_EVT"); break; + case ESP_GAP_BLE_LOCAL_IR_EVT: /* BLE local IR event */ log_i("ESP_GAP_BLE_LOCAL_IR_EVT"); break; + case ESP_GAP_BLE_LOCAL_ER_EVT: /* BLE local ER event */ log_i("ESP_GAP_BLE_LOCAL_ER_EVT"); break; + case ESP_GAP_BLE_NC_REQ_EVT: /* NUMERIC CONFIRMATION */ log_i("ESP_GAP_BLE_NC_REQ_EVT"); +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + if (BLEDevice::m_securityCallbacks != nullptr) { + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, BLEDevice::m_securityCallbacks->onConfirmPIN(param->ble_security.key_notif.passkey)); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + case ESP_GAP_BLE_PASSKEY_REQ_EVT: /* passkey request event */ + log_i("ESP_GAP_BLE_PASSKEY_REQ_EVT: "); + // esp_log_buffer_hex(m_remote_bda, sizeof(m_remote_bda)); +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + if (BLEDevice::m_securityCallbacks != nullptr) { + esp_ble_passkey_reply(param->ble_security.ble_req.bd_addr, true, BLEDevice::m_securityCallbacks->onPassKeyRequest()); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + /* + * TODO should we add white/black list comparison? + */ + case ESP_GAP_BLE_SEC_REQ_EVT: + /* send the positive(true) security response to the peer device to accept the security request. + If not accept the security request, should sent the security response with negative(false) accept value*/ + log_i("ESP_GAP_BLE_SEC_REQ_EVT"); +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + if (BLEDevice::m_securityCallbacks != nullptr) { + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, BLEDevice::m_securityCallbacks->onSecurityRequest()); + } else { + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + /* + * + */ + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: //the app will receive this evt when the IO has Output capability and the peer device IO has Input capability. + //display the passkey number to the user to input it in the peer device within 30 seconds + log_i("ESP_GAP_BLE_PASSKEY_NOTIF_EVT"); +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + log_i("passKey = %d", param->ble_security.key_notif.passkey); + if (BLEDevice::m_securityCallbacks != nullptr) { + BLEDevice::m_securityCallbacks->onPassKeyNotify(param->ble_security.key_notif.passkey); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + case ESP_GAP_BLE_KEY_EVT: + //shows the ble key type info share with peer device to the user. + log_d("ESP_GAP_BLE_KEY_EVT"); +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + log_i("key type = %s", BLESecurity::esp_key_type_to_str(param->ble_security.ble_key.key_type)); +#endif // CONFIG_BLE_SMP_ENABLE + break; + case ESP_GAP_BLE_AUTH_CMPL_EVT: log_i("ESP_GAP_BLE_AUTH_CMPL_EVT"); +#ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + if (BLEDevice::m_securityCallbacks != nullptr) { + BLEDevice::m_securityCallbacks->onAuthenticationComplete(param->ble_security.auth_cmpl); + } +#endif // CONFIG_BLE_SMP_ENABLE + break; + default: + { + break; + } + } // switch + + if (BLEDevice::m_pClient != nullptr) { + BLEDevice::m_pClient->handleGAPEvent(event, param); + } + + if (BLEDevice::m_pScan != nullptr) { + BLEDevice::getScan()->handleGAPEvent(event, param); + } + + if (m_bleAdvertising != nullptr) { + BLEDevice::getAdvertising()->handleGAPEvent(event, param); + } + + if (m_customGapHandler != nullptr) { + BLEDevice::m_customGapHandler(event, param); + } + +} // gapEventHandler void BLEDevice::setCustomGattcHandler(gattc_event_handler handler) { m_customGattcHandler = handler; } @@ -650,5 +896,170 @@ void BLEDevice::setCustomGattsHandler(gatts_event_handler handler) { m_customGattsHandler = handler; } -#endif /* CONFIG_BLUEDROID_ENABLED */ +/* + * @brief Set encryption level that will be negotiated with peer device durng connection + * @param [in] level Requested encryption level + */ +void BLEDevice::setEncryptionLevel(esp_ble_sec_act_t level) { + BLEDevice::m_securityLevel = level; +} + +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +/** + * @brief Checks if a peer device is whitelisted. + * @param [in] address The address to check for in the whitelist. + * @returns True if the address is in the whitelist. + */ +bool BLEDevice::onWhiteList(BLEAddress &address) { + for (auto &addr : m_whiteList) { + if (addr == address) { + return true; + } + } + return false; +} + +void BLEDevice::host_task(void *param) { + log_i("NimBLE host task started"); + nimble_port_run(); // This function will return only when nimble_port_stop() is executed + nimble_port_freertos_deinit(); +} + +void BLEDevice::onReset(int reason) { + if (!m_synced) { + return; + } + + m_synced = false; + + log_i("onReset, reason=%d, %s", reason, BLEUtils::returnCodeToString(reason)); + + if (initialized) { + if (m_pScan != nullptr) { + m_pScan->onHostReset(); + } + } +} + +void BLEDevice::onSync() { + log_d("onSync"); + + if (m_synced) { + log_d("onSync: already synced"); + return; + } + + int rc = ble_hs_util_ensure_addr(0); + if (rc == 0) { + rc = ble_hs_util_ensure_addr(1); + } + + if (rc != 0) { + log_e("onSync: failed to ensure BLE address. rc=%d", rc); + return; + } + + rc = ble_hs_id_copy_addr(BLE_OWN_ADDR_PUBLIC, NULL, NULL); + if (rc != 0) { + log_d("onSync: no public address available"); + m_ownAddrType = BLE_OWN_ADDR_RANDOM; + } + + // Yield for housekeeping tasks before returning to operations. + // Occasionally triggers exception without. + ble_npl_time_delay(1); + + m_synced = true; + + if (initialized) { + if (m_pScan != nullptr) { + m_pScan->onHostSync(); + } + if (m_bleAdvertising != nullptr) { + m_bleAdvertising->onHostSync(); + } + } +} + +void BLEDevice::setDeviceCallbacks(BLEDeviceCallbacks *cb) { + if (cb == nullptr) { + m_pDeviceCallbacks = &defaultDeviceCallbacks; + } else { + m_pDeviceCallbacks = cb; + } +} + +/** + * @brief Sets the address type to use. + * @param [in] type Bluetooth Device address type. + * The available types are defined as: + * * 0x00: BLE_OWN_ADDR_PUBLIC - Public address; Uses the hardware static address. + * * 0x01: BLE_OWN_ADDR_RANDOM - Random static address; Uses the hardware or generated random static address. + * * 0x02: BLE_OWN_ADDR_RPA_PUBLIC_DEFAULT - Resolvable private address, defaults to public if no RPA available. + * * 0x03: BLE_OWN_ADDR_RPA_RANDOM_DEFAULT - Resolvable private address, defaults to random static if no RPA available. + */ +bool BLEDevice::setOwnAddrType(uint8_t type) { + int rc = ble_hs_id_copy_addr(type & 1, NULL, NULL); // Odd values are random + if (rc != 0) { + log_e("Unable to set address type %d, rc=%d", type, rc); + return false; + } + + m_ownAddrType = type; + + if (type == BLE_OWN_ADDR_RPA_PUBLIC_DEFAULT || type == BLE_OWN_ADDR_RPA_RANDOM_DEFAULT) { +#ifdef CONFIG_IDF_TARGET_ESP32 + // esp32 controller does not support RPA so we must use the random static for calls to the stack + // the host will take care of the random private address generation/setting. + m_ownAddrType = BLE_OWN_ADDR_RANDOM; + rc = ble_hs_pvcy_rpa_config(NIMBLE_HOST_ENABLE_RPA); +#endif + } else { +#ifdef CONFIG_IDF_TARGET_ESP32 + rc = ble_hs_pvcy_rpa_config(NIMBLE_HOST_DISABLE_PRIVACY); +#endif + } + return rc == 0; +} // setOwnAddrType + +/** + * @brief Set the device address to use. + * @param [in] addr The address to set. + * @return True if the address was set successfully. + * @details To use the address generated the address type must be set to random with `setOwnAddrType`. + */ +bool BLEDevice::setOwnAddr(BLEAddress &addr) { + return setOwnAddr(addr.getNative()); +} // setOwnAddr + +/** + * @brief Set the device address to use. + * @param [in] addr The address to set. + * @return True if the address was set successfully. + * @details To use the address generated the address type must be set to random with `setOwnAddrType`. + */ +bool BLEDevice::setOwnAddr(uint8_t *addr) { + int rc = ble_hs_id_set_rnd(addr); + if (rc != 0) { + log_e("Failed to set address, rc=%d", rc); + return false; + } + return true; +} // setOwnAddr + +int BLEDeviceCallbacks::onStoreStatus(struct ble_store_status_event *event, void *arg) { + log_d("onStoreStatus: default"); + return ble_store_util_status_rr(event, arg); +} + +#endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEDevice.h b/libraries/BLE/src/BLEDevice.h index 01bf143c101..66cfa2b371a 100644 --- a/libraries/BLE/src/BLEDevice.h +++ b/libraries/BLE/src/BLEDevice.h @@ -3,6 +3,10 @@ * * Created on: Mar 16, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef MAIN_BLEDevice_H_ @@ -11,88 +15,252 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include // ESP32 BLE -#include // ESP32 BLE -#include // Part of C++ STL +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + +#include #include #include - #include "BLEServer.h" #include "BLEClient.h" #include "BLEUtils.h" #include "BLEScan.h" +#include "BLEAdvertising.h" +#include "BLESecurity.h" #include "BLEAddress.h" +#include "BLEUtils.h" -/** - * @brief BLE functions. - */ +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#include +#include +#endif + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#define ESP_GATT_IF_NONE BLE_HS_CONN_HANDLE_NONE + +// NimBLE configuration compatibility macros +#if defined(CONFIG_SCAN_DUPLICATE_BY_DEVICE_ADDR) && !defined(CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE) +#define CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE CONFIG_SCAN_DUPLICATE_BY_DEVICE_ADDR +#endif + +#if defined(CONFIG_SCAN_DUPLICATE_BY_ADV_DATA) && !defined(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA) +#define CONFIG_BTDM_SCAN_DUPL_TYPE_DATA CONFIG_SCAN_DUPLICATE_BY_ADV_DATA +#endif + +#if defined(CONFIG_SCAN_DUPLICATE_BY_ADV_DATA_AND_DEVICE_ADDR) && !defined(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE) +#define CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE CONFIG_SCAN_DUPLICATE_BY_ADV_DATA_AND_DEVICE_ADDR +#endif + +#if defined(CONFIG_SCAN_DUPLICATE_TYPE) && !defined(CONFIG_BTDM_SCAN_DUPL_TYPE) +#define CONFIG_BTDM_SCAN_DUPL_TYPE CONFIG_SCAN_DUPLICATE_TYPE +#endif + +#if defined(CONFIG_BT_CTRL_SCAN_DUPL_TYPE) && !defined(CONFIG_BTDM_SCAN_DUPL_TYPE) +#define CONFIG_BTDM_SCAN_DUPL_TYPE CONFIG_BT_CTRL_SCAN_DUPL_TYPE +#endif + +#if defined(CONFIG_BT_LE_SCAN_DUPL_TYPE) && !defined(CONFIG_BTDM_SCAN_DUPL_TYPE) +#define CONFIG_BTDM_SCAN_DUPL_TYPE CONFIG_BT_LE_SCAN_DUPL_TYPE +#endif + +#if defined(CONFIG_DUPLICATE_SCAN_CACHE_SIZE) && !defined(CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE) +#define CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE CONFIG_DUPLICATE_SCAN_CACHE_SIZE +#endif + +#if defined(CONFIG_BT_CTRL_SCAN_DUPL_CACHE_SIZE) && !defined(CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE) +#define CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE CONFIG_BT_CTRL_SCAN_DUPL_CACHE_SIZE +#endif + +#if defined(CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT) && !defined(CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE) +#define CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT +#endif + +#if defined(CONFIG_NIMBLE_MAX_CONNECTIONS) && !defined(CONFIG_BT_NIMBLE_MAX_CONNECTIONS) +#define CONFIG_BT_NIMBLE_MAX_CONNECTIONS CONFIG_NIMBLE_MAX_CONNECTIONS +#endif + +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ + +class BLEAddress; +class BLEDeviceCallbacks; +class BLESecurityCallbacks; +class BLEServer; +class BLEScan; +class BLEAdvertising; +class BLEClient; +class BLESecurity; + +/*************************************************************************** + * Bluedroid type definitions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) typedef void (*gap_event_handler)(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); typedef void (*gattc_event_handler)(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); typedef void (*gatts_event_handler)(esp_gatts_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gatts_cb_param_t *param); +#endif + +/*************************************************************************** + * NimBLE type definitions and externals * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +extern "C" void ble_store_config_init(void); +typedef int (*gap_event_handler)(struct ble_gap_event *event, void *param); +#endif class BLEDevice { public: - static BLEClient *createClient(); // Create a new BLE client. - static BLEServer *createServer(); // Create a new BLE server. - static BLEAddress getAddress(); // Retrieve our own local BD address. - static BLEScan *getScan(); // Get the scan object - static String getValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID); // Get the value of a characteristic of a service on a server. - static void init(String deviceName); // Initialize the local BLE environment. - static void setPower(esp_power_level_t powerLevel, esp_ble_power_type_t powerType = ESP_BLE_PWR_TYPE_DEFAULT); // Set our power level. - static void setValue( - BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID, String value - ); // Set the value of a characteristic on a service on a server. - static String toString(); // Return a string representation of our device. - static void whiteListAdd(BLEAddress address); // Add an entry to the BLE white list. - static void whiteListRemove(BLEAddress address); // Remove an entry from the BLE white list. - static void setEncryptionLevel(esp_ble_sec_act_t level); + /*************************************************************************** + * Common public properties * + ***************************************************************************/ + + static uint16_t m_appId; + static uint16_t m_localMTU; + static gap_event_handler m_customGapHandler; + + /*************************************************************************** + * Bluedroid public properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + static esp_ble_sec_act_t m_securityLevel; + static gattc_event_handler m_customGattcHandler; + static gatts_event_handler m_customGattsHandler; +#endif + + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + + static BLEClient *createClient(); + static BLEServer *createServer(); + static BLEAddress getAddress(); + static BLEServer *getServer(); + static BLEScan *getScan(); + static String getValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID); + static void init(String deviceName); + static void setPower(esp_power_level_t powerLevel, esp_ble_power_type_t powerType = ESP_BLE_PWR_TYPE_DEFAULT); + static int getPower(esp_ble_power_type_t powerType = ESP_BLE_PWR_TYPE_DEFAULT); + static void setValue(BLEAddress bdAddress, BLEUUID serviceUUID, BLEUUID characteristicUUID, String value); + static String toString(); + static void whiteListAdd(BLEAddress address); + static void whiteListRemove(BLEAddress address); static void setSecurityCallbacks(BLESecurityCallbacks *pCallbacks); static esp_err_t setMTU(uint16_t mtu); static uint16_t getMTU(); - static bool getInitialized(); // Returns the state of the device, is it initialized or not? - /* move advertising to BLEDevice for saving ram and flash in beacons */ + static bool getInitialized(); static BLEAdvertising *getAdvertising(); static void startAdvertising(); static void stopAdvertising(); - static uint16_t m_appId; - /* multi connect */ static std::map getPeerDevices(bool client); static void addPeerDevice(void *peer, bool is_client, uint16_t conn_id); static void updatePeerDevice(void *peer, bool _client, uint16_t conn_id); static void removePeerDevice(uint16_t conn_id, bool client); + static BLEClient *getClientByID(uint16_t conn_id); + static BLEClient *getClientByAddress(BLEAddress address); static BLEClient *getClientByGattIf(uint16_t conn_id); static void setCustomGapHandler(gap_event_handler handler); + static void deinit(bool release_memory = false); + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + static void setEncryptionLevel(esp_ble_sec_act_t level); static void setCustomGattcHandler(gattc_event_handler handler); static void setCustomGattsHandler(gatts_event_handler handler); - static void deinit(bool release_memory = false); - static uint16_t m_localMTU; - static esp_ble_sec_act_t m_securityLevel; +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static void onReset(int reason); + static void onSync(void); + static void host_task(void *param); + static bool setOwnAddrType(uint8_t type); + static bool setOwnAddr(BLEAddress &addr); + static bool setOwnAddr(uint8_t *addr); + static void setDeviceCallbacks(BLEDeviceCallbacks *cb); + static bool onWhiteList(BLEAddress &address); +#endif private: + friend class BLEClient; + friend class BLEScan; + friend class BLEServer; + friend class BLECharacteristic; + friend class BLEAdvertising; + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + static BLEServer *m_pServer; static BLEScan *m_pScan; static BLEClient *m_pClient; static BLESecurityCallbacks *m_securityCallbacks; static BLEAdvertising *m_bleAdvertising; - static esp_gatt_if_t getGattcIF(); static std::map m_connectedClientsMap; static portMUX_TYPE mux; - static void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ - static void gattServerEventHandler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#if defined(CONFIG_NIMBLE_ENABLED) + static uint8_t m_ownAddrType; + static bool m_synced; + static std::vector m_whiteList; + static BLEDeviceCallbacks defaultDeviceCallbacks; + static BLEDeviceCallbacks *m_pDeviceCallbacks; + static ble_gap_event_listener m_listener; +#endif + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + static esp_gatt_if_t getGattcIF(); + static void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + static void gattServerEventHandler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); static void gapEventHandler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); +#endif +}; // class BLE -public: - /* custom gap and gatt handlers for flexibility */ - static gap_event_handler m_customGapHandler; - static gattc_event_handler m_customGattcHandler; - static gatts_event_handler m_customGattsHandler; +/*************************************************************************** + * NimBLE specific classes * + ***************************************************************************/ -}; // class BLE +#if defined(CONFIG_NIMBLE_ENABLED) +class BLEDeviceCallbacks { +public: + virtual ~BLEDeviceCallbacks(){}; + virtual int onStoreStatus(struct ble_store_status_event *event, void *arg); +}; +#endif -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* MAIN_BLEDevice_H_ */ diff --git a/libraries/BLE/src/BLEEddystoneTLM.cpp b/libraries/BLE/src/BLEEddystoneTLM.cpp index 1a301f09011..ca0fb41b1a2 100644 --- a/libraries/BLE/src/BLEEddystoneTLM.cpp +++ b/libraries/BLE/src/BLEEddystoneTLM.cpp @@ -8,12 +8,16 @@ * Fix time stamp (0.1 second resolution) * Fixes based on EddystoneTLM frame specification https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on pcbreflux's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) #include #include #include "esp32-hal-log.h" @@ -176,5 +180,5 @@ void BLEEddystoneTLM::setTime(uint32_t tmil) { m_eddystoneData.tmil = tmil; } // setTime -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEEddystoneTLM.cppwithheadder b/libraries/BLE/src/BLEEddystoneTLM.cppwithheadder deleted file mode 100644 index 07002dbab1f..00000000000 --- a/libraries/BLE/src/BLEEddystoneTLM.cppwithheadder +++ /dev/null @@ -1,202 +0,0 @@ -/* - * BLEEddystoneTLM.cpp - * - * Created on: Mar 12, 2018 - * Author: pcbreflux - * Edited on: Mar 20, 2020 by beegee-tokyo - * Fix temperature value (8.8 fixed format) - * Fix time stamp (0.1 second resolution) - * Fixes based on EddystoneTLM frame specification https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md - * - */ -#include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include -#include -#include "esp32-hal-log.h" -#include "BLEEddystoneTLM.h" - -static const char LOG_TAG[] = "BLEEddystoneTLM"; - -BLEEddystoneTLM::BLEEddystoneTLM() { - m_eddystoneData.frameType = EDDYSTONE_TLM_FRAME_TYPE; - m_eddystoneData.version = 0; - m_eddystoneData.volt = 3300; // 3300mV = 3.3V - m_eddystoneData.temp = (uint16_t) ((float) 23.00)/256; - m_eddystoneData.advCount = 0; - m_eddystoneData.tmil = 0; - _initHeadder(); -} // BLEEddystoneTLM - -BLEEddystoneTLM::BLEEddystoneTLM(BLEAdvertisedDevice *advertisedDevice){ - char* payload = (char*)advertisedDevice->getPayload(); - for(int i = 0; i < advertisedDevice->getPayloadLength(); ++i){ - if(payload[i] == 0x16 && advertisedDevice->getPayloadLength() >= i+2+sizeof(m_eddystoneData) && payload[i+1] == 0xAA && payload[i+2] == 0xFE && payload[i+3] == 0x20){ - log_d("Eddystone TLM data frame starting at byte [%d]", i+3); - setData(std::string(payload+i+3, sizeof(m_eddystoneData))); - break; - } - } - _initHeadder(); -} - -String BLEEddystoneTLM::getData() { - return String((char*) &m_eddystoneData, sizeof(m_eddystoneData)); -} // getData - -BLEUUID BLEEddystoneTLM::getUUID() { - return beaconUUID; -} // getUUID - -uint8_t BLEEddystoneTLM::getVersion() { - return m_eddystoneData.version; -} // getVersion - -uint16_t BLEEddystoneTLM::getVolt() { - return ENDIAN_CHANGE_U16(m_eddystoneData.volt); -} // getVolt - -float BLEEddystoneTLM::getTemp() { - return EDDYSTONE_TEMP_U16_TO_FLOAT(m_eddystoneData.temp); -} // getTemp - -uint16_t BLEEddystoneTLM::getRawTemp() { - return ENDIAN_CHANGE_U16(m_eddystoneData.temp); -} // getRawTemp - -uint32_t BLEEddystoneTLM::getCount() { - return ENDIAN_CHANGE_U32(m_eddystoneData.advCount); -} // getCount - -uint32_t BLEEddystoneTLM::getTime() { - return (ENDIAN_CHANGE_U32(m_eddystoneData.tmil)) / 10; -} // getTime - -String BLEEddystoneTLM::getFrame(){ - String frame(BLEHeadder); - frame += String((char*) &m_eddystoneData, sizeof(m_eddystoneData)); - log_d("Compiled frame of length %d Bytes", frame.length()); - for(int i = 0; i < frame.length(); ++i){ - log_d("[%d]=0x%02X",i, frame[i]); - } - return frame; -} // getServiceData - -String BLEEddystoneTLM::toString() { - String out = ""; - uint32_t rawsec = ENDIAN_CHANGE_U32(m_eddystoneData.tmil); - char val[12]; - - out += "Version "; // + std::string(m_eddystoneData.version); - snprintf(val, sizeof(val), "%d", m_eddystoneData.version); - out += val; - out += "\n"; - out += "Battery Voltage "; // + ENDIAN_CHANGE_U16(m_eddystoneData.volt); - snprintf(val, sizeof(val), "%d", ENDIAN_CHANGE_U16(m_eddystoneData.volt)); - out += val; - out += " mV\n"; - - out += "Temperature "; - snprintf(val, sizeof(val), "%.2f", ((int16_t)ENDIAN_CHANGE_U16(m_eddystoneData.temp)) / 256.0f); - out += val; - out += " C\n"; - - out += "Adv. Count "; - snprintf(val, sizeof(val), "%d", ENDIAN_CHANGE_U32(m_eddystoneData.advCount)); - out += val; - out += "\n"; - - out += "Time in seconds "; - snprintf(val, sizeof(val), "%d", rawsec/10); - out += val; - out += "\n"; - - out += "Time "; - - snprintf(val, sizeof(val), "%04d", rawsec / 864000); - out += val; - out += "."; - - snprintf(val, sizeof(val), "%02d", (rawsec / 36000) % 24); - out += val; - out += ":"; - - snprintf(val, sizeof(val), "%02d", (rawsec / 600) % 60); - out += val; - out += ":"; - - snprintf(val, sizeof(val), "%02d", (rawsec / 10) % 60); - out += val; - out += "\n"; - - return out; -} // toString - -/** - * Set the raw data for the beacon record. - * Example: - * uint8_t *payload = advertisedDevice.getPayload(); - * eddystoneTLM.setData(std::string((char*)payload+22, advertisedDevice.getPayloadLength() - 22)); - * Note: the offset 22 works for current implementation of example BLE_EddystoneTLM Beacon.ino, however - * the position is not static and it is programmers responsibility to align the data. - * Data frame: - * | Field || Len | Type | UUID | EddyStone TLM | - * | Offset || 0 | 1 | 2 | 4 | - * | Len || 1 B | 1 B | 2 B | 14 B | - * | Data || ?? | ?? | 0xAA | 0xFE | ??? | - * - * EddyStone TLM frame: - * | Field || Type | Version | Batt mV | Beacon temp | Cnt since boot | Time since boot | - * | Offset || 0 | 1 | 2 | 4 | 6 | 10 | - * | Len || 1 B | 1 B | 2 B | 2 B | 4 B | 4 B | - * | Data || 0x20 | ?? | ?? | ?? | ?? | ?? | | | | | | | | | - */ -void BLEEddystoneTLM::setData(std::string data) { - if (data.length() != sizeof(m_eddystoneData)) { - log_e("Unable to set the data ... length passed in was %d and expected %d", data.length(), sizeof(m_eddystoneData)); - return; - } - memcpy(&m_eddystoneData, data.data(), data.length()); -} // setData - -void BLEEddystoneTLM::setUUID(BLEUUID l_uuid) { - beaconUUID = l_uuid; -} // setUUID - -void BLEEddystoneTLM::setVersion(uint8_t version) { - m_eddystoneData.version = version; -} // setVersion - -// Set voltage in ESP32 native Big endian and convert it to little endian used for BLE Frame -void BLEEddystoneTLM::setVolt(uint16_t volt) { - m_eddystoneData.volt = ENDIAN_CHANGE_U16(volt); -} // setVolt - -void BLEEddystoneTLM::setTemp(float temp) { - m_eddystoneData.temp = EDDYSTONE_TEMP_FLOAT_TO_U16(temp); -} // setTemp - -void BLEEddystoneTLM::setCount(uint32_t advCount) { - m_eddystoneData.advCount = advCount; -} // setCount - -void BLEEddystoneTLM::setTime(uint32_t tmil) { - m_eddystoneData.tmil = tmil; -} // setTime - -void BLEEddystoneTLM::_initHeadder(){ - BLEHeadder[0] = 0x02; // Len - BLEHeadder[1] = 0x01; // Type Flags - BLEHeadder[2] = 0x06; // GENERAL_DISC_MODE 0x02 | BR_EDR_NOT_SUPPORTED 0x04 - BLEHeadder[3] = 0x03; // Len - BLEHeadder[4] = 0x03; // Type 16-Bit UUID - BLEHeadder[5] = 0xAA; // Eddystone UUID 2 -> 0xFEAA LSB - BLEHeadder[6] = 0xFE; // Eddystone UUID 1 MSB - BLEHeadder[7] = 0x11; // Length of TLM Beacon Data is constant 17 B (not counting the length field itself) - BLEHeadder[8] = 0x16; // Type Service Data - BLEHeadder[9] = 0xAA; // Eddystone UUID 2 -> 0xFEAA LSB - BLEHeadder[10] = 0xFE; // Eddystone UUID 1 MSB - BLEHeadder[11] = 0x20; // Eddystone Frame Type - TLM -} - -#endif diff --git a/libraries/BLE/src/BLEEddystoneTLM.h b/libraries/BLE/src/BLEEddystoneTLM.h index 3981af4a4a9..17915a670da 100644 --- a/libraries/BLE/src/BLEEddystoneTLM.h +++ b/libraries/BLE/src/BLEEddystoneTLM.h @@ -1,8 +1,12 @@ /* - * BLEEddystoneTLM.cpp + * BLEEddystoneTLM.h * * Created on: Mar 12, 2018 * Author: pcbreflux + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on pcbreflux's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef _BLEEddystoneTLM_H_ @@ -10,6 +14,9 @@ #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED +#include "sdkconfig.h" +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + #include "BLEUUID.h" #include @@ -57,5 +64,6 @@ class BLEEddystoneTLM { } __attribute__((packed)) m_eddystoneData; }; // BLEEddystoneTLM +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* _BLEEddystoneTLM_H_ */ diff --git a/libraries/BLE/src/BLEEddystoneURL.cpp b/libraries/BLE/src/BLEEddystoneURL.cpp index ddee8af0b30..495671ca49b 100644 --- a/libraries/BLE/src/BLEEddystoneURL.cpp +++ b/libraries/BLE/src/BLEEddystoneURL.cpp @@ -3,14 +3,20 @@ * * Created on: Mar 12, 2018 * Author: pcbreflux + * * Upgraded on: Feb 20, 2023 * By: Tomas Pilny + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on pcbreflux's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) #include #include "esp32-hal-log.h" #include "BLEEddystoneURL.h" @@ -158,7 +164,11 @@ void BLEEddystoneURL::setData(String data) { } // setData void BLEEddystoneURL::setUUID(BLEUUID l_uuid) { +#if defined(CONFIG_BLUEDROID_ENABLED) uint16_t beaconUUID = l_uuid.getNative()->uuid.uuid16; +#elif defined(CONFIG_NIMBLE_ENABLED) + uint16_t beaconUUID = l_uuid.getNative()->u16.value; +#endif BLEHeadder[10] = beaconUUID >> 8; BLEHeadder[9] = beaconUUID & 0x00FF; } // setUUID diff --git a/libraries/BLE/src/BLEEddystoneURL.h b/libraries/BLE/src/BLEEddystoneURL.h index 92668eb6855..9ed89a23694 100644 --- a/libraries/BLE/src/BLEEddystoneURL.h +++ b/libraries/BLE/src/BLEEddystoneURL.h @@ -3,9 +3,13 @@ * * Created on: Mar 12, 2018 * Author: pcbreflux + * * Upgraded on: Feb 20, 2023 * By: Tomas Pilny * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on pcbreflux's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef _BLEEddystoneURL_H_ @@ -13,6 +17,9 @@ #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED +#include "sdkconfig.h" +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + #include "BLEUUID.h" #include #include "esp_bt.h" @@ -57,5 +64,6 @@ class BLEEddystoneURL { char BLEHeadder[12]; }; // BLEEddystoneURL +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* _BLEEddystoneURL_H_ */ diff --git a/libraries/BLE/src/BLEEddystoneURL.h.orig b/libraries/BLE/src/BLEEddystoneURL.h.orig deleted file mode 100644 index 57722d0b769..00000000000 --- a/libraries/BLE/src/BLEEddystoneURL.h.orig +++ /dev/null @@ -1,66 +0,0 @@ -/* - * BLEEddystoneURL.cpp - * - * Created on: Mar 12, 2018 - * Author: pcbreflux - * Upgraded on: Feb 17, 2023 - * By: Tomas Pilny - * - */ - -#ifndef _BLEEddystoneURL_H_ -#define _BLEEddystoneURL_H_ -#include "BLEUUID.h" -#include -#include - -#define EDDYSTONE_URL_FRAME_TYPE 0x10 - -extern String EDDYSTONE_URL_PREFIX[]; -extern String EDDYSTONE_URL_SUFFIX[]; - -/** - * @brief Representation of a beacon. - * See: - * * https://github.com/google/eddystone - */ -class BLEEddystoneURL { -public: - BLEEddystoneURL(); - BLEEddystoneURL(BLEAdvertisedDevice *advertisedDevice); - std::string getData(); - String getFrame(); - BLEUUID getUUID(); - int8_t getPower(); - std::string getURL(); - String getPrefix(); - String getSuffix(); - std::string getDecodedURL(); - void setData(std::string data); - void setUUID(BLEUUID l_uuid); - void setPower(int8_t advertisedTxPower); - void setURL(std::string url); - int setSmartURL(String url); - -private: -<<<<<<< Updated upstream - uint16_t beaconUUID; - uint8_t lengthURL; - struct { - uint8_t frameType; - int8_t advertisedTxPower; - uint8_t url[18]; // 18 bytes: 1 byte for URL scheme + up to 17 bytes of URL - } __attribute__((packed)) m_eddystoneData; - -======= - uint8_t lengthURL; // Describes length of URL part including prefix and suffix - max 18 B (excluding TX power, frame type and preceding header) - struct { - int8_t advertisedTxPower; - uint8_t url[18]; // Byte [0] is for prefix. Last byte **can** contain suffix - } __attribute__((packed)) m_eddystoneData; - void _initHeadder(); - char BLEHeadder[12]; ->>>>>>> Stashed changes -}; // BLEEddystoneURL - -#endif /* _BLEEddystoneURL_H_ */ diff --git a/libraries/BLE/src/BLEExceptions.cpp b/libraries/BLE/src/BLEExceptions.cpp index 4e6c31fca22..b88ea337493 100644 --- a/libraries/BLE/src/BLEExceptions.cpp +++ b/libraries/BLE/src/BLEExceptions.cpp @@ -5,4 +5,9 @@ * Author: kolban */ +#include "soc/soc_caps.h" +#if SOC_BLE_SUPPORTED + //#include "BLEExceptions.h" + +#endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEHIDDevice.cpp b/libraries/BLE/src/BLEHIDDevice.cpp index 0873aa1049f..a255879e2d8 100644 --- a/libraries/BLE/src/BLEHIDDevice.cpp +++ b/libraries/BLE/src/BLEHIDDevice.cpp @@ -3,15 +3,37 @@ * * Created on: Jan 03, 2018 * Author: chegewara + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include "BLEHIDDevice.h" #include "BLE2904.h" +#include "BLEDescriptor.h" + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#ifdef CONFIG_NIMBLE_ENABLED +#include +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ BLEHIDDevice::BLEHIDDevice(BLEServer *server) { /* @@ -45,10 +67,12 @@ BLEHIDDevice::BLEHIDDevice(BLEServer *server) { m_batteryLevelCharacteristic = m_batteryService->createCharacteristic((uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); m_batteryLevelCharacteristic->addDescriptor(batteryLevelDescriptor); +#if CONFIG_BLUEDROID_ENABLED BLE2902 *batLevelIndicator = new BLE2902(); // Battery Level Notification is ON by default, making it work always on BLE Pairing and Bonding batLevelIndicator->setNotifications(true); m_batteryLevelCharacteristic->addDescriptor(batLevelIndicator); +#endif /* * This value is setup here because its default value in most usage cases, its very rare to use boot mode @@ -117,16 +141,19 @@ BLECharacteristic *BLEHIDDevice::inputReport(uint8_t reportID) { BLECharacteristic *inputReportCharacteristic = m_hidService->createCharacteristic((uint16_t)0x2a4d, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); BLEDescriptor *inputReportDescriptor = new BLEDescriptor(BLEUUID((uint16_t)0x2908)); - BLE2902 *p2902 = new BLE2902(); inputReportCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED); inputReportDescriptor->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED); - p2902->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED); uint8_t desc1_val[] = {reportID, 0x01}; inputReportDescriptor->setValue((uint8_t *)desc1_val, 2); - inputReportCharacteristic->addDescriptor(p2902); inputReportCharacteristic->addDescriptor(inputReportDescriptor); +#if CONFIG_BLUEDROID_ENABLED + BLE2902 *p2902 = new BLE2902(); + p2902->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED); + inputReportCharacteristic->addDescriptor(p2902); +#endif + return inputReportCharacteristic; } @@ -175,7 +202,9 @@ BLECharacteristic *BLEHIDDevice::featureReport(uint8_t reportID) { */ BLECharacteristic *BLEHIDDevice::bootInput() { BLECharacteristic *bootInputCharacteristic = m_hidService->createCharacteristic((uint16_t)0x2a22, BLECharacteristic::PROPERTY_NOTIFY); +#if CONFIG_BLUEDROID_ENABLED bootInputCharacteristic->addDescriptor(new BLE2902()); +#endif return bootInputCharacteristic; } @@ -252,5 +281,5 @@ BLEService *BLEHIDDevice::batteryService() { return m_batteryService; } -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEHIDDevice.h b/libraries/BLE/src/BLEHIDDevice.h index a92a23c21d5..9dde9452c12 100644 --- a/libraries/BLE/src/BLEHIDDevice.h +++ b/libraries/BLE/src/BLEHIDDevice.h @@ -3,6 +3,10 @@ * * Created on: Jan 03, 2018 * Author: chegewara + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef _BLEHIDDEVICE_H_ @@ -12,7 +16,7 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) #include "BLECharacteristic.h" #include "BLEService.h" @@ -75,6 +79,6 @@ class BLEHIDDevice { BLECharacteristic *m_batteryLevelCharacteristic; //0x2a19 }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* _BLEHIDDEVICE_H_ */ diff --git a/libraries/BLE/src/BLERemoteCharacteristic.cpp b/libraries/BLE/src/BLERemoteCharacteristic.cpp index 60d5108c1fc..aec9500d6f3 100644 --- a/libraries/BLE/src/BLERemoteCharacteristic.cpp +++ b/libraries/BLE/src/BLERemoteCharacteristic.cpp @@ -3,46 +3,55 @@ * * Created on: Jul 8, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ -#include "BLERemoteCharacteristic.h" - #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ -#include #include #include +#include "WString.h" //#include "BLEExceptions.h" #include "BLEUtils.h" #include "GeneralUtils.h" +#include "BLERemoteCharacteristic.h" #include "BLERemoteDescriptor.h" #include "esp32-hal-log.h" -/** - * @brief Constructor. - * @param [in] handle The BLE server side handle of this characteristic. - * @param [in] uuid The UUID of this characteristic. - * @param [in] charProp The properties of this characteristic. - * @param [in] pRemoteService A reference to the remote service to which this remote characteristic pertains. - */ -BLERemoteCharacteristic::BLERemoteCharacteristic(uint16_t handle, BLEUUID uuid, esp_gatt_char_prop_t charProp, BLERemoteService *pRemoteService) { - log_v(">> BLERemoteCharacteristic: handle: %d 0x%d, uuid: %s", handle, handle, uuid.toString().c_str()); - m_handle = handle; - m_uuid = uuid; - m_charProp = charProp; - m_pRemoteService = pRemoteService; - m_notifyCallback = nullptr; - m_rawData = nullptr; - m_auth = ESP_GATT_AUTH_REQ_NONE; +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ - retrieveDescriptors(); // Get the descriptors for this characteristic - log_v("<< BLERemoteCharacteristic"); -} // BLERemoteCharacteristic +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#include +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ /** *@brief Destructor. @@ -100,211 +109,6 @@ bool BLERemoteCharacteristic::canWriteNoResponse() { return (m_charProp & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) != 0; } // canWriteNoResponse -/* -static bool compareSrvcId(esp_gatt_srvc_id_t id1, esp_gatt_srvc_id_t id2) { - if (id1.id.inst_id != id2.id.inst_id) { - return false; - } - if (!BLEUUID(id1.id.uuid).equals(BLEUUID(id2.id.uuid))) { - return false; - } - return true; -} // compareSrvcId -*/ - -/* -static bool compareGattId(esp_gatt_id_t id1, esp_gatt_id_t id2) { - if (id1.inst_id != id2.inst_id) { - return false; - } - if (!BLEUUID(id1.uuid).equals(BLEUUID(id2.uuid))) { - return false; - } - return true; -} // compareCharId -*/ - -/** - * @brief Handle GATT Client events. - * When an event arrives for a GATT client we give this characteristic the opportunity to - * take a look at it to see if there is interest in it. - * @param [in] event The type of event. - * @param [in] gattc_if The interface on which the event was received. - * @param [in] evtParam Payload data for the event. - * @returns N/A - */ -void BLERemoteCharacteristic::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam) { - switch (event) { - // ESP_GATTC_NOTIFY_EVT - // - // notify - // - uint16_t conn_id - The connection identifier of the server. - // - esp_bd_addr_t remote_bda - The device address of the BLE server. - // - uint16_t handle - The handle of the characteristic for which the event is being received. - // - uint16_t value_len - The length of the received data. - // - uint8_t* value - The received data. - // - bool is_notify - True if this is a notify, false if it is an indicate. - // - // We have received a notification event which means that the server wishes us to know about a notification - // piece of data. What we must now do is find the characteristic with the associated handle and then - // invoke its notification callback (if it has one). - case ESP_GATTC_NOTIFY_EVT: - { - if (evtParam->notify.handle != getHandle()) { - break; - } - if (m_notifyCallback != nullptr) { - log_d("Invoking callback for notification on characteristic %s", toString().c_str()); - m_notifyCallback(this, evtParam->notify.value, evtParam->notify.value_len, evtParam->notify.is_notify); - } // End we have a callback function ... - break; - } // ESP_GATTC_NOTIFY_EVT - - // ESP_GATTC_READ_CHAR_EVT - // This event indicates that the server has responded to the read request. - // - // read: - // - esp_gatt_status_t status - // - uint16_t conn_id - // - uint16_t handle - // - uint8_t* value - // - uint16_t value_len - case ESP_GATTC_READ_CHAR_EVT: - { - // If this event is not for us, then nothing further to do. - if (evtParam->read.handle != getHandle()) { - break; - } - - // At this point, we have determined that the event is for us, so now we save the value - // and unlock the semaphore to ensure that the requester of the data can continue. - if (evtParam->read.status == ESP_GATT_OK) { - m_value = String((char *)evtParam->read.value, evtParam->read.value_len); - if (m_rawData != nullptr) { - free(m_rawData); - } - m_rawData = (uint8_t *)calloc(evtParam->read.value_len, sizeof(uint8_t)); - memcpy(m_rawData, evtParam->read.value, evtParam->read.value_len); - } else { - m_value = ""; - } - - m_semaphoreReadCharEvt.give(); - break; - } // ESP_GATTC_READ_CHAR_EVT - - // ESP_GATTC_REG_FOR_NOTIFY_EVT - // - // reg_for_notify: - // - esp_gatt_status_t status - // - uint16_t handle - case ESP_GATTC_REG_FOR_NOTIFY_EVT: - { - // If the request is not for this BLERemoteCharacteristic then move on to the next. - if (evtParam->reg_for_notify.handle != getHandle()) { - break; - } - - // We have processed the notify registration and can unlock the semaphore. - m_semaphoreRegForNotifyEvt.give(); - break; - } // ESP_GATTC_REG_FOR_NOTIFY_EVT - - // ESP_GATTC_UNREG_FOR_NOTIFY_EVT - // - // unreg_for_notify: - // - esp_gatt_status_t status - // - uint16_t handle - case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: - { - if (evtParam->unreg_for_notify.handle != getHandle()) { - break; - } - // We have processed the notify un-registration and can unlock the semaphore. - m_semaphoreRegForNotifyEvt.give(); - break; - } // ESP_GATTC_UNREG_FOR_NOTIFY_EVT: - - // ESP_GATTC_WRITE_CHAR_EVT - // - // write: - // - esp_gatt_status_t status - // - uint16_t conn_id - // - uint16_t handle - case ESP_GATTC_WRITE_CHAR_EVT: - { - // Determine if this event is for us and, if not, pass onwards. - if (evtParam->write.handle != getHandle()) { - break; - } - - // There is nothing further we need to do here. This is merely an indication - // that the write has completed and we can unlock the caller. - m_semaphoreWriteCharEvt.give(); - break; - } // ESP_GATTC_WRITE_CHAR_EVT - - case ESP_GATTC_READ_DESCR_EVT: - case ESP_GATTC_WRITE_DESCR_EVT: - for (auto &myPair : m_descriptorMap) { - myPair.second->gattClientEventHandler(event, gattc_if, evtParam); - } - break; - - case ESP_GATTC_DISCONNECT_EVT: - // Cleanup semaphores to avoid deadlocks. - m_semaphoreReadCharEvt.give(1); - m_semaphoreWriteCharEvt.give(1); - break; - - default: break; - } // End switch -}; // gattClientEventHandler - -/** - * @brief Populate the descriptors (if any) for this characteristic. - */ -void BLERemoteCharacteristic::retrieveDescriptors() { - log_v(">> retrieveDescriptors() for characteristic: %s", getUUID().toString().c_str()); - - removeDescriptors(); // Remove any existing descriptors. - - // Loop over each of the descriptors within the service associated with this characteristic. - // For each descriptor we find, create a BLERemoteDescriptor instance. - uint16_t offset = 0; - esp_gattc_descr_elem_t result; - while (true) { - uint16_t count = 10; - esp_gatt_status_t status = ::esp_ble_gattc_get_all_descr( - getRemoteService()->getClient()->getGattcIf(), getRemoteService()->getClient()->getConnId(), getHandle(), &result, &count, offset - ); - - if (status == ESP_GATT_INVALID_OFFSET) { // We have reached the end of the entries. - break; - } - - if (status != ESP_GATT_OK) { - log_e("esp_ble_gattc_get_all_descr: %s", BLEUtils::gattStatusToString(status).c_str()); - break; - } - - if (count == 0) { - break; - } - - log_d("Found a descriptor: Handle: %d, UUID: %s", result.handle, BLEUUID(result.uuid).toString().c_str()); - - // We now have a new characteristic ... let us add that to our set of known characteristics - BLERemoteDescriptor *pNewRemoteDescriptor = new BLERemoteDescriptor(result.handle, BLEUUID(result.uuid), this); - - m_descriptorMap.insert(std::pair(pNewRemoteDescriptor->getUUID().toString().c_str(), pNewRemoteDescriptor)); - - offset++; - } // while true - //m_haveCharacteristics = true; // Remember that we have received the characteristics. - log_v("<< retrieveDescriptors(): Found %d descriptors.", offset); -} // getDescriptors - /** * @brief Retrieve the map of descriptors keyed by UUID. */ @@ -404,44 +208,6 @@ float BLERemoteCharacteristic::readFloat() { return 0.0; } // readFloat -/** - * @brief Read the value of the remote characteristic. - * @return The value of the remote characteristic. - */ -String BLERemoteCharacteristic::readValue() { - log_v(">> readValue(): uuid: %s, handle: %d 0x%.2x", getUUID().toString().c_str(), getHandle(), getHandle()); - - // Check to see that we are connected. - if (!getRemoteService()->getClient()->isConnected()) { - log_e("Disconnected"); - return String(); - } - - m_semaphoreReadCharEvt.take("readValue"); - - // Ask the BLE subsystem to retrieve the value for the remote hosted characteristic. - // This is an asynchronous request which means that we must block waiting for the response - // to become available. - esp_err_t errRc = ::esp_ble_gattc_read_char( - m_pRemoteService->getClient()->getGattcIf(), - m_pRemoteService->getClient()->getConnId(), // The connection ID to the BLE server - getHandle(), // The handle of this characteristic - m_auth - ); // Security - - if (errRc != ESP_OK) { - log_e("esp_ble_gattc_read_char: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return ""; - } - - // Block waiting for the event that indicates that the read has completed. When it has, the String found - // in m_value will contain our data. - m_semaphoreReadCharEvt.wait("readValue"); - - log_v("<< readValue(): length: %d", m_value.length()); - return m_value; -} // readValue - /** * @brief Register for notifications. * @param [in] notifyCallback A callback to be invoked for a notification. If NULL is provided then we are @@ -451,13 +217,14 @@ String BLERemoteCharacteristic::readValue() { void BLERemoteCharacteristic::registerForNotify(notify_callback notifyCallback, bool notifications, bool descriptorRequiresRegistration) { log_v(">> registerForNotify(): %s", toString().c_str()); +#if defined(CONFIG_BLUEDROID_ENABLED) m_notifyCallback = notifyCallback; // Save the notification callback. m_semaphoreRegForNotifyEvt.take("registerForNotify"); if (notifyCallback != nullptr) { // If we have a callback function, then this is a registration. esp_err_t errRc = ::esp_ble_gattc_register_for_notify( - m_pRemoteService->getClient()->getGattcIf(), *m_pRemoteService->getClient()->getPeerAddress().getNative(), getHandle() + m_pRemoteService->getClient()->getGattcIf(), m_pRemoteService->getClient()->getPeerAddress().getNative(), getHandle() ); if (errRc != ESP_OK) { @@ -475,7 +242,7 @@ void BLERemoteCharacteristic::registerForNotify(notify_callback notifyCallback, } // End Register else { // If we weren't passed a callback function, then this is an unregistration. esp_err_t errRc = ::esp_ble_gattc_unregister_for_notify( - m_pRemoteService->getClient()->getGattcIf(), *m_pRemoteService->getClient()->getPeerAddress().getNative(), getHandle() + m_pRemoteService->getClient()->getGattcIf(), m_pRemoteService->getClient()->getPeerAddress().getNative(), getHandle() ); if (errRc != ESP_OK) { @@ -491,8 +258,22 @@ void BLERemoteCharacteristic::registerForNotify(notify_callback notifyCallback, m_semaphoreRegForNotifyEvt.wait("registerForNotify"); - log_v("<< registerForNotify()"); -} // registerForNotify +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + bool success; + if (notifyCallback != nullptr) { + success = subscribe(notifications, notifyCallback, descriptorRequiresRegistration); + } else { + success = unsubscribe(descriptorRequiresRegistration); + } + + if (!success) { + log_e("Failed to subscribe/unsubscribe for notify"); + } +#endif + log_v("<< registerForNotify()"); +} // registerForNotify /** * @brief Delete the descriptors in the descriptor map. @@ -532,8 +313,8 @@ String BLERemoteCharacteristic::toString() { * @param [in] response Do we expect a response? * @return N/A. */ -void BLERemoteCharacteristic::writeValue(String newValue, bool response) { - writeValue((uint8_t *)newValue.c_str(), newValue.length(), response); +bool BLERemoteCharacteristic::writeValue(String newValue, bool response) { + return writeValue((uint8_t *)newValue.c_str(), newValue.length(), response); } // writeValue /** @@ -544,58 +325,678 @@ void BLERemoteCharacteristic::writeValue(String newValue, bool response) { * @param [in] response Whether we require a response from the write. * @return N/A. */ -void BLERemoteCharacteristic::writeValue(uint8_t newValue, bool response) { - writeValue(&newValue, 1, response); +bool BLERemoteCharacteristic::writeValue(uint8_t newValue, bool response) { + return writeValue(&newValue, 1, response); } // writeValue +/** + * @brief Read raw data from remote characteristic as hex bytes + * @return return pointer data read + */ +uint8_t *BLERemoteCharacteristic::readRawData() { + return m_rawData; +} + +/** + * @brief Set authentication request type for characteristic + * @param [in] auth Authentication request type. + */ +void BLERemoteCharacteristic::setAuth(uint8_t auth) { + m_auth = auth; +} + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +/** + * @brief Constructor. + * @param [in] handle The BLE server side handle of this characteristic. + * @param [in] uuid The UUID of this characteristic. + * @param [in] charProp The properties of this characteristic. + * @param [in] pRemoteService A reference to the remote service to which this remote characteristic pertains. + */ +BLERemoteCharacteristic::BLERemoteCharacteristic(uint16_t handle, BLEUUID uuid, esp_gatt_char_prop_t charProp, BLERemoteService *pRemoteService) { + log_v(">> BLERemoteCharacteristic: handle: %d 0x%d, uuid: %s", handle, handle, uuid.toString().c_str()); + m_handle = handle; + m_uuid = uuid; + m_charProp = charProp; + m_pRemoteService = pRemoteService; + m_notifyCallback = nullptr; + m_rawData = nullptr; + m_auth = ESP_GATT_AUTH_REQ_NONE; + + retrieveDescriptors(); // Get the descriptors for this characteristic + log_v("<< BLERemoteCharacteristic"); +} // BLERemoteCharacteristic + +/** + * @brief Handle GATT Client events. + * When an event arrives for a GATT client we give this characteristic the opportunity to + * take a look at it to see if there is interest in it. + * @param [in] event The type of event. + * @param [in] gattc_if The interface on which the event was received. + * @param [in] evtParam Payload data for the event. + * @returns N/A + */ +void BLERemoteCharacteristic::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam) { + switch (event) { + // ESP_GATTC_NOTIFY_EVT + // + // notify + // - uint16_t conn_id - The connection identifier of the server. + // - esp_bd_addr_t remote_bda - The device address of the BLE server. + // - uint16_t handle - The handle of the characteristic for which the event is being received. + // - uint16_t value_len - The length of the received data. + // - uint8_t* value - The received data. + // - bool is_notify - True if this is a notify, false if it is an indicate. + // + // We have received a notification event which means that the server wishes us to know about a notification + // piece of data. What we must now do is find the characteristic with the associated handle and then + // invoke its notification callback (if it has one). + case ESP_GATTC_NOTIFY_EVT: + { + if (evtParam->notify.handle != getHandle()) { + break; + } + if (m_notifyCallback != nullptr) { + log_d("Invoking callback for notification on characteristic %s", toString().c_str()); + m_notifyCallback(this, evtParam->notify.value, evtParam->notify.value_len, evtParam->notify.is_notify); + } // End we have a callback function ... + break; + } // ESP_GATTC_NOTIFY_EVT + + // ESP_GATTC_READ_CHAR_EVT + // This event indicates that the server has responded to the read request. + // + // read: + // - esp_gatt_status_t status + // - uint16_t conn_id + // - uint16_t handle + // - uint8_t* value + // - uint16_t value_len + case ESP_GATTC_READ_CHAR_EVT: + { + // If this event is not for us, then nothing further to do. + if (evtParam->read.handle != getHandle()) { + break; + } + + // At this point, we have determined that the event is for us, so now we save the value + // and unlock the semaphore to ensure that the requester of the data can continue. + if (evtParam->read.status == ESP_GATT_OK) { + m_value = String((char *)evtParam->read.value, evtParam->read.value_len); + if (m_rawData != nullptr) { + free(m_rawData); + } + m_rawData = (uint8_t *)calloc(evtParam->read.value_len, sizeof(uint8_t)); + memcpy(m_rawData, evtParam->read.value, evtParam->read.value_len); + } else { + m_value = ""; + } + + m_semaphoreReadCharEvt.give(); + break; + } // ESP_GATTC_READ_CHAR_EVT + + // ESP_GATTC_REG_FOR_NOTIFY_EVT + // + // reg_for_notify: + // - esp_gatt_status_t status + // - uint16_t handle + case ESP_GATTC_REG_FOR_NOTIFY_EVT: + { + // If the request is not for this BLERemoteCharacteristic then move on to the next. + if (evtParam->reg_for_notify.handle != getHandle()) { + break; + } + + // We have processed the notify registration and can unlock the semaphore. + m_semaphoreRegForNotifyEvt.give(); + break; + } // ESP_GATTC_REG_FOR_NOTIFY_EVT + + // ESP_GATTC_UNREG_FOR_NOTIFY_EVT + // + // unreg_for_notify: + // - esp_gatt_status_t status + // - uint16_t handle + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: + { + if (evtParam->unreg_for_notify.handle != getHandle()) { + break; + } + // We have processed the notify un-registration and can unlock the semaphore. + m_semaphoreRegForNotifyEvt.give(); + break; + } // ESP_GATTC_UNREG_FOR_NOTIFY_EVT: + + // ESP_GATTC_WRITE_CHAR_EVT + // + // write: + // - esp_gatt_status_t status + // - uint16_t conn_id + // - uint16_t handle + case ESP_GATTC_WRITE_CHAR_EVT: + { + // Determine if this event is for us and, if not, pass onwards. + if (evtParam->write.handle != getHandle()) { + break; + } + + // There is nothing further we need to do here. This is merely an indication + // that the write has completed and we can unlock the caller. + m_semaphoreWriteCharEvt.give(); + break; + } // ESP_GATTC_WRITE_CHAR_EVT + + case ESP_GATTC_READ_DESCR_EVT: + case ESP_GATTC_WRITE_DESCR_EVT: + for (auto &myPair : m_descriptorMap) { + myPair.second->gattClientEventHandler(event, gattc_if, evtParam); + } + break; + + case ESP_GATTC_DISCONNECT_EVT: + // Cleanup semaphores to avoid deadlocks. + m_semaphoreReadCharEvt.give(1); + m_semaphoreWriteCharEvt.give(1); + break; + + default: break; + } // End switch +}; // gattClientEventHandler + +/** + * @brief Populate the descriptors (if any) for this characteristic. + */ +void BLERemoteCharacteristic::retrieveDescriptors() { + log_v(">> retrieveDescriptors() for characteristic: %s", getUUID().toString().c_str()); + + removeDescriptors(); // Remove any existing descriptors. + + // Loop over each of the descriptors within the service associated with this characteristic. + // For each descriptor we find, create a BLERemoteDescriptor instance. + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + while (true) { + uint16_t count = 10; + esp_gatt_status_t status = ::esp_ble_gattc_get_all_descr( + getRemoteService()->getClient()->getGattcIf(), getRemoteService()->getClient()->getConnId(), getHandle(), &result, &count, offset + ); + + if (status == ESP_GATT_INVALID_OFFSET) { // We have reached the end of the entries. + break; + } + + if (status != ESP_GATT_OK) { + log_e("esp_ble_gattc_get_all_descr: %s", BLEUtils::gattStatusToString(status).c_str()); + break; + } + + if (count == 0) { + break; + } + + log_d("Found a descriptor: Handle: %d, UUID: %s", result.handle, BLEUUID(result.uuid).toString().c_str()); + + // We now have a new characteristic ... let us add that to our set of known characteristics + BLERemoteDescriptor *pNewRemoteDescriptor = new BLERemoteDescriptor(result.handle, BLEUUID(result.uuid), this); + + m_descriptorMap.insert(std::pair(pNewRemoteDescriptor->getUUID().toString().c_str(), pNewRemoteDescriptor)); + + offset++; + } // while true + //m_haveCharacteristics = true; // Remember that we have received the characteristics. + log_v("<< retrieveDescriptors(): Found %d descriptors.", offset); +} // getDescriptors + +/** + * @brief Read the value of the remote characteristic. + * @return The value of the remote characteristic. + */ +String BLERemoteCharacteristic::readValue() { + log_v(">> readValue(): uuid: %s, handle: %d 0x%.2x", getUUID().toString().c_str(), getHandle(), getHandle()); + + // Check to see that we are connected. + if (!getRemoteService()->getClient()->isConnected()) { + log_e("Disconnected"); + return String(); + } + + m_semaphoreReadCharEvt.take("readValue"); + + // Ask the BLE subsystem to retrieve the value for the remote hosted characteristic. + // This is an asynchronous request which means that we must block waiting for the response + // to become available. + esp_err_t errRc = ::esp_ble_gattc_read_char( + m_pRemoteService->getClient()->getGattcIf(), + m_pRemoteService->getClient()->getConnId(), // The connection ID to the BLE server + getHandle(), // The handle of this characteristic + (esp_gatt_auth_req_t)m_auth + ); // Security + + if (errRc != ESP_OK) { + log_e("esp_ble_gattc_read_char: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return ""; + } + + // Block waiting for the event that indicates that the read has completed. When it has, the String found + // in m_value will contain our data. + m_semaphoreReadCharEvt.wait("readValue"); + + log_v("<< readValue(): length: %d", m_value.length()); + return m_value; +} // readValue + /** * @brief Write the new value for the characteristic from a data buffer. * @param [in] data A pointer to a data buffer. * @param [in] length The length of the data in the data buffer. * @param [in] response Whether we require a response from the write. + * @return True if successful */ -void BLERemoteCharacteristic::writeValue(uint8_t *data, size_t length, bool response) { +bool BLERemoteCharacteristic::writeValue(uint8_t *data, size_t length, bool response) { // writeValue(String((char*)data, length), response); log_v(">> writeValue(), length: %d", length); // Check to see that we are connected. if (!getRemoteService()->getClient()->isConnected()) { log_e("Disconnected"); - return; + return false; } m_semaphoreWriteCharEvt.take("writeValue"); // Invoke the ESP-IDF API to perform the write. esp_err_t errRc = ::esp_ble_gattc_write_char( m_pRemoteService->getClient()->getGattcIf(), m_pRemoteService->getClient()->getConnId(), getHandle(), length, data, - response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, m_auth + response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, (esp_gatt_auth_req_t)m_auth ); if (errRc != ESP_OK) { log_e("esp_ble_gattc_write_char: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return; + return false; } m_semaphoreWriteCharEvt.wait("writeValue"); log_v("<< writeValue"); + return true; } // writeValue +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + /** - * @brief Read raw data from remote characteristic as hex bytes - * @return return pointer data read + * @brief Constructor. + * @param [in] reference to the service this characteristic belongs to. + * @param [in] ble_gatt_chr struct defined as: + * struct ble_gatt_chr { + * uint16_t def_handle; + * uint16_t val_handle; + * uint8_t properties; + * ble_uuid_any_t uuid; + * }; */ -uint8_t *BLERemoteCharacteristic::readRawData() { - return m_rawData; +BLERemoteCharacteristic::BLERemoteCharacteristic(BLERemoteService *pRemoteService, const struct ble_gatt_chr *chr) { + log_v(">> BLERemoteCharacteristic()"); + switch (chr->uuid.u.type) { + case BLE_UUID_TYPE_16: m_uuid = BLEUUID(chr->uuid.u16.value); break; + case BLE_UUID_TYPE_32: m_uuid = BLEUUID(chr->uuid.u32.value); break; + case BLE_UUID_TYPE_128: m_uuid = BLEUUID(const_cast(&chr->uuid.u128)); break; + default: break; + } + + m_handle = chr->val_handle; + m_defHandle = chr->def_handle; + m_endHandle = 0; + m_charProp = chr->properties; + m_pRemoteService = pRemoteService; + m_notifyCallback = nullptr; + m_rawData = nullptr; + m_auth = 0; + + retrieveDescriptors(); // Get the descriptors for this characteristic + + log_v("<< BLERemoteCharacteristic(): %s", m_uuid.toString().c_str()); +} // BLERemoteCharacteristic + +/** + * @brief Callback used by the API when a descriptor is discovered or search complete. + */ +int BLERemoteCharacteristic::descriptorDiscCB( + uint16_t conn_handle, const struct ble_gatt_error *error, uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc, void *arg +) { + int rc = error->status; + log_d("Descriptor Discovered >> status: %d handle: %d", rc, (rc == 0) ? dsc->handle : -1); + + desc_filter_t *filter = (desc_filter_t *)arg; + const BLEUUID *uuid_filter = filter->uuid; + BLETaskData *pTaskData = (BLETaskData *)filter->task_data; + BLERemoteCharacteristic *characteristic = (BLERemoteCharacteristic *)pTaskData->m_pInstance; + + if (characteristic->getRemoteService()->getClient()->getConnId() != conn_handle) { + return 0; + } + + if (rc == 0 && characteristic->getHandle() == chr_val_handle && (!uuid_filter || ble_uuid_cmp(&uuid_filter->getNative()->u, &dsc->uuid.u) == 0)) { + BLERemoteDescriptor *pNewRemoteDescriptor = new BLERemoteDescriptor(characteristic, dsc); + characteristic->m_descriptorMap.insert( + std::pair(pNewRemoteDescriptor->getUUID().toString().c_str(), pNewRemoteDescriptor) + ); + rc = !!uuid_filter * BLE_HS_EDONE; + } + + if (rc != 0) { + BLEUtils::taskRelease(*pTaskData, rc); + log_d("<< Descriptor Discovery"); + } + + return rc; } /** - * @brief Set authentication request type for characteristic - * @param [in] auth Authentication request type. + * @brief Callback for characteristic read operation. + * @return success == 0 or error code. */ -void BLERemoteCharacteristic::setAuth(esp_gatt_auth_req_t auth) { - m_auth = auth; +int BLERemoteCharacteristic::onReadCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg) { + BLETaskData *pTaskData = static_cast(arg); + BLERemoteCharacteristic *characteristic = static_cast(pTaskData->m_pInstance); + + if (error->status == BLE_HS_ENOTCONN) { + log_e("<< Characteristic Read; Not connected"); + BLEUtils::taskRelease(*pTaskData, error->status); + return error->status; + } + + if (characteristic->getRemoteService()->getClient()->getConnId() != conn_handle) { + return 0; + } + + int rc = error->status; + log_i("Read complete; status=%d conn_handle=%d", rc, conn_handle); + + String *strBuf = (String *)pTaskData->m_pBuf; + + if (rc == 0) { + if (attr) { + uint32_t data_len = OS_MBUF_PKTLEN(attr->om); + if (((*strBuf).length() + data_len) > BLE_ATT_ATTR_MAX_LEN) { + rc = BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } else { + log_i("Got %d bytes", data_len); + (*strBuf) += String((char *)attr->om->om_data, data_len); + return 0; + } + } + } + + BLEUtils::taskRelease(*pTaskData, rc); + return rc; } -#endif /* CONFIG_BLUEDROID_ENABLED */ +/** + * @brief Callback for characteristic write operation. + * @return success == 0 or error code. + */ +int BLERemoteCharacteristic::onWriteCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg) { + BLETaskData *pTaskData = static_cast(arg); + BLERemoteCharacteristic *characteristic = static_cast(pTaskData->m_pInstance); + + if (error->status == BLE_HS_ENOTCONN) { + log_e("<< Characteristic Write; Not connected"); + BLEUtils::taskRelease(*pTaskData, error->status); + return error->status; + } + + if (characteristic->getRemoteService()->getClient()->getConnId() != conn_handle) { + return 0; + } + + log_i("Write complete; status=%d conn_handle=%d", error->status, conn_handle); + BLEUtils::taskRelease(*pTaskData, error->status); + return 0; +} + +/** + * @brief Populate the descriptors (if any) for this characteristic. + * @param [in] the end handle of the characteristic, or the service, whichever comes first. + */ +bool BLERemoteCharacteristic::retrieveDescriptors(const BLEUUID *uuid_filter) { + log_d(">> retrieveDescriptors() for characteristic: %s", getUUID().toString().c_str()); + + // If this is the last handle then there are no descriptors + if (m_handle == getRemoteService()->getEndHandle()) { + log_d("<< retrieveDescriptors(): No descriptors found"); + return true; + } + + BLETaskData taskData(const_cast(this)); + desc_filter_t filter = {uuid_filter, &taskData}; + int rc = 0; + + rc = ble_gattc_disc_all_dscs(getRemoteService()->getClient()->getConnId(), m_handle, m_endHandle, BLERemoteCharacteristic::descriptorDiscCB, &filter); + + if (rc != 0) { + log_e("ble_gattc_disc_all_dscs: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + return false; + } + + [[maybe_unused]] + size_t prevDscCount = m_descriptorMap.size(); + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + rc = ((BLETaskData *)filter.task_data)->m_flags; + + if (rc != BLE_HS_EDONE) { + log_e("<< retrieveDescriptors(): failed: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + return false; + } + + log_d("<< retrieveDescriptors(): Found %d descriptors.", m_descriptorMap.size() - prevDscCount); + return true; +} // retrieveDescriptors + +/** + * @brief Read the value of the remote characteristic. + * @return The value of the remote characteristic. + */ +String BLERemoteCharacteristic::readValue() { + log_d(">> readValue(): uuid: %s, handle: %d 0x%.2x", getUUID().toString().c_str(), getHandle(), getHandle()); + + BLEClient *pClient = getRemoteService()->getClient(); + String value{}; + + if (!pClient->isConnected()) { + log_e("Disconnected"); + return value; + } + + int rc = 0; + int retryCount = 1; + BLETaskData taskData(const_cast(this), 0, &value); + + do { + rc = ble_gattc_read_long(pClient->getConnId(), m_handle, 0, BLERemoteCharacteristic::onReadCB, &taskData); + if (rc != 0) { + goto exit; + } + + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + rc = taskData.m_flags; + + switch (rc) { + case 0: + case BLE_HS_EDONE: rc = 0; break; + // Characteristic is not long-readable, return with what we have. + case BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG): + log_i("Attribute not long"); + rc = ble_gattc_read(pClient->getConnId(), m_handle, BLERemoteCharacteristic::onReadCB, &taskData); + if (rc != 0) { + goto exit; + } + retryCount++; + break; + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHEN): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHOR): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_ENC): + if (retryCount && pClient->secureConnection()) { + break; + } + /* Else falls through. */ + default: goto exit; + } + } while (rc != 0 && retryCount--); + + m_semaphoreReadCharEvt.take("readValue"); + m_value = value; + m_rawData = (uint8_t *)calloc(value.length(), sizeof(uint8_t)); + for (size_t i = 0; i < value.length(); i++) { + m_rawData[i] = value[i]; + } + m_semaphoreReadCharEvt.give(); + +exit: + if (rc != 0) { + log_e("<< readValue failed rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); + } else { + log_d("<< readValue length: %d rc=%d", value.length(), rc); + } + + return value; +} // readValue + +/** + * @brief Write the new value for the characteristic from a data buffer. + * @param [in] data A pointer to a data buffer. + * @param [in] length The length of the data in the data buffer. + * @param [in] response Whether we require a response from the write. + * @return false if not connected or cant perform write for some reason. + */ +bool BLERemoteCharacteristic::writeValue(uint8_t *data, size_t length, bool response) { + log_d(">> writeValue(), length: %d", length); + + BLEClient *pClient = getRemoteService()->getClient(); + + if (!pClient->isConnected()) { + log_e("Disconnected"); + return false; + } + + int rc = 0; + int retryCount = 1; + uint16_t mtu = ble_att_mtu(pClient->getConnId()) - 3; + BLETaskData taskData(const_cast(this)); + + // Check if the data length is longer than we can write in one connection event. + // If so we must do a long write which requires a response. + if (length <= mtu && !response) { + rc = ble_gattc_write_no_rsp_flat(pClient->getConnId(), m_handle, data, length); + goto exit; + } + + do { + if (length > mtu) { + log_i("long write %d bytes", length); + os_mbuf *om = ble_hs_mbuf_from_flat(data, length); + rc = ble_gattc_write_long(pClient->getConnId(), m_handle, 0, om, BLERemoteCharacteristic::onWriteCB, &taskData); + } else { + rc = ble_gattc_write_flat(pClient->getConnId(), m_handle, data, length, BLERemoteCharacteristic::onWriteCB, &taskData); + } + if (rc != 0) { + goto exit; + } + + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + rc = taskData.m_flags; + + switch (rc) { + case 0: + case BLE_HS_EDONE: rc = 0; break; + case BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG): + log_e("Long write not supported by peer; Truncating length to %d", mtu); + retryCount++; + length = mtu; + break; + + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHEN): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHOR): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_ENC): + if (retryCount && pClient->secureConnection()) { + break; + } + /* Else falls through. */ + default: goto exit; + } + } while (rc != 0 && retryCount--); + +exit: + if (rc != 0) { + log_e("<< writeValue failed rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); + } else { + log_d("<< writeValue success. length: %d rc=%d", length, rc); + } + + return (rc == 0); +} // writeValue + +/** + * @brief Subscribe or unsubscribe for notifications or indications. + * @param [in] val 0x00 to unsubscribe, 0x01 for notifications, 0x02 for indications. + * @param [in] notifyCallback A callback to be invoked for a notification. + * @param [in] response If write response required set this to true. + * If NULL is provided then no callback is performed. + * @return false if writing to the descriptor failed. + */ +bool BLERemoteCharacteristic::setNotify(uint16_t val, notify_callback notifyCallback, bool response) { + log_v(">> setNotify(): %s, %02x", toString().c_str(), val); + + m_notifyCallback = notifyCallback; + + BLERemoteDescriptor *desc = getDescriptor(BLEUUID((uint16_t)0x2902)); + if (desc == nullptr) { + log_w("<< setNotify(): Callback set, CCCD not found"); + return true; + } + + log_d("<< setNotify()"); + + response = true; // Always write with response as per Bluetooth core specification. + return desc->writeValue((uint8_t *)&val, 2, response); +} // setNotify + +/** + * @brief Subscribe for notifications or indications. + * @param [in] notifications If true, subscribe for notifications, false subscribe for indications. + * @param [in] notifyCallback A callback to be invoked for a notification. + * @param [in] response If true, require a write response from the descriptor write operation. + * If NULL is provided then no callback is performed. + * @return false if writing to the descriptor failed. + */ +bool BLERemoteCharacteristic::subscribe(bool notifications, notify_callback notifyCallback, bool response) { + if (notifications) { + return setNotify(0x01, notifyCallback, response); + } else { + return setNotify(0x02, notifyCallback, response); + } +} // subscribe + +/** + * @brief Unsubscribe for notifications or indications. + * @param [in] response bool if true, require a write response from the descriptor write operation. + * @return false if writing to the descriptor failed. + */ +bool BLERemoteCharacteristic::unsubscribe(bool response) { + return setNotify(0x00, nullptr, response); +} // unsubscribe + +#endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLERemoteCharacteristic.h b/libraries/BLE/src/BLERemoteCharacteristic.h index dc63a3bc1a6..81ad7b2f4f5 100644 --- a/libraries/BLE/src/BLERemoteCharacteristic.h +++ b/libraries/BLE/src/BLERemoteCharacteristic.h @@ -3,6 +3,10 @@ * * Created on: Jul 8, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEREMOTECHARACTERISTIC_H_ @@ -11,27 +15,80 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) -#include +/*************************************************************************** + * Common includes * + ***************************************************************************/ +#include #include "BLERemoteService.h" #include "BLERemoteDescriptor.h" #include "BLEUUID.h" #include "RTOS.h" +#include "BLEUtils.h" + +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes and definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include + +#define ESP_GATT_MAX_ATTR_LEN BLE_ATT_ATTR_MAX_LEN +#define ESP_GATT_CHAR_PROP_BIT_READ BLE_GATT_CHR_PROP_READ +#define ESP_GATT_CHAR_PROP_BIT_WRITE BLE_GATT_CHR_PROP_WRITE +#define ESP_GATT_CHAR_PROP_BIT_WRITE_NR BLE_GATT_CHR_PROP_WRITE_NO_RSP +#define ESP_GATT_CHAR_PROP_BIT_BROADCAST BLE_GATT_CHR_PROP_BROADCAST +#define ESP_GATT_CHAR_PROP_BIT_NOTIFY BLE_GATT_CHR_PROP_NOTIFY +#define ESP_GATT_CHAR_PROP_BIT_INDICATE BLE_GATT_CHR_PROP_INDICATE + +#endif + +/*************************************************************************** + * Common types * + ***************************************************************************/ + +typedef std::function notify_callback; + +/*************************************************************************** + * NimBLE types * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +typedef struct { + const BLEUUID *uuid; + void *task_data; +} desc_filter_t; +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ class BLERemoteService; class BLERemoteDescriptor; -typedef std::function notify_callback; + /** * @brief A model of a remote %BLE characteristic. */ class BLERemoteCharacteristic { public: - ~BLERemoteCharacteristic(); + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ - // Public member functions + ~BLERemoteCharacteristic(); bool canBroadcast(); bool canIndicate(); bool canNotify(); @@ -49,29 +106,34 @@ class BLERemoteCharacteristic { uint32_t readUInt32(); float readFloat(); void registerForNotify(notify_callback _callback, bool notifications = true, bool descriptorRequiresRegistration = true); - void writeValue(uint8_t *data, size_t length, bool response = false); - void writeValue(String newValue, bool response = false); - void writeValue(uint8_t newValue, bool response = false); + bool writeValue(uint8_t *data, size_t length, bool response = false); + bool writeValue(String newValue, bool response = false); + bool writeValue(uint8_t newValue, bool response = false); String toString(); uint8_t *readRawData(); - void setAuth(esp_gatt_auth_req_t auth); + void setAuth(uint8_t auth); + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + bool subscribe(bool notifications = true, notify_callback notifyCallback = nullptr, bool response = true); + bool unsubscribe(bool response = true); +#endif private: - BLERemoteCharacteristic(uint16_t handle, BLEUUID uuid, esp_gatt_char_prop_t charProp, BLERemoteService *pRemoteService); friend class BLEClient; friend class BLERemoteService; friend class BLERemoteDescriptor; - // Private member functions - void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam); - - void removeDescriptors(); - void retrieveDescriptors(); + /*************************************************************************** + * Common private properties * + ***************************************************************************/ - // Private properties BLEUUID m_uuid; - esp_gatt_char_prop_t m_charProp; - esp_gatt_auth_req_t m_auth; + uint8_t m_charProp; + uint8_t m_auth; uint16_t m_handle; BLERemoteService *m_pRemoteService; FreeRTOS::Semaphore m_semaphoreReadCharEvt = FreeRTOS::Semaphore("ReadCharEvt"); @@ -83,8 +145,46 @@ class BLERemoteCharacteristic { // We maintain a map of descriptors owned by this characteristic keyed by a string representation of the UUID. std::map m_descriptorMap; + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + uint16_t m_defHandle; + uint16_t m_endHandle; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + void removeDescriptors(); + + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + BLERemoteCharacteristic(uint16_t handle, BLEUUID uuid, uint8_t charProp, BLERemoteService *pRemoteService); + void retrieveDescriptors(); + void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam); +#endif + + /*************************************************************************** + * NimBLE private declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + BLERemoteCharacteristic(BLERemoteService *pRemoteservice, const struct ble_gatt_chr *chr); + bool setNotify(uint16_t val, notify_callback notifyCallback = nullptr, bool response = true); + bool retrieveDescriptors(const BLEUUID *uuid_filter = nullptr); + static int onReadCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg); + static int onWriteCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg); + static int descriptorDiscCB(uint16_t conn_handle, const struct ble_gatt_error *error, uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc, void *arg); +#endif }; // BLERemoteCharacteristic -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEREMOTECHARACTERISTIC_H_ */ diff --git a/libraries/BLE/src/BLERemoteDescriptor.cpp b/libraries/BLE/src/BLERemoteDescriptor.cpp index b6d654cf9ec..a142fe11880 100644 --- a/libraries/BLE/src/BLERemoteDescriptor.cpp +++ b/libraries/BLE/src/BLERemoteDescriptor.cpp @@ -3,23 +3,48 @@ * * Created on: Jul 8, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + +#include "WString.h" #include #include "BLERemoteDescriptor.h" #include "GeneralUtils.h" #include "esp32-hal-log.h" -BLERemoteDescriptor::BLERemoteDescriptor(uint16_t handle, BLEUUID uuid, BLERemoteCharacteristic *pRemoteCharacteristic) { +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#include +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + +BLERemoteDescriptor::BLERemoteDescriptor(uint16_t handle, BLEUUID uuid, BLERemoteCharacteristic *pRemoteCharacteristic) { m_handle = handle; m_uuid = uuid; m_pRemoteCharacteristic = pRemoteCharacteristic; - m_auth = ESP_GATT_AUTH_REQ_NONE; + m_auth = 0; } /** @@ -46,6 +71,75 @@ BLEUUID BLERemoteDescriptor::getUUID() { return m_uuid; } // getUUID +uint8_t BLERemoteDescriptor::readUInt8() { + String value = readValue(); + if (value.length() >= 1) { + return (uint8_t)value[0]; + } + return 0; +} // readUInt8 + +uint16_t BLERemoteDescriptor::readUInt16() { + String value = readValue(); + if (value.length() >= 2) { + return *(uint16_t *)value.c_str(); + } + return 0; +} // readUInt16 + +uint32_t BLERemoteDescriptor::readUInt32() { + String value = readValue(); + if (value.length() >= 4) { + return *(uint32_t *)value.c_str(); + } + return 0; +} // readUInt32 + +/** + * @brief Return a string representation of this BLE Remote Descriptor. + * @return A string representation of this BLE Remote Descriptor. + */ +String BLERemoteDescriptor::toString() { + char val[6]; + snprintf(val, sizeof(val), "%d", getHandle()); + String res = "handle: "; + res += val; + res += ", uuid: " + getUUID().toString(); + return res; +} // toString + +/** + * @brief Write data represented as a string to the BLE Remote Descriptor. + * @param [in] newValue The data to send to the remote descriptor. + * @param [in] response True if we expect a response. + */ +bool BLERemoteDescriptor::writeValue(String newValue, bool response) { + return writeValue((uint8_t *)newValue.c_str(), newValue.length(), response); +} // writeValue + +/** + * @brief Write a byte value to the Descriptor. + * @param [in] The single byte to write. + * @param [in] True if we expect a response. + */ +bool BLERemoteDescriptor::writeValue(uint8_t newValue, bool response) { + return writeValue(&newValue, 1, response); +} // writeValue + +/** + * @brief Set authentication request type for characteristic + * @param [in] auth Authentication request type. + */ +void BLERemoteDescriptor::setAuth(uint8_t auth) { + m_auth = auth; +} + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void BLERemoteDescriptor::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam) { switch (event) { // ESP_GATTC_READ_DESCR_EVT @@ -99,7 +193,7 @@ String BLERemoteDescriptor::readValue() { m_pRemoteCharacteristic->getRemoteService()->getClient()->getGattcIf(), m_pRemoteCharacteristic->getRemoteService()->getClient()->getConnId(), // The connection ID to the BLE server getHandle(), // The handle of this characteristic - m_auth + (esp_gatt_auth_req_t)m_auth ); // Security if (errRc != ESP_OK) { @@ -115,55 +209,19 @@ String BLERemoteDescriptor::readValue() { return m_value; } // readValue -uint8_t BLERemoteDescriptor::readUInt8() { - String value = readValue(); - if (value.length() >= 1) { - return (uint8_t)value[0]; - } - return 0; -} // readUInt8 - -uint16_t BLERemoteDescriptor::readUInt16() { - String value = readValue(); - if (value.length() >= 2) { - return *(uint16_t *)value.c_str(); - } - return 0; -} // readUInt16 - -uint32_t BLERemoteDescriptor::readUInt32() { - String value = readValue(); - if (value.length() >= 4) { - return *(uint32_t *)value.c_str(); - } - return 0; -} // readUInt32 - -/** - * @brief Return a string representation of this BLE Remote Descriptor. - * @return A string representation of this BLE Remote Descriptor. - */ -String BLERemoteDescriptor::toString() { - char val[6]; - snprintf(val, sizeof(val), "%d", getHandle()); - String res = "handle: "; - res += val; - res += ", uuid: " + getUUID().toString(); - return res; -} // toString - /** * @brief Write data to the BLE Remote Descriptor. * @param [in] data The data to send to the remote descriptor. * @param [in] length The length of the data to send. * @param [in] response True if we expect a response. + * @return True if successful */ -void BLERemoteDescriptor::writeValue(uint8_t *data, size_t length, bool response) { +bool BLERemoteDescriptor::writeValue(uint8_t *data, size_t length, bool response) { log_v(">> writeValue: %s", toString().c_str()); // Check to see that we are connected. if (!getRemoteCharacteristic()->getRemoteService()->getClient()->isConnected()) { log_e("Disconnected"); - return; + return false; } m_semaphoreWriteDescrEvt.take("writeValue"); @@ -172,7 +230,7 @@ void BLERemoteDescriptor::writeValue(uint8_t *data, size_t length, bool response m_pRemoteCharacteristic->getRemoteService()->getClient()->getGattcIf(), m_pRemoteCharacteristic->getRemoteService()->getClient()->getConnId(), getHandle(), length, // Data length data, // Data - response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, m_auth + response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, (esp_gatt_auth_req_t)m_auth ); if (errRc != ESP_OK) { log_e("esp_ble_gattc_write_char_descr: %d", errRc); @@ -180,33 +238,246 @@ void BLERemoteDescriptor::writeValue(uint8_t *data, size_t length, bool response m_semaphoreWriteDescrEvt.wait("writeValue"); log_v("<< writeValue"); + return (errRc == ESP_OK); } // writeValue +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + /** - * @brief Write data represented as a string to the BLE Remote Descriptor. - * @param [in] newValue The data to send to the remote descriptor. - * @param [in] response True if we expect a response. + * @brief Remote descriptor constructor. + * @param [in] pRemoteCharacteristic A pointer to the Characteristic that this belongs to. + * @param [in] dsc A pointer to the struct that contains the descriptor information. */ -void BLERemoteDescriptor::writeValue(String newValue, bool response) { - writeValue((uint8_t *)newValue.c_str(), newValue.length(), response); -} // writeValue +BLERemoteDescriptor::BLERemoteDescriptor(BLERemoteCharacteristic *pRemoteCharacteristic, const struct ble_gatt_dsc *dsc) { + log_d(">> BLERemoteDescriptor()"); + switch (dsc->uuid.u.type) { + case BLE_UUID_TYPE_16: m_uuid = BLEUUID(dsc->uuid.u16.value); break; + case BLE_UUID_TYPE_32: m_uuid = BLEUUID(dsc->uuid.u32.value); break; + case BLE_UUID_TYPE_128: m_uuid = BLEUUID(const_cast(&dsc->uuid.u128)); break; + default: break; + } + + m_handle = dsc->handle; + m_pRemoteCharacteristic = pRemoteCharacteristic; + m_auth = 0; + + log_d("<< BLERemoteDescriptor(): %s", m_uuid.toString().c_str()); +} /** - * @brief Write a byte value to the Descriptor. - * @param [in] The single byte to write. - * @param [in] True if we expect a response. + * @brief Read the value of the remote descriptor. + * @return The value of the remote descriptor. */ -void BLERemoteDescriptor::writeValue(uint8_t newValue, bool response) { - writeValue(&newValue, 1, response); -} // writeValue +String BLERemoteDescriptor::readValue() { + log_d(">> Descriptor readValue: %s", toString().c_str()); + + BLEClient *pClient = getRemoteCharacteristic()->getRemoteService()->getClient(); + String value{}; + + if (!pClient->isConnected()) { + log_e("Disconnected"); + return value; + } + + int rc = 0; + int retryCount = 1; + BLETaskData taskData(const_cast(this), 0, &value); + + do { + rc = ble_gattc_read_long(pClient->getConnId(), m_handle, 0, BLERemoteDescriptor::onReadCB, &taskData); + if (rc != 0) { + goto exit; + } + + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + rc = taskData.m_flags; + + switch (rc) { + case 0: + case BLE_HS_EDONE: rc = 0; break; + // Descriptor is not long-readable, return with what we have. + case BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG): + log_i("Attribute not long"); + rc = ble_gattc_read(pClient->getConnId(), m_handle, BLERemoteDescriptor::onReadCB, &taskData); + if (rc != 0) { + goto exit; + } + retryCount++; + break; + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHEN): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHOR): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_ENC): + if (retryCount && pClient->secureConnection()) { + break; + } + /* Else falls through. */ + default: goto exit; + } + } while (rc != 0 && retryCount--); + + m_semaphoreReadDescrEvt.take("readValue"); + m_value = value; + m_semaphoreReadDescrEvt.give(); + +exit: + if (rc != 0) { + log_e("<< readValue failed rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); + } else { + log_d("<< Descriptor readValue(): length: %d rc=%d", value.length(), rc); + } + + return value; +} // readValue /** - * @brief Set authentication request type for characteristic - * @param [in] auth Authentication request type. + * @brief Callback for Descriptor read operation. + * @return success == 0 or error code. */ -void BLERemoteDescriptor::setAuth(esp_gatt_auth_req_t auth) { - m_auth = auth; +int BLERemoteDescriptor::onReadCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg) { + BLETaskData *pTaskData = static_cast(arg); + BLERemoteDescriptor *desc = static_cast(pTaskData->m_pInstance); + uint16_t conn_id = desc->getRemoteCharacteristic()->getRemoteService()->getClient()->getConnId(); + + if (error->status == BLE_HS_ENOTCONN) { + log_e("<< Descriptor Read; Not connected"); + BLEUtils::taskRelease(*pTaskData, error->status); + return error->status; + } + + if (conn_id != conn_handle) { + return 0; + } + + log_d("Read complete; status=%d conn_handle=%d", error->status, conn_handle); + + String *strBuf = static_cast(pTaskData->m_pBuf); + int rc = error->status; + + if (rc == 0) { + if (attr) { + uint32_t data_len = OS_MBUF_PKTLEN(attr->om); + if (((*strBuf).length() + data_len) > BLE_ATT_ATTR_MAX_LEN) { + rc = BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } else { + log_d("Got %d bytes", data_len); + (*strBuf) += String((char *)attr->om->om_data, data_len); + return 0; + } + } + } + + BLEUtils::taskRelease(*pTaskData, rc); + return rc; +} + +/** + * @brief Callback for descriptor write operation. + * @return success == 0 or error code. + */ +int BLERemoteDescriptor::onWriteCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg) { + BLETaskData *pTaskData = static_cast(arg); + BLERemoteDescriptor *descriptor = static_cast(pTaskData->m_pInstance); + int rc = error->status; + + if (rc == BLE_HS_ENOTCONN) { + log_e("<< Descriptor Write; Not connected"); + BLEUtils::taskRelease(*pTaskData, rc); + return rc; + } + + if (descriptor->getRemoteCharacteristic()->getRemoteService()->getClient()->getConnId() != conn_handle) { + return 0; + } + + log_i("Write complete; status=%d conn_handle=%d", rc, conn_handle); + + BLEUtils::taskRelease(*pTaskData, rc); + return 0; } -#endif /* CONFIG_BLUEDROID_ENABLED */ +/** + * @brief Write data to the BLE Remote Descriptor. + * @param [in] data The data to send to the remote descriptor. + * @param [in] length The length of the data to send. + * @param [in] response True if we expect a write response. + * @return True if successful + */ +bool BLERemoteDescriptor::writeValue(uint8_t *data, size_t length, bool response) { + log_d(">> Descriptor writeValue: %s", toString().c_str()); + + BLEClient *pClient = getRemoteCharacteristic()->getRemoteService()->getClient(); + + // Check to see that we are connected. + if (!pClient->isConnected()) { + log_e("Disconnected"); + return false; + } + + int rc = 0; + int retryCount = 1; + uint16_t mtu = ble_att_mtu(pClient->getConnId()) - 3; + BLETaskData taskData(const_cast(this)); + + // Check if the data length is longer than we can write in 1 connection event. + // If so we must do a long write which requires a response. + if (length <= mtu && !response) { + rc = ble_gattc_write_no_rsp_flat(pClient->getConnId(), m_handle, data, length); + goto exit; + } + + do { + if (length > mtu) { + log_i("long write %d bytes", length); + os_mbuf *om = ble_hs_mbuf_from_flat(data, length); + rc = ble_gattc_write_long(pClient->getConnId(), m_handle, 0, om, BLERemoteDescriptor::onWriteCB, &taskData); + } else { + rc = ble_gattc_write_flat(pClient->getConnId(), m_handle, data, length, BLERemoteDescriptor::onWriteCB, &taskData); + } + + if (rc != 0) { + goto exit; + } + + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + rc = taskData.m_flags; + + switch (rc) { + case 0: + case BLE_HS_EDONE: rc = 0; break; + case BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG): + log_e("Long write not supported by peer; Truncating length to %d", mtu); + retryCount++; + length = mtu; + break; + + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHEN): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_AUTHOR): + case BLE_HS_ATT_ERR(BLE_ATT_ERR_INSUFFICIENT_ENC): + if (retryCount && pClient->secureConnection()) { + break; + } + /* Else falls through. */ + default: goto exit; + } + } while (rc != 0 && retryCount--); + +exit: + if (rc != 0) { + log_e("<< writeValue failed rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); + } else { + log_d("<< writeValue success. length: %d rc=%d", length, rc); + } + + return (rc == 0); +} // writeValue + +#endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLERemoteDescriptor.h b/libraries/BLE/src/BLERemoteDescriptor.h index 94b11f1490a..afe113df551 100644 --- a/libraries/BLE/src/BLERemoteDescriptor.h +++ b/libraries/BLE/src/BLERemoteDescriptor.h @@ -3,6 +3,10 @@ * * Created on: Jul 8, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEREMOTEDESCRIPTOR_H_ @@ -11,21 +15,41 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) -#include +/*************************************************************************** + * Common includes * + ***************************************************************************/ +#include #include "BLERemoteCharacteristic.h" #include "BLEUUID.h" #include "RTOS.h" +#include "BLEUtils.h" + +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ class BLERemoteCharacteristic; + /** * @brief A model of remote %BLE descriptor. */ class BLERemoteDescriptor { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + uint16_t getHandle(); BLERemoteCharacteristic *getRemoteCharacteristic(); BLEUUID getUUID(); @@ -34,24 +58,50 @@ class BLERemoteDescriptor { uint16_t readUInt16(void); uint32_t readUInt32(void); String toString(void); - void writeValue(uint8_t *data, size_t length, bool response = false); - void writeValue(String newValue, bool response = false); - void writeValue(uint8_t newValue, bool response = false); - void setAuth(esp_gatt_auth_req_t auth); + bool writeValue(uint8_t *data, size_t length, bool response = false); + bool writeValue(String newValue, bool response = false); + bool writeValue(uint8_t newValue, bool response = false); + void setAuth(uint8_t auth); + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam); +#endif private: friend class BLERemoteCharacteristic; - BLERemoteDescriptor(uint16_t handle, BLEUUID uuid, BLERemoteCharacteristic *pRemoteCharacteristic); - uint16_t m_handle; // Server handle of this descriptor. - BLEUUID m_uuid; // UUID of this descriptor. - String m_value; // Last received value of the descriptor. + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + uint16_t m_handle; // Server handle of this descriptor. + BLEUUID m_uuid; // UUID of this descriptor. + String m_value; // Last received value of the descriptor. + uint8_t m_auth; BLERemoteCharacteristic *m_pRemoteCharacteristic; // Reference to the Remote characteristic of which this descriptor is associated. FreeRTOS::Semaphore m_semaphoreReadDescrEvt = FreeRTOS::Semaphore("ReadDescrEvt"); FreeRTOS::Semaphore m_semaphoreWriteDescrEvt = FreeRTOS::Semaphore("WriteDescrEvt"); - esp_gatt_auth_req_t m_auth; + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + BLERemoteDescriptor(uint16_t handle, BLEUUID uuid, BLERemoteCharacteristic *pRemoteCharacteristic); + + /*************************************************************************** + * NimBLE private declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + BLERemoteDescriptor(BLERemoteCharacteristic *pRemoteCharacteristic, const struct ble_gatt_dsc *dsc); + static int onWriteCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg); + static int onReadCB(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr, void *arg); +#endif }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEREMOTEDESCRIPTOR_H_ */ diff --git a/libraries/BLE/src/BLERemoteService.cpp b/libraries/BLE/src/BLERemoteService.cpp index e4cc31dbb33..7baf6908d40 100644 --- a/libraries/BLE/src/BLERemoteService.cpp +++ b/libraries/BLE/src/BLERemoteService.cpp @@ -3,12 +3,21 @@ * * Created on: Jul 8, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include #include "BLERemoteService.h" @@ -17,97 +26,14 @@ #include #include "esp32-hal-log.h" -#pragma GCC diagnostic warning "-Wunused-but-set-parameter" - -BLERemoteService::BLERemoteService(esp_gatt_id_t srvcId, BLEClient *pClient, uint16_t startHandle, uint16_t endHandle) { - - log_v(">> BLERemoteService()"); - m_srvcId = srvcId; - m_pClient = pClient; - m_uuid = BLEUUID(m_srvcId); - m_haveCharacteristics = false; - m_startHandle = startHandle; - m_endHandle = endHandle; - - log_v("<< BLERemoteService()"); -} +/*************************************************************************** + * Common functions * + ***************************************************************************/ BLERemoteService::~BLERemoteService() { removeCharacteristics(); } -/* -static bool compareSrvcId(esp_gatt_srvc_id_t id1, esp_gatt_srvc_id_t id2) { - if (id1.id.inst_id != id2.id.inst_id) { - return false; - } - if (!BLEUUID(id1.id.uuid).equals(BLEUUID(id2.id.uuid))) { - return false; - } - return true; -} // compareSrvcId -*/ - -/** - * @brief Handle GATT Client events - */ -void BLERemoteService::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam) { - switch (event) { - // - // ESP_GATTC_GET_CHAR_EVT - // - // get_char: - // - esp_gatt_status_t status - // - uin1t6_t conn_id - // - esp_gatt_srvc_id_t srvc_id - // - esp_gatt_id_t char_id - // - esp_gatt_char_prop_t char_prop - // - /* - case ESP_GATTC_GET_CHAR_EVT: { - // Is this event for this service? If yes, then the local srvc_id and the event srvc_id will be - // the same. - if (compareSrvcId(m_srvcId, evtParam->get_char.srvc_id) == false) { - break; - } - - // If the status is NOT OK then we have a problem and continue. - if (evtParam->get_char.status != ESP_GATT_OK) { - m_semaphoreGetCharEvt.give(); - break; - } - - // This is an indication that we now have the characteristic details for a characteristic owned - // by this service so remember it. - m_characteristicMap.insert(std::pair( - BLEUUID(evtParam->get_char.char_id.uuid).toString().c_str(), - new BLERemoteCharacteristic(evtParam->get_char.char_id, evtParam->get_char.char_prop, this) )); - - - // Now that we have received a characteristic, lets ask for the next one. - esp_err_t errRc = ::esp_ble_gattc_get_characteristic( - m_pClient->getGattcIf(), - m_pClient->getConnId(), - &m_srvcId, - &evtParam->get_char.char_id); - if (errRc != ESP_OK) { - log_e("esp_ble_gattc_get_characteristic: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - break; - } - - //m_semaphoreGetCharEvt.give(); - break; - } // ESP_GATTC_GET_CHAR_EVT -*/ - default: break; - } // switch - - // Send the event to each of the characteristics owned by this service. - for (auto &myPair : m_characteristicMapByHandle) { - myPair.second->gattClientEventHandler(event, gattc_if, evtParam); - } -} // gattClientEventHandler - /** * @brief Get the remote characteristic object for the characteristic UUID. * @param [in] uuid Remote characteristic uuid. @@ -144,52 +70,6 @@ BLERemoteCharacteristic *BLERemoteService::getCharacteristic(BLEUUID uuid) { return nullptr; } // getCharacteristic -/** - * @brief Retrieve all the characteristics for this service. - * This function will not return until we have all the characteristics. - * @return N/A - */ -void BLERemoteService::retrieveCharacteristics() { - log_v(">> getCharacteristics() for service: %s", getUUID().toString().c_str()); - - removeCharacteristics(); // Forget any previous characteristics. - - uint16_t offset = 0; - esp_gattc_char_elem_t result; - while (true) { - uint16_t count = 1; // only room for 1 result allocated, so go one by one - esp_gatt_status_t status = - ::esp_ble_gattc_get_all_char(getClient()->getGattcIf(), getClient()->getConnId(), m_startHandle, m_endHandle, &result, &count, offset); - - if (status == ESP_GATT_INVALID_OFFSET) { // We have reached the end of the entries. - break; - } - - if (status != ESP_GATT_OK) { // If we got an error, end. - log_e("esp_ble_gattc_get_all_char: %s", BLEUtils::gattStatusToString(status).c_str()); - break; - } - - if (count == 0) { // If we failed to get any new records, end. - break; - } - - log_d("Found a characteristic: Handle: %d, UUID: %s", result.char_handle, BLEUUID(result.uuid).toString().c_str()); - - // We now have a new characteristic ... let us add that to our set of known characteristics - BLERemoteCharacteristic *pNewRemoteCharacteristic = new BLERemoteCharacteristic(result.char_handle, BLEUUID(result.uuid), result.properties, this); - - m_characteristicMap.insert( - std::pair(pNewRemoteCharacteristic->getUUID().toString().c_str(), pNewRemoteCharacteristic) - ); - m_characteristicMapByHandle.insert(std::pair(result.char_handle, pNewRemoteCharacteristic)); - offset++; // Increment our count of number of descriptors found. - } // Loop forever (until we break inside the loop). - - m_haveCharacteristics = true; // Remember that we have received the characteristics. - log_v("<< getCharacteristics()"); -} // getCharacteristics - /** * @brief Retrieve a map of all the characteristics of this service. * @return A map of all the characteristics of this service. @@ -248,10 +128,6 @@ uint16_t BLERemoteService::getEndHandle() { return m_endHandle; } // getEndHandle -esp_gatt_id_t *BLERemoteService::getSrvcId() { - return &m_srvcId; -} // getSrvcId - uint16_t BLERemoteService::getStartHandle() { return m_startHandle; } // getStartHandle @@ -330,5 +206,222 @@ String BLERemoteService::toString() { return res; } // toString -#endif /* CONFIG_BLUEDROID_ENABLED */ +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +BLERemoteService::BLERemoteService(esp_gatt_id_t srvcId, BLEClient *pClient, uint16_t startHandle, uint16_t endHandle) { + log_v(">> BLERemoteService()"); + m_srvcId = srvcId; + m_pClient = pClient; + m_uuid = BLEUUID(m_srvcId); + m_haveCharacteristics = false; + m_startHandle = startHandle; + m_endHandle = endHandle; + + log_v("<< BLERemoteService()"); +} + +esp_gatt_id_t *BLERemoteService::getSrvcId() { + return &m_srvcId; +} // getSrvcId + +/** + * @brief Retrieve all the characteristics for this service. + * This function will not return until we have all the characteristics. + * @return N/A + */ +void BLERemoteService::retrieveCharacteristics() { + log_v(">> getCharacteristics() for service: %s", getUUID().toString().c_str()); + + removeCharacteristics(); // Forget any previous characteristics. + + uint16_t offset = 0; + esp_gattc_char_elem_t result; + while (true) { + uint16_t count = 1; // only room for 1 result allocated, so go one by one + esp_gatt_status_t status = + ::esp_ble_gattc_get_all_char(getClient()->getGattcIf(), getClient()->getConnId(), m_startHandle, m_endHandle, &result, &count, offset); + + if (status == ESP_GATT_INVALID_OFFSET) { // We have reached the end of the entries. + break; + } + + if (status != ESP_GATT_OK) { // If we got an error, end. + log_e("esp_ble_gattc_get_all_char: %s", BLEUtils::gattStatusToString(status).c_str()); + break; + } + + if (count == 0) { // If we failed to get any new records, end. + break; + } + + log_d("Found a characteristic: Handle: %d, UUID: %s", result.char_handle, BLEUUID(result.uuid).toString().c_str()); + + // We now have a new characteristic ... let us add that to our set of known characteristics + BLERemoteCharacteristic *pNewRemoteCharacteristic = new BLERemoteCharacteristic(result.char_handle, BLEUUID(result.uuid), result.properties, this); + + m_characteristicMap.insert( + std::pair(pNewRemoteCharacteristic->getUUID().toString().c_str(), pNewRemoteCharacteristic) + ); + m_characteristicMapByHandle.insert(std::pair(result.char_handle, pNewRemoteCharacteristic)); + offset++; // Increment our count of number of descriptors found. + } // Loop forever (until we break inside the loop). + + m_haveCharacteristics = true; // Remember that we have received the characteristics. + log_v("<< getCharacteristics()"); +} // getCharacteristics + +/** + * @brief Handle GATT Client events + */ +void BLERemoteService::gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam) { + switch (event) { + // + // ESP_GATTC_GET_CHAR_EVT + // + // get_char: + // - esp_gatt_status_t status + // - uin1t6_t conn_id + // - esp_gatt_srvc_id_t srvc_id + // - esp_gatt_id_t char_id + // - esp_gatt_char_prop_t char_prop + // + /* + case ESP_GATTC_GET_CHAR_EVT: { + // Is this event for this service? If yes, then the local srvc_id and the event srvc_id will be + // the same. + if (compareSrvcId(m_srvcId, evtParam->get_char.srvc_id) == false) { + break; + } + + // If the status is NOT OK then we have a problem and continue. + if (evtParam->get_char.status != ESP_GATT_OK) { + m_semaphoreGetCharEvt.give(); + break; + } + + // This is an indication that we now have the characteristic details for a characteristic owned + // by this service so remember it. + m_characteristicMap.insert(std::pair( + BLEUUID(evtParam->get_char.char_id.uuid).toString().c_str(), + new BLERemoteCharacteristic(evtParam->get_char.char_id, evtParam->get_char.char_prop, this) )); + + + // Now that we have received a characteristic, lets ask for the next one. + esp_err_t errRc = ::esp_ble_gattc_get_characteristic( + m_pClient->getGattcIf(), + m_pClient->getConnId(), + &m_srvcId, + &evtParam->get_char.char_id); + if (errRc != ESP_OK) { + log_e("esp_ble_gattc_get_characteristic: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + break; + } + + //m_semaphoreGetCharEvt.give(); + break; + } // ESP_GATTC_GET_CHAR_EVT +*/ + default: break; + } // switch + + // Send the event to each of the characteristics owned by this service. + for (auto &myPair : m_characteristicMapByHandle) { + myPair.second->gattClientEventHandler(event, gattc_if, evtParam); + } +} // gattClientEventHandler + +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +BLERemoteService::BLERemoteService(BLEClient *pClient, const struct ble_gatt_svc *service) { + log_v(">> BLERemoteService()"); + m_pClient = pClient; + switch (service->uuid.u.type) { + case BLE_UUID_TYPE_16: m_uuid = BLEUUID(service->uuid.u16.value); break; + case BLE_UUID_TYPE_32: m_uuid = BLEUUID(service->uuid.u32.value); break; + case BLE_UUID_TYPE_128: m_uuid = BLEUUID(const_cast(&service->uuid.u128)); break; + default: break; + } + m_startHandle = service->start_handle; + m_endHandle = service->end_handle; + m_haveCharacteristics = false; + log_v("<< BLERemoteService(): %s", m_uuid.toString().c_str()); +} + +/** + * @brief Retrieve all the characteristics for this service. + * This function will not return until we have all the characteristics. + */ +void BLERemoteService::retrieveCharacteristics() { + log_v(">> retrieveCharacteristics() for service: %s", getUUID().toString().c_str()); + + int rc = 0; + BLETaskData taskData(const_cast(this)); + + rc = ble_gattc_disc_all_chrs(m_pClient->getConnId(), m_startHandle, m_endHandle, BLERemoteService::characteristicDiscCB, &taskData); + + if (rc != 0) { + log_e("ble_gattc_disc_all_chrs: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + return; + } + + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + rc = taskData.m_flags; + if (rc == 0 || rc == BLE_HS_EDONE) { + log_d("<< retrieveCharacteristics()"); + return; + } + + log_e("<< retrieveCharacteristics() rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); +} // retrieveCharacteristics + +/** + * @brief Callback for characteristic discovery. + * @return success == 0 or error code. + */ +int BLERemoteService::characteristicDiscCB(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_chr *chr, void *arg) { + log_d("Characteristic Discovered >> status: %d handle: %d", error->status, (error->status == 0) ? chr->val_handle : -1); + + BLETaskData *pTaskData = (BLETaskData *)arg; + BLERemoteService *service = (BLERemoteService *)pTaskData->m_pInstance; + + if (error->status == BLE_HS_ENOTCONN) { + log_e("<< Characteristic Discovery; Not connected"); + BLEUtils::taskRelease(*pTaskData, error->status); + return error->status; + } + + // Make sure the discovery is for this device + if (service->getClient()->getConnId() != conn_handle) { + return 0; + } + + if (error->status == 0) { + // Found a service - add it to the vector + BLERemoteCharacteristic *pRemoteCharacteristic = new BLERemoteCharacteristic(service, chr); + service->m_characteristicMap.insert( + std::pair(pRemoteCharacteristic->getUUID().toString().c_str(), pRemoteCharacteristic) + ); + service->m_characteristicMapByHandle.insert(std::pair(chr->val_handle, pRemoteCharacteristic)); + return 0; + } + + BLEUtils::taskRelease(*pTaskData, error->status); + service->m_haveCharacteristics = true; + log_d("<< Characteristic Discovered"); + return error->status; +} + +#endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLERemoteService.h b/libraries/BLE/src/BLERemoteService.h index 49845a0a1e8..c93d91f6852 100644 --- a/libraries/BLE/src/BLERemoteService.h +++ b/libraries/BLE/src/BLERemoteService.h @@ -3,6 +3,10 @@ * * Created on: Jul 8, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEREMOTESERVICE_H_ @@ -11,7 +15,11 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include @@ -19,6 +27,19 @@ #include "BLERemoteCharacteristic.h" #include "BLEUUID.h" #include "RTOS.h" +#include "BLEUtils.h" + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ class BLEClient; class BLERemoteCharacteristic; @@ -28,15 +49,18 @@ class BLERemoteCharacteristic; */ class BLERemoteService { public: - virtual ~BLERemoteService(); + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ - // Public methods + virtual ~BLERemoteService(); BLERemoteCharacteristic *getCharacteristic(const char *uuid); // Get the specified characteristic reference. BLERemoteCharacteristic *getCharacteristic(BLEUUID uuid); // Get the specified characteristic reference. BLERemoteCharacteristic *getCharacteristic(uint16_t uuid); // Get the specified characteristic reference. std::map *getCharacteristics(); std::map *getCharacteristicsByHandle(); // Get the characteristics map. void getCharacteristics(std::map **pCharacteristicMap); + void retrieveCharacteristics(); BLEClient *getClient(void); // Get a reference to the client associated with this service. uint16_t getHandle(); // Get the handle of this service. @@ -46,24 +70,12 @@ class BLERemoteService { String toString(void); private: - // Private constructor ... never meant to be created by a user application. - BLERemoteService(esp_gatt_id_t srvcId, BLEClient *pClient, uint16_t startHandle, uint16_t endHandle); - - // Friends friend class BLEClient; friend class BLERemoteCharacteristic; - // Private methods - void retrieveCharacteristics(void); // Retrieve the characteristics from the BLE Server. - esp_gatt_id_t *getSrvcId(void); - uint16_t getStartHandle(); // Get the start handle for this service. - uint16_t getEndHandle(); // Get the end handle for this service. - - void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam); - - void removeCharacteristics(); - - // Properties + /*************************************************************************** + * Common private properties * + ***************************************************************************/ // We maintain a map of characteristics owned by this service keyed by a string representation of the UUID. std::map m_characteristicMap; @@ -74,12 +86,47 @@ class BLERemoteService { bool m_haveCharacteristics; // Have we previously obtained the characteristics. BLEClient *m_pClient; FreeRTOS::Semaphore m_semaphoreGetCharEvt = FreeRTOS::Semaphore("GetCharEvt"); - esp_gatt_id_t m_srvcId; BLEUUID m_uuid; // The UUID of this service. uint16_t m_startHandle; // The starting handle of this service. uint16_t m_endHandle; // The ending handle of this service. + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_gatt_id_t m_srvcId; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + uint16_t getStartHandle(); // Get the start handle for this service. + uint16_t getEndHandle(); // Get the end handle for this service. + void removeCharacteristics(); + + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + // Private constructor ... never meant to be created by a user application. + BLERemoteService(esp_gatt_id_t srvcId, BLEClient *pClient, uint16_t startHandle, uint16_t endHandle); + esp_gatt_id_t *getSrvcId(); + void gattClientEventHandler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam); +#endif + + /*************************************************************************** + * NimBLE private declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + BLERemoteService(BLEClient *pClient, const struct ble_gatt_svc *service); + static int characteristicDiscCB(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_chr *chr, void *arg); +#endif }; // BLERemoteService -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEREMOTESERVICE_H_ */ diff --git a/libraries/BLE/src/BLEScan.cpp b/libraries/BLE/src/BLEScan.cpp index 0a99b46c61d..6d18bb2d751 100644 --- a/libraries/BLE/src/BLEScan.cpp +++ b/libraries/BLE/src/BLEScan.cpp @@ -6,12 +6,21 @@ * * Update: April, 2021 * add BLE5 support + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ #include @@ -23,12 +32,17 @@ #include "GeneralUtils.h" #include "esp32-hal-log.h" +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** - * Constructor + * @brief Constructor */ BLEScan::BLEScan() { memset(&m_scan_params, 0, sizeof(m_scan_params)); // Initialize all params - m_scan_params.scan_type = BLE_SCAN_TYPE_PASSIVE; // Default is a passive scan. +#if defined(CONFIG_BLUEDROID_ENABLED) + m_scan_params.scan_type = BLE_SCAN_TYPE_PASSIVE; // Default is a passive scan. m_scan_params.own_addr_type = BLE_ADDR_TYPE_PUBLIC; m_scan_params.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; m_scan_params.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE; @@ -38,15 +52,246 @@ BLEScan::BLEScan() { m_shouldParse = true; setInterval(100); setWindow(100); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_scan_params.filter_policy = BLE_HCI_SCAN_FILT_NO_WL; + m_scan_params.passive = 1; // If set, don’t send scan requests to advertisers (i.e., don’t request additional advertising data). + m_scan_params.limited = 0; // If set, only discover devices in limited discoverable mode. + m_scan_params.filter_duplicates = 1; // If set, the controller ignores all but the first advertisement from each device. + m_pAdvertisedDeviceCallbacks = nullptr; + m_ignoreResults = false; + m_pTaskData = nullptr; + m_duration = BLE_HS_FOREVER; // make sure this is non-zero in the event of a host reset + m_maxResults = 0xFF; + m_stopped = true; + m_wantDuplicates = false; + m_shouldParse = true; + // This is defined as the time interval from when the Controller started its last LE scan until it begins the subsequent LE scan. (units=0.625 msec) + setInterval(100); + // The duration of the LE scan. LE_Scan_Window shall be less than or equal to LE_Scan_Interval (units=0.625 msec) + setWindow(100); +#endif } // BLEScan +/** + * @brief Scan destructor, release any allocated resources. + */ +BLEScan::~BLEScan() { + clearResults(); +} + +/** + * @brief Should we perform an active or passive scan? + * The default is a passive scan. An active scan means that we will wish a scan response. + * @param [in] active If true, we perform an active scan otherwise a passive scan. + * @return N/A. + */ +void BLEScan::setActiveScan(bool active) { +#if defined(CONFIG_BLUEDROID_ENABLED) + if (active) { + m_scan_params.scan_type = BLE_SCAN_TYPE_ACTIVE; + } else { + m_scan_params.scan_type = BLE_SCAN_TYPE_PASSIVE; + } +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_scan_params.passive = !active; +#endif +} // setActiveScan + +/** + * @brief Set the call backs to be invoked. + * @param [in] pAdvertisedDeviceCallbacks Call backs to be invoked. + * @param [in] wantDuplicates True if we wish to be called back with duplicates. Default is false. + * @param [in] shouldParse True if we wish to parse advertised package or raw payload. Default is true. + */ +void BLEScan::setAdvertisedDeviceCallbacks(BLEAdvertisedDeviceCallbacks *pAdvertisedDeviceCallbacks, bool wantDuplicates, bool shouldParse) { + m_wantDuplicates = wantDuplicates; +#if defined(CONFIG_NIMBLE_ENABLED) + setDuplicateFilter(!wantDuplicates); +#endif + m_pAdvertisedDeviceCallbacks = pAdvertisedDeviceCallbacks; + m_shouldParse = shouldParse; +} // setAdvertisedDeviceCallbacks + +/** + * @brief Set the interval to scan. + * @param [in] The interval in msecs. + */ +void BLEScan::setInterval(uint16_t intervalMSecs) { +#if defined(CONFIG_BLUEDROID_ENABLED) + m_scan_params.scan_interval = intervalMSecs / 0.625; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_scan_params.itvl = intervalMSecs / 0.625; +#endif +} // setInterval + +/** + * @brief Set the window to actively scan. + * @param [in] windowMSecs How long to actively scan. + */ +void BLEScan::setWindow(uint16_t windowMSecs) { +#if defined(CONFIG_BLUEDROID_ENABLED) + m_scan_params.scan_window = windowMSecs / 0.625; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + m_scan_params.window = windowMSecs / 0.625; +#endif +} // setWindow + +// delete peer device from cache after disconnecting, it is required in case we are connecting to devices with not public address +void BLEScan::erase(BLEAddress address) { + log_i("erase device: %s", address.toString().c_str()); + BLEAdvertisedDevice *advertisedDevice = m_scanResults.m_vectorAdvertisedDevices.find(address.toString().c_str())->second; + m_scanResults.m_vectorAdvertisedDevices.erase(address.toString().c_str()); + delete advertisedDevice; +} + +/** + * @brief Dump the scan results to the log. + */ +void BLEScanResults::dump() { + log_v(">> Dump scan results:"); + for (int i = 0; i < getCount(); i++) { + log_d("- %s", getDevice(i).toString().c_str()); + } +} // dump + +/** + * @brief Return the count of devices found in the last scan. + * @return The number of devices found in the last scan. + */ +int BLEScanResults::getCount() { + return m_vectorAdvertisedDevices.size(); +} // getCount + +/** + * @brief Return the specified device at the given index. + * The index should be between 0 and getCount()-1. + * @param [in] i The index of the device. + * @return The device at the specified index. + */ +BLEAdvertisedDevice BLEScanResults::getDevice(uint32_t i) { + uint32_t x = 0; + BLEAdvertisedDevice dev = *m_vectorAdvertisedDevices.begin()->second; + for (auto it = m_vectorAdvertisedDevices.begin(); it != m_vectorAdvertisedDevices.end(); it++) { + dev = *it->second; + if (x == i) { + break; + } + x++; + } + return dev; +} + +BLEScanResults *BLEScan::getResults() { + return &m_scanResults; +} + +void BLEScan::clearResults() { + for (auto _dev : m_scanResults.m_vectorAdvertisedDevices) { + delete _dev.second; + } + m_scanResults.m_vectorAdvertisedDevices.clear(); +} + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +#if defined(SOC_BLE_50_SUPPORTED) + +void BLEScan::setExtendedScanCallback(BLEExtAdvertisingCallbacks *cb) { + m_pExtendedScanCb = cb; +} + +/** +* @brief This function is used to enable scanning. +* +* @param[in] duration : Scan duration +* @param[in] period : Time interval from when the Controller started its last Scan Duration until it begins the subsequent Scan Duration. +* +* @return - ESP_OK : success +* - other : failed +* +*/ +esp_err_t BLEScan::startExtScan(uint32_t duration, uint16_t period) { + esp_err_t rc = esp_ble_gap_start_ext_scan(duration, period); + if (rc) { + log_e("extended scan start failed: %d", rc); + } + return rc; +} + +esp_err_t BLEScan::stopExtScan() { + esp_err_t rc = esp_ble_gap_stop_ext_scan(); + if (rc) { + log_e("extended scan stop failed: %d", rc); + } + return rc; +} + +void BLEScan::setPeriodicScanCallback(BLEPeriodicScanCallbacks *cb) { + m_pPeriodicScanCb = cb; +} + +/** +* @brief This function is used to set the extended scan parameters to be used on the advertising channels. +* +* +* @return - ESP_OK : success +* - other : failed +* +*/ +esp_err_t BLEScan::setExtScanParams() { + esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK | ESP_BLE_GAP_EXT_SCAN_CFG_CODE_MASK, + .uncoded_cfg = {BLE_SCAN_TYPE_ACTIVE, 40, 40}, + .coded_cfg = {BLE_SCAN_TYPE_ACTIVE, 40, 40}, + }; + + esp_err_t rc = esp_ble_gap_set_ext_scan_params(&ext_scan_params); + if (rc) { + log_e("set extend scan params error, error code = %x", rc); + } + return rc; +} + +/** +* @brief This function is used to set the extended scan parameters to be used on the advertising channels. +* +* @param[in] params : scan parameters +* +* @return - ESP_OK : success +* - other : failed +* +*/ +esp_err_t BLEScan::setExtScanParams(esp_ble_ext_scan_params_t *ext_scan_params) { + esp_err_t rc = esp_ble_gap_set_ext_scan_params(ext_scan_params); + if (rc) { + log_e("set extend scan params error, error code = %x", rc); + } + return rc; +} + +#endif // SOC_BLE_50_SUPPORTED + /** * @brief Handle GAP events related to scans. * @param [in] event The event type for this event. * @param [in] param Parameter data for this event. */ void BLEScan::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - switch (event) { // --------------------------- @@ -118,6 +363,7 @@ void BLEScan::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_ advertisedDevice->setAddress(advertisedAddress); advertisedDevice->setRSSI(param->scan_rst.rssi); advertisedDevice->setAdFlag(param->scan_rst.flag); + advertisedDevice->setAdvType(param->scan_rst.ble_evt_type); if (m_shouldParse) { advertisedDevice->parseAdvertisement((uint8_t *)param->scan_rst.ble_adv, param->scan_rst.adv_data_len + param->scan_rst.scan_rsp_len); } else { @@ -245,126 +491,6 @@ void BLEScan::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_ } // End switch } // gapEventHandler -/** - * @brief Should we perform an active or passive scan? - * The default is a passive scan. An active scan means that we will wish a scan response. - * @param [in] active If true, we perform an active scan otherwise a passive scan. - * @return N/A. - */ -void BLEScan::setActiveScan(bool active) { - if (active) { - m_scan_params.scan_type = BLE_SCAN_TYPE_ACTIVE; - } else { - m_scan_params.scan_type = BLE_SCAN_TYPE_PASSIVE; - } -} // setActiveScan - -/** - * @brief Set the call backs to be invoked. - * @param [in] pAdvertisedDeviceCallbacks Call backs to be invoked. - * @param [in] wantDuplicates True if we wish to be called back with duplicates. Default is false. - * @param [in] shouldParse True if we wish to parse advertised package or raw payload. Default is true. - */ -void BLEScan::setAdvertisedDeviceCallbacks(BLEAdvertisedDeviceCallbacks *pAdvertisedDeviceCallbacks, bool wantDuplicates, bool shouldParse) { - m_wantDuplicates = wantDuplicates; - m_pAdvertisedDeviceCallbacks = pAdvertisedDeviceCallbacks; - m_shouldParse = shouldParse; -} // setAdvertisedDeviceCallbacks - -#ifdef SOC_BLE_50_SUPPORTED - -void BLEScan::setExtendedScanCallback(BLEExtAdvertisingCallbacks *cb) { - m_pExtendedScanCb = cb; -} - -/** -* @brief This function is used to set the extended scan parameters to be used on the advertising channels. -* -* -* @return - ESP_OK : success -* - other : failed -* -*/ -esp_err_t BLEScan::setExtScanParams() { - esp_ble_ext_scan_params_t ext_scan_params = { - .own_addr_type = BLE_ADDR_TYPE_PUBLIC, - .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, - .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, - .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK | ESP_BLE_GAP_EXT_SCAN_CFG_CODE_MASK, - .uncoded_cfg = {BLE_SCAN_TYPE_ACTIVE, 40, 40}, - .coded_cfg = {BLE_SCAN_TYPE_ACTIVE, 40, 40}, - }; - - esp_err_t rc = esp_ble_gap_set_ext_scan_params(&ext_scan_params); - if (rc) { - log_e("set extend scan params error, error code = %x", rc); - } - return rc; -} - -/** -* @brief This function is used to set the extended scan parameters to be used on the advertising channels. -* -* @param[in] params : scan parameters -* -* @return - ESP_OK : success -* - other : failed -* -*/ -esp_err_t BLEScan::setExtScanParams(esp_ble_ext_scan_params_t *ext_scan_params) { - esp_err_t rc = esp_ble_gap_set_ext_scan_params(ext_scan_params); - if (rc) { - log_e("set extend scan params error, error code = %x", rc); - } - return rc; -} - -/** -* @brief This function is used to enable scanning. -* -* @param[in] duration : Scan duration -* @param[in] period : Time interval from when the Controller started its last Scan Duration until it begins the subsequent Scan Duration. -* -* @return - ESP_OK : success -* - other : failed -* -*/ -esp_err_t BLEScan::startExtScan(uint32_t duration, uint16_t period) { - esp_err_t rc = esp_ble_gap_start_ext_scan(duration, period); - if (rc) { - log_e("extended scan start failed: %d", rc); - } - return rc; -} - -esp_err_t BLEScan::stopExtScan() { - esp_err_t rc; - rc = esp_ble_gap_stop_ext_scan(); - - return rc; -} - -void BLEScan::setPeriodicScanCallback(BLEPeriodicScanCallbacks *cb) { - m_pPeriodicScanCb = cb; -} - -#endif // SOC_BLE_50_SUPPORTED -/** - * @brief Set the interval to scan. - * @param [in] The interval in msecs. - */ -void BLEScan::setInterval(uint16_t intervalMSecs) { - m_scan_params.scan_interval = intervalMSecs / 0.625; -} // setInterval - -/** - * @brief Set the window to actively scan. - * @param [in] windowMSecs How long to actively scan. - */ -void BLEScan::setWindow(uint16_t windowMSecs) { - m_scan_params.scan_window = windowMSecs / 0.625; -} // setWindow - /** * @brief Start scanning. * @param [in] duration The duration in seconds for which to scan. @@ -425,7 +551,7 @@ BLEScanResults *BLEScan::start(uint32_t duration, bool is_continue) { * @brief Stop an in progress scan. * @return N/A. */ -void BLEScan::stop() { +bool BLEScan::stop() { log_v(">> stop()"); esp_err_t errRc = ::esp_ble_gap_stop_scanning(); @@ -435,67 +561,303 @@ void BLEScan::stop() { if (errRc != ESP_OK) { log_e("esp_ble_gap_stop_scanning: err: %d, text: %s", errRc, GeneralUtils::errorToString(errRc)); - return; + return false; } log_v("<< stop()"); + return true; } // stop -// delete peer device from cache after disconnecting, it is required in case we are connecting to devices with not public address -void BLEScan::erase(BLEAddress address) { - log_i("erase device: %s", address.toString().c_str()); - BLEAdvertisedDevice *advertisedDevice = m_scanResults.m_vectorAdvertisedDevices.find(address.toString().c_str())->second; - m_scanResults.m_vectorAdvertisedDevices.erase(address.toString().c_str()); - delete advertisedDevice; +#endif // CONFIG_BLUEDROID_ENABLED + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +/** + * @brief Set whether or not the BLE controller should only report results + * from devices it has not already seen. + * @param [in] enabled If true, scanned devices will only be reported once. + * @details The controller has a limited buffer and will start reporting + * duplicate devices once the limit is reached. + */ +void BLEScan::setDuplicateFilter(bool enabled) { + m_scan_params.filter_duplicates = enabled; +} // setDuplicateFilter + +/** + * @brief Get the status of the scanner. + * @return true if scanning or scan starting. + */ +bool BLEScan::isScanning() { + return ble_gap_disc_active(); } /** - * @brief Dump the scan results to the log. + * @brief Handle GAP events related to scans. + * @param [in] event The event type for this event. + * @param [in] param Parameter data for this event. */ -void BLEScanResults::dump() { - log_v(">> Dump scan results:"); - for (int i = 0; i < getCount(); i++) { - log_d("- %s", getDevice(i).toString().c_str()); +int BLEScan::handleGAPEvent(ble_gap_event *event, void *arg) { + BLEScan *pScan = (BLEScan *)arg; + + switch (event->type) { + case BLE_GAP_EVENT_DISC: + { + if (pScan->m_ignoreResults) { + log_i("Scan op in progress - ignoring results"); + return 0; + } + + const auto &disc = event->disc; + const auto event_type = disc.event_type; + const bool isLegacyAdv = true; + BLEAddress advertisedAddress(disc.addr); + + BLEClient *client = BLEDevice::getClientByAddress(advertisedAddress); + if (client != nullptr && client->isConnected()) { + log_i("Client %s connected - ignoring event", advertisedAddress.toString().c_str()); + return 0; + } + + BLEAdvertisedDevice *advertisedDevice = nullptr; + + // If we've seen this device before get a pointer to it from the vector + for (auto &it : pScan->m_scanResults.m_vectorAdvertisedDevices) { + if (it.second->getAddress() == advertisedAddress) { + advertisedDevice = it.second; + break; + } + } + + // If we haven't seen this device before; create a new instance and insert it in the vector. + // Otherwise just update the relevant parameters of the already known device. + if (advertisedDevice == nullptr) { + // Check if we have reach the scan results limit, ignore this one if so. + // We still need to store each device when maxResults is 0 to be able to append the scan results + if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF && (pScan->m_scanResults.m_vectorAdvertisedDevices.size() >= pScan->m_maxResults)) { + return 0; + } + + if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) { + log_i("Scan response without advertisement: %s", advertisedAddress.toString().c_str()); + } + + advertisedDevice = new BLEAdvertisedDevice(); + advertisedDevice->setAddress(advertisedAddress); + advertisedDevice->setAddressType(event->disc.addr.type); + advertisedDevice->setAdvType(event_type); + pScan->m_scanResults.m_vectorAdvertisedDevices.insert( + std::pair(advertisedAddress.toString().c_str(), advertisedDevice) + ); + log_i("New advertiser: %s", advertisedAddress.toString().c_str()); + } else if (advertisedDevice != nullptr) { + log_i("Updated advertiser: %s", advertisedAddress.toString().c_str()); + } else { + // Scan response from unknown device + return 0; + } + + advertisedDevice->setRSSI(event->disc.rssi); + + if (pScan->m_shouldParse) { + advertisedDevice->parseAdvertisement((uint8_t *)event->disc.data, event->disc.length_data); + } else { + advertisedDevice->setPayload((uint8_t *)event->disc.data, event->disc.length_data, event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP); + } + + advertisedDevice->setScan(pScan); + + if (pScan->m_pAdvertisedDeviceCallbacks) { + // If not active scanning or scan response is not available + // report the result to the callback now. + if (pScan->m_scan_params.passive || !isLegacyAdv || !advertisedDevice->isScannable()) { + advertisedDevice->m_callbackSent = true; + pScan->m_pAdvertisedDeviceCallbacks->onResult(*advertisedDevice); + + // Otherwise, wait for the scan response so we can report the complete data. + } else if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) { + advertisedDevice->m_callbackSent = true; + pScan->m_pAdvertisedDeviceCallbacks->onResult(*advertisedDevice); + } + } + + // If not storing results and we have invoked the callback, delete the device. + if (pScan->m_maxResults == 0 && advertisedDevice->m_callbackSent) { + pScan->erase(advertisedAddress); + } + + return 0; + } + + case BLE_GAP_EVENT_DISC_COMPLETE: + { + log_d("discovery complete; reason=%d", event->disc_complete.reason); + + // If a device advertised with scan response available and it was not received + // the callback would not have been invoked, so do it here. + if (pScan->m_pAdvertisedDeviceCallbacks) { + for (auto &it : pScan->m_scanResults.m_vectorAdvertisedDevices) { + if (!it.second->m_callbackSent) { + pScan->m_pAdvertisedDeviceCallbacks->onResult(*(it.second)); + } + } + } + + if (pScan->m_maxResults == 0) { + pScan->clearResults(); + } + + if (pScan->m_scanCompleteCB != nullptr) { + pScan->m_scanCompleteCB(pScan->m_scanResults); + } + + if (pScan->m_pTaskData != nullptr) { + BLEUtils::taskRelease(*pScan->m_pTaskData, event->disc_complete.reason); + } + + return 0; + } + + default: return 0; } -} // dump +} // gapEventHandler /** - * @brief Return the count of devices found in the last scan. - * @return The number of devices found in the last scan. + * @brief Start scanning. + * @param [in] duration The duration in seconds for which to scan. + * @param [in] scanCompleteCB A function to be called when scanning has completed. + * @param [in] is_continue Set to true to save previous scan results, false to clear them. + * @return True if scan started or false if there was an error. */ -int BLEScanResults::getCount() { - return m_vectorAdvertisedDevices.size(); -} // getCount +bool BLEScan::start(uint32_t duration, void (*scanCompleteCB)(BLEScanResults), bool is_continue) { + log_d(">> start(duration=%d)", duration); + + if (!is_continue) { + clearResults(); + } + + // Save the callback to be invoked when the scan completes. + m_scanCompleteCB = scanCompleteCB; + // Save the duration in the case that the host is reset so we can reuse it. + m_duration = duration; + + // If 0 duration specified then we assume a continuous scan is desired. + if (duration == 0) { + duration = BLE_HS_FOREVER; + } else { + // convert duration to milliseconds + duration = duration * 1000; + } + + // Set the flag to ignore the results while we are deleting the vector + if (!is_continue) { + m_ignoreResults = true; + } + + int rc = ble_gap_disc(BLEDevice::m_ownAddrType, duration, &m_scan_params, BLEScan::handleGAPEvent, this); + + switch (rc) { + case 0: + case BLE_HS_EALREADY: break; + + case BLE_HS_EBUSY: log_e("Unable to scan - connection in progress."); break; + + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: log_e("Unable to scan - Host Reset"); break; + + default: log_e("Error initiating GAP discovery procedure; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break; + } + + m_ignoreResults = false; + log_d("<< start()"); + + if (rc != 0 && rc != BLE_HS_EALREADY) { + return false; + } + return true; +} // start /** - * @brief Return the specified device at the given index. - * The index should be between 0 and getCount()-1. - * @param [in] i The index of the device. - * @return The device at the specified index. + * @brief Start scanning and block until scanning has been completed. + * @param [in] duration The duration in seconds for which to scan. + * @param [in] is_continue Set to true to save previous scan results, false to clear them. + * @return The BLEScanResults. */ -BLEAdvertisedDevice BLEScanResults::getDevice(uint32_t i) { - uint32_t x = 0; - BLEAdvertisedDevice dev = *m_vectorAdvertisedDevices.begin()->second; - for (auto it = m_vectorAdvertisedDevices.begin(); it != m_vectorAdvertisedDevices.end(); it++) { - dev = *it->second; - if (x == i) { - break; - } - x++; +BLEScanResults *BLEScan::start(uint32_t duration, bool is_continue) { + if (duration == 0) { + log_w("Blocking scan called with duration = forever"); } - return dev; -} -BLEScanResults *BLEScan::getResults() { + if (m_pTaskData != nullptr) { + log_e("Scan already in progress"); + return &m_scanResults; + } + + BLETaskData taskData(this); + m_pTaskData = &taskData; + + if (start(duration, nullptr, is_continue)) { + BLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + } + + m_pTaskData = nullptr; return &m_scanResults; +} // start + +/** + * @brief Stop an in progress scan. + * @return True if successful. + */ +bool BLEScan::stop() { + log_d(">> stop()"); + + int rc = ble_gap_disc_cancel(); + if (rc != 0 && rc != BLE_HS_EALREADY) { + log_e("Failed to cancel scan; rc=%d", rc); + return false; + } + + if (m_maxResults == 0) { + clearResults(); + } + + if (rc != BLE_HS_EALREADY && m_scanCompleteCB != nullptr) { + m_scanCompleteCB(m_scanResults); + } + + if (m_pTaskData != nullptr) { + BLEUtils::taskRelease(*m_pTaskData); + } + + log_d("<< stop()"); + return true; +} // stop + +/** + * @brief Called when host reset, we set a flag to stop scanning until synced. + */ +void BLEScan::onHostReset() { + m_ignoreResults = true; } -void BLEScan::clearResults() { - for (auto _dev : m_scanResults.m_vectorAdvertisedDevices) { - delete _dev.second; +/** + * @brief If the host reset and re-synced this is called. + * If the application was scanning indefinitely with a callback, restart it. + */ +void BLEScan::onHostSync() { + m_ignoreResults = false; + + if (m_duration == 0 && m_pAdvertisedDeviceCallbacks != nullptr) { + start(m_duration, m_scanCompleteCB); } - m_scanResults.m_vectorAdvertisedDevices.clear(); } -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif // CONFIG_NIMBLE_ENABLED + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEScan.h b/libraries/BLE/src/BLEScan.h index 080e3b803b2..842b7144f06 100644 --- a/libraries/BLE/src/BLEScan.h +++ b/libraries/BLE/src/BLEScan.h @@ -3,6 +3,10 @@ * * Created on: Jul 1, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLESCAN_H_ @@ -11,22 +15,51 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ -// #include #include #include "BLEAdvertisedDevice.h" #include "BLEClient.h" +#include "BLEUtils.h" #include "RTOS.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#endif + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ + class BLEAdvertisedDevice; class BLEAdvertisedDeviceCallbacks; class BLEExtAdvertisingCallbacks; class BLEClient; class BLEScan; class BLEPeriodicScanCallbacks; +struct BLETaskData; +/*************************************************************************** + * Bluedroid type definitions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) struct esp_ble_periodic_adv_sync_estab_param_t { uint8_t status; /*!< periodic advertising sync status */ uint16_t sync_handle; /*!< periodic advertising sync handle */ @@ -37,6 +70,7 @@ struct esp_ble_periodic_adv_sync_estab_param_t { uint16_t period_adv_interval; /*!< periodic advertising interval */ uint8_t adv_clk_accuracy; /*!< periodic advertising clock accuracy */ }; +#endif /** * @brief The result of having performed a scan. @@ -47,12 +81,21 @@ struct esp_ble_periodic_adv_sync_estab_param_t { */ class BLEScanResults { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + void dump(); int getCount(); BLEAdvertisedDevice getDevice(uint32_t i); private: friend BLEScan; + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + std::map m_vectorAdvertisedDevices; }; @@ -63,37 +106,52 @@ class BLEScanResults { */ class BLEScan { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + + ~BLEScan(); void setActiveScan(bool active); void setAdvertisedDeviceCallbacks(BLEAdvertisedDeviceCallbacks *pAdvertisedDeviceCallbacks, bool wantDuplicates = false, bool shouldParse = true); void setInterval(uint16_t intervalMSecs); void setWindow(uint16_t windowMSecs); bool start(uint32_t duration, void (*scanCompleteCB)(BLEScanResults), bool is_continue = false); BLEScanResults *start(uint32_t duration, bool is_continue = false); - void stop(); + bool stop(); void erase(BLEAddress address); BLEScanResults *getResults(); void clearResults(); -#ifdef SOC_BLE_50_SUPPORTED + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(SOC_BLE_50_SUPPORTED) && defined(CONFIG_BLUEDROID_ENABLED) void setExtendedScanCallback(BLEExtAdvertisingCallbacks *cb); void setPeriodicScanCallback(BLEPeriodicScanCallbacks *cb); - esp_err_t stopExtScan(); esp_err_t setExtScanParams(); - esp_err_t setExtScanParams(esp_ble_ext_scan_params_t *ext_scan_params); esp_err_t startExtScan(uint32_t duration, uint16_t period); - -private: - BLEExtAdvertisingCallbacks *m_pExtendedScanCb = nullptr; - BLEPeriodicScanCallbacks *m_pPeriodicScanCb = nullptr; + esp_err_t setExtScanParams(esp_ble_ext_scan_params_t *ext_scan_params); #endif // SOC_BLE_50_SUPPORTED + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void setDuplicateFilter(bool enabled); + bool isScanning(); + void clearDuplicateCache(); +#endif + private: - BLEScan(); // One doesn't create a new instance instead one asks the BLEDevice for the singleton. friend class BLEDevice; - void handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - esp_ble_scan_params_t m_scan_params; + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + BLEAdvertisedDeviceCallbacks *m_pAdvertisedDeviceCallbacks = nullptr; bool m_stopped = true; bool m_shouldParse = true; @@ -101,21 +159,79 @@ class BLEScan { BLEScanResults m_scanResults; bool m_wantDuplicates; void (*m_scanCompleteCB)(BLEScanResults scanResults); + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_ble_scan_params_t m_scan_params; +#if defined(SOC_BLE_50_SUPPORTED) + BLEExtAdvertisingCallbacks *m_pExtendedScanCb = nullptr; + BLEPeriodicScanCallbacks *m_pPeriodicScanCb = nullptr; +#endif // SOC_BLE_50_SUPPORTED +#endif + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + uint32_t m_duration; + ble_gap_disc_params m_scan_params; + bool m_ignoreResults; + BLETaskData *m_pTaskData; + uint8_t m_maxResults; +#endif + + /*************************************************************************** + * Common private definitions * + ***************************************************************************/ + + BLEScan(); // One doesn't create a new instance instead one asks the BLEDevice for the singleton. + + /*************************************************************************** + * Bluedroid private definitions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE private definitions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void onHostReset(); + void onHostSync(); + static int handleGAPEvent(ble_gap_event *event, void *arg); +#endif }; // BLEScan class BLEPeriodicScanCallbacks { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + virtual ~BLEPeriodicScanCallbacks() {} + virtual void onLostSync(uint16_t sync_handle) {} + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) virtual void onCreateSync(esp_bt_status_t status) {} virtual void onCancelSync(esp_bt_status_t status) {} virtual void onTerminateSync(esp_bt_status_t status) {} - virtual void onLostSync(uint16_t sync_handle) {} virtual void onSync(esp_ble_periodic_adv_sync_estab_param_t) {} virtual void onReport(esp_ble_gap_periodic_adv_report_t params) {} virtual void onStop(esp_bt_status_t status) {} +#endif }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLESCAN_H_ */ diff --git a/libraries/BLE/src/BLESecurity.cpp b/libraries/BLE/src/BLESecurity.cpp index 34fc3e69e9e..978026a3809 100644 --- a/libraries/BLE/src/BLESecurity.cpp +++ b/libraries/BLE/src/BLESecurity.cpp @@ -3,77 +3,114 @@ * * Created on: Dec 17, 2017 * Author: chegewara + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on chegewara's and h2zero's work) + * Description: Added support for NimBLE */ #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED -#include "BLESecurity.h" #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + +#include "BLESecurity.h" +#include "BLEUtils.h" +#include "esp32-hal-log.h" + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#endif + +/*************************************************************************** + * Common properties * + ***************************************************************************/ + +uint32_t BLESecurity::m_passkey = BLE_SM_DEFAULT_PASSKEY; + +/*************************************************************************** + * Common functions * + ***************************************************************************/ BLESecurity::BLESecurity() {} BLESecurity::~BLESecurity() {} -/* - * @brief Set requested authentication mode - */ -void BLESecurity::setAuthenticationMode(esp_ble_auth_req_t auth_req) { + +void BLESecurity::setAuthenticationMode(uint8_t auth_req) { m_authReq = auth_req; - esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &m_authReq, sizeof(uint8_t)); // <--- setup requested authentication mode +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &m_authReq, sizeof(uint8_t)); +#elif defined(CONFIG_NIMBLE_ENABLED) + BLESecurity::setAuthenticationMode( + (auth_req & BLE_SM_PAIR_AUTHREQ_BOND) != 0, (auth_req & BLE_SM_PAIR_AUTHREQ_MITM) != 0, (auth_req & BLE_SM_PAIR_AUTHREQ_SC) != 0 + ); +#endif } -/** - * @brief Set our device IO capability to let end user perform authorization - * either by displaying or entering generated 6-digits pin code - */ -void BLESecurity::setCapability(esp_ble_io_cap_t iocap) { +void BLESecurity::setCapability(uint8_t iocap) { m_iocap = iocap; +#if defined(CONFIG_BLUEDROID_ENABLED) esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); -} // setCapability +#elif defined(CONFIG_NIMBLE_ENABLED) + ble_hs_cfg.sm_io_cap = iocap; +#endif +} -/** - * @brief Init encryption key by server - * @param key_size is value between 7 and 16 - */ void BLESecurity::setInitEncryptionKey(uint8_t init_key) { m_initKey = init_key; +#if defined(CONFIG_BLUEDROID_ENABLED) esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &m_initKey, sizeof(uint8_t)); -} // setInitEncryptionKey +#elif defined(CONFIG_NIMBLE_ENABLED) + ble_hs_cfg.sm_our_key_dist = init_key; +#endif +} -/** - * @brief Init encryption key by client - * @param key_size is value between 7 and 16 - */ void BLESecurity::setRespEncryptionKey(uint8_t resp_key) { m_respKey = resp_key; +#if defined(CONFIG_BLUEDROID_ENABLED) esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &m_respKey, sizeof(uint8_t)); -} // setRespEncryptionKey +#elif defined(CONFIG_NIMBLE_ENABLED) + ble_hs_cfg.sm_their_key_dist = resp_key; +#endif +} -/** - * - * - */ void BLESecurity::setKeySize(uint8_t key_size) { +#if defined(CONFIG_BLUEDROID_ENABLED) m_keySize = key_size; esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &m_keySize, sizeof(uint8_t)); -} //setKeySize +#endif +} -/** - * Setup for static PIN connection, call it first and then call setAuthenticationMode eventually to change it - */ void BLESecurity::setStaticPIN(uint32_t pin) { - uint32_t passkey = pin; - esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t)); + m_passkey = pin; +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &m_passkey, sizeof(uint32_t)); setCapability(ESP_IO_CAP_OUT); setKeySize(); setAuthenticationMode(ESP_LE_AUTH_REQ_SC_ONLY); setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK); +#elif defined(CONFIG_NIMBLE_ENABLED) + setCapability(BLE_HS_IO_DISPLAY_ONLY); + setKeySize(); + setAuthenticationMode(false, false, true); // No bonding, no MITM, secure connection only + setInitEncryptionKey(BLE_HS_KEY_DIST_ENC_KEY | BLE_HS_KEY_DIST_ID_KEY); +#endif } -/** - * @brief Debug function to display what keys are exchanged by peers - */ +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) char *BLESecurity::esp_key_type_to_str(esp_ble_key_type_t key_type) { char *key_str = nullptr; switch (key_type) { @@ -89,7 +126,34 @@ char *BLESecurity::esp_key_type_to_str(esp_ble_key_type_t key_type) { default: key_str = (char *)"INVALID BLE KEY TYPE"; break; } return key_str; -} // esp_key_type_to_str +} +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +void BLESecurity::setAuthenticationMode(bool bonding, bool mitm, bool sc) { + m_authReq = bonding ? BLE_SM_PAIR_AUTHREQ_BOND : 0; + m_authReq |= mitm ? BLE_SM_PAIR_AUTHREQ_MITM : 0; + m_authReq |= sc ? BLE_SM_PAIR_AUTHREQ_SC : 0; + ble_hs_cfg.sm_bonding = bonding; + ble_hs_cfg.sm_mitm = mitm; + ble_hs_cfg.sm_sc = sc; +} + +bool BLESecurity::startSecurity(uint16_t connHandle, int *rcPtr) { + int rc = ble_gap_security_initiate(connHandle); + if (rc != 0) { + log_e("ble_gap_security_initiate: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); + } + if (rcPtr) { + *rcPtr = rc; + } + return rc == 0 || rc == BLE_HS_EALREADY; +} +#endif -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLESecurity.h b/libraries/BLE/src/BLESecurity.h index 2e3a44b42d1..574110e6118 100644 --- a/libraries/BLE/src/BLESecurity.h +++ b/libraries/BLE/src/BLESecurity.h @@ -3,74 +3,148 @@ * * Created on: Dec 17, 2017 * Author: chegewara + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on chegewara's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLESECURITY_H_ #define COMPONENTS_CPP_UTILS_BLESECURITY_H_ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + +#include "WString.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) #include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#endif +/*************************************************************************** + * Common definitions * + ***************************************************************************/ + +#define BLE_SM_DEFAULT_PASSKEY 123456 + +/*************************************************************************** + * Forward declarations * + ***************************************************************************/ + +class BLEServer; +class BLEClient; + +/** + * @brief Security management class + */ class BLESecurity { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLESecurity(); virtual ~BLESecurity(); - void setAuthenticationMode(esp_ble_auth_req_t auth_req); - void setCapability(esp_ble_io_cap_t iocap); - void setInitEncryptionKey(uint8_t init_key); - void setRespEncryptionKey(uint8_t resp_key); - void setKeySize(uint8_t key_size = 16); - void setStaticPIN(uint32_t pin); + static void setAuthenticationMode(uint8_t auth_req); + static void setCapability(uint8_t iocap); + static void setInitEncryptionKey(uint8_t init_key); + static void setRespEncryptionKey(uint8_t resp_key); + static void setKeySize(uint8_t key_size = 16); + static void setStaticPIN(uint32_t pin); + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) static char *esp_key_type_to_str(esp_ble_key_type_t key_type); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static void setAuthenticationMode(bool bonding, bool mitm, bool sc); + static bool startSecurity(uint16_t connHandle, int *rcPtr = nullptr); +#endif private: - esp_ble_auth_req_t m_authReq; - esp_ble_io_cap_t m_iocap; - uint8_t m_initKey; - uint8_t m_respKey; - uint8_t m_keySize; + friend class BLEServer; + friend class BLEClient; + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + + static uint8_t m_iocap; + static uint8_t m_authReq; + static uint8_t m_initKey; + static uint8_t m_respKey; + static uint32_t m_passkey; + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + static uint8_t m_keySize; +#endif }; // BLESecurity -/* +/** * @brief Callbacks to handle GAP events related to authorization */ class BLESecurityCallbacks { public: - virtual ~BLESecurityCallbacks(){}; + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ - /** - * @brief Its request from peer device to input authentication pin code displayed on peer device. - * It requires that our device is capable to input 6-digits code by end user - * @return Return 6-digits integer value from input device - */ + virtual ~BLESecurityCallbacks(){}; virtual uint32_t onPassKeyRequest() = 0; - - /** - * @brief Provide us 6-digits code to perform authentication. - * It requires that our device is capable to display this code to end user - * @param - */ virtual void onPassKeyNotify(uint32_t pass_key) = 0; + virtual bool onSecurityRequest() = 0; + virtual bool onConfirmPIN(uint32_t pin) = 0; - /** - * @brief Here we can make decision if we want to let negotiate authorization with peer device or not - * return Return true if we accept this peer device request - */ + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ - virtual bool onSecurityRequest() = 0; - /** - * Provide us information when authentication process is completed - */ +#if defined(CONFIG_BLUEDROID_ENABLED) virtual void onAuthenticationComplete(esp_ble_auth_cmpl_t) = 0; +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + virtual void onAuthenticationComplete(ble_gap_conn_desc *) = 0; +#endif - virtual bool onConfirmPIN(uint32_t pin) = 0; }; // BLESecurityCallbacks -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif // COMPONENTS_CPP_UTILS_BLESECURITY_H_ diff --git a/libraries/BLE/src/BLEServer.cpp b/libraries/BLE/src/BLEServer.cpp index a338cf61451..323237e965e 100644 --- a/libraries/BLE/src/BLEServer.cpp +++ b/libraries/BLE/src/BLEServer.cpp @@ -3,14 +3,23 @@ * * Created on: Apr 16, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/*************************************************************************** + * Common includes * + ***************************************************************************/ + #include -#include #include "GeneralUtils.h" #include "BLEDevice.h" #include "BLEServer.h" @@ -21,6 +30,27 @@ #include #include "esp32-hal-log.h" +/*************************************************************************** + * Bluedroid includes * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/*************************************************************************** + * NimBLE includes * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#endif + +/*************************************************************************** + * Common functions * + ***************************************************************************/ + /** * @brief Construct a %BLE Server * @@ -28,8 +58,17 @@ * the BLEDevice class. */ BLEServer::BLEServer() { - m_appId = ESP_GATT_IF_NONE; +#ifdef CONFIG_BLUEDROID_ENABLED m_gatts_if = ESP_GATT_IF_NONE; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + memset(m_indWait, BLE_HS_CONN_HANDLE_NONE, sizeof(m_indWait)); + m_svcChanged = false; +#endif + + m_appId = ESP_GATT_IF_NONE; + m_gattsStarted = false; m_connectedCount = 0; m_connId = ESP_GATT_IF_NONE; m_pServerCallbacks = nullptr; @@ -37,7 +76,9 @@ BLEServer::BLEServer() { void BLEServer::createApp(uint16_t appId) { m_appId = appId; +#ifdef CONFIG_BLUEDROID_ENABLED registerApp(appId); +#endif } // createApp /** @@ -64,7 +105,9 @@ BLEService *BLEServer::createService(const char *uuid) { */ BLEService *BLEServer::createService(BLEUUID uuid, uint32_t numHandles, uint8_t inst_id) { log_v(">> createService - %s", uuid.toString().c_str()); +#ifdef CONFIG_BLUEDROID_ENABLED m_semaphoreCreateEvt.take("createService"); +#endif // Check that a service with the supplied UUID does not already exist. if (m_serviceMap.getByUUID(uuid) != nullptr) { @@ -76,7 +119,14 @@ BLEService *BLEServer::createService(BLEUUID uuid, uint32_t numHandles, uint8_t m_serviceMap.setByUUID(uuid, pService); // Save a reference to this service being on this server. pService->executeCreate(this); // Perform the API calls to actually create the service. +#ifdef CONFIG_BLUEDROID_ENABLED m_semaphoreCreateEvt.wait("createService"); +#endif + +#ifdef CONFIG_NIMBLE_ENABLED + m_semaphoreCreateEvt.give(); + serviceChanged(); +#endif log_v("<< createService"); return pService; @@ -121,6 +171,190 @@ uint32_t BLEServer::getConnectedCount() { return m_connectedCount; } // getConnectedCount +void BLEServer::start() { + if (m_gattsStarted) { + return; + } + +#ifdef CONFIG_NIMBLE_ENABLED + int rc = ble_gatts_start(); + if (rc != 0) { + log_e("ble_gatts_start; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); + return; + } + +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + ble_gatts_show_local(); +#endif + + BLEService *svc = m_serviceMap.getFirst(); + while (svc != nullptr) { + if (svc->m_removed == 0) { + rc = ble_gatts_find_svc(&svc->getUUID().getNative()->u, &svc->m_handle); + if (rc != 0) { + abort(); + } + } + + BLECharacteristic *chr = svc->m_characteristicMap.getFirst(); + while (chr != nullptr) { + if ((chr->m_properties & BLE_GATT_CHR_F_INDICATE) || (chr->m_properties & BLE_GATT_CHR_F_NOTIFY)) { + m_notifyChrVec.push_back(chr); + } + chr = svc->m_characteristicMap.getNext(); + } + + svc = m_serviceMap.getNext(); + } + +#endif + + m_gattsStarted = true; +} + +/** + * @brief Set the server callbacks. + * + * As a %BLE server operates, it will generate server level events such as a new client connecting or a previous client + * disconnecting. This function can be called to register a callback handler that will be invoked when these + * events are detected. + * + * @param [in] pCallbacks The callbacks to be invoked. + */ +void BLEServer::setCallbacks(BLEServerCallbacks *pCallbacks) { + m_pServerCallbacks = pCallbacks; +} // setCallbacks + +/* + * Remove service + */ +void BLEServer::removeService(BLEService *service) { +#if defined(CONFIG_BLUEDROID_ENABLED) + service->stop(); + service->executeDelete(); + m_serviceMap.removeService(service); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + if (service->m_removed == 0) { + int rc = ble_gatts_svc_set_visibility(service->getHandle(), 0); + if (rc != 0) { + return; + } + service->m_removed = NIMBLE_ATT_REMOVE_DELETE; + serviceChanged(); + m_serviceMap.removeService(service); + BLEDevice::getAdvertising()->removeServiceUUID(service->getUUID()); + } +#endif +} + +/** + * @brief Start advertising. + * + * Start the server advertising its existence. This is a convenience function and is equivalent to + * retrieving the advertising object and invoking start upon it. + */ +void BLEServer::startAdvertising() { + log_v(">> startAdvertising"); + BLEDevice::startAdvertising(); + log_v("<< startAdvertising"); +} // startAdvertising + +/* multi connect support */ +/* TODO do some more tweaks */ +void BLEServer::updatePeerMTU(uint16_t conn_id, uint16_t mtu) { + // set mtu in conn_status_t + const std::map::iterator it = m_connectedServersMap.find(conn_id); + if (it != m_connectedServersMap.end()) { + it->second.mtu = mtu; + std::swap(m_connectedServersMap[conn_id], it->second); + } +} + +std::map BLEServer::getPeerDevices(bool _client) { + return m_connectedServersMap; +} + +uint16_t BLEServer::getPeerMTU(uint16_t conn_id) { +#if defined(CONFIG_BLUEDROID_ENABLED) + return m_connectedServersMap.find(conn_id)->second.mtu; +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + return ble_att_mtu(conn_id); +#endif +} + +void BLEServer::addPeerDevice(void *peer, bool _client, uint16_t conn_id) { + conn_status_t status = {.peer_device = peer, .connected = true, .mtu = 23}; + + m_connectedServersMap.insert(std::pair(conn_id, status)); +} + +bool BLEServer::removePeerDevice(uint16_t conn_id, bool _client) { + return m_connectedServersMap.erase(conn_id) > 0; +} + +void BLEServerCallbacks::onConnect(BLEServer *pServer) { + log_d("BLEServerCallbacks", ">> onConnect(): Default"); + log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); + log_d("BLEServerCallbacks", "<< onConnect()"); +} // onConnect + +void BLEServerCallbacks::onDisconnect(BLEServer *pServer) { + log_d("BLEServerCallbacks", ">> onDisconnect(): Default"); + log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); + log_d("BLEServerCallbacks", "<< onDisconnect()"); +} // onDisconnect + +/*************************************************************************** + * Bluedroid functions * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + +/** + * Allow to connect GATT server to peer device + * Probably can be used in ANCS for iPhone + */ +bool BLEServer::connect(BLEAddress address) { + esp_bd_addr_t addr; + memcpy(&addr, address.getNative(), 6); + // Perform the open connection request against the target BLE Server. + m_semaphoreOpenEvt.take("connect"); + esp_err_t errRc = ::esp_ble_gatts_open( + getGattsIf(), + addr, // address + 1 // direct connection + ); + if (errRc != ESP_OK) { + log_e("esp_ble_gattc_open: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return false; + } + + uint32_t rc = m_semaphoreOpenEvt.wait("connect"); // Wait for the connection to complete. + log_v("<< connect(), rc=%d", rc == ESP_GATT_OK); + return rc == ESP_GATT_OK; +} // connect + +/** + * Update connection parameters can be called only after connection has been established + */ +void BLEServer::updateConnParams(esp_bd_addr_t remote_bda, uint16_t minInterval, uint16_t maxInterval, uint16_t latency, uint16_t timeout) { + esp_ble_conn_update_params_t conn_params; + memcpy(conn_params.bda, remote_bda, sizeof(esp_bd_addr_t)); + conn_params.latency = latency; + conn_params.max_int = maxInterval; // max_int = 0x20*1.25ms = 40ms + conn_params.min_int = minInterval; // min_int = 0x10*1.25ms = 20ms + conn_params.timeout = timeout; // timeout = 400*10ms = 4000ms + esp_ble_gap_update_conn_params(&conn_params); +} + +void BLEServer::disconnect(uint16_t connId) { + esp_ble_gatts_close(m_gatts_if, connId); +} + uint16_t BLEServer::getGattsIf() { return m_gatts_if; } @@ -288,140 +522,450 @@ void BLEServer::registerApp(uint16_t m_appId) { log_v("<< registerApp"); } // registerApp +// Bluedroid callbacks + +void BLEServerCallbacks::onConnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { + log_d("BLEServerCallbacks", ">> onConnect(): Default"); + log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); + log_d("BLEServerCallbacks", "<< onConnect()"); +} // onConnect + +void BLEServerCallbacks::onDisconnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { + log_d("BLEServerCallbacks", ">> onDisconnect(): Default"); + log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); + log_d("BLEServerCallbacks", "<< onDisconnect()"); +} // onDisconnect + +void BLEServerCallbacks::onMtuChanged(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { + [[maybe_unused]] + uint16_t mtu = param->mtu.mtu; + log_d("BLEServerCallbacks", ">> onMtuChanged(): Default"); + log_d("BLEServerCallbacks", "Device: %s MTU: %d", BLEDevice::toString().c_str(), mtu); + log_d("BLEServerCallbacks", "<< onMtuChanged()"); +} // onMtuChanged + +#endif + +/*************************************************************************** + * NimBLE functions * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +uint16_t BLEServer::getHandle() { + return getConnId(); +} + /** - * @brief Set the server callbacks. + * @brief Resets the GATT server, used when services are added/removed after initialization. + */ +void BLEServer::resetGATT() { + if (getConnectedCount() > 0) { + return; + } + + BLEDevice::stopAdvertising(); + ble_gatts_reset(); + ble_svc_gap_init(); + ble_svc_gatt_init(); + + BLEService *svc = m_serviceMap.getFirst(); + while (svc != nullptr) { + if (svc->m_removed > 0) { + if (svc->m_removed == NIMBLE_ATT_REMOVE_DELETE) { + m_serviceMap.removeService(svc); + delete svc; + } + } else { + svc->start(); + } + + svc = m_serviceMap.getNext(); + } + + m_svcChanged = false; + m_gattsStarted = false; +} + +/** + * @brief Handle a GATT Server Event. * - * As a %BLE server operates, it will generate server level events such as a new client connecting or a previous client - * disconnecting. This function can be called to register a callback handler that will be invoked when these - * events are detected. + * @param [in] event + * @param [in] gatts_if + * @param [in] param * - * @param [in] pCallbacks The callbacks to be invoked. */ -void BLEServer::setCallbacks(BLEServerCallbacks *pCallbacks) { - m_pServerCallbacks = pCallbacks; -} // setCallbacks +int BLEServer::handleGATTServerEvent(struct ble_gap_event *event, void *arg) { + BLEServer *server = (BLEServer *)arg; + log_v(">> handleGAPEvent: %s", BLEUtils::gapEventToString(event->type)); + int rc = 0; + struct ble_gap_conn_desc desc; + + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + { + if (event->connect.status != 0) { + /* Connection failed; resume advertising */ + log_e("Connection failed"); + BLEDevice::startAdvertising(); + } else { + server->m_connId = event->connect.conn_handle; + server->addPeerDevice((void *)server, false, event->connect.conn_handle); + rc = ble_gap_conn_find(event->connect.conn_handle, &desc); + if (rc != 0) { + return 0; + } + + if (server->m_pServerCallbacks != nullptr) { + server->m_pServerCallbacks->onConnect(server); + server->m_pServerCallbacks->onConnect(server, &desc); + } + + server->m_connectedCount++; + } -/* - * Remove service - */ -void BLEServer::removeService(BLEService *service) { - service->stop(); - service->executeDelete(); - m_serviceMap.removeService(service); + return 0; + } // BLE_GAP_EVENT_CONNECT + + case BLE_GAP_EVENT_DISCONNECT: + { + // If Host reset tell the device now before returning to prevent + // any errors caused by calling host functions before resyncing. + switch (event->disconnect.reason) { + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: + log_d("Disconnect - host reset, rc=%d", event->disconnect.reason); + BLEDevice::onReset(event->disconnect.reason); + break; + default: break; + } + + if (server->removePeerDevice(event->disconnect.conn.conn_handle, false)) { + server->m_connectedCount--; + } + + if (server->m_svcChanged) { + server->resetGATT(); + } + + if (server->m_pServerCallbacks != nullptr) { + server->m_pServerCallbacks->onDisconnect(server); + server->m_pServerCallbacks->onDisconnect(server, &event->disconnect.conn); + } + + return 0; + } // BLE_GAP_EVENT_DISCONNECT + + case BLE_GAP_EVENT_SUBSCRIBE: + { + log_i("subscribe event; attr_handle=%d, subscribed: %s", event->subscribe.attr_handle, (event->subscribe.cur_notify ? "true" : "false")); + + for (auto &it : server->m_notifyChrVec) { + if (it->getHandle() == event->subscribe.attr_handle) { + uint16_t properties = it->getProperties(); + if ((properties & BLE_GATT_CHR_F_READ_AUTHEN) || (properties & BLE_GATT_CHR_F_READ_AUTHOR) || (properties & BLE_GATT_CHR_F_READ_ENC)) { + rc = ble_gap_conn_find(event->subscribe.conn_handle, &desc); + if (rc != 0) { + break; + } + + if (!desc.sec_state.encrypted) { + BLESecurity::startSecurity(event->subscribe.conn_handle); + } + } + + it->setSubscribe(event); + break; + } + } + + return 0; + } // BLE_GAP_EVENT_SUBSCRIBE + + case BLE_GAP_EVENT_MTU: + { + log_i("mtu update event; conn_handle=%d mtu=%d", event->mtu.conn_handle, event->mtu.value); + rc = ble_gap_conn_find(event->mtu.conn_handle, &desc); + if (rc != 0) { + return 0; + } + + if (server->m_pServerCallbacks != nullptr) { + server->m_pServerCallbacks->onMtuChanged(server, &desc, event->mtu.value); + } + return 0; + } // BLE_GAP_EVENT_MTU + + case BLE_GAP_EVENT_NOTIFY_TX: + { + BLECharacteristic *pChar = nullptr; + + for (auto &it : server->m_notifyChrVec) { + if (it->getHandle() == event->notify_tx.attr_handle) { + pChar = it; + } + } + + if (pChar == nullptr) { + return 0; + } + + BLECharacteristicCallbacks::Status statusRC; + + if (event->notify_tx.indication) { + if (event->notify_tx.status != 0) { + if (event->notify_tx.status == BLE_HS_EDONE) { + statusRC = BLECharacteristicCallbacks::Status::SUCCESS_INDICATE; + } else if (rc == BLE_HS_ETIMEOUT) { + statusRC = BLECharacteristicCallbacks::Status::ERROR_INDICATE_TIMEOUT; + } else { + statusRC = BLECharacteristicCallbacks::Status::ERROR_INDICATE_FAILURE; + } + } else { + return 0; + } + + server->clearIndicateWait(event->notify_tx.conn_handle); + } else { + if (event->notify_tx.status == 0) { + statusRC = BLECharacteristicCallbacks::Status::SUCCESS_NOTIFY; + } else { + statusRC = BLECharacteristicCallbacks::Status::ERROR_GATT; + } + } + + pChar->m_pCallbacks->onStatus(pChar, statusRC, event->notify_tx.status); + + return 0; + } // BLE_GAP_EVENT_NOTIFY_TX + + case BLE_GAP_EVENT_ADV_COMPLETE: + { + log_d("Advertising Complete"); + BLEDevice::getAdvertising()->advCompleteCB(); + return 0; + } + + case BLE_GAP_EVENT_CONN_UPDATE: + { + log_d("Connection parameters updated."); + return 0; + } // BLE_GAP_EVENT_CONN_UPDATE + + case BLE_GAP_EVENT_REPEAT_PAIRING: + { + /* We already have a bond with the peer, but it is attempting to + * establish a new secure link. This app sacrifices security for + * convenience: just throw away the old bond and accept the new link. + */ + + /* Delete the old bond. */ + rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc != 0) { + return BLE_GAP_REPEAT_PAIRING_IGNORE; + } + + ble_store_util_delete_peer(&desc.peer_id_addr); + + /* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should + * continue with the pairing operation. + */ + return BLE_GAP_REPEAT_PAIRING_RETRY; + } // BLE_GAP_EVENT_REPEAT_PAIRING + + case BLE_GAP_EVENT_ENC_CHANGE: + { + rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc); + if (rc != 0) { + return BLE_ATT_ERR_INVALID_HANDLE; + } + + if (BLEDevice::m_securityCallbacks != nullptr) { + BLEDevice::m_securityCallbacks->onAuthenticationComplete(&desc); + } else if (server->m_pServerCallbacks != nullptr) { + server->m_pServerCallbacks->onAuthenticationComplete(&desc); + } + + return 0; + } // BLE_GAP_EVENT_ENC_CHANGE + + case BLE_GAP_EVENT_PASSKEY_ACTION: + { + struct ble_sm_io pkey = {0, 0}; + + if (event->passkey.params.action == BLE_SM_IOACT_DISP) { + pkey.action = event->passkey.params.action; + // backward compatibility + pkey.passkey = BLESecurity::m_passkey; + // if the (static)passkey is the default, check the callback for custom value + // both values default to the same. + if (pkey.passkey == BLE_SM_DEFAULT_PASSKEY) { + if (server->m_pServerCallbacks != nullptr) { + pkey.passkey = server->m_pServerCallbacks->onPassKeyRequest(); + } + } + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("BLE_SM_IOACT_DISP; ble_sm_inject_io result: %d", rc); + + } else if (event->passkey.params.action == BLE_SM_IOACT_NUMCMP) { + log_d("Passkey on device's display: %d", event->passkey.params.numcmp); + pkey.action = event->passkey.params.action; + // Compatibility only - Do not use, should be removed the in future + if (BLEDevice::m_securityCallbacks != nullptr) { + pkey.numcmp_accept = BLEDevice::m_securityCallbacks->onConfirmPIN(event->passkey.params.numcmp); + } else if (server->m_pServerCallbacks != nullptr) { + pkey.numcmp_accept = server->m_pServerCallbacks->onConfirmPIN(event->passkey.params.numcmp); + } + + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("BLE_SM_IOACT_NUMCMP; ble_sm_inject_io result: %d", rc); + + //TODO: Handle out of band pairing + } else if (event->passkey.params.action == BLE_SM_IOACT_OOB) { + static uint8_t tem_oob[16] = {0}; + pkey.action = event->passkey.params.action; + for (int i = 0; i < 16; i++) { + pkey.oob[i] = tem_oob[i]; + } + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("BLE_SM_IOACT_OOB; ble_sm_inject_io result: %d", rc); + } else if (event->passkey.params.action == BLE_SM_IOACT_INPUT) { + log_d("Enter the passkey"); + pkey.action = event->passkey.params.action; + + // Compatibility only - Do not use, should be removed the in future + if (BLEDevice::m_securityCallbacks != nullptr) { + pkey.passkey = BLEDevice::m_securityCallbacks->onPassKeyRequest(); + } else if (server->m_pServerCallbacks != nullptr) { + pkey.passkey = server->m_pServerCallbacks->onPassKeyRequest(); + } + + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + log_d("BLE_SM_IOACT_INPUT; ble_sm_inject_io result: %d", rc); + + } else if (event->passkey.params.action == BLE_SM_IOACT_NONE) { + log_d("No passkey action required"); + } + + log_d("<< handleGATTServerEvent"); + return 0; + } // BLE_GAP_EVENT_PASSKEY_ACTION + + default: break; + } + + log_d("<< handleGATTServerEvent"); + return 0; } /** - * @brief Start advertising. - * - * Start the server advertising its existence. This is a convenience function and is equivalent to - * retrieving the advertising object and invoking start upon it. + * @brief Request an Update the connection parameters: + * * Can only be used after a connection has been established. + * @param [in] conn_handle The connection handle of the peer to send the request to. + * @param [in] minInterval The minimum connection interval in 1.25ms units. + * @param [in] maxInterval The maximum connection interval in 1.25ms units. + * @param [in] latency The number of packets allowed to skip (extends max interval). + * @param [in] timeout The timeout time in 10ms units before disconnecting. */ -void BLEServer::startAdvertising() { - log_v(">> startAdvertising"); - BLEDevice::startAdvertising(); - log_v("<< startAdvertising"); -} // startAdvertising +void BLEServer::updateConnParams(uint16_t conn_handle, uint16_t minInterval, uint16_t maxInterval, uint16_t latency, uint16_t timeout) { + ble_gap_upd_params params; + + params.latency = latency; + params.itvl_max = maxInterval; // max_int = 0x20*1.25ms = 40ms + params.itvl_min = minInterval; // min_int = 0x10*1.25ms = 20ms + params.supervision_timeout = timeout; // timeout = 400*10ms = 4000ms + params.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN; // Minimum length of connection event in 0.625ms units + params.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN; // Maximum length of connection event in 0.625ms units + + int rc = ble_gap_update_params(conn_handle, ¶ms); + if (rc != 0) { + log_e("Update params error: %d, %s", rc, BLEUtils::returnCodeToString(rc)); + } +} // updateConnParams + +bool BLEServer::setIndicateWait(uint16_t conn_handle) { + for (auto i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++) { + if (m_indWait[i] == conn_handle) { + return false; + } + } + + return true; +} + +void BLEServer::clearIndicateWait(uint16_t conn_handle) { + for (auto i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++) { + if (m_indWait[i] == conn_handle) { + m_indWait[i] = BLE_HS_CONN_HANDLE_NONE; + return; + } + } +} /** - * Allow to connect GATT server to peer device - * Probably can be used in ANCS for iPhone + * @brief Disconnect the specified client with optional reason. + * @param [in] connId Connection Id of the client to disconnect. + * @param [in] reason code for disconnecting. + * @return NimBLE host return code. */ -bool BLEServer::connect(BLEAddress address) { - esp_bd_addr_t addr; - memcpy(&addr, address.getNative(), 6); - // Perform the open connection request against the target BLE Server. - m_semaphoreOpenEvt.take("connect"); - esp_err_t errRc = ::esp_ble_gatts_open( - getGattsIf(), - addr, // address - 1 // direct connection - ); - if (errRc != ESP_OK) { - log_e("esp_ble_gattc_open: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return false; +int BLEServer::disconnect(uint16_t connId, uint8_t reason) { + log_d(">> disconnect()"); + + int rc = ble_gap_terminate(connId, reason); + if (rc != 0) { + log_e("ble_gap_terminate failed: rc=%d %s", rc, BLEUtils::returnCodeToString(rc)); } - uint32_t rc = m_semaphoreOpenEvt.wait("connect"); // Wait for the connection to complete. - log_v("<< connect(), rc=%d", rc == ESP_GATT_OK); - return rc == ESP_GATT_OK; -} // connect + log_d("<< disconnect()"); + return rc; +} // disconnect -void BLEServerCallbacks::onConnect(BLEServer *pServer) { - log_d("BLEServerCallbacks", ">> onConnect(): Default"); - log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); - log_d("BLEServerCallbacks", "<< onConnect()"); -} // onConnect +/** + * @brief Set the service changed flag + */ +void BLEServer::serviceChanged() { + if (m_gattsStarted) { + m_svcChanged = true; + } +} // serviceChanged -void BLEServerCallbacks::onConnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { +// NimBLE callbacks + +void BLEServerCallbacks::onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) { log_d("BLEServerCallbacks", ">> onConnect(): Default"); log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); log_d("BLEServerCallbacks", "<< onConnect()"); } // onConnect -void BLEServerCallbacks::onDisconnect(BLEServer *pServer) { +void BLEServerCallbacks::onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) { log_d("BLEServerCallbacks", ">> onDisconnect(): Default"); log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); log_d("BLEServerCallbacks", "<< onDisconnect()"); } // onDisconnect -void BLEServerCallbacks::onDisconnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { - log_d("BLEServerCallbacks", ">> onDisconnect(): Default"); - log_d("BLEServerCallbacks", "Device: %s", BLEDevice::toString().c_str()); - log_d("BLEServerCallbacks", "<< onDisconnect()"); -} // onDisconnect - -void BLEServerCallbacks::onMtuChanged(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { +void BLEServerCallbacks::onMtuChanged(BLEServer *pServer, ble_gap_conn_desc *desc, uint16_t mtu) { log_d("BLEServerCallbacks", ">> onMtuChanged(): Default"); - log_d("BLEServerCallbacks", "Device: %s MTU: %d", BLEDevice::toString().c_str(), param->mtu.mtu); + log_d("BLEServerCallbacks", "Device: %s MTU: %d", BLEDevice::toString().c_str(), mtu); log_d("BLEServerCallbacks", "<< onMtuChanged()"); } // onMtuChanged -/* multi connect support */ -/* TODO do some more tweaks */ -void BLEServer::updatePeerMTU(uint16_t conn_id, uint16_t mtu) { - // set mtu in conn_status_t - const std::map::iterator it = m_connectedServersMap.find(conn_id); - if (it != m_connectedServersMap.end()) { - it->second.mtu = mtu; - std::swap(m_connectedServersMap[conn_id], it->second); - } +uint32_t BLEServerCallbacks::onPassKeyRequest() { + log_d("BLEServerCallbacks", "onPassKeyRequest: default: 123456"); + return 123456; } -std::map BLEServer::getPeerDevices(bool _client) { - return m_connectedServersMap; +void BLEServerCallbacks::onAuthenticationComplete(ble_gap_conn_desc *) { + log_d("BLEServerCallbacks", "onAuthenticationComplete: default"); } -uint16_t BLEServer::getPeerMTU(uint16_t conn_id) { - return m_connectedServersMap.find(conn_id)->second.mtu; +bool BLEServerCallbacks::onConfirmPIN(uint32_t pin) { + log_d("BLEServerCallbacks", "onConfirmPIN: default: true"); + return true; } -void BLEServer::addPeerDevice(void *peer, bool _client, uint16_t conn_id) { - conn_status_t status = {.peer_device = peer, .connected = true, .mtu = 23}; - - m_connectedServersMap.insert(std::pair(conn_id, status)); -} - -bool BLEServer::removePeerDevice(uint16_t conn_id, bool _client) { - return m_connectedServersMap.erase(conn_id) > 0; -} -/* multi connect support */ - -/** - * Update connection parameters can be called only after connection has been established - */ -void BLEServer::updateConnParams(esp_bd_addr_t remote_bda, uint16_t minInterval, uint16_t maxInterval, uint16_t latency, uint16_t timeout) { - esp_ble_conn_update_params_t conn_params; - memcpy(conn_params.bda, remote_bda, sizeof(esp_bd_addr_t)); - conn_params.latency = latency; - conn_params.max_int = maxInterval; // max_int = 0x20*1.25ms = 40ms - conn_params.min_int = minInterval; // min_int = 0x10*1.25ms = 20ms - conn_params.timeout = timeout; // timeout = 400*10ms = 4000ms - esp_ble_gap_update_conn_params(&conn_params); -} - -void BLEServer::disconnect(uint16_t connId) { - esp_ble_gatts_close(m_gatts_if, connId); -} +#endif // CONFIG_NIMBLE_ENABLED -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEServer.h b/libraries/BLE/src/BLEServer.h index aa10f2210fa..865d1046312 100644 --- a/libraries/BLE/src/BLEServer.h +++ b/libraries/BLE/src/BLEServer.h @@ -3,6 +3,10 @@ * * Created on: Apr 16, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLESERVER_H_ @@ -11,13 +15,16 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ #include #include -// #include "BLEDevice.h" - +#include "BLEDevice.h" +#include "BLEConnInfo.h" #include "BLEUUID.h" #include "BLEAdvertising.h" #include "BLECharacteristic.h" @@ -25,24 +32,51 @@ #include "BLESecurity.h" #include "RTOS.h" #include "BLEAddress.h" +#include "BLEUtils.h" +#include "BLEUtils.h" + +/***************************************************************************** + * Bluedroid includes * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/***************************************************************************** + * NimBLE includes and definitions * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#define ESP_GATT_IF_NONE BLE_HS_CONN_HANDLE_NONE +#define NIMBLE_ATT_REMOVE_HIDE 1 +#define NIMBLE_ATT_REMOVE_DELETE 2 +#endif + +/***************************************************************************** + * Forward declarations * + *****************************************************************************/ class BLEServerCallbacks; -/* TODO possibly refactor this struct */ -typedef struct { - void *peer_device; // peer device BLEClient or BLEServer - maybe its better to have 2 structures or union here - bool connected; // do we need it? - uint16_t mtu; // every peer device negotiate own mtu -} conn_status_t; +class BLEService; +class BLECharacteristic; +class BLEDevice; +class BLESecurity; +class BLEAdvertising; /** - * @brief A data structure that manages the %BLE servers owned by a BLE server. + * @brief A data structure that manages the %BLE services owned by a BLE server. */ class BLEServiceMap { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEService *getByHandle(uint16_t handle); BLEService *getByUUID(const char *uuid); BLEService *getByUUID(BLEUUID uuid, uint8_t inst_id = 0); - void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void setByHandle(uint16_t handle, BLEService *service); void setByUUID(const char *uuid, BLEService *service); void setByUUID(BLEUUID uuid, BLEService *service); @@ -52,7 +86,19 @@ class BLEServiceMap { void removeService(BLEService *service); int getRegisteredServiceCount(); + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif + private: + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + std::map m_handleMap; std::map m_uuidMap; std::map::iterator m_iterator; @@ -63,6 +109,16 @@ class BLEServiceMap { */ class BLEServer { public: + /*************************************************************************** + * Common public properties * + ***************************************************************************/ + + uint16_t m_appId; + + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + uint32_t getConnectedCount(); BLEService *createService(const char *uuid); BLEService *createService(BLEUUID uuid, uint32_t numHandles = 15, uint8_t inst_id = 0); @@ -72,12 +128,9 @@ class BLEServer { void removeService(BLEService *service); BLEService *getServiceByUUID(const char *uuid); BLEService *getServiceByUUID(BLEUUID uuid); - bool connect(BLEAddress address); - void disconnect(uint16_t connId); - uint16_t m_appId; - void updateConnParams(esp_bd_addr_t remote_bda, uint16_t minInterval, uint16_t maxInterval, uint16_t latency, uint16_t timeout); + void start(); - /* multi connection support */ + // Connection management functions std::map getPeerDevices(bool client); void addPeerDevice(void *peer, bool is_client, uint16_t conn_id); bool removePeerDevice(uint16_t conn_id, bool client); @@ -86,28 +139,95 @@ class BLEServer { uint16_t getPeerMTU(uint16_t conn_id); uint16_t getConnId(); + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + bool connect(BLEAddress address); + void updateConnParams(esp_bd_addr_t remote_bda, uint16_t minInterval, uint16_t maxInterval, uint16_t latency, uint16_t timeout); + void disconnect(uint16_t connId); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + uint16_t getHandle(); + void updateConnParams(uint16_t conn_handle, uint16_t minInterval, uint16_t maxInterval, uint16_t latency, uint16_t timeout); + int disconnect(uint16_t connId, uint8_t reason = BLE_ERR_REM_USER_CONN_TERM); +#endif + private: - BLEServer(); friend class BLEService; friend class BLECharacteristic; friend class BLEDevice; - esp_ble_adv_data_t m_adv_data; - // BLEAdvertising m_bleAdvertising; + friend class BLESecurity; + friend class BLEAdvertising; + + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + uint16_t m_connId; uint32_t m_connectedCount; - uint16_t m_gatts_if; + bool m_gattsStarted; std::map m_connectedServersMap; - FreeRTOS::Semaphore m_semaphoreRegisterAppEvt = FreeRTOS::Semaphore("RegisterAppEvt"); FreeRTOS::Semaphore m_semaphoreCreateEvt = FreeRTOS::Semaphore("CreateEvt"); FreeRTOS::Semaphore m_semaphoreOpenEvt = FreeRTOS::Semaphore("OpenEvt"); BLEServiceMap m_serviceMap; BLEServerCallbacks *m_pServerCallbacks = nullptr; + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + uint16_t m_gatts_if; + esp_ble_adv_data_t m_adv_data; +#endif + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + bool m_svcChanged; + uint16_t m_indWait[CONFIG_BT_NIMBLE_MAX_CONNECTIONS]; + std::vector m_notifyChrVec; + ble_hs_adv_fields m_adv_data; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + BLEServer(); void createApp(uint16_t appId); + + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) uint16_t getGattsIf(); - void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void registerApp(uint16_t); + void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE private declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void serviceChanged(); + void resetGATT(); + bool setIndicateWait(uint16_t conn_handle); + void clearIndicateWait(uint16_t conn_handle); + static int handleGATTServerEvent(struct ble_gap_event *event, void *arg); +#endif }; // BLEServer /** @@ -115,37 +235,38 @@ class BLEServer { */ class BLEServerCallbacks { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + virtual ~BLEServerCallbacks(){}; - /** - * @brief Handle a new client connection. - * - * When a new client connects, we are invoked. - * - * @param [in] pServer A reference to the %BLE server that received the client connection. - */ virtual void onConnect(BLEServer *pServer); - virtual void onConnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param); - /** - * @brief Handle an existing client disconnection. - * - * When an existing client disconnects, we are invoked. - * - * @param [in] pServer A reference to the %BLE server that received the existing client disconnection. - */ virtual void onDisconnect(BLEServer *pServer); - virtual void onDisconnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param); - /** - * @brief Handle a new client connection. - * - * When the MTU changes this method is invoked. - * - * @param [in] pServer A reference to the %BLE server that received the client connection. - * @param [in] param A reference to esp_ble_gatts_cb_param_t. - */ + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + virtual void onConnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param); + virtual void onDisconnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param); virtual void onMtuChanged(BLEServer *pServer, esp_ble_gatts_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + virtual void onConnect(BLEServer *pServer, ble_gap_conn_desc *desc); + virtual void onDisconnect(BLEServer *pServer, ble_gap_conn_desc *desc); + virtual void onMtuChanged(BLEServer *pServer, ble_gap_conn_desc *desc, uint16_t mtu); + virtual uint32_t onPassKeyRequest(); + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc); + virtual bool onConfirmPIN(uint32_t pin); +#endif }; // BLEServerCallbacks -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLESERVER_H_ */ diff --git a/libraries/BLE/src/BLEService.cpp b/libraries/BLE/src/BLEService.cpp index 58c5d4eb9c3..60449719b6a 100644 --- a/libraries/BLE/src/BLEService.cpp +++ b/libraries/BLE/src/BLEService.cpp @@ -3,6 +3,10 @@ * * Created on: Mar 25, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ // A service is identified by a UUID. A service is also the container for one or more characteristics. @@ -11,10 +15,13 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include -#include +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) +/***************************************************************************** + * Common includes * + *****************************************************************************/ + +#include #include #include #include @@ -25,8 +32,24 @@ #include "GeneralUtils.h" #include "esp32-hal-log.h" +/***************************************************************************** + * Bluedroid includes * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/***************************************************************************** + * Common definitions * + *****************************************************************************/ + #define NULL_HANDLE (0xffff) +/***************************************************************************** + * Common functions * + *****************************************************************************/ + /** * @brief Construct an instance of the BLEService * @param [in] uuid The UUID of the service. @@ -44,10 +67,32 @@ BLEService::BLEService(BLEUUID uuid, uint16_t numHandles) { m_handle = NULL_HANDLE; m_pServer = nullptr; //m_serializeMutex.setName("BLEService"); - m_lastCreatedCharacteristic = nullptr; m_numHandles = numHandles; +#ifdef CONFIG_BLUEDROID_ENABLED + m_lastCreatedCharacteristic = nullptr; +#endif +#if defined(CONFIG_NIMBLE_ENABLED) + m_pSvcDef = nullptr; +#endif } // BLEService +BLEService::~BLEService() { +#if defined(CONFIG_NIMBLE_ENABLED) + if (m_pSvcDef != nullptr) { + if (m_pSvcDef->characteristics != nullptr) { + for (int i = 0; m_pSvcDef->characteristics[i].uuid != NULL; ++i) { + if (m_pSvcDef->characteristics[i].descriptors) { + delete (m_pSvcDef->characteristics[i].descriptors); + } + } + delete (m_pSvcDef->characteristics); + } + + delete (m_pSvcDef); + } +#endif +} + /** * @brief Create the service. * Create the service. @@ -56,8 +101,9 @@ BLEService::BLEService(BLEUUID uuid, uint16_t numHandles) { */ void BLEService::executeCreate(BLEServer *pServer) { - log_v(">> executeCreate() - Creating service (esp_ble_gatts_create_service) service uuid: %s", getUUID().toString().c_str()); + log_v(">> executeCreate() - Creating service with uuid: %s", getUUID().toString().c_str()); m_pServer = pServer; +#if defined(CONFIG_BLUEDROID_ENABLED) m_semaphoreCreateEvt.take("executeCreate"); // Take the mutex and release at event ESP_GATTS_CREATE_EVT esp_gatt_srvc_id_t srvc_id; @@ -73,6 +119,7 @@ void BLEService::executeCreate(BLEServer *pServer) { } m_semaphoreCreateEvt.wait("executeCreate"); +#endif log_v("<< executeCreate"); } // executeCreate @@ -83,6 +130,7 @@ void BLEService::executeCreate(BLEServer *pServer) { */ void BLEService::executeDelete() { +#if defined(CONFIG_BLUEDROID_ENABLED) log_v(">> executeDelete()"); m_semaphoreDeleteEvt.take("executeDelete"); // Take the mutex and release at event ESP_GATTS_DELETE_EVT @@ -95,6 +143,11 @@ void BLEService::executeDelete() { m_semaphoreDeleteEvt.wait("executeDelete"); log_v("<< executeDelete"); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) && CONFIG_BT_NIMBLE_DYNAMIC_SERVICE + ble_gatts_delete_svc(&m_uuid.getNative()->u); +#endif } // executeDelete /** @@ -114,49 +167,11 @@ BLEUUID BLEService::getUUID() { return m_uuid; } // getUUID -/** - * @brief Start the service. - * Here we wish to start the service which means that we will respond to partner requests about it. - * Starting a service also means that we can create the corresponding characteristics. - * @return Start the service. - */ -void BLEService::start() { - // We ask the BLE runtime to start the service and then create each of the characteristics. - // We start the service through its local handle which was returned in the ESP_GATTS_CREATE_EVT event - // obtained as a result of calling esp_ble_gatts_create_service(). - // - log_v(">> start(): Starting service (esp_ble_gatts_start_service): %s", toString().c_str()); - if (m_handle == NULL_HANDLE) { - log_e("<< !!! We attempted to start a service but don't know its handle!"); - return; - } - - BLECharacteristic *pCharacteristic = m_characteristicMap.getFirst(); - - while (pCharacteristic != nullptr) { - m_lastCreatedCharacteristic = pCharacteristic; - pCharacteristic->executeCreate(this); - - pCharacteristic = m_characteristicMap.getNext(); - } - // Start each of the characteristics ... these are found in the m_characteristicMap. - - m_semaphoreStartEvt.take("start"); - esp_err_t errRc = ::esp_ble_gatts_start_service(m_handle); - - if (errRc != ESP_OK) { - log_e("<< esp_ble_gatts_start_service: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); - return; - } - m_semaphoreStartEvt.wait("start"); - - log_v("<< start()"); -} // start - /** * @brief Stop the service. */ void BLEService::stop() { +#if defined(CONFIG_BLUEDROID_ENABLED) // We ask the BLE runtime to start the service and then create each of the characteristics. // We start the service through its local handle which was returned in the ESP_GATTS_CREATE_EVT event // obtained as a result of calling esp_ble_gatts_create_service(). @@ -176,13 +191,19 @@ void BLEService::stop() { m_semaphoreStopEvt.wait("stop"); log_v("<< stop()"); -} // start +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + log_w("NimBLE does not support stopping a service. Ignoring request."); +#endif +} // stop /** * @brief Set the handle associated with this service. * @param [in] handle The handle associated with the service. */ void BLEService::setHandle(uint16_t handle) { +#if defined(CONFIG_BLUEDROID_ENABLED) log_v(">> setHandle - Handle=0x%.2x, service UUID=%s)", handle, getUUID().toString().c_str()); if (m_handle != NULL_HANDLE) { log_e("!!! Handle is already set %.2x", m_handle); @@ -190,6 +211,11 @@ void BLEService::setHandle(uint16_t handle) { } m_handle = handle; log_v("<< setHandle"); +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + log_w("NimBLE does not support manually setting the handle of a service. Ignoring request."); +#endif } // setHandle /** @@ -213,14 +239,25 @@ void BLEService::addCharacteristic(BLECharacteristic *pCharacteristic) { log_d("Adding characteristic: uuid=%s to service: %s", pCharacteristic->getUUID().toString().c_str(), toString().c_str()); // Check that we don't add the same characteristic twice. - if (m_characteristicMap.getByUUID(pCharacteristic->getUUID()) != nullptr) { + BLECharacteristic *pExisting = m_characteristicMap.getByUUID(pCharacteristic->getUUID()); + if (pExisting != nullptr) { log_w("<< Adding a new characteristic with the same UUID as a previous one"); - //return; } - // Remember this characteristic in our map of characteristics. At this point, we can lookup by UUID - // but not by handle. The handle is allocated to us on the ESP_GATTS_ADD_CHAR_EVT. - m_characteristicMap.setByUUID(pCharacteristic, pCharacteristic->getUUID()); +#if defined(CONFIG_NIMBLE_ENABLED) + if (pExisting != nullptr) { + pExisting->m_removed = 0; + } else +#endif + { + // Remember this characteristic in our map of characteristics. At this point, we can lookup by UUID + // but not by handle. The handle is allocated to us on the ESP_GATTS_ADD_CHAR_EVT. + m_characteristicMap.setByUUID(pCharacteristic, pCharacteristic->getUUID()); + } + +#if defined(CONFIG_NIMBLE_ENABLED) + getServer()->serviceChanged(); +#endif log_v("<< addCharacteristic()"); } // addCharacteristic @@ -247,6 +284,54 @@ BLECharacteristic *BLEService::createCharacteristic(BLEUUID uuid, uint32_t prope return pCharacteristic; } // createCharacteristic +BLECharacteristic *BLEService::getCharacteristic(const char *uuid) { + return getCharacteristic(BLEUUID(uuid)); +} + +BLECharacteristic *BLEService::getCharacteristic(BLEUUID uuid) { + return m_characteristicMap.getByUUID(uuid); +} + +/** + * @brief Return a string representation of this service. + * A service is defined by: + * * Its UUID + * * Its handle + * @return A string representation of this service. + */ +String BLEService::toString() { + String res = "UUID: " + getUUID().toString(); + char hex[5]; + snprintf(hex, sizeof(hex), "%04x", getHandle()); + res += ", handle: 0x"; + res += hex; + return res; +} // toString + +/** + * @brief Get the BLE server associated with this service. + * @return The BLEServer associated with this service. + */ +BLEServer *BLEService::getServer() { + return m_pServer; +} // getServer + +/***************************************************************************** + * Bluedroid functions * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +/** + * @brief Get the last created characteristic. + * It is lamentable that this function has to exist. It returns the last created characteristic. + * We need this because the descriptor API is built around the notion that a new descriptor, when created, + * is associated with the last characteristics created and we need that information. + * @return The last created characteristic. + */ +BLECharacteristic *BLEService::getLastCreatedCharacteristic() { + return m_lastCreatedCharacteristic; +} // getLastCreatedCharacteristic + /** * @brief Handle a GATTS server event. */ @@ -347,48 +432,222 @@ void BLEService::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t m_characteristicMap.handleGATTServerEvent(event, gatts_if, param); } // handleGATTServerEvent -BLECharacteristic *BLEService::getCharacteristic(const char *uuid) { - return getCharacteristic(BLEUUID(uuid)); -} - -BLECharacteristic *BLEService::getCharacteristic(BLEUUID uuid) { - return m_characteristicMap.getByUUID(uuid); -} - /** - * @brief Return a string representation of this service. - * A service is defined by: - * * Its UUID - * * Its handle - * @return A string representation of this service. + * @brief Start the service. + * Here we wish to start the service which means that we will respond to partner requests about it. + * Starting a service also means that we can create the corresponding characteristics. + * @return Start the service. */ -String BLEService::toString() { - String res = "UUID: " + getUUID().toString(); - char hex[5]; - snprintf(hex, sizeof(hex), "%04x", getHandle()); - res += ", handle: 0x"; - res += hex; - return res; -} // toString +bool BLEService::start() { + // We ask the BLE runtime to start the service and then create each of the characteristics. + // We start the service through its local handle which was returned in the ESP_GATTS_CREATE_EVT event + // obtained as a result of calling esp_ble_gatts_create_service(). + // + log_v(">> start(): Starting service (esp_ble_gatts_start_service): %s", toString().c_str()); + if (m_handle == NULL_HANDLE) { + log_e("<< !!! We attempted to start a service but don't know its handle!"); + return false; + } + + BLECharacteristic *pCharacteristic = m_characteristicMap.getFirst(); + + while (pCharacteristic != nullptr) { + m_lastCreatedCharacteristic = pCharacteristic; + pCharacteristic->executeCreate(this); + + pCharacteristic = m_characteristicMap.getNext(); + } + // Start each of the characteristics ... these are found in the m_characteristicMap. + + m_semaphoreStartEvt.take("start"); + esp_err_t errRc = ::esp_ble_gatts_start_service(m_handle); + + if (errRc != ESP_OK) { + log_e("<< esp_ble_gatts_start_service: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); + return false; + } + m_semaphoreStartEvt.wait("start"); + + log_v("<< start()"); + return true; +} // start + +#endif // CONFIG_BLUEDROID_ENABLED + +/***************************************************************************** + * NimBLE functions * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) /** - * @brief Get the last created characteristic. - * It is lamentable that this function has to exist. It returns the last created characteristic. - * We need this because the descriptor API is built around the notion that a new descriptor, when created, - * is associated with the last characteristics created and we need that information. - * @return The last created characteristic. + * @brief Remove a characteristic from the service. Check if the characteristic was already removed and if so, check if this + * is being called to delete the object and do so if requested. Otherwise, ignore the call and return. + * @param [in] pCharacteristic - The characteristic to remove. + * @param [in] deleteChr - If true, delete the characteristic. */ -BLECharacteristic *BLEService::getLastCreatedCharacteristic() { - return m_lastCreatedCharacteristic; -} // getLastCreatedCharacteristic +void BLEService::removeCharacteristic(BLECharacteristic *pCharacteristic, bool deleteChr) { + if (pCharacteristic->m_removed > 0) { + if (deleteChr) { + BLECharacteristic *pExisting = m_characteristicMap.getByUUID(pCharacteristic->getUUID()); + if (pExisting != nullptr) { + m_characteristicMap.removeCharacteristic(pExisting); + delete pExisting; + } + } + + return; + } + + pCharacteristic->m_removed = deleteChr ? NIMBLE_ATT_REMOVE_DELETE : NIMBLE_ATT_REMOVE_HIDE; + getServer()->serviceChanged(); +} /** - * @brief Get the BLE server associated with this service. - * @return The BLEServer associated with this service. + * @brief Builds the database of characteristics/descriptors for the service + * and registers it with the NimBLE stack. + * @return bool success/failure . */ -BLEServer *BLEService::getServer() { - return m_pServer; -} // getServer +bool BLEService::start() { + log_d(">> start(): Starting service: %s", toString().c_str()); + + // Rebuild the service definition if the server attributes have changed. + if (getServer()->m_svcChanged && m_pSvcDef != nullptr) { + if (m_pSvcDef[0].characteristics) { + if (m_pSvcDef[0].characteristics[0].descriptors) { + delete (m_pSvcDef[0].characteristics[0].descriptors); + } + delete (m_pSvcDef[0].characteristics); + } + delete (m_pSvcDef); + m_pSvcDef = nullptr; + } + + if (m_pSvcDef == nullptr) { + // Nimble requires an array of services to be sent to the api + // Since we are adding 1 at a time we create an array of 2 and set the type + // of the second service to 0 to indicate the end of the array. + ble_gatt_svc_def *svc = new ble_gatt_svc_def[2]{}; + ble_gatt_chr_def *pChr_a = nullptr; + ble_gatt_dsc_def *pDsc_a = nullptr; + + svc[0].type = BLE_GATT_SVC_TYPE_PRIMARY; + svc[0].uuid = (const ble_uuid_t *)&(m_uuid.getNative()->u); + svc[0].includes = nullptr; + + int removedCount = 0; + BLECharacteristic *pCharacteristic; + + pCharacteristic = m_characteristicMap.getFirst(); + while (pCharacteristic != nullptr) { + if (pCharacteristic->m_removed > 0) { + if (pCharacteristic->m_removed == NIMBLE_ATT_REMOVE_DELETE) { + m_characteristicMap.removeCharacteristic(pCharacteristic); + delete pCharacteristic; + } else { + ++removedCount; + } + } else { + pCharacteristic->executeCreate(this); + } + + pCharacteristic = m_characteristicMap.getNext(); + } + + size_t numChrs = m_characteristicMap.getRegisteredCharacteristicCount() - removedCount; + log_d("Adding %d characteristics for service %s", numChrs, toString().c_str()); + + if (!numChrs) { + svc[0].characteristics = nullptr; + } else { + // Nimble requires the last characteristic to have it's uuid = 0 to indicate the end + // of the characteristics for the service. We create 1 extra and set it to null + // for this purpose. + pChr_a = new ble_gatt_chr_def[numChrs + 1]{}; + uint8_t i = 0; + pCharacteristic = m_characteristicMap.getFirst(); + while (pCharacteristic != nullptr) { + if (pCharacteristic->m_removed <= 0) { + removedCount = 0; + BLEDescriptor *pDescriptor; + + pDescriptor = pCharacteristic->m_descriptorMap.getFirst(); + while (pDescriptor != nullptr) { + if (pDescriptor->m_removed > 0) { + if (pDescriptor->m_removed == NIMBLE_ATT_REMOVE_DELETE) { + pCharacteristic->m_descriptorMap.removeDescriptor(pDescriptor); + delete pDescriptor; + } else { + ++removedCount; + } + } + pDescriptor = pCharacteristic->m_descriptorMap.getNext(); + } + + size_t numDscs = pCharacteristic->m_descriptorMap.getRegisteredDescriptorCount() - removedCount; + log_d("Adding %d descriptors for characteristic %s", numDscs, pCharacteristic->getUUID().toString().c_str()); + + if (!numDscs) { + pChr_a[i].descriptors = nullptr; + } else { + // Must have last descriptor uuid = 0 so we have to create 1 extra + pDsc_a = new ble_gatt_dsc_def[numDscs + 1]{}; + uint8_t d = 0; + pDescriptor = pCharacteristic->m_descriptorMap.getFirst(); + while (pDescriptor != nullptr) { + if (pDescriptor->m_removed <= 0) { + pDsc_a[d].uuid = (const ble_uuid_t *)&(pDescriptor->m_bleUUID.getNative()->u); + pDsc_a[d].att_flags = pDescriptor->m_permissions; + pDsc_a[d].min_key_size = 0; + pDsc_a[d].access_cb = BLEDescriptor::handleGATTServerEvent; + pDsc_a[d].arg = pDescriptor; + ++d; + } + pDescriptor = pCharacteristic->m_descriptorMap.getNext(); + } + + pDsc_a[numDscs].uuid = nullptr; + pChr_a[i].descriptors = pDsc_a; + } + + pChr_a[i].uuid = (const ble_uuid_t *)&(pCharacteristic->m_bleUUID.getNative()->u); + pChr_a[i].access_cb = BLECharacteristic::handleGATTServerEvent; + pChr_a[i].arg = pCharacteristic; + pChr_a[i].flags = pCharacteristic->m_properties; + pChr_a[i].min_key_size = 0; + pChr_a[i].val_handle = &pCharacteristic->m_handle; + ++i; + } + + pCharacteristic = m_characteristicMap.getNext(); + } + + pChr_a[numChrs].uuid = nullptr; + svc[0].characteristics = pChr_a; + } + + // end of services must indicate to api with type = 0 + svc[1].type = 0; + m_pSvcDef = svc; + } + + int rc = ble_gatts_count_cfg((const ble_gatt_svc_def *)m_pSvcDef); + if (rc != 0) { + log_e("ble_gatts_count_cfg failed, rc= %d, %s", rc, BLEUtils::returnCodeToString(rc)); + return false; + } + + rc = ble_gatts_add_svcs((const ble_gatt_svc_def *)m_pSvcDef); + if (rc != 0) { + log_e("ble_gatts_add_svcs, rc= %d, %s", rc, BLEUtils::returnCodeToString(rc)); + return false; + } + + log_d("<< start()"); + return true; +} // start + +#endif -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEService.h b/libraries/BLE/src/BLEService.h index 61f867b2a02..67088b60310 100644 --- a/libraries/BLE/src/BLEService.h +++ b/libraries/BLE/src/BLEService.h @@ -3,6 +3,10 @@ * * Created on: Mar 25, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLESERVICE_H_ @@ -11,15 +15,38 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) -#include +/***************************************************************************** + * Common includes * + *****************************************************************************/ #include "BLECharacteristic.h" #include "BLEServer.h" #include "BLEUUID.h" +#include "BLEUtils.h" #include "RTOS.h" +/***************************************************************************** + * Bluedroid includes * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#endif + +/***************************************************************************** + * NimBLE includes * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#endif + +/***************************************************************************** + * Forward declarations * + *****************************************************************************/ + class BLEServer; /** @@ -27,6 +54,10 @@ class BLEServer; */ class BLECharacteristicMap { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + void setByUUID(BLECharacteristic *pCharacteristic, const char *uuid); void setByUUID(BLECharacteristic *pCharacteristic, BLEUUID uuid); void setByHandle(uint16_t handle, BLECharacteristic *pCharacteristic); @@ -36,9 +67,30 @@ class BLECharacteristicMap { BLECharacteristic *getFirst(); BLECharacteristic *getNext(); String toString(); + int getRegisteredCharacteristicCount(); + void removeCharacteristic(BLECharacteristic *characteristic); + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, ble_gatt_access_ctxt *ctxt, void *arg); +#endif private: + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + std::map m_uuidMap; std::map m_handleMap; std::map::iterator m_iterator; @@ -46,10 +98,19 @@ class BLECharacteristicMap { /** * @brief The model of a %BLE service. - * */ class BLEService { public: + /*************************************************************************** + * Common properties * + ***************************************************************************/ + + uint8_t m_instId = 0; + + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + void addCharacteristic(BLECharacteristic *pCharacteristic); BLECharacteristic *createCharacteristic(const char *uuid, uint32_t properties); BLECharacteristic *createCharacteristic(BLEUUID uuid, uint32_t properties); @@ -60,40 +121,81 @@ class BLEService { BLECharacteristic *getCharacteristic(BLEUUID uuid); BLEUUID getUUID(); BLEServer *getServer(); - void start(); + bool start(); void stop(); String toString(); uint16_t getHandle(); - uint8_t m_instId = 0; + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + void removeCharacteristic(BLECharacteristic *pCharacteristic, bool deleteChr = false); +#endif private: - BLEService(const char *uuid, uint16_t numHandles); - BLEService(BLEUUID uuid, uint16_t numHandles); friend class BLEServer; friend class BLEServiceMap; friend class BLEDescriptor; friend class BLECharacteristic; friend class BLEDevice; + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + BLECharacteristicMap m_characteristicMap; uint16_t m_handle; BLECharacteristic *m_lastCreatedCharacteristic = nullptr; BLEServer *m_pServer = nullptr; BLEUUID m_uuid; + FreeRTOS::Semaphore m_semaphoreStartEvt = FreeRTOS::Semaphore("StartEvt"); + uint16_t m_numHandles; + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ +#if defined(CONFIG_BLUEDROID_ENABLED) FreeRTOS::Semaphore m_semaphoreCreateEvt = FreeRTOS::Semaphore("CreateEvt"); FreeRTOS::Semaphore m_semaphoreDeleteEvt = FreeRTOS::Semaphore("DeleteEvt"); - FreeRTOS::Semaphore m_semaphoreStartEvt = FreeRTOS::Semaphore("StartEvt"); FreeRTOS::Semaphore m_semaphoreStopEvt = FreeRTOS::Semaphore("StopEvt"); +#endif - uint16_t m_numHandles; + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ +#if defined(CONFIG_NIMBLE_ENABLED) + uint8_t m_removed; + ble_gatt_svc_def *m_pSvcDef; +#endif + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + BLEService(const char *uuid, uint16_t numHandles); + BLEService(BLEUUID uuid, uint16_t numHandles); + ~BLEService(); + + /*************************************************************************** + * Common private declarations * + ***************************************************************************/ + + void setHandle(uint16_t handle); + + /*************************************************************************** + * Bluedroid private declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) BLECharacteristic *getLastCreatedCharacteristic(); void handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void setHandle(uint16_t handle); - //void setService(esp_gatt_srvc_id_t srvc_id); +#endif }; // BLEService -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLESERVICE_H_ */ diff --git a/libraries/BLE/src/BLEServiceMap.cpp b/libraries/BLE/src/BLEServiceMap.cpp index 30a9db499f1..477da5b4cc2 100644 --- a/libraries/BLE/src/BLEServiceMap.cpp +++ b/libraries/BLE/src/BLEServiceMap.cpp @@ -3,16 +3,30 @@ * * Created on: Jun 22, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + #include #include #include "BLEService.h" +/***************************************************************************** + * Common functions * + *****************************************************************************/ + /** * @brief Return the service by UUID. * @param [in] UUID The UUID to look up the service. @@ -82,13 +96,6 @@ String BLEServiceMap::toString() { return res; } // toString -void BLEServiceMap::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - // Invoke the handler for every Service we have. - for (auto &myPair : m_uuidMap) { - myPair.first->handleGATTServerEvent(event, gatts_if, param); - } -} - /** * @brief Get the first service in the map. * @return The first service in the map. @@ -130,8 +137,21 @@ void BLEServiceMap::removeService(BLEService *service) { * @return amount of registered services */ int BLEServiceMap::getRegisteredServiceCount() { - return m_handleMap.size(); + return m_uuidMap.size(); +} + +/***************************************************************************** + * Bluedroid functions * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +void BLEServiceMap::handleGATTServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { + // Invoke the handler for every Service we have. + for (auto &myPair : m_uuidMap) { + myPair.first->handleGATTServerEvent(event, gatts_if, param); + } } +#endif -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEUUID.cpp b/libraries/BLE/src/BLEUUID.cpp index 8074ea82f8f..d61fb695799 100644 --- a/libraries/BLE/src/BLEUUID.cpp +++ b/libraries/BLE/src/BLEUUID.cpp @@ -3,12 +3,22 @@ * * Created on: Jun 21, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + #include #include #include @@ -18,25 +28,10 @@ #include "BLEUUID.h" #include "esp32-hal-log.h" -/** - * @brief Copy memory from source to target but in reverse order. - * - * When we move memory from one location it is normally: - * - * ``` - * [0][1][2]...[n] -> [0][1][2]...[n] - * ``` - * - * with this function, it is: - * - * ``` - * [0][1][2]...[n] -> [n][n-1][n-2]...[0] - * ``` - * - * @param [in] target The target of the copy - * @param [in] source The source of the copy - * @param [in] size The number of bytes to copy - */ +/***************************************************************************** + * Common functions * + *****************************************************************************/ + static void memrcpy(uint8_t *target, uint8_t *source, uint32_t size) { assert(size > 0); target += (size - 1); // Point target to the last byte of the target data @@ -48,29 +43,15 @@ static void memrcpy(uint8_t *target, uint8_t *source, uint32_t size) { } } // memrcpy -/** - * @brief Create a UUID from a string. - * - * Create a UUID from a string. There will be two possible stories here. Either the string represents - * a binary data field or the string represents a hex encoding of a UUID. - * For the hex encoding, here is an example: - * - * ``` - * "beb5483e-36e1-4688-b7f5-ea07361b26a8" - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - * 12345678-90ab-cdef-1234-567890abcdef - * ``` - * - * This has a length of 36 characters. We need to parse this into 16 bytes. - * - * @param [in] value The string to build a UUID from. - */ +BLEUUID::BLEUUID() { + m_valueSet = false; +} // BLEUUID + BLEUUID::BLEUUID(String value) { - //Serial.printf("BLEUUID constructor from String=\"%s\"\n", value.c_str()); m_valueSet = true; if (value.length() == 4) { - m_uuid.len = ESP_UUID_LEN_16; - m_uuid.uuid.uuid16 = 0; + UUID_LEN(m_uuid) = BLE_UUID_16_BITS; + UUID_VAL_16(m_uuid) = 0; for (int i = 0; i < value.length();) { uint8_t MSB = value.c_str()[i]; uint8_t LSB = value.c_str()[i + 1]; @@ -81,12 +62,12 @@ BLEUUID::BLEUUID(String value) { if (LSB > '9') { LSB -= 7; } - m_uuid.uuid.uuid16 += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (2 - i) * 4; + UUID_VAL_16(m_uuid) += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (2 - i) * 4; i += 2; } } else if (value.length() == 8) { - m_uuid.len = ESP_UUID_LEN_32; - m_uuid.uuid.uuid32 = 0; + UUID_LEN(m_uuid) = BLE_UUID_32_BITS; + UUID_VAL_32(m_uuid) = 0; for (int i = 0; i < value.length();) { uint8_t MSB = value.c_str()[i]; uint8_t LSB = value.c_str()[i + 1]; @@ -97,18 +78,14 @@ BLEUUID::BLEUUID(String value) { if (LSB > '9') { LSB -= 7; } - m_uuid.uuid.uuid32 += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (6 - i) * 4; + UUID_VAL_32(m_uuid) += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (6 - i) * 4; i += 2; } - } else if (value.length() - == 16) { // How we can have 16 byte length string representing 128 bit uuid??? needs to be investigated (lack of time) - maybe raw data encoded as String (128b==16B)? - m_uuid.len = ESP_UUID_LEN_128; - memrcpy(m_uuid.uuid.uuid128, (uint8_t *)value.c_str(), 16); + } else if (value.length() == 16) { + UUID_LEN(m_uuid) = BLE_UUID_128_BITS; + memrcpy(UUID_VAL_128(m_uuid), (uint8_t *)value.c_str(), 16); } else if (value.length() == 36) { - //log_d("36 characters:"); - // If the length of the string is 36 bytes then we will assume it is a long hex string in - // UUID format. - m_uuid.len = ESP_UUID_LEN_128; + UUID_LEN(m_uuid) = BLE_UUID_128_BITS; int n = 0; for (int i = 0; i < value.length();) { if (value.c_str()[i] == '-') { @@ -123,7 +100,7 @@ BLEUUID::BLEUUID(String value) { if (LSB > '9') { LSB -= 7; } - m_uuid.uuid.uuid128[15 - n++] = ((MSB & 0x0F) << 4) | (LSB & 0x0F); + UUID_VAL_128(m_uuid)[15 - n++] = ((MSB & 0x0F) << 4) | (LSB & 0x0F); i += 2; } } else { @@ -132,128 +109,73 @@ BLEUUID::BLEUUID(String value) { } } //BLEUUID(String) -/* -BLEUUID::BLEUUID(String value) { - this.BLEUUID(String(value.c_str(), value.length())); -} //BLEUUID(String) -*/ - -/** - * @brief Create a UUID from 16 bytes of memory. - * - * @param [in] pData The pointer to the start of the UUID. - * @param [in] size The size of the data. - * @param [in] msbFirst Is the MSB first in pData memory? - */ BLEUUID::BLEUUID(uint8_t *pData, size_t size, bool msbFirst) { if (size != 16) { log_e("ERROR: UUID length not 16 bytes"); return; } - m_uuid.len = ESP_UUID_LEN_128; + UUID_LEN(m_uuid) = BLE_UUID_128_BITS; if (msbFirst) { - memrcpy(m_uuid.uuid.uuid128, pData, 16); + memrcpy(UUID_VAL_128(m_uuid), pData, 16); } else { - memcpy(m_uuid.uuid.uuid128, pData, 16); + memcpy(UUID_VAL_128(m_uuid), pData, 16); } m_valueSet = true; } // BLEUUID -/** - * @brief Create a UUID from the 16bit value. - * - * @param [in] uuid The 16bit short form UUID. - */ BLEUUID::BLEUUID(uint16_t uuid) { - m_uuid.len = ESP_UUID_LEN_16; - m_uuid.uuid.uuid16 = uuid; + UUID_LEN(m_uuid) = BLE_UUID_16_BITS; + UUID_VAL_16(m_uuid) = uuid; m_valueSet = true; } // BLEUUID -/** - * @brief Create a UUID from the 32bit value. - * - * @param [in] uuid The 32bit short form UUID. - */ BLEUUID::BLEUUID(uint32_t uuid) { - m_uuid.len = ESP_UUID_LEN_32; - m_uuid.uuid.uuid32 = uuid; + UUID_LEN(m_uuid) = BLE_UUID_32_BITS; + UUID_VAL_32(m_uuid) = uuid; m_valueSet = true; } // BLEUUID -/** - * @brief Create a UUID from the native UUID. - * - * @param [in] uuid The native UUID. - */ -BLEUUID::BLEUUID(esp_bt_uuid_t uuid) { - m_uuid = uuid; +BLEUUID::BLEUUID(uint32_t first, uint16_t second, uint16_t third, uint64_t fourth) { + UUID_LEN(m_uuid) = BLE_UUID_128_BITS; + memcpy(UUID_VAL_128(m_uuid) + 12, &first, 4); + memcpy(UUID_VAL_128(m_uuid) + 10, &second, 2); + memcpy(UUID_VAL_128(m_uuid) + 8, &third, 2); + memcpy(UUID_VAL_128(m_uuid), &fourth, 8); m_valueSet = true; -} // BLEUUID - -/** - * @brief Create a UUID from the ESP32 esp_gat_id_t. - * - * @param [in] gattId The data to create the UUID from. - */ -BLEUUID::BLEUUID(esp_gatt_id_t gattId) : BLEUUID(gattId.uuid) {} // BLEUUID - -BLEUUID::BLEUUID() { - m_valueSet = false; -} // BLEUUID +} -/** - * @brief Get the number of bits in this uuid. - * @return The number of bits in the UUID. One of 16, 32 or 128. - */ uint8_t BLEUUID::bitSize() { if (!m_valueSet) { return 0; } - switch (m_uuid.len) { - case ESP_UUID_LEN_16: return 16; - case ESP_UUID_LEN_32: return 32; - case ESP_UUID_LEN_128: return 128; - default: log_e("Unknown UUID length: %d", m_uuid.len); return 0; + switch (UUID_LEN(m_uuid)) { + case BLE_UUID_16_BITS: return 16; + case BLE_UUID_32_BITS: return 32; + case BLE_UUID_128_BITS: return 128; + default: log_e("Unknown UUID length: %d", UUID_LEN(m_uuid)); return 0; } // End of switch } // bitSize -/** - * @brief Compare a UUID against this UUID. - * - * @param [in] uuid The UUID to compare against. - * @return True if the UUIDs are equal and false otherwise. - */ -bool BLEUUID::equals(BLEUUID uuid) { - //log_d("Comparing: %s to %s", toString().c_str(), uuid.toString().c_str()); +bool BLEUUID::equals(const BLEUUID &uuid) const { if (!m_valueSet || !uuid.m_valueSet) { return false; } - if (uuid.m_uuid.len != m_uuid.len) { + if (UUID_LEN(uuid.m_uuid) != UUID_LEN(m_uuid)) { return uuid.toString() == toString(); } - if (uuid.m_uuid.len == ESP_UUID_LEN_16) { - return uuid.m_uuid.uuid.uuid16 == m_uuid.uuid.uuid16; + if (UUID_LEN(uuid.m_uuid) == BLE_UUID_16_BITS) { + return UUID_VAL_16(uuid.m_uuid) == UUID_VAL_16(m_uuid); } - if (uuid.m_uuid.len == ESP_UUID_LEN_32) { - return uuid.m_uuid.uuid.uuid32 == m_uuid.uuid.uuid32; + if (UUID_LEN(uuid.m_uuid) == BLE_UUID_32_BITS) { + return UUID_VAL_32(uuid.m_uuid) == UUID_VAL_32(m_uuid); } - return memcmp(uuid.m_uuid.uuid.uuid128, m_uuid.uuid.uuid128, 16) == 0; + return memcmp(UUID_VAL_128(uuid.m_uuid), UUID_VAL_128(m_uuid), 16) == 0; } // equals -/** - * Create a BLEUUID from a string of the form: - * 0xNNNN - * 0xNNNNNNNN - * 0x - * NNNN - * NNNNNNNN - * - */ BLEUUID BLEUUID::fromString(String _uuid) { uint8_t start = 0; if (strstr(_uuid.c_str(), "0x") != nullptr) { // If the string starts with 0x, skip those characters. @@ -273,117 +195,142 @@ BLEUUID BLEUUID::fromString(String _uuid) { return BLEUUID(); } // fromString -/** - * @brief Get the native UUID value. - * - * @return The native UUID value or NULL if not set. - */ -esp_bt_uuid_t *BLEUUID::getNative() { - //log_d(">> getNative()") - if (m_valueSet == false) { - log_v("<< Return of un-initialized UUID!"); - return nullptr; - } - //log_d("<< getNative()"); - return &m_uuid; -} // getNative - -/** - * @brief Convert a UUID to its 128 bit representation. - * - * A UUID can be internally represented as 16bit, 32bit or the full 128bit. This method - * will convert 16 or 32 bit representations to the full 128bit. - */ BLEUUID BLEUUID::to128() { - //log_v(">> toFull() - %s", toString().c_str()); - - // If we either don't have a value or are already a 128 bit UUID, nothing further to do. - if (!m_valueSet || m_uuid.len == ESP_UUID_LEN_128) { + if (!m_valueSet || UUID_LEN(m_uuid) == BLE_UUID_128_BITS) { return *this; } - // If we are 16 bit or 32 bit, then set the 4 bytes of the variable part of the UUID. - if (m_uuid.len == ESP_UUID_LEN_16) { - uint16_t temp = m_uuid.uuid.uuid16; - m_uuid.uuid.uuid128[15] = 0; - m_uuid.uuid.uuid128[14] = 0; - m_uuid.uuid.uuid128[13] = (temp >> 8) & 0xff; - m_uuid.uuid.uuid128[12] = temp & 0xff; - - } else if (m_uuid.len == ESP_UUID_LEN_32) { - uint32_t temp = m_uuid.uuid.uuid32; - m_uuid.uuid.uuid128[15] = (temp >> 24) & 0xff; - m_uuid.uuid.uuid128[14] = (temp >> 16) & 0xff; - m_uuid.uuid.uuid128[13] = (temp >> 8) & 0xff; - m_uuid.uuid.uuid128[12] = temp & 0xff; + if (UUID_LEN(m_uuid) == BLE_UUID_16_BITS) { + uint16_t temp = UUID_VAL_16(m_uuid); + UUID_VAL_128(m_uuid)[15] = 0; + UUID_VAL_128(m_uuid)[14] = 0; + UUID_VAL_128(m_uuid)[13] = (temp >> 8) & 0xff; + UUID_VAL_128(m_uuid)[12] = temp & 0xff; + } else if (UUID_LEN(m_uuid) == BLE_UUID_32_BITS) { + uint16_t temp = UUID_VAL_32(m_uuid); + UUID_VAL_128(m_uuid)[15] = (temp >> 24) & 0xff; + UUID_VAL_128(m_uuid)[14] = (temp >> 16) & 0xff; + UUID_VAL_128(m_uuid)[13] = (temp >> 8) & 0xff; + UUID_VAL_128(m_uuid)[12] = temp & 0xff; } - // Set the fixed parts of the UUID. - m_uuid.uuid.uuid128[11] = 0x00; - m_uuid.uuid.uuid128[10] = 0x00; - - m_uuid.uuid.uuid128[9] = 0x10; - m_uuid.uuid.uuid128[8] = 0x00; + UUID_VAL_128(m_uuid)[11] = 0x00; + UUID_VAL_128(m_uuid)[10] = 0x00; + UUID_VAL_128(m_uuid)[9] = 0x10; + UUID_VAL_128(m_uuid)[8] = 0x00; + UUID_VAL_128(m_uuid)[7] = 0x80; + UUID_VAL_128(m_uuid)[6] = 0x00; + UUID_VAL_128(m_uuid)[5] = 0x00; + UUID_VAL_128(m_uuid)[4] = 0x80; + UUID_VAL_128(m_uuid)[3] = 0x5f; + UUID_VAL_128(m_uuid)[2] = 0x9b; + UUID_VAL_128(m_uuid)[1] = 0x34; + UUID_VAL_128(m_uuid)[0] = 0xfb; + + UUID_LEN(m_uuid) = BLE_UUID_128_BITS; + return *this; +} // to128 - m_uuid.uuid.uuid128[7] = 0x80; - m_uuid.uuid.uuid128[6] = 0x00; +BLEUUID BLEUUID::to16() { + if (!m_valueSet || UUID_LEN(m_uuid) == BLE_UUID_16_BITS) { + return *this; + } - m_uuid.uuid.uuid128[5] = 0x00; - m_uuid.uuid.uuid128[4] = 0x80; - m_uuid.uuid.uuid128[3] = 0x5f; - m_uuid.uuid.uuid128[2] = 0x9b; - m_uuid.uuid.uuid128[1] = 0x34; - m_uuid.uuid.uuid128[0] = 0xfb; + if (UUID_LEN(m_uuid) == BLE_UUID_128_BITS) { + uint8_t base128[] = {0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00}; + if (memcmp(UUID_VAL_128(m_uuid), base128, sizeof(base128)) == 0) { + *this = BLEUUID(*(uint16_t *)(UUID_VAL_128(m_uuid) + 12)); + } + } - m_uuid.len = ESP_UUID_LEN_128; - //log_d("<< toFull <- %s", toString().c_str()); return *this; -} // to128 +} -/** - * @brief Get a string representation of the UUID. - * - * The format of a string is: - * 01234567 8901 2345 6789 012345678901 - * 0000180d-0000-1000-8000-00805f9b34fb - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - * - * @return A string representation of the UUID. - */ -String BLEUUID::toString() { +String BLEUUID::toString() const { if (!m_valueSet) { return ""; // If we have no value, nothing to format. } - // If the UUIDs are 16 or 32 bit, pad correctly. - if (m_uuid.len == ESP_UUID_LEN_16) { // If the UUID is 16bit, pad correctly. + if (UUID_LEN(m_uuid) == BLE_UUID_16_BITS) { // If the UUID is 16bit, pad correctly. char hex[9]; - snprintf(hex, sizeof(hex), "%08x", m_uuid.uuid.uuid16); + snprintf(hex, sizeof(hex), "%08x", UUID_VAL_16(m_uuid)); return String(hex) + "-0000-1000-8000-00805f9b34fb"; } // End 16bit UUID - if (m_uuid.len == ESP_UUID_LEN_32) { // If the UUID is 32bit, pad correctly. + if (UUID_LEN(m_uuid) == BLE_UUID_32_BITS) { // If the UUID is 32bit, pad correctly. char hex[9]; - snprintf(hex, sizeof(hex), "%08lx", m_uuid.uuid.uuid32); + snprintf(hex, sizeof(hex), "%08lx", UUID_VAL_32(m_uuid)); return String(hex) + "-0000-1000-8000-00805f9b34fb"; } // End 32bit UUID // The UUID is not 16bit or 32bit which means that it is 128bit. - // - // UUID string format: - // AABBCCDD-EEFF-GGHH-IIJJ-KKLLMMNNOOPP auto size = 37; // 32 for UUID data, 4 for '-' delimiters and one for a terminator == 37 chars char *hex = (char *)malloc(size); snprintf( - hex, size, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", m_uuid.uuid.uuid128[15], m_uuid.uuid.uuid128[14], - m_uuid.uuid.uuid128[13], m_uuid.uuid.uuid128[12], m_uuid.uuid.uuid128[11], m_uuid.uuid.uuid128[10], m_uuid.uuid.uuid128[9], m_uuid.uuid.uuid128[8], - m_uuid.uuid.uuid128[7], m_uuid.uuid.uuid128[6], m_uuid.uuid.uuid128[5], m_uuid.uuid.uuid128[4], m_uuid.uuid.uuid128[3], m_uuid.uuid.uuid128[2], - m_uuid.uuid.uuid128[1], m_uuid.uuid.uuid128[0] + hex, size, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", UUID_VAL_128(m_uuid)[15], UUID_VAL_128(m_uuid)[14], + UUID_VAL_128(m_uuid)[13], UUID_VAL_128(m_uuid)[12], UUID_VAL_128(m_uuid)[11], UUID_VAL_128(m_uuid)[10], UUID_VAL_128(m_uuid)[9], UUID_VAL_128(m_uuid)[8], + UUID_VAL_128(m_uuid)[7], UUID_VAL_128(m_uuid)[6], UUID_VAL_128(m_uuid)[5], UUID_VAL_128(m_uuid)[4], UUID_VAL_128(m_uuid)[3], UUID_VAL_128(m_uuid)[2], + UUID_VAL_128(m_uuid)[1], UUID_VAL_128(m_uuid)[0] ); + String res(hex); free(hex); return res; } // toString -#endif /* CONFIG_BLUEDROID_ENABLED */ +bool BLEUUID::operator==(const BLEUUID &rhs) const { + return equals(rhs); +} + +bool BLEUUID::operator!=(const BLEUUID &rhs) const { + return !equals(rhs); +} + +/***************************************************************************** + * Bluedroid functions * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +BLEUUID::BLEUUID(esp_bt_uuid_t uuid) { + m_uuid = uuid; + m_valueSet = true; +} // BLEUUID + +BLEUUID::BLEUUID(esp_gatt_id_t gattId) : BLEUUID(gattId.uuid) {} // BLEUUID + +esp_bt_uuid_t *BLEUUID::getNative() { + if (m_valueSet == false) { + log_v("<< Return of un-initialized UUID!"); + return nullptr; + } + return &m_uuid; +} // getNative +#endif + +/***************************************************************************** + * NimBLE functions * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +BLEUUID::BLEUUID(ble_uuid_any_t uuid) { + m_uuid = uuid; + m_valueSet = true; +} // BLEUUID + +BLEUUID::BLEUUID(const ble_uuid128_t *uuid) { + m_uuid.u.type = BLE_UUID_TYPE_128; + memcpy(m_uuid.u128.value, uuid->value, 16); + m_valueSet = true; +} // BLEUUID + +const ble_uuid_any_t *BLEUUID::getNative() const { + if (m_valueSet == false) { + log_v("<< Return of un-initialized UUID!"); + return nullptr; + } + return &m_uuid; +} // getNative +#endif + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEUUID.h b/libraries/BLE/src/BLEUUID.h index 1be013942e3..43c4e91b01d 100644 --- a/libraries/BLE/src/BLEUUID.h +++ b/libraries/BLE/src/BLEUUID.h @@ -3,42 +3,125 @@ * * Created on: Jun 21, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEUUID_H_ #define COMPONENTS_CPP_UTILS_BLEUUID_H_ #include "soc/soc_caps.h" -#include "WString.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if CONFIG_BLUEDROID_ENABLED +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + +#include "WString.h" + +/***************************************************************************** + * Bluedroid includes and definitions * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) #include +#define BLE_UUID_16_BITS ESP_UUID_LEN_16 +#define BLE_UUID_32_BITS ESP_UUID_LEN_32 +#define BLE_UUID_128_BITS ESP_UUID_LEN_128 +#define UUID_LEN(s) s.len +#define UUID_VAL_16(s) s.uuid.uuid16 +#define UUID_VAL_32(s) s.uuid.uuid32 +#define UUID_VAL_128(s) s.uuid.uuid128 +#endif + +/***************************************************************************** + * NimBLE includes and definitions * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#define BLE_UUID_16_BITS BLE_UUID_TYPE_16 +#define BLE_UUID_32_BITS BLE_UUID_TYPE_32 +#define BLE_UUID_128_BITS BLE_UUID_TYPE_128 +#define UUID_LEN(s) s.u.type +#define UUID_VAL_16(s) s.u16.value +#define UUID_VAL_32(s) s.u32.value +#define UUID_VAL_128(s) s.u128.value +#endif /** * @brief A model of a %BLE UUID. */ class BLEUUID { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEUUID(String uuid); BLEUUID(uint16_t uuid); BLEUUID(uint32_t uuid); - BLEUUID(esp_bt_uuid_t uuid); BLEUUID(uint8_t *pData, size_t size, bool msbFirst); - BLEUUID(esp_gatt_id_t gattId); + BLEUUID(uint32_t first, uint16_t second, uint16_t third, uint64_t fourth); BLEUUID(); uint8_t bitSize(); // Get the number of bits in this uuid. - bool equals(BLEUUID uuid); - esp_bt_uuid_t *getNative(); + bool equals(const BLEUUID &uuid) const; BLEUUID to128(); - String toString(); + BLEUUID to16(); + String toString() const; static BLEUUID fromString(String uuid); // Create a BLEUUID from a string + bool operator==(const BLEUUID &rhs) const; + bool operator!=(const BLEUUID &rhs) const; + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + BLEUUID(esp_bt_uuid_t uuid); + BLEUUID(esp_gatt_id_t gattId); + esp_bt_uuid_t *getNative(); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + BLEUUID(ble_uuid_any_t uuid); + BLEUUID(const ble_uuid128_t *uuid); + const ble_uuid_any_t *getNative() const; +#endif + private: - esp_bt_uuid_t m_uuid; // The underlying UUID structure that this class wraps. + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + bool m_valueSet = false; // Is there a value set for this instance. + + /*************************************************************************** + * Bluedroid private properties * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + esp_bt_uuid_t m_uuid; // The underlying UUID structure that this class wraps. +#endif + + /*************************************************************************** + * NimBLE private properties * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + ble_uuid_any_t m_uuid; // The underlying UUID structure that this class wraps. +#endif }; // BLEUUID -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEUUID_H_ */ diff --git a/libraries/BLE/src/BLEUtils.cpp b/libraries/BLE/src/BLEUtils.cpp index 05e1e32deed..4ca04e6e2b6 100644 --- a/libraries/BLE/src/BLEUtils.cpp +++ b/libraries/BLE/src/BLEUtils.cpp @@ -3,12 +3,22 @@ * * Created on: Mar 25, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + #include "BLEAddress.h" #include "BLEClient.h" #include "BLEUtils.h" @@ -17,17 +27,56 @@ #include #include -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 ESP-IDF -#include // Part of C++ STL +#include + +#include +#include #include #include #include "esp32-hal-log.h" +/***************************************************************************** + * Bluedroid includes * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#include +#include +#endif + +/***************************************************************************** + * NimBLE includes and definitions * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#include +#include +#include +#include + +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG +#define CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT +#define CONFIG_NIMBLE_ENABLE_ADVERTISMENT_TYPE_TEXT +#define CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT +#endif + +#ifndef CONFIG_NIMBLE_FREERTOS_TASK_BLOCK_BIT +#define CONFIG_NIMBLE_FREERTOS_TASK_BLOCK_BIT 31 +#endif + +constexpr uint32_t TASK_BLOCK_BIT = (1 << CONFIG_NIMBLE_FREERTOS_TASK_BLOCK_BIT); +#endif + +/***************************************************************************** + * Bluedroid types and constants * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) typedef struct { uint32_t assignedNumber; const char *name; @@ -593,90 +642,58 @@ static const gattService_t g_gattServices[] = { #endif {"", "", 0} }; +#endif -/** - * @brief Convert characteristic properties into a string representation. - * @param [in] prop Characteristic properties. - * @return A string representation of characteristic properties. - */ -String BLEUtils::characteristicPropertiesToString(esp_gatt_char_prop_t prop) { - String res = "broadcast: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_BROADCAST) ? "1" : "0"); - res += ", read: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_READ) ? "1" : "0"); - res += ", write_nr: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) ? "1" : "0"); - res += ", write: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_WRITE) ? "1" : "0"); - res += ", notify: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_NOTIFY) ? "1" : "0"); - res += ", indicate: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_INDICATE) ? "1" : "0"); - res += ", auth: "; - res += ((prop & ESP_GATT_CHAR_PROP_BIT_AUTH) ? "1" : "0"); - return res; -} // characteristicPropertiesToString - -/** - * @brief Convert an esp_gatt_id_t to a string. - */ -static String gattIdToString(esp_gatt_id_t gattId) { - String res = "uuid: " + BLEUUID(gattId.uuid).toString() + ", inst_id: "; - char val[8]; - snprintf(val, sizeof(val), "%d", (int)gattId.inst_id); - res += val; - return res; -} // gattIdToString +/***************************************************************************** + * Common functions * + *****************************************************************************/ /** - * @brief Convert an esp_ble_addr_type_t to a string representation. + * @brief Create a hex representation of data. + * + * @param [in] target Where to write the hex string. If this is null, we malloc storage. + * @param [in] source The start of the binary data. + * @param [in] length The length of the data to convert. + * @return A pointer to the formatted buffer. */ -const char *BLEUtils::addressTypeToString(esp_ble_addr_type_t type) { - switch (type) { -#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG - case BLE_ADDR_TYPE_PUBLIC: return "BLE_ADDR_TYPE_PUBLIC"; - case BLE_ADDR_TYPE_RANDOM: return "BLE_ADDR_TYPE_RANDOM"; - case BLE_ADDR_TYPE_RPA_PUBLIC: return "BLE_ADDR_TYPE_RPA_PUBLIC"; - case BLE_ADDR_TYPE_RPA_RANDOM: return "BLE_ADDR_TYPE_RPA_RANDOM"; -#endif - default: return " esp_ble_addr_type_t"; +char *BLEUtils::buildHexData(uint8_t *target, uint8_t *source, uint8_t length) { + // Guard against too much data. + if (length > 100) { + length = 100; } -} // addressTypeToString -/** - * @brief Convert the BLE Advertising Data flags to a string. - * @param adFlags The flags to convert - * @return String A string representation of the advertising flags. - */ -String BLEUtils::adFlagsToString(uint8_t adFlags) { - String res; - if (adFlags & (1 << 0)) { - res += "[LE Limited Discoverable Mode] "; - } - if (adFlags & (1 << 1)) { - res += "[LE General Discoverable Mode] "; - } - if (adFlags & (1 << 2)) { - res += "[BR/EDR Not Supported] "; + if (target == nullptr) { + target = (uint8_t *)malloc(length * 2 + 1); + if (target == nullptr) { + log_e("buildHexData: malloc failed"); + return nullptr; + } } - if (adFlags & (1 << 3)) { - res += "[Simultaneous LE and BR/EDR to Same Device Capable (Controller)] "; + char *startOfData = (char *)target; + + for (int i = 0; i < length; i++) { + sprintf((char *)target, "%.2x", (char)*source); + source++; + target += 2; } - if (adFlags & (1 << 4)) { - res += "[Simultaneous LE and BR/EDR to Same Device Capable (Host)] "; + + // Handle the special case where there was no data. + if (length == 0) { + *startOfData = 0; } - return res; -} // adFlagsToString + + return startOfData; +} // buildHexData /** - * @brief Given an advertising type, return a string representation of the type. + * @brief Given an advertising data type, return a string representation of the type. * * For details see ... * https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile * * @return A string representation of the type. */ -const char *BLEUtils::advTypeToString(uint8_t advType) { +const char *BLEUtils::advDataTypeToString(uint8_t advType) { switch (advType) { #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG case ESP_BLE_AD_TYPE_FLAG: // 0x01 @@ -721,7 +738,8 @@ const char *BLEUtils::advTypeToString(uint8_t advType) { return "ESP_BLE_AD_TYPE_APPEARANCE"; case ESP_BLE_AD_TYPE_ADV_INT: // 0x1a return "ESP_BLE_AD_TYPE_ADV_INT"; - case ESP_BLE_AD_TYPE_32SOL_SRV_UUID: return "ESP_BLE_AD_TYPE_32SOL_SRV_UUID"; + case ESP_BLE_AD_TYPE_32SOL_SRV_UUID: // 0x1f + return "ESP_BLE_AD_TYPE_32SOL_SRV_UUID"; case ESP_BLE_AD_TYPE_32SERVICE_DATA: // 0x20 return "ESP_BLE_AD_TYPE_32SERVICE_DATA"; case ESP_BLE_AD_TYPE_128SERVICE_DATA: // 0x21 @@ -731,58 +749,36 @@ const char *BLEUtils::advTypeToString(uint8_t advType) { #endif default: log_v(" adv data type: 0x%x", advType); return ""; } // End switch -} // advTypeToString +} // advDataTypeToString -esp_gatt_id_t BLEUtils::buildGattId(esp_bt_uuid_t uuid, uint8_t inst_id) { - esp_gatt_id_t retGattId; - retGattId.uuid = uuid; - retGattId.inst_id = inst_id; - return retGattId; -} +/***************************************************************************** + * Bluedroid functions * + *****************************************************************************/ -esp_gatt_srvc_id_t BLEUtils::buildGattSrvcId(esp_gatt_id_t gattId, bool is_primary) { - esp_gatt_srvc_id_t retSrvcId; - retSrvcId.id = gattId; - retSrvcId.is_primary = is_primary; - return retSrvcId; -} +#if defined(CONFIG_BLUEDROID_ENABLED) /** - * @brief Create a hex representation of data. - * - * @param [in] target Where to write the hex string. If this is null, we malloc storage. - * @param [in] source The start of the binary data. - * @param [in] length The length of the data to convert. - * @return A pointer to the formatted buffer. + * @brief Convert characteristic properties into a string representation. + * @param [in] prop Characteristic properties. + * @return A string representation of characteristic properties. */ -char *BLEUtils::buildHexData(uint8_t *target, uint8_t *source, uint8_t length) { - // Guard against too much data. - if (length > 100) { - length = 100; - } - - if (target == nullptr) { - target = (uint8_t *)malloc(length * 2 + 1); - if (target == nullptr) { - log_e("buildHexData: malloc failed"); - return nullptr; - } - } - char *startOfData = (char *)target; - - for (int i = 0; i < length; i++) { - sprintf((char *)target, "%.2x", (char)*source); - source++; - target += 2; - } - - // Handle the special case where there was no data. - if (length == 0) { - *startOfData = 0; - } - - return startOfData; -} // buildHexData +String BLEUtils::characteristicPropertiesToString(uint8_t prop) { + String res = "broadcast: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_BROADCAST) ? "1" : "0"); + res += ", read: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_READ) ? "1" : "0"); + res += ", write_nr: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) ? "1" : "0"); + res += ", write: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_WRITE) ? "1" : "0"); + res += ", notify: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_NOTIFY) ? "1" : "0"); + res += ", indicate: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_INDICATE) ? "1" : "0"); + res += ", auth: "; + res += ((prop & ESP_GATT_CHAR_PROP_BIT_AUTH) ? "1" : "0"); + return res; +} // characteristicPropertiesToString /** * @brief Build a printable string of memory range. @@ -802,6 +798,71 @@ String BLEUtils::buildPrintData(uint8_t *source, size_t length) { return res; } // buildPrintData +/** + * @brief Convert an esp_gatt_id_t to a string. + */ +static String gattIdToString(esp_gatt_id_t gattId) { + String res = "uuid: " + BLEUUID(gattId.uuid).toString() + ", inst_id: "; + char val[8]; + snprintf(val, sizeof(val), "%d", (int)gattId.inst_id); + res += val; + return res; +} // gattIdToString + +/** + * @brief Convert an esp_ble_addr_type_t to a string representation. + */ +const char *BLEUtils::addressTypeToString(esp_ble_addr_type_t type) { + switch (type) { +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + case BLE_ADDR_TYPE_PUBLIC: return "BLE_ADDR_TYPE_PUBLIC"; + case BLE_ADDR_TYPE_RANDOM: return "BLE_ADDR_TYPE_RANDOM"; + case BLE_ADDR_TYPE_RPA_PUBLIC: return "BLE_ADDR_TYPE_RPA_PUBLIC"; + case BLE_ADDR_TYPE_RPA_RANDOM: return "BLE_ADDR_TYPE_RPA_RANDOM"; +#endif + default: return " esp_ble_addr_type_t"; + } +} // addressTypeToString + +/** + * @brief Convert the BLE Advertising Data flags to a string. + * @param adFlags The flags to convert + * @return String A string representation of the advertising flags. + */ +String BLEUtils::adFlagsToString(uint8_t adFlags) { + String res; + if (adFlags & (1 << 0)) { + res += "[LE Limited Discoverable Mode] "; + } + if (adFlags & (1 << 1)) { + res += "[LE General Discoverable Mode] "; + } + if (adFlags & (1 << 2)) { + res += "[BR/EDR Not Supported] "; + } + if (adFlags & (1 << 3)) { + res += "[Simultaneous LE and BR/EDR to Same Device Capable (Controller)] "; + } + if (adFlags & (1 << 4)) { + res += "[Simultaneous LE and BR/EDR to Same Device Capable (Host)] "; + } + return res; +} // adFlagsToString + +esp_gatt_id_t BLEUtils::buildGattId(esp_bt_uuid_t uuid, uint8_t inst_id) { + esp_gatt_id_t retGattId; + retGattId.uuid = uuid; + retGattId.inst_id = inst_id; + return retGattId; +} + +esp_gatt_srvc_id_t BLEUtils::buildGattSrvcId(esp_gatt_id_t gattId, bool is_primary) { + esp_gatt_srvc_id_t retSrvcId; + retSrvcId.id = gattId; + retSrvcId.is_primary = is_primary; + return retSrvcId; +} + /** * @brief Convert a close/disconnect reason to a string. * @param [in] reason The close reason. @@ -1817,5 +1878,380 @@ const char *BLEUtils::searchEventTypeToString(esp_gap_search_evt_t searchEvt) { } } // searchEventTypeToString -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif // CONFIG_BLUEDROID_ENABLED + +/***************************************************************************** + * NimBLE functions * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +/** + * @brief Construct a BLETaskData instance. + * @param [in] pInstance An instance of the class that will be waiting. + * @param [in] flags General purpose flags for the caller. + * @param [in] buf A buffer for data. + */ +BLETaskData::BLETaskData(void *pInstance, int flags, void *buf) : m_pInstance{pInstance}, m_flags{flags}, m_pBuf{buf}, m_pHandle{xTaskGetCurrentTaskHandle()} {} + +/** + * @brief Destructor. + */ +BLETaskData::~BLETaskData() {} + +/** + * @brief A function for checking validity of connection parameters. + * @param [in] params A pointer to the structure containing the parameters to check. + * @return valid == 0 or error code. + */ +int BLEUtils::checkConnParams(ble_gap_conn_params *params) { + /* Check connection interval min */ + if ((params->itvl_min < BLE_HCI_CONN_ITVL_MIN) || (params->itvl_min > BLE_HCI_CONN_ITVL_MAX)) { + return BLE_ERR_INV_HCI_CMD_PARMS; + } + /* Check connection interval max */ + if ((params->itvl_max < BLE_HCI_CONN_ITVL_MIN) || (params->itvl_max > BLE_HCI_CONN_ITVL_MAX) || (params->itvl_max < params->itvl_min)) { + return BLE_ERR_INV_HCI_CMD_PARMS; + } + + /* Check connection latency */ + if (params->latency > BLE_HCI_CONN_LATENCY_MAX) { + return BLE_ERR_INV_HCI_CMD_PARMS; + } + + /* Check supervision timeout */ + if ((params->supervision_timeout < BLE_HCI_CONN_SPVN_TIMEOUT_MIN) || (params->supervision_timeout > BLE_HCI_CONN_SPVN_TIMEOUT_MAX)) { + return BLE_ERR_INV_HCI_CMD_PARMS; + } + + /* Check connection event length */ + if (params->min_ce_len > params->max_ce_len) { + return BLE_ERR_INV_HCI_CMD_PARMS; + } + + return 0; +} + +/** + * @brief Converts a return code from the NimBLE stack to a text string. + * @param [in] rc The return code to convert. + * @return A string representation of the return code. + */ +const char *BLEUtils::returnCodeToString(int rc) { +#if defined(CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT) + switch (rc) { + case 0: return "SUCCESS"; + case BLE_HS_EAGAIN: return "Temporary failure; try again."; + case BLE_HS_EALREADY: return "Operation already in progress or completed."; + case BLE_HS_EINVAL: return "One or more arguments are invalid."; + case BLE_HS_EMSGSIZE: return "The provided buffer is too small."; + case BLE_HS_ENOENT: return "No entry matching the specified criteria."; + case BLE_HS_ENOMEM: return "Operation failed due to resource exhaustion."; + case BLE_HS_ENOTCONN: return "No open connection with the specified handle."; + case BLE_HS_ENOTSUP: return "Operation disabled at compile time."; + case BLE_HS_EAPP: return "Application callback behaved unexpectedly."; + case BLE_HS_EBADDATA: return "Command from peer is invalid."; + case BLE_HS_EOS: return "Mynewt OS error."; + case BLE_HS_ECONTROLLER: return "Event from controller is invalid."; + case BLE_HS_ETIMEOUT: return "Operation timed out."; + case BLE_HS_EDONE: return "Operation completed successfully."; + case BLE_HS_EBUSY: return "Operation cannot be performed until procedure completes."; + case BLE_HS_EREJECT: return "Peer rejected a connection parameter update request."; + case BLE_HS_EUNKNOWN: return "Unexpected failure; catch all."; + case BLE_HS_EROLE: return "Operation requires different role (e.g., central vs. peripheral)."; + case BLE_HS_ETIMEOUT_HCI: return "HCI request timed out; controller unresponsive."; + case BLE_HS_ENOMEM_EVT: return "Controller failed to send event due to memory exhaustion (combined host-controller only)."; + case BLE_HS_ENOADDR: return "Operation requires an identity address but none configured."; + case BLE_HS_ENOTSYNCED: return "Attempt to use the host before it is synced with controller."; + case BLE_HS_EAUTHEN: return "Insufficient authentication."; + case BLE_HS_EAUTHOR: return "Insufficient authorization."; + case BLE_HS_EENCRYPT: return "Insufficient encryption level."; + case BLE_HS_EENCRYPT_KEY_SZ: return "Insufficient key size."; + case BLE_HS_ESTORE_CAP: return "Storage at capacity."; + case BLE_HS_ESTORE_FAIL: return "Storage IO error."; + case BLE_HS_EPREEMPTED: return "Operation was preempted."; + case BLE_HS_EDISABLED: return "Operation disabled."; + case BLE_HS_ESTALLED: return "Operation stalled."; + case (0x0100 + BLE_ATT_ERR_INVALID_HANDLE): return "The attribute handle given was not valid on this server."; + case (0x0100 + BLE_ATT_ERR_READ_NOT_PERMITTED): return "The attribute cannot be read."; + case (0x0100 + BLE_ATT_ERR_WRITE_NOT_PERMITTED): return "The attribute cannot be written."; + case (0x0100 + BLE_ATT_ERR_INVALID_PDU): return "The attribute PDU was invalid."; + case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_AUTHEN): return "The attribute requires authentication before it can be read or written."; + case (0x0100 + BLE_ATT_ERR_REQ_NOT_SUPPORTED): return "Attribute server does not support the request received from the client."; + case (0x0100 + BLE_ATT_ERR_INVALID_OFFSET): return "Offset specified was past the end of the attribute."; + case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_AUTHOR): return "The attribute requires authorization before it can be read or written."; + case (0x0100 + BLE_ATT_ERR_PREPARE_QUEUE_FULL): return "Too many prepare writes have been queued."; + case (0x0100 + BLE_ATT_ERR_ATTR_NOT_FOUND): return "No attribute found within the given attribute handle range."; + case (0x0100 + BLE_ATT_ERR_ATTR_NOT_LONG): return "The attribute cannot be read or written using the Read Blob Request."; + case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_KEY_SZ): return "The Encryption Key Size used for encrypting this link is insufficient."; + case (0x0100 + BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN): return "The attribute value length is invalid for the operation."; + case (0x0100 + BLE_ATT_ERR_UNLIKELY): return "The attribute request has encountered an error that was unlikely, could not be completed as requested."; + case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_ENC): return "The attribute requires encryption before it can be read or written."; + case (0x0100 + BLE_ATT_ERR_UNSUPPORTED_GROUP): + return "The attribute type is not a supported grouping attribute as defined by a higher layer specification."; + case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_RES): return "Insufficient Resources to complete the request."; + case (0x0200 + BLE_ERR_UNKNOWN_HCI_CMD): return "Unknown HCI Command"; + case (0x0200 + BLE_ERR_UNK_CONN_ID): return "Unknown Connection Identifier"; + case (0x0200 + BLE_ERR_HW_FAIL): return "Hardware Failure"; + case (0x0200 + BLE_ERR_PAGE_TMO): return "Page Timeout"; + case (0x0200 + BLE_ERR_AUTH_FAIL): return "Authentication Failure"; + case (0x0200 + BLE_ERR_PINKEY_MISSING): return "PIN or Key Missing"; + case (0x0200 + BLE_ERR_MEM_CAPACITY): return "Memory Capacity Exceeded"; + case (0x0200 + BLE_ERR_CONN_SPVN_TMO): return "Connection Timeout"; + case (0x0200 + BLE_ERR_CONN_LIMIT): return "Connection Limit Exceeded"; + case (0x0200 + BLE_ERR_SYNCH_CONN_LIMIT): return "Synchronous Connection Limit To A Device Exceeded"; + case (0x0200 + BLE_ERR_ACL_CONN_EXISTS): return "ACL Connection Already Exists"; + case (0x0200 + BLE_ERR_CMD_DISALLOWED): return "Command Disallowed"; + case (0x0200 + BLE_ERR_CONN_REJ_RESOURCES): return "Connection Rejected due to Limited Resources"; + case (0x0200 + BLE_ERR_CONN_REJ_SECURITY): return "Connection Rejected Due To Security Reasons"; + case (0x0200 + BLE_ERR_CONN_REJ_BD_ADDR): return "Connection Rejected due to Unacceptable BD_ADDR"; + case (0x0200 + BLE_ERR_CONN_ACCEPT_TMO): return "Connection Accept Timeout Exceeded"; + case (0x0200 + BLE_ERR_UNSUPPORTED): return "Unsupported Feature or Parameter Value"; + case (0x0200 + BLE_ERR_INV_HCI_CMD_PARMS): return "Invalid HCI Command Parameters"; + case (0x0200 + BLE_ERR_REM_USER_CONN_TERM): return "Remote User Terminated Connection"; + case (0x0200 + BLE_ERR_RD_CONN_TERM_RESRCS): return "Remote Device Terminated Connection due to Low Resources"; + case (0x0200 + BLE_ERR_RD_CONN_TERM_PWROFF): return "Remote Device Terminated Connection due to Power Off"; + case (0x0200 + BLE_ERR_CONN_TERM_LOCAL): return "Connection Terminated By Local Host"; + case (0x0200 + BLE_ERR_REPEATED_ATTEMPTS): return "Repeated Attempts"; + case (0x0200 + BLE_ERR_NO_PAIRING): return "Pairing Not Allowed"; + case (0x0200 + BLE_ERR_UNK_LMP): return "Unknown LMP PDU"; + case (0x0200 + BLE_ERR_UNSUPP_REM_FEATURE): return "Unsupported Remote Feature / Unsupported LMP Feature"; + case (0x0200 + BLE_ERR_SCO_OFFSET): return "SCO Offset Rejected"; + case (0x0200 + BLE_ERR_SCO_ITVL): return "SCO Interval Rejected"; + case (0x0200 + BLE_ERR_SCO_AIR_MODE): return "SCO Air Mode Rejected"; + case (0x0200 + BLE_ERR_INV_LMP_LL_PARM): return "Invalid LMP Parameters / Invalid LL Parameters"; + case (0x0200 + BLE_ERR_UNSPECIFIED): return "Unspecified Error"; + case (0x0200 + BLE_ERR_UNSUPP_LMP_LL_PARM): return "Unsupported LMP Parameter Value / Unsupported LL Parameter Value"; + case (0x0200 + BLE_ERR_NO_ROLE_CHANGE): return "Role Change Not Allowed"; + case (0x0200 + BLE_ERR_LMP_LL_RSP_TMO): return "LMP Response Timeout / LL Response Timeout"; + case (0x0200 + BLE_ERR_LMP_COLLISION): return "LMP Error Transaction Collision"; + case (0x0200 + BLE_ERR_LMP_PDU): return "LMP PDU Not Allowed"; + case (0x0200 + BLE_ERR_ENCRYPTION_MODE): return "Encryption Mode Not Acceptable"; + case (0x0200 + BLE_ERR_LINK_KEY_CHANGE): return "Link Key cannot be Changed"; + case (0x0200 + BLE_ERR_UNSUPP_QOS): return "Requested QoS Not Supported"; + case (0x0200 + BLE_ERR_INSTANT_PASSED): return "Instant Passed"; + case (0x0200 + BLE_ERR_UNIT_KEY_PAIRING): return "Pairing With Unit Key Not Supported"; + case (0x0200 + BLE_ERR_DIFF_TRANS_COLL): return "Different Transaction Collision"; + case (0x0200 + BLE_ERR_QOS_PARM): return "QoS Unacceptable Parameter"; + case (0x0200 + BLE_ERR_QOS_REJECTED): return "QoS Rejected"; + case (0x0200 + BLE_ERR_CHAN_CLASS): return "Channel Classification Not Supported"; + case (0x0200 + BLE_ERR_INSUFFICIENT_SEC): return "Insufficient Security"; + case (0x0200 + BLE_ERR_PARM_OUT_OF_RANGE): return "Parameter Out Of Mandatory Range"; + case (0x0200 + BLE_ERR_PENDING_ROLE_SW): return "Role Switch Pending"; + case (0x0200 + BLE_ERR_RESERVED_SLOT): return "Reserved Slot Violation"; + case (0x0200 + BLE_ERR_ROLE_SW_FAIL): return "Role Switch Failed"; + case (0x0200 + BLE_ERR_INQ_RSP_TOO_BIG): return "Extended Inquiry Response Too Large"; + case (0x0200 + BLE_ERR_SEC_SIMPLE_PAIR): return "Secure Simple Pairing Not Supported By Host"; + case (0x0200 + BLE_ERR_HOST_BUSY_PAIR): return "Host Busy - Pairing"; + case (0x0200 + BLE_ERR_CONN_REJ_CHANNEL): return "Connection Rejected, No Suitable Channel Found"; + case (0x0200 + BLE_ERR_CTLR_BUSY): return "Controller Busy"; + case (0x0200 + BLE_ERR_CONN_PARMS): return "Unacceptable Connection Parameters"; + case (0x0200 + BLE_ERR_DIR_ADV_TMO): return "Directed Advertising Timeout"; + case (0x0200 + BLE_ERR_CONN_TERM_MIC): return "Connection Terminated due to MIC Failure"; + case (0x0200 + BLE_ERR_CONN_ESTABLISHMENT): return "Connection Failed to be Established"; + case (0x0200 + BLE_ERR_MAC_CONN_FAIL): return "MAC Connection Failed"; + case (0x0200 + BLE_ERR_COARSE_CLK_ADJ): return "Coarse Clock Adjustment Rejected"; + case (0x0300 + BLE_L2CAP_SIG_ERR_CMD_NOT_UNDERSTOOD): return "Invalid or unsupported incoming L2CAP sig command."; + case (0x0300 + BLE_L2CAP_SIG_ERR_MTU_EXCEEDED): return "Incoming packet too large."; + case (0x0300 + BLE_L2CAP_SIG_ERR_INVALID_CID): return "No channel with specified ID."; + case (0x0400 + BLE_SM_ERR_PASSKEY): return "The user input of passkey failed, for example, the user canceled the operation."; + case (0x0400 + BLE_SM_ERR_OOB): return "The OOB data is not available."; + case (0x0400 + BLE_SM_ERR_AUTHREQ): + return "The pairing procedure cannot be performed as authentication requirements cannot be met due to IO capabilities of one or both devices."; + case (0x0400 + BLE_SM_ERR_CONFIRM_MISMATCH): return "The confirm value does not match the calculated compare value."; + case (0x0400 + BLE_SM_ERR_PAIR_NOT_SUPP): return "Pairing is not supported by the device."; + case (0x0400 + BLE_SM_ERR_ENC_KEY_SZ): return "The resultant encryption key size is insufficient for the security requirements of this device."; + case (0x0400 + BLE_SM_ERR_CMD_NOT_SUPP): return "The SMP command received is not supported on this device."; + case (0x0400 + BLE_SM_ERR_UNSPECIFIED): return "Pairing failed due to an unspecified reason."; + case (0x0400 + BLE_SM_ERR_REPEATED): + return "Pairing or authentication procedure disallowed, too little time has elapsed since last pairing request or security request."; + case (0x0400 + BLE_SM_ERR_INVAL): return "Command length is invalid or that a parameter is outside of the specified range."; + case (0x0400 + BLE_SM_ERR_DHKEY): return "DHKey Check value received doesn't match the one calculated by the local device."; + case (0x0400 + BLE_SM_ERR_NUMCMP): return "Confirm values in the numeric comparison protocol do not match."; + case (0x0400 + BLE_SM_ERR_ALREADY): return "Pairing over the LE transport failed - Pairing Request sent over the BR/EDR transport in process."; + case (0x0400 + BLE_SM_ERR_CROSS_TRANS): + return "BR/EDR Link Key generated on the BR/EDR transport cannot be used to derive and distribute keys for the LE transport."; + case (0x0500 + BLE_SM_ERR_PASSKEY): return "The user input of passkey failed or the user canceled the operation."; + case (0x0500 + BLE_SM_ERR_OOB): return "The OOB data is not available."; + case (0x0500 + BLE_SM_ERR_AUTHREQ): + return "The pairing procedure cannot be performed as authentication requirements cannot be met due to IO capabilities of one or both devices."; + case (0x0500 + BLE_SM_ERR_CONFIRM_MISMATCH): return "The confirm value does not match the calculated compare value."; + case (0x0500 + BLE_SM_ERR_PAIR_NOT_SUPP): return "Pairing is not supported by the device."; + case (0x0500 + BLE_SM_ERR_ENC_KEY_SZ): return "The resultant encryption key size is insufficient for the security requirements of this device."; + case (0x0500 + BLE_SM_ERR_CMD_NOT_SUPP): return "The SMP command received is not supported on this device."; + case (0x0500 + BLE_SM_ERR_UNSPECIFIED): return "Pairing failed due to an unspecified reason."; + case (0x0500 + BLE_SM_ERR_REPEATED): + return "Pairing or authentication procedure is disallowed because too little time has elapsed since last pairing request or security request."; + case (0x0500 + BLE_SM_ERR_INVAL): return "Command length is invalid or a parameter is outside of the specified range."; + case (0x0500 + BLE_SM_ERR_DHKEY): + return "Indicates to the remote device that the DHKey Check value received doesn't match the one calculated by the local device."; + case (0x0500 + BLE_SM_ERR_NUMCMP): return "Confirm values in the numeric comparison protocol do not match."; + case (0x0500 + BLE_SM_ERR_ALREADY): return "Pairing over the LE transport failed - Pairing Request sent over the BR/EDR transport in process."; + case (0x0500 + BLE_SM_ERR_CROSS_TRANS): + return "BR/EDR Link Key generated on the BR/EDR transport cannot be used to derive and distribute keys for the LE transport."; + default: return "Unknown"; + } +#else // #if defined(CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT) + return ""; +#endif // #if defined(CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT) +} + +/** + * @brief Utility function to log the gap event info. + * @param [in] event A pointer to the gap event structure. + * @param [in] arg Unused. + */ +void BLEUtils::dumpGapEvent(ble_gap_event *event, void *arg) { +#if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT) + log_d("Received a GAP event: %s", gapEventToString(event->type)); +#endif +} + +/** + * @brief Convert a GAP event type to a string representation. + * @param [in] eventType The type of event. + * @return A string representation of the event type. + */ +const char *BLEUtils::gapEventToString(uint8_t eventType) { +#if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT) + switch (eventType) { + case BLE_GAP_EVENT_CONNECT: //0 + return "BLE_GAP_EVENT_CONNECT "; + + case BLE_GAP_EVENT_DISCONNECT: //1 + return "BLE_GAP_EVENT_DISCONNECT"; + + case BLE_GAP_EVENT_CONN_UPDATE: //3 + return "BLE_GAP_EVENT_CONN_UPDATE"; + + case BLE_GAP_EVENT_CONN_UPDATE_REQ: //4 + return "BLE_GAP_EVENT_CONN_UPDATE_REQ"; + + case BLE_GAP_EVENT_L2CAP_UPDATE_REQ: //5 + return "BLE_GAP_EVENT_L2CAP_UPDATE_REQ"; + + case BLE_GAP_EVENT_TERM_FAILURE: //6 + return "BLE_GAP_EVENT_TERM_FAILURE"; + + case BLE_GAP_EVENT_DISC: //7 + return "BLE_GAP_EVENT_DISC"; + + case BLE_GAP_EVENT_DISC_COMPLETE: //8 + return "BLE_GAP_EVENT_DISC_COMPLETE"; + + case BLE_GAP_EVENT_ADV_COMPLETE: //9 + return "BLE_GAP_EVENT_ADV_COMPLETE"; + + case BLE_GAP_EVENT_ENC_CHANGE: //10 + return "BLE_GAP_EVENT_ENC_CHANGE"; + + case BLE_GAP_EVENT_PASSKEY_ACTION: //11 + return "BLE_GAP_EVENT_PASSKEY_ACTION"; + + case BLE_GAP_EVENT_NOTIFY_RX: //12 + return "BLE_GAP_EVENT_NOTIFY_RX"; + + case BLE_GAP_EVENT_NOTIFY_TX: //13 + return "BLE_GAP_EVENT_NOTIFY_TX"; + + case BLE_GAP_EVENT_SUBSCRIBE: //14 + return "BLE_GAP_EVENT_SUBSCRIBE"; + + case BLE_GAP_EVENT_MTU: //15 + return "BLE_GAP_EVENT_MTU"; + + case BLE_GAP_EVENT_IDENTITY_RESOLVED: //16 + return "BLE_GAP_EVENT_IDENTITY_RESOLVED"; + + case BLE_GAP_EVENT_REPEAT_PAIRING: //17 + return "BLE_GAP_EVENT_REPEAT_PAIRING"; + + case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: //18 + return "BLE_GAP_EVENT_PHY_UPDATE_COMPLETE"; + + case BLE_GAP_EVENT_EXT_DISC: //19 + return "BLE_GAP_EVENT_EXT_DISC"; +#ifdef BLE_GAP_EVENT_PERIODIC_SYNC // IDF 4.0 does not support these + case BLE_GAP_EVENT_PERIODIC_SYNC: //20 + return "BLE_GAP_EVENT_PERIODIC_SYNC"; + + case BLE_GAP_EVENT_PERIODIC_REPORT: //21 + return "BLE_GAP_EVENT_PERIODIC_REPORT"; + + case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: //22 + return "BLE_GAP_EVENT_PERIODIC_SYNC_LOST"; + + case BLE_GAP_EVENT_SCAN_REQ_RCVD: //23 + return "BLE_GAP_EVENT_SCAN_REQ_RCVD"; +#endif + default: log_d("gapEventToString: Unknown event type %d 0x%.2x", eventType, eventType); return "Unknown event type"; + } +#else // #if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT) + return ""; +#endif // #if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT) +} // gapEventToString + +/** + * @brief Convert characteristic properties into a string representation. + * @param [in] prop Characteristic properties. + * @return A string representation of characteristic properties. + */ +String BLEUtils::characteristicPropertiesToString(uint8_t prop) { + String res = "broadcast: "; + res += ((prop & BLE_GATT_CHR_PROP_BROADCAST) ? "1" : "0"); + res += ", read: "; + res += ((prop & BLE_GATT_CHR_PROP_READ) ? "1" : "0"); + res += ", write_nr: "; + res += ((prop & BLE_GATT_CHR_PROP_WRITE_NO_RSP) ? "1" : "0"); + res += ", write: "; + res += ((prop & BLE_GATT_CHR_PROP_WRITE) ? "1" : "0"); + res += ", notify: "; + res += ((prop & BLE_GATT_CHR_PROP_NOTIFY) ? "1" : "0"); + res += ", indicate: "; + res += ((prop & BLE_GATT_CHR_PROP_INDICATE) ? "1" : "0"); + res += ", auth_sign_write: "; + res += ((prop & BLE_GATT_CHR_PROP_AUTH_SIGN_WRITE) ? "1" : "0"); + res += ", extended: "; + res += ((prop & BLE_GATT_CHR_PROP_EXTENDED) ? "1" : "0"); + return res; +} // characteristicPropertiesToString + +/** + * @brief Blocks the calling task until released or timeout. + * @param [in] taskData A pointer to the task data structure. + * @param [in] timeout The time to wait in milliseconds. + * @return True if the task completed, false if the timeout was reached. + */ +bool BLEUtils::taskWait(const BLETaskData &taskData, uint32_t timeout) { + ble_npl_time_t ticks; + if (timeout == BLE_NPL_TIME_FOREVER) { + ticks = BLE_NPL_TIME_FOREVER; + } else { + ble_npl_time_ms_to_ticks(timeout, &ticks); + } + + uint32_t notificationValue; + xTaskNotifyWait(0, TASK_BLOCK_BIT, ¬ificationValue, 0); + if (notificationValue & TASK_BLOCK_BIT) { + return true; + } + + return xTaskNotifyWait(0, TASK_BLOCK_BIT, nullptr, ticks) == pdTRUE; +} // taskWait + +/** + * @brief Release a task. + * @param [in] taskData A pointer to the task data structure. + * @param [in] flags A return value to set in the task data structure. + */ +void BLEUtils::taskRelease(const BLETaskData &taskData, int flags) { + taskData.m_flags = flags; + if (taskData.m_pHandle != nullptr) { + xTaskNotify(static_cast(taskData.m_pHandle), TASK_BLOCK_BIT, eSetBits); + } +} // taskRelease + +#endif // CONFIG_NIMBLE_ENABLED + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEUtils.h b/libraries/BLE/src/BLEUtils.h index 7c6f58d284b..2689706b7a1 100644 --- a/libraries/BLE/src/BLEUtils.h +++ b/libraries/BLE/src/BLEUtils.h @@ -3,6 +3,10 @@ * * Created on: Mar 25, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEUTILS_H_ @@ -11,24 +15,99 @@ #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) -#include // ESP32 BLE -#include // ESP32 BLE -#include // ESP32 BLE +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + #include -#include "BLEClient.h" +#include "BLEAddress.h" +#include "WString.h" +#include + +/***************************************************************************** + * Bluedroid includes * + *****************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) +#include +#include +#include +#endif + +/***************************************************************************** + * NimBLE includes * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) +#include +#include +#include +#endif + +/***************************************************************************** + * Common types * + *****************************************************************************/ + +typedef struct { + void *peer_device; // peer device BLEClient or BLEServer + bool connected; // connection status + uint16_t mtu; // negotiated MTU per peer device +} conn_status_t; + +/***************************************************************************** + * NimBLE types * + *****************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + +/** + * @brief A structure to hold data for a task that is waiting for a response. + * @details This structure is used in conjunction with BLEUtils::taskWait() and BLEUtils::taskRelease(). + * All items are optional, the m_pHandle will be set in taskWait(). + */ +struct BLETaskData { + BLETaskData(void *pInstance = nullptr, int flags = 0, void *buf = nullptr); + ~BLETaskData(); + void *m_pInstance{nullptr}; + mutable int m_flags{0}; + void *m_pBuf{nullptr}; + +private: + mutable void *m_pHandle{nullptr}; // semaphore or task handle + friend class BLEUtils; +}; + +#endif + +/***************************************************************************** + * Forward declarations * + *****************************************************************************/ + +class BLEClient; /** * @brief A set of general %BLE utilities. */ class BLEUtils { public: - static const char *addressTypeToString(esp_ble_addr_type_t type); - static String adFlagsToString(uint8_t adFlags); - static const char *advTypeToString(uint8_t advType); + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + static char *buildHexData(uint8_t *target, uint8_t *source, uint8_t length); static String buildPrintData(uint8_t *source, size_t length); - static String characteristicPropertiesToString(esp_gatt_char_prop_t prop); + static const char *advDataTypeToString(uint8_t advType); + static String characteristicPropertiesToString(uint8_t prop); + + /*************************************************************************** + * Bluedroid public declarations * + ***************************************************************************/ + +#if defined(CONFIG_BLUEDROID_ENABLED) + static const char *addressTypeToString(esp_ble_addr_type_t type); + static String adFlagsToString(uint8_t adFlags); static const char *devTypeToString(esp_bt_dev_type_t type); static esp_gatt_id_t buildGattId(esp_bt_uuid_t uuid, uint8_t inst_id = 0); static esp_gatt_srvc_id_t buildGattSrvcId(esp_gatt_id_t gattId, bool is_primary = true); @@ -52,8 +131,22 @@ class BLEUtils { static void registerByAddress(BLEAddress address, BLEClient *pDevice); static void registerByConnId(uint16_t conn_id, BLEClient *pDevice); static const char *searchEventTypeToString(esp_gap_search_evt_t searchEvt); +#endif + + /*************************************************************************** + * NimBLE public declarations * + ***************************************************************************/ + +#if defined(CONFIG_NIMBLE_ENABLED) + static void dumpGapEvent(ble_gap_event *event, void *arg); + static const char *gapEventToString(uint8_t eventType); + static const char *returnCodeToString(int rc); + static int checkConnParams(ble_gap_conn_params *params); + static bool taskWait(const BLETaskData &taskData, uint32_t timeout); + static void taskRelease(const BLETaskData &taskData, int rc = 0); +#endif }; -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEUTILS_H_ */ diff --git a/libraries/BLE/src/BLEValue.cpp b/libraries/BLE/src/BLEValue.cpp index 26811c985ac..efc97697baa 100644 --- a/libraries/BLE/src/BLEValue.cpp +++ b/libraries/BLE/src/BLEValue.cpp @@ -3,15 +3,29 @@ * * Created on: Jul 17, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ + #include "soc/soc_caps.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + #include "BLEValue.h" #include "esp32-hal-log.h" +/***************************************************************************** + * Common functions * + *****************************************************************************/ + BLEValue::BLEValue() { m_accumulation = ""; m_value = ""; @@ -120,5 +134,5 @@ void BLEValue::setValue(uint8_t *pData, size_t length) { m_value = String((char *)pData, length); } // setValue -#endif /* CONFIG_BLUEDROID_ENABLED */ +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ diff --git a/libraries/BLE/src/BLEValue.h b/libraries/BLE/src/BLEValue.h index f9c91bdcd4e..56a7a5bc4ec 100644 --- a/libraries/BLE/src/BLEValue.h +++ b/libraries/BLE/src/BLEValue.h @@ -3,22 +3,36 @@ * * Created on: Jul 17, 2017 * Author: kolban + * + * Modified on: Feb 18, 2025 + * Author: lucasssvaz (based on kolban's and h2zero's work) + * Description: Added support for NimBLE */ #ifndef COMPONENTS_CPP_UTILS_BLEVALUE_H_ #define COMPONENTS_CPP_UTILS_BLEVALUE_H_ + #include "soc/soc_caps.h" -#include "WString.h" #if SOC_BLE_SUPPORTED #include "sdkconfig.h" -#if defined(CONFIG_BLUEDROID_ENABLED) +#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED) + +/***************************************************************************** + * Common includes * + *****************************************************************************/ + +#include "WString.h" /** * @brief The model of a %BLE value. */ class BLEValue { public: + /*************************************************************************** + * Common public declarations * + ***************************************************************************/ + BLEValue(); void addPart(String part); void addPart(uint8_t *pData, size_t length); @@ -33,10 +47,15 @@ class BLEValue { void setValue(uint8_t *pData, size_t length); private: + /*************************************************************************** + * Common private properties * + ***************************************************************************/ + String m_accumulation; uint16_t m_readOffset; String m_value; }; -#endif /* CONFIG_BLUEDROID_ENABLED */ + +#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */ #endif /* SOC_BLE_SUPPORTED */ #endif /* COMPONENTS_CPP_UTILS_BLEVALUE_H_ */ From dc82467ba41c7ed589dfb480db742ea14e65b68f Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Mon, 23 Jun 2025 08:48:37 -0300 Subject: [PATCH 032/102] fix(esp_now): Fix broadcast example and use nullptr (#11490) * fix(esp_now): Fix example and use nullptr * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../ESP_NOW_Broadcast_Master.ino | 2 +- .../ESP_NOW_Broadcast_Slave.ino | 28 ++++++++--- .../ESP_NOW_Network/ESP_NOW_Network.ino | 13 +++-- libraries/ESP_NOW/src/ESP32_NOW.cpp | 42 ++++++++-------- libraries/ESP_NOW/src/ESP32_NOW.h | 4 +- libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp | 50 +++++++++---------- libraries/ESP_NOW/src/ESP32_NOW_Serial.h | 4 +- 7 files changed, 81 insertions(+), 62 deletions(-) diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino index 6ee7dc6f846..025a53c913b 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino @@ -58,7 +58,7 @@ public: uint32_t msg_count = 0; // Create a broadcast peer object -ESP_NOW_Broadcast_Peer broadcast_peer(ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL); +ESP_NOW_Broadcast_Peer broadcast_peer(ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, nullptr); /* Main */ diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino index 5f1a8bd8807..e61524b64f9 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino @@ -52,7 +52,8 @@ public: /* Global Variables */ // List of all the masters. It will be populated when a new master is registered -std::vector masters; +// Note: Using pointers instead of objects to prevent dangling pointers when the vector reallocates +std::vector masters; /* Callbacks */ @@ -62,13 +63,14 @@ void register_new_master(const esp_now_recv_info_t *info, const uint8_t *data, i Serial.printf("Unknown peer " MACSTR " sent a broadcast message\n", MAC2STR(info->src_addr)); Serial.println("Registering the peer as a master"); - ESP_NOW_Peer_Class new_master(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL); - - masters.push_back(new_master); - if (!masters.back().add_peer()) { + ESP_NOW_Peer_Class *new_master = new ESP_NOW_Peer_Class(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, nullptr); + if (!new_master->add_peer()) { Serial.println("Failed to register the new master"); + delete new_master; return; } + masters.push_back(new_master); + Serial.printf("Successfully registered master " MACSTR " (total masters: %zu)\n", MAC2STR(new_master->addr()), masters.size()); } else { // The slave will only receive broadcast messages log_v("Received a unicast message from " MACSTR, MAC2STR(info->src_addr)); @@ -103,11 +105,23 @@ void setup() { } // Register the new peer callback - ESP_NOW.onNewPeer(register_new_master, NULL); + ESP_NOW.onNewPeer(register_new_master, nullptr); Serial.println("Setup complete. Waiting for a master to broadcast a message..."); } void loop() { - delay(1000); + // Print debug information every 10 seconds + static unsigned long last_debug = 0; + if (millis() - last_debug > 10000) { + last_debug = millis(); + Serial.printf("Registered masters: %zu\n", masters.size()); + for (size_t i = 0; i < masters.size(); i++) { + if (masters[i]) { + Serial.printf(" Master %zu: " MACSTR "\n", i, MAC2STR(masters[i]->addr())); + } + } + } + + delay(100); } diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino b/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino index 48ea6b731ce..6731340c922 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino @@ -123,7 +123,7 @@ public: } bool send_message(const uint8_t *data, size_t len) { - if (data == NULL || len == 0) { + if (data == nullptr || len == 0) { log_e("Data to be sent is NULL or has a length of 0"); return false; } @@ -169,9 +169,12 @@ public: /* Peers */ -std::vector peers; // Create a vector to store the peer pointers -ESP_NOW_Network_Peer broadcast_peer(ESP_NOW.BROADCAST_ADDR, 0, NULL); // Register the broadcast peer (no encryption support for the broadcast address) -ESP_NOW_Network_Peer *master_peer = nullptr; // Pointer to peer that is the master +// Create a vector to store the peer pointers +std::vector peers; +// Register the broadcast peer (no encryption support for the broadcast address) +ESP_NOW_Network_Peer broadcast_peer(ESP_NOW.BROADCAST_ADDR, 0, nullptr); +// Pointer to the peer that is the master +ESP_NOW_Network_Peer *master_peer = nullptr; /* Helper functions */ @@ -279,7 +282,7 @@ void setup() { } // Register the callback to be called when a new peer is found - ESP_NOW.onNewPeer(register_new_peer, NULL); + ESP_NOW.onNewPeer(register_new_peer, nullptr); Serial.println("Setup complete. Broadcasting own priority to find the master..."); memset(&new_msg, 0, sizeof(new_msg)); diff --git a/libraries/ESP_NOW/src/ESP32_NOW.cpp b/libraries/ESP_NOW/src/ESP32_NOW.cpp index 6fd3ff0a0b1..25a5609e8db 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW.cpp +++ b/libraries/ESP_NOW/src/ESP32_NOW.cpp @@ -9,12 +9,12 @@ #include "esp32-hal.h" #include "esp_wifi.h" -static void (*new_cb)(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) = NULL; -static void *new_arg = NULL; // * tx_arg = NULL, * rx_arg = NULL, +static void (*new_cb)(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) = nullptr; +static void *new_arg = nullptr; // * tx_arg = nullptr, * rx_arg = nullptr, static bool _esp_now_has_begun = false; static ESP_NOW_Peer *_esp_now_peers[ESP_NOW_MAX_TOTAL_PEER_NUM]; -static esp_err_t _esp_now_add_peer(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk, ESP_NOW_Peer *_peer = NULL) { +static esp_err_t _esp_now_add_peer(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk, ESP_NOW_Peer *_peer = nullptr) { log_v(MACSTR, MAC2STR(mac_addr)); if (esp_now_is_peer_exist(mac_addr)) { log_e("Peer Already Exists"); @@ -26,16 +26,16 @@ static esp_err_t _esp_now_add_peer(const uint8_t *mac_addr, uint8_t channel, wif memcpy(peer.peer_addr, mac_addr, ESP_NOW_ETH_ALEN); peer.channel = channel; peer.ifidx = iface; - peer.encrypt = lmk != NULL; + peer.encrypt = lmk != nullptr; if (lmk) { memcpy(peer.lmk, lmk, ESP_NOW_KEY_LEN); } esp_err_t result = esp_now_add_peer(&peer); if (result == ESP_OK) { - if (_peer != NULL) { + if (_peer != nullptr) { for (uint8_t i = 0; i < ESP_NOW_MAX_TOTAL_PEER_NUM; i++) { - if (_esp_now_peers[i] == NULL) { + if (_esp_now_peers[i] == nullptr) { _esp_now_peers[i] = _peer; return ESP_OK; } @@ -67,8 +67,8 @@ static esp_err_t _esp_now_del_peer(const uint8_t *mac_addr) { } for (uint8_t i = 0; i < ESP_NOW_MAX_TOTAL_PEER_NUM; i++) { - if (_esp_now_peers[i] != NULL && memcmp(mac_addr, _esp_now_peers[i]->addr(), ESP_NOW_ETH_ALEN) == 0) { - _esp_now_peers[i] = NULL; + if (_esp_now_peers[i] != nullptr && memcmp(mac_addr, _esp_now_peers[i]->addr(), ESP_NOW_ETH_ALEN) == 0) { + _esp_now_peers[i] = nullptr; break; } } @@ -87,7 +87,7 @@ static esp_err_t _esp_now_modify_peer(const uint8_t *mac_addr, uint8_t channel, memcpy(peer.peer_addr, mac_addr, ESP_NOW_ETH_ALEN); peer.channel = channel; peer.ifidx = iface; - peer.encrypt = lmk != NULL; + peer.encrypt = lmk != nullptr; if (lmk) { memcpy(peer.lmk, lmk, ESP_NOW_KEY_LEN); } @@ -111,17 +111,17 @@ static void _esp_now_rx_cb(const esp_now_recv_info_t *info, const uint8_t *data, bool broadcast = memcmp(info->des_addr, ESP_NOW.BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0; log_v("%s from " MACSTR ", data length : %u", broadcast ? "Broadcast" : "Unicast", MAC2STR(info->src_addr), len); log_buf_v(data, len); - if (!esp_now_is_peer_exist(info->src_addr) && new_cb != NULL) { + if (!esp_now_is_peer_exist(info->src_addr) && new_cb != nullptr) { log_v("Calling new_cb, peer not found."); new_cb(info, data, len, new_arg); return; } //find the peer and call it's callback for (uint8_t i = 0; i < ESP_NOW_MAX_TOTAL_PEER_NUM; i++) { - if (_esp_now_peers[i] != NULL) { + if (_esp_now_peers[i] != nullptr) { log_v("Checking peer " MACSTR, MAC2STR(_esp_now_peers[i]->addr())); } - if (_esp_now_peers[i] != NULL && memcmp(info->src_addr, _esp_now_peers[i]->addr(), ESP_NOW_ETH_ALEN) == 0) { + if (_esp_now_peers[i] != nullptr && memcmp(info->src_addr, _esp_now_peers[i]->addr(), ESP_NOW_ETH_ALEN) == 0) { log_v("Calling onReceive"); _esp_now_peers[i]->onReceive(data, len, broadcast); return; @@ -133,7 +133,7 @@ static void _esp_now_tx_cb(const uint8_t *mac_addr, esp_now_send_status_t status log_v(MACSTR " : %s", MAC2STR(mac_addr), (status == ESP_NOW_SEND_SUCCESS) ? "SUCCESS" : "FAILED"); //find the peer and call it's callback for (uint8_t i = 0; i < ESP_NOW_MAX_TOTAL_PEER_NUM; i++) { - if (_esp_now_peers[i] != NULL && memcmp(mac_addr, _esp_now_peers[i]->addr(), ESP_NOW_ETH_ALEN) == 0) { + if (_esp_now_peers[i] != nullptr && memcmp(mac_addr, _esp_now_peers[i]->addr(), ESP_NOW_ETH_ALEN) == 0) { _esp_now_peers[i]->onSent(status == ESP_NOW_SEND_SUCCESS); return; } @@ -197,7 +197,7 @@ bool ESP_NOW_Class::end() { } //remove all peers for (uint8_t i = 0; i < ESP_NOW_MAX_TOTAL_PEER_NUM; i++) { - if (_esp_now_peers[i] != NULL) { + if (_esp_now_peers[i] != nullptr) { removePeer(*_esp_now_peers[i]); } } @@ -249,7 +249,7 @@ size_t ESP_NOW_Class::write(const uint8_t *data, size_t len) { if (len > ESP_NOW_MAX_DATA_LEN) { len = ESP_NOW_MAX_DATA_LEN; } - esp_err_t result = esp_now_send(NULL, data, len); + esp_err_t result = esp_now_send(nullptr, data, len); if (result == ESP_OK) { return len; } else if (result == ESP_ERR_ESPNOW_NOT_INIT) { @@ -292,7 +292,7 @@ ESP_NOW_Peer::ESP_NOW_Peer(const uint8_t *mac_addr, uint8_t channel, wifi_interf } chan = channel; ifc = iface; - encrypt = lmk != NULL; + encrypt = lmk != nullptr; if (encrypt) { memcpy(key, lmk, 16); } @@ -305,7 +305,7 @@ bool ESP_NOW_Peer::add() { if (added) { return true; } - if (_esp_now_add_peer(mac, chan, ifc, encrypt ? key : NULL, this) != ESP_OK) { + if (_esp_now_add_peer(mac, chan, ifc, encrypt ? key : nullptr, this) != ESP_OK) { return false; } log_v("Peer added - " MACSTR, MAC2STR(mac)); @@ -350,7 +350,7 @@ bool ESP_NOW_Peer::setChannel(uint8_t channel) { if (!_esp_now_has_begun || !added) { return true; } - return _esp_now_modify_peer(mac, chan, ifc, encrypt ? key : NULL) == ESP_OK; + return _esp_now_modify_peer(mac, chan, ifc, encrypt ? key : nullptr) == ESP_OK; } wifi_interface_t ESP_NOW_Peer::getInterface() const { @@ -362,7 +362,7 @@ bool ESP_NOW_Peer::setInterface(wifi_interface_t iface) { if (!_esp_now_has_begun || !added) { return true; } - return _esp_now_modify_peer(mac, chan, ifc, encrypt ? key : NULL) == ESP_OK; + return _esp_now_modify_peer(mac, chan, ifc, encrypt ? key : nullptr) == ESP_OK; } bool ESP_NOW_Peer::isEncrypted() const { @@ -370,14 +370,14 @@ bool ESP_NOW_Peer::isEncrypted() const { } bool ESP_NOW_Peer::setKey(const uint8_t *lmk) { - encrypt = lmk != NULL; + encrypt = lmk != nullptr; if (encrypt) { memcpy(key, lmk, 16); } if (!_esp_now_has_begun || !added) { return true; } - return _esp_now_modify_peer(mac, chan, ifc, encrypt ? key : NULL) == ESP_OK; + return _esp_now_modify_peer(mac, chan, ifc, encrypt ? key : nullptr) == ESP_OK; } size_t ESP_NOW_Peer::send(const uint8_t *data, int len) { diff --git a/libraries/ESP_NOW/src/ESP32_NOW.h b/libraries/ESP_NOW/src/ESP32_NOW.h index efba9243aee..5940cfa2221 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW.h +++ b/libraries/ESP_NOW/src/ESP32_NOW.h @@ -20,7 +20,7 @@ class ESP_NOW_Class : public Print { ESP_NOW_Class(); ~ESP_NOW_Class(); - bool begin(const uint8_t *pmk = NULL /* 16 bytes */); + bool begin(const uint8_t *pmk = nullptr /* 16 bytes */); bool end(); int getTotalPeerCount(); @@ -50,7 +50,7 @@ class ESP_NOW_Peer { bool remove(); size_t send(const uint8_t *data, int len); - ESP_NOW_Peer(const uint8_t *mac_addr, uint8_t channel = 0, wifi_interface_t iface = WIFI_IF_AP, const uint8_t *lmk = NULL); + ESP_NOW_Peer(const uint8_t *mac_addr, uint8_t channel = 0, wifi_interface_t iface = WIFI_IF_AP, const uint8_t *lmk = nullptr); public: virtual ~ESP_NOW_Peer() {} diff --git a/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp b/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp index 5603da2ba13..edd6e32aacc 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp +++ b/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp @@ -18,11 +18,11 @@ ESP_NOW_Serial_Class::ESP_NOW_Serial_Class(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk, bool remove_on_fail) : ESP_NOW_Peer(mac_addr, channel, iface, lmk) { - tx_ring_buf = NULL; - rx_queue = NULL; - tx_sem = NULL; + tx_ring_buf = nullptr; + rx_queue = nullptr; + tx_sem = nullptr; queued_size = 0; - queued_buff = NULL; + queued_buff = nullptr; resend_count = 0; _remove_on_fail = remove_on_fail; } @@ -34,7 +34,7 @@ ESP_NOW_Serial_Class::~ESP_NOW_Serial_Class() { size_t ESP_NOW_Serial_Class::setTxBufferSize(size_t tx_queue_len) { if (tx_ring_buf) { vRingbufferDelete(tx_ring_buf); - tx_ring_buf = NULL; + tx_ring_buf = nullptr; } if (!tx_queue_len) { return 0; @@ -49,7 +49,7 @@ size_t ESP_NOW_Serial_Class::setTxBufferSize(size_t tx_queue_len) { size_t ESP_NOW_Serial_Class::setRxBufferSize(size_t rx_queue_len) { if (rx_queue) { vQueueDelete(rx_queue); - rx_queue = NULL; + rx_queue = nullptr; } if (!rx_queue_len) { return 0; @@ -65,7 +65,7 @@ bool ESP_NOW_Serial_Class::begin(unsigned long baud) { if (!ESP_NOW.begin() || !add()) { return false; } - if (tx_sem == NULL) { + if (tx_sem == nullptr) { tx_sem = xSemaphoreCreateBinary(); //xSemaphoreTake(tx_sem, 0); xSemaphoreGive(tx_sem); @@ -79,22 +79,22 @@ void ESP_NOW_Serial_Class::end() { remove(); setRxBufferSize(0); setTxBufferSize(0); - if (tx_sem != NULL) { + if (tx_sem != nullptr) { vSemaphoreDelete(tx_sem); - tx_sem = NULL; + tx_sem = nullptr; } } //Stream int ESP_NOW_Serial_Class::available(void) { - if (rx_queue == NULL) { + if (rx_queue == nullptr) { return 0; } return uxQueueMessagesWaiting(rx_queue); } int ESP_NOW_Serial_Class::peek(void) { - if (rx_queue == NULL) { + if (rx_queue == nullptr) { return -1; } uint8_t c; @@ -105,7 +105,7 @@ int ESP_NOW_Serial_Class::peek(void) { } int ESP_NOW_Serial_Class::read(void) { - if (rx_queue == NULL) { + if (rx_queue == nullptr) { return -1; } uint8_t c = 0; @@ -116,7 +116,7 @@ int ESP_NOW_Serial_Class::read(void) { } size_t ESP_NOW_Serial_Class::read(uint8_t *buffer, size_t size) { - if (rx_queue == NULL) { + if (rx_queue == nullptr) { return -1; } uint8_t c = 0; @@ -128,11 +128,11 @@ size_t ESP_NOW_Serial_Class::read(uint8_t *buffer, size_t size) { } void ESP_NOW_Serial_Class::flush() { - if (tx_ring_buf == NULL) { + if (tx_ring_buf == nullptr) { return; } UBaseType_t uxItemsWaiting = 0; - vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + vRingbufferGetInfo(tx_ring_buf, nullptr, nullptr, nullptr, nullptr, &uxItemsWaiting); if (uxItemsWaiting) { // Now trigger the ISR to read data from the ring buffer. if (xSemaphoreTake(tx_sem, 0) == pdTRUE) { @@ -141,13 +141,13 @@ void ESP_NOW_Serial_Class::flush() { } while (uxItemsWaiting) { delay(5); - vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + vRingbufferGetInfo(tx_ring_buf, nullptr, nullptr, nullptr, nullptr, &uxItemsWaiting); } } //RX callback void ESP_NOW_Serial_Class::onReceive(const uint8_t *data, size_t len, bool broadcast) { - if (rx_queue == NULL) { + if (rx_queue == nullptr) { return; } for (uint32_t i = 0; i < len; i++) { @@ -165,7 +165,7 @@ void ESP_NOW_Serial_Class::onReceive(const uint8_t *data, size_t len, bool broad //Print int ESP_NOW_Serial_Class::availableForWrite() { //return ESP_NOW_MAX_DATA_LEN; - if (tx_ring_buf == NULL) { + if (tx_ring_buf == nullptr) { return 0; } return xRingbufferGetCurFreeSize(tx_ring_buf); @@ -178,7 +178,7 @@ size_t ESP_NOW_Serial_Class::tryToSend() { //_onSent will not be called anymore //the data is lost in this case vRingbufferReturnItem(tx_ring_buf, queued_buff); - queued_buff = NULL; + queued_buff = nullptr; xSemaphoreGive(tx_sem); end(); } @@ -188,12 +188,12 @@ size_t ESP_NOW_Serial_Class::tryToSend() { bool ESP_NOW_Serial_Class::checkForTxData() { //do we have something that failed the last time? resend_count = 0; - if (queued_buff == NULL) { + if (queued_buff == nullptr) { queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, ESP_NOW_MAX_DATA_LEN); } else { log_d(MACSTR " : PREVIOUS", MAC2STR(addr())); } - if (queued_buff != NULL) { + if (queued_buff != nullptr) { return tryToSend() > 0; } //log_d(MACSTR ": EMPTY", MAC2STR(addr())); @@ -203,7 +203,7 @@ bool ESP_NOW_Serial_Class::checkForTxData() { size_t ESP_NOW_Serial_Class::write(const uint8_t *buffer, size_t size, uint32_t timeout) { log_v(MACSTR ", size %u", MAC2STR(addr()), size); - if (tx_sem == NULL || tx_ring_buf == NULL || !added) { + if (tx_sem == nullptr || tx_ring_buf == nullptr || !added) { return 0; } size_t space = availableForWrite(); @@ -249,12 +249,12 @@ size_t ESP_NOW_Serial_Class::write(const uint8_t *buffer, size_t size, uint32_t //TX Done Callback void ESP_NOW_Serial_Class::onSent(bool success) { log_v(MACSTR " : %s", MAC2STR(addr()), success ? "OK" : "FAIL"); - if (tx_sem == NULL || tx_ring_buf == NULL || !added) { + if (tx_sem == nullptr || tx_ring_buf == nullptr || !added) { return; } if (success) { vRingbufferReturnItem(tx_ring_buf, queued_buff); - queued_buff = NULL; + queued_buff = nullptr; //send next packet? //log_d(MACSTR ": NEXT", MAC2STR(addr())); checkForTxData(); @@ -269,7 +269,7 @@ void ESP_NOW_Serial_Class::onSent(bool success) { //resend limit reached //the data is lost in this case vRingbufferReturnItem(tx_ring_buf, queued_buff); - queued_buff = NULL; + queued_buff = nullptr; log_e(MACSTR " : RE-SEND_MAX[%u]", MAC2STR(addr()), resend_count); //if we are not able to send the data and remove_on_fail is set, remove the peer if (_remove_on_fail) { diff --git a/libraries/ESP_NOW/src/ESP32_NOW_Serial.h b/libraries/ESP_NOW/src/ESP32_NOW_Serial.h index 7cc43d85ef8..5ccb7763ec2 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW_Serial.h +++ b/libraries/ESP_NOW/src/ESP32_NOW_Serial.h @@ -28,7 +28,9 @@ class ESP_NOW_Serial_Class : public Stream, public ESP_NOW_Peer { size_t tryToSend(); public: - ESP_NOW_Serial_Class(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface = WIFI_IF_AP, const uint8_t *lmk = NULL, bool remove_on_fail = false); + ESP_NOW_Serial_Class( + const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface = WIFI_IF_AP, const uint8_t *lmk = nullptr, bool remove_on_fail = false + ); ~ESP_NOW_Serial_Class(); size_t setRxBufferSize(size_t); size_t setTxBufferSize(size_t); From 95ae8cf9c63e3045034cdbc7b642d9bd0c5148b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Andr=C3=BDsek?= Date: Mon, 23 Jun 2025 14:02:47 +0200 Subject: [PATCH 033/102] feat(test): Enhance NVS test (#11481) * feat(test): Enhance NVS test * fix(nvs): Remove unused Unity header and improve Serial wait loop * refactor(nvs): Extract string increment logic into a separate function * refactor(test): Format long strings in expect_exact calls for better readability --- tests/validation/nvs/nvs.ino | 132 +++++++++++++++++++++++++++---- tests/validation/nvs/test_nvs.py | 24 ++++-- 2 files changed, 136 insertions(+), 20 deletions(-) diff --git a/tests/validation/nvs/nvs.ino b/tests/validation/nvs/nvs.ino index 20b5b460098..12c640c65b0 100644 --- a/tests/validation/nvs/nvs.ino +++ b/tests/validation/nvs/nvs.ino @@ -1,35 +1,139 @@ +#include #include +struct TestData { + uint8_t id; + uint16_t value; +}; + Preferences preferences; +void validate_types() { + assert(preferences.getType("char") == PT_I8); + assert(preferences.getType("uchar") == PT_U8); + assert(preferences.getType("short") == PT_I16); + assert(preferences.getType("ushort") == PT_U16); + assert(preferences.getType("int") == PT_I32); + assert(preferences.getType("uint") == PT_U32); + assert(preferences.getType("long") == PT_I32); + assert(preferences.getType("ulong") == PT_U32); + assert(preferences.getType("long64") == PT_I64); + assert(preferences.getType("ulong64") == PT_U64); + assert(preferences.getType("float") == PT_BLOB); + assert(preferences.getType("double") == PT_BLOB); + assert(preferences.getType("bool") == PT_U8); + assert(preferences.getType("str") == PT_STR); + assert(preferences.getType("strLen") == PT_STR); + assert(preferences.getType("struct") == PT_BLOB); +} + +// Function to increment string values +void incrementStringValues(String &val_string, char *val_string_buf, size_t buf_size) { + // Extract the number from string and increment it + val_string = "str" + String(val_string.substring(3).toInt() + 1); + + // Extract the number from strLen and increment it + String strLen_str = String(val_string_buf); + int strLen_num = strLen_str.substring(6).toInt(); + snprintf(val_string_buf, buf_size, "strLen%d", strLen_num + 1); +} + void setup() { Serial.begin(115200); - while (!Serial) { ; } preferences.begin("my-app", false); - // Get the counter value, if the key does not exist, return a default value of 0 - unsigned int counter = preferences.getUInt("counter", 0); + // Get the preferences value and if not exists, use default parameter + char val_char = preferences.getChar("char", 'A'); + unsigned char val_uchar = preferences.getUChar("uchar", 0); + int16_t val_short = preferences.getShort("short", 0); + uint16_t val_ushort = preferences.getUShort("ushort", 0); + int32_t val_int = preferences.getInt("int", 0); + uint32_t val_uint = preferences.getUInt("uint", 0); + int64_t val_long = preferences.getLong("long", 0); + uint32_t val_ulong = preferences.getULong("ulong", 0); + int64_t val_long64 = preferences.getLong64("long64", 0); + uint64_t val_ulong64 = preferences.getULong64("ulong64", 0); + float val_float = preferences.getFloat("float", 0.0f); + double val_double = preferences.getDouble("double", 0.0); + bool val_bool = preferences.getBool("bool", false); - // Print the counter to Serial Monitor - Serial.printf("Current counter value: %u\n", counter); + // Strings + String val_string = preferences.getString("str", "str0"); + char val_string_buf[20] = "strLen0"; + preferences.getString("strLen", val_string_buf, sizeof(val_string_buf)); - // Increase counter by 1 - counter++; + // Structure data + TestData test_data = {0, 0}; - // Store the counter to the Preferences - preferences.putUInt("counter", counter); + size_t struct_size = preferences.getBytes("struct", &test_data, sizeof(test_data)); + if (struct_size == 0) { + // First time - set initial values using parameter names + test_data.id = 1; + test_data.value = 100; + } - // Close the Preferences - preferences.end(); + Serial.printf("Values from Preferences: "); + Serial.printf("char: %c | uchar: %u | short: %d | ushort: %u | int: %ld | uint: %lu | ", val_char, val_uchar, val_short, val_ushort, val_int, val_uint); + Serial.printf("long: %lld | ulong: %lu | long64: %lld | ulong64: %llu | ", val_long, val_ulong, val_long64, val_ulong64); + Serial.printf( + "float: %.2f | double: %.2f | bool: %s | str: %s | strLen: %s | struct: {id:%u,val:%u}\n", val_float, val_double, val_bool ? "true" : "false", + val_string.c_str(), val_string_buf, test_data.id, test_data.value + ); - // Wait 1 second - delay(1000); + // Increment the values + val_char += 1; // Increment char A -> B + val_uchar += 1; + val_short += 1; + val_ushort += 1; + val_int += 1; + val_uint += 1; + val_long += 1; + val_ulong += 1; + val_long64 += 1; + val_ulong64 += 1; + val_float += 1.1f; + val_double += 1.1; + val_bool = !val_bool; // Toggle boolean value + + // Increment string values using function + incrementStringValues(val_string, val_string_buf, sizeof(val_string_buf)); + + test_data.id += 1; + test_data.value += 10; + + // Store the updated values back to Preferences + preferences.putChar("char", val_char); + preferences.putUChar("uchar", val_uchar); + preferences.putShort("short", val_short); + preferences.putUShort("ushort", val_ushort); + preferences.putInt("int", val_int); + preferences.putUInt("uint", val_uint); + preferences.putLong("long", val_long); + preferences.putULong("ulong", val_ulong); + preferences.putLong64("long64", val_long64); + preferences.putULong64("ulong64", val_ulong64); + preferences.putFloat("float", val_float); + preferences.putDouble("double", val_double); + preferences.putBool("bool", val_bool); + preferences.putString("str", val_string); + preferences.putString("strLen", val_string_buf); + preferences.putBytes("struct", &test_data, sizeof(test_data)); - // Restart ESP + // Check if the keys exist + assert(preferences.isKey("char")); + assert(preferences.isKey("struct")); + + // Validate the types of the keys + validate_types(); + + // Close the Preferences, wait and restart + preferences.end(); + Serial.flush(); + delay(1000); ESP.restart(); } diff --git a/tests/validation/nvs/test_nvs.py b/tests/validation/nvs/test_nvs.py index 424095a49ba..7f595b8e93d 100644 --- a/tests/validation/nvs/test_nvs.py +++ b/tests/validation/nvs/test_nvs.py @@ -4,11 +4,23 @@ def test_nvs(dut): LOGGER = logging.getLogger(__name__) - LOGGER.info("Expecting counter value 0") - dut.expect_exact("Current counter value: 0") + LOGGER.info("Expecting default values from Preferences") + dut.expect_exact( + "Values from Preferences: char: A | uchar: 0 | short: 0 | ushort: 0 | int: 0 | uint: 0 | long: 0 | ulong: 0 | " + "long64: 0 | ulong64: 0 | float: 0.00 | double: 0.00 | bool: false | str: str0 | strLen: strLen0 | " + "struct: {id:1,val:100}" + ) - LOGGER.info("Expecting counter value 1") - dut.expect_exact("Current counter value: 1") + LOGGER.info("Expecting updated preferences for the first time") + dut.expect_exact( + "Values from Preferences: char: B | uchar: 1 | short: 1 | ushort: 1 | int: 1 | uint: 1 | long: 1 | ulong: 1 | " + "long64: 1 | ulong64: 1 | float: 1.10 | double: 1.10 | bool: true | str: str1 | strLen: strLen1 | " + "struct: {id:2,val:110}" + ) - LOGGER.info("Expecting counter value 2") - dut.expect_exact("Current counter value: 2") + LOGGER.info("Expecting updated preferences for the second time") + dut.expect_exact( + "Values from Preferences: char: C | uchar: 2 | short: 2 | ushort: 2 | int: 2 | uint: 2 | long: 2 | ulong: 2 | " + "long64: 2 | ulong64: 2 | float: 2.20 | double: 2.20 | bool: false | str: str2 | strLen: strLen2 | " + "struct: {id:3,val:120}" + ) From bad975daa5910b42b48f9f1eda6154c45eb18e6f Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 24 Jun 2025 03:37:07 -0300 Subject: [PATCH 034/102] fix(uart): removes assert() to avoid reset (#11508) --- cores/esp32/esp32-hal-uart.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cores/esp32/esp32-hal-uart.c b/cores/esp32/esp32-hal-uart.c index 2163e9b5f42..21fe88b57fb 100644 --- a/cores/esp32/esp32-hal-uart.c +++ b/cores/esp32/esp32-hal-uart.c @@ -305,7 +305,10 @@ static bool _uartTrySetIomuxPin(uart_port_t uart_num, int io_num, uint32_t idx) } // Assign the correct function to the GPIO. - assert(upin->iomux_func != -1); + if (upin->iomux_func == -1) { + log_e("IO#%d has bad IOMUX internal information. Switching to GPIO Matrix UART function.", io_num); + return false; + } if (uart_num < SOC_UART_HP_NUM) { gpio_iomux_out(io_num, upin->iomux_func, false); // If the pin is input, we also have to redirect the signal, in order to bypass the GPIO matrix. From 36d049659bcea13e4e2afb4463e7f29bc998ffa3 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 24 Jun 2025 12:30:29 +0300 Subject: [PATCH 035/102] IDF release/v5.5 (#11504) * IDF release/v5.5 4c3d086c * fix(prov): Enable BLE provisioning with NimBLE * fix(uart): idf 5.5 new gpio_iomux_* functions (#11507) * fix(uart): idf 5.5 new gpio_iomux_* functions * fix(uart): formatting and style * fix(uart): commentaries style fix * fix(uart): commentaries style fix * fix(uart): commentaries style fix * fix(uart): support to any idf 5.x version * fix(uart): changing assert in order to avoid reset * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> * IDF release/v5.5 cbe9388f --------- Co-authored-by: Sugar Glider Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- cores/esp32/esp32-hal-uart.c | 19 ++++-- .../WiFiProv/examples/WiFiProv/WiFiProv.ino | 2 +- libraries/WiFiProv/src/WiFiProv.cpp | 20 +++--- libraries/WiFiProv/src/WiFiProv.h | 4 +- package/package_esp32_index.template.json | 68 +++++++++---------- 5 files changed, 62 insertions(+), 51 deletions(-) diff --git a/cores/esp32/esp32-hal-uart.c b/cores/esp32/esp32-hal-uart.c index 3e33c588c3b..6e5a22da9f8 100644 --- a/cores/esp32/esp32-hal-uart.c +++ b/cores/esp32/esp32-hal-uart.c @@ -317,13 +317,24 @@ static bool _uartTrySetIomuxPin(uart_port_t uart_num, int io_num, uint32_t idx) } // Assign the correct function to the GPIO. - assert(upin->iomux_func != -1); + if (upin->iomux_func == -1) { + log_e("IO#%d has bad IOMUX internal information. Switching to GPIO Matrix UART function.", io_num); + return false; + } if (uart_num < SOC_UART_HP_NUM) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + if (upin->input) { + gpio_iomux_input(io_num, upin->iomux_func, upin->signal); + } else { + gpio_iomux_output(io_num, upin->iomux_func); + } +#else gpio_iomux_out(io_num, upin->iomux_func, false); // If the pin is input, we also have to redirect the signal, in order to bypass the GPIO matrix. if (upin->input) { gpio_iomux_in(io_num, upin->signal); } +#endif } #if (SOC_UART_LP_NUM >= 1) && (SOC_RTCIO_PIN_COUNT >= 1) else { @@ -1276,11 +1287,11 @@ bool uartSetClockSource(uint8_t uartNum, uart_sclk_t clkSrc) { #if SOC_UART_LP_NUM >= 1 if (uart->num >= SOC_UART_HP_NUM) { switch (clkSrc) { - case UART_SCLK_XTAL: uart->_uart_clock_source = LP_UART_SCLK_XTAL_D2; break; + case UART_SCLK_XTAL: uart->_uart_clock_source = LP_UART_SCLK_XTAL_D2; break; #if CONFIG_IDF_TARGET_ESP32C5 - case UART_SCLK_RTC: uart->_uart_clock_source = LP_UART_SCLK_RC_FAST; break; + case UART_SCLK_RTC: uart->_uart_clock_source = LP_UART_SCLK_RC_FAST; break; #else - case UART_SCLK_RTC: uart->_uart_clock_source = LP_UART_SCLK_LP_FAST; break; + case UART_SCLK_RTC: uart->_uart_clock_source = LP_UART_SCLK_LP_FAST; break; #endif case UART_SCLK_DEFAULT: default: uart->_uart_clock_source = LP_UART_SCLK_DEFAULT; diff --git a/libraries/WiFiProv/examples/WiFiProv/WiFiProv.ino b/libraries/WiFiProv/examples/WiFiProv/WiFiProv.ino index 76025d75770..2f7d8c5089f 100644 --- a/libraries/WiFiProv/examples/WiFiProv/WiFiProv.ino +++ b/libraries/WiFiProv/examples/WiFiProv/WiFiProv.ino @@ -62,7 +62,7 @@ void setup() { WiFi.onEvent(SysProvEvent); // BLE Provisioning using the ESP SoftAP Prov works fine for any BLE SoC, including ESP32, ESP32S3 and ESP32C3. -#if CONFIG_BLUEDROID_ENABLED && !defined(USE_SOFT_AP) +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") && !defined(USE_SOFT_AP) Serial.println("Begin Provisioning using BLE"); // Sample uuid that user can pass during provisioning using BLE uint8_t uuid[16] = {0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02}; diff --git a/libraries/WiFiProv/src/WiFiProv.cpp b/libraries/WiFiProv/src/WiFiProv.cpp index 31337196b5f..f372caf0e49 100644 --- a/libraries/WiFiProv/src/WiFiProv.cpp +++ b/libraries/WiFiProv/src/WiFiProv.cpp @@ -34,7 +34,7 @@ #endif #include -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") #include "network_provisioning/scheme_ble.h" #endif #include @@ -47,7 +47,7 @@ bool wifiLowLevelInit(bool persistent); -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") static const uint8_t custom_service_uuid[16] = { 0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02, }; @@ -61,13 +61,13 @@ static void get_device_service_name(prov_scheme_t prov_scheme, char *service_nam log_e("esp_wifi_get_mac failed!"); return; } -#if CONFIG_IDF_TARGET_ESP32 && defined(CONFIG_BLUEDROID_ENABLED) +#if CONFIG_IDF_TARGET_ESP32 && (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") if (prov_scheme == NETWORK_PROV_SCHEME_BLE) { snprintf(service_name, max, "%s%02X%02X%02X", SERV_NAME_PREFIX_PROV, eth_mac[3], eth_mac[4], eth_mac[5]); } else { #endif snprintf(service_name, max, "%s%02X%02X%02X", SERV_NAME_PREFIX_PROV, eth_mac[3], eth_mac[4], eth_mac[5]); -#if CONFIG_IDF_TARGET_ESP32 && defined(CONFIG_BLUEDROID_ENABLED) +#if CONFIG_IDF_TARGET_ESP32 && (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") } #endif } @@ -78,20 +78,20 @@ void WiFiProvClass ::initProvision(prov_scheme_t prov_scheme, scheme_handler_t s return; } network_prov_mgr_config_t config; -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") if (prov_scheme == NETWORK_PROV_SCHEME_BLE) { config.scheme = network_prov_scheme_ble; } else { #endif config.scheme = network_prov_scheme_softap; -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") } if (scheme_handler == NETWORK_PROV_SCHEME_HANDLER_NONE) { #endif network_prov_event_handler_t scheme_event_handler = NETWORK_PROV_EVENT_HANDLER_NONE; memcpy(&config.scheme_event_handler, &scheme_event_handler, sizeof(network_prov_event_handler_t)); -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") } else if (scheme_handler == NETWORK_PROV_SCHEME_HANDLER_FREE_BTDM) { network_prov_event_handler_t scheme_event_handler = NETWORK_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM; memcpy(&config.scheme_event_handler, &scheme_event_handler, sizeof(network_prov_event_handler_t)); @@ -133,7 +133,7 @@ void WiFiProvClass ::beginProvision( } static char service_name_temp[32]; if (provisioned == false) { -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") if (prov_scheme == NETWORK_PROV_SCHEME_BLE) { service_key = NULL; if (uuid == NULL) { @@ -148,7 +148,7 @@ void WiFiProvClass ::beginProvision( service_name = (const char *)service_name_temp; } -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") if (prov_scheme == NETWORK_PROV_SCHEME_BLE) { log_i("Starting AP using BLE. service_name : %s, pop : %s", service_name, pop); } else { @@ -158,7 +158,7 @@ void WiFiProvClass ::beginProvision( } else { log_i("Starting provisioning AP using SOFTAP. service_name : %s, password : %s, pop : %s", service_name, service_key, pop); } -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") } #endif if (network_prov_mgr_start_provisioning(security, pop, service_name, service_key) != ESP_OK) { diff --git a/libraries/WiFiProv/src/WiFiProv.h b/libraries/WiFiProv/src/WiFiProv.h index b660f8cf064..d34727b6896 100644 --- a/libraries/WiFiProv/src/WiFiProv.h +++ b/libraries/WiFiProv/src/WiFiProv.h @@ -29,7 +29,7 @@ //Select the scheme using which you want to provision typedef enum { NETWORK_PROV_SCHEME_SOFTAP, -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") NETWORK_PROV_SCHEME_BLE, #endif NETWORK_PROV_SCHEME_MAX @@ -37,7 +37,7 @@ typedef enum { typedef enum { NETWORK_PROV_SCHEME_HANDLER_NONE, -#if CONFIG_BLUEDROID_ENABLED +#if (defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)) && __has_include("esp_bt.h") NETWORK_PROV_SCHEME_HANDLER_FREE_BTDM, NETWORK_PROV_SCHEME_HANDLER_FREE_BLE, NETWORK_PROV_SCHEME_HANDLER_FREE_BT, diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 3ae2ff09ee6..1d753792b43 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-28ac0243-v1" + "version": "idf-release_v5.5-cbe9388f-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-28ac0243-v1", + "version": "idf-release_v5.5-cbe9388f-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-28ac0243-v1.zip", - "checksum": "SHA-256:280401ea803d8a782c11ef4f96cfbf80eb12a0f51bd12eac9cb96d6c26489f6e", - "size": "405149394" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", + "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", + "size": "421300036" } ] }, From 882ef25a36399e3c32fc16986f00edddd3324d5a Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Mon, 23 Jun 2025 12:11:32 +0300 Subject: [PATCH 036/102] fix(p4): Update hosted and wifi_remote components --- idf_component.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index 7f090cb26a0..450929a4067 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -105,11 +105,11 @@ dependencies: rules: - if: "target in [esp32s3]" espressif/esp_hosted: - version: "^0.0.25" + version: "^2.0.12" rules: - if: "target == esp32p4" espressif/esp_wifi_remote: - version: "^0.4.1" + version: "^0.13.0" rules: - if: "target == esp32p4" espressif/libsodium: From b7e5169ea17eabedb82171457f08f43c72bd2c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:43:05 +0200 Subject: [PATCH 037/102] fix(spi): Update spi bus for esp32s2 (#11510) --- cores/esp32/esp32-hal-spi.c | 32 ++++++++++++-------------------- cores/esp32/esp32-hal-spi.h | 12 +++--------- libraries/SD/src/SD.cpp | 4 +++- libraries/SPI/src/SPI.cpp | 1 + 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/cores/esp32/esp32-hal-spi.c b/cores/esp32/esp32-hal-spi.c index 6b8e3f8c013..d39aceb5f8d 100644 --- a/cores/esp32/esp32-hal-spi.c +++ b/cores/esp32/esp32-hal-spi.c @@ -79,16 +79,15 @@ struct spi_struct_t { #if CONFIG_IDF_TARGET_ESP32S2 // ESP32S2 -#define SPI_COUNT (3) +#define SPI_COUNT (2) -#define SPI_CLK_IDX(p) ((p == 0) ? SPICLK_OUT_MUX_IDX : ((p == 1) ? FSPICLK_OUT_MUX_IDX : ((p == 2) ? SPI3_CLK_OUT_MUX_IDX : 0))) -#define SPI_MISO_IDX(p) ((p == 0) ? SPIQ_OUT_IDX : ((p == 1) ? FSPIQ_OUT_IDX : ((p == 2) ? SPI3_Q_OUT_IDX : 0))) -#define SPI_MOSI_IDX(p) ((p == 0) ? SPID_IN_IDX : ((p == 1) ? FSPID_IN_IDX : ((p == 2) ? SPI3_D_IN_IDX : 0))) +#define SPI_CLK_IDX(p) ((p == 0) ? FSPICLK_OUT_MUX_IDX : ((p == 1) ? SPI3_CLK_OUT_MUX_IDX : 0)) +#define SPI_MISO_IDX(p) ((p == 0) ? FSPIQ_OUT_IDX : ((p == 1) ? SPI3_Q_OUT_IDX : 0)) +#define SPI_MOSI_IDX(p) ((p == 0) ? FSPID_IN_IDX : ((p == 1) ? SPI3_D_IN_IDX : 0)) -#define SPI_SPI_SS_IDX(n) ((n == 0) ? SPICS0_OUT_IDX : ((n == 1) ? SPICS1_OUT_IDX : 0)) -#define SPI_HSPI_SS_IDX(n) ((n == 0) ? SPI3_CS0_OUT_IDX : ((n == 1) ? SPI3_CS1_OUT_IDX : ((n == 2) ? SPI3_CS2_OUT_IDX : SPI3_CS0_OUT_IDX))) -#define SPI_FSPI_SS_IDX(n) ((n == 0) ? FSPICS0_OUT_IDX : ((n == 1) ? FSPICS1_OUT_IDX : ((n == 2) ? FSPICS2_OUT_IDX : FSPICS0_OUT_IDX))) -#define SPI_SS_IDX(p, n) ((p == 0) ? SPI_SPI_SS_IDX(n) : ((p == 1) ? SPI_SPI_SS_IDX(n) : ((p == 2) ? SPI_HSPI_SS_IDX(n) : 0))) +#define SPI_HSPI_SS_IDX(n) ((n == 0) ? SPI3_CS0_OUT_IDX : ((n == 1) ? SPI3_CS1_OUT_IDX : ((n == 2) ? SPI3_CS2_OUT_IDX : 0))) +#define SPI_FSPI_SS_IDX(n) ((n == 0) ? FSPICS0_OUT_IDX : ((n == 1) ? FSPICS1_OUT_IDX : ((n == 2) ? FSPICS2_OUT_IDX : 0))) +#define SPI_SS_IDX(p, n) ((p == 0) ? SPI_FSPI_SS_IDX(n) : ((p == 1) ? SPI_HSPI_SS_IDX(n) : 0)) #elif CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 @@ -98,8 +97,8 @@ struct spi_struct_t { #define SPI_MISO_IDX(p) ((p == 0) ? FSPIQ_OUT_IDX : ((p == 1) ? SPI3_Q_OUT_IDX : 0)) #define SPI_MOSI_IDX(p) ((p == 0) ? FSPID_IN_IDX : ((p == 1) ? SPI3_D_IN_IDX : 0)) -#define SPI_HSPI_SS_IDX(n) ((n == 0) ? SPI3_CS0_OUT_IDX : ((n == 1) ? SPI3_CS1_OUT_IDX : 0)) -#define SPI_FSPI_SS_IDX(n) ((n == 0) ? FSPICS0_OUT_IDX : ((n == 1) ? FSPICS1_OUT_IDX : 0)) +#define SPI_HSPI_SS_IDX(n) ((n == 0) ? SPI3_CS0_OUT_IDX : ((n == 1) ? SPI3_CS1_OUT_IDX : ((n == 2) ? SPI3_CS2_OUT_IDX : 0))) +#define SPI_FSPI_SS_IDX(n) ((n == 0) ? FSPICS0_OUT_IDX : ((n == 1) ? FSPICS1_OUT_IDX : ((n == 2) ? FSPICS2_OUT_IDX : 0))) #define SPI_SS_IDX(p, n) ((p == 0) ? SPI_FSPI_SS_IDX(n) : ((p == 1) ? SPI_HSPI_SS_IDX(n) : 0)) #elif CONFIG_IDF_TARGET_ESP32P4 @@ -151,11 +150,7 @@ struct spi_struct_t { #define SPI_MUTEX_UNLOCK() // clang-format off static spi_t _spi_bus_array[] = { -#if CONFIG_IDF_TARGET_ESP32S2 - {(volatile spi_dev_t *)(DR_REG_SPI1_BASE), 0, -1, -1, -1, -1, false}, - {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), 1, -1, -1, -1, -1, false}, - {(volatile spi_dev_t *)(DR_REG_SPI3_BASE), 2, -1, -1, -1, -1, false} -#elif CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32S2 ||CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), 0, -1, -1, -1, -1, false}, {(volatile spi_dev_t *)(DR_REG_SPI3_BASE), 1, -1, -1, -1, -1, false} #elif CONFIG_IDF_TARGET_ESP32C2 @@ -179,11 +174,7 @@ static spi_t _spi_bus_array[] = { #define SPI_MUTEX_UNLOCK() xSemaphoreGive(spi->lock) static spi_t _spi_bus_array[] = { -#if CONFIG_IDF_TARGET_ESP32S2 - {(volatile spi_dev_t *)(DR_REG_SPI1_BASE), NULL, 0, -1, -1, -1, -1, false}, - {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), NULL, 1, -1, -1, -1, -1, false}, - {(volatile spi_dev_t *)(DR_REG_SPI3_BASE), NULL, 2, -1, -1, -1, -1, false} -#elif CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 +#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), NULL, 0, -1, -1, -1, -1, false}, {(volatile spi_dev_t *)(DR_REG_SPI3_BASE), NULL, 1, -1, -1, -1, -1, false} #elif CONFIG_IDF_TARGET_ESP32C2 {(volatile spi_dev_t *)(DR_REG_SPI2_BASE), NULL, 0, -1, -1, -1, -1, false} @@ -621,6 +612,7 @@ void spiStopBus(spi_t *spi) { spi_t *spiStartBus(uint8_t spi_num, uint32_t clockDiv, uint8_t dataMode, uint8_t bitOrder) { if (spi_num >= SPI_COUNT) { + log_e("SPI bus index %d is out of range", spi_num); return NULL; } diff --git a/cores/esp32/esp32-hal-spi.h b/cores/esp32/esp32-hal-spi.h index 565ca43f60d..0284fea8829 100644 --- a/cores/esp32/esp32-hal-spi.h +++ b/cores/esp32/esp32-hal-spi.h @@ -27,19 +27,13 @@ extern "C" { #include #define SPI_HAS_TRANSACTION - -#ifdef CONFIG_IDF_TARGET_ESP32S2 -#define FSPI 1 //SPI 1 bus. ESP32S2: for external memory only (can use the same data lines but different SS) -#define HSPI 2 //SPI 2 bus. ESP32S2: external memory or device - it can be matrixed to any pins -#define SPI2 2 // Another name for ESP32S2 SPI 2 -#define SPI3 3 //SPI 3 bus. ESP32S2: device only - it can be matrixed to any pins -#elif CONFIG_IDF_TARGET_ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32 #define FSPI 1 //SPI 1 bus attached to the flash (can use the same data lines but different SS) #define HSPI 2 //SPI 2 bus normally mapped to pins 12 - 15, but can be matrixed to any pins #define VSPI 3 //SPI 3 bus normally attached to pins 5, 18, 19 and 23, but can be matrixed to any pins #else -#define FSPI 0 // ESP32C2, C3, C6, H2, S3, P4 - SPI 2 bus -#define HSPI 1 // ESP32S3, P4 - SPI 3 bus +#define FSPI 0 // ESP32C2, C3, C6, H2, S2, S3, P4 - SPI 2 bus +#define HSPI 1 // ESP32S2, S3, P4 - SPI 3 bus #endif // This defines are not representing the real Divider of the ESP32 diff --git a/libraries/SD/src/SD.cpp b/libraries/SD/src/SD.cpp index eaec2483053..2d646276d87 100644 --- a/libraries/SD/src/SD.cpp +++ b/libraries/SD/src/SD.cpp @@ -27,7 +27,9 @@ bool SDFS::begin(uint8_t ssPin, SPIClass &spi, uint32_t frequency, const char *m return true; } - spi.begin(); + if (!spi.begin()) { + return false; + } _pdrv = sdcard_init(ssPin, &spi, frequency); if (_pdrv == 0xFF) { diff --git a/libraries/SPI/src/SPI.cpp b/libraries/SPI/src/SPI.cpp index c0d7665df03..7d8d44320a6 100644 --- a/libraries/SPI/src/SPI.cpp +++ b/libraries/SPI/src/SPI.cpp @@ -74,6 +74,7 @@ bool SPIClass::begin(int8_t sck, int8_t miso, int8_t mosi, int8_t ss) { _spi = spiStartBus(_spi_num, _div, SPI_MODE0, SPI_MSBFIRST); if (!_spi) { + log_e("SPI bus %d start failed.", _spi_num); return false; } From 30fb3cbb4f2eb749863f39a040601df3decd87a5 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Tue, 24 Jun 2025 08:43:27 -0300 Subject: [PATCH 038/102] fix(docs): Fix links and versions (#11505) * fix(docs): Fix links and versions * fix(docs): Apply suggestions and leftover substitutions --- docs/conf_common.py | 7 +++++++ docs/en/contributing.rst | 4 ++-- docs/en/esp-idf_component.rst | 10 ++++++---- docs/en/guides/docs_contributing.rst | 13 +++++-------- docs/en/index.rst | 1 + docs/en/lib_builder.rst | 16 ++++++++++------ docs/requirements.txt | 2 ++ 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/conf_common.py b/docs/conf_common.py index 676cca899d5..6945c0d190d 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -2,6 +2,12 @@ from esp_docs.conf_docs import * # noqa: F403,F401 +# Used for substituting variables in the documentation +rst_prolog = """ +.. |version| replace:: 3.2.0 +.. |idf_version| replace:: 5.4 +""" + languages = ["en"] # idf_targets = [ @@ -27,6 +33,7 @@ extensions += [ # noqa: F405 "sphinx_copybutton", "sphinx_tabs.tabs", + "sphinx_substitution_extensions", # For allowing substitutions inside code blocks "esp_docs.esp_extensions.dummy_build_system", ] diff --git a/docs/en/contributing.rst b/docs/en/contributing.rst index 4ebe01cbf5b..7a7b99894eb 100644 --- a/docs/en/contributing.rst +++ b/docs/en/contributing.rst @@ -222,7 +222,7 @@ Documentation ------------- If you are contributing to the documentation, please follow the instructions described in the -`documentation guidelines `_ to properly format and test your changes. +`documentation guidelines `_ to properly format and test your changes. Testing and CI -------------- @@ -435,7 +435,7 @@ Documentation Checks ^^^^^^^^^^^^^^^^^^^^ The CI also checks the documentation for any compilation errors. This is important to ensure that the documentation layout is not broken. -To build the documentation locally, please refer to the `documentation guidelines `_. +To build the documentation locally, please refer to the `documentation guidelines `_. Code Style Checks ^^^^^^^^^^^^^^^^^ diff --git a/docs/en/esp-idf_component.rst b/docs/en/esp-idf_component.rst index f38dc44ec0c..13542cc3c87 100644 --- a/docs/en/esp-idf_component.rst +++ b/docs/en/esp-idf_component.rst @@ -14,9 +14,9 @@ For a simplified method, see `Installing using Boards Manager `_. -.. note:: Latest Arduino Core ESP32 version (3.0.X) is now compatible with `ESP-IDF v5.1 `_. Please consider this compatibility when using Arduino as a component in ESP-IDF. +.. note:: Latest Arduino Core ESP32 version (|version|) is now compatible with ESP-IDF v\ |idf_version|\ . Please consider this compatibility when using Arduino as a component in ESP-IDF. -For easiest use of Arduino framework as a ESP-IDF component, you can use the `IDF Component Manager `_ to add the Arduino component to your project. +For easiest use of Arduino framework as a ESP-IDF component, you can use the `IDF Component Manager `_ to add the Arduino component to your project. This will automatically clone the repository and its submodules. You can find the Arduino component in the `ESP Registry `_ together with dependencies list and examples. Installation @@ -32,14 +32,16 @@ Installing using IDF Component Manager To add the Arduino component to your project using the IDF Component Manager, run the following command in your project directory: .. code-block:: bash + :substitutions: - idf.py add-dependency "espressif/arduino-esp32^3.0.2" + idf.py add-dependency "espressif/arduino-esp32^|version|" Or you can start a new project from a template with the Arduino component: .. code-block:: bash + :substitutions: - idf.py create-project-from-example "espressif/arduino-esp32^3.0.2:hello_world" + idf.py create-project-from-example "espressif/arduino-esp32^|version|:hello_world" Manual installation of Arduino framework **************************************** diff --git a/docs/en/guides/docs_contributing.rst b/docs/en/guides/docs_contributing.rst index 20dc4c84cab..13f08c47618 100644 --- a/docs/en/guides/docs_contributing.rst +++ b/docs/en/guides/docs_contributing.rst @@ -49,11 +49,11 @@ Before starting your collaboration, you need to get the documentation source cod Requirements ************ -To properly work with the documentation, you need to install some packages in your system. +To build the documentation properly, you need to install some packages in your system. Note that depending on +your system, you may need to use a virtual environment to install the packages. .. code-block:: - pip install -U Sphinx pip install -r requirements.txt The requirements file is under the ``docs`` folder. @@ -62,17 +62,14 @@ Using Visual Studio Code ************************ If you are using the Visual Studio Code, you can install some extensions to help you while writing documentation. +For reStructuredText, you can install the `reStructuredText Pack `_ extension. -`reStructuredText Pack `_ - -We also recommend you install to grammar check extension to help you to review English grammar. - -`Grammarly `_ +We also recommend you to install some grammar check extension to help you to review English grammar. Building ******** -To build the documentation and generate the HTML files, you can use the following command inside the ``docs`` folder. After a successful build, you can check the files inside the `_build/en/generic/html` folder. +To build the documentation and generate the HTML files, you can use the following command inside the ``docs`` folder. After a successful build, you can check the files inside the ``_build/en/generic/html`` folder. .. code-block:: diff --git a/docs/en/index.rst b/docs/en/index.rst index 1314a8fc78d..89fc7e3bd6e 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -3,6 +3,7 @@ Welcome to ESP32 Arduino Core's documentation ############################################# Here you will find all the relevant information about the project. +This documentation is valid for the Arduino Core for ESP32 version |version| based on ESP-IDF |idf_version|. .. note:: This is a work in progress documentation and we will appreciate your help! We are looking for contributors! diff --git a/docs/en/lib_builder.rst b/docs/en/lib_builder.rst index e7edb331fd3..a8126c18edc 100644 --- a/docs/en/lib_builder.rst +++ b/docs/en/lib_builder.rst @@ -293,8 +293,9 @@ You have two options to run the Docker image to build the libraries. Manually or To run the Docker image manually, use the following command from the root of the ``arduino-esp32`` repository: .. code-block:: bash + :substitutions: - docker run --rm -it -v $PWD:/arduino-esp32 -e TERM=xterm-256color espressif/esp32-arduino-lib-builder:release-v5.1 + docker run --rm -it -v $PWD:/arduino-esp32 -e TERM=xterm-256color espressif/esp32-arduino-lib-builder:release-v|idf_version| This will start the Lib Builder UI for compiling the libraries. The above command explained: @@ -304,7 +305,7 @@ This will start the Lib Builder UI for compiling the libraries. The above comman - ``-t`` Allocate a pseudo-TTY; - ``-e TERM=xterm-256color``: Optional. Sets the terminal type to ``xterm-256color`` to display colors correctly; - ``-v $PWD:/arduino-esp32``: Optional. Mounts the current folder at ``/arduino-esp32`` inside the container. If not provided, the container will not copy the compiled libraries to the host machine; -- ``espressif/esp32-arduino-lib-builder:release-v5.1``: uses Docker image ``espressif/esp32-arduino-lib-builder`` with tag ``release-v5.1``. +- :substitution-code:`espressif/esp32-arduino-lib-builder:release-v|idf_version|`: uses Docker image ``espressif/esp32-arduino-lib-builder`` with tag :substitution-code:`release-v|idf_version|`. The ``latest`` tag is implicitly added by Docker when no tag is specified. It is recommended to use a specific version tag to ensure reproducibility of the build process. .. warning:: @@ -324,24 +325,27 @@ By default the docker container will run the user interface script. If you want For example, to run a terminal inside the container, you can run: .. code-block:: bash + :substitutions: - docker run -it espressif/esp32-arduino-lib-builder:release-v5.1 /bin/bash + docker run -it espressif/esp32-arduino-lib-builder:release-v|idf_version| /bin/bash Running the Docker image using the provided run script will depend on the host OS. Use the following command from the root of the ``arduino-esp32`` repository to execute the image in a Linux or macOS environment for -the ``release-v5.1`` tag: +the :substitution-code:`release-v|idf_version|` tag: .. code-block:: bash + :substitutions: - curl -LJO https://raw.githubusercontent.com/espressif/esp32-arduino-lib-builder/refs/heads/release/v5.1/tools/docker/run.sh + curl -LJO https://raw.githubusercontent.com/espressif/esp32-arduino-lib-builder/refs/heads/release/v|idf_version|/tools/docker/run.sh chmod +x run.sh ./run.sh $PWD For Windows, use the following command in PowerShell from the root of the ``arduino-esp32`` repository: .. code-block:: powershell + :substitutions: - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/espressif/esp32-arduino-lib-builder/refs/heads/release/v5.1/tools/docker/run.ps1" -OutFile "run.ps1" + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/espressif/esp32-arduino-lib-builder/refs/heads/release/v|idf_version|/tools/docker/run.ps1" -OutFile "run.ps1" .\run.ps1 $pwd As the script is unsigned, you may need to change the execution policy of the current session before running the script. diff --git a/docs/requirements.txt b/docs/requirements.txt index d3017fb5adc..ef2ab88cb65 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,7 @@ +sphinx==4.5.0 esp-docs>=1.4.0 sphinx-copybutton==0.5.0 sphinx-tabs==3.2.0 numpydoc==1.5.0 standard-imghdr==3.13.0 +Sphinx-Substitution-Extensions==2022.2.16 From e9b0930f9dd1fb2e1df28c7156fa7892e4306b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:34:17 +0200 Subject: [PATCH 039/102] ci(sizes): Update sizes workflow action --- .github/workflows/publishsizes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publishsizes.yml b/.github/workflows/publishsizes.yml index 8ff591e052b..f3c401d635c 100644 --- a/.github/workflows/publishsizes.yml +++ b/.github/workflows/publishsizes.yml @@ -66,7 +66,7 @@ jobs: path: ./artifacts/sizes-report/pr_num.txt - name: Report results - uses: P-R-O-C-H-Y/report-size-deltas@2043188c68f483a7b50527c4eacf609d05bb67a5 # sizes_v2 + uses: P-R-O-C-H-Y/report-size-deltas@sizes_v2 #2043188c68f483a7b50527c4eacf609d05bb67a5 # sizes_v2 with: sketches-reports-source: ${{ env.SKETCHES_REPORTS_PATH }} github-token: ${{ env.GITHUB_TOKEN }} From 0213200b3d66cae7849623dc6551503346228de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:40:53 +0200 Subject: [PATCH 040/102] ci(sizes): Use commit id in report-size-delta action --- .github/workflows/publishsizes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publishsizes.yml b/.github/workflows/publishsizes.yml index f3c401d635c..fad2418668c 100644 --- a/.github/workflows/publishsizes.yml +++ b/.github/workflows/publishsizes.yml @@ -66,7 +66,7 @@ jobs: path: ./artifacts/sizes-report/pr_num.txt - name: Report results - uses: P-R-O-C-H-Y/report-size-deltas@sizes_v2 #2043188c68f483a7b50527c4eacf609d05bb67a5 # sizes_v2 + uses: P-R-O-C-H-Y/report-size-deltas@bea91d2c99ca80c88a883b39b1c4012f00ec3d09 # sizes_v2 with: sketches-reports-source: ${{ env.SKETCHES_REPORTS_PATH }} github-token: ${{ env.GITHUB_TOKEN }} From 9e61fa7e4bce59c05cb17c15b11b53b9bafca077 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 24 Jun 2025 17:23:50 +0300 Subject: [PATCH 041/102] IDF release/v5.4 (#11512) * IDF release/v5.4 f0f2980d * fix(p4): Allow custom pins on P4 for ESP-Hosted --- libraries/WiFi/src/WiFiGeneric.cpp | 12 +++- package/package_esp32_index.template.json | 68 +++++++++++------------ 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/libraries/WiFi/src/WiFiGeneric.cpp b/libraries/WiFi/src/WiFiGeneric.cpp index 3faf34fef34..bddcb5fd448 100644 --- a/libraries/WiFi/src/WiFiGeneric.cpp +++ b/libraries/WiFi/src/WiFiGeneric.cpp @@ -252,13 +252,23 @@ static bool wifiHostedInit() { if (!hosted_initialized) { hosted_initialized = true; struct esp_hosted_sdio_config conf = INIT_DEFAULT_HOST_SDIO_CONFIG(); +#ifdef BOARD_HAS_SDIO_ESP_HOSTED + conf.pin_clk.pin = BOARD_SDIO_ESP_HOSTED_CLK; + conf.pin_cmd.pin = BOARD_SDIO_ESP_HOSTED_CMD; + conf.pin_d0.pin = BOARD_SDIO_ESP_HOSTED_D0; + conf.pin_d1.pin = BOARD_SDIO_ESP_HOSTED_D1; + conf.pin_d2.pin = BOARD_SDIO_ESP_HOSTED_D2; + conf.pin_d3.pin = BOARD_SDIO_ESP_HOSTED_D3; + conf.pin_reset.pin = BOARD_SDIO_ESP_HOSTED_RESET; +#else conf.pin_clk.pin = CONFIG_ESP_SDIO_PIN_CLK; conf.pin_cmd.pin = CONFIG_ESP_SDIO_PIN_CMD; conf.pin_d0.pin = CONFIG_ESP_SDIO_PIN_D0; conf.pin_d1.pin = CONFIG_ESP_SDIO_PIN_D1; conf.pin_d2.pin = CONFIG_ESP_SDIO_PIN_D2; conf.pin_d3.pin = CONFIG_ESP_SDIO_PIN_D3; - //conf.pin_rst.pin = CONFIG_ESP_SDIO_GPIO_RESET_SLAVE; + conf.pin_reset.pin = CONFIG_ESP_SDIO_GPIO_RESET_SLAVE; +#endif // esp_hosted_sdio_set_config() will fail on second attempt but here temporarily to not cause exception on reinit if (esp_hosted_sdio_set_config(&conf) != ESP_OK || esp_hosted_init() != ESP_OK) { log_e("esp_hosted_init failed!"); diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 22d3cc05455..9c1516dbacd 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.4-aed8bdc8-v1" + "version": "idf-release_v5.4-f0f2980d-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.4-aed8bdc8-v1", + "version": "idf-release_v5.4-f0f2980d-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-aed8bdc8-v1.zip", - "checksum": "SHA-256:448691c3171f79b2136e4ab8006e9c78bd1627156dab1365fff8f8867a6a7e5b", - "size": "353758763" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", + "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", + "size": "353978504" } ] }, From 21640ac82a1bb5efa8cf0b3841be1ac80add6785 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:43:50 +0300 Subject: [PATCH 042/102] fix(webserver): Validate header inputs --- libraries/WebServer/src/WebServer.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libraries/WebServer/src/WebServer.cpp b/libraries/WebServer/src/WebServer.cpp index 652a86f587f..7523e40259b 100644 --- a/libraries/WebServer/src/WebServer.cpp +++ b/libraries/WebServer/src/WebServer.cpp @@ -502,6 +502,16 @@ void WebServer::stop() { } void WebServer::sendHeader(const String &name, const String &value, bool first) { + if (name.indexOf('\r') != -1 || name.indexOf('\n') != -1) { + log_e("Invalid character in HTTP header name"); + return; + } + + if (value.indexOf('\r') != -1 || value.indexOf('\n') != -1) { + log_e("Invalid character in HTTP header value"); + return; + } + RequestArgument *header = new RequestArgument(); header->key = name; header->value = value; From 875b92303510dca333620aa88140a993275d28fd Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Mon, 30 Jun 2025 05:50:17 -0300 Subject: [PATCH 043/102] ci(ext_lib): Skip P4 in ArduinoBLE test (#11520) --- .github/workflows/lib.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lib.json b/.github/workflows/lib.json index 5b93d6689ef..f1ff111d25b 100644 --- a/.github/workflows/lib.json +++ b/.github/workflows/lib.json @@ -9,7 +9,8 @@ { "name": "ArduinoBLE", "exclude_targets": [ - "esp32s2" + "esp32s2", + "esp32p4" ], "sketch_path": [ "~/Arduino/libraries/ArduinoBLE/examples/Central/Scan/Scan.ino" From 9a35d9455f23f0f7055fbc6afb6845979f27cb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Andr=C3=BDsek?= Date: Mon, 30 Jun 2025 10:50:49 +0200 Subject: [PATCH 044/102] feat(SDFS): Add destructor for SD card to clean up resources (#11521) * feat(test): Enhance NVS test * fix(nvs): Remove unused Unity header and improve Serial wait loop * refactor(nvs): Extract string increment logic into a separate function * refactor(test): Format long strings in expect_exact calls for better readability * feat(SDFS): Add destructor to clean up resources --- libraries/SD/src/SD.cpp | 4 ++++ libraries/SD/src/SD.h | 1 + 2 files changed, 5 insertions(+) diff --git a/libraries/SD/src/SD.cpp b/libraries/SD/src/SD.cpp index 2d646276d87..077a7c1121f 100644 --- a/libraries/SD/src/SD.cpp +++ b/libraries/SD/src/SD.cpp @@ -22,6 +22,10 @@ using namespace fs; SDFS::SDFS(FSImplPtr impl) : FS(impl), _pdrv(0xFF) {} +SDFS::~SDFS() { + end(); +} + bool SDFS::begin(uint8_t ssPin, SPIClass &spi, uint32_t frequency, const char *mountpoint, uint8_t max_files, bool format_if_empty) { if (_pdrv != 0xFF) { return true; diff --git a/libraries/SD/src/SD.h b/libraries/SD/src/SD.h index aebc781a5e3..d8252ee44f7 100644 --- a/libraries/SD/src/SD.h +++ b/libraries/SD/src/SD.h @@ -26,6 +26,7 @@ class SDFS : public FS { public: SDFS(FSImplPtr impl); + ~SDFS(); bool begin( uint8_t ssPin = SS, SPIClass &spi = SPI, uint32_t frequency = 4000000, const char *mountpoint = "/sd", uint8_t max_files = 5, bool format_if_empty = false ); From 6754b1962c791de6c0a06120c42ad7f248600703 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Mon, 30 Jun 2025 06:02:12 -0300 Subject: [PATCH 045/102] feat(esp_now): Add support for ESP NOW V2 (#11524) * feat(esp_now): Add support for ESP NOW V2 * fix(esp_now): Return -1 on error --- .../ESP_NOW_Broadcast_Master.ino | 2 + .../ESP_NOW_Broadcast_Slave.ino | 2 + .../ESP_NOW_Network/ESP_NOW_Network.ino | 9 ++- .../ESP_NOW_Serial/ESP_NOW_Serial.ino | 1 + libraries/ESP_NOW/src/ESP32_NOW.cpp | 65 ++++++++++++++++--- libraries/ESP_NOW/src/ESP32_NOW.h | 10 ++- libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp | 24 +++++-- 7 files changed, 98 insertions(+), 15 deletions(-) diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino index 025a53c913b..7b71c0e432d 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Master/ESP_NOW_Broadcast_Master.ino @@ -86,6 +86,8 @@ void setup() { ESP.restart(); } + Serial.printf("ESP-NOW version: %d, max data length: %d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen()); + Serial.println("Setup complete. Broadcasting messages every 5 seconds."); } diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino index e61524b64f9..ca0ece2fff8 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Broadcast_Slave/ESP_NOW_Broadcast_Slave.ino @@ -104,6 +104,8 @@ void setup() { ESP.restart(); } + Serial.printf("ESP-NOW version: %d, max data length: %d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen()); + // Register the new peer callback ESP_NOW.onNewPeer(register_new_master, nullptr); diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino b/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino index 6731340c922..d30d6cd40cf 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Network/ESP_NOW_Network.ino @@ -75,7 +75,12 @@ // The following struct is used to send data to the peer device. // We use the attribute "packed" to ensure that the struct is not padded (all data // is contiguous in the memory and without gaps). -// The maximum size of the complete message is 250 bytes (ESP_NOW_MAX_DATA_LEN). +// The maximum size of the payload is 250 bytes (ESP_NOW_MAX_DATA_LEN) for ESP-NOW v1.0. +// For ESP-NOW v2.0, the maximum size of the payload is 1470 bytes (ESP_NOW_MAX_DATA_LEN_V2). +// You can use ESP_NOW.getMaxDataLen() after calling ESP_NOW.begin() to get the maximum size +// of the data that can be sent. +// Read about the compatibility between ESP-NOW v1.0 and v2.0 in the ESP-IDF documentation: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_now.html#frame-format typedef struct { uint32_t count; @@ -276,6 +281,8 @@ void setup() { fail_reboot(); } + Serial.printf("ESP-NOW version: %d, max data length: %d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen()); + if (!broadcast_peer.begin()) { Serial.println("Failed to initialize broadcast peer"); fail_reboot(); diff --git a/libraries/ESP_NOW/examples/ESP_NOW_Serial/ESP_NOW_Serial.ino b/libraries/ESP_NOW/examples/ESP_NOW_Serial/ESP_NOW_Serial.ino index e1f28382fe2..2a5406cd844 100644 --- a/libraries/ESP_NOW/examples/ESP_NOW_Serial/ESP_NOW_Serial.ino +++ b/libraries/ESP_NOW/examples/ESP_NOW_Serial/ESP_NOW_Serial.ino @@ -64,6 +64,7 @@ void setup() { // Start the ESP-NOW communication Serial.println("ESP-NOW communication starting..."); NowSerial.begin(115200); + Serial.printf("ESP-NOW version: %d, max data length: %d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen()); Serial.println("You can now send data to the peer device using the Serial Monitor.\n"); } diff --git a/libraries/ESP_NOW/src/ESP32_NOW.cpp b/libraries/ESP_NOW/src/ESP32_NOW.cpp index 25a5609e8db..b5cdc3f3da7 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW.cpp +++ b/libraries/ESP_NOW/src/ESP32_NOW.cpp @@ -140,7 +140,10 @@ static void _esp_now_tx_cb(const uint8_t *mac_addr, esp_now_send_status_t status } } -ESP_NOW_Class::ESP_NOW_Class() {} +ESP_NOW_Class::ESP_NOW_Class() { + max_data_len = 0; + version = 0; +} ESP_NOW_Class::~ESP_NOW_Class() {} @@ -155,6 +158,23 @@ bool ESP_NOW_Class::begin(const uint8_t *pmk) { return false; } + // Unfortunately we can't get the ESP-NOW version before initializing the Wi-Fi + uint32_t esp_now_version; + err = esp_now_get_version(&esp_now_version); + if (err != ESP_OK) { + log_w("esp_now_get_version failed! Assuming ESP-NOW v1.0"); + esp_now_version = 1; + } + + if (esp_now_version == 1) { + max_data_len = ESP_NOW_MAX_DATA_LEN; + } else { + max_data_len = ESP_NOW_MAX_DATA_LEN_V2; + } + + version = esp_now_version; + log_i("ESP-NOW version: %lu, max_data_len: %lu", version, max_data_len); + _esp_now_has_begun = true; memset(_esp_now_peers, 0, sizeof(ESP_NOW_Peer *) * ESP_NOW_MAX_TOTAL_PEER_NUM); @@ -212,7 +232,7 @@ bool ESP_NOW_Class::end() { return true; } -int ESP_NOW_Class::getTotalPeerCount() { +int ESP_NOW_Class::getTotalPeerCount() const { if (!_esp_now_has_begun) { return -1; } @@ -225,7 +245,7 @@ int ESP_NOW_Class::getTotalPeerCount() { return num.total_num; } -int ESP_NOW_Class::getEncryptedPeerCount() { +int ESP_NOW_Class::getEncryptedPeerCount() const { if (!_esp_now_has_begun) { return -1; } @@ -238,16 +258,38 @@ int ESP_NOW_Class::getEncryptedPeerCount() { return num.encrypt_num; } +int ESP_NOW_Class::getMaxDataLen() const { + if (max_data_len == 0) { + log_e("ESP-NOW not initialized. Please call begin() first to get the max data length."); + return -1; + } + + return max_data_len; +} + +int ESP_NOW_Class::getVersion() const { + if (version == 0) { + log_e("ESP-NOW not initialized. Please call begin() first to get the version."); + return -1; + } + + return version; +} + int ESP_NOW_Class::availableForWrite() { - return ESP_NOW_MAX_DATA_LEN; + int available = getMaxDataLen(); + if (available < 0) { + return 0; + } + return available; } size_t ESP_NOW_Class::write(const uint8_t *data, size_t len) { if (!_esp_now_has_begun) { return 0; } - if (len > ESP_NOW_MAX_DATA_LEN) { - len = ESP_NOW_MAX_DATA_LEN; + if (len > max_data_len) { + len = max_data_len; } esp_err_t result = esp_now_send(nullptr, data, len); if (result == ESP_OK) { @@ -386,8 +428,15 @@ size_t ESP_NOW_Peer::send(const uint8_t *data, int len) { log_e("Peer not added."); return 0; } - if (len > ESP_NOW_MAX_DATA_LEN) { - len = ESP_NOW_MAX_DATA_LEN; + + int max_data_len = ESP_NOW.getMaxDataLen(); + if (max_data_len < 0) { + log_e("Error getting max data length."); + return 0; + } + + if (len > max_data_len) { + len = max_data_len; } esp_err_t result = esp_now_send(mac, data, len); if (result == ESP_OK) { diff --git a/libraries/ESP_NOW/src/ESP32_NOW.h b/libraries/ESP_NOW/src/ESP32_NOW.h index 5940cfa2221..5b5bbe72673 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW.h +++ b/libraries/ESP_NOW/src/ESP32_NOW.h @@ -23,8 +23,10 @@ class ESP_NOW_Class : public Print { bool begin(const uint8_t *pmk = nullptr /* 16 bytes */); bool end(); - int getTotalPeerCount(); - int getEncryptedPeerCount(); + int getTotalPeerCount() const; + int getEncryptedPeerCount() const; + int getMaxDataLen() const; + int getVersion() const; int availableForWrite(); size_t write(const uint8_t *data, size_t len); @@ -34,6 +36,10 @@ class ESP_NOW_Class : public Print { void onNewPeer(void (*cb)(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg), void *arg); bool removePeer(ESP_NOW_Peer &peer); + +protected: + size_t max_data_len; + uint32_t version; }; class ESP_NOW_Peer { diff --git a/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp b/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp index edd6e32aacc..c86c8546c5b 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp +++ b/libraries/ESP_NOW/src/ESP32_NOW_Serial.cpp @@ -70,8 +70,25 @@ bool ESP_NOW_Serial_Class::begin(unsigned long baud) { //xSemaphoreTake(tx_sem, 0); xSemaphoreGive(tx_sem); } - setRxBufferSize(1024); //default if not preset - setTxBufferSize(1024); //default if not preset + + size_t buf_size = 0; + if (ESP_NOW.getVersion() == 2) { + // ESP-NOW v2.0 has a larger maximum data length, so we need to increase the buffer sizes + // to hold around 3-4 packets + buf_size = setRxBufferSize(4096); + buf_size &= setTxBufferSize(4096); + } else { + // ESP-NOW v1.0 has a smaller maximum data length, so we can use the default buffer sizes + // to hold around 3-4 packets + buf_size = setRxBufferSize(1024); + buf_size &= setTxBufferSize(1024); + } + + if (buf_size == 0) { + log_e("Failed to set buffer size"); + return false; + } + return true; } @@ -164,7 +181,6 @@ void ESP_NOW_Serial_Class::onReceive(const uint8_t *data, size_t len, bool broad //Print int ESP_NOW_Serial_Class::availableForWrite() { - //return ESP_NOW_MAX_DATA_LEN; if (tx_ring_buf == nullptr) { return 0; } @@ -189,7 +205,7 @@ bool ESP_NOW_Serial_Class::checkForTxData() { //do we have something that failed the last time? resend_count = 0; if (queued_buff == nullptr) { - queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, ESP_NOW_MAX_DATA_LEN); + queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, ESP_NOW.getMaxDataLen()); } else { log_d(MACSTR " : PREVIOUS", MAC2STR(addr())); } From 6476260e8fc8759e96a9c4cbff499436e421b2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sch=C3=A4ffersmann?= <117294155+MattiasTF@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:04:11 +0200 Subject: [PATCH 046/102] fix(esp32): Fix appending to Strings longer than 64k (#11523) If oldLen is truncated to uint16_t, appending to a String that is longer than 65535 bytes will create a broken string. --- cores/esp32/WString.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cores/esp32/WString.cpp b/cores/esp32/WString.cpp index 18e64767545..6632aa5f85d 100644 --- a/cores/esp32/WString.cpp +++ b/cores/esp32/WString.cpp @@ -180,7 +180,7 @@ bool String::changeBuffer(unsigned int maxStrLen) { if (maxStrLen < sizeof(sso.buff) - 1) { if (isSSO() || !buffer()) { // Already using SSO, nothing to do - uint16_t oldLen = len(); + size_t oldLen = len(); setSSO(true); setLen(oldLen); } else { // if bufptr && !isSSO() @@ -188,7 +188,7 @@ bool String::changeBuffer(unsigned int maxStrLen) { char temp[sizeof(sso.buff)]; memcpy(temp, buffer(), maxStrLen); free(wbuffer()); - uint16_t oldLen = len(); + size_t oldLen = len(); setSSO(true); memcpy(wbuffer(), temp, maxStrLen); setLen(oldLen); @@ -201,7 +201,7 @@ bool String::changeBuffer(unsigned int maxStrLen) { if (newSize > CAPACITY_MAX) { return false; } - uint16_t oldLen = len(); + size_t oldLen = len(); char *newbuffer = (char *)realloc(isSSO() ? nullptr : wbuffer(), newSize); if (newbuffer) { size_t oldSize = capacity() + 1; // include NULL. From 6e60f2f6b9e28ac20082e7e7387eaa90f362da48 Mon Sep 17 00:00:00 2001 From: LusimiCollado Date: Mon, 30 Jun 2025 12:18:02 +0200 Subject: [PATCH 047/102] =?UTF-8?q?feat(variant):=20add=20kode=20dot=20ESP?= =?UTF-8?q?32-S3=20board=20with=20QSPI=20LCD,=20SD=20and=20GPIO=20?= =?UTF-8?q?=E2=80=A6=20(#11371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(variant): add KodeDot ESP32-S3 board with QSPI LCD, SD and GPIO header * fix(kodedot): Reorder board definitions and translate comments to English * fix(kodedot): Clean up OTA override and remove unused partition menu for kode dot * fix(kodedot): Build board changed from ESP32S3_DEV to KODE_DOT on kode dot board * ci(pre-commit): Fix whitespace, EOLs and codespell 'Analog' * ci(pre-commit): Add bash script formatter * fix(merge): New name and description for custom merge tool and change partitions table to variants folder --- boards.txt | 57 ++++++++++++ platform.txt | 16 ++++ variants/kodedot/custom_ota_override.cpp | 14 +++ variants/kodedot/kodedot_partitions.csv | 7 ++ variants/kodedot/pins_arduino.h | 107 +++++++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 variants/kodedot/custom_ota_override.cpp create mode 100644 variants/kodedot/kodedot_partitions.csv create mode 100644 variants/kodedot/pins_arduino.h diff --git a/boards.txt b/boards.txt index b51a8840757..ceb0dc63d02 100644 --- a/boards.txt +++ b/boards.txt @@ -50547,3 +50547,60 @@ cyobot_v2_esp32s3.menu.ZigbeeMode.zczr.build.zigbee_mode=-DZIGBEE_MODE_ZCZR cyobot_v2_esp32s3.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api.zczr -lzboss_stack.zczr -lzboss_port.remote ############################################################## + +kodedot.name=kode dot + +kodedot.bootloader.tool=esptool_py +kodedot.bootloader.tool.default=esptool_py + +kodedot.upload.tool=esptool_py_app_only +kodedot.upload.tool.default=esptool_py_app_only +kodedot.upload.tool.network=esp_ota + +kodedot.upload.maximum_size=8388608 +kodedot.upload.maximum_data_size=327680 +kodedot.upload.flags= +kodedot.upload.extra_flags= +kodedot.upload.use_1200bps_touch=false +kodedot.upload.wait_for_upload_port=false +kodedot.upload.speed=921600 + +kodedot.upload.erase_cmd= + +kodedot.serial.disableDTR=false +kodedot.serial.disableRTS=false + +kodedot.build.tarch=xtensa +kodedot.build.bootloader_addr=0x0 +kodedot.build.target=esp32s3 +kodedot.build.mcu=esp32s3 +kodedot.build.core=esp32 +kodedot.build.variant=kodedot +kodedot.build.board=KODE_DOT + +kodedot.build.usb_mode=1 +kodedot.build.cdc_on_boot=1 +kodedot.build.msc_on_boot=0 +kodedot.build.dfu_on_boot=0 + +kodedot.build.f_cpu=240000000L + +kodedot.build.flash_offset=0x400000 +kodedot.build.flash_size=16MB +kodedot.build.flash_freq=80m +kodedot.build.flash_mode=dio + +kodedot.build.custom_partitions=kodedot_partitions + +kodedot.build.psram_type=qspi +kodedot.build.defines= + +kodedot.build.loop_core=-DARDUINO_RUNNING_CORE=1 +kodedot.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=1 + +kodedot.recipe.hooks.objcopy.postobjcopy.3.pattern= +kodedot.recipe.hooks.objcopy.postobjcopy.3.pattern_args= + +kodedot.recipe.output.save_file={build.project_name}.ino.bin + +############################################################## diff --git a/platform.txt b/platform.txt index 8c7f4432377..7bc89426323 100644 --- a/platform.txt +++ b/platform.txt @@ -330,3 +330,19 @@ tools.dfu-util.cmd=dfu-util tools.dfu-util.upload.params.verbose=-d tools.dfu-util.upload.params.quiet= tools.dfu-util.upload.pattern="{path}/{cmd}" --device {vid.0}:{pid.0} -D "{build.path}/{build.project_name}.bin" -Q + +## -------------------------------------------------------------------------- +## esptool_py_app_only is used to upload only the application image +## It won't upload the bootloader or any other binary except for the main application +## -------------------------------------------------------------------------- +tools.esptool_py_app_only.path={runtime.tools.esptool_py.path} +tools.esptool_py_app_only.cmd=esptool +tools.esptool_py_app_only.cmd.windows=esptool.exe + +tools.esptool_py_app_only.upload.protocol=serial +tools.esptool_py_app_only.upload.params.verbose= +tools.esptool_py_app_only.upload.params.quiet= + +tools.esptool_py_app_only.upload.pattern_args=--chip {build.mcu} --port "{serial.port}" --baud {upload.speed} {upload.flags} --before default_reset --after hard_reset write_flash --flash_mode {build.flash_mode} --flash_freq {build.flash_freq} --flash_size {build.flash_size} {build.flash_offset} "{build.path}/{build.project_name}.bin" {upload.extra_flags} + +tools.esptool_py_app_only.upload.pattern="{path}/{cmd}" {tools.esptool_py_app_only.upload.pattern_args} diff --git a/variants/kodedot/custom_ota_override.cpp b/variants/kodedot/custom_ota_override.cpp new file mode 100644 index 00000000000..a41db01db16 --- /dev/null +++ b/variants/kodedot/custom_ota_override.cpp @@ -0,0 +1,14 @@ +// custom_ota_override.cpp +// This function overrides the weak definition of `verifyRollbackLater()` in the kode dot board. + +extern "C" { +// Declare the weak function symbol to override it +bool verifyRollbackLater() __attribute__((weak)); +} + +// Custom implementation of verifyRollbackLater() +// Returning `true` prevents the OTA image from being automatically marked as valid. +// This ensures that the system will roll back to the previous image unless it is explicitly validated later. +bool verifyRollbackLater() { + return true; +} diff --git a/variants/kodedot/kodedot_partitions.csv b/variants/kodedot/kodedot_partitions.csv new file mode 100644 index 00000000000..f0732330850 --- /dev/null +++ b/variants/kodedot/kodedot_partitions.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +otadata, data, ota, 0x10000, 0x2000, +ota_0, app, ota_0, 0x20000, 0x3E0000, +ota_1, app, ota_1, 0x400000, 0x800000, +storage, data, spiffs, 0xC00000, 0x400000, diff --git a/variants/kodedot/pins_arduino.h b/variants/kodedot/pins_arduino.h new file mode 100644 index 00000000000..f19ddb8a616 --- /dev/null +++ b/variants/kodedot/pins_arduino.h @@ -0,0 +1,107 @@ +/* + ──────────────────────────────────────────────────────────────────────── + KodeDot – ESP32-S3R8 Variant + Pin definition file for the Arduino-ESP32 core + ──────────────────────────────────────────────────────────────────────── + * External 2 × 10 connector → simple aliases PIN1 … PIN20 + * On-board QSPI LCD 410×502 @40 MHz (SPI3_HOST) + * micro-SD on SPI2_HOST + * Dual-I²C: external (GPIO37/36) + internal-sensors (GPIO48/47) + * USB VID/PID 0x303A:0x1001 +*/ + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +/*──────────────── USB device descriptor ────────────────*/ +#define USB_VID 0x303A // Espressif Systems VID +#define USB_PID 0x1001 // Product ID: KodeDot-S3 + +/*──────────────── UART0 (Arduino Serial) ────────────────*/ +static const uint8_t TX = 43; // U0TXD – PIN16 on the 2×10 header +static const uint8_t RX = 44; // U0RXD – PIN18 on the 2×10 header + +/*──────────────── I²C buses ─────────────────────────────*/ +/* External expansion bus → header pins 11/13 */ +static const uint8_t SCL = 37; // GPIO37 – PIN12 +static const uint8_t SDA = 36; // GPIO36 – PIN14 + +/* Internal sensor/touch bus (not on header) */ +#define INT_I2C_SCL 47 // GPIO47 +#define INT_I2C_SDA 48 // GPIO48 + +/*──────────────── SPI2 – micro-SD ───────────────────────*/ +static const uint8_t SS = 15; // SD_CS +static const uint8_t MOSI = 16; // SD_MOSI +static const uint8_t MISO = 18; // SD_MISO +static const uint8_t SCK = 17; // SD_CLK +#define BOARD_HAS_SD_SPI +#define SD_CS SS + +/*──────────────── QSPI LCD (SPI3_HOST) ─────────────────– + * Controller: ST7789 / 4-line SPI (no D/C pin) + * Resolution: 410×502 px, 16 bpp, RGB color-space + * Clock: 40 MHz + */ +#define BOARD_HAS_SPI_LCD +#define LCD_MODEL ST7789 +#define LCD_WIDTH 410 +#define LCD_HEIGHT 502 + +#define LCD_HOST SPI3_HOST +#define LCD_SCK 35 // GPIO35 • QSPI_CLK +#define LCD_MOSI 33 // GPIO33 • QSPI_IO0 (D0) +#define LCD_IO1 34 // GPIO34 • QSPI_IO1 (D1) +#define LCD_IO2 37 // GPIO37 • QSPI_IO2 (D2) +#define LCD_IO3 36 // GPIO36 • QSPI_IO3 (D3) +#define LCD_CS 10 // GPIO10 +#define LCD_RST 9 // GPIO09 +#define LCD_DC -1 // not used in 4-line SPI +/* Optional: back-light enable shares the NeoPixel pin */ +#define LCD_BL 5 // GPIO05 (same as NEOPIXEL) + +/*──────────────── Analog / Touch pads ────────────────*/ +static const uint8_t A0 = 11; // PIN4 – GPIO11 / TOUCH11 / ADC2_CH0 +static const uint8_t A1 = 12; // PIN6 – GPIO12 / TOUCH12 / ADC2_CH1 +static const uint8_t A2 = 13; // PIN8 – GPIO13 / TOUCH13 / ADC2_CH2 +static const uint8_t A3 = 14; // PIN10 – GPIO14 / TOUCH14 / ADC2_CH3 +static const uint8_t T0 = A0, T1 = A1, T2 = A2, T3 = A3; + +/*──────────────── On-board controls & indicator ─────────*/ +#define BUTTON_TOP 0 // GPIO00 – BOOT • active-LOW +#define BUTTON_BOTTOM 6 // GPIO06 • active-LOW +#define NEOPIXEL_PIN 5 // GPIO05 – WS2812 +#define LED_BUILTIN NEOPIXEL_PIN + +/*──────────────── JTAG (also on connector) ──────────────*/ +#define MTCK 39 // PIN11 – GPIO39 +#define MTDO 40 // PIN13 – GPIO40 +#define MTDI 41 // PIN15 – GPIO41 +#define MTMS 42 // PIN17 – GPIO42 + +/*──────────────── 2×10 header: simple aliases ─────────── + NOTE: power pins (1 = 5 V, 2 = 3 V3, 19/20 = GND) are **not** + exposed as GPIO numbers – they remain undefined here. */ +#define PIN3 1 // GPIO01 / TOUCH1 / ADC1_CH0 +#define PIN4 11 // GPIO11 / TOUCH11 / ADC2_CH0 +#define PIN5 2 // GPIO02 / TOUCH2 / ADC1_CH1 +#define PIN6 12 // GPIO12 / TOUCH12 / ADC2_CH1 +#define PIN7 3 // GPIO03 / TOUCH3 / ADC1_CH2 +#define PIN8 13 // GPIO13 / TOUCH13 / ADC2_CH2 +#define PIN9 4 // GPIO04 / TOUCH4 / ADC1_CH3 +#define PIN10 14 // GPIO14 / TOUCH14 / ADC2_CH3 +#define PIN11 39 // MTCK +#define PIN12 37 // SCL (external I²C) +#define PIN13 40 // MTDO +#define PIN14 36 // SDA (external I²C) +#define PIN15 41 // MTDI +#define PIN16 43 // TX (U0TXD) +#define PIN17 42 // MTMS +#define PIN18 44 // RX (U0RXD) +/* PIN1, PIN2, PIN19, PIN20 are power/ground and deliberately + left undefined – they are **not** usable as GPIO. */ + +#endif /* Pins_Arduino_h */ From 2592a7b3bb475554385b501a6a6f4a0d1b2fe060 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Mon, 30 Jun 2025 16:03:38 +0300 Subject: [PATCH 048/102] feat(p4): Add method to set the pins for SDIO to WiFi chip (#11513) * feat(p4): Add method to set the pins for SDIO to WiFi chip * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- libraries/WiFi/src/WiFiGeneric.cpp | 86 +++++++++++++++++++++--------- libraries/WiFi/src/WiFiGeneric.h | 5 ++ 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/libraries/WiFi/src/WiFiGeneric.cpp b/libraries/WiFi/src/WiFiGeneric.cpp index bddcb5fd448..ec661ef7d8f 100644 --- a/libraries/WiFi/src/WiFiGeneric.cpp +++ b/libraries/WiFi/src/WiFiGeneric.cpp @@ -246,29 +246,67 @@ extern "C" { extern esp_err_t esp_hosted_init(); extern esp_err_t esp_hosted_deinit(); }; +typedef struct { + uint8_t pin_clk; + uint8_t pin_cmd; + uint8_t pin_d0; + uint8_t pin_d1; + uint8_t pin_d2; + uint8_t pin_d3; + uint8_t pin_reset; +} sdio_pin_config_t; + static bool hosted_initialized = false; +static sdio_pin_config_t sdio_pin_config = { +#ifdef BOARD_HAS_SDIO_ESP_HOSTED + .pin_clk = BOARD_SDIO_ESP_HOSTED_CLK, + .pin_cmd = BOARD_SDIO_ESP_HOSTED_CMD, + .pin_d0 = BOARD_SDIO_ESP_HOSTED_D0, + .pin_d1 = BOARD_SDIO_ESP_HOSTED_D1, + .pin_d2 = BOARD_SDIO_ESP_HOSTED_D2, + .pin_d3 = BOARD_SDIO_ESP_HOSTED_D3, + .pin_reset = BOARD_SDIO_ESP_HOSTED_RESET +#else + .pin_clk = CONFIG_ESP_SDIO_PIN_CLK, + .pin_cmd = CONFIG_ESP_SDIO_PIN_CMD, + .pin_d0 = CONFIG_ESP_SDIO_PIN_D0, + .pin_d1 = CONFIG_ESP_SDIO_PIN_D1, + .pin_d2 = CONFIG_ESP_SDIO_PIN_D2, + .pin_d3 = CONFIG_ESP_SDIO_PIN_D3, + .pin_reset = CONFIG_ESP_SDIO_GPIO_RESET_SLAVE +#endif +}; + +bool WiFiGenericClass::setPins(int8_t clk, int8_t cmd, int8_t d0, int8_t d1, int8_t d2, int8_t d3, int8_t rst) { + if (clk < 0 || cmd < 0 || d0 < 0 || d1 < 0 || d2 < 0 || d3 < 0 || rst < 0) { + log_e("All SDIO pins must be defined"); + return false; + } + if (hosted_initialized) { + log_e("SDIO pins must be set before WiFi is initialized"); + return false; + } + sdio_pin_config.pin_clk = clk; + sdio_pin_config.pin_cmd = cmd; + sdio_pin_config.pin_d0 = d0; + sdio_pin_config.pin_d1 = d1; + sdio_pin_config.pin_d2 = d2; + sdio_pin_config.pin_d3 = d3; + sdio_pin_config.pin_reset = rst; + return true; +} static bool wifiHostedInit() { if (!hosted_initialized) { hosted_initialized = true; struct esp_hosted_sdio_config conf = INIT_DEFAULT_HOST_SDIO_CONFIG(); -#ifdef BOARD_HAS_SDIO_ESP_HOSTED - conf.pin_clk.pin = BOARD_SDIO_ESP_HOSTED_CLK; - conf.pin_cmd.pin = BOARD_SDIO_ESP_HOSTED_CMD; - conf.pin_d0.pin = BOARD_SDIO_ESP_HOSTED_D0; - conf.pin_d1.pin = BOARD_SDIO_ESP_HOSTED_D1; - conf.pin_d2.pin = BOARD_SDIO_ESP_HOSTED_D2; - conf.pin_d3.pin = BOARD_SDIO_ESP_HOSTED_D3; - conf.pin_reset.pin = BOARD_SDIO_ESP_HOSTED_RESET; -#else - conf.pin_clk.pin = CONFIG_ESP_SDIO_PIN_CLK; - conf.pin_cmd.pin = CONFIG_ESP_SDIO_PIN_CMD; - conf.pin_d0.pin = CONFIG_ESP_SDIO_PIN_D0; - conf.pin_d1.pin = CONFIG_ESP_SDIO_PIN_D1; - conf.pin_d2.pin = CONFIG_ESP_SDIO_PIN_D2; - conf.pin_d3.pin = CONFIG_ESP_SDIO_PIN_D3; - conf.pin_reset.pin = CONFIG_ESP_SDIO_GPIO_RESET_SLAVE; -#endif + conf.pin_clk.pin = sdio_pin_config.pin_clk; + conf.pin_cmd.pin = sdio_pin_config.pin_cmd; + conf.pin_d0.pin = sdio_pin_config.pin_d0; + conf.pin_d1.pin = sdio_pin_config.pin_d1; + conf.pin_d2.pin = sdio_pin_config.pin_d2; + conf.pin_d3.pin = sdio_pin_config.pin_d3; + conf.pin_reset.pin = sdio_pin_config.pin_reset; // esp_hosted_sdio_set_config() will fail on second attempt but here temporarily to not cause exception on reinit if (esp_hosted_sdio_set_config(&conf) != ESP_OK || esp_hosted_init() != ESP_OK) { log_e("esp_hosted_init failed!"); @@ -279,13 +317,13 @@ static bool wifiHostedInit() { } // Attach pins to PeriMan here // Slave chip model is CONFIG_IDF_SLAVE_TARGET - // CONFIG_ESP_SDIO_PIN_CMD - // CONFIG_ESP_SDIO_PIN_CLK - // CONFIG_ESP_SDIO_PIN_D0 - // CONFIG_ESP_SDIO_PIN_D1 - // CONFIG_ESP_SDIO_PIN_D2 - // CONFIG_ESP_SDIO_PIN_D3 - // CONFIG_ESP_SDIO_GPIO_RESET_SLAVE + // sdio_pin_config.pin_clk + // sdio_pin_config.pin_cmd + // sdio_pin_config.pin_d0 + // sdio_pin_config.pin_d1 + // sdio_pin_config.pin_d2 + // sdio_pin_config.pin_d3 + // sdio_pin_config.pin_reset return true; } diff --git a/libraries/WiFi/src/WiFiGeneric.h b/libraries/WiFi/src/WiFiGeneric.h index ed216229ed4..bdfa7b5dd85 100644 --- a/libraries/WiFi/src/WiFiGeneric.h +++ b/libraries/WiFi/src/WiFiGeneric.h @@ -82,6 +82,11 @@ class WiFiGenericClass { public: WiFiGenericClass(); +#if CONFIG_ESP_WIFI_REMOTE_ENABLED + // Set SDIO pins for connection to external ESP MCU + static bool setPins(int8_t clk, int8_t cmd, int8_t d0, int8_t d1, int8_t d2, int8_t d3, int8_t rst); +#endif + wifi_event_id_t onEvent(WiFiEventCb cbEvent, arduino_event_id_t event = ARDUINO_EVENT_MAX); wifi_event_id_t onEvent(WiFiEventFuncCb cbEvent, arduino_event_id_t event = ARDUINO_EVENT_MAX); wifi_event_id_t onEvent(WiFiEventSysCb cbEvent, arduino_event_id_t event = ARDUINO_EVENT_MAX); From 8cf0818d824837ff1b13b63f10643c93adeecef6 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:17:00 +0200 Subject: [PATCH 049/102] make adresses for partitions.bin and boot_app0.bin configureable (#11534) --- tools/pioarduino-build.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/pioarduino-build.py b/tools/pioarduino-build.py index b6a0c0435c5..b67580e264c 100644 --- a/tools/pioarduino-build.py +++ b/tools/pioarduino-build.py @@ -216,8 +216,14 @@ def add_tinyuf2_extra_image(): "0x1000" if build_mcu in ["esp32", "esp32s2"] else ("0x2000" if build_mcu in ["esp32p4"] else "0x0000"), get_bootloader_image(variants_dir), ), - ("0x8000", join(env.subst("$BUILD_DIR"), "partitions.bin")), - ("0xe000", join(FRAMEWORK_DIR, "tools", "partitions", "boot_app0.bin")), + ( + board_config.get("upload.arduino.partitions_bin", "0x8000"), + join(env.subst("$BUILD_DIR"), "partitions.bin"), + ), + ( + board_config.get("upload.arduino.boot_app0", "0xe000"), + join(FRAMEWORK_DIR, "tools", "partitions", "boot_app0.bin"), + ), ] + [(offset, join(FRAMEWORK_DIR, img)) for offset, img in board_config.get("upload.arduino.flash_extra_images", [])], ) From f4fdecc60c465384e465a4b1d2bd1eac8f67912e Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 2 Jul 2025 14:41:06 +0300 Subject: [PATCH 050/102] fix(csrf): Fix SCRF vulnerability in OTA examples and libraries (#11530) * fix(csrf): Fix SCRF vulnerability in WebUpdate.ino * fix(csrf): Prevent CSRF on other OTA examples * fix(csrf): Require auth user and pass to be changed * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../HTTPUpdateServer/src/HTTPUpdateServer.h | 19 ++++++++ .../examples/OTAWebUpdater/OTAWebUpdater.ino | 35 ++++++++++++++- .../examples/WebUpdate/WebUpdate.ino | 43 ++++++++++++++++--- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h b/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h index bb32bc03fdb..65d8cbaa783 100644 --- a/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h +++ b/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h @@ -27,6 +27,7 @@ static const char serverIndex[] PROGMEM = )"; static const char successResponse[] PROGMEM = "Update Success! Rebooting..."; +static const char *csrfHeaders[2] = {"Origin", "Host"}; class HTTPUpdateServer { public: @@ -56,6 +57,9 @@ class HTTPUpdateServer { _username = username; _password = password; + // collect headers for CSRF verification + _server->collectHeaders(csrfHeaders, 2); + // handler for the /update form page _server->on(path.c_str(), HTTP_GET, [&]() { if (_username != emptyString && _password != emptyString && !_server->authenticate(_username.c_str(), _password.c_str())) { @@ -69,6 +73,10 @@ class HTTPUpdateServer { path.c_str(), HTTP_POST, [&]() { if (!_authenticated) { + if (_username == emptyString || _password == emptyString) { + _server->send(200, F("text/html"), String(F("Update error: Wrong origin received!"))); + return; + } return _server->requestAuthentication(); } if (Update.hasError()) { @@ -100,6 +108,17 @@ class HTTPUpdateServer { return; } + String origin = _server->header(String(csrfHeaders[0])); + String host = _server->header(String(csrfHeaders[1])); + String expectedOrigin = String("http://") + host; + if (origin != expectedOrigin) { + if (_serial_output) { + Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); + } + _authenticated = false; + return; + } + if (_serial_output) { Serial.printf("Update: %s\n", upload.filename.c_str()); } diff --git a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino index 7059bef4496..39d6cbce4af 100644 --- a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino +++ b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino @@ -8,10 +8,17 @@ #define SSID_FORMAT "ESP32-%06lX" // 12 chars total //#define PASSWORD "test123456" // generate if remarked +// Set the username and password for firmware upload +const char *authUser = "........"; +const char *authPass = "........"; + WebServer server(80); Ticker tkSecond; uint8_t otaDone = 0; +const char *csrfHeaders[2] = {"Origin", "Host"}; +static bool authenticated = false; + const char *alphanum = "0123456789!@#$%^&*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; String generatePass(uint8_t str_len) { String buff; @@ -38,6 +45,9 @@ void apMode() { } void handleUpdateEnd() { + if (!authenticated) { + return server.requestAuthentication(); + } server.sendHeader("Connection", "close"); if (Update.hasError()) { server.send(502, "text/plain", Update.errorString()); @@ -45,6 +55,7 @@ void handleUpdateEnd() { server.sendHeader("Refresh", "10"); server.sendHeader("Location", "/"); server.send(307); + delay(500); ESP.restart(); } } @@ -56,18 +67,34 @@ void handleUpdate() { } HTTPUpload &upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { + authenticated = server.authenticate(authUser, authPass); + if (!authenticated) { + Serial.println("Authentication fail!"); + otaDone = 0; + return; + } + String origin = server.header(String(csrfHeaders[0])); + String host = server.header(String(csrfHeaders[1])); + String expectedOrigin = String("http://") + host; + if (origin != expectedOrigin) { + Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); + authenticated = false; + otaDone = 0; + return; + } + Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize); if (!Update.begin(fsize)) { otaDone = 0; Update.printError(Serial); } - } else if (upload.status == UPLOAD_FILE_WRITE) { + } else if (authenticated && upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } else { otaDone = 100 * Update.progress() / Update.size(); } - } else if (upload.status == UPLOAD_FILE_END) { + } else if (authenticated && upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { Serial.printf("Update Success: %u bytes\nRebooting...\n", upload.totalSize); } else { @@ -78,6 +105,7 @@ void handleUpdate() { } void webServerInit() { + server.collectHeaders(csrfHeaders, 2); server.on( "/update", HTTP_POST, []() { @@ -92,6 +120,9 @@ void webServerInit() { server.send_P(200, "image/x-icon", favicon_ico_gz, favicon_ico_gz_len); }); server.onNotFound([]() { + if (!server.authenticate(authUser, authPass)) { + return server.requestAuthentication(); + } server.send(200, "text/html", indexHtml); }); server.begin(); diff --git a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino index 10ddb5e7b64..9e45de7d985 100644 --- a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino +++ b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino @@ -12,10 +12,17 @@ const char *host = "esp32-webupdate"; const char *ssid = "........"; const char *password = "........"; +// Set the username and password for firmware upload +const char *authUser = "........"; +const char *authPass = "........"; + WebServer server(80); const char *serverIndex = "
"; +const char *csrfHeaders[2] = {"Origin", "Host"}; +static bool authenticated = false; + void setup(void) { Serial.begin(115200); Serial.println(); @@ -24,37 +31,63 @@ void setup(void) { WiFi.begin(ssid, password); if (WiFi.waitForConnectResult() == WL_CONNECTED) { MDNS.begin(host); + server.collectHeaders(csrfHeaders, 2); server.on("/", HTTP_GET, []() { + if (!server.authenticate(authUser, authPass)) { + return server.requestAuthentication(); + } server.sendHeader("Connection", "close"); server.send(200, "text/html", serverIndex); }); server.on( "/update", HTTP_POST, []() { + if (!authenticated) { + return server.requestAuthentication(); + } server.sendHeader("Connection", "close"); - server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); - ESP.restart(); + if (Update.hasError()) { + server.send(200, "text/plain", "FAIL"); + } else { + server.send(200, "text/plain", "Success! Rebooting..."); + delay(500); + ESP.restart(); + } }, []() { HTTPUpload &upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { Serial.setDebugOutput(true); + authenticated = server.authenticate(authUser, authPass); + if (!authenticated) { + Serial.println("Authentication fail!"); + return; + } + String origin = server.header(String(csrfHeaders[0])); + String host = server.header(String(csrfHeaders[1])); + String expectedOrigin = String("http://") + host; + if (origin != expectedOrigin) { + Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); + authenticated = false; + return; + } + Serial.printf("Update: %s\n", upload.filename.c_str()); if (!Update.begin()) { //start with max available size Update.printError(Serial); } - } else if (upload.status == UPLOAD_FILE_WRITE) { + } else if (authenticated && upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } - } else if (upload.status == UPLOAD_FILE_END) { + } else if (authenticated && upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { //true to set the size to the current progress Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); } else { Update.printError(Serial); } Serial.setDebugOutput(false); - } else { + } else if (authenticated) { Serial.printf("Update Failed Unexpectedly (likely broken connection): status=%d\n", upload.status); } } From 212b12b625f7dca9b646a2ae5c422936c7ef0ab6 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 2 Jul 2025 14:41:39 +0300 Subject: [PATCH 051/102] IDF release/v5.4 (#11517) * IDF release/v5.4 dfa785ed * IDF release/v5.4 72775cd6 * IDF release/v5.4 858a988d --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 9c1516dbacd..bbb77f8ef5a 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.4-f0f2980d-v1" + "version": "idf-release_v5.4-858a988d-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.4-f0f2980d-v1", + "version": "idf-release_v5.4-858a988d-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-f0f2980d-v1.zip", - "checksum": "SHA-256:b1a77c83634111d78a356f1d1756dc815eeeb91605c5d8714a0db7cccbd0bede", - "size": "353978504" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip", + "checksum": "SHA-256:4fc6eace642735036404c722d5602a379fe16378c72ffd060096cbef1c62d69d", + "size": "354134726" } ] }, From bc7a549876c33686f6d9050e71a2f045717c41e2 Mon Sep 17 00:00:00 2001 From: HighDoping Date: Wed, 2 Jul 2025 19:54:12 +0800 Subject: [PATCH 052/102] fix(example): led flash not working if not using default model in camera example. (#11466) * fix(example): led flash not working if not using default model in camera example fix(example): add camera_config.h and enable LED FLASH based on board model fix(example): Remove face detection description as no longer supported * fix(example): add header guard for board_config.h --- .../CameraWebServer/CameraWebServer.ino | 39 +++---------------- .../Camera/CameraWebServer/app_httpd.cpp | 30 +++++++------- .../Camera/CameraWebServer/board_config.h | 34 ++++++++++++++++ 3 files changed, 53 insertions(+), 50 deletions(-) create mode 100644 libraries/ESP32/examples/Camera/CameraWebServer/board_config.h diff --git a/libraries/ESP32/examples/Camera/CameraWebServer/CameraWebServer.ino b/libraries/ESP32/examples/Camera/CameraWebServer/CameraWebServer.ino index d483e11b1df..83733d4b5cf 100644 --- a/libraries/ESP32/examples/Camera/CameraWebServer/CameraWebServer.ino +++ b/libraries/ESP32/examples/Camera/CameraWebServer/CameraWebServer.ino @@ -1,37 +1,10 @@ #include "esp_camera.h" #include -// -// WARNING!!! PSRAM IC required for UXGA resolution and high JPEG quality -// Ensure ESP32 Wrover Module or other board with PSRAM is selected -// Partial images will be transmitted if image exceeds buffer size -// -// You must select partition scheme from the board menu that has at least 3MB APP space. -// Face Recognition is DISABLED for ESP32 and ESP32-S2, because it takes up from 15 -// seconds to process single frame. Face Detection is ENABLED if PSRAM is enabled as well - -// =================== -// Select camera model -// =================== -//#define CAMERA_MODEL_WROVER_KIT // Has PSRAM -#define CAMERA_MODEL_ESP_EYE // Has PSRAM -//#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM -//#define CAMERA_MODEL_M5STACK_PSRAM // Has PSRAM -//#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM -//#define CAMERA_MODEL_M5STACK_WIDE // Has PSRAM -//#define CAMERA_MODEL_M5STACK_ESP32CAM // No PSRAM -//#define CAMERA_MODEL_M5STACK_UNITCAM // No PSRAM -//#define CAMERA_MODEL_M5STACK_CAMS3_UNIT // Has PSRAM -//#define CAMERA_MODEL_AI_THINKER // Has PSRAM -//#define CAMERA_MODEL_TTGO_T_JOURNAL // No PSRAM -//#define CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM -// ** Espressif Internal Boards ** -//#define CAMERA_MODEL_ESP32_CAM_BOARD -//#define CAMERA_MODEL_ESP32S2_CAM_BOARD -//#define CAMERA_MODEL_ESP32S3_CAM_LCD -//#define CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3 // Has PSRAM -//#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3 // Has PSRAM -#include "camera_pins.h" +// =========================== +// Select camera model in board_config.h +// =========================== +#include "board_config.h" // =========================== // Enter your WiFi credentials @@ -40,7 +13,7 @@ const char *ssid = "**********"; const char *password = "**********"; void startCameraServer(); -void setupLedFlash(int pin); +void setupLedFlash(); void setup() { Serial.begin(115200); @@ -130,7 +103,7 @@ void setup() { // Setup LED FLash if LED pin is defined in camera_pins.h #if defined(LED_GPIO_NUM) - setupLedFlash(LED_GPIO_NUM); + setupLedFlash(); #endif WiFi.begin(ssid, password); diff --git a/libraries/ESP32/examples/Camera/CameraWebServer/app_httpd.cpp b/libraries/ESP32/examples/Camera/CameraWebServer/app_httpd.cpp index cc924bd5b3b..589fea33b7f 100644 --- a/libraries/ESP32/examples/Camera/CameraWebServer/app_httpd.cpp +++ b/libraries/ESP32/examples/Camera/CameraWebServer/app_httpd.cpp @@ -19,18 +19,14 @@ #include "esp32-hal-ledc.h" #include "sdkconfig.h" #include "camera_index.h" +#include "board_config.h" #if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_ARDUHAL_ESP_LOG) #include "esp32-hal-log.h" #endif -// Enable LED FLASH setting -#define CONFIG_LED_ILLUMINATOR_ENABLED 1 - // LED FLASH setup -#if CONFIG_LED_ILLUMINATOR_ENABLED - -#define LED_LEDC_GPIO 22 //configure LED pin +#if defined(LED_GPIO_NUM) #define CONFIG_LED_MAX_INTENSITY 255 int led_duty = 0; @@ -91,13 +87,13 @@ static int ra_filter_run(ra_filter_t *filter, int value) { } #endif -#if CONFIG_LED_ILLUMINATOR_ENABLED +#if defined(LED_GPIO_NUM) void enable_led(bool en) { // Turn LED On or Off int duty = en ? led_duty : 0; if (en && isStreaming && (led_duty > CONFIG_LED_MAX_INTENSITY)) { duty = CONFIG_LED_MAX_INTENSITY; } - ledcWrite(LED_LEDC_GPIO, duty); + ledcWrite(LED_GPIO_NUM, duty); //ledc_set_duty(CONFIG_LED_LEDC_SPEED_MODE, CONFIG_LED_LEDC_CHANNEL, duty); //ledc_update_duty(CONFIG_LED_LEDC_SPEED_MODE, CONFIG_LED_LEDC_CHANNEL); log_i("Set LED intensity to %d", duty); @@ -162,7 +158,7 @@ static esp_err_t capture_handler(httpd_req_t *req) { int64_t fr_start = esp_timer_get_time(); #endif -#if CONFIG_LED_ILLUMINATOR_ENABLED +#if defined(LED_GPIO_NUM) enable_led(true); vTaskDelay(150 / portTICK_PERIOD_MS); // The LED needs to be turned on ~150ms before the call to esp_camera_fb_get() fb = esp_camera_fb_get(); // or it won't be visible in the frame. A better way to do this is needed. @@ -230,7 +226,7 @@ static esp_err_t stream_handler(httpd_req_t *req) { httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "X-Framerate", "60"); -#if CONFIG_LED_ILLUMINATOR_ENABLED +#if defined(LED_GPIO_NUM) isStreaming = true; enable_led(true); #endif @@ -293,7 +289,7 @@ static esp_err_t stream_handler(httpd_req_t *req) { ); } -#if CONFIG_LED_ILLUMINATOR_ENABLED +#if defined(LED_GPIO_NUM) isStreaming = false; enable_led(false); #endif @@ -393,7 +389,7 @@ static esp_err_t cmd_handler(httpd_req_t *req) { } else if (!strcmp(variable, "ae_level")) { res = s->set_ae_level(s, val); } -#if CONFIG_LED_ILLUMINATOR_ENABLED +#if defined(LED_GPIO_NUM) else if (!strcmp(variable, "led_intensity")) { led_duty = val; if (isStreaming) { @@ -481,7 +477,7 @@ static esp_err_t status_handler(httpd_req_t *req) { p += sprintf(p, "\"vflip\":%u,", s->status.vflip); p += sprintf(p, "\"dcw\":%u,", s->status.dcw); p += sprintf(p, "\"colorbar\":%u", s->status.colorbar); -#if CONFIG_LED_ILLUMINATOR_ENABLED +#if defined(LED_GPIO_NUM) p += sprintf(p, ",\"led_intensity\":%u", led_duty); #else p += sprintf(p, ",\"led_intensity\":%d", -1); @@ -843,10 +839,10 @@ void startCameraServer() { } } -void setupLedFlash(int pin) { -#if CONFIG_LED_ILLUMINATOR_ENABLED - ledcAttach(pin, 5000, 8); +void setupLedFlash() { +#if defined(LED_GPIO_NUM) + ledcAttach(LED_GPIO_NUM, 5000, 8); #else - log_i("LED flash is disabled -> CONFIG_LED_ILLUMINATOR_ENABLED = 0"); + log_i("LED flash is disabled -> LED_GPIO_NUM undefined"); #endif } diff --git a/libraries/ESP32/examples/Camera/CameraWebServer/board_config.h b/libraries/ESP32/examples/Camera/CameraWebServer/board_config.h new file mode 100644 index 00000000000..ca7edbab73e --- /dev/null +++ b/libraries/ESP32/examples/Camera/CameraWebServer/board_config.h @@ -0,0 +1,34 @@ +#ifndef BOARD_CONFIG_H +#define BOARD_CONFIG_H + +// +// WARNING!!! PSRAM IC required for UXGA resolution and high JPEG quality +// Ensure ESP32 Wrover Module or other board with PSRAM is selected +// Partial images will be transmitted if image exceeds buffer size +// +// You must select partition scheme from the board menu that has at least 3MB APP space. + +// =================== +// Select camera model +// =================== +//#define CAMERA_MODEL_WROVER_KIT // Has PSRAM +#define CAMERA_MODEL_ESP_EYE // Has PSRAM +//#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM +//#define CAMERA_MODEL_M5STACK_PSRAM // Has PSRAM +//#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM +//#define CAMERA_MODEL_M5STACK_WIDE // Has PSRAM +//#define CAMERA_MODEL_M5STACK_ESP32CAM // No PSRAM +//#define CAMERA_MODEL_M5STACK_UNITCAM // No PSRAM +//#define CAMERA_MODEL_M5STACK_CAMS3_UNIT // Has PSRAM +//#define CAMERA_MODEL_AI_THINKER // Has PSRAM +//#define CAMERA_MODEL_TTGO_T_JOURNAL // No PSRAM +//#define CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM +// ** Espressif Internal Boards ** +//#define CAMERA_MODEL_ESP32_CAM_BOARD +//#define CAMERA_MODEL_ESP32S2_CAM_BOARD +//#define CAMERA_MODEL_ESP32S3_CAM_LCD +//#define CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3 // Has PSRAM +//#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3 // Has PSRAM +#include "camera_pins.h" + +#endif // BOARD_CONFIG_H From 7f75e445f7e3ff20f44da48a15160b247d93510e Mon Sep 17 00:00:00 2001 From: "Daniel.Cao" <144674500+DanielCao0@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:23:01 +0800 Subject: [PATCH 053/102] feat(board): add support for RAKwireless RAK3112 (#11485) * feat(rak3112): add pins_arduino.h for RAKWireless RAK3112 module * feat(rak3112): update pins_arduino.h to define LED pins and update board.txt * Delete the redundant configuration information in board.txt * Restore the incorrect modifications to board.txt * Delete blank lines * Move the rak configuration information to the end of the boards.txt . * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: Daniel.Cao Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Me No Dev --- boards.txt | 158 ++++++++++++++++++++ variants/rakwireless_rak3112/pins_arduino.h | 50 +++++++ 2 files changed, 208 insertions(+) create mode 100644 variants/rakwireless_rak3112/pins_arduino.h diff --git a/boards.txt b/boards.txt index ceb0dc63d02..fe67071ea17 100644 --- a/boards.txt +++ b/boards.txt @@ -50548,6 +50548,164 @@ cyobot_v2_esp32s3.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api.zczr -lzbo ############################################################## +rakwireless_rak3112.name=RAKwireless RAK3112 + +rakwireless_rak3112.upload.tool=esptool_py +rakwireless_rak3112.upload.tool.default=esptool_py +rakwireless_rak3112.upload.tool.network=esp_ota +rakwireless_rak3112.upload.maximum_size=1310720 +rakwireless_rak3112.upload.maximum_data_size=327680 +rakwireless_rak3112.upload.wait_for_upload_port=false +rakwireless_rak3112.upload.speed=460800 +rakwireless_rak3112.upload.flags= +rakwireless_rak3112.upload.extra_flags= + +rakwireless_rak3112.bootloader.tool=esptool_py +rakwireless_rak3112.bootloader.tool.default=esptool_py + +rakwireless_rak3112.serial.disableDTR=true +rakwireless_rak3112.serial.disableRTS=true + +rakwireless_rak3112.build.tarch=xtensa +rakwireless_rak3112.build.bootloader_addr=0x0 +rakwireless_rak3112.build.mcu=esp32s3 +rakwireless_rak3112.build.core=esp32 +rakwireless_rak3112.build.target=esp32s3 +rakwireless_rak3112.build.variant=rakwireless_rak3112 +rakwireless_rak3112.build.board=RAKWIRELESS_RAK3112 + +rakwireless_rak3112.build.usb_mode=1 +rakwireless_rak3112.build.cdc_on_boot=1 +rakwireless_rak3112.build.msc_on_boot=0 +rakwireless_rak3112.build.dfu_on_boot=0 + +rakwireless_rak3112.build.f_cpu=240000000L +rakwireless_rak3112.build.flash_size=16MB +rakwireless_rak3112.build.flash_freq=80m +rakwireless_rak3112.build.flash_mode=dio +rakwireless_rak3112.build.boot=dio +rakwireless_rak3112.build.partitions=default +rakwireless_rak3112.build.defines= + +rakwireless_rak3112.menu.PSRAM.enabled=Enabled +rakwireless_rak3112.menu.PSRAM.enabled.build.defines=-DBOARD_HAS_PSRAM +rakwireless_rak3112.menu.PSRAM.enabled.build.psram_type=opi +rakwireless_rak3112.menu.PSRAM.disabled=Disabled +rakwireless_rak3112.menu.PSRAM.disabled.build.defines= + +rakwireless_rak3112.menu.LoopCore.1=Core 1 +rakwireless_rak3112.menu.LoopCore.1.build.loop_core=-DARDUINO_RUNNING_CORE=1 +rakwireless_rak3112.menu.LoopCore.0=Core 0 +rakwireless_rak3112.menu.LoopCore.0.build.loop_core=-DARDUINO_RUNNING_CORE=0 + +rakwireless_rak3112.menu.EventsCore.1=Core 1 +rakwireless_rak3112.menu.EventsCore.1.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=1 +rakwireless_rak3112.menu.EventsCore.0=Core 0 +rakwireless_rak3112.menu.EventsCore.0.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=0 + +rakwireless_rak3112.menu.USBMode.hwcdc=Hardware CDC and JTAG +rakwireless_rak3112.menu.USBMode.hwcdc.build.usb_mode=1 +rakwireless_rak3112.menu.USBMode.default=USB-OTG (TinyUSB) +rakwireless_rak3112.menu.USBMode.default.build.usb_mode=0 + +rakwireless_rak3112.menu.CDCOnBoot.default=Enabled +rakwireless_rak3112.menu.CDCOnBoot.default.build.cdc_on_boot=1 +rakwireless_rak3112.menu.CDCOnBoot.cdc=Disabled +rakwireless_rak3112.menu.CDCOnBoot.cdc.build.cdc_on_boot=0 + +rakwireless_rak3112.menu.MSCOnBoot.default=Disabled +rakwireless_rak3112.menu.MSCOnBoot.default.build.msc_on_boot=0 +rakwireless_rak3112.menu.MSCOnBoot.msc=Enabled (Requires USB-OTG Mode) +rakwireless_rak3112.menu.MSCOnBoot.msc.build.msc_on_boot=1 + +rakwireless_rak3112.menu.DFUOnBoot.default=Disabled +rakwireless_rak3112.menu.DFUOnBoot.default.build.dfu_on_boot=0 +rakwireless_rak3112.menu.DFUOnBoot.dfu=Enabled (Requires USB-OTG Mode) +rakwireless_rak3112.menu.DFUOnBoot.dfu.build.dfu_on_boot=1 + +rakwireless_rak3112.menu.UploadMode.default=UART0 / Hardware CDC +rakwireless_rak3112.menu.UploadMode.default.upload.use_1200bps_touch=false +rakwireless_rak3112.menu.UploadMode.default.upload.wait_for_upload_port=false +rakwireless_rak3112.menu.UploadMode.cdc=USB-OTG CDC (TinyUSB) +rakwireless_rak3112.menu.UploadMode.cdc.upload.use_1200bps_touch=true +rakwireless_rak3112.menu.UploadMode.cdc.upload.wait_for_upload_port=true + +rakwireless_rak3112.menu.PartitionScheme.default_16MB=Default (6.25MB APP/3.43MB SPIFFS) +rakwireless_rak3112.menu.PartitionScheme.default_16MB.build.partitions=default_16MB +rakwireless_rak3112.menu.PartitionScheme.default_16MB.upload.maximum_size=6553600 +rakwireless_rak3112.menu.PartitionScheme.app3M_fat9M_16MB=16M Flash (3MB APP/9MB FATFS) +rakwireless_rak3112.menu.PartitionScheme.app3M_fat9M_16MB.build.partitions=app3M_fat9M_16MB +rakwireless_rak3112.menu.PartitionScheme.app3M_fat9M_16MB.upload.maximum_size=3145728 +rakwireless_rak3112.menu.PartitionScheme.tinyuf2=TinyUF2 16MB (2MB APP/11.6MB FATFS) +rakwireless_rak3112.menu.PartitionScheme.tinyuf2.build.custom_bootloader=bootloader-tinyuf2 +rakwireless_rak3112.menu.PartitionScheme.tinyuf2.build.partitions=tinyuf2-partitions-16MB +rakwireless_rak3112.menu.PartitionScheme.tinyuf2.upload.maximum_size=2097152 +rakwireless_rak3112.menu.PartitionScheme.tinyuf2.upload.extra_flags=0x410000 "{runtime.platform.path}/variants/{build.variant}/tinyuf2.bin" +rakwireless_rak3112.menu.PartitionScheme.tinyuf2_noota=TinyUF2 16MB No OTA(4MB APP/11.6MB FATFS) +rakwireless_rak3112.menu.PartitionScheme.tinyuf2_noota.build.custom_bootloader=bootloader-tinyuf2 +rakwireless_rak3112.menu.PartitionScheme.tinyuf2_noota.build.partitions=tinyuf2-partitions-16MB-noota +rakwireless_rak3112.menu.PartitionScheme.tinyuf2_noota.upload.maximum_size=4194304 +rakwireless_rak3112.menu.PartitionScheme.tinyuf2_noota.upload.extra_flags=0x410000 "{runtime.platform.path}/variants/{build.variant}/tinyuf2.bin" +rakwireless_rak3112.menu.PartitionScheme.large_spiffs=Large SPIFFS (4.5MB APP/6.93MB SPIFFS) +rakwireless_rak3112.menu.PartitionScheme.large_spiffs.build.partitions=large_spiffs_16MB +rakwireless_rak3112.menu.PartitionScheme.large_spiffs.upload.maximum_size=4718592 +rakwireless_rak3112.menu.PartitionScheme.custom=Custom +rakwireless_rak3112.menu.PartitionScheme.custom.build.partitions= +rakwireless_rak3112.menu.PartitionScheme.custom.upload.maximum_size=16777216 + +rakwireless_rak3112.menu.CPUFreq.240=240MHz (WiFi/BT) +rakwireless_rak3112.menu.CPUFreq.240.build.f_cpu=240000000L +rakwireless_rak3112.menu.CPUFreq.160=160MHz (WiFi/BT) +rakwireless_rak3112.menu.CPUFreq.160.build.f_cpu=160000000L +rakwireless_rak3112.menu.CPUFreq.80=80MHz (WiFi/BT) +rakwireless_rak3112.menu.CPUFreq.80.build.f_cpu=80000000L + +rakwireless_rak3112.menu.FlashMode.qio=QIO +rakwireless_rak3112.menu.FlashMode.qio.build.flash_mode=dio +rakwireless_rak3112.menu.FlashMode.qio.build.boot=qio +rakwireless_rak3112.menu.FlashMode.dio=DIO +rakwireless_rak3112.menu.FlashMode.dio.build.flash_mode=dio +rakwireless_rak3112.menu.FlashMode.dio.build.boot=dio + +rakwireless_rak3112.menu.FlashFreq.80=80MHz +rakwireless_rak3112.menu.FlashFreq.80.build.flash_freq=80m +rakwireless_rak3112.menu.FlashFreq.40=40MHz +rakwireless_rak3112.menu.FlashFreq.40.build.flash_freq=40m + +rakwireless_rak3112.menu.UploadSpeed.921600=921600 +rakwireless_rak3112.menu.UploadSpeed.921600.upload.speed=921600 +rakwireless_rak3112.menu.UploadSpeed.115200=115200 +rakwireless_rak3112.menu.UploadSpeed.115200.upload.speed=115200 +rakwireless_rak3112.menu.UploadSpeed.256000.windows=256000 +rakwireless_rak3112.menu.UploadSpeed.256000.upload.speed=256000 +rakwireless_rak3112.menu.UploadSpeed.230400.windows.upload.speed=256000 +rakwireless_rak3112.menu.UploadSpeed.230400=230400 +rakwireless_rak3112.menu.UploadSpeed.230400.upload.speed=230400 +rakwireless_rak3112.menu.UploadSpeed.460800.linux=460800 +rakwireless_rak3112.menu.UploadSpeed.460800.macosx=460800 +rakwireless_rak3112.menu.UploadSpeed.460800.upload.speed=460800 +rakwireless_rak3112.menu.UploadSpeed.512000.windows=512000 +rakwireless_rak3112.menu.UploadSpeed.512000.upload.speed=512000 + +rakwireless_rak3112.menu.DebugLevel.none=None +rakwireless_rak3112.menu.DebugLevel.none.build.code_debug=0 +rakwireless_rak3112.menu.DebugLevel.error=Error +rakwireless_rak3112.menu.DebugLevel.error.build.code_debug=1 +rakwireless_rak3112.menu.DebugLevel.warn=Warn +rakwireless_rak3112.menu.DebugLevel.warn.build.code_debug=2 +rakwireless_rak3112.menu.DebugLevel.info=Info +rakwireless_rak3112.menu.DebugLevel.info.build.code_debug=3 +rakwireless_rak3112.menu.DebugLevel.debug=Debug +rakwireless_rak3112.menu.DebugLevel.debug.build.code_debug=4 +rakwireless_rak3112.menu.DebugLevel.verbose=Verbose +rakwireless_rak3112.menu.DebugLevel.verbose.build.code_debug=5 + +rakwireless_rak3112.menu.EraseFlash.none=Disabled +rakwireless_rak3112.menu.EraseFlash.none.upload.erase_cmd= +rakwireless_rak3112.menu.EraseFlash.all=Enabled +rakwireless_rak3112.menu.EraseFlash.all.upload.erase_cmd=-e + +############################################################## kodedot.name=kode dot kodedot.bootloader.tool=esptool_py diff --git a/variants/rakwireless_rak3112/pins_arduino.h b/variants/rakwireless_rak3112/pins_arduino.h new file mode 100644 index 00000000000..5d1e451494a --- /dev/null +++ b/variants/rakwireless_rak3112/pins_arduino.h @@ -0,0 +1,50 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +// Reference: RAK3112 Module Datasheet +// https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/datasheet/ + +// Note:GPIO33,GPIO34,GPIO35.GPIO36,GPIO37 is not available in the 8MB and 16MB Octal PSRAM version + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#define LED_GREEN 46 +#define LED_BLUE 45 + +static const uint8_t LED_BUILTIN = LED_GREEN; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN + +static const uint8_t BAT_VOLT = 21; + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 9; +static const uint8_t SCL = 40; + +#define WIRE1_PIN_DEFINED +static const uint8_t SDA1 = 17; +static const uint8_t SCL1 = 18; + +static const uint8_t SS = 12; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 10; +static const uint8_t SCK = 13; + +#define LORA_ANT_SWITCH 4 // Antenna switch power control pin + +#define LORA_SCK 5 // SX1262 SCK +#define LORA_MISO 3 // SX1262 MISO +#define LORA_MOSI 6 // SX1262 MOSI +#define LORA_CS 7 // SX1262 CS +#define LORA_RST 8 // SX1262 RST + +#define LORA_DIO1 47 //SX1262 DIO1 +#define LORA_BUSY 48 +#define LORA_IRQ LORA_DIO1 + +#endif /* Pins_Arduino_h */ From 6474127dd4359bbf3860a6874c9b2b99b871659e Mon Sep 17 00:00:00 2001 From: John Date: Wed, 2 Jul 2025 08:48:03 -0400 Subject: [PATCH 054/102] Update ZigbeeColorDimmableLight to clamp color hue and saturation to 0-254 (Fixes #11527) (#11528) * Clamp Zigbee color saturation to 0-254 * Clamp hue to 0-254 for Zigbee color lights * Use std::min instead of ternary operator * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp b/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp index caac73b5c68..2fb07fc2187 100644 --- a/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp +++ b/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp @@ -1,3 +1,4 @@ +#include #include "ZigbeeColorDimmableLight.h" #if CONFIG_ZB_ENABLED @@ -127,7 +128,8 @@ bool ZigbeeColorDimmableLight::setLight(bool state, uint8_t level, uint8_t red, espXyColor_t xy_color = espRgbColorToXYColor(_current_color); espHsvColor_t hsv_color = espRgbColorToHsvColor(_current_color); - uint8_t hue = (uint8_t)hsv_color.h; + uint8_t hue = std::min((uint8_t)hsv_color.h, (uint8_t)254); // Clamp to 0-254 + uint8_t saturation = std::min((uint8_t)hsv_color.s, (uint8_t)254); // Clamp to 0-254 log_v("Updating light state: %d, level: %d, color: %d, %d, %d", state, level, red, green, blue); /* Update light clusters */ @@ -174,7 +176,7 @@ bool ZigbeeColorDimmableLight::setLight(bool state, uint8_t level, uint8_t red, } //set saturation ret = esp_zb_zcl_set_attribute_val( - _endpoint, ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_COLOR_CONTROL_CURRENT_SATURATION_ID, &hsv_color.s, false + _endpoint, ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_COLOR_CONTROL_CURRENT_SATURATION_ID, &saturation, false ); if (ret != ESP_ZB_ZCL_STATUS_SUCCESS) { log_e("Failed to set light saturation: 0x%x: %s", ret, esp_zb_zcl_status_to_name(ret)); From 33c84382635223489d23efd60e39926c3b175f02 Mon Sep 17 00:00:00 2001 From: Paul Price <56952812+strid3r21@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:48:40 -0400 Subject: [PATCH 055/102] Add FED4 board (#11536) * Added Fed 4 board * fixed boards.txt * fixed board.txt again * added usb pid address * fixed typo: updated name to upper case * fix(fed4): update PID and change partition scheme to default_16MB * fix(fed4): remove unused OPI flash mode configurations * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- boards.txt | 185 +++++++++++++++++++++++++++++++++++ variants/fed4/pins_arduino.h | 79 +++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 variants/fed4/pins_arduino.h diff --git a/boards.txt b/boards.txt index fe67071ea17..4e92d2665e9 100644 --- a/boards.txt +++ b/boards.txt @@ -50762,3 +50762,188 @@ kodedot.recipe.hooks.objcopy.postobjcopy.3.pattern_args= kodedot.recipe.output.save_file={build.project_name}.ino.bin ############################################################## + +# FED4 Board +fed4.name=FED4 +fed4.vid.0=0x303A +fed4.pid.0=0x82E5 +fed4.upload_port.0.vid=0x303A +fed4.upload_port.0.pid=0x82E5 +fed4.bootloader.tool=esptool_py +fed4.bootloader.tool.default=esptool_py + +fed4.upload.tool=esptool_py +fed4.upload.tool.default=esptool_py +fed4.upload.tool.network=esp_ota + +fed4.upload.maximum_size=1310720 +fed4.upload.maximum_data_size=327680 +fed4.upload.flags= +fed4.upload.extra_flags= +fed4.upload.use_1200bps_touch=false +fed4.upload.wait_for_upload_port=false + +fed4.serial.disableDTR=false +fed4.serial.disableRTS=false + +fed4.build.tarch=xtensa +fed4.build.bootloader_addr=0x0 +fed4.build.target=esp32s3 +fed4.build.mcu=esp32s3 +fed4.build.core=esp32 +fed4.build.variant=fed4 +fed4.build.board=FED4 + +fed4.build.usb_mode=1 +fed4.build.cdc_on_boot=0 +fed4.build.msc_on_boot=0 +fed4.build.dfu_on_boot=0 +fed4.build.f_cpu=240000000L +fed4.build.flash_size=16MB +fed4.build.flash_freq=80m +fed4.build.flash_mode=dio +fed4.build.boot=qio +fed4.build.boot_freq=80m +fed4.build.partitions=default_16MB +fed4.build.defines= +fed4.build.loop_core= +fed4.build.event_core= +fed4.build.psram_type=qspi +fed4.build.memory_type={build.boot}_{build.psram_type} + +## IDE 2.0 Seems to not update the value +fed4.menu.JTAGAdapter.default=Disabled +fed4.menu.JTAGAdapter.default.build.copy_jtag_files=0 +fed4.menu.JTAGAdapter.builtin=Integrated USB JTAG +fed4.menu.JTAGAdapter.builtin.build.openocdscript=fed4-builtin.cfg +fed4.menu.JTAGAdapter.builtin.build.copy_jtag_files=1 +fed4.menu.JTAGAdapter.external=FTDI Adapter +fed4.menu.JTAGAdapter.external.build.openocdscript=fed4-ftdi.cfg +fed4.menu.JTAGAdapter.external.build.copy_jtag_files=1 +fed4.menu.JTAGAdapter.bridge=ESP USB Bridge +fed4.menu.JTAGAdapter.bridge.build.openocdscript=fed4-bridge.cfg +fed4.menu.JTAGAdapter.bridge.build.copy_jtag_files=1 + +fed4.menu.FlashMode.qio=QIO 80MHz +fed4.menu.FlashMode.qio.build.flash_mode=dio +fed4.menu.FlashMode.qio.build.boot=qio +fed4.menu.FlashMode.qio.build.boot_freq=80m +fed4.menu.FlashMode.qio.build.flash_freq=80m +fed4.menu.FlashMode.qio120=QIO 120MHz +fed4.menu.FlashMode.qio120.build.flash_mode=dio +fed4.menu.FlashMode.qio120.build.boot=qio +fed4.menu.FlashMode.qio120.build.boot_freq=120m +fed4.menu.FlashMode.qio120.build.flash_freq=80m +fed4.menu.FlashMode.dio=DIO 80MHz +fed4.menu.FlashMode.dio.build.flash_mode=dio +fed4.menu.FlashMode.dio.build.boot=dio +fed4.menu.FlashMode.dio.build.boot_freq=80m +fed4.menu.FlashMode.dio.build.flash_freq=80m + +fed4.menu.FlashSize.16M=16MB (128Mb) +fed4.menu.FlashSize.16M.build.flash_size=16MB + +fed4.menu.LoopCore.1=Core 1 +fed4.menu.LoopCore.1.build.loop_core=-DARDUINO_RUNNING_CORE=1 +fed4.menu.LoopCore.0=Core 0 +fed4.menu.LoopCore.0.build.loop_core=-DARDUINO_RUNNING_CORE=0 + +fed4.menu.EventsCore.1=Core 1 +fed4.menu.EventsCore.1.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=1 +fed4.menu.EventsCore.0=Core 0 +fed4.menu.EventsCore.0.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=0 + +fed4.menu.USBMode.hwcdc=Hardware CDC and JTAG +fed4.menu.USBMode.hwcdc.build.usb_mode=1 +fed4.menu.USBMode.default=USB-OTG (TinyUSB) +fed4.menu.USBMode.default.build.usb_mode=0 + +fed4.menu.CDCOnBoot.default=Disabled +fed4.menu.CDCOnBoot.default.build.cdc_on_boot=0 +fed4.menu.CDCOnBoot.cdc=Enabled +fed4.menu.CDCOnBoot.cdc.build.cdc_on_boot=1 + +fed4.menu.MSCOnBoot.default=Disabled +fed4.menu.MSCOnBoot.default.build.msc_on_boot=0 +fed4.menu.MSCOnBoot.msc=Enabled (Requires USB-OTG Mode) +fed4.menu.MSCOnBoot.msc.build.msc_on_boot=1 + +fed4.menu.DFUOnBoot.default=Disabled +fed4.menu.DFUOnBoot.default.build.dfu_on_boot=0 +fed4.menu.DFUOnBoot.dfu=Enabled (Requires USB-OTG Mode) +fed4.menu.DFUOnBoot.dfu.build.dfu_on_boot=1 + +fed4.menu.UploadMode.default=UART0 / Hardware CDC +fed4.menu.UploadMode.default.upload.use_1200bps_touch=false +fed4.menu.UploadMode.default.upload.wait_for_upload_port=false +fed4.menu.UploadMode.cdc=USB-OTG CDC (TinyUSB) +fed4.menu.UploadMode.cdc.upload.use_1200bps_touch=true +fed4.menu.UploadMode.cdc.upload.wait_for_upload_port=true + +fed4.menu.PartitionScheme.default_16MB=Default (6.25MB APP/3.43MB SPIFFS) +fed4.menu.PartitionScheme.default_16MB.build.partitions=default_16MB +fed4.menu.PartitionScheme.default_16MB.upload.maximum_size=6553600 +fed4.menu.PartitionScheme.large_spiffs=Large SPIFFS (4.5MB APP/6.93MB SPIFFS) +fed4.menu.PartitionScheme.large_spiffs.build.partitions=large_spiffs_16MB +fed4.menu.PartitionScheme.large_spiffs.upload.maximum_size=4718592 +fed4.menu.PartitionScheme.app3M_fat9M_16MB=FFAT (3MB APP/9MB FATFS) +fed4.menu.PartitionScheme.app3M_fat9M_16MB.build.partitions=app3M_fat9M_16MB +fed4.menu.PartitionScheme.app3M_fat9M_16MB.upload.maximum_size=3145728 +fed4.menu.PartitionScheme.fatflash=Large FFAT (2MB APP/12.5MB FATFS) +fed4.menu.PartitionScheme.fatflash.build.partitions=ffat +fed4.menu.PartitionScheme.fatflash.upload.maximum_size=2097152 + +fed4.menu.CPUFreq.240=240MHz (WiFi) +fed4.menu.CPUFreq.240.build.f_cpu=240000000L +fed4.menu.CPUFreq.160=160MHz (WiFi) +fed4.menu.CPUFreq.160.build.f_cpu=160000000L +fed4.menu.CPUFreq.80=80MHz (WiFi) +fed4.menu.CPUFreq.80.build.f_cpu=80000000L +fed4.menu.CPUFreq.40=40MHz +fed4.menu.CPUFreq.40.build.f_cpu=40000000L +fed4.menu.CPUFreq.20=20MHz +fed4.menu.CPUFreq.20.build.f_cpu=20000000L +fed4.menu.CPUFreq.10=10MHz +fed4.menu.CPUFreq.10.build.f_cpu=10000000L + +fed4.menu.UploadSpeed.921600=921600 +fed4.menu.UploadSpeed.921600.upload.speed=921600 +fed4.menu.UploadSpeed.115200=115200 +fed4.menu.UploadSpeed.115200.upload.speed=115200 +fed4.menu.UploadSpeed.256000.windows=256000 +fed4.menu.UploadSpeed.256000.upload.speed=256000 +fed4.menu.UploadSpeed.230400.windows.upload.speed=256000 +fed4.menu.UploadSpeed.230400=230400 +fed4.menu.UploadSpeed.230400.upload.speed=230400 +fed4.menu.UploadSpeed.460800.linux=460800 +fed4.menu.UploadSpeed.460800.macosx=460800 +fed4.menu.UploadSpeed.460800.upload.speed=460800 +fed4.menu.UploadSpeed.512000.windows=512000 +fed4.menu.UploadSpeed.512000.upload.speed=512000 + +fed4.menu.DebugLevel.none=None +fed4.menu.DebugLevel.none.build.code_debug=0 +fed4.menu.DebugLevel.error=Error +fed4.menu.DebugLevel.error.build.code_debug=1 +fed4.menu.DebugLevel.warn=Warn +fed4.menu.DebugLevel.warn.build.code_debug=2 +fed4.menu.DebugLevel.info=Info +fed4.menu.DebugLevel.info.build.code_debug=3 +fed4.menu.DebugLevel.debug=Debug +fed4.menu.DebugLevel.debug.build.code_debug=4 +fed4.menu.DebugLevel.verbose=Verbose +fed4.menu.DebugLevel.verbose.build.code_debug=5 + +fed4.menu.EraseFlash.none=Disabled +fed4.menu.EraseFlash.none.upload.erase_cmd= +fed4.menu.EraseFlash.all=Enabled +fed4.menu.EraseFlash.all.upload.erase_cmd=-e + +fed4.menu.ZigbeeMode.default=Disabled +fed4.menu.ZigbeeMode.default.build.zigbee_mode= +fed4.menu.ZigbeeMode.default.build.zigbee_libs= +fed4.menu.ZigbeeMode.zczr=Zigbee ZCZR (coordinator/router) +fed4.menu.ZigbeeMode.zczr.build.zigbee_mode=-DZIGBEE_MODE_ZCZR +fed4.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api.zczr -lzboss_stack.zczr -lzboss_port.remote + +############################################################## diff --git a/variants/fed4/pins_arduino.h b/variants/fed4/pins_arduino.h new file mode 100644 index 00000000000..f3741700ffa --- /dev/null +++ b/variants/fed4/pins_arduino.h @@ -0,0 +1,79 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include "soc/soc_caps.h" + +#define USB_VID 0x303A +#define USB_PID 0x82E5 +#define USB_MANUFACTURER "Smart Bee Designs LLC" +#define USB_PRODUCT "FED4" +#define USB_SERIAL "" + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; +static const uint8_t SDA2 = 20; +static const uint8_t SCL2 = 19; + +static const uint8_t SS = 47; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; +static const uint8_t SDCS = 10; // sd cs pin +static const uint8_t DSCS = 14; //display cs pin + +static const uint8_t A1 = 1; +static const uint8_t A2 = 2; +static const uint8_t A3 = 3; +static const uint8_t A4 = 4; +static const uint8_t A5 = 5; +static const uint8_t A6 = 6; + +static const uint8_t D1 = 1; +static const uint8_t D2 = 2; +static const uint8_t D3 = 3; +static const uint8_t D4 = 4; +static const uint8_t D5 = 5; +static const uint8_t D6 = 6; +static const uint8_t D8 = 8; +static const uint8_t D13 = 13; +static const uint8_t D9 = 9; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; + +static const uint8_t BOOT_BTN = 0; +static const uint8_t VBAT_VOLTAGE = 7; +static const uint8_t LDO2 = 47; +static const uint8_t STATUS_RGB = 35; +static const uint8_t RGB_STRIP = 36; +static const uint8_t INTERRUPT_PIN = 18; +static const uint8_t USER_BTN_1 = 14; +static const uint8_t USER_BTN_2 = 39; +static const uint8_t USER_BTN_3 = 40; +static const uint8_t AMP_DIN = 39; +static const uint8_t AMP_SD = 42; +static const uint8_t AMP_BCLK = 45; +static const uint8_t AMP_LRCLK = 48; +static const uint8_t MSBY = 15; +static const uint8_t TRRS_1 = 4; +static const uint8_t TRRS_2 = 2; +static const uint8_t TRRS_3 = 3; + +#define PIN_RGB_LED STATUS_RGB +// BUILTIN_LED can be used in new Arduino API digitalWrite() like in Blink.ino +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + PIN_RGB_LED; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN +// RGB_BUILTIN and RGB_BRIGHTNESS can be used in new Arduino API rgbLedWrite() +#define RGB_BUILTIN LED_BUILTIN +#define RGB_BRIGHTNESS 64 + +#endif /* Pins_Arduino_h */ From 12e881b6d6cd91bfbe0f58feac10bb58ceeb34c2 Mon Sep 17 00:00:00 2001 From: Paula Scharf <48286621+PaulaScharf@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:22:20 +0200 Subject: [PATCH 056/102] fix(board): Update variant.cpp for senseBox MCU-S2 ESP32-S2 (#11532) * fix(board): Update variant.cpp for senseBox MCU-S2 ESP32-S2 * fix(board): translate comments * ci(pre-commit): Apply automatic fixes * fix(board): translate comments * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- boards.txt | 2 +- variants/sensebox_mcu_esp32s2/APOTA.bin | Bin 0 -> 783824 bytes variants/sensebox_mcu_esp32s2/APOTA.ino | 283 ++++++++++++++++++++++ variants/sensebox_mcu_esp32s2/variant.cpp | 74 +++--- 4 files changed, 331 insertions(+), 28 deletions(-) create mode 100644 variants/sensebox_mcu_esp32s2/APOTA.bin create mode 100644 variants/sensebox_mcu_esp32s2/APOTA.ino diff --git a/boards.txt b/boards.txt index 4e92d2665e9..4198f3856b0 100644 --- a/boards.txt +++ b/boards.txt @@ -40993,7 +40993,7 @@ sensebox_mcu_esp32s2.menu.PartitionScheme.tinyuf2=TinyUF2 4MB (1.3MB APP/960KB F sensebox_mcu_esp32s2.menu.PartitionScheme.tinyuf2.build.custom_bootloader=bootloader-tinyuf2 sensebox_mcu_esp32s2.menu.PartitionScheme.tinyuf2.build.custom_partitions=partitions-4MB-tinyuf2 sensebox_mcu_esp32s2.menu.PartitionScheme.tinyuf2.upload.maximum_size=1441792 -sensebox_mcu_esp32s2.menu.PartitionScheme.tinyuf2.upload.extra_flags=0x2d0000 "{runtime.platform.path}/variants/{build.variant}/tinyuf2.bin" +sensebox_mcu_esp32s2.menu.PartitionScheme.tinyuf2.upload.extra_flags=0x2d0000 "{runtime.platform.path}/variants/{build.variant}/tinyuf2.bin" 0x170000 "{runtime.platform.path}/variants/{build.variant}/APOTA.bin" sensebox_mcu_esp32s2.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS) sensebox_mcu_esp32s2.menu.PartitionScheme.default.build.partitions=default sensebox_mcu_esp32s2.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS) diff --git a/variants/sensebox_mcu_esp32s2/APOTA.bin b/variants/sensebox_mcu_esp32s2/APOTA.bin new file mode 100644 index 0000000000000000000000000000000000000000..0ea39335dce0e13a3c900367b76eacc9b413922a GIT binary patch literal 783824 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’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 KyFd;c3iu+q7se)RIqiEi%qr+Nis-^Sv9m4z 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

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy

*ptM@WNYc*CowO^YW?Cx0Nz9(wGHZ8tH; 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 p-}>{(!?V9VeR=xcqZxe( +#include +#include +#include +#include +#include +#ifdef DISPLAY_ENABLED +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 +#include +#include +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); +#include +Adafruit_NeoPixel rgb_led_1 = Adafruit_NeoPixel(1, 1, NEO_GRB + NEO_KHZ800); + +#endif +#include "esp_partition.h" +#include "esp_ota_ops.h" +#include "esp_system.h" + +String ssid; +uint8_t mac[6]; + +// Create an instance of the server +WebServer server(80); +bool displayEnabled; + +const int BUTTON_PIN = 0; // GPIO for the button +volatile unsigned long lastPressTime = 0; // Time of last button press +volatile bool doublePressDetected = false; // Flag for double press +const unsigned long doublePressInterval = 500; // Max. time (in ms) between two presses for double press +volatile int pressCount = 0; // Counts the button presses + +const unsigned char epd_bitmap_wifi[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0xff, 0x00, 0x00, + 0x00, 0x3f, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x7c, 0x00, 0x03, 0xe0, 0x00, 0x00, 0xf0, 0x00, 0x01, 0xf0, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x78, 0x00, + 0x03, 0xc0, 0x00, 0x00, 0x38, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1c, 0x00, 0x0f, 0x00, 0x06, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x7f, 0xe0, 0x0e, 0x00, + 0x0c, 0x01, 0xff, 0xf0, 0x06, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x02, 0x00, 0x00, 0x0f, 0x80, 0x3e, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x0f, 0x00, 0x00, + 0x00, 0x1c, 0x00, 0x07, 0x80, 0x00, 0x00, 0x38, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x70, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x70, 0x00, 0x00, 0xc0, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x01, 0xe0, 0xf0, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x78, 0x00, 0x00, 0x00, 0x03, 0x80, 0x38, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// 'checkmark', 44x44px +const unsigned char epd_bitmap_checkmark[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x0e, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0x0f, 0x01, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0x83, 0xc0, 0x00, 0x00, 0x00, 0x07, 0xc7, 0x80, 0x00, 0x00, 0x00, 0x03, 0xef, 0x00, 0x00, 0x00, + 0x00, 0x01, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +void IRAM_ATTR handleButtonPress() { + unsigned long currentTime = millis(); // Get current time + + // Debounce: If the current press is too close to the last one, ignore it + if (currentTime - lastPressTime > 50) { + pressCount++; // Count the button press + + // Check if this is the second press within the double-press interval + if (pressCount == 2 && (currentTime - lastPressTime <= doublePressInterval)) { + doublePressDetected = true; // Double press detected + pressCount = 0; // Reset counter + } + + lastPressTime = currentTime; // Update the time of the last press + } +} + +// Function to switch the boot partition to OTA1 +void setBootPartitionToOTA0() { + const esp_partition_t *ota0_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL); + + if (ota0_partition) { + // Set OTA1 as new boot partition + esp_ota_set_boot_partition(ota0_partition); + Serial.println("Boot partition changed to OTA0. Restarting..."); + + // Restart to boot from the new partition + esp_restart(); + } else { + Serial.println("OTA1 partition not found!"); + } +} + +void setupDisplay() { + displayEnabled = display.begin(SSD1306_SWITCHCAPVCC, 0x3D); + if (displayEnabled) { + display.display(); + delay(100); + display.clearDisplay(); + } +} + +void displayStatusBar(int progress) { + display.clearDisplay(); + display.setCursor(24, 8); + display.println("Sketch wird"); + display.setCursor(22, 22); + display.println("hochgeladen!"); + + display.fillRect(0, SCREEN_HEIGHT - 24, SCREEN_WIDTH - 4, 8, BLACK); // Clear status bar area + display.drawRect(0, SCREEN_HEIGHT - 24, SCREEN_WIDTH - 4, 8, WHITE); // Draw border + int filledWidth = (progress * SCREEN_WIDTH - 4) / 100; // Calculate progress width + display.fillRect(1, SCREEN_HEIGHT - 23, filledWidth - 4, 6, WHITE); // Fill progress bar + + display.setCursor((SCREEN_WIDTH / 2) - 12, SCREEN_HEIGHT - 10); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.print(progress); + display.println(" %"); + display.display(); +} + +void displayWelcomeScreen() { + display.clearDisplay(); + + // Draw WiFi symbol + display.drawBitmap(0, 12, epd_bitmap_wifi, 44, 44, WHITE); + + // Display SSID text + display.setCursor(40, 13); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.println("Verbinde dich"); // "Connect" + display.setCursor(60, 27); + display.println("mit:"); // "with" + + // Display SSID + display.setCursor(40, 43); + display.setTextSize(1); // Larger text for SSID + display.print(ssid); + + display.display(); +} + +void displaySuccessScreen() { + display.clearDisplay(); + + // Draw WiFi symbol + display.drawBitmap(0, 12, epd_bitmap_checkmark, 44, 44, WHITE); + + // Display SSID text + display.setCursor(48, 22); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.println("Erfolgreich"); // "Successfully" + display.setCursor(48, 36); + display.println("hochgeladen!"); // "uploaded!" + + display.display(); +} + +void wipeDisplay() { + display.clearDisplay(); + display.println(""); + display.display(); +} + +void setupWiFi() { + WiFi.macAddress(mac); + char macLastFour[5]; + snprintf(macLastFour, sizeof(macLastFour), "%02X%02X", mac[4], mac[5]); + ssid = "senseBox:" + String(macLastFour); + + // Define the IP address, gateway, and subnet mask + IPAddress local_IP(192, 168, 1, 1); // The new IP address + IPAddress gateway(192, 168, 1, 1); // Gateway address (can be the same as the AP's IP) + IPAddress subnet(255, 255, 255, 0); // Subnet mask + + // Set the IP address, gateway, and subnet mask of the access point + WiFi.softAPConfig(local_IP, gateway, subnet); + + // Start the access point + WiFi.softAP(ssid.c_str()); +} + +void setupOTA() { + // Handle updating process + server.on( + "/sketch", HTTP_POST, + []() { + server.sendHeader("Connection", "close"); + server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + ESP.restart(); + }, + []() { + HTTPUpload &upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + Serial.setDebugOutput(true); + size_t fsize = UPDATE_SIZE_UNKNOWN; + if (server.clientContentLength() > 0) { + fsize = server.clientContentLength(); + } + Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize); + + Serial.printf("Update: %s\n", upload.filename.c_str()); + if (!Update.begin(fsize)) { //start with max available size + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + /* flashing firmware to ESP*/ + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(Serial); + } else { + int progress = (Update.progress() * 100) / Update.size(); + displayStatusBar(progress); // Update progress on status bar + } + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) { //true to set the size to the current progress + displaySuccessScreen(); + delay(3000); + wipeDisplay(); + } else { + Update.printError(Serial); + } + Serial.setDebugOutput(false); + } + yield(); + } + ); +} + +void setup() { + // Start Serial communication + Serial.begin(115200); + rgb_led_1.begin(); + rgb_led_1.setBrightness(30); + rgb_led_1.setPixelColor(0, rgb_led_1.Color(51, 51, 255)); + rgb_led_1.show(); + + // Configure button pin as input + pinMode(BUTTON_PIN, INPUT_PULLUP); + + // Interrupt for the button + attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handleButtonPress, FALLING); + +#ifdef DISPLAY_ENABLED + setupDisplay(); +#endif + setupWiFi(); + // Set the ESP32 as an access point + setupOTA(); + server.begin(); +} + +void loop() { + // Handle client requests + server.handleClient(); + +#ifdef DISPLAY_ENABLED + displayWelcomeScreen(); +#endif + + if (doublePressDetected) { + Serial.println("Doppeldruck erkannt!"); // "Double press detected!" + setBootPartitionToOTA0(); +#ifdef DISPLAY_ENABLED + display.setCursor(0, 0); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.println(""); + display.display(); + delay(50); +#endif + // Restart to boot from the new partition + esp_restart(); + } +} diff --git a/variants/sensebox_mcu_esp32s2/variant.cpp b/variants/sensebox_mcu_esp32s2/variant.cpp index 0c58ef2cbe2..aa1eb3dc7c5 100644 --- a/variants/sensebox_mcu_esp32s2/variant.cpp +++ b/variants/sensebox_mcu_esp32s2/variant.cpp @@ -1,29 +1,10 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2021 Ha Thach (tinyusb.org) for Adafruit Industries - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - #include "esp32-hal-gpio.h" #include "pins_arduino.h" +#include "esp_partition.h" +#include "esp_system.h" +#include "esp_ota_ops.h" +#include "esp_log.h" +#include extern "C" { @@ -41,12 +22,51 @@ void initVariant(void) { pinMode(PIN_XB1_ENABLE, OUTPUT); digitalWrite(PIN_XB1_ENABLE, LOW); - //enable UART by default - pinMode(PIN_UART_ENABLE, OUTPUT); - digitalWrite(PIN_UART_ENABLE, LOW); + //enable UART only for chip without PSRAM + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + if (chip_info.revision <= 0) { + pinMode(PIN_UART_ENABLE, OUTPUT); + digitalWrite(PIN_UART_ENABLE, LOW); + } //enable PD-Sensor by default pinMode(PD_ENABLE, OUTPUT); digitalWrite(PD_ENABLE, HIGH); + + // define button pin + const int PIN_BUTTON = 0; + pinMode(PIN_BUTTON, INPUT_PULLUP); + + // keep button pressed + unsigned long pressStartTime = 0; + bool buttonPressed = false; + + // Wait 5 seconds for the button to be pressed + unsigned long startTime = millis(); + + // Check if button is pressed + while (millis() - startTime < 5000) { + if (digitalRead(PIN_BUTTON) == LOW) { + if (!buttonPressed) { + // The button was pressed + buttonPressed = true; + } + } else if (buttonPressed) { + // When the button is pressed and then released, boot into the OTA1 partition + const esp_partition_t *ota1_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); + + if (ota1_partition) { + esp_err_t err = esp_ota_set_boot_partition(ota1_partition); + if (err == ESP_OK) { + esp_restart(); // restart, to boot OTA1 partition + } else { + ESP_LOGE("OTA", "Error setting OTA1 partition: %s", esp_err_to_name(err)); + } + } + // Abort after releasing the button + break; + } + } } } From d4e5c5f969aba994e3e1c1994348bffb9856e95a Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Thu, 3 Jul 2025 12:25:33 +0300 Subject: [PATCH 057/102] IDF release/v5.5 adb3f2a5 (#11543) IDF release/v5.5 adb3f2a5 --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 1d753792b43..a02be509094 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-cbe9388f-v1" + "version": "idf-release_v5.5-adb3f2a5-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-cbe9388f-v1", + "version": "idf-release_v5.5-adb3f2a5-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cbe9388f-v1.zip", - "checksum": "SHA-256:b737ffb86a1b377db12dd610d06936ca8d85d877c872f532a68f6f0a3f666a3f", - "size": "421300036" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", + "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", + "size": "430508851" } ] }, From c2d23258f197d0f3422c31e75ab848919fc097bb Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Thu, 3 Jul 2025 06:25:51 -0300 Subject: [PATCH 058/102] feat(matter): enables BLE Matter commissioning with NimBLE (#11537) * feat(matter): enables BLE Matter commissioning with NimBLE * fix(matter): commentary typo and formatting * fix(matter): commentary typo and formatting * fix(matter): removes forcing second network clustter * fix(matter): adds matter source code to CMakeLists.txt * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CMakeLists.txt | 3 +- .../MatterColorLight/MatterColorLight.ino | 15 +- .../MatterCommissionTest.ino | 13 +- .../MatterComposedLights.ino | 13 +- .../MatterContactSensor.ino | 13 +- .../MatterDimmableLight.ino | 15 +- .../MatterEnhancedColorLight.ino | 15 +- .../examples/MatterEvents/MatterEvents.ino | 9 ++ .../Matter/examples/MatterFan/MatterFan.ino | 15 +- .../MatterHumiditySensor.ino | 13 +- .../examples/MatterMinimum/MatterMinimum.ino | 11 +- .../MatterOccupancySensor.ino | 13 +- .../MatterOnIdentify/MatterOnIdentify.ino | 13 +- .../MatterOnOffLight/MatterOnOffLight.ino | 15 +- .../MatterOnOffPlugin/MatterOnOffPlugin.ino | 15 +- .../MatterPressureSensor.ino | 13 +- .../MatterSmartButon/MatterSmartButon.ino | 15 +- .../MatterTemperatureLight.ino | 15 +- .../MatterTemperatureSensor.ino | 13 +- .../MatterThermostat/MatterThermostat.ino | 13 +- libraries/Matter/src/Matter.cpp | 18 ++- libraries/Matter/src/Matter.h | 2 +- libraries/Matter/src/MatterEndPoint.cpp | 139 ++++++++++++++++++ libraries/Matter/src/MatterEndPoint.h | 117 +++++---------- .../src/MatterEndpoints/MatterColorLight.cpp | 2 +- .../src/MatterEndpoints/MatterColorLight.h | 2 +- .../MatterColorTemperatureLight.cpp | 2 +- .../MatterColorTemperatureLight.h | 2 +- .../MatterEndpoints/MatterContactSensor.cpp | 3 +- .../src/MatterEndpoints/MatterContactSensor.h | 2 +- .../MatterEndpoints/MatterDimmableLight.cpp | 2 +- .../src/MatterEndpoints/MatterDimmableLight.h | 2 +- .../MatterEnhancedColorLight.cpp | 2 +- .../MatterEnhancedColorLight.h | 2 +- .../Matter/src/MatterEndpoints/MatterFan.cpp | 3 +- .../Matter/src/MatterEndpoints/MatterFan.h | 2 +- .../MatterEndpoints/MatterGenericSwitch.cpp | 3 +- .../src/MatterEndpoints/MatterGenericSwitch.h | 2 +- .../MatterEndpoints/MatterHumiditySensor.cpp | 3 +- .../MatterEndpoints/MatterHumiditySensor.h | 2 +- .../MatterEndpoints/MatterOccupancySensor.cpp | 3 +- .../MatterEndpoints/MatterOccupancySensor.h | 2 +- .../src/MatterEndpoints/MatterOnOffLight.cpp | 3 +- .../src/MatterEndpoints/MatterOnOffLight.h | 2 +- .../src/MatterEndpoints/MatterOnOffPlugin.cpp | 3 +- .../src/MatterEndpoints/MatterOnOffPlugin.h | 2 +- .../MatterEndpoints/MatterPressureSensor.cpp | 3 +- .../MatterEndpoints/MatterPressureSensor.h | 2 +- .../MatterTemperatureSensor.cpp | 3 +- .../MatterEndpoints/MatterTemperatureSensor.h | 2 +- .../src/MatterEndpoints/MatterThermostat.cpp | 3 +- .../src/MatterEndpoints/MatterThermostat.h | 2 +- 52 files changed, 447 insertions(+), 155 deletions(-) create mode 100644 libraries/Matter/src/MatterEndPoint.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 20cfc2b0955..e71911c1e38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,7 +184,8 @@ set(ARDUINO_LIBRARY_Matter_SRCS libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp - libraries/Matter/src/Matter.cpp) + libraries/Matter/src/Matter.cpp + libraries/Matter/src/MatterEndPoint.cpp) set(ARDUINO_LIBRARY_PPP_SRCS libraries/PPP/src/PPP.cpp diff --git a/libraries/Matter/examples/MatterColorLight/MatterColorLight.ino b/libraries/Matter/examples/MatterColorLight/MatterColorLight.ino index f3e45887576..e28c96e266c 100644 --- a/libraries/Matter/examples/MatterColorLight/MatterColorLight.ino +++ b/libraries/Matter/examples/MatterColorLight/MatterColorLight.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,22 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif #include // List of Matter Endpoints for this Node // Color Light Endpoint MatterColorLight ColorLight; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // it will keep last OnOff & HSV Color state stored, using Preferences Preferences matterPref; @@ -81,6 +87,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -95,6 +103,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize Matter EndPoint matterPref.begin("MatterPrefs", false); @@ -121,7 +130,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); Serial.printf( "Initial state: %s | RGB Color: (%d,%d,%d) \r\n", ColorLight ? "ON" : "OFF", ColorLight.getColorRGB().r, ColorLight.getColorRGB().g, ColorLight.getColorRGB().b @@ -154,7 +163,7 @@ void loop() { ); // configure the Light based on initial on-off state and its color ColorLight.updateAccessory(); - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A button is also used to control the light diff --git a/libraries/Matter/examples/MatterCommissionTest/MatterCommissionTest.ino b/libraries/Matter/examples/MatterCommissionTest/MatterCommissionTest.ino index 0e93ed6d155..aa593758548 100644 --- a/libraries/Matter/examples/MatterCommissionTest/MatterCommissionTest.ino +++ b/libraries/Matter/examples/MatterCommissionTest/MatterCommissionTest.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,19 +14,27 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // On/Off Light Endpoint MatterOnOffLight OnOffLight; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -41,6 +49,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize at least one Matter EndPoint OnOffLight.begin(); @@ -64,7 +73,7 @@ void loop() { Serial.println("Matter Fabric not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi."); + Serial.println("Matter Node is commissioned and connected to the network."); Serial.println("====> Decommissioning in 30 seconds. <===="); delay(30000); Matter.decommission(); diff --git a/libraries/Matter/examples/MatterComposedLights/MatterComposedLights.ino b/libraries/Matter/examples/MatterComposedLights/MatterComposedLights.ino index b98cc8e19c9..48a6db8bedd 100644 --- a/libraries/Matter/examples/MatterComposedLights/MatterComposedLights.ino +++ b/libraries/Matter/examples/MatterComposedLights/MatterComposedLights.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,10 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // There will be 3 On/Off Light Endpoints in the same Node @@ -22,9 +25,12 @@ MatterOnOffLight Light1; MatterDimmableLight Light2; MatterColorLight Light3; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - USED to decommission the Matter Node const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -56,6 +62,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -71,6 +79,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize all 3 Matter EndPoints Light1.begin(); @@ -103,7 +112,7 @@ void loop() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } //displays the Light state every 5 seconds diff --git a/libraries/Matter/examples/MatterContactSensor/MatterContactSensor.ino b/libraries/Matter/examples/MatterContactSensor/MatterContactSensor.ino index e4c41460d3a..6472012a2ff 100644 --- a/libraries/Matter/examples/MatterContactSensor/MatterContactSensor.ino +++ b/libraries/Matter/examples/MatterContactSensor/MatterContactSensor.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -30,15 +30,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Matter Contact Sensor Endpoint MatterContactSensor ContactSensor; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // LED will be used to indicate the Contact Sensor state // set your board RGB LED pin here @@ -67,6 +73,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -75,6 +83,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // set initial contact sensor state as false (default) ContactSensor.begin(); @@ -99,7 +108,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } diff --git a/libraries/Matter/examples/MatterDimmableLight/MatterDimmableLight.ino b/libraries/Matter/examples/MatterDimmableLight/MatterDimmableLight.ino index 79751905c20..2b853b25677 100644 --- a/libraries/Matter/examples/MatterDimmableLight/MatterDimmableLight.ino +++ b/libraries/Matter/examples/MatterDimmableLight/MatterDimmableLight.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,22 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif #include // List of Matter Endpoints for this Node // Dimmable Light Endpoint MatterDimmableLight DimmableLight; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // it will keep last OnOff & Brightness state stored, using Preferences Preferences matterPref; @@ -77,6 +83,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -91,6 +99,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize Matter EndPoint matterPref.begin("MatterPrefs", false); @@ -116,7 +125,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); Serial.printf("Initial state: %s | brightness: %d\r\n", DimmableLight ? "ON" : "OFF", DimmableLight.getBrightness()); // configure the Light based on initial on-off state and brightness DimmableLight.updateAccessory(); @@ -143,7 +152,7 @@ void loop() { Serial.printf("Initial state: %s | brightness: %d\r\n", DimmableLight ? "ON" : "OFF", DimmableLight.getBrightness()); // configure the Light based on initial on-off state and brightness DimmableLight.updateAccessory(); - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A button is also used to control the light diff --git a/libraries/Matter/examples/MatterEnhancedColorLight/MatterEnhancedColorLight.ino b/libraries/Matter/examples/MatterEnhancedColorLight/MatterEnhancedColorLight.ino index 8e12581fdf2..ac22ae768c5 100644 --- a/libraries/Matter/examples/MatterEnhancedColorLight/MatterEnhancedColorLight.ino +++ b/libraries/Matter/examples/MatterEnhancedColorLight/MatterEnhancedColorLight.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,22 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif #include // List of Matter Endpoints for this Node // Color Light Endpoint MatterEnhancedColorLight EnhancedColorLight; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // It will use HSV color to control all Matter Attribute Changes HsvColor_t currentHSVColor = {0, 0, 0}; @@ -85,6 +91,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -99,6 +107,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize Matter EndPoint matterPref.begin("MatterPrefs", false); @@ -143,7 +152,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); Serial.printf( "Initial state: %s | RGB Color: (%d,%d,%d) \r\n", EnhancedColorLight ? "ON" : "OFF", EnhancedColorLight.getColorRGB().r, EnhancedColorLight.getColorRGB().g, EnhancedColorLight.getColorRGB().b @@ -176,7 +185,7 @@ void loop() { ); // configure the Light based on initial on-off state and its color EnhancedColorLight.updateAccessory(); - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A button is also used to control the light diff --git a/libraries/Matter/examples/MatterEvents/MatterEvents.ino b/libraries/Matter/examples/MatterEvents/MatterEvents.ino index dac599bf9fa..f33b13cf7fb 100644 --- a/libraries/Matter/examples/MatterEvents/MatterEvents.ino +++ b/libraries/Matter/examples/MatterEvents/MatterEvents.ino @@ -14,11 +14,17 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // List of Matter Endpoints for this Node // On/Off Light Endpoint @@ -119,6 +125,8 @@ void setup() { delay(10); // Wait for Serial to initialize } +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -134,6 +142,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize at least one Matter EndPoint OnOffLight.begin(); diff --git a/libraries/Matter/examples/MatterFan/MatterFan.ino b/libraries/Matter/examples/MatterFan/MatterFan.ino index 705aa4853da..ac620167a40 100644 --- a/libraries/Matter/examples/MatterFan/MatterFan.ino +++ b/libraries/Matter/examples/MatterFan/MatterFan.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,15 +14,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Fan Endpoint - On/Off control + Speed Percent Control + Fan Modes MatterFan Fan; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - used for toggling On/Off and decommission the Matter Node const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -76,6 +82,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -90,6 +98,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // On Boot or Reset, Fan is set at 0% speed, OFF, changing between OFF, ON, SMART and HIGH Fan.begin(0, MatterFan::FAN_MODE_OFF, MatterFan::FAN_MODE_SEQ_OFF_HIGH); @@ -141,7 +150,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } @@ -162,7 +171,7 @@ void loop() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A builtin button is used to trigger and send a command to the Matter Controller diff --git a/libraries/Matter/examples/MatterHumiditySensor/MatterHumiditySensor.ino b/libraries/Matter/examples/MatterHumiditySensor/MatterHumiditySensor.ino index 1c7889db849..3149cf1dfbe 100644 --- a/libraries/Matter/examples/MatterHumiditySensor/MatterHumiditySensor.ino +++ b/libraries/Matter/examples/MatterHumiditySensor/MatterHumiditySensor.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,15 +21,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Matter Humidity Sensor Endpoint MatterHumiditySensor SimulatedHumiditySensor; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - decommissioning button const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -60,6 +66,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -68,6 +76,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // set initial humidity sensor measurement // Simulated Sensor - it shall initially print 95% and then move to the 10% to 30% humidity range @@ -92,7 +101,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } diff --git a/libraries/Matter/examples/MatterMinimum/MatterMinimum.ino b/libraries/Matter/examples/MatterMinimum/MatterMinimum.ino index db591ee2226..31599ad10cb 100644 --- a/libraries/Matter/examples/MatterMinimum/MatterMinimum.ino +++ b/libraries/Matter/examples/MatterMinimum/MatterMinimum.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,15 +22,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Single On/Off Light Endpoint - at least one per node MatterOnOffLight OnOffLight; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // Light GPIO that can be controlled by Matter APP #ifdef LED_BUILTIN @@ -62,6 +68,8 @@ void setup() { // Initialize the LED GPIO pinMode(ledPin, OUTPUT); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -73,6 +81,7 @@ void setup() { delay(500); } Serial.println(); +#endif // Initialize at least one Matter EndPoint OnOffLight.begin(); diff --git a/libraries/Matter/examples/MatterOccupancySensor/MatterOccupancySensor.ino b/libraries/Matter/examples/MatterOccupancySensor/MatterOccupancySensor.ino index 333f178e9de..ecab016b473 100644 --- a/libraries/Matter/examples/MatterOccupancySensor/MatterOccupancySensor.ino +++ b/libraries/Matter/examples/MatterOccupancySensor/MatterOccupancySensor.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,15 +28,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Matter Occupancy Sensor Endpoint MatterOccupancySensor OccupancySensor; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - decommissioning only const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -52,6 +58,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -60,6 +68,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // set initial occupancy sensor state as false and connected to a PIR sensor type (default) OccupancySensor.begin(); @@ -83,7 +92,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } diff --git a/libraries/Matter/examples/MatterOnIdentify/MatterOnIdentify.ino b/libraries/Matter/examples/MatterOnIdentify/MatterOnIdentify.ino index b2e77900e95..ec7129ecad9 100644 --- a/libraries/Matter/examples/MatterOnIdentify/MatterOnIdentify.ino +++ b/libraries/Matter/examples/MatterOnIdentify/MatterOnIdentify.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,15 +26,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Single On/Off Light Endpoint - at least one per node MatterOnOffLight OnOffLight; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // Light GPIO that can be controlled by Matter APP #ifdef LED_BUILTIN @@ -88,6 +94,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -96,6 +104,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // Initialize at least one Matter EndPoint OnOffLight.begin(); @@ -125,7 +134,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } diff --git a/libraries/Matter/examples/MatterOnOffLight/MatterOnOffLight.ino b/libraries/Matter/examples/MatterOnOffLight/MatterOnOffLight.ino index 5faa0a385b0..3310cb8c4e9 100644 --- a/libraries/Matter/examples/MatterOnOffLight/MatterOnOffLight.ino +++ b/libraries/Matter/examples/MatterOnOffLight/MatterOnOffLight.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,12 +14,18 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif #include +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // List of Matter Endpoints for this Node // On/Off Light Endpoint @@ -68,6 +74,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -82,6 +90,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize Matter EndPoint matterPref.begin("MatterPrefs", false); @@ -93,7 +102,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); Serial.printf("Initial state: %s\r\n", OnOffLight.getOnOff() ? "ON" : "OFF"); OnOffLight.updateAccessory(); // configure the Light based on initial state } @@ -118,7 +127,7 @@ void loop() { } Serial.printf("Initial state: %s\r\n", OnOffLight.getOnOff() ? "ON" : "OFF"); OnOffLight.updateAccessory(); // configure the Light based on initial state - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A button is also used to control the light diff --git a/libraries/Matter/examples/MatterOnOffPlugin/MatterOnOffPlugin.ino b/libraries/Matter/examples/MatterOnOffPlugin/MatterOnOffPlugin.ino index d14e2189ec1..372874ddc9a 100644 --- a/libraries/Matter/examples/MatterOnOffPlugin/MatterOnOffPlugin.ino +++ b/libraries/Matter/examples/MatterOnOffPlugin/MatterOnOffPlugin.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,22 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif #include // List of Matter Endpoints for this Node // On/Off Plugin Endpoint MatterOnOffPlugin OnOffPlugin; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // it will keep last OnOff state stored, using Preferences Preferences matterPref; @@ -67,6 +73,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -80,6 +88,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize Matter EndPoint matterPref.begin("MatterPrefs", false); @@ -91,7 +100,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); Serial.printf("Initial state: %s\r\n", OnOffPlugin.getOnOff() ? "ON" : "OFF"); OnOffPlugin.updateAccessory(); // configure the Plugin based on initial state } @@ -116,7 +125,7 @@ void loop() { } Serial.printf("Initial state: %s\r\n", OnOffPlugin.getOnOff() ? "ON" : "OFF"); OnOffPlugin.updateAccessory(); // configure the Plugin based on initial state - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // Check if the button has been pressed diff --git a/libraries/Matter/examples/MatterPressureSensor/MatterPressureSensor.ino b/libraries/Matter/examples/MatterPressureSensor/MatterPressureSensor.ino index db035e951c9..0a097ec979b 100644 --- a/libraries/Matter/examples/MatterPressureSensor/MatterPressureSensor.ino +++ b/libraries/Matter/examples/MatterPressureSensor/MatterPressureSensor.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,15 +21,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Matter Pressure Sensor Endpoint MatterPressureSensor SimulatedPressureSensor; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - decommissioning button const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -60,6 +66,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -68,6 +76,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // set initial pressure sensor measurement // Simulated Sensor - it shall initially print 900hPa and then move to the 950 to 1100 hPa as pressure range @@ -92,7 +101,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } diff --git a/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino b/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino index f8da970595d..29caf00004c 100644 --- a/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino +++ b/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,15 +14,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Generic Switch Endpoint - works as a smart button with a single click MatterGenericSwitch SmartButton; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -43,6 +49,8 @@ void setup() { Serial.print("Connecting to "); Serial.println(ssid); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -54,6 +62,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize the Matter EndPoint SmartButton.begin(); @@ -62,7 +71,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } @@ -83,7 +92,7 @@ void loop() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A builtin button is used to trigger a command to the Matter Controller diff --git a/libraries/Matter/examples/MatterTemperatureLight/MatterTemperatureLight.ino b/libraries/Matter/examples/MatterTemperatureLight/MatterTemperatureLight.ino index d46427591ab..b7fcc11d873 100644 --- a/libraries/Matter/examples/MatterTemperatureLight/MatterTemperatureLight.ino +++ b/libraries/Matter/examples/MatterTemperatureLight/MatterTemperatureLight.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,22 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif #include // List of Matter Endpoints for this Node // Color Temperature CW/WW Light Endpoint MatterColorTemperatureLight CW_WW_Light; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // it will keep last OnOff & Brightness state stored, using Preferences Preferences matterPref; @@ -88,6 +94,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); @@ -102,6 +110,7 @@ void setup() { Serial.println("IP address: "); Serial.println(WiFi.localIP()); delay(500); +#endif // Initialize Matter EndPoint matterPref.begin("MatterPrefs", false); @@ -133,7 +142,7 @@ void setup() { Matter.begin(); // This may be a restart of a already commissioned Matter accessory if (Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); Serial.printf( "Initial state: %s | brightness: %d | Color Temperature: %d mireds \r\n", CW_WW_Light ? "ON" : "OFF", CW_WW_Light.getBrightness(), CW_WW_Light.getColorTemperature() @@ -166,7 +175,7 @@ void loop() { ); // configure the Light based on initial on-off state and brightness CW_WW_Light.updateAccessory(); - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } // A button is also used to control the light diff --git a/libraries/Matter/examples/MatterTemperatureSensor/MatterTemperatureSensor.ino b/libraries/Matter/examples/MatterTemperatureSensor/MatterTemperatureSensor.ino index 086155aeffe..46d6ace361f 100644 --- a/libraries/Matter/examples/MatterTemperatureSensor/MatterTemperatureSensor.ino +++ b/libraries/Matter/examples/MatterTemperatureSensor/MatterTemperatureSensor.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,15 +21,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Matter Temperature Sensor Endpoint MatterTemperatureSensor SimulatedTemperatureSensor; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - decommissioning button const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -60,6 +66,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -68,6 +76,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // set initial temperature sensor measurement // Simulated Sensor - it shall initially print -25C and then move to the -10C to 10C range @@ -92,7 +101,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); } } diff --git a/libraries/Matter/examples/MatterThermostat/MatterThermostat.ino b/libraries/Matter/examples/MatterThermostat/MatterThermostat.ino index bf76477c846..2c446f59e34 100644 --- a/libraries/Matter/examples/MatterThermostat/MatterThermostat.ino +++ b/libraries/Matter/examples/MatterThermostat/MatterThermostat.ino @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,15 +21,21 @@ // Matter Manager #include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space #include +#endif // List of Matter Endpoints for this Node // Matter Thermostat Endpoint MatterThermostat SimulatedThermostat; +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // WiFi is manually set and started const char *ssid = "your-ssid"; // Change this to your WiFi SSID const char *password = "your-password"; // Change this to your WiFi password +#endif // set your board USER BUTTON pin here - decommissioning button const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. @@ -62,6 +68,8 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection @@ -70,6 +78,7 @@ void setup() { Serial.print("."); } Serial.println(); +#endif // Simulated Thermostat in COOLING and HEATING mode with Auto Mode to keep the temperature between setpoints // Auto Mode can only be used when the control sequence of operation is Cooling & Heating @@ -94,7 +103,7 @@ void setup() { Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); } } - Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use."); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); // after commissioning, set initial thermostat parameters // start the thermostat in AUTO mode diff --git a/libraries/Matter/src/Matter.cpp b/libraries/Matter/src/Matter.cpp index b16edfd85c1..5ddacc1622c 100644 --- a/libraries/Matter/src/Matter.cpp +++ b/libraries/Matter/src/Matter.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,10 @@ #include #include +#if CONFIG_ENABLE_MATTER_OVER_THREAD +#include "esp_openthread_types.h" +#include "platform/ESP32/OpenthreadLauncher.h" +#endif using namespace esp_matter; using namespace esp_matter::attribute; @@ -151,6 +155,18 @@ void ArduinoMatter::begin() { return; } +#if CONFIG_ENABLE_MATTER_OVER_THREAD + // Set OpenThread platform config + esp_openthread_platform_config_t config; + memset(&config, 0, sizeof(esp_openthread_platform_config_t)); + config.radio_config.radio_mode = RADIO_MODE_NATIVE; + config.host_config.host_connection_mode = HOST_CONNECTION_MODE_NONE; + config.port_config.storage_partition_name = "nvs"; + config.port_config.netif_queue_size = 10; + config.port_config.task_queue_size = 10; + set_openthread_platform_config(&config); +#endif + /* Matter start */ esp_err_t err = esp_matter::start(app_event_cb); if (err != ESP_OK) { diff --git a/libraries/Matter/src/Matter.h b/libraries/Matter/src/Matter.h index 682a0498076..09e59b4e04b 100644 --- a/libraries/Matter/src/Matter.h +++ b/libraries/Matter/src/Matter.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndPoint.cpp b/libraries/Matter/src/MatterEndPoint.cpp new file mode 100644 index 00000000000..ecf1acff579 --- /dev/null +++ b/libraries/Matter/src/MatterEndPoint.cpp @@ -0,0 +1,139 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#ifdef CONFIG_ESP_MATTER_ENABLE_DATA_MODEL + +#include + +uint16_t MatterEndPoint::secondary_network_endpoint_id = 0; + +// This function is called to create a secondary network interface endpoint. +// It can be used for devices that support multiple network interfaces, +// such as Ethernet, Thread and Wi-Fi. +bool MatterEndPoint::createSecondaryNetworkInterface() { + if (secondary_network_endpoint_id != 0) { + log_v("Secondary network interface endpoint already exists with ID %d", secondary_network_endpoint_id); + return false; + } + // Create a secondary network interface endpoint + endpoint::secondary_network_interface::config_t secondary_network_interface_config; + secondary_network_interface_config.network_commissioning.feature_map = chip::to_underlying( + //chip::app::Clusters::NetworkCommissioning::Feature::kWiFiNetworkInterface) | + chip::app::Clusters::NetworkCommissioning::Feature::kThreadNetworkInterface + ); + endpoint_t *endpoint = endpoint::secondary_network_interface::create(node::get(), &secondary_network_interface_config, ENDPOINT_FLAG_NONE, nullptr); + if (endpoint == nullptr) { + log_e("Failed to create secondary network interface endpoint"); + return false; + } + secondary_network_endpoint_id = endpoint::get_id(endpoint); + log_i("Secondary Network Interface created with endpoint_id %d", secondary_network_endpoint_id); + return true; +} + +uint16_t MatterEndPoint::getSecondaryNetworkEndPointId() { + return secondary_network_endpoint_id; +} + +uint16_t MatterEndPoint::getEndPointId() { + return endpoint_id; +} + +void MatterEndPoint::setEndPointId(uint16_t ep) { + if (ep == 0) { + log_e("Invalid endpoint ID"); + return; + } + log_v("Endpoint ID set to %d", ep); + + endpoint_id = ep; +} + +// helper functions for attribute manipulation +esp_matter::attribute_t *MatterEndPoint::getAttribute(uint32_t cluster_id, uint32_t attribute_id) { + if (endpoint_id == 0) { + log_e("Endpoint ID is not set"); + return nullptr; + } + endpoint_t *endpoint = endpoint::get(node::get(), endpoint_id); + if (endpoint == nullptr) { + log_e("Endpoint [%d] not found", endpoint_id); + return nullptr; + } + cluster_t *cluster = cluster::get(endpoint, cluster_id); + if (cluster == nullptr) { + log_e("Cluster [%d] not found", cluster_id); + return nullptr; + } + esp_matter::attribute_t *attribute = attribute::get(cluster, attribute_id); + if (attribute == nullptr) { + log_e("Attribute [%d] not found", attribute_id); + return nullptr; + } + return attribute; +} + +// get the value of an attribute from its cluster id and attribute it +bool MatterEndPoint::getAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal) { + esp_matter::attribute_t *attribute = getAttribute(cluster_id, attribute_id); + if (attribute == nullptr) { + return false; + } + if (attribute::get_val(attribute, attrVal) == ESP_OK) { + log_v("GET_VAL Success for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); + return true; + } + log_e("GET_VAL FAILED! for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); + return false; +} + +// set the value of an attribute from its cluster id and attribute it +bool MatterEndPoint::setAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal) { + esp_matter::attribute_t *attribute = getAttribute(cluster_id, attribute_id); + if (attribute == nullptr) { + return false; + } + if (attribute::set_val(attribute, attrVal) == ESP_OK) { + log_v("SET_VAL Success for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); + return true; + } + log_e("SET_VAL FAILED! for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); + return false; +} + +// update the value of an attribute from its cluster id and attribute it +bool MatterEndPoint::updateAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal) { + if (attribute::update(endpoint_id, cluster_id, attribute_id, attrVal) == ESP_OK) { + log_v("Update Success for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); + return true; + } + log_e("Update FAILED! for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); + return false; +} + +// This callback is invoked when clients interact with the Identify Cluster of an specific endpoint. +bool MatterEndPoint::endpointIdentifyCB(uint16_t endpoint_id, bool identifyIsEnabled) { + if (_onEndPointIdentifyCB) { + return _onEndPointIdentifyCB(identifyIsEnabled); + } + return true; +} + +// User callback for the Identify Cluster functionality +void MatterEndPoint::onIdentify(EndPointIdentifyCB onEndPointIdentifyCB) { + _onEndPointIdentifyCB = onEndPointIdentifyCB; +} + +#endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */ diff --git a/libraries/Matter/src/MatterEndPoint.h b/libraries/Matter/src/MatterEndPoint.h index 95d3d3c08df..3138014d624 100644 --- a/libraries/Matter/src/MatterEndPoint.h +++ b/libraries/Matter/src/MatterEndPoint.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ #include #ifdef CONFIG_ESP_MATTER_ENABLE_DATA_MODEL -#include +#include +#include #include using namespace esp_matter; @@ -29,93 +30,47 @@ class MatterEndPoint { ATTR_UPDATE = true }; - uint16_t getEndPointId() { - return endpoint_id; - } + using EndPointIdentifyCB = std::function; + + // this function is called by Matter internal event processor. It could be overwritten by the application, if necessary. + virtual bool attributeChangeCB(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val) = 0; + + // This function is called to create a secondary network interface endpoint. + // It can be used for devices that support multiple network interfaces, + // such as Ethernet, Thread and Wi-Fi. + bool createSecondaryNetworkInterface(); + + // This function is called to get the secondary network interface endpoint ID. + uint16_t getSecondaryNetworkEndPointId(); + + // This function is called to get the current Matter Accessory endpoint ID. + uint16_t getEndPointId(); - void setEndPointId(uint16_t ep) { - endpoint_id = ep; - } + // This function is called to set the current Matter Accessory endpoint ID. + void setEndPointId(uint16_t ep); // helper functions for attribute manipulation - esp_matter::attribute_t *getAttribute(uint32_t cluster_id, uint32_t attribute_id) { - if (endpoint_id == 0) { - log_e("Endpoint ID is not set"); - return nullptr; - } - endpoint_t *endpoint = endpoint::get(node::get(), endpoint_id); - if (endpoint == nullptr) { - log_e("Endpoint [%d] not found", endpoint_id); - return nullptr; - } - cluster_t *cluster = cluster::get(endpoint, cluster_id); - if (cluster == nullptr) { - log_e("Cluster [%d] not found", cluster_id); - return nullptr; - } - esp_matter::attribute_t *attribute = attribute::get(cluster, attribute_id); - if (attribute == nullptr) { - log_e("Attribute [%d] not found", attribute_id); - return nullptr; - } - return attribute; - } - - // get the value of an attribute from its cluster id and attribute it - bool getAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal) { - esp_matter::attribute_t *attribute = getAttribute(cluster_id, attribute_id); - if (attribute == nullptr) { - return false; - } - if (attribute::get_val(attribute, attrVal) == ESP_OK) { - log_v("GET_VAL Success for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); - return true; - } - log_e("GET_VAL FAILED! for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); - return false; - } - - // set the value of an attribute from its cluster id and attribute it - bool setAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal) { - esp_matter::attribute_t *attribute = getAttribute(cluster_id, attribute_id); - if (attribute == nullptr) { - return false; - } - if (attribute::set_val(attribute, attrVal) == ESP_OK) { - log_v("SET_VAL Success for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); - return true; - } - log_e("SET_VAL FAILED! for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); - return false; - } - - // update the value of an attribute from its cluster id and attribute it - bool updateAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal) { - if (attribute::update(endpoint_id, cluster_id, attribute_id, attrVal) == ESP_OK) { - log_v("Update Success for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); - return true; - } - log_e("Update FAILED! for cluster %d, attribute %d with value %d", cluster_id, attribute_id, attrVal->val.u32); - return false; - } + esp_matter::attribute_t *getAttribute(uint32_t cluster_id, uint32_t attribute_id); - // this function is called by Matter internal event processor. It could be overwritten by the application, if necessary. - virtual bool attributeChangeCB(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val) = 0; + // get the value of an attribute from its cluster id and + bool getAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal); + + // set the value of an attribute from its cluster id and + bool setAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal); + + // update the value of an attribute from its cluster id + bool updateAttributeVal(uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *attrVal); // This callback is invoked when clients interact with the Identify Cluster of an specific endpoint. - bool endpointIdentifyCB(uint16_t endpoint_id, bool identifyIsEnabled) { - if (_onEndPointIdentifyCB) { - return _onEndPointIdentifyCB(identifyIsEnabled); - } - return true; - } - // User callaback for the Identify Cluster functionality - using EndPointIdentifyCB = std::function; - void onIdentify(EndPointIdentifyCB onEndPointIdentifyCB) { - _onEndPointIdentifyCB = onEndPointIdentifyCB; - } + bool endpointIdentifyCB(uint16_t endpoint_id, bool identifyIsEnabled); + + // User callback for the Identify Cluster functionality + void onIdentify(EndPointIdentifyCB onEndPointIdentifyCB); protected: + // used for secondary network interface endpoints + static uint16_t secondary_network_endpoint_id; + // main endpoint ID uint16_t endpoint_id = 0; EndPointIdentifyCB _onEndPointIdentifyCB = nullptr; }; diff --git a/libraries/Matter/src/MatterEndpoints/MatterColorLight.cpp b/libraries/Matter/src/MatterEndpoints/MatterColorLight.cpp index 39d79e86325..aff0972528b 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterColorLight.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterColorLight.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterColorLight.h b/libraries/Matter/src/MatterEndpoints/MatterColorLight.h index 99449addd50..b07432a2cf5 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterColorLight.h +++ b/libraries/Matter/src/MatterEndpoints/MatterColorLight.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.cpp b/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.cpp index 3c4fccfa046..db90edf1177 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.h b/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.h index 539bc386e92..3c8b2dca882 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.h +++ b/libraries/Matter/src/MatterEndpoints/MatterColorTemperatureLight.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterContactSensor.cpp b/libraries/Matter/src/MatterEndpoints/MatterContactSensor.cpp index 17b0fe7a247..52dcc8da7c0 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterContactSensor.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterContactSensor.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -60,6 +60,7 @@ bool MatterContactSensor::begin(bool _contactState) { contactState = _contactState; setEndPointId(endpoint::get_id(endpoint)); log_i("Contact Sensor created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterContactSensor.h b/libraries/Matter/src/MatterEndpoints/MatterContactSensor.h index 257da785e53..687bf0d0b4b 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterContactSensor.h +++ b/libraries/Matter/src/MatterEndpoints/MatterContactSensor.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.cpp b/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.cpp index 5167cf1f21c..4659dd71675 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.h b/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.h index 4497edd2fe2..45b112100cb 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.h +++ b/libraries/Matter/src/MatterEndpoints/MatterDimmableLight.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.cpp b/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.cpp index 9b245fb9408..5e9feb5aadd 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.h b/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.h index a4baef968a4..97f0279b9e4 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.h +++ b/libraries/Matter/src/MatterEndpoints/MatterEnhancedColorLight.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterFan.cpp b/libraries/Matter/src/MatterEndpoints/MatterFan.cpp index 1647490aa05..5745604ebde 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterFan.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterFan.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -110,6 +110,7 @@ bool MatterFan::begin(uint8_t percent, FanMode_t fanMode, FanModeSequence_t fanM setEndPointId(endpoint::get_id(endpoint)); log_i("Fan created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterFan.h b/libraries/Matter/src/MatterEndpoints/MatterFan.h index a1cd6e42423..53736ac70f2 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterFan.h +++ b/libraries/Matter/src/MatterEndpoints/MatterFan.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.cpp b/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.cpp index e20479af088..f69d4a2ab7f 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -79,6 +79,7 @@ bool MatterGenericSwitch::begin() { setEndPointId(endpoint::get_id(endpoint)); log_i("Generic Switch created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.h b/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.h index 14118462932..ffcc6f47706 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.h +++ b/libraries/Matter/src/MatterEndpoints/MatterGenericSwitch.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.cpp b/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.cpp index d31d0e43728..ef8276e4a04 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -68,6 +68,7 @@ bool MatterHumiditySensor::begin(uint16_t _rawHumidity) { rawHumidity = _rawHumidity; setEndPointId(endpoint::get_id(endpoint)); log_i("Humidity Sensor created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.h b/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.h index aed758b7b7a..48c3a4b9e28 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.h +++ b/libraries/Matter/src/MatterEndpoints/MatterHumiditySensor.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp b/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp index 0d55c37708a..f91a02e14a9 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -71,6 +71,7 @@ bool MatterOccupancySensor::begin(bool _occupancyState, OccupancySensorType_t _o occupancyState = _occupancyState; setEndPointId(endpoint::get_id(endpoint)); log_i("Occupancy Sensor created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.h b/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.h index 30f312a9841..acfa7fec632 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.h +++ b/libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.cpp b/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.cpp index f400390f9a7..afd764088c5 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -79,6 +79,7 @@ bool MatterOnOffLight::begin(bool initialState) { setEndPointId(endpoint::get_id(endpoint)); log_i("On-Off Light created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.h b/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.h index ec524d2c300..2c52858cc9a 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.h +++ b/libraries/Matter/src/MatterEndpoints/MatterOnOffLight.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp b/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp index 9b08958684c..33cee105833 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -78,6 +78,7 @@ bool MatterOnOffPlugin::begin(bool initialState) { onOffState = initialState; setEndPointId(endpoint::get_id(endpoint)); log_i("On-Off Plugin created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.h b/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.h index 0b05c0944c4..e3489b355a8 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.h +++ b/libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.cpp b/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.cpp index 86d245d4041..8674ad5936b 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -61,6 +61,7 @@ bool MatterPressureSensor::begin(int16_t _rawPressure) { rawPressure = _rawPressure; setEndPointId(endpoint::get_id(endpoint)); log_i("Pressure Sensor created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.h b/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.h index 0715c05609d..6b1973049ca 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.h +++ b/libraries/Matter/src/MatterEndpoints/MatterPressureSensor.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.cpp b/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.cpp index 863f86386c1..248bd29f05f 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -61,6 +61,7 @@ bool MatterTemperatureSensor::begin(int16_t _rawTemperature) { rawTemperature = _rawTemperature; setEndPointId(endpoint::get_id(endpoint)); log_i("Temperature Sensor created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.h b/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.h index 3be6101166c..51a1eeb3b0a 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.h +++ b/libraries/Matter/src/MatterEndpoints/MatterTemperatureSensor.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp b/libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp index 5a68421bd8a..6ee18ef0cc9 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -202,6 +202,7 @@ bool MatterThermostat::begin(ControlSequenceOfOperation_t _controlSequence, Ther setEndPointId(endpoint::get_id(endpoint)); log_i("Thermostat created with endpoint_id %d", getEndPointId()); + started = true; return true; } diff --git a/libraries/Matter/src/MatterEndpoints/MatterThermostat.h b/libraries/Matter/src/MatterEndpoints/MatterThermostat.h index 2d64bdf3b01..53df61d191e 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterThermostat.h +++ b/libraries/Matter/src/MatterEndpoints/MatterThermostat.h @@ -1,4 +1,4 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 18f647611bda1a5e0f7fc5bb95a21c40f18e0f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:39:46 +0200 Subject: [PATCH 059/102] fix(spi): Fix bus clock for ESP32-P4 + remove S2 leftover (#11547) * fix(spi): Fix bus clock for ESP32-P4 * fix(ci): Ignore unused-variable warning * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- cores/esp32/esp32-hal-spi.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/cores/esp32/esp32-hal-spi.c b/cores/esp32/esp32-hal-spi.c index d39aceb5f8d..107b94da0d6 100644 --- a/cores/esp32/esp32-hal-spi.c +++ b/cores/esp32/esp32-hal-spi.c @@ -60,6 +60,7 @@ #elif CONFIG_IDF_TARGET_ESP32P4 #include "esp32p4/rom/ets_sys.h" #include "esp32p4/rom/gpio.h" +#include "hal/spi_ll.h" #else #error Target CONFIG_IDF_TARGET is not supported #endif @@ -639,9 +640,6 @@ spi_t *spiStartBus(uint8_t spi_num, uint32_t clockDiv, uint8_t dataMode, uint8_t } else if (spi_num == HSPI) { DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_SPI3_CLK_EN); DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI3_RST); - } else { - DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_SPI01_CLK_EN); - DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI01_RST); } #elif CONFIG_IDF_TARGET_ESP32S3 if (spi_num == FSPI) { @@ -662,6 +660,31 @@ spi_t *spiStartBus(uint8_t spi_num, uint32_t clockDiv, uint8_t dataMode, uint8_t DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_SPI01_CLK_EN); DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI01_RST); } +#elif CONFIG_IDF_TARGET_ESP32P4 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" + if (spi_num == FSPI) { + PERIPH_RCC_ACQUIRE_ATOMIC(PERIPH_GPSPI2_MODULE, ref_count) { + if (ref_count == 0) { + PERIPH_RCC_ATOMIC() { + spi_ll_enable_bus_clock(SPI2_HOST, true); + spi_ll_reset_register(SPI2_HOST); + spi_ll_enable_clock(SPI2_HOST, true); + } + } + } + } else if (spi_num == HSPI) { + PERIPH_RCC_ACQUIRE_ATOMIC(PERIPH_GPSPI3_MODULE, ref_count) { + if (ref_count == 0) { + PERIPH_RCC_ATOMIC() { + spi_ll_enable_bus_clock(SPI3_HOST, true); + spi_ll_reset_register(SPI3_HOST); + spi_ll_enable_clock(SPI3_HOST, true); + } + } + } + } +#pragma GCC diagnostic pop #elif defined(__PERIPH_CTRL_ALLOW_LEGACY_API) periph_ll_reset(PERIPH_SPI2_MODULE); periph_ll_enable_clk_clear_rst(PERIPH_SPI2_MODULE); From ac961f671abd5ae1da0a15fd4bee71ed807c2cf3 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Thu, 3 Jul 2025 16:42:28 +0300 Subject: [PATCH 060/102] Update core version to 3.2.1 --- cores/esp32/esp_arduino_version.h | 2 +- libraries/ArduinoOTA/library.properties | 2 +- libraries/AsyncUDP/library.properties | 2 +- libraries/BLE/library.properties | 2 +- libraries/BluetoothSerial/library.properties | 2 +- libraries/DNSServer/library.properties | 2 +- libraries/EEPROM/library.properties | 2 +- libraries/ESP32/library.properties | 2 +- libraries/ESP_I2S/library.properties | 2 +- libraries/ESP_NOW/library.properties | 2 +- libraries/ESP_SR/library.properties | 2 +- libraries/ESPmDNS/library.properties | 2 +- libraries/Ethernet/library.properties | 2 +- libraries/FFat/library.properties | 2 +- libraries/FS/library.properties | 2 +- libraries/HTTPClient/library.properties | 2 +- libraries/HTTPUpdate/library.properties | 2 +- libraries/HTTPUpdateServer/library.properties | 2 +- libraries/Insights/library.properties | 2 +- libraries/LittleFS/library.properties | 2 +- libraries/Matter/library.properties | 2 +- libraries/NetBIOS/library.properties | 2 +- libraries/Network/library.properties | 2 +- libraries/NetworkClientSecure/library.properties | 2 +- libraries/OpenThread/library.properties | 2 +- libraries/PPP/library.properties | 2 +- libraries/Preferences/library.properties | 2 +- libraries/RainMaker/library.properties | 2 +- libraries/SD/library.properties | 2 +- libraries/SD_MMC/library.properties | 2 +- libraries/SPI/library.properties | 2 +- libraries/SPIFFS/library.properties | 2 +- libraries/SimpleBLE/library.properties | 2 +- libraries/TFLiteMicro/library.properties | 2 +- libraries/Ticker/library.properties | 2 +- libraries/USB/library.properties | 2 +- libraries/Update/library.properties | 2 +- libraries/WebServer/library.properties | 2 +- libraries/WiFi/library.properties | 2 +- libraries/WiFiProv/library.properties | 2 +- libraries/Wire/library.properties | 2 +- libraries/Zigbee/library.properties | 2 +- package.json | 2 +- platform.txt | 2 +- 44 files changed, 44 insertions(+), 44 deletions(-) diff --git a/cores/esp32/esp_arduino_version.h b/cores/esp32/esp_arduino_version.h index b1355e908ae..97bb3ac794b 100644 --- a/cores/esp32/esp_arduino_version.h +++ b/cores/esp32/esp_arduino_version.h @@ -23,7 +23,7 @@ extern "C" { /** Minor version number (x.X.x) */ #define ESP_ARDUINO_VERSION_MINOR 2 /** Patch version number (x.x.X) */ -#define ESP_ARDUINO_VERSION_PATCH 0 +#define ESP_ARDUINO_VERSION_PATCH 1 /** * Macro to convert ARDUINO version number into an integer diff --git a/libraries/ArduinoOTA/library.properties b/libraries/ArduinoOTA/library.properties index 0796eddf318..18de5aa2180 100644 --- a/libraries/ArduinoOTA/library.properties +++ b/libraries/ArduinoOTA/library.properties @@ -1,5 +1,5 @@ name=ArduinoOTA -version=3.2.0 +version=3.2.1 author=Ivan Grokhotkov and Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables Over The Air upgrades, via wifi and espota.py UDP request/TCP download. diff --git a/libraries/AsyncUDP/library.properties b/libraries/AsyncUDP/library.properties index 116dcbacaa8..c64c60d0421 100644 --- a/libraries/AsyncUDP/library.properties +++ b/libraries/AsyncUDP/library.properties @@ -1,5 +1,5 @@ name=ESP32 Async UDP -version=3.2.0 +version=3.2.1 author=Me-No-Dev maintainer=Me-No-Dev sentence=Async UDP Library for ESP32 diff --git a/libraries/BLE/library.properties b/libraries/BLE/library.properties index 7ef636223ec..b009b14e194 100644 --- a/libraries/BLE/library.properties +++ b/libraries/BLE/library.properties @@ -1,5 +1,5 @@ name=BLE -version=3.2.0 +version=3.2.1 author=Neil Kolban maintainer=Dariusz Krempa sentence=BLE functions for ESP32 diff --git a/libraries/BluetoothSerial/library.properties b/libraries/BluetoothSerial/library.properties index 0a382410bba..4bc1427e3f8 100644 --- a/libraries/BluetoothSerial/library.properties +++ b/libraries/BluetoothSerial/library.properties @@ -1,5 +1,5 @@ name=BluetoothSerial -version=3.2.0 +version=3.2.1 author=Evandro Copercini maintainer=Evandro Copercini sentence=Simple UART to Classical Bluetooth bridge for ESP32 diff --git a/libraries/DNSServer/library.properties b/libraries/DNSServer/library.properties index 5e70a6ec03a..42e4c38dc9d 100644 --- a/libraries/DNSServer/library.properties +++ b/libraries/DNSServer/library.properties @@ -1,5 +1,5 @@ name=DNSServer -version=3.2.0 +version=3.2.1 author=Kristijan Novoselić maintainer=Kristijan Novoselić, sentence=A simple DNS server for ESP32. diff --git a/libraries/EEPROM/library.properties b/libraries/EEPROM/library.properties index c7e48501c04..ee1caae0792 100644 --- a/libraries/EEPROM/library.properties +++ b/libraries/EEPROM/library.properties @@ -1,5 +1,5 @@ name=EEPROM -version=3.2.0 +version=3.2.1 author=Ivan Grokhotkov maintainer=Paolo Becchi sentence=Enables reading and writing data a sequential, addressable FLASH storage diff --git a/libraries/ESP32/library.properties b/libraries/ESP32/library.properties index 7ebc69be71f..0815a69ce6b 100644 --- a/libraries/ESP32/library.properties +++ b/libraries/ESP32/library.properties @@ -1,5 +1,5 @@ name=ESP32 -version=3.2.0 +version=3.2.1 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 sketches examples diff --git a/libraries/ESP_I2S/library.properties b/libraries/ESP_I2S/library.properties index 263e9823275..ff1ad5d59da 100644 --- a/libraries/ESP_I2S/library.properties +++ b/libraries/ESP_I2S/library.properties @@ -1,5 +1,5 @@ name=ESP_I2S -version=3.2.0 +version=3.2.1 author=me-no-dev maintainer=me-no-dev sentence=Library for ESP I2S communication diff --git a/libraries/ESP_NOW/library.properties b/libraries/ESP_NOW/library.properties index f3e5c109a9b..426a9464ace 100644 --- a/libraries/ESP_NOW/library.properties +++ b/libraries/ESP_NOW/library.properties @@ -1,5 +1,5 @@ name=ESP_NOW -version=3.2.0 +version=3.2.1 author=me-no-dev maintainer=P-R-O-C-H-Y sentence=Library for ESP_NOW diff --git a/libraries/ESP_SR/library.properties b/libraries/ESP_SR/library.properties index 295761bd9fb..3b9777bca8f 100644 --- a/libraries/ESP_SR/library.properties +++ b/libraries/ESP_SR/library.properties @@ -1,5 +1,5 @@ name=ESP_SR -version=3.2.0 +version=3.2.1 author=me-no-dev maintainer=me-no-dev sentence=Library for ESP Sound Recognition diff --git a/libraries/ESPmDNS/library.properties b/libraries/ESPmDNS/library.properties index 6d36d61b783..b99a5f58e5c 100644 --- a/libraries/ESPmDNS/library.properties +++ b/libraries/ESPmDNS/library.properties @@ -1,5 +1,5 @@ name=ESPmDNS -version=3.2.0 +version=3.2.1 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 mDNS Library diff --git a/libraries/Ethernet/library.properties b/libraries/Ethernet/library.properties index d34ae036417..cd2d8ead018 100644 --- a/libraries/Ethernet/library.properties +++ b/libraries/Ethernet/library.properties @@ -1,5 +1,5 @@ name=Ethernet -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables network connection (local and Internet) using the ESP32 Ethernet. diff --git a/libraries/FFat/library.properties b/libraries/FFat/library.properties index 35940fd5472..898982536b0 100644 --- a/libraries/FFat/library.properties +++ b/libraries/FFat/library.properties @@ -1,5 +1,5 @@ name=FFat -version=3.2.0 +version=3.2.1 author=Hristo Gochkov, Ivan Grokhtkov, Larry Bernstone maintainer=Hristo Gochkov sentence=ESP32 FAT on Flash File System diff --git a/libraries/FS/library.properties b/libraries/FS/library.properties index 07bd296bb83..fe86d613bdf 100644 --- a/libraries/FS/library.properties +++ b/libraries/FS/library.properties @@ -1,5 +1,5 @@ name=FS -version=3.2.0 +version=3.2.1 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 File System diff --git a/libraries/HTTPClient/library.properties b/libraries/HTTPClient/library.properties index f2dafc36d1b..76da4c857e1 100644 --- a/libraries/HTTPClient/library.properties +++ b/libraries/HTTPClient/library.properties @@ -1,5 +1,5 @@ name=HTTPClient -version=3.2.0 +version=3.2.1 author=Markus Sattler maintainer=Markus Sattler sentence=HTTP Client for ESP32 diff --git a/libraries/HTTPUpdate/library.properties b/libraries/HTTPUpdate/library.properties index 419f3b97b3f..dfe0474c3f3 100644 --- a/libraries/HTTPUpdate/library.properties +++ b/libraries/HTTPUpdate/library.properties @@ -1,5 +1,5 @@ name=HTTPUpdate -version=3.2.0 +version=3.2.1 author=Markus Sattler maintainer=Markus Sattler sentence=Http Update for ESP32 diff --git a/libraries/HTTPUpdateServer/library.properties b/libraries/HTTPUpdateServer/library.properties index 9c793a26ac8..90aa966d27f 100644 --- a/libraries/HTTPUpdateServer/library.properties +++ b/libraries/HTTPUpdateServer/library.properties @@ -1,5 +1,5 @@ name=HTTPUpdateServer -version=3.2.0 +version=3.2.1 author=Hristo Kapanakov maintainer= sentence=Simple HTTP Update server based on the WebServer diff --git a/libraries/Insights/library.properties b/libraries/Insights/library.properties index fefe5aab177..c1ad9c72ce1 100644 --- a/libraries/Insights/library.properties +++ b/libraries/Insights/library.properties @@ -1,5 +1,5 @@ name=ESP Insights -version=3.2.0 +version=3.2.1 author=Sanket Wadekar maintainer=Sanket Wadekar sentence=ESP Insights diff --git a/libraries/LittleFS/library.properties b/libraries/LittleFS/library.properties index a9dae69b7f8..e00ed49c312 100644 --- a/libraries/LittleFS/library.properties +++ b/libraries/LittleFS/library.properties @@ -1,5 +1,5 @@ name=LittleFS -version=3.2.0 +version=3.2.1 author= maintainer= sentence=LittleFS for esp32 diff --git a/libraries/Matter/library.properties b/libraries/Matter/library.properties index ac9e0964ab5..b601fce0ff5 100644 --- a/libraries/Matter/library.properties +++ b/libraries/Matter/library.properties @@ -1,5 +1,5 @@ name=Matter -version=3.2.0 +version=3.2.1 author=Rodrigo Garcia | GitHub @SuGlider maintainer=Rodrigo Garcia sentence=Library for supporting Matter environment on ESP32. diff --git a/libraries/NetBIOS/library.properties b/libraries/NetBIOS/library.properties index 5f134bfdc55..a4bbf93c0ed 100644 --- a/libraries/NetBIOS/library.properties +++ b/libraries/NetBIOS/library.properties @@ -1,5 +1,5 @@ name=NetBIOS -version=3.2.0 +version=3.2.1 author=Pablo@xpablo.cz maintainer=Hristo Gochkov sentence=Enables NBNS (NetBIOS) name resolution. diff --git a/libraries/Network/library.properties b/libraries/Network/library.properties index 0b821e08d77..f2284981704 100644 --- a/libraries/Network/library.properties +++ b/libraries/Network/library.properties @@ -1,5 +1,5 @@ name=Networking -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=General network management library. diff --git a/libraries/NetworkClientSecure/library.properties b/libraries/NetworkClientSecure/library.properties index 455dea6a2bf..31af7a1bc8c 100644 --- a/libraries/NetworkClientSecure/library.properties +++ b/libraries/NetworkClientSecure/library.properties @@ -1,5 +1,5 @@ name=NetworkClientSecure -version=3.2.0 +version=3.2.1 author=Evandro Luis Copercini maintainer=Github Community sentence=Enables secure network connection (local and Internet) using the ESP32 built-in WiFi. diff --git a/libraries/OpenThread/library.properties b/libraries/OpenThread/library.properties index 0e547d188aa..2687b1dcd1c 100644 --- a/libraries/OpenThread/library.properties +++ b/libraries/OpenThread/library.properties @@ -1,5 +1,5 @@ name=OpenThread -version=3.2.0 +version=3.2.1 author=Rodrigo Garcia | GitHub @SuGlider maintainer=Rodrigo Garcia sentence=Library for OpenThread Network on ESP32. diff --git a/libraries/PPP/library.properties b/libraries/PPP/library.properties index 7158a027b0a..c39647cbe56 100644 --- a/libraries/PPP/library.properties +++ b/libraries/PPP/library.properties @@ -1,5 +1,5 @@ name=PPP -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables network connection using GSM Modem. diff --git a/libraries/Preferences/library.properties b/libraries/Preferences/library.properties index eb0158e4932..6e0d38348c0 100644 --- a/libraries/Preferences/library.properties +++ b/libraries/Preferences/library.properties @@ -1,5 +1,5 @@ name=Preferences -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Provides friendly access to ESP32's Non-Volatile Storage diff --git a/libraries/RainMaker/library.properties b/libraries/RainMaker/library.properties index 95ce14d6708..71b4082a0a7 100644 --- a/libraries/RainMaker/library.properties +++ b/libraries/RainMaker/library.properties @@ -1,5 +1,5 @@ name=ESP RainMaker -version=3.2.0 +version=3.2.1 author=Sweety Mhaiske maintainer=Hristo Gochkov sentence=ESP RainMaker Support diff --git a/libraries/SD/library.properties b/libraries/SD/library.properties index 66c4f5cfafd..cc51196ed54 100644 --- a/libraries/SD/library.properties +++ b/libraries/SD/library.properties @@ -1,5 +1,5 @@ name=SD -version=3.2.0 +version=3.2.1 author=Arduino, SparkFun maintainer=Arduino sentence=Enables reading and writing on SD cards. For all Arduino boards. diff --git a/libraries/SD_MMC/library.properties b/libraries/SD_MMC/library.properties index 855390e5057..590eb8ebc52 100644 --- a/libraries/SD_MMC/library.properties +++ b/libraries/SD_MMC/library.properties @@ -1,5 +1,5 @@ name=SD_MMC -version=3.2.0 +version=3.2.1 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 SDMMC File System diff --git a/libraries/SPI/library.properties b/libraries/SPI/library.properties index 64db93aceeb..724137030d1 100644 --- a/libraries/SPI/library.properties +++ b/libraries/SPI/library.properties @@ -1,5 +1,5 @@ name=SPI -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables the communication with devices that use the Serial Peripheral Interface (SPI) Bus. For all Arduino boards, BUT Arduino DUE. diff --git a/libraries/SPIFFS/library.properties b/libraries/SPIFFS/library.properties index 78f77fe9794..fc4601e512c 100644 --- a/libraries/SPIFFS/library.properties +++ b/libraries/SPIFFS/library.properties @@ -1,5 +1,5 @@ name=SPIFFS -version=3.2.0 +version=3.2.1 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 SPIFFS File System diff --git a/libraries/SimpleBLE/library.properties b/libraries/SimpleBLE/library.properties index ad5e10d3acb..768449ee1c4 100644 --- a/libraries/SimpleBLE/library.properties +++ b/libraries/SimpleBLE/library.properties @@ -1,5 +1,5 @@ name=SimpleBLE -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Provides really simple BLE advertizer with just on and off diff --git a/libraries/TFLiteMicro/library.properties b/libraries/TFLiteMicro/library.properties index 1e8db045610..6ad0c32c7d5 100644 --- a/libraries/TFLiteMicro/library.properties +++ b/libraries/TFLiteMicro/library.properties @@ -1,5 +1,5 @@ name=TFLite Micro -version=3.2.0 +version=3.2.1 author=Sanket Wadekar maintainer=Sanket Wadekar sentence=TensorFlow Lite for Microcontrollers diff --git a/libraries/Ticker/library.properties b/libraries/Ticker/library.properties index 975db96d1ad..8a2af554906 100644 --- a/libraries/Ticker/library.properties +++ b/libraries/Ticker/library.properties @@ -1,5 +1,5 @@ name=Ticker -version=3.2.0 +version=3.2.1 author=Bert Melis maintainer=Hristo Gochkov sentence=Allows to call functions with a given interval. diff --git a/libraries/USB/library.properties b/libraries/USB/library.properties index 9d47dfc6719..677d736a635 100644 --- a/libraries/USB/library.properties +++ b/libraries/USB/library.properties @@ -1,5 +1,5 @@ name=USB -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=ESP32S2 USB Library diff --git a/libraries/Update/library.properties b/libraries/Update/library.properties index c3ee8f7e506..4c756397aba 100644 --- a/libraries/Update/library.properties +++ b/libraries/Update/library.properties @@ -1,5 +1,5 @@ name=Update -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=ESP32 Sketch Update Library diff --git a/libraries/WebServer/library.properties b/libraries/WebServer/library.properties index 2a9ff530d57..bf6c26c65e7 100644 --- a/libraries/WebServer/library.properties +++ b/libraries/WebServer/library.properties @@ -1,5 +1,5 @@ name=WebServer -version=3.2.0 +version=3.2.1 author=Ivan Grokhotkov maintainer=Ivan Grokhtkov sentence=Simple web server library diff --git a/libraries/WiFi/library.properties b/libraries/WiFi/library.properties index 03112c2fcc6..a282570ff8a 100644 --- a/libraries/WiFi/library.properties +++ b/libraries/WiFi/library.properties @@ -1,5 +1,5 @@ name=WiFi -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables network connection (local and Internet) using the ESP32 built-in WiFi. diff --git a/libraries/WiFiProv/library.properties b/libraries/WiFiProv/library.properties index 13a63c50bb1..1cc8e4b0f91 100644 --- a/libraries/WiFiProv/library.properties +++ b/libraries/WiFiProv/library.properties @@ -1,5 +1,5 @@ name=WiFiProv -version=3.2.0 +version=3.2.1 author=Switi Mhaiske maintainer=Hristo Gochkov sentence=Enables provisioning. diff --git a/libraries/Wire/library.properties b/libraries/Wire/library.properties index 655f4bd3194..22cc7f26d86 100644 --- a/libraries/Wire/library.properties +++ b/libraries/Wire/library.properties @@ -1,5 +1,5 @@ name=Wire -version=3.2.0 +version=3.2.1 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Allows the communication between devices or sensors connected via Two Wire Interface Bus. For esp8266 boards. diff --git a/libraries/Zigbee/library.properties b/libraries/Zigbee/library.properties index 9a558d70216..d9587f49fd5 100644 --- a/libraries/Zigbee/library.properties +++ b/libraries/Zigbee/library.properties @@ -1,5 +1,5 @@ name=Zigbee -version=3.2.0 +version=3.2.1 author=P-R-O-C-H-Y maintainer=Jan Procházka sentence=Enables zigbee connection with the ESP32 diff --git a/package.json b/package.json index 9c918733209..85a15ab3615 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "framework-arduinoespressif32", - "version": "3.2.0", + "version": "3.2.1", "description": "Arduino Wiring-based Framework for the Espressif ESP32, ESP32-P4, ESP32-S and ESP32-C series of SoCs", "keywords": [ "framework", diff --git a/platform.txt b/platform.txt index 7bc89426323..62cb829dcf0 100644 --- a/platform.txt +++ b/platform.txt @@ -1,5 +1,5 @@ name=ESP32 Arduino -version=3.2.0 +version=3.2.1 tools.esp32-arduino-libs.path={runtime.platform.path}/tools/esp32-arduino-libs tools.esp32-arduino-libs.path.windows={runtime.platform.path}\tools\esp32-arduino-libs From 0b9c9362deb4d24953a4a5b1196915e551e7e860 Mon Sep 17 00:00:00 2001 From: Dogus Cendek Date: Thu, 3 Jul 2025 23:13:42 +0300 Subject: [PATCH 061/102] Add Deneyap Kart v2 (#11545) * Updated Pins of Devkits Deleted soc_caps.h library and related commands at Deneyap Kart 1A v2, Deneyap Kart 1A, Deneyap Mini and Deneyap Mini v2. Added TX1 and RX1 pins and updated LED pin definition at all Devkits. Added BOOT (BT) pins at Deneyap Kart, Deneyap Kart 1A, Deneyap Mini and Deneyap Kart G. Changed D0 and D1 pin numbers at Deneyap Kart G. Changed D12, D13, D14, D15, PWM0 and PWM1 pin numbers at Deneyap Kart 1A v2. Added A8, T0, T1, T2, T3, T4, T5, T6, T7, T8, D16, D17, D18, D19, PWM2, PWM3, PWM4 and BAT pin numbers at Deneyap Kart 1A v2. Changed A2, A3, A4 (T0) and A5 (T1) pin numbers at Deneyap Kart and Deneyap Kart 1A. Renamed DA2 (DAC2) pin as DA0 (DAC0) and changed DAC1 and DAC2 pin numbers at Deneyap Mini and Deneyap Mini v2. * Updated board.txt of all Devkits Updated board.txt of all Devkits * Remove Repeating Pin Definition Remove Repeating Pin Definition * Fix Pin Definition Remove repeating pin definitions of SPI, I2C and DAC. Update RGB LED definition for using digitalWrite() command with RGB LED. * Remove Repeating Pin Definitions Remove repeating pin definitions of LEDB, SPI, I2C and DAC. * Update RGB LED definition Update RGB LED definition for using digitalWrite() command with RGB LED. * Fix broken links for external library test Fix broken links for external library test * Update UploadMode Config of Deneyap Kart 1A v2 Update UploadMode Config of Deneyap Kart 1A v2 * Add Deneyap Kart v2 Add pin definitions and configs of Deneyap Kart v2. * Update UploadMode config Hardware CDC is default now. * Fixed typo fault Fixed typo fault * Fixed build.board parameter Fixed build.board parameter * Removed unsupported Flash sizes and RAM type Removed unsupported Flash sizes and RAM type from menu. * Remove unsupported partition options Remove unsupported partition options * Fixed Annotations and Space * Update pins_arduino.h --- boards.txt | 223 ++++++++++++++++++++++++++ variants/deneyapkartv2/pins_arduino.h | 123 ++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 variants/deneyapkartv2/pins_arduino.h diff --git a/boards.txt b/boards.txt index 4198f3856b0..e47995985ed 100644 --- a/boards.txt +++ b/boards.txt @@ -33287,6 +33287,229 @@ deneyapkart.menu.EraseFlash.all.upload.erase_cmd=-e ############################################################## +deneyapkartv2.name=Deneyap Kart v2 + +deneyapkartv2.vid.0=0x303a +deneyapkartv2.pid.0=0x82EB + +deneyapkartv2.bootloader.tool=esptool_py +deneyapkartv2.bootloader.tool.default=esptool_py + +deneyapkartv2.upload.tool=esptool_py +deneyapkartv2.upload.tool.default=esptool_py +deneyapkartv2.upload.tool.network=esp_ota + +deneyapkartv2.upload.maximum_size=1310720 +deneyapkartv2.upload.maximum_data_size=327680 +deneyapkartv2.upload.flags= +deneyapkartv2.upload.extra_flags= +deneyapkartv2.upload.use_1200bps_touch=false +deneyapkartv2.upload.wait_for_upload_port=false + +deneyapkartv2.serial.disableDTR=false +deneyapkartv2.serial.disableRTS=false + +deneyapkartv2.build.tarch=xtensa +deneyapkartv2.build.bootloader_addr=0x0 +deneyapkartv2.build.target=esp32s3 +deneyapkartv2.build.mcu=esp32s3 +deneyapkartv2.build.core=esp32 +deneyapkartv2.build.variant=deneyapkartv2 +deneyapkartv2.build.board=DYDKV2 + +deneyapkartv2.build.usb_mode=1 +deneyapkartv2.build.cdc_on_boot=1 +deneyapkartv2.build.msc_on_boot=0 +deneyapkartv2.build.dfu_on_boot=0 +deneyapkartv2.build.f_cpu=240000000L +deneyapkartv2.build.flash_size=4MB +deneyapkartv2.build.flash_freq=80m +deneyapkartv2.build.flash_mode=dio +deneyapkartv2.build.boot=qio +deneyapkartv2.build.boot_freq=80m +deneyapkartv2.build.partitions=default +deneyapkartv2.build.defines=-DBOARD_HAS_PSRAM +deneyapkartv2.build.loop_core= +deneyapkartv2.build.event_core= +deneyapkartv2.build.psram_type=opi +deneyapkartv2.build.memory_type={build.boot}_{build.psram_type} + +## IDE 2.0 Seems to not update the value +deneyapkartv2.menu.JTAGAdapter.default=Disabled +deneyapkartv2.menu.JTAGAdapter.default.build.copy_jtag_files=0 +deneyapkartv2.menu.JTAGAdapter.builtin=Integrated USB JTAG +deneyapkartv2.menu.JTAGAdapter.builtin.build.openocdscript=esp32s3-builtin.cfg +deneyapkartv2.menu.JTAGAdapter.builtin.build.copy_jtag_files=1 +deneyapkartv2.menu.JTAGAdapter.external=FTDI Adapter +deneyapkartv2.menu.JTAGAdapter.external.build.openocdscript=esp32s3-ftdi.cfg +deneyapkartv2.menu.JTAGAdapter.external.build.copy_jtag_files=1 +deneyapkartv2.menu.JTAGAdapter.bridge=ESP USB Bridge +deneyapkartv2.menu.JTAGAdapter.bridge.build.openocdscript=esp32s3-bridge.cfg +deneyapkartv2.menu.JTAGAdapter.bridge.build.copy_jtag_files=1 + +deneyapkartv2.menu.PSRAM.opi=OPI PSRAM +deneyapkartv2.menu.PSRAM.opi.build.defines=-DBOARD_HAS_PSRAM +deneyapkartv2.menu.PSRAM.opi.build.psram_type=opi +deneyapkartv2.menu.PSRAM.disabled=Disabled +deneyapkartv2.menu.PSRAM.disabled.build.defines= +deneyapkartv2.menu.PSRAM.disabled.build.psram_type=qspi + +deneyapkartv2.menu.FlashMode.qio=QIO 80MHz +deneyapkartv2.menu.FlashMode.qio.build.flash_mode=dio +deneyapkartv2.menu.FlashMode.qio.build.boot=qio +deneyapkartv2.menu.FlashMode.qio.build.boot_freq=80m +deneyapkartv2.menu.FlashMode.qio.build.flash_freq=80m +deneyapkartv2.menu.FlashMode.qio120=QIO 120MHz +deneyapkartv2.menu.FlashMode.qio120.build.flash_mode=dio +deneyapkartv2.menu.FlashMode.qio120.build.boot=qio +deneyapkartv2.menu.FlashMode.qio120.build.boot_freq=120m +deneyapkartv2.menu.FlashMode.qio120.build.flash_freq=80m +deneyapkartv2.menu.FlashMode.dio=DIO 80MHz +deneyapkartv2.menu.FlashMode.dio.build.flash_mode=dio +deneyapkartv2.menu.FlashMode.dio.build.boot=dio +deneyapkartv2.menu.FlashMode.dio.build.boot_freq=80m +deneyapkartv2.menu.FlashMode.dio.build.flash_freq=80m +deneyapkartv2.menu.FlashMode.opi=OPI 80MHz +deneyapkartv2.menu.FlashMode.opi.build.flash_mode=dout +deneyapkartv2.menu.FlashMode.opi.build.boot=opi +deneyapkartv2.menu.FlashMode.opi.build.boot_freq=80m +deneyapkartv2.menu.FlashMode.opi.build.flash_freq=80m + +deneyapkartv2.menu.FlashSize.4M=4MB (32Mb) +deneyapkartv2.menu.FlashSize.4M.build.flash_size=4MB + +deneyapkartv2.menu.LoopCore.1=Core 1 +deneyapkartv2.menu.LoopCore.1.build.loop_core=-DARDUINO_RUNNING_CORE=1 +deneyapkartv2.menu.LoopCore.0=Core 0 +deneyapkartv2.menu.LoopCore.0.build.loop_core=-DARDUINO_RUNNING_CORE=0 + +deneyapkartv2.menu.EventsCore.1=Core 1 +deneyapkartv2.menu.EventsCore.1.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=1 +deneyapkartv2.menu.EventsCore.0=Core 0 +deneyapkartv2.menu.EventsCore.0.build.event_core=-DARDUINO_EVENT_RUNNING_CORE=0 + +deneyapkartv2.menu.USBMode.hwcdc=Hardware CDC and JTAG +deneyapkartv2.menu.USBMode.hwcdc.build.usb_mode=1 +deneyapkartv2.menu.USBMode.default=USB-OTG (TinyUSB) +deneyapkartv2.menu.USBMode.default.build.usb_mode=0 + +deneyapkartv2.menu.CDCOnBoot.cdc=Enabled +deneyapkartv2.menu.CDCOnBoot.cdc.build.cdc_on_boot=1 +deneyapkartv2.menu.CDCOnBoot.default=Disabled +deneyapkartv2.menu.CDCOnBoot.default.build.cdc_on_boot=0 + +deneyapkartv2.menu.MSCOnBoot.default=Disabled +deneyapkartv2.menu.MSCOnBoot.default.build.msc_on_boot=0 +deneyapkartv2.menu.MSCOnBoot.msc=Enabled (Requires USB-OTG Mode) +deneyapkartv2.menu.MSCOnBoot.msc.build.msc_on_boot=1 + +deneyapkartv2.menu.DFUOnBoot.default=Disabled +deneyapkartv2.menu.DFUOnBoot.default.build.dfu_on_boot=0 +deneyapkartv2.menu.DFUOnBoot.dfu=Enabled (Requires USB-OTG Mode) +deneyapkartv2.menu.DFUOnBoot.dfu.build.dfu_on_boot=1 + +deneyapkartv2.menu.UploadMode.default=UART0 / Hardware CDC +deneyapkartv2.menu.UploadMode.default.upload.use_1200bps_touch=false +deneyapkartv2.menu.UploadMode.default.upload.wait_for_upload_port=false +deneyapkartv2.menu.UploadMode.cdc=USB-OTG CDC (TinyUSB) +deneyapkartv2.menu.UploadMode.cdc.upload.use_1200bps_touch=true +deneyapkartv2.menu.UploadMode.cdc.upload.wait_for_upload_port=true + +deneyapkartv2.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS) +deneyapkartv2.menu.PartitionScheme.default.build.partitions=default +deneyapkartv2.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS) +deneyapkartv2.menu.PartitionScheme.defaultffat.build.partitions=default_ffat +deneyapkartv2.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS) +deneyapkartv2.menu.PartitionScheme.minimal.build.partitions=minimal +deneyapkartv2.menu.PartitionScheme.no_fs=No FS 4MB (2MB APP x2) +deneyapkartv2.menu.PartitionScheme.no_fs.build.partitions=no_fs +deneyapkartv2.menu.PartitionScheme.no_fs.upload.maximum_size=2031616 +deneyapkartv2.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS) +deneyapkartv2.menu.PartitionScheme.no_ota.build.partitions=no_ota +deneyapkartv2.menu.PartitionScheme.no_ota.upload.maximum_size=2097152 +deneyapkartv2.menu.PartitionScheme.noota_3g=No OTA (1MB APP/3MB SPIFFS) +deneyapkartv2.menu.PartitionScheme.noota_3g.build.partitions=noota_3g +deneyapkartv2.menu.PartitionScheme.noota_3g.upload.maximum_size=1048576 +deneyapkartv2.menu.PartitionScheme.noota_ffat=No OTA (2MB APP/2MB FATFS) +deneyapkartv2.menu.PartitionScheme.noota_ffat.build.partitions=noota_ffat +deneyapkartv2.menu.PartitionScheme.noota_ffat.upload.maximum_size=2097152 +deneyapkartv2.menu.PartitionScheme.noota_3gffat=No OTA (1MB APP/3MB FATFS) +deneyapkartv2.menu.PartitionScheme.noota_3gffat.build.partitions=noota_3gffat +deneyapkartv2.menu.PartitionScheme.noota_3gffat.upload.maximum_size=1048576 +deneyapkartv2.menu.PartitionScheme.huge_app=Huge APP (3MB No OTA/1MB SPIFFS) +deneyapkartv2.menu.PartitionScheme.huge_app.build.partitions=huge_app +deneyapkartv2.menu.PartitionScheme.huge_app.upload.maximum_size=3145728 +deneyapkartv2.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS) +deneyapkartv2.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs +deneyapkartv2.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080 +deneyapkartv2.menu.PartitionScheme.rainmaker=RainMaker 4MB +deneyapkartv2.menu.PartitionScheme.rainmaker.build.partitions=rainmaker +deneyapkartv2.menu.PartitionScheme.rainmaker.upload.maximum_size=1966080 +deneyapkartv2.menu.PartitionScheme.rainmaker_4MB=RainMaker 4MB No OTA +deneyapkartv2.menu.PartitionScheme.rainmaker_4MB.build.partitions=rainmaker_4MB_no_ota +deneyapkartv2.menu.PartitionScheme.rainmaker_4MB.upload.maximum_size=4038656 +deneyapkartv2.menu.PartitionScheme.zigbee_zczr=Zigbee ZCZR 4MB with spiffs +deneyapkartv2.menu.PartitionScheme.zigbee_zczr.build.partitions=zigbee_zczr +deneyapkartv2.menu.PartitionScheme.zigbee_zczr.upload.maximum_size=1310720 +deneyapkartv2.menu.PartitionScheme.custom=Custom +deneyapkartv2.menu.PartitionScheme.custom.build.partitions= +deneyapkartv2.menu.PartitionScheme.custom.upload.maximum_size=16777216 + +deneyapkartv2.menu.CPUFreq.240=240MHz (WiFi) +deneyapkartv2.menu.CPUFreq.240.build.f_cpu=240000000L +deneyapkartv2.menu.CPUFreq.160=160MHz (WiFi) +deneyapkartv2.menu.CPUFreq.160.build.f_cpu=160000000L +deneyapkartv2.menu.CPUFreq.80=80MHz (WiFi) +deneyapkartv2.menu.CPUFreq.80.build.f_cpu=80000000L +deneyapkartv2.menu.CPUFreq.40=40MHz +deneyapkartv2.menu.CPUFreq.40.build.f_cpu=40000000L +deneyapkartv2.menu.CPUFreq.20=20MHz +deneyapkartv2.menu.CPUFreq.20.build.f_cpu=20000000L +deneyapkartv2.menu.CPUFreq.10=10MHz +deneyapkartv2.menu.CPUFreq.10.build.f_cpu=10000000L + +deneyapkartv2.menu.UploadSpeed.921600=921600 +deneyapkartv2.menu.UploadSpeed.921600.upload.speed=921600 +deneyapkartv2.menu.UploadSpeed.115200=115200 +deneyapkartv2.menu.UploadSpeed.115200.upload.speed=115200 +deneyapkartv2.menu.UploadSpeed.256000.windows=256000 +deneyapkartv2.menu.UploadSpeed.256000.upload.speed=256000 +deneyapkartv2.menu.UploadSpeed.230400.windows.upload.speed=256000 +deneyapkartv2.menu.UploadSpeed.230400=230400 +deneyapkartv2.menu.UploadSpeed.230400.upload.speed=230400 +deneyapkartv2.menu.UploadSpeed.460800.linux=460800 +deneyapkartv2.menu.UploadSpeed.460800.macosx=460800 +deneyapkartv2.menu.UploadSpeed.460800.upload.speed=460800 +deneyapkartv2.menu.UploadSpeed.512000.windows=512000 +deneyapkartv2.menu.UploadSpeed.512000.upload.speed=512000 + +deneyapkartv2.menu.DebugLevel.none=None +deneyapkartv2.menu.DebugLevel.none.build.code_debug=0 +deneyapkartv2.menu.DebugLevel.error=Error +deneyapkartv2.menu.DebugLevel.error.build.code_debug=1 +deneyapkartv2.menu.DebugLevel.warn=Warn +deneyapkartv2.menu.DebugLevel.warn.build.code_debug=2 +deneyapkartv2.menu.DebugLevel.info=Info +deneyapkartv2.menu.DebugLevel.info.build.code_debug=3 +deneyapkartv2.menu.DebugLevel.debug=Debug +deneyapkartv2.menu.DebugLevel.debug.build.code_debug=4 +deneyapkartv2.menu.DebugLevel.verbose=Verbose +deneyapkartv2.menu.DebugLevel.verbose.build.code_debug=5 + +deneyapkartv2.menu.EraseFlash.none=Disabled +deneyapkartv2.menu.EraseFlash.none.upload.erase_cmd= +deneyapkartv2.menu.EraseFlash.all=Enabled +deneyapkartv2.menu.EraseFlash.all.upload.erase_cmd=-e + +deneyapkartv2.menu.ZigbeeMode.default=Disabled +deneyapkartv2.menu.ZigbeeMode.default.build.zigbee_mode= +deneyapkartv2.menu.ZigbeeMode.default.build.zigbee_libs= +deneyapkartv2.menu.ZigbeeMode.zczr=Zigbee ZCZR (coordinator/router) +deneyapkartv2.menu.ZigbeeMode.zczr.build.zigbee_mode=-DZIGBEE_MODE_ZCZR +deneyapkartv2.menu.ZigbeeMode.zczr.build.zigbee_libs=-lesp_zb_api.zczr -lzboss_stack.zczr -lzboss_port.remote + +############################################################## + deneyapkart1A.name=Deneyap Kart 1A deneyapkart1A.bootloader.tool=esptool_py diff --git a/variants/deneyapkartv2/pins_arduino.h b/variants/deneyapkartv2/pins_arduino.h new file mode 100644 index 00000000000..f7eccadb13c --- /dev/null +++ b/variants/deneyapkartv2/pins_arduino.h @@ -0,0 +1,123 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include "soc/soc_caps.h" + +#define USB_VID 0x303A +#define USB_PID 0x82EB +#define USB_MANUFACTURER "Turkish Technology Team Foundation (T3)" +#define USB_PRODUCT "DENEYAP KART v2" +#define USB_SERIAL "" // Empty string for MAC address + +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + 46; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN +#define RGB_BUILTIN LED_BUILTIN +#define RGBLED LED_BUILTIN +#define RGB_BRIGHTNESS 64 + +static const uint8_t GPKEY = 0; +#define KEY_BUILTIN GPKEY +#define BUILTIN_KEY GPKEY + +static const uint8_t TX = 43; +static const uint8_t RX = 44; +#define TX1 TX +#define RX1 RX + +static const uint8_t SDA = 47; +static const uint8_t SCL = 21; + +static const uint8_t SS = 42; +static const uint8_t MOSI = 39; +static const uint8_t MISO = 40; +static const uint8_t SCK = 41; + +static const uint8_t A0 = 4; +static const uint8_t A1 = 5; +static const uint8_t A2 = 6; +static const uint8_t A3 = 7; +static const uint8_t A4 = 15; +static const uint8_t A5 = 16; +static const uint8_t A6 = 17; +static const uint8_t A7 = 18; +static const uint8_t A8 = 8; +static const uint8_t A9 = 9; +static const uint8_t A10 = 10; +static const uint8_t A11 = 11; +static const uint8_t A12 = 2; +static const uint8_t A13 = 1; +static const uint8_t A14 = 3; +static const uint8_t A15 = 12; +static const uint8_t A16 = 13; +static const uint8_t A17 = 14; + +static const uint8_t T0 = 4; +static const uint8_t T1 = 5; +static const uint8_t T2 = 6; +static const uint8_t T3 = 7; +static const uint8_t T4 = 8; +static const uint8_t T5 = 9; +static const uint8_t T6 = 10; +static const uint8_t T7 = 11; +static const uint8_t T8 = 2; +static const uint8_t T9 = 1; +static const uint8_t T10 = 3; +static const uint8_t T11 = 12; +static const uint8_t T12 = 13; +static const uint8_t T13 = 14; + +static const uint8_t D0 = 1; +static const uint8_t D1 = 2; +static const uint8_t D2 = 43; +static const uint8_t D3 = 44; +static const uint8_t D4 = 42; +static const uint8_t D5 = 41; +static const uint8_t D6 = 40; +static const uint8_t D7 = 39; +static const uint8_t D8 = 38; +static const uint8_t D9 = 48; +static const uint8_t D10 = 47; +static const uint8_t D11 = 21; +static const uint8_t D12 = 11; +static const uint8_t D13 = 10; +static const uint8_t D14 = 9; +static const uint8_t D15 = 8; +static const uint8_t D16 = 18; +static const uint8_t D17 = 17; +static const uint8_t D18 = 16; +static const uint8_t D19 = 15; +static const uint8_t D20 = 7; +static const uint8_t D21 = 6; +static const uint8_t D22 = 5; +static const uint8_t D23 = 4; +static const uint8_t D24 = 46; +static const uint8_t D25 = 0; +static const uint8_t D26 = 3; +static const uint8_t D27 = 12; +static const uint8_t D28 = 13; +static const uint8_t D29 = 14; + +static const uint8_t CAMSD = 4; +static const uint8_t CAMSC = 5; +static const uint8_t CAMD2 = 41; +static const uint8_t CAMD3 = 2; +static const uint8_t CAMD4 = 1; +static const uint8_t CAMD5 = 42; +static const uint8_t CAMD6 = 40; +static const uint8_t CAMD7 = 38; +static const uint8_t CAMD8 = 17; +static const uint8_t CAMD9 = 15; +static const uint8_t CAMPC = 39; +static const uint8_t CAMXC = 16; +static const uint8_t CAMH = 7; +static const uint8_t CAMV = 6; + +static const uint8_t SDCM = 12; +static const uint8_t SDCK = 13; +static const uint8_t SDDA = 14; + +static const uint8_t BAT = 3; + +#endif /* Pins_Arduino_h */ From aab542d658819c6ce20035558e88911e131c8aef Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Thu, 3 Jul 2025 23:14:53 +0300 Subject: [PATCH 062/102] Update Issue-report.yml to add v 3.2.1 --- .github/ISSUE_TEMPLATE/Issue-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/Issue-report.yml b/.github/ISSUE_TEMPLATE/Issue-report.yml index 9dba5e0ca8f..6dc1b0de171 100644 --- a/.github/ISSUE_TEMPLATE/Issue-report.yml +++ b/.github/ISSUE_TEMPLATE/Issue-report.yml @@ -43,6 +43,7 @@ body: - latest stable Release (if not listed below) - latest development Release Candidate (RC-X) - latest master (checkout manually) + - v3.2.1 - v3.2.0 - v3.1.3 - v3.1.2 From 1426927c837dfa0c10835eb3c769f7000db64c26 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Fri, 4 Jul 2025 18:37:56 +0300 Subject: [PATCH 063/102] fix(matter): Fix MatterSmartButon.ino when CHIPOBLE is on --- .../Matter/examples/MatterSmartButon/MatterSmartButon.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino b/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino index 29caf00004c..f4d978175cc 100644 --- a/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino +++ b/libraries/Matter/examples/MatterSmartButon/MatterSmartButon.ino @@ -45,12 +45,12 @@ void setup() { Serial.begin(115200); +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE // We start by connecting to a WiFi network Serial.print("Connecting to "); Serial.println(ssid); -// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network -#if !CONFIG_ENABLE_CHIPOBLE // Manually connect to WiFi WiFi.begin(ssid, password); // Wait for connection From d3c5a82eed1fb30403186c738dbaa6f45cac93bc Mon Sep 17 00:00:00 2001 From: "Daniel.Cao" <144674500+DanielCao0@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:56:44 +0800 Subject: [PATCH 064/102] fix(board): Update PSRAM configuration for RAK3112 to fix PSRAM error (#11552) * fix(board): Update PSRAM configuration for RAK3112 to fix PSRAM error * feat(board): RAK3112 add WisBlock module pin definitions to pins_arduino.h * fix(board): Update RAK3112 flash mode and boot settings for improved performance * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: Daniel.Cao Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- boards.txt | 6 ++++-- variants/rakwireless_rak3112/pins_arduino.h | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/boards.txt b/boards.txt index e47995985ed..ac2b1a336e4 100644 --- a/boards.txt +++ b/boards.txt @@ -50805,10 +50805,12 @@ rakwireless_rak3112.build.dfu_on_boot=0 rakwireless_rak3112.build.f_cpu=240000000L rakwireless_rak3112.build.flash_size=16MB rakwireless_rak3112.build.flash_freq=80m -rakwireless_rak3112.build.flash_mode=dio -rakwireless_rak3112.build.boot=dio +rakwireless_rak3112.build.flash_mode=qio +rakwireless_rak3112.build.boot=qio rakwireless_rak3112.build.partitions=default rakwireless_rak3112.build.defines= +rakwireless_rak3112.build.psram_type=opi +rakwireless_rak3112.build.memory_type={build.boot}_{build.psram_type} rakwireless_rak3112.menu.PSRAM.enabled=Enabled rakwireless_rak3112.menu.PSRAM.enabled.build.defines=-DBOARD_HAS_PSRAM diff --git a/variants/rakwireless_rak3112/pins_arduino.h b/variants/rakwireless_rak3112/pins_arduino.h index 5d1e451494a..f1bcc7a6120 100644 --- a/variants/rakwireless_rak3112/pins_arduino.h +++ b/variants/rakwireless_rak3112/pins_arduino.h @@ -47,4 +47,17 @@ static const uint8_t SCK = 13; #define LORA_BUSY 48 #define LORA_IRQ LORA_DIO1 +// For WisBlock modules, see: https://docs.rakwireless.com/Product-Categories/WisBlock/ +#define WB_IO1 21 +#define WB_IO2 14 +#define WB_IO3 41 +#define WB_IO4 42 +#define WB_IO5 38 +#define WB_IO6 39 +#define WB_A0 1 +#define WB_A1 2 +#define WB_CS 12 +#define WB_LED1 46 +#define WB_LED2 45 + #endif /* Pins_Arduino_h */ From b709a782839e4bfb143df5be9a108ff4414a565c Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:01:50 +0200 Subject: [PATCH 065/102] fix deprecated warnings caaused from esptool v5.0.0 (#11556) --- tools/pioarduino-build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pioarduino-build.py b/tools/pioarduino-build.py index b67580e264c..4d4161dd9ca 100644 --- a/tools/pioarduino-build.py +++ b/tools/pioarduino-build.py @@ -95,7 +95,7 @@ def generate_bootloader_image(bootloader_elf): env.VerboseAction( " ".join( [ - '"$PYTHONEXE" "$OBJCOPY"', + "$OBJCOPY", "--chip", build_mcu, "elf2image", From e2c7578fa87e9f20b0a86090b23d89ae22a0146f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:08:56 +0200 Subject: [PATCH 066/102] feat(zigbee): Add Fan Control endpoint support (#11559) * feat(zigbee): Add Fan Control endpoint support * fix(zigbee): Update logs and change device_id * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CMakeLists.txt | 1 + .../examples/Zigbee_Fan_Control/README.md | 83 +++++++++++ .../Zigbee_Fan_Control/Zigbee_Fan_Control.ino | 129 ++++++++++++++++++ .../examples/Zigbee_Fan_Control/ci.json | 6 + libraries/Zigbee/keywords.txt | 60 ++++++-- libraries/Zigbee/src/Zigbee.h | 1 + libraries/Zigbee/src/ep/ZigbeeFanControl.cpp | 60 ++++++++ libraries/Zigbee/src/ep/ZigbeeFanControl.h | 65 +++++++++ 8 files changed, 393 insertions(+), 12 deletions(-) create mode 100644 libraries/Zigbee/examples/Zigbee_Fan_Control/README.md create mode 100644 libraries/Zigbee/examples/Zigbee_Fan_Control/Zigbee_Fan_Control.ino create mode 100644 libraries/Zigbee/examples/Zigbee_Fan_Control/ci.json create mode 100644 libraries/Zigbee/src/ep/ZigbeeFanControl.cpp create mode 100644 libraries/Zigbee/src/ep/ZigbeeFanControl.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e8f44ac5ee0..f21183ee11e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -305,6 +305,7 @@ set(ARDUINO_LIBRARY_Zigbee_SRCS libraries/Zigbee/src/ep/ZigbeeElectricalMeasurement.cpp libraries/Zigbee/src/ep/ZigbeeBinary.cpp libraries/Zigbee/src/ep/ZigbeePowerOutlet.cpp + libraries/Zigbee/src/ep/ZigbeeFanControl.cpp ) set(ARDUINO_LIBRARY_BLE_SRCS diff --git a/libraries/Zigbee/examples/Zigbee_Fan_Control/README.md b/libraries/Zigbee/examples/Zigbee_Fan_Control/README.md new file mode 100644 index 00000000000..91700b669a0 --- /dev/null +++ b/libraries/Zigbee/examples/Zigbee_Fan_Control/README.md @@ -0,0 +1,83 @@ +# Arduino-ESP32 Zigbee Fan Control Example + +This example demonstrates how to use the Zigbee library to create a router device fan control and use it as a Home Automation (HA) fan control device. + +# Supported Targets + +Currently, this example supports the following targets. + +| Supported Targets | ESP32-C6 | ESP32-H2 | +| ----------------- | -------- | -------- | + +## Fan Control Functions + +1. Initialize a Zigbee fan control device. +2. Control fan modes (OFF, LOW, MEDIUM, HIGH, ON). +3. Respond to fan control commands from the Zigbee network. + +## Hardware Required + +* ESP32-H2 or ESP32-C6 development board +* A USB cable for power supply and programming +* RGB LED for visual feedback (built-in on most development boards) + +### Configure the Project + +In this example the RGB LED is used to indicate the current fan control mode. +The LED colors represent different fan modes: +- OFF: No light +- LOW: Blue +- MEDIUM: Yellow +- HIGH: Red +- ON: White + +Set the button GPIO by changing the `button` variable. By default, it's the pin `BOOT_PIN` (BOOT button on ESP32-C6 and ESP32-H2). + +#### Using Arduino IDE + +To get more information about the Espressif boards see [Espressif Development Kits](https://www.espressif.com/en/products/devkits). + +* Before Compile/Verify, select the correct board: `Tools -> Board`. +* Select the End device Zigbee mode: `Tools -> Zigbee mode: Zigbee ZCZR (coordinator/router)` +* Select Partition Scheme for Zigbee: `Tools -> Partition Scheme: Zigbee ZCZR 4MB with spiffs` +* Select the COM port: `Tools -> Port: xxx` where the `xxx` is the detected COM port. +* Optional: Set debug level to verbose to see all logs from Zigbee stack: `Tools -> Core Debug Level: Verbose`. + +## Troubleshooting + +If the End device flashed with this example is not connecting to the coordinator, erase the flash of the End device before flashing the example to the board. It is recommended to do this if you re-flash the coordinator. +You can do the following: + +* In the Arduino IDE go to the Tools menu and set `Erase All Flash Before Sketch Upload` to `Enabled`. +* Add to the sketch `Zigbee.factoryReset();` to reset the device and Zigbee stack. + +By default, the coordinator network is closed after rebooting or flashing new firmware. +To open the network you have 2 options: + +* Open network after reboot by setting `Zigbee.setRebootOpenNetwork(time);` before calling `Zigbee.begin();`. +* In application you can anytime call `Zigbee.openNetwork(time);` to open the network for devices to join. + + +***Important: Make sure you are using a good quality USB cable and that you have a reliable power source*** + +* **LED not blinking:** Check the wiring connection and the IO selection. +* **Programming Fail:** If the programming/flash procedure fails, try reducing the serial connection speed. +* **COM port not detected:** Check the USB cable and the USB to Serial driver installation. + +If the error persists, you can ask for help at the official [ESP32 forum](https://esp32.com) or see [Contribute](#contribute). + +## Contribute + +To know how to contribute to this project, see [How to contribute.](https://github.com/espressif/arduino-esp32/blob/master/CONTRIBUTING.rst) + +If you have any **feedback** or **issue** to report on this example/library, please open an issue or fix it by creating a new PR. Contributions are more than welcome! + +Before creating a new issue, be sure to try Troubleshooting and check if the same issue was already created by someone else. + +## Resources + +* Official ESP32 Forum: [Link](https://esp32.com) +* Arduino-ESP32 Official Repository: [espressif/arduino-esp32](https://github.com/espressif/arduino-esp32) +* ESP32-C6 Datasheet: [Link to datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c6_datasheet_en.pdf) +* ESP32-H2 Datasheet: [Link to datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-h2_datasheet_en.pdf) +* Official ESP-IDF documentation: [ESP-IDF](https://idf.espressif.com) diff --git a/libraries/Zigbee/examples/Zigbee_Fan_Control/Zigbee_Fan_Control.ino b/libraries/Zigbee/examples/Zigbee_Fan_Control/Zigbee_Fan_Control.ino new file mode 100644 index 00000000000..4c0d15aa563 --- /dev/null +++ b/libraries/Zigbee/examples/Zigbee_Fan_Control/Zigbee_Fan_Control.ino @@ -0,0 +1,129 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @brief This example demonstrates simple Zigbee fan control. + * + * The example demonstrates how to use Zigbee library to create a router device fan control. + * The fan control is a Zigbee router device, which is controlled by a Zigbee coordinator. + * + * Proper Zigbee mode must be selected in Tools->Zigbee mode + * and also the correct partition scheme must be selected in Tools->Partition Scheme. + * + * Please check the README.md for instructions and more detailed description. + * + * Created by Jan Procházka (https://github.com/P-R-O-C-H-Y/) + */ + +#ifndef ZIGBEE_MODE_ZCZR +#error "Zigbee coordinator mode is not selected in Tools->Zigbee mode" +#endif + +#include "Zigbee.h" + +/* Zigbee light bulb configuration */ +#define ZIGBEE_FAN_CONTROL_ENDPOINT 1 + +#ifdef RGB_BUILTIN +uint8_t led = RGB_BUILTIN; // To demonstrate the current fan control mode +#else +uint8_t led = 2; +#endif + +uint8_t button = BOOT_PIN; + +ZigbeeFanControl zbFanControl = ZigbeeFanControl(ZIGBEE_FAN_CONTROL_ENDPOINT); + +/********************* fan control callback function **************************/ +void setFan(ZigbeeFanMode mode) { + switch (mode) { + case FAN_MODE_OFF: + rgbLedWrite(led, 0, 0, 0); // Off + Serial.println("Fan mode: OFF"); + break; + case FAN_MODE_LOW: + rgbLedWrite(led, 0, 0, 255); // Blue + Serial.println("Fan mode: LOW"); + break; + case FAN_MODE_MEDIUM: + rgbLedWrite(led, 255, 255, 0); // Yellow + Serial.println("Fan mode: MEDIUM"); + break; + case FAN_MODE_HIGH: + rgbLedWrite(led, 255, 0, 0); // Red + Serial.println("Fan mode: HIGH"); + break; + case FAN_MODE_ON: + rgbLedWrite(led, 255, 255, 255); // White + Serial.println("Fan mode: ON"); + break; + default: log_e("Unhandled fan mode: %d", mode); break; + } +} + +/********************* Arduino functions **************************/ +void setup() { + Serial.begin(115200); + + // Init LED that will be used to indicate the current fan control mode + rgbLedWrite(led, 0, 0, 0); + + // Init button for factory reset + pinMode(button, INPUT_PULLUP); + + //Optional: set Zigbee device name and model + zbFanControl.setManufacturerAndModel("Espressif", "ZBFanControl"); + + // Set the fan mode sequence to LOW_MED_HIGH + zbFanControl.setFanModeSequence(FAN_MODE_SEQUENCE_LOW_MED_HIGH); + + // Set callback function for fan mode change + zbFanControl.onFanModeChange(setFan); + + //Add endpoint to Zigbee Core + Serial.println("Adding ZigbeeFanControl endpoint to Zigbee Core"); + Zigbee.addEndpoint(&zbFanControl); + + // When all EPs are registered, start Zigbee in ROUTER mode + if (!Zigbee.begin(ZIGBEE_ROUTER)) { + Serial.println("Zigbee failed to start!"); + Serial.println("Rebooting..."); + ESP.restart(); + } + Serial.println("Connecting to network"); + while (!Zigbee.connected()) { + Serial.print("."); + delay(100); + } + Serial.println(); +} + +void loop() { + // Checking button for factory reset + if (digitalRead(button) == LOW) { // Push button pressed + // Key debounce handling + delay(100); + int startTime = millis(); + while (digitalRead(button) == LOW) { + delay(50); + if ((millis() - startTime) > 3000) { + // If key pressed for more than 3secs, factory reset Zigbee and reboot + Serial.println("Resetting Zigbee to factory and rebooting in 1s."); + delay(1000); + Zigbee.factoryReset(); + } + } + } + delay(100); +} diff --git a/libraries/Zigbee/examples/Zigbee_Fan_Control/ci.json b/libraries/Zigbee/examples/Zigbee_Fan_Control/ci.json new file mode 100644 index 00000000000..15d6190e4ae --- /dev/null +++ b/libraries/Zigbee/examples/Zigbee_Fan_Control/ci.json @@ -0,0 +1,6 @@ +{ + "fqbn_append": "PartitionScheme=zigbee_zczr,ZigbeeMode=zczr", + "requires": [ + "CONFIG_ZB_ENABLED=y" + ] +} diff --git a/libraries/Zigbee/keywords.txt b/libraries/Zigbee/keywords.txt index 556b6408ea2..23f3af3bf02 100644 --- a/libraries/Zigbee/keywords.txt +++ b/libraries/Zigbee/keywords.txt @@ -12,25 +12,31 @@ Zigbee KEYWORD1 ZigbeeEP KEYWORD1 # Endpoint Classes -ZigbeeLight KEYWORD1 -ZigbeeSwitch KEYWORD1 -ZigbeeColorDimmableLight KEYWORD1 -ZigbeeColorDimmerSwitch KEYWORD1 -ZigbeeTempSensor KEYWORD1 -ZigbeeThermostat KEYWORD1 -ZigbeeFlowSensor KEYWORD1 -ZigbeePressureSensor KEYWORD1 -ZigbeeOccupancySensor KEYWORD1 ZigbeeAnalog KEYWORD1 +ZigbeeBinary KEYWORD1 ZigbeeCarbonDioxideSensor KEYWORD1 +ZigbeeColorDimmableLight KEYWORD1 +ZigbeeColorDimmerSwitch KEYWORD1 ZigbeeContactSwitch KEYWORD1 +ZigbeeDimableLight KEYWORD1 ZigbeeDoorWindowHandle KEYWORD1 +ZigbeeElectricalMeasurement KEYWORD1 +ZigbeeFanControl KEYWORD1 +ZigbeeFlowSensor KEYWORD1 ZigbeeGateway KEYWORD1 +ZigbeeIlluminanceSensor KEYWORD1 +ZigbeeLight KEYWORD1 +ZigbeeOccupancySensor KEYWORD1 +ZigbeePM25Sensor KEYWORD1 +ZigbeePowerOutlet KEYWORD1 +ZigbeePressureSensor KEYWORD1 ZigbeeRangeExtender KEYWORD1 +ZigbeeSwitch KEYWORD1 +ZigbeeTempSensor KEYWORD1 +ZigbeeThermostat KEYWORD1 ZigbeeVibrationSensor KEYWORD1 ZigbeeWindowCovering KEYWORD1 -ZigbeeIlluminanceSensor KEYWORD1 -ZigbeePowerOutlet KEYWORD1 +ZigbeeWindSpeedSensor KEYWORD1 # Other zigbee_role_t KEYWORD1 @@ -39,6 +45,8 @@ zb_device_params_t KEYWORD1 zigbee_scan_result_t KEYWORD1 zb_power_source_t KEYWORD1 ZigbeeWindowCoveringType KEYWORD1 +ZigbeeFanMode KEYWORD1 +ZigbeeFanModeSequence KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -73,6 +81,7 @@ printBoundDevices KEYWORD2 getBoundDevices KEYWORD2 bound KEYWORD2 allowMultipleBinding KEYWORD2 +setManualBinding KEYWORD2 setManufacturerAndModel KEYWORD2 setPowerSource KEYWORD2 setBatteryPercentage KEYWORD2 @@ -80,6 +89,13 @@ reportBatteryPercentage KEYWORD2 readManufacturer KEYWORD2 readModel KEYWORD2 onIdentify KEYWORD2 +addTimeCluster KEYWORD2 +setTime KEYWORD2 +setTimezone KEYWORD2 +getTime KEYWORD2 +getTimezone KEYWORD2 +addOTAClient KEYWORD2 +clearBoundDevices KEYWORD2 # ZigbeeLight + ZigbeeColorDimmableLight onLightChange KEYWORD2 @@ -171,7 +187,7 @@ setTilted KEYWORD2 # ZigbeeVibrationSensor setVibration KEYWORD2 -ZigbeeWindowCovering +# ZigbeeWindowCovering onOpen KEYWORD2 onClose KEYWORD2 onGoToLiftPercentage KEYWORD2 @@ -186,6 +202,26 @@ setConfigStatus KEYWORD2 setMode KEYWORD2 setLimits KEYWORD2 +# ZigbeeBinary +addBinaryInput KEYWORD2 +addBinaryOutput KEYWORD2 +onBinaryOutputChange KEYWORD2 +setBinaryInput KEYWORD2 +setBinaryOutput KEYWORD2 +getBinaryOutput KEYWORD2 +reportBinaryInput KEYWORD2 +reportBinaryOutput KEYWORD2 +setBinaryInputApplication KEYWORD2 +setBinaryInputDescription KEYWORD2 +setBinaryOutputApplication KEYWORD2 +setBinaryOutputDescription KEYWORD2 + +# ZigbeeFanControl +setFanModeSequence KEYWORD2 +getFanMode KEYWORD2 +getFanModeSequence KEYWORD2 +onFanModeChange KEYWORD2 + ####################################### # Constants (LITERAL1) ####################################### diff --git a/libraries/Zigbee/src/Zigbee.h b/libraries/Zigbee/src/Zigbee.h index b2e2e5dd027..65c9e7f0daa 100644 --- a/libraries/Zigbee/src/Zigbee.h +++ b/libraries/Zigbee/src/Zigbee.h @@ -16,6 +16,7 @@ #include "ep/ZigbeeLight.h" //// Controllers #include "ep/ZigbeeThermostat.h" +#include "ep/ZigbeeFanControl.h" ////Outlets #include "ep/ZigbeePowerOutlet.h" //// Sensors diff --git a/libraries/Zigbee/src/ep/ZigbeeFanControl.cpp b/libraries/Zigbee/src/ep/ZigbeeFanControl.cpp new file mode 100644 index 00000000000..f4b32ce1200 --- /dev/null +++ b/libraries/Zigbee/src/ep/ZigbeeFanControl.cpp @@ -0,0 +1,60 @@ +#include "ZigbeeFanControl.h" +#if CONFIG_ZB_ENABLED + +ZigbeeFanControl::ZigbeeFanControl(uint8_t endpoint) : ZigbeeEP(endpoint) { + _device_id = ESP_ZB_HA_THERMOSTAT_DEVICE_ID; //There is no FAN_CONTROL_DEVICE_ID in the Zigbee spec + + //Create basic analog sensor clusters without configuration + _cluster_list = esp_zb_zcl_cluster_list_create(); + esp_zb_cluster_list_add_basic_cluster(_cluster_list, esp_zb_basic_cluster_create(NULL), ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_zb_cluster_list_add_identify_cluster(_cluster_list, esp_zb_identify_cluster_create(NULL), ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_zb_cluster_list_add_fan_control_cluster(_cluster_list, esp_zb_fan_control_cluster_create(NULL), ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + + _ep_config = { + .endpoint = _endpoint, .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, .app_device_id = ESP_ZB_HA_HEATING_COOLING_UNIT_DEVICE_ID, .app_device_version = 0 + }; +} + +bool ZigbeeFanControl::setFanModeSequence(ZigbeeFanModeSequence sequence) { + esp_zb_attribute_list_t *fan_control_cluster = + esp_zb_cluster_list_get_cluster(_cluster_list, ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + esp_err_t ret = esp_zb_cluster_update_attr(fan_control_cluster, ESP_ZB_ZCL_ATTR_FAN_CONTROL_FAN_MODE_SEQUENCE_ID, (void *)&sequence); + if (ret != ESP_OK) { + log_e("Failed to set min value: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + _current_fan_mode_sequence = sequence; + _current_fan_mode = FAN_MODE_OFF; + // Set initial fan mode to OFF + ret = esp_zb_cluster_update_attr(fan_control_cluster, ESP_ZB_ZCL_ATTR_FAN_CONTROL_FAN_MODE_ID, (void *)&_current_fan_mode); + if (ret != ESP_OK) { + log_e("Failed to set fan mode: 0x%x: %s", ret, esp_err_to_name(ret)); + return false; + } + return true; +} + +//set attribute method -> method overridden in child class +void ZigbeeFanControl::zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) { + //check the data and call right method + if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL) { + if (message->attribute.id == ESP_ZB_ZCL_ATTR_FAN_CONTROL_FAN_MODE_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_8BIT_ENUM) { + _current_fan_mode = *(ZigbeeFanMode *)message->attribute.data.value; + fanModeChanged(); + } else { + log_w("Received message ignored. Attribute ID: %d not supported for Fan Control", message->attribute.id); + } + } else { + log_w("Received message ignored. Cluster ID: %d not supported for Fan Control", message->info.cluster); + } +} + +void ZigbeeFanControl::fanModeChanged() { + if (_on_fan_mode_change) { + _on_fan_mode_change(_current_fan_mode); + } else { + log_w("No callback function set for fan mode change"); + } +} + +#endif // CONFIG_ZB_ENABLED diff --git a/libraries/Zigbee/src/ep/ZigbeeFanControl.h b/libraries/Zigbee/src/ep/ZigbeeFanControl.h new file mode 100644 index 00000000000..25b5862c5c4 --- /dev/null +++ b/libraries/Zigbee/src/ep/ZigbeeFanControl.h @@ -0,0 +1,65 @@ +/* Class of Zigbee Pressure sensor endpoint inherited from common EP class */ + +#pragma once + +#include "soc/soc_caps.h" +#include "sdkconfig.h" +#if CONFIG_ZB_ENABLED + +#include "ZigbeeEP.h" +#include "ha/esp_zigbee_ha_standard.h" + +// Custom Arduino-friendly enums for fan mode values +enum ZigbeeFanMode { + FAN_MODE_OFF = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_OFF, + FAN_MODE_LOW = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_LOW, + FAN_MODE_MEDIUM = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_MEDIUM, + FAN_MODE_HIGH = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_HIGH, + FAN_MODE_ON = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_ON, + FAN_MODE_AUTO = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_AUTO, + FAN_MODE_SMART = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_SMART, +}; + +// Custom Arduino-friendly enums for fan mode sequence +enum ZigbeeFanModeSequence { + FAN_MODE_SEQUENCE_LOW_MED_HIGH = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_SEQUENCE_LOW_MED_HIGH, + FAN_MODE_SEQUENCE_LOW_HIGH = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_SEQUENCE_LOW_HIGH, + FAN_MODE_SEQUENCE_LOW_MED_HIGH_AUTO = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_SEQUENCE_LOW_MED_HIGH_AUTO, + FAN_MODE_SEQUENCE_LOW_HIGH_AUTO = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_SEQUENCE_LOW_HIGH_AUTO, + FAN_MODE_SEQUENCE_ON_AUTO = ESP_ZB_ZCL_FAN_CONTROL_FAN_MODE_SEQUENCE_ON_AUTO, +}; + +class ZigbeeFanControl : public ZigbeeEP { +public: + ZigbeeFanControl(uint8_t endpoint); + ~ZigbeeFanControl() {} + + // Set the fan mode sequence value + bool setFanModeSequence(ZigbeeFanModeSequence sequence); + + // Use to get fan mode + ZigbeeFanMode getFanMode() { + return _current_fan_mode; + } + + // Use to get fan mode sequence + ZigbeeFanModeSequence getFanModeSequence() { + return _current_fan_mode_sequence; + } + + // On fan mode change callback + void onFanModeChange(void (*callback)(ZigbeeFanMode mode)) { + _on_fan_mode_change = callback; + } + +private: + void zbAttributeSet(const esp_zb_zcl_set_attr_value_message_t *message) override; + //callback function to be called on fan mode change + void (*_on_fan_mode_change)(ZigbeeFanMode mode); + void fanModeChanged(); + + ZigbeeFanMode _current_fan_mode; + ZigbeeFanModeSequence _current_fan_mode_sequence; +}; + +#endif // CONFIG_ZB_ENABLED From 040e0ca42a04e644e5bffee62e2ce211aa3e02be Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Tue, 8 Jul 2025 07:09:59 -0300 Subject: [PATCH 067/102] fix(dangerjs): Disable target branch rule (#11565) --- .github/workflows/dangerjs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dangerjs.yml b/.github/workflows/dangerjs.yml index 13bc907566b..bba96bfedee 100644 --- a/.github/workflows/dangerjs.yml +++ b/.github/workflows/dangerjs.yml @@ -24,4 +24,5 @@ jobs: instructions-cla-link: "https://cla-assistant.io/espressif/arduino-esp32" instructions-contributions-file: "docs/en/contributing.rst" rule-max-commits: "false" + rule-target-branch: "false" commit-messages-min-summary-length: "10" From 241e2576be918218e10a615de851f61b8ebd5936 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 8 Jul 2025 16:00:51 +0300 Subject: [PATCH 068/102] fix(async): Update IP setup in AsyncUDP (#11569) * fix(async): Update IP setup in AsyncUDP * fix(udp): Revert to IP_SET_TYPE_VAL in connect --- libraries/AsyncUDP/src/AsyncUDP.cpp | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/libraries/AsyncUDP/src/AsyncUDP.cpp b/libraries/AsyncUDP/src/AsyncUDP.cpp index cab9c951921..2d533831cd5 100644 --- a/libraries/AsyncUDP/src/AsyncUDP.cpp +++ b/libraries/AsyncUDP/src/AsyncUDP.cpp @@ -582,8 +582,8 @@ bool AsyncUDP::listen(const ip_addr_t *addr, uint16_t port) { } close(); if (addr) { - IP_SET_TYPE_VAL(_pcb->local_ip, addr->type); - IP_SET_TYPE_VAL(_pcb->remote_ip, addr->type); + IP_SET_TYPE_VAL(_pcb->local_ip, IP_GET_TYPE(addr)); + IP_SET_TYPE_VAL(_pcb->remote_ip, IP_GET_TYPE(addr)); } if (_udp_bind(_pcb, addr, port) != ERR_OK) { return false; @@ -692,17 +692,8 @@ bool AsyncUDP::listenMulticast(const ip_addr_t *addr, uint16_t port, uint8_t ttl return false; } -#if CONFIG_LWIP_IPV6 - if (IP_IS_V6(addr)) { - IP_SET_TYPE(&bind_addr, IPADDR_TYPE_V6); - ip6_addr_set_any(&bind_addr.u_addr.ip6); - } else { -#endif - IP_SET_TYPE(&bind_addr, IPADDR_TYPE_V4); - ip4_addr_set_any(&bind_addr.u_addr.ip4); -#if CONFIG_LWIP_IPV6 - } -#endif + IP_SET_TYPE(&bind_addr, IP_GET_TYPE(addr)); + ip_addr_set_any(IP_IS_V6(addr), &bind_addr); if (!listen(&bind_addr, port)) { return false; } From 2cb6fbccdbb6f07e1d20951727875613047a235f Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 8 Jul 2025 16:18:42 +0300 Subject: [PATCH 069/102] Add access methods to get the Wire bus number and I2C bus handle (#11570) * feat(i2c): Add method to access the I2C bus handle * feat(wire): Add access method to get the I2C bus number * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- cores/esp32/esp32-hal-i2c-ng.c | 7 +++++++ cores/esp32/esp32-hal-i2c.h | 5 +++++ libraries/Wire/src/Wire.cpp | 6 +++++- libraries/Wire/src/Wire.h | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cores/esp32/esp32-hal-i2c-ng.c b/cores/esp32/esp32-hal-i2c-ng.c index 8e48d0e0397..a3b2307b8a8 100644 --- a/cores/esp32/esp32-hal-i2c-ng.c +++ b/cores/esp32/esp32-hal-i2c-ng.c @@ -56,6 +56,13 @@ static bool i2cDetachBus(void *bus_i2c_num) { return true; } +void *i2cBusHandle(uint8_t i2c_num) { + if (i2c_num >= SOC_I2C_NUM) { + return NULL; + } + return bus[i2c_num].bus_handle; +} + bool i2cIsInit(uint8_t i2c_num) { if (i2c_num >= SOC_I2C_NUM) { return false; diff --git a/cores/esp32/esp32-hal-i2c.h b/cores/esp32/esp32-hal-i2c.h index 35783d350b0..0e4f484bb46 100644 --- a/cores/esp32/esp32-hal-i2c.h +++ b/cores/esp32/esp32-hal-i2c.h @@ -19,6 +19,7 @@ #include "soc/soc_caps.h" #if SOC_I2C_SUPPORTED +#include "esp_idf_version.h" #ifdef __cplusplus extern "C" { @@ -39,6 +40,10 @@ esp_err_t i2cWriteReadNonStop( ); bool i2cIsInit(uint8_t i2c_num); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) +void *i2cBusHandle(uint8_t i2c_num); +#endif + #ifdef __cplusplus } #endif diff --git a/libraries/Wire/src/Wire.cpp b/libraries/Wire/src/Wire.cpp index f8d9496389f..34c814b5117 100644 --- a/libraries/Wire/src/Wire.cpp +++ b/libraries/Wire/src/Wire.cpp @@ -39,7 +39,7 @@ extern "C" { #include "Arduino.h" TwoWire::TwoWire(uint8_t bus_num) - : num(bus_num & 1), sda(-1), scl(-1), bufferSize(I2C_BUFFER_LENGTH) // default Wire Buffer Size + : num(bus_num), sda(-1), scl(-1), bufferSize(I2C_BUFFER_LENGTH) // default Wire Buffer Size , rxBuffer(NULL), rxIndex(0), rxLength(0), txBuffer(NULL), txLength(0), txAddress(0), _timeOutMillis(50), nonStop(false) #if !CONFIG_DISABLE_HAL_LOCKS @@ -62,6 +62,10 @@ TwoWire::~TwoWire() { #endif } +uint8_t TwoWire::getBusNum() { + return num; +} + bool TwoWire::initPins(int sdaPin, int sclPin) { if (sdaPin < 0) { // default param passed if (num == 0) { diff --git a/libraries/Wire/src/Wire.h b/libraries/Wire/src/Wire.h index 0deab7d4a57..b84aa5b2131 100644 --- a/libraries/Wire/src/Wire.h +++ b/libraries/Wire/src/Wire.h @@ -105,6 +105,8 @@ class TwoWire : public HardwareI2C { bool end() override; + uint8_t getBusNum(); + bool setClock(uint32_t freq) override; void beginTransmission(uint8_t address) override; From 6a5839acb2ead9b98bf0761c8bef16cbf327e6ca Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:56:23 -0300 Subject: [PATCH 070/102] change(esptool): Upgrade esptool to release v5.0.0 (#11562) --- .github/scripts/package_esptool.sh | 129 ------------ .github/scripts/update_esptool.py | 236 ++++++++++++++++++++++ package/package_esp32_index.template.json | 60 +++--- 3 files changed, 266 insertions(+), 159 deletions(-) delete mode 100755 .github/scripts/package_esptool.sh create mode 100644 .github/scripts/update_esptool.py diff --git a/.github/scripts/package_esptool.sh b/.github/scripts/package_esptool.sh deleted file mode 100755 index 32b87b277e9..00000000000 --- a/.github/scripts/package_esptool.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Check version argument -if [[ $# -ne 3 ]]; then - echo "Usage: $0 " - echo "Example: $0 5.0.dev1 /tmp/esptool /tmp/esptool-5.0.dev1.json" - exit 1 -fi - -VERSION=$1 -BASE_FOLDER=$2 -JSON_PATH=$3 - -export COPYFILE_DISABLE=1 - -shopt -s nullglob # So for loop doesn't run if no matches - -# Function to update JSON for a given host -function update_json_for_host { - local host=$1 - local archive=$2 - - # Extract the old url from the JSON for this host, then replace only the filename - old_url=$(jq -r --arg host "$host" ' - .packages[].tools[] | select(.name == "esptool_py") | .systems[] | select(.host == $host) | .url // empty - ' "$tmp_json") - if [[ -n "$old_url" ]]; then - base_url="${old_url%/*}" - url="$base_url/$archive" - else - echo "No old url found for $host" - exit 1 - fi - - archiveFileName="$archive" - checksum="SHA-256:$(shasum -a 256 "$archive" | awk '{print $1}')" - size=$(stat -f%z "$archive") - - # Use jq to update the JSON - jq --arg host "$host" \ - --arg url "$url" \ - --arg archiveFileName "$archiveFileName" \ - --arg checksum "$checksum" \ - --arg size "$size" \ - ' - .packages[].tools[] - |= if .name == "esptool_py" then - .systems = ( - ((.systems // []) | map(select(.host != $host))) + [{ - host: $host, - url: $url, - archiveFileName: $archiveFileName, - checksum: $checksum, - size: $size - }] - ) - else - . - end - ' "$tmp_json" > "$tmp_json.new" && mv "$tmp_json.new" "$tmp_json" -} - -cd "$BASE_FOLDER" - -# Delete all archives before starting -rm -f esptool-*.tar.gz esptool-*.zip - -for dir in esptool-*; do - # Check if directory exists and is a directory - if [[ ! -d "$dir" ]]; then - continue - fi - - base="${dir#esptool-}" - - # Add 'linux-' prefix if base doesn't contain linux/macos/win64 - if [[ "$base" != *linux* && "$base" != *macos* && "$base" != *win64* ]]; then - base="linux-${base}" - fi - - if [[ "$dir" == esptool-win* ]]; then - # Windows zip archive - zipfile="esptool-v${VERSION}-${base}.zip" - echo "Creating $zipfile from $dir ..." - zip -r "$zipfile" "$dir" - else - # Non-Windows: set permissions and tar.gz archive - tarfile="esptool-v${VERSION}-${base}.tar.gz" - echo "Setting permissions and creating $tarfile from $dir ..." - chmod -R u=rwx,g=rx,o=rx "$dir" - tar -cvzf "$tarfile" "$dir" - fi -done - -# After the for loop, update the JSON for each archive -# Create a temporary JSON file to accumulate changes -tmp_json="${JSON_PATH}.tmp" -cp "$JSON_PATH" "$tmp_json" - -for archive in esptool-v"${VERSION}"-*.tar.gz esptool-v"${VERSION}"-*.zip; do - [ -f "$archive" ] || continue - - echo "Updating JSON for $archive" - - # Determine host from archive name - case "$archive" in - *linux-amd64*) host="x86_64-pc-linux-gnu" ;; - *linux-armv7*) host="arm-linux-gnueabihf" ;; - *linux-aarch64*) host="aarch64-linux-gnu" ;; - *macos-amd64*) host="x86_64-apple-darwin" ;; - *macos-arm64*) host="arm64-apple-darwin" ;; - *win64*) hosts=("x86_64-mingw32" "i686-mingw32") ;; - *) echo "Unknown host for $archive"; continue ;; - esac - - # For win64, loop over both hosts; otherwise, use a single host - if [[ "$archive" == *win64* ]]; then - for host in "${hosts[@]}"; do - update_json_for_host "$host" "$archive" - done - else - update_json_for_host "$host" "$archive" - fi -done - -# After all archives are processed, move the temporary JSON to the final file -mv "$tmp_json" "$JSON_PATH" diff --git a/.github/scripts/update_esptool.py b/.github/scripts/update_esptool.py new file mode 100644 index 00000000000..d99462fcb8f --- /dev/null +++ b/.github/scripts/update_esptool.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 + +# This script is used to re-package the esptool if needed and update the JSON file +# for the Arduino ESP32 platform. +# +# The script has only been tested on macOS. +# +# For regular esptool releases, the generated packages already contain the correct permissions, +# extensions and are uploaded to the GitHub release assets. In this case, the script will only +# update the JSON file with the information from the GitHub release. +# +# The script can be used in two modes: +# 1. Local build: The build artifacts must be already downloaded and extracted in the base_folder. +# This is useful for esptool versions that are not yet released and that are grabbed from the +# GitHub build artifacts. +# 2. Release build: The script will get the release information from GitHub and update the JSON file. +# This is useful for esptool versions that are already released and that are uploaded to the +# GitHub release assets. +# +# For local build, the artifacts must be already downloaded and extracted in the base_folder +# set with the -l option. +# For example, a base folder "esptool" should contain the following folders extracted directly +# from the GitHub build artifacts: +# esptool/esptool-linux-aarch64 +# esptool/esptool-linux-amd64 +# esptool/esptool-linux-armv7 +# esptool/esptool-macos-amd64 +# esptool/esptool-macos-arm64 +# esptool/esptool-windows-amd64 + +import argparse +import json +import os +import shutil +import stat +import tarfile +import zipfile +import hashlib +import requests +from pathlib import Path + +def compute_sha256(filepath): + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for block in iter(lambda: f.read(4096), b""): + sha256.update(block) + return f"SHA-256:{sha256.hexdigest()}" + +def get_file_size(filepath): + return os.path.getsize(filepath) + +def update_json_for_host(tmp_json_path, version, host, url, archiveFileName, checksum, size): + with open(tmp_json_path) as f: + data = json.load(f) + + for pkg in data.get("packages", []): + for tool in pkg.get("tools", []): + if tool.get("name") == "esptool_py": + tool["version"] = version + + if url is None: + # If the URL is not set, we need to find the old URL and update it + for system in tool.get("systems", []): + if system.get("host") == host: + url = system.get("url").replace(system.get("archiveFileName"), archiveFileName) + break + else: + print(f"No old URL found for host {host}. Using empty URL.") + url = "" + + # Preserve existing systems order and update or append the new system + systems = tool.get("systems", []) + system_updated = False + for i, system in enumerate(systems): + if system.get("host") == host: + systems[i] = { + "host": host, + "url": url, + "archiveFileName": archiveFileName, + "checksum": checksum, + "size": str(size), + } + system_updated = True + break + + if not system_updated: + systems.append({ + "host": host, + "url": url, + "archiveFileName": archiveFileName, + "checksum": checksum, + "size": str(size), + }) + tool["systems"] = systems + + with open(tmp_json_path, "w") as f: + json.dump(data, f, indent=2, sort_keys=False, ensure_ascii=False) + f.write("\n") + +def update_tools_dependencies(tmp_json_path, version): + with open(tmp_json_path) as f: + data = json.load(f) + + for pkg in data.get("packages", []): + for platform in pkg.get("platforms", []): + for dep in platform.get("toolsDependencies", []): + if dep.get("name") == "esptool_py": + dep["version"] = version + + with open(tmp_json_path, "w") as f: + json.dump(data, f, indent=2, sort_keys=False, ensure_ascii=False) + f.write("\n") + +def create_archives(version, base_folder): + archive_files = [] + + for dirpath in Path(base_folder).glob("esptool-*"): + if not dirpath.is_dir(): + continue + + base = dirpath.name[len("esptool-"):] + + if "windows" in dirpath.name: + zipfile_name = f"esptool-v{version}-{base}.zip" + print(f"Creating {zipfile_name} from {dirpath} ...") + with zipfile.ZipFile(zipfile_name, "w", zipfile.ZIP_DEFLATED) as zipf: + for root, _, files in os.walk(dirpath): + for file in files: + full_path = os.path.join(root, file) + zipf.write(full_path, os.path.relpath(full_path, start=dirpath)) + archive_files.append(zipfile_name) + else: + tarfile_name = f"esptool-v{version}-{base}.tar.gz" + print(f"Creating {tarfile_name} from {dirpath} ...") + for root, dirs, files in os.walk(dirpath): + for name in dirs + files: + os.chmod(os.path.join(root, name), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | + stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH) + with tarfile.open(tarfile_name, "w:gz") as tar: + tar.add(dirpath, arcname=dirpath.name) + archive_files.append(tarfile_name) + + return archive_files + +def determine_hosts(archive_name): + if "linux-amd64" in archive_name: + return ["x86_64-pc-linux-gnu"] + elif "linux-armv7" in archive_name: + return ["arm-linux-gnueabihf"] + elif "linux-aarch64" in archive_name: + return ["aarch64-linux-gnu"] + elif "macos-amd64" in archive_name: + return ["x86_64-apple-darwin"] + elif "macos-arm64" in archive_name: + return ["arm64-apple-darwin"] + elif "windows-amd64" in archive_name: + return ["x86_64-mingw32", "i686-mingw32"] + else: + return [] + +def update_json_from_local_build(tmp_json_path, version, base_folder, archive_files): + for archive in archive_files: + print(f"Processing archive: {archive}") + hosts = determine_hosts(archive) + if not hosts: + print(f"Skipping unknown archive type: {archive}") + continue + + archive_path = Path(archive) + checksum = compute_sha256(archive_path) + size = get_file_size(archive_path) + + for host in hosts: + update_json_for_host(tmp_json_path, version, host, None, archive_path.name, checksum, size) + +def update_json_from_release(tmp_json_path, version, release_info): + assets = release_info.get("assets", []) + for asset in assets: + if (asset.get("name").endswith(".tar.gz") or asset.get("name").endswith(".zip")) and "esptool" in asset.get("name"): + asset_fname = asset.get("name") + print(f"Processing asset: {asset_fname}") + hosts = determine_hosts(asset_fname) + if not hosts: + print(f"Skipping unknown archive type: {asset_fname}") + continue + + asset_url = asset.get("browser_download_url") + asset_checksum = asset.get("digest").replace("sha256:", "SHA-256:") + asset_size = asset.get("size") + if asset_checksum is None: + asset_checksum = "" + print(f"Asset {asset_fname} has no checksum. Please set the checksum in the JSON file.") + + for host in hosts: + update_json_for_host(tmp_json_path, version, host, asset_url, asset_fname, asset_checksum, asset_size) + +def get_release_info(version): + url = f"https://api.github.com/repos/espressif/esptool/releases/tags/v{version}" + response = requests.get(url) + response.raise_for_status() + return response.json() + +def main(): + parser = argparse.ArgumentParser(description="Repack esptool and update JSON metadata.") + parser.add_argument("version", help="Version of the esptool (e.g. 5.0.dev1)") + parser.add_argument("-l", "--local", dest="base_folder", help="Enable local build mode and set the base folder with unpacked artifacts") + args = parser.parse_args() + + script_dir = Path(__file__).resolve().parent + json_path = (script_dir / "../../package/package_esp32_index.template.json").resolve() + tmp_json_path = Path(str(json_path) + ".tmp") + shutil.copy(json_path, tmp_json_path) + + local_build = args.base_folder is not None + + if local_build: + os.chdir(args.base_folder) + os.environ['COPYFILE_DISABLE'] = 'true' # this disables including resource forks in tar files on macOS + # Clear any existing archive files + for file in Path(args.base_folder).glob("esptool-*.*"): + file.unlink() + archive_files = create_archives(args.version, args.base_folder) + update_json_from_local_build(tmp_json_path, args.version, args.base_folder, archive_files) + else: + release_info = get_release_info(args.version) + update_json_from_release(tmp_json_path, args.version, release_info) + + print(f"Updating esptool version fields to {args.version}") + update_tools_dependencies(tmp_json_path, args.version) + + shutil.move(tmp_json_path, json_path) + print(f"Done. JSON updated at {json_path}") + +if __name__ == "__main__": + main() diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index bbb77f8ef5a..5f5d1f0e08b 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -81,7 +81,7 @@ { "packager": "esp32", "name": "esptool_py", - "version": "5.0.dev1" + "version": "5.0.0" }, { "packager": "esp32", @@ -469,56 +469,56 @@ }, { "name": "esptool_py", - "version": "5.0.dev1", + "version": "5.0.0", "systems": [ { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-aarch64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-linux-aarch64.tar.gz", - "checksum": "SHA-256:bfafa7a7723ebbabfd8b6e3ca5ae00bfead0331de923754aeddb43b2c116a078", - "size": "58241736" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-linux-aarch64.tar.gz", + "archiveFileName": "esptool-v5.0.0-linux-aarch64.tar.gz", + "checksum": "SHA-256:2bf239f3ed76141a957cadb205b94414ec6da9ace4e85f285e247d20a92b83e3", + "size": "58231895" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-amd64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-linux-amd64.tar.gz", - "checksum": "SHA-256:acd0486e96586b99d053a1479acbbbfcae8667227c831cdc53a171f9ccfa27ee", - "size": "100740042" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-linux-amd64.tar.gz", + "archiveFileName": "esptool-v5.0.0-linux-amd64.tar.gz", + "checksum": "SHA-256:3b3835d266ac61f3242758f2fe34e3b33dbe6ee4b5acde005da793356f9f7043", + "size": "100783748" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-armv7.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-linux-armv7.tar.gz", - "checksum": "SHA-256:ea77a38681506761bbb7b0b39c130811ed565667b67ebbdb4d6dcc6cb6e07368", - "size": "53451939" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-linux-armv7.tar.gz", + "archiveFileName": "esptool-v5.0.0-linux-armv7.tar.gz", + "checksum": "SHA-256:e55cd321abecfcf27f72a2bff5d5e19a5365fd400de66d71c5e7218e77556315", + "size": "53461760" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-macos-amd64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-macos-amd64.tar.gz", - "checksum": "SHA-256:900a8e90731208bee96647e0e207a43612b9452c2120c4fdc0ff4c6be226257b", - "size": "59631998" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-macos-amd64.tar.gz", + "archiveFileName": "esptool-v5.0.0-macos-amd64.tar.gz", + "checksum": "SHA-256:424da2bdf0435257ad81bcb7eae6fd8dd7f675ce5b2ee60032f4ecec4d6a5d45", + "size": "59629533" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-macos-arm64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-macos-arm64.tar.gz", - "checksum": "SHA-256:3653f4de73cb4fc6a25351eaf663708e91c65ae3265d75bd54ca4315a4350bb4", - "size": "56349992" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-macos-arm64.tar.gz", + "archiveFileName": "esptool-v5.0.0-macos-arm64.tar.gz", + "checksum": "SHA-256:b91dfe1da7b0041376683dec10a91dfb266fbda2fb86ed87c4a034ff7182ee56", + "size": "56343104" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-win64.zip", - "archiveFileName": "esptool-v5.0.dev1-win64.zip", - "checksum": "SHA-256:1e8fd89645daf94f2d4406ec73c9004e617ea921079515f9fd749205eece4d6d", - "size": "59102658" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-windows-amd64.zip", + "archiveFileName": "esptool-v5.0.0-windows-amd64.zip", + "checksum": "SHA-256:2294107f66db6f09b886b337728a981173c9e7eab45a030928a8a5a1370611ca", + "size": "59105322" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-win64.zip", - "archiveFileName": "esptool-v5.0.dev1-win64.zip", - "checksum": "SHA-256:1e8fd89645daf94f2d4406ec73c9004e617ea921079515f9fd749205eece4d6d", - "size": "59102658" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-windows-amd64.zip", + "archiveFileName": "esptool-v5.0.0-windows-amd64.zip", + "checksum": "SHA-256:2294107f66db6f09b886b337728a981173c9e7eab45a030928a8a5a1370611ca", + "size": "59105322" } ] }, From ccc0a69ef88202519c0773ec254ee31cbe2baf82 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:58:36 -0300 Subject: [PATCH 071/102] change(esptool): Upgrade esptool to release v5.0.0 (#11563) --- .github/scripts/package_esptool.sh | 129 ---------------------- .github/scripts/update_esptool.py | 2 +- package/package_esp32_index.template.json | 60 +++++----- 3 files changed, 31 insertions(+), 160 deletions(-) delete mode 100755 .github/scripts/package_esptool.sh diff --git a/.github/scripts/package_esptool.sh b/.github/scripts/package_esptool.sh deleted file mode 100755 index 32b87b277e9..00000000000 --- a/.github/scripts/package_esptool.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Check version argument -if [[ $# -ne 3 ]]; then - echo "Usage: $0 " - echo "Example: $0 5.0.dev1 /tmp/esptool /tmp/esptool-5.0.dev1.json" - exit 1 -fi - -VERSION=$1 -BASE_FOLDER=$2 -JSON_PATH=$3 - -export COPYFILE_DISABLE=1 - -shopt -s nullglob # So for loop doesn't run if no matches - -# Function to update JSON for a given host -function update_json_for_host { - local host=$1 - local archive=$2 - - # Extract the old url from the JSON for this host, then replace only the filename - old_url=$(jq -r --arg host "$host" ' - .packages[].tools[] | select(.name == "esptool_py") | .systems[] | select(.host == $host) | .url // empty - ' "$tmp_json") - if [[ -n "$old_url" ]]; then - base_url="${old_url%/*}" - url="$base_url/$archive" - else - echo "No old url found for $host" - exit 1 - fi - - archiveFileName="$archive" - checksum="SHA-256:$(shasum -a 256 "$archive" | awk '{print $1}')" - size=$(stat -f%z "$archive") - - # Use jq to update the JSON - jq --arg host "$host" \ - --arg url "$url" \ - --arg archiveFileName "$archiveFileName" \ - --arg checksum "$checksum" \ - --arg size "$size" \ - ' - .packages[].tools[] - |= if .name == "esptool_py" then - .systems = ( - ((.systems // []) | map(select(.host != $host))) + [{ - host: $host, - url: $url, - archiveFileName: $archiveFileName, - checksum: $checksum, - size: $size - }] - ) - else - . - end - ' "$tmp_json" > "$tmp_json.new" && mv "$tmp_json.new" "$tmp_json" -} - -cd "$BASE_FOLDER" - -# Delete all archives before starting -rm -f esptool-*.tar.gz esptool-*.zip - -for dir in esptool-*; do - # Check if directory exists and is a directory - if [[ ! -d "$dir" ]]; then - continue - fi - - base="${dir#esptool-}" - - # Add 'linux-' prefix if base doesn't contain linux/macos/win64 - if [[ "$base" != *linux* && "$base" != *macos* && "$base" != *win64* ]]; then - base="linux-${base}" - fi - - if [[ "$dir" == esptool-win* ]]; then - # Windows zip archive - zipfile="esptool-v${VERSION}-${base}.zip" - echo "Creating $zipfile from $dir ..." - zip -r "$zipfile" "$dir" - else - # Non-Windows: set permissions and tar.gz archive - tarfile="esptool-v${VERSION}-${base}.tar.gz" - echo "Setting permissions and creating $tarfile from $dir ..." - chmod -R u=rwx,g=rx,o=rx "$dir" - tar -cvzf "$tarfile" "$dir" - fi -done - -# After the for loop, update the JSON for each archive -# Create a temporary JSON file to accumulate changes -tmp_json="${JSON_PATH}.tmp" -cp "$JSON_PATH" "$tmp_json" - -for archive in esptool-v"${VERSION}"-*.tar.gz esptool-v"${VERSION}"-*.zip; do - [ -f "$archive" ] || continue - - echo "Updating JSON for $archive" - - # Determine host from archive name - case "$archive" in - *linux-amd64*) host="x86_64-pc-linux-gnu" ;; - *linux-armv7*) host="arm-linux-gnueabihf" ;; - *linux-aarch64*) host="aarch64-linux-gnu" ;; - *macos-amd64*) host="x86_64-apple-darwin" ;; - *macos-arm64*) host="arm64-apple-darwin" ;; - *win64*) hosts=("x86_64-mingw32" "i686-mingw32") ;; - *) echo "Unknown host for $archive"; continue ;; - esac - - # For win64, loop over both hosts; otherwise, use a single host - if [[ "$archive" == *win64* ]]; then - for host in "${hosts[@]}"; do - update_json_for_host "$host" "$archive" - done - else - update_json_for_host "$host" "$archive" - fi -done - -# After all archives are processed, move the temporary JSON to the final file -mv "$tmp_json" "$JSON_PATH" diff --git a/.github/scripts/update_esptool.py b/.github/scripts/update_esptool.py index dd5de5526c3..d99462fcb8f 100644 --- a/.github/scripts/update_esptool.py +++ b/.github/scripts/update_esptool.py @@ -186,7 +186,7 @@ def update_json_from_release(tmp_json_path, version, release_info): continue asset_url = asset.get("browser_download_url") - asset_checksum = asset.get("digest") + asset_checksum = asset.get("digest").replace("sha256:", "SHA-256:") asset_size = asset.get("size") if asset_checksum is None: asset_checksum = "" diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index a02be509094..9c2c754504e 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -81,7 +81,7 @@ { "packager": "esp32", "name": "esptool_py", - "version": "5.0.dev1" + "version": "5.0.0" }, { "packager": "esp32", @@ -469,56 +469,56 @@ }, { "name": "esptool_py", - "version": "5.0.dev1", + "version": "5.0.0", "systems": [ { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-aarch64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-linux-aarch64.tar.gz", - "checksum": "SHA-256:bfafa7a7723ebbabfd8b6e3ca5ae00bfead0331de923754aeddb43b2c116a078", - "size": "58241736" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-linux-aarch64.tar.gz", + "archiveFileName": "esptool-v5.0.0-linux-aarch64.tar.gz", + "checksum": "SHA-256:2bf239f3ed76141a957cadb205b94414ec6da9ace4e85f285e247d20a92b83e3", + "size": "58231895" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-amd64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-linux-amd64.tar.gz", - "checksum": "SHA-256:acd0486e96586b99d053a1479acbbbfcae8667227c831cdc53a171f9ccfa27ee", - "size": "100740042" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-linux-amd64.tar.gz", + "archiveFileName": "esptool-v5.0.0-linux-amd64.tar.gz", + "checksum": "SHA-256:3b3835d266ac61f3242758f2fe34e3b33dbe6ee4b5acde005da793356f9f7043", + "size": "100783748" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-linux-armv7.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-linux-armv7.tar.gz", - "checksum": "SHA-256:ea77a38681506761bbb7b0b39c130811ed565667b67ebbdb4d6dcc6cb6e07368", - "size": "53451939" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-linux-armv7.tar.gz", + "archiveFileName": "esptool-v5.0.0-linux-armv7.tar.gz", + "checksum": "SHA-256:e55cd321abecfcf27f72a2bff5d5e19a5365fd400de66d71c5e7218e77556315", + "size": "53461760" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-macos-amd64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-macos-amd64.tar.gz", - "checksum": "SHA-256:900a8e90731208bee96647e0e207a43612b9452c2120c4fdc0ff4c6be226257b", - "size": "59631998" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-macos-amd64.tar.gz", + "archiveFileName": "esptool-v5.0.0-macos-amd64.tar.gz", + "checksum": "SHA-256:424da2bdf0435257ad81bcb7eae6fd8dd7f675ce5b2ee60032f4ecec4d6a5d45", + "size": "59629533" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-macos-arm64.tar.gz", - "archiveFileName": "esptool-v5.0.dev1-macos-arm64.tar.gz", - "checksum": "SHA-256:3653f4de73cb4fc6a25351eaf663708e91c65ae3265d75bd54ca4315a4350bb4", - "size": "56349992" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-macos-arm64.tar.gz", + "archiveFileName": "esptool-v5.0.0-macos-arm64.tar.gz", + "checksum": "SHA-256:b91dfe1da7b0041376683dec10a91dfb266fbda2fb86ed87c4a034ff7182ee56", + "size": "56343104" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-win64.zip", - "archiveFileName": "esptool-v5.0.dev1-win64.zip", - "checksum": "SHA-256:1e8fd89645daf94f2d4406ec73c9004e617ea921079515f9fd749205eece4d6d", - "size": "59102658" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-windows-amd64.zip", + "archiveFileName": "esptool-v5.0.0-windows-amd64.zip", + "checksum": "SHA-256:2294107f66db6f09b886b337728a981173c9e7eab45a030928a8a5a1370611ca", + "size": "59105322" }, { "host": "i686-mingw32", - "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esptool-v5.0.dev1-win64.zip", - "archiveFileName": "esptool-v5.0.dev1-win64.zip", - "checksum": "SHA-256:1e8fd89645daf94f2d4406ec73c9004e617ea921079515f9fd749205eece4d6d", - "size": "59102658" + "url": "https://github.com/espressif/esptool/releases/download/v5.0.0/esptool-v5.0.0-windows-amd64.zip", + "archiveFileName": "esptool-v5.0.0-windows-amd64.zip", + "checksum": "SHA-256:2294107f66db6f09b886b337728a981173c9e7eab45a030928a8a5a1370611ca", + "size": "59105322" } ] }, From ee021855a156847dfcf75a9aeb8d585213f09e42 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:24:42 -0300 Subject: [PATCH 072/102] feat(docs_version): Update docs in update-version script (#11564) * feat(docs_version): Update docs in update-version script * fix(logging): Fix log message * fix(idf_version): Add error if IDF version is not found --- .github/scripts/update-version.sh | 14 ++++++++++++++ docs/conf_common.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/scripts/update-version.sh b/.github/scripts/update-version.sh index 9a38b27a57a..622f2fe8ff8 100755 --- a/.github/scripts/update-version.sh +++ b/.github/scripts/update-version.sh @@ -1,4 +1,5 @@ #!/bin/bash +# Disable shellcheck warning about using 'cat' to read a file. # shellcheck disable=SC2002 # For reference: add tools for all boards by replacing one line in each board @@ -23,7 +24,15 @@ ESP_ARDUINO_VERSION_MINOR="$2" ESP_ARDUINO_VERSION_PATCH="$3" ESP_ARDUINO_VERSION="$ESP_ARDUINO_VERSION_MAJOR.$ESP_ARDUINO_VERSION_MINOR.$ESP_ARDUINO_VERSION_PATCH" +# Get ESP-IDF version from push.yml (this way we can ensure that the version is correct even if the local libs are not up to date) +ESP_IDF_VERSION=$(grep "idf_ver:" .github/workflows/push.yml | sed 's/.*release-v\([^"]*\).*/\1/') +if [ -z "$ESP_IDF_VERSION" ]; then + echo "Error: ESP-IDF version not found in push.yml" >&2 + exit 1 +fi + echo "New Arduino Version: $ESP_ARDUINO_VERSION" +echo "ESP-IDF Version: $ESP_IDF_VERSION" echo "Updating platform.txt..." cat platform.txt | sed "s/version=.*/version=$ESP_ARDUINO_VERSION/g" > __platform.txt && mv __platform.txt platform.txt @@ -31,6 +40,11 @@ cat platform.txt | sed "s/version=.*/version=$ESP_ARDUINO_VERSION/g" > __platfor echo "Updating package.json..." cat package.json | sed "s/.*\"version\":.*/ \"version\": \"$ESP_ARDUINO_VERSION\",/g" > __package.json && mv __package.json package.json +echo "Updating docs/conf_common.py..." +cat docs/conf_common.py | \ +sed "s/.. |version| replace:: .*/.. |version| replace:: $ESP_ARDUINO_VERSION/g" | \ +sed "s/.. |idf_version| replace:: .*/.. |idf_version| replace:: $ESP_IDF_VERSION/g" > docs/__conf_common.py && mv docs/__conf_common.py docs/conf_common.py + echo "Updating cores/esp32/esp_arduino_version.h..." cat cores/esp32/esp_arduino_version.h | \ sed "s/#define ESP_ARDUINO_VERSION_MAJOR.*/#define ESP_ARDUINO_VERSION_MAJOR $ESP_ARDUINO_VERSION_MAJOR/g" | \ diff --git a/docs/conf_common.py b/docs/conf_common.py index 6945c0d190d..af1d615f753 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -4,7 +4,7 @@ # Used for substituting variables in the documentation rst_prolog = """ -.. |version| replace:: 3.2.0 +.. |version| replace:: 3.2.1 .. |idf_version| replace:: 5.4 """ From 87b718a59cbeed010eee2c6a79004e22f71564ac Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:25:16 -0300 Subject: [PATCH 073/102] fix(merge): Fix merging CN Json (#11574) --- .github/scripts/merge_packages.py | 16 ++++++++-------- .github/scripts/release_append_cn.py | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/scripts/merge_packages.py b/.github/scripts/merge_packages.py index 7e4f47ca8b3..8d1f200ec5c 100755 --- a/.github/scripts/merge_packages.py +++ b/.github/scripts/merge_packages.py @@ -4,6 +4,7 @@ # Usage: # python merge_packages.py package_esp8266com_index.json version/new/package_esp8266com_index.json # Written by Ivan Grokhotkov, 2015 +# Updated by lucasssvaz to handle Chinese version sorting, 2025 # from __future__ import print_function @@ -36,20 +37,19 @@ def merge_objects(versions, obj): # Normalize ESP release version string (x.x.x) by adding '-rc' (x.x.x-rc9223372036854775807) -# to ensure having REL above any RC +# to ensure having REL above any RC. CN version will be sorted after the official version if they happen +# to be mixed (normally, CN and non-CN versions should not be mixed) # Dummy approach, functional anyway for current ESP package versioning # (unlike NormalizedVersion/LooseVersion/StrictVersion & similar crap) def pkgVersionNormalized(versionString): - - verStr = str(versionString) + verStr = str(versionString).replace("-cn", "") verParts = re.split(r"\.|-rc|-alpha", verStr, flags=re.IGNORECASE) if len(verParts) == 3: - if sys.version_info > (3, 0): # Python 3 - verStr = str(versionString) + "-rc" + str(sys.maxsize) - else: # Python 2 - verStr = str(versionString) + "-rc" + str(sys.maxint) - + if "-cn" in str(versionString): + verStr = verStr + "-rc" + str(sys.maxsize // 2) + else: + verStr = verStr + "-rc" + str(sys.maxsize) elif len(verParts) != 4: print("pkgVersionNormalized WARNING: unexpected version format: {0})".format(verStr), file=sys.stderr) diff --git a/.github/scripts/release_append_cn.py b/.github/scripts/release_append_cn.py index b29fe0c31ba..2342834bb7e 100755 --- a/.github/scripts/release_append_cn.py +++ b/.github/scripts/release_append_cn.py @@ -17,8 +17,9 @@ def append_cn_to_versions(obj): if isinstance(obj, dict): - # dfu-util comes from arduino.cc and not from the Chinese mirrors, so we skip it - if obj.get("name") == "dfu-util": + # Skip tools that are not from the esp32 package + packager = obj.get("packager") + if packager is not None and packager != "esp32": return for key, value in obj.items(): From 4ee17dea045b2b4f8bd5965bdde6e9110b612026 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Fri, 11 Jul 2025 11:25:27 -0300 Subject: [PATCH 074/102] feat(matter): new matter lambda function example (#11561) Adds a new Matter Library example using lambda function to creat 6 endpoints using a single callback --- .../MatterLambdaSingleCallbackManyEPs.ino | 112 ++++++++++++++++++ .../MatterLambdaSingleCallbackManyEPs/ci.json | 7 ++ libraries/Matter/keywords.txt | 6 + 3 files changed, 125 insertions(+) create mode 100644 libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino create mode 100644 libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json diff --git a/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino new file mode 100644 index 00000000000..4992771d925 --- /dev/null +++ b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino @@ -0,0 +1,112 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + This example creates 6 on-off light endpoints that share the same onChangeOnOff() callback code. + It uses Lambda Function with an extra Lambda Capture information that links the Endpoint to its individual information. + After the Matter example is commissioned, the expected Serial output shall be similar to this: + +Matter App Control: 'Room 1' (OnOffLight[0], Endpoint 1, GPIO 2) changed to: OFF +Matter App Control: 'Room 1' (OnOffLight[0], Endpoint 1, GPIO 2) changed to: ON +Matter App Control: 'Room 5' (OnOffLight[4], Endpoint 5, GPIO 10) changed to: ON +Matter App Control: 'Room 2' (OnOffLight[1], Endpoint 2, GPIO 4) changed to: ON +Matter App Control: 'Room 4' (OnOffLight[3], Endpoint 4, GPIO 8) changed to: ON +Matter App Control: 'Room 6' (OnOffLight[5], Endpoint 6, GPIO 12) changed to: ON +Matter App Control: 'Room 3' (OnOffLight[2], Endpoint 3, GPIO 6) changed to: ON +Matter App Control: 'Room 5' (OnOffLight[4], Endpoint 5, GPIO 10) changed to: OFF +*/ + +// Matter Manager +#include +#include + +// WiFi is manually set and started +const char *ssid = "your-ssid"; // Change this to your WiFi SSID +const char *password = "your-password"; // Change this to your WiFi password + +//number of On-Off Lights: +const uint8_t MAX_LIGHT_NUMBER = 6; + +// array of OnOffLight endpoints +MatterOnOffLight OnOffLight[MAX_LIGHT_NUMBER]; + +// all pins, one for each on-off light +uint8_t lightPins[MAX_LIGHT_NUMBER] = {2, 4, 6, 8, 10, 12}; // must replace it by the real pin for the target SoC and application + +// friendly OnOffLights names used for printing a message in the callback +const char *lightName[MAX_LIGHT_NUMBER] = { + "Room 1", "Room 2", "Room 3", "Room 4", "Room 5", "Room 6", +}; + +// simple setup() function +void setup() { + Serial.begin(115200); // callback will just print a message in the console + + // We start by connecting to a WiFi network + Serial.print("Connecting to "); + Serial.println(ssid); + // Manually connect to WiFi + WiFi.begin(ssid, password); + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\r\nWiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + delay(500); + + // setup all the OnOff Light endpoint and their lambda callback functions + for (uint8_t i = 0; i < MAX_LIGHT_NUMBER; i++) { + pinMode(lightPins[i], OUTPUT); // set the GPIO function + OnOffLight[i].begin(false); // off + + // inline lambda function using capture array index -> it will just print a message in the console + OnOffLight[i].onChangeOnOff([i](bool state) -> bool { + // Display message with the specific light name and details + Serial.printf( + "Matter App Control: '%s' (OnOffLight[%d], Endpoint %d, GPIO %d) changed to: %s\r\n", lightName[i], i, OnOffLight[i].getEndPointId(), lightPins[i], + state ? "ON" : "OFF" + ); + + return true; + }); + } + // last step, starting Matter Stack + Matter.begin(); +} + +void loop() { + // Check Matter Plugin Commissioning state, which may change during execution of loop() + if (!Matter.isDeviceCommissioned()) { + Serial.println(""); + Serial.println("Matter Node is not commissioned yet."); + Serial.println("Initiate the device discovery in your Matter environment."); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str()); + // waits for Matter Plugin Commissioning. + uint32_t timeCount = 0; + while (!Matter.isDeviceCommissioned()) { + delay(100); + if ((timeCount++ % 50) == 0) { // 50*100ms = 5 sec + Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); + } + } + Serial.println("Matter Node is commissioned and connected to the WiFi network. Ready for use."); + } + + delay(500); +} diff --git a/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json new file mode 100644 index 00000000000..556a8a9ee6b --- /dev/null +++ b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json @@ -0,0 +1,7 @@ +{ + "fqbn_append": "PartitionScheme=huge_app", + "requires": [ + "CONFIG_SOC_WIFI_SUPPORTED=y", + "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" + ] +} diff --git a/libraries/Matter/keywords.txt b/libraries/Matter/keywords.txt index 68aaebb1d4d..edba06083bd 100644 --- a/libraries/Matter/keywords.txt +++ b/libraries/Matter/keywords.txt @@ -36,8 +36,10 @@ EndPointSpeedCB KEYWORD1 EndPointOnOffCB KEYWORD1 EndPointBrightnessCB KEYWORD1 EndPointRGBColorCB KEYWORD1 +EndPointIdentifyCB KEYWORD1 matterEvent_t KEYWORD1 matterEventCB KEYWORD1 +attrOperation_t KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -111,6 +113,10 @@ onChangeLocalTemperature KEYWORD2 onChangeCoolingSetpoint KEYWORD2 onChangeHeatingSetpoint KEYWORD2 onEvent KEYWORD2 +setEndPointId KEYWORD2 +getEndPointId KEYWORD2 +onIdentify KEYWORD2 +endpointIdentifyCB KEYWORD2 ####################################### # Constants (LITERAL1) From c6a3bcb014c7fff53e6c1c41fec50e3ec4de8514 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Sat, 12 Jul 2025 00:11:41 -0300 Subject: [PATCH 075/102] feat(matter): removing wifi requirement for H2 and C5 (#11581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes WiFi provisioning support from CI and examples (shifting to Thread/BLE provisioning), updates the CI configs for all Matter examples to drop the WiFi requirement, and adds new API keywords and a fresh “LambdaSingleCallbackManyEPs” example. - Deleted the WiFiProvWithinMatter example (its .ino and ci.json) since BLE is now used for provisioning. - Stripped "CONFIG_SOC_WIFI_SUPPORTED=y" from the CI JSON of existing examples to test Thread-only builds. - Updated keywords.txt with new Matter API identifiers and introduced a new “LambdaSingleCallbackManyEPs” example with CI and source --- .../Matter/examples/MatterColorLight/ci.json | 11 +- .../examples/MatterCommissionTest/ci.json | 1 - .../examples/MatterComposedLights/ci.json | 1 - .../examples/MatterContactSensor/ci.json | 1 - .../examples/MatterDimmableLight/ci.json | 1 - .../examples/MatterEnhancedColorLight/ci.json | 9 +- .../Matter/examples/MatterEvents/ci.json | 1 - libraries/Matter/examples/MatterFan/ci.json | 9 +- .../examples/MatterHumiditySensor/ci.json | 1 - .../MatterLambdaSingleCallbackManyEPs.ino | 126 +++++++++++++++ .../MatterLambdaSingleCallbackManyEPs/ci.json | 6 + .../Matter/examples/MatterMinimum/ci.json | 1 - .../examples/MatterOccupancySensor/ci.json | 1 - .../Matter/examples/MatterOnIdentify/ci.json | 1 - .../Matter/examples/MatterOnOffLight/ci.json | 1 - .../Matter/examples/MatterOnOffPlugin/ci.json | 1 - .../examples/MatterPressureSensor/ci.json | 1 - .../Matter/examples/MatterSmartButon/ci.json | 1 - .../examples/MatterTemperatureLight/ci.json | 1 - .../examples/MatterTemperatureSensor/ci.json | 1 - .../Matter/examples/MatterThermostat/ci.json | 1 - .../WiFiProvWithinMatter.ino | 152 ------------------ .../examples/WiFiProvWithinMatter/ci.json | 7 - libraries/Matter/keywords.txt | 8 + 24 files changed, 153 insertions(+), 191 deletions(-) create mode 100644 libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino create mode 100644 libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json delete mode 100644 libraries/Matter/examples/WiFiProvWithinMatter/WiFiProvWithinMatter.ino delete mode 100644 libraries/Matter/examples/WiFiProvWithinMatter/ci.json diff --git a/libraries/Matter/examples/MatterColorLight/ci.json b/libraries/Matter/examples/MatterColorLight/ci.json index d5f63487506..90b393f9156 100644 --- a/libraries/Matter/examples/MatterColorLight/ci.json +++ b/libraries/Matter/examples/MatterColorLight/ci.json @@ -1,7 +1,6 @@ { - "fqbn_append": "PartitionScheme=huge_app", - "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", - "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" - ] - } + "fqbn_append": "PartitionScheme=huge_app", + "requires": [ + "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" + ] +} diff --git a/libraries/Matter/examples/MatterCommissionTest/ci.json b/libraries/Matter/examples/MatterCommissionTest/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterCommissionTest/ci.json +++ b/libraries/Matter/examples/MatterCommissionTest/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterComposedLights/ci.json b/libraries/Matter/examples/MatterComposedLights/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterComposedLights/ci.json +++ b/libraries/Matter/examples/MatterComposedLights/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterContactSensor/ci.json b/libraries/Matter/examples/MatterContactSensor/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterContactSensor/ci.json +++ b/libraries/Matter/examples/MatterContactSensor/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterDimmableLight/ci.json b/libraries/Matter/examples/MatterDimmableLight/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterDimmableLight/ci.json +++ b/libraries/Matter/examples/MatterDimmableLight/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterEnhancedColorLight/ci.json b/libraries/Matter/examples/MatterEnhancedColorLight/ci.json index 0665800b12b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterEnhancedColorLight/ci.json +++ b/libraries/Matter/examples/MatterEnhancedColorLight/ci.json @@ -1,7 +1,6 @@ { - "fqbn_append": "PartitionScheme=huge_app", - "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", - "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" - ] + "fqbn_append": "PartitionScheme=huge_app", + "requires": [ + "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" + ] } diff --git a/libraries/Matter/examples/MatterEvents/ci.json b/libraries/Matter/examples/MatterEvents/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterEvents/ci.json +++ b/libraries/Matter/examples/MatterEvents/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterFan/ci.json b/libraries/Matter/examples/MatterFan/ci.json index 0665800b12b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterFan/ci.json +++ b/libraries/Matter/examples/MatterFan/ci.json @@ -1,7 +1,6 @@ { - "fqbn_append": "PartitionScheme=huge_app", - "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", - "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" - ] + "fqbn_append": "PartitionScheme=huge_app", + "requires": [ + "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" + ] } diff --git a/libraries/Matter/examples/MatterHumiditySensor/ci.json b/libraries/Matter/examples/MatterHumiditySensor/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterHumiditySensor/ci.json +++ b/libraries/Matter/examples/MatterHumiditySensor/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino new file mode 100644 index 00000000000..c60cadd784f --- /dev/null +++ b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/MatterLambdaSingleCallbackManyEPs.ino @@ -0,0 +1,126 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + This example create 6 on-off light endpoint that share the same onChangeOnOff() callback code. + It uses Lambda Function with an extra Lambda Capture information that links the Endpoint to its individual information. + After the Matter example is commissioned, the expected Serial output shall be similar to this: + +Matter App Control: 'Room 1' (OnOffLight[0], Endpoint 1, GPIO 2) changed to: OFF +Matter App Control: 'Room 1' (OnOffLight[0], Endpoint 1, GPIO 2) changed to: ON +Matter App Control: 'Room 5' (OnOffLight[4], Endpoint 5, GPIO 10) changed to: ON +Matter App Control: 'Room 2' (OnOffLight[1], Endpoint 2, GPIO 4) changed to: ON +Matter App Control: 'Room 4' (OnOffLight[3], Endpoint 4, GPIO 8) changed to: ON +Matter App Control: 'Room 6' (OnOffLight[5], Endpoint 6, GPIO 12) changed to: ON +Matter App Control: 'Room 3' (OnOffLight[2], Endpoint 3, GPIO 6) changed to: ON +Matter App Control: 'Room 5' (OnOffLight[4], Endpoint 5, GPIO 10) changed to: OFF +*/ + +// Matter Manager +#include +#include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space +#include +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +// WiFi is manually set and started +const char *ssid = "your-ssid"; // Change this to your WiFi SSID +const char *password = "your-password"; // Change this to your WiFi password +#endif + +//number of On-Off Lights: +const uint8_t MAX_LIGHT_NUMBER = 6; + +// array of OnOffLight endpoints +MatterOnOffLight OnOffLight[MAX_LIGHT_NUMBER]; + +// all pins, one for each on-off light +uint8_t lightPins[MAX_LIGHT_NUMBER] = {2, 4, 6, 8, 10, 12}; // must replace it by the real pin for the target SoC and application + +// friendly OnOffLights names used for printing a message in the callback +const char *lightName[MAX_LIGHT_NUMBER] = { + "Room 1", "Room 2", "Room 3", "Room 4", "Room 5", "Room 6", +}; + +// simple setup() function +void setup() { + Serial.begin(115200); // callback will just print a message in the console + +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE + // We start by connecting to a WiFi network + Serial.print("Connecting to "); + Serial.println(ssid); + // Manually connect to WiFi + WiFi.begin(ssid, password); + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\r\nWiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + delay(500); +#endif + + // setup all the OnOff Light endpoint and their lambda callback functions + for (uint8_t i = 0; i < MAX_LIGHT_NUMBER; i++) { + pinMode(lightPins[i], OUTPUT); // set the GPIO function + OnOffLight[i].begin(false); // off + + // inline lambda function using capture array index -> it will just print a message in the console + OnOffLight[i].onChangeOnOff([i](bool state) -> bool { + // Display message with the specific light name and details + Serial.printf( + "Matter App Control: '%s' (OnOffLight[%d], Endpoint %d, GPIO %d) changed to: %s\r\n", lightName[i], i, OnOffLight[i].getEndPointId(), lightPins[i], + state ? "ON" : "OFF" + ); + + return true; + }); + } + // last step, starting Matter Stack + Matter.begin(); +} + +void loop() { + // Check Matter Plugin Commissioning state, which may change during execution of loop() + if (!Matter.isDeviceCommissioned()) { + Serial.println(""); + Serial.println("Matter Node is not commissioned yet."); + Serial.println("Initiate the device discovery in your Matter environment."); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str()); + // waits for Matter Plugin Commissioning. + uint32_t timeCount = 0; + while (!Matter.isDeviceCommissioned()) { + delay(100); + if ((timeCount++ % 50) == 0) { // 50*100ms = 5 sec + Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); + } + } + } else { + if (Matter.isDeviceConnected()) { + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); + } else { + Serial.println("Matter Node is commissioned. Waiting for the network connection."); + } + // wait 3 seconds for the network connection + delay(3000); + } + + delay(100); +} diff --git a/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json new file mode 100644 index 00000000000..90b393f9156 --- /dev/null +++ b/libraries/Matter/examples/MatterLambdaSingleCallbackManyEPs/ci.json @@ -0,0 +1,6 @@ +{ + "fqbn_append": "PartitionScheme=huge_app", + "requires": [ + "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" + ] +} diff --git a/libraries/Matter/examples/MatterMinimum/ci.json b/libraries/Matter/examples/MatterMinimum/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterMinimum/ci.json +++ b/libraries/Matter/examples/MatterMinimum/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterOccupancySensor/ci.json b/libraries/Matter/examples/MatterOccupancySensor/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterOccupancySensor/ci.json +++ b/libraries/Matter/examples/MatterOccupancySensor/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterOnIdentify/ci.json b/libraries/Matter/examples/MatterOnIdentify/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterOnIdentify/ci.json +++ b/libraries/Matter/examples/MatterOnIdentify/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterOnOffLight/ci.json b/libraries/Matter/examples/MatterOnOffLight/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterOnOffLight/ci.json +++ b/libraries/Matter/examples/MatterOnOffLight/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterOnOffPlugin/ci.json b/libraries/Matter/examples/MatterOnOffPlugin/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterOnOffPlugin/ci.json +++ b/libraries/Matter/examples/MatterOnOffPlugin/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterPressureSensor/ci.json b/libraries/Matter/examples/MatterPressureSensor/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterPressureSensor/ci.json +++ b/libraries/Matter/examples/MatterPressureSensor/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterSmartButon/ci.json b/libraries/Matter/examples/MatterSmartButon/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterSmartButon/ci.json +++ b/libraries/Matter/examples/MatterSmartButon/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterTemperatureLight/ci.json b/libraries/Matter/examples/MatterTemperatureLight/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterTemperatureLight/ci.json +++ b/libraries/Matter/examples/MatterTemperatureLight/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterTemperatureSensor/ci.json b/libraries/Matter/examples/MatterTemperatureSensor/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterTemperatureSensor/ci.json +++ b/libraries/Matter/examples/MatterTemperatureSensor/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/MatterThermostat/ci.json b/libraries/Matter/examples/MatterThermostat/ci.json index 556a8a9ee6b..90b393f9156 100644 --- a/libraries/Matter/examples/MatterThermostat/ci.json +++ b/libraries/Matter/examples/MatterThermostat/ci.json @@ -1,7 +1,6 @@ { "fqbn_append": "PartitionScheme=huge_app", "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" ] } diff --git a/libraries/Matter/examples/WiFiProvWithinMatter/WiFiProvWithinMatter.ino b/libraries/Matter/examples/WiFiProvWithinMatter/WiFiProvWithinMatter.ino deleted file mode 100644 index 3434217624d..00000000000 --- a/libraries/Matter/examples/WiFiProvWithinMatter/WiFiProvWithinMatter.ino +++ /dev/null @@ -1,152 +0,0 @@ -/* - Please read README.md file in this folder, or on the web: - https://github.com/espressif/arduino-esp32/tree/master/libraries/WiFiProv/examples/WiFiProv - - Note: This sketch takes up a lot of space for the app and may not be able to flash with default setting on some chips. - If you see Error like this: "Sketch too big" - In Arduino IDE go to: Tools > Partition scheme > chose anything that has more than 1.4MB APP - - for example "No OTA (2MB APP/2MB SPIFFS)" - - This example demonstrates that it is possible to provision WiFi using BLE or Software AP using - the ESP BLE Prov APP or ESP SoftAP Provisioning APP from Android Play or/and iOS APP Store - - Once the WiFi is provisioned, Matter will start its process as usual. - - This same Example could be used for any other WiFi Provisioning method. -*/ - -// Matter Manager -#include -#include -#include - -#if !CONFIG_BLUEDROID_ENABLED -#define USE_SOFT_AP // ESP32-S2 has no BLE, therefore, it shall use SoftAP Provisioning -#endif -//#define USE_SOFT_AP // Uncomment if you want to enforce using the Soft AP method instead of BLE - -const char *pop = "abcd1234"; // Proof of possession - otherwise called a PIN - string provided by the device, entered by the user in the phone app -const char *service_name = "PROV_123"; // Name of your device (the Espressif apps expects by default device name starting with "Prov_") -const char *service_key = NULL; // Password used for SofAP method (NULL = no password needed) -bool reset_provisioned = true; // When true the library will automatically delete previously provisioned data. - -// List of Matter Endpoints for this Node -// Single On/Off Light Endpoint - at least one per node -MatterOnOffLight OnOffLight; - -// Light GPIO that can be controlled by Matter APP -#ifdef LED_BUILTIN -const uint8_t ledPin = LED_BUILTIN; -#else -const uint8_t ledPin = 2; // Set your pin here if your board has not defined LED_BUILTIN -#endif - -// set your board USER BUTTON pin here - decommissioning button -const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button. - -// Button control - decommision the Matter Node -uint32_t button_time_stamp = 0; // debouncing control -bool button_state = false; // false = released | true = pressed -const uint32_t decommissioningTimeout = 5000; // keep the button pressed for 5s, or longer, to decommission - -// Matter Protocol Endpoint (On/OFF Light) Callback -bool matterCB(bool state) { - digitalWrite(ledPin, state ? HIGH : LOW); - // This callback must return the success state to Matter core - return true; -} - -void setup() { - // Initialize the USER BUTTON (Boot button) that will be used to decommission the Matter Node - pinMode(buttonPin, INPUT_PULLUP); - - Serial.begin(115200); - // Initialize the LED GPIO - pinMode(ledPin, OUTPUT); - - WiFi.begin(); // no SSID/PWD - get it from the Provisioning APP or from NVS (last successful connection) - - // BLE Provisioning using the ESP SoftAP Prov works fine for any BLE SoC, including ESP32, ESP32S3 and ESP32C3. -#if CONFIG_BLUEDROID_ENABLED && !defined(USE_SOFT_AP) - Serial.println("Begin Provisioning using BLE"); - // Sample uuid that user can pass during provisioning using BLE - uint8_t uuid[16] = {0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02}; - WiFiProv.beginProvision( - NETWORK_PROV_SCHEME_BLE, NETWORK_PROV_SCHEME_HANDLER_FREE_BLE, NETWORK_PROV_SECURITY_1, pop, service_name, service_key, uuid, reset_provisioned - ); - Serial.println("You may use this BLE QRCode:"); - WiFiProv.printQR(service_name, pop, "ble"); -#else - Serial.println("Begin Provisioning using Soft AP"); - WiFiProv.beginProvision(NETWORK_PROV_SCHEME_SOFTAP, NETWORK_PROV_SCHEME_HANDLER_NONE, NETWORK_PROV_SECURITY_1, pop, service_name, service_key); - Serial.println("You may use this WiFi QRCode:"); - WiFiProv.printQR(service_name, pop, "softap"); -#endif - - // Wait for WiFi connection - uint32_t counter = 0; - while (WiFi.status() != WL_CONNECTED) { - // resets the device after 10 minutes - if (counter > 2 * 60 * 10) { - Serial.println("\r\n================================================"); - Serial.println("Already 10 minutes past. The device will reboot."); - Serial.println("================================================\r\n"); - Serial.flush(); // wait until the Serial has sent the whole message. - ESP.restart(); - } - // WiFi searching feedback - Serial.print("."); - delay(500); - // adds a new line every 30 seconds - counter++; - if (!(counter % 60)) { - Serial.println(); - } - } - - // WiFi shall be connected by now - Serial.println(); - - // Initialize at least one Matter EndPoint - OnOffLight.begin(); - - // Associate a callback to the Matter Controller - OnOffLight.onChange(matterCB); - - // Matter beginning - Last step, after all EndPoints are initialized - Matter.begin(); - - while (!Matter.isDeviceCommissioned()) { - Serial.println("Matter Node is not commissioned yet."); - Serial.println("Initiate the device discovery in your Matter environment."); - Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); - Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str()); - Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str()); - Serial.println(); - // waits 30 seconds for Matter Commissioning, keeping it blocked until done - delay(30000); - } -} - -void loop() { - // Check if the button has been pressed - if (digitalRead(buttonPin) == LOW && !button_state) { - // deals with button debouncing - button_time_stamp = millis(); // record the time while the button is pressed. - button_state = true; // pressed. - } - - if (digitalRead(buttonPin) == HIGH && button_state) { - button_state = false; // released - } - - // Onboard User Button is kept pressed for longer than 5 seconds in order to decommission matter node - uint32_t time_diff = millis() - button_time_stamp; - if (button_state && time_diff > decommissioningTimeout) { - Serial.println("Decommissioning the Light Matter Accessory. It shall be commissioned again."); - Matter.decommission(); - button_time_stamp = millis(); // avoid running decommissining again, reboot takes a second or so - } - - delay(500); -} diff --git a/libraries/Matter/examples/WiFiProvWithinMatter/ci.json b/libraries/Matter/examples/WiFiProvWithinMatter/ci.json deleted file mode 100644 index 0665800b12b..00000000000 --- a/libraries/Matter/examples/WiFiProvWithinMatter/ci.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "fqbn_append": "PartitionScheme=huge_app", - "requires": [ - "CONFIG_SOC_WIFI_SUPPORTED=y", - "CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y" - ] -} diff --git a/libraries/Matter/keywords.txt b/libraries/Matter/keywords.txt index 68aaebb1d4d..6c2e092e417 100644 --- a/libraries/Matter/keywords.txt +++ b/libraries/Matter/keywords.txt @@ -36,8 +36,10 @@ EndPointSpeedCB KEYWORD1 EndPointOnOffCB KEYWORD1 EndPointBrightnessCB KEYWORD1 EndPointRGBColorCB KEYWORD1 +EndPointIdentifyCB KEYWORD1 matterEvent_t KEYWORD1 matterEventCB KEYWORD1 +attrOperation_t KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -111,6 +113,12 @@ onChangeLocalTemperature KEYWORD2 onChangeCoolingSetpoint KEYWORD2 onChangeHeatingSetpoint KEYWORD2 onEvent KEYWORD2 +setEndPointId KEYWORD2 +getEndPointId KEYWORD2 +getSecondaryNetworkEndPointId KEYWORD2 +createSecondaryNetworkInterface KEYWORD2 +onIdentify KEYWORD2 +endpointIdentifyCB KEYWORD2 ####################################### # Constants (LITERAL1) From 0a45a0614244002f82fe7f006effeda7c8075469 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Sat, 12 Jul 2025 02:12:36 -0300 Subject: [PATCH 076/102] feat(wire): std::functional Wire slave callback functions (#11582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR enhances the Wire library to support std::function–based callbacks for I2C slave mode, enabling the use of lambdas and captured contexts. - Replaces raw function pointers in TwoWire and HardwareI2C with std::function for onRequest and onReceive - Updates constructors, method signatures, and default initializations to use std::function - Adds new example sketch, CI config, and documentation updates demonstrating the functional callback API --- cores/esp32/HardwareI2C.h | 6 +- docs/en/api/i2c.rst | 135 +++++++++++++++++- .../WireSlaveFunctionalCallback.ino | 37 +++++ .../WireSlaveFunctionalCallback/ci.json | 5 + libraries/Wire/src/Wire.cpp | 6 +- libraries/Wire/src/Wire.h | 12 +- 6 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 libraries/Wire/examples/WireSlaveFunctionalCallback/WireSlaveFunctionalCallback.ino create mode 100644 libraries/Wire/examples/WireSlaveFunctionalCallback/ci.json diff --git a/cores/esp32/HardwareI2C.h b/cores/esp32/HardwareI2C.h index 65b7e2036b2..c44f34e1ee7 100644 --- a/cores/esp32/HardwareI2C.h +++ b/cores/esp32/HardwareI2C.h @@ -20,6 +20,7 @@ #include #include "Stream.h" +#include class HardwareI2C : public Stream { public: @@ -36,6 +37,7 @@ class HardwareI2C : public Stream { virtual size_t requestFrom(uint8_t address, size_t len, bool stopBit) = 0; virtual size_t requestFrom(uint8_t address, size_t len) = 0; - virtual void onReceive(void (*)(int)) = 0; - virtual void onRequest(void (*)(void)) = 0; + // Update base class to use std::function + virtual void onReceive(const std::function &) = 0; + virtual void onRequest(const std::function &) = 0; }; diff --git a/docs/en/api/i2c.rst b/docs/en/api/i2c.rst index eac04b76a23..06d4d1953a6 100644 --- a/docs/en/api/i2c.rst +++ b/docs/en/api/i2c.rst @@ -347,20 +347,147 @@ This function will return ``true`` if the peripheral was initialized correctly. onReceive ^^^^^^^^^ -The ``onReceive`` function is used to define the callback for the data received from the master. +The ``onReceive`` function is used to define the callback for data received from the master device. .. code-block:: arduino - void onReceive( void (*)(int) ); + void onReceive(const std::function& callback); + +**Function Signature:** + +The callback function must have the signature ``void(int numBytes)`` where ``numBytes`` indicates how many bytes were received from the master. + +**Usage Examples:** + +.. code-block:: arduino + + // Method 1: Regular function + void handleReceive(int numBytes) { + Serial.printf("Received %d bytes: ", numBytes); + while (Wire.available()) { + char c = Wire.read(); + Serial.print(c); + } + Serial.println(); + } + Wire.onReceive(handleReceive); + + // Method 2: Lambda function + Wire.onReceive([](int numBytes) { + Serial.printf("Master sent %d bytes\n", numBytes); + while (Wire.available()) { + uint8_t data = Wire.read(); + // Process received data + Serial.printf("Data: 0x%02X\n", data); + } + }); + + // Method 3: Lambda with capture (for accessing variables) + int deviceId = 42; + Wire.onReceive([deviceId](int numBytes) { + Serial.printf("Device %d received %d bytes\n", deviceId, numBytes); + // Process data... + }); + + // Method 4: Using std::function variable + std::function receiveHandler = [](int bytes) { + Serial.printf("Handling %d received bytes\n", bytes); + }; + Wire.onReceive(receiveHandler); + + // Method 5: Class member function (using lambda wrapper) + class I2CDevice { + private: + int deviceAddress; + public: + I2CDevice(int addr) : deviceAddress(addr) {} + + void handleReceive(int numBytes) { + Serial.printf("Device 0x%02X received %d bytes\n", deviceAddress, numBytes); + } + + void setup() { + Wire.onReceive([this](int bytes) { + this->handleReceive(bytes); + }); + } + }; + +.. note:: + The ``onReceive`` callback is triggered when the I2C master sends data to this slave device. + Use ``Wire.available()`` and ``Wire.read()`` inside the callback to retrieve the received data. onRequest ^^^^^^^^^ -The ``onRequest`` function is used to define the callback for the data to be send to the master. +The ``onRequest`` function is used to define the callback for responding to master read requests. + +.. code-block:: arduino + + void onRequest(const std::function& callback); + +**Function Signature:** + +The callback function must have the signature ``void()`` with no parameters. This callback is triggered when the master requests data from this slave device. + +**Usage Examples:** .. code-block:: arduino - void onRequest( void (*)(void) ); + // Method 1: Regular function + void handleRequest() { + static int counter = 0; + Wire.printf("Response #%d", counter++); + } + Wire.onRequest(handleRequest); + + // Method 2: Lambda function + Wire.onRequest([]() { + // Send sensor data to master + int sensorValue = analogRead(A0); + Wire.write(sensorValue >> 8); // High byte + Wire.write(sensorValue & 0xFF); // Low byte + }); + + // Method 3: Lambda with capture (for accessing variables) + int deviceStatus = 1; + String deviceName = "Sensor1"; + Wire.onRequest([&deviceStatus, &deviceName]() { + Wire.write(deviceStatus); + Wire.write(deviceName.c_str(), deviceName.length()); + }); + + // Method 4: Using std::function variable + std::function requestHandler = []() { + Wire.write("Hello Master!"); + }; + Wire.onRequest(requestHandler); + + // Method 5: Class member function (using lambda wrapper) + class TemperatureSensor { + private: + float temperature; + public: + void updateTemperature() { + temperature = 25.5; // Read from actual sensor + } + + void sendTemperature() { + // Convert float to bytes and send + uint8_t* tempBytes = (uint8_t*)&temperature; + Wire.write(tempBytes, sizeof(float)); + } + + void setup() { + Wire.onRequest([this]() { + this->sendTemperature(); + }); + } + }; + +.. note:: + The ``onRequest`` callback is triggered when the I2C master requests data from this slave device. + Use ``Wire.write()`` inside the callback to send response data back to the master. slaveWrite ^^^^^^^^^^ diff --git a/libraries/Wire/examples/WireSlaveFunctionalCallback/WireSlaveFunctionalCallback.ino b/libraries/Wire/examples/WireSlaveFunctionalCallback/WireSlaveFunctionalCallback.ino new file mode 100644 index 00000000000..a18fd2f023e --- /dev/null +++ b/libraries/Wire/examples/WireSlaveFunctionalCallback/WireSlaveFunctionalCallback.ino @@ -0,0 +1,37 @@ +// This example demonstrates the use of functional callbacks with the Wire library +// for I2C slave communication. It shows how to handle requests and data reception + +#include "Wire.h" + +#define I2C_DEV_ADDR 0x55 + +uint32_t i = 0; + +void setup() { + Serial.begin(115200); + Serial.setDebugOutput(true); + + Wire.onRequest([]() { + Wire.print(i++); + Wire.print(" Packets."); + Serial.println("onRequest"); + }); + + Wire.onReceive([](int len) { + Serial.printf("onReceive[%d]: ", len); + while (Wire.available()) { + Serial.write(Wire.read()); + } + Serial.println(); + }); + + Wire.begin((uint8_t)I2C_DEV_ADDR); + +#if CONFIG_IDF_TARGET_ESP32 + char message[64]; + snprintf(message, 64, "%lu Packets.", i++); + Wire.slaveWrite((uint8_t *)message, strlen(message)); +#endif +} + +void loop() {} diff --git a/libraries/Wire/examples/WireSlaveFunctionalCallback/ci.json b/libraries/Wire/examples/WireSlaveFunctionalCallback/ci.json new file mode 100644 index 00000000000..3c877975d62 --- /dev/null +++ b/libraries/Wire/examples/WireSlaveFunctionalCallback/ci.json @@ -0,0 +1,5 @@ +{ + "requires": [ + "CONFIG_SOC_I2C_SUPPORT_SLAVE=y" + ] +} diff --git a/libraries/Wire/src/Wire.cpp b/libraries/Wire/src/Wire.cpp index 34c814b5117..cda098d2d5b 100644 --- a/libraries/Wire/src/Wire.cpp +++ b/libraries/Wire/src/Wire.cpp @@ -48,7 +48,7 @@ TwoWire::TwoWire(uint8_t bus_num) #endif #if SOC_I2C_SUPPORT_SLAVE , - is_slave(false), user_onRequest(NULL), user_onReceive(NULL) + is_slave(false), user_onRequest(nullptr), user_onReceive(nullptr) #endif /* SOC_I2C_SUPPORT_SLAVE */ { } @@ -596,14 +596,14 @@ void TwoWire::flush() { //i2cFlush(num); // cleanup } -void TwoWire::onReceive(void (*function)(int)) { +void TwoWire::onReceive(const std::function &function) { #if SOC_I2C_SUPPORT_SLAVE user_onReceive = function; #endif } // sets function called on slave read -void TwoWire::onRequest(void (*function)(void)) { +void TwoWire::onRequest(const std::function &function) { #if SOC_I2C_SUPPORT_SLAVE user_onRequest = function; #endif diff --git a/libraries/Wire/src/Wire.h b/libraries/Wire/src/Wire.h index b84aa5b2131..9cebdfaa304 100644 --- a/libraries/Wire/src/Wire.h +++ b/libraries/Wire/src/Wire.h @@ -48,10 +48,6 @@ #ifndef I2C_BUFFER_LENGTH #define I2C_BUFFER_LENGTH 128 // Default size, if none is set using Wire::setBuffersize(size_t) #endif -#if SOC_I2C_SUPPORT_SLAVE -typedef void (*user_onRequest)(void); -typedef void (*user_onReceive)(uint8_t *, int); -#endif /* SOC_I2C_SUPPORT_SLAVE */ class TwoWire : public HardwareI2C { protected: @@ -77,8 +73,8 @@ class TwoWire : public HardwareI2C { private: #if SOC_I2C_SUPPORT_SLAVE bool is_slave; - void (*user_onRequest)(void); - void (*user_onReceive)(int); + std::function user_onRequest; + std::function user_onReceive; static void onRequestService(uint8_t, void *); static void onReceiveService(uint8_t, uint8_t *, size_t, bool, void *); #endif /* SOC_I2C_SUPPORT_SLAVE */ @@ -116,8 +112,8 @@ class TwoWire : public HardwareI2C { size_t requestFrom(uint8_t address, size_t len, bool stopBit) override; size_t requestFrom(uint8_t address, size_t len) override; - void onReceive(void (*)(int)) override; - void onRequest(void (*)(void)) override; + void onReceive(const std::function &) override; + void onRequest(const std::function &) override; //call setPins() first, so that begin() can be called without arguments from libraries bool setPins(int sda, int scl); From 1f0d4b5dc0d6008583ac9527907325b9f2506caa Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:17:56 -0300 Subject: [PATCH 077/102] ci(gitlab): Initial GitLab setup (#11577) * ci(gitlab): Initial GitLab setup * fix(version): Add to version update script * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .github/scripts/update-version.sh | 5 +++++ .gitlab-ci.yml | 25 +++++++++++++++++++++++++ .gitlab/workflows/common.yml | 26 ++++++++++++++++++++++++++ .gitlab/workflows/sample.yml | 6 ++++++ 5 files changed, 63 insertions(+) create mode 100644 .gitlab-ci.yml create mode 100644 .gitlab/workflows/common.yml create mode 100644 .gitlab/workflows/sample.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 75a2b46d619..64d241ba20a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,7 @@ # CI /.github/ @lucasssvaz @me-no-dev @P-R-O-C-H-Y +/.gitlab/ @lucasssvaz /tests/ @lucasssvaz @P-R-O-C-H-Y # Tools diff --git a/.github/scripts/update-version.sh b/.github/scripts/update-version.sh index 622f2fe8ff8..59a95d01105 100755 --- a/.github/scripts/update-version.sh +++ b/.github/scripts/update-version.sh @@ -45,6 +45,11 @@ cat docs/conf_common.py | \ sed "s/.. |version| replace:: .*/.. |version| replace:: $ESP_ARDUINO_VERSION/g" | \ sed "s/.. |idf_version| replace:: .*/.. |idf_version| replace:: $ESP_IDF_VERSION/g" > docs/__conf_common.py && mv docs/__conf_common.py docs/conf_common.py +echo "Updating .gitlab/workflows/common.yml..." +cat .gitlab/workflows/common.yml | \ +sed "s/ESP_IDF_VERSION:.*/ESP_IDF_VERSION: \"$ESP_IDF_VERSION\"/g" | \ +sed "s/ESP_ARDUINO_VERSION:.*/ESP_ARDUINO_VERSION: \"$ESP_ARDUINO_VERSION\"/g" > .gitlab/workflows/__common.yml && mv .gitlab/workflows/__common.yml .gitlab/workflows/common.yml + echo "Updating cores/esp32/esp_arduino_version.h..." cat cores/esp32/esp_arduino_version.h | \ sed "s/#define ESP_ARDUINO_VERSION_MAJOR.*/#define ESP_ARDUINO_VERSION_MAJOR $ESP_ARDUINO_VERSION_MAJOR/g" | \ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000000..89a45022bc2 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,25 @@ +workflow: + rules: + # Disable those non-protected push triggered pipelines + - if: '$CI_COMMIT_REF_NAME != "master" && $CI_COMMIT_BRANCH !~ /^release\/v/ && $CI_COMMIT_TAG !~ /^\d+\.\d+(\.\d+)?($|-)/ && $CI_PIPELINE_SOURCE == "push"' + when: never + # when running merged result pipelines, CI_COMMIT_SHA represents the temp commit it created. + # Please use PIPELINE_COMMIT_SHA at all places that require a commit sha of the original commit. + - if: $CI_OPEN_MERGE_REQUESTS != null + variables: + PIPELINE_COMMIT_SHA: $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA + IS_MR_PIPELINE: 1 + - if: $CI_OPEN_MERGE_REQUESTS == null + variables: + PIPELINE_COMMIT_SHA: $CI_COMMIT_SHA + IS_MR_PIPELINE: 0 + - if: '$CI_PIPELINE_SOURCE == "schedule"' + variables: + IS_SCHEDULED_RUN: "true" + - when: always + +# Place the default settings in `.gitlab/workflows/common.yml` instead + +include: + - ".gitlab/workflows/common.yml" + - ".gitlab/workflows/sample.yml" diff --git a/.gitlab/workflows/common.yml b/.gitlab/workflows/common.yml new file mode 100644 index 00000000000..9086da018ab --- /dev/null +++ b/.gitlab/workflows/common.yml @@ -0,0 +1,26 @@ +##################### +# Default Variables # +##################### + +stages: + - pre_check + - build + - test + - result + +variables: + ESP_IDF_VERSION: "5.4" + ESP_ARDUINO_VERSION: "3.2.1" + +############# +# `default` # +############# + +default: + retry: + max: 2 + when: + # In case of a runner failure we could hop to another one, or a network error could go away. + - runner_system_failure + # Job execution timeout may be caused by a network issue. + - job_execution_timeout diff --git a/.gitlab/workflows/sample.yml b/.gitlab/workflows/sample.yml new file mode 100644 index 00000000000..32b6fce042d --- /dev/null +++ b/.gitlab/workflows/sample.yml @@ -0,0 +1,6 @@ +hello-world: + stage: test + script: + - echo "Hello, World from GitLab CI!" + rules: + - if: $CI_PIPELINE_SOURCE == "push" From 82d56bc679dfcd8de2fb06596ff4c4bcca8e6247 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Wed, 16 Jul 2025 05:51:31 -0300 Subject: [PATCH 078/102] feat(gpio): new functional interrupt lambda example (#11589) * feat(gpio): new functional interrupt lambda example * fix(readme): schematic diagram allignment * fix(example): uses volatile for ISR variables Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(example): uses volatile for ISR variables Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(example): uses volatile data type also for the pointers * fix(readme): clear documentation * feat(example): improves ISR execution time Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(gpio): simplifies the example and documentation * feat(gpio): uses IRAM lambda and fixes volatile operation * fix(doc): fixing documentation apresentation * ci(pre-commit): Apply automatic fixes * fix(ci): Update README.md --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Me No Dev --- .../FunctionalInterruptLambda.ino | 157 ++++++++++++++++++ .../GPIO/FunctionalInterruptLambda/README.md | 147 ++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/FunctionalInterruptLambda.ino create mode 100644 libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/README.md diff --git a/libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/FunctionalInterruptLambda.ino b/libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/FunctionalInterruptLambda.ino new file mode 100644 index 00000000000..57d35383f17 --- /dev/null +++ b/libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/FunctionalInterruptLambda.ino @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ESP32 Lambda FunctionalInterrupt Example + ======================================== + + This example demonstrates how to use lambda functions with FunctionalInterrupt + for GPIO pin interrupt callbacks on ESP32. It shows CHANGE mode detection + with LED toggle functionality and proper debouncing. + + Hardware Setup: + - Use BOOT Button or connect a button between BUTTON_PIN and GND (with internal pullup) + - Use Builtin Board LED or connect an LED with resistor to GPIO 2 (LED_PIN) + + Features Demonstrated: + 1. CHANGE mode lambda to detect both RISING and FALLING edges + 2. LED toggle on button press (FALLING edge) + 3. Edge type detection using digitalRead() within ISR + 4. Hardware debouncing with configurable timeout + + IMPORTANT NOTE ABOUT ESP32 INTERRUPT BEHAVIOR: + - Only ONE interrupt handler can be attached per GPIO pin at a time + - Calling attachInterrupt() on a pin that already has an interrupt will override the previous one + - This applies regardless of edge type (RISING, FALLING, CHANGE) + - If you need both RISING and FALLING detection on the same pin, use CHANGE mode + and determine the edge type within your handler by reading the pin state +*/ + +#include +#include + +// Pin definitions +#define BUTTON_PIN BOOT_PIN // BOOT BUTTON - change as needed +#ifdef LED_BUILTIN +#define LED_PIN LED_BUILTIN +#else +#warning Using LED_PIN = GPIO 2 as default - change as needed +#define LED_PIN 2 // change as needed +#endif + +// Global variables for interrupt handling (volatile for ISR safety) +volatile uint32_t buttonPressCount = 0; +volatile uint32_t buttonReleaseCount = 0; +volatile bool buttonPressed = false; +volatile bool buttonReleased = false; +volatile bool ledState = false; +volatile bool ledStateChanged = false; // Flag to indicate LED needs updating + +// Debouncing variables (volatile for ISR safety) +volatile unsigned long lastButtonInterruptTime = 0; +const unsigned long DEBOUNCE_DELAY_MS = 50; // 50ms debounce delay + +// State-based debouncing to prevent hysteresis issues +volatile bool lastButtonState = HIGH; // Track last stable state (HIGH = released) + +// Global lambda function (declared at file scope) - ISR in IRAM +IRAM_ATTR std::function changeModeLambda = []() { + // Simple debouncing: check if enough time has passed since last interrupt + unsigned long currentTime = millis(); + if (currentTime - lastButtonInterruptTime < DEBOUNCE_DELAY_MS) { + return; // Ignore this interrupt due to bouncing + } + + // Read current pin state to determine edge type + bool currentState = digitalRead(BUTTON_PIN); + + // State-based debouncing: only process if state actually changed + if (currentState == lastButtonState) { + return; // No real state change, ignore (hysteresis/noise) + } + + // Update timing and state + lastButtonInterruptTime = currentTime; + lastButtonState = currentState; + + if (currentState == LOW) { + // FALLING edge detected (button pressed) - set flag for main loop + // volatile variables require use of temporary value transfer + uint32_t temp = buttonPressCount + 1; + buttonPressCount = temp; + buttonPressed = true; + ledStateChanged = true; // Signal main loop to toggle LED + } else { + // RISING edge detected (button released) - set flag for main loop + // volatile variables require use of temporary value transfer + uint32_t temp = buttonReleaseCount + 1; + buttonReleaseCount = temp; + buttonReleased = true; + } +}; + +void setup() { + Serial.begin(115200); + delay(1000); // Allow serial monitor to connect + + Serial.println("ESP32 Lambda FunctionalInterrupt Example"); + Serial.println("========================================"); + + // Configure pins + pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // CHANGE mode lambda to handle both RISING and FALLING edges + // This toggles the LED on button press (FALLING edge) + Serial.println("Setting up CHANGE mode lambda for LED toggle"); + + // Use the global lambda function + attachInterrupt(BUTTON_PIN, changeModeLambda, CHANGE); + + Serial.println(); + Serial.printf("Lambda interrupt configured on Pin %d (CHANGE mode)\r\n", BUTTON_PIN); + Serial.printf("Debounce delay: %lu ms\r\n", DEBOUNCE_DELAY_MS); + Serial.println(); + Serial.println("Press the button to toggle the LED!"); + Serial.println("Button press (FALLING edge) will toggle the LED."); + Serial.println("Button release (RISING edge) will be detected and reported."); + Serial.println("Button includes debouncing to prevent mechanical bounce issues."); + Serial.println(); +} + +void loop() { + // Handle LED state changes (ISR-safe approach) + if (ledStateChanged) { + ledStateChanged = false; + ledState = !ledState; // Toggle LED state in main loop + digitalWrite(LED_PIN, ledState); + } + + // Check for button presses + if (buttonPressed) { + buttonPressed = false; + Serial.printf("==> Button PRESSED! Count: %lu, LED: %s (FALLING edge)\r\n", buttonPressCount, ledState ? "ON" : "OFF"); + } + + // Check for button releases + if (buttonReleased) { + buttonReleased = false; + Serial.printf("==> Button RELEASED! Count: %lu (RISING edge)\r\n", buttonReleaseCount); + } + + delay(10); +} diff --git a/libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/README.md b/libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/README.md new file mode 100644 index 00000000000..9488c317fde --- /dev/null +++ b/libraries/ESP32/examples/GPIO/FunctionalInterruptLambda/README.md @@ -0,0 +1,147 @@ +# ESP32 Lambda FunctionalInterrupt Example + +This example demonstrates how to use lambda functions with FunctionalInterrupt for GPIO pin interrupt callbacks on ESP32. It shows CHANGE mode detection with LED toggle functionality and proper debouncing. + +## Features Demonstrated + +1. **CHANGE mode lambda** to detect both RISING and FALLING edges +2. **LED toggle on button press** (FALLING edge) +3. **Edge type detection** using digitalRead() within ISR +4. **Hardware debouncing** with configurable timeout +5. **IRAM_ATTR lambda declaration** for optimal ISR performance in RAM + +## Hardware Setup + +- Use BOOT Button or connect a button between BUTTON_PIN and GND (with internal pullup) +- Use Builtin Board LED (no special hardware setup) or connect an LED with resistor to GPIO assigned as LED_PIN.\ + Some boards have an RGB LED that needs no special hardware setup to work as a simple white on/off LED. + +``` +ESP32 Board Button/LED +----------- --------- +BOOT_PIN ------------ [BUTTON] ---- GND +LED_PIN --------------- [LED] ----- GND + ¦ + [330O] (*) Only needed when using an external LED attached to the GPIO. + ¦ + 3V3 +``` + +## Important ESP32 Interrupt Behavior + +**CRITICAL:** Only ONE interrupt handler can be attached per GPIO pin at a time on ESP32. + +- Calling `attachInterrupt()` on a pin that already has an interrupt will **override** the previous one +- This applies regardless of edge type (RISING, FALLING, CHANGE) +- If you need both RISING and FALLING detection on the same pin, use **CHANGE mode** and determine the edge type within your handler by reading the pin state + +## Code Overview + +This example demonstrates a simple CHANGE mode lambda interrupt that: + +- **Detects both button press and release** using a single interrupt handler +- **Toggles LED only on button press** (FALLING edge) +- **Reports both press and release events** to Serial output +- **Uses proper debouncing** to prevent switch bounce issues +- **Implements minimal lambda captures** for simplicity + +## Lambda Function Pattern + +### CHANGE Mode Lambda with IRAM Declaration +```cpp +// Global lambda declared with IRAM_ATTR for optimal ISR performance +IRAM_ATTR std::function changeModeLambda = []() { + // Debouncing check + unsigned long currentTime = millis(); + if (currentTime - lastButtonInterruptTime < DEBOUNCE_DELAY_MS) { + return; // Ignore bouncing + } + + // Determine edge type + bool currentState = digitalRead(BUTTON_PIN); + if (currentState == lastButtonState) { + return; // No real state change + } + + // Update state and handle edges + lastButtonInterruptTime = currentTime; + lastButtonState = currentState; + + if (currentState == LOW) { + // Button pressed (FALLING edge) + buttonPressCount++; + buttonPressed = true; + ledStateChanged = true; // Signal LED toggle + } else { + // Button released (RISING edge) + buttonReleaseCount++; + buttonReleased = true; + } +}; + +attachInterrupt(BUTTON_PIN, changeModeLambda, CHANGE); +``` + +## Key Concepts + +### Edge Detection in CHANGE Mode +```cpp +if (digitalRead(pin) == LOW) { + // FALLING edge detected (button pressed) +} else { + // RISING edge detected (button released) +} +``` + +### Debouncing Strategy +This example implements dual-layer debouncing: +1. **Time-based**: Ignores interrupts within 50 ms of previous one +2. **State-based**: Only processes actual state changes + +### Main Loop Processing +```cpp +void loop() { + // Handle LED changes safely outside ISR + if (ledStateChanged) { + ledStateChanged = false; + ledState = !ledState; + digitalWrite(LED_PIN, ledState); + } + + // Report button events + if (buttonPressed) { + // Handle press event + } + if (buttonReleased) { + // Handle release event + } +} +``` + +## Expected Output + +``` +ESP32 Lambda FunctionalInterrupt Example +======================================== +Setting up CHANGE mode lambda for LED toggle + +Lambda interrupt configured on Pin 0 (CHANGE mode) +Debounce delay: 50 ms + +Press the button to toggle the LED! +Button press (FALLING edge) will toggle the LED. +Button release (RISING edge) will be detected and reported. +Button includes debouncing to prevent mechanical bounce issues. + +==> Button PRESSED! Count: 1, LED: ON (FALLING edge) +==> Button RELEASED! Count: 1 (RISING edge) +==> Button PRESSED! Count: 2, LED: OFF (FALLING edge) +==> Button RELEASED! Count: 2 (RISING edge) +``` + +## Pin Configuration + +The example uses these default pins: + +- `BUTTON_PIN`: BOOT_PIN (automatically assigned by the Arduino Core) +- `LED_PIN`: LED_BUILTIN (may not be available for your board - please verify it) From ce7ef9c2ba61deff89d86913a966d33c070fb3a4 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 16 Jul 2025 13:42:07 +0300 Subject: [PATCH 079/102] IDF release/v5.5 cf8dad07 (#11601) IDF release/v5.5 cf8dad07 --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 9c2c754504e..6e2dd0ab337 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-adb3f2a5-v1" + "version": "idf-release_v5.5-cf8dad07-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-adb3f2a5-v1", + "version": "idf-release_v5.5-cf8dad07-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-adb3f2a5-v1.zip", - "checksum": "SHA-256:c7bdda06e7ddae51880fc0e1c76114a8e766a9d682e3645e7794f7126b895a94", - "size": "430508851" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", + "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", + "size": "430423461" } ] }, From ac05f18720e83d1e9deee24db7ab13718219b4d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:12:43 +0000 Subject: [PATCH 080/102] ci(pre-commit): Apply automatic fixes --- cores/esp32/esp32-hal-cpu.c | 3 ++- libraries/SPI/src/SPI.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cores/esp32/esp32-hal-cpu.c b/cores/esp32/esp32-hal-cpu.c index d6bc6b0f191..e9113da4219 100644 --- a/cores/esp32/esp32-hal-cpu.c +++ b/cores/esp32/esp32-hal-cpu.c @@ -19,7 +19,8 @@ #include "esp_attr.h" #include "esp_log.h" #include "soc/rtc.h" -#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) && !defined(CONFIG_IDF_TARGET_ESP32C5) +#if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32P4) \ + && !defined(CONFIG_IDF_TARGET_ESP32C5) #include "soc/rtc_cntl_reg.h" #include "soc/syscon_reg.h" #endif diff --git a/libraries/SPI/src/SPI.cpp b/libraries/SPI/src/SPI.cpp index 0492065b798..6229f887553 100644 --- a/libraries/SPI/src/SPI.cpp +++ b/libraries/SPI/src/SPI.cpp @@ -84,7 +84,8 @@ bool SPIClass::begin(int8_t sck, int8_t miso, int8_t mosi, int8_t ss) { _miso = (_spi_num == FSPI) ? MISO : -1; _mosi = (_spi_num == FSPI) ? MOSI : -1; _ss = (_spi_num == FSPI) ? SS : -1; -#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32C5 +#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32P4 \ + || CONFIG_IDF_TARGET_ESP32C5 _sck = SCK; _miso = MISO; _mosi = MOSI; From 6015fd73e0d9d065c80fc79ef0f35d3273a217de Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Wed, 16 Jul 2025 13:53:36 -0300 Subject: [PATCH 081/102] feat(openthread): native API extension (#11598) * feat(openthread): native API extension * fix(openthread): wrong return type and parameter * fix(openthread): wrong field reference * fix(openthread): CR/LF fix * feat(openthread): print leader RLOC information * feat(openthread): code improvements * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../LeaderNode/LeaderNode.ino | 85 +++- .../RouterNode/RouterNode.ino | 61 ++- libraries/OpenThread/keywords.txt | 18 + libraries/OpenThread/src/OThread.cpp | 363 +++++++++++++++++- libraries/OpenThread/src/OThread.h | 63 ++- 5 files changed, 565 insertions(+), 25 deletions(-) diff --git a/libraries/OpenThread/examples/Native/SimpleThreadNetwork/LeaderNode/LeaderNode.ino b/libraries/OpenThread/examples/Native/SimpleThreadNetwork/LeaderNode/LeaderNode.ino index dfea9776838..b3c4091e1dc 100644 --- a/libraries/OpenThread/examples/Native/SimpleThreadNetwork/LeaderNode/LeaderNode.ino +++ b/libraries/OpenThread/examples/Native/SimpleThreadNetwork/LeaderNode/LeaderNode.ino @@ -3,6 +3,9 @@ OpenThread threadLeaderNode; DataSet dataset; +// Track last known device role for state change detection +ot_device_role_t lastKnownRole = OT_ROLE_DISABLED; + void setup() { Serial.begin(115200); @@ -27,8 +30,84 @@ void setup() { } void loop() { - // Print network information every 5 seconds - Serial.println("=============================================="); - threadLeaderNode.otPrintNetworkInformation(Serial); + // Get current device role + ot_device_role_t currentRole = threadLeaderNode.otGetDeviceRole(); + + // Only print network information when not detached + if (currentRole != OT_ROLE_DETACHED && currentRole != OT_ROLE_DISABLED) { + Serial.println("=============================================="); + Serial.println("OpenThread Network Information:"); + + // Basic network information + Serial.printf("Role: %s\r\n", threadLeaderNode.otGetStringDeviceRole()); + Serial.printf("RLOC16: 0x%04x\r\n", threadLeaderNode.getRloc16()); + Serial.printf("Network Name: %s\r\n", threadLeaderNode.getNetworkName().c_str()); + Serial.printf("Channel: %d\r\n", threadLeaderNode.getChannel()); + Serial.printf("PAN ID: 0x%04x\r\n", threadLeaderNode.getPanId()); + + // Extended PAN ID + const uint8_t *extPanId = threadLeaderNode.getExtendedPanId(); + if (extPanId) { + Serial.print("Extended PAN ID: "); + for (int i = 0; i < OT_EXT_PAN_ID_SIZE; i++) { + Serial.printf("%02x", extPanId[i]); + } + Serial.println(); + } + + // Network Key + const uint8_t *networkKey = threadLeaderNode.getNetworkKey(); + if (networkKey) { + Serial.print("Network Key: "); + for (int i = 0; i < OT_NETWORK_KEY_SIZE; i++) { + Serial.printf("%02x", networkKey[i]); + } + Serial.println(); + } + + // Mesh Local EID + IPAddress meshLocalEid = threadLeaderNode.getMeshLocalEid(); + Serial.printf("Mesh Local EID: %s\r\n", meshLocalEid.toString().c_str()); + + // Leader RLOC + IPAddress leaderRloc = threadLeaderNode.getLeaderRloc(); + Serial.printf("Leader RLOC: %s\r\n", leaderRloc.toString().c_str()); + + // Node RLOC + IPAddress nodeRloc = threadLeaderNode.getRloc(); + Serial.printf("Node RLOC: %s\r\n", nodeRloc.toString().c_str()); + + // Demonstrate address listing with two different methods: + // Method 1: Unicast addresses using counting API (individual access) + Serial.println("\r\n--- Unicast Addresses (Using Count + Index API) ---"); + size_t unicastCount = threadLeaderNode.getUnicastAddressCount(); + for (size_t i = 0; i < unicastCount; i++) { + IPAddress addr = threadLeaderNode.getUnicastAddress(i); + Serial.printf(" [%zu]: %s\r\n", i, addr.toString().c_str()); + } + + // Method 2: Multicast addresses using std::vector (bulk access) + Serial.println("\r\n--- Multicast Addresses (Using std::vector API) ---"); + std::vector allMulticast = threadLeaderNode.getAllMulticastAddresses(); + for (size_t i = 0; i < allMulticast.size(); i++) { + Serial.printf(" [%zu]: %s\r\n", i, allMulticast[i].toString().c_str()); + } + + // Check for role change and clear cache if needed (only when active) + if (currentRole != lastKnownRole) { + Serial.printf( + "Role changed from %s to %s - clearing address cache\r\n", (lastKnownRole < 5) ? otRoleString[lastKnownRole] : "Unknown", + threadLeaderNode.otGetStringDeviceRole() + ); + threadLeaderNode.clearAllAddressCache(); + lastKnownRole = currentRole; + } + } else { + Serial.printf("Thread Node Status: %s - Waiting for thread network start...\r\n", threadLeaderNode.otGetStringDeviceRole()); + + // Update role tracking even when detached/disabled, but don't clear cache + lastKnownRole = currentRole; + } + delay(5000); } diff --git a/libraries/OpenThread/examples/Native/SimpleThreadNetwork/RouterNode/RouterNode.ino b/libraries/OpenThread/examples/Native/SimpleThreadNetwork/RouterNode/RouterNode.ino index 5ffa535ad51..a8959792f5b 100644 --- a/libraries/OpenThread/examples/Native/SimpleThreadNetwork/RouterNode/RouterNode.ino +++ b/libraries/OpenThread/examples/Native/SimpleThreadNetwork/RouterNode/RouterNode.ino @@ -22,8 +22,63 @@ void setup() { } void loop() { - // Print network information every 5 seconds - Serial.println("=============================================="); - threadChildNode.otPrintNetworkInformation(Serial); + // Get current device role + ot_device_role_t currentRole = threadChildNode.otGetDeviceRole(); + + // Only print detailed network information when node is active + if (currentRole != OT_ROLE_DETACHED && currentRole != OT_ROLE_DISABLED) { + Serial.println("=============================================="); + Serial.println("OpenThread Network Information (Active Dataset):"); + + // Get and display the current active dataset + const DataSet &activeDataset = threadChildNode.getCurrentDataSet(); + + Serial.printf("Role: %s\r\n", threadChildNode.otGetStringDeviceRole()); + Serial.printf("RLOC16: 0x%04x\r\n", threadChildNode.getRloc16()); + + // Dataset information + Serial.printf("Network Name: %s\r\n", activeDataset.getNetworkName()); + Serial.printf("Channel: %d\r\n", activeDataset.getChannel()); + Serial.printf("PAN ID: 0x%04x\r\n", activeDataset.getPanId()); + + // Extended PAN ID from dataset + const uint8_t *extPanId = activeDataset.getExtendedPanId(); + if (extPanId) { + Serial.print("Extended PAN ID: "); + for (int i = 0; i < OT_EXT_PAN_ID_SIZE; i++) { + Serial.printf("%02x", extPanId[i]); + } + Serial.println(); + } + + // Network Key from dataset + const uint8_t *networkKey = activeDataset.getNetworkKey(); + if (networkKey) { + Serial.print("Network Key: "); + for (int i = 0; i < OT_NETWORK_KEY_SIZE; i++) { + Serial.printf("%02x", networkKey[i]); + } + Serial.println(); + } + + // Additional runtime information + IPAddress meshLocalEid = threadChildNode.getMeshLocalEid(); + Serial.printf("Mesh Local EID: %s\r\n", meshLocalEid.toString().c_str()); + + IPAddress nodeRloc = threadChildNode.getRloc(); + Serial.printf("Node RLOC: %s\r\n", nodeRloc.toString().c_str()); + + IPAddress leaderRloc = threadChildNode.getLeaderRloc(); + Serial.printf("Leader RLOC: %s\r\n", leaderRloc.toString().c_str()); + + Serial.println(); + + } else { + Serial.println("=============================================="); + Serial.printf("Thread Node Status: %s - Waiting for thread network start...\r\n", threadChildNode.otGetStringDeviceRole()); + + Serial.println(); + } + delay(5000); } diff --git a/libraries/OpenThread/keywords.txt b/libraries/OpenThread/keywords.txt index b62c2c23ddc..99821ce401c 100644 --- a/libraries/OpenThread/keywords.txt +++ b/libraries/OpenThread/keywords.txt @@ -13,6 +13,7 @@ OpenThread KEYWORD1 DataSet KEYWORD1 ot_cmd_return_t KEYWORD1 ot_device_role_t KEYWORD1 +OnReceiveCb_t KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -59,6 +60,23 @@ stop KEYWORD2 networkInterfaceUp KEYWORD2 networkInterfaceDown KEYWORD2 commitDataSet KEYWORD2 +getInstance KEYWORD2 +getCurrentDataSet KEYWORD2 +getMeshLocalPrefix KEYWORD2 +getMeshLocalEid KEYWORD2 +getLeaderRloc KEYWORD2 +getRloc KEYWORD2 +getRloc16 KEYWORD2 +getUnicastAddressCount KEYWORD2 +getUnicastAddress KEYWORD2 +getAllUnicastAddresses KEYWORD2 +getMulticastAddressCount KEYWORD2 +getMulticastAddress KEYWORD2 +getAllMulticastAddresses KEYWORD2 +clearUnicastAddressCache KEYWORD2 +clearMulticastAddressCache KEYWORD2 +clearAllAddressCache KEYWORD2 +end KEYWORD2 ####################################### # Constants (LITERAL1) diff --git a/libraries/OpenThread/src/OThread.cpp b/libraries/OpenThread/src/OThread.cpp index 7714d870cce..87b748d104d 100644 --- a/libraries/OpenThread/src/OThread.cpp +++ b/libraries/OpenThread/src/OThread.cpp @@ -2,6 +2,8 @@ #if SOC_IEEE802154_SUPPORTED #if CONFIG_OPENTHREAD_ENABLED +#include "IPAddress.h" +#include #include "esp_err.h" #include "esp_event.h" #include "esp_netif.h" @@ -132,16 +134,29 @@ const otOperationalDataset &DataSet::getDataset() const { } void DataSet::setNetworkName(const char *name) { - strncpy(mDataset.mNetworkName.m8, name, sizeof(mDataset.mNetworkName.m8)); + if (!name) { + log_w("Network name is null"); + return; + } + // char m8[OT_NETWORK_KEY_SIZE + 1] bytes space by definition + strncpy(mDataset.mNetworkName.m8, name, OT_NETWORK_KEY_SIZE); mDataset.mComponents.mIsNetworkNamePresent = true; } void DataSet::setExtendedPanId(const uint8_t *extPanId) { + if (!extPanId) { + log_w("Extended PAN ID is null"); + return; + } memcpy(mDataset.mExtendedPanId.m8, extPanId, OT_EXT_PAN_ID_SIZE); mDataset.mComponents.mIsExtendedPanIdPresent = true; } void DataSet::setNetworkKey(const uint8_t *key) { + if (!key) { + log_w("Network key is null"); + return; + } memcpy(mDataset.mNetworkKey.m8, key, OT_NETWORK_KEY_SIZE); mDataset.mComponents.mIsNetworkKeyPresent = true; } @@ -181,10 +196,18 @@ void DataSet::apply(otInstance *instance) { } // OpenThread Implementation -bool OpenThread::otStarted = false; +bool OpenThread::otStarted; +otInstance *OpenThread::mInstance; +DataSet OpenThread::mCurrentDataset; +otNetworkKey OpenThread::mNetworkKey; -otInstance *OpenThread::mInstance = nullptr; -OpenThread::OpenThread() {} +OpenThread::OpenThread() { + // static initialization (node data and stack starting information) + otStarted = false; + mCurrentDataset.clear(); // Initialize the current dataset + memset(&mNetworkKey, 0, sizeof(mNetworkKey)); // Initialize the network key + mInstance = nullptr; +} OpenThread::~OpenThread() { end(); @@ -214,13 +237,7 @@ void OpenThread::begin(bool OThreadAutoStart) { return; } log_d("OpenThread task created successfully"); - // get the OpenThread instance that will be used for all operations - mInstance = esp_openthread_get_instance(); - if (!mInstance) { - log_e("Error: Failed to initialize OpenThread instance"); - end(); - return; - } + // starts Thread with default dataset from NVS or from IDF default settings if (OThreadAutoStart) { otOperationalDatasetTlvs dataset; @@ -238,23 +255,46 @@ void OpenThread::begin(bool OThreadAutoStart) { log_i("AUTO start OpenThread done"); } } + + // get the OpenThread instance that will be used for all operations + mInstance = esp_openthread_get_instance(); + if (!mInstance) { + log_e("Error: Failed to initialize OpenThread instance"); + end(); + return; + } + otStarted = true; } void OpenThread::end() { + if (!otStarted) { + log_w("OpenThread already stopped"); + return; + } + if (s_ot_task != NULL) { vTaskDelete(s_ot_task); s_ot_task = NULL; - // Clean up - esp_openthread_deinit(); - esp_openthread_netif_glue_deinit(); -#if CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM - ot_lwip_netif = NULL; -#endif + } + + // Clean up in reverse order of initialization + if (openthread_netif != NULL) { esp_netif_destroy(openthread_netif); - esp_vfs_eventfd_unregister(); + openthread_netif = NULL; } + + esp_openthread_netif_glue_deinit(); + esp_openthread_deinit(); + esp_vfs_eventfd_unregister(); + +#if CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM + ot_lwip_netif = NULL; +#endif + + mInstance = nullptr; otStarted = false; + log_d("OpenThread ended successfully"); } void OpenThread::start() { @@ -262,6 +302,7 @@ void OpenThread::start() { log_w("Error: OpenThread instance not initialized"); return; } + clearAllAddressCache(); // Clear cache when starting network otThreadSetEnabled(mInstance, true); log_d("Thread network started"); } @@ -271,6 +312,7 @@ void OpenThread::stop() { log_w("Error: OpenThread instance not initialized"); return; } + clearAllAddressCache(); // Clear cache when stopping network otThreadSetEnabled(mInstance, false); log_d("Thread network stopped"); } @@ -285,6 +327,7 @@ void OpenThread::networkInterfaceUp() { if (error != OT_ERROR_NONE) { log_e("Error: Failed to enable Thread interface (error code: %d)\n", error); } + clearAllAddressCache(); // Clear cache when interface comes up log_d("OpenThread Network Interface is up"); } @@ -312,6 +355,7 @@ void OpenThread::commitDataSet(const DataSet &dataset) { log_e("Error: Failed to commit dataset (error code: %d)\n", error); return; } + clearAllAddressCache(); // Clear cache when dataset changes log_d("Dataset committed successfully"); } @@ -360,6 +404,289 @@ void OpenThread::otPrintNetworkInformation(Stream &output) { output.println(); } +// Get the Node Network Name +String OpenThread::getNetworkName() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return String(); // Return empty String, not nullptr + } + const char *networkName = otThreadGetNetworkName(mInstance); + return networkName ? String(networkName) : String(); +} + +// Get the Node Extended PAN ID +const uint8_t *OpenThread::getExtendedPanId() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return nullptr; + } + const otExtendedPanId *extPanId = otThreadGetExtendedPanId(mInstance); + return extPanId ? extPanId->m8 : nullptr; +} + +// Get the Node Network Key +const uint8_t *OpenThread::getNetworkKey() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return nullptr; + } + otThreadGetNetworkKey(mInstance, &mNetworkKey); + return mNetworkKey.m8; +} + +// Get the Node Channel +uint8_t OpenThread::getChannel() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return 0; + } + return otLinkGetChannel(mInstance); +} + +// Get the Node PAN ID +uint16_t OpenThread::getPanId() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return 0; + } + return otLinkGetPanId(mInstance); +} + +// Get the OpenThread instance +otInstance *OpenThread::getInstance() { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return nullptr; + } + return mInstance; +} + +// Get the current dataset +const DataSet &OpenThread::getCurrentDataSet() const { + + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + mCurrentDataset.clear(); + return mCurrentDataset; + } + + otOperationalDataset dataset; + otError error = otDatasetGetActive(mInstance, &dataset); + + if (error == OT_ERROR_NONE) { + mCurrentDataset.clear(); + + if (dataset.mComponents.mIsNetworkNamePresent) { + mCurrentDataset.setNetworkName(dataset.mNetworkName.m8); + } + if (dataset.mComponents.mIsExtendedPanIdPresent) { + mCurrentDataset.setExtendedPanId(dataset.mExtendedPanId.m8); + } + if (dataset.mComponents.mIsNetworkKeyPresent) { + mCurrentDataset.setNetworkKey(dataset.mNetworkKey.m8); + } + if (dataset.mComponents.mIsChannelPresent) { + mCurrentDataset.setChannel(dataset.mChannel); + } + if (dataset.mComponents.mIsPanIdPresent) { + mCurrentDataset.setPanId(dataset.mPanId); + } + } else { + log_w("Failed to get active dataset (error: %d)", error); + mCurrentDataset.clear(); + } + + return mCurrentDataset; +} + +// Get the Mesh Local Prefix +const otMeshLocalPrefix *OpenThread::getMeshLocalPrefix() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return nullptr; + } + return otThreadGetMeshLocalPrefix(mInstance); +} + +// Get the Mesh-Local EID +IPAddress OpenThread::getMeshLocalEid() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return IPAddress(IPv6); // Return empty IPv6 address + } + const otIp6Address *otAddr = otThreadGetMeshLocalEid(mInstance); + if (!otAddr) { + log_w("Failed to get Mesh Local EID"); + return IPAddress(IPv6); + } + return IPAddress(IPv6, otAddr->mFields.m8); +} + +// Get the Thread Leader RLOC +IPAddress OpenThread::getLeaderRloc() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return IPAddress(IPv6); // Return empty IPv6 address + } + otIp6Address otAddr; + otError error = otThreadGetLeaderRloc(mInstance, &otAddr); + if (error != OT_ERROR_NONE) { + log_w("Failed to get Leader RLOC"); + return IPAddress(IPv6); + } + return IPAddress(IPv6, otAddr.mFields.m8); +} + +// Get the Node RLOC +IPAddress OpenThread::getRloc() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return IPAddress(IPv6); // Return empty IPv6 address + } + const otIp6Address *otAddr = otThreadGetRloc(mInstance); + if (!otAddr) { + log_w("Failed to get Node RLOC"); + return IPAddress(IPv6); + } + return IPAddress(IPv6, otAddr->mFields.m8); +} + +// Get the RLOC16 ID +uint16_t OpenThread::getRloc16() const { + if (!mInstance) { + log_w("Error: OpenThread instance not initialized"); + return 0; + } + return otThreadGetRloc16(mInstance); +} + +// Populate unicast address cache from OpenThread +void OpenThread::populateUnicastAddressCache() const { + if (!mInstance) { + return; + } + + // Clear existing cache + mCachedUnicastAddresses.clear(); + + // Populate unicast addresses cache + const otNetifAddress *addr = otIp6GetUnicastAddresses(mInstance); + while (addr != nullptr) { + mCachedUnicastAddresses.push_back(IPAddress(IPv6, addr->mAddress.mFields.m8)); + addr = addr->mNext; + } + + log_d("Populated unicast address cache with %zu addresses", mCachedUnicastAddresses.size()); +} + +// Populate multicast address cache from OpenThread +void OpenThread::populateMulticastAddressCache() const { + if (!mInstance) { + return; + } + + // Clear existing cache + mCachedMulticastAddresses.clear(); + + // Populate multicast addresses cache + const otNetifMulticastAddress *mAddr = otIp6GetMulticastAddresses(mInstance); + while (mAddr != nullptr) { + mCachedMulticastAddresses.push_back(IPAddress(IPv6, mAddr->mAddress.mFields.m8)); + mAddr = mAddr->mNext; + } + + log_d("Populated multicast address cache with %zu addresses", mCachedMulticastAddresses.size()); +} + +// Clear unicast address cache +void OpenThread::clearUnicastAddressCache() const { + mCachedUnicastAddresses.clear(); + log_d("Cleared unicast address cache"); +} + +// Clear multicast address cache +void OpenThread::clearMulticastAddressCache() const { + mCachedMulticastAddresses.clear(); + log_d("Cleared multicast address cache"); +} + +// Clear all address caches +void OpenThread::clearAllAddressCache() const { + mCachedUnicastAddresses.clear(); + mCachedMulticastAddresses.clear(); + log_d("Cleared all address caches"); +} + +// Get count of unicast addresses +size_t OpenThread::getUnicastAddressCount() const { + // Populate cache if empty + if (mCachedUnicastAddresses.empty()) { + populateUnicastAddressCache(); + } + + return mCachedUnicastAddresses.size(); +} + +// Get unicast address by index +IPAddress OpenThread::getUnicastAddress(size_t index) const { + // Populate cache if empty + if (mCachedUnicastAddresses.empty()) { + populateUnicastAddressCache(); + } + + if (index >= mCachedUnicastAddresses.size()) { + log_w("Unicast address index %zu out of range (max: %zu)", index, mCachedUnicastAddresses.size()); + return IPAddress(IPv6); + } + + return mCachedUnicastAddresses[index]; +} + +// Get all unicast addresses +std::vector OpenThread::getAllUnicastAddresses() const { + // Populate cache if empty + if (mCachedUnicastAddresses.empty()) { + populateUnicastAddressCache(); + } + + return mCachedUnicastAddresses; // Return copy of cached vector +} + +// Get count of multicast addresses +size_t OpenThread::getMulticastAddressCount() const { + // Populate cache if empty + if (mCachedMulticastAddresses.empty()) { + populateMulticastAddressCache(); + } + + return mCachedMulticastAddresses.size(); +} + +// Get multicast address by index +IPAddress OpenThread::getMulticastAddress(size_t index) const { + // Populate cache if empty + if (mCachedMulticastAddresses.empty()) { + populateMulticastAddressCache(); + } + + if (index >= mCachedMulticastAddresses.size()) { + log_w("Multicast address index %zu out of range (max: %zu)", index, mCachedMulticastAddresses.size()); + return IPAddress(IPv6); + } + + return mCachedMulticastAddresses[index]; +} + +// Get all multicast addresses +std::vector OpenThread::getAllMulticastAddresses() const { + // Populate cache if empty + if (mCachedMulticastAddresses.empty()) { + populateMulticastAddressCache(); + } + + return mCachedMulticastAddresses; // Return copy of cached vector +} + OpenThread OThread; #endif /* CONFIG_OPENTHREAD_ENABLED */ diff --git a/libraries/OpenThread/src/OThread.h b/libraries/OpenThread/src/OThread.h index 359d581bb9d..6e21b854574 100644 --- a/libraries/OpenThread/src/OThread.h +++ b/libraries/OpenThread/src/OThread.h @@ -25,6 +25,8 @@ #include #include #include +#include "IPAddress.h" +#include typedef enum { OT_ROLE_DISABLED = 0, ///< The Thread stack is disabled. @@ -96,9 +98,68 @@ class OpenThread { // Set the dataset void commitDataSet(const DataSet &dataset); + // Get the Node Network Name + String getNetworkName() const; + + // Get the Node Extended PAN ID + const uint8_t *getExtendedPanId() const; + + // Get the Node Network Key + const uint8_t *getNetworkKey() const; + + // Get the Node Channel + uint8_t getChannel() const; + + // Get the Node PAN ID + uint16_t getPanId() const; + + // Get the OpenThread instance + otInstance *getInstance(); + + // Get the current dataset + const DataSet &getCurrentDataSet() const; + + // Get the Mesh Local Prefix + const otMeshLocalPrefix *getMeshLocalPrefix() const; + + // Get the Mesh-Local EID + IPAddress getMeshLocalEid() const; + + // Get the Thread Leader RLOC + IPAddress getLeaderRloc() const; + + // Get the Node RLOC + IPAddress getRloc() const; + + // Get the RLOC16 ID + uint16_t getRloc16() const; + + // Address management with caching + size_t getUnicastAddressCount() const; + IPAddress getUnicastAddress(size_t index) const; + std::vector getAllUnicastAddresses() const; + + size_t getMulticastAddressCount() const; + IPAddress getMulticastAddress(size_t index) const; + std::vector getAllMulticastAddresses() const; + + // Cache management + void clearUnicastAddressCache() const; + void clearMulticastAddressCache() const; + void clearAllAddressCache() const; + private: static otInstance *mInstance; - DataSet mCurrentDataSet; + static DataSet mCurrentDataset; // Current dataset being used by the OpenThread instance. + static otNetworkKey mNetworkKey; // Static storage to persist after function return + + // Address caching for performance (user-controlled) + mutable std::vector mCachedUnicastAddresses; + mutable std::vector mCachedMulticastAddresses; + + // Internal cache management + void populateUnicastAddressCache() const; + void populateMulticastAddressCache() const; }; extern OpenThread OThread; From a69c71f6ad771790ed4716fba063930f804806f6 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Wed, 16 Jul 2025 19:55:53 +0300 Subject: [PATCH 082/102] feat(core): Update core version to 3.3.0 --- .gitlab/workflows/common.yml | 4 ++-- cores/esp32/esp_arduino_version.h | 4 ++-- docs/conf_common.py | 4 ++-- libraries/ArduinoOTA/library.properties | 2 +- libraries/AsyncUDP/library.properties | 2 +- libraries/BLE/library.properties | 2 +- libraries/BluetoothSerial/library.properties | 2 +- libraries/DNSServer/library.properties | 2 +- libraries/EEPROM/library.properties | 2 +- libraries/ESP32/library.properties | 2 +- libraries/ESP_I2S/library.properties | 2 +- libraries/ESP_NOW/library.properties | 2 +- libraries/ESP_SR/library.properties | 2 +- libraries/ESPmDNS/library.properties | 2 +- libraries/Ethernet/library.properties | 2 +- libraries/FFat/library.properties | 2 +- libraries/FS/library.properties | 2 +- libraries/HTTPClient/library.properties | 2 +- libraries/HTTPUpdate/library.properties | 2 +- libraries/HTTPUpdateServer/library.properties | 2 +- libraries/Insights/library.properties | 2 +- libraries/LittleFS/library.properties | 2 +- libraries/Matter/library.properties | 2 +- libraries/NetBIOS/library.properties | 2 +- libraries/Network/library.properties | 2 +- libraries/NetworkClientSecure/library.properties | 2 +- libraries/OpenThread/library.properties | 2 +- libraries/PPP/library.properties | 2 +- libraries/Preferences/library.properties | 2 +- libraries/RainMaker/library.properties | 2 +- libraries/SD/library.properties | 2 +- libraries/SD_MMC/library.properties | 2 +- libraries/SPI/library.properties | 2 +- libraries/SPIFFS/library.properties | 2 +- libraries/SimpleBLE/library.properties | 2 +- libraries/TFLiteMicro/library.properties | 2 +- libraries/Ticker/library.properties | 2 +- libraries/USB/library.properties | 2 +- libraries/Update/library.properties | 2 +- libraries/WebServer/library.properties | 2 +- libraries/WiFi/library.properties | 2 +- libraries/WiFiProv/library.properties | 2 +- libraries/Wire/library.properties | 2 +- libraries/Zigbee/library.properties | 2 +- package.json | 2 +- platform.txt | 2 +- 46 files changed, 49 insertions(+), 49 deletions(-) diff --git a/.gitlab/workflows/common.yml b/.gitlab/workflows/common.yml index 9086da018ab..611e1d974f4 100644 --- a/.gitlab/workflows/common.yml +++ b/.gitlab/workflows/common.yml @@ -9,8 +9,8 @@ stages: - result variables: - ESP_IDF_VERSION: "5.4" - ESP_ARDUINO_VERSION: "3.2.1" + ESP_IDF_VERSION: "5.5" + ESP_ARDUINO_VERSION: "3.3.0" ############# # `default` # diff --git a/cores/esp32/esp_arduino_version.h b/cores/esp32/esp_arduino_version.h index 97bb3ac794b..120377c61f7 100644 --- a/cores/esp32/esp_arduino_version.h +++ b/cores/esp32/esp_arduino_version.h @@ -21,9 +21,9 @@ extern "C" { /** Major version number (X.x.x) */ #define ESP_ARDUINO_VERSION_MAJOR 3 /** Minor version number (x.X.x) */ -#define ESP_ARDUINO_VERSION_MINOR 2 +#define ESP_ARDUINO_VERSION_MINOR 3 /** Patch version number (x.x.X) */ -#define ESP_ARDUINO_VERSION_PATCH 1 +#define ESP_ARDUINO_VERSION_PATCH 0 /** * Macro to convert ARDUINO version number into an integer diff --git a/docs/conf_common.py b/docs/conf_common.py index af1d615f753..10d4bd715b2 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -4,8 +4,8 @@ # Used for substituting variables in the documentation rst_prolog = """ -.. |version| replace:: 3.2.1 -.. |idf_version| replace:: 5.4 +.. |version| replace:: 3.3.0 +.. |idf_version| replace:: 5.5 """ languages = ["en"] diff --git a/libraries/ArduinoOTA/library.properties b/libraries/ArduinoOTA/library.properties index 18de5aa2180..3a3f7e111e0 100644 --- a/libraries/ArduinoOTA/library.properties +++ b/libraries/ArduinoOTA/library.properties @@ -1,5 +1,5 @@ name=ArduinoOTA -version=3.2.1 +version=3.3.0 author=Ivan Grokhotkov and Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables Over The Air upgrades, via wifi and espota.py UDP request/TCP download. diff --git a/libraries/AsyncUDP/library.properties b/libraries/AsyncUDP/library.properties index c64c60d0421..ddf9f79b6d9 100644 --- a/libraries/AsyncUDP/library.properties +++ b/libraries/AsyncUDP/library.properties @@ -1,5 +1,5 @@ name=ESP32 Async UDP -version=3.2.1 +version=3.3.0 author=Me-No-Dev maintainer=Me-No-Dev sentence=Async UDP Library for ESP32 diff --git a/libraries/BLE/library.properties b/libraries/BLE/library.properties index 335ab2fc965..c6fae19a4a1 100644 --- a/libraries/BLE/library.properties +++ b/libraries/BLE/library.properties @@ -1,5 +1,5 @@ name=BLE -version=3.2.1 +version=3.3.0 author=Neil Kolban maintainer=lucasssvaz sentence=BLE functions for ESP32 diff --git a/libraries/BluetoothSerial/library.properties b/libraries/BluetoothSerial/library.properties index 4bc1427e3f8..49211bf3b63 100644 --- a/libraries/BluetoothSerial/library.properties +++ b/libraries/BluetoothSerial/library.properties @@ -1,5 +1,5 @@ name=BluetoothSerial -version=3.2.1 +version=3.3.0 author=Evandro Copercini maintainer=Evandro Copercini sentence=Simple UART to Classical Bluetooth bridge for ESP32 diff --git a/libraries/DNSServer/library.properties b/libraries/DNSServer/library.properties index 42e4c38dc9d..c193b919d02 100644 --- a/libraries/DNSServer/library.properties +++ b/libraries/DNSServer/library.properties @@ -1,5 +1,5 @@ name=DNSServer -version=3.2.1 +version=3.3.0 author=Kristijan Novoselić maintainer=Kristijan Novoselić, sentence=A simple DNS server for ESP32. diff --git a/libraries/EEPROM/library.properties b/libraries/EEPROM/library.properties index ee1caae0792..6d69f52a085 100644 --- a/libraries/EEPROM/library.properties +++ b/libraries/EEPROM/library.properties @@ -1,5 +1,5 @@ name=EEPROM -version=3.2.1 +version=3.3.0 author=Ivan Grokhotkov maintainer=Paolo Becchi sentence=Enables reading and writing data a sequential, addressable FLASH storage diff --git a/libraries/ESP32/library.properties b/libraries/ESP32/library.properties index 0815a69ce6b..e664022388d 100644 --- a/libraries/ESP32/library.properties +++ b/libraries/ESP32/library.properties @@ -1,5 +1,5 @@ name=ESP32 -version=3.2.1 +version=3.3.0 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 sketches examples diff --git a/libraries/ESP_I2S/library.properties b/libraries/ESP_I2S/library.properties index ff1ad5d59da..b2763f4e7e8 100644 --- a/libraries/ESP_I2S/library.properties +++ b/libraries/ESP_I2S/library.properties @@ -1,5 +1,5 @@ name=ESP_I2S -version=3.2.1 +version=3.3.0 author=me-no-dev maintainer=me-no-dev sentence=Library for ESP I2S communication diff --git a/libraries/ESP_NOW/library.properties b/libraries/ESP_NOW/library.properties index 426a9464ace..f8e627dbc03 100644 --- a/libraries/ESP_NOW/library.properties +++ b/libraries/ESP_NOW/library.properties @@ -1,5 +1,5 @@ name=ESP_NOW -version=3.2.1 +version=3.3.0 author=me-no-dev maintainer=P-R-O-C-H-Y sentence=Library for ESP_NOW diff --git a/libraries/ESP_SR/library.properties b/libraries/ESP_SR/library.properties index 3b9777bca8f..9d9787b7931 100644 --- a/libraries/ESP_SR/library.properties +++ b/libraries/ESP_SR/library.properties @@ -1,5 +1,5 @@ name=ESP_SR -version=3.2.1 +version=3.3.0 author=me-no-dev maintainer=me-no-dev sentence=Library for ESP Sound Recognition diff --git a/libraries/ESPmDNS/library.properties b/libraries/ESPmDNS/library.properties index b99a5f58e5c..062d3b90b51 100644 --- a/libraries/ESPmDNS/library.properties +++ b/libraries/ESPmDNS/library.properties @@ -1,5 +1,5 @@ name=ESPmDNS -version=3.2.1 +version=3.3.0 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 mDNS Library diff --git a/libraries/Ethernet/library.properties b/libraries/Ethernet/library.properties index cd2d8ead018..28f2a8697d9 100644 --- a/libraries/Ethernet/library.properties +++ b/libraries/Ethernet/library.properties @@ -1,5 +1,5 @@ name=Ethernet -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables network connection (local and Internet) using the ESP32 Ethernet. diff --git a/libraries/FFat/library.properties b/libraries/FFat/library.properties index 898982536b0..25b8c4e8acd 100644 --- a/libraries/FFat/library.properties +++ b/libraries/FFat/library.properties @@ -1,5 +1,5 @@ name=FFat -version=3.2.1 +version=3.3.0 author=Hristo Gochkov, Ivan Grokhtkov, Larry Bernstone maintainer=Hristo Gochkov sentence=ESP32 FAT on Flash File System diff --git a/libraries/FS/library.properties b/libraries/FS/library.properties index fe86d613bdf..0f05f1134d5 100644 --- a/libraries/FS/library.properties +++ b/libraries/FS/library.properties @@ -1,5 +1,5 @@ name=FS -version=3.2.1 +version=3.3.0 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 File System diff --git a/libraries/HTTPClient/library.properties b/libraries/HTTPClient/library.properties index 76da4c857e1..bb5b0936255 100644 --- a/libraries/HTTPClient/library.properties +++ b/libraries/HTTPClient/library.properties @@ -1,5 +1,5 @@ name=HTTPClient -version=3.2.1 +version=3.3.0 author=Markus Sattler maintainer=Markus Sattler sentence=HTTP Client for ESP32 diff --git a/libraries/HTTPUpdate/library.properties b/libraries/HTTPUpdate/library.properties index dfe0474c3f3..88466a3a72e 100644 --- a/libraries/HTTPUpdate/library.properties +++ b/libraries/HTTPUpdate/library.properties @@ -1,5 +1,5 @@ name=HTTPUpdate -version=3.2.1 +version=3.3.0 author=Markus Sattler maintainer=Markus Sattler sentence=Http Update for ESP32 diff --git a/libraries/HTTPUpdateServer/library.properties b/libraries/HTTPUpdateServer/library.properties index 90aa966d27f..c182eeb8d7f 100644 --- a/libraries/HTTPUpdateServer/library.properties +++ b/libraries/HTTPUpdateServer/library.properties @@ -1,5 +1,5 @@ name=HTTPUpdateServer -version=3.2.1 +version=3.3.0 author=Hristo Kapanakov maintainer= sentence=Simple HTTP Update server based on the WebServer diff --git a/libraries/Insights/library.properties b/libraries/Insights/library.properties index c1ad9c72ce1..3ef98d25be6 100644 --- a/libraries/Insights/library.properties +++ b/libraries/Insights/library.properties @@ -1,5 +1,5 @@ name=ESP Insights -version=3.2.1 +version=3.3.0 author=Sanket Wadekar maintainer=Sanket Wadekar sentence=ESP Insights diff --git a/libraries/LittleFS/library.properties b/libraries/LittleFS/library.properties index e00ed49c312..202d8ad4a6d 100644 --- a/libraries/LittleFS/library.properties +++ b/libraries/LittleFS/library.properties @@ -1,5 +1,5 @@ name=LittleFS -version=3.2.1 +version=3.3.0 author= maintainer= sentence=LittleFS for esp32 diff --git a/libraries/Matter/library.properties b/libraries/Matter/library.properties index b601fce0ff5..0b140bfa169 100644 --- a/libraries/Matter/library.properties +++ b/libraries/Matter/library.properties @@ -1,5 +1,5 @@ name=Matter -version=3.2.1 +version=3.3.0 author=Rodrigo Garcia | GitHub @SuGlider maintainer=Rodrigo Garcia sentence=Library for supporting Matter environment on ESP32. diff --git a/libraries/NetBIOS/library.properties b/libraries/NetBIOS/library.properties index a4bbf93c0ed..71d22d6f363 100644 --- a/libraries/NetBIOS/library.properties +++ b/libraries/NetBIOS/library.properties @@ -1,5 +1,5 @@ name=NetBIOS -version=3.2.1 +version=3.3.0 author=Pablo@xpablo.cz maintainer=Hristo Gochkov sentence=Enables NBNS (NetBIOS) name resolution. diff --git a/libraries/Network/library.properties b/libraries/Network/library.properties index f2284981704..f7e04f4de3b 100644 --- a/libraries/Network/library.properties +++ b/libraries/Network/library.properties @@ -1,5 +1,5 @@ name=Networking -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=General network management library. diff --git a/libraries/NetworkClientSecure/library.properties b/libraries/NetworkClientSecure/library.properties index 31af7a1bc8c..6ebf4c0ec70 100644 --- a/libraries/NetworkClientSecure/library.properties +++ b/libraries/NetworkClientSecure/library.properties @@ -1,5 +1,5 @@ name=NetworkClientSecure -version=3.2.1 +version=3.3.0 author=Evandro Luis Copercini maintainer=Github Community sentence=Enables secure network connection (local and Internet) using the ESP32 built-in WiFi. diff --git a/libraries/OpenThread/library.properties b/libraries/OpenThread/library.properties index 2687b1dcd1c..550d4eb1627 100644 --- a/libraries/OpenThread/library.properties +++ b/libraries/OpenThread/library.properties @@ -1,5 +1,5 @@ name=OpenThread -version=3.2.1 +version=3.3.0 author=Rodrigo Garcia | GitHub @SuGlider maintainer=Rodrigo Garcia sentence=Library for OpenThread Network on ESP32. diff --git a/libraries/PPP/library.properties b/libraries/PPP/library.properties index c39647cbe56..537708b1261 100644 --- a/libraries/PPP/library.properties +++ b/libraries/PPP/library.properties @@ -1,5 +1,5 @@ name=PPP -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables network connection using GSM Modem. diff --git a/libraries/Preferences/library.properties b/libraries/Preferences/library.properties index 6e0d38348c0..0a7e678aa6c 100644 --- a/libraries/Preferences/library.properties +++ b/libraries/Preferences/library.properties @@ -1,5 +1,5 @@ name=Preferences -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Provides friendly access to ESP32's Non-Volatile Storage diff --git a/libraries/RainMaker/library.properties b/libraries/RainMaker/library.properties index 71b4082a0a7..1d72a8faff5 100644 --- a/libraries/RainMaker/library.properties +++ b/libraries/RainMaker/library.properties @@ -1,5 +1,5 @@ name=ESP RainMaker -version=3.2.1 +version=3.3.0 author=Sweety Mhaiske maintainer=Hristo Gochkov sentence=ESP RainMaker Support diff --git a/libraries/SD/library.properties b/libraries/SD/library.properties index cc51196ed54..9d868dce799 100644 --- a/libraries/SD/library.properties +++ b/libraries/SD/library.properties @@ -1,5 +1,5 @@ name=SD -version=3.2.1 +version=3.3.0 author=Arduino, SparkFun maintainer=Arduino sentence=Enables reading and writing on SD cards. For all Arduino boards. diff --git a/libraries/SD_MMC/library.properties b/libraries/SD_MMC/library.properties index 590eb8ebc52..f96ee4377c2 100644 --- a/libraries/SD_MMC/library.properties +++ b/libraries/SD_MMC/library.properties @@ -1,5 +1,5 @@ name=SD_MMC -version=3.2.1 +version=3.3.0 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 SDMMC File System diff --git a/libraries/SPI/library.properties b/libraries/SPI/library.properties index 724137030d1..3403d1c5d4f 100644 --- a/libraries/SPI/library.properties +++ b/libraries/SPI/library.properties @@ -1,5 +1,5 @@ name=SPI -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables the communication with devices that use the Serial Peripheral Interface (SPI) Bus. For all Arduino boards, BUT Arduino DUE. diff --git a/libraries/SPIFFS/library.properties b/libraries/SPIFFS/library.properties index fc4601e512c..486ec1b4ce6 100644 --- a/libraries/SPIFFS/library.properties +++ b/libraries/SPIFFS/library.properties @@ -1,5 +1,5 @@ name=SPIFFS -version=3.2.1 +version=3.3.0 author=Hristo Gochkov, Ivan Grokhtkov maintainer=Hristo Gochkov sentence=ESP32 SPIFFS File System diff --git a/libraries/SimpleBLE/library.properties b/libraries/SimpleBLE/library.properties index 768449ee1c4..a7f12207afe 100644 --- a/libraries/SimpleBLE/library.properties +++ b/libraries/SimpleBLE/library.properties @@ -1,5 +1,5 @@ name=SimpleBLE -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Provides really simple BLE advertizer with just on and off diff --git a/libraries/TFLiteMicro/library.properties b/libraries/TFLiteMicro/library.properties index 6ad0c32c7d5..d2dc127f5ab 100644 --- a/libraries/TFLiteMicro/library.properties +++ b/libraries/TFLiteMicro/library.properties @@ -1,5 +1,5 @@ name=TFLite Micro -version=3.2.1 +version=3.3.0 author=Sanket Wadekar maintainer=Sanket Wadekar sentence=TensorFlow Lite for Microcontrollers diff --git a/libraries/Ticker/library.properties b/libraries/Ticker/library.properties index 8a2af554906..8795deb22ce 100644 --- a/libraries/Ticker/library.properties +++ b/libraries/Ticker/library.properties @@ -1,5 +1,5 @@ name=Ticker -version=3.2.1 +version=3.3.0 author=Bert Melis maintainer=Hristo Gochkov sentence=Allows to call functions with a given interval. diff --git a/libraries/USB/library.properties b/libraries/USB/library.properties index 677d736a635..4c2c032545e 100644 --- a/libraries/USB/library.properties +++ b/libraries/USB/library.properties @@ -1,5 +1,5 @@ name=USB -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=ESP32S2 USB Library diff --git a/libraries/Update/library.properties b/libraries/Update/library.properties index 4c756397aba..5fd633ec358 100644 --- a/libraries/Update/library.properties +++ b/libraries/Update/library.properties @@ -1,5 +1,5 @@ name=Update -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=ESP32 Sketch Update Library diff --git a/libraries/WebServer/library.properties b/libraries/WebServer/library.properties index bf6c26c65e7..913dd00e036 100644 --- a/libraries/WebServer/library.properties +++ b/libraries/WebServer/library.properties @@ -1,5 +1,5 @@ name=WebServer -version=3.2.1 +version=3.3.0 author=Ivan Grokhotkov maintainer=Ivan Grokhtkov sentence=Simple web server library diff --git a/libraries/WiFi/library.properties b/libraries/WiFi/library.properties index a282570ff8a..82ccb32b702 100644 --- a/libraries/WiFi/library.properties +++ b/libraries/WiFi/library.properties @@ -1,5 +1,5 @@ name=WiFi -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Enables network connection (local and Internet) using the ESP32 built-in WiFi. diff --git a/libraries/WiFiProv/library.properties b/libraries/WiFiProv/library.properties index 1cc8e4b0f91..1b19186c40b 100644 --- a/libraries/WiFiProv/library.properties +++ b/libraries/WiFiProv/library.properties @@ -1,5 +1,5 @@ name=WiFiProv -version=3.2.1 +version=3.3.0 author=Switi Mhaiske maintainer=Hristo Gochkov sentence=Enables provisioning. diff --git a/libraries/Wire/library.properties b/libraries/Wire/library.properties index 22cc7f26d86..182e98790bc 100644 --- a/libraries/Wire/library.properties +++ b/libraries/Wire/library.properties @@ -1,5 +1,5 @@ name=Wire -version=3.2.1 +version=3.3.0 author=Hristo Gochkov maintainer=Hristo Gochkov sentence=Allows the communication between devices or sensors connected via Two Wire Interface Bus. For esp8266 boards. diff --git a/libraries/Zigbee/library.properties b/libraries/Zigbee/library.properties index d9587f49fd5..dab96b82a61 100644 --- a/libraries/Zigbee/library.properties +++ b/libraries/Zigbee/library.properties @@ -1,5 +1,5 @@ name=Zigbee -version=3.2.1 +version=3.3.0 author=P-R-O-C-H-Y maintainer=Jan Procházka sentence=Enables zigbee connection with the ESP32 diff --git a/package.json b/package.json index 85a15ab3615..4c3e4725a9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "framework-arduinoespressif32", - "version": "3.2.1", + "version": "3.3.0", "description": "Arduino Wiring-based Framework for the Espressif ESP32, ESP32-P4, ESP32-S and ESP32-C series of SoCs", "keywords": [ "framework", diff --git a/platform.txt b/platform.txt index 9e98650ca62..82560190c8f 100644 --- a/platform.txt +++ b/platform.txt @@ -1,5 +1,5 @@ name=ESP32 Arduino -version=3.2.1 +version=3.3.0 tools.esp32-arduino-libs.path={runtime.platform.path}/tools/esp32-arduino-libs tools.esp32-arduino-libs.path.windows={runtime.platform.path}\tools\esp32-arduino-libs From 98d309f84a997138e018174c05232e91836e681f Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Fri, 18 Jul 2025 10:44:01 +0300 Subject: [PATCH 083/102] feat(ci): Enable builds on IDF 5.3, 5.4 and 5.5 --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 68148fbcff7..f2f2c0b5ca1 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -248,7 +248,7 @@ jobs: # See https://hub.docker.com/r/espressif/idf/tags and # https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-docker-image.html # for details. - idf_ver: ["release-v5.5"] + idf_ver: ["release-v5.3","release-v5.4","release-v5.5"] idf_target: [ "esp32", From 6cb518448793f0d42b81cf6e8d02662a8846c4fc Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Fri, 18 Jul 2025 11:12:22 +0300 Subject: [PATCH 084/102] fix(build): Fix build for IDF 5.3.3+ and older releases --- cores/esp32/esp32-hal-i2c-slave.c | 8 ++++++-- cores/esp32/esp32-hal-ledc.c | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index e616934958e..6d69418e787 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -338,7 +338,9 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t } #endif // !defined(CONFIG_IDF_TARGET_ESP32P4) -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)) \ + || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)) \ + || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)) i2c_ll_set_mode(i2c->dev, I2C_BUS_MODE_SLAVE); i2c_ll_enable_pins_open_drain(i2c->dev, true); i2c_ll_enable_fifo_mode(i2c->dev, true); @@ -366,7 +368,9 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t i2c_ll_disable_intr_mask(i2c->dev, I2C_LL_INTR_MASK); i2c_ll_clear_intr_mask(i2c->dev, I2C_LL_INTR_MASK); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)) \ + || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)) \ + || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)) i2c_ll_enable_fifo_mode(i2c->dev, true); #else i2c_ll_slave_set_fifo_mode(i2c->dev, true); diff --git a/cores/esp32/esp32-hal-ledc.c b/cores/esp32/esp32-hal-ledc.c index 18b722f80a2..ffb24db4599 100644 --- a/cores/esp32/esp32-hal-ledc.c +++ b/cores/esp32/esp32-hal-ledc.c @@ -88,6 +88,9 @@ static bool find_free_timer(uint8_t speed_mode, uint8_t *timer_num) { } } +#ifndef SOC_LEDC_TIMER_NUM +#define SOC_LEDC_TIMER_NUM 4 +#endif // Find first unused timer for (uint8_t i = 0; i < SOC_LEDC_TIMER_NUM; i++) { if (!(used_timers & (1 << i))) { From 346e7f41386bef558d56a6113d433f824667a278 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Fri, 18 Jul 2025 11:18:13 +0300 Subject: [PATCH 085/102] fix(build): Enable I2C FIFO mode only on IDF 5.5+ --- cores/esp32/esp32-hal-i2c-slave.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index 6d69418e787..2a357b3a747 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -368,9 +368,7 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t i2c_ll_disable_intr_mask(i2c->dev, I2C_LL_INTR_MASK); i2c_ll_clear_intr_mask(i2c->dev, I2C_LL_INTR_MASK); -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)) \ - || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)) \ - || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) i2c_ll_enable_fifo_mode(i2c->dev, true); #else i2c_ll_slave_set_fifo_mode(i2c->dev, true); From 530c1a43fe9425061c396486d744110ce6ca6f83 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Fri, 18 Jul 2025 11:19:50 +0300 Subject: [PATCH 086/102] fix(build): Enable I2C FIFO mode only on IDF 5.4.2+ --- cores/esp32/esp32-hal-i2c-slave.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index 2a357b3a747..c8e59653a8f 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -368,7 +368,7 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t i2c_ll_disable_intr_mask(i2c->dev, I2C_LL_INTR_MASK); i2c_ll_clear_intr_mask(i2c->dev, I2C_LL_INTR_MASK); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) i2c_ll_enable_fifo_mode(i2c->dev, true); #else i2c_ll_slave_set_fifo_mode(i2c->dev, true); From 000336bad0446d3b8ded72b1d92e5edc407f8f69 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Fri, 18 Jul 2025 11:25:36 +0300 Subject: [PATCH 087/102] fix(build): Enable I2C FIFO mode only on IDF 5.4.2+ --- cores/esp32/esp32-hal-i2c-slave.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index c8e59653a8f..79c994869e2 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -343,7 +343,11 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)) i2c_ll_set_mode(i2c->dev, I2C_BUS_MODE_SLAVE); i2c_ll_enable_pins_open_drain(i2c->dev, true); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) i2c_ll_enable_fifo_mode(i2c->dev, true); +#else + i2c_ll_slave_set_fifo_mode(i2c->dev, true); +#endif #else i2c_ll_slave_init(i2c->dev); i2c_ll_slave_set_fifo_mode(i2c->dev, true); From 211a0ce1438ad7108ded0299238876f22036d946 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Fri, 18 Jul 2025 09:15:48 -0300 Subject: [PATCH 088/102] fix(ci): Fix artifact names --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f2f2c0b5ca1..1523b7231be 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -283,7 +283,7 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: - name: sdkconfig-${{ matrix.idf_target }} + name: sdkconfig-${{ matrix.idf_ver }}-${{ matrix.idf_target }} path: ./components/arduino-esp32/idf_component_examples/**/sdkconfig # Save artifacts to gh-pages From 3ad17de6aa6f74f7d01bff4197f9f7a7481b2bb4 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Fri, 18 Jul 2025 13:02:55 -0300 Subject: [PATCH 089/102] fix(build): make core compatible with IDF 5.3 (#11607) * fix(uart): make it compatible with IDF 5.3 * fix(ci): Fix artifact names * fix(build): Fix ESP_NOW and WiFi for IDF 5.3 * fix(build): Fix NetworkClient for IDF 5.3 * fix(build): Fix WiFi for IDF 5.3 * fix(build): Fix WiFi Captive Portal for IDF 5.3 --------- Co-authored-by: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Co-authored-by: me-no-dev --- cores/esp32/esp32-hal-uart.c | 11 +++++++++++ libraries/ESP_NOW/src/ESP32_NOW.cpp | 4 ++++ libraries/Network/src/NetworkClient.cpp | 8 +++----- libraries/WiFi/examples/WiFiScan/WiFiScan.ino | 2 ++ libraries/WiFi/src/AP.cpp | 2 ++ libraries/WiFi/src/WiFiAP.h | 2 ++ libraries/WiFi/src/WiFiGeneric.cpp | 2 ++ libraries/WiFi/src/WiFiGeneric.h | 2 ++ 8 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cores/esp32/esp32-hal-uart.c b/cores/esp32/esp32-hal-uart.c index 6e5a22da9f8..8bd446a8eb8 100644 --- a/cores/esp32/esp32-hal-uart.c +++ b/cores/esp32/esp32-hal-uart.c @@ -389,7 +389,12 @@ static esp_err_t _uartInternalSetPin(uart_port_t uart_num, int tx_io_num, int rx #endif if (tx_rx_same_io || !_uartTrySetIomuxPin(uart_num, rx_io_num, SOC_UART_RX_PIN_IDX)) { if (uart_num < SOC_UART_HP_NUM) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) gpio_input_enable(rx_io_num); +#else + gpio_func_sel(rx_io_num, PIN_FUNC_GPIO); + gpio_ll_input_enable(&GPIO, rx_io_num); +#endif esp_rom_gpio_connect_in_signal(rx_io_num, UART_PERIPH_SIGNAL(uart_num, SOC_UART_RX_PIN_IDX), 0); } #if SOC_LP_GPIO_MATRIX_SUPPORTED @@ -422,8 +427,14 @@ static esp_err_t _uartInternalSetPin(uart_port_t uart_num, int tx_io_num, int rx if (cts_io_num >= 0 && !_uartTrySetIomuxPin(uart_num, cts_io_num, SOC_UART_CTS_PIN_IDX)) { if (uart_num < SOC_UART_HP_NUM) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) gpio_pullup_en(cts_io_num); gpio_input_enable(cts_io_num); +#else + gpio_func_sel(cts_io_num, PIN_FUNC_GPIO); + gpio_set_pull_mode(cts_io_num, GPIO_PULLUP_ONLY); + gpio_set_direction(cts_io_num, GPIO_MODE_INPUT); +#endif esp_rom_gpio_connect_in_signal(cts_io_num, UART_PERIPH_SIGNAL(uart_num, SOC_UART_CTS_PIN_IDX), 0); } #if SOC_LP_GPIO_MATRIX_SUPPORTED diff --git a/libraries/ESP_NOW/src/ESP32_NOW.cpp b/libraries/ESP_NOW/src/ESP32_NOW.cpp index d461fe1473d..e61160a16dd 100644 --- a/libraries/ESP_NOW/src/ESP32_NOW.cpp +++ b/libraries/ESP_NOW/src/ESP32_NOW.cpp @@ -9,6 +9,10 @@ #include "esp32-hal.h" #include "esp_wifi.h" +#ifndef ESP_NOW_MAX_DATA_LEN_V2 +#define ESP_NOW_MAX_DATA_LEN_V2 1470 +#endif + static void (*new_cb)(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) = nullptr; static void *new_arg = nullptr; // * tx_arg = nullptr, * rx_arg = nullptr, static bool _esp_now_has_begun = false; diff --git a/libraries/Network/src/NetworkClient.cpp b/libraries/Network/src/NetworkClient.cpp index 89411a42453..355cbc6c361 100644 --- a/libraries/Network/src/NetworkClient.cpp +++ b/libraries/Network/src/NetworkClient.cpp @@ -23,11 +23,9 @@ #include #include -// It is already defined in IDF as: -//#define IN6_IS_ADDR_V4MAPPED(a) ip6_addr_isipv4mappedipv6((ip6_addr_t*)(a)) -//#define ip6_addr_isipv4mappedipv6(ip6addr) (((ip6addr)->addr[0] == 0) && ((ip6addr)->addr[1] == 0) && (((ip6addr)->addr[2]) == PP_HTONL(0x0000FFFFUL))) -// Keeping as a memory of the change. -//#define _IN6_IS_ADDR_V4MAPPED(a) ((((__const uint32_t *)(a))[0] == 0) && (((__const uint32_t *)(a))[1] == 0) && (((__const uint32_t *)(a))[2] == htonl(0xffff))) +#ifndef IN6_IS_ADDR_V4MAPPED +#define IN6_IS_ADDR_V4MAPPED(a) ((((__const uint32_t *)(a))[0] == 0) && (((__const uint32_t *)(a))[1] == 0) && (((__const uint32_t *)(a))[2] == htonl(0xffff))) +#endif #define WIFI_CLIENT_DEF_CONN_TIMEOUT_MS (3000) #define WIFI_CLIENT_MAX_WRITE_RETRY (10) diff --git a/libraries/WiFi/examples/WiFiScan/WiFiScan.ino b/libraries/WiFi/examples/WiFiScan/WiFiScan.ino index 98733adb0bb..d840959dcb2 100644 --- a/libraries/WiFi/examples/WiFiScan/WiFiScan.ino +++ b/libraries/WiFi/examples/WiFiScan/WiFiScan.ino @@ -58,7 +58,9 @@ void loop() { Serial.println("-------------------------------------"); Serial.println("Default wifi band mode scan:"); Serial.println("-------------------------------------"); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) WiFi.setBandMode(WIFI_BAND_MODE_AUTO); +#endif ScanWiFi(); #if CONFIG_SOC_WIFI_SUPPORT_5G // Wait a bit before scanning again. diff --git a/libraries/WiFi/src/AP.cpp b/libraries/WiFi/src/AP.cpp index a649c3898cb..00c3a1e3733 100644 --- a/libraries/WiFi/src/AP.cpp +++ b/libraries/WiFi/src/AP.cpp @@ -305,6 +305,7 @@ bool APClass::enableNAPT(bool enable) { return true; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) bool APClass::enableDhcpCaptivePortal() { esp_err_t err = ESP_OK; static char captiveportal_uri[32] = { @@ -343,6 +344,7 @@ bool APClass::enableDhcpCaptivePortal() { return true; } +#endif String APClass::SSID(void) const { if (!started()) { diff --git a/libraries/WiFi/src/WiFiAP.h b/libraries/WiFi/src/WiFiAP.h index 2b7ce469801..67cc0f988d3 100644 --- a/libraries/WiFi/src/WiFiAP.h +++ b/libraries/WiFi/src/WiFiAP.h @@ -53,7 +53,9 @@ class APClass : public NetworkInterface { bool bandwidth(wifi_bandwidth_t bandwidth); bool enableNAPT(bool enable = true); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) bool enableDhcpCaptivePortal(); +#endif String SSID(void) const; uint8_t stationCount(); diff --git a/libraries/WiFi/src/WiFiGeneric.cpp b/libraries/WiFi/src/WiFiGeneric.cpp index 171bd293738..66f8908af77 100644 --- a/libraries/WiFi/src/WiFiGeneric.cpp +++ b/libraries/WiFi/src/WiFiGeneric.cpp @@ -781,6 +781,7 @@ wifi_ps_type_t WiFiGenericClass::getSleep() { return _sleepEnabled; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) /** * control wifi band mode * @param band_mode enum possible band modes @@ -843,6 +844,7 @@ wifi_band_mode_t WiFiGenericClass::getBandMode() { return WIFI_BAND_MODE_2G_ONLY; #endif } +#endif /** * get the current active wifi band diff --git a/libraries/WiFi/src/WiFiGeneric.h b/libraries/WiFi/src/WiFiGeneric.h index ebeecb51e16..cdc1519d30b 100644 --- a/libraries/WiFi/src/WiFiGeneric.h +++ b/libraries/WiFi/src/WiFiGeneric.h @@ -116,8 +116,10 @@ class WiFiGenericClass { bool setTxPower(wifi_power_t power); wifi_power_t getTxPower(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) bool setBandMode(wifi_band_mode_t band_mode); wifi_band_mode_t getBandMode(); +#endif wifi_band_t getBand(); bool initiateFTM(uint8_t frm_count = 16, uint16_t burst_period = 2, uint8_t channel = 1, const uint8_t *mac = NULL); From cb3329be60f7cb446faa4aaa4572975ffdabab42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:47:21 +0200 Subject: [PATCH 090/102] feat(zigbee): Add callback option for default response message (#11613) * feat(zigbee): Add cb option for default response message * fix(example): Add timeout and fix spelling * feat(zigbee): Add global default response cb option * fix(example): Use task for measure and sleep * fix(zigbee): Remove debug logs * ci(pre-commit): Apply automatic fixes * fix(example): Add retry and fix typo * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../Zigbee_Temp_Hum_Sensor_Sleepy.ino | 81 ++++++++++++++++--- libraries/Zigbee/keywords.txt | 2 + libraries/Zigbee/src/Zigbee.h | 3 + libraries/Zigbee/src/ZigbeeCore.cpp | 7 ++ libraries/Zigbee/src/ZigbeeCore.h | 12 +++ libraries/Zigbee/src/ZigbeeEP.cpp | 12 ++- libraries/Zigbee/src/ZigbeeEP.h | 15 +++- libraries/Zigbee/src/ZigbeeHandlers.cpp | 10 +++ libraries/Zigbee/src/ZigbeeTypes.h | 30 +++++++ 9 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 libraries/Zigbee/src/ZigbeeTypes.h diff --git a/libraries/Zigbee/examples/Zigbee_Temp_Hum_Sensor_Sleepy/Zigbee_Temp_Hum_Sensor_Sleepy.ino b/libraries/Zigbee/examples/Zigbee_Temp_Hum_Sensor_Sleepy/Zigbee_Temp_Hum_Sensor_Sleepy.ino index e9d08d32175..54c085fbfea 100644 --- a/libraries/Zigbee/examples/Zigbee_Temp_Hum_Sensor_Sleepy/Zigbee_Temp_Hum_Sensor_Sleepy.ino +++ b/libraries/Zigbee/examples/Zigbee_Temp_Hum_Sensor_Sleepy/Zigbee_Temp_Hum_Sensor_Sleepy.ino @@ -32,18 +32,49 @@ #include "Zigbee.h" +#define USE_GLOBAL_ON_RESPONSE_CALLBACK 1 // Set to 0 to use local callback specified directly for the endpoint. + /* Zigbee temperature + humidity sensor configuration */ #define TEMP_SENSOR_ENDPOINT_NUMBER 10 #define uS_TO_S_FACTOR 1000000ULL /* Conversion factor for micro seconds to seconds */ #define TIME_TO_SLEEP 55 /* Sleep for 55s will + 5s delay for establishing connection => data reported every 1 minute */ +#define REPORT_TIMEOUT 1000 /* Timeout for response from coordinator in ms */ uint8_t button = BOOT_PIN; ZigbeeTempSensor zbTempSensor = ZigbeeTempSensor(TEMP_SENSOR_ENDPOINT_NUMBER); +uint8_t dataToSend = 2; // Temperature and humidity values are reported in same endpoint, so 2 values are reported +bool resend = false; + +/************************ Callbacks *****************************/ +#if USE_GLOBAL_ON_RESPONSE_CALLBACK +void onGlobalResponse(zb_cmd_type_t command, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster) { + Serial.printf("Global response command: %d, status: %s, endpoint: %d, cluster: 0x%04x\r\n", command, esp_zb_zcl_status_to_name(status), endpoint, cluster); + if ((command == ZB_CMD_REPORT_ATTRIBUTE) && (endpoint == TEMP_SENSOR_ENDPOINT_NUMBER)) { + switch (status) { + case ESP_ZB_ZCL_STATUS_SUCCESS: dataToSend--; break; + case ESP_ZB_ZCL_STATUS_FAIL: resend = true; break; + default: break; // add more statuses like ESP_ZB_ZCL_STATUS_INVALID_VALUE, ESP_ZB_ZCL_STATUS_TIMEOUT etc. + } + } +} +#else +void onResponse(zb_cmd_type_t command, esp_zb_zcl_status_t status) { + Serial.printf("Response command: %d, status: %s\r\n", command, esp_zb_zcl_status_to_name(status)); + if (command == ZB_CMD_REPORT_ATTRIBUTE) { + switch (status) { + case ESP_ZB_ZCL_STATUS_SUCCESS: dataToSend--; break; + case ESP_ZB_ZCL_STATUS_FAIL: resend = true; break; + default: break; // add more statuses like ESP_ZB_ZCL_STATUS_INVALID_VALUE, ESP_ZB_ZCL_STATUS_TIMEOUT etc. + } + } +} +#endif + /************************ Temp sensor *****************************/ -void meausureAndSleep() { +static void meausureAndSleep(void *arg) { // Measure temperature sensor value float temperature = temperatureRead(); @@ -55,13 +86,35 @@ void meausureAndSleep() { zbTempSensor.setHumidity(humidity); // Report temperature and humidity values - zbTempSensor.report(); + zbTempSensor.report(); // reports temperature and humidity values (if humidity sensor is not added, only temperature is reported) Serial.printf("Reported temperature: %.2f°C, Humidity: %.2f%%\r\n", temperature, humidity); - // Add small delay to allow the data to be sent before going to sleep - delay(100); + unsigned long startTime = millis(); + const unsigned long timeout = REPORT_TIMEOUT; + + Serial.printf("Waiting for data report to be confirmed \r\n"); + // Wait until data was successfully sent + int tries = 0; + const int maxTries = 3; + while (dataToSend != 0 && tries < maxTries) { + if (resend) { + Serial.println("Resending data on failure!"); + resend = false; + dataToSend = 2; + zbTempSensor.report(); // report again + } + if (millis() - startTime >= timeout) { + Serial.println("\nReport timeout! Report Again"); + dataToSend = 2; + zbTempSensor.report(); // report again + startTime = millis(); + tries++; + } + Serial.printf("."); + delay(50); // 50ms delay to avoid busy-waiting + } - // Put device to deep sleep + // Put device to deep sleep after data was sent successfully or timeout Serial.println("Going to sleep now"); esp_deep_sleep_start(); } @@ -92,6 +145,16 @@ void setup() { // Add humidity cluster to the temperature sensor device with min, max and tolerance values zbTempSensor.addHumiditySensor(0, 100, 1); + // Set callback for default response to handle status of reported data, there are 2 options. + +#if USE_GLOBAL_ON_RESPONSE_CALLBACK + // Global callback for all endpoints with more params to determine the endpoint and cluster in the callback function. + Zigbee.onGlobalDefaultResponse(onGlobalResponse); +#else + // Callback specified for endpoint + zbTempSensor.onDefaultResponse(onResponse); +#endif + // Add endpoint to Zigbee Core Zigbee.addEndpoint(&zbTempSensor); @@ -117,8 +180,8 @@ void setup() { Serial.println(); Serial.println("Successfully connected to Zigbee network"); - // Delay approx 1s (may be adjusted) to allow establishing proper connection with coordinator, needed for sleepy devices - delay(1000); + // Start Temperature sensor reading task + xTaskCreate(meausureAndSleep, "temp_sensor_update", 2048, NULL, 10, NULL); } void loop() { @@ -141,7 +204,5 @@ void loop() { } } } - - // Call the function to measure temperature and put the device to sleep - meausureAndSleep(); + delay(100); } diff --git a/libraries/Zigbee/keywords.txt b/libraries/Zigbee/keywords.txt index 23f3af3bf02..68721c1a66f 100644 --- a/libraries/Zigbee/keywords.txt +++ b/libraries/Zigbee/keywords.txt @@ -47,6 +47,7 @@ zb_power_source_t KEYWORD1 ZigbeeWindowCoveringType KEYWORD1 ZigbeeFanMode KEYWORD1 ZigbeeFanModeSequence KEYWORD1 +zb_cmd_type_t KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -96,6 +97,7 @@ getTime KEYWORD2 getTimezone KEYWORD2 addOTAClient KEYWORD2 clearBoundDevices KEYWORD2 +onDefaultResponse KEYWORD2 # ZigbeeLight + ZigbeeColorDimmableLight onLightChange KEYWORD2 diff --git a/libraries/Zigbee/src/Zigbee.h b/libraries/Zigbee/src/Zigbee.h index 65c9e7f0daa..ab94a163f3e 100644 --- a/libraries/Zigbee/src/Zigbee.h +++ b/libraries/Zigbee/src/Zigbee.h @@ -2,6 +2,9 @@ #pragma once +// Common types and functions +#include "ZigbeeTypes.h" + // Core #include "ZigbeeCore.h" #include "ZigbeeEP.h" diff --git a/libraries/Zigbee/src/ZigbeeCore.cpp b/libraries/Zigbee/src/ZigbeeCore.cpp index c49dedb221f..bf652642ac1 100644 --- a/libraries/Zigbee/src/ZigbeeCore.cpp +++ b/libraries/Zigbee/src/ZigbeeCore.cpp @@ -32,6 +32,7 @@ ZigbeeCore::ZigbeeCore() { _scan_duration = 3; // default scan duration _rx_on_when_idle = true; _debug = false; + _global_default_response_cb = nullptr; // Initialize global callback to nullptr if (!lock) { lock = xSemaphoreCreateBinary(); if (lock == NULL) { @@ -792,6 +793,12 @@ const char *ZigbeeCore::getDeviceTypeString(esp_zb_ha_standard_devices_t deviceI } } +void ZigbeeCore::callDefaultResponseCallback(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster) { + if (_global_default_response_cb) { + _global_default_response_cb(resp_to_cmd, status, endpoint, cluster); + } +} + ZigbeeCore Zigbee = ZigbeeCore(); #endif // CONFIG_ZB_ENABLED diff --git a/libraries/Zigbee/src/ZigbeeCore.h b/libraries/Zigbee/src/ZigbeeCore.h index 69c91c63ac4..df334e1620d 100644 --- a/libraries/Zigbee/src/ZigbeeCore.h +++ b/libraries/Zigbee/src/ZigbeeCore.h @@ -11,6 +11,7 @@ #include "aps/esp_zigbee_aps.h" #include #include +#include "ZigbeeTypes.h" #include "ZigbeeEP.h" class ZigbeeEP; @@ -103,6 +104,9 @@ class ZigbeeCore { SemaphoreHandle_t lock; bool _debug; + // Global default response callback + void (*_global_default_response_cb)(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster); + bool zigbeeInit(esp_zb_cfg_t *zb_cfg, bool erase_nvs); static void scanCompleteCallback(esp_zb_zdp_status_t zdo_status, uint8_t count, esp_zb_network_descriptor_t *nwk_descriptor); const char *getDeviceTypeString(esp_zb_ha_standard_devices_t deviceId); @@ -176,6 +180,14 @@ class ZigbeeCore { return _debug; } + // Set global default response callback + void onGlobalDefaultResponse(void (*callback)(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster)) { + _global_default_response_cb = callback; + } + + // Call global default response callback (for internal use) + void callDefaultResponseCallback(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster); + // Friend function declaration to allow access to private members friend void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct); friend bool zb_apsde_data_indication_handler(esp_zb_apsde_data_ind_t ind); diff --git a/libraries/Zigbee/src/ZigbeeEP.cpp b/libraries/Zigbee/src/ZigbeeEP.cpp index efddbdd0368..6b63cae0312 100644 --- a/libraries/Zigbee/src/ZigbeeEP.cpp +++ b/libraries/Zigbee/src/ZigbeeEP.cpp @@ -608,7 +608,17 @@ void ZigbeeEP::removeBoundDevice(zb_device_params_t *device) { log_w("No matching device found for removal"); } -const char *ZigbeeEP::esp_zb_zcl_status_to_name(esp_zb_zcl_status_t status) { +void ZigbeeEP::zbDefaultResponse(const esp_zb_zcl_cmd_default_resp_message_t *message) { + log_v("Default response received for endpoint %d", _endpoint); + log_v("Status code: %s", esp_zb_zcl_status_to_name(message->status_code)); + log_v("Response to command: %d", message->resp_to_cmd); + if (_on_default_response) { + _on_default_response((zb_cmd_type_t)message->resp_to_cmd, message->status_code); + } +} + +// Global function implementation +const char *esp_zb_zcl_status_to_name(esp_zb_zcl_status_t status) { switch (status) { case ESP_ZB_ZCL_STATUS_SUCCESS: return "Success"; case ESP_ZB_ZCL_STATUS_FAIL: return "Fail"; diff --git a/libraries/Zigbee/src/ZigbeeEP.h b/libraries/Zigbee/src/ZigbeeEP.h index a3217cbd066..23217407003 100644 --- a/libraries/Zigbee/src/ZigbeeEP.h +++ b/libraries/Zigbee/src/ZigbeeEP.h @@ -38,6 +38,9 @@ typedef enum { ZB_POWER_SOURCE_BATTERY = 0x03, } zb_power_source_t; +// Global function for converting ZCL status to name +const char *esp_zb_zcl_status_to_name(esp_zb_zcl_status_t status); + /* Zigbee End Device Class */ class ZigbeeEP { public: @@ -138,6 +141,7 @@ class ZigbeeEP { virtual void zbReadTimeCluster(const esp_zb_zcl_attribute_t *attribute); //already implemented virtual void zbIASZoneStatusChangeNotification(const esp_zb_zcl_ias_zone_status_change_notification_message_t *message) {}; virtual void zbIASZoneEnrollResponse(const esp_zb_zcl_ias_zone_enroll_response_message_t *message) {}; + virtual void zbDefaultResponse(const esp_zb_zcl_cmd_default_resp_message_t *message); //already implemented virtual void addBoundDevice(zb_device_params_t *device) { _bound_devices.push_back(device); @@ -156,17 +160,21 @@ class ZigbeeEP { _on_identify = callback; } + void onDefaultResponse(void (*callback)(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status)) { + _on_default_response = callback; + } + + // Convert ZCL status to name + private: char *_read_manufacturer; char *_read_model; void (*_on_identify)(uint16_t time); + void (*_on_default_response)(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status); time_t _read_time; int32_t _read_timezone; protected: - // Convert ZCL status to name - const char *esp_zb_zcl_status_to_name(esp_zb_zcl_status_t status); - uint8_t _endpoint; esp_zb_ha_standard_devices_t _device_id; esp_zb_endpoint_config_t _ep_config; @@ -179,6 +187,7 @@ class ZigbeeEP { zb_power_source_t _power_source; uint8_t _time_status; + // Friend class declaration to allow access to protected members friend class ZigbeeCore; }; diff --git a/libraries/Zigbee/src/ZigbeeHandlers.cpp b/libraries/Zigbee/src/ZigbeeHandlers.cpp index 5d54e459058..0986056dcd9 100644 --- a/libraries/Zigbee/src/ZigbeeHandlers.cpp +++ b/libraries/Zigbee/src/ZigbeeHandlers.cpp @@ -398,6 +398,16 @@ static esp_err_t zb_cmd_default_resp_handler(const esp_zb_zcl_cmd_default_resp_m "Received default response: from address(0x%x), src_endpoint(%d) to dst_endpoint(%d), cluster(0x%x) with status 0x%x", message->info.src_address.u.short_addr, message->info.src_endpoint, message->info.dst_endpoint, message->info.cluster, message->status_code ); + + // Call global callback if set + Zigbee.callDefaultResponseCallback((zb_cmd_type_t)message->resp_to_cmd, message->status_code, message->info.dst_endpoint, message->info.cluster); + + // List through all Zigbee EPs and call the callback function, with the message + for (std::list::iterator it = Zigbee.ep_objects.begin(); it != Zigbee.ep_objects.end(); ++it) { + if (message->info.dst_endpoint == (*it)->getEndpoint()) { + (*it)->zbDefaultResponse(message); //method zbDefaultResponse is implemented in the common EP class + } + } return ESP_OK; } diff --git a/libraries/Zigbee/src/ZigbeeTypes.h b/libraries/Zigbee/src/ZigbeeTypes.h new file mode 100644 index 00000000000..5025f90db7c --- /dev/null +++ b/libraries/Zigbee/src/ZigbeeTypes.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esp_zigbee_core.h" + +// Foundation Command Types +typedef enum { + ZB_CMD_READ_ATTRIBUTE = 0x00U, /*!< Read attributes command */ + ZB_CMD_READ_ATTRIBUTE_RESPONSE = 0x01U, /*!< Read attributes response command */ + ZB_CMD_WRITE_ATTRIBUTE = 0x02U, /*!< Write attributes foundation command */ + ZB_CMD_WRITE_ATTRIBUTE_UNDIVIDED = 0x03U, /*!< Write attributes undivided command */ + ZB_CMD_WRITE_ATTRIBUTE_RESPONSE = 0x04U, /*!< Write attributes response command */ + ZB_CMD_WRITE_ATTRIBUTE_NO_RESPONSE = 0x05U, /*!< Write attributes no response command */ + ZB_CMD_CONFIGURE_REPORTING = 0x06U, /*!< Configure reporting command */ + ZB_CMD_CONFIGURE_REPORTING_RESPONSE = 0x07U, /*!< Configure reporting response command */ + ZB_CMD_READ_REPORTING_CONFIG = 0x08U, /*!< Read reporting config command */ + ZB_CMD_READ_REPORTING_CONFIG_RESPONSE = 0x09U, /*!< Read reporting config response command */ + ZB_CMD_REPORT_ATTRIBUTE = 0x0aU, /*!< Report attribute command */ + ZB_CMD_DEFAULT_RESPONSE = 0x0bU, /*!< Default response command */ + ZB_CMD_DISCOVER_ATTRIBUTES = 0x0cU, /*!< Discover attributes command */ + ZB_CMD_DISCOVER_ATTRIBUTES_RESPONSE = 0x0dU, /*!< Discover attributes response command */ + ZB_CMD_READ_ATTRIBUTE_STRUCTURED = 0x0eU, /*!< Read attributes structured */ + ZB_CMD_WRITE_ATTRIBUTE_STRUCTURED = 0x0fU, /*!< Write attributes structured */ + ZB_CMD_WRITE_ATTRIBUTE_STRUCTURED_RESPONSE = 0x10U, /*!< Write attributes structured response */ + ZB_CMD_DISCOVER_COMMANDS_RECEIVED = 0x11U, /*!< Discover Commands Received command */ + ZB_CMD_DISCOVER_COMMANDS_RECEIVED_RESPONSE = 0x12U, /*!< Discover Commands Received response command */ + ZB_CMD_DISCOVER_COMMANDS_GENERATED = 0x13U, /*!< Discover Commands Generated command */ + ZB_CMD_DISCOVER_COMMANDS_GENERATED_RESPONSE = 0x14U, /*!< Discover Commands Generated response command */ + ZB_CMD_DISCOVER_ATTRIBUTES_EXTENDED = 0x15U, /*!< Discover attributes extended command */ + ZB_CMD_DISCOVER_ATTRIBUTES_EXTENDED_RESPONSE = 0x16U, /*!< Discover attributes extended response command */ +} zb_cmd_type_t; From 995e603d3af8305959948a46ff0793c72e5c1687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:47:43 +0200 Subject: [PATCH 091/102] fix(zigbee): Replace assert with error log to solve immediate crash (#11614) * fix(zigbee): Replace assert with error log to solve immediate crash * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- libraries/Zigbee/src/ZigbeeCore.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/Zigbee/src/ZigbeeCore.cpp b/libraries/Zigbee/src/ZigbeeCore.cpp index bf652642ac1..90b29cf9d0a 100644 --- a/libraries/Zigbee/src/ZigbeeCore.cpp +++ b/libraries/Zigbee/src/ZigbeeCore.cpp @@ -238,7 +238,9 @@ void ZigbeeCore::closeNetwork() { } static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) { - ESP_ERROR_CHECK(esp_zb_bdb_start_top_level_commissioning(mode_mask)); + if (esp_zb_bdb_start_top_level_commissioning(mode_mask) != ESP_OK) { + log_e("Failed to start Zigbee commissioning"); + } } void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) { From f1712943b4960749316612bcf0fb4caf5d74efa0 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Mon, 21 Jul 2025 15:48:05 +0300 Subject: [PATCH 092/102] fix(ppp): Detach PPP RST pin from periman on end (#11620) --- libraries/PPP/src/PPP.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/PPP/src/PPP.cpp b/libraries/PPP/src/PPP.cpp index 87fee6920c3..2a5c5760287 100644 --- a/libraries/PPP/src/PPP.cpp +++ b/libraries/PPP/src/PPP.cpp @@ -432,6 +432,11 @@ void PPPClass::end(void) { _pin_cts = -1; perimanClearPinBus(pin); } + if (_pin_rst != -1) { + pin = _pin_rst; + _pin_rst = -1; + perimanClearPinBus(pin); + } _mode = ESP_MODEM_MODE_COMMAND; } From 4a3c6d7fbbb21ac7439e145680f8201c00b11b67 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Mon, 21 Jul 2025 15:49:15 +0300 Subject: [PATCH 093/102] feat(netif): Allow setting interface's routing priority (#11617) * feat(netif): Allow setting interface's routing priority * feat(netif): Rename route priority method names and add notes * feat(netif): Print route prio for each interface --- libraries/Network/src/NetworkInterface.cpp | 28 ++++++++++++++++++++-- libraries/Network/src/NetworkInterface.h | 5 +++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/libraries/Network/src/NetworkInterface.cpp b/libraries/Network/src/NetworkInterface.cpp index 01790ec2493..06cf2a377b0 100644 --- a/libraries/Network/src/NetworkInterface.cpp +++ b/libraries/Network/src/NetworkInterface.cpp @@ -606,13 +606,33 @@ int NetworkInterface::impl_index() const { return esp_netif_get_netif_impl_index(_esp_netif); } -int NetworkInterface::route_prio() const { +/** + * Every netif has a parameter named route_prio, you can refer to file esp_netif_defaults.h. + * A higher value of route_prio indicates a higher priority. + * The active interface with highest priority will be used for default route (gateway). + * Defaults are: STA=100, BR=70, ETH=50, PPP=20, AP/NAN=10 + */ +int NetworkInterface::getRoutePrio() const { if (_esp_netif == NULL) { return -1; } return esp_netif_get_route_prio(_esp_netif); } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +int NetworkInterface::setRoutePrio(int prio) { + if (_esp_netif == NULL) { + return -1; + } + return esp_netif_set_route_prio(_esp_netif, prio); +} +#endif + +/** + * This API overrides the automatic configuration of the default interface based on the route_prio + * If the selected netif is set default using this API, no other interface could be set-default disregarding + * its route_prio number (unless the selected netif gets destroyed) + */ bool NetworkInterface::setDefault() { if (_esp_netif == NULL) { return false; @@ -819,7 +839,11 @@ size_t NetworkInterface::printTo(Print &out) const { if (flags & ESP_NETIF_FLAG_MLDV6_REPORT) { bytes += out.print(",V6_REP"); } - bytes += out.println(")"); + bytes += out.print(")"); + + bytes += out.print(" PRIO: "); + bytes += out.print(getRoutePrio()); + bytes += out.println(""); bytes += out.print(" "); bytes += out.print("ether "); diff --git a/libraries/Network/src/NetworkInterface.h b/libraries/Network/src/NetworkInterface.h index 4f97181d4fd..fd26df77697 100644 --- a/libraries/Network/src/NetworkInterface.h +++ b/libraries/Network/src/NetworkInterface.h @@ -57,7 +57,10 @@ class NetworkInterface : public Printable { const char *desc() const; String impl_name() const; int impl_index() const; - int route_prio() const; + int getRoutePrio() const; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + int setRoutePrio(int prio); +#endif bool setDefault(); bool isDefault() const; From c369dca062ee99bf8b20ffee89537f3c66d9ea52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:35:28 +0200 Subject: [PATCH 094/102] feat(docs): Add Zigbee library API documentation (#11525) * feat(docs): Add Zigbee library documentation * fix: Remove helper scripts * fix: Proper class naming for better readability * fix(docs): Fix typos Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ci(pre-commit): Apply automatic fixes * fix(docs): Precommit fixes * fix(docs): Precommit fixes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- docs/en/libraries.rst | 12 + docs/en/zigbee/ep_analog.rst | 280 ++++++++++++++ docs/en/zigbee/ep_binary.rst | 137 +++++++ docs/en/zigbee/ep_carbon_dioxide_sensor.rst | 105 ++++++ docs/en/zigbee/ep_color_dimmable_light.rst | 223 +++++++++++ docs/en/zigbee/ep_color_dimmer_switch.rst | 186 +++++++++ docs/en/zigbee/ep_contact_switch.rst | 95 +++++ docs/en/zigbee/ep_dimmable_light.rst | 138 +++++++ docs/en/zigbee/ep_door_window_handle.rst | 95 +++++ docs/en/zigbee/ep_electrical_measurement.rst | 254 +++++++++++++ docs/en/zigbee/ep_flow_sensor.rst | 119 ++++++ docs/en/zigbee/ep_gateway.rst | 35 ++ docs/en/zigbee/ep_illuminance_sensor.rst | 109 ++++++ docs/en/zigbee/ep_light.rst | 88 +++++ docs/en/zigbee/ep_occupancy_sensor.rst | 81 ++++ docs/en/zigbee/ep_pm25_sensor.rst | 109 ++++++ docs/en/zigbee/ep_power_outlet.rst | 88 +++++ docs/en/zigbee/ep_pressure_sensor.rst | 109 ++++++ docs/en/zigbee/ep_range_extender.rst | 34 ++ docs/en/zigbee/ep_switch.rst | 136 +++++++ docs/en/zigbee/ep_temperature_sensor.rst | 184 +++++++++ docs/en/zigbee/ep_thermostat.rst | 238 ++++++++++++ docs/en/zigbee/ep_vibration_sensor.rst | 87 +++++ docs/en/zigbee/ep_wind_speed_sensor.rst | 119 ++++++ docs/en/zigbee/ep_window_covering.rst | 233 ++++++++++++ docs/en/zigbee/zigbee.rst | 159 ++++++++ docs/en/zigbee/zigbee_core.rst | 375 +++++++++++++++++++ docs/en/zigbee/zigbee_ep.rst | 357 ++++++++++++++++++ 28 files changed, 4185 insertions(+) create mode 100644 docs/en/zigbee/ep_analog.rst create mode 100644 docs/en/zigbee/ep_binary.rst create mode 100644 docs/en/zigbee/ep_carbon_dioxide_sensor.rst create mode 100644 docs/en/zigbee/ep_color_dimmable_light.rst create mode 100644 docs/en/zigbee/ep_color_dimmer_switch.rst create mode 100644 docs/en/zigbee/ep_contact_switch.rst create mode 100644 docs/en/zigbee/ep_dimmable_light.rst create mode 100644 docs/en/zigbee/ep_door_window_handle.rst create mode 100644 docs/en/zigbee/ep_electrical_measurement.rst create mode 100644 docs/en/zigbee/ep_flow_sensor.rst create mode 100644 docs/en/zigbee/ep_gateway.rst create mode 100644 docs/en/zigbee/ep_illuminance_sensor.rst create mode 100644 docs/en/zigbee/ep_light.rst create mode 100644 docs/en/zigbee/ep_occupancy_sensor.rst create mode 100644 docs/en/zigbee/ep_pm25_sensor.rst create mode 100644 docs/en/zigbee/ep_power_outlet.rst create mode 100644 docs/en/zigbee/ep_pressure_sensor.rst create mode 100644 docs/en/zigbee/ep_range_extender.rst create mode 100644 docs/en/zigbee/ep_switch.rst create mode 100644 docs/en/zigbee/ep_temperature_sensor.rst create mode 100644 docs/en/zigbee/ep_thermostat.rst create mode 100644 docs/en/zigbee/ep_vibration_sensor.rst create mode 100644 docs/en/zigbee/ep_wind_speed_sensor.rst create mode 100644 docs/en/zigbee/ep_window_covering.rst create mode 100644 docs/en/zigbee/zigbee.rst create mode 100644 docs/en/zigbee/zigbee_core.rst create mode 100644 docs/en/zigbee/zigbee_ep.rst diff --git a/docs/en/libraries.rst b/docs/en/libraries.rst index 525a5c4ba26..07f0978be68 100644 --- a/docs/en/libraries.rst +++ b/docs/en/libraries.rst @@ -91,3 +91,15 @@ The Arduino ESP32 offers some unique APIs, described in this section: :glob: api/* + +Zigbee APIs +----------- + +.. toctree:: + :maxdepth: 1 + :glob: + + zigbee/zigbee + zigbee/zigbee_core + zigbee/zigbee_ep + zigbee/ep_* diff --git a/docs/en/zigbee/ep_analog.rst b/docs/en/zigbee/ep_analog.rst new file mode 100644 index 00000000000..45740007881 --- /dev/null +++ b/docs/en/zigbee/ep_analog.rst @@ -0,0 +1,280 @@ +############ +ZigbeeAnalog +############ + +About +----- + +The ``ZigbeeAnalog`` class provides analog input and output endpoints for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for analog signal processing and control. +Analog Input (AI) is meant to be used for sensors that provide an analog signal, such as temperature, humidity, pressure to be sent to the coordinator. +Analog Output (AO) is meant to be used for actuators that require an analog signal, such as dimmers, valves, etc. to be controlled by the coordinator. + + +Common API +---------- + +Constructor +*********** + +ZigbeeAnalog +^^^^^^^^^^^^ + +Creates a new Zigbee analog endpoint. + +.. code-block:: arduino + + ZigbeeAnalog(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Cluster Management +****************** + +addAnalogInput +^^^^^^^^^^^^^^ + +Adds analog input cluster to the endpoint. + +.. code-block:: arduino + + bool addAnalogInput(); + +This function will return ``true`` if successful, ``false`` otherwise. + +addAnalogOutput +^^^^^^^^^^^^^^^ + +Adds analog output cluster to the endpoint. + +.. code-block:: arduino + + bool addAnalogOutput(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Analog Input API +---------------- + +Configuration Methods +********************* + +setAnalogInputApplication +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the application type for the analog input. + +.. code-block:: arduino + + bool setAnalogInputApplication(uint32_t application_type); + +* ``application_type`` - Application type constant (see esp_zigbee_zcl_analog_input.h for values) + +This function will return ``true`` if successful, ``false`` otherwise. + +setAnalogInputDescription +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets a custom description for the analog input. + +.. code-block:: arduino + + bool setAnalogInputDescription(const char *description); + +* ``description`` - Description string + +This function will return ``true`` if successful, ``false`` otherwise. + +setAnalogInputResolution +^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the resolution for the analog input. + +.. code-block:: arduino + + bool setAnalogInputResolution(float resolution); + +* ``resolution`` - Resolution value + +This function will return ``true`` if successful, ``false`` otherwise. + +setAnalogInputMinMax +^^^^^^^^^^^^^^^^^^^^ + +Sets the minimum and maximum values for the analog input. + +.. code-block:: arduino + + bool setAnalogInputMinMax(float min, float max); + +* ``min`` - Minimum value +* ``max`` - Maximum value + +This function will return ``true`` if successful, ``false`` otherwise. + +Value Control +************* + +setAnalogInput +^^^^^^^^^^^^^^ + +Sets the analog input value. + +.. code-block:: arduino + + bool setAnalogInput(float analog); + +* ``analog`` - Analog input value + +This function will return ``true`` if successful, ``false`` otherwise. + +Reporting Methods +***************** + +setAnalogInputReporting +^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the reporting configuration for analog input. + +.. code-block:: arduino + + bool setAnalogInputReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in value to trigger a report + +This function will return ``true`` if successful, ``false`` otherwise. + +reportAnalogInput +^^^^^^^^^^^^^^^^^ + +Manually reports the current analog input value. + +.. code-block:: arduino + + bool reportAnalogInput(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Analog Output API +----------------- + +Configuration Methods +********************* + +setAnalogOutputApplication +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the application type for the analog output. + +.. code-block:: arduino + + bool setAnalogOutputApplication(uint32_t application_type); + +* ``application_type`` - Application type constant (see esp_zigbee_zcl_analog_output.h for values) + +This function will return ``true`` if successful, ``false`` otherwise. + +setAnalogOutputDescription +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets a custom description for the analog output. + +.. code-block:: arduino + + bool setAnalogOutputDescription(const char *description); + +* ``description`` - Description string + +This function will return ``true`` if successful, ``false`` otherwise. + +setAnalogOutputResolution +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the resolution for the analog output. + +.. code-block:: arduino + + bool setAnalogOutputResolution(float resolution); + +* ``resolution`` - Resolution value + +This function will return ``true`` if successful, ``false`` otherwise. + +setAnalogOutputMinMax +^^^^^^^^^^^^^^^^^^^^^ + +Sets the minimum and maximum values for the analog output. + +.. code-block:: arduino + + bool setAnalogOutputMinMax(float min, float max); + +* ``min`` - Minimum value +* ``max`` - Maximum value + +This function will return ``true`` if successful, ``false`` otherwise. + +Value Control +************* + +setAnalogOutput +^^^^^^^^^^^^^^^ + +Sets the analog output value. + +.. code-block:: arduino + + bool setAnalogOutput(float analog); + +* ``analog`` - Analog output value + +This function will return ``true`` if successful, ``false`` otherwise. + +getAnalogOutput +^^^^^^^^^^^^^^^ + +Gets the current analog output value. + +.. code-block:: arduino + + float getAnalogOutput(); + +This function will return current analog output value. + +Reporting Methods +***************** + +reportAnalogOutput +^^^^^^^^^^^^^^^^^^ + +Manually reports the current analog output value. + +.. code-block:: arduino + + bool reportAnalogOutput(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Event Handling +************** + +onAnalogOutputChange +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when the analog output value changes. + +.. code-block:: arduino + + void onAnalogOutputChange(void (*callback)(float analog)); + +* ``callback`` - Function to call when analog output changes + +Example +------- + +Analog Input/Output +******************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Analog_Input_Output/Zigbee_Analog_Input_Output.ino + :language: arduino diff --git a/docs/en/zigbee/ep_binary.rst b/docs/en/zigbee/ep_binary.rst new file mode 100644 index 00000000000..950e20ef42b --- /dev/null +++ b/docs/en/zigbee/ep_binary.rst @@ -0,0 +1,137 @@ +############ +ZigbeeBinary +############ + +About +----- + +The ``ZigbeeBinary`` class provides an endpoint for binary input/output sensors in Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for binary sensors, supporting various application types for HVAC, security, and general binary sensing. +Binary Input (BI) is meant to be used for sensors that provide a binary signal, such as door/window sensors, motion detectors, etc. to be sent to the network. + +.. note:: + + Binary Output (BO) is not supported yet. + +API Reference +------------- + +Constructor +*********** + +ZigbeeBinary +^^^^^^^^^^^^ + +Creates a new Zigbee binary sensor endpoint. + +.. code-block:: arduino + + ZigbeeBinary(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Binary Input Application Types +****************************** + +HVAC Application Types +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: arduino + + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_BOILER_STATUS 0x00000003 + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_CHILLER_STATUS 0x00000013 + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_OCCUPANCY 0x00000031 + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_FAN_STATUS 0x00000035 + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_FILTER_STATUS 0x00000036 + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_HEATING_ALARM 0x0000003E + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_COOLING_ALARM 0x0000001D + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_UNIT_ENABLE 0x00000090 + #define BINARY_INPUT_APPLICATION_TYPE_HVAC_OTHER 0x0000FFFF + +Security Application Types +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: arduino + + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_GLASS_BREAKAGE_DETECTION_0 0x01000000 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_INTRUSION_DETECTION 0x01000001 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_MOTION_DETECTION 0x01000002 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_GLASS_BREAKAGE_DETECTION_1 0x01000003 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_ZONE_ARMED 0x01000004 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_GLASS_BREAKAGE_DETECTION_2 0x01000005 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_SMOKE_DETECTION 0x01000006 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_CARBON_DIOXIDE_DETECTION 0x01000007 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_HEAT_DETECTION 0x01000008 + #define BINARY_INPUT_APPLICATION_TYPE_SECURITY_OTHER 0x0100FFFF + +API Methods +*********** + +addBinaryInput +^^^^^^^^^^^^^^ + +Adds a binary input cluster to the endpoint. + +.. code-block:: arduino + + bool addBinaryInput(); + +This function will return ``true`` if successful, ``false`` otherwise. + +setBinaryInputApplication +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the application type for the binary input. + +.. code-block:: arduino + + bool setBinaryInputApplication(uint32_t application_type); + +* ``application_type`` - Application type constant (see above) + +This function will return ``true`` if successful, ``false`` otherwise. + +setBinaryInputDescription +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets a custom description for the binary input. + +.. code-block:: arduino + + bool setBinaryInputDescription(const char *description); + +* ``description`` - Description string + +This function will return ``true`` if successful, ``false`` otherwise. + +setBinaryInput +^^^^^^^^^^^^^^ + +Sets the binary input value. + +.. code-block:: arduino + + bool setBinaryInput(bool input); + +* ``input`` - Binary value (true/false) + +This function will return ``true`` if successful, ``false`` otherwise. + +reportBinaryInput +^^^^^^^^^^^^^^^^^ + +Manually reports the current binary input value. + +.. code-block:: arduino + + bool reportBinaryInput(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Binary Input Implementation +**************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Binary_Input/Zigbee_Binary_Input.ino + :language: arduino diff --git a/docs/en/zigbee/ep_carbon_dioxide_sensor.rst b/docs/en/zigbee/ep_carbon_dioxide_sensor.rst new file mode 100644 index 00000000000..6f219a16fd0 --- /dev/null +++ b/docs/en/zigbee/ep_carbon_dioxide_sensor.rst @@ -0,0 +1,105 @@ +######################### +ZigbeeCarbonDioxideSensor +######################### + +About +----- + +The ``ZigbeeCarbonDioxideSensor`` class provides a CO2 sensor endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for carbon dioxide measurement devices. + +API Reference +------------- + +Constructor +*********** + +ZigbeeCarbonDioxideSensor +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee CO2 sensor endpoint. + +.. code-block:: arduino + + ZigbeeCarbonDioxideSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setCarbonDioxide +^^^^^^^^^^^^^^^^ + +Sets the CO2 concentration measurement value. + +.. code-block:: arduino + + bool setCarbonDioxide(float carbon_dioxide); + +* ``carbon_dioxide`` - CO2 concentration value in ppm + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum measurement values. + +.. code-block:: arduino + + bool setMinMaxValue(float min, float max); + +* ``min`` - Minimum CO2 concentration value in ppm +* ``max`` - Maximum CO2 concentration value in ppm + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for measurements. + +.. code-block:: arduino + + bool setTolerance(float tolerance); + +* ``tolerance`` - Tolerance value in ppm + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting configuration for CO2 measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, uint16_t delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report in ppm + +**Note:** Delta reporting is currently not supported by the carbon dioxide sensor. + +This function will return ``true`` if successful, ``false`` otherwise. + +report +^^^^^^ + +Manually reports the current CO2 concentration value. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +CO2 Sensor Implementation +************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_CarbonDioxide_Sensor/Zigbee_CarbonDioxide_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_color_dimmable_light.rst b/docs/en/zigbee/ep_color_dimmable_light.rst new file mode 100644 index 00000000000..b2fa6d0bf91 --- /dev/null +++ b/docs/en/zigbee/ep_color_dimmable_light.rst @@ -0,0 +1,223 @@ +######################## +ZigbeeColorDimmableLight +######################## + +About +----- + +The ``ZigbeeColorDimmableLight`` class provides an endpoint for color dimmable lights in Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for color lighting devices, supporting RGB color control, dimming, and scene management. + +**Features:** +* On/off control +* Brightness level control (0-100%) +* RGB color control +* HSV color support +* Scene and group support +* Automatic state restoration +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Smart RGB light bulbs +* Color-changing LED strips +* Mood lighting systems +* Entertainment lighting +* Architectural lighting +* Smart home color lighting + +API Reference +------------- + +Constructor +*********** + +ZigbeeColorDimmableLight +^^^^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee color dimmable light endpoint. + +.. code-block:: arduino + + ZigbeeColorDimmableLight(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Callback Functions +****************** + +onLightChange +^^^^^^^^^^^^^ + +Sets the callback function for light state changes. + +.. code-block:: arduino + + void onLightChange(void (*callback)(bool, uint8_t, uint8_t, uint8_t, uint8_t)); + +* ``callback`` - Function pointer to the light change callback (state, red, green, blue, level) + +Control Methods +*************** + +setLightState +^^^^^^^^^^^^^ + +Sets the light on/off state. + +.. code-block:: arduino + + bool setLightState(bool state); + +* ``state`` - Light state (true = on, false = off) + +This function will return ``true`` if successful, ``false`` otherwise. + +setLightLevel +^^^^^^^^^^^^^ + +Sets the light brightness level. + +.. code-block:: arduino + + bool setLightLevel(uint8_t level); + +* ``level`` - Brightness level (0-100, where 0 is off, 100 is full brightness) + +This function will return ``true`` if successful, ``false`` otherwise. + +setLightColor (RGB) +^^^^^^^^^^^^^^^^^^^ + +Sets the light color using RGB values. + +.. code-block:: arduino + + bool setLightColor(uint8_t red, uint8_t green, uint8_t blue); + bool setLightColor(espRgbColor_t rgb_color); + +* ``red`` - Red component (0-255) +* ``green`` - Green component (0-255) +* ``blue`` - Blue component (0-255) +* ``rgb_color`` - RGB color structure + +This function will return ``true`` if successful, ``false`` otherwise. + +setLightColor (HSV) +^^^^^^^^^^^^^^^^^^^ + +Sets the light color using HSV values. + +.. code-block:: arduino + + bool setLightColor(espHsvColor_t hsv_color); + +* ``hsv_color`` - HSV color structure + +This function will return ``true`` if successful, ``false`` otherwise. + +setLight +^^^^^^^^ + +Sets all light parameters at once. + +.. code-block:: arduino + + bool setLight(bool state, uint8_t level, uint8_t red, uint8_t green, uint8_t blue); + +* ``state`` - Light state (true/false) +* ``level`` - Brightness level (0-100) +* ``red`` - Red component (0-255) +* ``green`` - Green component (0-255) +* ``blue`` - Blue component (0-255) + +This function will return ``true`` if successful, ``false`` otherwise. + +State Retrieval Methods +*********************** + +getLightState +^^^^^^^^^^^^^ + +Gets the current light state. + +.. code-block:: arduino + + bool getLightState(); + +This function will return current light state (true = on, false = off). + +getLightLevel +^^^^^^^^^^^^^ + +Gets the current brightness level. + +.. code-block:: arduino + + uint8_t getLightLevel(); + +This function will return current brightness level (0-100). + +getLightColor +^^^^^^^^^^^^^ + +Gets the current RGB color. + +.. code-block:: arduino + + espRgbColor_t getLightColor(); + +This function will return current RGB color structure. + +getLightRed +^^^^^^^^^^^ + +Gets the current red component. + +.. code-block:: arduino + + uint8_t getLightRed(); + +This function will return current red component (0-255). + +getLightGreen +^^^^^^^^^^^^^ + +Gets the current green component. + +.. code-block:: arduino + + uint8_t getLightGreen(); + +This function will return current green component (0-255). + +getLightBlue +^^^^^^^^^^^^ + +Gets the current blue component. + +.. code-block:: arduino + + uint8_t getLightBlue(); + +This function will return current blue component (0-255). + +Utility Methods +*************** + +restoreLight +^^^^^^^^^^^^ + +Restores the light to its last known state. + +.. code-block:: arduino + + void restoreLight(); + +Example +------- + +Color Dimmable Light Implementation +*********************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Color_Dimmable_Light/Zigbee_Color_Dimmable_Light.ino + :language: arduino diff --git a/docs/en/zigbee/ep_color_dimmer_switch.rst b/docs/en/zigbee/ep_color_dimmer_switch.rst new file mode 100644 index 00000000000..720c5d8f5bf --- /dev/null +++ b/docs/en/zigbee/ep_color_dimmer_switch.rst @@ -0,0 +1,186 @@ +####################### +ZigbeeColorDimmerSwitch +####################### + +About +----- + +The ``ZigbeeColorDimmerSwitch`` class provides a color dimmer switch endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for advanced lighting control switches that can control both dimming and color of lights. + +**Features:** +* On/off control for bound lights +* Brightness level control (0-100%) +* Color control (RGB, HSV) +* Color temperature control +* Scene and group support +* Special effects and timed operations +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Smart lighting switches +* Color control remotes +* Advanced lighting controllers +* Smart home lighting automation +* Entertainment lighting control + +API Reference +------------- + +Constructor +*********** + +ZigbeeColorDimmerSwitch +^^^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee color dimmer switch endpoint. + +.. code-block:: arduino + + ZigbeeColorDimmerSwitch(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Basic Control Commands +********************** + +lightToggle +^^^^^^^^^^^ + +Toggles the state of bound lights (on to off, or off to on). + +.. code-block:: arduino + + void lightToggle(); + void lightToggle(uint16_t group_addr); + void lightToggle(uint8_t endpoint, uint16_t short_addr); + void lightToggle(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +lightOn +^^^^^^^ + +Turns on bound lights. + +.. code-block:: arduino + + void lightOn(); + void lightOn(uint16_t group_addr); + void lightOn(uint8_t endpoint, uint16_t short_addr); + void lightOn(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +lightOff +^^^^^^^^ + +Turns off bound lights. + +.. code-block:: arduino + + void lightOff(); + void lightOff(uint16_t group_addr); + void lightOff(uint8_t endpoint, uint16_t short_addr); + void lightOff(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +Dimmer Control Commands +*********************** + +setLightLevel +^^^^^^^^^^^^^ + +Sets the brightness level of bound lights. + +.. code-block:: arduino + + void setLightLevel(uint8_t level); + void setLightLevel(uint8_t level, uint16_t group_addr); + void setLightLevel(uint8_t level, uint8_t endpoint, uint16_t short_addr); + void setLightLevel(uint8_t level, uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``level`` - Brightness level (0-100, where 0 is off, 100 is full brightness) +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +Color Control Commands +********************** + +setLightColor +^^^^^^^^^^^^^ + +Sets the color of bound lights using RGB values. + +.. code-block:: arduino + + void setLightColor(uint8_t red, uint8_t green, uint8_t blue); + void setLightColor(uint8_t red, uint8_t green, uint8_t blue, uint16_t group_addr); + void setLightColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t endpoint, uint16_t short_addr); + void setLightColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``red`` - Red component (0-255) +* ``green`` - Green component (0-255) +* ``blue`` - Blue component (0-255) +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +Advanced Control Commands +************************* + +lightOffWithEffect +^^^^^^^^^^^^^^^^^^ + +Turns off lights with a specific effect. + +.. code-block:: arduino + + void lightOffWithEffect(uint8_t effect_id, uint8_t effect_variant); + +* ``effect_id`` - Effect identifier +* ``effect_variant`` - Effect variant + +lightOnWithTimedOff +^^^^^^^^^^^^^^^^^^^ + +Turns on lights with automatic turn-off after specified time. + +.. code-block:: arduino + + void lightOnWithTimedOff(uint8_t on_off_control, uint16_t time_on, uint16_t time_off); + +* ``on_off_control`` - Control byte +* ``time_on`` - Time to stay on (in 1/10th seconds) +* ``time_off`` - Time to stay off (in 1/10th seconds) + +lightOnWithSceneRecall +^^^^^^^^^^^^^^^^^^^^^^ + +Turns on lights with scene recall. + +.. code-block:: arduino + + void lightOnWithSceneRecall(); + +Example +------- + +Color Dimmer Switch Implementation +********************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Color_Dimmer_Switch/Zigbee_Color_Dimmer_Switch.ino + :language: arduino diff --git a/docs/en/zigbee/ep_contact_switch.rst b/docs/en/zigbee/ep_contact_switch.rst new file mode 100644 index 00000000000..f7f6dc15c66 --- /dev/null +++ b/docs/en/zigbee/ep_contact_switch.rst @@ -0,0 +1,95 @@ +################### +ZigbeeContactSwitch +################### + +About +----- + +The ``ZigbeeContactSwitch`` class provides a contact switch endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for door/window contact sensors and other binary contact devices. + +**Features:** +* Contact state detection (open/closed) +* Configurable application types +* Automatic reporting capabilities +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Door and window sensors +* Security system contacts +* Cabinet and drawer sensors +* Industrial contact monitoring +* Smart home security applications + +API Reference +------------- + +Constructor +*********** + +ZigbeeContactSwitch +^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee contact switch endpoint. + +.. code-block:: arduino + + ZigbeeContactSwitch(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setClosed +^^^^^^^^^ + +Sets the contact switch to closed state. + +.. code-block:: arduino + + bool setClosed(); + +This function will return ``true`` if successful, ``false`` otherwise. + +setOpen +^^^^^^^ + +Sets the contact switch to open state. + +.. code-block:: arduino + + bool setOpen(); + +This function will return ``true`` if successful, ``false`` otherwise. + +setIASClientEndpoint +^^^^^^^^^^^^^^^^^^^^ + +Sets the IAS Client endpoint number (default is 1). + +.. code-block:: arduino + + void setIASClientEndpoint(uint8_t ep_number); + +* ``ep_number`` - IAS Client endpoint number + +report +^^^^^^ + +Manually reports the current contact state. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Contact Switch Implementation +***************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Contact_Switch/Zigbee_Contact_Switch.ino + :language: arduino diff --git a/docs/en/zigbee/ep_dimmable_light.rst b/docs/en/zigbee/ep_dimmable_light.rst new file mode 100644 index 00000000000..dcfe6e1fc9d --- /dev/null +++ b/docs/en/zigbee/ep_dimmable_light.rst @@ -0,0 +1,138 @@ +################### +ZigbeeDimmableLight +################### + +About +----- + +The ``ZigbeeDimmableLight`` class provides a dimmable light endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for dimmable lighting control with both on/off and brightness level control. + +**Features:** +* On/off control +* Brightness level control (0-100%) +* State and level change callbacks +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Dimmable smart bulbs +* LED strips with brightness control +* Any device requiring both on/off and dimming functionality + +API Reference +------------- + +Constructor +*********** + +ZigbeeDimmableLight +^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee dimmable light endpoint. + +.. code-block:: arduino + + ZigbeeDimmableLight(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Light Control +************* + +setLightState +^^^^^^^^^^^^^ + +Sets only the light state (on or off) without changing the brightness level. + +.. code-block:: arduino + + bool setLightState(bool state); + +* ``state`` - ``true`` to turn on, ``false`` to turn off + +This function will return ``true`` if successful, ``false`` otherwise. + +setLightLevel +^^^^^^^^^^^^^ + +Sets only the brightness level (0-100) without changing the on/off state. + +.. code-block:: arduino + + bool setLightLevel(uint8_t level); + +* ``level`` - Brightness level (0-100, where 0 is off, 100 is full brightness) + +This function will return ``true`` if successful, ``false`` otherwise. + +setLight +^^^^^^^^ + +Sets both the light state and brightness level simultaneously. + +.. code-block:: arduino + + bool setLight(bool state, uint8_t level); + +* ``state`` - ``true`` to turn on, ``false`` to turn off +* ``level`` - Brightness level (0-100) + +This function will return ``true`` if successful, ``false`` otherwise. + +getLightState +^^^^^^^^^^^^^ + +Gets the current light state. + +.. code-block:: arduino + + bool getLightState(); + +This function will return current light state (``true`` = on, ``false`` = off). + +getLightLevel +^^^^^^^^^^^^^ + +Gets the current brightness level. + +.. code-block:: arduino + + uint8_t getLightLevel(); + +This function will return current brightness level (0-100). + +restoreLight +^^^^^^^^^^^^ + +Restores the light state and triggers any registered callbacks. + +.. code-block:: arduino + + void restoreLight(); + +Event Handling +************** + +onLightChange +^^^^^^^^^^^^^ + +Sets a callback function to be called when the light state or level changes. + +.. code-block:: arduino + + void onLightChange(void (*callback)(bool, uint8_t)); + +* ``callback`` - Function to call when light state or level changes + +**Callback Parameters:** +* ``bool state`` - New light state (true = on, false = off) +* ``uint8_t level`` - New brightness level (0-100) + +Example +------- + +Dimmable Light Implementation +***************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Dimmable_Light/Zigbee_Dimmable_Light.ino + :language: arduino diff --git a/docs/en/zigbee/ep_door_window_handle.rst b/docs/en/zigbee/ep_door_window_handle.rst new file mode 100644 index 00000000000..53203f463dd --- /dev/null +++ b/docs/en/zigbee/ep_door_window_handle.rst @@ -0,0 +1,95 @@ +###################### +ZigbeeDoorWindowHandle +###################### + +About +----- + +The ``ZigbeeDoorWindowHandle`` class provides a door/window handle endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for handle position sensors and other handle-related devices. + +**Features:** +* Handle position detection +* Multiple position states support +* Configurable application types +* Automatic reporting capabilities + + +API Reference +------------- + +Constructor +*********** + +ZigbeeDoorWindowHandle +^^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee door/window handle endpoint. + +.. code-block:: arduino + + ZigbeeDoorWindowHandle(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setClosed +^^^^^^^^^ + +Sets the door/window handle to closed position. + +.. code-block:: arduino + + bool setClosed(); + +This function will return ``true`` if successful, ``false`` otherwise. + +setOpen +^^^^^^^ + +Sets the door/window handle to open position. + +.. code-block:: arduino + + bool setOpen(); + +This function will return ``true`` if successful, ``false`` otherwise. + +setTilted +^^^^^^^^^ + +Sets the door/window handle to tilted position. + +.. code-block:: arduino + + bool setTilted(); + +This function will return ``true`` if successful, ``false`` otherwise. + +setIASClientEndpoint +^^^^^^^^^^^^^^^^^^^^ + +Sets the IAS Client endpoint number (default is 1). + +.. code-block:: arduino + + void setIASClientEndpoint(uint8_t ep_number); + +* ``ep_number`` - IAS Client endpoint number + +report +^^^^^^ + +Manually reports the current handle position. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +*To be added* diff --git a/docs/en/zigbee/ep_electrical_measurement.rst b/docs/en/zigbee/ep_electrical_measurement.rst new file mode 100644 index 00000000000..86410768484 --- /dev/null +++ b/docs/en/zigbee/ep_electrical_measurement.rst @@ -0,0 +1,254 @@ +########################### +ZigbeeElectricalMeasurement +########################### + +About +----- + +The ``ZigbeeElectricalMeasurement`` class provides an endpoint for electrical measurement devices in Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for power monitoring and electrical measurement. + +**Features:** +* AC and DC electrical measurements +* Voltage, current, and power monitoring +* Power factor measurement +* Multi-phase support +* Configurable measurement ranges +* Automatic reporting capabilities +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Smart power monitoring +* Energy monitoring systems +* Solar panel monitoring +* Battery monitoring systems +* Electrical load monitoring + +Common API +---------- + +Constructor +*********** + +ZigbeeElectricalMeasurement +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee electrical measurement endpoint. + +.. code-block:: arduino + + ZigbeeElectricalMeasurement(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +DC Electrical Measurement +************************* + +addDCMeasurement +^^^^^^^^^^^^^^^^ + +Adds a DC measurement type to the endpoint. + +.. code-block:: arduino + + bool addDCMeasurement(ZIGBEE_DC_MEASUREMENT_TYPE measurement_type); + +* ``measurement_type`` - DC measurement type constant (ZIGBEE_DC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_DC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_DC_MEASUREMENT_TYPE_POWER) + +This function will return ``true`` if successful, ``false`` otherwise. + +setDCMeasurement +^^^^^^^^^^^^^^^^ + +Sets the DC measurement value for a specific measurement type. + +.. code-block:: arduino + + bool setDCMeasurement(ZIGBEE_DC_MEASUREMENT_TYPE measurement_type, int16_t value); + +* ``measurement_type`` - DC measurement type constant (ZIGBEE_DC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_DC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_DC_MEASUREMENT_TYPE_POWER) +* ``value`` - Measurement value + +This function will return ``true`` if successful, ``false`` otherwise. + +setDCMinMaxValue +^^^^^^^^^^^^^^^^ + +Sets the minimum and maximum values for a DC measurement type. + +.. code-block:: arduino + + bool setDCMinMaxValue(ZIGBEE_DC_MEASUREMENT_TYPE measurement_type, int16_t min, int16_t max); + +* ``measurement_type`` - DC measurement type constant (ZIGBEE_DC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_DC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_DC_MEASUREMENT_TYPE_POWER) +* ``min`` - Minimum value +* ``max`` - Maximum value + +This function will return ``true`` if successful, ``false`` otherwise. + +setDCMultiplierDivisor +^^^^^^^^^^^^^^^^^^^^^^ + +Sets the multiplier and divisor for scaling DC measurements. + +.. code-block:: arduino + + bool setDCMultiplierDivisor(ZIGBEE_DC_MEASUREMENT_TYPE measurement_type, uint16_t multiplier, uint16_t divisor); + +* ``measurement_type`` - DC measurement type constant (ZIGBEE_DC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_DC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_DC_MEASUREMENT_TYPE_POWER) +* ``multiplier`` - Multiplier value +* ``divisor`` - Divisor value + +This function will return ``true`` if successful, ``false`` otherwise. + +setDCReporting +^^^^^^^^^^^^^^ + +Sets the reporting configuration for DC measurements. + +.. code-block:: arduino + + bool setDCReporting(ZIGBEE_DC_MEASUREMENT_TYPE measurement_type, uint16_t min_interval, uint16_t max_interval, int16_t delta); + +* ``measurement_type`` - DC measurement type constant (ZIGBEE_DC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_DC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_DC_MEASUREMENT_TYPE_POWER) +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report + +This function will return ``true`` if successful, ``false`` otherwise. + +reportDC +^^^^^^^^ + +Manually reports a DC measurement value. + +.. code-block:: arduino + + bool reportDC(ZIGBEE_DC_MEASUREMENT_TYPE measurement_type); + +* ``measurement_type`` - DC measurement type constant (ZIGBEE_DC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_DC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_DC_MEASUREMENT_TYPE_POWER) + +This function will return ``true`` if successful, ``false`` otherwise. + +AC Electrical Measurement +************************* + +addACMeasurement +^^^^^^^^^^^^^^^^ + +Adds an AC measurement type for a specific phase. + +.. code-block:: arduino + + bool addACMeasurement(ZIGBEE_AC_MEASUREMENT_TYPE measurement_type, ZIGBEE_AC_PHASE_TYPE phase_type); + +* ``measurement_type`` - AC measurement type constant (ZIGBEE_AC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_AC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_AC_MEASUREMENT_TYPE_POWER, ZIGBEE_AC_MEASUREMENT_TYPE_POWER_FACTOR, ZIGBEE_AC_MEASUREMENT_TYPE_FREQUENCY) +* ``phase_type`` - Phase type constant (ZIGBEE_AC_PHASE_TYPE_NON_SPECIFIC, ZIGBEE_AC_PHASE_TYPE_A, ZIGBEE_AC_PHASE_TYPE_B, ZIGBEE_AC_PHASE_TYPE_C) + +This function will return ``true`` if successful, ``false`` otherwise. + +setACMeasurement +^^^^^^^^^^^^^^^^ + +Sets the AC measurement value for a specific measurement type and phase. + +.. code-block:: arduino + + bool setACMeasurement(ZIGBEE_AC_MEASUREMENT_TYPE measurement_type, ZIGBEE_AC_PHASE_TYPE phase_type, int32_t value); + +* ``measurement_type`` - AC measurement type constant (ZIGBEE_AC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_AC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_AC_MEASUREMENT_TYPE_POWER, ZIGBEE_AC_MEASUREMENT_TYPE_POWER_FACTOR, ZIGBEE_AC_MEASUREMENT_TYPE_FREQUENCY) +* ``phase_type`` - Phase type constant (ZIGBEE_AC_PHASE_TYPE_NON_SPECIFIC, ZIGBEE_AC_PHASE_TYPE_A, ZIGBEE_AC_PHASE_TYPE_B, ZIGBEE_AC_PHASE_TYPE_C) +* ``value`` - Measurement value + +This function will return ``true`` if successful, ``false`` otherwise. + +setACMinMaxValue +^^^^^^^^^^^^^^^^ + +Sets the minimum and maximum values for an AC measurement type and phase. + +.. code-block:: arduino + + bool setACMinMaxValue(ZIGBEE_AC_MEASUREMENT_TYPE measurement_type, ZIGBEE_AC_PHASE_TYPE phase_type, int32_t min, int32_t max); + +* ``measurement_type`` - AC measurement type constant (ZIGBEE_AC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_AC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_AC_MEASUREMENT_TYPE_POWER, ZIGBEE_AC_MEASUREMENT_TYPE_POWER_FACTOR, ZIGBEE_AC_MEASUREMENT_TYPE_FREQUENCY) +* ``phase_type`` - Phase type constant (ZIGBEE_AC_PHASE_TYPE_NON_SPECIFIC, ZIGBEE_AC_PHASE_TYPE_A, ZIGBEE_AC_PHASE_TYPE_B, ZIGBEE_AC_PHASE_TYPE_C) +* ``min`` - Minimum value +* ``max`` - Maximum value + +This function will return ``true`` if successful, ``false`` otherwise. + +setACMultiplierDivisor +^^^^^^^^^^^^^^^^^^^^^^ + +Sets the multiplier and divisor for scaling AC measurements. + +.. code-block:: arduino + + bool setACMultiplierDivisor(ZIGBEE_AC_MEASUREMENT_TYPE measurement_type, uint16_t multiplier, uint16_t divisor); + +* ``measurement_type`` - AC measurement type constant (ZIGBEE_AC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_AC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_AC_MEASUREMENT_TYPE_POWER, ZIGBEE_AC_MEASUREMENT_TYPE_POWER_FACTOR, ZIGBEE_AC_MEASUREMENT_TYPE_FREQUENCY) +* ``multiplier`` - Multiplier value +* ``divisor`` - Divisor value + +This function will return ``true`` if successful, ``false`` otherwise. + +setACPowerFactor +^^^^^^^^^^^^^^^^ + +Sets the power factor for a specific phase. + +.. code-block:: arduino + + bool setACPowerFactor(ZIGBEE_AC_PHASE_TYPE phase_type, int8_t power_factor); + +* ``phase_type`` - Phase type constant (ZIGBEE_AC_PHASE_TYPE_NON_SPECIFIC, ZIGBEE_AC_PHASE_TYPE_A, ZIGBEE_AC_PHASE_TYPE_B, ZIGBEE_AC_PHASE_TYPE_C) +* ``power_factor`` - Power factor value (-100 to 100) + +This function will return ``true`` if successful, ``false`` otherwise. + +setACReporting +^^^^^^^^^^^^^^ + +Sets the reporting configuration for AC measurements. + +.. code-block:: arduino + + bool setACReporting(ZIGBEE_AC_MEASUREMENT_TYPE measurement_type, ZIGBEE_AC_PHASE_TYPE phase_type, uint16_t min_interval, uint16_t max_interval, int32_t delta); + +* ``measurement_type`` - AC measurement type constant (ZIGBEE_AC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_AC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_AC_MEASUREMENT_TYPE_POWER, ZIGBEE_AC_MEASUREMENT_TYPE_POWER_FACTOR, ZIGBEE_AC_MEASUREMENT_TYPE_FREQUENCY) +* ``phase_type`` - Phase type constant (ZIGBEE_AC_PHASE_TYPE_NON_SPECIFIC, ZIGBEE_AC_PHASE_TYPE_A, ZIGBEE_AC_PHASE_TYPE_B, ZIGBEE_AC_PHASE_TYPE_C) +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report + +This function will return ``true`` if successful, ``false`` otherwise. + +reportAC +^^^^^^^^ + +Manually reports an AC measurement value. + +.. code-block:: arduino + + bool reportAC(ZIGBEE_AC_MEASUREMENT_TYPE measurement_type, ZIGBEE_AC_PHASE_TYPE phase_type); + +* ``measurement_type`` - AC measurement type constant (ZIGBEE_AC_MEASUREMENT_TYPE_VOLTAGE, ZIGBEE_AC_MEASUREMENT_TYPE_CURRENT, ZIGBEE_AC_MEASUREMENT_TYPE_POWER, ZIGBEE_AC_MEASUREMENT_TYPE_POWER_FACTOR, ZIGBEE_AC_MEASUREMENT_TYPE_FREQUENCY) +* ``phase_type`` - Phase type constant (ZIGBEE_AC_PHASE_TYPE_NON_SPECIFIC, ZIGBEE_AC_PHASE_TYPE_A, ZIGBEE_AC_PHASE_TYPE_B, ZIGBEE_AC_PHASE_TYPE_C) + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +DC Electrical Measurement +************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Electrical_DC_Sensor/Zigbee_Electrical_DC_Sensor.ino + :language: arduino + +AC Electrical Measurement +************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Electrical_AC_Sensor_MultiPhase/Zigbee_Electrical_AC_Sensor_MultiPhase.ino + :language: arduino diff --git a/docs/en/zigbee/ep_flow_sensor.rst b/docs/en/zigbee/ep_flow_sensor.rst new file mode 100644 index 00000000000..9423f321a5d --- /dev/null +++ b/docs/en/zigbee/ep_flow_sensor.rst @@ -0,0 +1,119 @@ +################ +ZigbeeFlowSensor +################ + +About +----- + +The ``ZigbeeFlowSensor`` class provides a flow sensor endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for liquid and gas flow measurement devices. + +**Features:** +* Flow rate measurement in m³/h +* Configurable measurement range +* Tolerance and reporting configuration +* Automatic reporting capabilities +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Water flow monitoring +* Gas flow measurement +* Industrial process monitoring +* Smart home water management +* HVAC system flow monitoring +* Agricultural irrigation systems + +API Reference +------------- + +Constructor +*********** + +ZigbeeFlowSensor +^^^^^^^^^^^^^^^^ + +Creates a new Zigbee flow sensor endpoint. + +.. code-block:: arduino + + ZigbeeFlowSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setFlow +^^^^^^^ + +Sets the flow rate measurement value. + +.. code-block:: arduino + + bool setFlow(float value); + +* ``value`` - Flow rate value in 0.1 m³/h + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum measurement values. + +.. code-block:: arduino + + bool setMinMaxValue(float min, float max); + +* ``min`` - Minimum flow rate value in 0.1 m³/h +* ``max`` - Maximum flow rate value in 0.1 m³/h + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for measurements. + +.. code-block:: arduino + + bool setTolerance(float tolerance); + +* ``tolerance`` - Tolerance value in 0.01 m³/h + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting configuration for flow rate measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report in 0.1 m³/h + +This function will return ``true`` if successful, ``false`` otherwise. + +report +^^^^^^ + +Manually reports the current flow rate value. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Flow + PressureSensor Implementation +************************************ + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Pressure_Flow_Sensor/Zigbee_Pressure_Flow_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_gateway.rst b/docs/en/zigbee/ep_gateway.rst new file mode 100644 index 00000000000..d436887a373 --- /dev/null +++ b/docs/en/zigbee/ep_gateway.rst @@ -0,0 +1,35 @@ +############# +ZigbeeGateway +############# + +About +----- + +The ``ZigbeeGateway`` class provides a gateway endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for network coordination and gateway functionality. +Gateway is a device that can be used to bridge Zigbee network to other networks (e.g. Wi-Fi, Ethernet, etc.). + +API Reference +------------- + +Constructor +*********** + +ZigbeeGateway +^^^^^^^^^^^^^ + +Creates a new Zigbee gateway endpoint. + +.. code-block:: arduino + + ZigbeeGateway(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Example +------- + +Gateway Implementation +********************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Gateway/Zigbee_Gateway.ino + :language: arduino diff --git a/docs/en/zigbee/ep_illuminance_sensor.rst b/docs/en/zigbee/ep_illuminance_sensor.rst new file mode 100644 index 00000000000..1e627f7dfe9 --- /dev/null +++ b/docs/en/zigbee/ep_illuminance_sensor.rst @@ -0,0 +1,109 @@ +####################### +ZigbeeIlluminanceSensor +####################### + +About +----- + +The ``ZigbeeIlluminanceSensor`` class provides an endpoint for illuminance sensors in Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for light level measurement devices, supporting ambient light monitoring. + +**Features:** +* Illuminance measurement in lux +* Configurable measurement range +* Tolerance and reporting configuration +* Automatic reporting capabilities + +API Reference +------------- + +Constructor +*********** + +ZigbeeIlluminanceSensor +^^^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee illuminance sensor endpoint. + +.. code-block:: arduino + + ZigbeeIlluminanceSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setIlluminance +^^^^^^^^^^^^^^ + +Sets the illuminance measurement value. + +.. code-block:: arduino + + bool setIlluminance(uint16_t value); + +* ``value`` - Illuminance value in lux + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum measurement values. + +.. code-block:: arduino + + bool setMinMaxValue(uint16_t min, uint16_t max); + +* ``min`` - Minimum illuminance value in lux +* ``max`` - Maximum illuminance value in lux + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for measurements. + +.. code-block:: arduino + + bool setTolerance(uint16_t tolerance); + +* ``tolerance`` - Tolerance value in lux + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting configuration for illuminance measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, uint16_t delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report in lux + +This function will return ``true`` if successful, ``false`` otherwise. + +report +^^^^^^ + +Manually reports the current illuminance value. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Illuminance Sensor Implementation +********************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Illuminance_Sensor/Zigbee_Illuminance_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_light.rst b/docs/en/zigbee/ep_light.rst new file mode 100644 index 00000000000..fb70320321b --- /dev/null +++ b/docs/en/zigbee/ep_light.rst @@ -0,0 +1,88 @@ +########### +ZigbeeLight +########### + +About +----- + +The ``ZigbeeLight`` class provides a simple on/off light endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for basic lighting control. + +**Features:** +* Simple on/off control +* State change callbacks + +API Reference +------------- + +Constructor +*********** + +ZigbeeLight +^^^^^^^^^^^ + +Creates a new Zigbee light endpoint. + +.. code-block:: arduino + + ZigbeeLight(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Light Control +************* + +setLight +^^^^^^^^ + +Sets the light state (on or off). + +.. code-block:: arduino + + bool setLight(bool state); + +* ``state`` - ``true`` to turn on, ``false`` to turn off + +This function will return ``true`` if successful, ``false`` otherwise. + +getLightState +^^^^^^^^^^^^^ + +Gets the current light state. + +.. code-block:: arduino + + bool getLightState(); + +This function will return current light state (``true`` = on, ``false`` = off). + +restoreLight +^^^^^^^^^^^^ + +Restores the light state and triggers any registered callbacks. + +.. code-block:: arduino + + void restoreLight(); + +Event Handling +************** + +onLightChange +^^^^^^^^^^^^^ + +Sets a callback function to be called when the light state changes. + +.. code-block:: arduino + + void onLightChange(void (*callback)(bool)); + +* ``callback`` - Function to call when light state changes + +Example +------- + +Basic Light Implementation +************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_On_Off_Light/Zigbee_On_Off_Light.ino + :language: arduino diff --git a/docs/en/zigbee/ep_occupancy_sensor.rst b/docs/en/zigbee/ep_occupancy_sensor.rst new file mode 100644 index 00000000000..7fe50d59ea4 --- /dev/null +++ b/docs/en/zigbee/ep_occupancy_sensor.rst @@ -0,0 +1,81 @@ +##################### +ZigbeeOccupancySensor +##################### + +About +----- + +The ``ZigbeeOccupancySensor`` class provides an endpoint for occupancy sensors in Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for occupancy detection devices, supporting various sensor types for detecting presence. + +**Features:** +* Occupancy detection (occupied/unoccupied) +* Multiple sensor type support (PIR, ultrasonic, etc.) +* Automatic reporting capabilities +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +API Reference +------------- + +Constructor +*********** + +ZigbeeOccupancySensor +^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee occupancy sensor endpoint. + +.. code-block:: arduino + + ZigbeeOccupancySensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setOccupancy +^^^^^^^^^^^^ + +Sets the occupancy state. + +.. code-block:: arduino + + bool setOccupancy(bool occupied); + +* ``occupied`` - Occupancy state (true = occupied, false = unoccupied) + +This function will return ``true`` if successful, ``false`` otherwise. + +setSensorType +^^^^^^^^^^^^^ + +Sets the sensor type. + +.. code-block:: arduino + + bool setSensorType(uint8_t sensor_type); + +* ``sensor_type`` - Sensor type identifier (see esp_zb_zcl_occupancy_sensing_occupancy_sensor_type_t) + +This function will return ``true`` if successful, ``false`` otherwise. + +report +^^^^^^ + +Manually reports the current occupancy state. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Occupancy Sensor Implementation +******************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Occupancy_Sensor/Zigbee_Occupancy_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_pm25_sensor.rst b/docs/en/zigbee/ep_pm25_sensor.rst new file mode 100644 index 00000000000..2f1432f8224 --- /dev/null +++ b/docs/en/zigbee/ep_pm25_sensor.rst @@ -0,0 +1,109 @@ +################ +ZigbeePM25Sensor +################ + +About +----- + +The ``ZigbeePM25Sensor`` class provides a PM2.5 air quality sensor endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for particulate matter measurement devices. + +**Features:** +* PM2.5 concentration measurement in μg/m³ +* Configurable measurement range +* Tolerance and reporting configuration +* Automatic reporting capabilities + +API Reference +------------- + +Constructor +*********** + +ZigbeePM25Sensor +^^^^^^^^^^^^^^^^ + +Creates a new Zigbee PM2.5 sensor endpoint. + +.. code-block:: arduino + + ZigbeePM25Sensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setPM25 +^^^^^^^ + +Sets the PM2.5 concentration measurement value. + +.. code-block:: arduino + + bool setPM25(float pm25); + +* ``pm25`` - PM2.5 concentration value in 0.1 μg/m³ + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum measurement values. + +.. code-block:: arduino + + bool setMinMaxValue(float min, float max); + +* ``min`` - Minimum PM2.5 concentration value in 0.1 μg/m³ +* ``max`` - Maximum PM2.5 concentration value in 0.1 μg/m³ + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for measurements. + +.. code-block:: arduino + + bool setTolerance(float tolerance); + +* ``tolerance`` - Tolerance value in 0.1 μg/m³ + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting configuration for PM2.5 measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report in 0.1 μg/m³ + +This function will return ``true`` if successful, ``false`` otherwise. + +report +^^^^^^ + +Manually reports the current PM2.5 concentration value. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +PM2.5 Sensor Implementation +*************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_PM25_Sensor/Zigbee_PM25_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_power_outlet.rst b/docs/en/zigbee/ep_power_outlet.rst new file mode 100644 index 00000000000..97b22cbbc2a --- /dev/null +++ b/docs/en/zigbee/ep_power_outlet.rst @@ -0,0 +1,88 @@ +################# +ZigbeePowerOutlet +################# + +About +----- + +The ``ZigbeePowerOutlet`` class provides a smart power outlet endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for power control, allowing remote on/off control of electrical devices. + +**Features:** +* On/off power control +* State change callbacks + +API Reference +------------- + +Constructor +*********** + +ZigbeePowerOutlet +^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee power outlet endpoint. + +.. code-block:: arduino + + ZigbeePowerOutlet(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Power Control +************* + +setState +^^^^^^^^ + +Sets the power outlet state (on or off). + +.. code-block:: arduino + + bool setState(bool state); + +* ``state`` - ``true`` to turn on, ``false`` to turn off + +This function will return ``true`` if successful, ``false`` otherwise. + +getPowerOutletState +^^^^^^^^^^^^^^^^^^^ + +Gets the current power outlet state. + +.. code-block:: arduino + + bool getPowerOutletState(); + +This function will return current power state (``true`` = on, ``false`` = off). + +restoreState +^^^^^^^^^^^^ + +Restores the power outlet state and triggers any registered callbacks. + +.. code-block:: arduino + + void restoreState(); + +Event Handling +************** + +onPowerOutletChange +^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when the power outlet state changes. + +.. code-block:: arduino + + void onPowerOutletChange(void (*callback)(bool)); + +* ``callback`` - Function to call when power outlet state changes + +Example +------- + +Smart Power Outlet Implementation +********************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Power_Outlet/Zigbee_Power_Outlet.ino + :language: arduino diff --git a/docs/en/zigbee/ep_pressure_sensor.rst b/docs/en/zigbee/ep_pressure_sensor.rst new file mode 100644 index 00000000000..5d857bb163e --- /dev/null +++ b/docs/en/zigbee/ep_pressure_sensor.rst @@ -0,0 +1,109 @@ +#################### +ZigbeePressureSensor +#################### + +About +----- + +The ``ZigbeePressureSensor`` class provides an endpoint for pressure sensors in Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for pressure measurement devices, supporting atmospheric pressure, barometric pressure, and other pressure measurements. + +**Features:** +* Pressure measurement in hPa (hectopascals) +* Configurable measurement range +* Tolerance and reporting configuration +* Automatic reporting capabilities + +API Reference +------------- + +Constructor +*********** + +ZigbeePressureSensor +^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee pressure sensor endpoint. + +.. code-block:: arduino + + ZigbeePressureSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setPressure +^^^^^^^^^^^ + +Sets the pressure measurement value. + +.. code-block:: arduino + + bool setPressure(int16_t value); + +* ``value`` - Pressure value in hPa + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum measurement values. + +.. code-block:: arduino + + bool setMinMaxValue(int16_t min, int16_t max); + +* ``min`` - Minimum pressure value in hPa +* ``max`` - Maximum pressure value in hPa + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for measurements. + +.. code-block:: arduino + + bool setTolerance(uint16_t tolerance); + +* ``tolerance`` - Tolerance value in hPa + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting configuration for pressure measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, uint16_t delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report in hPa + +This function will return ``true`` if successful, ``false`` otherwise. + +report +^^^^^^ + +Manually reports the current pressure value. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Pressure + Flow Sensor Implementation +************************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Pressure_Flow_Sensor/Zigbee_Pressure_Flow_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_range_extender.rst b/docs/en/zigbee/ep_range_extender.rst new file mode 100644 index 00000000000..b451f8764fc --- /dev/null +++ b/docs/en/zigbee/ep_range_extender.rst @@ -0,0 +1,34 @@ +################### +ZigbeeRangeExtender +################### + +About +----- + +The ``ZigbeeRangeExtender`` class provides a range extender endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for range extender devices that help extend the coverage of Zigbee networks by acting as repeaters. + +API Reference +------------- + +Constructor +*********** + +ZigbeeRangeExtender +^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee range extender endpoint. + +.. code-block:: arduino + + ZigbeeRangeExtender(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Example +------- + +Range Extender Implementation +***************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Range_Extender/Zigbee_Range_Extender.ino + :language: arduino diff --git a/docs/en/zigbee/ep_switch.rst b/docs/en/zigbee/ep_switch.rst new file mode 100644 index 00000000000..e1847996cc2 --- /dev/null +++ b/docs/en/zigbee/ep_switch.rst @@ -0,0 +1,136 @@ +############ +ZigbeeSwitch +############ + +About +----- + +The ``ZigbeeSwitch`` class provides a switch endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for controlling other devices (typically lights) through on/off commands. + +**Features:** +* On/off control commands for bound devices +* Group control support +* Direct device addressing + +API Reference +------------- + +Constructor +*********** + +ZigbeeSwitch +^^^^^^^^^^^^ + +Creates a new Zigbee switch endpoint. + +.. code-block:: arduino + + ZigbeeSwitch(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Basic Control Commands +********************** + +lightToggle +^^^^^^^^^^^ + +Toggles the state of bound lights (on to off, or off to on). + +.. code-block:: arduino + + void lightToggle(); + void lightToggle(uint16_t group_addr); + void lightToggle(uint8_t endpoint, uint16_t short_addr); + void lightToggle(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +lightOn +^^^^^^^ + +Turns on bound lights. + +.. code-block:: arduino + + void lightOn(); + void lightOn(uint16_t group_addr); + void lightOn(uint8_t endpoint, uint16_t short_addr); + void lightOn(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +lightOff +^^^^^^^^ + +Turns off bound lights. + +.. code-block:: arduino + + void lightOff(); + void lightOff(uint16_t group_addr); + void lightOff(uint8_t endpoint, uint16_t short_addr); + void lightOff(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``group_addr`` - Group address to control (optional) +* ``endpoint`` - Target device endpoint (optional) +* ``short_addr`` - Target device short address (optional) +* ``ieee_addr`` - Target device IEEE address (optional) + +Advanced Control Commands +************************* + +lightOffWithEffect +^^^^^^^^^^^^^^^^^^ + +Turns off lights with a specific effect. + +.. code-block:: arduino + + void lightOffWithEffect(uint8_t effect_id, uint8_t effect_variant); + +* ``effect_id`` - Effect identifier +* ``effect_variant`` - Effect variant + +lightOnWithTimedOff +^^^^^^^^^^^^^^^^^^^ + +Turns on lights with automatic turn-off after specified time. + +.. code-block:: arduino + + void lightOnWithTimedOff(uint8_t on_off_control, uint16_t time_on, uint16_t time_off); + +* ``on_off_control`` - Control byte +* ``time_on`` - Time to stay on (in 1/10th seconds) +* ``time_off`` - Time to stay off (in 1/10th seconds) + +lightOnWithSceneRecall +^^^^^^^^^^^^^^^^^^^^^^ + +Turns on lights by recalling a scene. + +.. code-block:: arduino + + void lightOnWithSceneRecall(); + +Example +------- + +Basic Switch Implementation +*************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_On_Off_Switch/Zigbee_On_Off_Switch.ino + :language: arduino + +Multi Switch Implementation +*************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_On_Off_MultiSwitch/Zigbee_On_Off_MultiSwitch.ino + :language: arduino diff --git a/docs/en/zigbee/ep_temperature_sensor.rst b/docs/en/zigbee/ep_temperature_sensor.rst new file mode 100644 index 00000000000..85c2147f1a1 --- /dev/null +++ b/docs/en/zigbee/ep_temperature_sensor.rst @@ -0,0 +1,184 @@ +################ +ZigbeeTempSensor +################ + +About +----- + +The ``ZigbeeTempSensor`` class provides a temperature and humidity sensor endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for environmental monitoring with configurable reporting intervals and thresholds. + +**Features:** +* Temperature measurement and reporting +* Optional humidity measurement +* Configurable reporting intervals +* Min/max value and tolerance settings + +API Reference +------------- + +Constructor +*********** + +ZigbeeTempSensor +^^^^^^^^^^^^^^^^ + +Creates a new Zigbee temperature sensor endpoint. + +.. code-block:: arduino + + ZigbeeTempSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Temperature Control +******************* + +setTemperature +^^^^^^^^^^^^^^ + +Sets the temperature value in 0.01°C resolution. + +.. code-block:: arduino + + bool setTemperature(float value); + +* ``value`` - Temperature value in degrees Celsius + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum temperature values for the sensor. + +.. code-block:: arduino + + bool setMinMaxValue(float min, float max); + +* ``min`` - Minimum temperature value in degrees Celsius +* ``max`` - Maximum temperature value in degrees Celsius + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for temperature reporting. + +.. code-block:: arduino + + bool setTolerance(float tolerance); + +* ``tolerance`` - Tolerance value in degrees Celsius + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting interval for temperature measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in temperature to trigger report (in 0.01°C) + +This function will return ``true`` if successful, ``false`` otherwise. + +reportTemperature +^^^^^^^^^^^^^^^^^ + +Manually reports the current temperature value. + +.. code-block:: arduino + + bool reportTemperature(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Humidity Control (Optional) +*************************** + +addHumiditySensor +^^^^^^^^^^^^^^^^^ + +Adds humidity measurement capability to the temperature sensor. + +.. code-block:: arduino + + void addHumiditySensor(float min, float max, float tolerance); + +* ``min`` - Minimum humidity value in percentage +* ``max`` - Maximum humidity value in percentage +* ``tolerance`` - Tolerance value in percentage + +setHumidity +^^^^^^^^^^^ + +Sets the humidity value in 0.01% resolution. + +.. code-block:: arduino + + bool setHumidity(float value); + +* ``value`` - Humidity value in percentage (0-100) + +This function will return ``true`` if successful, ``false`` otherwise. + +setHumidityReporting +^^^^^^^^^^^^^^^^^^^^ + +Sets the reporting interval for humidity measurements. + +.. code-block:: arduino + + bool setHumidityReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in humidity to trigger report (in 0.01%) + +This function will return ``true`` if successful, ``false`` otherwise. + +reportHumidity +^^^^^^^^^^^^^^ + +Manually reports the current humidity value. + +.. code-block:: arduino + + bool reportHumidity(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Combined Reporting +****************** + +report +^^^^^^ + +Reports both temperature and humidity values if humidity sensor is enabled. + +.. code-block:: arduino + + bool report(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Temperature Sensor Implementation +********************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Temperature_Sensor/Zigbee_Temperature_Sensor.ino + :language: arduino + +Temperature + Humidity Sleepy Sensor Implementation +*************************************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Temp_Hum_Sensor_Sleepy/Zigbee_Temp_Hum_Sensor_Sleepy.ino + :language: arduino diff --git a/docs/en/zigbee/ep_thermostat.rst b/docs/en/zigbee/ep_thermostat.rst new file mode 100644 index 00000000000..b7c79254d0f --- /dev/null +++ b/docs/en/zigbee/ep_thermostat.rst @@ -0,0 +1,238 @@ +################ +ZigbeeThermostat +################ + +About +----- + +The ``ZigbeeThermostat`` class provides a thermostat endpoint for Zigbee networks that receives temperature data from temperature sensors. This endpoint implements the Zigbee Home Automation (HA) standard for thermostats that can bind to temperature sensors and receive temperature readings. + +**Features:** +* Automatic discovery and binding to temperature sensors +* Temperature data reception from bound sensors +* Configurable temperature reporting intervals +* Sensor settings retrieval (min/max temperature, tolerance) +* Multiple addressing modes (group, specific endpoint, IEEE address) + +API Reference +------------- + +Constructor +*********** + +ZigbeeThermostat +^^^^^^^^^^^^^^^^ + +Creates a new Zigbee thermostat endpoint. + +.. code-block:: arduino + + ZigbeeThermostat(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Event Handling +************** + +onTempReceive +^^^^^^^^^^^^^ + +Sets a callback function for receiving temperature data. + +.. code-block:: arduino + + void onTempReceive(void (*callback)(float temperature)); + +* ``callback`` - Function to call when temperature data is received +* ``temperature`` - Temperature value in degrees Celsius + +onTempReceiveWithSource +^^^^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function for receiving temperature data with source information. + +.. code-block:: arduino + + void onTempReceiveWithSource(void (*callback)(float temperature, uint8_t src_endpoint, esp_zb_zcl_addr_t src_address)); + +* ``callback`` - Function to call when temperature data is received +* ``temperature`` - Temperature value in degrees Celsius +* ``src_endpoint`` - Source endpoint that sent the temperature data +* ``src_address`` - Source address information + +onConfigReceive +^^^^^^^^^^^^^^^ + +Sets a callback function for receiving sensor configuration data. + +.. code-block:: arduino + + void onConfigReceive(void (*callback)(float min_temp, float max_temp, float tolerance)); + +* ``callback`` - Function to call when sensor configuration is received +* ``min_temp`` - Minimum temperature supported by the sensor +* ``max_temp`` - Maximum temperature supported by the sensor +* ``tolerance`` - Temperature tolerance of the sensor + +Temperature Data Retrieval +************************** + +getTemperature +^^^^^^^^^^^^^^ + +Requests temperature data from all bound sensors. + +.. code-block:: arduino + + void getTemperature(); + +getTemperature (Group) +^^^^^^^^^^^^^^^^^^^^^^ + +Requests temperature data from a specific group. + +.. code-block:: arduino + + void getTemperature(uint16_t group_addr); + +* ``group_addr`` - Group address to send the request to + +getTemperature (Endpoint + Short Address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requests temperature data from a specific endpoint using short address. + +.. code-block:: arduino + + void getTemperature(uint8_t endpoint, uint16_t short_addr); + +* ``endpoint`` - Target endpoint number +* ``short_addr`` - Short address of the target device + +getTemperature (Endpoint + IEEE Address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requests temperature data from a specific endpoint using IEEE address. + +.. code-block:: arduino + + void getTemperature(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``endpoint`` - Target endpoint number +* ``ieee_addr`` - IEEE address of the target device + +Sensor Settings Retrieval +************************* + +getSensorSettings +^^^^^^^^^^^^^^^^^ + +Requests sensor settings from all bound sensors. + +.. code-block:: arduino + + void getSensorSettings(); + +getSensorSettings (Group) +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requests sensor settings from a specific group. + +.. code-block:: arduino + + void getSensorSettings(uint16_t group_addr); + +* ``group_addr`` - Group address to send the request to + +getSensorSettings (Endpoint + Short Address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requests sensor settings from a specific endpoint using short address. + +.. code-block:: arduino + + void getSensorSettings(uint8_t endpoint, uint16_t short_addr); + +* ``endpoint`` - Target endpoint number +* ``short_addr`` - Short address of the target device + +getSensorSettings (Endpoint + IEEE Address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requests sensor settings from a specific endpoint using IEEE address. + +.. code-block:: arduino + + void getSensorSettings(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr); + +* ``endpoint`` - Target endpoint number +* ``ieee_addr`` - IEEE address of the target device + +Temperature Reporting Configuration +*********************************** + +setTemperatureReporting +^^^^^^^^^^^^^^^^^^^^^^^ + +Configures temperature reporting for all bound sensors. + +.. code-block:: arduino + + void setTemperatureReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in temperature to trigger a report + +setTemperatureReporting (Group) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configures temperature reporting for a specific group. + +.. code-block:: arduino + + void setTemperatureReporting(uint16_t group_addr, uint16_t min_interval, uint16_t max_interval, float delta); + +* ``group_addr`` - Group address to configure +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in temperature to trigger a report + +setTemperatureReporting (Endpoint + Short Address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configures temperature reporting for a specific endpoint using short address. + +.. code-block:: arduino + + void setTemperatureReporting(uint8_t endpoint, uint16_t short_addr, uint16_t min_interval, uint16_t max_interval, float delta); + +* ``endpoint`` - Target endpoint number +* ``short_addr`` - Short address of the target device +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in temperature to trigger a report + +setTemperatureReporting (Endpoint + IEEE Address) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configures temperature reporting for a specific endpoint using IEEE address. + +.. code-block:: arduino + + void setTemperatureReporting(uint8_t endpoint, esp_zb_ieee_addr_t ieee_addr, uint16_t min_interval, uint16_t max_interval, float delta); + +* ``endpoint`` - Target endpoint number +* ``ieee_addr`` - IEEE address of the target device +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change in temperature to trigger a report + +Example +------- + +Thermostat Implementation +************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Thermostat/Zigbee_Thermostat.ino + :language: arduino diff --git a/docs/en/zigbee/ep_vibration_sensor.rst b/docs/en/zigbee/ep_vibration_sensor.rst new file mode 100644 index 00000000000..896c4672c6d --- /dev/null +++ b/docs/en/zigbee/ep_vibration_sensor.rst @@ -0,0 +1,87 @@ +##################### +ZigbeeVibrationSensor +##################### + +About +----- + +The ``ZigbeeVibrationSensor`` class provides a vibration sensor endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for vibration detection devices. + +**Features:** +* Vibration detection and measurement +* Configurable sensitivity levels +* Multiple detection modes +* Automatic reporting capabilities +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Security system vibration detection +* Industrial equipment monitoring +* Structural health monitoring +* Smart home security applications +* Machine condition monitoring + +API Reference +------------- + +Constructor +*********** + +ZigbeeVibrationSensor +^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee vibration sensor endpoint. + +.. code-block:: arduino + + ZigbeeVibrationSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setVibration +^^^^^^^^^^^^ + +Sets the vibration detection state. + +.. code-block:: arduino + + bool setVibration(bool sensed); + +* ``sensed`` - Vibration state (true = sensed, false = not sensed) + +This function will return ``true`` if successful, ``false`` otherwise. + +setIASClientEndpoint +^^^^^^^^^^^^^^^^^^^^ + +Sets the IAS Client endpoint number (default is 1). + +.. code-block:: arduino + + void setIASClientEndpoint(uint8_t ep_number); + +* ``ep_number`` - IAS Client endpoint number + +report +^^^^^^ + +Manually reports the current vibration state. + +.. code-block:: arduino + + void report(); + +This function does not return a value. + +Example +------- + +Vibration Sensor Implementation +******************************* + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Vibration_Sensor/Zigbee_Vibration_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_wind_speed_sensor.rst b/docs/en/zigbee/ep_wind_speed_sensor.rst new file mode 100644 index 00000000000..67c4958e37c --- /dev/null +++ b/docs/en/zigbee/ep_wind_speed_sensor.rst @@ -0,0 +1,119 @@ +##################### +ZigbeeWindSpeedSensor +##################### + +About +----- + +The ``ZigbeeWindSpeedSensor`` class provides a wind speed sensor endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for wind speed measurement devices. + +**Features:** +* Wind speed measurement in m/s +* Configurable measurement range +* Tolerance and reporting configuration +* Automatic reporting capabilities +* Integration with common endpoint features (binding, OTA, etc.) +* Zigbee HA standard compliance + +**Use Cases:** +* Weather stations +* Wind turbine monitoring +* Agricultural weather monitoring +* Marine applications +* Smart home weather systems +* Industrial wind monitoring + +API Reference +------------- + +Constructor +*********** + +ZigbeeWindSpeedSensor +^^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee wind speed sensor endpoint. + +.. code-block:: arduino + + ZigbeeWindSpeedSensor(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +API Methods +*********** + +setWindSpeed +^^^^^^^^^^^^ + +Sets the wind speed measurement value. + +.. code-block:: arduino + + bool setWindSpeed(float value); + +* ``value`` - Wind speed value in 0.01 m/s + +This function will return ``true`` if successful, ``false`` otherwise. + +setMinMaxValue +^^^^^^^^^^^^^^ + +Sets the minimum and maximum measurement values. + +.. code-block:: arduino + + bool setMinMaxValue(float min, float max); + +* ``min`` - Minimum wind speed value in 0.01 m/s +* ``max`` - Maximum wind speed value in 0.01 m/s + +This function will return ``true`` if successful, ``false`` otherwise. + +setTolerance +^^^^^^^^^^^^ + +Sets the tolerance value for measurements. + +.. code-block:: arduino + + bool setTolerance(float tolerance); + +* ``tolerance`` - Tolerance value in 0.01 m/s + +This function will return ``true`` if successful, ``false`` otherwise. + +setReporting +^^^^^^^^^^^^ + +Sets the reporting configuration for wind speed measurements. + +.. code-block:: arduino + + bool setReporting(uint16_t min_interval, uint16_t max_interval, float delta); + +* ``min_interval`` - Minimum reporting interval in seconds +* ``max_interval`` - Maximum reporting interval in seconds +* ``delta`` - Minimum change required to trigger a report in 0.01 m/s + +This function will return ``true`` if successful, ``false`` otherwise. + +reportWindSpeed +^^^^^^^^^^^^^^^ + +Manually reports the current wind speed value. + +.. code-block:: arduino + + bool reportWindSpeed(); + +This function will return ``true`` if successful, ``false`` otherwise. + +Example +------- + +Wind Speed Sensor Implementation +******************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Wind_Speed_Sensor/Zigbee_Wind_Speed_Sensor.ino + :language: arduino diff --git a/docs/en/zigbee/ep_window_covering.rst b/docs/en/zigbee/ep_window_covering.rst new file mode 100644 index 00000000000..a4b8cd918a6 --- /dev/null +++ b/docs/en/zigbee/ep_window_covering.rst @@ -0,0 +1,233 @@ +#################### +ZigbeeWindowCovering +#################### + +About +----- + +The ``ZigbeeWindowCovering`` class provides a window covering endpoint for Zigbee networks. This endpoint implements the Zigbee Home Automation (HA) standard for motorized blinds, shades, and other window coverings. + +**Features:** +* Position control (lift and tilt) +* Multiple window covering types support +* Configurable operation modes and limits +* Status reporting and callbacks +* Safety features and limits + +**Supported Window Covering Types:** +* ROLLERSHADE - Lift support +* ROLLERSHADE_2_MOTOR - Lift support +* ROLLERSHADE_EXTERIOR - Lift support +* ROLLERSHADE_EXTERIOR_2_MOTOR - Lift support +* DRAPERY - Lift support +* AWNING - Lift support +* SHUTTER - Tilt support +* BLIND_TILT_ONLY - Tilt support +* BLIND_LIFT_AND_TILT - Lift and Tilt support +* PROJECTOR_SCREEN - Lift support + +API Reference +------------- + +Constructor +*********** + +ZigbeeWindowCovering +^^^^^^^^^^^^^^^^^^^^ + +Creates a new Zigbee window covering endpoint. + +.. code-block:: arduino + + ZigbeeWindowCovering(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Position Control +**************** + +setLiftPosition +^^^^^^^^^^^^^^^ + +Sets the window covering lift position. + +.. code-block:: arduino + + bool setLiftPosition(uint16_t lift_position); + +* ``lift_position`` - Lift position + +This function will return ``true`` if successful, ``false`` otherwise. + +setLiftPercentage +^^^^^^^^^^^^^^^^^ + +Sets the window covering lift position as a percentage. + +.. code-block:: arduino + + bool setLiftPercentage(uint8_t lift_percentage); + +* ``lift_percentage`` - Lift percentage (0-100, where 0 is fully closed, 100 is fully open) + +This function will return ``true`` if successful, ``false`` otherwise. + +setTiltPosition +^^^^^^^^^^^^^^^ + +Sets the window covering tilt position in degrees. + +.. code-block:: arduino + + bool setTiltPosition(uint16_t tilt_position); + +* ``tilt_position`` - Tilt position in degrees + +This function will return ``true`` if successful, ``false`` otherwise. + +setTiltPercentage +^^^^^^^^^^^^^^^^^ + +Sets the window covering tilt position as a percentage. + +.. code-block:: arduino + + bool setTiltPercentage(uint8_t tilt_percentage); + +* ``tilt_percentage`` - Tilt percentage (0-100) + +This function will return ``true`` if successful, ``false`` otherwise. + +Configuration +************* + +setCoveringType +^^^^^^^^^^^^^^^ + +Sets the window covering type. + +.. code-block:: arduino + + bool setCoveringType(ZigbeeWindowCoveringType covering_type); + +* ``covering_type`` - Window covering type (see supported types above) + +This function will return ``true`` if successful, ``false`` otherwise. + +setConfigStatus +^^^^^^^^^^^^^^^ + +Sets the window covering configuration status. + +.. code-block:: arduino + + bool setConfigStatus(bool operational, bool online, bool commands_reversed, bool lift_closed_loop, bool tilt_closed_loop, bool lift_encoder_controlled, bool tilt_encoder_controlled); + +* ``operational`` - Operational status +* ``online`` - Online status +* ``commands_reversed`` - Commands reversed flag +* ``lift_closed_loop`` - Lift closed loop flag +* ``tilt_closed_loop`` - Tilt closed loop flag +* ``lift_encoder_controlled`` - Lift encoder controlled flag +* ``tilt_encoder_controlled`` - Tilt encoder controlled flag + +This function will return ``true`` if successful, ``false`` otherwise. + +setMode +^^^^^^^ + +Sets the window covering operation mode. + +.. code-block:: arduino + + bool setMode(bool motor_reversed, bool calibration_mode, bool maintenance_mode, bool leds_on); + +* ``motor_reversed`` - Motor reversed flag +* ``calibration_mode`` - Calibration mode flag +* ``maintenance_mode`` - Maintenance mode flag +* ``leds_on`` - LEDs on flag + +This function will return ``true`` if successful, ``false`` otherwise. + +setLimits +^^^^^^^^^ + +Sets the motion limits for the window covering. + +.. code-block:: arduino + + bool setLimits(uint16_t installed_open_limit_lift, uint16_t installed_closed_limit_lift, uint16_t installed_open_limit_tilt, uint16_t installed_closed_limit_tilt); + +* ``installed_open_limit_lift`` - Installed open limit for lift +* ``installed_closed_limit_lift`` - Installed closed limit for lift +* ``installed_open_limit_tilt`` - Installed open limit for tilt +* ``installed_closed_limit_tilt`` - Installed closed limit for tilt + +This function will return ``true`` if successful, ``false`` otherwise. + +Event Handling +************** + +onOpen +^^^^^^ + +Sets a callback function to be called when the window covering opens. + +.. code-block:: arduino + + void onOpen(void (*callback)()); + +* ``callback`` - Function to call when window covering opens + +onClose +^^^^^^^ + +Sets a callback function to be called when the window covering closes. + +.. code-block:: arduino + + void onClose(void (*callback)()); + +* ``callback`` - Function to call when window covering closes + +onGoToLiftPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when lift percentage changes. + +.. code-block:: arduino + + void onGoToLiftPercentage(void (*callback)(uint8_t)); + +* ``callback`` - Function to call when lift percentage changes + +onGoToTiltPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when tilt percentage changes. + +.. code-block:: arduino + + void onGoToTiltPercentage(void (*callback)(uint8_t)); + +* ``callback`` - Function to call when tilt percentage changes + +onStop +^^^^^^ + +Sets a callback function to be called when window covering stops. + +.. code-block:: arduino + + void onStop(void (*callback)()); + +* ``callback`` - Function to call when window covering stops + +Example +------- + +Window Covering Implementation +****************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Window_Covering/Zigbee_Window_Covering.ino + :language: arduino diff --git a/docs/en/zigbee/zigbee.rst b/docs/en/zigbee/zigbee.rst new file mode 100644 index 00000000000..ba14adad8e1 --- /dev/null +++ b/docs/en/zigbee/zigbee.rst @@ -0,0 +1,159 @@ +###### +Zigbee +###### + +About +----- + +The Zigbee library provides support for creating Zigbee 3.0 compatible devices including: + +* Support for different Zigbee roles (Coordinator, Router, End Device) +* Network management (scanning, joining, commissioning) +* Multiple endpoint types for various device categories +* OTA (Over-The-Air) update support +* Power management for battery-powered devices +* Time synchronization +* Advanced binding and group management + +The Zigbee library is built on top of `ESP-ZIGBEE-SDK `_ and provides a high-level Arduino-style interface for creating Zigbee devices. + +Zigbee Network Topology +*********************** + +.. code-block:: text + + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Coordinator │◄─────►│ Router │◄─────►│ Router │ + │ (Gateway) │ │ (Repeater) │ │ (Thermostat) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + │ ▼ │ + │ ┌─────────────────┐ │ + │ │ End Device │ │ + │ │ (Sensor) │ │ + │ └─────────────────┘ │ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ End Device │ │ End Device │ + │ (Sensor) │ │ (Sensor) │ + └─────────────────┘ └─────────────────┘ + + +**Device Roles** + +* **Coordinator**: Forms and manages the network, stores network information +* **Router**: Extends network range, routes messages, mains powered devices (typically lights, switches, etc.) +* **End Device**: Battery-powered devices that can sleep for extended periods (typically sensors) + +Zigbee Library Structure +------------------------ + +**The library is split into three main components:** + +* ``ZigbeeCore``: The main class that manages the Zigbee network +* ``ZigbeeEP``: The base class for all Zigbee endpoints, which provides common functionality for all endpoint types +* ``Specific endpoint classes``: The classes for all Zigbee endpoints, which provides the specific functionality for each endpoint type + +ZigbeeCore +********** + +The ``ZigbeeCore`` class is the main entry point for all Zigbee operations. It serves as the central coordinator that manages: + +* **Network Operations**: Starting, stopping, and managing the Zigbee network +* **Device Role Management**: Configuring the device as Coordinator, Router, or End Device +* **Endpoint Management**: Adding and managing multiple device endpoints +* **Network Discovery**: Scanning for and joining existing networks +* **OTA Updates**: Managing over-the-air firmware updates +* **Power Management**: Configuring sleep modes for battery-powered devices + +The ``ZigbeeCore`` class is implemented as a singleton, meaning there's only one instance available globally. You access it directly as ``Zigbee`` without creating an instance. + +.. toctree:: + :maxdepth: 3 + + zigbee_core + +ZigbeeEP +******** + +The ``ZigbeeEP`` class is the base class for all Zigbee endpoints. It provides common functionality for all endpoint types. + +* **Device Information**: Every endpoint can be configured with manufacturer and model information that helps identify the device on the network +* **Binding Management**: Binding allows endpoints to establish direct communication links with other devices. This enables automatic command transmission without requiring manual addressing +* **Power Management**: Endpoints can report their power source type and battery status, which helps the network optimize communication patterns +* **Time Synchronization**: Endpoints can synchronize with network time, enabling time-based operations and scheduling +* **OTA Support**: Endpoints can receive over-the-air firmware updates to add new features or fix bugs +* **Device Discovery**: Endpoints can read manufacturer and model information from other devices on the network + +.. toctree:: + :maxdepth: 2 + + zigbee_ep + +Specific endpoint classes +************************* + +Library provides the following endpoint classes from lights, switches, sensors, etc. Each endpoint class provides the specific functionality for each endpoint type and inherits from the ``ZigbeeEP`` class. + +.. toctree:: + :maxdepth: 1 + :glob: + + ep_* + + +Common Problems and Issues +-------------------------- + +Troubleshooting +--------------- + +Common Issues +************* + +**Device won't join network** + * Ensure the coordinator is in pairing mode + * Check that the device is configured with the correct role + * Verify the channel mask includes the coordinator's channel + +**OTA updates fail** + * Ensure the OTA server is properly configured + * Check that the device has sufficient memory for the update + * Verify network connectivity + +**Battery devices not working** + * Ensure proper power source configuration + * Check sleep/wake timing settings + * Verify parent device (router/coordinator) is always powered + +**Binding issues** + * Check that both devices support the required clusters + * Verify that binding is enabled on both devices + * Ensure devices are on the same network + +**Network connectivity problems** + * Check that devices are within range + * Verify that routers are properly configured + * Check for interference from other 2.4 GHz devices + +Factory Reset +************* + +If you have problems with connecting to the network, you can try to factory reset the device. This will erase all the network settings and act as brand new device. + +.. code-block:: arduino + + Zigbee.factoryReset(true); // true = restart after reset + +Debug Mode +********** + +For better debugging, you can enable debug mode to get detailed information about network operations. Call debug mode before starting Zigbee. +Also selecting zigbee mode with *debug* suffix is recommended. + +.. code-block:: arduino + + Zigbee.setDebugMode(true); + // Start Zigbee with debug output + Zigbee.begin(); diff --git a/docs/en/zigbee/zigbee_core.rst b/docs/en/zigbee/zigbee_core.rst new file mode 100644 index 00000000000..ee8f7234210 --- /dev/null +++ b/docs/en/zigbee/zigbee_core.rst @@ -0,0 +1,375 @@ +########## +ZigbeeCore +########## + +About +----- + +The ``ZigbeeCore`` class is the main entry point for all Zigbee operations. It serves as the central class that manages: + +* **Network Operations**: Starting, stopping, and managing the Zigbee network +* **Device Role Management**: Configuring the device as Coordinator, Router, or End Device +* **Endpoint Management**: Adding and managing multiple device endpoints +* **Network Discovery**: Scanning for and joining existing networks + +ZigbeeCore APIs +--------------- + +Network Initialization +********************** + +begin +^^^^^ + +Initializes the Zigbee stack and starts the network. + +.. code-block:: arduino + + bool begin(zigbee_role_t role = ZIGBEE_END_DEVICE, bool erase_nvs = false); + bool begin(esp_zb_cfg_t *role_cfg, bool erase_nvs = false); + +* ``role`` - Device role (default: ``ZIGBEE_END_DEVICE``) +* ``role_cfg`` - Custom role configuration structure +* ``erase_nvs`` - Whether to erase NVS storage (default: ``false``) + +This function will return ``true`` if initialization successful, ``false`` otherwise. + +**Available Roles:** + +* **ZIGBEE_COORDINATOR**: Network coordinator, forms and manages the network +* **ZIGBEE_ROUTER**: Network router, connects to existing network and extends network range and routes messages (if device is mains powered, always use this role) +* **ZIGBEE_END_DEVICE**: End device, connects to existing network (typically battery-powered which can sleep) + +.. note:: + + Depending on the Zigbee role, proper Zigbee mode and partition scheme must be set in the Arduino IDE. + + * **ZIGBEE_COORDINATOR** and **ZIGBEE_ROUTER**: + * Zigbee mode to ``Zigbee ZCZR (coordinator/router)``. + * Partition scheme to ``Zigbee ZCZR xMB with spiffs`` (where ``x`` is the number of MB of selected flash size). + * **ZIGBEE_END_DEVICE**: + * Zigbee mode to ``Zigbee ED (end device)``. + * Partition scheme to ``Zigbee xMB with spiffs`` (where ``x`` is the number of MB of selected flash size). + +Network Status +************** + +started +^^^^^^^ + +Checks if the Zigbee stack has been started. + +.. code-block:: arduino + + bool started(); + +This function will return ``true`` if Zigbee stack is running, ``false`` otherwise. + +connected +^^^^^^^^^ + +Checks if the device is connected to a Zigbee network. + +.. code-block:: arduino + + bool connected(); + +This function will return ``true`` if connected to network, ``false`` otherwise. + +getRole +^^^^^^^ + +Gets the current Zigbee device role. + +.. code-block:: arduino + + zigbee_role_t getRole(); + +This function will return current device role (``ZIGBEE_COORDINATOR``, ``ZIGBEE_ROUTER``, ``ZIGBEE_END_DEVICE``). + +Endpoint Management +******************* + +addEndpoint +^^^^^^^^^^^ + +Adds an endpoint to the Zigbee network. + +.. code-block:: arduino + + bool addEndpoint(ZigbeeEP *ep); + +* ``ep`` - Pointer to the endpoint object to add + +This function will return ``true`` if endpoint added successfully, ``false`` otherwise. + +Network Configuration +********************* + +setPrimaryChannelMask +^^^^^^^^^^^^^^^^^^^^^ + +Sets the primary channel mask for network scanning and joining. + +.. code-block:: arduino + + void setPrimaryChannelMask(uint32_t mask); + +* ``mask`` - Channel mask (default: all channels 11-26, mask 0x07FFF800) + +setScanDuration +^^^^^^^^^^^^^^^ + +Sets the scan duration for network discovery. + +.. code-block:: arduino + + void setScanDuration(uint8_t duration); + +* ``duration`` - Scan duration (1-4, where 1 is fastest, 4 is slowest) + +getScanDuration +^^^^^^^^^^^^^^^ + +Gets the current scan duration setting. + +.. code-block:: arduino + + uint8_t getScanDuration(); + +This function will return current scan duration (1-4). + +Power Management +**************** + +setRxOnWhenIdle +^^^^^^^^^^^^^^^ + +Sets whether the device keeps its receiver on when idle. + +.. code-block:: arduino + + void setRxOnWhenIdle(bool rx_on_when_idle); + +* ``rx_on_when_idle`` - ``true`` to keep receiver on, ``false`` to allow sleep + +getRxOnWhenIdle +^^^^^^^^^^^^^^^ + +Gets the current receiver idle setting. + +.. code-block:: arduino + + bool getRxOnWhenIdle(); + +This function will return current receiver idle setting. + +setTimeout +^^^^^^^^^^ + +Sets the timeout for network operations. + +.. code-block:: arduino + + void setTimeout(uint32_t timeout); + +* ``timeout`` - Timeout in milliseconds (default: 30000 ms) + +Network Discovery +***************** + +scanNetworks +^^^^^^^^^^^^ + +Scans for available Zigbee networks. + +.. code-block:: arduino + + void scanNetworks(uint32_t channel_mask = ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK, uint8_t scan_duration = 5); + +* ``channel_mask`` - Channels to scan (default: all channels) +* ``scan_duration`` - Scan duration (default: 5) + +scanComplete +^^^^^^^^^^^^ + +Checks if network scanning is complete. + +.. code-block:: arduino + + int16_t scanComplete(); + +This function will return: +* ``-2``: Scan failed or not started +* ``-1``: Scan running +* ``0``: No networks found +* ``>0``: Number of networks found + +getScanResult +^^^^^^^^^^^^^ + +Gets the scan results. + +.. code-block:: arduino + + zigbee_scan_result_t *getScanResult(); + +This function will return pointer to scan results, or ``NULL`` if no results. + +scanDelete +^^^^^^^^^^ + +Deletes the scan results from memory. + +.. code-block:: arduino + + void scanDelete(); + +Network Management (Coordinator only) +************************************* + +setRebootOpenNetwork +^^^^^^^^^^^^^^^^^^^^ + +Opens the network for joining after reboot for a specified time. + +.. code-block:: arduino + + void setRebootOpenNetwork(uint8_t time); + +* ``time`` - Time in seconds to keep network open after reboot + +openNetwork +^^^^^^^^^^^ + +Opens the network for device joining for a specified time. + +.. code-block:: arduino + + void openNetwork(uint8_t time); + +* ``time`` - Time in seconds to keep network open for device joining + +closeNetwork +^^^^^^^^^^^^ + +Closes the network to prevent new devices from joining. + +.. code-block:: arduino + + void closeNetwork(); + +Radio Configuration +******************* + +setRadioConfig +^^^^^^^^^^^^^^ + +Sets the radio configuration. + +.. code-block:: arduino + + void setRadioConfig(esp_zb_radio_config_t config); + +* ``config`` - Radio configuration structure + +getRadioConfig +^^^^^^^^^^^^^^ + +Gets the current radio configuration. + +.. code-block:: arduino + + esp_zb_radio_config_t getRadioConfig(); + +This function will return current radio configuration. + +Host Configuration +****************** + +setHostConfig +^^^^^^^^^^^^^ + +Sets the host configuration. + +.. code-block:: arduino + + void setHostConfig(esp_zb_host_config_t config); + +* ``config`` - Host configuration structure + +getHostConfig +^^^^^^^^^^^^^ + +Gets the current host configuration. + +.. code-block:: arduino + + esp_zb_host_config_t getHostConfig(); + +This function will return current host configuration. + +Debug and Utilities +******************* + +setDebugMode +^^^^^^^^^^^^ + +Enables or disables debug mode. + +.. code-block:: arduino + + void setDebugMode(bool debug); + +* ``debug`` - ``true`` to enable debug output, ``false`` to disable + +getDebugMode +^^^^^^^^^^^^ + +Gets the current debug mode setting. + +.. code-block:: arduino + + bool getDebugMode(); + +This function will return current debug mode setting. + +factoryReset +^^^^^^^^^^^^ + +Performs a factory reset, clearing all network settings. + +.. code-block:: arduino + + void factoryReset(bool restart = true); + +* ``restart`` - ``true`` to restart after reset (default: ``true``) + +Utility Functions +***************** + +formatIEEEAddress +^^^^^^^^^^^^^^^^^ + +Formats an IEEE address for display. + +.. code-block:: arduino + + static const char *formatIEEEAddress(const esp_zb_ieee_addr_t addr); + +* ``addr`` - IEEE address to format + +This function will return formatted address string. + +formatShortAddress +^^^^^^^^^^^^^^^^^^ + +Formats a short address for display. + +.. code-block:: arduino + + static const char *formatShortAddress(uint16_t addr); + +* ``addr`` - Short address to format + +This function will return formatted address string. diff --git a/docs/en/zigbee/zigbee_ep.rst b/docs/en/zigbee/zigbee_ep.rst new file mode 100644 index 00000000000..ce13d60bbaf --- /dev/null +++ b/docs/en/zigbee/zigbee_ep.rst @@ -0,0 +1,357 @@ +######## +ZigbeeEP +######## + +About +----- + +The ``ZigbeeEP`` class is the base class for all Zigbee endpoints. It provides common functionality for all endpoint types. + +* **Device Information**: Every endpoint can be configured with manufacturer and model information that helps identify the device on the network +* **Binding Management**: Binding allows endpoints to establish direct communication links with other devices. This enables automatic command transmission without requiring manual addressing +* **Power Management**: Endpoints can report their power source type and battery status, which helps the network optimize communication patterns +* **Time Synchronization**: Endpoints can synchronize with network time, enabling time-based operations and scheduling +* **OTA Support**: Endpoints can receive over-the-air firmware updates to add new features or fix bugs +* **Device Discovery**: Endpoints can read manufacturer and model information from other devices on the network + + +ZigbeeEP APIs +------------- + +Device Information +****************** + +setManufacturerAndModel +^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the manufacturer name and model identifier for the device. + +.. code-block:: arduino + + bool setManufacturerAndModel(const char *name, const char *model); + +* ``name`` - Manufacturer name (max 32 characters) +* ``model`` - Model identifier (max 32 characters) + +This function will return ``true`` if set successfully, ``false`` otherwise. + +getEndpoint +^^^^^^^^^^^ + +Gets the endpoint number assigned to this device. + +.. code-block:: arduino + + uint8_t getEndpoint(); + +This function will return the endpoint number (1-254). + +Binding Management +****************** + +bound +^^^^^ + +Checks if the endpoint has any bound devices. + +.. code-block:: arduino + + bool bound(); + +This function will return ``true`` if the endpoint has bound devices, ``false`` otherwise. + +getBoundDevices +^^^^^^^^^^^^^^^ + +Gets the list of devices bound to this endpoint. + +.. code-block:: arduino + + std::vector getBoundDevices(); + +This function will return list of bound device parameters. + +printBoundDevices +^^^^^^^^^^^^^^^^^ + +Prints information about bound devices to Serial or a custom Print object. + +.. code-block:: arduino + + void printBoundDevices(Print &print = Serial); + +* ``print`` - Custom Print object (optional, defaults to Serial) + +allowMultipleBinding +^^^^^^^^^^^^^^^^^^^^ + +Enables or disables multiple device binding for this endpoint. + +.. code-block:: arduino + + void allowMultipleBinding(bool bind); + +* ``bind`` - ``true`` to allow multiple bindings, ``false`` for single binding only + +setManualBinding +^^^^^^^^^^^^^^^^ + +Enables or disables manual binding mode. Manual binding mode is supposed to be used when using ZHA or Z2M where you bind devices manually. + +.. code-block:: arduino + + void setManualBinding(bool bind); + +* ``bind`` - ``true`` for manual binding, ``false`` for automatic binding (default) + +clearBoundDevices +^^^^^^^^^^^^^^^^^ + +Removes all bound devices from this endpoint. + +.. code-block:: arduino + + void clearBoundDevices(); + +Binding Status +************** + +epAllowMultipleBinding +^^^^^^^^^^^^^^^^^^^^^^ + +Gets whether multiple device binding is allowed for this endpoint. + +.. code-block:: arduino + + bool epAllowMultipleBinding(); + +This function will return ``true`` if multiple bindings are allowed, ``false`` otherwise. + +epUseManualBinding +^^^^^^^^^^^^^^^^^^ + +Gets whether manual binding mode is enabled for this endpoint. + +.. code-block:: arduino + + bool epUseManualBinding(); + +This function will return ``true`` if manual binding is enabled, ``false`` otherwise. + +Power Management +**************** + +setPowerSource +^^^^^^^^^^^^^^ + +Sets the power source type for the endpoint. + +.. code-block:: arduino + + bool setPowerSource(uint8_t source, uint8_t percentage = 0xff, uint8_t voltage = 0xff); + +* ``source`` - Power source type (``ZB_POWER_SOURCE_MAINS``, ``ZB_POWER_SOURCE_BATTERY``, etc.) +* ``percentage`` - Battery percentage (0-100, default: 0xff) +* ``voltage`` - Battery voltage in 100 mV units (default: 0xff) + +This function will return ``true`` if set successfully, ``false`` otherwise. + +setBatteryPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets the current battery percentage. + +.. code-block:: arduino + + bool setBatteryPercentage(uint8_t percentage); + +* ``percentage`` - Battery percentage (0-100) + +This function will return ``true`` if set successfully, ``false`` otherwise. + +setBatteryVoltage +^^^^^^^^^^^^^^^^^ + +Sets the battery voltage. + +.. code-block:: arduino + + bool setBatteryVoltage(uint8_t voltage); + +* ``voltage`` - Battery voltage in 100 mV units (e.g., 35 for 3.5 V) + +This function will return ``true`` if set successfully, ``false`` otherwise. + +reportBatteryPercentage +^^^^^^^^^^^^^^^^^^^^^^^ + +Reports the current battery percentage to the network. + +.. code-block:: arduino + + bool reportBatteryPercentage(); + +This function will return ``true`` if reported successfully, ``false`` otherwise. + +Time Synchronization +******************** + +addTimeCluster +^^^^^^^^^^^^^^ + +Adds time synchronization cluster to the endpoint. When you want to add a server cluster (have the time and GMT offset) fill the time structure with the current time and GMT offset. +For client cluster (get the time and GMT offset) keep the default parameters. + +.. code-block:: arduino + + bool addTimeCluster(tm time = {}, int32_t gmt_offset = 0); + +* ``time`` - Current time structure (default: empty) +* ``gmt_offset`` - GMT offset in seconds (default: 0) + +This function will return ``true`` if added successfully, ``false`` otherwise. + +setTime +^^^^^^^ + +Sets the current time for the endpoint. + +.. code-block:: arduino + + bool setTime(tm time); + +* ``time`` - Time structure to set + +This function will return ``true`` if set successfully, ``false`` otherwise. + +setTimezone +^^^^^^^^^^^ + +Sets the timezone offset for the endpoint. + +.. code-block:: arduino + + bool setTimezone(int32_t gmt_offset); + +* ``gmt_offset`` - GMT offset in seconds + +This function will return ``true`` if set successfully, ``false`` otherwise. + +getTime +^^^^^^^ + +Gets the current network time. + +.. code-block:: arduino + + struct tm getTime(uint8_t endpoint = 1, int32_t short_addr = 0x0000, esp_zb_ieee_addr_t ieee_addr = {}); + +* ``endpoint`` - Target endpoint (default: 1) +* ``short_addr`` - Target device short address (default: 0x0000) +* ``ieee_addr`` - Target device IEEE address (default: empty) + +This function will return network time structure. + +getTimezone +^^^^^^^^^^^ + +Gets the timezone offset. + +.. code-block:: arduino + + int32_t getTimezone(uint8_t endpoint = 1, int32_t short_addr = 0x0000, esp_zb_ieee_addr_t ieee_addr = {}); + +* ``endpoint`` - Target endpoint (default: 1) +* ``short_addr`` - Target device short address (default: 0x0000) +* ``ieee_addr`` - Target device IEEE address (default: empty) + +This function will return GMT offset in seconds. + +OTA Support +*********** + +addOTAClient +^^^^^^^^^^^^ + +Adds OTA client to the endpoint for firmware updates. + +.. code-block:: arduino + + bool addOTAClient(uint32_t file_version, uint32_t downloaded_file_ver, uint16_t hw_version, uint16_t manufacturer = 0x1001, uint16_t image_type = 0x1011, uint8_t max_data_size = 223); + +* ``file_version`` - Current firmware version +* ``downloaded_file_ver`` - Downloaded file version +* ``hw_version`` - Hardware version +* ``manufacturer`` - Manufacturer code (default: 0x1001) +* ``image_type`` - Image type code (default: 0x1011) +* ``max_data_size`` - Maximum data size for OTA transfer (default: 223) + +This function will return ``true`` if added successfully, ``false`` otherwise. + +requestOTAUpdate +^^^^^^^^^^^^^^^^ + +Requests OTA update from the server. + +.. code-block:: arduino + + void requestOTAUpdate(); + +Device Discovery +**************** + +readManufacturer +^^^^^^^^^^^^^^^^ + +Reads the manufacturer name from a remote device. + +.. code-block:: arduino + + char *readManufacturer(uint8_t endpoint, uint16_t short_addr, esp_zb_ieee_addr_t ieee_addr); + +* ``endpoint`` - Target endpoint number +* ``short_addr`` - Target device short address +* ``ieee_addr`` - Target device IEEE address + +This function will return pointer to manufacturer string, or ``NULL`` if read failed. + +readModel +^^^^^^^^^ + +Reads the model identifier from a remote device. + +.. code-block:: arduino + + char *readModel(uint8_t endpoint, uint16_t short_addr, esp_zb_ieee_addr_t ieee_addr); + +* ``endpoint`` - Target endpoint number +* ``short_addr`` - Target device short address +* ``ieee_addr`` - Target device IEEE address + +This function will return pointer to model string, or ``NULL`` if read failed. + +Event Handling +************** + +onIdentify +^^^^^^^^^^ + +Sets a callback function for identify events. + +.. code-block:: arduino + + void onIdentify(void (*callback)(uint16_t)); + +* ``callback`` - Function to call when identify event occurs +* ``time`` - Identify time in seconds + +Supported Endpoints +------------------- + +The Zigbee library provides specialized endpoint classes for different device types. Each endpoint type includes specific clusters and functionality relevant to that device category. + +.. toctree:: + :maxdepth: 1 + :glob: + + ep_* From 554de56f40e68d591b945bd1a8c931798e8a9167 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 22 Jul 2025 11:23:49 +0300 Subject: [PATCH 095/102] IDF release/v5.5 25c7c119 (#11623) --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index 6e2dd0ab337..f844a91161a 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-cf8dad07-v1" + "version": "idf-release_v5.5-25c7c119-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-cf8dad07-v1", + "version": "idf-release_v5.5-25c7c119-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-cf8dad07-v1.zip", - "checksum": "SHA-256:c8310c661871a5d82b8b09a2b8d792936bf9a5023ed6dcb683ff93ff9ea4aaa7", - "size": "430423461" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", + "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", + "size": "430472412" } ] }, From 69d891434bd5cd327cd93516fc60941f3e2bfd85 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 23 Jul 2025 01:37:23 +0300 Subject: [PATCH 096/102] IDF release/v5.5 b66b5448 (#11626) IDF release/v5.5 b66b5448 --- package/package_esp32_index.template.json | 68 +++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package/package_esp32_index.template.json b/package/package_esp32_index.template.json index f844a91161a..3909c833c56 100644 --- a/package/package_esp32_index.template.json +++ b/package/package_esp32_index.template.json @@ -51,7 +51,7 @@ { "packager": "esp32", "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-25c7c119-v1" + "version": "idf-release_v5.5-b66b5448-v1" }, { "packager": "esp32", @@ -104,63 +104,63 @@ "tools": [ { "name": "esp32-arduino-libs", - "version": "idf-release_v5.5-25c7c119-v1", + "version": "idf-release_v5.5-b66b5448-v1", "systems": [ { "host": "i686-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "x86_64-mingw32", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "arm64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "i686-pc-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "aarch64-linux-gnu", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-25c7c119-v1.zip", - "checksum": "SHA-256:5760b8d2215c07b0f054e9cd62a268f218c2026db8b395e90696d8038f69d9a8", - "size": "430472412" + "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.5/esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "archiveFileName": "esp32-arduino-libs-idf-release_v5.5-b66b5448-v1.zip", + "checksum": "SHA-256:a871d945c6bfb685ecff5e30ad759f280c841ea143071466b2e611bd1800f18f", + "size": "430471837" } ] }, From a14ce89715088edd5c97d51f4c14f6cd380dded8 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:38:01 -0300 Subject: [PATCH 097/102] fix(tamc_termod_s3): Fix header includes (#11625) --- variants/tamc_termod_s3/pins_arduino.h | 1 + variants/tamc_termod_s3/variant.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/variants/tamc_termod_s3/pins_arduino.h b/variants/tamc_termod_s3/pins_arduino.h index 3d846c9b111..89d0b5107ae 100644 --- a/variants/tamc_termod_s3/pins_arduino.h +++ b/variants/tamc_termod_s3/pins_arduino.h @@ -2,6 +2,7 @@ #define Pins_Arduino_h #include +#include #define USB_VID 0x303a #define USB_PID 0x1001 diff --git a/variants/tamc_termod_s3/variant.cpp b/variants/tamc_termod_s3/variant.cpp index 8079bdbba8d..cc255c31684 100644 --- a/variants/tamc_termod_s3/variant.cpp +++ b/variants/tamc_termod_s3/variant.cpp @@ -1,4 +1,5 @@ #include "Arduino.h" +#include "pins_arduino.h" float getBatteryVoltage() { int analogVolt = analogReadMilliVolts(1); From f08efa1fa3dd7c816ddfecc7ef26f771e4aff1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:43:23 +0200 Subject: [PATCH 098/102] feat(docs): Updaze Zigbee docs with latest changes (#11627) --- docs/en/zigbee/ep_fan_control.rst | 133 ++++++++++++++++++++++++++++++ docs/en/zigbee/zigbee_core.rst | 13 +++ docs/en/zigbee/zigbee_ep.rst | 13 +++ 3 files changed, 159 insertions(+) create mode 100644 docs/en/zigbee/ep_fan_control.rst diff --git a/docs/en/zigbee/ep_fan_control.rst b/docs/en/zigbee/ep_fan_control.rst new file mode 100644 index 00000000000..1cfff0820df --- /dev/null +++ b/docs/en/zigbee/ep_fan_control.rst @@ -0,0 +1,133 @@ +################ +ZigbeeFanControl +################ + +About +----- + +The ``ZigbeeFanControl`` class provides a Zigbee endpoint for controlling fan devices in a Home Automation (HA) network. This endpoint implements the Fan Control cluster and allows for controlling fan modes and sequences. + +**Features:** +* Fan mode control (OFF, LOW, MEDIUM, HIGH, ON, AUTO, SMART) +* Fan mode sequence configuration +* State change callbacks +* Zigbee HA standard compliance + +**Use Cases:** +* Smart ceiling fans +* HVAC system fans +* Exhaust fans +* Any device requiring fan speed control + +API Reference +------------- + +Constructor +*********** + +ZigbeeFanControl +^^^^^^^^^^^^^^^^ + +Creates a new Zigbee fan control endpoint. + +.. code-block:: arduino + + ZigbeeFanControl(uint8_t endpoint); + +* ``endpoint`` - Endpoint number (1-254) + +Fan Mode Enums +************** + +ZigbeeFanMode +^^^^^^^^^^^^^ + +Available fan modes for controlling the fan speed and operation. + +.. code-block:: arduino + + enum ZigbeeFanMode { + FAN_MODE_OFF, // Fan is off + FAN_MODE_LOW, // Low speed + FAN_MODE_MEDIUM, // Medium speed + FAN_MODE_HIGH, // High speed + FAN_MODE_ON, // Fan is on (full speed) + FAN_MODE_AUTO, // Automatic mode + FAN_MODE_SMART, // Smart mode + }; + +ZigbeeFanModeSequence +^^^^^^^^^^^^^^^^^^^^^ + +Available fan mode sequences that define which modes are available for the fan. + +.. code-block:: arduino + + enum ZigbeeFanModeSequence { + FAN_MODE_SEQUENCE_LOW_MED_HIGH, // Low -> Medium -> High + FAN_MODE_SEQUENCE_LOW_HIGH, // Low -> High + FAN_MODE_SEQUENCE_LOW_MED_HIGH_AUTO, // Low -> Medium -> High -> Auto + FAN_MODE_SEQUENCE_LOW_HIGH_AUTO, // Low -> High -> Auto + FAN_MODE_SEQUENCE_ON_AUTO, // On -> Auto + }; + +API Methods +*********** + +setFanModeSequence +^^^^^^^^^^^^^^^^^^ + +Sets the fan mode sequence and initializes the fan control. + +.. code-block:: arduino + + bool setFanModeSequence(ZigbeeFanModeSequence sequence); + +* ``sequence`` - The fan mode sequence to set + +This function will return ``true`` if successful, ``false`` otherwise. + +getFanMode +^^^^^^^^^^ + +Gets the current fan mode. + +.. code-block:: arduino + + ZigbeeFanMode getFanMode(); + +This function will return current fan mode. + +getFanModeSequence +^^^^^^^^^^^^^^^^^^ + +Gets the current fan mode sequence. + +.. code-block:: arduino + + ZigbeeFanModeSequence getFanModeSequence(); + +This function will return current fan mode sequence. + +Event Handling +************** + +onFanModeChange +^^^^^^^^^^^^^^^ + +Sets a callback function to be called when the fan mode changes. + +.. code-block:: arduino + + void onFanModeChange(void (*callback)(ZigbeeFanMode mode)); + +* ``callback`` - Function to call when fan mode changes + +Example +------- + +Fan Control Implementation +************************** + +.. literalinclude:: ../../../libraries/Zigbee/examples/Zigbee_Fan_Control/Zigbee_Fan_Control.ino + :language: arduino diff --git a/docs/en/zigbee/zigbee_core.rst b/docs/en/zigbee/zigbee_core.rst index ee8f7234210..89cf88ecca1 100644 --- a/docs/en/zigbee/zigbee_core.rst +++ b/docs/en/zigbee/zigbee_core.rst @@ -345,6 +345,19 @@ Performs a factory reset, clearing all network settings. * ``restart`` - ``true`` to restart after reset (default: ``true``) +onGlobalDefaultResponse +^^^^^^^^^^^^^^^^^^^^^^^ + +Sets a global callback for default response messages. + +.. code-block:: arduino + + void onGlobalDefaultResponse(void (*callback)(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster)); + +* ``callback`` - Function pointer to the callback function + +This callback will be called for all endpoints when a default response is received. + Utility Functions ***************** diff --git a/docs/en/zigbee/zigbee_ep.rst b/docs/en/zigbee/zigbee_ep.rst index ce13d60bbaf..10c0c9fd084 100644 --- a/docs/en/zigbee/zigbee_ep.rst +++ b/docs/en/zigbee/zigbee_ep.rst @@ -345,6 +345,19 @@ Sets a callback function for identify events. * ``callback`` - Function to call when identify event occurs * ``time`` - Identify time in seconds +onDefaultResponse +^^^^^^^^^^^^^^^^^ + +Sets a callback for default response messages for this endpoint. + +.. code-block:: arduino + + void onDefaultResponse(void (*callback)(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status)); + +* ``callback`` - Function pointer to the callback function + +This callback will be called when a default response is received for this specific endpoint. + Supported Endpoints ------------------- From ae634a92e3b3af3a68daf8e79b025a45ca9134ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <90197375+P-R-O-C-H-Y@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:44:36 +0200 Subject: [PATCH 099/102] fix(zigbee): Fix RGB color calculation (#11624) --- cores/esp32/ColorFormat.c | 12 +++++++----- cores/esp32/ColorFormat.h | 3 ++- libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp | 5 ++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cores/esp32/ColorFormat.c b/cores/esp32/ColorFormat.c index a01123545b3..052249f2127 100644 --- a/cores/esp32/ColorFormat.c +++ b/cores/esp32/ColorFormat.c @@ -119,10 +119,10 @@ espHsvColor_t espRgbColorToHsvColor(espRgbColor_t rgb) { } espRgbColor_t espXYColorToRgbColor(uint8_t Level, espXyColor_t xy) { - return espXYToRgbColor(Level, xy.x, xy.y); + return espXYToRgbColor(Level, xy.x, xy.y, true); } -espRgbColor_t espXYToRgbColor(uint8_t Level, uint16_t current_X, uint16_t current_Y) { +espRgbColor_t espXYToRgbColor(uint8_t Level, uint16_t current_X, uint16_t current_Y, bool addXYZScaling) { // convert xyY color space to RGB // https://www.easyrgb.com/en/math.php @@ -156,9 +156,11 @@ espRgbColor_t espXYToRgbColor(uint8_t Level, uint16_t current_X, uint16_t curren // X, Y and Z input refer to a D65/2° standard illuminant. // sR, sG and sB (standard RGB) output range = 0 ÷ 255 // convert XYZ to RGB - CIE XYZ to sRGB - X = X / 100.0f; - Y = Y / 100.0f; - Z = Z / 100.0f; + if (addXYZScaling) { + X = X / 100.0f; + Y = Y / 100.0f; + Z = Z / 100.0f; + } r = (X * 3.2406f) - (Y * 1.5372f) - (Z * 0.4986f); g = -(X * 0.9689f) + (Y * 1.8758f) + (Z * 0.0415f); diff --git a/cores/esp32/ColorFormat.h b/cores/esp32/ColorFormat.h index 0bb87145d16..288b79b5714 100644 --- a/cores/esp32/ColorFormat.h +++ b/cores/esp32/ColorFormat.h @@ -19,6 +19,7 @@ #pragma once #include +#include #ifdef __cplusplus extern "C" { #endif @@ -49,7 +50,7 @@ typedef struct HsvColor_t espHsvColor_t; typedef struct XyColor_t espXyColor_t; typedef struct CtColor_t espCtColor_t; -espRgbColor_t espXYToRgbColor(uint8_t Level, uint16_t current_X, uint16_t current_Y); +espRgbColor_t espXYToRgbColor(uint8_t Level, uint16_t current_X, uint16_t current_Y, bool addXYZScaling); espRgbColor_t espXYColorToRgb(uint8_t Level, espXyColor_t xy); espXyColor_t espRgbColorToXYColor(espRgbColor_t rgb); espXyColor_t espRgbToXYColor(uint8_t r, uint8_t g, uint8_t b); diff --git a/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp b/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp index 2fb07fc2187..3611c232c20 100644 --- a/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp +++ b/libraries/Zigbee/src/ep/ZigbeeColorDimmableLight.cpp @@ -76,14 +76,13 @@ void ZigbeeColorDimmableLight::zbAttributeSet(const esp_zb_zcl_set_attr_value_me return; } else { log_w("Received message ignored. Attribute ID: %d not supported for Level Control", message->attribute.id); - //TODO: implement more attributes -> includes/zcl/esp_zigbee_zcl_level.h } } else if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL) { if (message->attribute.id == ESP_ZB_ZCL_ATTR_COLOR_CONTROL_CURRENT_X_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U16) { uint16_t light_color_x = (*(uint16_t *)message->attribute.data.value); uint16_t light_color_y = getCurrentColorY(); //calculate RGB from XY and call setColor() - _current_color = espXYToRgbColor(255, light_color_x, light_color_y); //TODO: Check if level is correct + _current_color = espXYToRgbColor(255, light_color_x, light_color_y, false); lightChanged(); return; @@ -91,7 +90,7 @@ void ZigbeeColorDimmableLight::zbAttributeSet(const esp_zb_zcl_set_attr_value_me uint16_t light_color_x = getCurrentColorX(); uint16_t light_color_y = (*(uint16_t *)message->attribute.data.value); //calculate RGB from XY and call setColor() - _current_color = espXYToRgbColor(255, light_color_x, light_color_y); //TODO: Check if level is correct + _current_color = espXYToRgbColor(255, light_color_x, light_color_y, false); lightChanged(); return; } else if (message->attribute.id == ESP_ZB_ZCL_ATTR_COLOR_CONTROL_CURRENT_HUE_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_U8) { From 6fdfccf21b1532dede0e886dd4ab904581a036f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 22:46:36 +0000 Subject: [PATCH 100/102] ci(pre-commit): Apply automatic fixes --- cores/esp32/esp32-hal-i2c-slave.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cores/esp32/esp32-hal-i2c-slave.c b/cores/esp32/esp32-hal-i2c-slave.c index 79c994869e2..da3a819387a 100644 --- a/cores/esp32/esp32-hal-i2c-slave.c +++ b/cores/esp32/esp32-hal-i2c-slave.c @@ -338,8 +338,7 @@ esp_err_t i2cSlaveInit(uint8_t num, int sda, int scl, uint16_t slaveID, uint32_t } #endif // !defined(CONFIG_IDF_TARGET_ESP32P4) -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)) \ - || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)) \ +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)) || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)) \ || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)) i2c_ll_set_mode(i2c->dev, I2C_BUS_MODE_SLAVE); i2c_ll_enable_pins_open_drain(i2c->dev, true); From 3da3ad2f2d38badab90524c7c647681acbd8274e Mon Sep 17 00:00:00 2001 From: Wulu Date: Wed, 23 Jul 2025 20:28:19 +0800 Subject: [PATCH 101/102] feat(board): Add onboard LED support for Waveshare ESP32-S3 Zero (#11630) Defines the standard LED_BUILTIN and RGB_BUILTIN macros for the Waveshare ESP32-S3 Zero, allowing its onboard WS2812 RGB LED to be controlled via standard Arduino APIs. --- variants/waveshare_esp32_s3_zero/pins_arduino.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/variants/waveshare_esp32_s3_zero/pins_arduino.h b/variants/waveshare_esp32_s3_zero/pins_arduino.h index 0d73bee16d0..20ce1c88859 100644 --- a/variants/waveshare_esp32_s3_zero/pins_arduino.h +++ b/variants/waveshare_esp32_s3_zero/pins_arduino.h @@ -2,6 +2,7 @@ #define Pins_Arduino_h #include +#include "soc/soc_caps.h" #define USB_VID 0x303a #define USB_PID 0x822B @@ -9,9 +10,17 @@ #define USB_PRODUCT "ESP32-S3-Zero" #define USB_SERIAL "" // Empty string for MAC address -// Partial voltage measurement method +// Onboard WS2812 RGB LED #define WS_RGB 21 +// BUILTIN_LED can be used in new Arduino API digitalWrite() like in Blink.ino +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + WS_RGB; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN +// RGB_BUILTIN and RGB_BRIGHTNESS can be used in new Arduino API rgbLedWrite() +#define RGB_BUILTIN LED_BUILTIN +#define RGB_BRIGHTNESS 64 + // Mapping based on the ESP32S3 data sheet - alternate for OUTPUT static const uint8_t OUTPUT_IO1 = 1; static const uint8_t OUTPUT_IO2 = 2; From c7520ccef0c343611d7f9eb0afb3576f612de2f2 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Wed, 23 Jul 2025 17:15:59 +0300 Subject: [PATCH 102/102] fix(report): Update Issue-report.yml for version 3.3.0 --- .github/ISSUE_TEMPLATE/Issue-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/Issue-report.yml b/.github/ISSUE_TEMPLATE/Issue-report.yml index 6dc1b0de171..a16a6ae3d44 100644 --- a/.github/ISSUE_TEMPLATE/Issue-report.yml +++ b/.github/ISSUE_TEMPLATE/Issue-report.yml @@ -43,6 +43,7 @@ body: - latest stable Release (if not listed below) - latest development Release Candidate (RC-X) - latest master (checkout manually) + - v3.3.0 - v3.2.1 - v3.2.0 - v3.1.3 pFad - Phonifier reborn