diff --git a/examples/all-clusters-app/all-clusters-common/include/binding-handler.h b/examples/all-clusters-app/all-clusters-common/include/binding-handler.h new file mode 100644 index 00000000000000..b5d6df2b2acbb0 --- /dev/null +++ b/examples/all-clusters-app/all-clusters-common/include/binding-handler.h @@ -0,0 +1,22 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "lib/core/CHIPError.h" + +CHIP_ERROR InitBindingHandlers(); diff --git a/examples/all-clusters-app/all-clusters-common/src/binding-handler.cpp b/examples/all-clusters-app/all-clusters-common/src/binding-handler.cpp new file mode 100644 index 00000000000000..020c1210806171 --- /dev/null +++ b/examples/all-clusters-app/all-clusters-common/src/binding-handler.cpp @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "binding-handler.h" + +#include "app-common/zap-generated/ids/Clusters.h" +#include "app-common/zap-generated/ids/Commands.h" +#include "app/CommandSender.h" +#include "app/clusters/bindings/BindingManager.h" +#include "app/server/Server.h" +#include "controller/InvokeInteraction.h" +#include "lib/core/CHIPError.h" + +#if defined(ENABLE_CHIP_SHELL) +#include "lib/shell/Engine.h" + +using chip::Shell::Engine; +using chip::Shell::shell_command_t; +using chip::Shell::streamer_get; +using chip::Shell::streamer_printf; +#endif // defined(ENABLE_CHIP_SHELL) + +static bool sSwitchOnOffState = false; +#if defined(ENABLE_CHIP_SHELL) +static void ToggleSwitchOnOff(bool newState) +{ + sSwitchOnOffState = newState; + chip::BindingManager::GetInstance().NotifyBoundClusterChanged(1, chip::app::Clusters::OnOff::Id, nullptr); +} + +static CHIP_ERROR SwitchCommandHandler(int argc, char ** argv) +{ + if (argc == 1 && strcmp(argv[0], "on") == 0) + { + ToggleSwitchOnOff(true); + return CHIP_NO_ERROR; + } + if (argc == 1 && strcmp(argv[0], "off") == 0) + { + ToggleSwitchOnOff(false); + return CHIP_NO_ERROR; + } + streamer_printf(streamer_get(), "Usage: switch [on|off]"); + return CHIP_NO_ERROR; +} + +static void RegisterSwitchCommands() +{ + static const shell_command_t sSwitchCommand = { SwitchCommandHandler, "switch", "Switch commands. Usage: switch [on|off]" }; + Engine::Root().RegisterCommands(&sSwitchCommand, 1); + return; +} +#endif // defined(ENABLE_CHIP_SHELL) + +static void BoundDeviceChangedHandler(const EmberBindingTableEntry * binding, chip::DeviceProxy * peer_device, void * context) +{ + using namespace chip; + using namespace chip::app; + + if (binding->type == EMBER_MULTICAST_BINDING) + { + ChipLogError(NotSpecified, "Group binding is not supported now"); + return; + } + + if (binding->type == EMBER_UNICAST_BINDING && binding->local == 1 && binding->clusterId == Clusters::OnOff::Id) + { + auto onSuccess = [](const ConcreteCommandPath & commandPath, const StatusIB & status, const auto & dataResponse) { + ChipLogProgress(NotSpecified, "OnOff command succeeds"); + }; + auto onFailure = [](const StatusIB & status, CHIP_ERROR error) { + ChipLogError(NotSpecified, "OnOff command failed: %" CHIP_ERROR_FORMAT, error.Format()); + }; + + if (sSwitchOnOffState) + { + Clusters::OnOff::Commands::On::Type onCommand; + Controller::InvokeCommandRequest(peer_device->GetExchangeManager(), peer_device->GetSecureSession().Value(), + binding->remote, onCommand, onSuccess, onFailure); + } + else + { + Clusters::OnOff::Commands::Off::Type offCommand; + Controller::InvokeCommandRequest(peer_device->GetExchangeManager(), peer_device->GetSecureSession().Value(), + binding->remote, offCommand, onSuccess, onFailure); + } + } +} + +CHIP_ERROR InitBindingHandlers() +{ + chip::BindingManager::GetInstance().SetAppServer(&chip::Server::GetInstance()); + chip::BindingManager::GetInstance().RegisterBoundDeviceChangedHandler(BoundDeviceChangedHandler); +#if defined(ENABLE_CHIP_SHELL) + RegisterSwitchCommands(); +#endif + return CHIP_NO_ERROR; +} diff --git a/examples/all-clusters-app/esp32/main/main.cpp b/examples/all-clusters-app/esp32/main/main.cpp index 2738a4b4f9ebfb..a98e4e9f60cf54 100644 --- a/examples/all-clusters-app/esp32/main/main.cpp +++ b/examples/all-clusters-app/esp32/main/main.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -537,6 +538,7 @@ static void InitServer(intptr_t context) SetDeviceAttestationCredentialsProvider(Examples::GetExampleDACProvider()); NetWorkCommissioningInstInit(); SetupPretendDevices(); + InitBindingHandlers(); } static void InitOTARequestor(void) diff --git a/examples/all-clusters-app/linux/BUILD.gn b/examples/all-clusters-app/linux/BUILD.gn index 3e700fd1af5b5e..563c101cb91e7b 100644 --- a/examples/all-clusters-app/linux/BUILD.gn +++ b/examples/all-clusters-app/linux/BUILD.gn @@ -15,8 +15,11 @@ import("//build_overrides/build.gni") import("//build_overrides/chip.gni") +import("${chip_root}/src/lib/lib.gni") + executable("chip-all-clusters-app") { sources = [ + "${chip_root}/examples/all-clusters-app/all-clusters-common/src/binding-handler.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/bridged-actions-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "include/tv-callbacks.cpp", @@ -35,6 +38,10 @@ executable("chip-all-clusters-app") { cflags = [ "-Wconversion" ] + if (chip_build_libshell) { + defines = [ "ENABLE_CHIP_SHELL" ] + } + output_dir = root_out_dir } diff --git a/examples/all-clusters-app/linux/main.cpp b/examples/all-clusters-app/linux/main.cpp index f7a58ab4e5dcfc..2c02d212ac84f9 100644 --- a/examples/all-clusters-app/linux/main.cpp +++ b/examples/all-clusters-app/linux/main.cpp @@ -24,6 +24,7 @@ #include #include "AppMain.h" +#include "binding-handler.h" using namespace chip; using namespace chip::app; @@ -110,6 +111,7 @@ void ApplicationInit() int main(int argc, char * argv[]) { VerifyOrDie(ChipLinuxAppInit(argc, argv) == 0); + VerifyOrDie(InitBindingHandlers() == CHIP_NO_ERROR); ChipLinuxAppMainLoop(); return 0; } diff --git a/examples/all-clusters-app/mbed/CMakeLists.txt b/examples/all-clusters-app/mbed/CMakeLists.txt index ca364740a9e9f2..bbb871d89943ad 100644 --- a/examples/all-clusters-app/mbed/CMakeLists.txt +++ b/examples/all-clusters-app/mbed/CMakeLists.txt @@ -86,6 +86,7 @@ target_sources(${APP_TARGET} PRIVATE ${APP_CLUSTERS}/application-basic-server/application-basic-server.cpp ${APP_CLUSTERS}/basic/basic.cpp + ${APP_CLUSTERS}/bindings/BindingManager.cpp ${APP_CLUSTERS}/bindings/bindings.cpp ${APP_CLUSTERS}/on-off-server/on-off-server.cpp ${APP_CLUSTERS}/access-control-server/access-control-server.cpp diff --git a/examples/lighting-app/mbed/CMakeLists.txt b/examples/lighting-app/mbed/CMakeLists.txt index 5a4fdf42a2d176..bfad30ece1b85f 100644 --- a/examples/lighting-app/mbed/CMakeLists.txt +++ b/examples/lighting-app/mbed/CMakeLists.txt @@ -74,6 +74,7 @@ target_sources(${APP_TARGET} PRIVATE ${CHIP_ROOT}/src/app/server/CommissioningWindowManager.cpp ${CHIP_ROOT}/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp ${CHIP_ROOT}/src/app/clusters/basic/basic.cpp + ${CHIP_ROOT}/src/app/clusters/bindings/BindingManager.cpp ${CHIP_ROOT}/src/app/clusters/bindings/bindings.cpp ${CHIP_ROOT}/src/app/clusters/descriptor/descriptor.cpp ${CHIP_ROOT}/src/app/clusters/identify-server/identify-server.cpp diff --git a/examples/lighting-app/telink/CMakeLists.txt b/examples/lighting-app/telink/CMakeLists.txt index cabca3ee2eb539..f6bb6443f127ba 100644 --- a/examples/lighting-app/telink/CMakeLists.txt +++ b/examples/lighting-app/telink/CMakeLists.txt @@ -73,6 +73,7 @@ target_sources(app PRIVATE ${CHIP_ROOT}/src/app/server/CommissioningWindowManager.cpp ${CHIP_ROOT}/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp ${CHIP_ROOT}/src/app/clusters/basic/basic.cpp + ${CHIP_ROOT}/src/app/clusters/bindings/BindingManager.cpp ${CHIP_ROOT}/src/app/clusters/bindings/bindings.cpp ${CHIP_ROOT}/src/app/clusters/descriptor/descriptor.cpp ${CHIP_ROOT}/src/app/clusters/identify-server/identify-server.cpp diff --git a/examples/lock-app/mbed/CMakeLists.txt b/examples/lock-app/mbed/CMakeLists.txt index 49fa9d1c366efe..f7326fa9822375 100644 --- a/examples/lock-app/mbed/CMakeLists.txt +++ b/examples/lock-app/mbed/CMakeLists.txt @@ -72,6 +72,7 @@ target_sources(${APP_TARGET} PRIVATE ${CHIP_ROOT}/src/app/server/CommissioningWindowManager.cpp ${CHIP_ROOT}/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp ${CHIP_ROOT}/src/app/clusters/basic/basic.cpp + ${CHIP_ROOT}/src/app/clusters/bindings/BindingManager.cpp ${CHIP_ROOT}/src/app/clusters/bindings/bindings.cpp ${CHIP_ROOT}/src/app/clusters/descriptor/descriptor.cpp ${CHIP_ROOT}/src/app/clusters/diagnostic-logs-server/diagnostic-logs-server.cpp diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index 09e1a54d48acb4..fafa5afa0c8ab9 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -397,6 +397,7 @@ DeviceCommissioner * GetDeviceCommissioner() void ChipLinuxAppMainLoop() { #if defined(ENABLE_CHIP_SHELL) + Engine::Root().Init(); std::thread shellThread([]() { Engine::Root().RunMainLoop(); }); chip::Shell::RegisterCommissioneeCommands(); #endif diff --git a/src/app/CASESessionManager.cpp b/src/app/CASESessionManager.cpp index 08d5d42ed3376a..26d7e8cf75173e 100644 --- a/src/app/CASESessionManager.cpp +++ b/src/app/CASESessionManager.cpp @@ -55,7 +55,8 @@ CHIP_ERROR CASESessionManager::FindOrEstablishSession(PeerId peerId, Callback::C CHIP_ERROR err = session->Connect(onConnection, onFailure, mConfig.dnsResolver); if (err != CHIP_NO_ERROR) { - ReleaseSession(session); + // Release the peer rather than the pointer in case the failure handler has already released the session. + ReleaseSession(peerId); } return err; diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni index 3ea9d09b684dfc..0e18514f229d9a 100644 --- a/src/app/chip_data_model.gni +++ b/src/app/chip_data_model.gni @@ -154,6 +154,12 @@ template("chip_data_model") { "${_app_root}/clusters/${cluster}/BDXDownloader.h", "${_app_root}/clusters/${cluster}/OTARequestor.cpp", ] + } else if (cluster == "bindings") { + sources += [ + "${_app_root}/clusters/${cluster}/${cluster}.cpp", + "${_app_root}/clusters/${cluster}/BindingManager.cpp", + "${_app_root}/clusters/${cluster}/BindingManager.h", + ] } else { sources += [ "${_app_root}/clusters/${cluster}/${cluster}.cpp" ] } diff --git a/src/app/clusters/bindings/BindingManager.cpp b/src/app/clusters/bindings/BindingManager.cpp new file mode 100644 index 00000000000000..8198f77bfc39d9 --- /dev/null +++ b/src/app/clusters/bindings/BindingManager.cpp @@ -0,0 +1,211 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace chip { + +BindingManager BindingManager::sBindingManager; + +CHIP_ERROR BindingManager::EstablishConnection(FabricIndex fabric, NodeId node) +{ + VerifyOrReturnError(mAppServer != nullptr, CHIP_ERROR_INCORRECT_STATE); + + FabricInfo * fabricInfo = mAppServer->GetFabricTable().FindFabricWithIndex(fabric); + VerifyOrReturnError(fabricInfo != nullptr, CHIP_ERROR_NOT_FOUND); + PeerId peer = fabricInfo->GetPeerIdForNode(node); + CHIP_ERROR error = + mAppServer->GetCASESessionManager()->FindOrEstablishSession(peer, &mOnConnectedCallback, &mOnConnectionFailureCallback); + if (error == CHIP_ERROR_NO_MEMORY) + { + // Release the least recently used entry + // TODO: Some reference counting mechanism shall be added the CASESessionManager + // so that other session clients don't get accidentally closed. + PendingNotificationEntry * entry = mPendingNotificationMap.FindLRUEntry(); + if (entry != nullptr) + { + mAppServer->GetCASESessionManager()->ReleaseSession(entry->GetPeerId()); + mPendingNotificationMap.RemoveEntry(entry); + // Now retry + error = mAppServer->GetCASESessionManager()->FindOrEstablishSession(peer, &mOnConnectedCallback, + &mOnConnectionFailureCallback); + } + } + return error; +} + +CHIP_ERROR BindingManager::EnqueueUnicastNotification(FabricIndex fabric, NodeId node, EndpointId endpoint, ClusterId cluster, + void * context) +{ + VerifyOrReturnError(mAppServer != nullptr, CHIP_ERROR_INCORRECT_STATE); + + FabricInfo * fabricInfo = mAppServer->GetFabricTable().FindFabricWithIndex(fabric); + VerifyOrReturnError(fabricInfo != nullptr, CHIP_ERROR_NOT_FOUND); + PeerId peer = fabricInfo->GetPeerIdForNode(node); + return mPendingNotificationMap.AddPendingNotification(peer, endpoint, cluster, context); +} + +void BindingManager::HandleDeviceConnected(void * context, OperationalDeviceProxy * device) +{ + BindingManager * manager = static_cast(context); + manager->HandleDeviceConnected(device); +} + +void BindingManager::HandleDeviceConnected(OperationalDeviceProxy * device) +{ + mPendingNotificationMap.ForEachActiveObject([&](PendingNotificationEntry * entry) -> Loop { + if (entry->GetPeerId() == device->GetPeerId()) + { + SyncPendingNotificationsToPeer(device, entry); + } + + return Loop::Continue; + }); +} + +void BindingManager::SyncPendingNotificationsToPeer(OperationalDeviceProxy * device, PendingNotificationEntry * pendingClusters) +{ + for (const ClusterPath & path : *pendingClusters) + { + ClusterId cluster = path.cluster; + EndpointId endpoint = path.endpoint; + for (uint8_t j = 0; j < EMBER_BINDING_TABLE_SIZE; j++) + { + EmberBindingTableEntry entry; + if (emberGetBinding(j, &entry) == EMBER_SUCCESS && entry.type == EMBER_UNICAST_BINDING && entry.clusterId == cluster && + entry.local == endpoint && mBoundDeviceChangedHandler) + { + mBoundDeviceChangedHandler(&entry, device, path.context); + } + } + } + mPendingNotificationMap.RemoveEntry(pendingClusters); +} + +void BindingManager::HandleDeviceConnectionFailure(void * context, PeerId peerId, CHIP_ERROR error) +{ + BindingManager * manager = static_cast(context); + manager->HandleDeviceConnectionFailure(peerId, error); +} + +void BindingManager::HandleDeviceConnectionFailure(PeerId peerId, CHIP_ERROR error) +{ + // Simply release the entry, the connection will be re-established as needed. + ChipLogError(AppServer, "Failed to establish connection to node 0x" ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId())); + mAppServer->GetCASESessionManager()->ReleaseSession(peerId); +} + +CHIP_ERROR BindingManager::LastUnicastBindingRemoved(FabricIndex fabric, NodeId node) +{ + VerifyOrReturnError(mAppServer != nullptr, CHIP_ERROR_INCORRECT_STATE); + + FabricInfo * fabricInfo = mAppServer->GetFabricTable().FindFabricWithIndex(fabric); + VerifyOrReturnError(fabricInfo != nullptr, CHIP_ERROR_NOT_FOUND); + PeerId peer = fabricInfo->GetPeerIdForNode(node); + PendingNotificationEntry * entry = mPendingNotificationMap.FindEntry(peer); + if (entry) + { + mPendingNotificationMap.RemoveEntry(entry); + } + + mAppServer->GetCASESessionManager()->ReleaseSession(peer); + return CHIP_NO_ERROR; +} + +CHIP_ERROR BindingManager::NotifyBoundClusterChanged(EndpointId endpoint, ClusterId cluster, void * context) +{ + VerifyOrReturnError(mAppServer != nullptr, CHIP_ERROR_INCORRECT_STATE); + + for (uint8_t i = 0; i < EMBER_BINDING_TABLE_SIZE; i++) + { + EmberBindingTableEntry entry; + + if (emberGetBinding(i, &entry) == EMBER_SUCCESS && entry.type != EMBER_UNUSED_BINDING && entry.local == endpoint && + entry.clusterId == cluster) + { + if (entry.type == EMBER_UNICAST_BINDING) + { + FabricInfo * fabricInfo = mAppServer->GetFabricTable().FindFabricWithIndex(entry.fabricIndex); + VerifyOrReturnError(fabricInfo != nullptr, CHIP_ERROR_NOT_FOUND); + PeerId peer = fabricInfo->GetPeerIdForNode(entry.nodeId); + OperationalDeviceProxy * peerDevice = mAppServer->GetCASESessionManager()->FindExistingSession(peer); + if (peerDevice != nullptr && mBoundDeviceChangedHandler) + { + // We already have an active connection + mBoundDeviceChangedHandler(&entry, peerDevice, context); + } + else + { + // Enqueue pending cluster and establish connection + ReturnErrorOnFailure( + EnqueueUnicastNotification(entry.fabricIndex, entry.nodeId, entry.local, entry.clusterId, context)); + ReturnErrorOnFailure(EstablishConnection(entry.fabricIndex, entry.nodeId)); + } + } + else if (entry.type == EMBER_MULTICAST_BINDING) + { + mBoundDeviceChangedHandler(&entry, nullptr, context); + } + } + } + return CHIP_NO_ERROR; +} + +BindingManager::PendingNotificationEntry * BindingManager::PendingNotificationMap::FindLRUEntry() +{ + PendingNotificationEntry * lruEntry = nullptr; + mPendingNotificationMap.ForEachActiveObject([&](PendingNotificationEntry * entry) { + if (lruEntry == nullptr || lruEntry->GetLastUpdateTime() > entry->GetLastUpdateTime()) + { + lruEntry = entry; + } + return Loop::Continue; + }); + return lruEntry; +} + +BindingManager::PendingNotificationEntry * BindingManager::PendingNotificationMap::FindEntry(PeerId peerId) +{ + PendingNotificationEntry * foundEntry = nullptr; + mPendingNotificationMap.ForEachActiveObject([&](PendingNotificationEntry * entry) { + if (entry->GetPeerId() == peerId) + { + foundEntry = entry; + return Loop::Break; + } + return Loop::Continue; + }); + return foundEntry; +} + +CHIP_ERROR BindingManager::PendingNotificationMap::AddPendingNotification(PeerId peer, EndpointId endpoint, ClusterId cluster, + void * context) +{ + PendingNotificationEntry * entry = FindEntry(peer); + + if (entry == nullptr) + { + entry = mPendingNotificationMap.CreateObject(peer); + VerifyOrReturnError(entry != nullptr, CHIP_ERROR_NO_MEMORY); + } + entry->AddPendingNotification(endpoint, cluster, context); + entry->Touch(); + return CHIP_NO_ERROR; +} + +} // namespace chip diff --git a/src/app/clusters/bindings/BindingManager.h b/src/app/clusters/bindings/BindingManager.h new file mode 100644 index 00000000000000..8456f1ace7180e --- /dev/null +++ b/src/app/clusters/bindings/BindingManager.h @@ -0,0 +1,195 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace chip { + +/** + * Application callback function when a cluster associated with a binding changes. + * + * The connection is managed by the stack and peer_device is guaranteed to be available. + * The application shall decide the content to be sent to the peer. + * + * For unicast bindings peer_device will be a connected peer and group will be empty. + * For multicast bindings peer_device will be nullptr. + * + * E.g. The application will send on/off commands to peer for the OnOff cluster. + * + */ +using BoundDeviceChangedHandler = void (*)(const EmberBindingTableEntry * binding, DeviceProxy * peer_device, void * context); + +/** + * + * The BindingManager class manages the connections for unicast bindings and notifies the application + * when a binding is ready to be communicated with. + * + * A CASE connection will be triggered when: + * - The binding cluster adds a unicast entry to the binding table. + * - A watched cluster changes with a unicast binding but we cannot find an active connection to the peer. + * + * The class uses an LRU mechanism to choose the connection to eliminate when there is no space for a new connection. + * The BindingManager class will not actively re-establish connection and will connect on-demand (when binding cluster + * or watched cluster is changed). + * + */ +class BindingManager +{ +public: + BindingManager() : + mOnConnectedCallback(HandleDeviceConnected, this), mOnConnectionFailureCallback(HandleDeviceConnectionFailure, this) + {} + + void RegisterBoundDeviceChangedHandler(BoundDeviceChangedHandler handler) { mBoundDeviceChangedHandler = handler; } + + void SetAppServer(Server * appServer) { mAppServer = appServer; } + + /* + * Notifies the BindingManager that a new unicast binding is created. + * + */ + CHIP_ERROR UnicastBindingCreated(FabricIndex fabric, NodeId node) { return EstablishConnection(fabric, node); } + + /* + * Notfies the BindingManager that the **last** unicast binding to a device has been removed. + * + */ + CHIP_ERROR LastUnicastBindingRemoved(FabricIndex fabric, NodeId node); + + /* + * Notify a cluster change to **all** bound devices associated with the (endpoint, cluster) tuple. + * + * For unicast bindings with an active session and multicast bindings, the BoundDeviceChangedHandler + * will be called before the function returns. + * + * For unicast bindings without an active session, the notification will be queued and a new session will + * be initiated. The BoundDeviceChangedHandler will be called once the session is established. + * + */ + CHIP_ERROR NotifyBoundClusterChanged(EndpointId endpoint, ClusterId cluster, void * context); + + static BindingManager & GetInstance() { return sBindingManager; } + +private: + static BindingManager sBindingManager; + + static constexpr uint8_t kMaxPendingNotifications = 3; + + struct ClusterPath + { + void * context; + ClusterId cluster; + EndpointId endpoint; + }; + + // A pending notification to be sent to a binding waiting for the CASE session to be established. + class PendingNotificationEntry + { + public: + PendingNotificationEntry(PeerId peerId) : mPeerId(peerId) {} + + PeerId GetPeerId() { return mPeerId; } + + System::Clock::Timestamp GetLastUpdateTime() { return mLastUpdateTime; } + void Touch() { mLastUpdateTime = System::SystemClock().GetMonotonicTimestamp(); } + + ClusterPath * begin() { return &mPendingNotifications[0]; } + ClusterPath * end() { return &mPendingNotifications[mNumPendingNotifications]; } + + void AddPendingNotification(EndpointId endpoint, ClusterId cluster, void * context) + { + for (ClusterPath & path : *this) + { + // New notifications for the same (endpoint, cluster) shall + // simply overrride the old ones + if (path.cluster == cluster && path.endpoint == endpoint) + { + path.context = context; + return; + } + } + if (mNumPendingNotifications < kMaxPendingNotifications) + { + mPendingNotifications[mNumPendingNotifications++] = { context, cluster, endpoint }; + } + else + { + mPendingNotifications[mNextToOverride] = { context, cluster, endpoint }; + mNextToOverride++; + mNextToOverride %= kMaxPendingNotifications; + } + } + + private: + PeerId mPeerId; + System::Clock::Timestamp mLastUpdateTime; + // TODO: Make the pending notifications list of binding table indecies and list of contexts + ClusterPath mPendingNotifications[kMaxPendingNotifications]; + + uint8_t mNumPendingNotifications = 0; + uint8_t mNextToOverride = 0; + }; + + // The pool for all the pending comands. + class PendingNotificationMap + { + public: + PendingNotificationEntry * FindLRUEntry(); + + PendingNotificationEntry * FindEntry(PeerId peerId); + + CHIP_ERROR AddPendingNotification(PeerId peer, EndpointId endpoint, ClusterId cluster, void * context); + + void RemoveEntry(PendingNotificationEntry * entry) { mPendingNotificationMap.ReleaseObject(entry); } + + template + Loop ForEachActiveObject(Function && function) + { + return mPendingNotificationMap.ForEachActiveObject(std::forward(function)); + } + + private: + BitMapObjectPool mPendingNotificationMap; + }; + + static void HandleDeviceConnected(void * context, OperationalDeviceProxy * device); + void HandleDeviceConnected(OperationalDeviceProxy * device); + + static void HandleDeviceConnectionFailure(void * context, PeerId peerId, CHIP_ERROR error); + void HandleDeviceConnectionFailure(PeerId peerId, CHIP_ERROR error); + + CHIP_ERROR EstablishConnection(FabricIndex fabric, NodeId node); + + // Called when CASE session is established to a peer device. Will send all the pending commands to the peer. + void SyncPendingNotificationsToPeer(OperationalDeviceProxy * device, PendingNotificationEntry * pendingClusters); + + // Called when CASE session is not established to a peer device. Will enqueue the command and initialize connection. + CHIP_ERROR EnqueueUnicastNotification(FabricIndex fabric, NodeId node, EndpointId endpoint, ClusterId cluster, void * context); + + PendingNotificationMap mPendingNotificationMap; + BoundDeviceChangedHandler mBoundDeviceChangedHandler; + Server * mAppServer = nullptr; + + Callback::Callback mOnConnectedCallback; + Callback::Callback mOnConnectionFailureCallback; +}; + +} // namespace chip diff --git a/src/app/clusters/bindings/bindings.cpp b/src/app/clusters/bindings/bindings.cpp index 73c06996717139..13bfc15d7ebb8c 100644 --- a/src/app/clusters/bindings/bindings.cpp +++ b/src/app/clusters/bindings/bindings.cpp @@ -25,41 +25,16 @@ #include #include #include +#include #include #include using namespace chip; using namespace chip::app::Clusters::Binding; -EmberStatus prepareBinding(EmberBindingTableEntry & binding, NodeId nodeId, GroupId groupId, EndpointId endpointId, - ClusterId clusterId) -{ - if (groupId && nodeId) - { - return EMBER_BAD_ARGUMENT; - } - - binding.clusterId = clusterId; - binding.local = emberAfCurrentCommand()->apsFrame->destinationEndpoint; - binding.networkIndex = 0; - - if (groupId) - { - binding.type = EMBER_MULTICAST_BINDING; - binding.groupId = groupId; - binding.remote = 0; - } - else - { - binding.type = EMBER_UNICAST_BINDING; - binding.nodeId = nodeId; - binding.remote = endpointId; - } - - return EMBER_SUCCESS; -} +// TODO: add binding table to the persistent storage -EmberStatus getBindingIndex(EmberBindingTableEntry & newEntry, uint8_t * bindingIndex) +static EmberStatus getBindingIndex(EmberBindingTableEntry & newEntry, uint8_t * bindingIndex) { EmberBindingTableEntry currentEntry; for (uint8_t i = 0; i < EMBER_BINDING_TABLE_SIZE; i++) @@ -75,7 +50,7 @@ EmberStatus getBindingIndex(EmberBindingTableEntry & newEntry, uint8_t * binding return EMBER_NOT_FOUND; } -EmberStatus getUnusedBindingIndex(uint8_t * bindingIndex) +static EmberStatus getUnusedBindingIndex(uint8_t * bindingIndex) { EmberBindingTableEntry currentEntry; for (uint8_t i = 0; i < EMBER_BINDING_TABLE_SIZE; i++) @@ -94,20 +69,32 @@ EmberStatus getUnusedBindingIndex(uint8_t * bindingIndex) bool emberAfBindingClusterBindCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, const Commands::Bind::DecodableType & commandData) { - auto & nodeId = commandData.nodeId; - auto & groupId = commandData.groupId; - auto & endpointId = commandData.endpointId; - auto & clusterId = commandData.clusterId; + NodeId nodeId = commandData.nodeId; + GroupId groupId = commandData.groupId; + ClusterId clusterId = commandData.clusterId; + EndpointId remoteEndpoint = commandData.endpointId; + EndpointId localEndpoint = commandPath.mEndpointId; + FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex(); + EmberBindingTableEntry bindingEntry; ChipLogDetail(Zcl, "RX: BindCallback"); - EmberBindingTableEntry bindingEntry; - if (prepareBinding(bindingEntry, nodeId, groupId, endpointId, clusterId) != EMBER_SUCCESS) + if ((groupId != 0 && nodeId != 0) || (groupId == 0 && nodeId == 0) || (groupId != 0 && remoteEndpoint != 0)) { + ChipLogError(Zcl, "Binding: Invalid request"); emberAfSendImmediateDefaultResponse(EMBER_ZCL_STATUS_MALFORMED_COMMAND); return true; } + if (groupId) + { + bindingEntry = EmberBindingTableEntry::ForGroup(fabricIndex, groupId, localEndpoint, clusterId); + } + else + { + bindingEntry = EmberBindingTableEntry::ForNode(fabricIndex, nodeId, localEndpoint, remoteEndpoint, clusterId); + } + uint8_t bindingIndex; if (getBindingIndex(bindingEntry, &bindingIndex) != EMBER_NOT_FOUND) { @@ -121,27 +108,63 @@ bool emberAfBindingClusterBindCallback(app::CommandHandler * commandObj, const a return true; } + if (nodeId) + { + CHIP_ERROR err = BindingManager::GetInstance().UnicastBindingCreated(fabricIndex, nodeId); + if (err != CHIP_NO_ERROR) + { + ChipLogProgress( + Zcl, "Binding: Failed to create session for unicast binding to device " ChipLogFormatX64 ": %" CHIP_ERROR_FORMAT, + ChipLogValueX64(nodeId), err.Format()); + } + } emberSetBinding(bindingIndex, &bindingEntry); emberAfSendImmediateDefaultResponse(EMBER_ZCL_STATUS_SUCCESS); return true; } +static uint8_t GetNumberOfUnicastBindingForNode(FabricIndex fabric, NodeId node) +{ + uint8_t numBinding = 0; + EmberBindingTableEntry entry; + for (uint8_t i = 0; i < EMBER_BINDING_TABLE_SIZE; i++) + { + if (emberGetBinding(i, &entry) == EMBER_SUCCESS && entry.type == EMBER_UNICAST_BINDING && entry.fabricIndex == fabric && + entry.nodeId == node) + { + numBinding++; + } + } + return numBinding; +} + bool emberAfBindingClusterUnbindCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, const Commands::Unbind::DecodableType & commandData) { - auto & nodeId = commandData.nodeId; - auto & groupId = commandData.groupId; - auto & endpointId = commandData.endpointId; - auto & clusterId = commandData.clusterId; + NodeId nodeId = commandData.nodeId; + GroupId groupId = commandData.groupId; + ClusterId clusterId = commandData.clusterId; + EndpointId remoteEndpoint = commandData.endpointId; + EndpointId localEndpoint = commandPath.mEndpointId; + FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex(); + EmberBindingTableEntry bindingEntry; ChipLogDetail(Zcl, "RX: UnbindCallback"); - EmberBindingTableEntry bindingEntry; - if (prepareBinding(bindingEntry, nodeId, groupId, endpointId, clusterId) != EMBER_SUCCESS) + if ((groupId != 0 && nodeId != 0) || (groupId == 0 && nodeId == 0)) { + ChipLogError(Zcl, "Binding: Invalid request"); emberAfSendImmediateDefaultResponse(EMBER_ZCL_STATUS_MALFORMED_COMMAND); return true; } + if (groupId) + { + bindingEntry = EmberBindingTableEntry::ForGroup(fabricIndex, groupId, localEndpoint, clusterId); + } + else + { + bindingEntry = EmberBindingTableEntry::ForNode(fabricIndex, nodeId, localEndpoint, remoteEndpoint, clusterId); + } uint8_t bindingIndex; if (getBindingIndex(bindingEntry, &bindingIndex) != EMBER_SUCCESS) @@ -151,6 +174,16 @@ bool emberAfBindingClusterUnbindCallback(app::CommandHandler * commandObj, const } emberDeleteBinding(bindingIndex); + if (nodeId != 0 && GetNumberOfUnicastBindingForNode(fabricIndex, nodeId) == 0) + { + CHIP_ERROR err = BindingManager::GetInstance().LastUnicastBindingRemoved(fabricIndex, nodeId); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Binding: Failed to disconnect device " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId), + err.AsString()); + } + } + emberAfSendImmediateDefaultResponse(EMBER_ZCL_STATUS_SUCCESS); return true; } diff --git a/src/app/util/types_stub.h b/src/app/util/types_stub.h index 6410c6c032e8f9..ba97c524f74eab 100644 --- a/src/app/util/types_stub.h +++ b/src/app/util/types_stub.h @@ -495,8 +495,40 @@ enum */ struct EmberBindingTableEntry { + EmberBindingTableEntry() = default; + + static EmberBindingTableEntry ForNode(chip::FabricIndex fabric, chip::NodeId node, chip::EndpointId localEndpoint, + chip::EndpointId remoteEndpoint, chip::ClusterId cluster) + { + EmberBindingTableEntry entry = { + .type = EMBER_UNICAST_BINDING, + .fabricIndex = fabric, + .local = localEndpoint, + .clusterId = cluster, + .remote = remoteEndpoint, + .nodeId = node, + }; + return entry; + } + + static EmberBindingTableEntry ForGroup(chip::FabricIndex fabric, chip::GroupId group, chip::EndpointId localEndpoint, + chip::ClusterId cluster) + { + EmberBindingTableEntry entry = { + .type = EMBER_MULTICAST_BINDING, + .fabricIndex = fabric, + .local = localEndpoint, + .clusterId = cluster, + .remote = 0, + .groupId = group, + }; + return entry; + } + /** The type of binding. */ EmberBindingType type; + + chip::FabricIndex fabricIndex; /** The endpoint on the local node. */ chip::EndpointId local; /** A cluster ID that matches one from the local endpoint's simple descriptor. @@ -519,8 +551,6 @@ struct EmberBindingTableEntry chip::NodeId nodeId; chip::GroupId groupId; }; - /** The index of the network the binding belongs to. */ - uint8_t networkIndex; bool operator==(EmberBindingTableEntry const & other) const { @@ -539,7 +569,7 @@ struct EmberBindingTableEntry return false; } - return local == other.local && clusterId == other.clusterId && remote == other.remote && networkIndex == other.networkIndex; + return fabricIndex == other.fabricIndex && local == other.local && clusterId == other.clusterId && remote == other.remote; } };