Add Matter DALI support and enhance IPv6 handling
This commit fix DALI feedback on matter bridge. - Introduced MatterLevelFeedback structure to manage DALI level feedback. - Implemented MapDaliLevelToMatter function to map DALI levels to Matter states. - Added MatterDaliAction and ResolveMatterDaliAction for handling DALI actions. - Created MatterColorTemperatureWritePlan and ResolveMatterColorTemperatureWrites for color temperature management. - Enhanced GatewayMatterBridge to handle DALI commands and transitions, including a new command task for processing DALI actions. - Implemented factory reset functionality for clearing Matter-related NVS namespaces. - Improved GatewayNetworkService to refresh Matter IPv6 addresses upon network events and ensure proper link-local address promotion. - Added tests for new functionalities in gateway_matter_plan_test.cpp to validate DALI level mapping and action resolution. Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -101,6 +101,33 @@ a GPIO allows a long press to start ESP-Touch SmartConfig and reopen the Matter
|
||||
commissioning window. The long-press duration and active level are configurable
|
||||
beside the GPIO in `main/Kconfig.projbuild`.
|
||||
|
||||
The gateway creates an IPv6 link-local address whenever Ethernet or Wi-Fi joins
|
||||
the link and enables IPv6 router-advertisement autoconfiguration. This is part
|
||||
of the custom network owner rather than ESP-Matter's network commissioning
|
||||
driver. Because the W5500 link can become usable after CHIP starts, the network
|
||||
owner waits for the preferred link-local address and re-announces it after
|
||||
CHIP's event handler is ready, and repeats that recovery after reconnects. The
|
||||
resulting `IP_EVENT_GOT_IP6` event lets Apple Home and other Matter controllers
|
||||
establish their normal operational CASE sessions after the commissioning
|
||||
connection closes.
|
||||
If a W5500 link leaves its deterministic MAC-derived link-local address in the
|
||||
tentative state, the refresh task allows normal duplicate-address detection to
|
||||
settle first and then promotes that address through ESP-IDF's public IPv6 API.
|
||||
This keeps the operational CASE step reachable instead of allowing BLE/PASE and
|
||||
NOC installation to succeed only for Apple Home to roll the temporary fabric
|
||||
back when operational discovery times out.
|
||||
|
||||
Matter attribute callbacks never wait for physical DALI frames. On/off, level,
|
||||
color-temperature, XY, and hue/saturation changes are coalesced by a dedicated
|
||||
internal-RAM command task, which keeps CHIP acknowledgements and subscription
|
||||
feedback responsive while a DALI transaction is in flight. Transition updates
|
||||
from `MoveToColorTemperature` are collapsed at command ingress: the Matter
|
||||
attribute transition and reports continue normally, while DALI receives the
|
||||
final physical target once instead of every intermediate step. Native DT8
|
||||
color-temperature endpoints send only the Tc sequence; the longer RGBWAF
|
||||
sequence is reserved for extended-color endpoints whose configured physical
|
||||
method requires it.
|
||||
|
||||
The network service accepts Lua-style raw gateway frames on UDP port `2020` and,
|
||||
when enabled, TCP port `2020`. It also accepts JSON control frames on the same
|
||||
ports:
|
||||
@@ -222,6 +249,12 @@ certificate SHA-256 before committing the DAC/private/public keys, PAI,
|
||||
Certification Declaration, SPAKE2+ data, and device identity. Its response reports
|
||||
`factoryData.restartRequired`; `matter_restart` schedules a delayed reboot so
|
||||
the transport can deliver its response before ESP-Matter reloads the providers.
|
||||
The confirmed `matter_factory_reset` action clears fabrics, operational
|
||||
sessions, subscriptions, Matter attributes/counters, ESP-Matter bridge records,
|
||||
and DaliMaster Matter endpoint bindings/configuration, then restarts. It erases
|
||||
only runtime namespaces and deliberately preserves `chip-factory`, including
|
||||
the DAC/private key, PAI, Certification Declaration, SPAKE2+ data, and device
|
||||
identity.
|
||||
These actions 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.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -607,6 +608,7 @@ std::unique_ptr<gateway::GatewayController> s_controller;
|
||||
std::unique_ptr<gateway::GatewayBridgeService> s_bridge;
|
||||
std::unique_ptr<gateway::GatewayNetworkService> s_network;
|
||||
std::unique_ptr<gateway::GatewayMatterBridge> s_matter_bridge;
|
||||
std::atomic_bool s_matter_factory_reset_scheduled{false};
|
||||
std::unique_ptr<gateway::GatewayBleBridge> s_ble_bridge;
|
||||
std::unique_ptr<gateway::Gateway485ControlBridge> s_uart0_control_bridge;
|
||||
std::unique_ptr<gateway::GatewayUsbSetupBridge> s_usb_setup_bridge;
|
||||
@@ -1393,6 +1395,30 @@ extern "C" void app_main(void) {
|
||||
err = s_matter_bridge->resetConfiguration(*gateway_id);
|
||||
} else if (action == "matter_factory_data") {
|
||||
err = s_matter_bridge->installFactoryData(body);
|
||||
} else if (action == "matter_factory_reset") {
|
||||
if (s_matter_factory_reset_scheduled.exchange(true)) {
|
||||
err = ESP_ERR_INVALID_STATE;
|
||||
} else {
|
||||
err = s_matter_bridge->prepareFactoryReset(body);
|
||||
if (err == ESP_OK) {
|
||||
const BaseType_t created = xTaskCreate(
|
||||
[](void*) {
|
||||
vTaskDelay(pdMS_TO_TICKS(750));
|
||||
const esp_err_t reset_err = esp_matter::factory_reset();
|
||||
if (reset_err != ESP_OK) {
|
||||
ESP_LOGE(kTag, "Matter factory reset failed: %s",
|
||||
esp_err_to_name(reset_err));
|
||||
s_matter_factory_reset_scheduled.store(false);
|
||||
}
|
||||
vTaskDelete(nullptr);
|
||||
},
|
||||
"matter_factory_reset", 4096, nullptr, 3, nullptr);
|
||||
if (created != pdPASS) err = ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
s_matter_factory_reset_scheduled.store(false);
|
||||
}
|
||||
}
|
||||
} else if (action == "matter_restart") {
|
||||
const BaseType_t created = xTaskCreate(
|
||||
[](void*) {
|
||||
@@ -1620,6 +1646,12 @@ extern "C" void app_main(void) {
|
||||
const esp_err_t matter_err = s_matter_bridge->start();
|
||||
LogHeapSnapshot("after Matter");
|
||||
ESP_ERROR_CHECK(matter_err);
|
||||
if (s_network != nullptr) {
|
||||
// gateway_network starts before CHIP and may not have a live link yet.
|
||||
// Wait for a preferred link-local address, then replay it after CHIP has
|
||||
// registered its event handler so operational Matter discovery starts.
|
||||
s_network->refreshMatterIpv6();
|
||||
}
|
||||
}
|
||||
|
||||
// ESP-Matter needs several contiguous internal allocations for the CHIP
|
||||
|
||||
@@ -2377,9 +2377,11 @@ CONFIG_LWIP_AUTOIP_MAX_CONFLICTS=9
|
||||
CONFIG_LWIP_AUTOIP_RATE_LIMIT_INTERVAL=20
|
||||
CONFIG_LWIP_IPV4=y
|
||||
CONFIG_LWIP_IPV6=y
|
||||
# CONFIG_LWIP_IPV6_AUTOCONFIG is not set
|
||||
CONFIG_LWIP_IPV6_AUTOCONFIG=y
|
||||
CONFIG_LWIP_IPV6_NUM_ADDRESSES=3
|
||||
# CONFIG_LWIP_IPV6_FORWARD is not set
|
||||
CONFIG_LWIP_IPV6_RDNSS_MAX_DNS_SERVERS=0
|
||||
# CONFIG_LWIP_IPV6_DHCP6 is not set
|
||||
CONFIG_LWIP_NETIF_STATUS_CALLBACK=y
|
||||
CONFIG_LWIP_NETIF_LOOPBACK=y
|
||||
CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8
|
||||
@@ -3116,7 +3118,7 @@ CONFIG_CHIP_ENABLE_PAIRING_AUTOSTART=y
|
||||
# General Options
|
||||
#
|
||||
CONFIG_CHIP_PROJECT_CONFIG=""
|
||||
CONFIG_CHIP_TASK_STACK_SIZE=8192
|
||||
CONFIG_CHIP_TASK_STACK_SIZE=16384
|
||||
CONFIG_CHIP_TASK_PRIORITY=1
|
||||
CONFIG_MAX_EVENT_QUEUE_SIZE=40
|
||||
# CONFIG_ENABLE_EXTENDED_DISCOVERY is not set
|
||||
|
||||
@@ -64,6 +64,8 @@ CONFIG_ESP_MATTER_AGGREGATOR_ENDPOINT_COUNT=1
|
||||
CONFIG_ESP_MATTER_NVS_PART_NAME="nvs"
|
||||
CONFIG_ESP_MATTER_BRIDGE_INFO_PART_NAME="nvs"
|
||||
CONFIG_CUSTOM_NETWORK_CONFIG=y
|
||||
CONFIG_LWIP_IPV6=y
|
||||
CONFIG_LWIP_IPV6_AUTOCONFIG=y
|
||||
# Matter uses the already-configured W5500 interface under CustomNetworkConfig.
|
||||
# gateway_network owns Wi-Fi station credentials and starts ESP-Touch
|
||||
# SmartConfig only after the button is held; CHIP must not initialize Wi-Fi.
|
||||
|
||||
@@ -4830,7 +4830,8 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
||||
if (action == "matter_open_commissioning" ||
|
||||
action == "matter_close_commissioning" || action == "matter_rescan" ||
|
||||
action == "matter_config" || action == "matter_config_reset" ||
|
||||
action == "matter_factory_data" || action == "matter_restart") {
|
||||
action == "matter_factory_data" || action == "matter_factory_reset" ||
|
||||
action == "matter_restart") {
|
||||
if (!config_.matter_action_handler) {
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "Matter control is not available");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "esp_matter_bridge.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "system/SystemLayer.h"
|
||||
|
||||
namespace gateway {
|
||||
|
||||
@@ -28,6 +29,8 @@ struct GatewayMatterBridgeConfig {
|
||||
uint32_t scan_address_delay_ms{20};
|
||||
uint32_t scan_task_stack_size{8192};
|
||||
UBaseType_t scan_task_priority{3};
|
||||
uint32_t command_task_stack_size{6144};
|
||||
UBaseType_t command_task_priority{4};
|
||||
int wifi_provision_button_gpio{-1};
|
||||
bool wifi_provision_button_active_low{true};
|
||||
uint32_t wifi_provision_long_press_ms{5000};
|
||||
@@ -53,11 +56,19 @@ class GatewayMatterBridge {
|
||||
esp_err_t rescanDaliDevices();
|
||||
esp_err_t applyConfiguration(uint8_t gateway_id, std::string_view patch);
|
||||
esp_err_t resetConfiguration(uint8_t gateway_id);
|
||||
esp_err_t prepareFactoryReset(std::string_view payload);
|
||||
esp_err_t installFactoryData(std::string_view payload);
|
||||
std::string statusJson(std::optional<uint8_t> gateway_id = std::nullopt) const;
|
||||
std::string onboardingJson() const;
|
||||
|
||||
private:
|
||||
enum class PendingColorAction : uint8_t {
|
||||
none = 0,
|
||||
temperature = 1,
|
||||
xy = 2,
|
||||
hueSaturation = 3,
|
||||
};
|
||||
|
||||
struct Binding {
|
||||
GatewayMatterBridge* owner{nullptr};
|
||||
uint8_t channel_index{0};
|
||||
@@ -71,8 +82,16 @@ class GatewayMatterBridge {
|
||||
uint16_t endpoint_id{0};
|
||||
uint16_t current_x{32768};
|
||||
uint16_t current_y{32768};
|
||||
uint8_t current_level{254};
|
||||
uint8_t current_hue{0};
|
||||
uint8_t current_saturation{0};
|
||||
bool current_on{true};
|
||||
std::optional<bool> pending_on;
|
||||
std::optional<uint8_t> pending_level;
|
||||
std::optional<uint16_t> pending_color_temperature;
|
||||
std::optional<uint16_t> color_temperature_transition_target;
|
||||
TickType_t color_temperature_transition_deadline{0};
|
||||
PendingColorAction pending_color_action{PendingColorAction::none};
|
||||
bool suppress_attribute_write{false};
|
||||
esp_matter_bridge::device_t* matter_device{nullptr};
|
||||
};
|
||||
@@ -86,11 +105,15 @@ class GatewayMatterBridge {
|
||||
static esp_err_t Identify(esp_matter::identification::callback_type_t type,
|
||||
uint16_t endpoint_id, uint8_t effect_id,
|
||||
uint8_t effect_variant, void* private_data);
|
||||
static esp_err_t MoveToColorTemperatureCommand(
|
||||
const chip::app::ConcreteCommandPath& command_path,
|
||||
chip::TLV::TLVReader& tlv_data, void* opaque_ptr);
|
||||
static esp_err_t AddDeviceType(esp_matter::endpoint_t* endpoint,
|
||||
uint32_t device_type_id, void* private_data);
|
||||
static void MatterEvent(const chip::DeviceLayer::ChipDeviceEvent* event,
|
||||
intptr_t argument);
|
||||
static void ScanTaskEntry(void* argument);
|
||||
static void CommandTaskEntry(void* argument);
|
||||
static void ButtonTaskEntry(void* argument);
|
||||
static void ApplyStatusWork(intptr_t argument);
|
||||
static void OpenCommissioningWindowWork(intptr_t argument);
|
||||
@@ -105,6 +128,7 @@ class GatewayMatterBridge {
|
||||
esp_err_t storeConfiguration(const MatterChannelConfiguration& configuration) const;
|
||||
esp_err_t configureProvisioningButton();
|
||||
void scanDaliDevices();
|
||||
void commandTaskLoop();
|
||||
esp_err_t reconcileEndpoints();
|
||||
void buttonTaskLoop();
|
||||
void handleStatusUpdate(const DaliGatewayStatusUpdate& update);
|
||||
@@ -112,10 +136,13 @@ class GatewayMatterBridge {
|
||||
Binding* findBinding(uint16_t endpoint_id) const;
|
||||
esp_err_t createBinding(const MatterEndpointAllocation& allocation,
|
||||
uint16_t device_type_mask, uint8_t initial_level = 0);
|
||||
void configureColorCommandCallbacks(Binding& binding);
|
||||
esp_err_t removeBinding(size_t index);
|
||||
esp_err_t storeBinding(const Binding& binding) const;
|
||||
std::unique_ptr<Binding> readBinding(uint16_t endpoint_id) const;
|
||||
void updateMatterLevel(Binding& binding, uint8_t level);
|
||||
esp_err_t scheduleDaliAction(Binding& binding);
|
||||
void applyPendingDaliActions();
|
||||
void requestWifiProvisioning();
|
||||
|
||||
DaliDomainService& dali_domain_;
|
||||
@@ -128,6 +155,7 @@ class GatewayMatterBridge {
|
||||
MatterEndpointPlan endpoint_plan_;
|
||||
std::string last_apply_error_;
|
||||
TaskHandle_t scan_task_{nullptr};
|
||||
TaskHandle_t command_task_{nullptr};
|
||||
TaskHandle_t button_task_{nullptr};
|
||||
bool started_{false};
|
||||
std::atomic_bool scan_in_progress_{false};
|
||||
|
||||
@@ -74,11 +74,48 @@ struct MatterEndpointPlan {
|
||||
std::vector<MatterDroppedAllocation> dropped;
|
||||
};
|
||||
|
||||
struct MatterLevelFeedback {
|
||||
bool on{false};
|
||||
std::optional<uint8_t> current_level;
|
||||
};
|
||||
|
||||
enum class MatterDaliActionKind : uint8_t { none = 0, off = 1, setLevel = 2 };
|
||||
|
||||
struct MatterDaliAction {
|
||||
MatterDaliActionKind kind{MatterDaliActionKind::none};
|
||||
uint8_t level{254};
|
||||
};
|
||||
|
||||
struct MatterColorTemperatureWritePlan {
|
||||
bool native_temperature{false};
|
||||
bool rgbcw{false};
|
||||
};
|
||||
|
||||
MatterEndpointPlan BuildMatterEndpointPlan(
|
||||
const std::vector<MatterChannelCandidates>& candidates,
|
||||
const std::vector<MatterChannelConfiguration>& configurations,
|
||||
size_t capacity);
|
||||
|
||||
// Matter CurrentLevel is nullable and constrained to 1..254. DALI level zero
|
||||
// therefore updates OnOff only and must retain the last nonzero Matter level.
|
||||
MatterLevelFeedback MapDaliLevelToMatter(uint8_t level);
|
||||
|
||||
// A single Matter command can update OnOff and CurrentLevel several times as
|
||||
// the standard level-control server applies its on/off effect. Resolve the
|
||||
// settled values to one DALI action so an Off command cannot finish with a
|
||||
// restored CurrentLevel frame that turns the light back on.
|
||||
MatterDaliAction ResolveMatterDaliAction(std::optional<bool> requested_on,
|
||||
std::optional<uint8_t> requested_level,
|
||||
bool settled_on,
|
||||
uint8_t retained_level);
|
||||
|
||||
// Temperature-only endpoints represent native DALI DT8 Tc gear and must not
|
||||
// also receive the much longer RGBWAF sequence. Extended-color endpoints keep
|
||||
// following their configured physical color method.
|
||||
MatterColorTemperatureWritePlan ResolveMatterColorTemperatureWrites(
|
||||
MatterEndpointType endpoint_type,
|
||||
std::optional<MatterColorMethod> color_method);
|
||||
|
||||
const char* MatterTargetKindName(MatterTargetKind kind);
|
||||
const char* MatterEndpointTypeName(MatterEndpointType type);
|
||||
const char* MatterColorMethodName(MatterColorMethod method);
|
||||
|
||||
@@ -39,6 +39,7 @@ constexpr char kFactoryNamespace[] = "chip-factory";
|
||||
constexpr char kFactoryProfile[] = "matter-test-paa";
|
||||
constexpr char kFactoryInstallConfirmation[] =
|
||||
"install-matter-test-factory-data";
|
||||
constexpr char kFactoryResetConfirmation[] = "reset-matter-runtime-data";
|
||||
constexpr char kFactoryPaiSha256[] =
|
||||
"27cf4e8cbf73f8fac2b9ab78b7d0fe59a10100b8e9a2df136a85b41a5d271bae";
|
||||
constexpr char kFactoryCertificationDeclarationSha256[] =
|
||||
@@ -52,6 +53,8 @@ constexpr uint8_t kBindingVersion = 2;
|
||||
constexpr uint8_t kConfigurationVersion = 1;
|
||||
constexpr uint16_t kAllDaliDeviceTypesMask = 0x01FF;
|
||||
constexpr uint32_t kCommissioningWindowSeconds = 300;
|
||||
constexpr uint32_t kMatterCommandSettleDelayMs = 15;
|
||||
constexpr uint32_t kMatterTransitionDeadlineMarginMs = 1000;
|
||||
GatewayMatterBridge* s_active_matter_bridge = nullptr;
|
||||
static_assert(MAX_BRIDGED_DEVICE_COUNT == 82,
|
||||
"DaliMaster Matter capacity expects 82 bridge slots");
|
||||
@@ -183,6 +186,17 @@ bool HasNvsBlob(nvs_handle_t handle, const char* key) {
|
||||
return nvs_get_blob(handle, key, nullptr, &length) == ESP_OK && length > 0;
|
||||
}
|
||||
|
||||
esp_err_t EraseNvsNamespace(const char* partition, const char* nvs_namespace) {
|
||||
nvs_handle_t handle = 0;
|
||||
esp_err_t err = nvs_open_from_partition(partition, nvs_namespace,
|
||||
NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_erase_all(handle);
|
||||
if (err == ESP_OK) err = nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
return err;
|
||||
}
|
||||
|
||||
std::string ReadNvsString(nvs_handle_t handle, const char* key) {
|
||||
size_t length = 0;
|
||||
if (nvs_get_str(handle, key, nullptr, &length) != ESP_OK || length <= 1) {
|
||||
@@ -427,11 +441,28 @@ esp_err_t GatewayMatterBridge::start() {
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)));
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (xTaskCreate(CommandTaskEntry, "matter_dali_cmd",
|
||||
config_.command_task_stack_size, this,
|
||||
config_.command_task_priority, &command_task_) != pdPASS) {
|
||||
vTaskDelete(scan_task_);
|
||||
scan_task_ = nullptr;
|
||||
scan_in_progress_.store(false, std::memory_order_release);
|
||||
ESP_LOGE(kTag,
|
||||
"failed to allocate Matter DALI command stack bytes=%u free=%u largest=%u",
|
||||
static_cast<unsigned>(config_.command_task_stack_size),
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL |
|
||||
MALLOC_CAP_8BIT)),
|
||||
static_cast<unsigned>(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;
|
||||
vTaskDelete(command_task_);
|
||||
command_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;
|
||||
@@ -443,6 +474,8 @@ esp_err_t GatewayMatterBridge::start() {
|
||||
if (button_err != ESP_OK) {
|
||||
vTaskDelete(scan_task_);
|
||||
scan_task_ = nullptr;
|
||||
vTaskDelete(command_task_);
|
||||
command_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));
|
||||
@@ -559,6 +592,7 @@ esp_err_t GatewayMatterBridge::resumeBindings() {
|
||||
}
|
||||
esp_matter::endpoint::enable(binding->matter_device->endpoint);
|
||||
binding->suppress_attribute_write = false;
|
||||
configureColorCommandCallbacks(*binding);
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bindings_mutex_);
|
||||
bindings_.push_back(std::move(binding));
|
||||
@@ -582,6 +616,10 @@ esp_err_t GatewayMatterBridge::createBinding(
|
||||
binding->color_method = allocation.color_method;
|
||||
binding->dali_device_type_mask = device_type_mask;
|
||||
binding->matter_device_type = MatterDeviceType(allocation.type);
|
||||
binding->current_on = initial_level > 0 && initial_level <= 254;
|
||||
if (binding->current_on) {
|
||||
binding->current_level = initial_level;
|
||||
}
|
||||
// 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
|
||||
@@ -605,6 +643,7 @@ esp_err_t GatewayMatterBridge::createBinding(
|
||||
}
|
||||
esp_matter::endpoint::enable(binding->matter_device->endpoint);
|
||||
binding->suppress_attribute_write = false;
|
||||
configureColorCommandCallbacks(*binding);
|
||||
Binding* created = binding.get();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bindings_mutex_);
|
||||
@@ -852,6 +891,24 @@ void GatewayMatterBridge::ScanTaskEntry(void* argument) {
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void GatewayMatterBridge::CommandTaskEntry(void* argument) {
|
||||
static_cast<GatewayMatterBridge*>(argument)->commandTaskLoop();
|
||||
}
|
||||
|
||||
void GatewayMatterBridge::commandTaskLoop() {
|
||||
ESP_LOGI(kTag, "Matter DALI command task started stack=%lu bytes",
|
||||
static_cast<unsigned long>(config_.command_task_stack_size));
|
||||
while (true) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
// Pair coupled attribute updates (for example X then Y) without adding
|
||||
// visible delay to individual on/off and level commands. Long Matter
|
||||
// transitions are collapsed at command ingress before reaching this task.
|
||||
while (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(kMatterCommandSettleDelayMs)) > 0) {
|
||||
}
|
||||
applyPendingDaliActions();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::rescanDaliDevices() {
|
||||
if (!started_) return ESP_ERR_INVALID_STATE;
|
||||
bool expected = false;
|
||||
@@ -975,6 +1032,33 @@ esp_err_t GatewayMatterBridge::resetConfiguration(uint8_t gateway_id) {
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::prepareFactoryReset(std::string_view payload) {
|
||||
cJSON* root = cJSON_ParseWithLength(payload.data(), payload.size());
|
||||
std::string confirmation;
|
||||
const bool confirmed =
|
||||
root != nullptr && cJSON_IsObject(root) &&
|
||||
JsonStringValue(root, "confirm", 1, 64, &confirmation) &&
|
||||
confirmation == kFactoryResetConfirmation;
|
||||
cJSON_Delete(root);
|
||||
if (!confirmed) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
// These namespaces contain DaliMaster's Matter endpoint plan and bridge
|
||||
// bindings. The standard ESP-Matter factory reset below owns fabrics,
|
||||
// sessions, attributes, counters, and its KVS. Never erase chip-factory:
|
||||
// it contains the permanent DAC/PAI/CD and commissionable data.
|
||||
ESP_RETURN_ON_ERROR(EraseNvsNamespace("nvs", kBindingNamespace), kTag,
|
||||
"failed to clear Matter DALI bindings");
|
||||
ESP_RETURN_ON_ERROR(EraseNvsNamespace("nvs", kConfigurationNamespace), kTag,
|
||||
"failed to clear Matter endpoint configuration");
|
||||
ESP_RETURN_ON_ERROR(EraseNvsNamespace(CONFIG_ESP_MATTER_NVS_PART_NAME, "node"),
|
||||
kTag, "failed to clear legacy Matter node state");
|
||||
ESP_RETURN_ON_ERROR(esp_matter_bridge::factory_reset(), kTag,
|
||||
"failed to clear ESP-Matter bridge state");
|
||||
ESP_LOGW(kTag,
|
||||
"Matter runtime reset prepared; chip-factory credentials preserved");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::installFactoryData(std::string_view payload) {
|
||||
cJSON* root = cJSON_ParseWithLength(payload.data(), payload.size());
|
||||
if (root == nullptr || !cJSON_IsObject(root)) {
|
||||
@@ -1466,68 +1550,121 @@ esp_err_t GatewayMatterBridge::AttributeUpdate(
|
||||
*value);
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::MoveToColorTemperatureCommand(
|
||||
const chip::app::ConcreteCommandPath& command_path,
|
||||
chip::TLV::TLVReader& tlv_data, void* opaque_ptr) {
|
||||
(void)opaque_ptr;
|
||||
chip::app::Clusters::ColorControl::Commands::MoveToColorTemperature::
|
||||
DecodableType command_data;
|
||||
if (command_data.Decode(tlv_data) != CHIP_NO_ERROR) {
|
||||
// Leave validation and the command response to the standard Matter
|
||||
// handler, which receives its own copy of the TLV reader after this hook.
|
||||
return ESP_OK;
|
||||
}
|
||||
auto* bridge = s_active_matter_bridge;
|
||||
if (bridge == nullptr) return ESP_OK;
|
||||
|
||||
std::lock_guard<std::mutex> guard(bridge->bindings_mutex_);
|
||||
const auto binding = std::find_if(
|
||||
bridge->bindings_.begin(), bridge->bindings_.end(),
|
||||
[&command_path](const auto& item) {
|
||||
return item->endpoint_id == command_path.mEndpointId;
|
||||
});
|
||||
if (binding == bridge->bindings_.end()) return ESP_OK;
|
||||
|
||||
Binding& target = **binding;
|
||||
target.color_temperature_transition_target =
|
||||
command_data.colorTemperatureMireds;
|
||||
const uint32_t transition_ms =
|
||||
static_cast<uint32_t>(command_data.transitionTime) * 100U;
|
||||
target.color_temperature_transition_deadline =
|
||||
xTaskGetTickCount() + pdMS_TO_TICKS(
|
||||
transition_ms +
|
||||
kMatterTransitionDeadlineMarginMs);
|
||||
target.pending_color_temperature = command_data.colorTemperatureMireds;
|
||||
target.pending_color_action = PendingColorAction::temperature;
|
||||
ESP_LOGI(kTag,
|
||||
"collapsed Matter color-temperature transition endpoint=%u target=%u "
|
||||
"transition_ms=%lu",
|
||||
target.endpoint_id, command_data.colorTemperatureMireds,
|
||||
static_cast<unsigned long>(transition_ms));
|
||||
return bridge->scheduleDaliAction(target);
|
||||
}
|
||||
|
||||
void GatewayMatterBridge::configureColorCommandCallbacks(Binding& binding) {
|
||||
using namespace chip::app::Clusters;
|
||||
if (binding.endpoint_type != MatterEndpointType::colorTemperature &&
|
||||
binding.endpoint_type != MatterEndpointType::color) {
|
||||
return;
|
||||
}
|
||||
esp_matter::command_t* command = esp_matter::command::get(
|
||||
binding.endpoint_id, ColorControl::Id,
|
||||
ColorControl::Commands::MoveToColorTemperature::Id);
|
||||
if (command == nullptr) {
|
||||
ESP_LOGW(kTag,
|
||||
"MoveToColorTemperature command unavailable endpoint=%u",
|
||||
binding.endpoint_id);
|
||||
return;
|
||||
}
|
||||
esp_matter::command::set_user_callback(
|
||||
command, MoveToColorTemperatureCommand);
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::handleAttributeUpdate(
|
||||
Binding& binding, uint32_t cluster_id, uint32_t attribute_id,
|
||||
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);
|
||||
std::lock_guard<std::mutex> guard(bindings_mutex_);
|
||||
if (cluster_id == OnOff::Id && attribute_id == OnOff::Attributes::OnOff::Id) {
|
||||
accepted = value.val.b ? dali_domain_.on(binding.gateway_id, target)
|
||||
: dali_domain_.off(binding.gateway_id, target);
|
||||
binding.current_on = value.val.b;
|
||||
binding.pending_on = value.val.b;
|
||||
return scheduleDaliAction(binding);
|
||||
} else if (cluster_id == LevelControl::Id &&
|
||||
attribute_id == LevelControl::Attributes::CurrentLevel::Id) {
|
||||
accepted = dali_domain_.setBright(binding.gateway_id, target,
|
||||
std::min<uint8_t>(value.val.u8, 254));
|
||||
if (value.type != ESP_MATTER_VAL_TYPE_UINT8 &&
|
||||
value.type != ESP_MATTER_VAL_TYPE_NULLABLE_UINT8) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (value.type == ESP_MATTER_VAL_TYPE_NULLABLE_UINT8 &&
|
||||
chip::app::NumericAttributeTraits<uint8_t>::IsNullValue(value.val.u8)) {
|
||||
return ESP_OK;
|
||||
}
|
||||
binding.current_level = std::clamp<uint8_t>(value.val.u8, 1, 254);
|
||||
binding.pending_level = binding.current_level;
|
||||
return scheduleDaliAction(binding);
|
||||
} else if (cluster_id == ColorControl::Id &&
|
||||
attribute_id == ColorControl::Attributes::ColorTemperatureMireds::Id) {
|
||||
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<int>(std::round(254.0 * cool_ratio)),
|
||||
static_cast<int>(std::round(254.0 * (1.0 - cool_ratio))));
|
||||
accepted = native_ok && rgbcw_ok;
|
||||
if (binding.color_temperature_transition_target.has_value()) {
|
||||
const uint16_t target = *binding.color_temperature_transition_target;
|
||||
if (value.val.u16 == target) {
|
||||
binding.color_temperature_transition_target.reset();
|
||||
return ESP_OK;
|
||||
}
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
if (static_cast<int32_t>(now -
|
||||
binding.color_temperature_transition_deadline) <
|
||||
0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGW(kTag,
|
||||
"Matter color-temperature transition timed out endpoint=%u "
|
||||
"target=%u current=%u",
|
||||
binding.endpoint_id, target, value.val.u16);
|
||||
binding.color_temperature_transition_target.reset();
|
||||
}
|
||||
binding.pending_color_temperature = value.val.u16;
|
||||
binding.pending_color_action = PendingColorAction::temperature;
|
||||
return scheduleDaliAction(binding);
|
||||
} else if (cluster_id == ColorControl::Id &&
|
||||
attribute_id == ColorControl::Attributes::CurrentX::Id) {
|
||||
binding.current_x = value.val.u16;
|
||||
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;
|
||||
binding.pending_color_action = PendingColorAction::xy;
|
||||
return scheduleDaliAction(binding);
|
||||
} else if (cluster_id == ColorControl::Id &&
|
||||
attribute_id == ColorControl::Attributes::CurrentY::Id) {
|
||||
binding.current_y = value.val.u16;
|
||||
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;
|
||||
binding.pending_color_action = PendingColorAction::xy;
|
||||
return scheduleDaliAction(binding);
|
||||
} else if (cluster_id == ColorControl::Id &&
|
||||
(attribute_id == ColorControl::Attributes::CurrentHue::Id ||
|
||||
attribute_id == ColorControl::Attributes::CurrentSaturation::Id)) {
|
||||
@@ -1536,25 +1673,148 @@ esp_err_t GatewayMatterBridge::handleAttributeUpdate(
|
||||
} else {
|
||||
binding.current_saturation = value.val.u8;
|
||||
}
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
HsvToRgb(binding.current_hue, binding.current_saturation, &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;
|
||||
binding.pending_color_action = PendingColorAction::hueSaturation;
|
||||
return scheduleDaliAction(binding);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::scheduleDaliAction(Binding& binding) {
|
||||
if (command_task_ == nullptr) {
|
||||
ESP_LOGW(kTag, "Matter DALI command task unavailable endpoint=%u",
|
||||
binding.endpoint_id);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
xTaskNotifyGive(command_task_);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void GatewayMatterBridge::applyPendingDaliActions() {
|
||||
struct Work {
|
||||
uint16_t endpoint_id;
|
||||
uint8_t gateway_id;
|
||||
MatterTargetKind target_kind;
|
||||
uint8_t target_address;
|
||||
MatterEndpointType endpoint_type;
|
||||
MatterColorMethod color_method;
|
||||
MatterDaliAction action;
|
||||
PendingColorAction color_action;
|
||||
uint16_t color_temperature;
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
uint8_t hue;
|
||||
uint8_t saturation;
|
||||
};
|
||||
std::vector<Work> work;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bindings_mutex_);
|
||||
work.reserve(bindings_.size());
|
||||
for (const auto& item : bindings_) {
|
||||
Binding& binding = *item;
|
||||
const MatterDaliAction action = ResolveMatterDaliAction(
|
||||
binding.pending_on, binding.pending_level, binding.current_on,
|
||||
binding.current_level);
|
||||
const PendingColorAction color_action = binding.pending_color_action;
|
||||
if (action.kind == MatterDaliActionKind::none &&
|
||||
color_action == PendingColorAction::none) {
|
||||
continue;
|
||||
}
|
||||
work.push_back({
|
||||
binding.endpoint_id,
|
||||
binding.gateway_id,
|
||||
binding.target_kind,
|
||||
binding.target_address,
|
||||
binding.endpoint_type,
|
||||
binding.color_method.value_or(MatterColorMethod::both),
|
||||
action,
|
||||
color_action,
|
||||
binding.pending_color_temperature.value_or(0),
|
||||
binding.current_x,
|
||||
binding.current_y,
|
||||
binding.current_hue,
|
||||
binding.current_saturation,
|
||||
});
|
||||
binding.pending_on.reset();
|
||||
binding.pending_level.reset();
|
||||
binding.pending_color_temperature.reset();
|
||||
binding.pending_color_action = PendingColorAction::none;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& item : work) {
|
||||
bool accepted = true;
|
||||
if (item.action.kind == MatterDaliActionKind::off) {
|
||||
accepted = dali_domain_.off(item.gateway_id, item.target_address);
|
||||
} else if (item.action.kind == MatterDaliActionKind::setLevel) {
|
||||
accepted = dali_domain_.setBright(item.gateway_id, item.target_address,
|
||||
item.action.level);
|
||||
}
|
||||
|
||||
if (item.color_action == PendingColorAction::temperature) {
|
||||
bool native_ok = true;
|
||||
bool rgbcw_ok = true;
|
||||
const auto plan = ResolveMatterColorTemperatureWrites(
|
||||
item.endpoint_type, item.color_method);
|
||||
if (plan.native_temperature) {
|
||||
native_ok = dali_domain_.setColTempRaw(
|
||||
item.gateway_id, item.target_address, item.color_temperature);
|
||||
}
|
||||
if (plan.rgbcw) {
|
||||
const int kelvin = item.color_temperature == 0
|
||||
? 6500
|
||||
: 1000000 / item.color_temperature;
|
||||
const double cool_ratio = std::clamp(
|
||||
(kelvin - 2700.0) / (6500.0 - 2700.0), 0.0, 1.0);
|
||||
rgbcw_ok = dali_domain_.setColourRGBCW(
|
||||
item.gateway_id, item.target_address, 0, 0, 0,
|
||||
static_cast<int>(std::round(254.0 * cool_ratio)),
|
||||
static_cast<int>(std::round(254.0 * (1.0 - cool_ratio))));
|
||||
}
|
||||
accepted = accepted && native_ok && rgbcw_ok;
|
||||
} else if (item.color_action == PendingColorAction::xy) {
|
||||
bool xy_ok = true;
|
||||
bool rgbcw_ok = true;
|
||||
if (item.color_method != MatterColorMethod::rgbcw) {
|
||||
xy_ok = dali_domain_.setColourXY(item.gateway_id, item.target_address,
|
||||
item.x, item.y);
|
||||
}
|
||||
if (item.color_method != MatterColorMethod::xy) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
XyToRgb(item.x, item.y, &red, &green, &blue);
|
||||
rgbcw_ok = dali_domain_.setColourRGBCW(
|
||||
item.gateway_id, item.target_address, red, green, blue, 0, 0);
|
||||
}
|
||||
accepted = accepted && xy_ok && rgbcw_ok;
|
||||
} else if (item.color_action == PendingColorAction::hueSaturation) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
HsvToRgb(item.hue, item.saturation, &red, &green, &blue);
|
||||
bool xy_ok = true;
|
||||
bool rgbcw_ok = true;
|
||||
if (item.color_method != MatterColorMethod::rgbcw) {
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
RgbToXy(red, green, blue, &x, &y);
|
||||
xy_ok = dali_domain_.setColourXY(item.gateway_id, item.target_address,
|
||||
x, y);
|
||||
}
|
||||
if (item.color_method != MatterColorMethod::xy) {
|
||||
rgbcw_ok = dali_domain_.setColourRGBCW(
|
||||
item.gateway_id, item.target_address, red, green, blue, 0, 0);
|
||||
}
|
||||
accepted = accepted && xy_ok && rgbcw_ok;
|
||||
}
|
||||
|
||||
if (!accepted) {
|
||||
ESP_LOGW(kTag,
|
||||
"deferred Matter command failed endpoint=%u gateway=%u target=%s:%u",
|
||||
item.endpoint_id, item.gateway_id,
|
||||
MatterTargetKindName(item.target_kind), item.target_address);
|
||||
}
|
||||
}
|
||||
return accepted ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t GatewayMatterBridge::Identify(
|
||||
@@ -1608,15 +1868,31 @@ void GatewayMatterBridge::ApplyStatusWork(intptr_t argument) {
|
||||
|
||||
void GatewayMatterBridge::updateMatterLevel(Binding& binding, uint8_t level) {
|
||||
using namespace chip::app::Clusters;
|
||||
const MatterLevelFeedback feedback = MapDaliLevelToMatter(level);
|
||||
std::lock_guard<std::mutex> guard(bindings_mutex_);
|
||||
binding.current_on = feedback.on;
|
||||
if (feedback.current_level.has_value()) {
|
||||
binding.current_level = *feedback.current_level;
|
||||
}
|
||||
binding.suppress_attribute_write = true;
|
||||
auto on = esp_matter_bool(level > 0);
|
||||
esp_matter::attribute::update(binding.endpoint_id, OnOff::Id,
|
||||
OnOff::Attributes::OnOff::Id, &on);
|
||||
auto on = esp_matter_bool(feedback.on);
|
||||
const esp_err_t on_err = esp_matter::attribute::update(
|
||||
binding.endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &on);
|
||||
if (on_err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to mirror DALI on/off endpoint=%u: %s",
|
||||
binding.endpoint_id, esp_err_to_name(on_err));
|
||||
}
|
||||
if (binding.matter_device_type != ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID) {
|
||||
auto current_level = esp_matter_uint8(std::min<uint8_t>(level, 254));
|
||||
esp_matter::attribute::update(binding.endpoint_id, LevelControl::Id,
|
||||
LevelControl::Attributes::CurrentLevel::Id,
|
||||
¤t_level);
|
||||
auto current_level =
|
||||
esp_matter_nullable_uint8(nullable<uint8_t>(binding.current_level));
|
||||
const esp_err_t level_err = esp_matter::attribute::update(
|
||||
binding.endpoint_id, LevelControl::Id,
|
||||
LevelControl::Attributes::CurrentLevel::Id, ¤t_level);
|
||||
if (level_err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to mirror DALI level endpoint=%u level=%u: %s",
|
||||
binding.endpoint_id, binding.current_level,
|
||||
esp_err_to_name(level_err));
|
||||
}
|
||||
}
|
||||
binding.suppress_attribute_write = false;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,42 @@ MatterEndpointPlan BuildMatterEndpointPlan(
|
||||
return plan;
|
||||
}
|
||||
|
||||
MatterLevelFeedback MapDaliLevelToMatter(uint8_t level) {
|
||||
MatterLevelFeedback feedback;
|
||||
feedback.on = level > 0 && level <= 254;
|
||||
if (feedback.on) feedback.current_level = level;
|
||||
return feedback;
|
||||
}
|
||||
|
||||
MatterDaliAction ResolveMatterDaliAction(std::optional<bool> requested_on,
|
||||
std::optional<uint8_t> requested_level,
|
||||
bool settled_on,
|
||||
uint8_t retained_level) {
|
||||
if (requested_on.has_value() && !*requested_on) {
|
||||
return {MatterDaliActionKind::off, retained_level};
|
||||
}
|
||||
if (requested_on.has_value() && *requested_on) {
|
||||
return {MatterDaliActionKind::setLevel,
|
||||
requested_level.value_or(retained_level)};
|
||||
}
|
||||
if (requested_level.has_value() && settled_on) {
|
||||
return {MatterDaliActionKind::setLevel, *requested_level};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
MatterColorTemperatureWritePlan ResolveMatterColorTemperatureWrites(
|
||||
MatterEndpointType endpoint_type,
|
||||
std::optional<MatterColorMethod> color_method) {
|
||||
if (endpoint_type == MatterEndpointType::colorTemperature) {
|
||||
return {true, false};
|
||||
}
|
||||
const MatterColorMethod method =
|
||||
color_method.value_or(MatterColorMethod::both);
|
||||
return {method != MatterColorMethod::rgbcw,
|
||||
method != MatterColorMethod::xy};
|
||||
}
|
||||
|
||||
const char* MatterTargetKindName(MatterTargetKind kind) {
|
||||
switch (kind) {
|
||||
case MatterTargetKind::shortAddress: return "shortAddress";
|
||||
|
||||
@@ -69,4 +69,52 @@ int main() {
|
||||
assert(plan.dropped.front().allocation.target.address == 31);
|
||||
assert(plan.dropped.front().reason == "capacity");
|
||||
}
|
||||
{
|
||||
const auto off = MapDaliLevelToMatter(0);
|
||||
assert(!off.on);
|
||||
assert(!off.current_level.has_value());
|
||||
const auto on = MapDaliLevelToMatter(137);
|
||||
assert(on.on);
|
||||
assert(on.current_level == 137);
|
||||
const auto invalid = MapDaliLevelToMatter(255);
|
||||
assert(!invalid.on);
|
||||
assert(!invalid.current_level.has_value());
|
||||
}
|
||||
{
|
||||
// Matter's instantaneous Off effect visits level 1, switches OnOff off,
|
||||
// then restores CurrentLevel. The settled action must still be only OFF.
|
||||
const auto off = ResolveMatterDaliAction(false, 254, false, 254);
|
||||
assert(off.kind == MatterDaliActionKind::off);
|
||||
const auto on = ResolveMatterDaliAction(true, 254, true, 254);
|
||||
assert(on.kind == MatterDaliActionKind::setLevel);
|
||||
assert(on.level == 254);
|
||||
const auto level_while_off =
|
||||
ResolveMatterDaliAction(std::nullopt, 137, false, 137);
|
||||
assert(level_while_off.kind == MatterDaliActionKind::none);
|
||||
const auto level_while_on =
|
||||
ResolveMatterDaliAction(std::nullopt, 137, true, 137);
|
||||
assert(level_while_on.kind == MatterDaliActionKind::setLevel);
|
||||
assert(level_while_on.level == 137);
|
||||
}
|
||||
{
|
||||
const auto temperature_only = ResolveMatterColorTemperatureWrites(
|
||||
MatterEndpointType::colorTemperature, std::nullopt);
|
||||
assert(temperature_only.native_temperature);
|
||||
assert(!temperature_only.rgbcw);
|
||||
|
||||
const auto xy = ResolveMatterColorTemperatureWrites(
|
||||
MatterEndpointType::color, MatterColorMethod::xy);
|
||||
assert(xy.native_temperature);
|
||||
assert(!xy.rgbcw);
|
||||
|
||||
const auto rgbcw = ResolveMatterColorTemperatureWrites(
|
||||
MatterEndpointType::color, MatterColorMethod::rgbcw);
|
||||
assert(!rgbcw.native_temperature);
|
||||
assert(rgbcw.rgbcw);
|
||||
|
||||
const auto both = ResolveMatterColorTemperatureWrites(
|
||||
MatterEndpointType::color, MatterColorMethod::both);
|
||||
assert(both.native_temperature);
|
||||
assert(both.rgbcw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -92,6 +93,10 @@ class GatewayNetworkService {
|
||||
// Enables the opt-in Wi-Fi provisioning path used by physical adapters such
|
||||
// as the Matter commissioning button. Ethernet remains the preferred route.
|
||||
esp_err_t startWifiProvisioning();
|
||||
// Re-announces IPv6 after the Matter server has registered its ESP event
|
||||
// handler. The network service starts first, so the initial link event may
|
||||
// otherwise predate CHIP startup.
|
||||
void refreshMatterIpv6();
|
||||
// Physical adapter for the Part 103 IDENTIFY DEVICE action. The DALI core
|
||||
// remains hardware-independent and invokes this only through the app wiring.
|
||||
void identify();
|
||||
@@ -107,6 +112,7 @@ class GatewayNetworkService {
|
||||
static void TcpControlTaskEntry(void* arg);
|
||||
static void BootButtonTaskEntry(void* arg);
|
||||
static void IdentifyTaskEntry(void* arg);
|
||||
static void MatterIpv6RefreshTaskEntry(void* arg);
|
||||
static esp_err_t HandleInfoGet(httpd_req_t* req);
|
||||
static esp_err_t HandleCommandGet(httpd_req_t* req);
|
||||
static esp_err_t HandleCommandPost(httpd_req_t* req);
|
||||
@@ -144,6 +150,10 @@ class GatewayNetworkService {
|
||||
void tcpControlTaskLoop();
|
||||
void bootButtonTaskLoop();
|
||||
void identifyTaskLoop();
|
||||
void matterIpv6RefreshTaskLoop();
|
||||
void scheduleMatterIpv6Refresh();
|
||||
esp_err_t promoteMatterIpv6LinkLocal(esp_netif_t* netif,
|
||||
const char* interface_name);
|
||||
void handleNetworkControlBytes(const uint8_t* data, size_t len);
|
||||
std::optional<std::string> handleJsonControlFrame(const uint8_t* data, size_t len);
|
||||
bool enqueueControlFrameForTargets(const std::vector<uint8_t>& frame);
|
||||
@@ -160,6 +170,7 @@ class GatewayNetworkService {
|
||||
void handleEspNowReceive(const esp_now_recv_info_t* info, const uint8_t* data, int data_len);
|
||||
void handleSetupUartFrame(int setup_id, const std::vector<uint8_t>& frame);
|
||||
void handleDaliRawFrame(const DaliRawFrame& frame);
|
||||
esp_err_t ensureMatterIpv6(esp_netif_t* netif, const char* interface_name);
|
||||
std::string deviceInfoJson() const;
|
||||
std::string deviceInfoDoubleEncodedJson() const;
|
||||
std::string gatewaySnapshotJson();
|
||||
@@ -196,6 +207,10 @@ class GatewayNetworkService {
|
||||
TaskHandle_t identify_task_handle_{nullptr};
|
||||
TaskHandle_t udp_task_handle_{nullptr};
|
||||
TaskHandle_t tcp_control_task_handle_{nullptr};
|
||||
std::atomic_bool matter_event_handlers_ready_{false};
|
||||
std::atomic_bool matter_ipv6_refresh_running_{false};
|
||||
std::atomic_bool ethernet_link_up_{false};
|
||||
std::atomic_bool wifi_station_connected_{false};
|
||||
int udp_socket_{-1};
|
||||
int tcp_control_socket_{-1};
|
||||
int tcp_control_client_socket_{-1};
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if CONFIG_GATEWAY_MATTER_SUPPORTED && \
|
||||
(!CONFIG_LWIP_IPV6 || !CONFIG_LWIP_IPV6_AUTOCONFIG)
|
||||
#error "Matter gateway builds require IPv6 and IPv6 autoconfiguration"
|
||||
#endif
|
||||
|
||||
namespace gateway {
|
||||
|
||||
namespace {
|
||||
@@ -40,6 +45,11 @@ constexpr const char* kSetupApSsid = "LAMMIN_Gateway";
|
||||
constexpr size_t kUdpBufferSize = 1024;
|
||||
constexpr size_t kTcpControlBufferSize = 512;
|
||||
constexpr uint8_t kEspNowBroadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
#ifdef CONFIG_GATEWAY_MATTER_SUPPORTED
|
||||
constexpr TickType_t kMatterIpv6RefreshInterval = pdMS_TO_TICKS(500);
|
||||
constexpr int kMatterIpv6RefreshAttempts = 150;
|
||||
constexpr int kMatterIpv6PromotionChecks = 3;
|
||||
#endif
|
||||
|
||||
GatewayNetworkService* s_espnow_service = nullptr;
|
||||
|
||||
@@ -491,6 +501,168 @@ esp_err_t GatewayNetworkService::startWifiProvisioning() {
|
||||
return startSmartconfig();
|
||||
}
|
||||
|
||||
void GatewayNetworkService::refreshMatterIpv6() {
|
||||
matter_event_handlers_ready_.store(true, std::memory_order_release);
|
||||
scheduleMatterIpv6Refresh();
|
||||
}
|
||||
|
||||
void GatewayNetworkService::scheduleMatterIpv6Refresh() {
|
||||
#ifndef CONFIG_GATEWAY_MATTER_SUPPORTED
|
||||
return;
|
||||
#else
|
||||
if (!matter_event_handlers_ready_.load(std::memory_order_acquire) ||
|
||||
matter_ipv6_refresh_running_.exchange(true, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
const BaseType_t created =
|
||||
xTaskCreate(MatterIpv6RefreshTaskEntry, "matter_ipv6", 3072, this, 3,
|
||||
nullptr);
|
||||
if (created != pdPASS) {
|
||||
matter_ipv6_refresh_running_.store(false, std::memory_order_release);
|
||||
ESP_LOGE(kTag, "failed to start Matter IPv6 refresh task");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void GatewayNetworkService::MatterIpv6RefreshTaskEntry(void* arg) {
|
||||
static_cast<GatewayNetworkService*>(arg)->matterIpv6RefreshTaskLoop();
|
||||
}
|
||||
|
||||
void GatewayNetworkService::matterIpv6RefreshTaskLoop() {
|
||||
#ifdef CONFIG_GATEWAY_MATTER_SUPPORTED
|
||||
bool ethernet_requested = false;
|
||||
bool wifi_requested = false;
|
||||
bool ethernet_promoted = false;
|
||||
bool wifi_promoted = false;
|
||||
int ethernet_pending_checks = 0;
|
||||
int wifi_pending_checks = 0;
|
||||
for (int attempt = 0; attempt < kMatterIpv6RefreshAttempts; ++attempt) {
|
||||
const auto announce = [this](esp_netif_t* netif,
|
||||
const char* interface_name,
|
||||
bool* requested,
|
||||
bool* promoted,
|
||||
int* pending_checks) {
|
||||
if (!*requested) {
|
||||
const esp_err_t err = ensureMatterIpv6(netif, interface_name);
|
||||
if (err != ESP_OK) return false;
|
||||
*requested = true;
|
||||
*pending_checks = 0;
|
||||
ESP_LOGI(kTag, "requested %s Matter IPv6 link-local address",
|
||||
interface_name);
|
||||
}
|
||||
|
||||
esp_ip6_addr_t address{};
|
||||
if (esp_netif_get_ip6_linklocal(netif, &address) != ESP_OK) {
|
||||
++*pending_checks;
|
||||
if (*pending_checks >= kMatterIpv6PromotionChecks && !*promoted) {
|
||||
const esp_err_t err =
|
||||
promoteMatterIpv6LinkLocal(netif, interface_name);
|
||||
if (err == ESP_OK) *promoted = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ip_event_got_ip6_t event{};
|
||||
event.esp_netif = netif;
|
||||
event.ip_index = 0;
|
||||
event.ip6_info.ip = address;
|
||||
const esp_err_t err = esp_event_post(
|
||||
IP_EVENT, IP_EVENT_GOT_IP6, &event, sizeof(event),
|
||||
kMatterIpv6RefreshInterval);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to announce %s Matter IPv6 address: %s",
|
||||
interface_name, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(kTag, "announced %s Matter IPv6 " IPV6STR,
|
||||
interface_name, IPV62STR(address));
|
||||
return true;
|
||||
};
|
||||
|
||||
const bool ethernet_connected =
|
||||
ethernet_link_up_.load(std::memory_order_acquire);
|
||||
const bool wifi_connected =
|
||||
wifi_station_connected_.load(std::memory_order_acquire);
|
||||
bool ethernet_ready = !ethernet_connected;
|
||||
bool wifi_ready = !wifi_connected;
|
||||
if (ethernet_connected) {
|
||||
ethernet_ready = announce(eth_netif_, "Ethernet", ðernet_requested,
|
||||
ðernet_promoted,
|
||||
ðernet_pending_checks);
|
||||
} else {
|
||||
ethernet_requested = false;
|
||||
ethernet_promoted = false;
|
||||
ethernet_pending_checks = 0;
|
||||
}
|
||||
if (wifi_connected) {
|
||||
wifi_ready = announce(wifi_sta_netif_, "Wi-Fi", &wifi_requested,
|
||||
&wifi_promoted, &wifi_pending_checks);
|
||||
} else {
|
||||
wifi_requested = false;
|
||||
wifi_promoted = false;
|
||||
wifi_pending_checks = 0;
|
||||
}
|
||||
if ((ethernet_connected || wifi_connected) && ethernet_ready && wifi_ready) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(kMatterIpv6RefreshInterval);
|
||||
}
|
||||
#endif
|
||||
matter_ipv6_refresh_running_.store(false, std::memory_order_release);
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
esp_err_t GatewayNetworkService::promoteMatterIpv6LinkLocal(
|
||||
esp_netif_t* netif, const char* interface_name) {
|
||||
uint8_t mac[6] = {};
|
||||
esp_err_t err = esp_netif_get_mac(netif, mac);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to read %s MAC for Matter IPv6: %s",
|
||||
interface_name, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
char address_text[48] = {};
|
||||
std::snprintf(address_text, sizeof(address_text),
|
||||
"fe80::%02x%02x:%02xff:fe%02x:%02x%02x",
|
||||
static_cast<unsigned>(mac[0] ^ 0x02),
|
||||
static_cast<unsigned>(mac[1]),
|
||||
static_cast<unsigned>(mac[2]),
|
||||
static_cast<unsigned>(mac[3]),
|
||||
static_cast<unsigned>(mac[4]),
|
||||
static_cast<unsigned>(mac[5]));
|
||||
esp_ip6_addr_t address{};
|
||||
err = esp_netif_str_to_ip6(address_text, &address);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to parse %s Matter IPv6 address %s: %s",
|
||||
interface_name, address_text, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_netif_add_ip6_address(netif, address, true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to promote %s Matter IPv6 address %s: %s",
|
||||
interface_name, address_text, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
ESP_LOGW(kTag,
|
||||
"%s Matter IPv6 DAD did not settle; promoted MAC-derived address %s",
|
||||
interface_name, address_text);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t GatewayNetworkService::ensureMatterIpv6(
|
||||
esp_netif_t* netif, const char* interface_name) {
|
||||
if (netif == nullptr || !esp_netif_is_netif_up(netif)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
const esp_err_t err = esp_netif_create_ip6_linklocal(netif);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to create %s Matter IPv6 address: %s",
|
||||
interface_name, esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
void GatewayNetworkService::identify() {
|
||||
if (config_.status_led_gpio < 0) {
|
||||
ESP_LOGI(kTag, "Part 103 identity requested; no status LED is configured");
|
||||
@@ -556,6 +728,18 @@ esp_err_t GatewayNetworkService::startEthernet() {
|
||||
stopEthernet();
|
||||
return err;
|
||||
}
|
||||
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6,
|
||||
&GatewayNetworkService::HandleEthernetEvent, this);
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(kTag, "failed to register Ethernet IPv6 event handler: %s",
|
||||
esp_err_to_name(err));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
IP_EVENT, IP_EVENT_ETH_GOT_IP, &GatewayNetworkService::HandleEthernetEvent));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
ETH_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleEthernetEvent));
|
||||
stopEthernet();
|
||||
return err;
|
||||
}
|
||||
ethernet_event_handlers_registered_ = true;
|
||||
}
|
||||
|
||||
@@ -709,6 +893,8 @@ esp_err_t GatewayNetworkService::probeEthernetStartup() {
|
||||
|
||||
void GatewayNetworkService::stopEthernet() {
|
||||
if (ethernet_event_handlers_registered_) {
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
IP_EVENT, IP_EVENT_GOT_IP6, &GatewayNetworkService::HandleEthernetEvent));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
IP_EVENT, IP_EVENT_ETH_GOT_IP, &GatewayNetworkService::HandleEthernetEvent));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
@@ -802,6 +988,17 @@ esp_err_t GatewayNetworkService::startWifi() {
|
||||
ESP_LOGE(kTag, "failed to register IP event handler: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6,
|
||||
&GatewayNetworkService::HandleWifiEvent, this);
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(kTag, "failed to register Wi-Fi IPv6 event handler: %s",
|
||||
esp_err_to_name(err));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
IP_EVENT, IP_EVENT_STA_GOT_IP, &GatewayNetworkService::HandleWifiEvent));
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleWifiEvent));
|
||||
return err;
|
||||
}
|
||||
wifi_event_handlers_registered_ = true;
|
||||
}
|
||||
|
||||
@@ -1279,6 +1476,7 @@ void GatewayNetworkService::handleEthernetEvent(esp_event_base_t event_base, int
|
||||
}
|
||||
|
||||
if (event_id == ETHERNET_EVENT_CONNECTED) {
|
||||
ethernet_link_up_.store(true, std::memory_order_release);
|
||||
uint8_t mac[6] = {};
|
||||
if (handle != nullptr && esp_eth_ioctl(handle, ETH_CMD_G_MAC_ADDR, mac) == ESP_OK) {
|
||||
const std::string mac_hex = MacToHex(mac);
|
||||
@@ -1289,9 +1487,16 @@ void GatewayNetworkService::handleEthernetEvent(esp_event_base_t event_base, int
|
||||
} else {
|
||||
ESP_LOGI(kTag, "Ethernet link up");
|
||||
}
|
||||
ensureMatterIpv6(eth_netif_, "Ethernet");
|
||||
scheduleMatterIpv6Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_id == ETHERNET_EVENT_DISCONNECTED ||
|
||||
event_id == ETHERNET_EVENT_STOP) {
|
||||
ethernet_link_up_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
if (event_id == ETHERNET_EVENT_DISCONNECTED) {
|
||||
runtime_.clearEthernetIp();
|
||||
selectPreferredDefaultNetwork();
|
||||
@@ -1325,6 +1530,15 @@ void GatewayNetworkService::handleEthernetEvent(esp_event_base_t event_base, int
|
||||
runtime_.setEthernetInfo(std::move(info));
|
||||
selectPreferredDefaultNetwork();
|
||||
ESP_LOGI(kTag, "Ethernet got IP %s", ip);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6 &&
|
||||
event_data != nullptr) {
|
||||
auto* event = static_cast<ip_event_got_ip6_t*>(event_data);
|
||||
if (event->esp_netif != eth_netif_) return;
|
||||
ESP_LOGI(kTag, "Ethernet got Matter IPv6 " IPV6STR,
|
||||
IPV62STR(event->ip6_info.ip));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,7 +1592,15 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) {
|
||||
wifi_station_connected_.store(true, std::memory_order_release);
|
||||
ensureMatterIpv6(wifi_sta_netif_, "Wi-Fi");
|
||||
scheduleMatterIpv6Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
wifi_station_connected_.store(false, std::memory_order_release);
|
||||
auto info = runtime_.deviceInfo();
|
||||
if (info.wlan.has_value()) {
|
||||
const bool has_credentials = !info.wlan->ssid.empty();
|
||||
@@ -1405,6 +1627,15 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t
|
||||
runtime_.setWirelessInfo(std::move(wireless));
|
||||
selectPreferredDefaultNetwork();
|
||||
ESP_LOGI(kTag, "Wi-Fi got IP %s", ip);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6 &&
|
||||
event_data != nullptr) {
|
||||
auto* event = static_cast<ip_event_got_ip6_t*>(event_data);
|
||||
if (event->esp_netif != wifi_sta_netif_) return;
|
||||
ESP_LOGI(kTag, "Wi-Fi got Matter IPv6 " IPV6STR,
|
||||
IPV62STR(event->ip6_info.ip));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user