Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[thread-host] implement join for rcp host #2631

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions src/ncp/rcp_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ void RcpHost::Deinit(void)
mThreadEnabledStateChangedCallbacks.clear();
mResetHandlers.clear();

mJoinReceiver = nullptr;
mSetThreadEnabledReceiver = nullptr;
mScheduleMigrationReceiver = nullptr;
mDetachGracefullyCallbacks.clear();
Expand All @@ -344,6 +345,12 @@ void RcpHost::HandleStateChanged(otChangedFlags aFlags)
}

mThreadHelper->StateChangedCallback(aFlags);

if ((aFlags & OT_CHANGED_THREAD_ROLE) && IsAttached() && mJoinReceiver != nullptr)
{
otbrLogInfo("Join succeeded");
SafeInvokeAndClear(mJoinReceiver, OT_ERROR_NONE, "Join succeeded");
}
}

void RcpHost::Update(MainloopContext &aMainloop)
Expand Down Expand Up @@ -438,12 +445,82 @@ const char *RcpHost::GetThreadVersion(void)
return version;
}

static bool noNeedRejoin(const otOperationalDatasetTlvs &aLhs, const otOperationalDatasetTlvs &aRhs)
{
bool result = false;

otOperationalDataset lhsDataset;
otOperationalDataset rhsDataset;

SuccessOrExit(otDatasetParseTlvs(&aLhs, &lhsDataset));
jwhui marked this conversation as resolved.
Show resolved Hide resolved
SuccessOrExit(otDatasetParseTlvs(&aRhs, &rhsDataset));

result =
(lhsDataset.mChannel == rhsDataset.mChannel) &&
(memcmp(lhsDataset.mNetworkKey.m8, rhsDataset.mNetworkKey.m8, sizeof(lhsDataset.mNetworkKey)) == 0) &&
(memcmp(lhsDataset.mExtendedPanId.m8, rhsDataset.mExtendedPanId.m8, sizeof(lhsDataset.mExtendedPanId)) == 0);

jwhui marked this conversation as resolved.
Show resolved Hide resolved
exit:
return result;
}

void RcpHost::Join(const otOperationalDatasetTlvs &aActiveOpDatasetTlvs, const AsyncResultReceiver &aReceiver)
{
OT_UNUSED_VARIABLE(aActiveOpDatasetTlvs);
otError error = OT_ERROR_NONE;
std::string errorMsg;
bool receiveResultHere = true;
otOperationalDatasetTlvs curDatasetTlvs;

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");
VerifyOrExit(mThreadEnabledState == ThreadEnabledState::kStateEnabled, error = OT_ERROR_INVALID_STATE,
errorMsg = "Thread is not enabled");

otbrLogInfo("Start joining...");

error = otDatasetGetActiveTlvs(mInstance, &curDatasetTlvs);
if (error == OT_ERROR_NONE && noNeedRejoin(aActiveOpDatasetTlvs, curDatasetTlvs) && IsAttached())
{
// Do not leave and re-join if this device has already joined the same network. This can help elimilate
// unnecessary connectivity and topology disruption and save the time for re-joining. It's more useful for use
// cases where Thread networks are dynamically brought up and torn down (e.g. Thread on mobile phones).
SuccessOrExit(error = otDatasetSetActiveTlvs(mInstance, &aActiveOpDatasetTlvs),
errorMsg = "Failed to set Active Operational Dataset");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If already joined to same network, do we need to set the active dataset again?
This can trigger registration/resync of dataset with leader? Is this desired/interntional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is out of consideration for unknown TLVs in the dataset. It's very difficult to check if two datasets are completely the same with the existence of unknown TLVs (because of the order). So we simply set the active dataset again. But we want to avoid rejoin as possible. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it sounds okay to do so. The leader should combine them. This may add some extra traffic/exchanges when the parameters fully match, but I expect it should be harmless otherwise.

errorMsg = "Already Joined the target network";
ExitNow();
}

if (GetDeviceRole() != OT_DEVICE_ROLE_DISABLED)
{
ThreadDetachGracefully([aActiveOpDatasetTlvs, aReceiver, this] {
ConditionalErasePersistentInfo(true);
Join(aActiveOpDatasetTlvs, aReceiver);
});
receiveResultHere = false;
ExitNow();
}

SuccessOrExit(error = otDatasetSetActiveTlvs(mInstance, &aActiveOpDatasetTlvs),
errorMsg = "Failed to set Active Operational Dataset");

// TODO: Implement Join under RCP mode.
mTaskRunner.Post([aReceiver](void) { aReceiver(OT_ERROR_NOT_IMPLEMENTED, "Not implemented!"); });
// TODO(b/273160198): check how we can implement join as a child
SuccessOrExit(error = otIp6SetEnabled(mInstance, true), errorMsg = "Failed to bring up Thread interface");
SuccessOrExit(error = otThreadSetEnabled(mInstance, true), errorMsg = "Failed to bring up Thread stack");

// Abort an ongoing join()
if (mJoinReceiver != nullptr)
{
SafeInvoke(mJoinReceiver, OT_ERROR_ABORT, "Join() is aborted");
jwhui marked this conversation as resolved.
Show resolved Hide resolved
}
mJoinReceiver = aReceiver;
receiveResultHere = false;

exit:
if (receiveResultHere)
{
mTaskRunner.Post([aReceiver, error, errorMsg](void) { aReceiver(error, errorMsg); });
}
}

void RcpHost::Leave(bool aEraseDataset, const AsyncResultReceiver &aReceiver)
Expand Down Expand Up @@ -636,6 +713,7 @@ void RcpHost::ThreadDetachGracefullyCallback(void *aContext)

void RcpHost::ThreadDetachGracefullyCallback(void)
{
SafeInvokeAndClear(mJoinReceiver, OT_ERROR_ABORT, "Aborted by leave/disable operation");
SafeInvokeAndClear(mScheduleMigrationReceiver, OT_ERROR_ABORT, "Aborted by leave/disable operation");

for (auto &callback : mDetachGracefullyCallbacks)
Expand Down
1 change: 1 addition & 0 deletions src/ncp/rcp_host.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class RcpHost : public MainloopProcessor, public ThreadHost, public OtNetworkPro
std::vector<ThreadEnabledStateCallback> mThreadEnabledStateChangedCallbacks;
bool mEnableAutoAttach = false;
ThreadEnabledState mThreadEnabledState;
AsyncResultReceiver mJoinReceiver;
AsyncResultReceiver mSetThreadEnabledReceiver;
AsyncResultReceiver mScheduleMigrationReceiver;
std::vector<DetachGracefullyCallback> mDetachGracefullyCallbacks;
Expand Down
119 changes: 119 additions & 0 deletions tests/gtest/test_rcp_host_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,122 @@ TEST(RcpHostApi, StateChangesCorrectlyAfterScheduleMigration)

host.Deinit();
}

TEST(RcpHostApi, StateChangesCorrectlyAfterJoin)
{
otError error = OT_ERROR_NONE;
otError error_ = OT_ERROR_NONE;
std::string errorMsg = "";
std::string errorMsg_ = "";
bool resultReceived = false;
bool resultReceived_ = false;
otbr::MainloopContext mainloop;
otbr::Ncp::ThreadHost::AsyncResultReceiver receiver = [&resultReceived, &error,
&errorMsg](otError aError, const std::string &aErrorMsg) {
resultReceived = true;
error = aError;
errorMsg = 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<const char *>(), /* aBackboneInterfaceName */ "", /* aDryRun */ false,
/* aEnableAutoAttach */ false);

otOperationalDataset dataset;
(void)dataset;
otOperationalDatasetTlvs datasetTlvs;

// 1. Call Join when host hasn't been initialized.
otbr::MainloopManager::GetInstance().RemoveMainloopProcessor(
&host); // Temporarily remove RcpHost because it's not initialized yet.
host.Join(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();
OT_UNUSED_VARIABLE(otDatasetCreateNewNetwork(ot::FakePlatform::CurrentInstance(), &dataset));
otDatasetConvertToTlvs(&dataset, &datasetTlvs);

// 2. Call Join when Thread is not enabled.
error = OT_ERROR_NONE;
resultReceived = false;
host.Join(datasetTlvs, receiver);
MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; });
EXPECT_EQ(error, OT_ERROR_INVALID_STATE);
EXPECT_STREQ(errorMsg.c_str(), "Thread is not enabled");

// 3. Call two consecutive Join. The first one should be aborted. The second one should succeed.
error = OT_ERROR_NONE;
resultReceived = false;
host.SetThreadEnabled(true, receiver);
MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; });
error = OT_ERROR_NONE;
resultReceived = false;
host.Join(datasetTlvs, receiver_);
host.Join(datasetTlvs, receiver);

MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0,
[&resultReceived, &resultReceived_]() { return resultReceived && resultReceived_; });
EXPECT_EQ(error_, OT_ERROR_ABORT);
EXPECT_STREQ(errorMsg_.c_str(), "Aborted by leave/disable operation"); // The second Join will trigger Leave first.
EXPECT_EQ(error, OT_ERROR_NONE);
EXPECT_STREQ(errorMsg.c_str(), "Join succeeded");
EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_LEADER);

// 4. Call Join with the same dataset.
error = OT_ERROR_NONE;
resultReceived = false;
host.Join(datasetTlvs, receiver);
MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; });
EXPECT_EQ(error, OT_ERROR_NONE);
EXPECT_STREQ(errorMsg.c_str(), "Already Joined the target network");

jwhui marked this conversation as resolved.
Show resolved Hide resolved
// 5. Call Disable right after Join (Already Attached).
error = OT_ERROR_NONE;
resultReceived = false;
error_ = OT_ERROR_NONE;
resultReceived_ = false;

OT_UNUSED_VARIABLE(otDatasetCreateNewNetwork(ot::FakePlatform::CurrentInstance(), &dataset));
otDatasetConvertToTlvs(&dataset, &datasetTlvs); // Use a different dataset.

host.Join(datasetTlvs, receiver_);
host.SetThreadEnabled(false, receiver);

MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0,
[&resultReceived, &resultReceived_]() { return resultReceived && resultReceived_; });
EXPECT_EQ(error_, OT_ERROR_BUSY);
EXPECT_STREQ(errorMsg_.c_str(), "Thread is disabling");
EXPECT_EQ(error, OT_ERROR_NONE);
EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_DISABLED);

// 6. Call Disable right after Join (not attached).
resultReceived = false;
host.Leave(true, receiver); // Leave the network first.
MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; });
resultReceived = false; // Enale Thread.
host.SetThreadEnabled(true, receiver);
MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0, [&resultReceived]() { return resultReceived; });

error = OT_ERROR_NONE;
resultReceived = false;
error_ = OT_ERROR_NONE;
resultReceived_ = false;
host.Join(datasetTlvs, receiver_);
host.SetThreadEnabled(false, receiver);

MainloopProcessUntil(mainloop, /* aTimeoutSec */ 0,
[&resultReceived, &resultReceived_]() { return resultReceived && resultReceived_; });
EXPECT_EQ(error_, OT_ERROR_ABORT);
EXPECT_STREQ(errorMsg_.c_str(), "Aborted by leave/disable operation");
EXPECT_EQ(error, OT_ERROR_NONE);
EXPECT_EQ(host.GetDeviceRole(), OT_DEVICE_ROLE_DISABLED);

host.Deinit();
}
Loading