From df45240665b4fa187a3448a064b2d48a72f32263 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Mon, 16 Apr 2018 11:17:09 -0700 Subject: [PATCH 1/4] [channel-monitor] use "channel occupancy" instead of "quality" This commit changes the API names (and spinel properties and documentation) related to "channel monitor" feature to use "channel occupancy" instead of the term "channel quality" to represent the rate/percentage of RSSI samples which are higher than a threshold. --- .../spinel-feature-channel-monitor.md | 6 +-- include/openthread/channel_monitor.h | 17 +++---- src/core/api/channel_monitor_api.cpp | 4 +- src/core/utils/channel_monitor.cpp | 48 +++++++++---------- src/core/utils/channel_monitor.hpp | 28 +++++------ src/ncp/ncp_base.cpp | 2 +- src/ncp/ncp_base.hpp | 2 +- src/ncp/ncp_base_mtd.cpp | 4 +- src/ncp/spinel.c | 4 +- src/ncp/spinel.h | 8 ++-- 10 files changed, 62 insertions(+), 61 deletions(-) diff --git a/doc/spinel-protocol-src/spinel-feature-channel-monitor.md b/doc/spinel-protocol-src/spinel-feature-channel-monitor.md index 8125c307f..a9d7c1cdf 100644 --- a/doc/spinel-protocol-src/spinel-feature-channel-monitor.md +++ b/doc/spinel-protocol-src/spinel-feature-channel-monitor.md @@ -49,7 +49,7 @@ Total number of RSSI samples (per channel) taken by the channel monitoring module since its start (since Thread network interface was enabled). -### PROP 4618: SPINEL_PROP_CHANNEL_MONITOR_CHANNEL_QUALITY (#prop-channel-monitor-channel-quality) +### PROP 4618: SPINEL_PROP_CHANNEL_MONITOR_CHANNEL_OCCUPANCY (#prop-channel-monitor-channel-occupancy) * Type: Read-Only * Packing-Encoding: `A(t(cU))` @@ -57,9 +57,9 @@ was enabled). Data per item is: * `C`: Channel - * `U`: Channel quality indicator + * `U`: Channel occupancy indicator -The channel quality value represents the average rate/percentage of +The channel occupancy value represents the average rate/percentage of RSSI samples that were above RSSI threshold ("bad" RSSI samples) within (approximately) latest sample window RSSI samples. diff --git a/include/openthread/channel_monitor.h b/include/openthread/channel_monitor.h index 042e8b45a..669b8cf9d 100644 --- a/include/openthread/channel_monitor.h +++ b/include/openthread/channel_monitor.h @@ -56,7 +56,8 @@ extern "C" { * When channel monitoring is active, a zero-duration Energy Scan is performed, collecting a single RSSI sample on * every channel per sample interval. The RSSI samples are compared with a pre-specified RSSI threshold. As an * indicator of channel quality, the channel monitoring module maintains and provides the average rate/percentage of - * RSSI samples that are above the threshold within (approximately) a specified sample window. + * RSSI samples that are above the threshold within (approximately) a specified sample window (referred to as channel + * occupancy). * * @{ * @@ -134,25 +135,25 @@ uint32_t otChannelMonitorGetSampleWindow(otInstance *aInstance); uint32_t otChannelMonitorGetSampleCount(otInstance *aInstance); /** - * Gets the current channel quality value for a given channel. + * Gets the current channel occupancy for a given channel. * - * The channel quality value represents the average rate/percentage of RSSI samples that were above RSSI threshold + * The channel occupancy value represents the average rate/percentage of RSSI samples that were above RSSI threshold * ("bad" RSSI samples). * * For the first "sample window" samples, the average is maintained as the actual percentage (i.e., ratio of number * of "bad" samples by total number of samples). After "window" samples, the averager uses an exponentially - * weighted moving average. Practically, this means the quality is representative of up to `3 * window` last samples - * with highest weight given to latest `kSampleWindow` samples. + * weighted moving average. Practically, this means the average is representative of up to `3 * window` last samples + * with highest weight given to the latest `kSampleWindow` samples. * * Max value of `0xffff` indicates all RSSI samples were above RSSI threshold (i.e. 100% of samples were "bad"). * * @param[in] aInstance A pointer to an OpenThread instance. - * @param[in] aChannel The channel for which to get the link quality. + * @param[in] aChannel The channel for which to get the link occupancy. * - * @returns The current channel quality value for the given channel. + * @returns The current channel occupancy for the given channel. * */ -uint16_t otChannelMonitorGetChannelQuality(otInstance *aInstance, uint8_t aChannel); +uint16_t otChannelMonitorGetChannelOccupancy(otInstance *aInstance, uint8_t aChannel); /** * @} diff --git a/src/core/api/channel_monitor_api.cpp b/src/core/api/channel_monitor_api.cpp index 48bbb2546..48b0b5ab7 100644 --- a/src/core/api/channel_monitor_api.cpp +++ b/src/core/api/channel_monitor_api.cpp @@ -82,11 +82,11 @@ uint32_t otChannelMonitorGetSampleCount(otInstance *aInstance) return instance.GetChannelMonitor().GetSampleCount(); } -uint16_t otChannelMonitorGetChannelQuality(otInstance *aInstance, uint8_t aChannel) +uint16_t otChannelMonitorGetChannelOccupancy(otInstance *aInstance, uint8_t aChannel) { Instance &instance = *static_cast(aInstance); - return instance.GetChannelMonitor().GetChannelQuality(aChannel); + return instance.GetChannelMonitor().GetChannelOccupancy(aChannel); } #endif // OPENTHREAD_ENABLE_CHANNEL_MONITOR diff --git a/src/core/utils/channel_monitor.cpp b/src/core/utils/channel_monitor.cpp index 42efe564b..9ddb878bf 100644 --- a/src/core/utils/channel_monitor.cpp +++ b/src/core/utils/channel_monitor.cpp @@ -56,7 +56,7 @@ ChannelMonitor::ChannelMonitor(Instance &aInstance) , mSampleCount(0) , mTimer(aInstance, &ChannelMonitor::HandleTimer, this) { - memset(mChannelQuality, 0, sizeof(mChannelQuality)); + memset(mChannelOccupancy, 0, sizeof(mChannelOccupancy)); } otError ChannelMonitor::Start(void) @@ -88,20 +88,20 @@ void ChannelMonitor::Clear(void) { mChannelMaskIndex = 0; mSampleCount = 0; - memset(mChannelQuality, 0, sizeof(mChannelQuality)); + memset(mChannelOccupancy, 0, sizeof(mChannelOccupancy)); otLogDebgUtil(GetInstance(), "ChannelMonitor: Clearing data"); } -uint16_t ChannelMonitor::GetChannelQuality(uint8_t aChannel) const +uint16_t ChannelMonitor::GetChannelOccupancy(uint8_t aChannel) const { - uint16_t quality = 0; + uint16_t occupancy = 0; VerifyOrExit((OT_RADIO_CHANNEL_MIN <= aChannel) && (aChannel <= OT_RADIO_CHANNEL_MAX)); - quality = mChannelQuality[aChannel - OT_RADIO_CHANNEL_MIN]; + occupancy = mChannelOccupancy[aChannel - OT_RADIO_CHANNEL_MIN]; exit: - return quality; + return occupancy; } void ChannelMonitor::RestartTimer(void) @@ -162,7 +162,7 @@ void ChannelMonitor::HandleEnergyScanResult(otEnergyScanResult *aResult) else { uint8_t channelIndex = (aResult->mChannel - OT_RADIO_CHANNEL_MIN); - uint32_t newAverage = mChannelQuality[channelIndex]; + uint32_t newAverage = mChannelOccupancy[channelIndex]; uint32_t newValue = 0; uint32_t weight; @@ -172,19 +172,19 @@ void ChannelMonitor::HandleEnergyScanResult(otEnergyScanResult *aResult) if (aResult->mMaxRssi != OT_RADIO_RSSI_INVALID) { - newValue = (aResult->mMaxRssi >= kRssiThreshold) ? kMaxQualityIndicator : 0; + newValue = (aResult->mMaxRssi >= kRssiThreshold) ? kMaxOccupancy : 0; } - // `mChannelQuality` stores the average rate/percentage of RSS samples - // that are higher than a given RSS threshold ("bad" RSS samples). For - // the first `kSampleWindow` samples, the average is maintained as the - // actual percentage (i.e., ratio of number of "bad" samples by total - // number of samples). After `kSampleWindow` samples, the averager - // uses an exponentially weighted moving average logic with weight - // coefficient `1/kSampleWindow` for new values. Practically, this - // means the quality is representative of up to `3 * kSampleWindow` - // last samples with highest weight given to latest `kSampleWindow` - // samples. + // `mChannelOccupancy` stores the average rate/percentage of RSS + // samples that are higher than a given RSS threshold ("bad" RSS + // samples). For the first `kSampleWindow` samples, the average is + // maintained as the actual percentage (i.e., ratio of number of + // "bad" samples by total number of samples). After `kSampleWindow` + // samples, the averager uses an exponentially weighted moving + // average logic with weight coefficient `1/kSampleWindow` for new + // values. Practically, this means the average is representative + // of up to `3 * kSampleWindow` samples with highest weight given + // to the latest `kSampleWindow` samples. if (mSampleCount >= kSampleWindow) { @@ -197,7 +197,7 @@ void ChannelMonitor::HandleEnergyScanResult(otEnergyScanResult *aResult) newAverage = (newAverage * weight + newValue) / (weight + 1); - mChannelQuality[channelIndex] = static_cast(newAverage); + mChannelOccupancy[channelIndex] = static_cast(newAverage); } } @@ -206,11 +206,11 @@ void ChannelMonitor::LogResults(void) otLogInfoUtil( GetInstance(), "ChannelMonitor: %u [%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x]", - mSampleCount, mChannelQuality[0] >> 8, mChannelQuality[1] >> 8, mChannelQuality[2] >> 8, - mChannelQuality[3] >> 8, mChannelQuality[4] >> 8, mChannelQuality[5] >> 8, mChannelQuality[6] >> 8, - mChannelQuality[7] >> 8, mChannelQuality[8] >> 8, mChannelQuality[9] >> 8, mChannelQuality[10] >> 8, - mChannelQuality[11] >> 8, mChannelQuality[12] >> 8, mChannelQuality[13] >> 8, mChannelQuality[14] >> 8, - mChannelQuality[15] >> 8); + mSampleCount, mChannelOccupancy[0] >> 8, mChannelOccupancy[1] >> 8, mChannelOccupancy[2] >> 8, + mChannelOccupancy[3] >> 8, mChannelOccupancy[4] >> 8, mChannelOccupancy[5] >> 8, mChannelOccupancy[6] >> 8, + mChannelOccupancy[7] >> 8, mChannelOccupancy[8] >> 8, mChannelOccupancy[9] >> 8, mChannelOccupancy[10] >> 8, + mChannelOccupancy[11] >> 8, mChannelOccupancy[12] >> 8, mChannelOccupancy[13] >> 8, mChannelOccupancy[14] >> 8, + mChannelOccupancy[15] >> 8); } } // namespace Utils diff --git a/src/core/utils/channel_monitor.hpp b/src/core/utils/channel_monitor.hpp index d54f7f0f3..0ce3f5dcd 100644 --- a/src/core/utils/channel_monitor.hpp +++ b/src/core/utils/channel_monitor.hpp @@ -66,7 +66,7 @@ namespace Utils { * channel collecting a single RSSI sample per channel. The RSSI samples are compared with a pre-specified RSSI * threshold `kRssiThreshold`. As an indicator of channel quality, the `ChannelMonitor` maintains and provides the * average rate/percentage of RSSI samples that are above the threshold within (approximately) a specified sample - * window. + * window (referred to as "channel occupancy"). * */ class ChannelMonitor : public InstanceLocator @@ -148,34 +148,34 @@ public: uint32_t GetSampleCount(void) const { return mSampleCount; } /** - * This method returns the current channel quality value for a given channel. + * This method returns the current channel occupancy for a given channel. * - * The channel quality value represents the average rate/percentage of RSSI samples that were above RSSI threshold + * The channel occupancy represents the average rate/percentage of RSSI samples that were above RSSI threshold * `kRssiThreshold` ("bad" RSSI samples). * * For the first `kSampleWindow` samples, the average is maintained as the actual percentage (i.e., ratio of number * of "bad" samples by total number of samples). After `kSampleWindow` samples, the averager uses an exponentially * weighted moving average logic with weight coefficient `1/kSampleWindow` for new values. Practically, this means - * the quality is representative of up to `3 * kSampleWindow` last samples with highest weight given to latest - * `kSampleWindow` samples. + * the occupancy is representative of up to `3 * kSampleWindow` last samples with highest weight given to the + * latest `kSampleWindow` samples. * * Max value of `0xffff` indicates all RSSI samples were above RSSI threshold (i.e. 100% of samples were "bad"). * - * @param[in] aChannel The channel for which to get the link quality. + * @param[in] aChannel The channel for which to get the link occupancy. * - * @returns the current channel quality value for the given channel. + * @returns the current channel occupancy for the given channel. * */ - uint16_t GetChannelQuality(uint8_t aChannel) const; + uint16_t GetChannelOccupancy(uint8_t aChannel) const; private: enum { - kNumChannels = (OT_RADIO_CHANNEL_MAX - OT_RADIO_CHANNEL_MIN + 1), - kNumChannelMasks = 4, - kTimerInterval = (kSampleInterval / kNumChannelMasks), - kMaxJitterInterval = 4096, - kMaxQualityIndicator = 0xffff, + kNumChannels = (OT_RADIO_CHANNEL_MAX - OT_RADIO_CHANNEL_MIN + 1), + kNumChannelMasks = 4, + kTimerInterval = (kSampleInterval / kNumChannelMasks), + kMaxJitterInterval = 4096, + kMaxOccupancy = 0xffff, }; void RestartTimer(void); @@ -189,7 +189,7 @@ private: uint8_t mChannelMaskIndex : 2; uint32_t mSampleCount : 30; - uint16_t mChannelQuality[kNumChannels]; + uint16_t mChannelOccupancy[kNumChannels]; TimerMilli mTimer; }; diff --git a/src/ncp/ncp_base.cpp b/src/ncp/ncp_base.cpp index fa1f41977..dcb47b28e 100644 --- a/src/ncp/ncp_base.cpp +++ b/src/ncp/ncp_base.cpp @@ -161,7 +161,7 @@ const NcpBase::PropertyHandlerEntry NcpBase::mGetPropertyHandlerTable[] = NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MONITOR_RSSI_THRESHOLD), NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MONITOR_SAMPLE_WINDOW), NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MONITOR_SAMPLE_COUNT), - NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MONITOR_CHANNEL_QUALITY), + NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MONITOR_CHANNEL_OCCUPANCY), #endif #if OPENTHREAD_ENABLE_LEGACY NCP_GET_PROP_HANDLER_ENTRY(NEST_LEGACY_ULA_PREFIX), diff --git a/src/ncp/ncp_base.hpp b/src/ncp/ncp_base.hpp index dd6551823..f4ca49d79 100644 --- a/src/ncp/ncp_base.hpp +++ b/src/ncp/ncp_base.hpp @@ -605,7 +605,7 @@ protected: NCP_GET_PROP_HANDLER(CHANNEL_MONITOR_RSSI_THRESHOLD); NCP_GET_PROP_HANDLER(CHANNEL_MONITOR_SAMPLE_WINDOW); NCP_GET_PROP_HANDLER(CHANNEL_MONITOR_SAMPLE_COUNT); - NCP_GET_PROP_HANDLER(CHANNEL_MONITOR_CHANNEL_QUALITY); + NCP_GET_PROP_HANDLER(CHANNEL_MONITOR_CHANNEL_OCCUPANCY); #endif #if OPENTHREAD_ENABLE_LEGACY diff --git a/src/ncp/ncp_base_mtd.cpp b/src/ncp/ncp_base_mtd.cpp index 65767b1d1..23f309e0b 100644 --- a/src/ncp/ncp_base_mtd.cpp +++ b/src/ncp/ncp_base_mtd.cpp @@ -1565,7 +1565,7 @@ otError NcpBase::GetPropertyHandler_CHANNEL_MONITOR_SAMPLE_COUNT(void) return mEncoder.WriteUint32(otChannelMonitorGetSampleCount(mInstance)); } -otError NcpBase::GetPropertyHandler_CHANNEL_MONITOR_CHANNEL_QUALITY(void) +otError NcpBase::GetPropertyHandler_CHANNEL_MONITOR_CHANNEL_OCCUPANCY(void) { otError error = OT_ERROR_NONE; @@ -1574,7 +1574,7 @@ otError NcpBase::GetPropertyHandler_CHANNEL_MONITOR_CHANNEL_QUALITY(void) SuccessOrExit(error = mEncoder.OpenStruct()); SuccessOrExit(error = mEncoder.WriteUint8(channel)); - SuccessOrExit(error = mEncoder.WriteUint16(otChannelMonitorGetChannelQuality(mInstance, channel))); + SuccessOrExit(error = mEncoder.WriteUint16(otChannelMonitorGetChannelOccupancy(mInstance, channel))); SuccessOrExit(error = mEncoder.CloseStruct()); } diff --git a/src/ncp/spinel.c b/src/ncp/spinel.c index ede17b583..dc3a10be6 100644 --- a/src/ncp/spinel.c +++ b/src/ncp/spinel.c @@ -1209,8 +1209,8 @@ spinel_prop_key_to_cstr(spinel_prop_key_t prop_key) ret = "PROP_CHANNEL_MONITOR_SAMPLE_COUNT"; break; - case SPINEL_PROP_CHANNEL_MONITOR_CHANNEL_QUALITY: - ret = "PROP_CHANNEL_MONITOR_CHANNEL_QUALITY"; + case SPINEL_PROP_CHANNEL_MONITOR_CHANNEL_OCCUPANCY: + ret = "PROP_CHANNEL_MONITOR_CHANNEL_OCCUPANCY"; break; case SPINEL_PROP_MAC_SCAN_STATE: diff --git a/src/ncp/spinel.h b/src/ncp/spinel.h index e046aabad..940652af7 100644 --- a/src/ncp/spinel.h +++ b/src/ncp/spinel.h @@ -768,7 +768,7 @@ typedef enum SPINEL_PROP_CHANNEL_MONITOR_SAMPLE_COUNT = SPINEL_PROP_PHY_EXT__BEGIN + 9, - /// Channel monitoring channel quality + /// Channel monitoring channel occupancy /** Format: `A(t(CU))` (read-only) * * Required capability: SPINEL_CAP_CHANNEL_MONITOR @@ -776,9 +776,9 @@ typedef enum * Data per item is: * * `C`: Channel - * `U`: Channel quality indicator + * `U`: Channel occupancy indicator * - * The channel quality value represents the average rate/percentage of + * The channel occupancy value represents the average rate/percentage of * RSSI samples that were above RSSI threshold ("bad" RSSI samples) within * (approximately) sample window latest RSSI samples. * @@ -786,7 +786,7 @@ typedef enum * threshold (i.e. 100% of samples were "bad"). * */ - SPINEL_PROP_CHANNEL_MONITOR_CHANNEL_QUALITY + SPINEL_PROP_CHANNEL_MONITOR_CHANNEL_OCCUPANCY = SPINEL_PROP_PHY_EXT__BEGIN + 10, SPINEL_PROP_PHY_EXT__END = 0x1300, From 51ca76c242e7080b28667ea1a112d7476627bf8f Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 21 Mar 2018 11:04:02 -0700 Subject: [PATCH 2/4] [channel-manager] add (auto) "channel selection" algorithm This commit updates `ChannelManager` class to add support for "channel selection" logic where the `ChannelManager` can select a channel (for network to switch to) based on the collected channel/link quality data (by `ChannelMonitor` module). This commit also adds support for auto- channel-selection functionality where `ChannelManager` will periodically check to see if network can/should switch to a better channel. This commit also adds public OpenThread APIs for all newly added functionalities. --- include/openthread/channel_manager.h | 102 +++++++- include/openthread/instance.h | 1 + src/core/api/channel_manager_api.cpp | 53 +++- src/core/common/notifier.cpp | 4 + src/core/meshcop/meshcop_tlvs.hpp | 15 +- src/core/openthread-core-default-config.h | 102 +++++++- src/core/utils/channel_manager.cpp | 290 +++++++++++++++++++--- src/core/utils/channel_manager.hpp | 144 +++++++++-- src/core/utils/channel_monitor.cpp | 31 +++ src/core/utils/channel_monitor.hpp | 16 ++ src/ncp/ncp_base_ftd.cpp | 2 +- 11 files changed, 701 insertions(+), 59 deletions(-) diff --git a/include/openthread/channel_manager.h b/include/openthread/channel_manager.h index 247fd4e84..f91d868d4 100644 --- a/include/openthread/channel_manager.h +++ b/include/openthread/channel_manager.h @@ -64,12 +64,9 @@ extern "C" { * * @param[in] aInstance A pointer to an OpenThread instance. * @param[in] aChannel The new channel for the Thread network. - - * @retval OT_ERROR_NONE Channel change request successfully processed. - * @retval OT_ERROR_INVALID_ARGS The channel is not a supported channel (@sa otChannelManagerGetSupportedChannels). * */ -otError otChannelManagerRequestChannelChange(otInstance *aInstance, uint8_t aChannel); +void otChannelManagerRequestChannelChange(otInstance *aInstance, uint8_t aChannel); /** * This function gets the channel from the last successful call to `otChannelManagerRequestChannelChange()` @@ -104,12 +101,88 @@ uint16_t otChannelManagerGetDelay(otInstance *aInstance); */ otError otChannelManagerSetDelay(otInstance *aInstance, uint16_t aDelay); +/** + * This function requests that `ChannelManager` checks and selects a new channel and starts a channel change. + * + * Unlike the `otChannelManagerRequestChannelChange()` where the channel must be given as a parameter, this function + * asks the `ChannelManager` to select a channel by itself (based of collected channel quality info). + * + * Once called, the Channel Manager will perform the following 3 steps: + * + * 1) `ChannelManager` decides if the channel change would be helpful. This check can be skipped if + * `aSkipQualityCheck` is set to true (forcing a channel selection to happen and skipping the quality check). + * This step uses the collected link quality metrics on the device (such as CCA failure rate, frame and message + * error rates per neighbor, etc.) to determine if the current channel quality is at the level that justifies + * a channel change. + * + * 2) If the first step passes, then `ChannelManager` selects a potentially better channel. It uses the collected + * channel quality data by `ChannelMonitor` module. The supported and favored channels are used at this step. + * (@sa otChannelManagerSetSupportedChannels, @sa otChannelManagerSetFavoredChannels). + * + * 3) If the newly selected channel is different from the current channel, `ChannelManager` requests/starts the + * channel change process (internally invoking a `RequestChannelChange()`). + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aSkipQualityCheck Indicates whether the quality check (step 1) should be skipped. + * + * @retval OT_ERROR_NONE Channel selection finished successfully. + * @retval OT_ERROR_NOT_FOUND Supported channel mask is empty, therefore could not select a channel. + * @retval OT_ERROR_INVALID_STATE Thread is not enabled or not enough data to select a new channel. + * @retval OT_ERROR_DISABLED_FEATURE `ChannelMonintor` feature is disabled by build-time configuration options. + * + */ +otError otChannelManagerRequestChannelSelect(otInstance *aInstance, bool aSkipQualityCheck); + +/** + * This function enables/disables the auto-channel-selection functionality. + * + * When enabled, `ChannelManager` will periodically invoke a `RequestChannelSelect(false)`. The period interval + * can be set by `SetAutoChannelSelectionInterval()`. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aEnabled Indicates whether to enable or disable this functionality. + * + */ +void otChannelManagerSetAutoChannelSelectionEnabled(otInstance *aInstance, bool aEnabled); + +/** + * This function indicates whether the auto-channel-selection functionality is enabled or not. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @returns TRUE if enabled, FALSE if disabled. + * + */ +bool otChannelManagerGetAutoChannelSelectionEnabled(otInstance *aInstance); + +/** + * This function sets the period interval (in seconds) used by auto-channel-selection functionality. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aInterval The interval in seconds. + * + * @retval OT_ERROR_NONE The interval was set successfully. + * @retval OT_ERROR_INVALID_ARGS The @p aInterval is not valid (zero). + * + */ +otError otChannelManagerSetAutoChannelSelectionInterval(otInstance *aInstance, uint32_t aInterval); + +/** + * This function gets the period interval (in seconds) used by auto-channel-selection functionality. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @returns The interval in seconds. + * + */ +uint32_t otChannelManagerGetAutoChannelSelectionInterval(otInstance *aInstance); + /** * This function gets the supported channel mask. * * @param[in] aInstance A pointer to an OpenThread instance. * - * @returns The supported channels as bit-mask. + * @returns The supported channels as a bit-mask. * */ uint32_t otChannelManagerGetSupportedChannels(otInstance *aInstance); @@ -123,6 +196,25 @@ uint32_t otChannelManagerGetSupportedChannels(otInstance *aInstance); */ void otChannelManagerSetSupportedChannels(otInstance *aInstance, uint32_t aChannelMask); +/** + * This function gets the favored channel mask. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @returns The favored channels as a bit-mask. + * + */ +uint32_t otChannelManagerGetFavoredChannels(otInstance *aInstance); + +/** + * This function sets the favored channel mask. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aChannelMask A channel mask. + * + */ +void otChannelManagerSetFavoredChannels(otInstance *aInstance, uint32_t aChannelMask); + /** * @} * diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 6c2eaf853..4dd0f397a 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -247,6 +247,7 @@ enum OT_CHANGED_MASTER_KEY = 1 << 20, ///< Master key changed OT_CHANGED_PSKC = 1 << 21, ///< PSKc changed OT_CHANGED_SECURITY_POLICY = 1 << 22, ///< Security Policy changed + OT_CHANGED_CHANNEL_MANAGER_NEW_CHANNEL = 1 << 23, ///< Channel Manager new pending Thread channel changed }; /** diff --git a/src/core/api/channel_manager_api.cpp b/src/core/api/channel_manager_api.cpp index e8afed6db..317ac25c3 100644 --- a/src/core/api/channel_manager_api.cpp +++ b/src/core/api/channel_manager_api.cpp @@ -41,11 +41,11 @@ using namespace ot; #if OPENTHREAD_ENABLE_CHANNEL_MANAGER && OPENTHREAD_FTD -otError otChannelManagerRequestChannelChange(otInstance *aInstance, uint8_t aChannel) +void otChannelManagerRequestChannelChange(otInstance *aInstance, uint8_t aChannel) { Instance &instance = *static_cast(aInstance); - return instance.GetChannelManager().RequestChannelChange(aChannel); + instance.GetChannelManager().RequestChannelChange(aChannel); } uint8_t otChannelManagerGetRequestedChannel(otInstance *aInstance) @@ -69,6 +69,41 @@ otError otChannelManagerSetDelay(otInstance *aInstance, uint16_t aMinDelay) return instance.GetChannelManager().SetDelay(aMinDelay); } +otError otChannelManagerRequestChannelSelect(otInstance *aInstance, bool aSkipQualityCheck) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetChannelManager().RequestChannelSelect(aSkipQualityCheck); +} + +void otChannelManagerSetAutoChannelSelectionEnabled(otInstance *aInstance, bool aEnabled) +{ + Instance &instance = *static_cast(aInstance); + + instance.GetChannelManager().SetAutoChannelSelectionEnabled(aEnabled); +} + +bool otChannelManagerGetAutoChannelSelectionEnabled(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetChannelManager().GetAutoChannelSelectionEnabled(); +} + +otError otChannelManagerSetAutoChannelSelectionInterval(otInstance *aInstance, uint32_t aInterval) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetChannelManager().SetAutoChannelSelectionInterval(aInterval); +} + +uint32_t otChannelManagerGetAutoChannelSelectionInterval(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetChannelManager().GetAutoChannelSelectionInterval(); +} + uint32_t otChannelManagerGetSupportedChannels(otInstance *aInstance) { Instance &instance = *static_cast(aInstance); @@ -83,4 +118,18 @@ void otChannelManagerSetSupportedChannels(otInstance *aInstance, uint32_t aChann return instance.GetChannelManager().SetSupportedChannels(aChannelMask); } +uint32_t otChannelManagerGetFavoredChannels(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetChannelManager().GetFavoredChannels(); +} + +void otChannelManagerSetFavoredChannels(otInstance *aInstance, uint32_t aChannelMask) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetChannelManager().SetFavoredChannels(aChannelMask); +} + #endif // OPENTHREAD_ENABLE_CHANNEL_MANAGER && OPENTHREAD_FTD diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp index 4e115d6b7..29e414c95 100644 --- a/src/core/common/notifier.cpp +++ b/src/core/common/notifier.cpp @@ -320,6 +320,10 @@ const char *Notifier::FlagToString(uint32_t aFlag) const retval = "SecPolicy"; break; + case OT_CHANGED_CHANNEL_MANAGER_NEW_CHANNEL: + retval = "CMNewChan"; + break; + default: break; } diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp index 6889116be..eef66f114 100644 --- a/src/core/meshcop/meshcop_tlvs.hpp +++ b/src/core/meshcop/meshcop_tlvs.hpp @@ -1261,9 +1261,18 @@ public: enum { - kMaxDelayTimer = 259200, ///< maximum delay timer value for a Pending Dataset in seconds - kDelayTimerMinimal = 30000, ///< Minimum Delay Timer value for a Pending Operational Dataset (ms) - kDelayTimerDefault = 300000, ///< Default Delay Timer value for a Pending Operational Dataset (ms) + kMaxDelayTimer = 259200, ///< maximum delay timer value for a Pending Dataset in seconds + + /** + * Minimum Delay Timer value for a Pending Operational Dataset (ms) + * + */ + kDelayTimerMinimal = OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY, + + /** + * Default Delay Timer value for a Pending Operational Dataset (ms) + */ + kDelayTimerDefault = OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_DEFAULT_DELAY, }; private: diff --git a/src/core/openthread-core-default-config.h b/src/core/openthread-core-default-config.h index 5b9283d65..4ea774b9f 100644 --- a/src/core/openthread-core-default-config.h +++ b/src/core/openthread-core-default-config.h @@ -527,6 +527,31 @@ #define OPENTHREAD_CONFIG_STORE_FRAME_COUNTER_AHEAD 1000 #endif +/** + * @def OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY + * + * Minimum Delay Timer value for a Pending Operational Dataset (in ms). + * + * Thread specification defines this value as 30,000. Changing from the specified value should be done for testing only. + * + */ +#ifndef OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY +#define OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY 30000 +#endif + +/** + * @def OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_DEFAULT_DELAY + * + * Default Delay Timer value for a Pending Operational Dataset (in ms). + * + * Thread specification defines this value as 300,000. Changing from the specified value should be done for testing + * only. + * + */ +#ifndef OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_DEFAULT_DELAY +#define OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_DEFAULT_DELAY 300000 +#endif + /** * @def OPENTHREAD_CONFIG_LOG_OUTPUT * @@ -1098,7 +1123,7 @@ /** * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY * - * The minimum delay in seconds used by Channel Manager module for performing a channel change. + * The minimum delay (in seconds) used by Channel Manager module for performing a channel change. * * The minimum delay should preferably be longer than maximum data poll interval used by all sleepy-end-devices within * the Thread network. @@ -1110,6 +1135,81 @@ #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY 120 #endif +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_MONITOR_SAMPLE_COUNT + * + * The minimum number of RSSI samples per channel by Channel Monitoring feature before the collected data can be used + * by the Channel Manager module to (auto) select a better channel. + * + * Applicable only if Channel Manager and Channel Monitoring features are both enabled (i.e., + * `OPENTHREAD_ENABLE_CHANNEL_MANAGER` and `OPENTHREAD_ENABLE_CHANNEL_MONITOR` are set). + * + */ +#ifndef OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_MONITOR_SAMPLE_COUNT +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_MONITOR_SAMPLE_COUNT 500 +#endif + +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_SKIP_FAVORED + * + * This threshold specifies the minimum occupancy rate difference between two channels for the Channel Manager to + * prefer an unfavored channel over the best favored one. This is used when (auto) selecting a channel based on the + * collected channel quality data by "channel monitor" feature. + * + * The difference is based on the `ChannelMonitor::GetChannelOccupancy()` definition which provides the average + * percentage of RSSI samples (within a time window) indicating that channel was busy (i.e., RSSI value higher than + * a threshold). Value 0 maps to 0% and 0xffff maps to 100%. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#ifndef OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_SKIP_FAVORED +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_SKIP_FAVORED (0xffff * 7 / 100) +#endif + +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL + * + * This threshold specifies the minimum occupancy rate difference required between the current channel and a newly + * selected channel for Channel Manager to allow channel change to the new channel. + * + * The difference is based on the `ChannelMonitor::GetChannelOccupancy()` definition which provides the average + * percentage of RSSI samples (within a time window) indicating that channel was busy (i.e., RSSI value higher than + * a threshold). Value 0 maps to 0% rate and 0xffff maps to 100%. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#ifndef OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL (0xffff * 10 / 100) +#endif + +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_DEFAULT_AUTO_SELECT_INTERVAL + * + * The default time interval (in seconds) used by Channel Manager for auto-channel-selection functionality. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#ifndef OPENTHREAD_CONFIG_CHANNEL_MANAGER_DEFAULT_AUTO_SELECT_INTERVAL +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_DEFAULT_AUTO_SELECT_INTERVAL (3 * 60 * 60) +#endif + +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_CCA_FAILURE_THRESHOLD + * + * Minimum CCA failure rate threshold on current channel before Channel Manager starts channel selection attempt. + * + * Value 0 maps to 0% and 0xffff maps to 100%. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#ifndef OPENTHREAD_CONFIG_CHANNEL_MANAGER_CCA_FAILURE_THRESHOLD +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_CCA_FAILURE_THRESHOLD (0xffff * 14 / 100) +#endif + /** * @def OPENTHREAD_CONFIG_CHILD_SUPERVISION_INTERVAL * diff --git a/src/core/utils/channel_manager.cpp b/src/core/utils/channel_manager.cpp index 00d4fb855..f244d0b37 100644 --- a/src/core/utils/channel_manager.cpp +++ b/src/core/utils/channel_manager.cpp @@ -49,30 +49,28 @@ namespace Utils { ChannelManager::ChannelManager(Instance &aInstance) : InstanceLocator(aInstance) - , mSupportedChannels(kDefaultSupprotedChannelMask) + , mSupportedChannelMask(0) + , mFavoredChannelMask(0) , mActiveTimestamp(0) , mNotifierCallback(&ChannelManager::HandleStateChanged, this) , mDelay(kMinimumDelay) , mChannel(0) , mState(kStateIdle) , mTimer(aInstance, &ChannelManager::HandleTimer, this) + , mAutoSelectInterval(kDefaultAutoSelectInterval) + , mAutoSelectEnabled(false) { + aInstance.GetNotifier().RegisterCallback(mNotifierCallback); } -otError ChannelManager::RequestChannelChange(uint8_t aChannel) +void ChannelManager::RequestChannelChange(uint8_t aChannel) { - otError error = OT_ERROR_NONE; - otLogInfoUtil(GetInstance(), "ChannelManager: Request to change to channel %d with delay %d sec", aChannel, mDelay); - VerifyOrExit(aChannel != GetInstance().Get().GetChannel()); - - if ((mSupportedChannels & (1U << aChannel)) == 0) + if (aChannel == GetInstance().Get().GetChannel()) { - otLogInfoUtil(GetInstance(), "ChannelManager: Request rejected! Channel %d not in supported mask 0x%x", - aChannel, mSupportedChannels); - - ExitNow(error = OT_ERROR_INVALID_ARGS); + otLogInfoUtil(GetInstance(), "ChannelManager: Already operating on the requested channel %d", aChannel); + ExitNow(); } mState = kStateChangeRequested; @@ -81,8 +79,10 @@ otError ChannelManager::RequestChannelChange(uint8_t aChannel) mTimer.Start(1 + Random::GetUint32InRange(0, kRequestStartJitterInterval)); + GetNotifier().SetFlags(OT_CHANGED_CHANNEL_MANAGER_NEW_CHANNEL); + exit: - return error; + return; } otError ChannelManager::SetDelay(uint16_t aDelay) @@ -109,14 +109,6 @@ void ChannelManager::PreparePendingDataset(void) VerifyOrExit(mChannel != GetInstance().Get().GetChannel()); - if ((mSupportedChannels & (1U << mChannel)) == 0) - { - otLogInfoUtil(GetInstance(), "ChannelManager: Request rejected! Channel %d not in supported mask 0x%x", - mChannel, mSupportedChannels); - mState = kStateIdle; - ExitNow(); - } - if (netif.GetPendingDataset().Get(dataset) == OT_ERROR_NONE) { if (dataset.mIsPendingTimestampSet) @@ -156,9 +148,11 @@ void ChannelManager::PreparePendingDataset(void) } else { - mState = kStateIdle; otLogInfoUtil(GetInstance(), "ChannelManager: Request to change to channel %d failed. Device is disabled", mChannel); + + mState = kStateIdle; + StartAutoSelectTimer(); } ExitNow(); @@ -205,17 +199,14 @@ void ChannelManager::PreparePendingDataset(void) mActiveTimestamp = dataset.mActiveTimestamp + 1 + Random::GetUint32InRange(0, kMaxTimestampIncrease); } - dataset.mActiveTimestamp = mActiveTimestamp; - dataset.mIsActiveTimestampSet = true; - - dataset.mChannel = mChannel; - dataset.mIsChannelSet = true; - + dataset.mActiveTimestamp = mActiveTimestamp; + dataset.mIsActiveTimestampSet = true; + dataset.mChannel = mChannel; + dataset.mIsChannelSet = true; dataset.mPendingTimestamp = pendingTimestamp; dataset.mIsPendingTimestampSet = true; - - dataset.mDelay = delayInMs; - dataset.mIsDelaySet = true; + dataset.mDelay = delayInMs; + dataset.mIsDelaySet = true; error = netif.GetPendingDataset().SendSetRequest(dataset, NULL, 0); @@ -248,6 +239,9 @@ void ChannelManager::HandleTimer(void) switch (mState) { case kStateIdle: + otLogInfoUtil(GetInstance(), "ChannelManager: Auto-triggered channel select"); + IgnoreReturnValue(RequestChannelSelect(false)); + StartAutoSelectTimer(); break; case kStateSentMgmtPendingDataset: @@ -273,7 +267,7 @@ void ChannelManager::HandleStateChanged(uint32_t aFlags) VerifyOrExit(mChannel == GetInstance().Get().GetChannel()); mState = kStateIdle; - mTimer.Stop(); + StartAutoSelectTimer(); otLogInfoUtil(GetInstance(), "ChannelManager: Channel successfully changed to %d", mChannel); @@ -281,6 +275,240 @@ exit: return; } +#if OPENTHREAD_ENABLE_CHANNEL_MONITOR + +/** + * This function randomly chooses a channel from a given channel mask. + * + * @param[in] aMask A channel mask. + * + * @returns A randomly chosen channel from the given mask, or `ChannelMask::kChannelIteratorFirst` if the mask is empty. + * + */ +static uint8_t ChooseRandomChannel(const Mac::ChannelMask &aMask) +{ + uint8_t channel = Mac::ChannelMask::kChannelIteratorFirst; + uint8_t numChannels = 0; + uint8_t randomIndex; + + VerifyOrExit(!aMask.IsEmpty()); + + while (aMask.GetNextChannel(channel) == OT_ERROR_NONE) + { + numChannels++; + } + + randomIndex = Random::GetUint8InRange(0, numChannels); + + channel = Mac::ChannelMask::kChannelIteratorFirst; + SuccessOrExit(aMask.GetNextChannel(channel)); + + while (randomIndex-- != 0) + { + SuccessOrExit(aMask.GetNextChannel(channel)); + } + +exit: + return channel; +} + +otError ChannelManager::FindBetterChannel(uint8_t &aNewChannel, uint16_t &aOccupancy) +{ + otError error = OT_ERROR_NONE; + ChannelMonitor & monitor = GetInstance().GetChannelMonitor(); + Mac::ChannelMask favoredAndSupported; + Mac::ChannelMask favoredBest; + Mac::ChannelMask supportedBest; + uint16_t favoredOccupancy; + uint16_t supportedOccupancy; + char string[Mac::ChannelMask::kInfoStringSize]; + + if (monitor.GetSampleCount() <= kMinChannelMonitorSampleCount) + { + otLogInfoUtil(GetInstance(), "ChannelManager: Too few samples (%d <= %d) to select channel", + monitor.GetSampleCount(), kMinChannelMonitorSampleCount); + ExitNow(error = OT_ERROR_INVALID_STATE); + } + + favoredAndSupported = mFavoredChannelMask; + favoredAndSupported.Intersect(mSupportedChannelMask); + + favoredBest = monitor.FindBestChannels(favoredAndSupported, favoredOccupancy); + supportedBest = monitor.FindBestChannels(mSupportedChannelMask, supportedOccupancy); + + otLogInfoUtil(GetInstance(), "ChannelManager: Best favored %s, occupancy 0x%04x", + favoredBest.ToString(string, sizeof(string)), favoredOccupancy); + otLogInfoUtil(GetInstance(), "ChannelManager: Best overall %s, occupancy 0x%04x", + supportedBest.ToString(string, sizeof(string)), supportedOccupancy); + + // Prefer favored channels unless there is no favored channel, + // or the occupancy rate of the best favored channel is worse + // than the best overall by at least `kThresholdToSkipFavored`. + + if (favoredBest.IsEmpty() || ((favoredOccupancy >= kThresholdToSkipFavored) && + (supportedOccupancy < favoredOccupancy - kThresholdToSkipFavored))) + { + if (!favoredBest.IsEmpty()) + { + otLogInfoUtil(GetInstance(), + "ChannelManager: Preferring an unfavored channel due to high occupancy rate diff"); + } + + favoredBest = supportedBest; + favoredOccupancy = supportedOccupancy; + } + + VerifyOrExit(!favoredBest.IsEmpty(), error = OT_ERROR_NOT_FOUND); + + aNewChannel = ChooseRandomChannel(favoredBest); + aOccupancy = favoredOccupancy; + + OT_UNUSED_VARIABLE(string); + +exit: + return error; +} + +#else // OPENTHREAD_ENABLE_CHANNEL_MONITOR + +otError ChannelManager::FindBetterChannel(uint8_t &, uint16_t &) +{ + otLogInfoUtil(GetInstance(), "ChannelManager: ChannelMonitor feature is disabled - cannot select channel"); + return OT_ERROR_DISABLED_FEATURE; +} + +#endif // OPENTHREAD_ENABLE_CHANNEL_MONITOR + +bool ChannelManager::ShouldAttamptChannelChange(void) +{ + uint16_t ccaFailureRate = GetInstance().Get().GetCcaFailureRate(); + bool shouldAttempt = (ccaFailureRate >= kCcaFailureRateThreshold); + + otLogInfoUtil(GetInstance(), "ChannelManager: CCA-err-rate: 0x%04x %s 0x%04x, selecting channel: %s", + ccaFailureRate, shouldAttempt ? ">=" : "<", kCcaFailureRateThreshold, shouldAttempt ? "yes" : "no"); + + return shouldAttempt; +} + +otError ChannelManager::RequestChannelSelect(bool aSkipQualityCheck) +{ + otError error = OT_ERROR_NONE; + uint8_t curChannel, newChannel; + uint16_t curOccupancy, newOccupancy; + + otLogInfoUtil(GetInstance(), "ChannelManager: Request to select channel (skip quality check: %s)", + aSkipQualityCheck ? "yes" : "no"); + + VerifyOrExit(GetInstance().Get().GetRole() != OT_DEVICE_ROLE_DISABLED, error = OT_ERROR_INVALID_STATE); + + VerifyOrExit(aSkipQualityCheck || ShouldAttamptChannelChange()); + + SuccessOrExit(error = FindBetterChannel(newChannel, newOccupancy)); + + curChannel = GetInstance().Get().GetChannel(); + curOccupancy = GetInstance().GetChannelMonitor().GetChannelOccupancy(curChannel); + + if (newChannel == curChannel) + { + otLogInfoUtil(GetInstance(), "ChannelManager: Already on best possible channel %d", curChannel); + ExitNow(); + } + + otLogInfoUtil(GetInstance(), "ChannelManager: Cur channel %d, occupancy 0x%04x - Best channel %d, occupancy 0x%04x", + curChannel, curOccupancy, newChannel, newOccupancy); + + // Switch only if new channel's occupancy rate is better than current + // channel's occupancy rate by threshold `kThresholdToChangeChannel`. + + if ((newOccupancy >= curOccupancy) || + (static_cast(curOccupancy - newOccupancy) < kThresholdToChangeChannel)) + { + otLogInfoUtil(GetInstance(), "ChannelManager: Occupancy rate diff too small to change channel"); + ExitNow(); + } + + RequestChannelChange(newChannel); + +exit: + + if (error != OT_ERROR_NONE) + { + otLogInfoUtil(GetInstance(), "ChannelManager: Request to select better channel failed, error: %s", + otThreadErrorToString(error)); + } + + return error; +} + +void ChannelManager::StartAutoSelectTimer(void) +{ + VerifyOrExit(mState == kStateIdle); + + if (mAutoSelectEnabled) + { + mTimer.Start(TimerMilli::SecToMsec(mAutoSelectInterval)); + } + else + { + mTimer.Stop(); + } + +exit: + return; +} + +void ChannelManager::SetAutoChannelSelectionEnabled(bool aEnabled) +{ + if (aEnabled != mAutoSelectEnabled) + { + mAutoSelectEnabled = aEnabled; + IgnoreReturnValue(RequestChannelSelect(false)); + StartAutoSelectTimer(); + } +} + +otError ChannelManager::SetAutoChannelSelectionInterval(uint32_t aInterval) +{ + otError error = OT_ERROR_NONE; + uint32_t prevInterval = mAutoSelectInterval; + + VerifyOrExit((aInterval != 0) && (aInterval < TimerMilli::MsecToSec(Timer::kMaxDt)), error = OT_ERROR_INVALID_ARGS); + + mAutoSelectInterval = aInterval; + + if (mAutoSelectEnabled && (mState == kStateIdle) && mTimer.IsRunning() && (prevInterval != aInterval)) + { + mTimer.StartAt(mTimer.GetFireTime() - prevInterval, aInterval); + } + +exit: + return error; +} + +void ChannelManager::SetSupportedChannels(uint32_t aChannelMask) +{ + char string[Mac::ChannelMask::kInfoStringSize]; + + mSupportedChannelMask.SetMask(aChannelMask & OT_RADIO_SUPPORTED_CHANNELS); + + otLogInfoUtil(GetInstance(), "ChannelManager: Supported channels: %s", + mSupportedChannelMask.ToString(string, sizeof(string))); + + OT_UNUSED_VARIABLE(string); +} + +void ChannelManager::SetFavoredChannels(uint32_t aChannelMask) +{ + char string[Mac::ChannelMask::kInfoStringSize]; + + mFavoredChannelMask.SetMask(aChannelMask & OT_RADIO_SUPPORTED_CHANNELS); + + otLogInfoUtil(GetInstance(), "ChannelManager: Favored channels: %s", + mFavoredChannelMask.ToString(string, sizeof(string))); + + OT_UNUSED_VARIABLE(string); +} + } // namespace Utils } // namespace ot diff --git a/src/core/utils/channel_manager.hpp b/src/core/utils/channel_manager.hpp index 0a355cfd4..bfe939057 100644 --- a/src/core/utils/channel_manager.hpp +++ b/src/core/utils/channel_manager.hpp @@ -42,6 +42,7 @@ #include "common/locator.hpp" #include "common/notifier.hpp" #include "common/timer.hpp" +#include "mac/mac.hpp" namespace ot { namespace Utils { @@ -69,7 +70,7 @@ public: enum { /** - * Minimum delay in seconds used for network channel change. + * Minimum delay (in seconds) used for network channel change. * */ kMinimumDelay = OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY, @@ -91,13 +92,12 @@ public: * * A subsequent call to this method will cancel an ongoing previously requested channel change. * + * If the requested channel changes, it will trigger a `Notifier` event `OT_CHANGED_CHANNEL_MANAGER_NEW_CHANNEL`. + * * @param[in] aChannel The new channel for the Thread network. * - * @retval OT_ERROR_NONE Channel change request successfully processed. - * @retval OT_ERROR_INVALID_ARGS The new channel is not a supported channel. - * */ - otError RequestChannelChange(uint8_t aChannel); + void RequestChannelChange(uint8_t aChannel); /** * This method gets the channel from the last successful call to `RequestChannelChange()`. @@ -129,13 +129,83 @@ public: */ otError SetDelay(uint16_t aDelay); + /** + * This method requests that `ChannelManager` checks and selects a new channel and starts a channel change. + * + * Unlike the `RequestChannelChange()` where the channel must be given as a parameter, this method asks the + * `ChannelManager` to select a channel by itself (based on the collected channel quality info). + * + * Once called, the `ChannelManager` will perform the following 3 steps: + * + * 1) `ChannelManager` decides if the channel change would be helpful. This check can be skipped if + * `aSkipQualityCheck` is set to true (forcing a channel selection to happen and skipping the quality check). + * This step uses the collected link quality metrics on the device (such as CCA failure rate, frame and message + * error rates per neighbor, etc.) to determine if the current channel quality is at the level that justifies + * a channel change. + * + * 2) If the first step passes, then `ChannelManager` selects a potentially better channel. It uses the collected + * channel occupancy data by `ChannelMonitor` module. The supported and favored channels are used at this step. + * (@sa SetSupportedChannels, @sa SetFavoredChannels). + * + * 3) If the newly selected channel is different from the current channel, `ChannelManager` requests/starts the + * channel change process (internally invoking a `RequestChannelChange()`). + * + * + * @param[in] aSkipQualityCheck Indicates whether the quality check (step 1) should be skipped. + * + * @retval OT_ERROR_NONE Channel selection finished successfully. + * @retval OT_ERROR_NOT_FOUND Supported channels is empty, therefore could not select a channel. + * @retval OT_ERROR_INVALID_STATE Thread is not enabled or not enough data to select new channel. + * @retval OT_ERROR_DISABLED_FEATURE `ChannelMonintor` feature is disabled by build-time configuration options. + * + */ + otError RequestChannelSelect(bool aSkipQualityCheck); + + /** + * This method enables/disables the auto-channel-selection functionality. + * + * When enabled, `ChannelManager` will periodically invoke a `RequestChannelSelect(false)`. The period interval + * can be set by `SetAutoChannelSelectionInterval()`. + * + * @param[in] aEnabled Indicates whether to enable or disable this functionality. + * + */ + void SetAutoChannelSelectionEnabled(bool aEnabled); + + /** + * This method indicates whether the auto-channel-selection functionality is enabled or not. + * + * @returns TRUE if enabled, FALSE if disabled. + * + */ + bool GetAutoChannelSelectionEnabled(void) const { return mAutoSelectEnabled; } + + /** + * This method sets the period interval (in seconds) used by auto-channel-selection functionality. + * + * @param[in] aInterval The interval (in seconds). + * + * @retval OT_ERROR_NONE The interval was set successfully. + * @retval OT_ERROR_INVALID_ARGS The @p aInterval is not valid (zero). + * + */ + otError SetAutoChannelSelectionInterval(uint32_t aInterval); + + /** + * This method gets the period interval (in seconds) used by auto-channel-selection functionality. + * + * @returns The interval (in seconds). + * + */ + uint32_t GetAutoChannelSelectionInterval(void) { return mAutoSelectInterval; } + /** * This method gets the supported channel mask. * * @returns The supported channels mask. * */ - uint32_t GetSupportedChannels(void) const { return mSupportedChannels; } + uint32_t GetSupportedChannels(void) const { return mSupportedChannelMask.GetMask(); } /** * This method sets the supported channel mask. @@ -143,19 +213,55 @@ public: * @param[in] aChannelMask A channel mask. * */ - void SetSupportedChannels(uint32_t aChannelMask) - { - mSupportedChannels = (aChannelMask & OT_RADIO_SUPPORTED_CHANNELS); - } + void SetSupportedChannels(uint32_t aChannelMask); + + /** + * This method gets the favored channel mask. + * + * @returns The favored channels mask. + * + */ + uint32_t GetFavoredChannels(void) const { return mFavoredChannelMask.GetMask(); } + + /** + * This method sets the favored channel mask. + * + * @param[in] aChannelMask A channel mask. + * + */ + void SetFavoredChannels(uint32_t aChannelMask); private: enum { - kDefaultSupprotedChannelMask = OT_RADIO_SUPPORTED_CHANNELS, - kMaxTimestampIncrease = 128, - kPendingDatasetTxRetryInterval = 20000, // in ms - kChangeCheckWaitInterval = 30000, // in ms - kRequestStartJitterInterval = 10000, // in ms + // Maximum increase of Pending/Active Dataset Timestamp per channel change request. + kMaxTimestampIncrease = 128, + + // Retry interval to resend Pending Dataset in case of tx failure (in ms). + kPendingDatasetTxRetryInterval = 20000, + + // Wait time after sending Pending Dataset to check whether the channel was changed (in ms). + kChangeCheckWaitInterval = 30000, + + // Maximum jitter/wait time to start a requested channel change (in ms). + kRequestStartJitterInterval = 10000, + + // The minimum number of RSSI samples required before using the collected data (by `ChannelMonitor`) to select + // a channel. + kMinChannelMonitorSampleCount = OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_MONITOR_SAMPLE_COUNT, + + // Minimum channel occupancy difference to prefer an unfavored channel over a favored one. + kThresholdToSkipFavored = OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_SKIP_FAVORED, + + // Minimum channel occupancy difference between current channel and the selected channel to trigger the channel + // change process to start. + kThresholdToChangeChannel = OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL, + + // Default auto-channel-selection period (in seconds). + kDefaultAutoSelectInterval = OPENTHREAD_CONFIG_CHANNEL_MANAGER_DEFAULT_AUTO_SELECT_INTERVAL, + + // Minimum CCA failure rate on current channel to start the channel selection process. + kCcaFailureRateThreshold = OPENTHREAD_CONFIG_CHANNEL_MANAGER_CCA_FAILURE_THRESHOLD, }; enum State @@ -170,14 +276,20 @@ private: static void HandleStateChanged(Notifier::Callback &aCallback, uint32_t aChangedFlags); void HandleStateChanged(uint32_t aChangedFlags); void PreparePendingDataset(void); + otError FindBetterChannel(uint8_t &aNewChannel, uint16_t &aOccupancy); + bool ShouldAttamptChannelChange(void); + void StartAutoSelectTimer(void); - uint32_t mSupportedChannels; + Mac::ChannelMask mSupportedChannelMask; + Mac::ChannelMask mFavoredChannelMask; uint64_t mActiveTimestamp; Notifier::Callback mNotifierCallback; uint16_t mDelay; uint8_t mChannel; State mState; TimerMilli mTimer; + uint32_t mAutoSelectInterval; + bool mAutoSelectEnabled; }; #else // OPENTHREAD_FTD diff --git a/src/core/utils/channel_monitor.cpp b/src/core/utils/channel_monitor.cpp index 9ddb878bf..9efae0c8e 100644 --- a/src/core/utils/channel_monitor.cpp +++ b/src/core/utils/channel_monitor.cpp @@ -213,6 +213,37 @@ void ChannelMonitor::LogResults(void) mChannelOccupancy[15] >> 8); } +Mac::ChannelMask ChannelMonitor::FindBestChannels(const Mac::ChannelMask &aMask, uint16_t &aOccupancy) +{ + uint8_t channel; + Mac::ChannelMask bestMask; + uint16_t minOccupancy = 0xffff; + + bestMask.Clear(); + + channel = Mac::ChannelMask::kChannelIteratorFirst; + + while (aMask.GetNextChannel(channel) == OT_ERROR_NONE) + { + uint16_t occupancy = GetChannelOccupancy(channel); + + if (bestMask.IsEmpty() || (occupancy <= minOccupancy)) + { + if (occupancy < minOccupancy) + { + bestMask.Clear(); + } + + bestMask.AddChannel(channel); + minOccupancy = occupancy; + } + } + + aOccupancy = minOccupancy; + + return bestMask; +} + } // namespace Utils } // namespace ot diff --git a/src/core/utils/channel_monitor.hpp b/src/core/utils/channel_monitor.hpp index 0ce3f5dcd..cdabaded2 100644 --- a/src/core/utils/channel_monitor.hpp +++ b/src/core/utils/channel_monitor.hpp @@ -41,6 +41,7 @@ #include "common/locator.hpp" #include "common/timer.hpp" +#include "mac/mac.hpp" namespace ot { namespace Utils { @@ -168,6 +169,21 @@ public: */ uint16_t GetChannelOccupancy(uint8_t aChannel) const; + /** + * This method finds the best channel(s) (with least occupancy rate) in a given channel mask. + * + * The channels are compared based on their occupancy rate from `GetChannelOccupancy()` and lower occupancy rate + * is considered better. + * + * @param[in] aMask A channel mask (the search is limited to channels in @p aMask). + * @param[out] aOccupancy A reference to `uint16` to return the occupancy rate associated with best channel(s). + * + * @returns A channel mask containing the best channels. A mask is returned in case there are more than one + * channel with the same occupancy rate value. + * + */ + Mac::ChannelMask FindBestChannels(const Mac::ChannelMask &aMask, uint16_t &aOccupancy); + private: enum { diff --git a/src/ncp/ncp_base_ftd.cpp b/src/ncp/ncp_base_ftd.cpp index be217788b..2ce700e86 100644 --- a/src/ncp/ncp_base_ftd.cpp +++ b/src/ncp/ncp_base_ftd.cpp @@ -872,7 +872,7 @@ otError NcpBase::SetPropertyHandler_CHANNEL_MANAGER_NEW_CHANNEL(void) SuccessOrExit(error = mDecoder.ReadUint8(channel)); - error = otChannelManagerRequestChannelChange(mInstance, channel); + otChannelManagerRequestChannelChange(mInstance, channel); exit: return error; From 9fa066a5920f6daaf5d5cac6aec366a0b31c6981 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 21 Mar 2018 11:15:39 -0700 Subject: [PATCH 3/4] [ncp] implement "Channel Manager" spinel properties This commit adds new spinel properties along with their set/get handlers for `ChannelManager` module. --- src/ncp/changed_props_set.cpp | 4 +++ src/ncp/ncp_base.cpp | 16 +++++++++ src/ncp/ncp_base.hpp | 8 +++++ src/ncp/ncp_base_ftd.cpp | 68 +++++++++++++++++++++++++++++++++++ src/ncp/ncp_base_mtd.cpp | 1 + src/ncp/spinel.c | 16 +++++++++ src/ncp/spinel.h | 64 +++++++++++++++++++++++++++++++++ 7 files changed, 177 insertions(+) diff --git a/src/ncp/changed_props_set.cpp b/src/ncp/changed_props_set.cpp index 2b59c0d05..54a8da408 100644 --- a/src/ncp/changed_props_set.cpp +++ b/src/ncp/changed_props_set.cpp @@ -79,6 +79,10 @@ const ChangedPropsSet::Entry ChangedPropsSet::mSupportedProps[] = { SPINEL_PROP_NET_XPANID, SPINEL_STATUS_OK, true }, // 25 { SPINEL_PROP_NET_MASTER_KEY, SPINEL_STATUS_OK, true }, // 26 { SPINEL_PROP_NET_PSKC, SPINEL_STATUS_OK, true }, // 27 +#if OPENTHREAD_ENABLE_CHANNEL_MANAGER + { SPINEL_PROP_CHANNEL_MANAGER_NEW_CHANNEL, SPINEL_STATUS_OK, true }, // 28 +#endif + }; uint8_t ChangedPropsSet::GetNumEntries(void) const diff --git a/src/ncp/ncp_base.cpp b/src/ncp/ncp_base.cpp index dcb47b28e..e1b65339b 100644 --- a/src/ncp/ncp_base.cpp +++ b/src/ncp/ncp_base.cpp @@ -245,6 +245,10 @@ const NcpBase::PropertyHandlerEntry NcpBase::mGetPropertyHandlerTable[] = NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_NEW_CHANNEL), NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_DELAY), NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_SUPPORTED_CHANNELS), + NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_FAVORED_CHANNELS), + NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_CHANNEL_SELECT), + NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_AUTO_SELECT_ENABLED), + NCP_GET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_AUTO_SELECT_INTERVAL), #endif #endif // OPENTHREAD_FTD @@ -347,6 +351,10 @@ const NcpBase::PropertyHandlerEntry NcpBase::mSetPropertyHandlerTable[] = NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_NEW_CHANNEL), NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_DELAY), NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_SUPPORTED_CHANNELS), + NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_FAVORED_CHANNELS), + NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_CHANNEL_SELECT), + NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_AUTO_SELECT_ENABLED), + NCP_SET_PROP_HANDLER_ENTRY(CHANNEL_MANAGER_AUTO_SELECT_INTERVAL), #endif #endif // #if OPENTHREAD_FTD }; @@ -1253,8 +1261,12 @@ otError NcpBase::HandleCommandPropertySet(uint8_t aHeader, spinel_prop_key_t aKe ExitNow(error = PrepareLastStatusResponse(aHeader, SPINEL_STATUS_PROP_NOT_FOUND)); } + mDisableStreamWrite = false; + error = (this->*handler)(); + mDisableStreamWrite = true; + if (error == OT_ERROR_NONE) { error = PrepareSetResponse(aHeader, aKey); @@ -1304,8 +1316,12 @@ otError NcpBase::HandleCommandPropertyInsertRemove(uint8_t aHeader, spinel_prop_ mDecoder.ReadData(valuePtr, valueLen); mDecoder.ResetToSaved(); + mDisableStreamWrite = false; + error = (this->*handler)(); + mDisableStreamWrite = true; + VerifyOrExit(error == OT_ERROR_NONE, error = PrepareLastStatusResponse(aHeader, ThreadErrorToSpinelStatus(error))); error = WritePropertyValueInsertedRemovedFrame(aHeader, responseCommand, aKey, valuePtr, valueLen); diff --git a/src/ncp/ncp_base.hpp b/src/ncp/ncp_base.hpp index f4ca49d79..667b2d8b7 100644 --- a/src/ncp/ncp_base.hpp +++ b/src/ncp/ncp_base.hpp @@ -671,6 +671,14 @@ protected: NCP_SET_PROP_HANDLER(CHANNEL_MANAGER_DELAY); NCP_GET_PROP_HANDLER(CHANNEL_MANAGER_SUPPORTED_CHANNELS); NCP_SET_PROP_HANDLER(CHANNEL_MANAGER_SUPPORTED_CHANNELS); + NCP_GET_PROP_HANDLER(CHANNEL_MANAGER_FAVORED_CHANNELS); + NCP_SET_PROP_HANDLER(CHANNEL_MANAGER_FAVORED_CHANNELS); + NCP_GET_PROP_HANDLER(CHANNEL_MANAGER_CHANNEL_SELECT); + NCP_SET_PROP_HANDLER(CHANNEL_MANAGER_CHANNEL_SELECT); + NCP_GET_PROP_HANDLER(CHANNEL_MANAGER_AUTO_SELECT_ENABLED); + NCP_SET_PROP_HANDLER(CHANNEL_MANAGER_AUTO_SELECT_ENABLED); + NCP_GET_PROP_HANDLER(CHANNEL_MANAGER_AUTO_SELECT_INTERVAL); + NCP_SET_PROP_HANDLER(CHANNEL_MANAGER_AUTO_SELECT_INTERVAL); #endif #endif // OPENTHREAD_FTD diff --git a/src/ncp/ncp_base_ftd.cpp b/src/ncp/ncp_base_ftd.cpp index 2ce700e86..2de6453ce 100644 --- a/src/ncp/ncp_base_ftd.cpp +++ b/src/ncp/ncp_base_ftd.cpp @@ -913,6 +913,74 @@ exit: return error; } +otError NcpBase::GetPropertyHandler_CHANNEL_MANAGER_FAVORED_CHANNELS(void) +{ + return EncodeChannelMask(otChannelManagerGetFavoredChannels(mInstance)); +} + +otError NcpBase::SetPropertyHandler_CHANNEL_MANAGER_FAVORED_CHANNELS(void) +{ + uint32_t channelMask = 0; + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = DecodeChannelMask(channelMask)); + otChannelManagerSetFavoredChannels(mInstance, channelMask); + +exit: + return error; +} + +otError NcpBase::GetPropertyHandler_CHANNEL_MANAGER_CHANNEL_SELECT(void) +{ + return mEncoder.WriteBool(false); +} + +otError NcpBase::SetPropertyHandler_CHANNEL_MANAGER_CHANNEL_SELECT(void) +{ + bool skipQualityCheck = false; + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = mDecoder.ReadBool(skipQualityCheck)); + error = otChannelManagerRequestChannelSelect(mInstance, skipQualityCheck); + +exit: + return error; +} + +otError NcpBase::GetPropertyHandler_CHANNEL_MANAGER_AUTO_SELECT_ENABLED(void) +{ + return mEncoder.WriteBool(otChannelManagerGetAutoChannelSelectionEnabled(mInstance)); +} + +otError NcpBase::SetPropertyHandler_CHANNEL_MANAGER_AUTO_SELECT_ENABLED(void) +{ + bool enabled = false; + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = mDecoder.ReadBool(enabled)); + otChannelManagerSetAutoChannelSelectionEnabled(mInstance, enabled); + +exit: + return error; +} + +otError NcpBase::GetPropertyHandler_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL(void) +{ + return mEncoder.WriteUint32(otChannelManagerGetAutoChannelSelectionInterval(mInstance)); +} + +otError NcpBase::SetPropertyHandler_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL(void) +{ + uint32_t interval; + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = mDecoder.ReadUint32(interval)); + error = otChannelManagerSetAutoChannelSelectionInterval(mInstance, interval); + +exit: + return error; +} + #endif // OPENTHREAD_ENABLE_CHANNEL_MANAGER } // namespace Ncp diff --git a/src/ncp/ncp_base_mtd.cpp b/src/ncp/ncp_base_mtd.cpp index 23f309e0b..4f519634e 100644 --- a/src/ncp/ncp_base_mtd.cpp +++ b/src/ncp/ncp_base_mtd.cpp @@ -2969,6 +2969,7 @@ void NcpBase::ProcessThreadChangedFlags(void) { OT_CHANGED_THREAD_EXT_PANID, SPINEL_PROP_NET_XPANID }, { OT_CHANGED_MASTER_KEY, SPINEL_PROP_NET_MASTER_KEY }, { OT_CHANGED_PSKC, SPINEL_PROP_NET_PSKC }, + { OT_CHANGED_CHANNEL_MANAGER_NEW_CHANNEL, SPINEL_PROP_CHANNEL_MANAGER_NEW_CHANNEL }, }; VerifyOrExit(mThreadChangedFlags != 0); diff --git a/src/ncp/spinel.c b/src/ncp/spinel.c index dc3a10be6..79f7b335d 100644 --- a/src/ncp/spinel.c +++ b/src/ncp/spinel.c @@ -1605,6 +1605,22 @@ spinel_prop_key_to_cstr(spinel_prop_key_t prop_key) ret = "PROP_CHANNEL_MANAGER_SUPPORTED_CHANNELS"; break; + case SPINEL_PROP_CHANNEL_MANAGER_FAVORED_CHANNELS: + ret = "PROP_CHANNEL_MANAGER_FAVORED_CHANNELS"; + break; + + case SPINEL_PROP_CHANNEL_MANAGER_CHANNEL_SELECT: + ret = "PROP_CHANNEL_MANAGER_CHANNEL_SELECT"; + break; + + case SPINEL_PROP_CHANNEL_MANAGER_AUTO_SELECT_ENABLED: + ret = "PROP_CHANNEL_MANAGER_AUTO_SELECT_ENABLED"; + break; + + case SPINEL_PROP_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL: + ret = "PROP_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL"; + break; + case SPINEL_PROP_UART_BITRATE: ret = "PROP_UART_BITRATE"; break; diff --git a/src/ncp/spinel.h b/src/ncp/spinel.h index 940652af7..7abfd6246 100644 --- a/src/ncp/spinel.h +++ b/src/ncp/spinel.h @@ -1500,6 +1500,70 @@ typedef enum SPINEL_PROP_CHANNEL_MANAGER_SUPPORTED_CHANNELS = SPINEL_PROP_OPENTHREAD__BEGIN + 2, + /// Channel Manager Favored Channels + /** Format 'A(C)' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property specifies the list of favored channels (when `ChannelManager` is asked to select channel) + * + */ + SPINEL_PROP_CHANNEL_MANAGER_FAVORED_CHANNELS + = SPINEL_PROP_OPENTHREAD__BEGIN + 3, + + /// Channel Manager Channel Select Trigger + /** Format 'b' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * Writing to this property triggers a request on `ChannelManager` to select a new channel. + * + * Once a Channel Select is triggered, the Channel Manager will perform the following 3 steps: + * + * 1) `ChannelManager` decides if the channel change would be helpful. This check can be skipped if in the input + * boolean to this property is set to `true` (skipping the quality check). + * This step uses the collected link quality metrics on the device such as CCA failure rate, frame and message + * error rates per neighbor, etc. to determine if the current channel quality is at the level that justifies + * a channel change. + * + * 2) If first step passes, then `ChannelManager` selects a potentially better channel. It uses the collected + * channel quality data by `ChannelMonitor` module. The supported and favored channels are used at this step. + * + * 3) If the newly selected channel is different from the current channel, `ChannelManager` requests/starts the + * channel change process. + * + * Reading this property always yields `false`. + * + */ + SPINEL_PROP_CHANNEL_MANAGER_CHANNEL_SELECT + = SPINEL_PROP_OPENTHREAD__BEGIN + 4, + + /// Channel Manager Auto Channel Selection Enabled + /** Format 'b' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property indicates if auto-channel-selection functionality is enabled/disabled on `ChannelManager`. + * + * When enabled, `ChannelManager` will periodically checks and attempts to select a new channel. The period interval + * is specified by `SPINEL_PROP_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL`. + * + */ + SPINEL_PROP_CHANNEL_MANAGER_AUTO_SELECT_ENABLED + = SPINEL_PROP_OPENTHREAD__BEGIN + 5, + + /// Channel Manager Auto Channel Selection Interval + /** Format 'L' + * units: seconds + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property specifies the auto-channel-selection check interval (in seconds). + * + */ + SPINEL_PROP_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL + = SPINEL_PROP_OPENTHREAD__BEGIN + 6, + SPINEL_PROP_OPENTHREAD__END = 0x2000, /// UART Bitrate From 00ec62e485c23b2e7ce365d629082b86b9511202 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 21 Mar 2018 11:16:21 -0700 Subject: [PATCH 4/4] [tests] add new toranj test scripts for "ChannelManager" module This commit adds new `toranj` test scripts to test newly added functionality by `ChannelManager` including: - Test to check new spinel/wpan properties, - Test to check channel change request feature, - Test to check channel selection feature. This commit also updates the posix/radio `otPlatRadioGetRssi()` to emulate a simple interference model where it would return either a high or a low RSSI value with a fixed probability per each channel. This is used to verify the `ChannelMonitor` sampling logic and channel selection functionality. --- examples/platforms/posix/radio.c | 26 ++- src/core/meshcop/meshcop_tlvs.hpp | 1 + src/core/openthread-core-default-config.h | 5 +- tests/toranj/openthread-core-toranj-config.h | 55 +++++++ tests/toranj/start.sh | 5 + .../test-600-channel-manager-properties.py | 120 ++++++++++++++ ...test-601-channel-manager-channel-change.py | 146 +++++++++++++++++ ...test-602-channel-manager-channel-select.py | 151 ++++++++++++++++++ tests/toranj/wpan.py | 8 + 9 files changed, 514 insertions(+), 3 deletions(-) create mode 100644 tests/toranj/test-600-channel-manager-properties.py create mode 100644 tests/toranj/test-601-channel-manager-channel-change.py create mode 100644 tests/toranj/test-602-channel-manager-channel-select.py diff --git a/examples/platforms/posix/radio.c b/examples/platforms/posix/radio.c index b57af7e36..82fa79707 100644 --- a/examples/platforms/posix/radio.c +++ b/examples/platforms/posix/radio.c @@ -33,6 +33,7 @@ #include #include #include +#include #include "utils/code_utils.h" @@ -82,6 +83,10 @@ enum { POSIX_RECEIVE_SENSITIVITY = -100, // dBm POSIX_MAX_SRC_MATCH_ENTRIES = OPENTHREAD_CONFIG_MAX_CHILDREN, + + POSIX_HIGH_RSSI_SAMPLE = -30, // dBm + POSIX_LOW_RSSI_SAMPLE = -98, // dBm + POSIX_HIGH_RSSI_PROB_INC_PER_CHANNEL = 5, }; OT_TOOL_PACKED_BEGIN @@ -504,8 +509,27 @@ otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *aInstance) int8_t otPlatRadioGetRssi(otInstance *aInstance) { + int8_t rssi = POSIX_LOW_RSSI_SAMPLE; + uint8_t channel = sReceiveFrame.mChannel; + uint32_t probabilityThreshold; + (void)aInstance; - return 0; + + otEXPECT((OT_RADIO_CHANNEL_MIN <= channel) && channel <= (OT_RADIO_CHANNEL_MAX)); + + // To emulate a simple interference model, we return either a high or + // a low RSSI value with a fixed probability per each channel. The + // probability is increased per channel by a constant. + + probabilityThreshold = (channel - OT_RADIO_CHANNEL_MIN) * POSIX_HIGH_RSSI_PROB_INC_PER_CHANNEL; + + if ((otPlatRandomGet() & 0xffff) < (probabilityThreshold * 0xffff / 100)) + { + rssi = POSIX_HIGH_RSSI_SAMPLE; + } + +exit: + return rssi; } otRadioCaps otPlatRadioGetCaps(otInstance *aInstance) diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp index eef66f114..d41ca829a 100644 --- a/src/core/meshcop/meshcop_tlvs.hpp +++ b/src/core/meshcop/meshcop_tlvs.hpp @@ -1271,6 +1271,7 @@ public: /** * Default Delay Timer value for a Pending Operational Dataset (ms) + * */ kDelayTimerDefault = OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_DEFAULT_DELAY, }; diff --git a/src/core/openthread-core-default-config.h b/src/core/openthread-core-default-config.h index 4ea774b9f..207484f4d 100644 --- a/src/core/openthread-core-default-config.h +++ b/src/core/openthread-core-default-config.h @@ -532,7 +532,8 @@ * * Minimum Delay Timer value for a Pending Operational Dataset (in ms). * - * Thread specification defines this value as 30,000. Changing from the specified value should be done for testing only. + * Thread specification defines this value as 30,000 ms. Changing from the specified value should be done for testing + * only. * */ #ifndef OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY @@ -544,7 +545,7 @@ * * Default Delay Timer value for a Pending Operational Dataset (in ms). * - * Thread specification defines this value as 300,000. Changing from the specified value should be done for testing + * Thread specification defines this value as 300,000 ms. Changing from the specified value should be done for testing * only. * */ diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index 6da446eca..c095f42f6 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -183,6 +183,61 @@ */ #define OPENTHREAD_CONFIG_MLE_SEND_LINK_REQUEST_ON_ADV_TIMEOUT 1 +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY + * + * The minimum delay in seconds used by Channel Manager module for performing a channel change. + * + * The minimum delay should preferably be longer than maximum data poll interval used by all sleepy-end-devices within + * the Thread network. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY 2 + +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_SKIP_FAVORED + * + * This threshold specifies the minimum occupancy rate difference between two channels for the Channel Manager to + * prefer an unfavored channel over the best favored one. This is used when (auto) selecting a channel based on the + * collected channel quality data by "channel monitor" feature. + * + * The difference is based on the `ChannelMonitor::GetChannelOccupancy()` definition which provides the average + * percentage of RSSI samples (within a time window) indicating that channel was busy (i.e., RSSI value higher than + * a threshold). Value 0 maps to 0% and 0xffff maps to 100%. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_SKIP_FAVORED (0xffff * 7 / 100) + +/** + * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL + * + * This threshold specifies the minimum occupancy rate difference required between the current channel and a newly + * selected channel for Channel Manager to allow channel change to the new channel. + * + * The difference is based on the `ChannelMonitor::GetChannelOccupancy()` definition which provides the average + * percentage of RSSI samples (within a time window) indicating that channel was busy (i.e., RSSI value higher than + * a threshold). Value 0 maps to 0% rate and 0xffff maps to 100%. + * + * Applicable only if Channel Manager feature is enabled (i.e., `OPENTHREAD_ENABLE_CHANNEL_MANAGER` is set). + * + */ +#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL (0xffff * 10 / 100) + +/** + * @def OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY + * + * Minimum Delay Timer value for a Pending Operational Dataset (in ms). + * + * Thread specification defines this value as 30,000. Changing from the specified value should be done for testing only. + * + * For `toranj` test script the value is decreased so that the tests can be run faster. + * + */ +#define OPENTHREAD_CONFIG_MESHCOP_PENDING_DATASET_MINIMUM_DELAY 1000 #endif /* OPENTHREAD_CORE_TORANJ_CONFIG_H_ */ diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index 9dc622234..e169caee0 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -95,6 +95,7 @@ cd ../.. --enable-mac-filter \ --enable-service \ --enable-channel-monitor \ + --enable-channel-manager \ --disable-docs \ --disable-test || die @@ -115,4 +116,8 @@ run test-006-traffic-router-end-device.py run test-007-traffic-router-sleepy.py run test-008-permit-join.py +run test-600-channel-manager-properties.py +run test-601-channel-manager-channel-change.py +run test-602-channel-manager-channel-select.py + exit 0 diff --git a/tests/toranj/test-600-channel-manager-properties.py b/tests/toranj/test-600-channel-manager-properties.py new file mode 100644 index 000000000..83058dc71 --- /dev/null +++ b/tests/toranj/test-600-channel-manager-properties.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, 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 time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: This test verifies wpantund properties related to `ChannelManager` feature + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +node.form("channel-manager", channel=11) + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Check default property values + +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 0) +verify(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED) == 'false') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK), 0) == 0) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK), 0) == 0) + +# Set different wpan Channel Manager properties and get and check the output + +node.set(wpan.WPAN_CHANNEL_MANAGER_DELAY, '180') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_DELAY), 0) == 180) + +node.set(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED, '1') +verify(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED) == 'true') + +node.set(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED, '0') +verify(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED) == 'false') + +node.set(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL, '1000') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL), 0) == 1000) + +all_channls_mask = int('0x7fff800', 0) +chan_11_mask = int('0x800', 0) +chan_11_to_13_mask = int('0x3800', 0) + +node.set(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK, str(all_channls_mask)) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK), 0) == all_channls_mask) + +node.set(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK, str(chan_11_mask)) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK), 0) == chan_11_mask) + +node.set(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK, str(chan_11_to_13_mask)) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK), 0) == chan_11_to_13_mask) + +node.set(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK, str(all_channls_mask)) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK), 0) == all_channls_mask) + +node.set(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED, '1') +verify(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED) == 'true') + +# Check to ensure the property values are retained after an NCP reset + +node.reset() + +start_time = time.time() +wait_time = 20 + +while node.get(wpan.WPAN_STATE) != wpan.STATE_ASSOCIATED: + if time.time() - start_time > wait_time: + print 'Took too long to restore after reset ({}>{} sec)'.format(time.time() - start_time, wait_time) + exit(1) + time.sleep(2) + +verify(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED) == 'true') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK), 0) == all_channls_mask) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK), 0) == chan_11_to_13_mask) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL), 0) == 1000) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_DELAY), 0) == 180) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-601-channel-manager-channel-change.py b/tests/toranj/test-601-channel-manager-channel-change.py new file mode 100644 index 000000000..1c8be0582 --- /dev/null +++ b/tests/toranj/test-601-channel-manager-channel-change.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, 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 time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: verifies `ChannelManager` channel change process + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + + +def verify_channel(nodes, new_channel, wait_time=20): + """ + This function checks the channel on a given list of `nodes` and verifies that all nodes + switch to a given `new_channel` (as int) within certain `wait_time` (int and in seconds) + """ + start_time = time.time() + + while not all([ (new_channel == int(node.get(wpan.WPAN_CHANNEL), 0)) for node in nodes ]): + if time.time() - start_time > wait_time: + print 'Took too long to switch to channel {} ({}>{} sec)'.format(new_channel, time.time() - start_time, + wait_time) + exit(1) + time.sleep(0.1) + + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +speedup = 4 +wpan.Node.set_time_speedup_factor(speedup) + +r1 = wpan.Node() +r2 = wpan.Node() +r3 = wpan.Node() +sc1 = wpan.Node() +ec1 = wpan.Node() +sc2 = wpan.Node() +sc3 = wpan.Node() + +all_nodes = [r1, r2, r3, sc1, ec1, sc2, sc3] + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +for node in all_nodes: + node.set(wpan.WPAN_OT_LOG_LEVEL, '0') + +r1.whitelist_node(r2) +r2.whitelist_node(r1) +r1.whitelist_node(r3) +r3.whitelist_node(r1) + +r1.whitelist_node(sc1) +r1.whitelist_node(ec1) +r2.whitelist_node(sc2) +r3.whitelist_node(sc3) + +r1.form('channel-manager', channel=12) +r2.join_node(r1, node_type=wpan.JOIN_TYPE_ROUTER) +r3.join_node(r1, node_type=wpan.JOIN_TYPE_ROUTER) +sc1.join_node(r1, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) +ec1.join_node(r1, node_type=wpan.JOIN_TYPE_END_DEVICE) +sc2.join_node(r2, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) +sc3.join_node(r3, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) + +sc1.set(wpan.WPAN_POLL_INTERVAL, '500') +sc2.set(wpan.WPAN_POLL_INTERVAL, '500') +sc3.set(wpan.WPAN_POLL_INTERVAL, '500') + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# The channel manager delay is set from "openthread-core-toranj-config.h". Verify that it is 2 seconds. + +verify(int(r1.get(wpan.WPAN_CHANNEL_MANAGER_DELAY), 0) == 2) + +verify_channel(all_nodes, 12) + +# Request a channel change to channel 13 from router r1 + +r1.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '13') +verify(int(r1.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 13) +verify_channel(all_nodes, 13) + +# Request same channel change on multiple routers at the same time + +r1.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '14') +r2.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '14') +r3.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '14') +verify_channel(all_nodes, 14) + +# Request different channel changes from same router (router r1). + +r1.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '15') +verify_channel(all_nodes, 14) +r1.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '16') +verify_channel(all_nodes, 16) + +# Request different channels from two routers (r1 and r2) + +r1.set(wpan.WPAN_CHANNEL_MANAGER_DELAY, '20') # increase the time to ensure r1 change is in process +r1.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '17') +time.sleep(10.5 / speedup) +verify_channel(all_nodes, 16) +r2.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '18') +verify_channel(all_nodes, 18) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-602-channel-manager-channel-select.py b/tests/toranj/test-602-channel-manager-channel-select.py new file mode 100644 index 000000000..8b5dddc4d --- /dev/null +++ b/tests/toranj/test-602-channel-manager-channel-select.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, 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 time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: verifies `ChannelManager` channel selection procedure + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +def verify_channel(nodes, new_channel, wait_time=20): + """ + This function checks the channel on a given list of `nodes` and verifies that all nodes + switch to a given `new_channel` (as int) within certain `wait_time` (int and in seconds) + """ + start_time = time.time() + + while not all([ (new_channel == int(node.get(wpan.WPAN_CHANNEL), 0)) for node in nodes ]): + if time.time() - start_time > wait_time: + print 'Took too long to switch to channel {} ({}>{} sec)'.format(new_channel, time.time() - start_time, + wait_time) + exit(1) + time.sleep(0.1) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +# Run the test with 10,000 time speedup factor +wpan.Node.set_time_speedup_factor(10000) + +node = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +node.set(wpan.WPAN_OT_LOG_LEVEL, '0') + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +node.form('channel-manager', channel=24) + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +all_channls_mask = int('0x7fff800', 0) +chan_12_to_15_mask = int('0x000f000', 0) +chan_15_to_17_mask = int('0x0038000', 0) + +# Set supported channel mask to be all channels +node.set(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK, str(all_channls_mask)) +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK), 0) == all_channls_mask) + +# Sleep for 4 second with speedup factor of 10,000 this is more than 11 hours. +time.sleep(4) + +verify(int(node.get(wpan.WPAN_CHANNEL_MONITOR_SAMPLE_COUNT), 0) > 970) + +# Verify the initial value of `NEW_CHANNEL` (should be zero if there has been no channel change so far). + +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 0) + +# Issue a channel-select with quality check enabled, and verify that no action is taken. + +node.set(wpan.WPAN_CHANNEL_MANAGER_CHANNEL_SELECT, 'false') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 0) + +# Issue a channel-select with quality check disabled, verify that channel is switched to channel 11. + +node.set(wpan.WPAN_CHANNEL_MANAGER_CHANNEL_SELECT, 'true') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 11) +verify_channel([node], 11) + +# Set channels 12-15 as favorable and request a channel select, verify that channel is switched to 12. +# +# Even though 11 would be best, quality difference between 11 and 12 is not high enough for selection +# algorithm to pick an unfavored channel. + +node.set(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK, str(chan_12_to_15_mask)) +node.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '25') # request a channel change to 25 +verify_channel([node], 25) +node.set(wpan.WPAN_CHANNEL_MANAGER_CHANNEL_SELECT, 'true') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 12) +verify_channel([node], 12) + +# Set channels 15-17 as favorables and request a channel select, verify that channel is switched to 11. +# +# This time the quality difference between 11 and 15 should be high enough for selection +# algorithm to pick the best though unfavored channel (i.e., channel 11). + +node.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '25') # request a channel change to 25 +verify_channel([node], 25) +node.set(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK, str(chan_15_to_17_mask)) +node.set(wpan.WPAN_CHANNEL_MANAGER_CHANNEL_SELECT, 'true') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 11) +verify_channel([node], 11) + +# Set channels 12-15 as favorable and request a channel select, verify that channel is not switched. + +node.set(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK, str(chan_12_to_15_mask)) +node.set(wpan.WPAN_CHANNEL_MANAGER_CHANNEL_SELECT, 'true') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 11) +verify_channel([node], 11) + +# Starting from channel 12 and issuing a channel select (which would pick 11 as best channel). +# However, since quality difference between current channel 12 and new best channel 11 is not large +# enough, no action should be taken. + +node.set(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL, '12') # request a channel change to 12 +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 12) +verify_channel([node], 12) +node.set(wpan.WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK, str(all_channls_mask)) +node.set(wpan.WPAN_CHANNEL_MANAGER_CHANNEL_SELECT, 'true') +verify(int(node.get(wpan.WPAN_CHANNEL_MANAGER_NEW_CHANNEL), 0) == 12) +verify_channel([node], 12) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/wpan.py b/tests/toranj/wpan.py index 2881ce8b0..024c63cc4 100644 --- a/tests/toranj/wpan.py +++ b/tests/toranj/wpan.py @@ -123,6 +123,14 @@ WPAN_CHANNEL_MONITOR_SAMPLE_COUNT = "ChannelMonitor:SampleCount" WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY = "ChannelMonitor:ChannelQuality" WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY_ASVALMAP = "ChannelMonitor:ChannelQuality:AsValMap" +WPAN_CHANNEL_MANAGER_NEW_CHANNEL = "ChannelManager:NewChannel" +WPAN_CHANNEL_MANAGER_DELAY = "ChannelManager:Delay" +WPAN_CHANNEL_MANAGER_CHANNEL_SELECT = "ChannelManager:ChannelSelect" +WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED = "ChannelManager:AutoSelect:Enabled" +WPAN_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL = "ChannelManager:AutoSelect:Interval" +WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK = "ChannelManager:SupportedChannelMask" +WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK = "ChannelManager:FavoredChannelMask" + #---------------------------------------------------------------------------------------------------------------------- # Valid state values