ESP-NOW Protocol Specification
Overview
The ESP-NOW protocol provides wireless communication between the remote controller (UI board) and the fatigue test unit. It uses a custom packet format with CRC16-CCITT error detection.
Protocol Version
Header Version: 1
The protocol header includes a version field for future compatibility.
Packet Format
Header Structure (6 bytes)
1
2
3
4
5
6
7
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EspNowHeader (6 bytes) β
ββββββββββ¬ββββββββββ¬ββββββββββββ¬βββββββββ¬βββββββββ¬βββββββββββββββββ€
β Sync β Version β Device ID β Type β ID β Length β
β (1B) β (1B) β (1B) β (1B) β (1B) β (1B) β
β 0xAA β 1 β 1 β MsgTypeβ SeqNum β 0-200 bytes β
ββββββββββ΄ββββββββββ΄ββββββββββββ΄βββββββββ΄βββββββββ΄βββββββββββββββββ
Field Descriptions:
- Sync: Always
0xAA(sync byte for packet detection) - Version: Protocol version (currently
1) - Device ID: Device type identifier (0 = broadcast, 1 = Fatigue Tester)
- Type: Message type (see MsgType enum below)
- ID: Sequence ID (increments per message, wraps at 255)
- Length: Payload length in bytes (0-200)
Full Packet Structure
1
2
3
4
5
6
7
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EspNowHeader (6 bytes) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Payload (0-200 bytes, variable length per hdr.len) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β CRC16 (2 bytes, CRC16-CCITT over header + payload) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total Packet Size: 8-208 bytes (6 header + 0-200 payload + 2 CRC)
Important: The CRC is placed immediately after the payload, NOT at a fixed offset. For a 29-byte payload, CRC is at offset 35 (6 + 29).
Message Types (MsgType Enum)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum class MsgType : uint8_t {
DeviceDiscovery = 1, // Discover devices on network
DeviceInfo = 2, // Device information response
ConfigRequest = 3, // Request current configuration
ConfigResponse = 4, // Send current configuration
ConfigSet = 5, // Set new configuration
ConfigAck = 6, // Acknowledge config set
Command = 7, // Send command (Start/Pause/Resume/Stop)
CommandAck = 8, // Acknowledge command receipt
StatusUpdate = 9, // Periodic status update
Error = 10, // Error notification
ErrorClear = 11, // Clear error state
TestComplete = 12, // Test completion notification
// Fatigue-test extensions
BoundsResult = 13,
// Security / pairing (20-29)
PairingRequest = 20,
PairingResponse = 21,
PairingConfirm = 22,
PairingReject = 23,
Unpair = 24
};
Message Summary
| Type | Value | Direction | Payload | Description |
|---|---|---|---|---|
DeviceDiscovery |
1 | Controller β Unit | None | Discover devices |
DeviceInfo |
2 | Unit β Controller | Device info | Device information |
ConfigRequest |
3 | Controller β Unit | None | Request configuration |
ConfigResponse |
4 | Unit β Controller | ConfigPayload (17-34B) | Current configuration |
ConfigSet |
5 | Controller β Unit | ConfigPayload (17-34B) | Set configuration |
ConfigAck |
6 | Unit β Controller | ConfigAckPayload (2B) | Acknowledge config |
Command |
7 | Controller β Unit | CommandPayload (1B+) | Start/Pause/Resume/Stop |
CommandAck |
8 | Unit β Controller | None | Acknowledge command |
StatusUpdate |
9 | Unit β Controller | StatusPayload (6B) | Periodic status |
Error |
10 | Unit β Controller | ErrorPayload (5B) | Error notification |
ErrorClear |
11 | Unit β Controller | None | Error cleared |
TestComplete |
12 | Unit β Controller | None | Test completed |
BoundsResult |
13 | Unit β Controller | BoundsResultPayload | Bounds finding result |
Payload Structures
ConfigPayload (17-34 bytes)
Used in ConfigSet and ConfigResponse messages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma pack(push, 1)
struct ConfigPayload {
// Base fields (17 bytes) - required, always present
uint32_t cycle_amount; // Target number of cycles (0 = infinite)
float oscillation_vmax_rpm; // Max velocity during oscillation (RPM)
float oscillation_amax_rev_s2; // Acceleration during oscillation (rev/sΒ²)
uint32_t dwell_time_ms; // Dwell time at endpoints (milliseconds)
uint8_t bounds_method; // 0 = StallGuard, 1 = Encoder
// Extended fields (16 bytes) - optional advanced configuration for bounds finding
float bounds_search_velocity_rpm; // Search speed (RPM), 0 = use default
float stallguard_min_velocity_rpm; // SG2 min velocity (RPM), 0 = use default
float stall_detection_current_factor; // Current reduction (0.0-1.0), 0 = use default
float bounds_search_accel_rev_s2; // Search acceleration (rev/sΒ²), 0 = use default
// Extended v2 field (optional)
int8_t stallguard_sgt; // StallGuard threshold [-64..63], 127 = default
};
#pragma pack(pop)
Extended Field Behavior:
- Value of
0.0fmeans βuse test unitβs default from TestConfigβ - Non-zero values override the test unitβs defaults
- Backward compatible: older controllers can send only 17 bytes (base fields)
Byte Layout (base + optional extensions):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Offset Size Field
------ ---- -----
0 4 cycle_amount
4 4 oscillation_vmax_rpm (float)
8 4 oscillation_amax_rev_s2 (float)
12 4 dwell_time_ms
16 1 bounds_method
17 4 bounds_search_velocity_rpm (float)
21 4 stallguard_min_velocity_rpm (float)
25 4 stall_detection_current_factor (float)
29 4 bounds_search_accel_rev_s2 (float)
33 1 stallguard_sgt (int8)
------ ----
Total: 17 bytes (base)
33 bytes (base + bounds tuning)
34 bytes (base + bounds tuning + SGT)
CommandPayload (1+ bytes)
Used in Command messages.
1
2
3
4
struct CommandPayload {
uint8_t command_id; // CommandId enum value
// Additional command-specific data may follow
};
Command IDs:
1
2
3
4
5
6
7
8
enum class CommandId : uint8_t {
Start = 1, // Start fatigue test
Pause = 2, // Pause test
Resume = 3, // Resume test
Stop = 4, // Stop test
RunBoundsFinding = 5 // Run bounds finding routine (independent of starting the test)
};
ConfigAckPayload (2 bytes)
Used in ConfigAck messages.
1
2
3
4
struct ConfigAckPayload {
uint8_t ok; // 1 = success, 0 = failure
uint8_t err_code; // Error code if ok == 0
};
StatusPayload (6 bytes)
Used in StatusUpdate messages.
1
2
3
4
5
struct StatusPayload {
uint32_t cycle_number; // Current cycle count
uint8_t state; // TestState enum value
uint8_t err_code; // Error code if state == Error
};
Byte Layout:
1
2
3
4
5
6
7
Offset Size Field
------ ---- -----
0 4 cycle_number
4 1 state
5 1 err_code
------ ----
Total: 6 bytes
ErrorPayload (5 bytes)
Used in Error messages.
1
2
3
4
struct ErrorPayload {
uint8_t err_code; // Error code
uint32_t at_cycle; // Cycle number when error occurred
};
Test States
1
2
3
4
5
6
7
enum class TestState : uint8_t {
Idle = 0, // Not running, ready for commands
Running = 1, // Test in progress (includes bounds finding)
Paused = 2, // Test paused, motor de-energized
Completed = 3, // Test completed successfully
Error = 4 // Error state
};
Error Codes
| Code | Description |
|---|---|
| 0 | Success / No error |
| 1 | Bounds not found |
| 2 | Start failed |
| 3 | Configuration error |
| 4 | Motion control error |
| 5 | Communication error |
CRC16-CCITT Calculation
The CRC is calculated over the header (6 bytes) + payload (0-200 bytes). The CRC field itself is NOT included in the calculation.
Parameters:
- Polynomial:
0x1021(CRC16-CCITT) - Initial Value:
0xFFFF
Algorithm:
1
2
3
4
5
6
7
8
9
10
11
12
13
uint16_t crc16_ccitt(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; ++j) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}
CRC Placement: The CRC is placed immediately after the payload in the transmitted packet:
- Offset =
sizeof(EspNowHeader) + payload_len=6 + len - For a ConfigPayload (29 bytes): CRC at offset 35
Communication Flow
Initialization Sequence
1
2
3
4
5
6
7
8
Remote Controller Test Unit
β β
βββββ CONFIG_REQUEST (8 bytes) βββββββΊβ
β [hdr:6][crc:2] β
β β
ββββ CONFIG_RESPONSE (37 bytes) βββββββ
β [hdr:6][ConfigPayload:29][crc:2] β
β β
Configuration Update Sequence
1
2
3
4
5
6
7
8
Remote Controller Test Unit
β β
βββββ CONFIG_SET (37 bytes) ββββββββββΊβ
β [hdr:6][ConfigPayload:29][crc:2]β
β β
ββββ CONFIG_ACK (10 bytes) ββββββββββββ
β [hdr:6][ConfigAckPayload:2][crc:2]β
β β
Test Execution Sequence
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Remote Controller Test Unit
β β
βββββ Command:Start (9 bytes) ββββββββΊβ
β [hdr:6][cmd_id:1][crc:2] β
β β
ββββ COMMAND_ACK (8 bytes) ββββββββββββ
β [hdr:6][crc:2] β
β β
ββββ STATUS_UPDATE (14 bytes, 1Hz) ββββ (periodic while running)
β [hdr:6][StatusPayload:6][crc:2] β
β β
βββββ Command:Pause (9 bytes) ββββββββΊβ
β β
ββββ COMMAND_ACK ββββββββββββββββββββββ
β β
βββββ Command:Resume (9 bytes) βββββββΊβ
β β
ββββ COMMAND_ACK ββββββββββββββββββββββ
β β
ββββ STATUS_UPDATE (14 bytes, 1Hz) ββββ (periodic continues)
β β
βββββ Command:Stop (9 bytes) βββββββββΊβ
β β
ββββ COMMAND_ACK ββββββββββββββββββββββ
β β
ββββ TEST_COMPLETE (8 bytes) ββββββββββ (on normal completion)
β [hdr:6][crc:2] β
β β
Status Update Timing
The test unit sends STATUS_UPDATE messages:
- Periodic: Every 1 second while state is
RunningorBoundsFinding - Immediate: On START, PAUSE, RESUME, STOP command acknowledgment
- On Completion: Final status before
TEST_COMPLETE
WiFi Configuration
Default WiFi Channel: 1
Both devices must use the same WiFi channel for ESP-NOW communication.
Configured in both projectsβ config.hpp / espnow_protocol.hpp.
MAC Address Configuration
Remote Controller β Test Unit
The remote controller must be configured with the test unitβs MAC address:
1
2
// In remote controller's config.hpp
static constexpr uint8_t TEST_UNIT_MAC_[6] = { 0x24, 0x6F, 0x28, 0xXX, 0xXX, 0xXX };
Test Unit β Remote Controller (Optional)
The test unit can optionally be pre-configured with the remote controllerβs MAC:
1
2
// In test unit's espnow_protocol.hpp
static constexpr uint8_t UI_BOARD_MAC[6] = { 0x9C, 0x9E, 0x6E, 0xXX, 0xXX, 0xXX };
If not pre-configured (all zeros), the test unit learns the MAC from the first received packet.
Sequence ID Management
- Sequence IDs increment for each sent message (per device)
- Used for tracking message order and detecting duplicates
- Wraps around at 255 (uint8_t)
- Not currently used for retry logic
Reliability Features
| Feature | Implementation |
|---|---|
| CRC Validation | All packets validated with CRC16-CCITT |
| Sync Byte | All packets must start with 0xAA |
| Version Check | Protocol version must match |
| Length Validation | Payload length checked against expected |
| Retry Logic | Application layer (manual via UI) |
Performance Characteristics
| Metric | Value |
|---|---|
| Latency | < 10ms typical (ESP-NOW is low-latency) |
| Status Update Rate | 1 Hz (every 1000ms) |
| Range | ~100-200m line-of-sight (environment dependent) |
| Max Payload | 200 bytes |
| Max Packet | 208 bytes (6 + 200 + 2) |
Backward Compatibility
The protocol supports backward compatibility:
- Base ConfigPayload (13 bytes): Older remote controllers can send only base fields
- Extended ConfigPayload (29 bytes): Newer controllers include extended float fields
- Extended fields = 0.0f: Interpreted as βuse test unit defaultsβ
The test unit checks payload_len to determine which format was received.
Implementation Files
| Component | File | Description |
|---|---|---|
| Test Unit Protocol | espnow_protocol.hpp |
Protocol definitions |
| Test Unit Receiver | espnow_receiver.cpp |
Receive/send implementation |
| Test Unit Main | main.cpp |
Event handling |
| Remote Controller Protocol | protocol/espnow_protocol.hpp |
Protocol definitions |
| Remote Controller Impl | protocol/espnow_protocol.cpp |
Send/receive implementation |
| Device Payloads | protocol/device_protocols.hpp |
Payload structures |
| Fatigue Tester UI | devices/fatigue_tester.cpp |
UI event handling |