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 by implementing the CommissioningProxyTransport driver interface (one per physical transport, e.g. BLE or Wi-Fi PAF) and registering 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 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

Create a class that inherits from chip::app::Clusters::CommissioningProxy::CommissioningProxyTransport and implement its virtual methods — one class per physical transport. 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 MyBleTransport : public chip::app::Clusters::CommissioningProxy::CommissioningProxyTransport
{
public:
    CapabilitiesBitmap GetTransportType() const override { return CapabilitiesBitmap::kBle; }
    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().
    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;

MyBleTransport gBleTransport;

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(gBleTransport); // 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);
}

A complete working example (device wiring plus the BLE and Wi-Fi PAF drivers) lands with the example-app change later in this series.

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.

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 (idempotent)
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 this transport's cached results (host->ScanCache().ClearTransport(...)) whenever the last record is removed

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>

class MyBleBgScanHardware : public CommissioningProxyBgScanRegistry::HardwareControl
{
public:
    // 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(); }
    void ClearCachedResults() override { sHost->ScanCache().ClearTransport(CapabilitiesBitmap::kBle); }
};

// Declared before the registry so it outlives it: the registry's destructor may
// call back into these hooks.
MyBleBgScanHardware sHardware;
CommissioningProxyBgScanRegistry sBgScan(sHardware);

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

Status MyBleTransport::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 and extend the exchange response timeout:

// Store the handle before returning nullopt
CommandHandler::Handle 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 = handle.Get();
if (handler != nullptr)
{
    handler->AddResponse(commandPath, response);
}

BLE Transport Integration

When the build enables BLE (CONFIG_NETWORK_LAYER_BLE) the driver (CommissioningProxyBleTransport) drives a BTP connection through chip::Ble::BleLayer. The Linux example driver, landing with the example-app change later in this series, (examples/all-devices-app/all-devices-common/device/types/commissioning-proxy/CommissioningProxyBleTransport.cpp) shows the full integration:

  • Connect() — on the first BLE connect the proxy flips its own BLE role from peripheral to central via BLEManagerImpl::SwitchToCentralMode() (a one-way switch; IsCentralMode() reports the state and central-mode advertising is then refused), 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 BleProxyDelegate (chip::Ble::BleLayerDelegate) that wraps the original BleLayer transport, matches the connection against the active session map, and calls host->Sessions().DispatchMessageResponse().

Wi-Fi PAF Transport Integration

When the build enables Wi-Fi PAF (CHIP_DEVICE_CONFIG_ENABLE_WIFIPAF) the driver (CommissioningProxyPafTransport) interacts with chip::WiFiPAF::WiFiPAFLayer to open, send over, receive from, and close PAF (NAN) sessions:

  • Connect() — calls WiFiPAFLayer::WiFiPAFSubscribe() to open a PAF session identified by the commissionee discriminator and peer address.
  • SendMessage() — calls WiFiPAFLayer::SendMessage() to send the tunneled commissioning packet over PAFTP.
  • Disconnect() — calls WiFiPAFLayer::RmPafSession() to release the PAF session.

Incoming PAF messages are routed back to the cluster via a WiFiPAFLayerDelegate subclass that intercepts WiFiPAFMessageReceived, matches the peer against the active session map, and calls host->Sessions().DispatchMessageResponse().

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_CPDisconnected