Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/network-manager-app/linux/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ executable("matter-network-manager-app") {
if (matter_enable_ubus) {
defines += [ "MATTER_ENABLE_UBUS=1" ]
sources += [
"MatterUbusService.cpp",
"MatterUbusService.h",
"ThreadBROpenThreadUbus.cpp",
"ThreadBROpenThreadUbus.h",
"UboxUtils.cpp",
Expand Down
171 changes: 171 additions & 0 deletions examples/network-manager-app/linux/MatterUbusService.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* 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 "MatterUbusService.h"

#include "UboxUtils.h"

#include <app/server/Server.h>
#include <lib/support/CodeUtils.h>
#include <platform/CHIPDeviceLayer.h>
#include <setup_payload/OnboardingCodesUtil.h>

extern "C" {
#include <libubus.h>
#undef fallthrough
}

using namespace chip::app::Clusters::AdministratorCommissioning;

namespace chip {

namespace {

// The spec caps a commissioning window at 15 minutes; default to the maximum,
// since the code is meant to be typed into a controller by a person.
constexpr uint32_t kDefaultWindowSeconds = 900;
constexpr uint32_t kMinWindowSeconds = 180;

const char * WindowStatusString()
{
// Not CommissioningWindowStatusForCluster(): that deliberately reports
// locally opened windows as closed, because the cluster attribute only
// covers windows opened through it. Here the local ones are the point.
auto & mgr = Server::GetInstance().GetCommissioningWindowManager();
if (!mgr.IsCommissioningWindowOpen())
{
return "closed";
}
switch (mgr.CommissioningWindowStatusForCluster())
{
case CommissioningWindowStatusEnum::kEnhancedWindowOpen:
return "enhanced";
case CommissioningWindowStatusEnum::kBasicWindowOpen:
default:
// A window opened locally is always a basic window.
return "basic";
}
}

void AddOnboarding(ubus::BlobMsgBuf & buf)
{
PayloadContents payload;
if (GetPayloadContents(payload, RendezvousInformationFlag::kOnNetwork) == CHIP_NO_ERROR)
{
char code[32] = {};
char qr[128] = {};
MutableCharSpan codeSpan(code), qrSpan(qr);
if (GetManualPairingCode(codeSpan, payload) == CHIP_NO_ERROR)
{
buf.Add("ManualCode", static_cast<const char *>(code));
}
if (GetQRCode(qrSpan, payload) == CHIP_NO_ERROR)
{
buf.Add("QrCode", static_cast<const char *>(qr));
}
buf.Add("VendorId", static_cast<uint32_t>(payload.vendorID));
buf.Add("ProductId", static_cast<uint32_t>(payload.productID));
buf.Add("Discriminator", static_cast<uint32_t>(payload.discriminator.GetLongValue()));
}
}

int HandleStatus(ubus_context * ctx, ubus_object * obj, ubus_request_data * req, const char * method, blob_attr * msg)
{
ubus::BlobMsgBuf buf;
Comment thread
LorbusChris marked this conversation as resolved.
buf.Add("Fabrics", static_cast<uint32_t>(Server::GetInstance().GetFabricTable().FabricCount()));
buf.Add("Window", WindowStatusString());
// The initial onboarding code authenticates commissioning only while no
// fabric is on the device (the initial basic window) or while a basic
// window is open; report it so a UI can decide what to show.
AddOnboarding(buf);
ubus_send_reply(ctx, req, buf.head);
return 0;
}

enum
{
OPEN_WINDOW_TIMEOUT,
__OPEN_WINDOW_MAX,
};

const blobmsg_policy kOpenWindowPolicy[__OPEN_WINDOW_MAX] = {
[OPEN_WINDOW_TIMEOUT] = { .name = "timeout", .type = BLOBMSG_TYPE_INT32 },
};

int HandleOpenWindow(ubus_context * ctx, ubus_object * obj, ubus_request_data * req, const char * method, blob_attr * msg)
{
// A caller can omit the argument table entirely; msg is NULL then.
blob_attr * tb[__OPEN_WINDOW_MAX] = {};
if (msg != nullptr)
{
blobmsg_parse(kOpenWindowPolicy, __OPEN_WINDOW_MAX, tb, blobmsg_data(msg), blobmsg_len(msg));
}

uint32_t timeout = kDefaultWindowSeconds;
if (tb[OPEN_WINDOW_TIMEOUT] != nullptr)
{
timeout = blobmsg_get_u32(tb[OPEN_WINDOW_TIMEOUT]);
VerifyOrReturnValue(timeout >= kMinWindowSeconds && timeout <= kDefaultWindowSeconds, UBUS_STATUS_INVALID_ARGUMENT);
}

// Opens a basic window: the device's own onboarding code becomes valid
// for the duration, which is what lets a router UI show a code that a
// controller can actually use after the device is already commissioned.
auto & mgr = Server::GetInstance().GetCommissioningWindowManager();
CHIP_ERROR err = mgr.OpenBasicCommissioningWindow(System::Clock::Seconds32(timeout));
Comment thread
LorbusChris marked this conversation as resolved.
Comment thread
LorbusChris marked this conversation as resolved.
Comment thread
LorbusChris marked this conversation as resolved.
Comment thread
LorbusChris marked this conversation as resolved.

ubus::BlobMsgBuf buf;
buf.Add("Error", static_cast<uint32_t>(err == CHIP_NO_ERROR ? 0 : 1));
buf.Add("Window", WindowStatusString());
ubus_send_reply(ctx, req, buf.head);
return 0;
}

int HandleCloseWindow(ubus_context * ctx, ubus_object * obj, ubus_request_data * req, const char * method, blob_attr * msg)
{
Server::GetInstance().GetCommissioningWindowManager().CloseCommissioningWindow();
Comment thread
LorbusChris marked this conversation as resolved.

ubus::BlobMsgBuf buf;
buf.Add("Error", static_cast<uint32_t>(0));
buf.Add("Window", WindowStatusString());
ubus_send_reply(ctx, req, buf.head);
return 0;
}

ubus_method sMethods[] = {
UBUS_METHOD_NOARG("status", HandleStatus),
UBUS_METHOD("open_commissioning_window", HandleOpenWindow, kOpenWindowPolicy),
UBUS_METHOD_NOARG("close_commissioning_window", HandleCloseWindow),
};

ubus_object_type sObjectType = UBUS_OBJECT_TYPE("matter", sMethods);

ubus_object sObject = {
.name = "matter",
.type = &sObjectType,
.methods = sMethods,
.n_methods = MATTER_ARRAY_SIZE(sMethods),
};

} // namespace

CHIP_ERROR MatterUbusService::Init()
{
return mUbusManager.Host(sObject);
}

} // namespace chip
38 changes: 38 additions & 0 deletions examples/network-manager-app/linux/MatterUbusService.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.
*/

#pragma once

#include "UbusManager.h"

namespace chip {

// Publishes a "matter" ubus object exposing this node's onboarding
// information and local control of the commissioning window, so a router UI
// can pair the device with a controller without access to the daemon's log.
class MatterUbusService
{
public:
MatterUbusService(ubus::UbusManager & ubusManager) : mUbusManager(ubusManager) {}

CHIP_ERROR Init();

private:
ubus::UbusManager & mUbusManager;
};

} // namespace chip
41 changes: 37 additions & 4 deletions examples/network-manager-app/linux/ThreadBRFake.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,44 @@ class FakeBorderRouterDelegate final : public app::Clusters::ThreadBorderRouterM

mActivateDatasetCallback = callback;
mActivateDatasetSequence = sequenceNum;
TEMPORARY_RETURN_IGNORED DeviceLayer::SystemLayer().StartTimer(System::Clock::Milliseconds32(1000), ActivateActiveDataset,
this);
mActivationPending = true;
VerifyOrReturn(DeviceLayer::SystemLayer()
.StartTimer(System::Clock::Milliseconds32(1000), ActivateActiveDataset, this)
.Handle([&](CHIP_ERROR error) {
// Without the timer nothing would ever complete this activation;
// undo the state so the next attempt is not refused as Busy.
mActivateDatasetCallback = nullptr;
mActivationPending = false;
mActiveDataset.Clear();
callback->OnActivateDatasetComplete(sequenceNum, error);
}));
}

CHIP_ERROR CommitActiveDataset() override { return CHIP_NO_ERROR; }
CHIP_ERROR RevertActiveDataset() override { return CHIP_ERROR_NOT_IMPLEMENTED; }
CHIP_ERROR CommitActiveDataset() override
{
mActivationPending = false;
return CHIP_NO_ERROR;
}

CHIP_ERROR RevertActiveDataset() override
{
// Parity with the ubus delegate: the fail-safe handler calls this for
// every expiry, and only an activation that was not committed reverts.
VerifyOrReturnError(mActivationPending, CHIP_NO_ERROR);
mActivationPending = false;

// The activation timer may still be armed. A reverted activation must
// neither report success nor block the next attempt.
DeviceLayer::SystemLayer().CancelTimer(ActivateActiveDataset, this);
mActivateDatasetCallback = nullptr;

// SetActiveDataset is only accepted when no dataset is configured, so
// reverting it means returning to the unconfigured state.
mActiveDataset.Clear();
mAttributeChangeCallback->ReportAttributeChanged(
app::Clusters::ThreadBorderRouterManagement::Attributes::ActiveDatasetTimestamp::Id);
return CHIP_NO_ERROR;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

CHIP_ERROR SetPendingDataset(const Thread::OperationalDataset & pendingDataset) override
{
Expand Down Expand Up @@ -130,6 +162,7 @@ class FakeBorderRouterDelegate final : public app::Clusters::ThreadBorderRouterM

ActivateDatasetCallback * mActivateDatasetCallback = nullptr;
uint32_t mActivateDatasetSequence;
bool mActivationPending = false;
};

} // namespace chip
Loading
Loading