| /* |
| * |
| * Copyright (c) 2024 Project CHIP Authors |
| * All rights reserved. |
| * |
| * 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 "pw_rpc/server.h" |
| #include "pw_rpc_system_server/rpc_server.h" |
| #include "pw_rpc_system_server/socket.h" |
| |
| #include <map> |
| #include <thread> |
| |
| #include "RpcClient.h" |
| #include <commands/common/IcdManager.h> |
| #include <commands/common/StayActiveSender.h> |
| #include <commands/fabric-sync/FabricSyncCommand.h> |
| #include <commands/interactive/InteractiveCommands.h> |
| #include <device_manager/DeviceManager.h> |
| #include <setup_payload/QRCodeSetupPayloadGenerator.h> |
| #include <system/SystemClock.h> |
| |
| #if defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE |
| #include "pigweed/rpc_services/FabricAdmin.h" |
| #endif |
| |
| using namespace ::chip; |
| |
| namespace admin { |
| |
| namespace { |
| |
| #if defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE |
| |
| struct ScopedNodeIdHasher |
| { |
| std::size_t operator()(const chip::ScopedNodeId & scopedNodeId) const |
| { |
| std::size_t h1 = std::hash<uint64_t>{}(scopedNodeId.GetFabricIndex()); |
| std::size_t h2 = std::hash<uint64_t>{}(scopedNodeId.GetNodeId()); |
| // Bitshifting h2 reduces collisions when fabricIndex == nodeId. |
| return h1 ^ (h2 << 1); |
| } |
| }; |
| |
| class FabricAdmin final : public rpc::FabricAdmin, public admin::PairingDelegate, public IcdManager::Delegate |
| { |
| public: |
| void OnCheckInCompleted(const app::ICDClientInfo & clientInfo) override |
| { |
| // Accessing mPendingCheckIn should only be done while holding ChipStackLock |
| assertChipStackLockedByCurrentThread(); |
| ScopedNodeId scopedNodeId = clientInfo.peer_node; |
| auto it = mPendingCheckIn.find(scopedNodeId); |
| VerifyOrReturn(it != mPendingCheckIn.end()); |
| |
| KeepActiveDataForCheckIn checkInData = it->second; |
| // Removed from pending map as check-in from this node has occured and we will handle the pending KeepActive |
| // request. |
| mPendingCheckIn.erase(scopedNodeId); |
| |
| auto timeNow = System::SystemClock().GetMonotonicTimestamp(); |
| if (timeNow > checkInData.mRequestExpiryTimestamp) |
| { |
| ChipLogError(NotSpecified, |
| "ICD check-in for device we have been waiting, came after KeepActive expiry. Request dropped for ID: " |
| "[%d:0x " ChipLogFormatX64 "]", |
| scopedNodeId.GetFabricIndex(), ChipLogValueX64(scopedNodeId.GetNodeId())); |
| return; |
| } |
| |
| // TODO https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/10448. Spec does |
| // not define what to do if we fail to send the StayActiveRequest. We are assuming that any |
| // further attempts to send a StayActiveRequest will result in a similar failure. Because |
| // there is no mechanism for us to communicate with the client that sent out the KeepActive |
| // command that there was a failure, we simply fail silently. After spec issue is |
| // addressed, we can implement what spec defines here. |
| auto onDone = [=](uint32_t promisedActiveDuration) { ActiveChanged(scopedNodeId, promisedActiveDuration); }; |
| CHIP_ERROR err = StayActiveSender::SendStayActiveCommand(checkInData.mStayActiveDurationMs, clientInfo.peer_node, |
| app::InteractionModelEngine::GetInstance(), onDone); |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogError(NotSpecified, "Failed to send StayActive command %s", err.AsString()); |
| } |
| } |
| |
| void OnCommissioningComplete(NodeId deviceId, CHIP_ERROR err) override |
| { |
| if (mNodeId != deviceId) |
| { |
| ChipLogError(NotSpecified, |
| "Tried to pair a non-bridge device (0x:" ChipLogFormatX64 ") with result: %" CHIP_ERROR_FORMAT, |
| ChipLogValueX64(deviceId), err.Format()); |
| return; |
| } |
| else |
| { |
| ChipLogProgress(NotSpecified, "Reverse commission succeeded for NodeId:" ChipLogFormatX64, ChipLogValueX64(mNodeId)); |
| } |
| |
| if (err == CHIP_NO_ERROR) |
| { |
| DeviceManager::Instance().SetRemoteBridgeNodeId(deviceId); |
| } |
| else |
| { |
| ChipLogError(NotSpecified, "Failed to pair bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT, |
| ChipLogValueX64(deviceId), err.Format()); |
| } |
| |
| mNodeId = kUndefinedNodeId; |
| } |
| |
| pw::Status OpenCommissioningWindow(const chip_rpc_DeviceCommissioningWindowInfo & request, |
| chip_rpc_OperationStatus & response) override |
| { |
| VerifyOrReturnValue(request.has_id, pw::Status::InvalidArgument()); |
| ScopedNodeId scopedNodeId(request.id.node_id, request.id.fabric_index); |
| uint32_t iterations = request.iterations; |
| uint16_t discriminator = request.discriminator; |
| uint16_t commissioningTimeoutSec = static_cast<uint16_t>(request.commissioning_timeout); |
| |
| // Log the request details for debugging |
| ChipLogProgress(NotSpecified, |
| "Received OpenCommissioningWindow request: NodeId " ChipLogFormatX64 |
| ", Timeout: %u, Iterations: %u, Discriminator: %u", |
| ChipLogValueX64(scopedNodeId.GetNodeId()), commissioningTimeoutSec, iterations, discriminator); |
| |
| // Open the device commissioning window using raw binary data for salt and verifier |
| DeviceManager::Instance().OpenDeviceCommissioningWindow(scopedNodeId, iterations, commissioningTimeoutSec, discriminator, |
| ByteSpan(request.salt.bytes, request.salt.size), |
| ByteSpan(request.verifier.bytes, request.verifier.size)); |
| |
| response.success = true; |
| |
| return pw::OkStatus(); |
| } |
| |
| pw::Status CommissionNode(const chip_rpc_DeviceCommissioningInfo & request, pw_protobuf_Empty & response) override |
| { |
| char saltHex[Crypto::kSpake2p_Max_PBKDF_Salt_Length * 2 + 1]; |
| Encoding::BytesToHex(request.salt.bytes, request.salt.size, saltHex, sizeof(saltHex), Encoding::HexFlags::kNullTerminate); |
| |
| ChipLogProgress(NotSpecified, "Received CommissionNode request"); |
| |
| SetupPayload setupPayload = SetupPayload(); |
| |
| setupPayload.setUpPINCode = request.setup_pin; |
| setupPayload.version = 0; |
| setupPayload.vendorID = request.vendor_id; |
| setupPayload.productID = request.product_id; |
| setupPayload.rendezvousInformation.SetValue(RendezvousInformationFlag::kOnNetwork); |
| |
| SetupDiscriminator discriminator{}; |
| discriminator.SetLongValue(request.discriminator); |
| setupPayload.discriminator = discriminator; |
| |
| QRCodeSetupPayloadGenerator generator(setupPayload); |
| std::string code; |
| CHIP_ERROR error = generator.payloadBase38RepresentationWithAutoTLVBuffer(code); |
| |
| if (error == CHIP_NO_ERROR) |
| { |
| mNodeId = DeviceManager::Instance().GetNextAvailableNodeId(); |
| |
| // After responding with RequestCommissioningApproval to the node where the client initiated the |
| // RequestCommissioningApproval, you need to wait for it to open a commissioning window on its bridge. |
| usleep(kCommissionPrepareTimeMs * 1000); |
| PairingManager::Instance().SetPairingDelegate(this); |
| DeviceManager::Instance().PairRemoteDevice(mNodeId, code.c_str()); |
| } |
| else |
| { |
| ChipLogError(NotSpecified, "Unable to generate pairing code for setup payload: %" CHIP_ERROR_FORMAT, error.Format()); |
| } |
| |
| return pw::OkStatus(); |
| } |
| |
| pw::Status KeepActive(const chip_rpc_KeepActiveParameters & request, pw_protobuf_Empty & response) override |
| { |
| VerifyOrReturnValue(request.has_id, pw::Status::InvalidArgument()); |
| ScopedNodeId scopedNodeId(request.id.node_id, request.id.fabric_index); |
| ChipLogProgress(NotSpecified, "Received KeepActive request: Id[%d, 0x" ChipLogFormatX64 "], %u", |
| scopedNodeId.GetFabricIndex(), ChipLogValueX64(scopedNodeId.GetNodeId()), request.stay_active_duration_ms); |
| |
| KeepActiveWorkData * data = |
| Platform::New<KeepActiveWorkData>(this, scopedNodeId, request.stay_active_duration_ms, request.timeout_ms); |
| VerifyOrReturnValue(data, pw::Status::Internal()); |
| DeviceLayer::PlatformMgr().ScheduleWork(KeepActiveWork, reinterpret_cast<intptr_t>(data)); |
| return pw::OkStatus(); |
| } |
| |
| void ScheduleSendingKeepActiveOnCheckIn(ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, uint32_t timeoutMs) |
| { |
| // Accessing mPendingCheckIn should only be done while holding ChipStackLock |
| assertChipStackLockedByCurrentThread(); |
| |
| auto timeNow = System::SystemClock().GetMonotonicTimestamp(); |
| System::Clock::Timestamp expiryTimestamp = timeNow + System::Clock::Milliseconds64(timeoutMs); |
| KeepActiveDataForCheckIn checkInData = { .mStayActiveDurationMs = stayActiveDurationMs, |
| .mRequestExpiryTimestamp = expiryTimestamp }; |
| |
| auto it = mPendingCheckIn.find(scopedNodeId); |
| if (it != mPendingCheckIn.end()) |
| { |
| checkInData.mStayActiveDurationMs = std::max(checkInData.mStayActiveDurationMs, it->second.mStayActiveDurationMs); |
| checkInData.mRequestExpiryTimestamp = std::max(checkInData.mRequestExpiryTimestamp, it->second.mRequestExpiryTimestamp); |
| } |
| |
| mPendingCheckIn[scopedNodeId] = checkInData; |
| } |
| |
| private: |
| struct KeepActiveDataForCheckIn |
| { |
| uint32_t mStayActiveDurationMs = 0; |
| System::Clock::Timestamp mRequestExpiryTimestamp; |
| }; |
| |
| struct KeepActiveWorkData |
| { |
| KeepActiveWorkData(FabricAdmin * fabricAdmin, ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, |
| uint32_t timeoutMs) : |
| mFabricAdmin(fabricAdmin), |
| mScopedNodeId(scopedNodeId), mStayActiveDurationMs(stayActiveDurationMs), mTimeoutMs(timeoutMs) |
| {} |
| |
| FabricAdmin * mFabricAdmin; |
| ScopedNodeId mScopedNodeId; |
| uint32_t mStayActiveDurationMs; |
| uint32_t mTimeoutMs; |
| }; |
| |
| static void KeepActiveWork(intptr_t arg) |
| { |
| KeepActiveWorkData * data = reinterpret_cast<KeepActiveWorkData *>(arg); |
| data->mFabricAdmin->ScheduleSendingKeepActiveOnCheckIn(data->mScopedNodeId, data->mStayActiveDurationMs, data->mTimeoutMs); |
| Platform::Delete(data); |
| } |
| |
| NodeId mNodeId = chip::kUndefinedNodeId; |
| |
| // Modifications to mPendingCheckIn should be done on the MatterEventLoop thread |
| // otherwise we would need a mutex protecting this data to prevent race as this |
| // data is accessible by both RPC thread and Matter eventloop. |
| std::unordered_map<ScopedNodeId, KeepActiveDataForCheckIn, ScopedNodeIdHasher> mPendingCheckIn; |
| }; |
| |
| FabricAdmin fabric_admin_service; |
| #endif // defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE |
| |
| void RegisterServices(pw::rpc::Server & server) |
| { |
| #if defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE |
| server.RegisterService(fabric_admin_service); |
| IcdManager::Instance().SetDelegate(&fabric_admin_service); |
| #endif |
| } |
| |
| } // namespace |
| |
| void RunRpcService() |
| { |
| pw::rpc::system_server::Init(); |
| RegisterServices(pw::rpc::system_server::Server()); |
| pw::rpc::system_server::Start(); |
| } |
| |
| void InitRpcServer(uint16_t rpcServerPort) |
| { |
| pw::rpc::system_server::set_socket_port(rpcServerPort); |
| std::thread rpc_service(RunRpcService); |
| rpc_service.detach(); |
| } |
| |
| } // namespace admin |