From ead53c8b8c95c7927220c48a624e8759e50f50c3 Mon Sep 17 00:00:00 2001 From: Yakun Xu Date: Sun, 9 Nov 2025 19:16:53 +0800 Subject: [PATCH] Thread: commissioning over Thread MeshCoP This commit adds support of Matter commissioning over Thread. --- .github/workflows/tests.yaml | 23 +- examples/all-clusters-app/linux/args.gni | 5 + .../linux/include/CHIPProjectAppConfig.h | 5 + examples/chip-tool/BUILD.gn | 15 +- .../commands/pairing/CommissionProxy.cpp | 436 ++++++++++++++++++ .../commands/pairing/CommissionProxy.h | 107 +++++ .../commands/pairing/PairingCommand.cpp | 109 ++++- .../commands/pairing/PairingCommand.h | 16 + examples/platform/linux/AppMain.cpp | 5 +- scripts/tests/chiptest/linux.py | 13 + scripts/tests/chiptest/test_definition.py | 20 +- scripts/tests/run_test_suite.py | 14 +- .../NetworkCommissioningCluster.cpp | 4 +- src/app/server/BUILD.gn | 9 + src/app/server/CommissioningWindowManager.cpp | 6 + src/app/server/Dnssd.cpp | 38 +- src/app/server/Dnssd.h | 9 + src/app/server/Server.cpp | 2 + .../server/ThreadRendezvousAnnouncement.cpp | 149 ++++++ src/app/server/ThreadRendezvousAnnouncement.h | 76 +++ src/app/server/tests/BUILD.gn | 1 + .../TestThreadRendezvousAnnouncement.cpp | 109 +++++ src/controller/AutoCommissioner.cpp | 3 +- src/controller/AutoCommissioner.h | 2 + src/controller/CHIPDeviceController.cpp | 4 + src/include/platform/ThreadStackManager.h | 36 ++ src/inet/UDPEndPointImplOpenThread.cpp | 17 +- src/lib/dnssd/BUILD.gn | 2 + src/lib/dnssd/Types.h | 1 + src/lib/dnssd/minimal_mdns/core/QNameString.h | 19 + .../core/tests/TestQNameString.cpp | 17 + src/lib/support/BUILD.gn | 3 + src/lib/support/ThreadDiscoveryCode.cpp | 48 ++ src/lib/support/ThreadDiscoveryCode.h | 46 ++ src/lib/support/tests/BUILD.gn | 1 + .../support/tests/TestThreadDiscoveryCode.cpp | 60 +++ src/platform/BUILD.gn | 5 +- src/platform/Linux/ThreadStackManagerImpl.h | 9 + .../ThreadStackManagerImpl_OpenThread.cpp | 5 + .../Linux/ThreadStackManagerImpl_OpenThread.h | 1 + ...GenericThreadStackManagerImpl_OpenThread.h | 28 ++ ...nericThreadStackManagerImpl_OpenThread.hpp | 159 ++++++- src/platform/device.gni | 6 + src/setup_payload/SetupPayload.cpp | 2 +- src/setup_payload/SetupPayload.h | 1 + src/setup_payload/tests/TestQRCode.cpp | 2 +- third_party/openthread/openthread-config.h | 20 + third_party/openthread/repo | 2 +- 48 files changed, 1633 insertions(+), 37 deletions(-) create mode 100644 examples/chip-tool/commands/pairing/CommissionProxy.cpp create mode 100644 examples/chip-tool/commands/pairing/CommissionProxy.h create mode 100644 src/app/server/ThreadRendezvousAnnouncement.cpp create mode 100644 src/app/server/ThreadRendezvousAnnouncement.h create mode 100644 src/app/server/tests/TestThreadRendezvousAnnouncement.cpp create mode 100644 src/lib/support/ThreadDiscoveryCode.cpp create mode 100644 src/lib/support/ThreadDiscoveryCode.h create mode 100644 src/lib/support/tests/TestThreadDiscoveryCode.cpp create mode 100644 third_party/openthread/openthread-config.h diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 17bd23e8d9f139..784aabc283ab6a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -282,6 +282,7 @@ jobs: --target linux-x64-chip-tool${CHIP_TOOL_VARIANT}-${BUILD_VARIANT} \ --target linux-x64-all-clusters-${BUILD_VARIANT} \ --target linux-x64-all-clusters-no-wifi-openthread-endpoint-${BUILD_VARIANT} \ + --target linux-x64-all-clusters-no-wifi-no-ble-openthread-endpoint-${BUILD_VARIANT} \ --target linux-x64-ota-requestor-${BUILD_VARIANT} \ --target linux-x64-tv-app-${BUILD_VARIANT} \ --pw-command-launcher=ccache \ @@ -365,9 +366,29 @@ jobs: --iterations 1 \ --test-timeout-seconds 120 \ --tool-path chip-tool:./objdir-clone/linux-x64-chip-tool${CHIP_TOOL_VARIANT}-${BUILD_VARIANT}/chip-tool \ - --all-clusters-app ./objdir-clone/linux-x64-all-clusters-no-wifi-openthread-endpoint-${BUILD_VARIANT}/chip-all-clusters-app \ + --app-path all-clusters:./objdir-clone/linux-x64-all-clusters-no-wifi-openthread-endpoint-${BUILD_VARIANT}/chip-all-clusters-app \ --commissioning-method ble-thread \ " + rm -rf tmp/ + + - name: Run Thread-MeshCoP commissioning test + env: + TSAN_OPTIONS: report_bugs=0 + run: | + PATH=$PWD/objdir-clone:$PATH ./scripts/run_in_build_env.sh \ + "./scripts/tests/run_test_suite.py \ + --find-path $PWD/objdir-clone \ + --find-path $PWD/scripts \ + --runner chip_tool_python \ + --target TestOperationalState \ + run \ + --iterations 1 \ + --test-timeout-seconds 120 \ + --tool-path chip-tool:./objdir-clone/linux-x64-chip-tool${CHIP_TOOL_VARIANT}-${BUILD_VARIANT}/chip-tool \ + --app-path all-clusters:./objdir-clone/linux-x64-all-clusters-no-wifi-no-ble-openthread-endpoint-${BUILD_VARIANT}/chip-all-clusters-app \ + --commissioning-method thread-meshcop \ + " + rm -rf tmp/ - name: Run Tests using the python parser sending commands to chip-tool run: | diff --git a/examples/all-clusters-app/linux/args.gni b/examples/all-clusters-app/linux/args.gni index b10d702c1989d8..3aa3dd5444458f 100644 --- a/examples/all-clusters-app/linux/args.gni +++ b/examples/all-clusters-app/linux/args.gni @@ -33,6 +33,11 @@ chip_enable_software_diagnostics_trigger = true chip_enable_wifi_diagnostics_trigger = true chip_minmdns_high_verbosity = true +_openthread_config_file = + rebase_path( + get_path_info("${chip_root}/third_party/openthread/openthread-config.h", + "abspath")) +openthread_config_file = "\"$_openthread_config_file\"" openthread_config_ecdsa_enable = true openthread_config_ip6_slaac_enable = true openthread_config_full_logs = true diff --git a/examples/all-clusters-app/linux/include/CHIPProjectAppConfig.h b/examples/all-clusters-app/linux/include/CHIPProjectAppConfig.h index 8a86e53f80553d..77bc3eb0425c91 100644 --- a/examples/all-clusters-app/linux/include/CHIPProjectAppConfig.h +++ b/examples/all-clusters-app/linux/include/CHIPProjectAppConfig.h @@ -27,6 +27,11 @@ #pragma once +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP && !CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE +#define CHIP_DEVICE_CONFIG_SUPPORTS_CONCURRENT_CONNECTION 0 +#define CONFIG_RENDEZVOUS_MODE (1 << 5) // Thread MeshCoP +#endif + // include the CHIPProjectConfig from config/standalone #include diff --git a/examples/chip-tool/BUILD.gn b/examples/chip-tool/BUILD.gn index ff4ef41b23e985..4c19ca0d627708 100644 --- a/examples/chip-tool/BUILD.gn +++ b/examples/chip-tool/BUILD.gn @@ -28,6 +28,10 @@ import("${chip_root}/src/crypto/crypto.gni") assert(chip_build_tools) +# Thread Commissioner is using newer version of mbedtls +_enable_thread_meshcop = chip_device_platform == "linux" && + chip_enable_ot_commissioner && chip_crypto != "mbedtls" + config("config") { include_dirs = [ ".", @@ -51,6 +55,12 @@ config("config") { defines += [ "CONFIG_ENABLE_HTTPS_REQUESTS" ] } + if (_enable_thread_meshcop) { + defines += [ "CHIP_ENABLE_OT_COMMISSIONER=1" ] + } else { + defines += [ "CHIP_ENABLE_OT_COMMISSIONER=0" ] + } + cflags = [ "-Wconversion" ] } @@ -145,8 +155,9 @@ static_library("chip-tool-utils") { [ "${chip_root}/examples/common/tracing:trace_handlers_decoder" ] } - if (chip_device_platform == "linux" && chip_enable_ot_commissioner) { - deps += [ "${chip_root}/third_party/ot-commissioner" ] + if (_enable_thread_meshcop) { + sources += [ "commands/pairing/CommissionProxy.cpp" ] + public_deps += [ "${chip_root}/third_party/ot-commissioner" ] } output_dir = root_out_dir diff --git a/examples/chip-tool/commands/pairing/CommissionProxy.cpp b/examples/chip-tool/commands/pairing/CommissionProxy.cpp new file mode 100644 index 00000000000000..9e4b827648b434 --- /dev/null +++ b/examples/chip-tool/commands/pairing/CommissionProxy.cpp @@ -0,0 +1,436 @@ +/* + * 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 "CommissionProxy.h" + +#include +#include +#include // nogncheck +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace chip; + +namespace { +/** + * Internal OT Commissioner Logger implementation. + */ +class CommissionerLogger : public ot::commissioner::Logger +{ +public: + void Log(ot::commissioner::LogLevel level, const std::string & region, const std::string & message) override + { + ChipLogProgress(chipTool, "[ot-commissioner][%u][%s] %s", static_cast(level), region.c_str(), message.c_str()); + } +}; + +constexpr char kMatterCServiceSuffix[] = "_matterc._udp.local"; + +uint64_t JoinerIdFromBytes(const std::vector & bytes) +{ + const uint8_t * buffer = bytes.data(); + return Encoding::BigEndian::Read64(buffer); +} + +std::vector DiscoveryCodeToVector(Thread::DiscoveryCode code) +{ + uint8_t bytes[sizeof(uint64_t)]; + Encoding::BigEndian::Put64(bytes, code.AsUInt64()); + return std::vector(bytes, bytes + sizeof(bytes)); +} +} // namespace + +CommissionProxy::CommissionProxy() : mState(State::kConnecting), mPromiseFulfilled(false) +{ + mCommissioner = ot::commissioner::Commissioner::Create(*this); +} + +CommissionProxy::~CommissionProxy() +{ + if (mProxyFd != -1) + { + close(mProxyFd); + mProxyFd = -1; + } + + if (mProxyThread.joinable()) + { + mProxyThread.join(); + } +} + +void CommissionProxy::SetState(State state) +{ + mState = state; +} + +void CommissionProxy::OnHeader(mdns::Minimal::ConstHeaderRef & header) +{ + ChipLogDetail(chipTool, "mDNS Response: ID=%u, Answers=%u, Additional=%u", header.GetMessageId(), header.GetAnswerCount(), + header.GetAdditionalCount()); +} + +void CommissionProxy::OnQuery(const mdns::Minimal::QueryData & data) +{ + if (mState != State::kDiscovering) + { + ChipLogProgress(chipTool, "Received mDNS query but proxy is not in discovery state"); + } + + ChipLogDetail(chipTool, "mDNS query: %s", mdns::Minimal::QNameString(data.GetName()).c_str()); + mNodeData.Set(); +} + +void CommissionProxy::OnResource(mdns::Minimal::ResourceType section, const mdns::Minimal::ResourceData & data) +{ + if (mState != State::kDiscovering) + { + return; + } + + auto name = mdns::Minimal::QNameString(data.GetName()); + auto & commissionData = mNodeData.Get(); + + commissionData.threadMeshcop = true; + + switch (data.GetType()) + { + case mdns::Minimal::QType::A: + case mdns::Minimal::QType::AAAA: + Platform::CopyString(commissionData.hostName, name.c_str()); + break; + + case mdns::Minimal::QType::SRV: { + mdns::Minimal::SrvRecord srv; + if (!srv.Parse(data.GetData(), mDnsPacket)) + { + ChipLogError(chipTool, "Failed to parse mDNS SRV record"); + return; + } + + if (!name.EndsWith(kMatterCServiceSuffix)) + { + ChipLogDetail(chipTool, "Ignoring non-Matter service: %s", name.c_str()); + return; + } + + // Extract the instance label (portion before "._matterc._udp.local") for CommissionNodeData::instanceName. + std::string fullName(name.c_str()); + constexpr size_t kMatterCServiceSuffixLen = sizeof(kMatterCServiceSuffix) - 1; // exclude null terminator + if (fullName.length() >= kMatterCServiceSuffixLen) + { + fullName.erase(fullName.length() - kMatterCServiceSuffixLen); + } + Platform::CopyString(commissionData.instanceName, fullName.c_str()); + + mServicePort = srv.GetPort(); + + if (mProxyFd == -1) + { + CHIP_ERROR err = CreateProxySocket(commissionData); + if (err != CHIP_NO_ERROR) + { + ChipLogError(chipTool, "Failed to setup proxy socket: %" CHIP_ERROR_FORMAT, err.Format()); + SetState(State::kAborted); + } + } + break; + } + + case mdns::Minimal::QType::TXT: + mdns::Minimal::ParseTxtRecord(data.GetData(), this); + break; + + default: + break; + } +} + +CHIP_ERROR CommissionProxy::CreateProxySocket(chip::Dnssd::CommissionNodeData & commissionData) +{ + mProxyFd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + VerifyOrReturnError(mProxyFd >= 0, CHIP_ERROR_POSIX(errno)); + + sockaddr_in6 addr = {}; + addr.sin6_family = AF_INET6; + addr.sin6_port = 0; + addr.sin6_addr = in6addr_loopback; + + if (bind(mProxyFd, reinterpret_cast(&addr), sizeof(addr)) != 0) + { + close(mProxyFd); + mProxyFd = -1; + return CHIP_ERROR_POSIX(errno); + } + + socklen_t addr_len = sizeof(addr); + if (getsockname(mProxyFd, reinterpret_cast(&addr), &addr_len) == -1) + { + close(mProxyFd); + mProxyFd = -1; + return CHIP_ERROR_POSIX(errno); + } + + commissionData.numIPs = 1; + commissionData.port = ntohs(addr.sin6_port); + commissionData.ipAddress[0] = Inet::IPAddress::FromSockAddr(addr); + commissionData.interfaceId = Inet::InterfaceId::FromIPAddress(commissionData.ipAddress[0]); + + ChipLogProgress(chipTool, "Proxy socket created on port %u", commissionData.port); + return CHIP_NO_ERROR; +} + +void CommissionProxy::OnRecord(const mdns::Minimal::BytesRange & name, const mdns::Minimal::BytesRange & value) +{ + ByteSpan key(name.Start(), name.Size()); + ByteSpan val(value.Start(), value.Size()); + + Dnssd::FillNodeDataFromTxt(key, val, mNodeData.Get()); +} + +void CommissionProxy::ProcessAnnouncement(const std::vector & joinerIdBytes, uint16_t joinerPort, + const std::vector & payload) +{ + std::lock_guard lock(mMutex); + + if (mPromiseFulfilled) + { + return; + } + + mNodeData.Set(); + mDnsPacket = mdns::Minimal::BytesRange(payload.data(), payload.data() + payload.size()); + + if (!mdns::Minimal::ParsePacket(mDnsPacket, this)) + { + ChipLogError(chipTool, "Failed to parse joiner mDNS announcement"); + return; + } + + uint32_t discoveredDiscriminator = mNodeData.Get().longDiscriminator; + ChipLogProgress(chipTool, "Discovered joiner with discriminator: %u", discoveredDiscriminator); + + if (!mExpectedDiscriminator.MatchesLongDiscriminator(static_cast(discoveredDiscriminator))) + { + ChipLogProgress(chipTool, "Discriminator mismatch (Expected %u, Got %u). Ignoring announcement.", + mExpectedDiscriminator.GetLongValue(), discoveredDiscriminator); + return; + } + + mDiscoveredNodePromise.set_value(mNodeData); + mPromiseFulfilled = true; + + SetState(State::kDiscovered); + + if (mProxyThread.joinable()) + { + mProxyThread.join(); + } + + mProxyThread = std::thread([id = joinerIdBytes, this]() { + struct sockaddr_storage addr; + socklen_t len = sizeof(addr); + uint8_t buf[chip::detail::kMaxIPPacketSizeBytes]; + ssize_t received; + + while ((received = recvfrom(mProxyFd, buf, sizeof(buf), 0, reinterpret_cast(&addr), &len)) > 0) + { + switch (mState) + { + case State::kDiscovered: { + int rval = connect(mProxyFd, reinterpret_cast(&addr), len); + if (rval < 0) + { + ChipLogError(chipTool, "Failed to connect to Matter Commissioner: %s", strerror(errno)); + continue; + } + SetState(State::kCommissioning); + FALLTHROUGH; + } + + case State::kCommissioning: { + std::vector pkt(buf, buf + received); + + auto error = mCommissioner->SendToJoiner(id, mServicePort, pkt); + if (error != ot::commissioner::ErrorCode::kNone) + { + ChipLogError(chipTool, "Failed to send packet to joiner: %s", error.GetMessage().c_str()); + return; + } + break; + } + default: + ChipLogError(chipTool, "Invalid CommissionProxy state: %d", static_cast(mState.load())); + return; + } + } + }); +} + +void CommissionProxy::OnJoinerMessage(const std::vector & joinerIdBytes, uint16_t joinerPort, + const std::vector & payload) +{ + std::lock_guard lock(mMutex); + + if (joinerIdBytes.size() != sizeof(uint64_t) || mState == State::kAborted) + { + return; + } + + uint64_t joinerId = JoinerIdFromBytes(joinerIdBytes); + ChipLogDetail(chipTool, "Message from joiner 0x%" PRIx64 " on port %u", joinerId, joinerPort); + + if (mJoinerId == 0) + { + mJoinerId = joinerId; + } + else if (mJoinerId != joinerId) + { + ChipLogProgress(chipTool, "Ignoring message from unexpected joiner 0x%" PRIx64, joinerId); + return; + } + + switch (mState) + { + case State::kCommissioning: + if (mProxyFd != -1) + { + if (send(mProxyFd, payload.data(), payload.size(), 0) < 0) + { + ChipLogError(chipTool, "Failed to forward packet to local proxy: %s", strerror(errno)); + SetState(State::kAborted); + } + } + break; + case State::kAborted: + break; + case State::kConnecting: + // First message from joiner is usually the mDNS announcement + SetState(State::kDiscovering); + FALLTHROUGH; + case State::kDiscovering: + ProcessAnnouncement(joinerIdBytes, joinerPort, payload); + break; + case State::kDiscovered: + ChipLogProgress(chipTool, "WARNING ignore unsolicited messages after joiner is already discovered"); + break; + } +} + +ot::commissioner::CommissionerDataset CommissionProxy::MakeCommissionerDataset(Thread::DiscoveryCode code) +{ + ot::commissioner::CommissionerDataset dataset; + + dataset.mJoinerUdpPort = ot::commissioner::kDefaultJoinerUdpPort; + dataset.mPresentFlags |= ot::commissioner::CommissionerDataset::kJoinerUdpPortBit; + dataset.mPresentFlags &= + ~(ot::commissioner::CommissionerDataset::kSessionIdBit | ot::commissioner::CommissionerDataset::kBorderAgentLocatorBit); + + if (code.IsAny()) + { + dataset.mSteeringData = std::vector{ 0xff }; + } + else + { + std::vector steeringData(ot::commissioner::kMaxSteeringDataLength); + ot::commissioner::Commissioner::AddJoiner(steeringData, DiscoveryCodeToVector(code)); + dataset.mSteeringData = steeringData; + } + + dataset.mPresentFlags |= ot::commissioner::CommissionerDataset::kSteeringDataBit; + return dataset; +} +CHIP_ERROR CommissionProxy::InitializeCommissioner(uint8_t (&pskc)[Thread::kSizePSKc]) +{ + ot::commissioner::Config config; + config.mLogger = std::make_shared(); + config.mEnableCcm = false; + config.mProxyMode = true; + config.mPSKc = std::vector(pskc, pskc + Thread::kSizePSKc); + + auto error = mCommissioner->Init(config); + if (error != ot::commissioner::ErrorCode::kNone) + { + ChipLogError(chipTool, "OT Commissioner Init failed: %s", error.GetMessage().c_str()); + return CHIP_ERROR_INTERNAL; + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR CommissionProxy::Discover(uint8_t (&pskc)[Thread::kSizePSKc], const char * host, uint16_t port, + + const Thread::DiscoveryCode code, SetupDiscriminator expectedDiscriminator, + + Dnssd::DiscoveredNodeData & nodeData, uint16_t timeout) +{ + // Reset the promise and state for a new discovery session + std::future future; + { + std::lock_guard lock(mMutex); + mExpectedDiscriminator = expectedDiscriminator; + SetState(State::kConnecting); + mDiscoveredNodePromise = std::promise(); + future = mDiscoveredNodePromise.get_future(); + mPromiseFulfilled = false; + mJoinerId = 0; + } + + ReturnErrorOnFailure(InitializeCommissioner(pskc)); + + ChipLogProgress(chipTool, "Petitioning Thread Border Agent at %s:%u", host, port); + std::string id; + auto error = mCommissioner->Petition(id, std::string(host), port); + if (error != ot::commissioner::ErrorCode::kNone) + { + ChipLogError(chipTool, "Petition failed: %s", error.GetMessage().c_str()); + SetState(State::kAborted); + return CHIP_ERROR_INTERNAL; + } + + ChipLogProgress(chipTool, "Thread Commissioner active with ID: %s", id.c_str()); + + error = mCommissioner->SetCommissionerDataset(MakeCommissionerDataset(code)); + if (error != ot::commissioner::ErrorCode::kNone) + { + ChipLogError(chipTool, "Failed to set Steering Data: %s", error.GetMessage().c_str()); + SetState(State::kAborted); + return CHIP_ERROR_INTERNAL; + } + + ChipLogProgress(chipTool, "Waiting for mDNS announcement from joiner..."); + auto waitDuration = std::chrono::seconds(timeout); + if (future.wait_for(waitDuration) == std::future_status::timeout) + { + ChipLogError(chipTool, "Timed out waiting for joiner mDNS announcement after %u seconds", timeout); + SetState(State::kAborted); + return CHIP_ERROR_TIMEOUT; + } + nodeData = future.get(); + return CHIP_NO_ERROR; +} diff --git a/examples/chip-tool/commands/pairing/CommissionProxy.h b/examples/chip-tool/commands/pairing/CommissionProxy.h new file mode 100644 index 00000000000000..aec81de8e1eb62 --- /dev/null +++ b/examples/chip-tool/commands/pairing/CommissionProxy.h @@ -0,0 +1,107 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../third_party/ot-commissioner/repo/include/commissioner/commissioner.hpp" + +/** + * CommissionProxy acts as a bridge between the OpenThread Commissioner and Matter commissioning. + * It handles Thread-specific commissioning (MeshCoP) and proxies mDNS discovery data + * to facilitate the transition into Matter's operational commissioning flow. + */ +class CommissionProxy : public ot::commissioner::CommissionerHandler, + public mdns::Minimal::ParserDelegate, + public mdns::Minimal::TxtRecordDelegate +{ +public: + enum class State + { + kConnecting, // Establishing connection to Thread Border Agent + kDiscovering, // Waiting for mDNS announcements from the joiner + kDiscovered, // Discovered a matching joiner + kCommissioning, // Proxying packets between local socket and joiner + kAborted, // Error or user cancellation + }; + + CommissionProxy(); + ~CommissionProxy() override; + + /** + * Entry point to start the Thread commissioning and discover the device. + */ + CHIP_ERROR Discover(uint8_t (&pskc)[chip::Thread::kSizePSKc], const char * host, uint16_t port, + const chip::Thread::DiscoveryCode code, chip::SetupDiscriminator expectedDiscriminator, + chip::Dnssd::DiscoveredNodeData & nodeData, uint16_t timeout); + + // ot::commissioner::CommissionerHandler + void OnJoinerMessage(const std::vector & joinerIdBytes, uint16_t joinerPort, + const std::vector & payload) override; + + // mdns::Minimal::ParserDelegate + void OnHeader(mdns::Minimal::ConstHeaderRef & header) override; + void OnQuery(const mdns::Minimal::QueryData & data) override; + void OnResource(mdns::Minimal::ResourceType section, const mdns::Minimal::ResourceData & data) override; + + // mdns::Minimal::TxtRecordDelegate + void OnRecord(const mdns::Minimal::BytesRange & name, const mdns::Minimal::BytesRange & value) override; + +private: + // Internal Helper Methods + CHIP_ERROR InitializeCommissioner(uint8_t (&pskc)[chip::Thread::kSizePSKc]); + CHIP_ERROR CreateProxySocket(chip::Dnssd::CommissionNodeData & commissionData); + void ProcessAnnouncement(const std::vector & joinerIdBytes, uint16_t joinerPort, const std::vector & payload); + void SetState(State state); + + ot::commissioner::CommissionerDataset MakeCommissionerDataset(chip::Thread::DiscoveryCode code); + + // Member Variables + chip::Dnssd::DiscoveredNodeData mNodeData; + mdns::Minimal::BytesRange mDnsPacket; + + int mProxyFd = -1; + uint16_t mServicePort = 0; + std::atomic mState; + chip::SetupDiscriminator mExpectedDiscriminator; + + uint64_t mJoinerId = 0; + + std::recursive_mutex mMutex; + bool mPromiseFulfilled = false; + std::promise mDiscoveredNodePromise; + + std::shared_ptr mCommissioner; + std::thread mProxyThread; +}; diff --git a/examples/chip-tool/commands/pairing/PairingCommand.cpp b/examples/chip-tool/commands/pairing/PairingCommand.cpp index 07756d7c9e8baf..cc6ad468a7d841 100644 --- a/examples/chip-tool/commands/pairing/PairingCommand.cpp +++ b/examples/chip-tool/commands/pairing/PairingCommand.cpp @@ -17,11 +17,21 @@ */ #include "PairingCommand.h" -#include "platform/PlatformManager.h" #include #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -31,12 +41,38 @@ #include "../dcl/DCLClient.h" #include "../dcl/DisplayTermsAndConditions.h" +#include #include +#include +#include +#include #include +#include using namespace ::chip; using namespace ::chip::Controller; +namespace { + +[[maybe_unused]] CHIP_ERROR ParseSetupPayload(SetupPayload & setupPayload, const char * onboardingPayload) +{ + + bool isQRCode = strncmp(onboardingPayload, kQRCodePrefix, strlen(kQRCodePrefix)) == 0; + if (isQRCode) + { + ReturnErrorOnFailure(QRCodeSetupPayloadParser(onboardingPayload).populatePayload(setupPayload)); + VerifyOrReturnError(setupPayload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT); + } + else + { + ReturnErrorOnFailure(ManualSetupPayloadParser(onboardingPayload).populatePayload(setupPayload)); + VerifyOrReturnError(setupPayload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT); + } + return CHIP_NO_ERROR; +} + +} // namespace + CHIP_ERROR PairingCommand::RunCommand() { CurrentCommissioner().RegisterPairingDelegate(this); @@ -70,6 +106,13 @@ CHIP_ERROR PairingCommand::RunInternal(NodeId remoteId) err = Unpair(remoteId); break; case PairingMode::Code: +#if CHIP_ENABLE_OT_COMMISSIONER + if (mThreadBaHost.HasValue() && mThreadBaPort.HasValue()) + { + err = PairWithMeshCoP(); + break; + } +#endif #if CHIP_DEVICE_CONFIG_ENABLE_WIFIPAF chip::DeviceLayer::ConnectivityMgr().WiFiPafSetApFreq( mApFreqStr.HasValue() ? static_cast(std::stol(mApFreqStr.Value())) : 0); @@ -324,18 +367,7 @@ CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uin // be because the device is a ble device. In this case let's fall back to looking for // a device with this index and some RendezvousParameters. SetupPayload payload; - bool isQRCode = strncmp(mOnboardingPayload, kQRCodePrefix, strlen(kQRCodePrefix)) == 0; - if (isQRCode) - { - ReturnErrorOnFailure(QRCodeSetupPayloadParser(mOnboardingPayload).populatePayload(payload)); - VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT); - } - else - { - ReturnErrorOnFailure(ManualSetupPayloadParser(mOnboardingPayload).populatePayload(payload)); - VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT); - } - + ReturnErrorOnFailure(ParseSetupPayload(payload, mOnboardingPayload)); mSetupPINCode.emplace(payload.setUpPINCode); return PairWithMdnsOrBleByIndex(remoteId, index); } @@ -387,6 +419,57 @@ CHIP_ERROR PairingCommand::PairWithMdns(NodeId remoteId) return CurrentCommissioner().DiscoverCommissionableNodes(filter); } +#if CHIP_ENABLE_OT_COMMISSIONER +CHIP_ERROR PairingCommand::PairWithMeshCoP() +{ + SetupPayload payload; + + ReturnErrorOnFailure(ParseSetupPayload(payload, mOnboardingPayload)); + + if (payload.rendezvousInformation.HasValue() && !payload.rendezvousInformation.Value().Has(RendezvousInformationFlag::kThread)) + { + // Proceed even if the device doesn't claim rendezvous over Thread MeshCoP because in-market devices may not + // be able to update their QR Code. + ChipLogProgress(chipTool, "WARNING: device may not support commissioning over Thread meshcop"); + } + + mSetupPINCode.emplace(payload.setUpPINCode); + + Thread::DiscoveryCode code; + if (payload.discriminator.IsShortDiscriminator()) + { + code = Thread::DiscoveryCode(payload.discriminator.GetShortValue()); + ChipLogProgress(chipTool, "Discovery code from short discriminator: 0x%" PRIx64, code.AsUInt64()); + } + else + { + code = Thread::DiscoveryCode(payload.discriminator.GetLongValue()); + ChipLogProgress(chipTool, "Discovery code from long discriminator: 0x%" PRIx64, code.AsUInt64()); + } + + uint8_t pskc[Thread::kSizePSKc]; + + { + Thread::OperationalDatasetView dataset; + ReturnErrorAndLogOnFailure(dataset.Init(mOperationalDataset), chipTool, "Failed to parse Thread dataset"); + + ReturnErrorAndLogOnFailure(dataset.GetPSKc(pskc), chipTool, "Failed to retrieve PSKc"); + } + + { + Dnssd::DiscoveredNodeData discoveredNodeData; + ReturnErrorOnFailure(mCommissionProxy.Discover(pskc, mThreadBaHost.Value(), mThreadBaPort.Value(), code, + payload.discriminator, discoveredNodeData, mTimeout.ValueOr(30))); + + CurrentCommissioner().RegisterDeviceDiscoveryDelegate(this); + CurrentCommissioner().OnNodeDiscovered(discoveredNodeData); + } + + ChipLogProgress(chipTool, "Joiner discovered"); + return CHIP_NO_ERROR; +} +#endif // CHIP_ENABLE_OT_COMMISSIONER + CHIP_ERROR PairingCommand::Unpair(NodeId remoteId) { mCurrentFabricRemover = Platform::MakeUnique(&CurrentCommissioner()); diff --git a/examples/chip-tool/commands/pairing/PairingCommand.h b/examples/chip-tool/commands/pairing/PairingCommand.h index 20cd041f4d576d..86ffbbd12ffcd9 100644 --- a/examples/chip-tool/commands/pairing/PairingCommand.h +++ b/examples/chip-tool/commands/pairing/PairingCommand.h @@ -19,6 +19,9 @@ #pragma once #include "../common/CHIPCommand.h" +#if CHIP_ENABLE_OT_COMMISSIONER +#include "CommissionProxy.h" +#endif #include #include @@ -116,6 +119,10 @@ class PairingCommand : public CHIPCommand, case PairingMode::CodePaseOnly: AddArgument("payload", &mOnboardingPayload); AddArgument("discover-once", 0, 1, &mDiscoverOnce); +#if CHIP_ENABLE_OT_COMMISSIONER + AddArgument("thread-ba-host", &mThreadBaHost, "Thread Border Agent host"); + AddArgument("thread-ba-port", 0, UINT16_MAX, &mThreadBaPort, "Thread Border Agent port"); +#endif AddArgument("use-only-onnetwork-discovery", 0, 1, &mUseOnlyOnNetworkDiscovery, "Whether to only use DNS-SD for discovery. The default is true if no network credentials are provided, " "false otherwise."); @@ -263,6 +270,9 @@ class PairingCommand : public CHIPCommand, CHIP_ERROR RunInternal(NodeId remoteId); CHIP_ERROR Pair(NodeId remoteId, PeerAddress address); CHIP_ERROR PairWithMdns(NodeId remoteId); +#if CHIP_ENABLE_OT_COMMISSIONER + CHIP_ERROR PairWithMeshCoP(); +#endif CHIP_ERROR PairWithCode(NodeId remoteId); CHIP_ERROR PaseWithCode(NodeId remoteId); CHIP_ERROR PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index); @@ -331,6 +341,12 @@ class PairingCommand : public CHIPCommand, static void OnCurrentFabricRemove(void * context, NodeId remoteNodeId, CHIP_ERROR status); void PersistIcdInfo(); +#if CHIP_ENABLE_OT_COMMISSIONER + chip::Optional mThreadBaHost; + chip::Optional mThreadBaPort; + CommissionProxy mCommissionProxy; +#endif + std::optional mPrompterThread; std::string mPromptedSSID; diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index 62194c165285ae..8c8b7f2cab1285 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -741,8 +741,9 @@ int ChipLinuxAppInit(int argc, char * const argv[], OptionSet * customOptions, #if CHIP_SYSTEM_CONFIG_USE_OPENTHREAD_ENDPOINT if (LinuxDeviceOptions::GetInstance().mThreadNodeId) { - std::string nodeid = std::to_string(LinuxDeviceOptions::GetInstance().mThreadNodeId); - char * args[] = { argv[0], nodeid.data() }; + std::string nodeid = std::to_string(LinuxDeviceOptions::GetInstance().mThreadNodeId); + std::string logfile = "--log-file=thread.log"; + char * args[] = { argv[0], logfile.data(), nodeid.data() }; otSysInit(MATTER_ARRAY_SIZE(args), args); SuccessOrExit(err = DeviceLayer::ThreadStackMgrImpl().InitThreadStack()); diff --git a/scripts/tests/chiptest/linux.py b/scripts/tests/chiptest/linux.py index 654b59d1390a6e..dff18b29ba5acc 100644 --- a/scripts/tests/chiptest/linux.py +++ b/scripts/tests/chiptest/linux.py @@ -360,6 +360,19 @@ def _otbr_read_stdout(self): if self._pattern.search(line): self._event.set() + def get_border_agent_port(self) -> int: + cmd = f'ip netns exec {self._netns_app} ot-ctl ba port' + output = subprocess.check_output(shlex.split(cmd), text=True) + # ot-ctl output includes the port number followed by "Done" + # Using regex to find the first number in the output + match = re.search(r'(\d+)', output) + if not match: + raise RuntimeError(f"Failed to parse border agent port from: {output}") + return int(match.group(1)) + + def get_border_agent_host(self) -> str: + return '10.10.10.1' + def terminate(self): if self._otbr: self._otbr.terminate() diff --git a/scripts/tests/chiptest/test_definition.py b/scripts/tests/chiptest/test_definition.py index 434eac364bf6a7..2209ba182d07e4 100644 --- a/scripts/tests/chiptest/test_definition.py +++ b/scripts/tests/chiptest/test_definition.py @@ -466,6 +466,8 @@ def Run(self, runner: Runner, apps_register: AppsRegister, subproc_info_repo: Su ble_controller_app: int | None = None, ble_controller_tool: int | None = None, op_network: str = 'WiFi', + thread_ba_host: str | None = None, + thread_ba_port: int | None = None, ): """ Executes the given test case using the provided runner for execution. @@ -474,14 +476,16 @@ def Run(self, runner: Runner, apps_register: AppsRegister, subproc_info_repo: Su for target in self.targets: log.info('Executing %s::%s', self.name, target.name) self._RunImpl(target, runner, apps_register, subproc_info_repo, pics_file, timeout_seconds, dry_run, - test_runtime, ble_controller_app, ble_controller_tool, op_network) + test_runtime, ble_controller_app, ble_controller_tool, op_network, thread_ba_host, thread_ba_port) def _RunImpl(self, target: TestTarget, runner: Runner, apps_register: AppsRegister, subproc_info_repo: SubprocessInfoRepo, pics_file: Path, timeout_seconds: int | None, dry_run: bool = False, test_runtime: TestRunTime = TestRunTime.CHIP_TOOL_PYTHON, ble_controller_app: int | None = None, ble_controller_tool: int | None = None, - op_network: str = 'WiFi'): + op_network: str = 'WiFi', + thread_ba_host: str | None = None, + thread_ba_port: int | None = None): runner.capture_delegate = ExecutionCapture() tool_storage_dir = None @@ -503,13 +507,13 @@ def _RunImpl(self, target: TestTarget, runner: Runner, apps_register: AppsRegist for arg in target.arguments: subproc = subproc.with_args(arg) - if ble_controller_app is not None: + if op_network == 'Thread': + # The node id must not conflict with ThreadBorderRouter.NODE_ID + subproc = subproc.with_args("--thread-node-id=2") + elif ble_controller_app is not None: subproc = subproc.with_args("--ble-controller", str(ble_controller_app)) if op_network == 'WiFi': subproc = subproc.with_args("--wifi") - elif op_network == 'Thread': - # The node id must not conflict with ThreadBorderRouter.NODE_ID - subproc = subproc.with_args("--thread-node-id=2") app = App(runner, subproc) # Add the App to the register immediately, so if it fails during @@ -570,6 +574,10 @@ def _RunImpl(self, target: TestTarget, runner: Runner, apps_register: AppsRegist pairing_cmd = pairing_cmd.with_args( "pairing", "code-thread", TEST_NODE_ID, f"hex:{TEST_THREAD_DATASET}", TEST_SETUP_QR_CODE) pairing_server_args = ["--ble-controller", str(ble_controller_tool)] + elif op_network == 'Thread' and thread_ba_host is not None and thread_ba_port is not None: + pairing_cmd = pairing_cmd.with_args( + "pairing", "code-thread", TEST_NODE_ID, f"hex:{TEST_THREAD_DATASET}", setupCode, + "--thread-ba-host", thread_ba_host, "--thread-ba-port", str(thread_ba_port)) else: pairing_cmd = pairing_cmd.with_args('pairing', 'code', TEST_NODE_ID, setupCode) diff --git a/scripts/tests/run_test_suite.py b/scripts/tests/run_test_suite.py index 6cb3ef72b7d139..9493aa2e06712b 100755 --- a/scripts/tests/run_test_suite.py +++ b/scripts/tests/run_test_suite.py @@ -408,9 +408,9 @@ def terminate(self) -> None: ... help='Number of tests that are expected to fail in each iteration. Overall test will pass if the number of failures matches this. Nonzero values require --keep-going') @click.option( '--commissioning-method', - type=click.Choice(['on-network', 'ble-wifi', 'ble-thread'], case_sensitive=False), + type=click.Choice(['on-network', 'ble-wifi', 'ble-thread', 'thread-meshcop'], case_sensitive=False), default='on-network', - help='Commissioning method to use. "on-network" is the default one available on all platforms, "ble-wifi" performs BLE-WiFi commissioning using Bluetooth and WiFi mock servers. "ble-thread" performs BLE-Thread commissioning using Bluetooth and Thread mock servers. This option is Linux-only.') + help='Commissioning method to use. "on-network" is the default one available on all platforms, "ble-wifi" performs BLE-WiFi commissioning using Bluetooth and WiFi mock servers. "ble-thread" performs BLE-Thread commissioning using Bluetooth and Thread mock servers. "thread-meshcop" performs Thread commissioning using Thread mock server. This option is Linux-only.') @click.pass_context def cmd_run(context: click.Context, dry_run: bool, iterations: int, app_path: list[str], tool_path: list[str], discover_paths: bool, help_paths: bool, @@ -493,7 +493,7 @@ def handle_deprecated_pathopt(key, path, kind): # Derive boolean flags from commissioning_method parameter wifi_required = commissioning_method in ['ble-wifi'] - thread_required = commissioning_method in ['ble-thread'] + thread_required = commissioning_method in ['ble-thread', 'thread-meshcop'] if (wifi_required or thread_required) and sys.platform != "linux": raise click.BadOptionUsage("commissioning-method", @@ -501,6 +501,8 @@ def handle_deprecated_pathopt(key, path, kind): ble_controller_app = None ble_controller_tool = None + thread_ba_host = None + thread_ba_port = None to_terminate: list[Terminable] = [] def cleanup() -> None: @@ -535,6 +537,10 @@ def cleanup() -> None: to_terminate.append(chiptest.linux.ThreadBorderRouter(ns)) ble_controller_app = 0 # Bind app to the first BLE controller ble_controller_tool = 1 # Bind tool to the second BLE controller + elif commissioning_method == 'thread-meshcop': + to_terminate.append(tbr := chiptest.linux.ThreadBorderRouter(ns)) + thread_ba_host = tbr.get_border_agent_host() + thread_ba_port = tbr.get_border_agent_port() to_terminate.append(executor := chiptest.linux.LinuxNamespacedExecutor(ns)) elif sys.platform == 'darwin': @@ -567,6 +573,8 @@ def cleanup() -> None: ble_controller_app=ble_controller_app, ble_controller_tool=ble_controller_tool, op_network='Thread' if thread_required else 'WiFi', + thread_ba_host=thread_ba_host, + thread_ba_port=thread_ba_port, ) if not dry_run: test_end = time.monotonic() diff --git a/src/app/clusters/network-commissioning/NetworkCommissioningCluster.cpp b/src/app/clusters/network-commissioning/NetworkCommissioningCluster.cpp index 3beefdea15c64d..a6d8b7e35b812e 100644 --- a/src/app/clusters/network-commissioning/NetworkCommissioningCluster.cpp +++ b/src/app/clusters/network-commissioning/NetworkCommissioningCluster.cpp @@ -231,6 +231,8 @@ void NetworkCommissioningCluster::SendNonConcurrentConnectNetworkResponse() #if CONFIG_NETWORK_LAYER_BLE DeviceLayer::ConnectivityMgr().GetBleLayer()->IndicateBleClosing(); +#else + LogErrorOnFailure(DeviceLayer::DeviceControlServer::DeviceControlSvr().PostOperationalNetworkStartedEvent()); #endif // CONFIG_NETWORK_LAYER_BLE ChipLogProgress(NetworkProvisioning, "Non-concurrent mode. Send ConnectNetworkResponse(Success)"); Commands::ConnectNetworkResponse::Type response; @@ -841,7 +843,7 @@ void NetworkCommissioningCluster::OnResult(Status commissioningError, CharSpan d SetLastNetworkId(ByteSpan{ mConnectingNetworkID, mConnectingNetworkIDLen }); SetLastNetworkingStatusValue(MakeNullable(commissioningError)); -#if CONFIG_NETWORK_LAYER_BLE && !CHIP_DEVICE_CONFIG_SUPPORTS_CONCURRENT_CONNECTION +#if (CONFIG_NETWORK_LAYER_BLE || CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP) && !CHIP_DEVICE_CONFIG_SUPPORTS_CONCURRENT_CONNECTION ChipLogProgress(NetworkProvisioning, "Non-concurrent mode, ConnectNetworkResponse will NOT be sent"); // Do not send the ConnectNetworkResponse if in non-concurrent mode // TODO(#30576) raised to modify CommandHandler to notify it if no response required diff --git a/src/app/server/BUILD.gn b/src/app/server/BUILD.gn index 984900cbe456b5..e6ff77aa418aa0 100644 --- a/src/app/server/BUILD.gn +++ b/src/app/server/BUILD.gn @@ -76,6 +76,8 @@ static_library("server") { "EchoHandler.h", "Server.cpp", "Server.h", + "ThreadRendezvousAnnouncement.cpp", + "ThreadRendezvousAnnouncement.h", ] public_configs = [ ":server_config" ] @@ -99,6 +101,13 @@ static_library("server") { "${chip_root}/src/transport", ] + if (chip_device_config_enable_thread_meshcop) { + public_deps += [ + "${chip_root}/src/lib/dnssd/minimal_mdns", + "${chip_root}/src/lib/dnssd/minimal_mdns/records", + ] + } + if (chip_terms_and_conditions_required) { public_deps += [ ":terms_and_conditions" ] } diff --git a/src/app/server/CommissioningWindowManager.cpp b/src/app/server/CommissioningWindowManager.cpp index 7ea068fcb3bf5b..88d3ea2a9ea3c7 100644 --- a/src/app/server/CommissioningWindowManager.cpp +++ b/src/app/server/CommissioningWindowManager.cpp @@ -192,6 +192,9 @@ void CommissioningWindowManager::HandleFailedAttempt(CHIP_ERROR err) void CommissioningWindowManager::OnSessionEstablishmentStarted() { +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + DeviceLayer::ThreadStackMgr().CancelRendezvousAnnouncement(); +#endif // As per specifications, section 5.5: Commissioning Flows constexpr System::Clock::Timeout kPASESessionEstablishmentTimeout = System::Clock::Seconds16(60); TEMPORARY_RETURN_IGNORED DeviceLayer::SystemLayer().StartTimer(kPASESessionEstablishmentTimeout, @@ -423,6 +426,9 @@ void CommissioningWindowManager::CloseCommissioningWindow() // manually here. mServer->GetBleLayerObject()->CloseAllBleConnections(); } +#endif +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + DeviceLayer::ThreadStackMgr().RendezvousStop(); #endif ChipLogProgress(AppServer, "Closing pairing window"); Cleanup(); diff --git a/src/app/server/Dnssd.cpp b/src/app/server/Dnssd.cpp index 33f93d16942939..486ee9999b6b17 100644 --- a/src/app/server/Dnssd.cpp +++ b/src/app/server/Dnssd.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -41,6 +40,15 @@ #include +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +#include +#include +#include +#include + +#include +#endif + using namespace chip; using namespace chip::DeviceLayer; @@ -138,6 +146,16 @@ CHIP_ERROR DnssdServer::GetCommissionableInstanceName(char * buffer, size_t buff return mdnsAdvertiser.GetCommissionableInstanceName(buffer, bufferLen); } +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +CHIP_ERROR DnssdServer::SendThreadRendezvousAnnouncement(void * context, const Transport::PeerAddress & peerAddr) +{ + auto * self = static_cast(context); + VerifyOrReturnError(!self->mThreadRendezvousAnnouncement.IsNull(), CHIP_ERROR_INCORRECT_STATE); + + return chip::Server::GetInstance().GetTransportManager().SendMessage(peerAddr, self->mThreadRendezvousAnnouncement.CloneData()); +} +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + CHIP_ERROR DnssdServer::SetEphemeralDiscriminator(Optional discriminator) { VerifyOrReturnError(discriminator.ValueOr(0) <= kMaxDiscriminatorValue, CHIP_ERROR_INVALID_ARGUMENT); @@ -391,6 +409,19 @@ CHIP_ERROR DnssdServer::Advertise(bool commissionableNode, chip::Dnssd::Commissi #endif // CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC ); +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + if (commissionableNode && !DeviceLayer::ThreadStackMgr().IsThreadProvisioned()) + { + ReturnErrorOnFailure(BuildThreadRendezvousAnnouncement(advertiseParameters, mThreadRendezvousAnnouncement)); + return DeviceLayer::ThreadStackMgr().RendezvousStart(SendThreadRendezvousAnnouncement, this); + } + else + { + DeviceLayer::ThreadStackMgr().RendezvousStop(); + mThreadRendezvousAnnouncement = nullptr; + } +#endif + return mdnsAdvertiser.Advertise(advertiseParameters); } @@ -449,6 +480,11 @@ void DnssdServer::StopServer() Dnssd::ServiceAdvertiser::Instance().Shutdown(); } + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + DeviceLayer::ThreadStackMgr().RendezvousStop(); + mThreadRendezvousAnnouncement = nullptr; +#endif } void DnssdServer::StartServer(Dnssd::CommissioningMode mode) diff --git a/src/app/server/Dnssd.h b/src/app/server/Dnssd.h index 1b82fb909653a7..43bec7fcb5ff3b 100644 --- a/src/app/server/Dnssd.h +++ b/src/app/server/Dnssd.h @@ -176,6 +176,10 @@ class DLL_EXPORT DnssdServer : public ICDStateObserver void GetPrimaryOrFallbackMACAddress(MutableByteSpan & mac); +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + static CHIP_ERROR SendThreadRendezvousAnnouncement(void * context, const Transport::PeerAddress & peerAddr); +#endif + // // Check if we have any valid operational credentials present in the fabric table and return true // if we do. @@ -215,6 +219,11 @@ class DLL_EXPORT DnssdServer : public ICDStateObserver // Ephemeral discriminator to use instead of the default if set Optional mEphemeralDiscriminator; +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + // This holds the rendezvous announcement over Thread MeshCoP + chip::System::PacketBufferHandle mThreadRendezvousAnnouncement; +#endif + #if CHIP_DEVICE_CONFIG_ENABLE_EXTENDED_DISCOVERY Time::TimeSource mTimeSource; diff --git a/src/app/server/Server.cpp b/src/app/server/Server.cpp index 7a2814a64bd7d2..cdddd43d2c0b82 100644 --- a/src/app/server/Server.cpp +++ b/src/app/server/Server.cpp @@ -674,6 +674,7 @@ CHIP_ERROR Server::Init(const ServerInitParams & initParams) return mUdcTransportMgr->Init(Transport::UdpListenParameters(DeviceLayer::UDPEndPointManager()) .SetAddressType(Inet::IPAddressType::kIPv6) .SetListenPort(port) + .SetNativeParams(initParams.endpointNativeParams) #if INET_CONFIG_ENABLE_IPV4 , Transport::UdpListenParameters(DeviceLayer::UDPEndPointManager()) @@ -689,6 +690,7 @@ CHIP_ERROR Server::Init(const ServerInitParams & initParams) err = mUdcTransportMgr->Init(Transport::UdpListenParameters(DeviceLayer::UDPEndPointManager()) .SetAddressType(Inet::IPAddressType::kIPv6) .SetListenPort(mCdcListenPort) + .SetNativeParams(initParams.endpointNativeParams) #if INET_CONFIG_ENABLE_IPV4 , Transport::UdpListenParameters(DeviceLayer::UDPEndPointManager()) diff --git a/src/app/server/ThreadRendezvousAnnouncement.cpp b/src/app/server/ThreadRendezvousAnnouncement.cpp new file mode 100644 index 00000000000000..d2511021d45873 --- /dev/null +++ b/src/app/server/ThreadRendezvousAnnouncement.cpp @@ -0,0 +1,149 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * 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 + +#include +#include +#include + +#include + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +#include // nogncheck +#include // nogncheck +#include // nogncheck +#endif + +namespace chip { +namespace app { + +CHIP_ERROR TxtStringsBuilder::Fill(const Dnssd::CommissionAdvertisingParameters & params) +{ + // VP + auto vendorId = params.GetVendorId(); + auto productId = params.GetProductId(); + if (vendorId.has_value() && productId.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("VP=%d+%d", *vendorId, *productId)); + } + else if (vendorId.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("VP=%d", *vendorId)); + } + + // D + ReturnErrorOnFailure(FormatAndAdd("D=%d", params.GetLongDiscriminator())); + + // CM + ReturnErrorOnFailure(FormatAndAdd("CM=%d", static_cast(params.GetCommissioningMode()))); + + // DT + auto deviceType = params.GetDeviceType(); + if (deviceType.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("DT=%" PRIu32, *deviceType)); + } + + // DN + auto deviceName = params.GetDeviceName(); + if (deviceName.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("DN=%s", *deviceName)); + } + + // RI + auto rotatingId = params.GetRotatingDeviceId(); + if (rotatingId.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("RI=%s", *rotatingId)); + } + + // PH + auto pairingHint = params.GetPairingHint(); + if (pairingHint.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("PH=%d", *pairingHint)); + } + + // PI + auto pairingInstr = params.GetPairingInstruction(); + if (pairingInstr.has_value()) + { + ReturnErrorOnFailure(FormatAndAdd("PI=%s", *pairingInstr)); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR TxtStringsBuilder::FormatAndAdd(const char * format, ...) +{ + VerifyOrReturnError(mCount < MATTER_ARRAY_SIZE(mTxtStrings), CHIP_ERROR_BUFFER_TOO_SMALL); + + size_t offset = static_cast(mNextStart - &mTxtBuffer[0]); + VerifyOrReturnError(sizeof(mTxtBuffer) > offset, CHIP_ERROR_BUFFER_TOO_SMALL); + + va_list args; + va_start(args, format); + int len = vsnprintf(mNextStart, sizeof(mTxtBuffer) - offset, format, args); + va_end(args); + + // Make sure not overflowed. + VerifyOrReturnError(len > 0 && offset + static_cast(len) < sizeof(mTxtBuffer), CHIP_ERROR_BUFFER_TOO_SMALL); + + mTxtStrings[mCount++] = mNextStart; + + // skip the null terminator. + mNextStart += len + 1; + + return CHIP_NO_ERROR; +} + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +CHIP_ERROR BuildThreadRendezvousAnnouncement(const Dnssd::CommissionAdvertisingParameters & params, + System::PacketBufferHandle & outBuffer) +{ + System::PacketBufferHandle buffer = System::PacketBufferHandle::New(System::PacketBuffer::kMaxSize); + VerifyOrReturnError(!buffer.IsNull(), CHIP_ERROR_NO_MEMORY); + + { + mdns::Minimal::ResponseBuilder builder(std::move(buffer)); + builder.Header().SetMessageId(0); + builder.Header().SetFlags(builder.Header().GetFlags().SetResponse().SetAuthoritative()); + + static const char * matterc_udp_local[] = { "_matterc", "_udp", "local" }; + mdns::Minimal::FullQName serviceName(matterc_udp_local); + + static const char * root[] = { "" }; + mdns::Minimal::FullQName targetName(root); + mdns::Minimal::SrvResourceRecord srvRecord(serviceName, targetName, params.GetPort()); + builder.AddRecord(mdns::Minimal::ResourceType::kAnswer, srvRecord); + + TxtStringsBuilder txtStringsBuilder; + ReturnErrorOnFailure(txtStringsBuilder.Fill(params)); + mdns::Minimal::TxtResourceRecord txtRecord(serviceName, txtStringsBuilder.GetEntries(), txtStringsBuilder.GetCount()); + builder.AddRecord(mdns::Minimal::ResourceType::kAnswer, txtRecord); + + outBuffer = builder.ReleasePacket(); + } + + return CHIP_NO_ERROR; +} +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + +} // namespace app +} // namespace chip diff --git a/src/app/server/ThreadRendezvousAnnouncement.h b/src/app/server/ThreadRendezvousAnnouncement.h new file mode 100644 index 00000000000000..eb48e3409a7ddf --- /dev/null +++ b/src/app/server/ThreadRendezvousAnnouncement.h @@ -0,0 +1,76 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * 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 +#include +#include +#include +#include + +#include + +namespace chip { +namespace app { + +/** + * @brief Helper class to build TXT record entries for Thread MeshCoP advertisement. + */ +class TxtStringsBuilder +{ +public: + TxtStringsBuilder() = default; + + /** + * @brief Fills the TXT record entries from the given advertising parameters. + * + * @param params The advertising parameters. + * @return CHIP_ERROR CHIP_NO_ERROR on success, or CHIP_ERROR_BUFFER_TOO_SMALL if the buffer is too small. + */ + CHIP_ERROR Fill(const Dnssd::CommissionAdvertisingParameters & params); + + size_t GetCount() const { return mCount; } + const char ** GetEntries() const { return const_cast(mTxtStrings); } + +private: + CHIP_ERROR FormatAndAdd(const char * format, ...) ENFORCE_FORMAT(2, 3); + + static constexpr size_t kMaxTxtStringsBuffer = 256; + + char mTxtBuffer[kMaxTxtStringsBuffer]; + char * mNextStart = &mTxtBuffer[0]; + size_t mCount = 0; + const char * mTxtStrings[Dnssd::CommissionAdvertisingParameters::kTxtMaxNumber]; +}; + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + +/** + * @brief Builds the Thread Rendezvous Announcement packet buffer. + * + * @param params The advertising parameters. + * @param outBuffer The resulting packet buffer handle. + * @return CHIP_ERROR CHIP_NO_ERROR on success, or other error on failure. + */ +CHIP_ERROR BuildThreadRendezvousAnnouncement(const Dnssd::CommissionAdvertisingParameters & params, + System::PacketBufferHandle & outBuffer); + +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + +} // namespace app +} // namespace chip diff --git a/src/app/server/tests/BUILD.gn b/src/app/server/tests/BUILD.gn index 4570359562943d..47c50776d659fc 100644 --- a/src/app/server/tests/BUILD.gn +++ b/src/app/server/tests/BUILD.gn @@ -24,6 +24,7 @@ chip_test_suite("tests") { test_sources = [ "TestJointFabricDatastore.cpp", "TestServerPortRetry.cpp", + "TestThreadRendezvousAnnouncement.cpp", ] public_deps = [ diff --git a/src/app/server/tests/TestThreadRendezvousAnnouncement.cpp b/src/app/server/tests/TestThreadRendezvousAnnouncement.cpp new file mode 100644 index 00000000000000..4d41c4d113dcb2 --- /dev/null +++ b/src/app/server/tests/TestThreadRendezvousAnnouncement.cpp @@ -0,0 +1,109 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * 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 + +#include +#include +#include + +#include + +using namespace chip; +using namespace chip::app; + +class TestThreadRendezvousAnnouncement : public ::testing::Test +{ +}; + +TEST_F(TestThreadRendezvousAnnouncement, TxtStringsBuilder) +{ + Dnssd::CommissionAdvertisingParameters params; + params.SetVendorId(std::make_optional(123)) + .SetProductId(std::make_optional(456)) + .SetLongDiscriminator(789) + .SetCommissioningMode(Dnssd::CommissioningMode::kEnabledBasic) + .SetDeviceType(std::make_optional(1)) + .SetDeviceName(std::make_optional("TestDevice")) + .SetRotatingDeviceId(std::make_optional("1234567890")) + .SetPairingHint(std::make_optional(2)) + .SetPairingInstruction(std::make_optional("Press button")); + + TxtStringsBuilder builder; + EXPECT_EQ(builder.Fill(params), CHIP_NO_ERROR); + + // Expected entries: + // VP=123+456 + // D=789 + // CM=1 + // DT=1 + // DN=TestDevice + // RI=1234567890 + // PH=2 + // PI=Press button + + EXPECT_EQ(builder.GetCount(), 8u); + const char * const * entries = builder.GetEntries(); + + EXPECT_STREQ(entries[0], "VP=123+456"); + EXPECT_STREQ(entries[1], "D=789"); + EXPECT_STREQ(entries[2], "CM=1"); + EXPECT_STREQ(entries[3], "DT=1"); + EXPECT_STREQ(entries[4], "DN=TestDevice"); + EXPECT_STREQ(entries[5], "RI=1234567890"); + EXPECT_STREQ(entries[6], "PH=2"); + EXPECT_STREQ(entries[7], "PI=Press button"); +} + +TEST_F(TestThreadRendezvousAnnouncement, TxtStringsBuilderOverflow) +{ + Dnssd::CommissionAdvertisingParameters params; + params.SetVendorId(std::make_optional(65535)) + .SetProductId(std::make_optional(65535)) + .SetLongDiscriminator(789) + .SetCommissioningMode(Dnssd::CommissioningMode::kEnabledBasic) + .SetDeviceType(std::make_optional(0xFFFFFFFF)) + .SetPairingHint(std::make_optional(0xFFFF)); + + char longString[128]; + memset(longString, 'A', sizeof(longString) - 1); + longString[sizeof(longString) - 1] = '\0'; + + params.SetDeviceName(std::make_optional(longString)); // max 32 + params.SetRotatingDeviceId(std::make_optional(longString)); // max 100 + params.SetPairingInstruction(std::make_optional(longString)); // max 128 + + TxtStringsBuilder builder; + CHIP_ERROR err = builder.Fill(params); + EXPECT_EQ(err, CHIP_ERROR_BUFFER_TOO_SMALL); +} + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + +TEST_F(TestThreadRendezvousAnnouncement, BuildThreadRendezvousAnnouncement) +{ + Dnssd::CommissionAdvertisingParameters params; + params.SetPort(5540); + params.SetLongDiscriminator(789); + + System::PacketBufferHandle buffer; + CHIP_ERROR err = BuildThreadRendezvousAnnouncement(params, buffer); + EXPECT_EQ(err, CHIP_NO_ERROR); + EXPECT_FALSE(buffer.IsNull()); +} + +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP diff --git a/src/controller/AutoCommissioner.cpp b/src/controller/AutoCommissioner.cpp index 5aa4204152eb8d..7fa021795f6b86 100644 --- a/src/controller/AutoCommissioner.cpp +++ b/src/controller/AutoCommissioner.cpp @@ -673,7 +673,8 @@ CHIP_ERROR AutoCommissioner::StartCommissioning(DeviceCommissioner * commissione mCommissioneeDeviceProxy->GetSecureSession().Value()->AsSecureSession()->GetPeerAddress().GetTransportType(); } - mNeedsNetworkSetup = (transportType == Transport::Type::kBle); + // mNeedsNetworkSetup may be set by SetNetworkSetupNeeded(). + mNeedsNetworkSetup = mNeedsNetworkSetup || (transportType == Transport::Type::kBle); #if CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING mNeedsNetworkSetup = mNeedsNetworkSetup || (transportType == Transport::Type::kNfc); #endif diff --git a/src/controller/AutoCommissioner.h b/src/controller/AutoCommissioner.h index e09f9cd7942d6d..9153836390b591 100644 --- a/src/controller/AutoCommissioner.h +++ b/src/controller/AutoCommissioner.h @@ -58,6 +58,8 @@ class AutoCommissioner : public CommissioningDelegate ByteSpan GetAttestationSignature() const { return ByteSpan(mAttestationSignature, mAttestationSignatureLen); } ByteSpan GetAttestationNonce() const { return ByteSpan(mAttestationNonce); } + void SetNetworkSetupNeeded(bool needed) { mNeedsNetworkSetup = needed; } + protected: virtual void CleanupCommissioning(); CommissioningStage GetNextCommissioningStage(CommissioningStage currentStage, CHIP_ERROR & lastErr); diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 33ab04747e8efb..0e8501172383a7 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -1924,6 +1924,10 @@ void DeviceCommissioner::OnNodeDiscovered(const chip::Dnssd::DiscoveredNodeData mUdcServer->OnCommissionableNodeFound(nodeData); } #endif // CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY + if (nodeData.Get().threadMeshcop) + { + mAutoCommissioner.SetNetworkSetupNeeded(true); + } AbstractDnssdDiscoveryController::OnNodeDiscovered(nodeData); mSetUpCodePairer.NotifyCommissionableDeviceDiscovered(nodeData); } diff --git a/src/include/platform/ThreadStackManager.h b/src/include/platform/ThreadStackManager.h index e07a3b19eaffb7..4f1098b75c69fc 100644 --- a/src/include/platform/ThreadStackManager.h +++ b/src/include/platform/ThreadStackManager.h @@ -35,12 +35,17 @@ namespace chip { namespace Dnssd { struct TextEntry; struct DnssdService; +class CommissionAdvertisingParameters; } // namespace Dnssd namespace Thread { class OperationalDataset; } // namespace Thread +namespace Transport { +class PeerAddress; +} // namespace Transport + namespace DeviceLayer { class PlatformManagerImpl; @@ -48,6 +53,14 @@ class ThreadStackManagerImpl; class ConfigurationManagerImpl; class DeviceControlServer; +/** + * Provides a callback to send a Rendezvous announcement to a peer. + * + * @param[in] context A pointer to a context provided by the caller. + * @param[in] peerAddr The address of the peer to send the announcement to. + */ +typedef CHIP_ERROR (*RendezvousAnnouncementRequestCallback)(void * context, const Transport::PeerAddress & peerAddr); + namespace Internal { class NFCCommissioningManagerImpl; class BLEManagerImpl; @@ -106,6 +119,12 @@ class ThreadStackManager CHIP_ERROR GetPrimary802154MACAddress(uint8_t * buf); CHIP_ERROR GetThreadVersion(uint16_t & version); +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + CHIP_ERROR RendezvousStart(RendezvousAnnouncementRequestCallback announcementRequest, void * context); + void RendezvousStop(); + void CancelRendezvousAnnouncement(); +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + CHIP_ERROR SetThreadProvision(ByteSpan aDataset); CHIP_ERROR SetThreadEnabled(bool val); CHIP_ERROR AttachToThreadNetwork(const Thread::OperationalDataset & dataset, @@ -426,6 +445,23 @@ inline CHIP_ERROR ThreadStackManager::GetThreadVersion(uint16_t & version) return static_cast(this)->_GetThreadVersion(version); } +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +inline CHIP_ERROR ThreadStackManager::RendezvousStart(RendezvousAnnouncementRequestCallback announcementRequest, void * context) +{ + return static_cast(this)->_RendezvousStart(announcementRequest, context); +} + +inline void ThreadStackManager::RendezvousStop() +{ + static_cast(this)->_RendezvousStop(); +} + +inline void ThreadStackManager::CancelRendezvousAnnouncement() +{ + static_cast(this)->_CancelRendezvousAnnouncement(); +} +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + inline void ThreadStackManager::ResetThreadNetworkDiagnosticsCounts() { static_cast(this)->_ResetThreadNetworkDiagnosticsCounts(); diff --git a/src/inet/UDPEndPointImplOpenThread.cpp b/src/inet/UDPEndPointImplOpenThread.cpp index 88526b9fe1f581..5197c2d61f3fec 100644 --- a/src/inet/UDPEndPointImplOpenThread.cpp +++ b/src/inet/UDPEndPointImplOpenThread.cpp @@ -249,6 +249,7 @@ CHIP_ERROR UDPEndPointImplOT::SendMsgImpl(const IPPacketInfo * aPktInfo, System: otError error = OT_ERROR_NONE; otMessage * message; otMessageInfo messageInfo; + otMessageSettings settings = {}; // For now the entire message must fit within a single buffer. VerifyOrReturnError(!msg->HasChainedBuffer() && msg->DataLength() <= UINT16_MAX, CHIP_ERROR_MESSAGE_TOO_LONG); @@ -260,7 +261,21 @@ CHIP_ERROR UDPEndPointImplOT::SendMsgImpl(const IPPacketInfo * aPktInfo, System: messageInfo.mPeerPort = aPktInfo->DestPort; LockOpenThread(); - message = otUdpNewMessage(mOTInstance, NULL); + + switch (otThreadGetDeviceRole(mOTInstance)) + { + case OT_DEVICE_ROLE_DISABLED: + case OT_DEVICE_ROLE_DETACHED: + settings.mLinkSecurityEnabled = false; + break; + default: + settings.mLinkSecurityEnabled = true; + break; + } + + settings.mPriority = OT_MESSAGE_PRIORITY_NORMAL; + + message = otUdpNewMessage(mOTInstance, &settings); VerifyOrExit(message != NULL, error = OT_ERROR_NO_BUFS); error = otMessageAppend(message, msg->Start(), static_cast(msg->DataLength())); diff --git a/src/lib/dnssd/BUILD.gn b/src/lib/dnssd/BUILD.gn index 58ce25513c945f..a1cdfecb92cc91 100644 --- a/src/lib/dnssd/BUILD.gn +++ b/src/lib/dnssd/BUILD.gn @@ -104,6 +104,8 @@ static_library("dnssd") { ] public_deps += [ "${chip_root}/src/lib/dnssd/minimal_mdns", + "${chip_root}/src/lib/dnssd/minimal_mdns/core", + "${chip_root}/src/lib/dnssd/minimal_mdns/records", "${chip_root}/src/tracing", "${chip_root}/src/tracing:macros", ] diff --git a/src/lib/dnssd/Types.h b/src/lib/dnssd/Types.h index 6237ba0fd1ed6b..13671a9b348d8f 100644 --- a/src/lib/dnssd/Types.h +++ b/src/lib/dnssd/Types.h @@ -243,6 +243,7 @@ struct CommissionNodeData : public CommonResolutionData char instanceName[Commission::kInstanceNameMaxLength + 1] = {}; char deviceName[kMaxDeviceNameLen + 1] = {}; char pairingInstruction[kMaxPairingInstructionLen + 1] = {}; + bool threadMeshcop = false; #if CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC BitFlags jointFabricMode; #endif // CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC diff --git a/src/lib/dnssd/minimal_mdns/core/QNameString.h b/src/lib/dnssd/minimal_mdns/core/QNameString.h index 6a548e8b474394..0c57954dd5d637 100644 --- a/src/lib/dnssd/minimal_mdns/core/QNameString.h +++ b/src/lib/dnssd/minimal_mdns/core/QNameString.h @@ -19,6 +19,8 @@ #include #include +#include + namespace mdns { namespace Minimal { @@ -34,7 +36,24 @@ class QNameString inline bool Fit() const { return mBuffer.Fit(); } + template + bool EndsWith(const char (&aSuffix)[N]) const + { + return EndsWith(&aSuffix[0], N - 1); + } + private: + bool EndsWith(const char * aSuffix, size_t aLength) const + { + const char * buffer = mBuffer.c_str(); + size_t bufferLength = strlen(buffer); + if (bufferLength < aLength) + { + return false; + } + return memcmp(buffer + bufferLength - aLength, aSuffix, aLength) == 0; + } + static constexpr size_t kMaxQNameLength = 128; chip::StringBuilder mBuffer; }; diff --git a/src/lib/dnssd/minimal_mdns/core/tests/TestQNameString.cpp b/src/lib/dnssd/minimal_mdns/core/tests/TestQNameString.cpp index b403ee30ee0bb1..a3d9a39b06767a 100644 --- a/src/lib/dnssd/minimal_mdns/core/tests/TestQNameString.cpp +++ b/src/lib/dnssd/minimal_mdns/core/tests/TestQNameString.cpp @@ -41,10 +41,27 @@ TEST_F(TestQNameString, Construction) const testing::TestQName<2> kShort({ "some", "test" }); QNameString heapQName(kShort.Serialized()); EXPECT_STREQ(heapQName.c_str(), "some.test"); + EXPECT_TRUE(heapQName.EndsWith("test")); + EXPECT_TRUE(heapQName.EndsWith(".test")); + EXPECT_FALSE(heapQName.EndsWith("some")); mdns::Minimal::SerializedQNameIterator SInvalid; QNameString heapQNameI(SInvalid); EXPECT_STREQ(heapQNameI.c_str(), "(!INVALID!)"); } } + +TEST_F(TestQNameString, EndsWith) +{ + const testing::TestQName<3> kLong({ "abc", "test", "abc" }); + QNameString qName(kLong.Serialized()); + EXPECT_STREQ(qName.c_str(), "abc.test.abc"); + + // Verify that EndsWith matches only the suffix, not the first occurrence of "abc" + EXPECT_TRUE(qName.EndsWith("abc")); + EXPECT_TRUE(qName.EndsWith(".abc")); + EXPECT_TRUE(qName.EndsWith("test.abc")); + EXPECT_FALSE(qName.EndsWith("test")); + EXPECT_FALSE(qName.EndsWith("abc.test")); +} } // namespace diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn index 2d244d6d81d136..c9882f242e30bb 100644 --- a/src/lib/support/BUILD.gn +++ b/src/lib/support/BUILD.gn @@ -309,6 +309,8 @@ static_library("support") { "StringBuilder.h", "StringSplitter.h", "TemporaryFileStream.h", + "ThreadDiscoveryCode.cpp", + "ThreadDiscoveryCode.h", "ThreadOperationalDataset.cpp", "ThreadOperationalDataset.h", "TimeUtils.cpp", @@ -356,6 +358,7 @@ static_library("support") { ":type-traits", ":verifymacros", ":verifymacros_no_logging", + "${chip_root}/src/app/common:enums", "${chip_root}/src/lib/core:chip_config_header", "${chip_root}/src/lib/core:encoding", "${chip_root}/src/lib/core:error", diff --git a/src/lib/support/ThreadDiscoveryCode.cpp b/src/lib/support/ThreadDiscoveryCode.cpp new file mode 100644 index 00000000000000..0f24f0a6fbf471 --- /dev/null +++ b/src/lib/support/ThreadDiscoveryCode.cpp @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * 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 "ThreadDiscoveryCode.h" + +#include + +#include +#include + +namespace chip { +namespace Thread { + +namespace { +// Magic number "MT" in ASCII +constexpr uint32_t kMagicNumber = 0x4d540000; +} // namespace + +DiscoveryCode::DiscoveryCode(uint16_t discriminator) +{ + // The discovery code packing: + // Bits 32-63: Magic number "MT" (0x4D540000) + // Bits 8-15: Lower 8 bits of the 12-bit discriminator + // Bits 0-3: Upper 4 bits of the 12-bit discriminator + + uint64_t magic = static_cast(kMagicNumber) << 32; + uint64_t discLow8 = static_cast(discriminator & 0xFF) << 8; + uint64_t discHigh4 = static_cast((discriminator >> 8) & 0x0F); + + mCode = magic | discLow8 | discHigh4; +} + +} // namespace Thread +} // namespace chip diff --git a/src/lib/support/ThreadDiscoveryCode.h b/src/lib/support/ThreadDiscoveryCode.h new file mode 100644 index 00000000000000..e8a932b7c9ea43 --- /dev/null +++ b/src/lib/support/ThreadDiscoveryCode.h @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * 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 +#include + +namespace chip { +namespace Thread { + +class DiscoveryCode +{ +public: + DiscoveryCode() : mCode(0) {} + + // This is initializing the DiscoveryCode with a 12-bit discriminator + DiscoveryCode(uint16_t discriminator); + + // This is initializing the DiscoveryCode with a 4-bit short discriminator + DiscoveryCode(uint8_t discriminator) : DiscoveryCode(static_cast(discriminator << 8)) {} + + uint64_t AsUInt64Short() const { return (mCode & 0xffff'ffff'ffff'000fULL); } + uint64_t AsUInt64() const { return mCode; } + bool IsAny() const { return mCode == 0; } + +private: + uint64_t mCode; +}; + +} // namespace Thread +} // namespace chip diff --git a/src/lib/support/tests/BUILD.gn b/src/lib/support/tests/BUILD.gn index 882fe172dc43a1..671cc618a75bf5 100644 --- a/src/lib/support/tests/BUILD.gn +++ b/src/lib/support/tests/BUILD.gn @@ -67,6 +67,7 @@ chip_test_suite("tests") { "TestStringBuilder.cpp", "TestStringSplitter.cpp", "TestTestPersistentStorageDelegate.cpp", + "TestThreadDiscoveryCode.cpp", "TestThreadOperationalDataset.cpp", "TestTimeUtils.cpp", "TestTlvJson.cpp", diff --git a/src/lib/support/tests/TestThreadDiscoveryCode.cpp b/src/lib/support/tests/TestThreadDiscoveryCode.cpp new file mode 100644 index 00000000000000..22d19e85086a3a --- /dev/null +++ b/src/lib/support/tests/TestThreadDiscoveryCode.cpp @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * 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 + +#include + +namespace { + +using namespace chip; + +class TestThreadDiscoveryCode : public ::testing::Test +{ +}; + +TEST_F(TestThreadDiscoveryCode, TestLongDiscriminator) +{ + // Long discriminator 0xABC (12 bits) + // Packing should be: 4D 54 00 00 00 00 BC 0A + Thread::DiscoveryCode code(static_cast(0xABC)); + + EXPECT_EQ(code.AsUInt64(), 0x4D5400000000BC0AULL); +} + +TEST_F(TestThreadDiscoveryCode, TestShortDiscriminator) +{ + // Short discriminator 0xA (4 bits) + // Initialized using uint8_t constructor: code(0xA) calls uint16_t constructor with 0xA << 8 = 0xA00 + // Packing for 0xA00: discLow8 = 0, discHigh4 = 0xA + // Result: 4D 54 00 00 00 00 00 0A + Thread::DiscoveryCode code(static_cast(0xA)); + + EXPECT_EQ(code.AsUInt64(), 0x4D5400000000000AULL); +} + +TEST_F(TestThreadDiscoveryCode, TestAsUInt64Short) +{ + // Long discriminator 0xABC + // Full: 4D 54 00 00 00 00 BC 0A + // Short: 4D 54 00 00 00 00 00 0A + Thread::DiscoveryCode code(static_cast(0xABC)); + + EXPECT_EQ(code.AsUInt64Short(), 0x4D5400000000000AULL); +} + +} // namespace diff --git a/src/platform/BUILD.gn b/src/platform/BUILD.gn index 737f2b2b0b2380..39dbba7f422f2e 100644 --- a/src/platform/BUILD.gn +++ b/src/platform/BUILD.gn @@ -148,6 +148,7 @@ if (chip_device_platform != "none" && chip_device_platform != "external") { "CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG=${chip_device_config_enable_dynamic_mrp_config}", "CHIP_DEVICE_CONFIG_ENABLE_WIFIPAF=${chip_device_config_enable_wifipaf}", "CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC=${chip_device_config_enable_joint_fabric}", + "CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP=${chip_device_config_enable_thread_meshcop}", ] public_deps = [ "${chip_root}/src/app/icd/server:icd-server-config" ] @@ -418,7 +419,9 @@ if (chip_device_platform != "none" && chip_device_platform != "external") { defines += [ "CHIP_DEVICE_CONFIG_MAX_DISCOVERED_IP_ADDRESSES=${chip_max_discovered_ip_addresses}" ] - if (chip_enable_openthread && chip_device_platform != "linux" && + if (chip_enable_openthread && + (chip_device_platform != "linux" || + chip_system_config_use_openthread_inet_endpoints) && chip_device_platform != "tizen" && chip_device_platform != "webos") { defines += [ "CHIP_DEVICE_CONFIG_THREAD_NETWORK_ENDPOINT_ID=${chip_device_config_thread_network_endpoint_id}" ] } diff --git a/src/platform/Linux/ThreadStackManagerImpl.h b/src/platform/Linux/ThreadStackManagerImpl.h index e0c57bc0661fb3..37a0efebceca60 100755 --- a/src/platform/Linux/ThreadStackManagerImpl.h +++ b/src/platform/Linux/ThreadStackManagerImpl.h @@ -110,6 +110,15 @@ class ThreadStackManagerImpl : public ThreadStackManager CHIP_ERROR _GetThreadVersion(uint16_t & version); +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + CHIP_ERROR _RendezvousStart(RendezvousAnnouncementRequestCallback announcementRequest, void * context) + { + return CHIP_ERROR_NOT_IMPLEMENTED; + } + void _CancelRendezvousAnnouncement() {} + void _RendezvousStop() {} +#endif + void _ResetThreadNetworkDiagnosticsCounts(); CHIP_ERROR _StartThreadScan(NetworkCommissioning::ThreadDriver::ScanCallback * callback); diff --git a/src/platform/Linux/ThreadStackManagerImpl_OpenThread.cpp b/src/platform/Linux/ThreadStackManagerImpl_OpenThread.cpp index a5ce10989f7c9c..b610884bcf095c 100644 --- a/src/platform/Linux/ThreadStackManagerImpl_OpenThread.cpp +++ b/src/platform/Linux/ThreadStackManagerImpl_OpenThread.cpp @@ -48,6 +48,11 @@ ThreadStackManagerImpl & ThreadStackManagerImpl::Instance() return instance; } +ThreadStackManagerImpl::~ThreadStackManagerImpl() +{ + static_cast(SystemLayer()).EventSourceRemove(this); +} + CHIP_ERROR ThreadStackManagerImpl::_InitThreadStack() { return GenericThreadStackManagerImpl_OpenThread::DoInit(nullptr); diff --git a/src/platform/Linux/ThreadStackManagerImpl_OpenThread.h b/src/platform/Linux/ThreadStackManagerImpl_OpenThread.h index 6eb6324bd30e1c..fedb7d07a22c2d 100644 --- a/src/platform/Linux/ThreadStackManagerImpl_OpenThread.h +++ b/src/platform/Linux/ThreadStackManagerImpl_OpenThread.h @@ -56,6 +56,7 @@ class ThreadStackManagerImpl final : public ThreadStackManager, private: ThreadStackManagerImpl() = default; + ~ThreadStackManagerImpl(); }; } // namespace DeviceLayer diff --git a/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.h b/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.h index 6b8d9b4510f20f..85890367604648 100644 --- a/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.h +++ b/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.h @@ -26,8 +26,14 @@ #pragma once #include +#include #include #include +#include + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +#include +#endif #if CHIP_DEVICE_CONFIG_ENABLE_THREAD_SRP_CLIENT #include @@ -42,6 +48,7 @@ #include #include #include +#include namespace chip { namespace DeviceLayer { @@ -141,6 +148,11 @@ class GenericThreadStackManagerImpl_OpenThread CHIP_ERROR ConfigureThreadStack(otInstance * otInst); CHIP_ERROR DoInit(otInstance * otInst); +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + void _RendezvousStop(); + CHIP_ERROR _RendezvousStart(RendezvousAnnouncementRequestCallback announcementRequest, void * context); + void _CancelRendezvousAnnouncement(); +#endif bool IsThreadAttachedNoLock(); bool IsThreadInterfaceUpNoLock(); @@ -148,15 +160,31 @@ class GenericThreadStackManagerImpl_OpenThread // ===== Private members for use by this class only. otInstance * mOTInst; + +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + static otSeekerVerdict _HandleSeekerScanEvaluator(void * aContext, const otSeekerScanResult * aResult); + static void _HandleRendezvousRetransmissionTimer(System::Layer * aLayer, void * aAppState); + void SendRendezvousAnnouncement(); +#endif + uint64_t mOverrunCount = 0; bool mIsAttached = false; bool mTemporaryRxOnWhenIdle = false; + chip::Transport::PeerAddress mRendezvousPeerAddr; + + static constexpr uint8_t kMaxRendezvousRetransmissions = 5; + uint8_t mRendezvousRetransmissionCount = 0; + RendezvousAnnouncementRequestCallback mRendezvousAnnouncementRequestCallback = nullptr; + void * mRendezvousAnnouncementRequestContext = nullptr; + NetworkCommissioning::GenericThreadDriver * mpCommissioningDriver = nullptr; NetworkCommissioning::ThreadDriver::ScanCallback * mpScanCallback; NetworkCommissioning::Internal::WirelessDriver::ConnectCallback * mpConnectCallback; NetworkCommissioning::Internal::BaseDriver::NetworkStatusChangeCallback * mpStatusChangeCallback = nullptr; + void TryNextNetwork(); + #if CHIP_DEVICE_CONFIG_ENABLE_THREAD_SRP_CLIENT struct SrpClient diff --git a/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp b/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp index a2b73d9e736284..b4b6cb79181300 100644 --- a/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp +++ b/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp @@ -30,21 +30,24 @@ #include #include -#include -#include #include #include #include #include #include #include +#include #if CHIP_DEVICE_CONFIG_THREAD_FTD #include #include #endif +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +#include +#endif + #if CHIP_DEVICE_CONFIG_ENABLE_THREAD_SRP_CLIENT #include #endif @@ -53,8 +56,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -841,6 +846,156 @@ void GenericThreadStackManagerImpl_OpenThread::_ErasePersistentInfo() Impl()->UnlockThreadStack(); } +#if CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP +template +void GenericThreadStackManagerImpl_OpenThread::_RendezvousStop() +{ + otSeekerStop(mOTInst); + _CancelRendezvousAnnouncement(); +} + +template +void GenericThreadStackManagerImpl_OpenThread::_CancelRendezvousAnnouncement() +{ + DeviceLayer::SystemLayer().CancelTimer(_HandleRendezvousRetransmissionTimer, this); + mRendezvousRetransmissionCount = 0; +} + +template +CHIP_ERROR +GenericThreadStackManagerImpl_OpenThread::_RendezvousStart(RendezvousAnnouncementRequestCallback announcementRequest, + void * context) +{ + CHIP_ERROR error = CHIP_NO_ERROR; + + Impl()->LockThreadStack(); + VerifyOrExit(otThreadGetDeviceRole(mOTInst) == OT_DEVICE_ROLE_DISABLED, error = MapOpenThreadError(OT_ERROR_INVALID_STATE)); + VerifyOrExit(!otSeekerIsRunning(mOTInst), error = MapOpenThreadError(OT_ERROR_BUSY)); + + if (!otIp6IsEnabled(mOTInst)) + { + SuccessOrExit(error = MapOpenThreadError(otIp6SetEnabled(mOTInst, true))); + } + + _CancelRendezvousAnnouncement(); + + mRendezvousAnnouncementRequestCallback = announcementRequest; + mRendezvousAnnouncementRequestContext = context; + + SuccessOrExit(error = MapOpenThreadError(otSeekerSetUdpPort(mOTInst, CHIP_PORT))); + SuccessOrExit(error = MapOpenThreadError(otSeekerStart(mOTInst, _HandleSeekerScanEvaluator, this))); + +exit: + Impl()->UnlockThreadStack(); + + ChipLogProgress(DeviceLayer, "Rendezvous start: %s", chip::ErrorStr(error)); + + return error; +} + +template +otSeekerVerdict GenericThreadStackManagerImpl_OpenThread::_HandleSeekerScanEvaluator(void * aContext, + const otSeekerScanResult * aResult) +{ + auto * self = static_cast(aContext); + + if (aResult == nullptr) + { + self->TryNextNetwork(); + return OT_SEEKER_ACCEPT; + } + + { + uint16_t discriminator; + if (DeviceLayer::GetCommissionableDataProvider()->GetSetupDiscriminator(discriminator) != CHIP_NO_ERROR) + { + return OT_SEEKER_IGNORE; + } + + Thread::DiscoveryCode code(discriminator); + otJoinerDiscerner discerner; + discerner.mValue = code.AsUInt64(); + discerner.mLength = 64; + + if (otSteeringDataContainsDiscerner(&aResult->mSteeringData, &discerner)) + { + return OT_SEEKER_ACCEPT_PREFERRED; + } + + discerner.mValue = code.AsUInt64Short(); + discerner.mLength = 64; + if (otSteeringDataContainsDiscerner(&aResult->mSteeringData, &discerner)) + { + return OT_SEEKER_ACCEPT; + } + } + + return OT_SEEKER_IGNORE; +} + +template +void GenericThreadStackManagerImpl_OpenThread::TryNextNetwork() +{ + otSockAddr targetAddr; + + if (otSeekerSetUpNextConnection(mOTInst, &targetAddr) == OT_ERROR_NONE) + { + mRendezvousPeerAddr = + chip::Transport::PeerAddress::UDP(ToIPAddress(targetAddr.mAddress), targetAddr.mPort, Inet::InterfaceId::Null()); + + DeviceLayer::SystemLayer().ScheduleLambda([this]() { SendRendezvousAnnouncement(); }); + } + else if (otSeekerIsRunning(mOTInst)) + { + otSeekerStop(mOTInst); + + auto err = MapOpenThreadError(otSeekerStart(mOTInst, _HandleSeekerScanEvaluator, this)); + + ChipLogProgress(DeviceLayer, "Restart rendezvous: %s", chip::ErrorStr(err)); + } +} + +template +void GenericThreadStackManagerImpl_OpenThread::SendRendezvousAnnouncement() +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + if (mRendezvousAnnouncementRequestCallback != nullptr) + { + err = mRendezvousAnnouncementRequestCallback(mRendezvousAnnouncementRequestContext, mRendezvousPeerAddr); + } + + if (err == CHIP_NO_ERROR) + { + mRendezvousRetransmissionCount++; + if (mRendezvousRetransmissionCount < kMaxRendezvousRetransmissions) + { + ChipLogProgress(DeviceLayer, "Try the current Thread network #%u", mRendezvousRetransmissionCount); + DeviceLayer::SystemLayer().StartTimer(System::Clock::Milliseconds32(1250), _HandleRendezvousRetransmissionTimer, this); + } + else + { + ChipLogProgress(DeviceLayer, "Give up the current Thread network!"); + TryNextNetwork(); + } + } + + if (err != CHIP_NO_ERROR) + { + ChipLogError(DeviceLayer, "Failed to send rendezvous announcement: %" CHIP_ERROR_FORMAT, err.Format()); + _CancelRendezvousAnnouncement(); + } +} + +template +void GenericThreadStackManagerImpl_OpenThread::_HandleRendezvousRetransmissionTimer(System::Layer * aLayer, + void * aAppState) +{ + auto * self = static_cast(aAppState); + self->SendRendezvousAnnouncement(); +} +#endif // CHIP_DEVICE_CONFIG_ENABLE_THREAD_MESHCOP + template void GenericThreadStackManagerImpl_OpenThread::_UpdateNetworkStatus() { diff --git a/src/platform/device.gni b/src/platform/device.gni index b013e9dd77f158..d44fe6504e4696 100644 --- a/src/platform/device.gni +++ b/src/platform/device.gni @@ -136,6 +136,12 @@ declare_args() { # and the supplicant can support. chip_device_config_enable_wifipaf = chip_enable_wifi && chip_device_platform == "linux" + + # Disable Thread MeshCoP if cross-compiling by default. + chip_device_config_enable_thread_meshcop = + chip_device_platform == "linux" && + chip_system_config_use_openthread_inet_endpoints && + current_cpu == host_cpu && current_os == host_os } declare_args() { diff --git a/src/setup_payload/SetupPayload.cpp b/src/setup_payload/SetupPayload.cpp index 94c96fa2aa8ea0..a5bc9129447ced 100644 --- a/src/setup_payload/SetupPayload.cpp +++ b/src/setup_payload/SetupPayload.cpp @@ -66,7 +66,7 @@ bool PayloadContents::isValidQRCodePayload(ValidationMode mode) const { chip::RendezvousInformationFlags valid(RendezvousInformationFlag::kBLE, RendezvousInformationFlag::kOnNetwork, RendezvousInformationFlag::kSoftAP, RendezvousInformationFlag::kWiFiPAF, - RendezvousInformationFlag::kNFC); + RendezvousInformationFlag::kNFC, RendezvousInformationFlag::kThread); VerifyOrReturnValue(rendezvousInformation.Value().HasOnly(valid), false); } diff --git a/src/setup_payload/SetupPayload.h b/src/setup_payload/SetupPayload.h index 936e202fe58659..82fdceef8988da 100644 --- a/src/setup_payload/SetupPayload.h +++ b/src/setup_payload/SetupPayload.h @@ -104,6 +104,7 @@ enum class RendezvousInformationFlag : uint8_t kOnNetwork = 1 << 2, ///< Device supports Setup on network kWiFiPAF = 1 << 3, ///< Device supports Wi-Fi Public Action Frame for discovery kNFC = 1 << 4, ///< Device supports NFC-based Commissioning + kThread = 1 << 5, ///< Device supports Thread }; using RendezvousInformationFlags = chip::BitFlags; diff --git a/src/setup_payload/tests/TestQRCode.cpp b/src/setup_payload/tests/TestQRCode.cpp index 89e174a80c1ddc..2e2e70932f6c5c 100644 --- a/src/setup_payload/tests/TestQRCode.cpp +++ b/src/setup_payload/tests/TestQRCode.cpp @@ -339,7 +339,7 @@ TEST(TestQRCode, TestSetupPayloadVerify) test_payload = payload; RendezvousInformationFlags invalid = RendezvousInformationFlags( RendezvousInformationFlag::kBLE, RendezvousInformationFlag::kSoftAP, RendezvousInformationFlag::kOnNetwork, - RendezvousInformationFlag::kWiFiPAF, RendezvousInformationFlag::kNFC); + RendezvousInformationFlag::kWiFiPAF, RendezvousInformationFlag::kNFC, RendezvousInformationFlag::kThread); invalid.SetRaw(static_cast(invalid.Raw() + 1)); test_payload.rendezvousInformation.SetValue(invalid); EXPECT_EQ(test_payload.isValidQRCodePayload(), false); diff --git a/third_party/openthread/openthread-config.h b/third_party/openthread/openthread-config.h new file mode 100644 index 00000000000000..590037e3976edd --- /dev/null +++ b/third_party/openthread/openthread-config.h @@ -0,0 +1,20 @@ +/* + * 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 + +#define OPENTHREAD_CONFIG_MESHCOP_STEERING_DATA_API_ENABLE 1 diff --git a/third_party/openthread/repo b/third_party/openthread/repo index 6a9d92545beefa..3fec404effd231 160000 --- a/third_party/openthread/repo +++ b/third_party/openthread/repo @@ -1 +1 @@ -Subproject commit 6a9d92545beefad519dba170c9646cbb2c4744f5 +Subproject commit 3fec404effd231648f018b2ecab0f7d5611c84d9