Files
openthread/tests/unit/test_sub_mac_recv_at.cpp
Abtin Keshavarzian 5c8c318627 [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.
2026-08-31 14:27:24 -07:00

987 lines
31 KiB
C++

/*
* 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;
}