tree: 195b10e7889b6e052ea0a049e72b6a8733e995fb [search]
  1. tests/
  2. app_config_dependent_sources.cmake
  3. app_config_dependent_sources.gni
  4. BUILD.gn
  5. CodegenIntegration.cpp
  6. CodegenIntegration.h
  7. CommissioningProxyBgScanRegistry.cpp
  8. CommissioningProxyBgScanRegistry.h
  9. CommissioningProxyBleAdapter.h
  10. CommissioningProxyBleTransport.cpp
  11. CommissioningProxyBleTransport.h
  12. CommissioningProxyCluster.cpp
  13. CommissioningProxyCluster.h
  14. CommissioningProxyPafAdapter.h
  15. CommissioningProxyPafTransport.cpp
  16. CommissioningProxyPafTransport.h
  17. CommissioningProxyScanAggregator.cpp
  18. CommissioningProxyScanAggregator.h
  19. CommissioningProxyScanCache.cpp
  20. CommissioningProxyScanCache.h
  21. CommissioningProxySessionManager.cpp
  22. CommissioningProxySessionManager.h
  23. CommissioningProxyTestEventTriggerHandler.h
  24. CommissioningProxyTransport.h
  25. README.md
src/app/clusters/commissioning-proxy-server/README.md

Commissioning Proxy Cluster

The Commissioning Proxy cluster (cluster ID 0x0455) provides a proxy service that allows a Commissioner to use commissioning transports not supported locally, or to extend its commissioning range. Commissioners can use the proxy to discover and establish a connection to commissionable devices that are reachable by the proxy. The proxy connection acts as a tunnel through which the Commissioner can run a PASE session with the commissionable device and ultimately commission it.

Overview

This directory contains an implementation of the Matter Commissioning Proxy cluster server (CommissioningProxyCluster.{h,cpp}) using the code-driven DefaultServerCluster pattern.

The cluster owns all transport-agnostic behavior: command validation, feature-based attribute/command gating, transport/WiFiBand constraint checks, the writable-attribute storage and change-reporting, and — via three subsystems it composes — proxy-session bookkeeping, ProxyMessage routing, the background-scan result cache, and multi-transport ProxyScanRequest aggregation:

SubsystemResponsibility
CommissioningProxySessionManagerSession-id allocation, id→{transport,fabric} table, per-fabric isolation, ProxyMessage routing + response-timeout timer
CommissioningProxyScanCacheCachedResults / NumCachedResults (one entry per device, per-entry TTL, MaxCachedResults cap)
CommissioningProxyScanAggregatorCombines a multi-transport ProxyScanRequest into one ProxyScanResponse

A fourth transport-agnostic component, CommissioningProxyBgScanRegistry, ships with the cluster but is instantiated per transport and owned by the driver rather than composed by the cluster: ProxyBackGroundScanStartRequest/ProxyBackGroundScanStopRequest fan out to every matching driver, so the per-fabric records they arbitrate are necessarily transport-local. It holds the per-fabric scan requests and their lifetime timers, the spec transport/band overlap arithmetic on Stop, and the paused/deferred state used while the radio is held by a connect or foreground scan — leaving the driver only the hardware start/stop. See Background scanning.

The application supplies only the platform-specific transport work. A BLE driver, CommissioningProxyBleTransport, ships with the cluster and needs only a CommissioningProxyBleAdapter from the application; for any other physical transport the application implements the CommissioningProxyTransport driver interface itself and registers it with the cluster. There is no do-everything application delegate: static device capabilities are passed to the constructor via Config, writable-attribute state is owned by the cluster, and the driver is scoped to transport actions only. See Architecture below.

How it works

A Commissioner that lacks (or is out of range of) the transport a commissionable device advertises on uses the proxy as a tunnel. The flow is:

  1. Discover — the Commissioner sends ProxyScanRequest (or subscribes to the background-scan cache); the proxy scans its local transports and returns the commissionable devices it can see as ScanResultStruct entries.
  2. Connect — the Commissioner sends ProxyConnectRequest naming a single transport plus the target device (address/discriminator/VID/PID). The proxy opens a transport connection to the device and replies with a unique SessionID in the ProxyConnectResponse.
  3. Tunnel — the Commissioner drives the PASE handshake (and the rest of commissioning) by sending each Matter packet in a ProxyMessageRequest keyed by SessionID; the proxy forwards it over the transport and returns the device's reply in the ProxyMessageResponse. The proxy is a dumb relay — the PASE session is end-to-end between the Commissioner and the device.
  4. Disconnect — the Commissioner sends ProxyDisconnectRequest with the SessionID to close an established session, or with a null SessionID to cancel an in-flight connect; the proxy tears the transport connection down.

The cluster server itself is transport-agnostic: it validates the requested transport against the set it advertises, then dispatches the work to the registered CommissioningProxyTransport driver whose GetTransportType() matches the request's transport bit. Today drivers exist for BLE (BTP) and Wi-Fi PAF (PAFTP) — see the transport integration sections below.

Features

The cluster supports the following optional features:

  • WiFiNetworkInterface (WI): Gates the Wi-Fi-band surface — the WiFiBand attribute and the WiFiBand(s) command fields. It does not gate the Wi-Fi PAF transport itself (see Transports below).
  • BackgroundScan (BGS): Allows background scanning of commissioning transports, with results cached in the CachedResults / NumCachedResults attributes.

Transports

The transports the proxy advertises in the Transport attribute (and accepts in ProxyConnectRequest / ProxyScanRequest) are exactly the ones for which a driver has been registered (RegisterTransport), independent of the WI feature. An application typically registers a driver per compiled-in transport:

Transport (CapabilitiesBitmap)Driver registered whenWire protocol
kBle (BTP)CONFIG_NETWORK_LAYER_BLEBTP over BLE
kWiFiPAF (PAFTP)CHIP_DEVICE_CONFIG_ENABLE_WIFIPAFPAFTP over Wi-Fi NAN

ProxyConnectRequest selects exactly one transport per session. A request naming a transport with no registered driver is rejected with INVALID_TRANSPORT_TYPE. (The spec's NTL bit is not currently supported.)

Architecture: why a driver, not a delegate

Per the SDK cluster guidance, a cluster that triggers platform-specific actions uses a Delegate (or Driver) interface, while a cluster's own attribute state is pushed through setters rather than fetched from a delegate. This cluster follows that split precisely, so instead of one do-everything delegate there are four homes for what such a delegate would otherwise hold:

ConcernLives inWhy
Transport actions (Connect / Scan / SendMessage / Disconnect / …)CommissioningProxyTransport driverThe only genuinely platform-specific surface (BlueZ GATT, wpa_supplicant NAN).
Writable attributes (ScanMaxTime, CacheTimeout) + cache viewThe cluster (members + setters) and its subsystemsThe cluster owns change-reporting, so a driver can never forget NotifyAttributeChanged.
Static capabilities (MaxSessions, MaxCachedResults, bands)Config (constructor argument)Fixed device facts, not actions.
Background-scan fabric records, band arithmetic, pause/resumeCommissioningProxyBgScanRegistry (one per driver)Transport-agnostic logic, but held per transport because background Start/Stop fan out to every matching driver.

How multiple platforms plug in. A platform provides one CommissioningProxyTransport implementation per physical transport and registers it with RegisterTransport(). The cluster dispatches each command to the driver whose GetTransportType() matches the request‘s transport bit, and drivers report async results back through the cluster’s shared subsystems (Sessions(), ScanCache(), ScanAggregator()). Background scanning is inherited the same way: the driver holds a CommissioningProxyBgScanRegistry and implements its three-method HardwareControl hook. A new platform therefore writes only its GATT/NAN driver and inherits all session/scan/message bookkeeping unchanged.

RegisterTransport() is used (rather than a single constructor-injected delegate) because there can be several drivers — one per transport — and they are typically constructed after the cluster, whose endpoint is only known at registration time.

Usage

For new applications using the CodeDrivenDataModelProvider, instantiate and register the cluster directly. This provides the most flexibility and control.

1. Implement a transport driver

BLE needs no driver of your own — register the cluster's own CommissioningProxyBleTransport (see BLE Transport Integration). For a transport the SDK does not ship, create a class that inherits from chip::app::Clusters::CommissioningProxy::CommissioningProxyTransport and implement its virtual methods — one class per physical transport; Wi-Fi PAF is the example below. A driver only handles the transport-specific work; the cluster performs all spec validation first and owns the session/scan/message bookkeeping. Methods run on the Matter task; the driver reports async results back through its host cluster (given in SetHost) via Sessions(), ScanCache(), and ScanAggregator().

#include <app/clusters/commissioning-proxy-server/CommissioningProxyTransport.h>

class MyPafTransport : public chip::app::Clusters::CommissioningProxy::CommissioningProxyTransport
{
public:
    CapabilitiesBitmap GetTransportType() const override { return CapabilitiesBitmap::kWiFiPAF; }
    void SetHost(CommissioningProxyCluster * cluster) override { mHost = cluster; }

    // Open a transport connection. On success, allocate a session via
    // mHost->Sessions().AllocSessionId()/RegisterSession() and call
    // commandObj->AddResponse() with a ProxyConnectResponse carrying the sessionID.
    Protocols::InteractionModel::Status Connect(chip::app::CommandHandler * commandObj,
                                                const DataModel::InvokeRequest & request, uint16_t discriminator,
                                                System::Clock::Seconds16 timeout) override;

    // Forward a Matter packet; deliver the reply via
    // mHost->Sessions().DispatchMessageResponse(), having first checked it
    // with ValidateCommissioneeMessage() (see below).
    CHIP_ERROR SendMessage(uint16_t sessionId, chip::System::PacketBufferHandle && buf) override;

    // Report foreground-scan results to mHost->ScanAggregator().Contribute();
    // background-scan results to mHost->ScanCache().Report().
    Protocols::InteractionModel::Status Scan(System::Clock::Seconds16 scanMaxTime) override;

    // Background scan: forward to the driver's CommissioningProxyBgScanRegistry,
    // which owns the per-fabric records, lifetime timers and band arithmetic
    // (see Background scanning below).
    Protocols::InteractionModel::Status BgScanStart(System::Clock::Seconds16 timeout, BitMask<WiFiBandBitmap> wiFiBands,
                                                    FabricIndex fabricIndex, NodeId nodeId) override;
    Protocols::InteractionModel::Status BgScanStop(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> wiFiBands,
                                                   FabricIndex fabricIndex, NodeId nodeId) override;

    // ... CancelPendingConnect / Disconnect / OnAllSessionsClosed /
    //     IsConnectPending / Shutdown

private:
    CommissioningProxyCluster * mHost = nullptr;
};

2. Instantiate the cluster and register the driver(s)

Construct the CommissioningProxyCluster for the endpoint (the endpoint id is a constructor argument; Config carries the fixed feature set and device capabilities), then register a driver per available transport.

#include <app/clusters/commissioning-proxy-server/CommissioningProxyCluster.h>
#include <app/server-cluster/ServerClusterInterfaceRegistry.h>

using namespace chip::app::Clusters::CommissioningProxy;

MyPafTransport gPafTransport;

chip::BitMask<Feature> gFeatures(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface);

// Supplies the response-timeout and scan-watchdog timers.
chip::app::DefaultTimerDelegate gTimerDelegate;

// Config: featureFlags plus the supported Wi-Fi bands. MaxSessions and
// MaxCachedResults are Fixed-quality attributes and come from
// CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS / _MAX_CACHED_RESULTS.
chip::app::RegisteredServerCluster<CommissioningProxyCluster> gCPCluster(
    CommissioningProxyEndpoint, CommissioningProxyCluster::Config(gFeatures), gTimerDelegate);

void SetUpProxy()
{
    gCPCluster.Cluster().RegisterTransport(gPafTransport); // before registration/Startup
}

3. Register the Cluster

In your application's initialization sequence, register the cluster instance with the CodegenDataModelProvider:

#include <data-model-providers/codegen/CodegenDataModelProvider.h>

void ApplicationInit()
{
    VerifyOrDie(chip::app::CodegenDataModelProvider::Instance().Registry().Register(
        gCPCluster.Registration()) == CHIP_NO_ERROR);
}

For a complete working example of the device wiring, see examples/all-devices-app/all-devices-common/device/types/commissioning-proxy/. It registers the cluster's CommissioningProxyBleTransport and supplies the platform adapters for both transports from examples/all-devices-app/posix/linux/.

Transport driver methods

A CommissioningProxyTransport driver implements the following. Each is invoked only after the cluster has validated the request; the driver does the transport-specific work and reports results back through the host cluster's subsystems.

MethodDescription
GetTransportType()The single transport bit this driver services (kBle / kWiFiPAF)
SetHost()Bind the host cluster (set to null at cluster teardown)
Connect()Open a transport session; allocate + register it via Sessions()
SendMessage()Forward a packet; reply routed back via Sessions()
Scan()Foreground scan; results reported to ScanAggregator()
BgScanStart()Start a background scan for the given (fabricIndex, nodeId); normally forwarded to the driver's CommissioningProxyBgScanRegistry::Start(). Results reported to ScanCache() (BGS)
BgScanStop()Stop a background scan for the given (fabricIndex, nodeId); normally forwarded to CommissioningProxyBgScanRegistry::Stop() (BGS)
CancelPendingConnect()Cancel an in-flight connect (null-SessionID disconnect)
Disconnect()Tear down an active proxy session
OnAllSessionsClosed()Notified when the last session across all transports closes
IsConnectPending()Whether a connect is in flight (counted against MaxSessions)
Shutdown()Cancel driver timers/state before cluster destruction

Note there is no ProxyScanRequest/ProxyMessageRequest/etc. delegate hook: those commands' spec logic, session tracking, message routing, and scan aggregation live in the cluster and its subsystems; the driver only exposes the transport primitives above.

Every message from the commissionee must be checked before it is forwarded. Call CommissioningProxySessionManager::ValidateCommissioneeMessage() on it. If that returns an error, the message must not be forwarded, and the driver must terminate the transport connection and remove the session. The spec requires both, so this is a conformance requirement, not an option. DispatchMessageResponse() repeats the check and drops invalid data, but it cannot tear the session down: that part is the driver's.

Background scanning

ProxyBackGroundScanStartRequest/ProxyBackGroundScanStopRequest are per-fabric and MAY name several transports, so the cluster validates the request (BGS/WI feature gating, reserved transport and band bits, supported bands) and then fans it out to every registered driver whose transport bit is set, passing the requesting fabric index and node id taken from the subject descriptor. Start reports the first non-success status a driver returned; Stop returns NOT_FOUND only when no driver had a matching record.

Everything a driver then has to do that is not radio work is provided by CommissioningProxyBgScanRegistry — one instance per driver, constructed with a reference to the driver's HardwareControl implementation:

Registry callResponsibility
Start()Add or refresh the (fabricIndex, nodeId) record and its transport/band mask, arm its lifetime timer, start the hardware if it is the first record
Stop()Remove the requested transports/bands from the record; stop the hardware once no records remain
Pause()Suspend the hardware scan while the radio is needed for a connect or foreground scan; records stay registered; a second call does nothing
ResumeIfNeeded()Restart a paused scan once the radio is free; no-op if not paused or if no records remain
Shutdown()Cancel every lifetime timer and stop the hardware scan if the registry owns it

Start() with timeoutSecs == 0 means no lifetime timer, i.e. scan until an explicit Stop. Stop() applies the spec's transport/band arithmetic: a zero transport mask means “stop the listed bands only”; a record left with no transports or no bands is removed; SUCCESS is returned even when nothing overlapped, and NOT_FOUND only when the fabric has no record at all.

The driver supplies the only transport-specific parts via HardwareControl:

HookContract
StartHardwareScan()Start or resume the hardware scan, wiring the driver's own discovery callback (return codes below)
StopHardwareScan()Stop the hardware scan; called only while the registry owns the radio, never while paused
ClearCachedResults()Drop cached results for the bands that stopped (host->ScanCache().ClearTransport(<transport>, bands)); 0 = all

StartHardwareScan() returns CHIP_NO_ERROR when the scan is running, CHIP_ERROR_BUSY when the radio is currently held — the registry keeps the records, stays paused and retries on the next ResumeIfNeeded() — and any other error is a hard failure that rejects the triggering Start().

A driver therefore wires background scanning up as a small HardwareControl implementation plus one registry instance, and forwards the two driver methods to it:

#include <app/clusters/commissioning-proxy-server/CommissioningProxyBgScanRegistry.h>
#include <platform/DefaultTimerDelegate.h>

// The registry drives these hooks from its own lifetime timer and from resume, i.e.
// outside the driver's call stack, so the hardware object holds its own host pointer.
// MyPafTransport::SetHost() hands it over: mHost = cluster; sHardware.SetHost(cluster);
class MyPafBgScanHardware : public CommissioningProxyBgScanRegistry::HardwareControl
{
public:
    void SetHost(CommissioningProxyCluster * host) { mHost = host; }

    // Returns CHIP_ERROR_BUSY when the single scanner is held by a connect or a
    // foreground scan; the registry then defers and retries on resume.
    CHIP_ERROR StartHardwareScan() override { return StartMyPlatformScan(OnBgScanDiscovery); }
    void StopHardwareScan() override { StopMyPlatformScan(); }

    // bands names the bands that stopped; 0 means the transport stopped entirely.
    void ClearCachedResults(BitMask<WiFiBandBitmap> bands) override
    {
        if (mHost != nullptr)
        {
            mHost->ScanCache().ClearTransport(CapabilitiesBitmap::kWiFiPAF, bands);
        }
    }

private:
    CommissioningProxyCluster * mHost = nullptr;
};

// Declared before the registry so it outlives it: the registry's destructor may
// call back into these hooks.
MyPafBgScanHardware sHardware;
chip::app::DefaultTimerDelegate sBgScanTimerDelegate;
CommissioningProxyBgScanRegistry sBgScan(sHardware, sBgScanTimerDelegate);

Status MyPafTransport::BgScanStart(System::Clock::Seconds16 timeout, BitMask<WiFiBandBitmap> wiFiBands, FabricIndex fabricIndex,
                                   NodeId nodeId)
{
    return sBgScan.Start(fabricIndex, nodeId, GetTransportType(), wiFiBands, timeout);
}

Status MyPafTransport::BgScanStop(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> wiFiBands,
                                 FabricIndex fabricIndex, NodeId nodeId)
{
    return sBgScan.Stop(fabricIndex, nodeId, transport, wiFiBands);
}

CHIP_ERROR_BUSY is the load-bearing case: on BLE the scanner is owned by a connect or foreground scan, and on Wi-Fi PAF a ProxyConnect owns the single NAN subscribe slot. A driver reports the conflict from StartHardwareScan(), calls Pause() when it takes the radio and ResumeIfNeeded() when it releases it, and the registry keeps the Commissioner's background scan registered across the gap. ResumeIfNeeded() must be wrapped in PlatformMgr().ScheduleWork() if the “radio freed” path could otherwise re-enter the driver.

Async Command Handling

ProxyConnectRequest and ProxyMessageRequest are asynchronous — they return std::nullopt from InvokeCommand to prevent an immediate response, and call CommandHandler::AddResponse() or CommandHandler::AddStatus() from a transport callback once the operation completes.

To keep the exchange alive across the async operation, store a CommandHandler::Handle in the driver's pending-operation state — not on the stack, which unwinds when InvokeCommand returns — and extend the exchange response timeout:

// mPending outlives InvokeCommand; the handle must live there, not here
mPending.handle = CommandHandler::Handle(commandObj);
if (auto * ec = commandObj->GetExchangeContext())
{
    ec->SetResponseTimeout(chip::System::Clock::Seconds16(responseTimeout + 10));
}
// … return std::nullopt from InvokeCommand …

// Later, in your transport callback:
auto * handler = mPending.handle.Get();
if (handler != nullptr)
{
    handler->AddResponse(commandPath, response);
}

BLE Transport Integration

CommissioningProxyBleTransport ships with the cluster as the separate ble-transport target, so an application that proxies over another transport — or a platform with no BLE stack — can depend on commissioning-proxy-server without pulling in src/ble. It drives a BTP connection through chip::Ble::BleLayer:

  • Connect() — on the first BLE connect the proxy flips its own BLE role from peripheral to central, then calls BleLayer::NewBleConnectionByDiscriminator() to open an L2CAP/BTP connection to the commissionee.
  • SendMessage() — calls BLEEndPoint::Send() to push the tunneled commissioning packet over BTP.
  • Disconnect() — calls BLEEndPoint::Close() to drop the connection.

Incoming BTP messages are routed back to the cluster via a ProxyBleDelegate (chip::Ble::BleLayerDelegate) that wraps the original BleLayer transport, matches the connection against the active session slots, checks the message with ValidateCommissioneeMessage() (closing the endpoint if it fails), and calls host->Sessions().DispatchMessageResponse().

The platform adapter

Two BLE operations have no portable form, so they are not in the transport: switching the local BLE role from peripheral to central, and driving a scan for commissionable devices. The application supplies both by implementing CommissioningProxyBleAdapter and passing it to the transport's constructor:

class MyBleProxyAdapter : public CommissioningProxyBleAdapter
{
    CHIP_ERROR EnableCentralRole() override;
    CHIP_ERROR StartScan(DiscoveryCallback cb, void * context) override;
    void StopScan() override;
};

MyBleProxyAdapter gBleAdapter;
CommissioningProxyBleTransport gBleTransport(gBleAdapter, gTimerDelegate);

examples/all-devices-app/posix/linux/CommissioningProxyBleAdapter.cpp is a worked implementation over BLEManagerImpl, wired up in that app's posix/linux/DeviceFactoryPlatformOverride.cpp.

Wi-Fi PAF Transport Integration

CommissioningProxyPafTransport ships with the cluster as the separate paf-transport target, on the same terms as ble-transport: an application that proxies over BLE only — or a platform with no Wi-Fi PAF stack — can depend on commissioning-proxy-server without pulling in src/wifipaf. It drives NAN and PAFTP sessions through chip::WiFiPAF::WiFiPAFLayer:

  • Connect() — calls ConnectivityMgr().WiFiPAFSubscribe() to open a PAF session to the commissionee discriminator.
  • SendMessage() — calls WiFiPAFLayer::SendMessage() to send the tunneled commissioning packet over PAFTP (the PAFTP endpoint, not the raw NAN transmit, which would bypass framing).
  • Disconnect() — calls WiFiPAFLayer::RmPafSession(), closes the PAFTP endpoint, and cancels the NAN subscribe that backed the session.

Incoming PAF messages are routed back to the cluster via a ProxyPafDelegate (chip::WiFiPAF::WiFiPAFLayerDelegate) that wraps the original transport, matches the peer against the active session slots, checks the message with ValidateCommissioneeMessage() (returning the error if it fails, which makes the PAFTP endpoint close itself), and calls host->Sessions().DispatchMessageResponse().

The platform adapter

Session setup and teardown above are portable — they go through the generic ConnectivityManager facade. Discovery is not, so it is not in the transport: scanning for commissionable devices, and recovering the subscribe id the platform assigned to a request, both need the platform implementation. The application supplies them by implementing CommissioningProxyPafAdapter:

class MyPafProxyAdapter : public CommissioningProxyPafAdapter
{
    CHIP_ERROR StartForegroundScan(System::Clock::Seconds16 window, DiscoveryCallback onDevice,
                                   ScanCompleteCallback onDone, void * context) override;
    CHIP_ERROR StartBackgroundScan(DiscoveryCallback cb, void * context) override;
    void StopBackgroundScan() override;
    uint32_t PendingConnectSubscribeId() const override;
};

MyPafProxyAdapter gPafAdapter;
CommissioningProxyPafTransport gPafTransport(gPafAdapter, gTimerDelegate);

Unlike BLE, the platform owns the scan window: StartForegroundScan() takes the duration and the adapter signals completion, so the transport arms no scan timer of its own. examples/all-devices-app/posix/linux/CommissioningProxyPafAdapter.cpp is a worked implementation over ConnectivityManagerImpl, and is also where the platform‘s peer descriptor is unpacked into the interface’s scalars so no platform type reaches the cluster.

Driving the Proxy from a Commissioner

Everything above is the proxy side. The commissioner side is chip-tool, which implements the tunnel as a pairing mode: pairing proxy establishes a CASE session to the proxy, sends ProxyConnectRequest, then carries every PASE and commissioning packet to the commissionee inside ProxyMessageRequest and ProxyMessageResponse.

The proxy must already be commissioned onto the fabric, since the commissioner needs a CASE session before it can tunnel anything:

chip-tool pairing onnetwork <proxy-node-id> 20202021

chip-tool commissioningproxy proxy-scan-request 0x0A <proxy-node-id> <endpoint> \
    --WiFiBands 0x01 --allow-large-payload true --timeout 30

chip-tool pairing proxy <node-id> <ssid> <password> <pin> <discriminator> \
    <proxy-node-id> <proxy-connect-timeout> ble|wifipaf \
    --proxy-endpoint <endpoint> [--proxy-wifi-band 2g4|5g]

ProxyScanRequest carries the large-message quality, so it needs --allow-large-payload true; the background-scan commands do not. <proxy-connect-timeout> is in seconds, with 0 meaning no timeout.

Full argument documentation, worked examples, and the background-scan commands are in the chip-tool README and Working with the CHIP Tool.

Cluster State

The cluster tracks proxy state internally:

  • kState_CPDisconnected — no active proxy session
  • kState_CPConnected — a transport session (BLE or Wi-Fi PAF) is open and ready to forward messages

State transitions:

ProxyConnectRequest ──► transport connect success ──► kState_CPConnected
kState_CPConnected  ──► ProxyDisconnectRequest    ──► kState_CPConnected
                                                      (sessions remain)
kState_CPConnected  ──► ProxyDisconnectRequest    ──► kState_CPDisconnected
                        for the last session
kState_CPConnected  ──► peer closes the transport ──► kState_CPDisconnected
                        connection, last session

With MaxSessions > 1 several sessions can be open at once, so a ProxyDisconnectRequest only returns the cluster to kState_CPDisconnected — and only notifies the transports via OnAllSessionsClosed() — once no session is left.

A session can also end without a ProxyDisconnectRequest, when the commissionee closes the transport connection. A transport reports that through SetDisconnectedIfLastSession() rather than setting the state itself: the cluster counts the sessions of every transport, plus any connect still in flight, so the last session on one transport is not necessarily the last session on the proxy.