| /* |
| * |
| * 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. |
| switch (request.path.mCommandId) |
| { |
| case ProxyConnectRequest::Id: |
| return HandleProxyConnectRequest(request, input_arguments, handler); |
| |
| case ProxyDisconnectRequest::Id: |
| 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: |
| 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(); |
| } |
| |
| CHIP_ERROR stateErr = SetCPState(kState_CPDisconnected); |
| if (stateErr != CHIP_NO_ERROR) |
| { |
| ChipLogError(Zcl, "HandleProxyDisconnectRequest: SetCPState failed: %" CHIP_ERROR_FORMAT, stateErr.Format()); |
| } |
| } |
| |
| return Status::Success; |
| } |
| |
| 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 |