diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 242a97024..43af9710d 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (175) +#define OPENTHREAD_API_VERSION (176) /** * @addtogroup api-instance diff --git a/include/openthread/link.h b/include/openthread/link.h index 228913533..1a6b0a84b 100644 --- a/include/openthread/link.h +++ b/include/openthread/link.h @@ -864,6 +864,33 @@ void otLinkFilterClearAllRssIn(otInstance *aInstance); */ otError otLinkFilterGetNextRssIn(otInstance *aInstance, otMacFilterIterator *aIterator, otMacFilterEntry *aEntry); +/** + * This function enables/disables IEEE 802.15.4 radio filter mode. + * + * This function is available when OPENTHREAD_CONFIG_MAC_FILTER_ENABLE configuration is enabled. + * + * The radio filter is mainly intended for testing. It can be used to temporarily block all tx/rx on the 802.15.4 radio. + * When radio filter is enabled, radio is put to sleep instead of receive (to ensure device does not receive any frame + * and/or potentially send ack). Also the frame transmission requests return immediately without sending the frame over + * the air (return "no ack" error if ack is requested, otherwise return success). + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aFilterEnabled TRUE to enable radio filter, FALSE to disable + * + */ +void otLinkSetRadioFilterEnabled(otInstance *aInstance, bool aFilterEnabled); + +/** + * This function indicates whether the IEEE 802.15.4 radio filter is enabled or not. + * + * This function is available when OPENTHREAD_CONFIG_MAC_FILTER_ENABLE configuration is enabled. + * + * @retval TRUE If the radio filter is enabled. + * @retval FALSE If the radio filter is disabled. + * + */ +bool otLinkIsRadioFilterEnabled(otInstance *aInstance); + /** * This method converts received signal strength to link quality. * diff --git a/src/cli/README.md b/src/cli/README.md index 707cf74ec..c76aadae4 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -89,6 +89,7 @@ Done - [prefix](#prefix) - [promiscuous](#promiscuous) - [pskc](#pskc--p-keypassphrase) +- [radiofilter](#radiofilter) - [rcp](#rcp) - [region](#region) - [releaserouterid](#releaserouterid-routerid) @@ -2183,6 +2184,44 @@ Disable radio promiscuous operation. Done ``` +### radiofilter + +`OPENTHREAD_CONFIG_MAC_FILTER_ENABLE` is required. + +The radio filter is mainly intended for testing. It can be used to temporarily block all tx/rx on the IEEE 802.15.4 radio. + +When radio filter is enabled, radio is put to sleep instead of receive (to ensure device does not receive any frame and/or potentially send ack). Also the frame transmission requests return immediately without sending the frame over the air (return "no ack" error if ack is requested, otherwise return success). + +Get radio filter status (enabled or disabled). + +```bash +> radiofilter +Disabled +Done +``` + +### radiofilter enable + +`OPENTHREAD_CONFIG_MAC_FILTER_ENABLE` is required. + +Enable radio radio filter. + +```bash +> radiofilter enable +Done +``` + +### radiofilter disable + +`OPENTHREAD_CONFIG_MAC_FILTER_ENABLE` is required. + +Disable radio radio filter. + +```bash +> radiofilter disable +Done +``` + ### rcp RCP-related commands. diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index d882d2209..0235d5bd0 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -3666,6 +3666,28 @@ otError Interpreter::ProcessPreferRouterId(Arg aArgs[]) } #endif +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE +otError Interpreter::ProcessRadioFilter(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + if (aArgs[0].IsEmpty()) + { + OutputEnabledDisabledStatus(otLinkIsRadioFilterEnabled(GetInstancePtr())); + } + else + { + bool enable; + + SuccessOrExit(error = ParseEnableOrDisable(aArgs[0], enable)); + otLinkSetRadioFilterEnabled(GetInstancePtr(), enable); + } + +exit: + return error; +} +#endif + otError Interpreter::ProcessRcp(Arg aArgs[]) { otError error = OT_ERROR_NONE; diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index cf6326ddb..365c13f6e 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -511,6 +511,7 @@ private: otError ProcessPskcRef(Arg aArgs[]); #endif #endif + otError ProcessRadioFilter(Arg aArgs[]); otError ProcessRcp(Arg aArgs[]); otError ProcessRegion(Arg aArgs[]); #if OPENTHREAD_FTD @@ -816,6 +817,9 @@ private: #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE {"pskcref", &Interpreter::ProcessPskcRef}, #endif +#endif +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + {"radiofilter", &Interpreter::ProcessRadioFilter}, #endif {"rcp", &Interpreter::ProcessRcp}, {"region", &Interpreter::ProcessRegion}, diff --git a/src/core/api/link_api.cpp b/src/core/api/link_api.cpp index a3270548a..a1d38be7d 100644 --- a/src/core/api/link_api.cpp +++ b/src/core/api/link_api.cpp @@ -266,6 +266,20 @@ otError otLinkFilterGetNextRssIn(otInstance *aInstance, otMacFilterIterator *aIt return AsCoreType(aInstance).Get().GetNextRssIn(*aIterator, *aEntry); } +#if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE +void otLinkSetRadioFilterEnabled(otInstance *aInstance, bool aFilterEnabled) +{ + return AsCoreType(aInstance).Get().SetRadioFilterEnabled(aFilterEnabled); +} + +bool otLinkIsRadioFilterEnabled(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsRadioFilterEnabled(); +} +#endif + +#endif // OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + uint8_t otLinkConvertRssToLinkQuality(otInstance *aInstance, int8_t aRss) { return LinkQualityInfo::ConvertRssToLinkQuality(AsCoreType(aInstance).Get().GetNoiseFloor(), aRss); @@ -277,8 +291,6 @@ int8_t otLinkConvertLinkQualityToRss(otInstance *aInstance, uint8_t aLinkQuality aLinkQuality); } -#endif // OPENTHREAD_CONFIG_MAC_FILTER_ENABLE - #if OPENTHREAD_CONFIG_MAC_RETRY_SUCCESS_HISTOGRAM_ENABLE const uint32_t *otLinkGetTxDirectRetrySuccessHistogram(otInstance *aInstance, uint8_t *aNumberOfEntries) { diff --git a/src/core/mac/mac.cpp b/src/core/mac/mac.cpp index b203f9ef6..a0f20073c 100644 --- a/src/core/mac/mac.cpp +++ b/src/core/mac/mac.cpp @@ -2481,5 +2481,13 @@ exit: } #endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE && OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE +void Mac::SetRadioFilterEnabled(bool aFilterEnabled) +{ + mLinks.GetSubMac().SetRadioFilterEnabled(aFilterEnabled); + UpdateIdleMode(); +} +#endif + } // namespace Mac } // namespace ot diff --git a/src/core/mac/mac.hpp b/src/core/mac/mac.hpp index 7746909fa..d9262b1fb 100644 --- a/src/core/mac/mac.hpp +++ b/src/core/mac/mac.hpp @@ -752,6 +752,29 @@ public: } #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE && OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE + /** + * This method enables/disables the 802.15.4 radio filter. + * + * When radio filter is enabled, radio is put to sleep instead of receive (to ensure device does not receive any + * frame and/or potentially send ack). Also the frame transmission requests return immediately without sending the + * frame over the air (return "no ack" error if ack is requested, otherwise return success). + * + * @param[in] aFilterEnabled TRUE to enable radio filter, FALSE to disable. + * + */ + void SetRadioFilterEnabled(bool aFilterEnabled); + + /** + * This method indicates whether the 802.15.4 radio filter is enabled or not. + * + * @retval TRUE If the radio filter is enabled. + * @retval FALSE If the radio filter is disabled. + * + */ + bool IsRadioFilterEnabled(void) const { return mLinks.GetSubMac().IsRadioFilterEnabled(); } +#endif + private: static constexpr int8_t kInvalidRssiValue = SubMac::kInvalidRssiValue; static constexpr uint16_t kMaxCcaSampleCount = OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW; diff --git a/src/core/mac/sub_mac.cpp b/src/core/mac/sub_mac.cpp index 85b90641c..220a8ca3a 100644 --- a/src/core/mac/sub_mac.cpp +++ b/src/core/mac/sub_mac.cpp @@ -77,6 +77,10 @@ void SubMac::Init(void) mEnergyScanMaxRssi = kInvalidRssiValue; mEnergyScanEndTime = Time{0}; +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + mRadioFilterEnabled = false; +#endif + mPrevKey.Clear(); mCurrKey.Clear(); mNextKey.Clear(); @@ -219,7 +223,18 @@ exit: Error SubMac::Receive(uint8_t aChannel) { - Error error = Get().Receive(aChannel); + Error error; + +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + if (mRadioFilterEnabled) + { + error = Get().Sleep(); + } + else +#endif + { + error = Get().Receive(aChannel); + } if (error != kErrorNone) { @@ -243,6 +258,10 @@ Error SubMac::CslSample(uint8_t aPanChannel) mCslChannel = aPanChannel; } +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + VerifyOrExit(!mRadioFilterEnabled, error = Get().Sleep()); +#endif + switch (mCslState) { case kCslSample: @@ -268,7 +287,7 @@ exit: } return error; } -#endif +#endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE void SubMac::HandleReceiveDone(RxFrame *aFrame, Error aError) { @@ -301,7 +320,12 @@ void SubMac::HandleReceiveDone(RxFrame *aFrame, Error aError) } #endif - mCallbacks.ReceiveDone(aFrame, aError); +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + if (!mRadioFilterEnabled) +#endif + { + mCallbacks.ReceiveDone(aFrame, aError); + } } Error SubMac::Send(void) @@ -328,6 +352,14 @@ Error SubMac::Send(void) break; } +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + if (mRadioFilterEnabled) + { + mCallbacks.TransmitDone(mTransmitFrame, nullptr, mTransmitFrame.GetAckRequest() ? kErrorNoAck : kErrorNone); + ExitNow(); + } +#endif + ProcessTransmitSecurity(); mCsmaBackoffs = 0; mTransmitRetries = 0; @@ -606,7 +638,20 @@ exit: int8_t SubMac::GetRssi(void) const { - return Get().GetRssi(); + int8_t rssi; + +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + if (mRadioFilterEnabled) + { + rssi = kInvalidRssiValue; + } + else +#endif + { + rssi = Get().GetRssi(); + } + + return rssi; } int8_t SubMac::GetNoiseFloor(void) @@ -637,6 +682,10 @@ Error SubMac::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration) break; } +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + VerifyOrExit(!mRadioFilterEnabled, HandleEnergyScanDone(kInvalidRssiValue)); +#endif + if (RadioSupportsEnergyScan()) { IgnoreError(Get().EnergyScan(aScanChannel, aScanDuration)); diff --git a/src/core/mac/sub_mac.hpp b/src/core/mac/sub_mac.hpp index 64b7a3aa3..20c93bcbd 100644 --- a/src/core/mac/sub_mac.hpp +++ b/src/core/mac/sub_mac.hpp @@ -548,6 +548,29 @@ public: */ void SetFrameCounter(uint32_t aFrameCounter); +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + /** + * This method enables/disables the radio filter. + * + * When radio filter is enabled, radio is put to sleep instead of receive (to ensure device does not receive any + * frame and/or potentially send ack). Also the frame transmission requests return immediately without sending the + * frame over the air (return "no ack" error if ack is requested, otherwise return success). + * + * @param[in] aFilterEnabled TRUE to enable radio filter, FALSE to disable. + * + */ + void SetRadioFilterEnabled(bool aFilterEnabled) { mRadioFilterEnabled = aFilterEnabled; } + + /** + * This method indicates whether the radio filter is enabled or not. + * + * @retval TRUE If the radio filter is enabled. + * @retval FALSE If the radio filter is disabled. + * + */ + bool IsRadioFilterEnabled(void) const { return mRadioFilterEnabled; } +#endif + private: #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE static void HandleCslTimer(Timer &aTimer); @@ -645,13 +668,16 @@ private: static const char *CslStateToString(CslState aCslState); #endif - otRadioCaps mRadioCaps; - State mState; - uint8_t mCsmaBackoffs; - uint8_t mTransmitRetries; - ShortAddress mShortAddress; - ExtAddress mExtAddress; - bool mRxOnWhenBackoff; + otRadioCaps mRadioCaps; + State mState; + uint8_t mCsmaBackoffs; + uint8_t mTransmitRetries; + ShortAddress mShortAddress; + ExtAddress mExtAddress; + bool mRxOnWhenBackoff : 1; +#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE + bool mRadioFilterEnabled : 1; +#endif int8_t mEnergyScanMaxRssi; TimeMilli mEnergyScanEndTime; TxFrame & mTransmitFrame; diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am index d570e6239..1b517e190 100644 --- a/tests/scripts/thread-cert/Makefile.am +++ b/tests/scripts/thread-cert/Makefile.am @@ -179,6 +179,7 @@ EXTRA_DIST = \ test_on_mesh_prefix.py \ test_pbbr_aloc.py \ test_ping.py \ + test_radio_filter.py \ test_reed_address_solicit_rejected.py \ test_reset.py \ test_route_table.py \ @@ -252,6 +253,7 @@ check_SCRIPTS = \ test_on_mesh_prefix.py \ test_pbbr_aloc.py \ test_ping.py \ + test_radio_filter.py \ test_reed_address_solicit_rejected.py \ test_reset.py \ test_route_table.py \ diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 62fb0a1c2..dabbc1e1f 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -839,6 +839,21 @@ class NodeImpl: self.send_command(cmd) self._expect_done() + def radiofilter_is_enabled(self) -> bool: + states = [r'Disabled', r'Enabled'] + self.send_command('radiofilter') + return self._expect_result(states) == 'Enabled' + + def radiofilter_enable(self): + cmd = 'radiofilter enable' + self.send_command(cmd) + self._expect_done() + + def radiofilter_disable(self): + cmd = 'radiofilter disable' + self.send_command(cmd) + self._expect_done() + def get_bbr_registration_jitter(self): self.send_command('bbr jitter') return int(self._expect_result(r'\d+')) @@ -2000,6 +2015,18 @@ class NodeImpl: }) return networks + def scan_energy(self, timeout=10): + self.send_command('scan energy') + self.simulator.go(timeout) + rssi_list = [] + for line in self._expect_command_output()[2:]: + _, channel, rssi, _ = line.split('|') + rssi_list.append({ + 'channel': int(channel.strip()), + 'rssi': int(rssi.strip()), + }) + return rssi_list + def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64, interface=None): args = f'{ipaddr} {size} {count} {interval} {hoplimit} {timeout}' if interface is not None: diff --git a/tests/scripts/thread-cert/test_radio_filter.py b/tests/scripts/thread-cert/test_radio_filter.py new file mode 100755 index 000000000..5103aa47f --- /dev/null +++ b/tests/scripts/thread-cert/test_radio_filter.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, 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. +# + +import ipaddress +import unittest + +import config +import command +import thread_cert + +# Test description: +# +# This test verifies the radio filter mechanism. +# +# Topology: +# +# Leader -- Router +# | +# | +# Child +# +# + +LEADER = 1 +ROUTER = 2 +SED = 3 + +WAIT_TIME = 5 + + +class RadioFilter(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + SUPPORT_NCP = False + + TOPOLOGY = { + LEADER: { + 'mode': 'rdn', + 'allowlist': [ROUTER, SED] + }, + ROUTER: { + 'mode': 'rdn', + 'allowlist': [LEADER] + }, + SED: { + 'is_mtd': True, + 'mode': '-', + 'timeout': config.DEFAULT_CHILD_TIMEOUT, + 'allowlist': [LEADER] + }, + } + + def test(self): + leader = self.nodes[LEADER] + router = self.nodes[ROUTER] + sed = self.nodes[SED] + + nodes = [leader, router, sed] + + leader.start() + self.simulator.go(WAIT_TIME) + self.assertEqual(leader.get_state(), 'leader') + + router.start() + self.simulator.go(WAIT_TIME) + self.assertEqual(router.get_state(), 'router') + + sed.start() + sed.set_pollperiod(40) + self.simulator.go(WAIT_TIME) + self.assertEqual(sed.get_state(), 'child') + + # Validate initial state of `radiofilter` upon start + for node in nodes: + self.assertFalse(node.radiofilter_is_enabled()) + + leader_mleid = leader.get_mleid() + router_mleid = router.get_mleid() + sed_mleid = sed.get_mleid() + + # Validate connections by pinging from leader, SED, and router + self.assertTrue(leader.ping(router_mleid)) + self.assertTrue(sed.ping(leader_mleid)) + self.assertTrue(router.ping(leader_mleid)) + + # Validate behavior when `radiofilter` is enabled on router + router.radiofilter_enable() + self.assertTrue(router.radiofilter_is_enabled()) + self.assertFalse(leader.ping(router_mleid)) + self.assertFalse(router.ping(leader_mleid)) + + # Validate behavior when `radiofilter` is disabled on router + router.radiofilter_disable() + self.assertFalse(router.radiofilter_is_enabled()) + self.assertTrue(leader.ping(router_mleid)) + self.assertTrue(router.ping(leader_mleid)) + + # Validate `radiofilter` behavior on sed. + self.assertTrue(sed.ping(leader_mleid)) + + sed.radiofilter_enable() + self.assertTrue(sed.radiofilter_is_enabled()) + self.assertFalse(sed.ping(leader_mleid)) + + self.assertTrue(sed.radiofilter_is_enabled()) + self.assertEqual(sed.get_state(), 'detached') + + sed.radiofilter_disable() + self.assertFalse(sed.radiofilter_is_enabled()) + + self.simulator.go(WAIT_TIME) + self.assertEqual(sed.get_state(), 'child') + self.assertTrue(leader.ping(sed_mleid)) + self.assertTrue(sed.ping(leader_mleid)) + + # Validate energy scan when `radiofilter` is enabled + scan_result = leader.scan_energy() + self.assertTrue(len(scan_result) > 0) + leader.radiofilter_enable() + self.assertTrue(leader.radiofilter_is_enabled()) + scan_result = leader.scan_energy() + self.assertTrue(len(scan_result) == 0) + + +if __name__ == '__main__': + unittest.main()