Files
openthread/tests/gtest/radio_spinel_rcp_test.cpp
Sven Kirmess 2a8b4a57ed [spinel] make max power table restoration work and survive old RCPs (#13524)
`RestoreProperties()` re-applies the max power table behind `#if
OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE`, but
`radio_spinel.cpp` never sees that macro. It is a POSIX macro, and
this is a platform-agnostic library whose only project config hook is
OPENTHREAD_PROJECT_LIB_CONFIG_FILE, from `OT_LIB_CONFIG`, which is not
set anywhere. `-DOT_POSIX_MAX_POWER_TABLE=ON` puts the definition on
`ot-posix-config`, which only `openthread-posix` links.

So the block is compiled out everywhere, including in
`script/cmake-build posix` and `script/check-posix-pty`, which passes
`max-power-table` in its radio URL.  The values are applied at startup
and silently lost on the next RCP reset.

Dropping the guard exposes a second problem. An RCP without
`SPINEL_PROP_PHY_CHAN_MAX_POWER` answers
`SPINEL_STATUS_PROP_NOT_FOUND`, which `spinel_status_to_ot_error()`
maps to `OT_ERROR_NOT_IMPLEMENTED` -- not the `OT_ERROR_NOT_FOUND` the
code tolerates. Every RCP reset would then be fatal.  The loop now
warns once, clears `mMaxPowerTableSet` and stops, so a refusing RCP is
not asked again on later recoveries either.

That flag also guards the loop, following the `mTransmitPowerSet` and
`mCcaEnergyDetectThresholdSet` flags beside it, and is set only after
`Set()` succeeds. The loop calls `Set()` directly rather than
`SetChannelMaxTransmitPower()`, as the other restores here do: the
channel range was already checked when the value was configured, and
writing the value back into the table it was just read from would be
circular now that the setter also raises the flag. `MaxPowerTable`
initialises every channel to `kPowerDefault`, so without it the loop
would issue 16 blocking transactions on every restore for everyone,
including those who never pass `max-power-table`. Comparing against
`kPowerDefault` instead would have been cheaper but not equivalent: it
cannot tell an unconfigured channel from one deliberately set to 30
dBm.

With the macro no longer used outside the POSIX platform, enabling it by
default there makes `max-power-table` reachable, like `cca-threshold` and
`fem-lnagain` already are.

Three gtest cases cover it. The fake platform has to implement
`otPlatRadioSetChannelMaxTransmitPower` for them: the weak default in
`radio_platform.cpp` reports `kErrorNotImplemented`, so without an
implementation a test cannot tell a correct restore from one that
never arrived.

  shouldRestoreEachChannelWithItsOwnPower       three channels, distinct
                                                powers, asserted per channel
                                                after a restore
  shouldNotTouchTheRcpWhenNoChannelWasConfigured  pins the flag
  shouldSurviveAnRcpThatDoesNotImplementIt      refusing RCP, must not abort
                                                and must not retry

The first and third fail without the corresponding fix; the second
passes either way, because the removed guard takes the same loop with
it.

Also measured against a simulated RCP behind a socat PTY, with an RCP
reset forced by freezing the RCP process, using
`SPINEL_PROP_MAC_15_4_PANID` as the control for whether a restore ran
at all:

  config                                     startup  control  restored  alive
  before                                          16      10x         0    yes
  after                                           16      10x        80    yes
  after, no max-power-table in the URL             0      10x         0    yes
  old error handling, block on, old RCP            0       6x         0     NO
  after,                        old RCP            0      10x         0    yes
  old error handling, block on, capable RCP       16      10x        80    yes

Built and tested with OT_RCP_RESTORATION_MAX_COUNT at both 0 and 2.
2026-08-26 13:50:54 -07:00

611 lines
23 KiB
C++

/*
* 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.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <openthread/platform/radio.h>
#include "common/error.hpp"
#include "lib/spinel/openthread-spinel-config.h"
#include "mac/mac_frame.hpp"
#include "mac/mac_types.hpp"
#include "fake_coprocessor_platform.hpp"
#include "fake_platform.hpp"
using namespace ot;
using ::testing::AnyNumber;
using ::testing::Truly;
TEST(RadioSpinelTransmit, shouldPassDesiredTxPowerToRadioPlatform)
{
class MockPlatform : public FakeCoprocessorPlatform
{
public:
MOCK_METHOD(otError, Transmit, (otRadioFrame * aFrame), (override));
};
MockPlatform platform;
constexpr Mac::PanId kSrcPanId = 0x1234;
constexpr Mac::PanId kDstPanId = 0x4321;
constexpr uint8_t kDstAddr[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr uint16_t kSrcAddr = 0xac00;
constexpr int8_t kTxPower = 100;
uint8_t frameBuffer[OT_RADIO_FRAME_MAX_SIZE];
Mac::TxFrame txFrame{};
txFrame.mPsdu = frameBuffer;
{
Mac::TxFrame::BuildInfo buildInfo;
buildInfo.mType = Mac::Frame::kTypeData;
buildInfo.mVersion = Mac::Frame::kVersion2006;
buildInfo.mAddrs.mSource.SetShort(kSrcAddr);
buildInfo.mAddrs.mDestination.SetExtended(kDstAddr);
buildInfo.mPanIds.SetSource(kSrcPanId);
buildInfo.mPanIds.SetDestination(kDstPanId);
buildInfo.mSecurityLevel = Mac::Frame::kSecurityEncMic32;
txFrame.PrepareHeadersWithEmptyPayload(buildInfo);
}
txFrame.mInfo.mTxInfo.mTxPower = kTxPower;
txFrame.mChannel = 11;
EXPECT_CALL(platform, Transmit(Truly([](otRadioFrame *aFrame) -> bool {
Mac::Frame &frame = *static_cast<Mac::Frame *>(aFrame);
return frame.mInfo.mTxInfo.mTxPower == kTxPower;
})))
.Times(1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Transmit(txFrame), kErrorNone);
platform.GoInMs(1000);
}
TEST(RadioSpinelTransmit, shouldCauseSwitchingToRxChannelAfterTxDone)
{
FakeCoprocessorPlatform platform;
constexpr Mac::PanId kSrcPanId = 0x1234;
constexpr Mac::PanId kDstPanId = 0x4321;
constexpr uint8_t kDstAddr[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr uint16_t kSrcAddr = 0xac00;
constexpr int8_t kTxPower = 100;
uint8_t frameBuffer[OT_RADIO_FRAME_MAX_SIZE];
Mac::TxFrame txFrame;
txFrame.mPsdu = frameBuffer;
{
Mac::TxFrame::BuildInfo buildInfo;
buildInfo.mType = Mac::Frame::kTypeData;
buildInfo.mVersion = Mac::Frame::kVersion2006;
buildInfo.mAddrs.mSource.SetShort(kSrcAddr);
buildInfo.mAddrs.mDestination.SetExtended(kDstAddr);
buildInfo.mPanIds.SetSource(kSrcPanId);
buildInfo.mPanIds.SetDestination(kDstPanId);
buildInfo.mSecurityLevel = Mac::Frame::kSecurityEncMic32;
txFrame.PrepareHeadersWithEmptyPayload(buildInfo);
}
txFrame.mInfo.mTxInfo.mTxPower = kTxPower;
txFrame.mChannel = 11;
txFrame.mInfo.mTxInfo.mRxChannelAfterTxDone = 25;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Transmit(txFrame), kErrorNone);
platform.GoInMs(1000);
EXPECT_EQ(platform.GetReceiveChannel(), 25);
}
TEST(RadioSpinelTransmit, shouldSkipCsmaCaWhenDisabled)
{
class MockPlatform : public FakeCoprocessorPlatform
{
public:
MOCK_METHOD(otError, Transmit, (otRadioFrame * aFrame), (override));
MOCK_METHOD(otError, Receive, (uint8_t aChannel), (override));
};
MockPlatform platform;
constexpr Mac::PanId kSrcPanId = 0x1234;
constexpr Mac::PanId kDstPanId = 0x4321;
constexpr uint8_t kDstAddr[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr uint16_t kSrcAddr = 0xac00;
constexpr int8_t kTxPower = 100;
uint8_t frameBuffer[OT_RADIO_FRAME_MAX_SIZE];
Mac::TxFrame txFrame{};
txFrame.mPsdu = frameBuffer;
{
Mac::TxFrame::BuildInfo buildInfo;
buildInfo.mType = Mac::Frame::kTypeData;
buildInfo.mVersion = Mac::Frame::kVersion2006;
buildInfo.mAddrs.mSource.SetShort(kSrcAddr);
buildInfo.mAddrs.mDestination.SetExtended(kDstAddr);
buildInfo.mPanIds.SetSource(kSrcPanId);
buildInfo.mPanIds.SetDestination(kDstPanId);
buildInfo.mSecurityLevel = Mac::Frame::kSecurityEncMic32;
txFrame.PrepareHeadersWithEmptyPayload(buildInfo);
}
txFrame.mInfo.mTxInfo.mCsmaCaEnabled = false;
txFrame.mChannel = 11;
EXPECT_CALL(platform, Transmit(Truly([](otRadioFrame *aFrame) -> bool {
Mac::Frame &frame = *static_cast<Mac::Frame *>(aFrame);
return frame.mInfo.mTxInfo.mCsmaCaEnabled == false;
})))
.Times(1);
EXPECT_CALL(platform, Receive).Times(AnyNumber());
// Receive(11) will be called exactly once to prepare for TX because the fake platform doesn't support sleep-to-tx
// capability.
EXPECT_CALL(platform, Receive(11)).Times(1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Transmit(txFrame), kErrorNone);
platform.GoInMs(1000);
}
TEST(RadioSpinelTransmit, shouldPerformCsmaCaWhenEnabled)
{
class MockPlatform : public FakeCoprocessorPlatform
{
public:
MOCK_METHOD(otError, Transmit, (otRadioFrame * aFrame), (override));
MOCK_METHOD(otError, Receive, (uint8_t aChannel), (override));
};
MockPlatform platform;
constexpr Mac::PanId kSrcPanId = 0x1234;
constexpr Mac::PanId kDstPanId = 0x4321;
constexpr uint8_t kDstAddr[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr uint16_t kSrcAddr = 0xac00;
constexpr int8_t kTxPower = 100;
uint8_t frameBuffer[OT_RADIO_FRAME_MAX_SIZE];
Mac::TxFrame txFrame{};
txFrame.mPsdu = frameBuffer;
{
Mac::TxFrame::BuildInfo buildInfo;
buildInfo.mType = Mac::Frame::kTypeData;
buildInfo.mVersion = Mac::Frame::kVersion2006;
buildInfo.mAddrs.mSource.SetShort(kSrcAddr);
buildInfo.mAddrs.mDestination.SetExtended(kDstAddr);
buildInfo.mPanIds.SetSource(kSrcPanId);
buildInfo.mPanIds.SetDestination(kDstPanId);
buildInfo.mSecurityLevel = Mac::Frame::kSecurityEncMic32;
txFrame.PrepareHeadersWithEmptyPayload(buildInfo);
}
txFrame.mInfo.mTxInfo.mCsmaCaEnabled = true;
txFrame.mInfo.mTxInfo.mMaxCsmaBackoffs = 1;
txFrame.mChannel = 11;
EXPECT_CALL(platform, Transmit(Truly([](otRadioFrame *aFrame) -> bool {
Mac::Frame &frame = *static_cast<Mac::Frame *>(aFrame);
return frame.mInfo.mTxInfo.mCsmaCaEnabled == true;
})))
.Times(1);
// Receive(11) will be called exactly twice:
// 1. one time to prepare for TX because the fake platform doesn't support sleep-to-tx capability.
// 2. one time in CSMA backoff because rx-on-when-idle is true.
EXPECT_CALL(platform, Receive(11)).Times(2);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Transmit(txFrame), kErrorNone);
platform.GoInMs(1000);
}
TEST(RadioSpinelTransmit, shouldNotCauseSwitchingToRxAfterTxDoneIfNotRxOnWhenIdle)
{
FakeCoprocessorPlatform platform;
constexpr Mac::PanId kSrcPanId = 0x1234;
constexpr Mac::PanId kDstPanId = 0x4321;
constexpr uint8_t kDstAddr[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr uint16_t kSrcAddr = 0xac00;
constexpr int8_t kTxPower = 100;
uint8_t frameBuffer[OT_RADIO_FRAME_MAX_SIZE];
Mac::TxFrame txFrame;
txFrame.mPsdu = frameBuffer;
{
Mac::TxFrame::BuildInfo buildInfo;
buildInfo.mType = Mac::Frame::kTypeData;
buildInfo.mVersion = Mac::Frame::kVersion2006;
buildInfo.mAddrs.mSource.SetShort(kSrcAddr);
buildInfo.mAddrs.mDestination.SetExtended(kDstAddr);
buildInfo.mPanIds.SetSource(kSrcPanId);
buildInfo.mPanIds.SetDestination(kDstPanId);
buildInfo.mSecurityLevel = Mac::Frame::kSecurityEncMic32;
txFrame.PrepareHeadersWithEmptyPayload(buildInfo);
}
txFrame.mInfo.mTxInfo.mTxPower = kTxPower;
txFrame.mChannel = 11;
txFrame.mInfo.mTxInfo.mRxChannelAfterTxDone = 25;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Receive(11), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetRxOnWhenIdle(false), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Transmit(txFrame), kErrorNone);
platform.GoInMs(1000);
EXPECT_EQ(platform.GetReceiveChannel(), 11);
}
TEST(RadioSpinelReceiveAt, shouldReceiveAtGiveRadioTime)
{
class MockPlatform : public FakeCoprocessorPlatform
{
public:
MOCK_METHOD(otError, ReceiveAt, (uint8_t aChannel, uint32_t aStart, uint32_t aDuration), (override));
};
MockPlatform platform;
ON_CALL(platform, ReceiveAt)
.WillByDefault([&platform](uint8_t aChannel, uint32_t aStart, uint32_t aDuration) -> otError {
return platform.FakePlatform::ReceiveAt(aChannel, aStart, aDuration);
});
EXPECT_CALL(platform, ReceiveAt).Times(1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetRxOnWhenIdle(false), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.ReceiveAt(100000, 10000, 11), kErrorNone);
platform.GoInUs(100000);
EXPECT_EQ(platform.GetReceiveChannel(), 0);
platform.GoInUs(1);
EXPECT_EQ(platform.GetReceiveChannel(), 11);
platform.GoInUs(10000);
EXPECT_EQ(platform.GetReceiveChannel(), 0);
}
TEST(RadioSpinelTransmit, shouldSkipCsmaBackoffWhenCsmaCaIsEnabledAndMaxBackoffsIsZero)
{
class MockPlatform : public FakeCoprocessorPlatform
{
public:
MOCK_METHOD(otError, Transmit, (otRadioFrame * aFrame), (override));
MOCK_METHOD(otError, Receive, (uint8_t aChannel), (override));
};
MockPlatform platform;
constexpr Mac::PanId kSrcPanId = 0x1234;
constexpr Mac::PanId kDstPanId = 0x4321;
constexpr uint8_t kDstAddr[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr uint16_t kSrcAddr = 0xac00;
constexpr int8_t kTxPower = 100;
uint8_t frameBuffer[OT_RADIO_FRAME_MAX_SIZE];
Mac::TxFrame txFrame{};
txFrame.mPsdu = frameBuffer;
{
Mac::TxFrame::BuildInfo buildInfo;
buildInfo.mType = Mac::Frame::kTypeData;
buildInfo.mVersion = Mac::Frame::kVersion2006;
buildInfo.mAddrs.mSource.SetShort(kSrcAddr);
buildInfo.mAddrs.mDestination.SetExtended(kDstAddr);
buildInfo.mPanIds.SetSource(kSrcPanId);
buildInfo.mPanIds.SetDestination(kDstPanId);
buildInfo.mSecurityLevel = Mac::Frame::kSecurityEncMic32;
txFrame.PrepareHeadersWithEmptyPayload(buildInfo);
}
txFrame.mInfo.mTxInfo.mCsmaCaEnabled = true;
txFrame.mInfo.mTxInfo.mMaxCsmaBackoffs = 0;
txFrame.mChannel = 11;
EXPECT_CALL(platform, Transmit(Truly([](otRadioFrame *aFrame) -> bool {
Mac::Frame &frame = *static_cast<Mac::Frame *>(aFrame);
return frame.mInfo.mTxInfo.mCsmaCaEnabled == true && frame.mInfo.mTxInfo.mMaxCsmaBackoffs == 0;
})))
.Times(1);
EXPECT_CALL(platform, Receive).Times(AnyNumber());
// Receive(11) will be called exactly once to prepare for TX because the fake platform doesn't support sleep-to-tx
// capability.
EXPECT_CALL(platform, Receive(11)).Times(1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.Transmit(txFrame), kErrorNone);
platform.GoInMs(1000);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToEnableRadioSrcMatch)
{
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(false);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.EnableSrcMatch(true), kErrorNone);
ASSERT_EQ(platform.SrcMatchIsEnabled(), true);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToDisableRadioSrcMatch)
{
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.EnableSrcMatch(false), kErrorNone);
ASSERT_EQ(platform.SrcMatchIsEnabled(), false);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToAddRadioSrcMatchShortEntry)
{
constexpr uint16_t kTestShortAddr = 0x1234;
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 0);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.AddSrcMatchShortEntry(kTestShortAddr), kErrorNone);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 1);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToClearRadioSrcMatchShortEntry)
{
constexpr uint16_t kTestShortAddr = 0x1234;
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
platform.SrcMatchAddShortEntry(kTestShortAddr);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.ClearSrcMatchShortEntry(kTestShortAddr), kErrorNone);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 0);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToAddRadioSrcMatchExtEntry)
{
constexpr otExtAddress kTestExtAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr otExtAddress kTestExtAddrReversed{0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11};
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
platform.SrcMatchClearExtEntries();
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddr), 0);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 0);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.AddSrcMatchExtEntry(kTestExtAddr), kErrorNone);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddr), 0);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 1);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToClearRadioSrcMatchExtEntry)
{
constexpr otExtAddress kTestExtAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr otExtAddress kTestExtAddrReversed{0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11};
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
platform.SrcMatchAddExtEntry(kTestExtAddrReversed);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.ClearSrcMatchExtEntry(kTestExtAddr), kErrorNone);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 0);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToClearAllRadioSrcMatchShortEntres)
{
constexpr uint16_t kTestShortAddr = 0x1234;
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
platform.SrcMatchAddShortEntry(kTestShortAddr);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.ClearSrcMatchShortEntries(), kErrorNone);
ASSERT_EQ(platform.SrcMatchCountShortEntries(), 0);
}
TEST(RadioSpinelSrcMatch, shouldBeAbleToClearAllRadioSrcMatchExtEntres)
{
constexpr otExtAddress kTestExtAddrReversed{0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11};
FakeCoprocessorPlatform platform;
platform.SrcMatchEnable(true);
platform.SrcMatchAddExtEntry(kTestExtAddrReversed);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 1);
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.ClearSrcMatchExtEntries(), kErrorNone);
ASSERT_EQ(platform.SrcMatchCountExtEntries(), 0);
}
#if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
TEST(RadioSpinelMaxPowerTable, shouldRestoreEachChannelWithItsOwnPower)
{
constexpr uint8_t kChannelA = 11;
constexpr uint8_t kChannelB = 15;
constexpr uint8_t kChannelC = 26;
constexpr int8_t kPowerA = 5;
constexpr int8_t kPowerB = 14;
constexpr int8_t kPowerC = 20;
FakeCoprocessorPlatform platform;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetChannelMaxTransmitPower(kChannelA, kPowerA), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetChannelMaxTransmitPower(kChannelB, kPowerB), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetChannelMaxTransmitPower(kChannelC, kPowerC), kErrorNone);
ASSERT_EQ(platform.ChannelMaxTxPowerGet(kChannelA), kPowerA);
ASSERT_EQ(platform.ChannelMaxTxPowerGet(kChannelB), kPowerB);
ASSERT_EQ(platform.ChannelMaxTxPowerGet(kChannelC), kPowerC);
// Forget what the RCP was told, so what follows can only come from the
// restore itself.
platform.ChannelMaxTxPowerClear();
ASSERT_EQ(platform.ChannelMaxTxPowerCount(), 0u);
platform.mRadioSpinel.RestoreProperties();
// Each configured channel comes back with its own power, not with a
// neighbour's and not with the table default.
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kChannelA), kPowerA);
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kChannelB), kPowerB);
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kChannelC), kPowerC);
// Channels never configured keep the table default, which is what the
// restore sends for them. Copied into a local first: `kPowerDefault` is a
// `static constexpr` with no out-of-line definition, and EXPECT_EQ binds
// its arguments by reference, which would odr-use it.
constexpr int8_t kDefaultPower = MaxPowerTable::kPowerDefault;
EXPECT_EQ(platform.ChannelMaxTxPowerGet(12), kDefaultPower);
}
TEST(RadioSpinelMaxPowerTable, shouldNotTouchTheRcpWhenNoChannelWasConfigured)
{
FakeCoprocessorPlatform platform;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
platform.ChannelMaxTxPowerClear();
platform.mRadioSpinel.RestoreProperties();
// Nothing was configured, so the restore must not send the table's
// defaults to the RCP -- 16 blocking transactions on every recovery for a
// feature nobody asked for.
EXPECT_EQ(platform.ChannelMaxTxPowerCount(), 0u);
}
TEST(RadioSpinelMaxPowerTable, shouldSurviveAnRcpThatDoesNotImplementIt)
{
constexpr uint8_t kChannel = 11;
constexpr int8_t kPower = 5;
FakeCoprocessorPlatform platform;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetChannelMaxTransmitPower(kChannel, kPower), kErrorNone);
// From here the RCP refuses the property. A firmware that lacks it answers
// `SPINEL_STATUS_PROP_NOT_FOUND` while this returns
// `SPINEL_STATUS_UNIMPLEMENTED`, but both reach the host as
// `OT_ERROR_NOT_IMPLEMENTED`, which is the path under test.
platform.ChannelMaxTxPowerFailWith(OT_ERROR_NOT_IMPLEMENTED);
platform.ChannelMaxTxPowerClear();
// Must not abort: this used to be a `DieNow()`, so a regression takes the
// whole test binary down rather than failing an assertion.
platform.mRadioSpinel.RestoreProperties();
EXPECT_EQ(platform.ChannelMaxTxPowerCount(), 0u);
// And it must give up rather than ask again on every later recovery, even
// once the RCP would accept it.
platform.ChannelMaxTxPowerFailWith(OT_ERROR_NONE);
platform.mRadioSpinel.RestoreProperties();
EXPECT_EQ(platform.ChannelMaxTxPowerCount(), 0u);
}
TEST(RadioSpinelSrcMatch, shouldNotDuplicateSrcMatchEntriesOnRestoreProperties)
{
constexpr uint16_t kTestShortAddr = 0x1234;
constexpr otExtAddress kTestExtAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
constexpr otExtAddress kTestExtAddrReversed{0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11};
FakeCoprocessorPlatform platform;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.AddSrcMatchShortEntry(kTestShortAddr), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.AddSrcMatchExtEntry(kTestExtAddr), kErrorNone);
ASSERT_EQ(platform.SrcMatchCountShortEntries(), 1);
ASSERT_EQ(platform.SrcMatchCountExtEntries(), 1);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 1);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 1);
// Simulate RCP recovery by calling RestoreProperties multiple times.
// Without clearing entries first, this would create duplicates.
platform.mRadioSpinel.RestoreProperties();
platform.mRadioSpinel.RestoreProperties();
ASSERT_EQ(platform.SrcMatchCountShortEntries(), 1);
ASSERT_EQ(platform.SrcMatchCountExtEntries(), 1);
ASSERT_EQ(platform.SrcMatchHasShortEntry(kTestShortAddr), 1);
ASSERT_EQ(platform.SrcMatchHasExtEntry(kTestExtAddrReversed), 1);
}
#endif // OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0