1 /*
2  *  Copyright (c) 2019, The OpenThread Authors.
3  *  All rights reserved.
4  *
5  *  Redistribution and use in source and binary forms, with or without
6  *  modification, are permitted provided that the following conditions are met:
7  *  1. Redistributions of source code must retain the above copyright
8  *     notice, this list of conditions and the following disclaimer.
9  *  2. Redistributions in binary form must reproduce the above copyright
10  *     notice, this list of conditions and the following disclaimer in the
11  *     documentation and/or other materials provided with the distribution.
12  *  3. Neither the name of the copyright holder nor the
13  *     names of its contributors may be used to endorse or promote products
14  *     derived from this software without specific prior written permission.
15  *
16  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17  *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18  *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19  *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20  *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21  *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22  *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23  *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24  *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25  *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26  *  POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 /**
30  * @file
31  *   This file includes definitions for handling indirect transmission.
32  */
33 
34 #include "indirect_sender.hpp"
35 
36 #include "instance/instance.hpp"
37 
38 namespace ot {
39 
40 #if OPENTHREAD_FTD || OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
41 
GetMacAddress(Mac::Address & aMacAddress) const42 const Mac::Address &IndirectSender::NeighborInfo::GetMacAddress(Mac::Address &aMacAddress) const
43 {
44     if (mUseShortAddress)
45     {
46         aMacAddress.SetShort(static_cast<const CslNeighbor *>(this)->GetRloc16());
47     }
48     else
49     {
50         aMacAddress.SetExtended(static_cast<const CslNeighbor *>(this)->GetExtAddress());
51     }
52 
53     return aMacAddress;
54 }
55 
IndirectSender(Instance & aInstance)56 IndirectSender::IndirectSender(Instance &aInstance)
57     : InstanceLocator(aInstance)
58     , mEnabled(false)
59 #if OPENTHREAD_FTD
60     , mSourceMatchController(aInstance)
61     , mDataPollHandler(aInstance)
62 #endif
63 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
64     , mCslTxScheduler(aInstance)
65 #endif
66 {
67 }
68 
Stop(void)69 void IndirectSender::Stop(void)
70 {
71     VerifyOrExit(mEnabled);
72 
73 #if OPENTHREAD_FTD
74     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptInvalid))
75     {
76         child.SetIndirectMessage(nullptr);
77         mSourceMatchController.ResetMessageCount(child);
78     }
79 
80     mDataPollHandler.Clear();
81 #endif
82 
83 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
84     mCslTxScheduler.Clear();
85 #endif
86 
87 exit:
88     mEnabled = false;
89 }
90 
91 #if OPENTHREAD_FTD
92 
AddMessageForSleepyChild(Message & aMessage,Child & aChild)93 void IndirectSender::AddMessageForSleepyChild(Message &aMessage, Child &aChild)
94 {
95     uint16_t childIndex;
96 
97     OT_ASSERT(!aChild.IsRxOnWhenIdle());
98 
99     childIndex = Get<ChildTable>().GetChildIndex(aChild);
100     VerifyOrExit(!aMessage.GetIndirectTxChildMask().Has(childIndex));
101 
102     aMessage.GetIndirectTxChildMask().Add(childIndex);
103     mSourceMatchController.IncrementMessageCount(aChild);
104 
105     if ((aMessage.GetType() != Message::kTypeSupervision) && (aChild.GetIndirectMessageCount() > 1))
106     {
107         Message *supervisionMessage = FindQueuedMessageForSleepyChild(aChild, AcceptSupervisionMessage);
108 
109         if (supervisionMessage != nullptr)
110         {
111             IgnoreError(RemoveMessageFromSleepyChild(*supervisionMessage, aChild));
112             Get<MeshForwarder>().RemoveMessageIfNoPendingTx(*supervisionMessage);
113         }
114     }
115 
116     RequestMessageUpdate(aChild);
117 
118 exit:
119     return;
120 }
121 
RemoveMessageFromSleepyChild(Message & aMessage,Child & aChild)122 Error IndirectSender::RemoveMessageFromSleepyChild(Message &aMessage, Child &aChild)
123 {
124     Error    error      = kErrorNone;
125     uint16_t childIndex = Get<ChildTable>().GetChildIndex(aChild);
126 
127     VerifyOrExit(aMessage.GetIndirectTxChildMask().Has(childIndex), error = kErrorNotFound);
128 
129     aMessage.GetIndirectTxChildMask().Remove(childIndex);
130     mSourceMatchController.DecrementMessageCount(aChild);
131 
132     RequestMessageUpdate(aChild);
133 
134 exit:
135     return error;
136 }
137 
ClearAllMessagesForSleepyChild(Child & aChild)138 void IndirectSender::ClearAllMessagesForSleepyChild(Child &aChild)
139 {
140     VerifyOrExit(aChild.GetIndirectMessageCount() > 0);
141 
142     for (Message &message : Get<MeshForwarder>().mSendQueue)
143     {
144         message.GetIndirectTxChildMask().Remove(Get<ChildTable>().GetChildIndex(aChild));
145 
146         Get<MeshForwarder>().RemoveMessageIfNoPendingTx(message);
147     }
148 
149     aChild.SetIndirectMessage(nullptr);
150     mSourceMatchController.ResetMessageCount(aChild);
151 
152     mDataPollHandler.RequestFrameChange(DataPollHandler::kPurgeFrame, aChild);
153 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
154     mCslTxScheduler.Update();
155 #endif
156 
157 exit:
158     return;
159 }
160 
FindQueuedMessageForSleepyChild(const Child & aChild,MessageChecker aChecker) const161 const Message *IndirectSender::FindQueuedMessageForSleepyChild(const Child &aChild, MessageChecker aChecker) const
162 {
163     const Message *match      = nullptr;
164     uint16_t       childIndex = Get<ChildTable>().GetChildIndex(aChild);
165 
166     for (const Message &message : Get<MeshForwarder>().mSendQueue)
167     {
168         if (message.GetIndirectTxChildMask().Has(childIndex) && aChecker(message))
169         {
170             match = &message;
171             break;
172         }
173     }
174 
175     return match;
176 }
177 
SetChildUseShortAddress(Child & aChild,bool aUseShortAddress)178 void IndirectSender::SetChildUseShortAddress(Child &aChild, bool aUseShortAddress)
179 {
180     VerifyOrExit(aChild.IsIndirectSourceMatchShort() != aUseShortAddress);
181 
182     mSourceMatchController.SetSrcMatchAsShort(aChild, aUseShortAddress);
183 
184 exit:
185     return;
186 }
187 
HandleChildModeChange(Child & aChild,Mle::DeviceMode aOldMode)188 void IndirectSender::HandleChildModeChange(Child &aChild, Mle::DeviceMode aOldMode)
189 {
190     if (!aChild.IsRxOnWhenIdle() && (aChild.IsStateValid()))
191     {
192         SetChildUseShortAddress(aChild, true);
193     }
194 
195     // On sleepy to non-sleepy mode change, convert indirect messages in
196     // the send queue destined to the child to direct.
197 
198     if (!aOldMode.IsRxOnWhenIdle() && aChild.IsRxOnWhenIdle() && (aChild.GetIndirectMessageCount() > 0))
199     {
200         uint16_t childIndex = Get<ChildTable>().GetChildIndex(aChild);
201 
202         for (Message &message : Get<MeshForwarder>().mSendQueue)
203         {
204             if (message.GetIndirectTxChildMask().Has(childIndex))
205             {
206                 message.GetIndirectTxChildMask().Remove(childIndex);
207                 message.SetDirectTransmission();
208                 message.SetTimestampToNow();
209             }
210         }
211 
212         aChild.SetIndirectMessage(nullptr);
213         mSourceMatchController.ResetMessageCount(aChild);
214 
215         mDataPollHandler.RequestFrameChange(DataPollHandler::kPurgeFrame, aChild);
216 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
217         mCslTxScheduler.Update();
218 #endif
219     }
220 
221     // Since the queuing delays for direct transmissions are expected to
222     // be relatively small especially when compared to indirect, for a
223     // non-sleepy to sleepy mode change, we allow any direct message
224     // (for the child) already in the send queue to remain as is. This
225     // is equivalent to dropping the already queued messages in this
226     // case.
227 }
228 
RequestMessageUpdate(Child & aChild)229 void IndirectSender::RequestMessageUpdate(Child &aChild)
230 {
231     Message *curMessage = aChild.GetIndirectMessage();
232     Message *newMessage;
233 
234     // Purge the frame if the current message is no longer destined
235     // for the child. This check needs to be done first to cover the
236     // case where we have a pending "replace frame" request and while
237     // waiting for the callback, the current message is removed.
238 
239     if ((curMessage != nullptr) && !curMessage->GetIndirectTxChildMask().Has(Get<ChildTable>().GetChildIndex(aChild)))
240     {
241         // Set the indirect message for this child to `nullptr` to ensure
242         // it is not processed on `HandleSentFrameToChild()` callback.
243 
244         aChild.SetIndirectMessage(nullptr);
245 
246         // Request a "frame purge" using `RequestFrameChange()` and
247         // wait for `HandleFrameChangeDone()` callback for completion
248         // of the request. Note that the callback may be directly
249         // called from the `RequestFrameChange()` itself when the
250         // request can be handled immediately.
251 
252         aChild.SetWaitingForMessageUpdate(true);
253         mDataPollHandler.RequestFrameChange(DataPollHandler::kPurgeFrame, aChild);
254 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
255         mCslTxScheduler.Update();
256 #endif
257 
258         ExitNow();
259     }
260 
261     VerifyOrExit(!aChild.IsWaitingForMessageUpdate());
262 
263     newMessage = FindQueuedMessageForSleepyChild(aChild, AcceptAnyMessage);
264 
265     VerifyOrExit(curMessage != newMessage);
266 
267     if (curMessage == nullptr)
268     {
269         // Current message is `nullptr`, but new message is not.
270         // We have a new indirect message.
271 
272         UpdateIndirectMessage(aChild);
273         ExitNow();
274     }
275 
276     // Current message and new message differ and are both
277     // non-`nullptr`. We need to request the frame to be replaced.
278     // The current indirect message can be replaced only if it is
279     // the first fragment. If a next fragment frame for message is
280     // already prepared, we wait for the entire message to be
281     // delivered.
282 
283     VerifyOrExit(aChild.GetIndirectFragmentOffset() == 0);
284 
285     aChild.SetWaitingForMessageUpdate(true);
286     mDataPollHandler.RequestFrameChange(DataPollHandler::kReplaceFrame, aChild);
287 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
288     mCslTxScheduler.Update();
289 #endif
290 
291 exit:
292     return;
293 }
294 
HandleFrameChangeDone(Child & aChild)295 void IndirectSender::HandleFrameChangeDone(Child &aChild)
296 {
297     VerifyOrExit(aChild.IsWaitingForMessageUpdate());
298     UpdateIndirectMessage(aChild);
299 
300 exit:
301     return;
302 }
303 
UpdateIndirectMessage(Child & aChild)304 void IndirectSender::UpdateIndirectMessage(Child &aChild)
305 {
306     Message *message = FindQueuedMessageForSleepyChild(aChild, AcceptAnyMessage);
307 
308     aChild.SetWaitingForMessageUpdate(false);
309     aChild.SetIndirectMessage(message);
310     aChild.SetIndirectFragmentOffset(0);
311     aChild.SetIndirectTxSuccess(true);
312 
313 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
314     mCslTxScheduler.Update();
315 #endif
316 
317     if (message != nullptr)
318     {
319         Mac::Address childAddress;
320 
321         aChild.GetMacAddress(childAddress);
322         Get<MeshForwarder>().LogMessage(MeshForwarder::kMessagePrepareIndirect, *message, kErrorNone, &childAddress);
323     }
324 }
325 
PrepareFrameForChild(Mac::TxFrame & aFrame,FrameContext & aContext,Child & aChild)326 Error IndirectSender::PrepareFrameForChild(Mac::TxFrame &aFrame, FrameContext &aContext, Child &aChild)
327 {
328     Error    error   = kErrorNone;
329     Message *message = aChild.GetIndirectMessage();
330 
331     VerifyOrExit(mEnabled, error = kErrorAbort);
332 
333     if (message == nullptr)
334     {
335         PrepareEmptyFrame(aFrame, aChild, /* aAckRequest */ true);
336         aContext.mMessageNextOffset = 0;
337         ExitNow();
338     }
339 
340     switch (message->GetType())
341     {
342     case Message::kTypeIp6:
343         aContext.mMessageNextOffset = PrepareDataFrame(aFrame, aChild, *message);
344         break;
345 
346     case Message::kTypeSupervision:
347         PrepareEmptyFrame(aFrame, aChild, /* aAckRequest */ true);
348         aContext.mMessageNextOffset = message->GetLength();
349         break;
350 
351     default:
352         OT_ASSERT(false);
353     }
354 
355 exit:
356     return error;
357 }
358 
PrepareDataFrame(Mac::TxFrame & aFrame,Child & aChild,Message & aMessage)359 uint16_t IndirectSender::PrepareDataFrame(Mac::TxFrame &aFrame, Child &aChild, Message &aMessage)
360 {
361     Ip6::Header    ip6Header;
362     Mac::Addresses macAddrs;
363     uint16_t       directTxOffset;
364     uint16_t       nextOffset;
365 
366     // Determine the MAC source and destination addresses.
367 
368     IgnoreError(aMessage.Read(0, ip6Header));
369 
370     Get<MeshForwarder>().GetMacSourceAddress(ip6Header.GetSource(), macAddrs.mSource);
371 
372     if (ip6Header.GetDestination().IsLinkLocalUnicast())
373     {
374         Get<MeshForwarder>().GetMacDestinationAddress(ip6Header.GetDestination(), macAddrs.mDestination);
375     }
376     else
377     {
378         aChild.GetMacAddress(macAddrs.mDestination);
379     }
380 
381     // Prepare the data frame from previous child's indirect offset.
382 
383     directTxOffset = aMessage.GetOffset();
384     aMessage.SetOffset(aChild.GetIndirectFragmentOffset());
385 
386     nextOffset = Get<MeshForwarder>().PrepareDataFrameWithNoMeshHeader(aFrame, aMessage, macAddrs);
387 
388     aMessage.SetOffset(directTxOffset);
389 
390     // Set `FramePending` if there are more queued messages (excluding
391     // the current one being sent out) for the child (note `> 1` check).
392     // The case where the current message itself requires fragmentation
393     // is already checked and handled in `PrepareDataFrame()` method.
394 
395     if (aChild.GetIndirectMessageCount() > 1)
396     {
397         aFrame.SetFramePending(true);
398     }
399 
400     return nextOffset;
401 }
402 
PrepareEmptyFrame(Mac::TxFrame & aFrame,Child & aChild,bool aAckRequest)403 void IndirectSender::PrepareEmptyFrame(Mac::TxFrame &aFrame, Child &aChild, bool aAckRequest)
404 {
405     Mac::Address macDest;
406     aChild.GetMacAddress(macDest);
407     Get<MeshForwarder>().PrepareEmptyFrame(aFrame, macDest, aAckRequest);
408 }
409 
HandleSentFrameToChild(const Mac::TxFrame & aFrame,const FrameContext & aContext,Error aError,Child & aChild)410 void IndirectSender::HandleSentFrameToChild(const Mac::TxFrame &aFrame,
411                                             const FrameContext &aContext,
412                                             Error               aError,
413                                             Child              &aChild)
414 {
415     Message *message    = aChild.GetIndirectMessage();
416     uint16_t nextOffset = aContext.mMessageNextOffset;
417 
418     VerifyOrExit(mEnabled);
419 
420     if (aError == kErrorNone)
421     {
422         Get<ChildSupervisor>().UpdateOnSend(aChild);
423     }
424 
425     // A zero `nextOffset` indicates that the sent frame is an empty
426     // frame generated by `PrepareFrameForChild()` when there was no
427     // indirect message in the send queue for the child. This can happen
428     // in the (not common) case where the radio platform does not
429     // support the "source address match" feature and always includes
430     // "frame pending" flag in acks to data poll frames. In such a case,
431     // `IndirectSender` prepares and sends an empty frame to the child
432     // after it sends a data poll. Here in `HandleSentFrameToChild()` we
433     // exit quickly if we detect the "send done" is for the empty frame
434     // to ensure we do not update any newly added indirect message after
435     // preparing the empty frame.
436 
437     VerifyOrExit(nextOffset != 0);
438 
439     switch (aError)
440     {
441     case kErrorNone:
442         break;
443 
444     case kErrorNoAck:
445     case kErrorChannelAccessFailure:
446     case kErrorAbort:
447 
448         aChild.SetIndirectTxSuccess(false);
449 
450 #if OPENTHREAD_CONFIG_DROP_MESSAGE_ON_FRAGMENT_TX_FAILURE
451         // We set the nextOffset to end of message, since there is no need to
452         // send any remaining fragments in the message to the child, if all tx
453         // attempts of current frame already failed.
454 
455         if (message != nullptr)
456         {
457             nextOffset = message->GetLength();
458         }
459 #endif
460         break;
461 
462     default:
463         OT_ASSERT(false);
464     }
465 
466     if ((message != nullptr) && (nextOffset < message->GetLength()))
467     {
468         aChild.SetIndirectFragmentOffset(nextOffset);
469 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
470         mCslTxScheduler.Update();
471 #endif
472         ExitNow();
473     }
474 
475     if (message != nullptr)
476     {
477         // The indirect tx of this message to the child is done.
478 
479         Error        txError    = aError;
480         uint16_t     childIndex = Get<ChildTable>().GetChildIndex(aChild);
481         Mac::Address macDest;
482 
483         aChild.SetIndirectMessage(nullptr);
484         aChild.GetLinkInfo().AddMessageTxStatus(aChild.GetIndirectTxSuccess());
485 
486         // Enable short source address matching after the first indirect
487         // message transmission attempt to the child. We intentionally do
488         // not check for successful tx here to address the scenario where
489         // the child does receive "Child ID Response" but parent misses the
490         // 15.4 ack from child. If the "Child ID Response" does not make it
491         // to the child, then the child will need to send a new "Child ID
492         // Request" which will cause the parent to switch to using long
493         // address mode for source address matching.
494 
495         mSourceMatchController.SetSrcMatchAsShort(aChild, true);
496 
497 #if !OPENTHREAD_CONFIG_DROP_MESSAGE_ON_FRAGMENT_TX_FAILURE
498 
499         // When `CONFIG_DROP_MESSAGE_ON_FRAGMENT_TX_FAILURE` is
500         // disabled, all fragment frames of a larger message are
501         // sent even if the transmission of an earlier fragment fail.
502         // Note that `GetIndirectTxSuccess() tracks the tx success of
503         // the entire message to the child, while `txError = aError`
504         // represents the error status of the last fragment frame
505         // transmission.
506 
507         if (!aChild.GetIndirectTxSuccess() && (txError == kErrorNone))
508         {
509             txError = kErrorFailed;
510         }
511 #endif
512 
513         if (!aFrame.IsEmpty())
514         {
515             IgnoreError(aFrame.GetDstAddr(macDest));
516             Get<MeshForwarder>().LogMessage(MeshForwarder::kMessageTransmit, *message, txError, &macDest);
517         }
518 
519         if (message->GetType() == Message::kTypeIp6)
520         {
521             if (aChild.GetIndirectTxSuccess())
522             {
523                 Get<MeshForwarder>().mIpCounters.mTxSuccess++;
524             }
525             else
526             {
527                 Get<MeshForwarder>().mIpCounters.mTxFailure++;
528             }
529         }
530 
531         if (message->GetIndirectTxChildMask().Has(childIndex))
532         {
533             message->GetIndirectTxChildMask().Remove(childIndex);
534             mSourceMatchController.DecrementMessageCount(aChild);
535         }
536 
537         Get<MeshForwarder>().RemoveMessageIfNoPendingTx(*message);
538     }
539 
540     UpdateIndirectMessage(aChild);
541 
542 exit:
543     if (mEnabled)
544     {
545         ClearMessagesForRemovedChildren();
546     }
547 }
548 
ClearMessagesForRemovedChildren(void)549 void IndirectSender::ClearMessagesForRemovedChildren(void)
550 {
551     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptValidOrRestoring))
552     {
553         if (child.GetIndirectMessageCount() == 0)
554         {
555             continue;
556         }
557 
558         ClearAllMessagesForSleepyChild(child);
559     }
560 }
561 
AcceptAnyMessage(const Message & aMessage)562 bool IndirectSender::AcceptAnyMessage(const Message &aMessage)
563 {
564     OT_UNUSED_VARIABLE(aMessage);
565 
566     return true;
567 }
568 
AcceptSupervisionMessage(const Message & aMessage)569 bool IndirectSender::AcceptSupervisionMessage(const Message &aMessage)
570 {
571     return aMessage.GetType() == Message::kTypeSupervision;
572 }
573 
574 #endif // OPENTHREAD_FTD
575 
576 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
577 
PrepareFrameForCslNeighbor(Mac::TxFrame & aFrame,FrameContext & aContext,CslNeighbor & aCslNeighbor)578 Error IndirectSender::PrepareFrameForCslNeighbor(Mac::TxFrame &aFrame,
579                                                  FrameContext &aContext,
580                                                  CslNeighbor  &aCslNeighbor)
581 {
582     Error error = kErrorNotFound;
583 
584 #if OPENTHREAD_FTD
585     // `CslNeighbor` can only be a `Child` for now, but can be changed later.
586     error = PrepareFrameForChild(aFrame, aContext, static_cast<Child &>(aCslNeighbor));
587 #else
588     OT_UNUSED_VARIABLE(aFrame);
589     OT_UNUSED_VARIABLE(aContext);
590     OT_UNUSED_VARIABLE(aCslNeighbor);
591 #endif
592 
593     return error;
594 }
595 
HandleSentFrameToCslNeighbor(const Mac::TxFrame & aFrame,const FrameContext & aContext,Error aError,CslNeighbor & aCslNeighbor)596 void IndirectSender::HandleSentFrameToCslNeighbor(const Mac::TxFrame &aFrame,
597                                                   const FrameContext &aContext,
598                                                   Error               aError,
599                                                   CslNeighbor        &aCslNeighbor)
600 {
601 #if OPENTHREAD_FTD
602     HandleSentFrameToChild(aFrame, aContext, aError, static_cast<Child &>(aCslNeighbor));
603 #else
604     OT_UNUSED_VARIABLE(aFrame);
605     OT_UNUSED_VARIABLE(aContext);
606     OT_UNUSED_VARIABLE(aError);
607     OT_UNUSED_VARIABLE(aCslNeighbor);
608 #endif
609 }
610 
611 #endif // OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
612 
613 #endif // OPENTHREAD_FTD || OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
614 
615 } // namespace ot
616