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.
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 & Integrations: Device implementations and their associated OOB hooks live together in
all-devices-common/device/types/<device-name>/:Core logic and
OOBAccessors.h/.cppbelong to the platform-neutral:<device-name>target.NamedPipes.h/.cppbelongs to the POSIX-only:posixsub-target.
Platform Transport Infrastructure: Dispatchers and base interfaces (e.g., POSIX Named Pipes) live in platform directories under
posix/named_pipe/.
2. Directory Layout#
examples/all-devices-app/
├── all-devices-common/
│ ├── device-factory/
│ │ ├── DeviceFactory.h
│ │ └── BUILD.gn
│ ├── device/
│ │ └── types/
│ │ └── <device-name>/
│ │ ├── <DeviceName>.h/.cpp # Core device implementation (platform-neutral)
│ │ ├── OOBAccessors.h/.cpp # Device OOB accessor registration (platform-neutral)
│ │ ├── NamedPipes.h/.cpp # Device Named Pipe registration (POSIX-only)
│ │ └── 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)
│ ├── 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
├── NamedPipeCommandTranslator.h # Base interface: TranslateAndExecute(json, registry)
├── PosixNamedPipeDispatcher.h/.cpp # POSIX pipe listener & JSON command router
└── translators/
└── <Command>Translator.h/.cpp # Command translators (e.g. OnOffTranslator, OccupancyTranslator)
3. Interfaces & Core Types#
CreatedDevice Struct#
namespace chip::app {
struct CreatedDevice
{
std::unique_ptr<DeviceInterface> device;
/**
* @brief Optional callback to execute actions after endpoint registration.
*
* This callback MUST be invoked after `device->Register(...)` completes so that
* any actions requiring a valid allocated EndpointId (such as registering OOB
* cluster accessors) have access to `device->GetEndpointId()`.
*/
std::function<void(OOBAccessorRegistry & registry)> postRegistrationCallback;
};
} // 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:
/**
* @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:
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
NamedPipeCommandTranslator Interface#
namespace chip::app {
class OOBAccessorRegistry;
class NamedPipeCommandTranslator
{
public:
virtual ~NamedPipeCommandTranslator() = default;
/**
* @brief Translates a JSON payload into TLV and executes the action via OOBAccessorRegistry.
* @param json Parsed JSON payload from named pipe.
* @param registry Target registry to dispatch the translated action.
*/
virtual CHIP_ERROR TranslateAndExecute(const Json::Value & json, OOBAccessorRegistry & registry) = 0;
};
} // namespace chip::app
PosixNamedPipeDispatcher Interface#
namespace chip::app {
class PosixNamedPipeDispatcher
{
public:
explicit PosixNamedPipeDispatcher(OOBAccessorRegistry & oobRegistry) :
mOobRegistry(oobRegistry) {}
~PosixNamedPipeDispatcher();
/**
* @brief Starts listening on the named pipe FIFO.
* @param fifoPath Path to the named pipe file (e.g. "/tmp/chip_all_devices_fifo").
*/
CHIP_ERROR Start(const char * fifoPath);
/**
* @brief Stops listening on the named pipe and cleans up the FIFO file.
*/
CHIP_ERROR Stop();
/**
* @brief Checks if a translator is registered for the specified action name.
*/
bool HasTranslator(const std::string & actionName) const;
/**
* @brief Registers a command translator instance under the specified action name.
*/
CHIP_ERROR RegisterTranslator(const std::string & actionName, std::unique_ptr<NamedPipeCommandTranslator> translator);
/**
* @brief Registers a translator if not already present, deduping by TranslatorType::kName.
*/
template <typename TranslatorType, typename... Args>
CHIP_ERROR EnsureTranslatorRegistered(Args &&... args)
{
if (HasTranslator(TranslatorType::kName))
{
return CHIP_NO_ERROR;
}
return RegisterTranslator(
TranslatorType::kName,
std::make_unique<TranslatorType>(std::forward<Args>(args)...)
);
}
/**
* @brief Parses and dispatches a JSON command to the registered translator.
*/
CHIP_ERROR DispatchJson(const Json::Value & json);
private:
OOBAccessorRegistry & mOobRegistry;
std::unordered_map<std::string, std::unique_ptr<NamedPipeCommandTranslator>> mTranslators;
};
} // namespace chip::app
4. Execution Flow#
Named Pipe Command Flow#
sequenceDiagram
participant Pipe as Named Pipe
participant Disp as Posix Named Pipe 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: {"action": "SetOnOff", "endpoint": 1, "value": true}
Disp->>Trans: TranslateAndExecute(json, Reg)
Trans->>Reg: HandleAction("SetOnOff", TLV[endpoint: 1, value: 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):{ "action": "SetOnOff", "endpoint": 1, "value": true }
Dispatch:
PosixNamedPipeDispatcherreads pipe, parses JSON, and extracts"action".Translation: Dispatcher invokes
OnOffTranslator::TranslateAndExecute(json, mOobRegistry):Extracts
endpoint = 1andvalue = true.Encodes flat TLV payload:
Tag 1:
EndpointId(uint16_t)Tag 2:
Value(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,
PosixNamedPipeDispatchersynchronizes 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 <oob-accessors/OOBAccessorRegistry.h>
namespace chip::app {
class OnOffLight;
void RegisterOOBAccessors(OnOffLight & device, OOBAccessorRegistry & registry);
} // namespace chip::app
In all-devices-common/device/types/<device-name>/OOBAccessors.cpp:
#include "OOBAccessors.h"
#include "OnOffLight.h"
#if ALL_DEVICES_APP_ENABLE_OOB_ACCESSORS
#include <oob-accessors/clusters/OnOffOOBAccessor.h>
#endif
namespace chip::app {
void RegisterOOBAccessors(OnOffLight & device, OOBAccessorRegistry & registry)
{
#if ALL_DEVICES_APP_ENABLE_OOB_ACCESSORS
registry.Register(std::make_unique<OnOffOOBAccessor>(device.OnOffCluster(), device.GetEndpointId()));
#endif
}
} // namespace chip::app
Step 2: Register Named Pipe Translators (POSIX-Only)#
In all-devices-common/device/types/<device-name>/NamedPipes.h:
#pragma once
#include <posix/named_pipe/PosixNamedPipeDispatcher.h>
namespace chip::app {
class OnOffLight;
void RegisterNamedPipes(OnOffLight & device, PosixNamedPipeDispatcher & dispatcher);
} // namespace chip::app
In all-devices-common/device/types/<device-name>/NamedPipes.cpp:
#include "NamedPipes.h"
#include "OnOffLight.h"
#include <posix/named_pipe/translators/OnOffTranslator.h>
namespace chip::app {
void RegisterNamedPipes(OnOffLight & device, PosixNamedPipeDispatcher & dispatcher)
{
dispatcher.EnsureTranslatorRegistered<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-only named pipe integration (pulled exclusively by POSIX builds)
source_set("posix") {
sources = [
"NamedPipes.cpp",
"NamedPipes.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](const std::string & label) -> CreatedDevice {
VerifyOrDie(mContext.has_value());
auto device = std::make_unique<LoggingOnOffLight>(mContext->timerDelegate);
auto * rawDevice = device.get();
return CreatedDevice{
.device = std::move(device),
.postRegistrationCallback = [rawDevice](OOBAccessorRegistry & registry) {
RegisterOOBAccessors(*rawDevice, registry);
},
};
});
In application initialization (posix/main.cpp and embedded setup):
for (const auto & entry : AppOptions::GetDeviceTypeEntries())
{
// 1. Create device + post-registration callback via factory
auto created = DeviceFactory::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.postRegistrationCallback)
{
created.postRegistrationCallback(mOobRegistry);
}
mConstructedDevices.push_back(std::move(created.device));
}
// 4. Start named pipe listener in POSIX main
mNamedPipeDispatcher.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 both the base device target and the:posixsub-target:deps = [ "${chip_root}/examples/all-devices-app/all-devices-common/device/types/on-off-light", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/on-off-light:posix", ]Embedded GN Targets (
silabs/BUILD.gn): Pulls only the platform-neutral base targetdevice/types/<name>(and any:silabs/:loggingsub-targets). The:posixtarget is never referenced, eliminating transitive POSIX headers.Embedded CMake Targets (
esp32,telink):enabled_devices.cmakecollects${DEVICE_DIR}/<DeviceName>.cppand${DEVICE_DIR}/OOBAccessors.cpp.NamedPipes.cppis excluded from CMake source lists.
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
7. TODO / Implementation Checklist#
[!NOTE] The unified Out-of-Band Control architecture is specified above and tracked for implementation via the following phased checklist.
Phase 1: Core OOB Registry & Cluster Accessors#
[ ] Create
all-devices-common/oob-accessors/all_devices_config.gni,all_devices_config.cmake, andall_devices_config.h.in.[ ] Create
all-devices-common/oob-accessors/OOBAccessor.h.[ ] Create
all-devices-common/oob-accessors/InMemoryOOBAccessorRegistry.hand.cpp.[ ] Create
all-devices-common/oob-accessors/NoopOOBAccessorRegistry.h.[ ] Create
all-devices-common/oob-accessors/OOBAccessorRegistry.h(aliasing header).[ ] Implement shared cluster accessors in
all-devices-common/oob-accessors/clusters/:[ ]
OnOffOOBAccessor.h/.cpp[ ]
OccupancyOOBAccessor.h/.cpp[ ]
BooleanStateOOBAccessor.h/.cpp[ ]
AmbientContextOOBAccessor.h/.cpp[ ]
BasicInformationOOBAccessor.h/.cpp
[ ] Update
all-devices-common/oob-accessors/BUILD.gnwith new OOB source targets.
Phase 2: Device-Type OOB Accessor Registration#
[ ] Add
OOBAccessors.handOOBAccessors.cppfor all existing device types:[ ]
all-devices-common/device/types/on-off-light/[ ]
all-devices-common/device/types/dimmable-light/[ ]
all-devices-common/device/types/occupancy-sensor/[ ]
all-devices-common/device/types/contact-sensor/[ ]
all-devices-common/device/types/light-sensor/[ ]
all-devices-common/device/types/air-quality-sensor/[ ]
all-devices-common/device/types/speaker/[ ]
all-devices-common/device/types/on-off-plug-in-unit/[ ]
all-devices-common/device/types/dimmable-plug-in-unit/
[ ] Hook
created.postRegistrationCallback(mOobRegistry)after endpoint registration inmain.cpp.
Phase 3: POSIX Named Pipe Dispatcher & Translators#
[ ] Create
posix/named_pipe/NamedPipeCommandTranslator.h.[ ] Create
posix/named_pipe/PosixNamedPipeDispatcher.hand.cpp.[ ] Implement granular translators in
posix/named_pipe/translators/:[ ]
OnOffTranslator.h/.cpp[ ]
OccupancyTranslator.h/.cpp[ ]
BooleanStateTranslator.h/.cpp[ ]
AmbientContextTranslator.h/.cpp[ ]
BasicInformationTranslator.h/.cpp
Phase 4: Device-Type Named Pipe Registration & Sub-Targets#
[ ] Add
NamedPipes.handNamedPipes.cppunderall-devices-common/device/types/<device-name>/for each supported device.[ ] Add
source_set("posix")to each device’sBUILD.gn.[ ] Wire
mNamedPipeDispatcher.Start(path)inposix/main.cpp.
Phase 5: Legacy Cleanup & Build Verification#
[ ] Remove legacy
AppCommandDelegate.h/.cpp.[ ] Remove legacy
ClusterTypeMappings.h/.cpp.[ ] Remove legacy
AllDevicesAppClusterImplementationRegistry.h.[ ] Update build files (
posix/BUILD.gn,all-devices-common/oob-accessors/BUILD.gn,posix/named_pipe/BUILD.gn).[ ] Build target
linux-x64-all-devices-clangand verify with sample named pipe commands.