diff --git a/src/ncp/ncp_host.cpp b/src/ncp/ncp_host.cpp index e729a7cff4e..b37a4a51b98 100644 --- a/src/ncp/ncp_host.cpp +++ b/src/ncp/ncp_host.cpp @@ -229,6 +229,12 @@ void NcpHost::AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback OT_UNUSED_VARIABLE(aCallback); } +void NcpHost::AddThreadEnabledStateChangedCallback(ThreadEnabledStateCallback aCallback) +{ + // TODO: Implement AddThreadEnabledStateChangedCallback under NCP mode. + OT_UNUSED_VARIABLE(aCallback); +} + void NcpHost::Process(const MainloopContext &aMainloop) { mSpinelDriver.Process(&aMainloop); diff --git a/src/ncp/ncp_host.hpp b/src/ncp/ncp_host.hpp index 65d932ee289..9a94f2fd870 100644 --- a/src/ncp/ncp_host.hpp +++ b/src/ncp/ncp_host.hpp @@ -102,6 +102,7 @@ class NcpHost : public MainloopProcessor, public ThreadHost, public NcpNetworkPr const AsyncResultReceiver &aReceiver) override; #endif void AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback) override; + void AddThreadEnabledStateChangedCallback(ThreadEnabledStateCallback aCallback) override; CoprocessorType GetCoprocessorType(void) override { return OT_COPROCESSOR_NCP; diff --git a/src/ncp/rcp_host.cpp b/src/ncp/rcp_host.cpp index 1fcb7664a39..0061e29f597 100644 --- a/src/ncp/rcp_host.cpp +++ b/src/ncp/rcp_host.cpp @@ -125,6 +125,7 @@ RcpHost::RcpHost(const char *aInterfaceName, bool aEnableAutoAttach) : mInstance(nullptr) , mEnableAutoAttach(aEnableAutoAttach) + , mThreadEnabledState(ThreadEnabledState::kStateDisabled) { VerifyOrDie(aRadioUrls.size() <= OT_PLATFORM_CONFIG_MAX_RADIO_URLS, "Too many Radio URLs!"); @@ -327,6 +328,7 @@ void RcpHost::Deinit(void) OtNetworkProperties::SetInstance(nullptr); mThreadStateChangedCallbacks.clear(); + mThreadEnabledStateChangedCallbacks.clear(); mResetHandlers.clear(); mSetThreadEnabledReceiver = nullptr; @@ -390,6 +392,11 @@ void RcpHost::AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback mThreadStateChangedCallbacks.emplace_back(std::move(aCallback)); } +void RcpHost::AddThreadEnabledStateChangedCallback(ThreadEnabledStateCallback aCallback) +{ + mThreadEnabledStateChangedCallbacks.push_back(aCallback); +} + void RcpHost::Reset(void) { gPlatResetReason = OT_PLAT_RESET_REASON_SOFTWARE; @@ -452,8 +459,13 @@ void RcpHost::ScheduleMigration(const otOperationalDatasetTlvs &aPendingOpDatase otOperationalDataset emptyDataset; VerifyOrExit(mInstance != nullptr, error = OT_ERROR_INVALID_STATE, errorMsg = "OT is not initialized"); - VerifyOrExit(IsAttached(), error = OT_ERROR_FAILED, - errorMsg = "Cannot schedule migration when this device is detached"); + + VerifyOrExit(mThreadEnabledState != ThreadEnabledState::kStateDisabling, error = OT_ERROR_BUSY, + errorMsg = "Thread is disabling"); + VerifyOrExit(mThreadEnabledState == ThreadEnabledState::kStateEnabled, error = OT_ERROR_INVALID_STATE, + errorMsg = "Thread is disabled"); + + VerifyOrExit(IsAttached(), error = OT_ERROR_INVALID_STATE, errorMsg = "Device is detached"); // TODO: check supported channel mask @@ -488,25 +500,35 @@ void RcpHost::SendMgmtPendingSetCallback(otError aError) void RcpHost::SetThreadEnabled(bool aEnabled, const AsyncResultReceiver aReceiver) { - otError error = OT_ERROR_NONE; - bool receiveResultHere = true; + otError error = OT_ERROR_NONE; + std::string errorMsg = ""; + bool receiveResultHere = true; - VerifyOrExit(mInstance != nullptr, error = OT_ERROR_INVALID_STATE); - VerifyOrExit(mSetThreadEnabledReceiver == nullptr, error = OT_ERROR_BUSY); + VerifyOrExit(mInstance != nullptr, error = OT_ERROR_INVALID_STATE, errorMsg = "OT is not initialized"); + VerifyOrExit(mThreadEnabledState != ThreadEnabledState::kStateDisabling, error = OT_ERROR_BUSY, + errorMsg = "Thread is disabling"); if (aEnabled) { otOperationalDatasetTlvs datasetTlvs; + if (mThreadEnabledState == ThreadEnabledState::kStateEnabled) + { + ExitNow(); + } + if (otDatasetGetActiveTlvs(mInstance, &datasetTlvs) != OT_ERROR_NOT_FOUND && datasetTlvs.mLength > 0 && otThreadGetDeviceRole(mInstance) == OT_DEVICE_ROLE_DISABLED) { SuccessOrExit(error = otIp6SetEnabled(mInstance, true)); SuccessOrExit(error = otThreadSetEnabled(mInstance, true)); } + UpdateThreadEnabledState(ThreadEnabledState::kStateEnabled); } else { + UpdateThreadEnabledState(ThreadEnabledState::kStateDisabling); + SuccessOrExit(error = otThreadDetachGracefully(mInstance, DisableThreadAfterDetach, this)); mSetThreadEnabledReceiver = aReceiver; receiveResultHere = false; @@ -515,7 +537,7 @@ void RcpHost::SetThreadEnabled(bool aEnabled, const AsyncResultReceiver aReceive exit: if (receiveResultHere) { - mTaskRunner.Post([aReceiver, error](void) { aReceiver(error, ""); }); + mTaskRunner.Post([aReceiver, error, errorMsg](void) { aReceiver(error, errorMsg); }); } } @@ -586,6 +608,8 @@ void RcpHost::DisableThreadAfterDetach(void) SuccessOrExit(error = otThreadSetEnabled(mInstance, false), errorMsg = "Failed to disable Thread stack"); SuccessOrExit(error = otIp6SetEnabled(mInstance, false), errorMsg = "Failed to disable Thread interface"); + UpdateThreadEnabledState(ThreadEnabledState::kStateDisabled); + exit: SafeInvokeAndClear(mSetThreadEnabledReceiver, error, errorMsg); } @@ -617,6 +641,16 @@ bool RcpHost::IsAttached(void) return role == OT_DEVICE_ROLE_CHILD || role == OT_DEVICE_ROLE_ROUTER || role == OT_DEVICE_ROLE_LEADER; } +void RcpHost::UpdateThreadEnabledState(ThreadEnabledState aState) +{ + mThreadEnabledState = aState; + + for (auto &callback : mThreadEnabledStateChangedCallbacks) + { + callback(mThreadEnabledState); + } +} + /* * Provide, if required an "otPlatLog()" function */ diff --git a/src/ncp/rcp_host.hpp b/src/ncp/rcp_host.hpp index 024617e2846..e1f19675cac 100644 --- a/src/ncp/rcp_host.hpp +++ b/src/ncp/rcp_host.hpp @@ -210,6 +210,7 @@ class RcpHost : public MainloopProcessor, public ThreadHost, public OtNetworkPro const AsyncResultReceiver &aReceiver) override; #endif void AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback) override; + void AddThreadEnabledStateChangedCallback(ThreadEnabledStateCallback aCallback) override; CoprocessorType GetCoprocessorType(void) override { @@ -260,6 +261,8 @@ class RcpHost : public MainloopProcessor, public ThreadHost, public OtNetworkPro bool IsAttached(void); + void UpdateThreadEnabledState(ThreadEnabledState aState); + otError SetOtbrAndOtLogLevel(otbrLogLevel aLevel); otInstance *mInstance; @@ -268,11 +271,13 @@ class RcpHost : public MainloopProcessor, public ThreadHost, public OtNetworkPro std::unique_ptr mThreadHelper; std::vector> mResetHandlers; TaskRunner mTaskRunner; - std::vector mThreadStateChangedCallbacks; - bool mEnableAutoAttach = false; - AsyncResultReceiver mSetThreadEnabledReceiver; - AsyncResultReceiver mScheduleMigrationReceiver; + std::vector mThreadStateChangedCallbacks; + std::vector mThreadEnabledStateChangedCallbacks; + bool mEnableAutoAttach = false; + ThreadEnabledState mThreadEnabledState; + AsyncResultReceiver mSetThreadEnabledReceiver; + AsyncResultReceiver mScheduleMigrationReceiver; #if OTBR_ENABLE_FEATURE_FLAGS // The applied FeatureFlagList in ApplyFeatureFlagList call, used for debugging purpose. diff --git a/src/ncp/thread_host.hpp b/src/ncp/thread_host.hpp index 3ff5d9f69ee..d364a14c739 100644 --- a/src/ncp/thread_host.hpp +++ b/src/ncp/thread_host.hpp @@ -98,6 +98,14 @@ class NetworkProperties virtual ~NetworkProperties(void) = default; }; +enum ThreadEnabledState +{ + kStateDisabled = 0, + kStateEnabled = 1, + kStateDisabling = 2, + kStateInvalid = 255, +}; + /** * This class is an interface which provides a set of async APIs to control the * Thread network. @@ -112,6 +120,7 @@ class ThreadHost : virtual public NetworkProperties std::function; using DeviceRoleHandler = std::function; using ThreadStateChangedCallback = std::function; + using ThreadEnabledStateCallback = std::function; struct ChannelMaxPower { @@ -233,6 +242,13 @@ class ThreadHost : virtual public NetworkProperties */ virtual void AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback) = 0; + /** + * This method adds a event listener for Thread Enabled state changes. + * + * @param[in] aCallback The callback to receive Thread Enabled state changed events. + */ + virtual void AddThreadEnabledStateChangedCallback(ThreadEnabledStateCallback aCallback) = 0; + /** * Returns the co-processor type. */ diff --git a/tests/gtest/test_rcp_host_api.cpp b/tests/gtest/test_rcp_host_api.cpp index 4f49257fd9c..59e84140c4c 100644 --- a/tests/gtest/test_rcp_host_api.cpp +++ b/tests/gtest/test_rcp_host_api.cpp @@ -64,8 +64,9 @@ static void MainloopProcessUntil(otbr::MainloopContext &aMainloop, TEST(RcpHostApi, DeviceRoleChangesCorrectlyAfterSetThreadEnabled) { - otError error = OT_ERROR_FAILED; - bool resultReceived = false; + otError error = OT_ERROR_FAILED; + bool resultReceived = false; + otbr::Ncp::ThreadEnabledState threadEnabledState = otbr::Ncp::ThreadEnabledState::kStateInvalid; otbr::MainloopContext mainloop; otbr::Ncp::ThreadHost::AsyncResultReceiver receiver = [&resultReceived, &error](otError aError, const std::string &aErrorMsg) { @@ -73,18 +74,22 @@ TEST(RcpHostApi, DeviceRoleChangesCorrectlyAfterSetThreadEnabled) resultReceived = true; error = aError; }; + otbr::Ncp::ThreadHost::ThreadEnabledStateCallback enabledStateCallback = + [&threadEnabledState](otbr::Ncp::ThreadEnabledState aState) { threadEnabledState = aState; }; otbr::Ncp::RcpHost host("wpan0", std::vector(), /* aBackboneInterfaceName */ "", /* aDryRun */ false, /* aEnableAutoAttach */ false); host.Init(); + host.AddThreadEnabledStateChangedCallback(enabledStateCallback); // 1. Active dataset hasn't been set, should succeed with device role still being disabled. host.SetThreadEnabled(true, receiver); MainloopProcessUntil(mainloop, /* aTimeoutSec */ 1, [&resultReceived]() { return resultReceived; }); EXPECT_EQ(error, OT_ERROR_NONE); EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_DISABLED); + EXPECT_EQ(threadEnabledState, otbr::Ncp::ThreadEnabledState::kStateEnabled); - // 2. Set active dataset and enable it + // 2. Set active dataset and start it { otOperationalDataset dataset; otOperationalDatasetTlvs datasetTlvs; @@ -92,26 +97,32 @@ TEST(RcpHostApi, DeviceRoleChangesCorrectlyAfterSetThreadEnabled) otDatasetConvertToTlvs(&dataset, &datasetTlvs); OT_UNUSED_VARIABLE(otDatasetSetActiveTlvs(ot::FakePlatform::CurrentInstance(), &datasetTlvs)); } + OT_UNUSED_VARIABLE(otIp6SetEnabled(ot::FakePlatform::CurrentInstance(), true)); + OT_UNUSED_VARIABLE(otThreadSetEnabled(ot::FakePlatform::CurrentInstance(), true)); + + MainloopProcessUntil(mainloop, /* aTimeoutSec */ 1, + [&host]() { return host.GetDeviceRole() != OT_DEVICE_ROLE_DETACHED; }); + EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_LEADER); + + // 3. Enable again, the enabled state should not change. error = OT_ERROR_FAILED; resultReceived = false; host.SetThreadEnabled(true, receiver); MainloopProcessUntil(mainloop, /* aTimeoutSec */ 1, [&resultReceived]() { return resultReceived; }); EXPECT_EQ(error, OT_ERROR_NONE); - EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_DETACHED); + EXPECT_EQ(threadEnabledState, otbr::Ncp::ThreadEnabledState::kStateEnabled); - MainloopProcessUntil(mainloop, /* aTimeoutSec */ 1, - [&host]() { return host.GetDeviceRole() != OT_DEVICE_ROLE_DETACHED; }); - EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_LEADER); - - // 3. Disable it + // 4. Disable it error = OT_ERROR_FAILED; resultReceived = false; host.SetThreadEnabled(false, receiver); + EXPECT_EQ(threadEnabledState, otbr::Ncp::ThreadEnabledState::kStateDisabling); MainloopProcessUntil(mainloop, /* aTimeoutSec */ 1, [&resultReceived]() { return resultReceived; }); EXPECT_EQ(error, OT_ERROR_NONE); EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_DISABLED); + EXPECT_EQ(threadEnabledState, otbr::Ncp::ThreadEnabledState::kStateDisabled); - // 4. Duplicate call, should get OT_ERROR_BUSY + // 5. Duplicate call, should get OT_ERROR_BUSY error = OT_ERROR_FAILED; resultReceived = false; otError error2 = OT_ERROR_FAILED; @@ -126,6 +137,7 @@ TEST(RcpHostApi, DeviceRoleChangesCorrectlyAfterSetThreadEnabled) [&resultReceived, &resultReceived2]() { return resultReceived && resultReceived2; }); EXPECT_EQ(error, OT_ERROR_NONE); EXPECT_EQ(error2, OT_ERROR_BUSY); + EXPECT_EQ(threadEnabledState, otbr::Ncp::ThreadEnabledState::kStateDisabled); host.Deinit(); } @@ -185,13 +197,14 @@ TEST(RcpHostApi, SetCountryCodeWorkCorrectly) TEST(RcpHostApi, StateChangesCorrectlyAfterScheduleMigration) { otError error = OT_ERROR_NONE; + std::string errorMsg = ""; bool resultReceived = false; otbr::MainloopContext mainloop; - otbr::Ncp::ThreadHost::AsyncResultReceiver receiver = [&resultReceived, &error](otError aError, - const std::string &aErrorMsg) { - OT_UNUSED_VARIABLE(aErrorMsg); + otbr::Ncp::ThreadHost::AsyncResultReceiver receiver = [&resultReceived, &error, + &errorMsg](otError aError, const std::string &aErrorMsg) { resultReceived = true; error = aError; + errorMsg = aErrorMsg; }; otbr::Ncp::RcpHost host("wpan0", std::vector(), /* aBackboneInterfaceName */ "", /* aDryRun */ false, /* aEnableAutoAttach */ false); @@ -205,16 +218,18 @@ TEST(RcpHostApi, StateChangesCorrectlyAfterScheduleMigration) host.ScheduleMigration(datasetTlvs, receiver); MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; }); EXPECT_EQ(error, OT_ERROR_INVALID_STATE); + EXPECT_STREQ(errorMsg.c_str(), "OT is not initialized"); otbr::MainloopManager::GetInstance().AddMainloopProcessor(&host); host.Init(); - // 2. Call ScheduleMigration when the device is not attached. + // 2. Call ScheduleMigration when the Thread is not enabled. error = OT_ERROR_NONE; resultReceived = false; host.ScheduleMigration(datasetTlvs, receiver); MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; }); - EXPECT_EQ(error, OT_ERROR_FAILED); + EXPECT_EQ(error, OT_ERROR_INVALID_STATE); + EXPECT_STREQ(errorMsg.c_str(), "Thread is disabled"); // 3. Schedule migration to another network. OT_UNUSED_VARIABLE(otDatasetCreateNewNetwork(ot::FakePlatform::CurrentInstance(), &dataset));