diff --git a/libraries/Matter/examples/matter_lightbulb_binding/matter_lightbulb_binding.ino b/libraries/Matter/examples/matter_lightbulb_binding/matter_lightbulb_binding.ino new file mode 100644 index 00000000..acbcdfe5 --- /dev/null +++ b/libraries/Matter/examples/matter_lightbulb_binding/matter_lightbulb_binding.ino @@ -0,0 +1,91 @@ +/* + Matter lightbulb example - Binding target + + Companion example to matter_switch_binding: a plain on/off Matter lightbulb + meant to be bound to a matter_switch_binding device so it can be toggled + directly by the switch, without a hub relaying every button press. + + This device itself needs no special code for binding - a Matter Binding + only requires configuration on the initiating side (the switch's Binding + cluster + logic in MatterSwitch::beginWithBinding()). This lightbulb is a + plain OnOff server, identical to the matter_lightbulb example; it simply + accepts On/Off/Toggle commands from whichever node is authorized to send + them - which includes a bound switch once your Matter controller has: + 1. Commissioned this device AND a matter_switch_binding device onto the + same Matter fabric. + 2. Created the binding from the switch's endpoint to this device's + endpoint (see matter_switch_binding.ino for how). + 3. Granted the switch's node the "Operate" ACL privilege on this + device's OnOff cluster - most controllers set this up automatically + as part of creating the binding. + + The device has to be commissioned to a Matter hub first. + + Compatible boards: + - Arduino Nano Matter + - SparkFun Thing Plus MGM240P + - xG24 Explorer Kit + - xG24 Dev Kit + - Seeed Studio XIAO MG24 (Sense) + + Author: Silicon Labs + */ +#include +#include + +MatterLightbulb matter_bulb; + +void setup() +{ + Serial.begin(115200); + Matter.begin(); + matter_bulb.begin(); + + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE); + + Serial.println("Matter lightbulb - binding target"); + + if (!Matter.isDeviceCommissioned()) { + Serial.println("Matter device is not commissioned"); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\n", Matter.getOnboardingQRCodeUrl().c_str()); + } + while (!Matter.isDeviceCommissioned()) { + delay(200); + } + + Serial.println("Waiting for Thread network..."); + while (!Matter.isDeviceThreadConnected()) { + delay(200); + } + Serial.println("Connected to Thread network"); + + Serial.println("Waiting for Matter device discovery..."); + while (!matter_bulb.is_online()) { + delay(200); + } + Serial.println("Matter device is now online"); + Serial.println("Bind a matter_switch_binding device to this endpoint's OnOff cluster from your Matter controller"); +} + +void loop() +{ + static bool matter_lightbulb_last_state = false; + bool matter_lightbulb_current_state = matter_bulb.get_onoff(); + + // If the current state is ON and the previous was OFF - turn on the LED + if (matter_lightbulb_current_state && !matter_lightbulb_last_state) { + matter_lightbulb_last_state = matter_lightbulb_current_state; + digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE); + Serial.println("Bulb ON"); + } + + // If the current state is OFF and the previous was ON - turn off the LED + if (!matter_lightbulb_current_state && matter_lightbulb_last_state) { + matter_lightbulb_last_state = matter_lightbulb_current_state; + digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE); + Serial.println("Bulb OFF"); + } +} diff --git a/libraries/Matter/examples/matter_switch_binding/matter_switch_binding.ino b/libraries/Matter/examples/matter_switch_binding/matter_switch_binding.ino new file mode 100644 index 00000000..442715d8 --- /dev/null +++ b/libraries/Matter/examples/matter_switch_binding/matter_switch_binding.ino @@ -0,0 +1,132 @@ +/* + Matter switch with Binding cluster example + + The example shows how to create a Matter momentary switch that uses the + standard Matter Binding cluster to control a bound light directly, without + a hub relaying every button press (see MatterSwitch::beginWithBinding()). + + The device has to be commissioned to a Matter hub/controller first, same as + the plain matter_switch example. Once commissioned: + 1. Commission this device AND a matter_lightbulb_binding device (the + companion example - a plain lightbulb, no special code needed on its + side) onto the same Matter fabric. + 2. Create a binding from this switch's endpoint to the light's endpoint. + This isn't done from Home Assistant's own UI - use the underlying + Matter Server's dashboard (matterjs-server, the component behind HA's + Matter integration) to add the binding, or another controller with + binding support such as chip-tool. + 3. Make sure the light grants this switch's node the "Operate" ACL + privilege on its OnOff cluster - most controllers set this up + automatically as part of creating the binding. + + Once bound, pressing the on-board button toggles the bound light directly, + switch-to-light, without going through the controller for that command. + + Compatible boards: + - Arduino Nano Matter + - SparkFun Thing Plus MGM240P + - xG24 Explorer Kit + - xG24 Dev Kit + - Seeed Studio XIAO MG24 (Sense) + + Author: Silicon Labs + */ +#include +#include + +MatterSwitch matter_switch; + +void handle_button_press(); +void handle_button_release(); +volatile bool button_pressed = false; + +void setup() +{ + Serial.begin(115200); + Matter.begin(); + matter_switch.beginWithBinding(); + + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE); + + // Set up the onboard button + #ifndef BTN_BUILTIN + #define BTN_BUILTIN PA0 + #endif + pinMode(BTN_BUILTIN, INPUT_PULLUP); + attachInterrupt(BTN_BUILTIN, &handle_button_press, FALLING); + attachInterrupt(BTN_BUILTIN, &handle_button_release, RISING); + + Serial.println("Matter switch with binding"); + + if (!Matter.isDeviceCommissioned()) { + Serial.println("Matter device is not commissioned"); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\n", Matter.getOnboardingQRCodeUrl().c_str()); + } + while (!Matter.isDeviceCommissioned()) { + delay(200); + } + + Serial.println("Waiting for Thread network..."); + while (!Matter.isDeviceThreadConnected()) { + delay(200); + } + Serial.println("Connected to Thread network"); + + Serial.println("Waiting for Matter device discovery..."); + while (!matter_switch.is_online()) { + delay(200); + } + Serial.println("Matter device is now online"); + Serial.println("Use your Matter controller to bind this switch to a light, then press the button to toggle it directly"); +} + +void loop() +{ + // If the physical button state changes - update the switch's state + static bool button_pressed_last = false; + if (button_pressed != button_pressed_last) { + button_pressed_last = button_pressed; + matter_switch.set_state(button_pressed); + } + + // Get the current state of the Matter switch + static bool switch_last_state = false; + bool switch_current_state = matter_switch.get_state(); + + // If the current state is 'pressed' and the previous was 'not pressed' - switch pressed + if (switch_current_state && !switch_last_state) { + switch_last_state = switch_current_state; + digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE); + Serial.println("Switch pressed - toggling bound light(s)"); + } + + // If the current state is 'not pressed' and the previous was 'pressed' - switch released + if (!switch_current_state && switch_last_state) { + switch_last_state = switch_current_state; + digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE); + Serial.println("Switch released"); + } +} + +void handle_button_press() +{ + static uint32_t btn_last_press = 0; + if (millis() < btn_last_press + 200) { + return; + } + btn_last_press = millis(); + button_pressed = true; +} + +void handle_button_release() +{ + static uint32_t btn_last_press = 0; + if (millis() < btn_last_press + 200) { + return; + } + btn_last_press = millis(); + button_pressed = false; +} diff --git a/libraries/Matter/src/MatterBindingManager.cpp b/libraries/Matter/src/MatterBindingManager.cpp new file mode 100644 index 00000000..76d3a006 --- /dev/null +++ b/libraries/Matter/src/MatterBindingManager.cpp @@ -0,0 +1,323 @@ +/* + * This file is part of the Silicon Labs Arduino Core + * + * The MIT License (MIT) + * + * Copyright 2026 Silicon Laboratories Inc. www.silabs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "MatterBindingManager.h" +#include +#include +#include +#include +#include + +using namespace ::chip; +using namespace ::chip::app; +using namespace ::chip::app::Clusters; + +namespace { +// Builds and sends the OnOff command identified by commandId through `send`, +// which forwards it either to Controller::InvokeCommandRequest (unicast) or +// Controller::InvokeGroupCommandRequest (group) - the two call sites only +// differ in how the resulting command object is transmitted. +template +void SendOnOffCommand(CommandId commandId, SendFn&& send) +{ + switch (commandId) { + case OnOff::Commands::Toggle::Id: { + OnOff::Commands::Toggle::Type command; + send(command); + break; + } + case OnOff::Commands::On::Id: { + OnOff::Commands::On::Type command; + send(command); + break; + } + case OnOff::Commands::Off::Id: { + OnOff::Commands::Off::Type command; + send(command); + break; + } + default: + ChipLogError(DeviceLayer, "Matter binding: unsupported bound command 0x%08lx", static_cast(commandId)); + break; + } +} + +// Owns the pair of CHIP session-establishment callbacks for one outstanding +// unicast bound-command dispatch. Heap-allocated per dispatch (rather than +// reused as a manager member) because several bindings can be in flight for +// the same local endpoint at once, each needing its own callback identity and +// its own copy of "which remote endpoint/command this is for". +struct PendingCommand { + PendingCommand(EndpointId remote, CommandId command) : + remote_endpoint(remote), + command_id(command) + { + ; + } + + EndpointId remote_endpoint; + CommandId command_id; + Callback::Callback on_connected{ &MatterBindingManager::handle_device_connected, this }; + Callback::Callback on_failure{ &MatterBindingManager::handle_device_connection_failure, this }; +}; +} // namespace + +/***************************************************************************//** + * Constructor for MatterBindingAttributeAccess + ******************************************************************************/ +MatterBindingAttributeAccess::MatterBindingAttributeAccess(EndpointId endpoint) : + AttributeAccessInterface(MakeOptional(endpoint), Binding::Id) +{ + ; +} + +/***************************************************************************//** + * Serves the Binding cluster's `Binding` list attribute + ******************************************************************************/ +CHIP_ERROR MatterBindingAttributeAccess::Read(const ConcreteReadAttributePath& path, AttributeValueEncoder& encoder) +{ + if (path.mAttributeId != Binding::Attributes::Binding::Id) { + return CHIP_NO_ERROR; // Let ember handle every other attribute of this cluster + } + + EndpointId local_endpoint = path.mEndpointId; + return encoder.EncodeList([local_endpoint](const auto& listEncoder) -> CHIP_ERROR { + for (auto& entry : chip::BindingTable::GetInstance()) { + if (entry.local != local_endpoint) { + continue; + } + + Binding::Structs::TargetStruct::Type value; + value.fabricIndex = entry.fabricIndex; + value.cluster = FromStdOptional(entry.clusterId); + + if (entry.type == MATTER_UNICAST_BINDING) { + value.node = MakeOptional(entry.nodeId); + value.group = NullOptional; + value.endpoint = MakeOptional(entry.remote); + } else if (entry.type == MATTER_MULTICAST_BINDING) { + value.node = NullOptional; + value.group = MakeOptional(entry.groupId); + value.endpoint = NullOptional; + } else { + continue; + } + + ReturnErrorOnFailure(listEncoder.Encode(value)); + } + return CHIP_NO_ERROR; + }); +} + +/***************************************************************************//** + * Handles writes to the Binding cluster's `Binding` list attribute - a + * controller (e.g. Home Assistant, chip-tool) uses this to configure which + * remote node/endpoint or group this endpoint's commands should be relayed to. + ******************************************************************************/ +CHIP_ERROR MatterBindingAttributeAccess::Write(const ConcreteDataAttributePath& path, AttributeValueDecoder& decoder) +{ + if (path.mAttributeId != Binding::Attributes::Binding::Id) { + return CHIP_NO_ERROR; // Let ember handle every other attribute of this cluster + } + + auto add_entry = [](EndpointId local_endpoint, const Binding::Structs::TargetStruct::Type& target) -> CHIP_ERROR { + EmberBindingTableEntry entry; + if (target.group.HasValue()) { + entry = EmberBindingTableEntry::ForGroup(target.fabricIndex, target.group.Value(), local_endpoint, + target.cluster.std_optional()); + } else if (target.node.HasValue() && target.endpoint.HasValue()) { + entry = EmberBindingTableEntry::ForNode(target.fabricIndex, target.node.Value(), local_endpoint, target.endpoint.Value(), + target.cluster.std_optional()); + } else { + return CHIP_IM_GLOBAL_STATUS(ConstraintError); + } + return chip::BindingTable::GetInstance().Add(entry); + }; + + if (!path.IsListOperation() || path.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll) { + Binding::Attributes::Binding::TypeInfo::DecodableType new_list; + ReturnErrorOnFailure(decoder.Decode(new_list)); + + // Replace this endpoint's entries in their entirety. + auto& table = chip::BindingTable::GetInstance(); + auto iter = table.begin(); + while (iter != table.end()) { + if (iter->local == path.mEndpointId) { + ReturnErrorOnFailure(table.RemoveAt(iter)); + } else { + ++iter; + } + } + + auto list_iter = new_list.begin(); + while (list_iter.Next()) { + ReturnErrorOnFailure(add_entry(path.mEndpointId, list_iter.GetValue())); + } + return list_iter.GetStatus(); + } + + if (path.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem) { + Binding::Structs::TargetStruct::DecodableType item; + ReturnErrorOnFailure(decoder.Decode(item)); + return add_entry(path.mEndpointId, item); + } + + return CHIP_IM_GLOBAL_STATUS(UnsupportedWrite); +} + +/***************************************************************************//** + * Constructor for MatterBindingManager + ******************************************************************************/ +MatterBindingManager::MatterBindingManager() : + initialized(false) +{ + ; +} + +/***************************************************************************//** + * Returns the MatterBindingManager singleton + ******************************************************************************/ +MatterBindingManager& MatterBindingManager::instance() +{ + static MatterBindingManager sInstance; + return sInstance; +} + +/***************************************************************************//** + * Loads the persistent binding table once the Matter server is running + ******************************************************************************/ +void MatterBindingManager::begin() +{ + if (this->initialized) { + return; + } + this->initialized = true; + + // The binding table's persistent storage backend is only available once the + // Matter server has started - defer to the CHIP event queue so this runs + // after Matter.begin() has finished bringing the server up. + PlatformMgr().ScheduleWork([](intptr_t) { + chip::Server& server = chip::Server::GetInstance(); + chip::BindingTable::GetInstance().SetPersistentStorage(&server.GetPersistentStorage()); + CHIP_ERROR err = chip::BindingTable::GetInstance().LoadFromStorage(); + if (err != CHIP_NO_ERROR) { + ChipLogError(DeviceLayer, "Matter binding: failed to load the binding table: %" CHIP_ERROR_FORMAT, err.Format()); + } + }, 0); +} + +/***************************************************************************//** + * Dispatches commandId of clusterId to every binding table entry registered + * against localEndpoint + ******************************************************************************/ +void MatterBindingManager::notify_bound_cluster_changed(EndpointId local_endpoint, ClusterId cluster_id, CommandId command_id) +{ + for (auto& entry : chip::BindingTable::GetInstance()) { + if (entry.local != local_endpoint) { + continue; + } + if (entry.clusterId.has_value() && entry.clusterId.value() != cluster_id) { + continue; + } + + if (entry.type == MATTER_UNICAST_BINDING) { + this->dispatch_unicast(entry, command_id); + } else if (entry.type == MATTER_MULTICAST_BINDING) { + this->dispatch_group(entry, command_id); + } + } +} + +/***************************************************************************//** + * Establishes (or reuses) a CASE session to a unicast binding target, then + * sends the bound command once connected + ******************************************************************************/ +void MatterBindingManager::dispatch_unicast(const EmberBindingTableEntry& entry, CommandId command_id) +{ + PendingCommand* pending = new (std::nothrow) PendingCommand(entry.remote, command_id); + if (pending == nullptr) { + ChipLogError(DeviceLayer, "Matter binding: out of memory dispatching bound command"); + return; + } + + chip::Server::GetInstance().GetCASESessionManager()->FindOrEstablishSession( + ScopedNodeId(entry.nodeId, entry.fabricIndex), + &pending->on_connected, + &pending->on_failure); +} + +/***************************************************************************//** + * Sends the bound command to every member of a multicast (group) binding + * target - no session establishment needed for group messaging + ******************************************************************************/ +void MatterBindingManager::dispatch_group(const EmberBindingTableEntry& entry, CommandId command_id) +{ + Messaging::ExchangeManager& exchange_mgr = chip::Server::GetInstance().GetExchangeManager(); + SendOnOffCommand(command_id, [&](auto& command) { + CHIP_ERROR err = Controller::InvokeGroupCommandRequest(&exchange_mgr, entry.fabricIndex, entry.groupId, command); + if (err != CHIP_NO_ERROR) { + ChipLogError(DeviceLayer, "Matter binding: failed to send group command: %" CHIP_ERROR_FORMAT, err.Format()); + } + }); +} + +/***************************************************************************//** + * CASESessionManager success callback - sends the pending command once the + * session to the bound node is ready + ******************************************************************************/ +void MatterBindingManager::handle_device_connected(void* context, Messaging::ExchangeManager& exchange_mgr, const SessionHandle& session_handle) +{ + PendingCommand* pending = static_cast(context); + + auto on_success = [](const ConcreteCommandPath&, const StatusIB&, const auto&) { + ChipLogProgress(DeviceLayer, "Matter binding: bound command succeeded"); + }; + auto on_failure = [](CHIP_ERROR error) { + ChipLogError(DeviceLayer, "Matter binding: bound command failed: %" CHIP_ERROR_FORMAT, error.Format()); + }; + + SendOnOffCommand(pending->command_id, [&](auto& command) { + CHIP_ERROR err = Controller::InvokeCommandRequest(&exchange_mgr, session_handle, pending->remote_endpoint, command, + on_success, on_failure); + if (err != CHIP_NO_ERROR) { + ChipLogError(DeviceLayer, "Matter binding: failed to send bound command: %" CHIP_ERROR_FORMAT, err.Format()); + } + }); + + delete(pending); +} + +/***************************************************************************//** + * CASESessionManager failure callback - the bound command is simply dropped, + * matching how a real Matter light switch behaves when its bound peer is + * unreachable + ******************************************************************************/ +void MatterBindingManager::handle_device_connection_failure(void* context, const ScopedNodeId& peer_id, CHIP_ERROR error) +{ + ChipLogError(DeviceLayer, "Matter binding: failed to connect to bound node 0x" ChipLogFormatX64 ": %" CHIP_ERROR_FORMAT, + ChipLogValueX64(peer_id.GetNodeId()), error.Format()); + delete(static_cast(context)); +} diff --git a/libraries/Matter/src/MatterBindingManager.h b/libraries/Matter/src/MatterBindingManager.h new file mode 100644 index 00000000..e0182e3e --- /dev/null +++ b/libraries/Matter/src/MatterBindingManager.h @@ -0,0 +1,90 @@ +/* + * This file is part of the Silicon Labs Arduino Core + * + * The MIT License (MIT) + * + * Copyright 2026 Silicon Laboratories Inc. www.silabs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef MATTER_BINDING_MANAGER_H +#define MATTER_BINDING_MANAGER_H + +#include "Matter.h" +#include +#include +#include +#include + +using namespace chip; +using namespace ::chip::DeviceLayer; + +// Serves the standard Matter Binding cluster's list-type `Binding` attribute, +// backed by chip::BindingTable. One instance is created per endpoint that opts +// into binding support (see MatterSwitch::beginWithBinding()). +class MatterBindingAttributeAccess : public chip::app::AttributeAccessInterface { +public: + explicit MatterBindingAttributeAccess(chip::EndpointId endpoint); + + CHIP_ERROR Read(const chip::app::ConcreteReadAttributePath& path, chip::app::AttributeValueEncoder& encoder) override; + CHIP_ERROR Write(const chip::app::ConcreteDataAttributePath& path, chip::app::AttributeValueDecoder& decoder) override; +}; + +// Dispatches locally-triggered cluster commands (e.g. OnOff Toggle) to every +// target listed in the standard Matter Binding cluster's table for a given +// local endpoint - the "switch controls a bound light directly" behavior. +// +// The vendored SDK ships the Binding cluster's ZAP-generated metadata (it's +// part of the generic codegen template used for every example) but not the +// official `bindings` cluster server plugin or `BindingManager` - neither the +// source nor precompiled object code for those is present. This class instead +// builds the same behavior directly out of pieces that *are* already available +// (either precompiled into the SDK static library, or header-only templates +// compiled fresh with the sketch): chip::BindingTable, chip::CASESessionManager, +// chip::Server, chip::app::CommandSender and Controller::InvokeCommandRequest / +// InvokeGroupCommandRequest. No SDK vendoring is required. +class MatterBindingManager { +public: + static MatterBindingManager& instance(); + + // Idempotent. Schedules loading the persistent binding table once the + // Matter server is up - safe to call multiple times / before the server + // has started. + void begin(); + + // Sends commandId (e.g. OnOff::Commands::Toggle::Id) of clusterId to every + // binding table entry registered against localEndpoint. + void notify_bound_cluster_changed(chip::EndpointId local_endpoint, chip::ClusterId cluster_id, chip::CommandId command_id); + + // Internal - public only so the free-standing CHIP callback trampolines can + // reach them. Not part of the public API. + static void handle_device_connected(void* context, chip::Messaging::ExchangeManager& exchange_mgr, const chip::SessionHandle& session_handle); + static void handle_device_connection_failure(void* context, const chip::ScopedNodeId& peer_id, CHIP_ERROR error); + +private: + MatterBindingManager(); + + void dispatch_unicast(const EmberBindingTableEntry& entry, chip::CommandId command_id); + void dispatch_group(const EmberBindingTableEntry& entry, chip::CommandId command_id); + + bool initialized; +}; + +#endif // MATTER_BINDING_MANAGER_H diff --git a/libraries/Matter/src/MatterSwitch.cpp b/libraries/Matter/src/MatterSwitch.cpp index 2f147e39..810a0b43 100644 --- a/libraries/Matter/src/MatterSwitch.cpp +++ b/libraries/Matter/src/MatterSwitch.cpp @@ -25,6 +25,7 @@ */ #include "MatterSwitch.h" +#include using namespace ::chip; using namespace ::chip::Platform; @@ -52,6 +53,12 @@ DECLARE_DYNAMIC_ATTRIBUTE(Switch::Attributes::MultiPressMax::Id, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE(Switch::Attributes::FeatureMap::Id, BITMAP32, 4, 0), /* FeatureMap */ DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); /* ClusterRevision auto added by LIST_END */ +// Binding cluster attributes - the `Binding` list attribute itself is served +// by MatterBindingAttributeAccess, not by this ember buffer. +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(switchBindingAttrs) +DECLARE_DYNAMIC_ATTRIBUTE(Binding::Attributes::Binding::Id, ARRAY, 0, 0), /* Binding - served by MatterBindingAttributeAccess */ +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + // Switch cluster list DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(switchEndpointClusters) DECLARE_DYNAMIC_CLUSTER(Switch::Id, switchAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, switchOutgoingCommands), @@ -59,6 +66,15 @@ DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, ZAP_CLUSTER_MASK(SERVER DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr) DECLARE_DYNAMIC_CLUSTER_LIST_END; +// Switch cluster list with the Binding cluster added - only used by beginWithBinding() +// so the plain begin() path (and every existing sketch using it) is byte-for-byte unaffected. +DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(switchEndpointClustersWithBinding) +DECLARE_DYNAMIC_CLUSTER(Switch::Id, switchAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, switchOutgoingCommands), +DECLARE_DYNAMIC_CLUSTER(Binding::Id, switchBindingAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), +DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), +DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr) +DECLARE_DYNAMIC_CLUSTER_LIST_END; + /***************************************************************************//** * Constructor for MatterSwitch ******************************************************************************/ @@ -66,6 +82,8 @@ MatterSwitch::MatterSwitch() : switch_device(nullptr), device_endpoint(nullptr), endpoint_dataversion_storage(nullptr), + binding_attribute_access(nullptr), + binding_enabled(false), initialized(false) { ; @@ -85,6 +103,29 @@ MatterSwitch::~MatterSwitch() * @return true if the initialization succeeded, false otherwise ******************************************************************************/ bool MatterSwitch::begin() +{ + return this->begin_internal(false); +} + +/***************************************************************************//** + * Initializes the MatterSwitch instance with the Binding cluster added, so it + * can be bound directly to another Matter device (e.g. a light) and control + * it without a hub relaying every command + * + * @return true if the initialization succeeded, false otherwise + ******************************************************************************/ +bool MatterSwitch::beginWithBinding() +{ + return this->begin_internal(true); +} + +/***************************************************************************//** + * Shared implementation behind begin() and beginWithBinding() + * + * @param[in] enable_binding whether to add the Binding cluster to the endpoint + * @return true if the initialization succeeded, false otherwise + ******************************************************************************/ +bool MatterSwitch::begin_internal(bool enable_binding) { if (this->initialized) { return false; @@ -107,12 +148,17 @@ bool MatterSwitch::begin() delete(new_switch_device); return false; } - new_endpoint->cluster = switchEndpointClusters; - new_endpoint->clusterCount = ArraySize(switchEndpointClusters); + + const EmberAfCluster* cluster_list = enable_binding ? switchEndpointClustersWithBinding : switchEndpointClusters; + uint8_t cluster_count = enable_binding ? static_cast(ArraySize(switchEndpointClustersWithBinding)) + : static_cast(ArraySize(switchEndpointClusters)); + + new_endpoint->cluster = cluster_list; + new_endpoint->clusterCount = cluster_count; new_endpoint->endpointSize = 0; // Create data version storage for the endpoint - size_t dataversion_size = ArraySize(switchEndpointClusters) * sizeof(DataVersion); + size_t dataversion_size = cluster_count * sizeof(DataVersion); DataVersion* new_switch_data_version = (DataVersion*)malloc(dataversion_size); if (new_switch_data_version == nullptr) { delete(new_switch_device); @@ -124,7 +170,7 @@ bool MatterSwitch::begin() int result = AddDeviceEndpoint(new_switch_device, new_endpoint, Span(gSwitchDeviceTypes), - Span(new_switch_data_version, ArraySize(switchEndpointClusters)), + Span(new_switch_data_version, cluster_count), 1); if (result < 0) { delete(new_switch_device); @@ -140,6 +186,17 @@ bool MatterSwitch::begin() this->switch_device = new_switch_device; this->device_endpoint = new_endpoint; this->endpoint_dataversion_storage = new_switch_data_version; + this->binding_enabled = enable_binding; + + if (enable_binding) { + EndpointId assigned_endpoint_id = new_switch_device->GetEndpointId(); + MatterBindingManager::instance().begin(); + this->binding_attribute_access = new (std::nothrow)MatterBindingAttributeAccess(assigned_endpoint_id); + if (this->binding_attribute_access != nullptr) { + app::AttributeAccessInterfaceRegistry::Instance().Register(this->binding_attribute_access); + } + } + this->initialized = true; return true; } @@ -152,6 +209,11 @@ void MatterSwitch::end() if (!this->initialized) { return; } + if (this->binding_attribute_access != nullptr) { + app::AttributeAccessInterfaceRegistry::Instance().Unregister(this->binding_attribute_access); + delete(this->binding_attribute_access); + this->binding_attribute_access = nullptr; + } (void)RemoveDeviceEndpoint(this->switch_device); free(this->device_endpoint); free(this->endpoint_dataversion_storage); @@ -170,6 +232,13 @@ void MatterSwitch::set_state(bool state) return; } this->switch_device->SetCurrentPosition(state); + + // On a bound switch, a press (front edge) toggles every bound target - + // mirrors how a real Matter light switch reacts to a binding. + if (this->binding_enabled && state) { + MatterBindingManager::instance().notify_bound_cluster_changed(this->switch_device->GetEndpointId(), OnOff::Id, + OnOff::Commands::Toggle::Id); + } } /***************************************************************************//** diff --git a/libraries/Matter/src/MatterSwitch.h b/libraries/Matter/src/MatterSwitch.h index 3064eaf4..bb6566ab 100644 --- a/libraries/Matter/src/MatterSwitch.h +++ b/libraries/Matter/src/MatterSwitch.h @@ -28,6 +28,7 @@ #define MATTER_SWITCH_H #include "Matter.h" +#include "MatterBindingManager.h" #include "devices/DeviceSwitch.h" #include #include @@ -41,15 +42,24 @@ class MatterSwitch : public ArduinoMatterAppliance { MatterSwitch(); ~MatterSwitch(); bool begin(); + // Same as begin(), but also adds the standard Matter Binding cluster to the + // switch's endpoint so it can be bound (by a Matter controller, e.g. Home + // Assistant or chip-tool) directly to a light and toggle it without going + // through a hub for every press. See the matter_switch_binding example. + bool beginWithBinding(); void end(); void set_state(bool state); bool get_state(); void operator=(bool state); private: + bool begin_internal(bool enable_binding); + DeviceSwitch* switch_device; EmberAfEndpointType* device_endpoint; DataVersion* endpoint_dataversion_storage; + MatterBindingAttributeAccess* binding_attribute_access; + bool binding_enabled; bool initialized; }; diff --git a/test/build/test_build.py b/test/build/test_build.py index bea246ee..9fe5a8f4 100644 --- a/test/build/test_build.py +++ b/test/build/test_build.py @@ -237,6 +237,7 @@ "../../libraries/Matter/examples/matter_lightbulb_color/matter_lightbulb_color.ino": all_matter, "../../libraries/Matter/examples/matter_lightbulb_custom_name/matter_lightbulb_custom_name.ino": all_matter, "../../libraries/Matter/examples/matter_lightbulb_dimmable/matter_lightbulb_dimmable.ino": all_matter, + "../../libraries/Matter/examples/matter_lightbulb_binding/matter_lightbulb_binding.ino": all_matter, "../../libraries/Matter/examples/matter_lightbulb_dimmable_multiple/matter_lightbulb_dimmable_multiple.ino": all_matter, "../../libraries/Matter/examples/matter_lightbulb_identify/matter_lightbulb_identify.ino": all_matter, "../../libraries/Matter/examples/matter_lightbulb_multiple/matter_lightbulb_multiple.ino": all_matter, @@ -251,6 +252,7 @@ "../../libraries/Matter/examples/matter_sensor_and_bulb/matter_sensor_and_bulb.ino": all_matter, "../../libraries/Matter/examples/matter_sensor_multiple/matter_sensor_multiple.ino": all_matter, "../../libraries/Matter/examples/matter_switch/matter_switch.ino": all_matter, + "../../libraries/Matter/examples/matter_switch_binding/matter_switch_binding.ino": all_matter, "../../libraries/Matter/examples/matter_temp_sensor/matter_temp_sensor.ino": all_matter, "../../libraries/Matter/examples/matter_thermostat/matter_thermostat.ino": all_matter, "../../libraries/Matter/examples/matter_tvoc_sensor/matter_tvoc_sensor.ino": all_matter,