From 8505958b653cb24591a029b654071834a11b40c6 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 8 Aug 2026 08:36:42 +0800 Subject: [PATCH] Implement Matter Endpoint Plan Logic and Unit Tests - Added `gateway_matter_plan.cpp` to define the logic for building Matter endpoint plans, including functions for inferring capabilities and managing allocations based on channel configurations. - Introduced helper functions to determine the type of Matter endpoints and color methods based on device capabilities. - Created `gateway_matter_plan_test.cpp` to validate the functionality of the endpoint plan logic with various test cases, ensuring correct behavior for different configurations and address handling. Signed-off-by: Tony --- README.md | 56 +- apps/gateway/main/Kconfig.projbuild | 29 +- apps/gateway/main/app_main.cpp | 80 +- apps/gateway/sdkconfig | 57 +- apps/gateway/sdkconfig.defaults | 27 +- apps/gateway/sdkconfig.old | 85 +- .../dali_domain/include/dali_domain.hpp | 3 + components/dali_domain/src/dali_domain.cpp | 13 + .../gateway_bacnet/include/gateway_bacnet.hpp | 1 + .../gateway_bacnet/src/gateway_bacnet.cpp | 20 +- .../gateway_bridge/include/gateway_bridge.hpp | 5 + .../gateway_bridge/src/gateway_bridge.cpp | 21 + .../include/gateway_controller.hpp | 2 + .../src/gateway_controller.cpp | 34 +- components/gateway_matter/CMakeLists.txt | 4 +- .../gateway_matter/include/gateway_matter.hpp | 39 +- .../include/gateway_matter_plan.hpp | 86 ++ .../gateway_matter/src/gateway_matter.cpp | 1016 +++++++++++++++-- .../src/gateway_matter_plan.cpp | 212 ++++ .../tests/gateway_matter_plan_test.cpp | 72 ++ 20 files changed, 1683 insertions(+), 179 deletions(-) create mode 100644 components/gateway_matter/include/gateway_matter_plan.hpp create mode 100644 components/gateway_matter/src/gateway_matter_plan.cpp create mode 100644 components/gateway_matter/tests/gateway_matter_plan_test.cpp diff --git a/README.md b/README.md index bf586fe..42674a4 100644 --- a/README.md +++ b/README.md @@ -123,17 +123,46 @@ gateway group matches the target. ## Matter-to-DALI development bridge `GATEWAY_MATTER_SUPPORTED` enables the Matter bridge by default. It creates one -aggregator and resumes or discovers up to -`GATEWAY_MATTER_MAX_BRIDGED_DEVICES` direct-address DALI control gear. The -adapter queries all DALI device types `0` through `8`: DT8 becomes an extended -color light, DT7 becomes an on/off light, and the remaining types become -dimmable lights. Matter On/Off, Level Control, Color Control (mirek, XY, and -hue/saturation converted to DALI RGB), Groups, Scenes, and Identify are the +aggregator and owns a pool of 32 bridged DALI endpoints. ESP-Matter is configured +for 34 dynamic entries: the root, the aggregator, and the 32-slot DALI pool. +Every enabled channel receives a broadcast endpoint first, then cached non-empty +groups (including manually retained empty groups), explicitly included short +addresses, and automatic online short addresses in ascending order. Allocation +stops at 32 and reports every dropped target; this means the highest automatic +short addresses are removed first. Broadcast and group targets cannot be +disabled. On a single DALI channel, the pool can therefore retain broadcast, +all 16 groups, and 15 direct addresses when every group is present. + +The portable DALI cache persists device-type masks and DT8 color-feature bits in +addition to group, scene, settings, and runtime state. The planner infers the +highest required multi-target capability in the order color, color temperature, +dimmer, then switch. DT8 feature bits select XY, RGBCW, or both. Per-channel +versioned configuration is stored in normal NVS by fixed channel index and can +override the automatic short-address policy, sparse direct-address inclusion, +and each broadcast/group type and color method. Existing direct-only bindings +are migrated to stable channel-index plus target-kind bindings. Reconciliation +retains unchanged endpoints and recreates one only when its Matter device type +changes. + +Matter On/Off, Level Control, Color Control (mirek, XY, and hue/saturation +converted to equivalent DALI writes), Groups, Scenes, and Identify are the only application clusters retained. Root-node commissioning, credentials, descriptor, access-control, diagnostics, and the ESP-Matter internal Binding manager are retained because the server requires them; unrelated device-type clusters, Thread, OTA, and the Matter shell are excluded. +The checked-in ESP32-S3 profile uses only ESP-IDF 5.5.4-supported PSRAM paths: +CHIP, ESP-Matter, mDNS, TLS, NVS cache, Wi-Fi/lwIP allocations, and eligible BSS +prefer external RAM. The mDNS task and gateway-owned BACnet and on-demand DALI +operation stacks can use PSRAM with internal-RAM fallback. Tasks that write NVS, +change network settings, run the DALI PHY, or have timing/DMA constraints remain +in internal RAM; in particular, the Matter DALI scan stack stays internal because +the scan commits its discovered capabilities. Eligible non-ISR FreeRTOS code is +placed in flash to leave internal RAM headroom. This selective policy does not depend on +the unreleased global `FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM` option. Wi-Fi IRAM +throughput optimizations are disabled because Ethernet is the preferred data +path and Wi-Fi is retained as a provisioning and fallback path. + CHIP owns the single NimBLE controller and host while Matter is enabled. The legacy `gateway_ble` transport registers its distinct FFF7 service in CHIP's GATT database through ESP-Matter's extra-service hook, and adds the FFF7 UUID plus @@ -173,6 +202,21 @@ private key/certificate, PAI, certification declaration, passcode `20202021`, and discriminator `3840`. Flashing that image replaces all existing normal NVS data and must never be used for production hardware. +DaliMaster's Matter Gateway page uses the transport-independent bridge action +surface to read live Matter state, open or close the five-minute commissioning +window, configure endpoints, and rescan DALI control gear. `matter_status` +returns schema version, effective 32-slot capacity, candidates, saved +configuration, active and dropped allocations, target kind/address, inferred or +manual source, and the last partial-apply error. `matter_onboarding` returns the +live serial number, VID/PID, QR payload, and manual pairing code from the active +ESP-Matter factory providers; controllers must treat this response as sensitive +and DaliMaster keeps it only in page memory. `matter_config` applies a selected +channel patch, while `matter_config_reset` restores automatic selection. +`matter_open_commissioning`, `matter_close_commissioning`, `matter_rescan`, +`matter_config`, and `matter_config_reset` are POST actions. They are available through the active +BLE/USB/IP/KNX/cloud bridge transport as well as the local HTTP `/bridge` +endpoint, so Matter setup does not require a working IP profile. + ## Gateway operation protocol Opcode `0x67` starts, aborts, and polls gateway-executed high-level DALI diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index e715f32..1927004 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -1594,12 +1594,22 @@ config GATEWAY_DALI_BAUDRATE config GATEWAY_CONTROLLER_TASK_STACK_SIZE int "Gateway controller task stack bytes" range 6144 24576 - default 12288 + default 10240 help Stack used by the gateway command controller. BLE bridge transport requests are decoded in this task and may execute JSON-heavy bridge management actions such as KNX programming-mode changes. +config GATEWAY_PREFER_PSRAM_TASK_STACKS + bool "Prefer PSRAM for supported gateway task stacks" + depends on SPIRAM && FREERTOS_TASK_CREATE_ALLOW_EXT_MEM + default y + help + Moves only gateway tasks that do not write flash or require DMA-capable + stacks to PSRAM, with an internal-RAM fallback if PSRAM allocation + fails. Cache, settings, Matter scan, network configuration, DALI PHY, + and other flash- or timing-sensitive tasks remain in internal RAM. + endmenu menu "Connectivity Startup" @@ -1669,13 +1679,15 @@ config GATEWAY_MATTER_SUPPORTED so the legacy gateway BLE GATT transport is not started in this build. config GATEWAY_MATTER_MAX_BRIDGED_DEVICES - int "Maximum bridged DALI devices" + int "Maximum bridged DALI endpoints" depends on GATEWAY_MATTER_SUPPORTED - range 1 16 - default 16 + range 1 82 + default 32 help - Limits dynamic Matter endpoints and DALI startup discovery work. Every - DALI device type 0 through 8 remains supported regardless of this count. + Limits bridged DALI broadcast, group, and short-address endpoints. + Matter root and aggregator endpoints are additional to this count. The + 32-slot default fits broadcast plus all 16 DALI groups and leaves 15 + automatic or explicitly included short-address endpoints. config GATEWAY_MATTER_SCAN_START_DELAY_MS int "DALI discovery startup delay ms" @@ -1692,8 +1704,11 @@ config GATEWAY_MATTER_SCAN_ADDRESS_DELAY_MS config GATEWAY_MATTER_SCAN_TASK_STACK_SIZE int "Matter DALI scan task stack bytes" depends on GATEWAY_MATTER_SUPPORTED - range 6144 16384 + range 8192 32768 default 8192 + help + This task performs NVS commits and therefore intentionally remains in + internal RAM even when supported gateway task stacks prefer PSRAM. config GATEWAY_MATTER_WIFI_PROVISION_BUTTON_GPIO int "Matter Wi-Fi provisioning button GPIO" diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index eb4a138..e7230aa 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -11,6 +11,7 @@ #include "gateway_usb_setup.hpp" #include "gateway_tridonic_hid.hpp" +#include "esp_heap_caps.h" #include "esp_log.h" #include "sdkconfig.h" @@ -80,7 +81,7 @@ #endif #ifndef CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES -#define CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES 16 +#define CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES 32 #endif #ifndef CONFIG_GATEWAY_MATTER_SCAN_START_DELAY_MS @@ -392,6 +393,12 @@ constexpr bool kMatterSupported = true; constexpr bool kMatterSupported = false; #endif +#ifdef CONFIG_GATEWAY_PREFER_PSRAM_TASK_STACKS +constexpr bool kPreferPsramTaskStacks = true; +#else +constexpr bool kPreferPsramTaskStacks = false; +#endif + #ifdef CONFIG_GATEWAY_START_BLE_ENABLED constexpr bool kBleStartupEnabled = true; #else @@ -1038,6 +1045,20 @@ bool HasConfiguredDaliChannel(const std::vector& chann }); } +void LogHeapSnapshot(const char* phase) { + ESP_LOGI(kTag, + "heap %s internal_free=%u internal_largest=%u psram_free=%u psram_largest=%u", + phase, + static_cast(heap_caps_get_free_size(MALLOC_CAP_INTERNAL | + MALLOC_CAP_8BIT)), + static_cast(heap_caps_get_largest_free_block( + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + static_cast(heap_caps_get_free_size(MALLOC_CAP_SPIRAM | + MALLOC_CAP_8BIT)), + static_cast(heap_caps_get_largest_free_block( + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT))); +} + } // namespace extern "C" void app_main(void) { @@ -1147,6 +1168,7 @@ extern "C" void app_main(void) { gateway::GatewayControllerConfig controller_config; controller_config.task_stack_size = static_cast(CONFIG_GATEWAY_CONTROLLER_TASK_STACK_SIZE); + controller_config.operation_task_stack_in_psram = kPreferPsramTaskStacks; const bool network_transport_supported = profile.enable_wifi || profile.enable_eth; controller_config.setup_supported = true; controller_config.ble_supported = profile.enable_ble; @@ -1234,6 +1256,7 @@ extern "C" void app_main(void) { static_cast(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE); bridge_config.bacnet_task_priority = static_cast(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY); + bridge_config.bacnet_task_stack_in_psram = kPreferPsramTaskStacks; if (kKnxBridgeSupported) { gateway::GatewayKnxConfig default_knx; default_knx.dali_router_enabled = true; @@ -1326,6 +1349,56 @@ extern "C" void app_main(void) { } return s_network->gatewaySettingsCloudResponse(request); }; + bridge_config.matter_status_getter = [](std::optional gateway_id) { + if (!kMatterSupported) { + return gateway::GatewayBridgeHttpResponse{ + ESP_OK, "{\"matter\":{\"supported\":false,\"enabled\":false,\"started\":false}}"}; + } + if (s_matter_bridge == nullptr) { + return gateway::GatewayBridgeHttpResponse{ + ESP_ERR_INVALID_STATE, + "{\"status\":\"error\",\"error\":\"Matter bridge is not ready\"}"}; + } + return gateway::GatewayBridgeHttpResponse{ESP_OK, + s_matter_bridge->statusJson(gateway_id)}; + }; + bridge_config.matter_onboarding_getter = []() { + if (!kMatterSupported || s_matter_bridge == nullptr) { + return gateway::GatewayBridgeHttpResponse{ + ESP_ERR_NOT_SUPPORTED, + "{\"status\":\"error\",\"error\":\"Matter bridge is not available\"}"}; + } + return gateway::GatewayBridgeHttpResponse{ESP_OK, + s_matter_bridge->onboardingJson()}; + }; + bridge_config.matter_action_handler = [](std::string_view action, + std::optional gateway_id, + std::string_view body) { + if (!kMatterSupported || s_matter_bridge == nullptr) { + return gateway::GatewayBridgeHttpResponse{ + ESP_ERR_NOT_SUPPORTED, + "{\"status\":\"error\",\"error\":\"Matter bridge is not available\"}"}; + } + esp_err_t err = ESP_ERR_INVALID_ARG; + if (action == "matter_open_commissioning") { + err = s_matter_bridge->openCommissioningWindow(); + } else if (action == "matter_close_commissioning") { + err = s_matter_bridge->closeCommissioningWindow(); + } else if (action == "matter_rescan") { + err = s_matter_bridge->rescanDaliDevices(); + } else if (action == "matter_config" && gateway_id.has_value()) { + err = s_matter_bridge->applyConfiguration(*gateway_id, body); + } else if (action == "matter_config_reset" && gateway_id.has_value()) { + err = s_matter_bridge->resetConfiguration(*gateway_id); + } + if (err != ESP_OK) { + const std::string message = std::string("{\"status\":\"error\",\"error\":\"") + + esp_err_to_name(err) + "\"}"; + return gateway::GatewayBridgeHttpResponse{err, message}; + } + return gateway::GatewayBridgeHttpResponse{ESP_OK, + s_matter_bridge->statusJson(gateway_id)}; + }; bridge_config.knx_gateway_snapshot_provider = []() { gateway::GatewayKnxGatewaySnapshot out; if (s_controller == nullptr) { @@ -1532,7 +1605,10 @@ extern "C" void app_main(void) { } s_matter_bridge = std::make_unique(*s_dali_domain, matter_config); - ESP_ERROR_CHECK(s_matter_bridge->start()); + LogHeapSnapshot("before Matter"); + const esp_err_t matter_err = s_matter_bridge->start(); + LogHeapSnapshot("after Matter"); + ESP_ERROR_CHECK(matter_err); } // ESP-Matter needs several contiguous internal allocations for the CHIP diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 74d753a..9286c03 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -711,7 +711,8 @@ CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y # end of Gateway Cache # CONFIG_GATEWAY_ENABLE_DALI_BUS is not set -CONFIG_GATEWAY_CONTROLLER_TASK_STACK_SIZE=12288 +CONFIG_GATEWAY_CONTROLLER_TASK_STACK_SIZE=10240 +CONFIG_GATEWAY_PREFER_PSRAM_TASK_STACKS=y # end of DALI Settings # @@ -730,7 +731,7 @@ CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED=y # Matter-to-DALI Bridge # CONFIG_GATEWAY_MATTER_SUPPORTED=y -CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES=16 +CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES=82 CONFIG_GATEWAY_MATTER_SCAN_START_DELAY_MS=2000 CONFIG_GATEWAY_MATTER_SCAN_ADDRESS_DELAY_MS=20 CONFIG_GATEWAY_MATTER_SCAN_TASK_STACK_SIZE=8192 @@ -1463,6 +1464,7 @@ CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y # Common ESP-related # CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y # end of Common ESP-related # @@ -1551,7 +1553,6 @@ CONFIG_RMT_OBJ_CACHE_SAFE=y # # ESP-Driver:SPI Configurations # -# CONFIG_SPI_MASTER_IN_IRAM is not set CONFIG_SPI_MASTER_ISR_IN_IRAM=y # CONFIG_SPI_SLAVE_IN_IRAM is not set CONFIG_SPI_SLAVE_ISR_IN_IRAM=y @@ -1884,15 +1885,14 @@ CONFIG_SPIRAM_SPEED=80 CONFIG_SPIRAM_BOOT_HW_INIT=y CONFIG_SPIRAM_BOOT_INIT=y CONFIG_SPIRAM_PRE_CONFIGURE_MEMORY_PROTECTION=y -CONFIG_SPIRAM_IGNORE_NOTFOUND=y # CONFIG_SPIRAM_USE_MEMMAP is not set # CONFIG_SPIRAM_USE_CAPS_ALLOC is not set CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_MEMTEST=y -CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=512 CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65535 -# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set +CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y # CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set # end of SPI RAM config # end of ESP PSRAM @@ -2042,10 +2042,10 @@ CONFIG_ESP_TIMER_IMPL_SYSTIMER=y CONFIG_ESP_WIFI_ENABLED=y CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10 CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 -# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set -CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y -CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 -CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=0 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=16 +CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM=32 # CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER is not set CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER=y CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=1 @@ -2055,14 +2055,15 @@ CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y CONFIG_ESP_WIFI_TX_BA_WIN=6 CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y CONFIG_ESP_WIFI_RX_BA_WIN=6 +# CONFIG_ESP_WIFI_AMSDU_TX_ENABLED is not set # CONFIG_ESP_WIFI_NVS_ENABLED is not set # CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0 is not set CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752 CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32 -CONFIG_ESP_WIFI_IRAM_OPT=y +# CONFIG_ESP_WIFI_IRAM_OPT is not set # CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set -CONFIG_ESP_WIFI_RX_IRAM_OPT=y +# CONFIG_ESP_WIFI_RX_IRAM_OPT is not set CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y CONFIG_ESP_WIFI_ENABLE_SAE_PK=y CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y @@ -2215,7 +2216,7 @@ CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y # CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y -# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y # CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set # end of Port @@ -2406,6 +2407,7 @@ CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 CONFIG_LWIP_TCP_OVERSIZE_MSS=y # CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set # CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +# CONFIG_LWIP_WND_SCALE is not set CONFIG_LWIP_TCP_RTO_TIME=1500 # end of TCP @@ -2510,8 +2512,8 @@ CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y # mbedTLS # # CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set -# CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC is not set -CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC=y +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set # CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 @@ -3287,7 +3289,7 @@ CONFIG_MRP_MAX_RETRANS=4 # # ESP Matter # -CONFIG_ESP_MATTER_MAX_DEVICE_TYPE_COUNT=16 +CONFIG_ESP_MATTER_MAX_DEVICE_TYPE_COUNT=5 CONFIG_ESP_MATTER_ATTRIBUTE_BUFFER_LARGEST=259 CONFIG_ESP_MATTER_NVS_PART_NAME="nvs" CONFIG_ESP_MATTER_DEFERRED_ATTR_PERSISTENCE_TIME_MS=3000 @@ -3303,7 +3305,7 @@ CONFIG_FACTORY_DEVICE_INSTANCE_INFO_PROVIDER=y # CONFIG_CUSTOM_DEVICE_INSTANCE_INFO_PROVIDER is not set CONFIG_NONE_DEVICE_INFO_PROVIDER=y # CONFIG_CUSTOM_DEVICE_INFO_PROVIDER is not set -CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=18 +CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=84 CONFIG_ESP_MATTER_MODE_SELECT_CLUSTER_ENDPOINT_COUNT=0 CONFIG_ESP_MATTER_TEMPERATURE_CONTROL_CLUSTER_ENDPOINT_COUNT=0 CONFIG_ESP_MATTER_SCENES_TABLE_SIZE=16 @@ -3605,10 +3607,10 @@ CONFIG_MDNS_TASK_AFFINITY=0x0 # # MDNS Memory Configuration # -# CONFIG_MDNS_TASK_CREATE_FROM_SPIRAM is not set -CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL=y -# CONFIG_MDNS_MEMORY_ALLOC_SPIRAM is not set -CONFIG_MDNS_MEMORY_ALLOC_INTERNAL=y +CONFIG_MDNS_TASK_CREATE_FROM_SPIRAM=y +# CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL is not set +CONFIG_MDNS_MEMORY_ALLOC_SPIRAM=y +# CONFIG_MDNS_MEMORY_ALLOC_INTERNAL is not set # CONFIG_MDNS_MEMORY_CUSTOM_IMPL is not set # end of MDNS Memory Configuration @@ -3853,22 +3855,23 @@ CONFIG_TIMER_TASK_STACK_SIZE=3584 CONFIG_ESP32_WIFI_ENABLED=y CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 -# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set -CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y -CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 -CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP32_WIFI_STATIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=0 +CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM=16 +CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM=32 # CONFIG_ESP32_WIFI_CSI_ENABLED is not set CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y CONFIG_ESP32_WIFI_TX_BA_WIN=6 CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y CONFIG_ESP32_WIFI_RX_BA_WIN=6 +# CONFIG_ESP32_WIFI_AMSDU_TX_ENABLED is not set # CONFIG_ESP32_WIFI_NVS_ENABLED is not set # CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0 is not set CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1=y CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 -CONFIG_ESP32_WIFI_IRAM_OPT=y -CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +# CONFIG_ESP32_WIFI_IRAM_OPT is not set +# CONFIG_ESP32_WIFI_RX_IRAM_OPT is not set CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y CONFIG_WPA_MBEDTLS_CRYPTO=y diff --git a/apps/gateway/sdkconfig.defaults b/apps/gateway/sdkconfig.defaults index 6214e58..61cd124 100644 --- a/apps/gateway/sdkconfig.defaults +++ b/apps/gateway/sdkconfig.defaults @@ -25,7 +25,7 @@ CONFIG_TINYUSB_HID_COUNT=1 # Matter commissioning and legacy gateway GATT services share its database. # Wired Ethernet remains the default route and Wi-Fi starts only when provisioned. CONFIG_GATEWAY_MATTER_SUPPORTED=y -CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES=16 +CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES=32 CONFIG_GATEWAY_MATTER_SCAN_START_DELAY_MS=2000 CONFIG_GATEWAY_MATTER_SCAN_ADDRESS_DELAY_MS=20 CONFIG_GATEWAY_MATTER_SCAN_TASK_STACK_SIZE=8192 @@ -33,16 +33,33 @@ CONFIG_GATEWAY_MATTER_WIFI_PROVISION_BUTTON_GPIO=-1 CONFIG_ESP_MATTER_ENABLE_MATTER_SERVER=y CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y CONFIG_ESP_MATTER_ENABLE_OPENTHREAD=n -# Keep the large CHIP core and ESP-Matter data model in PSRAM so the internal -# heap retains a contiguous block for the FreeRTOS event queue and radio/DMA -# allocations. The queue itself keeps Espressif's Wi-Fi-safe default length. +# Keep the large CHIP core, ESP-Matter data model, mDNS state, TLS allocations, +# and NVS cache in PSRAM so the internal heap retains contiguous blocks for +# event queues and radio/DMA allocations. Only stable, explicitly supported +# task stacks are moved; flash-writing tasks remain in internal RAM. CONFIG_CHIP_MEM_ALLOC_MODE_EXTERNAL=y CONFIG_ESP_MATTER_MEM_ALLOC_MODE_EXTERNAL=y +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y +CONFIG_MDNS_TASK_CREATE_FROM_SPIRAM=y +CONFIG_MDNS_MEMORY_ALLOC_SPIRAM=y +CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM=y +CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=512 +CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y +CONFIG_GATEWAY_PREFER_PSRAM_TASK_STACKS=y +# Ethernet is the normal data path and Wi-Fi is a provisioning/fallback path, +# so trade Wi-Fi peak throughput for substantial internal instruction RAM. +CONFIG_ESP_WIFI_IRAM_OPT=n +CONFIG_ESP_WIFI_RX_IRAM_OPT=n +# This stable IDF 5.5 option moves eligible FreeRTOS code out of IRAM. ISR +# functions remain in IRAM because the separate ISR option is not enabled. +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y # Preserve enough internal/DMA-capable RAM for gateway Wi-Fi RX buffers when # ESP-Touch is requested; app_main starts Matter before the allocation-heavy # KNX bridge so CHIP can obtain its queue and event-loop stack first. CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65535 -CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=18 +CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=34 CONFIG_ESP_MATTER_AGGREGATOR_ENDPOINT_COUNT=1 CONFIG_ESP_MATTER_NVS_PART_NAME="nvs" CONFIG_ESP_MATTER_BRIDGE_INFO_PART_NAME="nvs" diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index aa9e8b1..ca26f77 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -711,7 +711,8 @@ CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y # end of Gateway Cache # CONFIG_GATEWAY_ENABLE_DALI_BUS is not set -CONFIG_GATEWAY_CONTROLLER_TASK_STACK_SIZE=12288 +CONFIG_GATEWAY_CONTROLLER_TASK_STACK_SIZE=10240 +CONFIG_GATEWAY_PREFER_PSRAM_TASK_STACKS=y # end of DALI Settings # @@ -730,7 +731,7 @@ CONFIG_GATEWAY_SMARTCONFIG_SUPPORTED=y # Matter-to-DALI Bridge # CONFIG_GATEWAY_MATTER_SUPPORTED=y -CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES=16 +CONFIG_GATEWAY_MATTER_MAX_BRIDGED_DEVICES=32 CONFIG_GATEWAY_MATTER_SCAN_START_DELAY_MS=2000 CONFIG_GATEWAY_MATTER_SCAN_ADDRESS_DELAY_MS=20 CONFIG_GATEWAY_MATTER_SCAN_TASK_STACK_SIZE=8192 @@ -1022,8 +1023,15 @@ CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=256 CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=64 CONFIG_BT_NIMBLE_GATT_MAX_PROCS=4 # CONFIG_BT_NIMBLE_BLE_GATT_BLOB_TRANSFER is not set -# CONFIG_BT_NIMBLE_GATT_CACHING is not set -# CONFIG_BT_NIMBLE_INCL_SVC_DISCOVERY is not set +CONFIG_BT_NIMBLE_GATT_CACHING=y +# CONFIG_BT_NIMBLE_GATT_CACHING_INCLUDE_SERVICES is not set +CONFIG_BT_NIMBLE_GATT_CACHING_MAX_CONNS=4 +CONFIG_BT_NIMBLE_GATT_CACHING_MAX_SVCS=8 +CONFIG_BT_NIMBLE_GATT_CACHING_MAX_INCL_SVCS=4 +CONFIG_BT_NIMBLE_GATT_CACHING_MAX_CHRS=16 +CONFIG_BT_NIMBLE_GATT_CACHING_MAX_DSCS=16 +# CONFIG_BT_NIMBLE_GATT_CACHING_DISABLE_AUTO is not set +# CONFIG_BT_NIMBLE_GATT_CACHING_ASSOC_ENABLE is not set # end of GATT / ATT # @@ -1130,7 +1138,7 @@ CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SUPERVISION_TMO=0 # # Extra Features # -# CONFIG_BT_NIMBLE_DYNAMIC_SERVICE is not set +CONFIG_BT_NIMBLE_DYNAMIC_SERVICE=y # CONFIG_BT_NIMBLE_BLUFI_ENABLE is not set # CONFIG_BT_NIMBLE_ENC_ADV_DATA is not set # CONFIG_BT_NIMBLE_ADV_UUID_CONCAT is not set @@ -1456,6 +1464,7 @@ CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y # Common ESP-related # CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y # end of Common ESP-related # @@ -1544,7 +1553,6 @@ CONFIG_RMT_OBJ_CACHE_SAFE=y # # ESP-Driver:SPI Configurations # -# CONFIG_SPI_MASTER_IN_IRAM is not set CONFIG_SPI_MASTER_ISR_IN_IRAM=y # CONFIG_SPI_SLAVE_IN_IRAM is not set CONFIG_SPI_SLAVE_ISR_IN_IRAM=y @@ -1877,15 +1885,14 @@ CONFIG_SPIRAM_SPEED=80 CONFIG_SPIRAM_BOOT_HW_INIT=y CONFIG_SPIRAM_BOOT_INIT=y CONFIG_SPIRAM_PRE_CONFIGURE_MEMORY_PROTECTION=y -CONFIG_SPIRAM_IGNORE_NOTFOUND=y # CONFIG_SPIRAM_USE_MEMMAP is not set # CONFIG_SPIRAM_USE_CAPS_ALLOC is not set CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_MEMTEST=y -CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=512 CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65535 -# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set +CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y # CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set # end of SPI RAM config # end of ESP PSRAM @@ -2035,10 +2042,10 @@ CONFIG_ESP_TIMER_IMPL_SYSTIMER=y CONFIG_ESP_WIFI_ENABLED=y CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10 CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 -# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set -CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y -CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 -CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=0 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=16 +CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM=32 # CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER is not set CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER=y CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=1 @@ -2048,18 +2055,18 @@ CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y CONFIG_ESP_WIFI_TX_BA_WIN=6 CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y CONFIG_ESP_WIFI_RX_BA_WIN=6 -CONFIG_ESP_WIFI_NVS_ENABLED=y +# CONFIG_ESP_WIFI_AMSDU_TX_ENABLED is not set +# CONFIG_ESP_WIFI_NVS_ENABLED is not set # CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0 is not set CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752 CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32 -CONFIG_ESP_WIFI_IRAM_OPT=y +# CONFIG_ESP_WIFI_IRAM_OPT is not set # CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set -CONFIG_ESP_WIFI_RX_IRAM_OPT=y +# CONFIG_ESP_WIFI_RX_IRAM_OPT is not set CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y CONFIG_ESP_WIFI_ENABLE_SAE_PK=y CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y -CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y # CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50 @@ -2070,7 +2077,7 @@ CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y # CONFIG_ESP_WIFI_GCMP_SUPPORT is not set CONFIG_ESP_WIFI_GMAC_SUPPORT=y -CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y +# CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set # CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7 CONFIG_ESP_WIFI_MBEDTLS_CRYPTO=y @@ -2081,7 +2088,6 @@ CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y # CONFIG_ESP_WIFI_MBO_SUPPORT is not set # CONFIG_ESP_WIFI_DPP_SUPPORT is not set # CONFIG_ESP_WIFI_11R_SUPPORT is not set -# CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR is not set # # WPS Configuration Options @@ -2210,7 +2216,7 @@ CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y # CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y -# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y # CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set # end of Port @@ -2401,6 +2407,7 @@ CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 CONFIG_LWIP_TCP_OVERSIZE_MSS=y # CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set # CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +# CONFIG_LWIP_WND_SCALE is not set CONFIG_LWIP_TCP_RTO_TIME=1500 # end of TCP @@ -2505,8 +2512,8 @@ CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y # mbedTLS # # CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set -# CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC is not set -CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC=y +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set # CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 @@ -3066,8 +3073,8 @@ CONFIG_MAX_GROUPS_PER_FABRIC_PER_ENDPOINT=4 CONFIG_MAX_GROUP_KEYS_PER_FABRIC=3 # CONFIG_CHIP_DEVICE_ENABLE_DYNAMIC_SERVER is not set # CONFIG_CHIP_MEM_ALLOC_MODE_INTERNAL is not set -# CONFIG_CHIP_MEM_ALLOC_MODE_EXTERNAL is not set -CONFIG_CHIP_MEM_ALLOC_MODE_DEFAULT=y +CONFIG_CHIP_MEM_ALLOC_MODE_EXTERNAL=y +# CONFIG_CHIP_MEM_ALLOC_MODE_DEFAULT is not set # end of General Options # @@ -3130,13 +3137,7 @@ CONFIG_DEVICE_TYPE=0 # # WiFi Station Options # -CONFIG_ENABLE_WIFI_STATION=y -CONFIG_DEFAULT_WIFI_SSID="" -CONFIG_DEFAULT_WIFI_PASSWORD="" -CONFIG_WIFI_STATION_RECONNECT_INTERVAL=100 -CONFIG_MAX_SCAN_NETWORKS_RESULTS=10 -CONFIG_WIFI_SCAN_COMPLETION_TIMEOUT=10000 -CONFIG_WIFI_CONNECTIVITY_TIMEOUT=30000 +# CONFIG_ENABLE_WIFI_STATION is not set # end of WiFi Station Options # @@ -3148,9 +3149,9 @@ CONFIG_BLE_FAST_ADVERTISING_INTERVAL_MIN=40 CONFIG_BLE_FAST_ADVERTISING_INTERVAL_MAX=40 CONFIG_BLE_SLOW_ADVERTISING_INTERVAL_MIN=800 CONFIG_BLE_SLOW_ADVERTISING_INTERVAL_MAX=800 -CONFIG_CHIPOBLE_SINGLE_CONNECTION=y +# CONFIG_CHIPOBLE_SINGLE_CONNECTION is not set CONFIG_CHIPOBLE_ENABLE_ADVERTISING_AUTOSTART=0 -CONFIG_USE_BLE_ONLY_FOR_COMMISSIONING=y +# CONFIG_USE_BLE_ONLY_FOR_COMMISSIONING is not set # end of BLE Options # @@ -3275,8 +3276,6 @@ CONFIG_MRP_MAX_RETRANS=4 # # Network Commissioning Driver Endpoint Id # -CONFIG_WIFI_NETWORK_COMMISSIONING_DRIVER=y -CONFIG_WIFI_NETWORK_ENDPOINT_ID=0 # CONFIG_ETHERNET_NETWORK_COMMISSIONING_DRIVER is not set # end of Network Commissioning Driver Endpoint Id @@ -3290,7 +3289,7 @@ CONFIG_WIFI_NETWORK_ENDPOINT_ID=0 # # ESP Matter # -CONFIG_ESP_MATTER_MAX_DEVICE_TYPE_COUNT=16 +CONFIG_ESP_MATTER_MAX_DEVICE_TYPE_COUNT=5 CONFIG_ESP_MATTER_ATTRIBUTE_BUFFER_LARGEST=259 CONFIG_ESP_MATTER_NVS_PART_NAME="nvs" CONFIG_ESP_MATTER_DEFERRED_ATTR_PERSISTENCE_TIME_MS=3000 @@ -3306,13 +3305,13 @@ CONFIG_FACTORY_DEVICE_INSTANCE_INFO_PROVIDER=y # CONFIG_CUSTOM_DEVICE_INSTANCE_INFO_PROVIDER is not set CONFIG_NONE_DEVICE_INFO_PROVIDER=y # CONFIG_CUSTOM_DEVICE_INFO_PROVIDER is not set -CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=18 +CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=84 CONFIG_ESP_MATTER_MODE_SELECT_CLUSTER_ENDPOINT_COUNT=0 CONFIG_ESP_MATTER_TEMPERATURE_CONTROL_CLUSTER_ENDPOINT_COUNT=0 CONFIG_ESP_MATTER_SCENES_TABLE_SIZE=16 CONFIG_ESP_MATTER_BINDING_TABLE_SIZE=10 -CONFIG_ESP_MATTER_MEM_ALLOC_MODE_INTERNAL=y -# CONFIG_ESP_MATTER_MEM_ALLOC_MODE_EXTERNAL is not set +# CONFIG_ESP_MATTER_MEM_ALLOC_MODE_INTERNAL is not set +CONFIG_ESP_MATTER_MEM_ALLOC_MODE_EXTERNAL=y # CONFIG_ESP_MATTER_MEM_ALLOC_MODE_DEFAULT is not set CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y CONFIG_ESP_MATTER_ENABLE_MATTER_SERVER=y @@ -3608,10 +3607,10 @@ CONFIG_MDNS_TASK_AFFINITY=0x0 # # MDNS Memory Configuration # -# CONFIG_MDNS_TASK_CREATE_FROM_SPIRAM is not set -CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL=y -# CONFIG_MDNS_MEMORY_ALLOC_SPIRAM is not set -CONFIG_MDNS_MEMORY_ALLOC_INTERNAL=y +CONFIG_MDNS_TASK_CREATE_FROM_SPIRAM=y +# CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL is not set +CONFIG_MDNS_MEMORY_ALLOC_SPIRAM=y +# CONFIG_MDNS_MEMORY_ALLOC_INTERNAL is not set # CONFIG_MDNS_MEMORY_CUSTOM_IMPL is not set # end of MDNS Memory Configuration diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index 2c46b96..1f676f5 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -190,6 +190,9 @@ class DaliDomainService { bool logarithmic_curve = false) const; bool setColTempRaw(uint8_t gateway_id, int short_address, int mirek) const; bool setColTemp(uint8_t gateway_id, int short_address, int kelvin) const; + // Accepts a DALI logical target (short address 0-63, group 64-79, or + // broadcast 127). Callers must not construct encoded command addresses. + bool setColourXY(uint8_t gateway_id, int logical_target, int x, int y) const; bool setColourRaw(uint8_t gateway_id, int raw_addr, int x, int y) const; bool setColourRGB(uint8_t gateway_id, int short_address, int r, int g, int b) const; bool setColourRGBW(uint8_t gateway_id, int short_address, int r, int g, int b, int w) const; diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index ccc13cb..1a4959c 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -1528,6 +1528,19 @@ bool DaliDomainService::setColTemp(uint8_t gateway_id, int short_address, int ke return channel->dali->dt8.setColorTemperature(short_address, kelvin); } +bool DaliDomainService::setColourXY(uint8_t gateway_id, int logical_target, int x, + int y) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr || logical_target < 0 || + !((logical_target <= 79) || logical_target == 127)) { + return false; + } + markBusActivity(gateway_id); + const double normalized_x = std::clamp(x, 0, 65535) / 65535.0; + const double normalized_y = std::clamp(y, 0, 65535) / 65535.0; + return channel->dali->dt8.setColour(logical_target, normalized_x, normalized_y); +} + bool DaliDomainService::setColourRaw(uint8_t gateway_id, int raw_addr, int x, int y) const { const auto* channel = findChannelByGateway(gateway_id); if (channel == nullptr || channel->dali == nullptr) { diff --git a/components/gateway_bacnet/include/gateway_bacnet.hpp b/components/gateway_bacnet/include/gateway_bacnet.hpp index a2c193b..733bc82 100644 --- a/components/gateway_bacnet/include/gateway_bacnet.hpp +++ b/components/gateway_bacnet/include/gateway_bacnet.hpp @@ -23,6 +23,7 @@ struct GatewayBacnetServerConfig { uint16_t udp_port{47808}; uint32_t task_stack_size{8192}; UBaseType_t task_priority{5}; + bool task_stack_in_psram{false}; }; struct GatewayBacnetObjectBinding { diff --git a/components/gateway_bacnet/src/gateway_bacnet.cpp b/components/gateway_bacnet/src/gateway_bacnet.cpp index 8273e19..ee2c765 100644 --- a/components/gateway_bacnet/src/gateway_bacnet.cpp +++ b/components/gateway_bacnet/src/gateway_bacnet.cpp @@ -2,7 +2,9 @@ #include "gateway_bacnet_stack_port.h" +#include "esp_heap_caps.h" #include "esp_log.h" +#include "freertos/idf_additions.h" #include "freertos/semphr.h" #include @@ -318,9 +320,21 @@ esp_err_t GatewayBacnetServer::startStackLocked(const GatewayBacnetServerConfig& return ESP_FAIL; } - const BaseType_t created = xTaskCreate(&GatewayBacnetServer::TaskEntry, "gw_bacnet_ip", - active_config_.task_stack_size, this, - active_config_.task_priority, &task_handle_); + BaseType_t created = pdFAIL; + if (active_config_.task_stack_in_psram) { + created = xTaskCreateWithCaps( + &GatewayBacnetServer::TaskEntry, "gw_bacnet_ip", + active_config_.task_stack_size, this, active_config_.task_priority, + &task_handle_, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (created != pdPASS) { + ESP_LOGW(kTag, "PSRAM BACnet stack allocation failed; using internal RAM"); + } + } + if (created != pdPASS) { + created = xTaskCreate(&GatewayBacnetServer::TaskEntry, "gw_bacnet_ip", + active_config_.task_stack_size, this, + active_config_.task_priority, &task_handle_); + } if (created != pdPASS) { task_handle_ = nullptr; gateway_bacnet_stack_cleanup(); diff --git a/components/gateway_bridge/include/gateway_bridge.hpp b/components/gateway_bridge/include/gateway_bridge.hpp index 38005cd..07740f0 100644 --- a/components/gateway_bridge/include/gateway_bridge.hpp +++ b/components/gateway_bridge/include/gateway_bridge.hpp @@ -45,6 +45,7 @@ struct GatewayBridgeServiceConfig { std::vector reserved_uart_ports; uint32_t bacnet_task_stack_size{8192}; UBaseType_t bacnet_task_priority{5}; + bool bacnet_task_stack_in_psram{false}; uint32_t knx_task_stack_size{12288}; UBaseType_t knx_task_priority{5}; std::optional default_knx_config; @@ -52,6 +53,10 @@ struct GatewayBridgeServiceConfig { std::function gateway_settings_getter; std::function gateway_settings_setter; std::function gateway_settings_cloud_handler; + std::function)> matter_status_getter; + std::function matter_onboarding_getter; + std::function, + std::string_view)> matter_action_handler; GatewayKnxGatewaySnapshotProvider knx_gateway_snapshot_provider; GatewayKnxGatewayCommandTransactor knx_gateway_command_transactor; }; diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 2bd3886..2fe1521 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -2066,6 +2066,7 @@ struct GatewayBridgeService::ChannelRuntime { } config.task_stack_size = service_config.bacnet_task_stack_size; config.task_priority = service_config.bacnet_task_priority; + config.task_stack_in_psram = service_config.bacnet_task_stack_in_psram; if (bacnet_server_config.has_value()) { config.device_instance = bacnet_server_config->deviceInstance; config.local_address = bacnet_server_config->localAddress; @@ -4636,6 +4637,18 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet( } return config_.gateway_settings_getter(); } + if (action == "matter_status") { + if (!config_.matter_status_getter) { + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "Matter status is not available"); + } + return config_.matter_status_getter(gateway_id); + } + if (action == "matter_onboarding") { + if (!config_.matter_onboarding_getter) { + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "Matter onboarding is not available"); + } + return config_.matter_onboarding_getter(); + } if (action == "status" && !gateway_id.has_value()) { cJSON* root = cJSON_CreateObject(); @@ -4814,6 +4827,14 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( } return config_.gateway_settings_setter(body); } + if (action == "matter_open_commissioning" || + action == "matter_close_commissioning" || action == "matter_rescan" || + action == "matter_config" || action == "matter_config_reset") { + if (!config_.matter_action_handler) { + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "Matter control is not available"); + } + return config_.matter_action_handler(action, gateway_id, body); + } if (!gateway_id.has_value()) { return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required"); } diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index d2c7a7a..32ec0a2 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -27,6 +27,7 @@ class GatewayRuntime; struct GatewayControllerConfig { uint32_t task_stack_size{12288}; UBaseType_t task_priority{5}; + bool operation_task_stack_in_psram{false}; int color_temperature_min{2000}; int color_temperature_max{6500}; bool setup_supported{true}; @@ -187,6 +188,7 @@ class GatewayController { struct GatewayOperationTaskContext { GatewayController* controller{nullptr}; + bool external_stack{false}; uint8_t gateway_id{0}; uint8_t request_id{0}; uint16_t operation_id{0}; diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 6c519ca..8cf320c 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -1,8 +1,10 @@ #include "gateway_controller.hpp" #include "dali_domain.hpp" +#include "esp_heap_caps.h" #include "esp_log.h" #include "esp_system.h" +#include "freertos/idf_additions.h" #include "gateway_bridge.hpp" #include "gateway_runtime.hpp" @@ -924,14 +926,20 @@ void GatewayController::TaskEntry(void* arg) { void GatewayController::OperationTaskEntry(void* arg) { auto* context = static_cast(arg); - if (context == nullptr || context->controller == nullptr) { - delete context; + if (context == nullptr) { vTaskDelete(nullptr); return; } - context->controller->runOperationTask(context); + const bool external_stack = context->external_stack; + if (context->controller != nullptr) { + context->controller->runOperationTask(context); + } delete context; - vTaskDelete(nullptr); + if (external_stack) { + vTaskDeleteWithCaps(nullptr); + } else { + vTaskDelete(nullptr); + } } void GatewayController::taskLoop() { @@ -2495,9 +2503,21 @@ bool GatewayController::startOperation(uint8_t gateway_id, uint8_t request_id, context->request_id = request_id; context->operation_id = operation_id; context->fields = std::move(fields); - const BaseType_t created = xTaskCreate(&GatewayController::OperationTaskEntry, "gateway_op", - config_.task_stack_size, context, - config_.task_priority, nullptr); + BaseType_t created = pdFAIL; + if (config_.operation_task_stack_in_psram) { + created = xTaskCreateWithCaps( + &GatewayController::OperationTaskEntry, "gateway_op", config_.task_stack_size, + context, config_.task_priority, nullptr, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + context->external_stack = created == pdPASS; + if (created != pdPASS) { + ESP_LOGW(kTag, "PSRAM operation stack allocation failed; using internal RAM"); + } + } + if (created != pdPASS) { + created = xTaskCreate(&GatewayController::OperationTaskEntry, "gateway_op", + config_.task_stack_size, context, + config_.task_priority, nullptr); + } if (created != pdPASS) { delete context; { diff --git a/components/gateway_matter/CMakeLists.txt b/components/gateway_matter/CMakeLists.txt index 6360ad9..420f85c 100644 --- a/components/gateway_matter/CMakeLists.txt +++ b/components/gateway_matter/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( - SRCS "src/gateway_matter.cpp" + SRCS "src/gateway_matter.cpp" "src/gateway_matter_plan.cpp" INCLUDE_DIRS "include" - REQUIRES dali_domain esp_driver_gpio esp_matter freertos log nvs_flash + REQUIRES dali_domain esp_driver_gpio esp_matter freertos json log nvs_flash ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/components/gateway_matter/include/gateway_matter.hpp b/components/gateway_matter/include/gateway_matter.hpp index 0be432c..f148a07 100644 --- a/components/gateway_matter/include/gateway_matter.hpp +++ b/components/gateway_matter/include/gateway_matter.hpp @@ -1,13 +1,18 @@ #pragma once +#include #include #include #include #include #include +#include +#include +#include #include #include "dali_domain.hpp" +#include "gateway_matter_plan.hpp" #include "esp_err.h" #include "esp_matter.h" #include "esp_matter_bridge.h" @@ -18,7 +23,7 @@ namespace gateway { struct GatewayMatterBridgeConfig { bool enabled{false}; - size_t max_bridged_devices{16}; + size_t max_bridged_devices{32}; uint32_t scan_start_delay_ms{2000}; uint32_t scan_address_delay_ms{20}; uint32_t scan_task_stack_size{8192}; @@ -43,12 +48,23 @@ class GatewayMatterBridge { ~GatewayMatterBridge() = default; esp_err_t start(); + esp_err_t openCommissioningWindow(); + esp_err_t closeCommissioningWindow(); + esp_err_t rescanDaliDevices(); + esp_err_t applyConfiguration(uint8_t gateway_id, std::string_view patch); + esp_err_t resetConfiguration(uint8_t gateway_id); + std::string statusJson(std::optional gateway_id = std::nullopt) const; + std::string onboardingJson() const; private: struct Binding { GatewayMatterBridge* owner{nullptr}; + uint8_t channel_index{0}; uint8_t gateway_id{0}; - uint8_t short_address{0}; + MatterTargetKind target_kind{MatterTargetKind::shortAddress}; + uint8_t target_address{0}; + MatterEndpointType endpoint_type{MatterEndpointType::switchLight}; + std::optional color_method; uint16_t dali_device_type_mask{0}; uint32_t matter_device_type{0}; uint16_t endpoint_id{0}; @@ -77,21 +93,25 @@ class GatewayMatterBridge { static void ButtonTaskEntry(void* argument); static void ApplyStatusWork(intptr_t argument); static void OpenCommissioningWindowWork(intptr_t argument); + static void CloseCommissioningWindowWork(intptr_t argument); esp_err_t handleAttributeUpdate(Binding& binding, uint32_t cluster_id, uint32_t attribute_id, const esp_matter_attr_val_t& value); esp_err_t initializeBridgeStorage(); esp_err_t resumeBindings(); + esp_err_t loadConfigurations(); + esp_err_t storeConfiguration(const MatterChannelConfiguration& configuration) const; esp_err_t configureProvisioningButton(); void scanDaliDevices(); + esp_err_t reconcileEndpoints(); void buttonTaskLoop(); void handleStatusUpdate(const DaliGatewayStatusUpdate& update); - Binding* findBinding(uint8_t gateway_id, uint8_t short_address) const; + Binding* findBinding(const MatterTarget& target) const; Binding* findBinding(uint16_t endpoint_id) const; - esp_err_t createBinding(uint8_t gateway_id, uint8_t short_address, - uint16_t device_type_mask, - uint8_t initial_level = 0); + esp_err_t createBinding(const MatterEndpointAllocation& allocation, + uint16_t device_type_mask, uint8_t initial_level = 0); + esp_err_t removeBinding(size_t index); esp_err_t storeBinding(const Binding& binding) const; std::unique_ptr readBinding(uint16_t endpoint_id) const; void updateMatterLevel(Binding& binding, uint8_t level); @@ -103,9 +123,16 @@ class GatewayMatterBridge { uint16_t aggregator_endpoint_id_{0}; mutable std::mutex bindings_mutex_; std::vector> bindings_; + std::vector configurations_; + MatterEndpointPlan endpoint_plan_; + std::string last_apply_error_; TaskHandle_t scan_task_{nullptr}; TaskHandle_t button_task_{nullptr}; bool started_{false}; + std::atomic_bool scan_in_progress_{false}; + std::atomic_bool ble_advertising_{false}; + std::atomic_bool commissioning_window_open_{false}; + std::atomic_uint8_t fabric_count_{0}; }; } // namespace gateway diff --git a/components/gateway_matter/include/gateway_matter_plan.hpp b/components/gateway_matter/include/gateway_matter_plan.hpp new file mode 100644 index 0000000..14c9e81 --- /dev/null +++ b/components/gateway_matter/include/gateway_matter_plan.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace gateway { + +enum class MatterTargetKind : uint8_t { shortAddress = 0, group = 1, broadcast = 2 }; +enum class MatterEndpointType : uint8_t { switchLight = 0, dimmer = 1, colorTemperature = 2, color = 3 }; +enum class MatterColorMethod : uint8_t { xy = 0, rgbcw = 1, both = 2 }; +enum class MatterAddressOverride : uint8_t { automatic = 0, include = 1, exclude = 2 }; + +struct MatterTarget { + uint8_t channel_index{0}; + uint8_t gateway_id{0}; + MatterTargetKind kind{MatterTargetKind::shortAddress}; + uint8_t address{0}; + + bool sameIdentity(const MatterTarget& other) const { + return channel_index == other.channel_index && kind == other.kind && address == other.address; + } +}; + +struct MatterEndpointOverride { + std::optional type; + std::optional color_method; + + bool manual() const { return type.has_value() || color_method.has_value(); } +}; + +struct MatterChannelConfiguration { + uint8_t channel_index{0}; + bool automatic_short_addresses{true}; + std::array short_address_overrides{}; + MatterEndpointOverride broadcast; + std::array groups{}; +}; + +struct MatterAddressCandidate { + bool online{false}; + bool group_mask_known{false}; + uint16_t group_mask{0}; + std::optional dali_device_type_mask; + std::optional dt8_color_type_features; +}; + +struct MatterChannelCandidates { + uint8_t channel_index{0}; + uint8_t gateway_id{0}; + std::array addresses{}; +}; + +struct MatterEndpointAllocation { + MatterTarget target; + MatterEndpointType type{MatterEndpointType::switchLight}; + std::optional color_method; + bool type_is_manual{false}; + bool color_method_is_manual{false}; + bool explicitly_included{false}; +}; + +struct MatterDroppedAllocation { + MatterEndpointAllocation allocation; + std::string reason; +}; + +struct MatterEndpointPlan { + size_t capacity{0}; + std::vector active; + std::vector dropped; +}; + +MatterEndpointPlan BuildMatterEndpointPlan( + const std::vector& candidates, + const std::vector& configurations, + size_t capacity); + +const char* MatterTargetKindName(MatterTargetKind kind); +const char* MatterEndpointTypeName(MatterEndpointType type); +const char* MatterColorMethodName(MatterColorMethod method); + +} // namespace gateway diff --git a/components/gateway_matter/src/gateway_matter.cpp b/components/gateway_matter/src/gateway_matter.cpp index ebec910..6b00fb1 100644 --- a/components/gateway_matter/src/gateway_matter.cpp +++ b/components/gateway_matter/src/gateway_matter.cpp @@ -10,8 +10,13 @@ #include "app/server/CommissioningWindowManager.h" #include "app/server/Server.h" +#include "platform/DeviceInstanceInfoProvider.h" +#include "setup_payload/OnboardingCodesUtil.h" +#include "setup_payload/QRCodeSetupPayloadGenerator.h" +#include "cJSON.h" #include "esp_check.h" #include "driver/gpio.h" +#include "esp_heap_caps.h" #include "esp_log.h" #include "freertos/task.h" #include "nvs.h" @@ -23,12 +28,16 @@ namespace { constexpr char kTag[] = "gateway_matter"; constexpr char kBindingNamespace[] = "dali_matter"; -constexpr uint8_t kBindingVersion = 1; +constexpr char kConfigurationNamespace[] = "matter_cfg"; +constexpr uint8_t kBindingVersion = 2; +constexpr uint8_t kConfigurationVersion = 1; constexpr uint16_t kAllDaliDeviceTypesMask = 0x01FF; constexpr uint32_t kCommissioningWindowSeconds = 300; GatewayMatterBridge* s_active_matter_bridge = nullptr; +static_assert(MAX_BRIDGED_DEVICE_COUNT == 82, + "DaliMaster Matter capacity expects 82 bridge slots"); -struct PersistedBinding { +struct PersistedBindingV1 { uint8_t version; uint8_t gateway_id; uint8_t short_address; @@ -38,12 +47,44 @@ struct PersistedBinding { uint32_t matter_device_type; }; +struct PersistedBinding { + uint8_t version; + uint8_t channel_index; + uint8_t gateway_id; + uint8_t target_kind; + uint8_t target_address; + uint8_t endpoint_type; + uint8_t color_method; + uint8_t color_method_known; + uint16_t dali_device_type_mask; + uint16_t endpoint_id; + uint32_t matter_device_type; +}; + +struct PersistedChannelConfiguration { + uint8_t version; + uint8_t channel_index; + uint8_t automatic_short_addresses; + uint8_t reserved; + uint8_t short_address_overrides[64]; + uint8_t broadcast_type; + uint8_t broadcast_color_method; + uint8_t group_types[16]; + uint8_t group_color_methods[16]; +}; + std::string BindingKey(uint16_t endpoint_id) { char key[16] = {}; std::snprintf(key, sizeof(key), "b%04x", endpoint_id); return key; } +std::string ConfigurationKey(uint8_t channel_index) { + char key[16] = {}; + std::snprintf(key, sizeof(key), "c%02x", channel_index); + return key; +} + uint16_t DeviceTypeMask(const DaliDomainSnapshot& snapshot) { uint16_t mask = 0; const auto types = snapshot.int_arrays.find("types"); @@ -59,14 +100,74 @@ uint16_t DeviceTypeMask(const DaliDomainSnapshot& snapshot) { return mask == 0 ? 1U : static_cast(mask & kAllDaliDeviceTypesMask); } -uint32_t MatterDeviceType(uint16_t dali_device_type_mask) { - if ((dali_device_type_mask & (1U << 8)) != 0) { - return ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID; +uint32_t MatterDeviceType(MatterEndpointType type) { + switch (type) { + case MatterEndpointType::switchLight: + return ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID; + case MatterEndpointType::dimmer: + return ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID; + case MatterEndpointType::colorTemperature: + return ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID; + case MatterEndpointType::color: + return ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID; } - if ((dali_device_type_mask & (1U << 7)) != 0) { - return ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID; + return ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID; +} + +const char* MatterDeviceTypeName(uint32_t matter_device_type) { + switch (matter_device_type) { + case ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID: + return "onOffLight"; + case ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID: + return "dimmableLight"; + case ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID: + return "extendedColorLight"; + case ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID: + return "colorTemperatureLight"; + default: + return "unknown"; } - return ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID; +} + +std::optional DecodeEndpointType(uint8_t value) { + if (value == 0 || value > 4) return std::nullopt; + return static_cast(value - 1); +} + +std::optional DecodeColorMethod(uint8_t value) { + if (value == 0 || value > 3) return std::nullopt; + return static_cast(value - 1); +} + +uint8_t EncodeEndpointType(std::optional value) { + return value.has_value() ? static_cast(*value) + 1 : 0; +} + +uint8_t EncodeColorMethod(std::optional value) { + return value.has_value() ? static_cast(*value) + 1 : 0; +} + +MatterChannelConfiguration DefaultConfiguration(uint8_t channel_index) { + MatterChannelConfiguration configuration; + configuration.channel_index = channel_index; + return configuration; +} + +MatterEndpointType ParseEndpointType(const char* value, MatterEndpointType fallback) { + if (value == nullptr) return fallback; + if (std::strcmp(value, "switch") == 0) return MatterEndpointType::switchLight; + if (std::strcmp(value, "dimmer") == 0) return MatterEndpointType::dimmer; + if (std::strcmp(value, "colorTemperature") == 0) return MatterEndpointType::colorTemperature; + if (std::strcmp(value, "color") == 0) return MatterEndpointType::color; + return fallback; +} + +MatterColorMethod ParseColorMethod(const char* value, MatterColorMethod fallback) { + if (value == nullptr) return fallback; + if (std::strcmp(value, "xy") == 0) return MatterColorMethod::xy; + if (std::strcmp(value, "rgbcw") == 0) return MatterColorMethod::rgbcw; + if (std::strcmp(value, "both") == 0) return MatterColorMethod::both; + return fallback; } void HsvToRgb(uint8_t hue, uint8_t saturation, int* red, int* green, int* blue) { @@ -102,6 +203,43 @@ void HsvToRgb(uint8_t hue, uint8_t saturation, int* red, int* green, int* blue) *blue = static_cast(std::round((b + m) * 255.0)); } +void RgbToXy(int red, int green, int blue, uint16_t* x, uint16_t* y) { + const double r = red / 255.0; + const double g = green / 255.0; + const double b = blue / 255.0; + const double linear_r = r > 0.04045 ? std::pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + const double linear_g = g > 0.04045 ? std::pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + const double linear_b = b > 0.04045 ? std::pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + const double tristimulus_x = linear_r * 0.664511 + linear_g * 0.154324 + linear_b * 0.162028; + const double tristimulus_y = linear_r * 0.283881 + linear_g * 0.668433 + linear_b * 0.047685; + const double tristimulus_z = linear_r * 0.000088 + linear_g * 0.072310 + linear_b * 0.986039; + const double sum = tristimulus_x + tristimulus_y + tristimulus_z; + *x = static_cast(std::round(std::clamp(sum == 0 ? 0.0 : tristimulus_x / sum, + 0.0, 1.0) * 65535.0)); + *y = static_cast(std::round(std::clamp(sum == 0 ? 0.0 : tristimulus_y / sum, + 0.0, 1.0) * 65535.0)); +} + +void XyToRgb(uint16_t raw_x, uint16_t raw_y, int* red, int* green, int* blue) { + const double x = raw_x / 65535.0; + const double y = std::max(raw_y / 65535.0, 0.0001); + const double tristimulus_x = x / y; + const double tristimulus_y = 1.0; + const double tristimulus_z = (1.0 - x - y) / y; + double r = tristimulus_x * 1.656492 - tristimulus_y * 0.354851 - tristimulus_z * 0.255038; + double g = -tristimulus_x * 0.707196 + tristimulus_y * 1.655397 + tristimulus_z * 0.036152; + double b = tristimulus_x * 0.051713 - tristimulus_y * 0.121364 + tristimulus_z * 1.011530; + const double max_value = std::max({r, g, b, 1.0}); + const auto gamma = [max_value](double value) { + value = std::clamp(value / max_value, 0.0, 1.0); + return value <= 0.0031308 ? 12.92 * value + : 1.055 * std::pow(value, 1.0 / 2.4) - 0.055; + }; + *red = static_cast(std::round(gamma(r) * 255.0)); + *green = static_cast(std::round(gamma(g) * 255.0)); + *blue = static_cast(std::round(gamma(b) * 255.0)); +} + } // namespace struct GatewayMatterBridge::StatusWork { @@ -149,18 +287,48 @@ esp_err_t GatewayMatterBridge::start() { } ESP_RETURN_ON_ERROR(initializeBridgeStorage(), kTag, "failed to initialize Matter binding storage"); - ESP_RETURN_ON_ERROR(resumeBindings(), kTag, "failed to resume DALI bindings"); + ESP_RETURN_ON_ERROR(loadConfigurations(), kTag, + "failed to load Matter endpoint configuration"); + + // Reserve the large internal-RAM stack before persisted endpoints fragment + // the heap. The task blocks on a notification and cannot touch DALI or the + // partially initialized bridge until startup finishes. + scan_in_progress_.store(true, std::memory_order_release); + if (xTaskCreate(ScanTaskEntry, "matter_dali_scan", config_.scan_task_stack_size, this, + config_.scan_task_priority, &scan_task_) != pdPASS) { + scan_in_progress_.store(false, std::memory_order_release); + ESP_LOGE(kTag, + "failed to allocate internal Matter scan stack bytes=%u free=%u largest=%u", + static_cast(config_.scan_task_stack_size), + static_cast(heap_caps_get_free_size(MALLOC_CAP_INTERNAL | + MALLOC_CAP_8BIT)), + static_cast(heap_caps_get_largest_free_block( + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))); + return ESP_ERR_NO_MEM; + } + + const esp_err_t resume_err = resumeBindings(); + if (resume_err != ESP_OK) { + vTaskDelete(scan_task_); + scan_task_ = nullptr; + scan_in_progress_.store(false, std::memory_order_release); + ESP_LOGE(kTag, "failed to resume DALI bindings: %s", esp_err_to_name(resume_err)); + return resume_err; + } dali_domain_.gatewayCache().addStatusUpdateCallback( [this](const DaliGatewayStatusUpdate& update) { handleStatusUpdate(update); }); - ESP_RETURN_ON_ERROR(configureProvisioningButton(), kTag, - "failed to configure Matter provisioning button"); - - if (xTaskCreate(ScanTaskEntry, "matter_dali_scan", config_.scan_task_stack_size, this, - config_.scan_task_priority, &scan_task_) != pdPASS) { - return ESP_ERR_NO_MEM; + const esp_err_t button_err = configureProvisioningButton(); + if (button_err != ESP_OK) { + vTaskDelete(scan_task_); + scan_task_ = nullptr; + scan_in_progress_.store(false, std::memory_order_release); + ESP_LOGE(kTag, "failed to configure Matter provisioning button: %s", + esp_err_to_name(button_err)); + return button_err; } started_ = true; + xTaskNotifyGive(scan_task_); ESP_LOGI(kTag, "Matter-to-DALI bridge started aggregator=%u restored=%u max=%u", aggregator_endpoint_id_, static_cast(bindings_.size()), static_cast(config_.max_bridged_devices)); @@ -173,6 +341,76 @@ esp_err_t GatewayMatterBridge::initializeBridgeStorage() { return esp_matter_bridge::initialize(node_, AddDeviceType); } +esp_err_t GatewayMatterBridge::loadConfigurations() { + configurations_.clear(); + nvs_handle_t handle = 0; + const esp_err_t open_err = nvs_open_from_partition( + "nvs", kConfigurationNamespace, NVS_READONLY, &handle); + for (const auto& channel : dali_domain_.channelInfo()) { + MatterChannelConfiguration configuration = + DefaultConfiguration(channel.channel_index); + if (open_err == ESP_OK) { + PersistedChannelConfiguration persisted{}; + size_t length = sizeof(persisted); + const std::string key = ConfigurationKey(channel.channel_index); + if (nvs_get_blob(handle, key.c_str(), &persisted, &length) == ESP_OK && + length == sizeof(persisted) && + persisted.version == kConfigurationVersion && + persisted.channel_index == channel.channel_index) { + configuration.automatic_short_addresses = + persisted.automatic_short_addresses != 0; + for (size_t address = 0; + address < configuration.short_address_overrides.size(); ++address) { + if (persisted.short_address_overrides[address] <= + static_cast(MatterAddressOverride::exclude)) { + configuration.short_address_overrides[address] = + static_cast(persisted.short_address_overrides[address]); + } + } + configuration.broadcast.type = DecodeEndpointType(persisted.broadcast_type); + configuration.broadcast.color_method = + DecodeColorMethod(persisted.broadcast_color_method); + for (size_t group = 0; group < configuration.groups.size(); ++group) { + configuration.groups[group].type = DecodeEndpointType(persisted.group_types[group]); + configuration.groups[group].color_method = + DecodeColorMethod(persisted.group_color_methods[group]); + } + } + } + configurations_.push_back(configuration); + } + if (open_err == ESP_OK) nvs_close(handle); + return open_err == ESP_ERR_NVS_NOT_FOUND ? ESP_OK : open_err; +} + +esp_err_t GatewayMatterBridge::storeConfiguration( + const MatterChannelConfiguration& configuration) const { + PersistedChannelConfiguration persisted{}; + persisted.version = kConfigurationVersion; + persisted.channel_index = configuration.channel_index; + persisted.automatic_short_addresses = configuration.automatic_short_addresses ? 1 : 0; + for (size_t address = 0; address < configuration.short_address_overrides.size(); ++address) { + persisted.short_address_overrides[address] = + static_cast(configuration.short_address_overrides[address]); + } + persisted.broadcast_type = EncodeEndpointType(configuration.broadcast.type); + persisted.broadcast_color_method = EncodeColorMethod(configuration.broadcast.color_method); + for (size_t group = 0; group < configuration.groups.size(); ++group) { + persisted.group_types[group] = EncodeEndpointType(configuration.groups[group].type); + persisted.group_color_methods[group] = EncodeColorMethod( + configuration.groups[group].color_method); + } + nvs_handle_t handle = 0; + ESP_RETURN_ON_ERROR(nvs_open_from_partition( + "nvs", kConfigurationNamespace, NVS_READWRITE, &handle), + kTag, "failed to open Matter configuration NVS"); + const std::string key = ConfigurationKey(configuration.channel_index); + esp_err_t err = nvs_set_blob(handle, key.c_str(), &persisted, sizeof(persisted)); + if (err == ESP_OK) err = nvs_commit(handle); + nvs_close(handle); + return err; +} + esp_err_t GatewayMatterBridge::resumeBindings() { uint16_t endpoint_ids[MAX_BRIDGED_DEVICE_COUNT] = {}; ESP_RETURN_ON_ERROR(esp_matter_bridge::get_bridged_endpoint_ids(endpoint_ids), kTag, @@ -186,10 +424,20 @@ esp_err_t GatewayMatterBridge::resumeBindings() { continue; } binding->owner = this; + // Resuming/enabling an endpoint can replay persisted Matter attributes. + // Do not reflect that initialization traffic back onto the physical DALI + // bus while the ESP-Matter endpoint creation stack is still active. + binding->suppress_attribute_write = true; binding->matter_device = esp_matter_bridge::resume_device(node_, endpoint_id, binding.get()); if (binding->matter_device == nullptr) continue; + const esp_err_t migrate_err = storeBinding(*binding); + if (migrate_err != ESP_OK) { + ESP_LOGW(kTag, "failed to migrate binding endpoint=%u: %s", endpoint_id, + esp_err_to_name(migrate_err)); + } esp_matter::endpoint::enable(binding->matter_device->endpoint); + binding->suppress_attribute_write = false; { std::lock_guard guard(bindings_mutex_); bindings_.push_back(std::move(binding)); @@ -199,16 +447,25 @@ esp_err_t GatewayMatterBridge::resumeBindings() { return ESP_OK; } -esp_err_t GatewayMatterBridge::createBinding(uint8_t gateway_id, uint8_t short_address, - uint16_t device_type_mask, +esp_err_t GatewayMatterBridge::createBinding( + const MatterEndpointAllocation& allocation, uint16_t device_type_mask, uint8_t initial_level) { if (bindings_.size() >= config_.max_bridged_devices) return ESP_ERR_NO_MEM; auto binding = std::make_unique(); binding->owner = this; - binding->gateway_id = gateway_id; - binding->short_address = short_address; + binding->channel_index = allocation.target.channel_index; + binding->gateway_id = allocation.target.gateway_id; + binding->target_kind = allocation.target.kind; + binding->target_address = allocation.target.address; + binding->endpoint_type = allocation.type; + binding->color_method = allocation.color_method; binding->dali_device_type_mask = device_type_mask; - binding->matter_device_type = MatterDeviceType(device_type_mask); + binding->matter_device_type = MatterDeviceType(allocation.type); + // Endpoint enablement may restore OnOff/Level attributes and invoke the + // application callback synchronously. Suppress those writes until the new + // endpoint is fully registered; updateMatterLevel() applies the discovered + // cache state afterward under the same suppression guard. + binding->suppress_attribute_write = true; binding->matter_device = esp_matter_bridge::create_device( node_, aggregator_endpoint_id_, binding->matter_device_type, binding.get()); ESP_RETURN_ON_FALSE(binding->matter_device != nullptr, ESP_FAIL, kTag, @@ -226,14 +483,17 @@ esp_err_t GatewayMatterBridge::createBinding(uint8_t gateway_id, uint8_t short_a return store_err; } esp_matter::endpoint::enable(binding->matter_device->endpoint); + binding->suppress_attribute_write = false; Binding* created = binding.get(); { std::lock_guard guard(bindings_mutex_); bindings_.push_back(std::move(binding)); } updateMatterLevel(*created, initial_level); - ESP_LOGI(kTag, "bound gateway=%u short=%u types=0x%03x endpoint=%u matter=0x%lx", - gateway_id, short_address, device_type_mask, created->endpoint_id, + ESP_LOGI(kTag, "bound channel=%u gateway=%u target=%s:%u endpoint=%u matter=0x%lx", + created->channel_index, created->gateway_id, + MatterTargetKindName(created->target_kind), created->target_address, + created->endpoint_id, static_cast(created->matter_device_type)); return ESP_OK; } @@ -245,9 +505,14 @@ esp_err_t GatewayMatterBridge::storeBinding(const Binding& binding) const { kTag, "failed to open DALI Matter NVS"); const PersistedBinding persisted{ kBindingVersion, + binding.channel_index, binding.gateway_id, - binding.short_address, - 0, + static_cast(binding.target_kind), + binding.target_address, + static_cast(binding.endpoint_type), + static_cast(binding.color_method.has_value() + ? static_cast(*binding.color_method) : 0), + static_cast(binding.color_method.has_value() ? 1 : 0), binding.dali_device_type_mask, binding.endpoint_id, binding.matter_device_type, @@ -270,20 +535,46 @@ std::unique_ptr GatewayMatterBridge::readBinding( const std::string key = BindingKey(endpoint_id); const esp_err_t err = nvs_get_blob(handle, key.c_str(), &persisted, &length); nvs_close(handle); - const uint16_t dali_device_type_mask = - persisted.dali_device_type_mask & kAllDaliDeviceTypesMask; - const uint32_t expected_matter_device_type = MatterDeviceType(dali_device_type_mask); - if (err != ESP_OK || length != sizeof(persisted) || - persisted.version != kBindingVersion || persisted.endpoint_id != endpoint_id || - persisted.short_address >= 64 || dali_device_type_mask == 0 || - persisted.matter_device_type != expected_matter_device_type) { + auto binding = std::make_unique(); + if (err != ESP_OK) return nullptr; + if (length == sizeof(PersistedBindingV1)) { + const auto* old = reinterpret_cast(&persisted); + if (old->version != 1 || old->endpoint_id != endpoint_id || old->short_address >= 64) { + return nullptr; + } + const auto channels = dali_domain_.channelInfo(); + const auto channel = std::find_if(channels.begin(), channels.end(), [old](const auto& item) { + return item.gateway_id == old->gateway_id; + }); + if (channel == channels.end()) return nullptr; + binding->channel_index = channel->channel_index; + binding->gateway_id = old->gateway_id; + binding->target_kind = MatterTargetKind::shortAddress; + binding->target_address = old->short_address; + binding->dali_device_type_mask = old->dali_device_type_mask & kAllDaliDeviceTypesMask; + binding->matter_device_type = old->matter_device_type; + binding->endpoint_type = old->matter_device_type == ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID + ? MatterEndpointType::color + : (old->matter_device_type == ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID + ? MatterEndpointType::switchLight + : MatterEndpointType::dimmer); + } else if (length == sizeof(PersistedBinding) && persisted.version == kBindingVersion && + persisted.endpoint_id == endpoint_id && persisted.target_kind <= 2 && + persisted.endpoint_type <= 3) { + binding->channel_index = persisted.channel_index; + binding->gateway_id = persisted.gateway_id; + binding->target_kind = static_cast(persisted.target_kind); + binding->target_address = persisted.target_address; + binding->endpoint_type = static_cast(persisted.endpoint_type); + if (persisted.color_method_known != 0 && persisted.color_method <= 2) { + binding->color_method = static_cast(persisted.color_method); + } + binding->dali_device_type_mask = persisted.dali_device_type_mask; + binding->matter_device_type = persisted.matter_device_type; + if (binding->matter_device_type != MatterDeviceType(binding->endpoint_type)) return nullptr; + } else { return nullptr; } - auto binding = std::make_unique(); - binding->gateway_id = persisted.gateway_id; - binding->short_address = persisted.short_address; - binding->dali_device_type_mask = dali_device_type_mask; - binding->matter_device_type = persisted.matter_device_type; binding->endpoint_id = endpoint_id; return binding; } @@ -293,42 +584,523 @@ void GatewayMatterBridge::scanDaliDevices() { static const std::vector kSupportedDaliDeviceTypes{0, 1, 2, 3, 4, 5, 6, 7, 8}; for (const auto& channel : dali_domain_.channelInfo()) { for (uint8_t short_address = 0; short_address < 64; ++short_address) { - if (bindings_.size() >= config_.max_bridged_devices) { - ESP_LOGW(kTag, "DALI scan stopped at configured endpoint limit=%u", - static_cast(config_.max_bridged_devices)); - return; - } - if (findBinding(channel.gateway_id, short_address) != nullptr) continue; const auto level = dali_domain_.queryActualLevel(channel.gateway_id, short_address); - if (!level.has_value() || *level > 254) continue; + const bool online = level.has_value() && *level <= 254; + dali_domain_.gatewayCache().markAddressPresence( + channel.gateway_id, short_address, + online ? DaliGatewayPresence::online : DaliGatewayPresence::offline); + if (!online) continue; + const auto group_mask = dali_domain_.queryGroupMask(channel.gateway_id, short_address); + if (group_mask.has_value()) { + dali_domain_.gatewayCache().setGroupMask(channel.gateway_id, short_address, + group_mask); + } const auto discovery = dali_domain_.discoverDeviceTypes( channel.gateway_id, short_address, kSupportedDaliDeviceTypes); const uint16_t type_mask = discovery.has_value() ? DeviceTypeMask(*discovery) : 1U; - const esp_err_t err = - createBinding(channel.gateway_id, short_address, type_mask, *level); - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed binding gateway=%u short=%u: %s", channel.gateway_id, - short_address, esp_err_to_name(err)); + std::optional color_features; + if ((type_mask & (1U << 8)) != 0) { + const auto dt8 = dali_domain_.dt8StatusSnapshot(channel.gateway_id, short_address); + if (dt8.has_value()) { + const auto value = dt8->ints.find("colorTypeFeaturesRaw"); + if (value != dt8->ints.end() && value->second >= 0 && value->second <= 255) { + color_features = static_cast(value->second); + } + } } + dali_domain_.gatewayCache().setCapabilities(channel.gateway_id, short_address, + type_mask, color_features); vTaskDelay(pdMS_TO_TICKS(config_.scan_address_delay_ms)); } } + if (!dali_domain_.gatewayCache().flush()) { + ESP_LOGW(kTag, "failed to persist DALI capabilities after Matter scan"); + } + const esp_err_t reconcile_err = reconcileEndpoints(); + if (reconcile_err != ESP_OK) { + last_apply_error_ = esp_err_to_name(reconcile_err); + ESP_LOGW(kTag, "Matter endpoint reconciliation incomplete: %s", + last_apply_error_.c_str()); + } else { + last_apply_error_.clear(); + } +} + +esp_err_t GatewayMatterBridge::removeBinding(size_t index) { + std::unique_ptr removed; + { + std::lock_guard guard(bindings_mutex_); + if (index >= bindings_.size()) return ESP_ERR_INVALID_ARG; + removed = std::move(bindings_[index]); + bindings_.erase(bindings_.begin() + index); + } + const std::string key = BindingKey(removed->endpoint_id); + nvs_handle_t handle = 0; + if (nvs_open_from_partition("nvs", kBindingNamespace, NVS_READWRITE, &handle) == ESP_OK) { + nvs_erase_key(handle, key.c_str()); + nvs_commit(handle); + nvs_close(handle); + } + return esp_matter_bridge::remove_device(removed->matter_device); +} + +esp_err_t GatewayMatterBridge::reconcileEndpoints() { + std::vector candidates; + const auto channels = dali_domain_.channelInfo(); + for (const auto& channel : channels) { + MatterChannelCandidates channel_candidates; + channel_candidates.channel_index = channel.channel_index; + channel_candidates.gateway_id = channel.gateway_id; + const auto states = dali_domain_.gatewayCache().addressStates(channel.gateway_id); + for (uint8_t address = 0; address < 64; ++address) { + auto& candidate = channel_candidates.addresses[address]; + candidate.online = dali_domain_.gatewayCache().addressPresence( + channel.gateway_id, address) == DaliGatewayPresence::online; + candidate.group_mask_known = states[address].groupMaskKnown; + candidate.group_mask = states[address].groupMask; + candidate.dali_device_type_mask = states[address].daliDeviceTypeMask; + candidate.dt8_color_type_features = states[address].dt8ColorTypeFeatures; + } + candidates.push_back(channel_candidates); + } + const MatterEndpointPlan next_plan = BuildMatterEndpointPlan( + candidates, configurations_, config_.max_bridged_devices); + + // Remove obsolete bindings and bindings whose Matter device type changed. + for (size_t index = 0;;) { + Binding snapshot; + { + std::lock_guard guard(bindings_mutex_); + if (index >= bindings_.size()) break; + snapshot = *bindings_[index]; + } + const auto desired = std::find_if( + next_plan.active.begin(), next_plan.active.end(), [&snapshot](const auto& allocation) { + return allocation.target.channel_index == snapshot.channel_index && + allocation.target.kind == snapshot.target_kind && + allocation.target.address == snapshot.target_address; + }); + if (desired == next_plan.active.end() || desired->type != snapshot.endpoint_type) { + ESP_RETURN_ON_ERROR(removeBinding(index), kTag, "failed removing Matter endpoint"); + continue; + } + { + std::lock_guard guard(bindings_mutex_); + bindings_[index]->gateway_id = desired->target.gateway_id; + bindings_[index]->color_method = desired->color_method; + storeBinding(*bindings_[index]); + } + ++index; + } + + for (const auto& allocation : next_plan.active) { + if (findBinding(allocation.target) != nullptr) continue; + uint16_t type_mask = 0; + uint8_t initial_level = 0; + if (allocation.target.kind == MatterTargetKind::shortAddress) { + const auto state = dali_domain_.gatewayCache().addressState( + allocation.target.gateway_id, allocation.target.address); + type_mask = state.daliDeviceTypeMask.value_or(0); + initial_level = state.status.actualLevel.value_or(0); + } + const esp_err_t err = createBinding(allocation, type_mask, initial_level); + if (err != ESP_OK) { + std::lock_guard guard(bindings_mutex_); + endpoint_plan_ = next_plan; + return err; + } + } + { + std::lock_guard guard(bindings_mutex_); + endpoint_plan_ = next_plan; + } + return ESP_OK; } void GatewayMatterBridge::ScanTaskEntry(void* argument) { auto* bridge = static_cast(argument); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + ESP_LOGI(kTag, "Matter DALI scan task started stack=%lu bytes", + static_cast(bridge->config_.scan_task_stack_size)); bridge->scanDaliDevices(); + const UBaseType_t stack_low_water = uxTaskGetStackHighWaterMark(nullptr); + ESP_LOGI(kTag, "Matter DALI scan task completed stack_low_water=%u bytes", + static_cast(stack_low_water)); bridge->scan_task_ = nullptr; + bridge->scan_in_progress_.store(false, std::memory_order_release); vTaskDelete(nullptr); } +esp_err_t GatewayMatterBridge::rescanDaliDevices() { + if (!started_) return ESP_ERR_INVALID_STATE; + bool expected = false; + if (!scan_in_progress_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) { + return ESP_ERR_INVALID_STATE; + } + if (xTaskCreate(ScanTaskEntry, "matter_dali_scan", config_.scan_task_stack_size, this, + config_.scan_task_priority, &scan_task_) != pdPASS) { + scan_in_progress_.store(false, std::memory_order_release); + return ESP_ERR_NO_MEM; + } + xTaskNotifyGive(scan_task_); + return ESP_OK; +} + +esp_err_t GatewayMatterBridge::applyConfiguration(uint8_t gateway_id, + std::string_view patch) { + const auto channels = dali_domain_.channelInfo(); + const auto channel = std::find_if(channels.begin(), channels.end(), [gateway_id](const auto& item) { + return item.gateway_id == gateway_id; + }); + if (channel == channels.end()) return ESP_ERR_NOT_FOUND; + auto configuration = std::find_if( + configurations_.begin(), configurations_.end(), [channel](const auto& item) { + return item.channel_index == channel->channel_index; + }); + if (configuration == configurations_.end()) { + configurations_.push_back(DefaultConfiguration(channel->channel_index)); + configuration = configurations_.end() - 1; + } + cJSON* root = cJSON_ParseWithLength(patch.data(), patch.size()); + if (root == nullptr) return ESP_ERR_INVALID_ARG; + if (const cJSON* enabled = cJSON_GetObjectItemCaseSensitive( + root, "automaticShortAddresses"); cJSON_IsBool(enabled)) { + configuration->automatic_short_addresses = cJSON_IsTrue(enabled); + } + if (const cJSON* overrides = cJSON_GetObjectItemCaseSensitive( + root, "shortAddressOverrides"); cJSON_IsArray(overrides)) { + cJSON* item = nullptr; + cJSON_ArrayForEach(item, overrides) { + const cJSON* address = cJSON_GetObjectItemCaseSensitive(item, "address"); + const cJSON* mode = cJSON_GetObjectItemCaseSensitive(item, "mode"); + if (!cJSON_IsNumber(address) || address->valueint < 0 || address->valueint > 63 || + !cJSON_IsString(mode)) { + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + MatterAddressOverride parsed = MatterAddressOverride::automatic; + if (std::strcmp(mode->valuestring, "include") == 0) { + parsed = MatterAddressOverride::include; + } else if (std::strcmp(mode->valuestring, "exclude") == 0) { + parsed = MatterAddressOverride::exclude; + } else if (std::strcmp(mode->valuestring, "auto") != 0) { + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + configuration->short_address_overrides[address->valueint] = parsed; + } + } + const auto apply_endpoint_override = [](const cJSON* json, + MatterEndpointOverride* override) { + if (!cJSON_IsObject(json)) return; + if (const cJSON* type = cJSON_GetObjectItemCaseSensitive(json, "type"); + cJSON_IsString(type)) { + if (std::strcmp(type->valuestring, "auto") == 0) { + override->type.reset(); + } else { + override->type = ParseEndpointType(type->valuestring, + MatterEndpointType::switchLight); + } + } + if (const cJSON* method = cJSON_GetObjectItemCaseSensitive(json, "colorMethod"); + cJSON_IsString(method)) { + if (std::strcmp(method->valuestring, "auto") == 0) { + override->color_method.reset(); + } else { + override->color_method = ParseColorMethod(method->valuestring, + MatterColorMethod::both); + } + } + }; + apply_endpoint_override(cJSON_GetObjectItemCaseSensitive(root, "broadcast"), + &configuration->broadcast); + if (const cJSON* groups = cJSON_GetObjectItemCaseSensitive(root, "groups"); + cJSON_IsArray(groups)) { + cJSON* item = nullptr; + cJSON_ArrayForEach(item, groups) { + const cJSON* group = cJSON_GetObjectItemCaseSensitive(item, "group"); + if (!cJSON_IsNumber(group) || group->valueint < 0 || group->valueint > 15) { + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + apply_endpoint_override(item, &configuration->groups[group->valueint]); + } + } + cJSON_Delete(root); + ESP_RETURN_ON_ERROR(storeConfiguration(*configuration), kTag, + "failed to persist Matter configuration"); + const esp_err_t err = reconcileEndpoints(); + last_apply_error_ = err == ESP_OK ? std::string() : esp_err_to_name(err); + return err; +} + +esp_err_t GatewayMatterBridge::resetConfiguration(uint8_t gateway_id) { + const auto channels = dali_domain_.channelInfo(); + const auto channel = std::find_if(channels.begin(), channels.end(), [gateway_id](const auto& item) { + return item.gateway_id == gateway_id; + }); + if (channel == channels.end()) return ESP_ERR_NOT_FOUND; + const auto configuration = std::find_if( + configurations_.begin(), configurations_.end(), [channel](const auto& item) { + return item.channel_index == channel->channel_index; + }); + if (configuration == configurations_.end()) return ESP_ERR_NOT_FOUND; + *configuration = DefaultConfiguration(channel->channel_index); + ESP_RETURN_ON_ERROR(storeConfiguration(*configuration), kTag, + "failed to reset Matter configuration"); + const esp_err_t err = reconcileEndpoints(); + last_apply_error_ = err == ESP_OK ? std::string() : esp_err_to_name(err); + return err; +} + +std::string GatewayMatterBridge::statusJson(std::optional gateway_id) const { + bool commissioning_window_open = + commissioning_window_open_.load(std::memory_order_acquire); + uint8_t fabric_count = fabric_count_.load(std::memory_order_acquire); + if (started_ && chip::DeviceLayer::PlatformMgr().TryLockChipStack()) { + auto& server = chip::Server::GetInstance(); + commissioning_window_open = + server.GetCommissioningWindowManager().IsCommissioningWindowOpen(); + fabric_count = server.GetFabricTable().FabricCount(); + chip::DeviceLayer::PlatformMgr().UnlockChipStack(); + } + + cJSON* root = cJSON_CreateObject(); + cJSON* matter = cJSON_CreateObject(); + cJSON* endpoints = cJSON_CreateArray(); + if (root == nullptr || matter == nullptr || endpoints == nullptr) { + cJSON_Delete(root); + cJSON_Delete(matter); + cJSON_Delete(endpoints); + return R"({"matter":{"supported":true}})"; + } + cJSON_AddItemToObject(root, "matter", matter); + cJSON_AddBoolToObject(matter, "supported", true); + cJSON_AddNumberToObject(matter, "schemaVersion", kConfigurationVersion); + cJSON_AddBoolToObject(matter, "enabled", config_.enabled); + cJSON_AddBoolToObject(matter, "started", started_); + cJSON_AddBoolToObject(matter, "commissioningWindowOpen", + commissioning_window_open); + cJSON_AddBoolToObject(matter, "bleAdvertising", + ble_advertising_.load(std::memory_order_acquire)); + cJSON_AddBoolToObject(matter, "scanInProgress", + scan_in_progress_.load(std::memory_order_acquire)); + cJSON_AddNumberToObject(matter, "fabricCount", fabric_count); + cJSON_AddNumberToObject(matter, "maxBridgedDevices", + static_cast(config_.max_bridged_devices)); + cJSON_AddNumberToObject(matter, "globalCapacity", + static_cast(config_.max_bridged_devices)); + cJSON_AddNumberToObject(matter, "commissioningWindowSeconds", + kCommissioningWindowSeconds); + cJSON_AddBoolToObject(matter, "wifiProvisionButtonConfigured", + config_.wifi_provision_button_gpio >= 0); + cJSON_AddNumberToObject(matter, "wifiProvisionLongPressMs", + config_.wifi_provision_long_press_ms); + cJSON_AddStringToObject(matter, "storage", "plainNvs"); + cJSON_AddStringToObject(matter, "networkPreference", "ethernet"); + if (!last_apply_error_.empty()) { + cJSON_AddStringToObject(matter, "lastApplyError", last_apply_error_.c_str()); + } else { + cJSON_AddNullToObject(matter, "lastApplyError"); + } + + cJSON* saved_configurations = cJSON_CreateArray(); + cJSON* channel_candidates = cJSON_CreateArray(); + for (const auto& channel : dali_domain_.channelInfo()) { + if (gateway_id.has_value() && channel.gateway_id != *gateway_id) continue; + const auto configuration = std::find_if( + configurations_.begin(), configurations_.end(), [&channel](const auto& item) { + return item.channel_index == channel.channel_index; + }); + const MatterChannelConfiguration defaults = DefaultConfiguration(channel.channel_index); + const auto& selected = configuration == configurations_.end() ? defaults : *configuration; + cJSON* config_json = cJSON_CreateObject(); + cJSON_AddNumberToObject(config_json, "channelIndex", channel.channel_index); + cJSON_AddNumberToObject(config_json, "gatewayId", channel.gateway_id); + cJSON_AddBoolToObject(config_json, "automaticShortAddresses", + selected.automatic_short_addresses); + cJSON* direct_overrides = cJSON_CreateArray(); + for (uint8_t address = 0; address < 64; ++address) { + if (selected.short_address_overrides[address] == MatterAddressOverride::automatic) continue; + cJSON* item = cJSON_CreateObject(); + cJSON_AddNumberToObject(item, "address", address); + cJSON_AddStringToObject( + item, "mode", selected.short_address_overrides[address] == MatterAddressOverride::include + ? "include" : "exclude"); + cJSON_AddItemToArray(direct_overrides, item); + } + cJSON_AddItemToObject(config_json, "shortAddressOverrides", direct_overrides); + const auto add_override = [](cJSON* json, const MatterEndpointOverride& override) { + cJSON_AddStringToObject(json, "type", + override.type.has_value() + ? MatterEndpointTypeName(*override.type) : "auto"); + cJSON_AddStringToObject(json, "colorMethod", + override.color_method.has_value() + ? MatterColorMethodName(*override.color_method) : "auto"); + }; + cJSON* broadcast = cJSON_CreateObject(); + add_override(broadcast, selected.broadcast); + cJSON_AddItemToObject(config_json, "broadcast", broadcast); + cJSON* groups = cJSON_CreateArray(); + for (uint8_t group = 0; group < 16; ++group) { + if (!selected.groups[group].manual()) continue; + cJSON* item = cJSON_CreateObject(); + cJSON_AddNumberToObject(item, "group", group); + add_override(item, selected.groups[group]); + cJSON_AddItemToArray(groups, item); + } + cJSON_AddItemToObject(config_json, "groups", groups); + cJSON_AddItemToArray(saved_configurations, config_json); + + cJSON* candidates_json = cJSON_CreateObject(); + cJSON_AddNumberToObject(candidates_json, "channelIndex", channel.channel_index); + cJSON_AddNumberToObject(candidates_json, "gatewayId", channel.gateway_id); + cJSON* addresses = cJSON_CreateArray(); + const auto states = dali_domain_.gatewayCache().addressStates(channel.gateway_id); + for (uint8_t address = 0; address < 64; ++address) { + cJSON* item = cJSON_CreateObject(); + cJSON_AddNumberToObject(item, "address", address); + cJSON_AddBoolToObject(item, "online", + dali_domain_.gatewayCache().addressPresence( + channel.gateway_id, address) == DaliGatewayPresence::online); + cJSON_AddBoolToObject(item, "groupMaskKnown", states[address].groupMaskKnown); + cJSON_AddNumberToObject(item, "groupMask", states[address].groupMask); + if (states[address].daliDeviceTypeMask.has_value()) { + cJSON_AddNumberToObject(item, "daliDeviceTypeMask", + *states[address].daliDeviceTypeMask); + } + if (states[address].dt8ColorTypeFeatures.has_value()) { + cJSON_AddNumberToObject(item, "dt8ColorTypeFeatures", + *states[address].dt8ColorTypeFeatures); + } + cJSON_AddItemToArray(addresses, item); + } + cJSON_AddItemToObject(candidates_json, "addresses", addresses); + cJSON_AddItemToArray(channel_candidates, candidates_json); + } + cJSON_AddItemToObject(matter, "savedConfigurations", saved_configurations); + cJSON_AddItemToObject(matter, "channelCandidates", channel_candidates); + + { + std::lock_guard guard(bindings_mutex_); + cJSON_AddNumberToObject(matter, "bridgedDeviceCount", + static_cast(bindings_.size())); + for (const auto& binding : bindings_) { + if (gateway_id.has_value() && binding->gateway_id != *gateway_id) continue; + cJSON* endpoint = cJSON_CreateObject(); + if (endpoint == nullptr) continue; + cJSON_AddNumberToObject(endpoint, "endpointId", binding->endpoint_id); + cJSON_AddNumberToObject(endpoint, "channelIndex", binding->channel_index); + cJSON_AddNumberToObject(endpoint, "gatewayId", binding->gateway_id); + cJSON_AddStringToObject(endpoint, "targetKind", + MatterTargetKindName(binding->target_kind)); + cJSON_AddNumberToObject(endpoint, "targetAddress", binding->target_address); + if (binding->target_kind == MatterTargetKind::shortAddress) { + cJSON_AddNumberToObject(endpoint, "shortAddress", binding->target_address); + } + cJSON_AddNumberToObject(endpoint, "daliDeviceTypeMask", + binding->dali_device_type_mask); + cJSON_AddNumberToObject(endpoint, "matterDeviceType", + static_cast(binding->matter_device_type)); + cJSON_AddStringToObject(endpoint, "deviceType", + MatterDeviceTypeName(binding->matter_device_type)); + cJSON_AddStringToObject(endpoint, "endpointType", + MatterEndpointTypeName(binding->endpoint_type)); + if (binding->color_method.has_value()) { + cJSON_AddStringToObject(endpoint, "colorMethod", + MatterColorMethodName(*binding->color_method)); + } + const auto allocation = std::find_if( + endpoint_plan_.active.begin(), endpoint_plan_.active.end(), [&binding](const auto& item) { + return item.target.channel_index == binding->channel_index && + item.target.kind == binding->target_kind && + item.target.address == binding->target_address; + }); + const char* source = "auto"; + if (allocation != endpoint_plan_.active.end()) { + if (allocation->type_is_manual || allocation->color_method_is_manual) source = "manual"; + if (allocation->explicitly_included) source = "include"; + } + cJSON_AddStringToObject(endpoint, "source", source); + cJSON_AddItemToArray(endpoints, endpoint); + } + cJSON* dropped = cJSON_CreateArray(); + for (const auto& item : endpoint_plan_.dropped) { + if (gateway_id.has_value() && item.allocation.target.gateway_id != *gateway_id) continue; + cJSON* dropped_item = cJSON_CreateObject(); + cJSON_AddNumberToObject(dropped_item, "channelIndex", + item.allocation.target.channel_index); + cJSON_AddNumberToObject(dropped_item, "gatewayId", + item.allocation.target.gateway_id); + cJSON_AddStringToObject(dropped_item, "targetKind", + MatterTargetKindName(item.allocation.target.kind)); + cJSON_AddNumberToObject(dropped_item, "targetAddress", + item.allocation.target.address); + cJSON_AddStringToObject(dropped_item, "reason", item.reason.c_str()); + cJSON_AddItemToArray(dropped, dropped_item); + } + cJSON_AddItemToObject(matter, "droppedAllocations", dropped); + } + cJSON_AddItemToObject(matter, "endpoints", endpoints); + cJSON_AddItemReferenceToObject(matter, "activeAllocations", endpoints); + + char* printed = cJSON_PrintUnformatted(root); + std::string json = printed == nullptr ? R"({"matter":{"supported":true}})" + : std::string(printed); + cJSON_free(printed); + cJSON_Delete(root); + return json; +} + +std::string GatewayMatterBridge::onboardingJson() const { + char qr_buffer[chip::QRCodeBasicSetupPayloadGenerator::kMaxQRCodeBase38RepresentationLength + 1] = {}; + char manual_buffer[32] = {}; + char serial_buffer[64] = {}; + chip::MutableCharSpan qr_span(qr_buffer, sizeof(qr_buffer)); + chip::MutableCharSpan manual_span(manual_buffer, sizeof(manual_buffer)); + const auto flags = chip::RendezvousInformationFlags( + chip::RendezvousInformationFlag::kBLE); + const CHIP_ERROR qr_err = GetQRCode(qr_span, flags); + const CHIP_ERROR manual_err = GetManualPairingCode(manual_span, flags); + uint16_t vendor_id = 0; + uint16_t product_id = 0; + auto* provider = chip::DeviceLayer::GetDeviceInstanceInfoProvider(); + const CHIP_ERROR serial_err = provider == nullptr + ? CHIP_ERROR_INCORRECT_STATE + : provider->GetSerialNumber(serial_buffer, + sizeof(serial_buffer)); + if (provider != nullptr) { + provider->GetVendorId(vendor_id); + provider->GetProductId(product_id); + } + cJSON* root = cJSON_CreateObject(); + cJSON* onboarding = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "matterOnboarding", onboarding); + cJSON_AddBoolToObject(onboarding, "available", + qr_err == CHIP_NO_ERROR && manual_err == CHIP_NO_ERROR && + serial_err == CHIP_NO_ERROR); + cJSON_AddStringToObject(onboarding, "serialNumber", serial_buffer); + cJSON_AddNumberToObject(onboarding, "vendorId", vendor_id); + cJSON_AddNumberToObject(onboarding, "productId", product_id); + cJSON_AddStringToObject(onboarding, "qrPayload", qr_buffer); + cJSON_AddStringToObject(onboarding, "manualPairingCode", manual_buffer); + char* printed = cJSON_PrintUnformatted(root); + std::string json = printed == nullptr ? R"({"matterOnboarding":{"available":false}})" + : std::string(printed); + cJSON_free(printed); + cJSON_Delete(root); + return json; +} + GatewayMatterBridge::Binding* GatewayMatterBridge::findBinding( - uint8_t gateway_id, uint8_t short_address) const { + const MatterTarget& target) const { std::lock_guard guard(bindings_mutex_); const auto found = std::find_if(bindings_.begin(), bindings_.end(), - [gateway_id, short_address](const auto& binding) { - return binding->gateway_id == gateway_id && - binding->short_address == short_address; + [&target](const auto& binding) { + return binding->channel_index == target.channel_index && + binding->target_kind == target.kind && + binding->target_address == target.address; }); return found == bindings_.end() ? nullptr : found->get(); } @@ -356,6 +1128,10 @@ esp_err_t GatewayMatterBridge::AddDeviceType(esp_matter::endpoint_t* endpoint, esp_matter::endpoint::dimmable_light::config_t config; return esp_matter::endpoint::dimmable_light::add(endpoint, &config); } + case ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID: { + esp_matter::endpoint::color_temperature_light::config_t config; + return esp_matter::endpoint::color_temperature_light::add(endpoint, &config); + } case ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID: { esp_matter::endpoint::extended_color_light::config_t config; return esp_matter::endpoint::extended_color_light::add(endpoint, &config); @@ -387,29 +1163,63 @@ esp_err_t GatewayMatterBridge::handleAttributeUpdate( const esp_matter_attr_val_t& value) { using namespace chip::app::Clusters; bool accepted = true; + const int target = binding.target_address; + const MatterColorMethod method = binding.color_method.value_or(MatterColorMethod::both); if (cluster_id == OnOff::Id && attribute_id == OnOff::Attributes::OnOff::Id) { - accepted = value.val.b ? dali_domain_.on(binding.gateway_id, binding.short_address) - : dali_domain_.off(binding.gateway_id, binding.short_address); + accepted = value.val.b ? dali_domain_.on(binding.gateway_id, target) + : dali_domain_.off(binding.gateway_id, target); } else if (cluster_id == LevelControl::Id && attribute_id == LevelControl::Attributes::CurrentLevel::Id) { - accepted = dali_domain_.setBright(binding.gateway_id, binding.short_address, + accepted = dali_domain_.setBright(binding.gateway_id, target, std::min(value.val.u8, 254)); } else if (cluster_id == ColorControl::Id && attribute_id == ColorControl::Attributes::ColorTemperatureMireds::Id) { - accepted = dali_domain_.setColTempRaw(binding.gateway_id, binding.short_address, - value.val.u16); + const bool native_ok = dali_domain_.setColTempRaw(binding.gateway_id, target, + value.val.u16); + const int kelvin = value.val.u16 == 0 ? 6500 : 1000000 / value.val.u16; + const double cool_ratio = std::clamp((kelvin - 2700.0) / (6500.0 - 2700.0), + 0.0, 1.0); + const bool rgbcw_ok = dali_domain_.setColourRGBCW( + binding.gateway_id, target, 0, 0, 0, + static_cast(std::round(254.0 * cool_ratio)), + static_cast(std::round(254.0 * (1.0 - cool_ratio)))); + accepted = native_ok && rgbcw_ok; } else if (cluster_id == ColorControl::Id && attribute_id == ColorControl::Attributes::CurrentX::Id) { binding.current_x = value.val.u16; - accepted = dali_domain_.setColourRaw(binding.gateway_id, - binding.short_address * 2 + 1, - binding.current_x, binding.current_y); + bool xy_ok = true; + bool rgbcw_ok = true; + if (method != MatterColorMethod::rgbcw) { + xy_ok = dali_domain_.setColourXY(binding.gateway_id, target, + binding.current_x, binding.current_y); + } + if (method != MatterColorMethod::xy) { + int red = 0; + int green = 0; + int blue = 0; + XyToRgb(binding.current_x, binding.current_y, &red, &green, &blue); + rgbcw_ok = dali_domain_.setColourRGBCW(binding.gateway_id, target, + red, green, blue, 0, 0); + } + accepted = xy_ok && rgbcw_ok; } else if (cluster_id == ColorControl::Id && attribute_id == ColorControl::Attributes::CurrentY::Id) { binding.current_y = value.val.u16; - accepted = dali_domain_.setColourRaw(binding.gateway_id, - binding.short_address * 2 + 1, - binding.current_x, binding.current_y); + bool xy_ok = true; + bool rgbcw_ok = true; + if (method != MatterColorMethod::rgbcw) { + xy_ok = dali_domain_.setColourXY(binding.gateway_id, target, + binding.current_x, binding.current_y); + } + if (method != MatterColorMethod::xy) { + int red = 0; + int green = 0; + int blue = 0; + XyToRgb(binding.current_x, binding.current_y, &red, &green, &blue); + rgbcw_ok = dali_domain_.setColourRGBCW(binding.gateway_id, target, + red, green, blue, 0, 0); + } + accepted = xy_ok && rgbcw_ok; } else if (cluster_id == ColorControl::Id && (attribute_id == ColorControl::Attributes::CurrentHue::Id || attribute_id == ColorControl::Attributes::CurrentSaturation::Id)) { @@ -422,8 +1232,19 @@ esp_err_t GatewayMatterBridge::handleAttributeUpdate( int green = 0; int blue = 0; HsvToRgb(binding.current_hue, binding.current_saturation, &red, &green, &blue); - accepted = dali_domain_.setColourRGB(binding.gateway_id, binding.short_address, - red, green, blue); + bool xy_ok = true; + bool rgbcw_ok = true; + if (method != MatterColorMethod::rgbcw) { + uint16_t x = 0; + uint16_t y = 0; + RgbToXy(red, green, blue, &x, &y); + xy_ok = dali_domain_.setColourXY(binding.gateway_id, target, x, y); + } + if (method != MatterColorMethod::xy) { + rgbcw_ok = dali_domain_.setColourRGBCW(binding.gateway_id, target, + red, green, blue, 0, 0); + } + accepted = xy_ok && rgbcw_ok; } return accepted ? ESP_OK : ESP_FAIL; } @@ -433,8 +1254,9 @@ esp_err_t GatewayMatterBridge::Identify( uint8_t effect_id, uint8_t effect_variant, void* private_data) { auto* binding = static_cast(private_data); if (binding != nullptr) { - ESP_LOGI(kTag, "identify endpoint=%u gateway=%u short=%u type=%u effect=%u variant=%u", - endpoint_id, binding->gateway_id, binding->short_address, + ESP_LOGI(kTag, "identify endpoint=%u gateway=%u target=%s:%u type=%u effect=%u variant=%u", + endpoint_id, binding->gateway_id, + MatterTargetKindName(binding->target_kind), binding->target_address, static_cast(type), effect_id, effect_variant); } return ESP_OK; @@ -445,8 +1267,14 @@ void GatewayMatterBridge::handleStatusUpdate(const DaliGatewayStatusUpdate& upda *update.status.actualLevel > 254) { return; } - const auto schedule = [this, &update](uint8_t short_address) { - Binding* binding = findBinding(update.channel, short_address); + const auto channels = dali_domain_.channelInfo(); + const auto channel = std::find_if(channels.begin(), channels.end(), [&update](const auto& item) { + return item.gateway_id == update.channel; + }); + if (channel == channels.end()) return; + const auto schedule = [this, &update, &channel](MatterTargetKind kind, uint8_t address) { + Binding* binding = findBinding( + {channel->channel_index, update.channel, kind, address}); if (binding == nullptr) return; auto* work = new (std::nothrow) StatusWork{this, binding->endpoint_id, *update.status.actualLevel}; @@ -455,9 +1283,11 @@ void GatewayMatterBridge::handleStatusUpdate(const DaliGatewayStatusUpdate& upda ApplyStatusWork, reinterpret_cast(work)); }; if (update.target.kind == DaliGatewayTargetKind::shortAddress) { - schedule(update.target.value); + schedule(MatterTargetKind::shortAddress, update.target.value); + } else if (update.target.kind == DaliGatewayTargetKind::group) { + schedule(MatterTargetKind::group, static_cast(64 + update.target.value)); } else { - for (const uint8_t address : update.affectedShortAddresses) schedule(address); + schedule(MatterTargetKind::broadcast, 127); } } @@ -541,6 +1371,20 @@ void GatewayMatterBridge::requestWifiProvisioning() { OpenCommissioningWindowWork, reinterpret_cast(this)); } +esp_err_t GatewayMatterBridge::openCommissioningWindow() { + if (!started_) return ESP_ERR_INVALID_STATE; + chip::DeviceLayer::PlatformMgr().ScheduleWork( + OpenCommissioningWindowWork, reinterpret_cast(this)); + return ESP_OK; +} + +esp_err_t GatewayMatterBridge::closeCommissioningWindow() { + if (!started_) return ESP_ERR_INVALID_STATE; + chip::DeviceLayer::PlatformMgr().ScheduleWork( + CloseCommissioningWindowWork, reinterpret_cast(this)); + return ESP_OK; +} + void GatewayMatterBridge::OpenCommissioningWindowWork(intptr_t argument) { auto* bridge = reinterpret_cast(argument); if (bridge == nullptr || !bridge->started_) return; @@ -552,9 +1396,20 @@ void GatewayMatterBridge::OpenCommissioningWindowWork(intptr_t argument) { chip::CommissioningWindowAdvertisement::kAllSupported); if (err != CHIP_NO_ERROR) { ESP_LOGE(kTag, "failed to open commissioning window: %s", err.AsString()); + } else { + bridge->commissioning_window_open_.store(true, std::memory_order_release); } } +void GatewayMatterBridge::CloseCommissioningWindowWork(intptr_t argument) { + auto* bridge = reinterpret_cast(argument); + if (bridge == nullptr || !bridge->started_) return; + chip::Server::GetInstance() + .GetCommissioningWindowManager() + .CloseCommissioningWindow(); + bridge->commissioning_window_open_.store(false, std::memory_order_release); +} + void GatewayMatterBridge::MatterEvent( const chip::DeviceLayer::ChipDeviceEvent* event, intptr_t argument) { (void)argument; @@ -562,14 +1417,33 @@ void GatewayMatterBridge::MatterEvent( switch (event->Type) { case chip::DeviceLayer::DeviceEventType::kCommissioningComplete: ESP_LOGI(kTag, "Matter commissioning complete"); + if (bridge != nullptr) { + bridge->fabric_count_.store( + chip::Server::GetInstance().GetFabricTable().FabricCount(), + std::memory_order_release); + } break; case chip::DeviceLayer::DeviceEventType::kCommissioningWindowOpened: ESP_LOGI(kTag, "Matter commissioning window opened"); + if (bridge != nullptr) { + bridge->commissioning_window_open_.store(true, + std::memory_order_release); + } break; case chip::DeviceLayer::DeviceEventType::kCommissioningWindowClosed: ESP_LOGI(kTag, "Matter commissioning window closed"); + if (bridge != nullptr) { + bridge->commissioning_window_open_.store(false, + std::memory_order_release); + } break; case chip::DeviceLayer::DeviceEventType::kCHIPoBLEAdvertisingChange: + if (bridge != nullptr) { + bridge->ble_advertising_.store( + event->CHIPoBLEAdvertisingChange.Result == + chip::DeviceLayer::kActivity_Started, + std::memory_order_release); + } if (bridge != nullptr && bridge->config_.ble_advertising_changed_callback) { bridge->config_.ble_advertising_changed_callback( event->CHIPoBLEAdvertisingChange.Result == diff --git a/components/gateway_matter/src/gateway_matter_plan.cpp b/components/gateway_matter/src/gateway_matter_plan.cpp new file mode 100644 index 0000000..8a41298 --- /dev/null +++ b/components/gateway_matter/src/gateway_matter_plan.cpp @@ -0,0 +1,212 @@ +#include "gateway_matter_plan.hpp" + +#include + +namespace gateway { +namespace { + +constexpr uint16_t kDt8Mask = 1U << 8; +constexpr uint16_t kDt7Mask = 1U << 7; + +struct InferredCapability { + MatterEndpointType type{MatterEndpointType::switchLight}; + bool has_xy{false}; + bool has_rgbcw{false}; + bool usable{false}; +}; + +const MatterChannelConfiguration* FindConfiguration( + const std::vector& configurations, + uint8_t channel_index) { + const auto found = std::find_if( + configurations.begin(), configurations.end(), + [channel_index](const auto& item) { return item.channel_index == channel_index; }); + return found == configurations.end() ? nullptr : &*found; +} + +InferredCapability InferAddress(const MatterAddressCandidate& address) { + InferredCapability result; + if (!address.dali_device_type_mask.has_value()) return result; + result.usable = true; + const uint16_t mask = *address.dali_device_type_mask; + if ((mask & kDt8Mask) != 0) { + const uint8_t features = address.dt8_color_type_features.value_or(0); + result.has_xy = (features & 0x01) != 0; + result.has_rgbcw = ((features >> 5) & 0x07) != 0; + if (result.has_xy || result.has_rgbcw) { + result.type = MatterEndpointType::color; + } else if ((features & 0x02) != 0) { + result.type = MatterEndpointType::colorTemperature; + } else { + result.type = MatterEndpointType::dimmer; + } + } else if ((mask & ~kDt7Mask) != 0) { + result.type = MatterEndpointType::dimmer; + } + return result; +} + +MatterEndpointAllocation InferMultiTarget( + const MatterChannelCandidates& channel, MatterTargetKind kind, uint8_t address, + const MatterEndpointOverride& override) { + MatterEndpointAllocation allocation; + allocation.target = {channel.channel_index, channel.gateway_id, kind, address}; + bool any_member = false; + bool has_xy = false; + bool has_rgbcw = false; + for (const auto& member : channel.addresses) { + const uint8_t group = kind == MatterTargetKind::group + ? static_cast(address - 64) + : 0; + const bool included = kind == MatterTargetKind::broadcast || + (member.group_mask_known && + (member.group_mask & (1U << group)) != 0); + if (!included) continue; + any_member = true; + const auto inferred = InferAddress(member); + if (static_cast(inferred.type) > static_cast(allocation.type)) { + allocation.type = inferred.type; + } + has_xy = has_xy || inferred.has_xy; + has_rgbcw = has_rgbcw || inferred.has_rgbcw; + } + if (override.type.has_value()) { + allocation.type = *override.type; + allocation.type_is_manual = true; + } + if (allocation.type == MatterEndpointType::color) { + if (override.color_method.has_value()) { + allocation.color_method = *override.color_method; + allocation.color_method_is_manual = true; + } else if (has_xy && has_rgbcw) { + allocation.color_method = MatterColorMethod::both; + } else if (has_xy) { + allocation.color_method = MatterColorMethod::xy; + } else if (has_rgbcw) { + allocation.color_method = MatterColorMethod::rgbcw; + } else if (!any_member || override.manual()) { + allocation.color_method = MatterColorMethod::both; + } else { + allocation.color_method = MatterColorMethod::both; + } + } + return allocation; +} + +MatterEndpointAllocation InferShortAddress( + const MatterChannelCandidates& channel, uint8_t address, bool explicitly_included) { + MatterEndpointAllocation allocation; + allocation.target = {channel.channel_index, channel.gateway_id, + MatterTargetKind::shortAddress, address}; + allocation.explicitly_included = explicitly_included; + const auto inferred = InferAddress(channel.addresses[address]); + allocation.type = inferred.type; + if (inferred.type == MatterEndpointType::color) { + allocation.color_method = inferred.has_xy && inferred.has_rgbcw + ? MatterColorMethod::both + : (inferred.has_xy ? MatterColorMethod::xy + : MatterColorMethod::rgbcw); + } + return allocation; +} + +void AppendWithinCapacity(MatterEndpointPlan* plan, + const MatterEndpointAllocation& allocation) { + if (plan->active.size() < plan->capacity) { + plan->active.push_back(allocation); + } else { + plan->dropped.push_back({allocation, "capacity"}); + } +} + +} // namespace + +MatterEndpointPlan BuildMatterEndpointPlan( + const std::vector& raw_candidates, + const std::vector& configurations, + size_t capacity) { + auto candidates = raw_candidates; + std::sort(candidates.begin(), candidates.end(), [](const auto& a, const auto& b) { + return a.channel_index < b.channel_index; + }); + MatterEndpointPlan plan; + plan.capacity = capacity; + + // Broadcasts and retained groups are deliberately allocated across every + // channel before any short address can consume the shared pool. + for (const auto& channel : candidates) { + const auto* config = FindConfiguration(configurations, channel.channel_index); + const MatterEndpointOverride empty; + AppendWithinCapacity(&plan, InferMultiTarget( + channel, MatterTargetKind::broadcast, 127, + config == nullptr ? empty : config->broadcast)); + } + for (const auto& channel : candidates) { + const auto* config = FindConfiguration(configurations, channel.channel_index); + for (uint8_t group = 0; group < 16; ++group) { + const bool cached_non_empty = std::any_of( + channel.addresses.begin(), channel.addresses.end(), [group](const auto& address) { + return address.group_mask_known && (address.group_mask & (1U << group)) != 0; + }); + const MatterEndpointOverride empty; + const auto& override = config == nullptr ? empty : config->groups[group]; + if (!cached_non_empty && !override.manual()) continue; + AppendWithinCapacity(&plan, + InferMultiTarget(channel, MatterTargetKind::group, + static_cast(64 + group), override)); + } + } + for (const auto& channel : candidates) { + const auto* config = FindConfiguration(configurations, channel.channel_index); + if (config == nullptr) continue; + for (uint8_t address = 0; address < 64; ++address) { + if (config->short_address_overrides[address] == MatterAddressOverride::include) { + AppendWithinCapacity(&plan, InferShortAddress(channel, address, true)); + } + } + } + for (const auto& channel : candidates) { + const auto* config = FindConfiguration(configurations, channel.channel_index); + const bool automatic_enabled = config == nullptr || config->automatic_short_addresses; + if (!automatic_enabled) continue; + for (uint8_t address = 0; address < 64; ++address) { + const auto mode = config == nullptr ? MatterAddressOverride::automatic + : config->short_address_overrides[address]; + if (mode != MatterAddressOverride::automatic || !channel.addresses[address].online) { + continue; + } + AppendWithinCapacity(&plan, InferShortAddress(channel, address, false)); + } + } + return plan; +} + +const char* MatterTargetKindName(MatterTargetKind kind) { + switch (kind) { + case MatterTargetKind::shortAddress: return "shortAddress"; + case MatterTargetKind::group: return "group"; + case MatterTargetKind::broadcast: return "broadcast"; + } + return "shortAddress"; +} + +const char* MatterEndpointTypeName(MatterEndpointType type) { + switch (type) { + case MatterEndpointType::switchLight: return "switch"; + case MatterEndpointType::dimmer: return "dimmer"; + case MatterEndpointType::colorTemperature: return "colorTemperature"; + case MatterEndpointType::color: return "color"; + } + return "switch"; +} + +const char* MatterColorMethodName(MatterColorMethod method) { + switch (method) { + case MatterColorMethod::xy: return "xy"; + case MatterColorMethod::rgbcw: return "rgbcw"; + case MatterColorMethod::both: return "both"; + } + return "both"; +} + +} // namespace gateway diff --git a/components/gateway_matter/tests/gateway_matter_plan_test.cpp b/components/gateway_matter/tests/gateway_matter_plan_test.cpp new file mode 100644 index 0000000..080d015 --- /dev/null +++ b/components/gateway_matter/tests/gateway_matter_plan_test.cpp @@ -0,0 +1,72 @@ +#include "gateway_matter_plan.hpp" + +#include + +using namespace gateway; + +MatterChannelCandidates Channel(uint8_t index) { + MatterChannelCandidates channel; + channel.channel_index = index; + channel.gateway_id = static_cast(10 + index); + return channel; +} + +int main() { + { + auto channel = Channel(0); + channel.addresses[1].group_mask_known = true; + channel.addresses[1].group_mask = 1U << 2; + channel.addresses[1].dali_device_type_mask = 1U << 8; + channel.addresses[1].dt8_color_type_features = 0x23; + const auto plan = BuildMatterEndpointPlan({channel}, {}, 64); + assert(plan.active.size() == 2); + assert(plan.active[0].target.kind == MatterTargetKind::broadcast); + assert(plan.active[1].target.address == 66); + assert(plan.active[1].type == MatterEndpointType::color); + assert(plan.active[1].color_method == MatterColorMethod::both); + } + { + auto channel = Channel(0); + MatterChannelConfiguration config; + config.channel_index = 0; + config.groups[4].type = MatterEndpointType::color; + const auto plan = BuildMatterEndpointPlan({channel}, {config}, 64); + assert(plan.active.size() == 2); + assert(plan.active[1].target.address == 68); + assert(plan.active[1].type_is_manual); + assert(plan.active[1].color_method == MatterColorMethod::both); + } + { + auto channel = Channel(0); + channel.addresses[7].online = true; + MatterChannelConfiguration config; + config.channel_index = 0; + config.automatic_short_addresses = false; + config.short_address_overrides[9] = MatterAddressOverride::include; + const auto plan = BuildMatterEndpointPlan({channel}, {config}, 64); + assert(plan.active.size() == 2); + assert(plan.active[1].target.address == 9); + assert(plan.active[1].explicitly_included); + } + { + auto channel1 = Channel(1); + auto channel0 = Channel(0); + channel0.addresses[0].online = true; + channel1.addresses[0].online = true; + const auto plan = BuildMatterEndpointPlan({channel1, channel0}, {}, 64); + assert(plan.active[0].target.channel_index == 0); + assert(plan.active[1].target.channel_index == 1); + assert(plan.active[2].target.channel_index == 0); + assert(plan.active[3].target.channel_index == 1); + } + { + auto channel = Channel(0); + for (auto& address : channel.addresses) address.online = true; + const auto plan = BuildMatterEndpointPlan({channel}, {}, 32); + assert(plan.active.size() == 32); + assert(plan.dropped.size() == 33); + assert(plan.active.back().target.address == 30); + assert(plan.dropped.front().allocation.target.address == 31); + assert(plan.dropped.front().reason == "capacity"); + } +}