[sub-mac] introduce unified ReceiveAt logic for timed RX (#13491)

This commit introduces a unified `ReceiveAt` abstraction in `SubMac` to
handle timed reception windows (CSL receiver, WED / Thread-Direct wakeup
listener)

Motivation & Benefits:

Previously, CSL sample window calculation and scheduling differed between
platform-offloaded timing (`OT_RADIO_CAPS_RECEIVE_TIMING`) and stack-driven
timing (`SubMac` software timers):
- `GetCslWindowEdges()` added `kCslReceiveTimeAhead` lead time to the window
  margin, only for offloaded callers to manually subtract it back out before
  calling `Radio::ReceiveAt()`.
- Stack-driven mode relied on separate timers for sample/sleep transitions
  (`HandleCslReceiveOrSleep`, `HandleWedReceiveOrSleep`), leading to code
  duplication and inconsistent timing margins across platform vs stack-driven
  executions.

This commit harmonizes timed reception handling:
- `GetCslWindowEdges()` cleanly computes target window bounds without caller-side
  adjustments.
- `SubMac::ReceiveAt()` provides a single, consistent API for both offloaded
  and stack-driven timing.
- Provides a clean foundation for Thread-Direct wakeup listening (WED) and
  future timed reception features.

Key changes:

- State Machine Refactoring: Simplifies the state management adding
  `kStateTimedReceive`. The radio state is `kStateTimedReceive` when
  actively receiving in a target rx window, and `kStateSleep` otherwise.
- `TimedRx` Helper Class: Encapsulates window parameters (`mStartTime`,
  `mDuration`, `mChannel`) and state checks (`HasStarted`, `HasEnded`).
- Pending & Active Window Preemption: `UpdateTimedRxState()` manages
  the timed RX schedules and sets the timer accordingly. It ensures
  pending windows starting before an active window ends are promoted
  seamlessly.
- Unit Test: Adds a detailed `test_sub_mac_recv_at.cpp` covering various
  behaviors (including edge-cases) of `ReceiveAt()` logic.
This commit is contained in:
Abtin Keshavarzian
2026-08-31 14:27:24 -07:00
committed by GitHub
parent ec3e9402e3
commit 5c8c318627
9 changed files with 1352 additions and 313 deletions
+254 -137
View File
@@ -93,6 +93,11 @@ void SubMac::Init(void)
mFrameCounter = 0;
mTimer.Stop();
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
mActiveTimedRx.Clear();
mPendingTimedRx.Clear();
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
CslInit();
#endif
@@ -197,6 +202,11 @@ Error SubMac::Disable(void)
{
Error error;
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
mActiveTimedRx.Clear();
mPendingTimedRx.Clear();
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
mCslTimer.Stop();
#endif
@@ -217,27 +227,38 @@ Error SubMac::Sleep(void)
{
Error error = kErrorNone;
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
if (IsRadioSampleEnabled())
{
RadioSample();
ExitNow();
}
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
// `ProcessTimedRx()` evaluates active and pending timed RX windows.
//
// If the current time is within a timed reception window, it transitions
// the state to `kStateTimedReceive`.
//
// `ProcessTimedRx()` does not put the radio to sleep unless we were
// already in `kStateTimedReceive` and the RX window has ended.
//
// Therefore, after calling `ProcessTimedRx()`, we verify that `mState` is
// not `kStateTimedReceive` before proceeding to transition the radio to sleep.
ProcessTimedRx();
VerifyOrExit(mState != kStateTimedReceive);
#endif
// Even if the radio platform supports `kCapRxOnWhenIdle`, when
// `SubMac::Sleep()` is explicitly called while `mRxOnWhenIdle`
// is true, we still call `Radio::Sleep()`. This supports radio
// validation and test scenarios where the radio is being forced
// to sleep.
// If the radio platform layer supports `kCapRxOnWhenIdle`, it is
// responsible for putting the radio to sleep, so we skip calling
// `Radio::Sleep()`.
//
// However, if `SubMac::Sleep()` is explicitly called while `mRxOnWhenIdle`
// is true, we still call `Radio::Sleep()` to support test scenarios where
// the radio is forced to sleep.
SetState(kStateSleep);
if (!RadioSupports(kCapRxOnWhenIdle) || mRxOnWhenIdle)
{
SuccessOrExit(error = Get<Radio::Radio>().Sleep());
}
SetState(kStateSleep);
exit:
LogWarnOnError(error, "Sleep()");
return error;
@@ -308,8 +329,8 @@ Error SubMac::Send(void)
#endif
case kStateSleep:
case kStateReceive:
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
case kStateRadioSample:
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
case kStateTimedReceive:
#endif
break;
@@ -317,6 +338,8 @@ Error SubMac::Send(void)
ExitNow(error = kErrorInvalidState);
}
mTimer.Stop();
#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
if (mRadioFilterEnabled)
{
@@ -702,6 +725,13 @@ Error SubMac::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration)
switch (mState)
{
case kStateSleep:
case kStateReceive:
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
case kStateTimedReceive:
#endif
break;
case kStateDisabled:
case kStateCsmaBackoff:
case kStateTransmit:
@@ -713,15 +743,10 @@ Error SubMac::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration)
#endif
case kStateEnergyScan:
ExitNow(error = kErrorInvalidState);
case kStateReceive:
case kStateSleep:
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
case kStateRadioSample:
#endif
break;
}
mTimer.Stop();
#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
if (mRadioFilterEnabled)
{
@@ -783,6 +808,202 @@ void SubMac::HandleEnergyScanDone(int8_t aMaxRssi)
mCallbacks.EnergyScanDone(aMaxRssi);
}
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
void SubMac::ReceiveAt(Radio::Time64 aStartTime, uint32_t aDuration, uint8_t aChannel)
{
Radio::SyncedTime now;
TimedRx timedRx;
VerifyOrExit(mState != kStateDisabled);
#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
if (mRadioFilterEnabled)
{
ExitNow();
}
#endif
timedRx.Init(aStartTime, aDuration, aChannel);
now.SetToNow(Get<Radio::Radio>());
VerifyOrExit(!timedRx.HasEnded(now));
if (!ShouldHandle(kCapReceiveTiming))
{
timedRx.ScheduleOnRadio(Get<Radio::Radio>());
ExitNow();
}
if (mPendingTimedRx.IsSpecified() && mPendingTimedRx.HasStarted(now) && !mPendingTimedRx.HasEnded(now))
{
// Before replacing `mPendingTimedRx` check if the existing one
// should be started, and start if we can (we are in right states).
// Otherwise copy it as `mActiveTimedRx` (resume/start it once
// state changes and timed-rx is allowed).
switch (mState)
{
case kStateSleep:
case kStateTimedReceive:
StartPendingTimedRx();
break;
default:
mActiveTimedRx = mPendingTimedRx;
break;
}
}
mPendingTimedRx = timedRx;
switch (mState)
{
case kStateSleep:
case kStateTimedReceive:
ProcessTimedRx();
break;
default:
break;
}
exit:
return;
}
void SubMac::CancelPendingReceiveAt(void)
{
VerifyOrExit(mPendingTimedRx.IsSpecified());
mPendingTimedRx.Clear();
switch (mState)
{
case kStateSleep:
case kStateTimedReceive:
ProcessTimedRx();
break;
default:
break;
}
exit:
return;
}
void SubMac::StartPendingTimedRx(void)
{
if ((mState == kStateTimedReceive) && (mActiveTimedRx.GetChannel() == mPendingTimedRx.GetChannel()))
{
// Skip transitioning the radio if already receiving on the same
// channel
}
else
{
IgnoreError(Get<Radio::Radio>().Receive(mPendingTimedRx.GetChannel()));
}
mActiveTimedRx = mPendingTimedRx;
SetState(kStateTimedReceive);
}
void SubMac::ProcessTimedRx(void)
{
// Processes the timed RX state machine, starting any due pending
// timed RX, scheduling the timer for upcoming windows, or putting
// the radio to sleep when active reception window ends.
Radio::Time64 fireTime = Radio::kMaxTime64;
Radio::SyncedTime now;
mTimer.Stop();
now.SetToNow(Get<Radio::Radio>());
// Start pending `TimedRx` if due, or clear it if missed.
if (mPendingTimedRx.IsSpecified())
{
if (mPendingTimedRx.HasStarted(now))
{
if (!mPendingTimedRx.HasEnded(now))
{
StartPendingTimedRx();
}
mPendingTimedRx.Clear();
}
else
{
fireTime = mPendingTimedRx.GetStartTime();
}
}
VerifyOrExit(mActiveTimedRx.IsSpecified());
if (!mActiveTimedRx.HasEnded(now))
{
if (mState != kStateTimedReceive)
{
IgnoreError(Get<Radio::Radio>().Receive(mActiveTimedRx.GetChannel()));
SetState(kStateTimedReceive);
}
fireTime = Min(fireTime, mActiveTimedRx.GetEndTime());
ExitNow();
}
// Active `TimedRx` has ended. clear it and transition the radio
// to sleep if it was actively receiving.
mActiveTimedRx.Clear();
if (mState == kStateTimedReceive)
{
SetState(kStateSleep);
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
// Don't actually sleep for debugging when `MAC_CSL_DEBUG_ENABLE`.
ExitNow();
#endif
IgnoreError(Get<Radio::Radio>().Sleep());
}
exit:
if (fireTime != Radio::kMaxTime64)
{
// Schedule the timer to fire at a target radio time `fireTime`,
// using the synced reference `now` to translate radio time to
// local time.
uint32_t delay = 0;
if (fireTime > now.GetAsTime64())
{
delay = ClampToUint32(fireTime - now.GetAsTime64());
}
StartTimerAt(now.GetAsLocalTimeMicro(), delay);
}
}
void SubMac::TimedRx::Init(Radio::Time64 aStartTime, uint32_t aDuration, uint8_t aChannel)
{
mStartTime = aStartTime;
mDuration = aDuration;
mChannel = aChannel;
mIsSpecified = true;
}
void SubMac::TimedRx::ScheduleOnRadio(Radio::Radio &aRadio) const
{
Error error = aRadio.ReceiveAt(mChannel, Radio::ConvertTime64To32(mStartTime), mDuration);
LogWarnOnError(error, "Radio::ReceiveAt()");
}
#endif // OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
void SubMac::HandleTimer(void)
{
switch (mState)
@@ -810,6 +1031,13 @@ void SubMac::HandleTimer(void)
SampleRssi();
break;
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
case kStateTimedReceive:
case kStateSleep:
ProcessTimedRx();
break;
#endif
default:
break;
}
@@ -957,117 +1185,6 @@ void SubMac::StartTimerAt(Time aStartTime, uint32_t aDelayUs)
#endif
}
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
void SubMac::RadioSample(void)
{
#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
if (mRadioFilterEnabled)
{
IgnoreError(Get<Radio::Radio>().Sleep());
ExitNow();
}
#endif
SetState(kStateRadioSample);
if (!RadioSupports(kCapReceiveTiming))
{
UpdateRadioSampleState();
ExitNow();
}
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
// Don't sleep for debugging when `MAC_CSL_DEBUG_ENABLE`.
ExitNow();
#endif
if (!RadioSupports(kCapRxOnWhenIdle))
{
IgnoreError(Get<Radio::Radio>().Sleep());
}
exit:
return;
}
bool SubMac::IsRadioSampleEnabled(void) const
{
bool ret = false;
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
ret = IsCslEnabled();
#endif
#if OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
ret = ret || mIsWedEnabled;
#endif
return ret;
}
/*
* The radio state (receive/sleep) is determined by the request from both CSL and WED:
* 1. If both CSL and WED request to enter sleep state, the radio is set to sleep state.
* 2. If either CSL or WED requests to enter the receive state and the other requests to enter sleep state, the radio
* is set to receive state using the channel that is requested to enter the receive state.
* 3. If both CSL and WED request to enter the receive state, the radio is set to the receive state using the CSL
* channel.
*
* The diagram below illustrates how to set the radio state based on the request of WED and CSL.
*
* CSL ------========------------========------------========------------========---
* ^ ^
* | |
* | mIsCslSampling=false
* mIsCslSampling=true
*
* WED -----------++++++++----------------++++++++----------------++++++++----------
* ^ ^
* | |
* | mIsWedSampling=false
* mIsWedSampling=true
*
* Radio ------========+++++-------========-++++++++---========-----+++++++========---
* ^ ^ ^
* | | |
* | | Radio::Sleep()
* | Radio::Receive(WedCh)
* Radio::Receive(CslCh)
*/
void SubMac::UpdateRadioSampleState(void)
{
VerifyOrExit(mState == kStateRadioSample);
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
if (mIsCslSampling)
{
IgnoreError(Get<Radio::Radio>().Receive(mCslChannel));
ExitNow();
}
#endif
#if OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
if (mIsWedSampling)
{
IgnoreError(Get<Radio::Radio>().Receive(mWakeupChannel));
ExitNow();
}
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
// Don't sleep for debugging when `MAC_CSL_DEBUG_ENABLE`.
ExitNow();
#endif
IgnoreError(Get<Radio::Radio>().Sleep());
exit:
return;
}
#endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
// LCOV_EXCL_START
const char *SubMac::StateToString(State aState)
@@ -1079,7 +1196,7 @@ const char *SubMac::StateToString(State aState)
_(kStateCsmaBackoff, "CsmaBackoff") \
_(kStateTransmit, "Transmit") \
_(kStateEnergyScan, "EnergyScan") \
DelayBeforeRetxStateMapList(_) TimedTxStateMapList(_) RadioSampleMapList(_)
DelayBeforeRetxStateMapList(_) TimedTxStateMapList(_) TimedRxStateMapList(_)
#if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
#define DelayBeforeRetxStateMapList(_) _(kStateDelayBeforeRetx, "DelayBeforeRetx")
@@ -1093,10 +1210,10 @@ const char *SubMac::StateToString(State aState)
#define TimedTxStateMapList(_)
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
#define RadioSampleMapList(_) _(kStateRadioSample, "RadioSample")
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
#define TimedRxStateMapList(_) _(kStateTimedReceive, "TimedReceive")
#else
#define RadioSampleMapList(_)
#define TimedRxStateMapList(_)
#endif
DefineEnumStringArray(StateMapList);
+73 -28
View File
@@ -41,6 +41,7 @@
#include <openthread/platform/crypto.h>
#include "common/callback.hpp"
#include "common/clearable.hpp"
#include "common/locator.hpp"
#include "common/non_copyable.hpp"
#include "common/timer.hpp"
@@ -71,6 +72,14 @@ namespace Mac {
(OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_INITIATOR_ENABLE || \
((OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE) && OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_TIMING_ENABLE))
#ifdef OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
#error "OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE MUST NOT be defined directly. It is derived from other configs"
#endif
#define OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE \
(OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE || \
((OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE) && OPENTHREAD_CONFIG_MAC_SOFTWARE_RX_TIMING_ENABLE))
//----------------------------------------------------------------------------------------------------------------------
// Config validity checks
@@ -78,17 +87,13 @@ namespace Mac {
#error "Thread 1.2 or higher version is required for OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE."
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
#if (OPENTHREAD_CONFIG_THREAD_VERSION < OT_THREAD_VERSION_1_2)
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && (OPENTHREAD_CONFIG_THREAD_VERSION < OT_THREAD_VERSION_1_2)
#error "Thread 1.2 or higher version is required for OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE."
#endif
#if !OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE && !OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
#error "Microsecond timer OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE is required for "\
"OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE"
#endif
"TARGET TIME RX feature (CSL, Thread direct wakeup listener, etc)."
#endif
//----------------------------------------------------------------------------------------------------------------------
@@ -338,6 +343,28 @@ public:
*/
Error Receive(uint8_t aChannel);
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
/**
* Schedules a radio reception window at a specific time and duration.
*
* `SubMac` supports one active and one pending reception window. If an unstarted receive window is already
* pending when this method is called, the new window will replace the existing pending window.
*
* @param[in] aStartTime The start time in microseconds.
* @param[in] aDuration The duration of the receive window in microseconds.
* @param[in] aChannel The channel to use for receiving.
*/
void ReceiveAt(Radio::Time64 aStartTime, uint32_t aDuration, uint8_t aChannel);
/**
* Cancels any pending scheduled receive window (`ReceiveAt()`).
*
* If a scheduled reception window is currently pending (waiting to start), it is cancelled. If a timed reception
* window is already active (radio is currently receiving), this method does not interrupt the ongoing reception.
*/
void CancelPendingReceiveAt(void);
#endif
/**
* Gets the radio transmit frame.
*
@@ -552,9 +579,8 @@ private:
#if OT_CONFIG_MAC_TARGET_TIME_TX_ENABLE
kStateTimedTransmit, // Timed TX (e.g., for CSL)
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
kStateRadioSample, // Mac layer has requested the SubMac to enter sleep state, but the SubMac is in the periodic
// sample state.
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
kStateTimedReceive, // Timed RX (CSL sampling or wake listening)
#endif
};
@@ -582,6 +608,29 @@ private:
OPENTHREAD_CONFIG_CSL_TRANSMIT_TIME_AHEAD + kCcaSampleInterval + Radio::kHeaderShrDuration;
#endif
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
class TimedRx : public Clearable<TimedRx>
{
public:
TimedRx(void) { Clear(); }
void Init(Radio::Time64 aStartTime, uint32_t aDuration, uint8_t aChannel);
bool IsSpecified(void) const { return mIsSpecified; }
Radio::Time64 GetStartTime(void) const { return mStartTime; }
Radio::Time64 GetEndTime(void) const { return mStartTime + mDuration; }
uint8_t GetChannel(void) const { return mChannel; }
bool HasStarted(const Radio::SyncedTime &aNow) const { return mStartTime <= aNow.GetAsTime64(); }
bool HasEnded(const Radio::SyncedTime &aNow) const { return GetEndTime() <= aNow.GetAsTime64(); }
void ScheduleOnRadio(Radio::Radio &aRadio) const;
private:
Radio::Time64 mStartTime;
uint32_t mDuration;
uint8_t mChannel;
bool mIsSpecified;
};
#endif
void Init(void);
bool RadioSupports(Capability aCapability) const { return (mRadioCaps & aCapability) != 0; }
@@ -605,15 +654,14 @@ private:
void HandleEnergyScanDone(int8_t aMaxRssi);
void HandleTimer(void);
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
void StartPendingTimedRx(void);
void ProcessTimedRx(void);
#endif
void SetState(State aState);
static const char *StateToString(State aState);
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
bool IsRadioSampleEnabled(void) const;
void UpdateRadioSampleState(void);
void RadioSample(void);
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
void CslInit(void);
void RestartCslTimerAfterSyncUpdate(void);
@@ -625,19 +673,15 @@ private:
uint32_t DetermineClockDrift(uint32_t aIntervalUs) const;
uint32_t GetNextCycleDrift(void) const;
bool IsCslEnabled(void) const { return mCslPeriod > 0; }
void LogCslWindow(Radio::Time64 aStart, uint32_t aDuration);
#if OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
void LogReceived(RxFrame *aFrame);
#endif
void HandleCslReceiveAt(uint32_t aTimeAhead, uint32_t aTimeAfter);
void HandleCslReceiveOrSleep(uint32_t aTimeAhead, uint32_t aTimeAfter);
void LogCslWindow(uint32_t aWinStart, uint32_t aWinDuration);
#endif
#if OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
void WedInit(void);
void HandleWedTimer(void);
void HandleWedReceiveAt(void);
void HandleWedReceiveOrSleep(void);
#endif
#if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
@@ -667,15 +711,18 @@ private:
#if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
uint8_t mRetxDelayBackoffExponent;
#endif
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
TimedRx mActiveTimedRx;
TimedRx mPendingTimedRx;
#endif
SubMacTimer mTimer;
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
using CslTimer = TimerMicroIn<SubMac, &SubMac::HandleCslTimer>;
uint16_t mCslPeriod; // The CSL sample period, in units of 10 symbols (160 microseconds).
uint8_t mCslChannel : 7; // The CSL sample channel.
bool mIsCslSampling : 1; // Indicates that the current time is in CSL sample window
// for platforms not supporting `Radio::ReceiveAt()`.
uint16_t mCslPeriod; // The CSL sample period, in units of 10 symbols (160 microseconds).
uint8_t mCslChannel; // The CSL sample channel.
uint16_t mCslPeerShort; // The CSL peer short address.
Radio::SyncedTime mCslSampleTime; // The CSL sample time for current period.
CslAccuracy mCslParentAccuracy; // The parent's CSL accuracy (clock accuracy and uncertainty).
@@ -690,9 +737,7 @@ private:
#if OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
using WedTimer = TimerMicroIn<SubMac, &SubMac::HandleWedTimer>;
bool mIsWedSampling : 1; // Indicates that the current time is in WED's sample window
// for platforms not supporting `Radio::ReceiveAt()`.
bool mIsWedEnabled : 1; // Indicates if the WED is enabled.
bool mIsWedEnabled; // Indicates if the WED is enabled.
uint32_t mWakeupListenInterval; // The wake-up listen interval, in microseconds.
uint32_t mWakeupListenDuration; // The wake-up listen duration, in microseconds.
uint8_t mWakeupChannel; // The wake-up sample channel.
+27 -97
View File
@@ -44,10 +44,9 @@ RegisterLogModule("SubMac");
void SubMac::CslInit(void)
{
mCslPeriod = 0;
mCslChannel = 0;
mCslPeerShort = 0;
mIsCslSampling = false;
mCslPeriod = 0;
mCslChannel = 0;
mCslPeerShort = 0;
mCslSampleTime.Clear();
mCslTimer.Stop();
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_LOCAL_TIME_SYNC
@@ -59,8 +58,7 @@ void SubMac::CslInit(void)
void SubMac::RestartCslTimerAfterSyncUpdate(void)
{
// Only applies for the case where radio supports receive timing.
if (RadioSupports(kCapReceiveTiming) && mCslTimer.IsRunning())
if (mCslTimer.IsRunning())
{
uint32_t periodUs = CslPeriodToUsec(mCslPeriod);
@@ -118,8 +116,7 @@ void SubMac::SetCslParams(uint16_t aPeriod, uint8_t aChannel, ShortAddress aShor
mCslPeerShort = aShortAddr;
IgnoreError(Get<Radio::Radio>().EnableCsl(aPeriod, aShortAddr, aExtAddr));
mIsCslSampling = false;
mCslPeriod = aPeriod;
mCslPeriod = aPeriod;
mCslTimer.Stop();
@@ -134,9 +131,9 @@ void SubMac::SetCslParams(uint16_t aPeriod, uint8_t aChannel, ShortAddress aShor
#endif
HandleCslTimer();
}
else if (!RadioSupports(kCapReceiveTiming))
else
{
UpdateRadioSampleState();
CancelPendingReceiveAt();
}
exit:
@@ -144,32 +141,11 @@ exit:
}
void SubMac::HandleCslTimer(void)
{
uint32_t timeAhead, timeAfter;
GetCslWindowEdges(timeAhead, timeAfter);
// The handler works in different ways when the radio supports receive-timing and doesn't.
if (RadioSupports(kCapReceiveTiming))
{
HandleCslReceiveAt(timeAhead, timeAfter);
}
else
{
HandleCslReceiveOrSleep(timeAhead, timeAfter);
}
}
void SubMac::HandleCslReceiveAt(uint32_t aTimeAhead, uint32_t aTimeAfter)
{
/*
* When the radio supports receive-timing:
* The handler will be called once per CSL period. When the handler is called, it will set the timer to
* fire at the next CSL sample time and call `Radio::ReceiveAt` to start sampling for the current CSL period.
* The timer fires some time before the actual sample time. After `Radio::ReceiveAt` is called, the radio will
* remain in sleep state until the actual sample time.
* Note that it never call `Radio::Sleep` explicitly. The radio will fall into sleep after `ReceiveAt` ends. This
* will be done by the platform as part of the `otPlatRadioReceiveAt` API.
* fire at the next CSL sample time and call `ReceiveAt` to start sampling for the current CSL period.
* The timer fires some time before the next sample time.
*
* Timer fires Timer fires
* ^ ^
@@ -177,73 +153,27 @@ void SubMac::HandleCslReceiveAt(uint32_t aTimeAhead, uint32_t aTimeAfter)
* sample sleep sample sleep
*/
uint32_t periodUs = CslPeriodToUsec(mCslPeriod);
Radio::Time32 winStart;
uint32_t timeAhead, timeAfter;
Radio::Time64 winStart;
uint32_t winDuration;
mCslTimer.FireAt(mCslSampleTime.GetAsLocalTimeMicro() + periodUs - aTimeAhead - GetNextCycleDrift());
aTimeAhead -= kCslReceiveTimeAhead;
winStart = mCslSampleTime.GetAsTime32() - aTimeAhead;
winDuration = aTimeAhead + aTimeAfter;
GetCslWindowEdges(timeAhead, timeAfter);
mCslTimer.FireAt(mCslSampleTime.GetAsLocalTimeMicro() + periodUs - timeAhead - kCslReceiveTimeAhead -
GetNextCycleDrift());
winStart = mCslSampleTime.GetAsTime64() - timeAhead;
winDuration = timeAhead + timeAfter;
mCslSampleTime += periodUs;
Get<Radio::Radio>().UpdateCslSampleTime(mCslSampleTime.GetAsTime32());
// Schedule reception window for any state except RX - so that CSL RX Window has lower priority
// than scanning or RX after the data poll.
if ((mState != kStateDisabled) && (mState != kStateReceive) &&
Radio::IsTimeStrictlyBefore(Get<Radio::Radio>().GetNowAsTime32(), winStart + winDuration))
{
IgnoreError(Get<Radio::Radio>().ReceiveAt(mCslChannel, winStart, winDuration));
}
ReceiveAt(winStart, winDuration, mCslChannel);
LogCslWindow(winStart, winDuration);
}
void SubMac::HandleCslReceiveOrSleep(uint32_t aTimeAhead, uint32_t aTimeAfter)
{
/*
* When the radio doesn't support receive-timing:
* The handler will be called twice per CSL period: at the beginning of sample and sleep. When the handler is
* called, it will explicitly change the radio state due to the current state by calling `Radio::Receive` or
* `Radio::Sleep`.
*
* Timer fires Timer fires Timer fires Timer fires
* ^ ^ ^ ^
* |------------|---------------------------------------|------------|---------------------------------------|
* sample sleep sample sleep
*/
if (mIsCslSampling)
{
mIsCslSampling = false;
mCslTimer.FireAt(mCslSampleTime.GetAsLocalTimeMicro() - aTimeAhead - GetNextCycleDrift());
if (mState == kStateRadioSample)
{
LogDebg("CSL sleep %lu", ToUlong(mCslTimer.GetNow().GetValue()));
}
}
else
{
uint32_t periodUs = CslPeriodToUsec(mCslPeriod);
uint32_t winStart;
uint32_t winDuration;
mCslTimer.FireAt(mCslSampleTime.GetAsLocalTimeMicro() + aTimeAfter);
mIsCslSampling = true;
winStart = TimerMicro::GetNow().GetValue();
winDuration = aTimeAhead + aTimeAfter;
mCslSampleTime += periodUs;
Get<Radio::Radio>().UpdateCslSampleTime(mCslSampleTime.GetAsTime32());
LogCslWindow(winStart, winDuration);
}
UpdateRadioSampleState();
}
void SubMac::GetCslWindowEdges(uint32_t &aAhead, uint32_t &aAfter)
{
/*
@@ -275,7 +205,7 @@ void SubMac::GetCslWindowEdges(uint32_t &aAhead, uint32_t &aAfter)
semiWindow +=
Radio::ConvertUncertaintyToUsec(mCslParentAccuracy.GetUncertainty() + Get<Radio::Radio>().GetCslUncertainty());
aAhead = Min(semiPeriod, semiWindow + kMinReceiveOnAhead + kCslReceiveTimeAhead);
aAhead = Min(semiPeriod, semiWindow + kMinReceiveOnAhead);
aAfter = Min(semiPeriod, semiWindow + kMinReceiveOnAfter);
}
@@ -298,12 +228,12 @@ void SubMac::SetCslLastSyncToNow(void)
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_DEBG)
void SubMac::LogCslWindow(uint32_t aWinStart, uint32_t aWinDuration)
void SubMac::LogCslWindow(Radio::Time64 aWinStart, uint32_t aWinDuration)
{
LogDebg("CSL window start %lu, duration %lu", ToUlong(aWinStart), ToUlong(aWinDuration));
LogDebg("CSL window start %lu, duration %lu", ToUlong(Radio::ConvertTime64To32(aWinStart)), ToUlong(aWinDuration));
}
#else
void SubMac::LogCslWindow(uint32_t, uint32_t) {}
void SubMac::LogCslWindow(Radio::Time64, uint32_t) {}
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
@@ -321,13 +251,13 @@ void SubMac::LogReceived(RxFrame *aFrame)
VerifyOrExit((dst.GetType() == Address::kTypeShort && dst.GetShort() == GetShortAddress()) ||
(dst.GetType() == Address::kTypeExtended && dst.GetExtended() == GetExtAddress()));
LogDebg("Received frame in state (SubMac %s, CSL %s), timestamp %lu", StateToString(mState),
mIsCslSampling ? "CslSample" : "CslSleep", ToUlong(Radio::ConvertTime64To32(aFrame->GetTimestamp())));
LogDebg("Received frame in state %s, timestamp %lu", StateToString(mState),
ToUlong(Radio::ConvertTime64To32(aFrame->GetTimestamp())));
VerifyOrExit(mState == kStateRadioSample);
VerifyOrExit((mState == kStateTimedReceive) || (mState == kStateSleep));
GetCslWindowEdges(ahead, after);
ahead -= kMinReceiveOnAhead + kCslReceiveTimeAhead;
ahead -= kMinReceiveOnAhead;
sampleTime = mCslSampleTime.GetAsTime32() - CslPeriodToUsec(mCslPeriod);
deviation = Radio::ConvertTime64To32(aFrame->GetTimestamp()) + Radio::kHeaderPhrDuration - sampleTime;
+1 -42
View File
@@ -44,7 +44,6 @@ RegisterLogModule("SubMac");
void SubMac::WedInit(void)
{
mIsWedSampling = false;
mIsWedEnabled = false;
mWakeupListenInterval = 0;
mWedTimer.Stop();
@@ -55,7 +54,6 @@ void SubMac::UpdateWakeupListening(bool aEnable, uint32_t aInterval, uint32_t aD
mWakeupListenInterval = aInterval;
mWakeupListenDuration = aDuration;
mWakeupChannel = aChannel;
mIsWedSampling = false;
mIsWedEnabled = aEnable;
mWedTimer.Stop();
@@ -68,56 +66,17 @@ void SubMac::UpdateWakeupListening(bool aEnable, uint32_t aInterval, uint32_t aD
HandleWedTimer();
}
else if (!RadioSupports(kCapReceiveTiming))
{
UpdateRadioSampleState();
}
}
void SubMac::HandleWedTimer(void)
{
if (RadioSupports(kCapReceiveTiming))
{
HandleWedReceiveAt();
}
else
{
HandleWedReceiveOrSleep();
}
}
void SubMac::HandleWedReceiveAt(void)
{
mWedSampleTime += mWakeupListenInterval;
mWedTimer.FireAt(mWedSampleTime.GetAsLocalTimeMicro() + mWakeupListenDuration + kWedReceiveTimeAfter);
if (mState != kStateDisabled)
{
IgnoreError(Get<Radio::Radio>().ReceiveAt(mWakeupChannel, mWedSampleTime.GetAsTime32(), mWakeupListenDuration));
}
ReceiveAt(mWedSampleTime.GetAsTime64(), mWakeupListenDuration, mWakeupChannel);
}
void SubMac::HandleWedReceiveOrSleep(void)
{
TimeMilli fireTime;
mIsWedSampling = !mIsWedSampling;
if (mIsWedSampling)
{
fireTime = mWedSampleTime.GetAsLocalTimeMicro() + mWakeupListenDuration + kMinReceiveOnAfter;
}
else
{
mWedSampleTime += mWakeupListenInterval;
fireTime = mWedSampleTime.GetAsLocalTimeMicro() - kMinReceiveOnAhead;
}
mWedTimer.FireAt(fireTime);
UpdateRadioSampleState();
}
} // namespace Mac
} // namespace ot
+1 -7
View File
@@ -573,7 +573,6 @@ public:
*/
Error Receive(uint8_t aChannel);
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
/**
* Schedules a radio reception window at a specific time and duration.
*
@@ -585,7 +584,6 @@ public:
* @retval kErrorFailed The receive window could not be scheduled.
*/
Error ReceiveAt(uint8_t aChannel, Time32 aStart, uint32_t aDuration);
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
/**
@@ -989,7 +987,6 @@ inline Error Radio::Receive(uint8_t aChannel)
return otPlatRadioReceive(GetInstancePtr(), aChannel);
}
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
inline Error Radio::ReceiveAt(uint8_t aChannel, Time32 aStart, uint32_t aDuration)
{
Error error = otPlatRadioReceiveAt(GetInstancePtr(), aChannel, aStart, aDuration);
@@ -1001,7 +998,6 @@ inline Error Radio::ReceiveAt(uint8_t aChannel, Time32 aStart, uint32_t aDuratio
#endif
return error;
}
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
inline void Radio::UpdateCslSampleTime(Time32 aCslSampleTime)
@@ -1103,9 +1099,7 @@ inline Error Radio::Sleep(void) { return kErrorNone; }
inline Error Radio::Receive(uint8_t) { return kErrorNone; }
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_TD_WAKE_LISTENER_ENABLE
inline Error Radio::ReceiveAt(uint8_t, uint32_t, uint32_t) { return kErrorNone; }
#endif
inline Error Radio::ReceiveAt(uint8_t, uint32_t, uint32_t) { return kErrorNotImplemented; }
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
inline void Radio::UpdateCslSampleTime(Time32) {}
+6
View File
@@ -39,6 +39,7 @@
#include <openthread/platform/radio.h>
#include "common/clearable.hpp"
#include "common/numeric_limits.hpp"
#include "common/string.hpp"
#include "common/time.hpp"
@@ -71,6 +72,11 @@ typedef otRadioTime64 Time64;
*/
typedef otRadioTime32 Time32;
/**
* Represents the maximum value of a 64-bit radio time.
*/
constexpr Time64 kMaxTime64 = NumericLimits<Time64>::kMax;
/**
* Converts a 64-bit radio time to a 32-bit radio time.
*
@@ -37,7 +37,6 @@
#include "openthread-core-toranj-config.h"
#if OPENTHREAD_RADIO
#define OPENTHREAD_CONFIG_PLATFORM_INFO "SIMULATION-RCP-toranj"
#else
#define OPENTHREAD_CONFIG_PLATFORM_INFO "SIMULATION-toranj"
@@ -85,7 +84,9 @@
#define OPENTHREAD_SIMULATION_MDNS_SOCKET_IMPLEMENT_POSIX 1
#define OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE 0
#define OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE 1
#define OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE 1
#define OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE 1
+1
View File
@@ -265,6 +265,7 @@ ot_unit_test(spinel_prop_codec)
ot_unit_test(srp_adv_proxy)
ot_unit_test(srp_server)
ot_unit_test(string)
ot_unit_test(sub_mac_recv_at)
ot_unit_test(tasklet)
ot_unit_test(tcat)
ot_unit_test(timer)
+986
View File
@@ -0,0 +1,986 @@
/*
* Copyright (c) 2026, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "test_platform.h"
#include <openthread/config.h>
#include "instance/instance.hpp"
namespace ot {
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
enum RadioState
{
kRadioStateSleep,
kRadioStateReceive,
};
static uint64_t sNow = 10000000;
static RadioState sRadioState = kRadioStateSleep;
static uint8_t sReceiveChannel = 0;
static uint32_t sReceiveCallCount = 0;
static uint32_t sLocalRadioTimeDifference = 0;
static bool sAlarmMicroOn = false;
static uint32_t sAlarmMicroT0 = 0;
static uint32_t sAlarmMicroDt = 0;
static bool sAlarmMilliOn = false;
static uint32_t sAlarmMilliT0 = 0;
static uint32_t sAlarmMilliDt = 0;
extern "C" {
otRadioTime64 otPlatRadioGetNow(otInstance *) { return sNow + sLocalRadioTimeDifference; }
otError otPlatRadioReceive(otInstance *, uint8_t aChannel)
{
sRadioState = kRadioStateReceive;
sReceiveChannel = aChannel;
sReceiveCallCount++;
return OT_ERROR_NONE;
}
otError otPlatRadioSleep(otInstance *)
{
sRadioState = kRadioStateSleep;
return OT_ERROR_NONE;
}
otRadioCaps otPlatRadioGetCaps(otInstance *) { return OT_RADIO_CAPS_ACK_TIMEOUT | OT_RADIO_CAPS_CSMA_BACKOFF; }
void otPlatAlarmMicroStop(otInstance *) { sAlarmMicroOn = false; }
void otPlatAlarmMicroStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
{
sAlarmMicroOn = true;
sAlarmMicroT0 = aT0;
sAlarmMicroDt = aDt;
}
uint32_t otPlatAlarmMicroGetNow(void) { return static_cast<uint32_t>(sNow); }
void otPlatAlarmMilliStop(otInstance *) { sAlarmMilliOn = false; }
void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
{
sAlarmMilliOn = true;
sAlarmMilliT0 = aT0;
sAlarmMilliDt = aDt;
}
uint32_t otPlatAlarmMilliGetNow(void) { return static_cast<uint32_t>(sNow / 1000); }
} // extern "C"
static void ResetPlatformState(void)
{
sNow = 10000000;
sRadioState = kRadioStateSleep;
sReceiveChannel = 0;
sReceiveCallCount = 0;
sAlarmMicroOn = false;
sAlarmMicroT0 = 0;
sAlarmMicroDt = 0;
sAlarmMilliOn = false;
sAlarmMilliT0 = 0;
sAlarmMilliDt = 0;
}
static void AdvanceTime(Instance &aInstance, uint32_t aDurationUs)
{
uint64_t targetTime = sNow + aDurationUs;
while (sNow < targetTime)
{
uint32_t step = static_cast<uint32_t>(targetTime - sNow);
if (sAlarmMicroOn)
{
uint32_t alarmFireTime = sAlarmMicroT0 + sAlarmMicroDt;
uint32_t timeToAlarm = alarmFireTime - static_cast<uint32_t>(sNow);
if (timeToAlarm <= step)
{
step = timeToAlarm;
}
}
sNow += step;
if (sAlarmMicroOn && (static_cast<uint32_t>(sNow) == sAlarmMicroT0 + sAlarmMicroDt))
{
sAlarmMicroOn = false;
otPlatAlarmMicroFired(&aInstance);
}
if (sAlarmMilliOn && (static_cast<uint32_t>(sNow / 1000) == sAlarmMilliT0 + sAlarmMilliDt))
{
sAlarmMilliOn = false;
otPlatAlarmMilliFired(&aInstance);
}
otTaskletsProcess(&aInstance);
}
}
//---------------------------------------------------------------------------------------------------------------------
void TestSimpleReceiveAt(void)
{
Instance *instance;
Mac::SubMac *subMac;
uint64_t startTime;
uint32_t duration;
uint8_t channel;
printf("TestSimpleReceiveAt()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateSleep);
// Schedule a simple future `ReceiveAt` window and verify that the
// radio stays in sleep before the start time, transitions to receive
// during the window, and returns to sleep when the window ends.
startTime = otPlatRadioGetNow(instance) + 10000;
duration = 5000;
channel = 11;
subMac->ReceiveAt(startTime, duration, channel);
AdvanceTime(*instance, 9999);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should be in sleep before window start");
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should enter receive at window start");
VerifyOrQuit(sReceiveChannel == channel, "Receive channel mismatch");
AdvanceTime(*instance, 4999);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should stay in receive during window");
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should return to sleep at window end");
testFreeInstance(instance);
}
void TestPendingReceiveAtReplaced(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
printf("TestPendingReceiveAtReplaced()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, replace it before it starts.
// Validate that the new `ReceiveAt` is executed.
startTime1 = otPlatRadioGetNow(instance) + 100;
subMac->ReceiveAt(startTime1, 50, 11);
AdvanceTime(*instance, 20);
startTime2 = otPlatRadioGetNow(instance) + 200;
subMac->ReceiveAt(startTime2, 50, 12);
AdvanceTime(*instance, 140);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should NOT enter receive for replaced window 1");
AdvanceTime(*instance, static_cast<uint32_t>(startTime2 - otPlatRadioGetNow(instance)));
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should enter receive for window 2");
VerifyOrQuit(sReceiveChannel == 12, "Receive channel mismatch for window 2");
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should return to sleep after window 2");
testFreeInstance(instance);
}
void TestReceiveAtAlreadyStarted(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime;
uint32_t duration;
uint8_t channel;
printf("TestReceiveAtAlreadyStarted()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt` which is already started.
startTime = otPlatRadioGetNow(instance) - 2000;
duration = 10000;
channel = 15;
subMac->ReceiveAt(startTime, duration, channel);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should immediately enter receive for started window");
VerifyOrQuit(sReceiveChannel == channel, "Receive channel mismatch");
AdvanceTime(*instance, 7999);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should remain in receive");
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should return to sleep when window ends");
testFreeInstance(instance);
}
void TestReceiveAtAlreadyExpired(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime;
uint32_t duration;
printf("TestReceiveAtAlreadyExpired()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt` which is already expired.
startTime = otPlatRadioGetNow(instance) - 10000;
duration = 5000;
subMac->ReceiveAt(startTime, duration, 11);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep for expired window");
AdvanceTime(*instance, 10000);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
VerifyOrQuit(sReceiveCallCount == 0);
testFreeInstance(instance);
}
void TestReceiveAtWhileActive(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
printf("TestReceiveAtWhileActive()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, wait for it to start,
// then schedule a new one while the previous one is
// active.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 100, 11);
AdvanceTime(*instance, 70);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should be in receive for window 1");
startTime2 = startTime1 + 200;
subMac->ReceiveAt(startTime2, 50, 12);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should remain in receive for active window 1");
VerifyOrQuit(sReceiveChannel == 11, "Radio should remain on channel 11");
AdvanceTime(*instance, 80);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 1 ends");
AdvanceTime(*instance, static_cast<uint32_t>(startTime2 - otPlatRadioGetNow(instance)));
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should enter receive for window 2");
VerifyOrQuit(sReceiveChannel == 12, "Radio should switch to channel 12 for window 2");
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 2 ends");
testFreeInstance(instance);
}
void TestSleepDuringActiveReceiveAt(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime;
uint32_t duration;
uint8_t channel;
printf("TestSleepDuringActiveReceiveAt()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, wait for it to start, then while in the
// middle of the active window call `Sleep()` multiple times.
// Ensure that explicit calls to `Sleep()` do not interrupt the
// active timed RX window and the radio remains in receive until
// the window ends.
startTime = otPlatRadioGetNow(instance) + 50;
duration = 50;
channel = 11;
subMac->ReceiveAt(startTime, duration, channel);
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == channel);
AdvanceTime(*instance, 20);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == channel);
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateReceive, "Sleep() during active window should not stop timed-rx");
VerifyOrQuit(sReceiveChannel == channel);
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == channel);
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateReceive, "Subsequent Sleep() should not stop timed-rx");
VerifyOrQuit(sReceiveChannel == channel);
AdvanceTime(*instance, 19);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == channel);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
void TestOverlappingReceiveAtSameChannel(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
uint32_t initialRxCallCount;
printf("TestOverlappingReceiveAtSameChannel()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, wait for it to start, then schedule an
// overlapping one on the same channel, which ends earlier than
// the original one.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 100, 11);
AdvanceTime(*instance, 60);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
initialRxCallCount = sReceiveCallCount;
startTime2 = startTime1 + 20;
subMac->ReceiveAt(startTime2, 5, 11);
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
VerifyOrQuit(sReceiveCallCount == initialRxCallCount);
AdvanceTime(*instance, 4);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
VerifyOrQuit(sReceiveCallCount == initialRxCallCount);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after overlapping window 2 ends");
testFreeInstance(instance);
}
void TestOverlappingReceiveAtDifferentChannel(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
printf("TestOverlappingReceiveAtDifferentChannel()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, wait for it to start, then schedule an
// overlapping one on a different channel, which ends earlier than
// the original one.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 100, 11);
AdvanceTime(*instance, 60);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
startTime2 = startTime1 + 20;
subMac->ReceiveAt(startTime2, 6, 15);
AdvanceTime(*instance, 9);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15, "Radio should switch to channel 15 when window 2 starts");
AdvanceTime(*instance, 5);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 2 ends");
testFreeInstance(instance);
}
void TestOverlappingReceiveAtExtendingPastEnd(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
printf("TestOverlappingReceiveAtExtendingPastEnd()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt` window 1, wait for it to start, then
// schedule an overlapping window 2 on a different channel that
// starts before window 1 ends and extends past window 1's end.
// Ensure channel switches when window 2 starts and reception
// continues until window 2 ends.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 50, 11);
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 20);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
startTime2 = startTime1 + 30;
subMac->ReceiveAt(startTime2, 60, 15);
AdvanceTime(*instance, 9);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15, "Radio should switch to channel 15 when window 2 starts");
AdvanceTime(*instance, 19);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15, "Radio should remain in receive past window 1 end");
AdvanceTime(*instance, 39);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 2 ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
void TestAdjacentBackToBackReceiveAt(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
printf("TestAdjacentBackToBackReceiveAt()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt` window 1, then while it is active schedule
// a back-to-back adjacent window 2 on a different channel (with zero
// gap between windows). Ensure the radio switches channels at the
// boundary tick without glitching into sleep, and concludes when
// window 2 ends.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 50, 11);
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 20);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
startTime2 = startTime1 + 50;
subMac->ReceiveAt(startTime2, 40, 15);
AdvanceTime(*instance, 29);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should switch to window 2 channel at boundary");
VerifyOrQuit(sReceiveChannel == 15, "Receive channel mismatch for window 2");
AdvanceTime(*instance, 39);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 2 ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
void TestOverlappingReceiveAtSubsumingWindow(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
printf("TestOverlappingReceiveAtSubsumingWindow()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt` window 1, wait for it to start, then
// schedule an overlapping window 2 on a different channel that
// started earlier and ends later (fully subsuming window 1).
// Ensure channel switches immediately to window 2 channel and
// continues receiving until window 2 ends.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 50, 11);
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 20);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
// Schedule window 2 ([t0 + 40, t0 + 120]) on channel 15 which
// started earlier than current time (t0 + 70) and ends later
// than window 1 (t0 + 100).
startTime2 = startTime1 - 10;
subMac->ReceiveAt(startTime2, 80, 15);
// Radio should immediately switch to window 2 channel
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15, "Radio should switch to window 2 channel immediately");
AdvanceTime(*instance, 29);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
// Window 1 original end (t0 + 100); verify radio remains receiving on channel 15
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15, "Radio should remain in receive past window 1 end");
AdvanceTime(*instance, 19);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
// Window 2 ends at t0 + 120; verify transition to sleep
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 2 ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
void TestFullyMissedReceiveAt(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime;
uint8_t rxChannel;
printf("TestFullyMissedReceiveAt()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, before it can start call `Receive()` on
// a different channel blocking timed-rx, wait until after the end
// of the scheduled rx to call `Sleep()` to ensure the schedule is
// fully missed.
startTime = otPlatRadioGetNow(instance) + 60;
subMac->ReceiveAt(startTime, 40, 11);
AdvanceTime(*instance, 30);
VerifyOrQuit(sRadioState == kRadioStateSleep);
rxChannel = 16;
SuccessOrQuit(subMac->Receive(rxChannel));
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
AdvanceTime(*instance, 70);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep since timed window was fully missed");
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
for (uint16_t i = 0; i < 100; i++)
{
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
}
testFreeInstance(instance);
}
void TestInterruptedReceiveAt(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime;
uint8_t rxChannel;
printf("TestInterruptedReceiveAt()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, before it can start call `Receive()` on
// a different channel blocking timed-rx, then before the scheduled
// rx window ends call `Sleep()`. Ensure the timed-rx is started
// on its channel after `Sleep()` and then concludes properly when
// the window ends.
startTime = otPlatRadioGetNow(instance) + 60;
subMac->ReceiveAt(startTime, 40, 11);
AdvanceTime(*instance, 50);
VerifyOrQuit(sRadioState == kRadioStateSleep);
rxChannel = 18;
SuccessOrQuit(subMac->Receive(rxChannel));
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
AdvanceTime(*instance, 25);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should switch to timed-rx channel");
VerifyOrQuit(sReceiveChannel == 11, "Receive channel mismatch");
AdvanceTime(*instance, 24);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after resumed window ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
void TestInterruptedActiveReceiveAt(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime;
uint8_t rxChannel;
printf("TestInterruptedActiveReceiveAt()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt`, wait for it to start, then interrupt it
// by calling `Receive()` on a different channel, then before the
// scheduled rx window ends call `Sleep()`. Ensure the timed-rx
// is resumed on its channel and then concludes properly when the
// window ends.
startTime = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime, 50, 11);
AdvanceTime(*instance, 49);
VerifyOrQuit(sRadioState == kRadioStateSleep);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
rxChannel = 18;
SuccessOrQuit(subMac->Receive(rxChannel));
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
AdvanceTime(*instance, 15);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should switch back to timed-rx channel");
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 24);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after resumed window ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
void TestInterruptedReceiveAtWithSecondSchedule(void)
{
Instance *instance;
Mac::SubMac *subMac;
Radio::Time64 startTime1;
Radio::Time64 startTime2;
uint8_t rxChannel;
printf("TestInterruptedReceiveAtWithSecondSchedule()\n");
ResetPlatformState();
instance = testInitInstance();
subMac = &instance->Get<Mac::SubMac>();
SuccessOrQuit(subMac->Enable());
SuccessOrQuit(subMac->Sleep());
// Schedule a `ReceiveAt` window 1, before it starts block it by
// calling `Receive()` on a different channel. Wait until window 1
// starts and while blocked in receive state, schedule a future
// `ReceiveAt` window 2. Advance time a bit (still within window 1),
// then call `Sleep()`. Ensure window 1 is resumed on its channel,
// ends properly, and window 2 starts and ends as scheduled.
startTime1 = otPlatRadioGetNow(instance) + 50;
subMac->ReceiveAt(startTime1, 50, 11);
AdvanceTime(*instance, 30);
VerifyOrQuit(sRadioState == kRadioStateSleep);
rxChannel = 20;
SuccessOrQuit(subMac->Receive(rxChannel));
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
AdvanceTime(*instance, 40);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
startTime2 = startTime1 + 200;
subMac->ReceiveAt(startTime2, 7, 15);
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == rxChannel);
SuccessOrQuit(subMac->Sleep());
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should switch back to window 1 channel");
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 19);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 11);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 1 ends");
AdvanceTime(*instance, 149);
VerifyOrQuit(sRadioState == kRadioStateSleep);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateReceive, "Radio should enter receive for window 2");
VerifyOrQuit(sReceiveChannel == 15);
AdvanceTime(*instance, 6);
VerifyOrQuit(sRadioState == kRadioStateReceive);
VerifyOrQuit(sReceiveChannel == 15);
AdvanceTime(*instance, 1);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should sleep after window 2 ends");
AdvanceTime(*instance, 10);
VerifyOrQuit(sRadioState == kRadioStateSleep, "Radio should remain in sleep");
testFreeInstance(instance);
}
#endif // OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
} // namespace ot
int main(void)
{
#if OT_CONFIG_MAC_TARGET_TIME_RX_ENABLE
const uint32_t kLocalRadioTimeDifferences[] = {0, 1000, 1000000};
for (uint32_t diff : kLocalRadioTimeDifferences)
{
printf("------------------------------------------------------\n");
printf("Setting sLocalRadioTimeDifference: %lu\n", ot::ToUlong(diff));
ot::sLocalRadioTimeDifference = diff;
ot::TestSimpleReceiveAt();
ot::TestPendingReceiveAtReplaced();
ot::TestReceiveAtAlreadyStarted();
ot::TestReceiveAtAlreadyExpired();
ot::TestReceiveAtWhileActive();
ot::TestSleepDuringActiveReceiveAt();
ot::TestOverlappingReceiveAtSameChannel();
ot::TestOverlappingReceiveAtDifferentChannel();
ot::TestOverlappingReceiveAtExtendingPastEnd();
ot::TestAdjacentBackToBackReceiveAt();
ot::TestOverlappingReceiveAtSubsumingWindow();
ot::TestFullyMissedReceiveAt();
ot::TestInterruptedReceiveAt();
ot::TestInterruptedActiveReceiveAt();
ot::TestInterruptedReceiveAtWithSecondSchedule();
}
printf("All tests passed!\n");
#else
printf("Timed RX feature is not enabled\n");
#endif
return 0;
}