Mesh-NOW messages travel as binary frames over ESP-NOW. The wire format uses MessagePack serialization with optional AES-128-GCM encryption.

The canonical protocol specification is mesh_now.ksy at the repository root.

Frame Layout

Offset  Size    Field
------  ------  -----
0       2       magic (0x4d 0x4e)
2       1       version (1)
3       1       flags
4       1       msg_type
5       1       group_id
6       1       hop_limit
7       1       hop_count
8       4       message_id (LE)
12      4       reply_to (LE)
16      6       sender_mac
22      6       target_mac
28      4       timestamp (LE)
32      ...     payload (MessagePack or encrypted)
------  ------  -----

The header is always 32 bytes. The payload follows immediately after.

Field Encoding

All multi-byte fields use little-endian byte order.

magic (2 bytes)

Fixed bytes 0x4d 0x4e (“MN”). Identifies a Mesh-NOW frame.

version (1 byte)

Protocol version. Currently 1.

flags (1 byte)

Bitfield encoding:

Bit 0 (0x01): MSG_FLAG_REQUIRES_ACK
Bit 1 (0x02): MSG_FLAG_ENCRYPTED
Bit 2 (0x04): MSG_FLAG_HAS_NODE_NAME
Bits 3-7:     Reserved (must be 0)

msg_type (1 byte)

Message type identifier. Values 0-9 are defined.

Value Name Description
0 BEACON Peer discovery broadcast
1 CHAT Broadcast chat message
2 DIRECT Point-to-point message
3 ACK Acknowledgment
4 GROUP Group-scoped broadcast
5 PRESENCE Status announcement
6 TYPING Typing indicator
7 ROUTE_REQUEST Route discovery flood
8 ROUTE_REPLY Route discovery reply
9 ROUTE_ERROR Broken next-hop announcement

group_id (1 byte)

Unsigned integer 0-255. Value 0 means no group filter.

hop_limit (1 byte)

Maximum hops for this frame. Set at the origin to DEFAULT_ROUTE_TTL (3) and never modified by relays. The frame is dropped once hop_count reaches hop_limit, so a limit of N permits at most N-1 relays.

hop_count (1 byte)

Hops already travelled. The origin sends 0 and each relay increments it. Dropped when it reaches hop_limit.

message_id (4 bytes)

A monotonically increasing uint32_t. Assigned by the sender, seeded from esp_random() on first use. Used for duplicate detection and ACK matching.

reply_to (4 bytes)

For ACK frames, the message_id of the message being acknowledged. For all other frame types this is 0. Giving each ACK its own message_id (instead of reusing the acked message’s id) lets ACKs participate in seen-message dedup, so routed ACKs are not re-flooded by every relay.

sender_mac / target_mac (6 bytes each)

IEEE 802.11 MAC addresses. Set automatically by the library.

timestamp (4 bytes)

Network time in milliseconds: a monotonic clock advanced by the beacon timestamps received via mesh_now_sync_time(). Relative to init, little-endian.

Payload (unencrypted)

When MSG_FLAG_ENCRYPTED is not set, the payload is a raw MessagePack-encoded map:

type: map
fields:
  content:        str         # message text (variable length)
  node_name:      str         # optional, in beacons/RREQ/RREP when HAS_NODE_NAME flag set
  neighbor_macs:  array       # zone announce: one-hop peer MACs (beacons only)
  neighbor_names: array       # parallel peer names, one per neighbor_macs entry

Payload (encrypted)

When MSG_FLAG_ENCRYPTED is set, the payload contains:

Offset  Size    Field
------  ------  -----
0       12      nonce (IV)
12      ...     ciphertext (MessagePack-encoded plaintext)
...     16      AES-GCM auth tag

Nonce Construction

The 12-byte nonce is built from:

  • message_id (4 bytes, little-endian)
  • sender_mac (6 bytes)
  • a fixed 0x00 0x00 pad (2 bytes)

This gives each message a unique nonce as long as message IDs are unique per sender. The fixed pad is a domain separator, and the sender MAC disambiguates nodes that share a network key.

Auth Tag

16-byte AES-GCM authentication tag appended after the ciphertext. Provides integrity and authenticity for both the ciphertext and the AAD (header fields).

AAD (Additional Authenticated Data)

The following header fields are authenticated but not encrypted:

  • msg_type (1 byte)
  • sender_mac (6 bytes)
  • target_mac (6 bytes)
  • group_id (1 byte)
  • timestamp (4 bytes)

Total AAD: 18 bytes.

Wire Size Comparison

The payload carries only the message data, so total frame size is the 32-byte header plus a small typed map plus the message text.

Message Legacy (fixed 152B) New Savings
ACK 152 bytes 41 bytes 73%
Typing 152 bytes 47 bytes 69%
Short “ok” 152 bytes 43 bytes 72%
100-char chat 152 bytes 142 bytes 7%
Full 128B 152 bytes 170 bytes (198 encrypted) -12%
ESP-NOW Compatibility

ESP-NOW supports frames up to 250 bytes. A max-length 128-char message encodes to 170 bytes unencrypted and 198 bytes encrypted, both comfortably inside the cap.

Next Steps