Repository: openthread/openthread (language: python)

Issue:
[Thread in Mobile] List of PRs and discussion
This issue is used to track all PRs and discussions related to the implementation of Thread in Mobile functionality defined in https://threadgroup.atlassian.net/browse/SPEC-1267 that enables direct communication between two sleepy devices.

The prototype of the functionality has been fully implemented and demonstrated on Nordic SoCs. The entire change set is available at https://github.com/nrfconnect/sdk-openthread/commit/collab-thread-s2s but it will be contributed in parts according to the below list to enable more effective code review process and API design discussions.

List of PRs (posted or planned; the list may evolve over time):
- [x] [Multipurpose wake-up frame support](https://github.com/openthread/openthread/pull/10728)
- [x] [Dataset CSL Wake-up Channel TLV support](https://github.com/openthread/openthread/pull/10736)
- [x] [Wake-up frame periodic sniffing](https://github.com/openthread/openthread/pull/10762)
- [x] [Wake-up frame sequence transmission](https://github.com/openthread/openthread/pull/10771)
- [ ] [[child-supervision] Support Short Supervision Interval TLV](https://github.com/openthread/openthread/pull/10980)
- [ ] [Send Parent Request when receiving a wake-up frame](https://github.com/openthread/openthread/pull/10842)
- [ ] MLE on wake-up sequence transmission (WC side)
- [ ] eCSL support in POSIX platform

Followups:
- [x] [Use `FrameBuilder` class to generate a wake-up frame](https://github.com/openthread/openthread/pull/10728#discussion_r1767603402)
- [ ] Move wake-up frame security check to `SubMac`.
- [ ] Extend MAC counters with Multipurpose frames.
- [ ] [Add LinkRaw support for WED](https://github.com/openthread/openthread/pull/10762#discussion_r1793076907)

Related resources:
- [Wireshark fix for Multipurpose frame parsing](https://gitlab.com/wireshark/wireshark/-/merge_requests/17419)

[mac] add multipurpose frame support
This commit sets the `Mac::Frame` to be able to generate and parse the 15.4 multipurpose frame.

Implements changes that support [SPEC-1267](https://threadgroup.atlassian.net/browse/SPEC-1267).
[mac] add multipurpose frame support
This commit sets the `Mac::Frame` to be able to generate and parse the 15.4 multipurpose frame.

Implements changes that support [SPEC-1267](https://threadgroup.atlassian.net/browse/SPEC-1267).


Discussion / hints:
The prototype requires the radio driver's support. Is there a guide for us to get the radio driver to build this prototype on nrf52840DK? And there are lots of compile errors when I build it on simulation platform via the command `./script/cmake-build simulation -DOT_MTD=ON -DOT_CSL_PERIPHERAL=ON -DOT_FTD=OFF -DOT_CSL_RECEIVER=ON`.


# Size Report of **OpenThread**
> Merging [#10648](https://github.com/openthread/openthread/commit/f3d9d0b299a7b32c82ee4048f7bece29781fd422) into [main](https://github.com/openthread/openthread/commit/01cb5b08cf1dde2397de036b393d92c77c64cb80)(01cb5b08cf1dde2397de036b393d92c77c64cb80).

|  name  |  branch  |  text  | data  | bss  | total |
| :----: | :------: | -----: | ----: | ---: | ----: |
| ot-cli-ftd | main | 469368 | 856 | 66484 | 536708 |
|  | #10648 | 469560 | 856 | 66484 | 536900 |
|  | +/- | +192 | 0 | 0 | +192 |
| ot-ncp-ftd | main | 437948 | 760 | 61712 | 500420 |
|  | #10648 | 438156 | 760 | 61712 | 500628 |
|  | +/- | +208 | 0 | 0 | +208 |
| libopenthread-ftd.a | main | 237651 | 95 | 40414 | 278160 |
|  | #10648 | 237861 | 95 | 40414 | 278370 |
|  | +/- | +210 | 0 | 0 | +210 |
| libopenthread-cli-ftd.a | main | 58712 | 0 | 8075 | 66787 |
|  | #10648 | 58712 | 0 | 8075 | 66787 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ncp-ftd.a | main | 32541 | 0 | 5924 | 38465 |
|  | #10648 | 32545 | 0 | 5924 | 38469 |
|  | +/- | +4 | 0 | 0 | +4 |
| ot-cli-mtd | main | 366320 | 760 | 51292 | 418372 |
|  | #10648 | 366512 | 760 | 51292 | 418564 |
|  | +/- | +192 | 0 | 0 | +192 |
| ot-ncp-mtd | main | 348996 | 760 | 46536 | 396292 |
|  | #10648 | 349172 | 760 | 46536 | 396468 |
|  | +/- | +176 | 0 | 0 | +176 |
| libopenthread-mtd.a | main | 159853 | 0 | 25238 | 185091 |
|  | #10648 | 160051 | 0 | 25238 | 185289 |
|  | +/- | +198 | 0 | 0 | +198 |
| libopenthread-cli-mtd.a | main | 39795 | 0 | 8059 | 47854 |
|  | #10648 | 39795 | 0 | 8059 | 47854 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ncp-mtd.a | main | 25349 | 0 | 5924 | 31273 |
|  | #10648 | 25353 | 0 | 5924 | 31277 |
|  | +/- | +4 | 0 | 0 | +4 |
| ot-cli-ftd-br | main | 554768 | 864 | 131348 | 686980 |
|  | #10648 | 554960 | 864 | 131348 | 687172 |
|  | +/- | +192 | 0 | 0 | +192 |
| libopenthread-ftd-br.a | main | 327141 | 100 | 105254 | 432495 |
|  | #10648 | 327347 | 100 | 105254 | 432701 |
|  | +/- | +206 | 0 | 0 | +206 |
| libopenthread-cli-ftd-br.a | main | 72843 | 0 | 8099 | 80942 |
|  | #10648 | 72843 | 0 | 8099 | 80942 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-rcp | main | 62712 | 564 | 20644 | 83920 |
|  | #10648 | 62856 | 564 | 20644 | 84064 |
|  | +/- | +144 | 0 | 0 | +144 |
| libopenthread-rcp.a | main | 9750 | 0 | 5060 | 14810 |
|  | #10648 | 9754 | 0 | 5060 | 14814 |
|  | +/- | +4 | 0 | 0 | +4 |
| libopenthread-radio.a | main | 19305 | 0 | 222 | 19527 |
|  | #10648 | 19483 | 0 | 222 | 19705 |
|  | +/- | +178 | 0 | 0 | +178 |



# Size Report of **OpenThread**
> Merging [#10648](https://github.com/openthread/openthread/commit/f3d9d0b299a7b32c82ee4048f7bece29781fd422) into [main](https://github.com/openthread/openthread/commit/01cb5b08cf1dde2397de036b393d92c77c64cb80)(01cb5b08cf1dde2397de036b393d92c77c64cb80).

|  name  |  branch  |  text  | data  | bss  | total |
| :----: | :------: | -----: | ----: | ---: | ----: |
| ot-cli-ftd | main | 469368 | 856 | 66484 | 536708 |
|  | #10648 | 469560 | 856 | 66484 | 536900 |
|  | +/- | +192 | 0 | 0 | +192 |
| ot-ncp-ftd | main | 437948 | 760 | 61712 | 500420 |
|  | #10648 | 438156 | 760 | 61712 | 500628 |
|  | +/- | +208 | 0 | 0 | +208 |
| libopenthread-ftd.a | main | 237651 | 95 | 40414 | 278160 |
|  | #10648 | 237861 | 95 | 40414 | 278370 |
|  | +/- | +210 | 0 | 0 | +210 |
| libopenthread-cli-ftd.a | main | 58712 | 0 | 8075 | 66787 |
|  | #10648 | 58712 | 0 | 8075 | 66787 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ncp-ftd.a | main | 32541 | 0 | 5924 | 38465 |
|  | #10648 | 32545 | 0 | 5924 | 38469 |
|  | +/- | +4 | 0 | 0 | +4 |
| ot-cli-mtd | main | 366320 | 760 | 51292 | 418372 |
|  | #10648 | 366512 | 760 | 51292 | 418564 |
|  | +/- | +192 | 0 | 0 | +192 |
| ot-ncp-mtd | main | 348996 | 760 | 46536 | 396292 |
|  | #10648 | 349172 | 760 | 46536 | 396468 |
|  | +/- | +176 | 0 | 0 | +176 |
| libopenthread-mtd.a | main | 159853 | 0 | 25238 | 185091 |
|  | #10648 | 160051 | 0 | 25238 | 185289 |
|  | +/- | +198 | 0 | 0 | +198 |
| libopenthread-cli-mtd.a | main | 39795 | 0 | 8059 | 47854 |
|  | #10648 | 39795 | 0 | 8059 | 47854 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ncp-mtd.a | main | 25349 | 0 | 5924 | 31273 |
|  | #10648 | 25353 | 0 | 5924 | 31277 |
|  | +/- | +4 | 0 | 0 | +4 |
| ot-cli-ftd-br | main | 554768 | 864 | 131348 | 686980 |
|  | #10648 | 554960 | 864 | 131348 | 687172 |
|  | +/- | +192 | 0 | 0 | +192 |
| libopenthread-ftd-br.a | main | 327141 | 100 | 105254 | 432495 |
|  | #10648 | 327347 | 100 | 105254 | 432701 |
|  | +/- | +206 | 0 | 0 | +206 |
| libopenthread-cli-ftd-br.a | main | 72843 | 0 | 8099 | 80942 |
|  | #10648 | 72843 | 0 | 8099 | 80942 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-rcp | main | 62712 | 564 | 20644 | 83920 |
|  | #10648 | 62856 | 564 | 20644 | 84064 |
|  | +/- | +144 | 0 | 0 | +144 |
| libopenthread-rcp.a | main | 9750 | 0 | 5060 | 14810 |
|  | #10648 | 9754 | 0 | 5060 | 14814 |
|  | +/- | +4 | 0 | 0 | +4 |
| libopenthread-radio.a | main | 19305 | 0 | 222 | 19527 |
|  | #10648 | 19483 | 0 | 222 | 19705 |
|  | +/- | +178 | 0 | 0 | +178 |


Fix patch:
diff --git a/doc/ot_config_doc.h b/doc/ot_config_doc.h
index c61b92e35ba..96d8f503869 100644
--- a/doc/ot_config_doc.h
+++ b/doc/ot_config_doc.h
@@ -85,6 +85,7 @@
  * @defgroup config-time-sync                Time Sync Service
  * @defgroup config-tmf                      Thread Management Framework Service
  * @defgroup config-trel                     TREL
+ * @defgroup config-wakeup                   Wake-up
  *
  * @}
  *
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index f1669465246..d85360b1e32 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -256,6 +256,8 @@ ot_option(OT_TX_QUEUE_STATS OPENTHREAD_CONFIG_TX_QUEUE_STATISTICS_ENABLE "tx que
 ot_option(OT_UDP_FORWARD OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE "UDP forward")
 ot_option(OT_UPTIME OPENTHREAD_CONFIG_UPTIME_ENABLE "uptime")
 ot_option(OT_VERHOEFF_CHECKSUM OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE "verhoeff checksum")
+ot_option(OT_WAKEUP_COORDINATOR OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE "wake-up coordinator")
+ot_option(OT_WAKEUP_END_DEVICE OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE "wake-up end device")
 
 option(OT_DOC "build OpenThread documentation")
 message(STATUS "- - - - - - - - - - - - - - - - ")
diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn
index 5d2daef98cf..ac5f933891b 100644
--- a/src/core/BUILD.gn
+++ b/src/core/BUILD.gn
@@ -842,6 +842,7 @@ source_set("libopenthread_core_config") {
     "config/time_sync.h",
     "config/tmf.h",
     "config/trel.h",
+    "config/wakeup.h",
     "openthread-core-config.h",
   ]
   public_configs = [
diff --git a/src/core/config/mac.h b/src/core/config/mac.h
index ec7e2f9d29b..ec07918126e 100644
--- a/src/core/config/mac.h
+++ b/src/core/config/mac.h
@@ -46,6 +46,7 @@
  */
 
 #include "config/time_sync.h"
+#include "config/wakeup.h"
 
 /**
  * @def OPENTHREAD_CONFIG_MAC_MAX_CSMA_BACKOFFS_DIRECT
@@ -435,6 +436,17 @@
 #define OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE 0
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+ *
+ * Define to 1 to enable support for IEEE 802.15.4 MAC Multipurpose frame format.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+#define OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME \
+    (OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE)
+#endif
+
 /**
  * @def OPENTHREAD_CONFIG_MAC_CSL_AUTO_SYNC_ENABLE
  *
diff --git a/src/core/config/wakeup.h b/src/core/config/wakeup.h
new file mode 100644
index 00000000000..a473e1e4232
--- /dev/null
+++ b/src/core/config/wakeup.h
@@ -0,0 +1,75 @@
+/*
+ *  Copyright (c) 2024, 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.
+ */
+
+/**
+ * @file
+ *   This file includes compile-time configurations for the Wake-up Coordinator and Wake-up End Device roles.
+ *
+ */
+
+#ifndef CONFIG_WAKEUP_H_
+#define CONFIG_WAKEUP_H_
+
+/**
+ * @addtogroup config-wakeup
+ *
+ * @brief
+ *   This module includes configuration variables for the Wake-up Coordinator and Wake-up End Device roles.
+ *
+ * @{
+ *
+ */
+
+/**
+ * @def OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+ *
+ * Define to 1 to enable the Wake-up Coordinator role that is capable of establishing
+ * a link with one or more Wake-up End Devices by sending a sequence of wake-up frames.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+#define OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+ *
+ * Define to 1 to enable the Wake-up End Device role that periodically listens for wake-up
+ * frames to establish a link with a Wake-up Coordinator device.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+#define OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE 0
+#endif
+
+/**
+ * @}
+ *
+ */
+
+#endif // CONFIG_WAKEUP_H_
diff --git a/src/core/mac/mac_frame.cpp b/src/core/mac/mac_frame.cpp
index 8400a194f16..3e2bb374365 100644
--- a/src/core/mac/mac_frame.cpp
+++ b/src/core/mac/mac_frame.cpp
@@ -39,6 +39,7 @@
 #include "common/debug.hpp"
 #include "common/frame_builder.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "radio/trel_link.hpp"
 #if !OPENTHREAD_RADIO || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
 #include "crypto/aes_ccm.hpp"
@@ -193,13 +194,13 @@ void Frame::InitMacHeader(Type             aType,
 
     if (aSuppressSequence)
     {
-        fcf |= kFcfSequenceSupression;
+        fcf |= kFcfSequenceSuppression;
     }
 
     builder.Init(mPsdu, GetMtu());
     IgnoreError(builder.AppendLittleEndianUint16(fcf));
 
-    if (!IsSequenceSuppressed(fcf))
+    if (IsSequencePresent(fcf))
     {
         IgnoreError(builder.AppendUint8(0)); // Seq number
     }
@@ -238,9 +239,20 @@ void Frame::InitMacHeader(Type             aType,
     mLength += GetFcsSize();
 }
 
-uint16_t Frame::GetFrameControlField(void) const { return LittleEndian::ReadUint16(mPsdu); }
-
-void Frame::SetFrameControlField(uint16_t aFcf) { LittleEndian::WriteUint16(aFcf, mPsdu); }
+void Frame::SetFrameControlField(uint16_t aFcf)
+{
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+    if (IsShortFcf(aFcf))
+    {
+        OT_ASSERT((aFcf >> 8) == 0);
+        mPsdu[0] = static_cast<uint8_t>(aFcf);
+    }
+    else
+#endif
+    {
+        LittleEndian::WriteUint16(aFcf, mPsdu);
+    }
+}
 
 Error Frame::ValidatePsdu(void) const
 {
@@ -254,53 +266,115 @@ Error Frame::ValidatePsdu(void) const
     return error;
 }
 
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+bool Frame::IsWakeupFrame(void) const
+{
+    const uint16_t fcf    = GetFrameControlField();
+    bool           result = false;
+    uint8_t        keyIdMode;
+    uint8_t        firstIeIndex;
+    Address        srcAddress;
+
+    // Wake-up frame is a Multipurpose frame without Ack Request...
+    VerifyOrExit((fcf & kFcfFrameTypeMask) == kTypeMultipurpose);
+    VerifyOrExit((fcf & kMpFcfAckRequest) == 0);
+
+    // ... with extended source address...
+    SuccessOrExit(GetSrcAddr(srcAddress));
+    VerifyOrExit(srcAddress.IsExtended());
+
+    // ... secured with Key Id Mode 2...
+    SuccessOrExit(GetKeyIdMode(keyIdMode));
+    VerifyOrExit(keyIdMode == kKeyIdMode2);
+
+    // ... that has Rendezvous Time IE and Connection IE...
+    VerifyOrExit(GetRendezvousTimeIe() != nullptr);
+    VerifyOrExit(GetConnectionIe() != nullptr);
+
+    // ... but no other IEs nor payload.
+    firstIeIndex = FindHeaderIeIndex();
+    VerifyOrExit(mPsdu + firstIeIndex + sizeof(HeaderIe) + RendezvousTimeIe::kIeContentSize + sizeof(HeaderIe) +
+                     ConnectionIe::kIeContentSize ==
+                 GetFooter());
+
+    result = true;
+
+exit:
+    return result;
+}
+#endif // OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+
 void Frame::SetAckRequest(bool aAckRequest)
 {
+    uint16_t fcf  = GetFrameControlField();
+    uint16_t mask = Select<kFcfAckRequest, kMpFcfAckRequest>(fcf);
+
     if (aAckRequest)
     {
-        mPsdu[0] |= kFcfAckRequest;
+        fcf |= mask;
     }
     else
     {
-        mPsdu[0] &= ~kFcfAckRequest;
+        fcf &= ~mask;
     }
+
+    SetFrameControlField(fcf);
 }
 
 void Frame::SetFramePending(bool aFramePending)
 {
+    uint16_t fcf  = GetFrameControlField();
+    uint16_t mask = Select<kFcfFramePending, kMpFcfFramePending>(fcf);
+
     if (aFramePending)
     {
-        mPsdu[0] |= kFcfFramePending;
+        fcf |= mask;
     }
     else
     {
-        mPsdu[0] &= ~kFcfFramePending;
+        fcf &= ~mask;
     }
+
+    SetFrameControlField(fcf);
 }
 
 void Frame::SetIePresent(bool aIePresent)
 {
-    uint16_t fcf = GetFrameControlField();
+    uint16_t fcf  = GetFrameControlField();
+    uint16_t mask = Select<kFcfIePresent, kMpFcfIePresent>(fcf);
 
     if (aIePresent)
     {
-        fcf |= kFcfIePresent;
+        fcf |= mask;
     }
     else
     {
-        fcf &= ~kFcfIePresent;
+        fcf &= ~mask;
     }
 
     SetFrameControlField(fcf);
 }
 
+uint8_t Frame::SkipSequenceIndex(void) const
+{
+    uint16_t fcf   = GetFrameControlField();
+    uint8_t  index = GetFcfSize(fcf);
+
+    if (IsSequencePresent(fcf))
+    {
+        index += kDsnSize;
+    }
+
+    return index;
+}
+
 uint8_t Frame::FindDstPanIdIndex(void) const
 {
     uint8_t index;
 
     VerifyOrExit(IsDstPanIdPresent(), index = kInvalidIndex);
 
-    index = kFcfSize + GetSeqNumSize();
+    index = SkipSequenceIndex();
 
 exit:
     return index;
@@ -308,9 +382,16 @@ uint8_t Frame::FindDstPanIdIndex(void) const
 
 bool Frame::IsDstPanIdPresent(uint16_t aFcf)
 {
-    bool present = true;
+    bool present;
 
-    if (IsVersion2015(aFcf))
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+    if (IsMultipurpose(aFcf))
+    {
+        present = (aFcf & kMpFcfPanidPresent) != 0;
+    }
+    else
+#endif
+        if (IsVersion2015(aFcf))
     {
         // Original table at `InitMacHeader()`
         //
@@ -348,6 +429,7 @@ bool Frame::IsDstPanIdPresent(uint16_t aFcf)
             present = false;
             break;
         default:
+            present = true;
             break;
         }
     }
@@ -382,20 +464,19 @@ void Frame::SetDstPanId(PanId aPanId)
 uint8_t Frame::GetSequence(void) const
 {
     OT_ASSERT(IsSequencePresent());
-    return GetPsdu()[kSequenceIndex];
+
+    return GetPsdu()[GetFcfSize(GetFrameControlField())];
 }
 
 void Frame::SetSequence(uint8_t aSequence)
 {
     OT_ASSERT(IsSequencePresent());
-    GetPsdu()[kSequenceIndex] = aSequence;
-}
 
-uint8_t Frame::FindDstAddrIndex(void) const
-{
-    return kFcfSize + GetSeqNumSize() + (IsDstPanIdPresent() ? sizeof(PanId) : 0);
+    GetPsdu()[GetFcfSize(GetFrameControlField())] = aSequence;
 }
 
+uint8_t Frame::FindDstAddrIndex(void) const { return SkipSequenceIndex() + (IsDstPanIdPresent() ? sizeof(PanId) : 0); }
+
 Error Frame::GetDstAddr(Address &aAddress) const
 {
     Error   error = kErrorNone;
@@ -403,13 +484,13 @@ Error Frame::GetDstAddr(Address &aAddress) const
 
     VerifyOrExit(index != kInvalidIndex, error = kErrorParse);
 
-    switch (GetFrameControlField() & kFcfDstAddrMask)
+    switch (GetFcfDstAddr(GetFrameControlField()))
     {
-    case kFcfDstAddrShort:
+    case kFcfAddrShort:
         aAddress.SetShort(LittleEndian::ReadUint16(&mPsdu[index]));
         break;
 
-    case kFcfDstAddrExt:
+    case kFcfAddrExt:
         aAddress.SetExtended(&mPsdu[index], ExtAddress::kReverseByteOrder);
         break;
 
@@ -424,7 +505,7 @@ Error Frame::GetDstAddr(Address &aAddress) const
 
 void Frame::SetDstAddr(ShortAddress aShortAddress)
 {
-    OT_ASSERT((GetFrameControlField() & kFcfDstAddrMask) == kFcfDstAddrShort);
+    OT_ASSERT(GetFcfDstAddr(GetFrameControlField()) == kFcfAddrShort);
     LittleEndian::WriteUint16(aShortAddress, &mPsdu[FindDstAddrIndex()]);
 }
 
@@ -432,7 +513,7 @@ void Frame::SetDstAddr(const ExtAddress &aExtAddress)
 {
     uint8_t index = FindDstAddrIndex();
 
-    OT_ASSERT((GetFrameControlField() & kFcfDstAddrMask) == kFcfDstAddrExt);
+    OT_ASSERT(GetFcfDstAddr(GetFrameControlField()) == kFcfAddrExt);
     OT_ASSERT(index != kInvalidIndex);
 
     aExtAddress.CopyTo(&mPsdu[index], ExtAddress::kReverseByteOrder);
@@ -458,25 +539,25 @@ void Frame::SetDstAddr(const Address &aAddress)
 
 uint8_t Frame::FindSrcPanIdIndex(void) const
 {
-    uint8_t  index = 0;
-    uint16_t fcf   = GetFrameControlField();
+    uint16_t fcf = GetFrameControlField();
+    uint8_t  index;
 
-    VerifyOrExit(IsSrcPanIdPresent(), index = kInvalidIndex);
+    VerifyOrExit(IsSrcPanIdPresent(fcf), index = kInvalidIndex);
 
-    index += kFcfSize + GetSeqNumSize();
+    index = SkipSequenceIndex();
 
     if (IsDstPanIdPresent(fcf))
     {
         index += sizeof(PanId);
     }
 
-    switch (fcf & kFcfDstAddrMask)
+    switch (GetFcfDstAddr(fcf))
     {
-    case kFcfDstAddrShort:
+    case kFcfAddrShort:
         index += sizeof(ShortAddress);
         break;
 
-    case kFcfDstAddrExt:
+    case kFcfAddrExt:
         index += sizeof(ExtAddress);
         break;
     }
@@ -487,40 +568,52 @@ uint8_t Frame::FindSrcPanIdIndex(void) const
 
 bool Frame::IsSrcPanIdPresent(uint16_t aFcf)
 {
-    bool present = IsSrcAddrPresent(aFcf) && ((aFcf & kFcfPanidCompression) == 0);
-
-    // Special case for a IEEE 802.15.4-2015 frame: When both
-    // addresses are extended, then the source PAN iD is not present
-    // independent of PAN ID Compression. In this case, if the PAN ID
-    // compression is set, it indicates that no PAN ID is in the
-    // frame, while if the PAN ID Compression is zero, it indicates
-    // the presence of the destination PAN ID in the frame.
-    //
-    // +----+--------------+--------------+--------------++--------------+
-    // | No |  Dest Addr   |   Src Addr   |  PAN ID Comp ||  Src PAN ID  |
-    // +----+--------------+--------------+--------------++--------------+
-    // |  1 | Not Present  | Not Present  |      0       || Not Present  |
-    // |  2 | Not Present  | Not Present  |      1       || Not Present  |
-    // |  3 | Present      | Not Present  |      0       || Not Present  |
-    // |  4 | Present      | Not Present  |      1       || Not Present  |
-    // |  5 | Not Present  | Present      |      0       || Present      |
-    // |  6 | Not Present  | Present      |      1       || Not Present  |
-    // +----+--------------+--------------+--------------++--------------+
-    // |  7 | Extended     | Extended     |      0       || Not Present  |
-    // |  8 | Extended     | Extended     |      1       || Not Present  |
-    // |----+--------------+--------------+--------------++--------------+
-    // |  9 | Short        | Short        |      0       || Present      |
-    // | 10 | Short        | Extended     |      0       || Present      |
-    // | 11 | Extended     | Short        |      0       || Present      |
-    // | 12 | Short        | Extended     |      1       || Not Present  |
-    // | 13 | Extended     | Short        |      1       || Not Present  |
-    // | 14 | Short        | Short        |      1       || Not Present  |
-    // +----+--------------+--------------+--------------++--------------+
-
-    if (IsVersion2015(aFcf) && ((aFcf & (kFcfDstAddrMask | kFcfSrcAddrMask)) == (kFcfDstAddrExt | kFcfSrcAddrExt)))
+    bool present;
+
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+    if (IsMultipurpose(aFcf))
+    {
+        // Sources PAN ID is implicitly equal to Destination PAN ID in Multipurpose frames
+        present = false;
+    }
+    else
+#endif
+        if (IsVersion2015(aFcf) && ((aFcf & (kFcfDstAddrMask | kFcfSrcAddrMask)) == (kFcfDstAddrExt | kFcfSrcAddrExt)))
     {
+        // Special case for a IEEE 802.15.4-2015 frame: When both
+        // addresses are extended, then the source PAN iD is not present
+        // independent of PAN ID Compression. In this case, if the PAN ID
+        // compression is set, it indicates that no PAN ID is in the
+        // frame, while if the PAN ID Compression is zero, it indicates
+        // the presence of the destination PAN ID in the frame.
+        //
+        // +----+--------------+--------------+--------------++--------------+
+        // | No |  Dest Addr   |   Src Addr   |  PAN ID Comp ||  Src PAN ID  |
+        // +----+--------------+--------------+--------------++--------------+
+        // |  1 | Not Present  | Not Present  |      0       || Not Present  |
+        // |  2 | Not Present  | Not Present  |      1       || Not Present  |
+        // |  3 | Present      | Not Present  |      0       || Not Present  |
+        // |  4 | Present      | Not Present  |      1       || Not Present  |
+        // |  5 | Not Present  | Present      |      0       || Present      |
+        // |  6 | Not Present  | Present      |      1       || Not Present  |
+        // +----+--------------+--------------+--------------++--------------+
+        // |  7 | Extended     | Extended     |      0       || Not Present  |
+        // |  8 | Extended     | Extended     |      1       || Not Present  |
+        // |----+--------------+--------------+--------------++--------------+
+        // |  9 | Short        | Short        |      0       || Present      |
+        // | 10 | Short        | Extended     |      0       || Present      |
+        // | 11 | Extended     | Short        |      0       || Present      |
+        // | 12 | Short        | Extended     |      1       || Not Present  |
+        // | 13 | Extended     | Short        |      1       || Not Present  |
+        // | 14 | Short        | Short        |      1       || Not Present  |
+        // +----+--------------+--------------+--------------++--------------+
+
         present = false;
     }
+    else
+    {
+        present = IsSrcAddrPresent(aFcf) && ((aFcf & kFcfPanidCompression) == 0);
+    }
 
     return present;
 }
@@ -551,23 +644,21 @@ Error Frame::SetSrcPanId(PanId aPanId)
 
 uint8_t Frame::FindSrcAddrIndex(void) const
 {
-    uint8_t  index = 0;
     uint16_t fcf   = GetFrameControlField();
-
-    index += kFcfSize + GetSeqNumSize();
+    uint8_t  index = SkipSequenceIndex();
 
     if (IsDstPanIdPresent(fcf))
     {
         index += sizeof(PanId);
     }
 
-    switch (fcf & kFcfDstAddrMask)
+    switch (GetFcfDstAddr(fcf))
     {
-    case kFcfDstAddrShort:
+    case kFcfAddrShort:
         index += sizeof(ShortAddress);
         break;
 
-    case kFcfDstAddrExt:
+    case kFcfAddrExt:
         index += sizeof(ExtAddress);
         break;
     }
@@ -588,17 +679,17 @@ Error Frame::GetSrcAddr(Address &aAddress) const
 
     VerifyOrExit(index != kInvalidIndex, error = kErrorParse);
 
-    switch (fcf & kFcfSrcAddrMask)
+    switch (GetFcfSrcAddr(fcf))
     {
-    case kFcfSrcAddrShort:
+    case kFcfAddrShort:
         aAddress.SetShort(LittleEndian::ReadUint16(&mPsdu[index]));
         break;
 
-    case kFcfSrcAddrExt:
+    case kFcfAddrExt:
         aAddress.SetExtended(&mPsdu[index], ExtAddress::kReverseByteOrder);
         break;
 
-    case kFcfSrcAddrNone:
+    case kFcfAddrNone:
         aAddress.SetNone();
         break;
 
@@ -616,7 +707,7 @@ void Frame::SetSrcAddr(ShortAddress aShortAddress)
 {
     uint8_t index = FindSrcAddrIndex();
 
-    OT_ASSERT((GetFrameControlField() & kFcfSrcAddrMask) == kFcfSrcAddrShort);
+    OT_ASSERT(GetFcfSrcAddr(GetFrameControlField()) == kFcfAddrShort);
     OT_ASSERT(index != kInvalidIndex);
 
     LittleEndian::WriteUint16(aShortAddress, &mPsdu[index]);
@@ -626,7 +717,7 @@ void Frame::SetSrcAddr(const ExtAddress &aExtAddress)
 {
     uint8_t index = FindSrcAddrIndex();
 
-    OT_ASSERT((GetFrameControlField() & kFcfSrcAddrMask) == kFcfSrcAddrExt);
+    OT_ASSERT(GetFcfSrcAddr(GetFrameControlField()) == kFcfAddrExt);
     OT_ASSERT(index != kInvalidIndex);
 
     aExtAddress.CopyTo(&mPsdu[index], ExtAddress::kReverseByteOrder);
@@ -964,8 +1055,6 @@ uint8_t Frame::SkipAddrFieldIndex(void) const
 
     VerifyOrExit(kFcfSize + GetFcsSize() <= mLength, index = kInvalidIndex);
 
-    VerifyOrExit(!IsSequencePresent() || kFcfSize + kDsnSize + GetFcsSize() <= mLength, index = kInvalidIndex);
-
     index = CalculateAddrFieldSize(GetFrameControlField());
 
 exit:
@@ -974,7 +1063,7 @@ uint8_t Frame::SkipAddrFieldIndex(void) const
 
 uint8_t Frame::CalculateAddrFieldSize(uint16_t aFcf)
 {
-    uint8_t size = kFcfSize + GetSeqNumSize(aFcf);
+    uint8_t size = GetFcfSize(aFcf) + (IsSequencePresent(aFcf) ? kDsnSize : 0);
 
     // This static method calculates the size (number of bytes) of
     // Address header field for a given Frame Control `aFcf` value.
@@ -988,16 +1077,16 @@ uint8_t Frame::CalculateAddrFieldSize(uint16_t aFcf)
         size += sizeof(PanId);
     }
 
-    switch (aFcf & kFcfDstAddrMask)
+    switch (GetFcfDstAddr(aFcf))
     {
-    case kFcfDstAddrNone:
+    case kFcfAddrNone:
         break;
 
-    case kFcfDstAddrShort:
+    case kFcfAddrShort:
         size += sizeof(ShortAddress);
         break;
 
-    case kFcfDstAddrExt:
+    case kFcfAddrExt:
         size += sizeof(ExtAddress);
         break;
 
@@ -1010,16 +1099,16 @@ uint8_t Frame::CalculateAddrFieldSize(uint16_t aFcf)
         size += sizeof(PanId);
     }
 
-    switch (aFcf & kFcfSrcAddrMask)
+    switch (GetFcfSrcAddr(aFcf))
     {
-    case kFcfSrcAddrNone:
+    case kFcfAddrNone:
         break;
 
-    case kFcfSrcAddrShort:
+    case kFcfAddrShort:
         size += sizeof(ShortAddress);
         break;
 
-    case kFcfSrcAddrExt:
+    case kFcfAddrExt:
         size += sizeof(ExtAddress);
         break;
 
@@ -1151,6 +1240,16 @@ template <> void Frame::InitIeContentAt<TimeIe>(uint8_t &aIndex)
 }
 #endif
 
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+template <> void Frame::InitIeContentAt<RendezvousTimeIe>(uint8_t &aIndex) { aIndex += sizeof(RendezvousTimeIe); }
+
+template <> void Frame::InitIeContentAt<ConnectionIe>(uint8_t &aIndex)
+{
+    reinterpret_cast<ConnectionIe *>(mPsdu + aIndex)->Init();
+    aIndex += sizeof(ConnectionIe);
+}
+#endif
+
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 template <> void Frame::InitIeContentAt<CslIe>(uint8_t &aIndex) { aIndex += sizeof(CslIe); }
 #endif
@@ -1186,7 +1285,8 @@ const uint8_t *Frame::GetHeaderIe(uint8_t aIeId) const
     return header;
 }
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE || \
+    OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
 const uint8_t *Frame::GetThreadIe(uint8_t aSubType) const
 {
     uint8_t        index        = FindHeaderIeIndex();
@@ -1218,7 +1318,8 @@ const uint8_t *Frame::GetThreadIe(uint8_t aSubType) const
 exit:
     return header;
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE ||
+       // OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
 
 #endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
 
@@ -1342,6 +1443,10 @@ uint8_t Frame::GetFcsSize(void) const { return Trel::Link::kFcsSize; }
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 template Error Frame::AppendHeaderIeAt<TimeIe>(uint8_t &aIndex);
 #endif
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+template Error Frame::AppendHeaderIeAt<RendezvousTimeIe>(uint8_t &aIndex);
+template Error Frame::AppendHeaderIeAt<ConnectionIe>(uint8_t &aIndex);
+#endif
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 template Error Frame::AppendHeaderIeAt<CslIe>(uint8_t &aIndex);
 #endif
@@ -1433,7 +1538,7 @@ void TxFrame::GenerateImmAck(const RxFrame &aFrame, bool aIsFramePending)
     }
     LittleEndian::WriteUint16(fcf, mPsdu);
 
-    mPsdu[kSequenceIndex] = aFrame.GetSequence();
+    mPsdu[kFcfSize] = aFrame.GetSequence();
 
     mLength = kImmAckLength;
 }
@@ -1518,6 +1623,62 @@ Error TxFrame::GenerateEnhAck(const RxFrame &aRxFrame, bool aIsFramePending, con
 }
 #endif // OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2
 
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+Error TxFrame::GenerateWakeupFrame(PanId aPanId, const Address &aDest, const Address &aSource)
+{
+    Error    error = kErrorNone;
+    uint16_t fcf   = kTypeMultipurpose | kMpFcfLongFrame | kMpFcfPanidPresent | kMpFcfSecurityEnabled |
+                   kMpFcfSequenceSuppression | kMpFcfIePresent;
+    uint8_t index = 0;
+
+    switch (aDest.GetType())
+    {
+    case Address::Type::kTypeShort:
+        fcf |= kMpFcfDstAddrShort;
+        break;
+    case Address::Type::kTypeExtended:
+        fcf |= kMpFcfDstAddrExt;
+        break;
+    default:
+        ExitNow(error = kErrorInvalidArgs);
+        break;
+    }
+
+    switch (aSource.GetType())
+    {
+    case Address::Type::kTypeShort:
+        fcf |= kMpFcfSrcAddrShort;
+        break;
+    case Address::Type::kTypeExtended:
+        fcf |= kMpFcfSrcAddrExt;
+        break;
+    default:
+        ExitNow(error = kErrorInvalidArgs);
+        break;
+    }
+
+    mLength = CalculateAddrFieldSize(fcf);
+
+    OT_ASSERT(mLength != kInvalidSize);
+
+    SetFrameControlField(fcf);
+    SetDstPanId(aPanId);
+    SetDstAddr(aDest);
+    SetSrcAddr(aSource);
+
+    mPsdu[mLength] = kKeyIdMode2 | kSecurityEncMic32;
+    mLength += CalculateSecurityHeaderSize(kKeyIdMode2 | kSecurityEncMic32);
+    mLength += CalculateMicSize(kKeyIdMode2 | kSecurityEncMic32);
+    mLength += GetFcsSize();
+
+    SuccessOrExit(error = AppendHeaderIeAt<RendezvousTimeIe>(index));
+    SuccessOrExit(error = AppendHeaderIeAt<ConnectionIe>(index));
+
+exit:
+    return error;
+}
+#endif
+
 Error RxFrame::ProcessReceiveAesCcm(const ExtAddress &aExtAddress, const KeyMaterial &aMacKey)
 {
 #if OPENTHREAD_RADIO
@@ -1575,16 +1736,20 @@ Frame::InfoString Frame::ToInfoString(void) const
     InfoString string;
     uint8_t    commandId, type;
     Address    src, dst;
+    uint32_t   frameCounter;
+    bool       sequencePresent;
 
-    if (IsSequencePresent())
-    {
-        string.Append("len:%d, seqnum:%d, type:", mLength, GetSequence());
-    }
-    else
+    string.Append("len:%d", mLength);
+
+    sequencePresent = IsSequencePresent();
+
+    if (sequencePresent)
     {
-        string.Append("len:%d, type:", mLength);
+        string.Append(", seqnum:%d", GetSequence());
     }
 
+    string.Append(", type:");
+
     type = GetType();
 
     switch (type)
@@ -1624,6 +1789,12 @@ Frame::InfoString Frame::ToInfoString(void) const
 
         break;
 
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+    case kTypeMultipurpose:
+        string.Append("MP");
+        break;
+#endif
+
     default:
         string.Append("%d", type);
         break;
@@ -1635,6 +1806,11 @@ Frame::InfoString Frame::ToInfoString(void) const
     string.Append(", src:%s, dst:%s, sec:%s, ackreq:%s", src.ToString().AsCString(), dst.ToString().AsCString(),
                   ToYesNo(GetSecurityEnabled()), ToYesNo(GetAckRequest()));
 
+    if (!sequencePresent && GetFrameCounter(frameCounter) == kErrorNone)
+    {
+        string.Append(", fc:%lu", ToUlong(frameCounter));
+    }
+
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     string.Append(", radio:%s", RadioTypeToString(GetRadioType()));
 #endif
diff --git a/src/core/mac/mac_frame.hpp b/src/core/mac/mac_frame.hpp
index d51e45caacc..575a0a0e772 100644
--- a/src/core/mac/mac_frame.hpp
+++ b/src/core/mac/mac_frame.hpp
@@ -69,10 +69,11 @@ class Frame : public otRadioFrame
      */
     enum Type : uint16_t
     {
-        kTypeBeacon = 0, ///< Beacon Frame Type.
-        kTypeData   = 1, ///< Data Frame Type.
-        kTypeAck    = 2, ///< Ack Frame Type.
-        kTypeMacCmd = 3, ///< MAC Command Frame Type.
+        kTypeBeacon       = 0, ///< Beacon Frame Type.
+        kTypeData         = 1, ///< Data Frame Type.
+        kTypeAck          = 2, ///< Ack Frame Type.
+        kTypeMacCmd       = 3, ///< MAC Command Frame Type.
+        kTypeMultipurpose = 5, ///< Multipurpose Frame Type.
     };
 
     /**
@@ -215,6 +216,59 @@ class Frame : public otRadioFrame
      */
     bool IsAck(void) const { return GetType() == kTypeAck; }
 
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+    /**
+     * This method returns whether the frame is an IEEE 802.15.4 Wake-up frame.
+     *
+     * @retval TRUE   If this is a Wake-up frame.
+     * @retval FALSE  If this is not a Wake-up frame.
+     *
+     */
+    bool IsWakeupFrame(void) const;
+
+    /**
+     * This method returns the Rendezvous Time IE of a wake-up frame.
+     *
+     * @returns Pointer to the Rendezvous Time IE.
+     *
+     */
+    RendezvousTimeIe *GetRendezvousTimeIe(void) { return AsNonConst(AsConst(this)->GetRendezvousTimeIe()); }
+
+    /**
+     * This method returns the Rendezvous Time IE of a wake-up frame.
+     *
+     * @returns Const pointer to the Rendezvous Time IE.
+     *
+     */
+    const RendezvousTimeIe *GetRendezvousTimeIe(void) const
+    {
+        const uint8_t *ie = GetHeaderIe(RendezvousTimeIe::kHeaderIeId);
+
+        return (ie != nullptr) ? reinterpret_cast<const RendezvousTimeIe *>(ie + sizeof(HeaderIe)) : nullptr;
+    }
+
+    /**
+     * This method returns the Connection IE of a wake-up frame.
+     *
+     * @returns Pointer to the Connection IE.
+     *
+     */
+    ConnectionIe *GetConnectionIe(void) { return AsNonConst(AsConst(this)->GetConnectionIe()); }
+
+    /**
+     * This method returns the Connection IE of a wake-up frame.
+     *
+     * @returns Const pointer to the Connection IE.
+     *
+     */
+    const ConnectionIe *GetConnectionIe(void) const
+    {
+        const uint8_t *ie = GetThreadIe(ConnectionIe::kThreadIeSubtype);
+
+        return (ie != nullptr) ? reinterpret_cast<const ConnectionIe *>(ie + sizeof(HeaderIe)) : nullptr;
+    }
+#endif // OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+
     /**
      * Returns the IEEE 802.15.4 Frame Version.
      *
@@ -238,7 +292,7 @@ class Frame : public otRadioFrame
      * @retval FALSE  If security is not enabled.
      *
      */
-    bool GetSecurityEnabled(void) const { return (GetPsdu()[0] & kFcfSecurityEnabled) != 0; }
+    bool GetSecurityEnabled(void) const { return IsSecurityEnabled(GetFrameControlField()); }
 
     /**
      * Indicates whether or not the Frame Pending bit is set.
@@ -247,11 +301,13 @@ class Frame : public otRadioFrame
      * @retval FALSE  If the Frame Pending bit is not set.
      *
      */
-    bool GetFramePending(void) const { return (GetPsdu()[0] & kFcfFramePending) != 0; }
+    bool GetFramePending(void) const { return IsFramePending(GetFrameControlField()); }
 
     /**
      * Sets the Frame Pending bit.
      *
+     * @note This method must not be called on a Multipurpose frame with short Frame Control field.
+     *
      * @param[in]  aFramePending  The Frame Pending bit.
      *
      */
@@ -264,11 +320,13 @@ class Frame : public otRadioFrame
      * @retval FALSE  If the Ack Request bit is not set.
      *
      */
-    bool GetAckRequest(void) const { return (GetPsdu()[0] & kFcfAckRequest) != 0; }
+    bool GetAckRequest(void) const { return IsAckRequest(GetFrameControlField()); }
 
     /**
      * Sets the Ack Request bit.
      *
+     * @note This method must not be called on a Multipurpose frame with short Frame Control field.
+     *
      * @param[in]  aAckRequest  The Ack Request bit.
      *
      */
@@ -277,6 +335,8 @@ class Frame : public otRadioFrame
     /**
      * Indicates whether or not the PanId Compression bit is set.
      *
+     * @note This method must not be called on a Multipurpose frame, which lacks this flag.
+     *
      * @retval TRUE   If the PanId Compression bit is set.
      * @retval FALSE  If the PanId Compression bit is not set.
      *
@@ -290,11 +350,13 @@ class Frame : public otRadioFrame
      * @retval FALSE  If no IE present.
      *
      */
-    bool IsIePresent(void) const { return (GetFrameControlField() & kFcfIePresent) != 0; }
+    bool IsIePresent(void) const { return IsIePresent(GetFrameControlField()); }
 
     /**
      * Sets the IE Present bit.
      *
+     * @note This method must not be called on a Multipurpose frame with short Frame Control field.
+     *
      * @param[in]  aIePresent   The IE Present bit.
      *
      */
@@ -322,16 +384,7 @@ class Frame : public otRadioFrame
      * @returns TRUE if the Sequence Number is present, FALSE otherwise.
      *
      */
-    uint8_t IsSequencePresent(void) const { return !IsSequenceSuppressed(GetFrameControlField()); }
-
-    /**
-     * Get the size of the sequence number.
-     *
-     * @retval      0       The size of sequence number is 0, indicating it's not present.
-     * @retval      1       The size of sequence number is 1, indicating it's present.
-     *
-     */
-    uint8_t GetSeqNumSize(void) const { return GetSeqNumSize(GetFrameControlField()); }
+    uint8_t IsSequencePresent(void) const { return IsSequencePresent(GetFrameControlField()); }
 
     /**
      * Indicates whether or not the Destination PAN ID is present.
@@ -919,30 +972,71 @@ class Frame : public otRadioFrame
      * @returns The Frame Control field.
      *
      */
-    uint16_t GetFrameControlField(void) const;
+    uint16_t GetFrameControlField(void) const
+    {
+        uint16_t fcf = mPsdu[0];
+
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+        if (!IsShortFcf(fcf))
+#endif
+        {
+            fcf |= (mPsdu[1] << 8);
+        }
+
+        return fcf;
+    }
 
 protected:
+    static constexpr uint8_t kShortFcfSize        = sizeof(uint8_t);
     static constexpr uint8_t kSecurityControlSize = sizeof(uint8_t);
     static constexpr uint8_t kFrameCounterSize    = sizeof(uint32_t);
     static constexpr uint8_t kCommandIdSize       = sizeof(uint8_t);
     static constexpr uint8_t kKeyIndexSize        = sizeof(uint8_t);
 
-    static constexpr uint16_t kFcfFrameTypeMask      = 7 << 0;
-    static constexpr uint16_t kFcfSecurityEnabled    = 1 << 3;
-    static constexpr uint16_t kFcfFramePending       = 1 << 4;
-    static constexpr uint16_t kFcfAckRequest         = 1 << 5;
-    static constexpr uint16_t kFcfPanidCompression   = 1 << 6;
-    static constexpr uint16_t kFcfSequenceSupression = 1 << 8;
-    static constexpr uint16_t kFcfIePresent          = 1 << 9;
-    static constexpr uint16_t kFcfDstAddrNone        = 0 << 10;
-    static constexpr uint16_t kFcfDstAddrShort       = 2 << 10;
-    static constexpr uint16_t kFcfDstAddrExt         = 3 << 10;
-    static constexpr uint16_t kFcfDstAddrMask        = 3 << 10;
-    static constexpr uint16_t kFcfFrameVersionMask   = 3 << 12;
-    static constexpr uint16_t kFcfSrcAddrNone        = 0 << 14;
-    static constexpr uint16_t kFcfSrcAddrShort       = 2 << 14;
-    static constexpr uint16_t kFcfSrcAddrExt         = 3 << 14;
-    static constexpr uint16_t kFcfSrcAddrMask        = 3 << 14;
+    static constexpr uint16_t kFcfFrameTypeMask = 7 << 0;
+
+    static constexpr uint16_t kFcfAddrNone  = 0;
+    static constexpr uint16_t kFcfAddrShort = 2;
+    static constexpr uint16_t kFcfAddrExt   = 3;
+    static constexpr uint16_t kFcfAddrMask  = 3;
+
+    // Frame Control field format for general MAC frame
+    static constexpr uint16_t kFcfSecurityEnabled     = 1 << 3;
+    static constexpr uint16_t kFcfFramePending        = 1 << 4;
+    static constexpr uint16_t kFcfAckRequest          = 1 << 5;
+    static constexpr uint16_t kFcfPanidCompression    = 1 << 6;
+    static constexpr uint16_t kFcfSequenceSuppression = 1 << 8;
+    static constexpr uint16_t kFcfIePresent           = 1 << 9;
+    static constexpr uint16_t kFcfDstAddrShift        = 10;
+    static constexpr uint16_t kFcfDstAddrNone         = kFcfAddrNone << kFcfDstAddrShift;
+    static constexpr uint16_t kFcfDstAddrShort        = kFcfAddrShort << kFcfDstAddrShift;
+    static constexpr uint16_t kFcfDstAddrExt          = kFcfAddrExt << kFcfDstAddrShift;
+    static constexpr uint16_t kFcfDstAddrMask         = kFcfAddrMask << kFcfDstAddrShift;
+    static constexpr uint16_t kFcfFrameVersionMask    = 3 << 12;
+    static constexpr uint16_t kFcfSrcAddrShift        = 14;
+    static constexpr uint16_t kFcfSrcAddrNone         = kFcfAddrNone << kFcfSrcAddrShift;
+    static constexpr uint16_t kFcfSrcAddrShort        = kFcfAddrShort << kFcfSrcAddrShift;
+    static constexpr uint16_t kFcfSrcAddrExt          = kFcfAddrExt << kFcfSrcAddrShift;
+    static constexpr uint16_t kFcfSrcAddrMask         = kFcfAddrMask << kFcfSrcAddrShift;
+
+    // Frame Control field format for MAC Multipurpose frame
+    static constexpr uint16_t kMpFcfLongFrame           = 1 << 3;
+    static constexpr uint16_t kMpFcfDstAddrShift        = 4;
+    static constexpr uint16_t kMpFcfDstAddrNone         = kFcfAddrNone << kMpFcfDstAddrShift;
+    static constexpr uint16_t kMpFcfDstAddrShort        = kFcfAddrShort << kMpFcfDstAddrShift;
+    static constexpr uint16_t kMpFcfDstAddrExt          = kFcfAddrExt << kMpFcfDstAddrShift;
+    static constexpr uint16_t kMpFcfDstAddrMask         = kFcfAddrMask << kMpFcfDstAddrShift;
+    static constexpr uint16_t kMpFcfSrcAddrShift        = 6;
+    static constexpr uint16_t kMpFcfSrcAddrNone         = kFcfAddrNone << kMpFcfSrcAddrShift;
+    static constexpr uint16_t kMpFcfSrcAddrShort        = kFcfAddrShort << kMpFcfSrcAddrShift;
+    static constexpr uint16_t kMpFcfSrcAddrExt          = kFcfAddrExt << kMpFcfSrcAddrShift;
+    static constexpr uint16_t kMpFcfSrcAddrMask         = kFcfAddrMask << kMpFcfSrcAddrShift;
+    static constexpr uint16_t kMpFcfPanidPresent        = 1 << 8;
+    static constexpr uint16_t kMpFcfSecurityEnabled     = 1 << 9;
+    static constexpr uint16_t kMpFcfSequenceSuppression = 1 << 10;
+    static constexpr uint16_t kMpFcfFramePending        = 1 << 11;
+    static constexpr uint16_t kMpFcfAckRequest          = 1 << 14;
+    static constexpr uint16_t kMpFcfIePresent           = 1 << 15;
 
     static constexpr uint8_t kSecLevelMask  = 7 << 0;
     static constexpr uint8_t kKeyIdModeMask = 3 << 3;
@@ -958,12 +1052,12 @@ class Frame : public otRadioFrame
     static constexpr uint8_t kKeySourceSizeMode2 = 4;
     static constexpr uint8_t kKeySourceSizeMode3 = 8;
 
-    static constexpr uint8_t kInvalidIndex  = 0xff;
-    static constexpr uint8_t kInvalidSize   = kInvalidIndex;
-    static constexpr uint8_t kMaxPsduSize   = kInvalidSize - 1;
-    static constexpr uint8_t kSequenceIndex = kFcfSize;
+    static constexpr uint8_t kInvalidIndex = 0xff;
+    static constexpr uint8_t kInvalidSize  = kInvalidIndex;
+    static constexpr uint8_t kMaxPsduSize  = kInvalidSize - 1;
 
     void    SetFrameControlField(uint16_t aFcf);
+    uint8_t SkipSequenceIndex(void) const;
     uint8_t FindDstPanIdIndex(void) const;
     uint8_t FindDstAddrIndex(void) const;
     uint8_t FindSrcPanIdIndex(void) const;
@@ -981,16 +1075,55 @@ class Frame : public otRadioFrame
 
     static uint8_t GetKeySourceLength(uint8_t aKeyIdMode);
 
-    static bool IsDstAddrPresent(uint16_t aFcf) { return (aFcf & kFcfDstAddrMask) != kFcfDstAddrNone; }
-    static bool IsDstPanIdPresent(uint16_t aFcf);
-    static bool IsSequenceSuppressed(uint16_t aFcf)
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+    static uint8_t GetFcfSize(uint16_t aFcf) { return IsShortFcf(aFcf) ? kShortFcfSize : kFcfSize; }
+#else
+    // clang-format off
+    static uint8_t GetFcfSize(uint16_t /* aFcf */) { return kFcfSize; }
+    // clang-format on
+#endif
+
+#if OPENTHREAD_CONFIG_MAC_MULTIPURPOSE_FRAME
+    template <uint16_t kValue, uint16_t kMpValue> static uint16_t Select(uint16_t aFcf)
+    {
+        return IsMultipurpose(aFcf) ? kMpValue : kValue;
+    }
+#else
+    template <uint16_t kValue, uint16_t kMpValue> static uint16_t Select(uint16_t /* aFcf */) { return kValue; }
+#endif
+
+    template <uint16_t kValue, uint16_t kMpValue> static uint16_t MaskFcf(uint16_t aFcf)
     {
-        return (aFcf & (kFcfSequenceSupression | kFcfFrameVersionMask)) == (kFcfSequenceSupression | kVersion2015);
+        return aFcf & Select<kValue, kMpValue>(aFcf);
     }
-    static uint8_t GetSeqNumSize(uint16_t aFcf) { return !IsSequenceSuppressed(aFcf) ? kDsnSize : 0; }
 
-    static bool IsSrcAddrPresent(uint16_t aFcf) { return (aFcf & kFcfSrcAddrMask) != kFcfSrcAddrNone; }
+    static uint16_t GetFcfDstAddr(uint16_t aFcf)
+    {
+        return MaskFcf<kFcfDstAddrMask, kMpFcfDstAddrMask>(aFcf) >> Select<kFcfDstAddrShift, kMpFcfDstAddrShift>(aFcf);
+    }
+
+    static uint16_t GetFcfSrcAddr(uint16_t aFcf)
+    {
+        return MaskFcf<kFcfSrcAddrMask, kMpFcfSrcAddrMask>(aFcf) >> Select<kFcfSrcAddrShift, kMpFcfSrcAddrShift>(aFcf);
+    }
+
+    static bool IsMultipurpose(uint16_t aFcf) { return (aFcf & kFcfFrameTypeMask) == kTypeMultipurpose; }
+    static bool IsShortFcf(uint16_t aFcf)
+    {
+        return (aFcf & (kFcfFrameTypeMask | kMpFcfLongFrame)) == (kTypeMultipurpose | 0);
+    }
+    static bool IsSequencePresent(uint16_t aFcf)
+    {
+        return !MaskFcf<kFcfSequenceSuppression, kMpFcfSequenceSuppression>(aFcf);
+    }
+    static bool IsDstAddrPresent(uint16_t aFcf) { return MaskFcf<kFcfDstAddrMask, kMpFcfDstAddrMask>(aFcf); }
+    static bool IsDstPanIdPresent(uint16_t aFcf);
+    static bool IsSrcAddrPresent(uint16_t aFcf) { return MaskFcf<kFcfSrcAddrMask, kMpFcfSrcAddrMask>(aFcf); }
     static bool IsSrcPanIdPresent(uint16_t aFcf);
+    static bool IsSecurityEnabled(uint16_t aFcf) { return MaskFcf<kFcfSecurityEnabled, kMpFcfSecurityEnabled>(aFcf); }
+    static bool IsFramePending(uint16_t aFcf) { return MaskFcf<kFcfFramePending, kMpFcfFramePending>(aFcf); }
+    static bool IsIePresent(uint16_t aFcf) { return MaskFcf<kFcfIePresent, kMpFcfIePresent>(aFcf); }
+    static bool IsAckRequest(uint16_t aFcf) { return MaskFcf<kFcfAckRequest, kMpFcfAckRequest>(aFcf); }
     static bool IsVersion2015(uint16_t aFcf) { return (aFcf & kFcfFrameVersionMask) == kVersion2015; }
 
     static uint8_t CalculateAddrFieldSize(uint16_t aFcf);
@@ -1375,6 +1508,21 @@ class TxFrame : public Frame
      */
     Error GenerateEnhAck(const RxFrame &aRxFrame, bool aIsFramePending, const uint8_t *aIeData, uint8_t aIeLength);
 
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+    /**
+     * Generate IEE 802.15.4 Wake-up frame.
+     *
+     * @param[in]    aPanId     A destination PAN identifier
+     * @param[in]    aDest      A destination address (short or extended)
+     * @param[in]    aSource    A source address (short or extended)
+     *
+     * @retval  kErrorNone        Successfully generated Wake-up frame.
+     * @retval  kErrorInvalidArgs @p aDest or @p aSource have incorrect type.
+     *
+     */
+    Error GenerateWakeupFrame(PanId aPanId, const Address &aDest, const Address &aSource);
+#endif
+
 #if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2
     /**
      * Set TX delay field for the frame.
diff --git a/src/core/mac/mac_header_ie.hpp b/src/core/mac/mac_header_ie.hpp
index 5e25dc9cd89..41f0845bb39 100644
--- a/src/core/mac/mac_header_ie.hpp
+++ b/src/core/mac/mac_header_ie.hpp
@@ -193,8 +193,6 @@ class Termination2Ie
     static constexpr uint8_t kIeContentSize = 0;
 };
 
-#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || \
-    OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 /**
  * Implements vendor specific Header IE generation and parsing.
  *
@@ -307,7 +305,6 @@ class TimeIe : public VendorIeHeader
 } OT_TOOL_PACKED_END;
 #endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 class ThreadIe
 {
 public:
@@ -316,10 +313,118 @@ class ThreadIe
     static constexpr uint32_t kVendorOuiThreadCompanyId = 0xeab89b;
     static constexpr uint8_t  kEnhAckProbingIe          = 0x00;
 };
-#endif
 
-#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE ||
-       // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
+/**
+ * This class implements Rendezvous Time IE data structure.
+ *
+ * IEEE 802.15.4 Rendezvous Time IE contains two fields, Rendezvous Time and
+ * Wake-up Interval, but the Wake-up Interval is not used in Thread, so it is
+ * not included in this class.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+class RendezvousTimeIe
+{
+public:
+    static constexpr uint8_t kHeaderIeId    = 0x1d;
+    static constexpr uint8_t kIeContentSize = sizeof(uint16_t);
+
+    /**
+     * This method returns the Rendezvous Time.
+     *
+     * @returns the Rendezvous Time in the units of 10 symbols.
+     *
+     */
+    uint16_t GetRendezvousTime(void) const { return LittleEndian::HostSwap16(mRendezvousTime); }
+
+    /**
+     * This method sets the Rendezvous Time.
+     *
+     * @param[in]  aRendezvousTime  The Rendezvous Time in the units of 10 symbols.
+     *
+     */
+    void SetRendezvousTime(uint16_t aRendezvousTime) { mRendezvousTime = LittleEndian::HostSwap16(aRendezvousTime); }
+
+private:
+    uint16_t mRendezvousTime;
+} OT_TOOL_PACKED_END;
+
+/**
+ * Implements Connection IE data structure.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+class ConnectionIe : public VendorIeHeader
+{
+public:
+    static constexpr uint8_t kHeaderIeId      = ThreadIe::kHeaderIeId;
+    static constexpr uint8_t kIeContentSize   = ThreadIe::kIeContentSize + sizeof(uint8_t);
+    static constexpr uint8_t kThreadIeSubtype = 0x01;
+
+    /**
+     * Initializes the Connection IE.
+     *
+     */
+    void Init(void)
+    {
+        SetVendorOui(ThreadIe::kVendorOuiThreadCompanyId);
+        SetSubType(kThreadIeSubtype);
+        mConnectionWindow = 0;
+    }
+
+    /**
+     * Returns the Retry Interval.
+     *
+     * The Retry Interval defines how frequently the Wake-up End Device is
+     * supposed to retry sending the Parent Request to the Wake-up Coordinator.
+     *
+     * @returns the Retry Interval in the units of Wake-up Intervals (7.5ms by default).
+     *
+     */
+    uint8_t GetRetryInterval(void) const { return (mConnectionWindow & kRetryIntervalMask) >> kRetryIntervalOffset; }
+
+    /**
+     * Sets the Retry Interval.
+     *
+     * @param[in]  aRetryInterval  The Retry Interval in the units of Wake-up Intervals (7.5ms by default).
+     *
+     */
+    void SetRetryInterval(uint8_t aRetryInterval)
+    {
+        mConnectionWindow = (aRetryInterval << kRetryIntervalOffset) | (mConnectionWindow & ~kRetryIntervalMask);
+    }
+
+    /**
+     * Returns the Retry Count.
+     *
+     * The Retry Count defines how many times the Wake-up End Device is supposed
+     * to retry sending the Parent Request to the Wakeup Coordinator.
+     *
+     * @returns the Retry Count.
+     *
+     */
+    uint8_t GetRetryCount(void) const { return mConnectionWindow & kRetryCountMask; }
+
+    /**
+     * Sets the Retry Count
+     *
+     * @param[in]  aRetryCount  The Retry Count.
+     *
+     */
+    void SetRetryCount(uint8_t aRetryCount)
+    {
+        mConnectionWindow = aRetryCount | (mConnectionWindow & ~kRetryCountMask);
+    }
+
+private:
+    static constexpr uint8_t kRetryIntervalOffset = 4;
+    static constexpr uint8_t kRetryIntervalMask   = 0x3 << kRetryIntervalOffset;
+    static constexpr uint8_t kRetryCountMask      = 0xf;
+
+    uint8_t mConnectionWindow;
+} OT_TOOL_PACKED_END;
+#endif // OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE || OPENTHREAD_CONFIG_WAKEUP_END_DEVICE_ENABLE
 
 /**
  * @}
diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h
index dfbaf72f50b..8652c74fb9d 100644
--- a/src/core/openthread-core-config.h
+++ b/src/core/openthread-core-config.h
@@ -110,6 +110,7 @@
 #include "config/time_sync.h"
 #include "config/tmf.h"
 #include "config/trel.h"
+#include "config/wakeup.h"
 
 #undef OPENTHREAD_CORE_CONFIG_H_IN
 


Test patch:
diff --git a/script/test b/script/test
index be04e237e1e..c6f5c63bf88 100755
--- a/script/test
+++ b/script/test
@@ -131,6 +131,8 @@ build_simulation()
         options+=("-DOT_LINK_METRICS_INITIATOR=ON")
         options+=("-DOT_LINK_METRICS_SUBJECT=ON")
         options+=("-DOT_LINK_METRICS_MANAGER=ON")
+        options+=("-DOT_WAKEUP_COORDINATOR=ON")
+        options+=("-DOT_WAKEUP_END_DEVICE=ON")
     fi
 
     if [[ ${OT_NODE_TYPE} == cli* ]]; then
diff --git a/tests/unit/test_mac_frame.cpp b/tests/unit/test_mac_frame.cpp
index cf7e2960386..41bfd8d6254 100644
--- a/tests/unit/test_mac_frame.cpp
+++ b/tests/unit/test_mac_frame.cpp
@@ -787,6 +787,218 @@ void TestMacFrameAckGeneration(void)
 #endif // (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
 }
 
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+constexpr uint16_t kMpFcfLongFrame           = 1 << 3;
+constexpr uint16_t kMpFcfDstAddrShift        = 4;
+constexpr uint16_t kMpFcfDstAddrExt          = 3 << kMpFcfDstAddrShift;
+constexpr uint16_t kMpFcfSrcAddrShift        = 6;
+constexpr uint16_t kMpFcfSrcAddrShort        = 2 << kMpFcfSrcAddrShift;
+constexpr uint16_t kMpFcfSrcAddrExt          = 3 << kMpFcfSrcAddrShift;
+constexpr uint16_t kMpFcfPanidPresent        = 1 << 8;
+constexpr uint16_t kMpFcfSecurityEnabled     = 1 << 9;
+constexpr uint16_t kMpFcfSequenceSuppression = 1 << 10;
+constexpr uint16_t kMpFcfAckRequest          = 1 << 14;
+constexpr uint16_t kMpFcfIePresent           = 1 << 15;
+
+void TestMacWakeupFrameGeneration(void)
+{
+    constexpr static uint8_t srcExtaddr[] = {0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55};
+    constexpr static uint8_t dstExtaddr[] = {0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD};
+    constexpr static uint8_t keySource[]  = {0, 0, 0, 0x1C};
+
+    constexpr static uint8_t wakeupPsdu[] = {
+        // Frame Control
+        Mac::Frame::kTypeMultipurpose | kMpFcfLongFrame | kMpFcfDstAddrExt | kMpFcfSrcAddrExt,
+        (kMpFcfPanidPresent | kMpFcfSecurityEnabled | kMpFcfSequenceSuppression | kMpFcfIePresent) >> 8,
+        // PAN ID
+        0xCE, 0xFA,
+        // Destination Address
+        0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD,
+        // Source Address
+        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+        // Security Header
+        Mac::Frame::kKeyIdMode2 | Mac::Frame::kSecurityEncMic32, 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x1C, 0x1D,
+        // Rendezvous Time IE
+        0x82, 0x0E, 0xCD, 0xAB,
+        // Connection IE
+        0x05, 0x00, 0x9B, 0xB8, 0xEA, 0x01, 0x1C};
+
+    uint8_t            psdu[OT_RADIO_FRAME_MAX_SIZE];
+    Mac::Address       src;
+    Mac::Address       dst;
+    Mac::Address       addr;
+    Mac::TxFrame       txFrame;
+    Mac::Frame         rxFrame;
+    Mac::ConnectionIe *connectionIe;
+
+    printf("TestMacWakeupFrameGeneration\n");
+
+    src.SetExtended(srcExtaddr);
+    dst.SetExtended(dstExtaddr);
+    txFrame.mPsdu      = psdu;
+    txFrame.mLength    = 0;
+    txFrame.mRadioType = 0;
+
+    SuccessOrQuit(txFrame.GenerateWakeupFrame(0xface, dst, src));
+
+    // Validate that the frame satisfies the wake-up frame definition
+    VerifyOrQuit(txFrame.GetType() == Mac::Frame::kTypeMultipurpose);
+    VerifyOrQuit(!txFrame.GetAckRequest());
+    VerifyOrQuit(txFrame.GetRendezvousTimeIe() != nullptr);
+    VerifyOrQuit(txFrame.GetConnectionIe() != nullptr);
+    VerifyOrQuit(txFrame.GetPayloadLength() == 0);
+    SuccessOrQuit(txFrame.GetSrcAddr(addr));
+    VerifyOrQuit(CompareAddresses(src, addr));
+    SuccessOrQuit(txFrame.GetDstAddr(addr));
+    VerifyOrQuit(CompareAddresses(dst, addr));
+
+    // Initialize remaining fields and check if the frame has the expected contents
+    txFrame.SetFrameCounter(0xFCFCFCFC);
+    txFrame.SetKeySource(keySource);
+    txFrame.SetKeyId(0x1D);
+    txFrame.GetRendezvousTimeIe()->SetRendezvousTime(0xABCD);
+    connectionIe = txFrame.GetConnectionIe();
+    connectionIe->SetRetryInterval(1);
+    connectionIe->SetRetryCount(12);
+
+    VerifyOrQuit(txFrame.GetRendezvousTimeIe()->GetRendezvousTime() == 0xABCD);
+    VerifyOrQuit(connectionIe->GetRetryInterval() == 1);
+    VerifyOrQuit(connectionIe->GetRetryCount() == 12);
+    VerifyOrQuit(txFrame.GetLength() == sizeof(wakeupPsdu) + txFrame.GetFooterLength());
+    VerifyOrQuit(memcmp(psdu, wakeupPsdu, sizeof(wakeupPsdu)) == 0);
+
+    // Initialize RX Frame with the same PSDU and check if it's recognized as wake-up frame
+    rxFrame.mPsdu      = psdu;
+    rxFrame.mLength    = txFrame.GetLength();
+    rxFrame.mRadioType = 0;
+
+    SuccessOrQuit(rxFrame.ValidatePsdu());
+    VerifyOrQuit(rxFrame.IsWakeupFrame());
+}
+
+void TestMacWakeupFrameDetectionNegative(void)
+{
+    struct TestCase
+    {
+        uint8_t *mPsdu;
+        uint8_t  mLength;
+    };
+
+    uint8_t ackRequestedPsdu[] = {
+        // Frame Control
+        Mac::Frame::kTypeMultipurpose | kMpFcfLongFrame | kMpFcfDstAddrExt | kMpFcfSrcAddrExt,
+        (kMpFcfPanidPresent | kMpFcfSecurityEnabled | kMpFcfSequenceSuppression | kMpFcfAckRequest | kMpFcfIePresent) >>
+            8,
+        // PAN ID
+        0xCE, 0xFA,
+        // Destination Address
+        0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD,
+        // Source Address
+        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+        // Security Header
+        Mac::Frame::kKeyIdMode2 | Mac::Frame::kSecurityEncMic32, 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x1C, 0x1D,
+        // Rendezvous Time IE
+        0x82, 0x0E, 0xCD, 0xAB,
+        // Connection IE
+        0x05, 0x00, 0x9B, 0xB8, 0xEA, 0x01, 0x1C,
+        // Footer
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+    uint8_t shortAddressPsdu[] = {
+        // Frame Control
+        Mac::Frame::kTypeMultipurpose | kMpFcfLongFrame | kMpFcfDstAddrExt | kMpFcfSrcAddrShort,
+        (kMpFcfPanidPresent | kMpFcfSecurityEnabled | kMpFcfSequenceSuppression | kMpFcfIePresent) >> 8,
+        // PAN ID
+        0xCE, 0xFA,
+        // Destination Address
+        0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD,
+        // Source Address
+        0x55, 0x55,
+        // Security Header
+        Mac::Frame::kKeyIdMode2 | Mac::Frame::kSecurityEncMic32, 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x1C, 0x1D,
+        // Rendezvous Time IE
+        0x82, 0x0E, 0xCD, 0xAB,
+        // Connection IE
+        0x05, 0x00, 0x9B, 0xB8, 0xEA, 0x01, 0x1C,
+        // Footer
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+    uint8_t noRendezvousIePsdu[] = {
+        // Frame Control
+        Mac::Frame::kTypeMultipurpose | kMpFcfLongFrame | kMpFcfDstAddrExt | kMpFcfSrcAddrExt,
+        (kMpFcfPanidPresent | kMpFcfSecurityEnabled | kMpFcfSequenceSuppression | kMpFcfIePresent) >> 8,
+        // PAN ID
+        0xCE, 0xFA,
+        // Destination Address
+        0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD,
+        // Source Address
+        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+        // Security Header
+        Mac::Frame::kKeyIdMode2 | Mac::Frame::kSecurityEncMic32, 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x1C, 0x1D,
+        // Connection IE
+        0x05, 0x00, 0x9B, 0xB8, 0xEA, 0x01, 0x1C,
+        // Footer
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+    uint8_t noConnectionIePsdu[] = {
+        // Frame Control
+        Mac::Frame::kTypeMultipurpose | kMpFcfLongFrame | kMpFcfDstAddrExt | kMpFcfSrcAddrExt,
+        (kMpFcfPanidPresent | kMpFcfSecurityEnabled | kMpFcfSequenceSuppression | kMpFcfIePresent) >> 8,
+        // PAN ID
+        0xCE, 0xFA,
+        // Destination Address
+        0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD,
+        // Source Address
+        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+        // Security Header
+        Mac::Frame::kKeyIdMode2 | Mac::Frame::kSecurityEncMic32, 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x1C, 0x1D,
+        // Rendezvous Time IE
+        0x82, 0x0E, 0xCD, 0xAB,
+        // Connection IE
+        0x05, 0x00, 0x9B, 0xB8, 0xEA, 0x02, 0x1C,
+        // Footer
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+    uint8_t keyIdMode1Psdu[] = {
+        // Frame Control
+        Mac::Frame::kTypeMultipurpose | kMpFcfLongFrame | kMpFcfDstAddrExt | kMpFcfSrcAddrExt,
+        (kMpFcfPanidPresent | kMpFcfSecurityEnabled | kMpFcfSequenceSuppression | kMpFcfIePresent) >> 8,
+        // PAN ID
+        0xCE, 0xFA,
+        // Destination Address
+        0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD,
+        // Source Address
+        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+        // Security Header
+        Mac::Frame::kKeyIdMode1 | Mac::Frame::kSecurityEncMic32, 0xFC, 0xFC, 0xFC, 0xFC, 0x1D,
+        // Rendezvous Time IE
+        0x82, 0x0E, 0xCD, 0xAB,
+        // Connection IE
+        0x05, 0x00, 0x9B, 0xB8, 0xEA, 0x01, 0x1C,
+        // Footer
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+    const TestCase testCases[] = {
+        {ackRequestedPsdu, sizeof(ackRequestedPsdu)},     {shortAddressPsdu, sizeof(shortAddressPsdu)},
+        {noRendezvousIePsdu, sizeof(noRendezvousIePsdu)}, {noConnectionIePsdu, sizeof(noConnectionIePsdu)},
+        {keyIdMode1Psdu, sizeof(keyIdMode1Psdu)},
+    };
+
+    Mac::Frame rxFrame;
+
+    printf("TestMacWakeupFrameDetectionNegative\n");
+
+    for (const TestCase &testCase : testCases)
+    {
+        rxFrame.mPsdu      = testCase.mPsdu;
+        rxFrame.mLength    = testCase.mLength;
+        rxFrame.mRadioType = 0;
+
+        SuccessOrQuit(rxFrame.ValidatePsdu());
+        VerifyOrQuit(!rxFrame.IsWakeupFrame());
+    }
+}
+#endif
 } // namespace ot
 
 int main(void)
@@ -796,6 +1008,10 @@ int main(void)
     ot::TestMacChannelMask();
     ot::TestMacFrameApi();
     ot::TestMacFrameAckGeneration();
+#if OPENTHREAD_CONFIG_WAKEUP_COORDINATOR_ENABLE
+    ot::TestMacWakeupFrameGeneration();
+    ot::TestMacWakeupFrameDetectionNegative();
+#endif
     printf("All tests passed\n");
     return 0;
 }