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:
Tony
2026-08-09 07:06:20 +08:00
parent ca9e95e985
commit 2d55d7f9d1
12 changed files with 816 additions and 75 deletions
+33
View File
@@ -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.
+32
View File
@@ -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
+4 -2
View File
@@ -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
+2
View File
@@ -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);
+348 -72
View File
@@ -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,
&current_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, &current_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", &ethernet_requested,
&ethernet_promoted,
&ethernet_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));
}
}