diff --git a/src/dbus/server/dbus_thread_object_ncp.cpp b/src/dbus/server/dbus_thread_object_ncp.cpp index 3e996c84bf8..526a477f6d9 100644 --- a/src/dbus/server/dbus_thread_object_ncp.cpp +++ b/src/dbus/server/dbus_thread_object_ncp.cpp @@ -148,8 +148,10 @@ void DBusThreadObjectNcp::ScheduleMigrationHandler(DBusRequest &aRequest) SuccessOrExit(error = agent::ThreadHelper::ProcessDatasetForMigration(pendingOpDatasetTlvs, delayInMilli)); - // TODO: Change to use Migrate API - error = OT_ERROR_NOT_IMPLEMENTED; + mHost.ScheduleMigration(pendingOpDatasetTlvs, [aRequest](otError aError, const std::string &aErrorInfo) mutable { + OT_UNUSED_VARIABLE(aErrorInfo); + aRequest.ReplyOtResult(aError); + }); exit: if (error != OT_ERROR_NONE) diff --git a/src/ncp/ncp_host.cpp b/src/ncp/ncp_host.cpp index 0b78f09d622..f6819055999 100644 --- a/src/ncp/ncp_host.cpp +++ b/src/ncp/ncp_host.cpp @@ -114,6 +114,25 @@ void NcpHost::Leave(const AsyncResultReceiver &aReceiver) task->Run(); } +void NcpHost::ScheduleMigration(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, + const AsyncResultReceiver aReceiver) +{ + otDeviceRole role = GetDeviceRole(); + otError error = OT_ERROR_NONE; + auto errorHandler = [aReceiver](otError aError, const std::string &aErrorInfo) { aReceiver(aError, aErrorInfo); }; + + VerifyOrExit(role != OT_DEVICE_ROLE_DISABLED && role != OT_DEVICE_ROLE_DETACHED, error = OT_ERROR_INVALID_STATE); + + mNcpSpinel.DatasetMgmtSetPending(aPendingOpDatasetTlvs, std::make_shared(errorHandler)); + +exit: + if (error != OT_ERROR_NONE) + { + mTaskRunner.Post( + [aReceiver, error](void) { aReceiver(error, "Cannot schedule migration when this device is detached"); }); + } +} + void NcpHost::Process(const MainloopContext &aMainloop) { mSpinelDriver.Process(&aMainloop); diff --git a/src/ncp/ncp_host.hpp b/src/ncp/ncp_host.hpp index 7aef3084963..fbcb5214b4d 100644 --- a/src/ncp/ncp_host.hpp +++ b/src/ncp/ncp_host.hpp @@ -88,6 +88,8 @@ class NcpHost : public MainloopProcessor, public ThreadHost, public NcpNetworkPr // ThreadHost methods void Join(const otOperationalDatasetTlvs &aActiveOpDatasetTlvs, const AsyncResultReceiver &aReceiver) override; void Leave(const AsyncResultReceiver &aReceiver) override; + void ScheduleMigration(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, + const AsyncResultReceiver aReceiver) override; CoprocessorType GetCoprocessorType(void) override { return OT_COPROCESSOR_NCP; } const char *GetCoprocessorVersion(void) override; const char *GetInterfaceName(void) const override { return mConfig.mInterfaceName; } @@ -102,6 +104,7 @@ class NcpHost : public MainloopProcessor, public ThreadHost, public NcpNetworkPr ot::Spinel::SpinelDriver &mSpinelDriver; otPlatformConfig mConfig; NcpSpinel mNcpSpinel; + TaskRunner mTaskRunner; }; } // namespace Ncp diff --git a/src/ncp/ncp_spinel.cpp b/src/ncp/ncp_spinel.cpp index 428a12ad76a..fc4a46dd99d 100644 --- a/src/ncp/ncp_spinel.cpp +++ b/src/ncp/ncp_spinel.cpp @@ -110,6 +110,25 @@ void NcpSpinel::DatasetSetActiveTlvs(const otOperationalDatasetTlvs &aActiveOpDa } } +void NcpSpinel::DatasetMgmtSetPending(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, AsyncTaskPtr aAsyncTask) +{ + otError error = OT_ERROR_NONE; + EncodingFunc encodingFunc = [this, &aPendingOpDatasetTlvs] { + return mEncoder.WriteData(aPendingOpDatasetTlvs.mTlvs, aPendingOpDatasetTlvs.mLength); + }; + + VerifyOrExit(mDatasetMgmtSetPendingTask == nullptr, error = OT_ERROR_BUSY); + + SuccessOrExit(error = SetProperty(SPINEL_PROP_THREAD_MGMT_SET_PENDING_DATASET_TLVS, encodingFunc)); + mDatasetMgmtSetPendingTask = aAsyncTask; + +exit: + if (error != OT_ERROR_NONE) + { + mTaskRunner.Post([aAsyncTask, error] { aAsyncTask->SetResult(error, "Failed to set pending dataset!"); }); + } +} + void NcpSpinel::Ip6SetEnabled(bool aEnable, AsyncTaskPtr aAsyncTask) { otError error = OT_ERROR_NONE; @@ -327,6 +346,15 @@ void NcpSpinel::HandleValueIs(spinel_prop_key_t aKey, const uint8_t *aBuffer, ui break; } + case SPINEL_PROP_THREAD_MGMT_SET_PENDING_DATASET_TLVS: + { + spinel_status_t status = SPINEL_STATUS_OK; + + SuccessOrExit(error = SpinelDataUnpack(aBuffer, aLength, SPINEL_DATATYPE_UINT_PACKED_S, &status)); + CallAndClear(mDatasetMgmtSetPendingTask, ot::Spinel::SpinelStatusToOtError(status)); + break; + } + default: otbrLogWarning("Received uncognized key: %u", aKey); break; @@ -364,6 +392,20 @@ otbrError NcpSpinel::HandleResponseForPropSet(spinel_tid_t aTid, CallAndClear(mThreadSetEnabledTask, OT_ERROR_NONE); break; + case SPINEL_PROP_THREAD_MGMT_SET_PENDING_DATASET_TLVS: + if (aKey == SPINEL_PROP_LAST_STATUS) + { // Failed case + spinel_status_t status = SPINEL_STATUS_OK; + + SuccessOrExit(error = SpinelDataUnpack(aData, aLength, SPINEL_DATATYPE_UINT_PACKED_S, &status)); + CallAndClear(mDatasetMgmtSetPendingTask, ot::Spinel::SpinelStatusToOtError(status)); + } + else if (aKey != SPINEL_PROP_THREAD_MGMT_SET_PENDING_DATASET_TLVS) + { + ExitNow(error = OTBR_ERROR_INVALID_STATE); + } + break; + default: VerifyOrExit(aKey == mWaitingKeyTable[aTid], error = OTBR_ERROR_INVALID_STATE); break; diff --git a/src/ncp/ncp_spinel.hpp b/src/ncp/ncp_spinel.hpp index 11a52d4c721..37146f26566 100644 --- a/src/ncp/ncp_spinel.hpp +++ b/src/ncp/ncp_spinel.hpp @@ -122,6 +122,18 @@ class NcpSpinel */ void DatasetSetActiveTlvs(const otOperationalDatasetTlvs &aActiveOpDatasetTlvs, AsyncTaskPtr aAsyncTask); + /** + * This method instructs the NCP to send a MGMT_SET to set Thread Pending Operational Dataset. + * + * If this method is called again before the previous call completed, no action will be taken. + * The new receiver @p aAsyncTask will be set a result OT_ERROR_BUSY. + * + * @param[in] aPendingOpDatasetTlvs A reference to the pending operational dataset of the Thread network. + * @param[in] aAsyncTask A pointer to an async result to receive the result of this operation. + * + */ + void DatasetMgmtSetPending(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, AsyncTaskPtr aAsyncTask); + /** * This method enableds/disables the IP6 on the NCP. * @@ -241,6 +253,7 @@ class NcpSpinel PropsObserver *mPropsObserver; AsyncTaskPtr mDatasetSetActiveTask; + AsyncTaskPtr mDatasetMgmtSetPendingTask; AsyncTaskPtr mIp6SetEnabledTask; AsyncTaskPtr mThreadSetEnabledTask; AsyncTaskPtr mThreadDetachGracefullyTask; diff --git a/src/ncp/rcp_host.cpp b/src/ncp/rcp_host.cpp index f00552b0710..97d6c7306ef 100644 --- a/src/ncp/rcp_host.cpp +++ b/src/ncp/rcp_host.cpp @@ -408,6 +408,15 @@ void RcpHost::Leave(const AsyncResultReceiver &aReceiver) mTaskRunner.Post([aReceiver](void) { aReceiver(OT_ERROR_NOT_IMPLEMENTED, "Not implemented!"); }); } +void RcpHost::ScheduleMigration(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, + const AsyncResultReceiver aReceiver) +{ + OT_UNUSED_VARIABLE(aPendingOpDatasetTlvs); + + // TODO: Implement ScheduleMigration under RCP mode. + mTaskRunner.Post([aReceiver](void) { aReceiver(OT_ERROR_NOT_IMPLEMENTED, "Not implemented!"); }); +} + /* * Provide, if required an "otPlatLog()" function */ diff --git a/src/ncp/rcp_host.hpp b/src/ncp/rcp_host.hpp index 42e90fb9af8..a13e70ccb30 100644 --- a/src/ncp/rcp_host.hpp +++ b/src/ncp/rcp_host.hpp @@ -220,6 +220,8 @@ class RcpHost : public MainloopProcessor, public ThreadHost, public OtNetworkPro // Thread Control virtual methods void Join(const otOperationalDatasetTlvs &aActiveOpDatasetTlvs, const AsyncResultReceiver &aRecevier) override; void Leave(const AsyncResultReceiver &aRecevier) override; + void ScheduleMigration(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, + const AsyncResultReceiver aReceiver) override; CoprocessorType GetCoprocessorType(void) override { diff --git a/src/ncp/thread_host.hpp b/src/ncp/thread_host.hpp index 2537946b024..65e06356141 100644 --- a/src/ncp/thread_host.hpp +++ b/src/ncp/thread_host.hpp @@ -133,6 +133,16 @@ class ThreadHost : virtual public NetworkProperties */ virtual void Leave(const AsyncResultReceiver &aRecevier) = 0; + /** + * This method migrates this device to the new network specified by @p aPendingOpDatasetTlvs. + * + * @param[in] aPendingOpDatasetTlvs A reference to the pending operational dataset of the Thread network. + * @param[in] aReceiver A receiver to get the async result of this operation. + * + */ + virtual void ScheduleMigration(const otOperationalDatasetTlvs &aPendingOpDatasetTlvs, + const AsyncResultReceiver aReceiver) = 0; + /** * Returns the co-processor type. * diff --git a/tests/scripts/expect/ncp_schedule_migration.exp b/tests/scripts/expect/ncp_schedule_migration.exp new file mode 100644 index 00000000000..c32656f52f5 --- /dev/null +++ b/tests/scripts/expect/ncp_schedule_migration.exp @@ -0,0 +1,80 @@ +#!/usr/bin/expect -f +# +# Copyright (c) 2024, 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. +# + +source "tests/scripts/expect/_common.exp" + +# Dataset of the initial Thread network +set dataset "0e080000000000010000000300001435060004001fffe002087d61eb42cdc48d6a0708fd0d07fca1b9f0500510ba088fc2bd6c3b3897f7a10f58263ff3030f4f70656e5468726561642d353234660102524f04109dc023ccd447b12b50997ef68020f19e0c0402a0f7f8" +set dataset_dbus "0x0e,0x08,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x03,0x00,0x00,0x14,0x35,0x06,0x00,0x04,0x00,0x1f,0xff,0xe0,0x02,0x08,0x7d,0x61,0xeb,0x42,0xcd,0xc4,0x8d,0x6a,0x07,0x08,0xfd,0x0d,0x07,0xfc,0xa1,0xb9,0xf0,0x50,0x05,0x10,0xba,0x08,0x8f,0xc2,0xbd,0x6c,0x3b,0x38,0x97,0xf7,0xa1,0x0f,0x58,0x26,0x3f,0xf3,0x03,0x0f,0x4f,0x70,0x65,0x6e,0x54,0x68,0x72,0x65,0x61,0x64,0x2d,0x35,0x32,0x34,0x66,0x01,0x02,0x52,0x4f,0x04,0x10,0x9d,0xc0,0x23,0xcc,0xd4,0x47,0xb1,0x2b,0x50,0x99,0x7e,0xf6,0x80,0x20,0xf1,0x9e,0x0c,0x04,0x02,0xa0,0xf7,0xf8" + +# Dataset of the Thread network to migrate to +# (Only updates active timestamp and panid, panid is set to 0x9999) +set dataset1 "0e080000000000020000000300001435060004001fffe002087d61eb42cdc48d6a0708fd0d07fca1b9f0500510ba088fc2bd6c3b3897f7a10f58263ff3030f4f70656e5468726561642d353234660102999904109dc023ccd447b12b50997ef68020f19e0c0402a0f7f8" +set dataset1_dbus "0x0e,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x14,0x35,0x06,0x00,0x04,0x00,0x1f,0xff,0xe0,0x02,0x08,0x7d,0x61,0xeb,0x42,0xcd,0xc4,0x8d,0x6a,0x07,0x08,0xfd,0x0d,0x07,0xfc,0xa1,0xb9,0xf0,0x50,0x05,0x10,0xba,0x08,0x8f,0xc2,0xbd,0x6c,0x3b,0x38,0x97,0xf7,0xa1,0x0f,0x58,0x26,0x3f,0xf3,0x03,0x0f,0x4f,0x70,0x65,0x6e,0x54,0x68,0x72,0x65,0x61,0x64,0x2d,0x35,0x32,0x34,0x66,0x01,0x02,0x99,0x99,0x04,0x10,0x9d,0xc0,0x23,0xcc,0xd4,0x47,0xb1,0x2b,0x50,0x99,0x7e,0xf6,0x80,0x20,0xf1,0x9e,0x0c,0x04,0x02,0xa0,0xf7,0xf8" + +# Step 1. Start otbr-agent with a NCP and join the network by dbus join method +spawn_node 1 otbr $::env(EXP_OT_NCP_PATH) +sleep 1 +spawn dbus-send --system --dest=io.openthread.BorderRouter.wpan0 --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 io.openthread.BorderRouter.Join "array:byte:${dataset_dbus}" +expect eof + +# Step 2. Wait 10 seconds, check if the otbr-agent has attached successfully +sleep 10 +spawn dbus-send --system --dest=io.openthread.BorderRouter.wpan0 --print-reply --reply-timeout=1000 /io/openthread/BorderRouter/wpan0 org.freedesktop.DBus.Properties.Get string:io.openthread.BorderRouter string:DeviceRole +expect -re {leader} { +} timeout { + puts "timeout!" + exit 1 +} +expect eof + +# Step 3. Start a Thread node and create a Thread network +spawn_node 2 cli $::env(EXP_OT_CLI_PATH) + +send "dataset set active ${dataset}\n" +expect_line "Done" +send "mode rn\n" +expect_line "Done" +send "ifconfig up\n" +expect_line "Done" +send "thread start\n" +expect_line "Done" +wait_for "state" "child" +expect_line "Done" + +# Step 4. Call ScheduleMigration method to migrate to another Thread network after 30s +spawn dbus-send --system --dest=io.openthread.BorderRouter.wpan0 --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 io.openthread.BorderRouter.ScheduleMigration "array:byte:${dataset1_dbus}" "uint32:0x7530" +expect eof + +# Step 5. Wait 31 seconds, check if the otbr-agent has migrated successfully by checking child's panid +sleep 31 +switch_node 2 +send "panid\n" +expect_line "0x9999" +expect_line "Done" diff --git a/tests/scripts/expect/ncp_test_schedule_migration_dbus_api.exp b/tests/scripts/expect/ncp_test_schedule_migration_dbus_api.exp index 00c4270bd7b..94909534d8e 100755 --- a/tests/scripts/expect/ncp_test_schedule_migration_dbus_api.exp +++ b/tests/scripts/expect/ncp_test_schedule_migration_dbus_api.exp @@ -39,7 +39,7 @@ spawn_node 1 otbr $::env(EXP_OT_NCP_PATH) sleep 1 spawn dbus-send --system --dest=io.openthread.BorderRouter.wpan0 --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 io.openthread.BorderRouter.ScheduleMigration "array:byte:${dataset_valid}" "uint32:0x7530" -expect Error.NotImplemented +expect Error.InvalidState expect eof spawn dbus-send --system --dest=io.openthread.BorderRouter.wpan0 --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 io.openthread.BorderRouter.ScheduleMigration "array:byte:${dataset_has_pending_timestamp}" "uint32:0x7530" diff --git a/tests/scripts/ncp_mode b/tests/scripts/ncp_mode index d0e5b874132..8b3f0b2bc17 100755 --- a/tests/scripts/ncp_mode +++ b/tests/scripts/ncp_mode @@ -257,6 +257,7 @@ main() otbr_exec_expect_script "${EXPECT_SCRIPT_DIR}/ncp_get_device_role.exp" || die "ncp expect script failed!" otbr_exec_expect_script "${EXPECT_SCRIPT_DIR}/ncp_join_leave.exp" || die "ncp expect script failed!" otbr_exec_expect_script "${EXPECT_SCRIPT_DIR}/ncp_test_schedule_migration_dbus_api.exp" || die "ncp expect script failed!" + otbr_exec_expect_script "${EXPECT_SCRIPT_DIR}/ncp_schedule_migration.exp" || die "ncp expect script failed!" fi }