| /* | |
| * | |
| * Copyright (c) 2026 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 <app/clusters/commissioning-proxy-server/CommissioningProxyCluster.h> | |
| #include <app/data-model/Decode.h> | |
| #include <app/data-model/Encode.h> | |
| #include <app/server-cluster/AttributeListBuilder.h> | |
| #include <clusters/CommissioningProxy/Attributes.h> | |
| #include <clusters/CommissioningProxy/Commands.h> | |
| #include <clusters/CommissioningProxy/Metadata.h> | |
| #include <clusters/CommissioningProxy/Structs.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| #include <messaging/ExchangeContext.h> | |
| #include <platform/CommissionableDataProvider.h> // for kMaxDiscriminatorValue | |
| #include <protocols/interaction_model/StatusCode.h> | |
| #include <system/SystemClock.h> | |
| #include <system/SystemPacketBuffer.h> | |
| using chip::Protocols::InteractionModel::Status; | |
| namespace chip { | |
| namespace app { | |
| namespace Clusters { | |
| namespace CommissioningProxy { | |
| using namespace CommissioningProxy::Attributes; | |
| template <typename T> | |
| constexpr bool HasExactlyOneBitSet(T v) | |
| { | |
| static_assert(std::is_integral_v<T>, "Integral type required"); | |
| return v != 0 && (v & (v - 1)) == 0; | |
| } | |
| // Masks of all spec-defined bits for each bitmap type. | |
| // Any bit outside these masks is a reserved bit and SHALL be rejected with InvalidCommand. | |
| constexpr uint8_t kValidTransportBits = static_cast<uint8_t>(CapabilitiesBitmap::kBle) | | |
| static_cast<uint8_t>(CapabilitiesBitmap::kWiFiPAF) | static_cast<uint8_t>(CapabilitiesBitmap::kNtl); | |
| constexpr uint16_t kValidWiFiBandBits = static_cast<uint16_t>(WiFiBandBitmap::k2g4) | static_cast<uint16_t>(WiFiBandBitmap::k5g); | |
| CHIP_ERROR CommissioningProxyCluster::Startup(ServerClusterContext & context) | |
| { | |
| ReturnErrorOnFailure(DefaultServerCluster::Startup(context)); | |
| if (mFabricTable != nullptr) | |
| { | |
| ReturnErrorOnFailure(mFabricTable->AddFabricDelegate(this)); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| void CommissioningProxyCluster::OnFabricRemoved(const FabricTable & /*fabricTable*/, FabricIndex fabricIndex) | |
| { | |
| ChipLogProgress(Zcl, "CommissioningProxy: fabricIndex=%u removed; dropping its proxy state", fabricIndex); | |
| // An in-flight ProxyConnectRequest has no session yet, so cancel it separately. | |
| (void) CancelPendingConnect(fabricIndex); | |
| // Drain the fabric's sessions, telling the owning transport to drop each link. | |
| while (auto sessionId = mSessions.FindAnySessionIdOnFabric(fabricIndex)) | |
| { | |
| if (auto info = mSessions.FindSession(*sessionId)) | |
| { | |
| if (CommissioningProxyTransport * transport = FindTransport(info->transport)) | |
| { | |
| (void) transport->Disconnect(*sessionId); | |
| } | |
| } | |
| mSessions.RemoveSession(*sessionId); | |
| } | |
| // Background scans live in each driver's own registry. | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| mTransports[i]->OnFabricRemoved(fabricIndex); | |
| } | |
| } | |
| DataModel::ActionReturnStatus CommissioningProxyCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request, | |
| AttributeValueDecoder & decoder) | |
| { | |
| // These writable attributes are stored by the cluster so that change-reporting is | |
| // always the cluster's responsibility. SetAttributeValue reports only on an actual | |
| // value change. | |
| switch (request.path.mAttributeId) | |
| { | |
| case ScanMaxTime::Id: { | |
| uint8_t time; | |
| ReturnErrorOnFailure(decoder.Decode(time)); | |
| // Spec: ScanMaxTime has constraint "min 1"; reject 0 with ConstraintError. | |
| VerifyOrReturnError(time >= 1, Status::ConstraintError); | |
| SetAttributeValue(mScanMaxTime, time, ScanMaxTime::Id); | |
| return Status::Success; | |
| } | |
| case CacheTimeout::Id: { | |
| uint16_t cacheTimeout; | |
| ReturnErrorOnFailure(decoder.Decode(cacheTimeout)); | |
| // Spec: CacheTimeout has constraint "min 1"; reject 0 with ConstraintError. | |
| VerifyOrReturnError(cacheTimeout >= 1, Status::ConstraintError); | |
| SetAttributeValue(mCacheTimeout, cacheTimeout, CacheTimeout::Id); | |
| return Status::Success; | |
| } | |
| default: | |
| return Protocols::InteractionModel::Status::UnsupportedWrite; | |
| } | |
| } | |
| DataModel::ActionReturnStatus CommissioningProxyCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request, | |
| AttributeValueEncoder & encoder) | |
| { | |
| switch (request.path.mAttributeId) | |
| { | |
| case Globals::Attributes::ClusterRevision::Id: | |
| // Per spec ClusterRevision SHALL be the highest revision number in the | |
| // Revision History table; sourced from the cluster's generated Metadata | |
| // (CommissioningProxy::kRevision in Metadata.h). | |
| return encoder.Encode(kRevision); | |
| case FeatureMap::Id: | |
| return encoder.Encode(mFeatureFlags); | |
| case Transport::Id: | |
| return encoder.Encode(GetSupportedTransports()); | |
| case MaxSessions::Id: | |
| return encoder.Encode(mMaxSessions); | |
| case ScanMaxTime::Id: | |
| return encoder.Encode(mScanMaxTime); | |
| case MaxCachedResults::Id: | |
| return encoder.Encode(mMaxCachedResults); | |
| case NumCachedResults::Id: | |
| return encoder.Encode(mScanCache.Count()); | |
| case CacheTimeout::Id: | |
| return encoder.Encode(mCacheTimeout); | |
| case CachedResults::Id: | |
| return mScanCache.Encode(encoder); | |
| case WiFiBand::Id: | |
| return encoder.Encode(mSupportedWiFiBands); | |
| default: | |
| return Status::UnsupportedAttribute; | |
| } | |
| } | |
| chip::BitMask<CapabilitiesBitmap> CommissioningProxyCluster::GetSupportedTransports() const | |
| { | |
| // A transport is supported iff a driver is registered for it. | |
| chip::BitMask<CapabilitiesBitmap> supported; | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| supported.Set(mTransports[i]->GetTransportType()); | |
| } | |
| return supported; | |
| } | |
| CommissioningProxyTransport * CommissioningProxyCluster::FindTransport(CapabilitiesBitmap bit) const | |
| { | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| if (mTransports[i]->GetTransportType() == bit) | |
| { | |
| return mTransports[i]; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| std::optional<DataModel::ActionReturnStatus> CommissioningProxyCluster::InvokeCommand(const DataModel::InvokeRequest & request, | |
| TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| using namespace Commands; | |
| // Each handler returns its own std::optional result: std::nullopt when the | |
| // delegate has taken ownership of the response (sync or async AddResponse) so | |
| // the framework must not add a duplicate status, or a concrete status otherwise. | |
| // ProxyConnectRequest, ProxyDisconnectRequest and ProxyMessageRequest must fail with | |
| // UNSUPPORTED_ACCESS when not invoked over a CASE session. Fabric scoping does not cover | |
| // this: the interaction model only rejects an undefined accessing fabric, and both a group | |
| // session and a PASE session that has been through AddNOC carry a defined one. So the | |
| // session has to be checked by auth mode. The scan commands carry no CASE requirement. | |
| switch (request.path.mCommandId) | |
| { | |
| case ProxyConnectRequest::Id: | |
| VerifyOrReturnValue(request.subjectDescriptor.authMode == Access::AuthMode::kCase, Status::UnsupportedAccess); | |
| return HandleProxyConnectRequest(request, input_arguments, handler); | |
| case ProxyDisconnectRequest::Id: | |
| VerifyOrReturnValue(request.subjectDescriptor.authMode == Access::AuthMode::kCase, Status::UnsupportedAccess); | |
| return HandleProxyDisconnectRequest(request, input_arguments, handler); | |
| case ProxyScanRequest::Id: | |
| return HandleProxyScanRequest(request, input_arguments, handler); | |
| case ProxyBackGroundScanStartRequest::Id: | |
| return HandleProxyBackGroundScanStartRequest(request, input_arguments, handler); | |
| case ProxyBackGroundScanStopRequest::Id: | |
| return HandleProxyBackGroundScanStopRequest(request, input_arguments, handler); | |
| case ProxyMessageRequest::Id: | |
| VerifyOrReturnValue(request.subjectDescriptor.authMode == Access::AuthMode::kCase, Status::UnsupportedAccess); | |
| return HandleProxyMessageRequest(request, input_arguments, handler); | |
| default: | |
| return Status::UnsupportedCommand; | |
| } | |
| } | |
| std::optional<DataModel::ActionReturnStatus> | |
| CommissioningProxyCluster::HandleProxyConnectRequest(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| Commands::ProxyConnectRequest::DecodableType commandData; | |
| ReturnErrorOnFailure(commandData.Decode(input_arguments, request.GetAccessingFabricIndex())); | |
| // Only a single transport SHALL be selected per spec | |
| VerifyOrReturnError(HasExactlyOneBitSet(commandData.transport.Raw()), Status::InvalidCommand); | |
| // Transport must not contain reserved (undefined) bits. Checked before the supported | |
| // set below so a reserved bit is rejected the same way here as it is on the scan | |
| // commands, rather than being reported as an unsupported transport. | |
| VerifyOrReturnError((commandData.transport.Raw() & ~kValidTransportBits) == 0, Status::InvalidCommand); | |
| // Spec: Discriminator field constraint is "0 to 4095" (12-bit); any other | |
| // field being invalid SHALL return InvalidCommand. Matches the validation in | |
| // AdministratorCommissioning / JointFabricAdministrator. | |
| VerifyOrReturnError(commandData.discriminator <= kMaxDiscriminatorValue, Status::InvalidCommand); | |
| // The selected transport must be supported by this proxy instance. | |
| if (!GetSupportedTransports().HasAny(commandData.transport)) | |
| { | |
| ChipLogError(Zcl, "Commissioning Proxy: requested transport not supported by this instance"); | |
| return Status::InvalidTransportType; | |
| } | |
| // WiFiBand is meaningful only with the WiFiPAF transport per spec; if the | |
| // selected transport is not kWiFiPAF, the field must be absent. | |
| if (commandData.wiFiBand.HasValue() && commandData.transport != CapabilitiesBitmap::kWiFiPAF) | |
| { | |
| ChipLogError(Zcl, "Commissioning Proxy: WiFiBand provided with non-WiFiPAF transport"); | |
| return Status::InvalidCommand; | |
| } | |
| // WiFiBand is only valid when the WI feature is enabled per spec. | |
| if (commandData.wiFiBand.HasValue()) | |
| { | |
| VerifyOrReturnError(mFeatureFlags.Has(Feature::kWiFiNetworkInterface), Status::InvalidCommand); | |
| // WiFiBand must not contain reserved bits. | |
| VerifyOrReturnError((commandData.wiFiBand.Value().Raw() & ~kValidWiFiBandBits) == 0, Status::InvalidCommand); | |
| // WiFiBand must be a subset of the bands supported by this proxy. | |
| if ((commandData.wiFiBand.Value().Raw() & ~mSupportedWiFiBands.Raw()) != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: Requested WiFiBand not in supported bands"); | |
| return Status::InvalidTransportType; | |
| } | |
| } | |
| // Spec: "If MaxSessions are in use, a RESOURCE_EXHAUSTED status SHALL be | |
| // returned." Enforced here so the check covers established sessions and any | |
| // in-flight connect across all transports (GetActiveSessionCount()). | |
| if (GetActiveSessionCount() >= mMaxSessions) | |
| { | |
| ChipLogError(Zcl, "Commissioning Proxy: MaxSessions reached (%u/%u)", GetActiveSessionCount(), mMaxSessions); | |
| return Status::ResourceExhausted; | |
| } | |
| // The transport driver establishes the connection, allocates/registers a session | |
| // via Sessions(), and calls handler->AddResponse() with the ProxyConnectResponse | |
| // (sync or async). The state transition to kState_CPConnected happens in the | |
| // driver's success path once the transport is actually up. | |
| CommissioningProxyTransport * transport = FindTransport(static_cast<CapabilitiesBitmap>(commandData.transport.Raw())); | |
| VerifyOrReturnValue(transport != nullptr, Status::InvalidTransportType); | |
| auto status = transport->Connect(handler, request, commandData.discriminator, System::Clock::Seconds16(commandData.timeout)); | |
| // On error, surface the status; on success the driver owns the response, so | |
| // return nullopt to avoid a duplicate framework status. | |
| VerifyOrReturnValue(status == Status::Success, DataModel::ActionReturnStatus(status)); | |
| return std::nullopt; | |
| } | |
| std::optional<DataModel::ActionReturnStatus> | |
| CommissioningProxyCluster::HandleProxyDisconnectRequest(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| Commands::ProxyDisconnectRequest::DecodableType commandData; | |
| ReturnErrorOnFailure(commandData.Decode(input_arguments, request.GetAccessingFabricIndex())); | |
| // A null SessionID SHALL cancel any ongoing ProxyConnectRequest for the invoking fabric, not disconnect a session. | |
| if (commandData.sessionID.IsNull()) | |
| { | |
| ChipLogProgress(Zcl, "HandleProxyDisconnectRequest: sessionID=null (cancel pending connect)"); | |
| return CancelPendingConnect(request.subjectDescriptor.fabricIndex); | |
| } | |
| const uint16_t sessionId = commandData.sessionID.Value(); | |
| ChipLogProgress(Zcl, "HandleProxyDisconnectRequest: sessionID=0x%04x", sessionId); | |
| auto info = mSessions.FindSession(sessionId); | |
| VerifyOrReturnValue(info.has_value(), Status::NotFound); | |
| // Per spec, a session may only be disconnected by the fabric that owns it. | |
| VerifyOrReturnValue(request.subjectDescriptor.fabricIndex == info->fabricIndex, Status::NotFound); | |
| const CapabilitiesBitmap sessTransport = info->transport; | |
| if (CommissioningProxyTransport * transport = FindTransport(sessTransport)) | |
| { | |
| auto s = transport->Disconnect(sessionId); | |
| // On failure, leave the session intact and surface the error. | |
| VerifyOrReturnValue(s == Status::Success, DataModel::ActionReturnStatus(s)); | |
| } | |
| // Drop the session record (and any pending ProxyMessage, without answering it). | |
| mSessions.RemoveSession(sessionId); | |
| // With MaxSessions > 1 several sessions may be open at once. Only transition the | |
| // cluster back to disconnected, and notify transports, once the last session is | |
| // gone; otherwise the proxy would report disconnected while sessions are active. | |
| if (GetActiveSessionCount() == 0) | |
| { | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| mTransports[i]->OnAllSessionsClosed(); | |
| } | |
| } | |
| SetDisconnectedIfLastSession(); | |
| return Status::Success; | |
| } | |
| void CommissioningProxyCluster::SetDisconnectedIfLastSession() | |
| { | |
| VerifyOrReturn(GetActiveSessionCount() == 0); | |
| CHIP_ERROR stateErr = SetCPState(kState_CPDisconnected); | |
| if (stateErr != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "SetDisconnectedIfLastSession: SetCPState failed: %" CHIP_ERROR_FORMAT, stateErr.Format()); | |
| } | |
| } | |
| std::optional<DataModel::ActionReturnStatus> | |
| CommissioningProxyCluster::HandleProxyScanRequest(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| Commands::ProxyScanRequest::DecodableType commandData; | |
| ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData)); | |
| // Transport must be non-zero — at least one transport must be selected. | |
| VerifyOrReturnError(commandData.transport.Raw() != 0, Status::InvalidCommand); | |
| // Transport must not contain reserved (undefined) bits. | |
| VerifyOrReturnError((commandData.transport.Raw() & ~kValidTransportBits) == 0, Status::InvalidCommand); | |
| // Each selected transport must be supported by this proxy instance. | |
| // The supported set is derived from enabled feature flags (same as the Transport attribute). | |
| { | |
| auto supported = GetSupportedTransports(); | |
| if ((commandData.transport.Raw() & ~supported.Raw()) != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: one or more requested transports not supported by this instance"); | |
| return Status::InvalidTransportType; | |
| } | |
| } | |
| if (commandData.wiFiBands.HasValue()) | |
| { | |
| // WiFiBands field is only valid when the WI feature is enabled. | |
| VerifyOrReturnError(mFeatureFlags.Has(Feature::kWiFiNetworkInterface), Status::InvalidCommand); | |
| // WiFiBands must not contain reserved bits. | |
| VerifyOrReturnError((commandData.wiFiBands.Value().Raw() & ~kValidWiFiBandBits) == 0, Status::InvalidCommand); | |
| // WiFiBands must be a subset of the bands supported by this proxy. | |
| if ((commandData.wiFiBands.Value().Raw() & ~mSupportedWiFiBands.Raw()) != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: Requested WiFiBand not in supported bands"); | |
| return Status::InvalidTransportType; | |
| } | |
| } | |
| // A ProxyScanRequest MAY select multiple transports: scan each requested, | |
| // registered transport in parallel and aggregate into one ProxyScanResponse. | |
| if (mScanAggregator.InProgress()) | |
| { | |
| return Status::Busy; | |
| } | |
| const System::Clock::Seconds16 scanMaxTime{ mScanMaxTime }; | |
| if (CHIP_ERROR err = mScanAggregator.Begin(handler, request.path, scanMaxTime); err != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: could not begin scan aggregation: %" CHIP_ERROR_FORMAT, err.Format()); | |
| return Status::Failure; | |
| } | |
| // First non-success from a sub-scan that could not start; only returned if no | |
| // sub-scan starts at all. | |
| auto firstError = Status::Success; | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| CommissioningProxyTransport * transport = mTransports[i]; | |
| if (!commandData.transport.Has(transport->GetTransportType())) | |
| { | |
| continue; | |
| } | |
| auto s = transport->Scan(scanMaxTime); | |
| if (s == Status::Success) | |
| { | |
| mScanAggregator.AddPendingContributor(); | |
| } | |
| else if (firstError == Status::Success) | |
| { | |
| firstError = s; | |
| } | |
| } | |
| if (mScanAggregator.PendingContributors() == 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: no requested transport scan could be started"); | |
| mScanAggregator.Abort(); | |
| return (firstError == Status::Success) ? DataModel::ActionReturnStatus(Status::Failure) | |
| : DataModel::ActionReturnStatus(firstError); | |
| } | |
| // The aggregator owns the exchange now; keep it alive for the full scan window. | |
| handler->FlushAcksRightAwayOnSlowCommand(); | |
| if (auto * exchange = handler->GetExchangeContext()) | |
| { | |
| exchange->SetResponseTimeout(scanMaxTime + System::Clock::Seconds16(5)); | |
| } | |
| // If every started sub-scan already reported synchronously, emit now. | |
| mScanAggregator.MaybeEmitIfComplete(); | |
| return std::nullopt; | |
| } | |
| std::optional<DataModel::ActionReturnStatus> | |
| CommissioningProxyCluster::HandleProxyBackGroundScanStartRequest(const DataModel::InvokeRequest & request, | |
| TLV::TLVReader & input_arguments, CommandHandler * handler) | |
| { | |
| Commands::ProxyBackGroundScanStartRequest::DecodableType commandData; | |
| ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData)); | |
| // Transport must be non-zero — at least one transport must be selected. | |
| VerifyOrReturnError(commandData.transport.Raw() != 0, Status::InvalidCommand); | |
| // Transport must not contain reserved (undefined) bits. | |
| VerifyOrReturnError((commandData.transport.Raw() & ~kValidTransportBits) == 0, Status::InvalidCommand); | |
| // Each selected transport must be supported by this proxy instance. | |
| // The supported set is derived from enabled feature flags (same as the Transport attribute). | |
| { | |
| auto supported = GetSupportedTransports(); | |
| if ((commandData.transport.Raw() & ~supported.Raw()) != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: one or more requested transports not supported by this instance"); | |
| return Status::InvalidTransportType; | |
| } | |
| } | |
| chip::BitMask<WiFiBandBitmap> wiFiBands; | |
| if (commandData.wiFiBands.HasValue()) | |
| { | |
| // WiFiBands field is only valid when the WI feature is enabled. | |
| VerifyOrReturnError(mFeatureFlags.Has(Feature::kWiFiNetworkInterface), Status::InvalidCommand); | |
| // WiFiBands must not contain reserved bits. | |
| VerifyOrReturnError((commandData.wiFiBands.Value().Raw() & ~kValidWiFiBandBits) == 0, Status::InvalidCommand); | |
| // WiFiBands must be a subset of the bands supported by this proxy. | |
| if ((commandData.wiFiBands.Value().Raw() & ~mSupportedWiFiBands.Raw()) != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: Requested WiFiBand not in supported bands"); | |
| return Status::InvalidTransportType; | |
| } | |
| wiFiBands = commandData.wiFiBands.Value(); | |
| } | |
| const FabricIndex fabricIndex = request.subjectDescriptor.fabricIndex; | |
| const NodeId nodeId = request.subjectDescriptor.subject; | |
| // A background scan MAY select multiple transports. Either all of them start, or | |
| // the ones that did are stopped again, so a failed command leaves nothing running. | |
| auto result = Status::Success; | |
| chip::BitMask<CapabilitiesBitmap> started; | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| CommissioningProxyTransport * transport = mTransports[i]; | |
| if (!commandData.transport.Has(transport->GetTransportType())) | |
| { | |
| continue; | |
| } | |
| result = transport->BgScanStart(System::Clock::Seconds16(commandData.timeout), wiFiBands, fabricIndex, nodeId); | |
| if (result != Status::Success) | |
| { | |
| break; | |
| } | |
| started.Set(transport->GetTransportType()); | |
| } | |
| if (result != Status::Success && started.Raw() != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: background scan start failed; rolling back started transport(s)"); | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| CommissioningProxyTransport * transport = mTransports[i]; | |
| if (started.Has(transport->GetTransportType())) | |
| { | |
| auto stopStatus = transport->BgScanStop(chip::BitMask<CapabilitiesBitmap>(transport->GetTransportType()), wiFiBands, | |
| fabricIndex, nodeId); | |
| if (stopStatus != Status::Success) | |
| { | |
| // The scan we just started is still running and its cached results | |
| // were not cleared; the commissioner has been told nothing started. | |
| ChipLogError(Zcl, "CommissioningProxy: rollback of transport 0x%x failed with status 0x%02x", | |
| chip::to_underlying(transport->GetTransportType()), chip::to_underlying(stopStatus)); | |
| } | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| std::optional<DataModel::ActionReturnStatus> | |
| CommissioningProxyCluster::HandleProxyBackGroundScanStopRequest(const DataModel::InvokeRequest & request, | |
| TLV::TLVReader & input_arguments, CommandHandler * handler) | |
| { | |
| Commands::ProxyBackGroundScanStopRequest::DecodableType commandData; | |
| ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData)); | |
| // transport=0 and no wiFiBands → nothing to stop | |
| VerifyOrReturnError(commandData.transport.Raw() != 0 || commandData.wiFiBands.HasValue(), Status::InvalidCommand); | |
| // Reserved bits in transport must be clear | |
| VerifyOrReturnError((commandData.transport.Raw() & ~kValidTransportBits) == 0, Status::InvalidCommand); | |
| // Each non-zero transport bit must be in the supported set. | |
| // transport=0 (band-only stop) skips this check — there are no unsupported bits in zero. | |
| { | |
| auto supported = GetSupportedTransports(); | |
| if ((commandData.transport.Raw() & ~supported.Raw()) != 0) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: one or more requested transports not supported by this instance"); | |
| return Status::InvalidTransportType; | |
| } | |
| } | |
| chip::BitMask<WiFiBandBitmap> wiFiBands; | |
| if (commandData.wiFiBands.HasValue()) | |
| { | |
| // WiFiBands field is only valid when the WI feature is enabled. | |
| VerifyOrReturnError(mFeatureFlags.Has(Feature::kWiFiNetworkInterface), Status::InvalidCommand); | |
| // Reserved bits must be clear. | |
| VerifyOrReturnError((commandData.wiFiBands.Value().Raw() & ~kValidWiFiBandBits) == 0, Status::InvalidCommand); | |
| wiFiBands = commandData.wiFiBands.Value(); | |
| } | |
| const FabricIndex fabricIndex = request.subjectDescriptor.fabricIndex; | |
| const NodeId nodeId = request.subjectDescriptor.subject; | |
| // Fan the stop out to every registered transport with the full request mask; | |
| // each matches it against its own per-fabric record (a driver may partially | |
| // stop, e.g. one band of several) and returns NotFound if nothing matched. | |
| // NotFound is returned to the commissioner only if no transport matched at all. | |
| bool matched = false; | |
| auto err = Status::Success; | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| auto s = mTransports[i]->BgScanStop(commandData.transport, wiFiBands, fabricIndex, nodeId); | |
| if (s != Status::NotFound) | |
| { | |
| matched = true; | |
| if (s != Status::Success) | |
| { | |
| err = s; | |
| } | |
| } | |
| } | |
| return matched ? err : Status::NotFound; | |
| } | |
| std::optional<DataModel::ActionReturnStatus> | |
| CommissioningProxyCluster::HandleProxyMessageRequest(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| Commands::ProxyMessageRequest::DecodableType commandData; | |
| ReturnErrorOnFailure(commandData.Decode(input_arguments, request.GetAccessingFabricIndex())); | |
| const uint16_t sessionId = commandData.sessionID; | |
| auto info = mSessions.FindSession(sessionId); | |
| VerifyOrReturnValue(info.has_value(), Status::NotFound); | |
| // A session's messages may only be forwarded by the fabric that owns it. | |
| VerifyOrReturnValue(request.subjectDescriptor.fabricIndex == info->fabricIndex, Status::NotFound); | |
| // Per spec: a null/empty message is the Commissioner polling for a queued | |
| // response. Answer immediately with a null message. | |
| if (commandData.message.IsNull() || commandData.message.Value().empty()) | |
| { | |
| Commands::ProxyMessageResponse::Type pollResponse; | |
| pollResponse.sessionID = sessionId; | |
| pollResponse.message.SetNull(); | |
| handler->AddResponse(request.path, pollResponse); | |
| return std::nullopt; | |
| } | |
| CommissioningProxyTransport * transport = FindTransport(info->transport); | |
| VerifyOrReturnValue(transport != nullptr, Status::Failure); | |
| System::PacketBufferHandle buf = | |
| System::PacketBufferHandle::NewWithData(commandData.message.Value().data(), commandData.message.Value().size()); | |
| VerifyOrReturnValue(!buf.IsNull(), Status::Failure); | |
| // Per spec ResponseTimeout=0: forward best-effort and answer immediately. | |
| if (commandData.responseTimeout == 0) | |
| { | |
| CHIP_ERROR sendErr = transport->SendMessage(sessionId, std::move(buf)); | |
| if (sendErr != CHIP_NO_ERROR) | |
| { | |
| ChipLogDetail(Zcl, "ProxyMessageRequest(ResponseTimeout=0): SendMessage failed: %" CHIP_ERROR_FORMAT, sendErr.Format()); | |
| } | |
| Commands::ProxyMessageResponse::Type immediate; | |
| immediate.sessionID = sessionId; | |
| immediate.message.SetNull(); | |
| handler->AddResponse(request.path, immediate); | |
| return std::nullopt; | |
| } | |
| // Keep the exchange open until the commissionee replies (or the timeout fires). | |
| auto begin = mSessions.BeginMessage(sessionId, handler, request, commandData.responseTimeout); | |
| VerifyOrReturnValue(begin == Status::Success, DataModel::ActionReturnStatus(begin)); | |
| CHIP_ERROR err = transport->SendMessage(sessionId, std::move(buf)); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "ProxyMessageRequest: SendMessage failed: %" CHIP_ERROR_FORMAT, err.Format()); | |
| mSessions.AbortPending(sessionId); | |
| return DataModel::ActionReturnStatus(Status::Failure); | |
| } | |
| // Reply is delivered asynchronously via Sessions().DispatchMessageResponse(). | |
| return std::nullopt; | |
| } | |
| Protocols::InteractionModel::Status CommissioningProxyCluster::CancelPendingConnect(FabricIndex fabricIndex) | |
| { | |
| // A null-SessionID ProxyDisconnectRequest cancels the invoking fabric's own | |
| // pending connect(s). Every transport is offered the cancel (no early return): | |
| // the spec says null cancels *any* ongoing connect for the fabric, and a foreign | |
| // connect on one transport must not mask this fabric's connect on another. | |
| // Success — a connect owned by this fabric was cancelled | |
| // NotFound — a connect is pending but owned by a different fabric | |
| // InvalidInState — no connect pending on that transport | |
| bool cancelledOwn = false; | |
| bool sawForeignPending = false; | |
| for (size_t i = 0; i < mTransportCount; i++) | |
| { | |
| auto s = mTransports[i]->CancelPendingConnect(fabricIndex); | |
| if (s == Status::Success) | |
| { | |
| cancelledOwn = true; | |
| } | |
| else if (s == Status::NotFound) | |
| { | |
| sawForeignPending = true; | |
| } | |
| } | |
| if (cancelledOwn) | |
| { | |
| return Status::Success; | |
| } | |
| return sawForeignPending ? Status::NotFound : Status::InvalidInState; | |
| } | |
| CHIP_ERROR CommissioningProxyCluster::Attributes(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) | |
| { | |
| static constexpr DataModel::AttributeEntry optionalAttributes[] = { | |
| MaxCachedResults::kMetadataEntry, NumCachedResults::kMetadataEntry, CacheTimeout::kMetadataEntry, | |
| CachedResults::kMetadataEntry, WiFiBand::kMetadataEntry, | |
| }; | |
| AttributeListBuilder listBuilder(builder); | |
| return listBuilder.Append(Span(kMandatoryMetadata), Span(optionalAttributes), mEnabledOptionalAttributes); | |
| } | |
| CHIP_ERROR CommissioningProxyCluster::AcceptedCommands(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) | |
| { | |
| ReturnErrorOnFailure( | |
| builder.AppendElements({ Commands::ProxyConnectRequest::kMetadataEntry, Commands::ProxyDisconnectRequest::kMetadataEntry, | |
| Commands::ProxyScanRequest::kMetadataEntry, Commands::ProxyMessageRequest::kMetadataEntry })); | |
| if (mFeatureFlags.Has(Feature::kBackgroundScan)) | |
| { | |
| ReturnErrorOnFailure(builder.AppendElements({ | |
| Commands::ProxyBackGroundScanStartRequest::kMetadataEntry, | |
| Commands::ProxyBackGroundScanStopRequest::kMetadataEntry, | |
| })); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR CommissioningProxyCluster::GeneratedCommands(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<CommandId> & builder) | |
| { | |
| // The three response commands the server can generate. All are mandatory and | |
| // independent of the feature flags (the BackgroundScan commands respond with a | |
| // plain status, not a generated command). | |
| return builder.AppendElements({ | |
| Commands::ProxyConnectResponse::Id, | |
| Commands::ProxyScanResponse::Id, | |
| Commands::ProxyMessageResponse::Id, | |
| }); | |
| } | |
| CHIP_ERROR CommissioningProxyCluster::SetCPState(CommissioningProxyCluster::State state) | |
| { | |
| mMainCommissioningProxyState = state; | |
| return CHIP_NO_ERROR; | |
| } | |
| CommissioningProxyCluster::State CommissioningProxyCluster::GetCPState() | |
| { | |
| return mMainCommissioningProxyState; | |
| } | |
| } // namespace CommissioningProxy | |
| } // namespace Clusters | |
| } // namespace app | |
| } // namespace chip |