diff --git a/examples/network-manager-app/linux/BUILD.gn b/examples/network-manager-app/linux/BUILD.gn index 27119a8276b8..580f03f83030 100644 --- a/examples/network-manager-app/linux/BUILD.gn +++ b/examples/network-manager-app/linux/BUILD.gn @@ -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", diff --git a/examples/network-manager-app/linux/MatterUbusService.cpp b/examples/network-manager-app/linux/MatterUbusService.cpp new file mode 100644 index 000000000000..61a6e1aa73a9 --- /dev/null +++ b/examples/network-manager-app/linux/MatterUbusService.cpp @@ -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 +#include +#include +#include + +extern "C" { +#include +#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(code)); + } + if (GetQRCode(qrSpan, payload) == CHIP_NO_ERROR) + { + buf.Add("QrCode", static_cast(qr)); + } + buf.Add("VendorId", static_cast(payload.vendorID)); + buf.Add("ProductId", static_cast(payload.productID)); + buf.Add("Discriminator", static_cast(payload.discriminator.GetLongValue())); + } +} + +int HandleStatus(ubus_context * ctx, ubus_object * obj, ubus_request_data * req, const char * method, blob_attr * msg) +{ + ubus::BlobMsgBuf buf; + buf.Add("Fabrics", static_cast(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)); + + ubus::BlobMsgBuf buf; + buf.Add("Error", static_cast(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(); + + ubus::BlobMsgBuf buf; + buf.Add("Error", static_cast(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 diff --git a/examples/network-manager-app/linux/MatterUbusService.h b/examples/network-manager-app/linux/MatterUbusService.h new file mode 100644 index 000000000000..165ff5906d7e --- /dev/null +++ b/examples/network-manager-app/linux/MatterUbusService.h @@ -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 diff --git a/examples/network-manager-app/linux/ThreadBRFake.h b/examples/network-manager-app/linux/ThreadBRFake.h index 86a92c7c54f2..453c78ebebb3 100644 --- a/examples/network-manager-app/linux/ThreadBRFake.h +++ b/examples/network-manager-app/linux/ThreadBRFake.h @@ -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; + } CHIP_ERROR SetPendingDataset(const Thread::OperationalDataset & pendingDataset) override { @@ -130,6 +162,7 @@ class FakeBorderRouterDelegate final : public app::Clusters::ThreadBorderRouterM ActivateDatasetCallback * mActivateDatasetCallback = nullptr; uint32_t mActivateDatasetSequence; + bool mActivationPending = false; }; } // namespace chip diff --git a/examples/network-manager-app/linux/ThreadBROpenThreadUbus.cpp b/examples/network-manager-app/linux/ThreadBROpenThreadUbus.cpp index 708ac27fa340..24b22e9836ee 100644 --- a/examples/network-manager-app/linux/ThreadBROpenThreadUbus.cpp +++ b/examples/network-manager-app/linux/ThreadBROpenThreadUbus.cpp @@ -53,6 +53,12 @@ CHIP_ERROR OpenThreadUbusBorderRouterDelegate::Init(AttributeChangeCallback * at static_cast(req->priv)->OnDataReceived(msg, false); }), self, kInvokeTimeout); + // A revert may have run while otbr was away; its deprovision could + // not be delivered then, so deliver it now. + if (self->mRevertPending && self->SubmitDeprovision() == CHIP_NO_ERROR) + { + self->mRevertPending = false; + } }); mOtbr.SetNotificationCallback([](UbusWatch & watch, void * appState, ubus_request_data * req, const char * notification, blob_attr * msg) { static_cast(appState)->OnDataReceived(msg, true); }); @@ -83,18 +89,37 @@ bool OpenThreadUbusBorderRouterDelegate::GetInterfaceEnabled() CHIP_ERROR OpenThreadUbusBorderRouterDelegate::GetDataset(Thread::OperationalDataset & dataset, DatasetType type) { - VerifyOrReturnError(type == DatasetType::kActive, CHIP_ERROR_INVALID_ARGUMENT); - VerifyOrReturnError(!mActiveDataset.IsEmpty(), CHIP_ERROR_NOT_FOUND); - dataset = mActiveDataset; + VerifyOrReturnError(type == DatasetType::kActive || type == DatasetType::kPending, CHIP_ERROR_INVALID_ARGUMENT); + + const Thread::OperationalDataset & source = (type == DatasetType::kPending) ? mPendingDataset : mActiveDataset; + VerifyOrReturnError(!source.IsEmpty(), CHIP_ERROR_NOT_FOUND); + dataset = source; return CHIP_NO_ERROR; } using ErrorField = BlobMsgField; +namespace { + +// Owns the ubus_request of an in-flight provision invocation. otbr does not +// reply to provision until the device has attached, which can take tens of +// seconds, so the request has to outlive SetActiveDataset() and must not +// block the event loop the way ubus_invoke() would. +struct ProvisionRequest +{ + ubus_request req = {}; + OpenThreadUbusBorderRouterDelegate * delegate; + uint32_t sequence = 0; + uint16_t otError = 0; +}; + +} // namespace + void OpenThreadUbusBorderRouterDelegate::SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNum, ActivateDatasetCallback * callback) { - CHIP_ERROR err = CHIP_ERROR_INTERNAL; + CHIP_ERROR err = CHIP_ERROR_INTERNAL; + ProvisionRequest * invoke = nullptr; VerifyOrExit(activeDataset.IsCommissioned(), err = CHIP_ERROR_INVALID_ARGUMENT); VerifyOrExit(mActiveDataset.IsEmpty(), err = CHIP_ERROR_INCORRECT_STATE); VerifyOrExit(mActivateDatasetCallback == nullptr, err = CHIP_ERROR_BUSY); @@ -104,31 +129,175 @@ void OpenThreadUbusBorderRouterDelegate::SetActiveDataset(const Thread::Operatio BlobMsgBuf buf; buf.Add("dataset", activeDataset.AsByteSpan()); ChipLogDetail(AppServer, "SetActiveDataset invoking on %d", mOtbr.ObjectID()); - VerifyOrExit(!ubus_invoke(&mUbusManager.Context(), mOtbr.ObjectID(), "provision", buf.head, - ([](ubus_request * req, int type, blob_attr * msg) { - ErrorField otError; - VerifyOrReturn(BlobMsgParse(msg, otError) && otError.value_or(0) == 0); - *static_cast(req->priv) = CHIP_NO_ERROR; - }), - &err, kInvokeTimeout), + invoke = new ProvisionRequest; + invoke->delegate = this; + invoke->sequence = sequenceNum; + VerifyOrExit(!ubus_invoke_async(&mUbusManager.Context(), mOtbr.ObjectID(), "provision", buf.head, &invoke->req), err = CHIP_ERROR_INTERNAL); } + invoke->req.priv = invoke; + invoke->req.data_cb = [](ubus_request * req, int type, blob_attr * msg) { + ErrorField otError; + VerifyOrReturn(BlobMsgParse(msg, otError)); + static_cast(req->priv)->otError = otError.value_or(0); + }; + invoke->req.complete_cb = [](ubus_request * req, int ret) { + auto * self = static_cast(req->priv); + + // provision replies once the dataset is committed and the join is + // under way; completing the activation here keeps the Matter command + // response well inside the controller's interaction timeout, which an + // attach (>10s even for a lone border router becoming leader) would + // overrun. The attach itself is reported through the + // device_role_changed notification and the cluster's attributes. + // A fail-safe revert may have detached this activation and a new + // one may have started; complete only the activation this request + // belongs to. + if (auto * cb = self->delegate->mActivateDatasetCallback; + cb != nullptr && self->sequence == self->delegate->mActivateDatasetSequence) + { + const bool failed = (ret != 0 || self->otError != 0); + self->delegate->mActivateDatasetCallback = nullptr; + if (failed) + { + self->delegate->mActiveDataset.Clear(); + self->delegate->mActivationPending = false; + ChipLogError(AppServer, "provision failed: ubus %d, otError %u", ret, self->otError); + } + cb->OnActivateDatasetComplete(self->delegate->mActivateDatasetSequence, failed ? CHIP_ERROR_INTERNAL : CHIP_NO_ERROR); + } + delete self; + }; + ubus_complete_request_async(&mUbusManager.Context(), &invoke->req); + mActiveDataset = activeDataset; mActivateDatasetCallback = callback; mActivateDatasetSequence = sequenceNum; + mActivationPending = true; return; exit: + delete invoke; callback->OnActivateDatasetComplete(sequenceNum, err); } +CHIP_ERROR OpenThreadUbusBorderRouterDelegate::InvokeWithDataset(const char * method, const Thread::OperationalDataset & dataset) +{ + CHIP_ERROR err = CHIP_ERROR_INTERNAL; + BlobMsgBuf buf; + + VerifyOrReturnError(mOtbr.Resolved(), CHIP_ERROR_NOT_CONNECTED); + + buf.Add("dataset", dataset.AsByteSpan()); + ChipLogDetail(AppServer, "%s invoking on %d", method, mOtbr.ObjectID()); + VerifyOrReturnError(!ubus_invoke(&mUbusManager.Context(), mOtbr.ObjectID(), method, buf.head, + ([](ubus_request * req, int type, blob_attr * msg) { + ErrorField otError; + VerifyOrReturn(BlobMsgParse(msg, otError) && otError.value_or(0) == 0); + *static_cast(req->priv) = CHIP_NO_ERROR; + }), + &err, kInvokeTimeout), + CHIP_ERROR_INTERNAL); + + return err; +} + +CHIP_ERROR OpenThreadUbusBorderRouterDelegate::SetPendingDataset(const Thread::OperationalDataset & pendingDataset) +{ + // otbr schedules the migration; the switch happens when the dataset's delay + // timer expires, and the pending_dataset_changed notification reports it. + return InvokeWithDataset("set_pending", pendingDataset); +} + +CHIP_ERROR OpenThreadUbusBorderRouterDelegate::RevertActiveDataset() +{ + // The fail-safe expiry handler calls this for every expired fail-safe, + // whether or not it carried a dataset activation: a failed commissioning + // attempt by an unrelated controller must not wipe the network. Only an + // activation that has not been committed may be reverted. + VerifyOrReturnError(mActivationPending, CHIP_NO_ERROR); + mActivationPending = false; + // The provision may still be in flight; without the callback its + // completion is a no-op instead of completing the reverted activation + // (and a new attempt meanwhile would read as Busy). + mActivateDatasetCallback = nullptr; + + // SetActiveDataset is only accepted when no dataset is configured, so + // reverting means returning to the unprovisioned state rather than + // restoring a previous dataset. + + // deprovision detaches gracefully before erasing, so its reply can be + // seconds away; fire the request asynchronously and let the otbr + // notifications resync the cached state. Local state is cleared right + // away: returning to unprovisioned is the outcome either way. + mActiveDataset.Clear(); + mAttributeChangeCallback->ReportAttributeChanged(ActiveDatasetTimestamp::Id); + + CHIP_ERROR err = SubmitDeprovision(); + if (err != CHIP_NO_ERROR) + { + // The fail-safe fires once; remember the revert so the deprovision is + // delivered when otbr comes back rather than never. + mRevertPending = true; + } + return err; +} + +CHIP_ERROR OpenThreadUbusBorderRouterDelegate::SubmitDeprovision() +{ + VerifyOrReturnError(mOtbr.Resolved(), CHIP_ERROR_NOT_CONNECTED); + + auto * invoke = new ProvisionRequest; + invoke->delegate = this; + if (ubus_invoke_async(&mUbusManager.Context(), mOtbr.ObjectID(), "deprovision", nullptr, &invoke->req)) + { + delete invoke; + return CHIP_ERROR_INTERNAL; + } + invoke->req.priv = invoke; + invoke->req.data_cb = [](ubus_request * req, int type, blob_attr * msg) { + ErrorField otError; + VerifyOrReturn(BlobMsgParse(msg, otError)); + static_cast(req->priv)->otError = otError.value_or(0); + }; + invoke->req.complete_cb = [](ubus_request * req, int ret) { + auto * self = static_cast(req->priv); + if (ret != 0 || self->otError != 0) + { + ChipLogError(AppServer, "deprovision failed: ubus %d, otError %u", ret, self->otError); + // otbr kept state this delegate no longer reports. Pull its truth + // back into the cache so the two do not quietly diverge; a + // connection-failed completion needs no resync and the context + // may already be gone. + if (ret != UBUS_STATUS_CONNECTION_FAILED) + { + self->delegate->ResyncFromOtbr(); + } + } + delete self; + }; + ubus_complete_request_async(&mUbusManager.Context(), &invoke->req); + + return CHIP_NO_ERROR; +} + +void OpenThreadUbusBorderRouterDelegate::ResyncFromOtbr() +{ + VerifyOrReturn(mOtbr.Resolved()); + ubus_invoke(&mUbusManager.Context(), mOtbr.ObjectID(), "status", nullptr, ([](ubus_request * req, int type, blob_attr * msg) { + static_cast(req->priv)->OnDataReceived(msg, false); + }), + this, kInvokeTimeout); +} + void OpenThreadUbusBorderRouterDelegate::OnDataReceived(blob_attr * msg, bool notification) { BlobMsgField borderAgentID; BlobMsgField activeDataset; + BlobMsgField pendingDataset; BlobMsgField attached; - BlobMsgParse(msg, borderAgentID, attached, activeDataset); + BlobMsgParse(msg, borderAgentID, attached, activeDataset, pendingDataset); if (!mBorderAgentIDValid && borderAgentID.has_value() && borderAgentID->size() == sizeof(mBorderAgentID)) { @@ -152,6 +321,36 @@ void OpenThreadUbusBorderRouterDelegate::OnDataReceived(blob_attr * msg, bool no } } + if (pendingDataset.has_value()) + { + bool updated = false; + // An empty payload means a scheduled migration has completed, so the + // dataset is cleared rather than left reporting a stale timestamp. + // Only an actual change is reported; a snapshot repeating the known + // dataset must not wake subscribers. + if (pendingDataset->empty()) + { + if (!mPendingDataset.IsEmpty()) + { + mPendingDataset.Clear(); + updated = true; + } + } + else if (!mPendingDataset.AsByteSpan().data_equal(pendingDataset.value())) + { + Thread::OperationalDatasetView dataset; + if (dataset.Init(pendingDataset.value()) == CHIP_NO_ERROR) + { + mPendingDataset = dataset; + updated = true; + } + } + if (notification && updated) + { + mAttributeChangeCallback->ReportAttributeChanged(PendingDatasetTimestamp::Id); + } + } + if (attached.has_value()) { ChipLogProgress(AppServer, "Received OTBR Attached = %d", attached.value()); diff --git a/examples/network-manager-app/linux/ThreadBROpenThreadUbus.h b/examples/network-manager-app/linux/ThreadBROpenThreadUbus.h index 42690da15ce2..ce53dcc53a2e 100644 --- a/examples/network-manager-app/linux/ThreadBROpenThreadUbus.h +++ b/examples/network-manager-app/linux/ThreadBROpenThreadUbus.h @@ -39,13 +39,24 @@ class OpenThreadUbusBorderRouterDelegate final : public app::Clusters::ThreadBor void SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNum, ActivateDatasetCallback * callback) override; - bool GetPanChangeSupported() override { return false; } - CHIP_ERROR CommitActiveDataset() override { return CHIP_NO_ERROR; } - CHIP_ERROR RevertActiveDataset() override { return CHIP_ERROR_NOT_IMPLEMENTED; } - CHIP_ERROR SetPendingDataset(const chip::Thread::OperationalDataset &) override { return CHIP_ERROR_NOT_IMPLEMENTED; } + // otbr implements MGMT_PENDING_SET via its set_pending method, so a running + // network can be migrated rather than only formed. + bool GetPanChangeSupported() override { return true; } + CHIP_ERROR CommitActiveDataset() override + { + mActivationPending = false; + return CHIP_NO_ERROR; + } + CHIP_ERROR RevertActiveDataset() override; + CHIP_ERROR SetPendingDataset(const chip::Thread::OperationalDataset & pendingDataset) override; private: void OnDataReceived(blob_attr * msg, bool notification); + CHIP_ERROR SubmitDeprovision(); + void ResyncFromOtbr(); + + // Invokes an otbr method that takes a hex encoded dataset argument. + CHIP_ERROR InvokeWithDataset(const char * method, const Thread::OperationalDataset & dataset); AttributeChangeCallback * mAttributeChangeCallback; @@ -56,8 +67,18 @@ class OpenThreadUbusBorderRouterDelegate final : public app::Clusters::ThreadBor uint8_t mBorderAgentID[app::Clusters::ThreadBorderRouterManagement::kBorderAgentIdLength]; Thread::OperationalDataset mActiveDataset; + Thread::OperationalDataset mPendingDataset; ActivateDatasetCallback * mActivateDatasetCallback = nullptr; uint32_t mActivateDatasetSequence; + // An activation that has not been committed yet. Only such an activation + // may be reverted: the fail-safe expiry handler reverts unconditionally, + // including for fail-safes that never touched the dataset, and reverting + // then would wipe a network provisioned outside Matter. + bool mActivationPending = false; + // A revert whose deprovision could not be delivered. The fail-safe fires + // once, so without this the dataset would stay on the router forever if + // otbr happened to be away at that moment; retried when it comes back. + bool mRevertPending = false; }; } // namespace chip diff --git a/examples/network-manager-app/linux/UbusManager.cpp b/examples/network-manager-app/linux/UbusManager.cpp index 9595e11ec567..c1e2bf25532f 100644 --- a/examples/network-manager-app/linux/UbusManager.cpp +++ b/examples/network-manager-app/linux/UbusManager.cpp @@ -87,6 +87,21 @@ void UbusManager::Shutdown() void UbusManager::HandleConnectionLost() { ChipLogProgress(DeviceLayer, "Ubus connection lost, reconnection will be attempted periodically"); + + // Requests still in flight will never be answered on this connection, and + // ubus_shutdown() leaves them registered without completing them. Fail + // them so their owners see the outcome and can release what they hold. + // Abort first, so the completion cannot re-enter a half-torn list. + while (!list_empty(&Context().requests)) + { + ubus_request * req = list_first_entry(&Context().requests, ubus_request, list); + ubus_abort_request(&Context(), req); + if (req->complete_cb != nullptr) + { + req->complete_cb(req, UBUS_STATUS_CONNECTION_FAILED); + } + } + ubus_shutdown(&Context()); ResetEventHandler(); @@ -138,9 +153,41 @@ bool UbusManager::Connect() VerifyOrReturnValue(CheckAndLog(ubus_connect_ctx(&Context(), mUbusSocketPath), "ubus_connect_ctx"), false); Context().connection_lost = [](ubus_context * ctx) { static_cast(ctx)->HandleConnectionLost(); }; ubus_add_uloop(&Context()); + if (mHostedObject != nullptr) + { + // A new connection means a new bus client: the object has to be + // published again. Ids assigned by a previous connection are stale -- + // including the type id, which ubus_add_object() would otherwise send + // in place of the method table, re-hosting the object without its + // methods. + mHostedObject->id = 0; + if (mHostedObject->type != nullptr) + { + mHostedObject->type->id = 0; + } + if (!CheckAndLog(ubus_add_object(&Context(), mHostedObject), "ubus_add_object")) + { + // Without the object this connection does not provide what it is + // supposed to; treat the attempt as failed so the retry timer + // keeps trying rather than reporting a restored connection. + ubus_shutdown(&Context()); + return false; + } + } return true; } +CHIP_ERROR UbusManager::Host(ubus_object & object) +{ + VerifyOrDie(mInitialized && mHostedObject == nullptr); + mHostedObject = &object; + if (Connected()) + { + VerifyOrReturnError(CheckAndLog(ubus_add_object(&Context(), mHostedObject), "ubus_add_object"), CHIP_ERROR_INTERNAL); + } + return CHIP_NO_ERROR; +} + void UbusManager::Register(UbusWatch & watch) { VerifyOrDie(mInitialized); diff --git a/examples/network-manager-app/linux/UbusManager.h b/examples/network-manager-app/linux/UbusManager.h index 5f8c8b718b38..4bbcb5d96974 100644 --- a/examples/network-manager-app/linux/UbusManager.h +++ b/examples/network-manager-app/linux/UbusManager.h @@ -50,10 +50,15 @@ class UbusManager : private ubus_context, private uloop_timeout, private ubus_ev void Register(UbusWatch & watch); void Unregister(UbusWatch & watch); + // Publishes a ubus object on the bus, re-adding it after reconnects. + // The object must outlive the manager; only one object is supported. + CHIP_ERROR Host(ubus_object & object); + private: const char * const mUbusSocketPath; IntrusiveList mWatches{}; - bool mInitialized = false; + ubus_object * mHostedObject = nullptr; + bool mInitialized = false; ///// Connection management diff --git a/examples/network-manager-app/linux/include/CHIPProjectAppConfig.h b/examples/network-manager-app/linux/include/CHIPProjectAppConfig.h index 71b853be2c39..fb2ed7e32adc 100644 --- a/examples/network-manager-app/linux/include/CHIPProjectAppConfig.h +++ b/examples/network-manager-app/linux/include/CHIPProjectAppConfig.h @@ -20,6 +20,10 @@ #define CHIP_DEVICE_CONFIG_DEVICE_TYPE 144 // 0x0090 Network Infrastructure Manager #define CHIP_DEVICE_CONFIG_DEVICE_NAME "Network Infrastructure Manager" +// Advertise the device name and type while commissionable; without +// them, commissioners fall back to a generic accessory placeholder. +#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONABLE_DEVICE_NAME 1 +#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONABLE_DEVICE_TYPE 1 #define CHIP_DEVICE_CONFIG_DEVICE_PRODUCT_ID 0x8013 // Sufficient space for ArlReviewEvent of several fabrics. diff --git a/examples/network-manager-app/linux/main.cpp b/examples/network-manager-app/linux/main.cpp index 78efe1a85335..a086f5850830 100644 --- a/examples/network-manager-app/linux/main.cpp +++ b/examples/network-manager-app/linux/main.cpp @@ -29,6 +29,7 @@ #include #if MATTER_ENABLE_UBUS +#include "MatterUbusService.h" #include "ThreadBROpenThreadUbus.h" #include "UbusManager.h" #else @@ -43,6 +44,7 @@ using namespace chip::app::Clusters; #if MATTER_ENABLE_UBUS ubus::UbusManager gUbusManager{}; +MatterUbusService gMatterUbusService{ gUbusManager }; #endif std::optional gThreadNetworkDirectoryServer; @@ -105,6 +107,11 @@ void ApplicationInit() { TEMPORARY_RETURN_IGNORED gWiFiNetworkManagementServer->SetNetworkCredentials(ByteSpan::fromCharSpan("MatterAP"_span), ByteSpan::fromCharSpan("Setec Astronomy"_span)); +#if MATTER_ENABLE_UBUS + // Publish the "matter" ubus object once the server is up; its handlers + // read commissioning state owned by the server. + SuccessOrDie(gMatterUbusService.Init()); +#endif } void ApplicationShutdown()