[radio] support max-power-table-mbm backed by SPINEL_PROP_PHY_CHAN_TARGET_POWER (#13790)

This commit adds the `max-power-table-mbm` radio URL parameter on POSIX to configure per-channel max transmit power in 0.01 dBm (mBm) units, while keeping the existing 1-dBm `max-power-table` parameter working.

- Add `OT_RADIO_POWER_INVALID_IN_MBM` (`INT16_MAX`) and bump `OPENTHREAD_API_VERSION`.

- Update `MaxPowerTable` and `RadioSpinel::SetChannelMaxTransmitPower()` to store and transmit per-channel max power in 0.01 dBm using `SPINEL_PROP_PHY_CHAN_TARGET_POWER`, with automatic fallback to `SPINEL_PROP_PHY_CHAN_MAX_POWER` when the RCP does not support `SPINEL_PROP_PHY_CHAN_TARGET_POWER`.

- Update POSIX `Radio::ProcessMaxPowerTable()` to parse `max-power-table-mbm` (in 0.01 dBm) and `max-power-table` (in 1 dBm).

- Add GTest and expect test coverage for `max-power-table-mbm` and legacy fallback.
This commit is contained in:
Yakun Xu
2026-10-30 17:50:58 -07:00
committed by GitHub
parent 4f9098421a
commit 9de1bba33c
11 changed files with 313 additions and 55 deletions
+29
View File
@@ -729,6 +729,24 @@ otError otPlatRadioGetTransmitPower(otInstance *aInstance, int8_t *aPower)
assert(aInstance != NULL);
#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
if (maxPower == OT_RADIO_POWER_INVALID)
{
int16_t targetPower = OT_RADIO_POWER_INVALID_IN_MBM;
int16_t actualPower = OT_RADIO_POWER_INVALID_IN_MBM;
uint8_t rawPowerSetting[1] = {0};
uint16_t rawPowerSettingLength = sizeof(rawPowerSetting);
otError error = otPlatDiagRadioGetPowerSettings(aInstance, sCurrentChannel, &targetPower, &actualPower,
rawPowerSetting, &rawPowerSettingLength);
OT_UNUSED_VARIABLE(error);
if (targetPower != OT_RADIO_POWER_INVALID_IN_MBM)
{
maxPower = (int8_t)(targetPower / 100 - ((targetPower < 0 && targetPower % 100 != 0) ? 1 : 0));
}
}
#endif
*aPower = sTxPower < maxPower ? sTxPower : maxPower;
return OT_ERROR_NONE;
@@ -962,6 +980,17 @@ exit:
return error;
}
#if !(OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE)
otError otPlatRadioSetChannelTargetPower(otInstance *aInstance, uint8_t aChannel, int16_t aTargetPower)
{
int16_t powerDbm = aTargetPower / 100 - ((aTargetPower < 0 && aTargetPower % 100 != 0) ? 1 : 0);
int8_t maxPower =
(aTargetPower == OT_RADIO_POWER_INVALID_IN_MBM) ? (int8_t)OT_RADIO_POWER_INVALID : (int8_t)powerDbm;
return otPlatRadioSetChannelMaxTransmitPower(aInstance, aChannel, maxPower);
}
#endif
#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
otError otPlatRadioConfigureEnhAckProbing(otInstance *aInstance,
otLinkMetrics aLinkMetrics,
+1 -1
View File
@@ -51,7 +51,7 @@ extern "C" {
*
* @note This number versions both OpenThread platform and user APIs.
*/
#define OPENTHREAD_API_VERSION (635)
#define OPENTHREAD_API_VERSION (636)
/**
* @addtogroup api-instance
+6 -5
View File
@@ -81,9 +81,10 @@ enum
OT_RADIO_SYMBOL_TIME = 1000000 * 1 / OT_RADIO_SYMBOL_RATE, ///< Symbol duration time in unit of microseconds
OT_RADIO_TEN_SYMBOLS_TIME = 10 * OT_RADIO_SYMBOL_TIME, ///< Time for 10 symbols in unit of microseconds
OT_RADIO_LQI_NONE = 0, ///< LQI measurement not supported
OT_RADIO_RSSI_INVALID = 127, ///< Invalid or unknown RSSI value
OT_RADIO_POWER_INVALID = 127, ///< Invalid or unknown power value
OT_RADIO_LQI_NONE = 0, ///< LQI measurement not supported
OT_RADIO_RSSI_INVALID = 127, ///< Invalid or unknown RSSI value
OT_RADIO_POWER_INVALID = 127, ///< Invalid or unknown power value in dBm
OT_RADIO_POWER_INVALID_IN_MBM = INT16_MAX, ///< Invalid or unknown power value in mBm (0.01 dBm)
OT_RADIO_INVALID_SHORT_ADDR = 0xfffe, ///< Invalid short address.
OT_RADIO_BROADCAST_SHORT_ADDR = 0xffff, ///< Broadcast short address.
@@ -1936,8 +1937,8 @@ otError otPlatRadioClearCalibratedPowers(otInstance *aInstance);
* as possible to the @p aTargetPower. If the @p aTargetPower is lower than the minimum output power supported
* by the platform, the output power should be set to the minimum output power supported by the platform. If the
* @p aTargetPower is higher than the maximum output power supported by the platform, the output power should be
* set to the maximum output power supported by the platform. If the @p aTargetPower is set to `INT16_MAX`, the
* corresponding channel is disabled.
* set to the maximum output power supported by the platform. If the @p aTargetPower is set to
* `OT_RADIO_POWER_INVALID_IN_MBM`, the corresponding channel is disabled.
*
* @param[in] aInstance The OpenThread instance structure.
* @param[in] aChannel The radio channel.
+15 -8
View File
@@ -36,26 +36,33 @@ namespace ot {
class MaxPowerTable
{
public:
static constexpr int8_t kPowerDefault = 30; ///< Default power 1 watt (30 dBm).
static constexpr int16_t kPowerDefault = 3000; ///< Default power 1 watt (30 dBm in 0.01 dBm).
static constexpr int16_t kPowerInvalid = OT_RADIO_POWER_INVALID_IN_MBM; ///< Invalid power.
MaxPowerTable(void) { memset(mPowerTable, kPowerDefault, sizeof(mPowerTable)); }
MaxPowerTable(void)
{
for (int16_t &power : mPowerTable)
{
power = kPowerDefault;
}
}
/**
* Gets the max allowed transmit power of channel @p aChannel.
*
* @param[in] aChannel The radio channel number.
*
* @returns The max supported transmit power in dBm.
* @returns The max supported transmit power in 0.01 dBm.
*/
int8_t GetTransmitPower(uint8_t aChannel) const { return mPowerTable[aChannel - Radio::kChannelMin]; }
int16_t GetTransmitPower(uint8_t aChannel) const { return mPowerTable[aChannel - Radio::kChannelMin]; }
/**
* Sets the max allowed transmit power of channel @p aChannel.
*
* @param[in] aChannel The radio channel number.
* @param[in] aPower The max supported transmit power in dBm.
* @param[in] aPower The max supported transmit power in 0.01 dBm.
*/
void SetTransmitPower(uint8_t aChannel, int8_t aPower) { mPowerTable[aChannel - Radio::kChannelMin] = aPower; }
void SetTransmitPower(uint8_t aChannel, int16_t aPower) { mPowerTable[aChannel - Radio::kChannelMin] = aPower; }
/**
* Gets the supported channel masks.
@@ -66,7 +73,7 @@ public:
for (uint8_t i = Radio::kChannelMin; i <= Radio::kChannelMax; ++i)
{
if (mPowerTable[i - Radio::kChannelMin] != OT_RADIO_POWER_INVALID)
if (mPowerTable[i - Radio::kChannelMin] != kPowerInvalid)
{
SetBit<uint32_t>(channelMask, i);
}
@@ -76,7 +83,7 @@ public:
}
private:
int8_t mPowerTable[Radio::kChannelMax - Radio::kChannelMin + 1];
int16_t mPowerTable[Radio::kChannelMax - Radio::kChannelMin + 1];
};
} // namespace ot
+6
View File
@@ -152,6 +152,12 @@ Error PowerCalibration::GetPowerSettings(uint8_t aChannel,
chIndex = aChannel - Radio::kChannelMin;
targetPower = mTargetPowerTable[chIndex];
VerifyOrExit(targetPower != kInvalidPower, error = kErrorNotFound);
if (aTargetPower != nullptr)
{
*aTargetPower = targetPower;
}
VerifyOrExit(mCalibratedPowerTables[chIndex].GetLength() > 0, error = kErrorNotFound);
for (uint8_t i = 0; i < mCalibratedPowerTables[chIndex].GetLength(); i++)
+63 -10
View File
@@ -44,6 +44,7 @@
#include "common/code_utils.hpp"
#include "common/new.hpp"
#include "common/num_utils.hpp"
#include "lib/platform/exit_code.h"
#include "lib/spinel/logger.hpp"
#include "lib/spinel/spinel_driver.hpp"
@@ -121,6 +122,7 @@ RadioSpinel::RadioSpinel(void)
#endif
, mTimeSyncEnabled(false)
, mTimeSyncOn(false)
, mSupportsTargetPower(true)
, mSpinelDriver(nullptr)
{
memset(&mCallbacks, 0, sizeof(mCallbacks));
@@ -2327,12 +2329,28 @@ void RadioSpinel::RestoreProperties(void)
{
for (uint8_t channel = Radio::kChannelMin; channel <= Radio::kChannelMax; channel++)
{
int8_t power = mMaxPowerTable.GetTransmitPower(channel);
int16_t power = mMaxPowerTable.GetTransmitPower(channel);
if (power != OT_RADIO_POWER_INVALID)
if (power != MaxPowerTable::kPowerInvalid)
{
otError error =
Set(SPINEL_PROP_PHY_CHAN_MAX_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT8_S, channel, power);
otError error = OT_ERROR_NONE;
if (mSupportsTargetPower)
{
error = Set(SPINEL_PROP_PHY_CHAN_TARGET_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT16_S,
channel, power);
if (error == OT_ERROR_NOT_IMPLEMENTED || error == OT_ERROR_NOT_FOUND)
{
mSupportsTargetPower = false;
}
}
if (!mSupportsTargetPower)
{
error = Set(SPINEL_PROP_PHY_CHAN_MAX_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT8_S, channel,
ConvertMbmToDbm(power));
}
// An RCP without `SPINEL_PROP_PHY_CHAN_MAX_POWER` answers with
// `SPINEL_STATUS_PROP_NOT_FOUND`, which maps to
@@ -2400,20 +2418,55 @@ exit:
return error;
}
otError RadioSpinel::SetChannelMaxTransmitPower(uint8_t aChannel, int8_t aMaxPower)
int8_t RadioSpinel::ConvertMbmToDbm(int16_t aPowerMbm)
{
int8_t powerDbm;
if (aPowerMbm == MaxPowerTable::kPowerInvalid)
{
powerDbm = static_cast<int8_t>(OT_RADIO_POWER_INVALID);
}
else
{
int16_t dbm = aPowerMbm / 100 - ((aPowerMbm < 0 && aPowerMbm % 100 != 0) ? 1 : 0);
powerDbm = static_cast<int8_t>(Clamp<int16_t>(dbm, INT8_MIN, OT_RADIO_POWER_INVALID - 1));
}
return powerDbm;
}
otError RadioSpinel::SetChannelMaxTransmitPower(uint8_t aChannel, int16_t aMaxPower)
{
otError error = OT_ERROR_NONE;
VerifyOrExit(Radio::IsChannelValid(aChannel), error = OT_ERROR_INVALID_ARGS);
mMaxPowerTable.SetTransmitPower(aChannel, aMaxPower);
SuccessOrExit(error = Set(SPINEL_PROP_PHY_CHAN_MAX_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT8_S, aChannel,
aMaxPower));
#if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
mMaxPowerTableSet = true;
#endif
if (mSupportsTargetPower)
{
error = Set(SPINEL_PROP_PHY_CHAN_TARGET_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT16_S, aChannel,
aMaxPower);
if (error != OT_ERROR_NOT_IMPLEMENTED && error != OT_ERROR_NOT_FOUND)
{
ExitNow();
}
mSupportsTargetPower = false;
}
error = Set(SPINEL_PROP_PHY_CHAN_MAX_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT8_S, aChannel,
ConvertMbmToDbm(aMaxPower));
exit:
#if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
if (error == OT_ERROR_NONE)
{
mMaxPowerTableSet = true;
}
#endif
return error;
}
+9 -4
View File
@@ -855,15 +855,16 @@ public:
const char *GetVersion(void) const { return mSpinelDriver->GetVersion(); }
/**
* Sets the max transmit power.
* Sets the max transmit power for a given channel.
*
* @param[in] aChannel The radio channel.
* @param[in] aMaxPower The max transmit power in dBm.
* @param[in] aMaxPower The max transmit power in 0.01 dBm. Passing `OT_RADIO_POWER_INVALID_IN_MBM` will disable
* this channel.
*
* @retval OT_ERROR_NONE Successfully set the max transmit power.
* @retval OT_ERROR_INVALID_ARGS Channel is not in valid range.
*/
otError SetChannelMaxTransmitPower(uint8_t aChannel, int8_t aMaxPower);
otError SetChannelMaxTransmitPower(uint8_t aChannel, int16_t aMaxPower);
/**
* Tries to retrieve a spinel property from OpenThread transceiver.
@@ -990,7 +991,8 @@ public:
* Set the target power for the given channel.
*
* @param[in] aChannel The radio channel.
* @param[in] aTargetPower The target power in 0.01dBm. Passing `INT16_MAX` will disable this channel.
* @param[in] aTargetPower The target power in 0.01dBm. Passing `OT_RADIO_POWER_INVALID_IN_MBM` will disable this
* channel.
*
* @retval OT_ERROR_NONE Successfully set the target power.
* @retval OT_ERROR_INVALID_ARGS The @p aChannel or @p aTargetPower is invalid..
@@ -1217,6 +1219,8 @@ private:
void HandleCompatibilityError(void);
static int8_t ConvertMbmToDbm(int16_t aPowerMbm);
typedef otRadioSpinelMetrics Metrics;
class MetricsTracker
@@ -1368,6 +1372,7 @@ private:
bool mTimeSyncEnabled : 1;
bool mTimeSyncOn : 1;
bool mSupportsTargetPower : 1;
SpinelDriver *mSpinelDriver;
};
+56 -12
View File
@@ -207,16 +207,28 @@ void Radio::ProcessMaxPowerTable(const RadioUrl &aRadioUrl)
OT_UNUSED_VARIABLE(aRadioUrl);
#if OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE
otError error;
constexpr int8_t kPowerDefault = 30; // Default power 1 watt (30 dBm).
char *str = nullptr;
char *pSave = nullptr;
uint8_t channel = ot::Radio::kChannelMin;
int8_t power = kPowerDefault;
const char *maxPowerTable = nullptr;
char *maxPowerTableCopy = nullptr;
otError error;
constexpr int16_t kPowerDefault = MaxPowerTable::kPowerDefault;
char *str = nullptr;
char *pSave = nullptr;
uint8_t channel = ot::Radio::kChannelMin;
int16_t power = kPowerDefault;
const char *maxPowerTable = nullptr;
char *maxPowerTableCopy = nullptr;
bool inMbm = false;
VerifyOrExit((maxPowerTable = aRadioUrl.GetValue("max-power-table")) != nullptr);
if ((maxPowerTable = aRadioUrl.GetValue("max-power-table-mbm")) != nullptr)
{
inMbm = true;
}
else if ((maxPowerTable = aRadioUrl.GetValue("max-power-table")) != nullptr)
{
inMbm = false;
}
else
{
ExitNow();
}
maxPowerTableCopy = strdup(maxPowerTable);
VerifyOrDie(maxPowerTableCopy != nullptr, OT_EXIT_FAILURE);
@@ -224,7 +236,36 @@ void Radio::ProcessMaxPowerTable(const RadioUrl &aRadioUrl)
for (str = strtok_r(maxPowerTableCopy, ",", &pSave); str != nullptr && channel <= ot::Radio::kChannelMax;
str = strtok_r(nullptr, ",", &pSave))
{
power = static_cast<int8_t>(strtol(str, nullptr, 0));
long val = strtol(str, nullptr, 0);
if (inMbm)
{
if (val >= OT_RADIO_POWER_INVALID_IN_MBM)
{
power = MaxPowerTable::kPowerInvalid;
}
else if (val < INT16_MIN)
{
power = INT16_MIN;
}
else
{
power = static_cast<int16_t>(val);
}
}
else if (val < INT8_MIN)
{
power = INT8_MIN * 100;
}
else if (val < OT_RADIO_POWER_INVALID)
{
power = static_cast<int16_t>(val * 100);
}
else
{
power = MaxPowerTable::kPowerInvalid;
}
error = mRadioSpinel.SetChannelMaxTransmitPower(channel, power);
VerifyOrDie((error == OT_ERROR_NONE) || (error == OT_ERROR_NOT_IMPLEMENTED), OT_EXIT_FAILURE);
if (error == OT_ERROR_NOT_IMPLEMENTED)
@@ -255,7 +296,6 @@ exit:
{
free(maxPowerTableCopy);
}
return;
#endif // OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE
}
@@ -1023,7 +1063,11 @@ uint8_t otPlatRadioGetCslUncertainty(otInstance *aInstance)
otError otPlatRadioSetChannelMaxTransmitPower(otInstance *aInstance, uint8_t aChannel, int8_t aMaxPower)
{
OT_UNUSED_VARIABLE(aInstance);
return GetRadioSpinel().SetChannelMaxTransmitPower(aChannel, aMaxPower);
int16_t maxPower = (aMaxPower == OT_RADIO_POWER_INVALID) ? ot::MaxPowerTable::kPowerInvalid
: static_cast<int16_t>(aMaxPower * 100);
return GetRadioSpinel().SetChannelMaxTransmitPower(aChannel, maxPower);
}
#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+5 -1
View File
@@ -108,7 +108,11 @@ const char *otSysGetRadioUrlHelpString(void)
" max-power-table Max power for channels in ascending order separated by commas,\n" \
" If the number of values is less than that of supported channels,\n" \
" the last value will be applied to all remaining channels.\n" \
" Special value 0x7f disables a channel.\n"
" Special value 0x7f disables a channel.\n" \
" max-power-table-mbm Max power (in 0.01 dBm) for channels in ascending order\n" \
" separated by commas. If the number of values is less than that\n" \
" of supported channels, the last value will be applied to all\n" \
" remaining channels. Special value 0x7fff disables a channel.\n"
// Only meaningful while max-power-table is compiled in; without it the note
// would point at a parameter the help does not list.
+107 -14
View File
@@ -503,9 +503,9 @@ 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;
constexpr int16_t kPowerA = 500;
constexpr int16_t kPowerB = 1400;
constexpr int16_t kPowerC = 2000;
FakeCoprocessorPlatform platform;
@@ -515,29 +515,84 @@ TEST(RadioSpinelMaxPowerTable, shouldRestoreEachChannelWithItsOwnPower)
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);
#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
{
const uint8_t kRawPowerSetting[] = {0x01};
int16_t targetPower = 0;
int16_t actualPower = 0;
uint8_t rawPowerSetting[sizeof(kRawPowerSetting)];
uint16_t rawPowerSettingLength;
for (uint8_t channel = Radio::kChannelMin; channel <= Radio::kChannelMax; channel++)
{
ASSERT_EQ(platform.mRadioSpinel.AddCalibratedPower(channel, 0, kRawPowerSetting, sizeof(kRawPowerSetting)),
kErrorNone);
}
// Forget what the RCP was told, so what follows can only come from the
// restore itself.
ASSERT_EQ(
otPlatRadioSetChannelTargetPower(FakePlatform::CurrentInstance(), kChannelA, OT_RADIO_POWER_INVALID_IN_MBM),
kErrorNone);
ASSERT_EQ(
otPlatRadioSetChannelTargetPower(FakePlatform::CurrentInstance(), kChannelB, OT_RADIO_POWER_INVALID_IN_MBM),
kErrorNone);
ASSERT_EQ(
otPlatRadioSetChannelTargetPower(FakePlatform::CurrentInstance(), kChannelC, OT_RADIO_POWER_INVALID_IN_MBM),
kErrorNone);
platform.mRadioSpinel.RestoreProperties();
rawPowerSettingLength = sizeof(rawPowerSetting);
ASSERT_EQ(otPlatDiagRadioGetPowerSettings(FakePlatform::CurrentInstance(), kChannelA, &targetPower,
&actualPower, rawPowerSetting, &rawPowerSettingLength),
kErrorNone);
EXPECT_EQ(targetPower, kPowerA);
rawPowerSettingLength = sizeof(rawPowerSetting);
ASSERT_EQ(otPlatDiagRadioGetPowerSettings(FakePlatform::CurrentInstance(), kChannelB, &targetPower,
&actualPower, rawPowerSetting, &rawPowerSettingLength),
kErrorNone);
EXPECT_EQ(targetPower, kPowerB);
rawPowerSettingLength = sizeof(rawPowerSetting);
ASSERT_EQ(otPlatDiagRadioGetPowerSettings(FakePlatform::CurrentInstance(), kChannelC, &targetPower,
&actualPower, rawPowerSetting, &rawPowerSettingLength),
kErrorNone);
EXPECT_EQ(targetPower, kPowerC);
rawPowerSettingLength = sizeof(rawPowerSetting);
ASSERT_EQ(otPlatDiagRadioGetPowerSettings(FakePlatform::CurrentInstance(), 12, &targetPower, &actualPower,
rawPowerSetting, &rawPowerSettingLength),
kErrorNone);
constexpr int16_t kDefaultPower = MaxPowerTable::kPowerDefault;
EXPECT_EQ(targetPower, kDefaultPower);
}
#else
ASSERT_EQ(platform.ChannelTargetPowerGet(kChannelA), kPowerA);
ASSERT_EQ(platform.ChannelTargetPowerGet(kChannelB), kPowerB);
ASSERT_EQ(platform.ChannelTargetPowerGet(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.ChannelTargetPowerClear();
ASSERT_EQ(platform.ChannelTargetPowerCount(), 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);
EXPECT_EQ(platform.ChannelTargetPowerGet(kChannelA), kPowerA);
EXPECT_EQ(platform.ChannelTargetPowerGet(kChannelB), kPowerB);
EXPECT_EQ(platform.ChannelTargetPowerGet(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);
constexpr int16_t kDefaultPower = MaxPowerTable::kPowerDefault;
EXPECT_EQ(platform.ChannelTargetPowerGet(12), kDefaultPower);
#endif
}
TEST(RadioSpinelMaxPowerTable, shouldNotTouchTheRcpWhenNoChannelWasConfigured)
@@ -547,18 +602,21 @@ TEST(RadioSpinelMaxPowerTable, shouldNotTouchTheRcpWhenNoChannelWasConfigured)
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
platform.ChannelMaxTxPowerClear();
platform.ChannelTargetPowerClear();
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);
EXPECT_EQ(platform.ChannelTargetPowerCount(), 0u);
}
#if !(OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE)
TEST(RadioSpinelMaxPowerTable, shouldSurviveAnRcpThatDoesNotImplementIt)
{
constexpr uint8_t kChannel = 11;
constexpr int8_t kPower = 5;
constexpr int16_t kPower = 500;
FakeCoprocessorPlatform platform;
@@ -570,22 +628,57 @@ TEST(RadioSpinelMaxPowerTable, shouldSurviveAnRcpThatDoesNotImplementIt)
// `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.ChannelTargetPowerFailWith(OT_ERROR_NOT_IMPLEMENTED);
platform.ChannelMaxTxPowerClear();
platform.ChannelTargetPowerClear();
// 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);
EXPECT_EQ(platform.ChannelTargetPowerCount(), 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.ChannelTargetPowerFailWith(OT_ERROR_NONE);
platform.mRadioSpinel.RestoreProperties();
EXPECT_EQ(platform.ChannelMaxTxPowerCount(), 0u);
EXPECT_EQ(platform.ChannelTargetPowerCount(), 0u);
}
TEST(RadioSpinelMaxPowerTable, shouldFallbackToLegacyMaxPowerWhenTargetPowerNotSupported)
{
constexpr uint8_t kChannel = 11;
constexpr uint8_t kNegChannel = 12;
constexpr int16_t kPower = 1234;
constexpr int16_t kNegPowerMbm = -550;
FakeCoprocessorPlatform platform;
ASSERT_EQ(platform.mRadioSpinel.Enable(FakePlatform::CurrentInstance()), kErrorNone);
platform.ChannelTargetPowerFailWith(OT_ERROR_NOT_IMPLEMENTED);
ASSERT_EQ(platform.mRadioSpinel.SetChannelMaxTransmitPower(kChannel, kPower), kErrorNone);
ASSERT_EQ(platform.mRadioSpinel.SetChannelMaxTransmitPower(kNegChannel, kNegPowerMbm), kErrorNone);
// Fallback sends SPINEL_PROP_PHY_CHAN_MAX_POWER in 1 dBm (1234 -> 12, -550 -> -6).
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kChannel), 12);
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kNegChannel), -6);
EXPECT_EQ(platform.ChannelTargetPowerCount(), 0u);
// Restore should also use legacy max power table.
platform.ChannelMaxTxPowerClear();
platform.mRadioSpinel.RestoreProperties();
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kChannel), 12);
EXPECT_EQ(platform.ChannelMaxTxPowerGet(kNegChannel), -6);
}
#endif
TEST(RadioSpinelSrcMatch, shouldNotDuplicateSrcMatchEntriesOnRestoreProperties)
{
constexpr uint16_t kTestShortAddr = 0x1234;
@@ -49,6 +49,22 @@ expect_line "Done"
send "\x04"
expect eof
# allows 11-25 and forbidden 26 (with mBm values)
spawn_node 1 "rcp" "spinel+hdlc+uart://$env(OT_SIMULATION_APPS)/ncp/ot-rcp?max-power-table-mbm=1100,1200,1300,1400,1500,1600,1700,1800,1900,2000,2100,2200,2300,2400,-100,0x7fff&forkpty-arg=1"
send "channel supported\n"
expect "0x3fff800"
expect_line "Done"
send "channel preferred\n"
expect "0x3fff800"
expect_line "Done"
send "txpower 20\n"
expect_line "Done"
send "txpower\n"
expect "11 dBm"
expect_line "Done"
send "\x04"
expect eof
# restore the configuration file
spawn mv src/posix/platform/openthread.conf.example.backup src/posix/platform/openthread.conf.example
expect eof