Out-of-Band Control Architecture#
This document describes the Out-of-Band (OOB) control architecture in
all-devices-app.
OOB control allows external interfaces (POSIX Named Pipes, Pigweed RPC, test runners, platform buttons) to mutate cluster state, simulate sensor triggers, and execute device actions independently of the Matter Interaction Model.
1. Core Architecture#
The architecture separates Transport Translation from Action Execution:
flowchart LR
subgraph External["External Inputs"]
NP["Named Pipes (JSON)"]
RPC["Pigweed RPC (Proto)"]
TET["Test Event Triggers"]
end
subgraph Translators["Translators"]
TR["Command Translators"]
end
subgraph Backend["OOB Execution Backend"]
REG["OOB Accessor Registry"]
OOB["OOB Accessors"]
end
subgraph Device["Device Data Model"]
CLUSTER["Cluster Instances"]
end
External --> TR
TR -->|Action Name + TLV Payload| REG
REG --> OOB
OOB -->|Direct C++ Method Calls| CLUSTER
How OOB Control Works#
Single Control Surface:
OOBAccessoris the only mechanism to control simulated devices and clusters outside of the Matter protocol.Generic Action Dispatch: An
OOBAccessorreceives a semantic action name and a TLV data buffer. The application maintains a registry of accessors and queries them in order:std::nullopt: The action is not supported by this accessor. Registry continues querying the next accessor.CHIP_NO_ERROR: The action was recognized and executed successfully. Dispatch terminates.Other
CHIP_ERROR: The action was recognized by this accessor, but execution failed. Dispatch terminates immediately and propagates the error.If no registered accessor handles the action (all return
std::nullopt),HandleActionreturnsCHIP_ERROR_NOT_FOUND.
Standardized Attribute Writes: To reduce boilerplate and unify handling, attribute updates use the standardized
"SetAttribute"action name along with the standard TLV payload format (encoding Endpoint ID, Cluster ID, Attribute ID, and Value) defined byOOBDataSerializer. Cluster OOB accessors handle"SetAttribute"for the attributes they support and returnstd::nulloptfor unrecognized attributes so other registered accessors can process them.Transport Translation: External protocols and transports (Named Pipe JSON, Pigweed RPC, Test Event Triggers) parse incoming requests, convert them into an action name and TLV payload, and forward them to
OOBAccessorRegistry.Code Organization:
Shared Cluster Accessors: Accessors for common Matter clusters (e.g.,
OnOff,OccupancySensing) live inall-devices-common/oob-accessors/clusters/so any device containing that cluster reuses the same implementation.Device-Specific Accessors & Translators: Device implementations and their associated OOB accessor and named pipe registrations live together in
all-devices-common/device/types/<device-name>/:Core logic and
OOBAccessors.h/.cppbelong to the platform-neutral:<device-name>target.NamedPipeTranslators.h/.cppbelong to the target’s:<device-name>:posixsource set.
Platform Transport Infrastructure: Dispatchers, hooks, and base interfaces (e.g., POSIX Named Pipes and JSON translators) live under
posix/named_pipe/andall-devices-common/oob-accessors/.
2. Directory Layout#
examples/all-devices-app/
├── all-devices-common/
│ ├── device-factory/
│ │ ├── DeviceFactory.h # Variadic DeviceFactory<Hooks...> & NoHooksDeviceFactory
│ │ └── BUILD.gn
│ ├── device/
│ │ └── types/
│ │ └── <device-name>/
│ │ ├── <DeviceName>.h/.cpp # Core device implementation (platform-neutral)
│ │ ├── OOBAccessors.h/.cpp # Device OOB accessor registration (platform-neutral)
│ │ ├── NamedPipeTranslators.h/.cpp # POSIX NamedPipe translator registration (POSIX-specific)
│ │ └── BUILD.gn # GN targets: :<device-name>, :posix, :logging
│ └── oob-accessors/
│ ├── all_devices_config.gni # GN build configuration header generator
│ ├── all_devices_config.cmake # CMake build configuration header generator
│ ├── all_devices_config.h.in # CMake configuration template
│ ├── BUILD.gn # GN rules for oob-accessors
│ ├── OOBAccessor.h # Base interface: HandleAction(action, tlvData)
│ ├── OOBAccessorHook.h # Static OOB accessor registration hook for DeviceFactory
│ ├── OOBAccessorRegistry.h # Active alias (InMemory vs Noop)
│ ├── InMemoryOOBAccessorRegistry.h/.cpp # Container of registered OOBAccessors
│ ├── NoopOOBAccessorRegistry.h # Zero-cost inline stub for disabled targets
│ └── clusters/
│ └── <Cluster>OOBAccessor.h/.cpp # Cluster accessors (e.g. OnOffOOBAccessor, OccupancyOOBAccessor)
└── posix/
└── named_pipe/
├── BUILD.gn # GN rules for POSIX dispatcher & translators
├── CommandTranslator.h # Base interface: TranslateAndExecute(endpointId, json, registry)
├── Dispatcher.h/.cpp # POSIX pipe listener & JSON command router
├── Hook.h # Static named pipe translator registration hook for DeviceFactory
└── translators/
└── <Command>Translator.h/.cpp # Command-specific JSON-to-TLV translators
3. Interfaces & Core Types#
DeviceRegistrationEntry Struct#
namespace chip::app {
/// Bundles an allocated device with its post-registration hook callback.
struct DeviceRegistrationEntry
{
std::unique_ptr<DeviceInterface> device;
/// Hook callback that must be invoked after `device->Register(...)` completes while the device remains valid.
std::function<void()> onDeviceRegistered;
};
} // namespace chip::app
OOBAccessor Interface#
namespace chip::app {
class OOBAccessor
{
public:
virtual ~OOBAccessor() = default;
/**
* @brief Executes an out-of-band action on a target endpoint.
* @param action Semantic action string (e.g. "SetOnOff", "SetOccupancy").
* @param tlvData Encoded TLV payload containing endpoint ID and action parameters.
* @return std::nullopt if action is not supported (registry continues dispatch).
* @return CHIP_NO_ERROR if action was recognized and executed successfully.
* @return Other CHIP_ERROR on execution failure (registry stops dispatch).
*
* @note Asynchronous Safety: The tlvData parameter is a non-owning temporary view valid
* only during synchronous execution of this call.
*/
virtual std::optional<CHIP_ERROR> HandleAction(CharSpan action, ByteSpan tlvData) = 0;
};
} // namespace chip::app
InMemoryOOBAccessorRegistry Class#
namespace chip::app {
class InMemoryOOBAccessorRegistry
{
public:
static InMemoryOOBAccessorRegistry & Instance();
/**
* @brief Registers an OOB accessor instance.
* @param accessor The accessor instance to register.
*/
CHIP_ERROR Register(std::unique_ptr<OOBAccessor> accessor);
/**
* @brief Dispatches an action to registered accessors in order.
* @return CHIP_NO_ERROR on success, CHIP_ERROR_NOT_FOUND if unhandled, or specific error on execution failure.
*/
CHIP_ERROR HandleAction(CharSpan action, ByteSpan tlvData);
/**
* @brief Clears all registered accessors during device teardown.
*/
void Clear() { mAccessors.clear(); }
private:
std::vector<std::unique_ptr<OOBAccessor>> mAccessors;
};
} // namespace chip::app
NoopOOBAccessorRegistry Class#
namespace chip::app {
class NoopOOBAccessorRegistry
{
public:
static NoopOOBAccessorRegistry & Instance();
CHIP_ERROR Register(std::unique_ptr<OOBAccessor> /* accessor */) { return CHIP_NO_ERROR; }
CHIP_ERROR HandleAction(CharSpan /* action */, ByteSpan /* tlvData */) { return CHIP_ERROR_NOT_FOUND; }
void Clear() {}
};
} // namespace chip::app
OOBAccessorHook Class#
namespace chip::app {
class OOBAccessorHook
{
public:
template <typename TDevice>
static void OnDeviceRegistered(TDevice & device)
{
if constexpr (detail::HasOOBAccessors<TDevice>::value)
{
RegisterOOBAccessors(device, OOBAccessorRegistry::Instance());
}
}
};
} // namespace chip::app
NamedPipe::Hook Class#
namespace chip::app::NamedPipe {
class Hook
{
public:
template <typename TDevice>
static void OnDeviceRegistered(TDevice & device)
{
if constexpr (detail::HasNamedPipeTranslators<TDevice>::value)
{
RegisterNamedPipeTranslators(device, Dispatcher::Instance());
}
}
};
} // namespace chip::app::NamedPipe
NamedPipe::CommandTranslator Interface#
namespace chip::app::NamedPipe {
class OOBAccessorRegistry;
class CommandTranslator
{
public:
virtual ~CommandTranslator() = default;
/**
* @brief Translates a JSON payload into TLV and executes the action via OOBAccessorRegistry.
* @param endpointId Target endpoint ID.
* @param json Parsed JSON payload from named pipe.
* @param registry Target registry to dispatch the translated action.
*/
virtual CHIP_ERROR TranslateAndExecute(EndpointId endpointId, const Json::Value & json,
OOBAccessorRegistry & registry) const = 0;
};
} // namespace chip::app::NamedPipe
NamedPipe::Dispatcher Interface#
namespace chip::app::NamedPipe {
class Dispatcher
{
public:
static Dispatcher & Instance();
explicit Dispatcher(OOBAccessorRegistry & oobRegistry) :
mOobRegistry(oobRegistry) {}
~Dispatcher() override;
CHIP_ERROR Start(const char * fifoPath);
CHIP_ERROR Stop();
bool HasTranslator(CharSpan actionName) const;
CHIP_ERROR RegisterTranslator(CharSpan actionName, std::shared_ptr<const CommandTranslator> translator);
/**
* @brief Registers a translator if not already present, registering all action names exposed by TranslatorType.
*/
template <typename TranslatorType>
CHIP_ERROR EnsureTranslatorRegistered()
{
auto actionNames = TranslatorType::GetActionNames();
if (actionNames.empty())
{
return CHIP_NO_ERROR;
}
bool anyMissing = false;
for (const auto & name : actionNames)
{
if (!HasTranslator(name))
{
anyMissing = true;
break;
}
}
if (!anyMissing)
{
return CHIP_NO_ERROR;
}
auto translator = std::make_shared<TranslatorType>();
for (const auto & name : actionNames)
{
if (!HasTranslator(name))
{
ReturnErrorOnFailure(RegisterTranslator(name, translator));
}
}
return CHIP_NO_ERROR;
}
CHIP_ERROR DispatchJson(const Json::Value & json);
private:
OOBAccessorRegistry & mOobRegistry;
std::unordered_map<std::string, std::shared_ptr<const CommandTranslator>> mTranslators;
};
} // namespace chip::app::NamedPipe
4. Execution Flow#
Named Pipe Command Flow#
sequenceDiagram
participant Pipe as Named Pipe
participant Disp as NamedPipe::Dispatcher
participant Trans as Command Translator
participant Reg as OOB Accessor Registry
participant Accessor as Cluster OOB Accessor
participant Cluster as OnOff Cluster
Pipe->>Disp: Raw JSON string: {"Name": "SetOnOff", "EndpointId": 1, "OnOff": true}
Disp->>Trans: TranslateAndExecute(endpointId, json, Reg)
Trans->>Reg: HandleAction("SetOnOff", TLV[endpoint: 1, OnOff: true])
Reg->>Accessor: HandleAction("SetOnOff", TLV)
Accessor->>Cluster: SetOnOff(true)
Ingress: External process writes JSON string to named pipe (e.g.
/tmp/chip_all_devices_fifo):{ "Name": "SetOnOff", "EndpointId": 1, "OnOff": true }
Dispatch:
NamedPipe::Dispatcherreads pipe, parses JSON, and extracts"Name"and"EndpointId".Translation: Dispatcher invokes
NamedPipe::OnOffTranslator::TranslateAndExecute(endpointId, json, mOobRegistry):Extracts
OnOff = true.Encodes flat TLV payload:
Tag 1:
EndpointId(uint16_t)Tag 2:
OnOff(bool)
Calls
registry.HandleAction("SetOnOff", tlvBuffer).
Execution:
OOBAccessorRegistryroutes toOnOffOOBAccessorregistered for Endpoint 1:Decodes TLV fields.
Calls
mCluster.SetOnOff(true)on target cluster instance.
[!NOTE]
Thread Safety & Stack Synchronization: The POSIX named pipe listener runs on a background worker thread. When a command is received,
NamedPipe::Dispatchersynchronizes execution onto the Matter event loop viachip::DeviceLayer::PlatformMgr().ScheduleWork(...)or acquireschip::DeviceLayer::PlatformMgr().LockChipStack()before invokingHandleActiononOOBAccessorRegistry.
5. Adding OOB Support for a Device#
Step 1: Register Cluster OOB Accessors (Platform-Neutral)#
In all-devices-common/device/types/<device-name>/OOBAccessors.h:
#pragma once
#include <device/capabilities/on-off-load/OnOffLoad.h>
#include <oob-accessors/OOBAccessorRegistry.h>
namespace chip::app {
void RegisterOOBAccessors(OnOffLoad & device, OOBAccessorRegistry & registry);
} // namespace chip::app
In all-devices-common/device/types/<device-name>/OOBAccessors.cpp:
#include "OOBAccessors.h"
#include <device/capabilities/on-off-load/OnOffLoad.h>
#include <oob-accessors/clusters/OnOffOOBAccessor.h>
namespace chip::app {
void RegisterOOBAccessors(OnOffLoad & device, OOBAccessorRegistry & registry)
{
LogErrorOnFailure(registry.Register(
std::make_unique<OnOffOOBAccessor>(device.OnOffCluster(), device.GetEndpointId())));
}
} // namespace chip::app
Step 2: Register Named Pipe Translators (POSIX-Only)#
In all-devices-common/device/types/<device-name>/NamedPipeTranslators.h:
#pragma once
#include <device/capabilities/on-off-load/OnOffLoad.h>
#include <posix/named_pipe/Dispatcher.h>
namespace chip::app {
void RegisterNamedPipeTranslators(OnOffLoad & device, NamedPipe::Dispatcher & dispatcher);
} // namespace chip::app
In all-devices-common/device/types/<device-name>/NamedPipeTranslators.cpp:
#include "NamedPipeTranslators.h"
#include <posix/named_pipe/translators/OnOffTranslator.h>
namespace chip::app {
void RegisterNamedPipeTranslators(OnOffLoad & device, NamedPipe::Dispatcher & dispatcher)
{
LogErrorOnFailure(dispatcher.EnsureTranslatorRegistered<NamedPipe::OnOffTranslator>());
}
} // namespace chip::app
Step 3: Define Granular GN Sub-Targets#
In all-devices-common/device/types/<device-name>/BUILD.gn:
import("//build_overrides/chip.gni")
# Platform-neutral device target (used by all platforms)
source_set("<device-name>") {
sources = [
"<DeviceName>.cpp",
"<DeviceName>.h",
"OOBAccessors.cpp",
"OOBAccessors.h",
]
public_deps = [
"${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors",
]
}
# POSIX target for named pipe translator registration
source_set("posix") {
sources = [
"NamedPipeTranslators.cpp",
"NamedPipeTranslators.h",
]
public_deps = [
":<device-name>",
"${chip_root}/examples/all-devices-app/posix/named_pipe",
]
}
Step 4: Factory Creation & Application Lifecycle#
In DeviceFactory.h (creator registration):
RegisterCreator("on-off-light", [this]() {
VerifyOrDie(mContext.has_value());
return MakeDevice<LoggingOnOffLight>(LoggingOnOffLight::Context{
.groupDataProvider = mContext->groupDataProvider,
.fabricTable = mContext->fabricTable,
.timerDelegate = mContext->timerDelegate,
});
});
In application initialization (posix/main.cpp and embedded setup):
// In posix/main.cpp:
using PosixDeviceFactory = DeviceFactory<OOBAccessorHook, NamedPipe::Hook>;
for (const auto & entry : AppOptions::GetDeviceTypeEntries())
{
// 1. Create device + post-registration callback via factory
auto created = PosixDeviceFactory::GetInstance().Create(entry.type, entry.label);
VerifyOrReturnError(created.device != nullptr, CHIP_ERROR_NO_MEMORY);
// 2. Register endpoint with data model provider (allocates valid endpoint ID)
ReturnErrorOnFailure(
created.device->Register(endpointIdAllocator, mDataModelProvider, EndpointComposition::WithParent(entry.parentId)));
// 3. Invoke post-registration callback once EndpointId is assigned
if (created.onDeviceRegistered)
{
created.onDeviceRegistered();
}
mConstructedDevices.push_back(std::move(created.device));
}
// 4. Start named pipe listener in POSIX main
NamedPipe::Dispatcher::Instance().Start(kDefaultFifoPath);
6. Build Configuration & Target Isolation#
Target separation prevents platform-specific dependencies from leaking into embedded builds:
POSIX GN Target (
posix/BUILD.gn): Pulls the platform-neutral device targets and theposix/named_pipedispatcher target.Embedded GN Targets (
silabs/BUILD.gn): Pulls only the platform-neutral base targetdevice/types/<name>(and any:silabs/:loggingsub-targets). POSIX dispatchers and named pipe headers are never referenced.Embedded CMake Targets (
esp32,telink):enabled_devices.cmakecollects${DEVICE_DIR}/<DeviceName>.cppand${DEVICE_DIR}/OOBAccessors.cpp.
Configuration defines are generated into app_config/all_devices_config.h for
both GN and CMake:
GN Build (
oob-accessors/all_devices_config.gni): Usesbuildconfig_headerto emitapp_config/all_devices_config.h.CMake Build (
oob-accessors/all_devices_config.cmake): Usesconfigure_filewithall_devices_config.h.into emit${CMAKE_CURRENT_BINARY_DIR}/app_config/all_devices_config.h.
Build Target / Flag |
|
|
|
|---|---|---|---|
POSIX Linux ( |
1 |
0 (or 1 if |
1 |
Embedded Targets (ESP32, SiLabs, Telink) |
0 |
0 |
0 |
Header aliasing in all-devices-common/oob-accessors/OOBAccessorRegistry.h:
#pragma once
#include <app_config/all_devices_config.h>
#if ALL_DEVICES_APP_ENABLE_OOB_ACCESSORS
#include <oob-accessors/InMemoryOOBAccessorRegistry.h>
#else
#include <oob-accessors/NoopOOBAccessorRegistry.h>
#endif
namespace chip::app {
#if ALL_DEVICES_APP_ENABLE_OOB_ACCESSORS
using OOBAccessorRegistry = InMemoryOOBAccessorRegistry;
#else
using OOBAccessorRegistry = NoopOOBAccessorRegistry;
#endif
} // namespace chip::app