From d388095b665f37e005c18c61093b9f707472a42c Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 3 Feb 2026 07:58:39 -0800 Subject: [PATCH] [mle] add config and test for discovery request callback (#12284) Introduces `OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE` to conditionally compile the MLE Discovery Request callback feature. Disabling this feature allows for code size reduction on builds where it is not needed. A new Nexus test (`test_discover_scan.cpp`) is added to directly validate the `otThreadSetDiscoveryRequestCallback()` API behavior. This new test replaces a now-removed CLI-based test which used (`discover reqcallback`). This CLI command was originally added for testing purposes. The CLI command is not helpful as an async event would produce unsolicited output in the CLI. The new direct C++ test is a cleaner approach. --- include/openthread/instance.h | 2 +- include/openthread/thread.h | 2 + src/cli/cli.cpp | 6 +- src/cli/cli.hpp | 2 +- src/core/api/thread_ftd_api.cpp | 2 + src/core/config/mle.h | 10 ++ src/core/thread/mle.hpp | 18 ++- src/core/thread/mle_ftd.cpp | 4 +- tests/nexus/CMakeLists.txt | 1 + tests/nexus/openthread-core-nexus-config.h | 1 + tests/nexus/test_discover_scan.cpp | 166 +++++++++++++++++++++ tests/scripts/expect/cli-discover.exp | 29 ---- 12 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 tests/nexus/test_discover_scan.cpp diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 64ee8d644..3f8af477e 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (575) +#define OPENTHREAD_API_VERSION (576) /** * @addtogroup api-instance diff --git a/include/openthread/thread.h b/include/openthread/thread.h index ba6f6e909..c7b7ea25a 100644 --- a/include/openthread/thread.h +++ b/include/openthread/thread.h @@ -998,6 +998,8 @@ typedef void (*otThreadDiscoveryRequestCallback)(const otThreadDiscoveryRequestI /** * Sets a callback to receive MLE Discovery Request data. * + * Requires `OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE`. + * * @param[in] aInstance A pointer to an OpenThread instance. * @param[in] aCallback A pointer to a function that is called upon receiving an MLE Discovery Request message. * @param[in] aContext A pointer to callback application-specific context. diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 9c4dc4669..80998c221 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -2636,7 +2636,7 @@ template <> otError Interpreter::Process(Arg aArgs[]) otError error = OT_ERROR_NONE; uint32_t scanChannels = 0; -#if OPENTHREAD_FTD +#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE /** * @cli discover reqcallback (enable,disable) * @code @@ -2664,7 +2664,7 @@ template <> otError Interpreter::Process(Arg aArgs[]) otThreadSetDiscoveryRequestCallback(GetInstancePtr(), callback, context); ExitNow(); } -#endif // OPENTHREAD_FTD +#endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE if (!aArgs[0].IsEmpty()) { @@ -7886,7 +7886,7 @@ void Interpreter::OutputChildTableEntry(uint8_t aIndentSize, const otNetworkDiag } #endif // OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE -#if OPENTHREAD_FTD +#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE void Interpreter::HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext) { static_cast(aContext)->HandleDiscoveryRequest(*aInfo); diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index 981baad8c..489bd5063 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -297,7 +297,7 @@ private: static void HandleDetachGracefullyResult(void *aContext); void HandleDetachGracefullyResult(void); -#if OPENTHREAD_FTD +#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE static void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext); void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aInfo); #endif diff --git a/src/core/api/thread_ftd_api.cpp b/src/core/api/thread_ftd_api.cpp index 251598cbb..d838f1770 100644 --- a/src/core/api/thread_ftd_api.cpp +++ b/src/core/api/thread_ftd_api.cpp @@ -320,12 +320,14 @@ void otThreadRegisterNeighborTableCallback(otInstance *aInstance, otNeighborTabl AsCoreType(aInstance).Get().RegisterCallback(aCallback); } +#if OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE void otThreadSetDiscoveryRequestCallback(otInstance *aInstance, otThreadDiscoveryRequestCallback aCallback, void *aContext) { AsCoreType(aInstance).Get().SetDiscoveryRequestCallback(aCallback, aContext); } +#endif #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE diff --git a/src/core/config/mle.h b/src/core/config/mle.h index ac74e6ee4..7792c7716 100644 --- a/src/core/config/mle.h +++ b/src/core/config/mle.h @@ -302,6 +302,16 @@ #define OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE + * + * Define as 1 to support `otThreadSetDiscoveryRequestCallback()`. It allows registering a callback to be notified of + * received MLE Discovery Requests. + */ +#ifndef OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE +#define OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE 0 +#endif + /** * @def OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE * diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp index b12c5ee23..12febc1f8 100644 --- a/src/core/thread/mle.hpp +++ b/src/core/thread/mle.hpp @@ -1092,16 +1092,28 @@ public: */ Error GetMaxChildTimeout(uint32_t &aTimeout) const; +#if OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE + /** + * Callback function pointer invoked reporting a receiving a MLE Discovery Request. + */ + typedef otThreadDiscoveryRequestCallback DiscoveryRequestCallback; + + /** + * Represents info about a received Discovery Request. + */ + typedef otThreadDiscoveryRequestInfo DiscoveryRequestInfo; + /** * Sets the callback that is called when processing an MLE Discovery Request message. * * @param[in] aCallback A pointer to a function that is called to deliver MLE Discovery Request data. * @param[in] aContext A pointer to application-specific context. */ - void SetDiscoveryRequestCallback(otThreadDiscoveryRequestCallback aCallback, void *aContext) + void SetDiscoveryRequestCallback(DiscoveryRequestCallback aCallback, void *aContext) { mDiscoveryRequestCallback.Set(aCallback, aContext); } +#endif /** * Resets the MLE Advertisement Trickle timer interval. @@ -2523,7 +2535,9 @@ private: #if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE MeshCoP::SteeringData mSteeringData; #endif - Callback mDiscoveryRequestCallback; +#if OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE + Callback mDiscoveryRequestCallback; +#endif #endif // OPENTHREAD_FTD diff --git a/src/core/thread/mle_ftd.cpp b/src/core/thread/mle_ftd.cpp index e391af62f..bc7e1ce5a 100644 --- a/src/core/thread/mle_ftd.cpp +++ b/src/core/thread/mle_ftd.cpp @@ -2749,9 +2749,10 @@ void Mle::HandleDiscoveryRequest(RxInfo &aRxInfo) if (parsedDiscoveryRequestTlv) { +#if OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE if (mDiscoveryRequestCallback.IsSet()) { - otThreadDiscoveryRequestInfo info; + DiscoveryRequestInfo info; AsCoreType(&info.mExtAddress).SetFromIid(aRxInfo.mMessageInfo.GetPeerAddr().GetIid()); info.mVersion = discoveryRequestTlvValue.GetVersion(); @@ -2759,6 +2760,7 @@ void Mle::HandleDiscoveryRequest(RxInfo &aRxInfo) mDiscoveryRequestCallback.Invoke(&info); } +#endif if (discoveryRequestTlvValue.GetJoinerFlag()) { diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index e7d2c423d..b240ed5a7 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -116,6 +116,7 @@ endmacro() ot_nexus_test(5_1_1) ot_nexus_test(border_agent) ot_nexus_test(border_agent_tracker) +ot_nexus_test(discover_scan) ot_nexus_test(dtls) ot_nexus_test(form_join) ot_nexus_test(full_network_reset) diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index df3eec2d9..644d43066 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -91,6 +91,7 @@ #define OPENTHREAD_CONFIG_MESH_DIAG_ENABLE 1 #define OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_MLE_DEVICE_PROPERTY_LEADER_WEIGHT_ENABLE 1 +#define OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE 1 #define OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH 1 #define OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD 10 #define OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE 0 diff --git a/tests/nexus/test_discover_scan.cpp b/tests/nexus/test_discover_scan.cpp new file mode 100644 index 000000000..f947fdeee --- /dev/null +++ b/tests/nexus/test_discover_scan.cpp @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +struct DiscoverContext +{ + static constexpr uint16_t kMaxResults = 16; + + void Clear(void) + { + mDiscoverDone = false; + mScanResults.Clear(); + } + + bool mDiscoverDone; + Array mScanResults; +}; + +struct RequestCallbackContext : public Clearable +{ + bool mInvoked; + Mle::Mle::DiscoveryRequestInfo mInfo; +}; + +void HandleDiscoverResult(otActiveScanResult *aResult, void *aContext) +{ + DiscoverContext *context = static_cast(aContext); + + VerifyOrQuit(aContext != nullptr); + + Log(" HandleDiscoverResult() called%s", aResult == nullptr ? " (done)" : ""); + + if (aResult == nullptr) + { + context->mDiscoverDone = true; + } + else + { + VerifyOrQuit(!context->mDiscoverDone); + SuccessOrQuit(context->mScanResults.PushBack(*aResult)); + } +} + +void HandleDiscoverRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext) +{ + RequestCallbackContext *context = static_cast(aContext); + + VerifyOrQuit(aInfo != nullptr); + VerifyOrQuit(aContext != nullptr); + + Log(" HandleDiscoverRequest() called"); + Log(" ExtAddress: %s", AsCoreType(&aInfo->mExtAddress).ToString().AsCString()); + Log(" Version: %u", aInfo->mVersion); + Log(" IsJoiner: %u", aInfo->mIsJoiner); + + VerifyOrQuit(!context->mInvoked); + + context->mInvoked = true; + context->mInfo = *aInfo; +} + +void TestDiscoverScanRequestCallback(void) +{ + Core nexus; + Node &leader = nexus.CreateNode(); + Node &scanner = nexus.CreateNode(); + DiscoverContext resultContext; + RequestCallbackContext requestContext; + Mle::DiscoverScanner::ScanResult *result; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestDiscoverScanRequestCallback"); + + nexus.AdvanceTime(0); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Form the network"); + + leader.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + + VerifyOrQuit(leader.Get().IsLeader()); + + scanner.Get().Up(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Register Discovery Request callback on `leader`"); + + requestContext.Clear(); + leader.Get().SetDiscoveryRequestCallback(HandleDiscoverRequest, &requestContext); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Perform discover scan from `scanner`"); + resultContext.Clear(); + + SuccessOrQuit(scanner.Get().Discover(Mac::ChannelMask(0), 0xffff, /* aJoiner */ false, + /* aFilter */ false, /* aFilterIndexes */ nullptr, + HandleDiscoverResult, &resultContext)); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check the Discovery Request callback is invoked correctly"); + + VerifyOrQuit(requestContext.mInvoked); + VerifyOrQuit(requestContext.mInfo.mVersion == kThreadVersion); + VerifyOrQuit(AsCoreType(&requestContext.mInfo.mExtAddress) == scanner.Get().GetExtAddress()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check the Discovery Result"); + + VerifyOrQuit(resultContext.mDiscoverDone); + VerifyOrQuit(resultContext.mScanResults.GetLength() == 1); + + result = &resultContext.mScanResults[0]; + + VerifyOrQuit(AsCoreType(&result->mExtAddress) == leader.Get().GetExtAddress()); + VerifyOrQuit(AsCoreType(&result->mExtendedPanId) == leader.Get().GetExtPanId()); + VerifyOrQuit(result->mPanId == leader.Get().GetPanId()); + VerifyOrQuit(result->mChannel == leader.Get().GetPanChannel()); + VerifyOrQuit(result->mDiscover); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestDiscoverScanRequestCallback(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/expect/cli-discover.exp b/tests/scripts/expect/cli-discover.exp index 420ba93b3..248dd3e2a 100755 --- a/tests/scripts/expect/cli-discover.exp +++ b/tests/scripts/expect/cli-discover.exp @@ -114,33 +114,4 @@ if {$::env(THREAD_VERSION) != "1.1" && $::env(OT_NODE_TYPE) == "cli"} { wait_for "" "Done" } -switch_node 1 -send "discover reqcallback enable\n" -expect_line "Done" - -switch_node 5 -send "discover\n" -expect "Error 13: InvalidState" -send "ifconfig up\n" -expect_line "Done" -send "discover 12\n" - -expect "| Network Name | Extended PAN | PAN | MAC Address | Ch | dBm | LQI |" -expect "+------------------+------------------+------+------------------+----+-----+-----+" -wait_for "" "\\| OpenThread-ch12 +\\| $extpan_1 \\| $pan_1 \\| $extaddr_1 \\| 12 \\| +-?\\d+ \\| +\\d \\|" -wait_for "" "Done" - -switch_node 1 -expect -re {version=\d,joiner=0} - -switch_node 5 -send "ifconfig up\n" -expect_line "Done" -send "joiner start 123456\n" -set timeout 10 -expect "NotFound" - -switch_node 1 -expect -re {version=\d,joiner=1} - dispose_all