Fatigue Test ESP-NOW Unit - Architecture Documentation

Overview

The Fatigue Test ESP-NOW Unit is a unified fatigue testing system that combines motor control, bounds detection, and wireless communication. It supports dual communication interfaces (ESP-NOW and UART) and dual bounds detection methods (StallGuard2 and encoder-based).

System Architecture

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
29
30
31
32
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Fatigue Test Unit                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   ESP-NOW       β”‚       β”‚   FreeRTOS      β”‚       β”‚   Motion        β”‚  β”‚
β”‚  β”‚   Receiver      │──────▢│   Event Queue   │──────▢│   Controller    β”‚  β”‚
β”‚  β”‚   (ISR-safe)    β”‚       β”‚                 β”‚       β”‚ (Point-to-Point)β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚           β”‚                                                    β”‚          β”‚
β”‚           β”‚                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚          β”‚
β”‚           β”‚                β”‚  Global State   β”‚                 β”‚          β”‚
β”‚           └───────────────▢│  & Settings     β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚
β”‚                                     β”‚                                     β”‚
β”‚        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚        β”‚                            β”‚                            β”‚        β”‚
β”‚        β–Ό                            β–Ό                            β–Ό        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚    UART       β”‚          β”‚   Bounds      β”‚          β”‚   TMC51x0     β”‚  β”‚
β”‚  β”‚   Command     β”‚          β”‚   Finding     β”‚          β”‚   Driver      β”‚  β”‚
β”‚  β”‚   Parser      β”‚          β”‚   (Library)   β”‚          β”‚   (SPI)       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                     β”‚                          β”‚          β”‚
β”‚                                     β”‚                          β”‚          β”‚
β”‚                             β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚                             β”‚   driver.     β”‚          β”‚    Motor      β”‚  β”‚
β”‚                             β”‚   homing      β”‚          β”‚   Hardware    β”‚  β”‚
β”‚                             β”‚   FindBounds  β”‚          β”‚               β”‚  β”‚
β”‚                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Component Overview

1. ESP-NOW Communication Layer

Files: espnow_protocol.hpp, espnow_receiver.hpp/cpp

  • Purpose: Wireless communication with remote controller
  • Protocol Version: 1
  • Header Size: 6 bytes (sync, version, device_id, type, id, len)
  • Max Payload: 200 bytes
  • Error Detection: CRC16-CCITT

Features:

  • Sync byte validation (0xAA)
  • Protocol version checking
  • Sequence ID tracking
  • CRC16-CCITT checksum
  • Queue-based event delivery (ISR-safe)
  • MAC address learning (optional pre-configuration)

Key Functions:

1
2
3
4
5
6
EspNowReceiver::init(QueueHandle_t event_queue);
EspNowReceiver::send_config_response(const Settings& s);
EspNowReceiver::send_config_ack(bool ok, uint8_t err_code);
EspNowReceiver::send_status_update(uint32_t cycle, TestState state);
EspNowReceiver::send_error(uint8_t err_code, uint32_t at_cycle);
EspNowReceiver::send_test_complete();

2. UART Command Interface

Location: main.cpp (UartCommandParser)

  • Purpose: Direct serial control for debugging and development
  • Protocol: Text-based command parser
  • Baud Rate: 115200
  • Commands: -f, -d, -b, -c, -a, -s, -h

3. Bounds Detection System

Implementation: TMC51x0 driver library built-in homing subsystem

  • Purpose: Detect physical limits of motion and establish coordinate frame
  • API: g_driver->homing.FindBounds(method, options, home_config, cancel_cb)

Supported Methods: | Method | Description | |——–|β€”β€”β€”β€”-| | StallGuard | Sensorless stall detection via TMC51x0 StallGuard2 | | Encoder | Encoder-based bounds detection | | Switch | Limit switch detection (available in library) |

BoundsOptions Structure (passed to FindBounds):

1
2
3
4
5
6
7
8
9
10
11
12
13
struct BoundsOptions {
    float search_speed;           // Search velocity (RPM)
    float search_span;            // Max search distance (degrees)
    float backoff_distance;       // Backoff from detected bound (degrees)
    uint32_t timeout_ms;          // Search timeout
    float search_accel;           // Search acceleration (rev/sΒ²)
    float search_decel;           // Search deceleration (rev/sΒ²)
    float current_reduction_factor; // Current reduction during SG search
    StallGuardConfig* stallguard_override; // Optional SG configuration
    Unit speed_unit;              // Unit for speed values
    Unit position_unit;           // Unit for position values
    Unit accel_unit;              // Unit for acceleration values
};

4. Motion Controller

Files: fatigue_motion.hpp, fatigue_motion_impl.hpp

  • Purpose: Generate point-to-point back-and-forth motion between bounds
  • Features:
    • Point-to-point position control (direct VMAX/AMAX)
    • Cycle counting (center-crossing detection)
    • Configurable dwell time at bounds
    • Thread-safe operation (RAII mutex guards)
    • Support for both bounded and unbounded operation

Key Methods:

1
2
3
4
5
6
7
8
9
10
void SetFrequency(float freq_hz);
void SetTargetCycles(uint32_t cycles);
void SetDwellTimes(uint32_t min_ms, uint32_t max_ms);
void SetGlobalBounds(float min_deg, float max_deg);
void SetLocalBoundsFromCenterDegrees(float range);
void Start();
void Pause();
void Resume();
void Stop();
FatigueStatus GetStatus() const;

5. Settings Management

Location: espnow_protocol.hpp

Settings are stored in memory and synchronized via ESP-NOW. Extended fields support backward compatibility.

TestUnitSettings Structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct TestUnitSettings {
    // Base fields (always synchronized)
    uint32_t cycle_amount   = 1000;   // Target cycles
        float    oscillation_vmax_rpm = 60.0f;    // Max velocity during oscillation (RPM)
        float    oscillation_amax_rev_s2 = 10.0f; // Acceleration during oscillation (rev/sΒ²)
        uint32_t dwell_time_ms = 1000;            // Dwell time at endpoints (milliseconds)
    bool bounds_method_stallguard = true; // Detection method
    
    // Extended fields (0.0f = use TestConfig defaults)
    float bounds_search_velocity_rpm = 0.0f;       // Search speed (RPM)
    float stallguard_min_velocity_rpm = 0.0f;      // SG min velocity (RPM)
        int8_t stallguard_sgt = 127;                   // StallGuard threshold [-64..63], 127=default
    float stall_detection_current_factor = 0.0f;   // Current reduction (0.0-1.0)
    float bounds_search_accel_rev_s2 = 0.0f;       // Search acceleration (rev/sΒ²)
};

Extended Field Behavior:

  • Value of 0.0f β†’ test unit uses TestConfig defaults
  • Non-zero value β†’ overrides the default
  • Ensures backward compatibility with older remote controllers

Task Architecture

The system uses FreeRTOS tasks for concurrent operation:

Task Priority Stack Period Description
espnow_command_task 5 4KB Event-driven Process ESP-NOW commands
motion_control_task 5 8KB 10ms Motor motion updates
status_update_task 3 4KB 1000ms Send status to remote
uart_command_task 3 4KB 50ms Process UART commands
bounds_finding_task 4 8KB Dynamic Created on START, deleted on completion

Task Communication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ESP-NOW ISR     │────▢│  Raw RX Queue   │────▢│ espnow_recv_taskβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                         β”‚
                                                         β–Ό
                                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                               β”‚ ProtoEvent Queueβ”‚
                                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                        β”‚
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚                               β”‚                  β”‚
                        β–Ό                               β–Ό                  β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚espnow_cmd_task  β”‚           β”‚bounds_find_task β”‚  β”‚motion_ctrl  β”‚
               β”‚(commands)       β”‚           β”‚(on-demand)      β”‚  β”‚task         β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

State Machine

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
29
30
31
32
33
34
35
36
37
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚      IDLE       │◀─────────────────────────────────┐
                        β”‚  (motor off)    β”‚                                  β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                  β”‚
                                 β”‚                                           β”‚
                                 β”‚ START command                             β”‚
                                 β–Ό                                           β”‚
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                  β”‚
                        β”‚ BOUNDS_FINDING  β”‚                                  β”‚
                        β”‚ (task running)  β”‚                                  β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                  β”‚
                                 β”‚                                           β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”‚
                    β”‚            β”‚            β”‚                              β”‚
                    β–Ό            β–Ό            β–Ό                              β”‚
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
           β”‚ Bounds found β”‚  β”‚ Cancelledβ”‚  β”‚ Timeout/ β”‚                      β”‚
           β”‚              β”‚  β”‚ (PAUSE)  β”‚  β”‚  Error   β”‚                      β”‚
           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                      β”‚
                  β”‚               β”‚             β”‚                            β”‚
                  β”‚               β”‚             └─────────────────────────────
                  β”‚               β”‚                                          β”‚
                  β–Ό               β–Ό                                          β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
         β”‚    RUNNING      β”‚   β”‚     PAUSED      β”‚                           β”‚
         β”‚(point-to-point) │◀─▢│  (motor off)    │────────────────────────────
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        STOP               β”‚
                  β”‚                    β–²                                     β”‚
                  β”‚ PAUSE              β”‚ RESUME                              β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                     β”‚
                  β”‚                                                          β”‚
                  β”‚ Target cycles reached                                    β”‚
                  β–Ό                                                          β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                 β”‚
         β”‚   COMPLETED     β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ (TEST_COMPLETE) β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow

Command Flow (ESP-NOW β†’ Motor)

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
29
30
31
32
33
34
35
36
Remote Controller
        β”‚
        β”‚ ESP-NOW Packet [hdr:6][payload:N][crc:2]
        β–Ό
ESP-NOW Receiver (ISR) ─── espnowRecvCb()
        β”‚
        β”‚ RawMsg (data + len)
        β–Ό
s_raw_recv_queue (FreeRTOS)
        β”‚
        β–Ό
recvTask() ─── handlePacket()
        β”‚  β€’ CRC validation
        β”‚  β€’ Header parsing
        β”‚  β€’ Payload extraction
        β–Ό
ProtoEvent {type, data}
        β”‚
        β–Ό
g_espnowQueue (FreeRTOS)
        β”‚
        β–Ό
espnow_command_task()
        β”‚  β€’ ConfigSet β†’ update g_settings
        β”‚  β€’ CommandStart β†’ RequestStart()
        β”‚  β€’ CommandPause β†’ g_cancel_bounds, Stop()
        β”‚  β€’ etc.
        β–Ό
bounds_finding_task() or motion_control_task()
        β”‚
        β”‚ g_driver->homing.FindBounds() or g_motion->Update()
        β–Ό
TMC51x0 Driver (SPI)
        β”‚
        β–Ό
Motor Hardware

Status Flow (Motor β†’ Remote)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
motion_control_task() or bounds_finding_task()
        β”‚
        β”‚ Motion status (cycles, position, state)
        β–Ό
g_motion->GetStatus()
        β”‚
        β–Ό
status_update_task() (every 1000ms if RUNNING/BOUNDS_FINDING)
        β”‚
        β”‚ EspNowReceiver::send_status_update(cycle, state)
        β–Ό
sendPacketToUi(MsgType::StatusUpdate, StatusPayload)
        β”‚  β€’ Build header
        β”‚  β€’ Copy payload
        β”‚  β€’ Calculate CRC
        β”‚  β€’ Place CRC at correct offset
        β–Ό
esp_now_send(s_ui_board_mac, buffer, len)
        β”‚
        β”‚ ESP-NOW Packet [hdr:6][StatusPayload:6][crc:2]
        β–Ό
Remote Controller

Thread Safety

Mutex Protection

  • TMC Driver Access: Protected by Esp32TmcMutex for all SPI operations
  • Motion Controller: Internal mutex for state access

Queue Communication

  • ProtoEvent Queue: ESP-NOW events to command task
  • Raw RX Queue: ISR to receive task (ISR-safe xQueueSendFromISR)

RAII Guards

1
2
3
4
5
// Automatic mutex locking/unlocking
{
    std::lock_guard<std::mutex> lock(motion_mutex_);
    // Safe access to motion state
}

Error Handling

Error Type Detection Response
Protocol CRC CRC mismatch Packet dropped, log warning
Protocol Version Version check Packet dropped, log warning
Bounds Finding Timeout Timer expiration Error state, report to remote
Bounds Not Found Library result Error state, report to remote
Motion Error State validation Error state, motor disabled

Memory Management

Type Usage Notes
Stack Task stacks (4-8KB each) Sized for worst-case recursion
Heap FreeRTOS queues Created at init, fixed size
Static Global state, settings, driver Single instances
BSS Uninitialized globals Zero-initialized at startup

Performance Characteristics

Metric Value Notes
Motion Update Rate 100 Hz 10ms period in motion_control_task
Status Update Rate 1 Hz 1000ms period in status_update_task
ESP-NOW Latency < 10ms Event-driven, low latency
Command Response < 50ms Including bounds check
Bounds Finding 5-30s Depends on search speed/span

Extension Points

  1. New Motion Patterns: Extend FatigueTestMotion class
    • Add new motion profile generators
    • Implement custom cycle counting logic
  2. New Commands: Extend protocol
    • Add to MsgType enum in both projects
    • Implement handler in espnow_command_task
  3. New Settings: Extend TestUnitSettings
    • Add field to struct
    • Update CONFIG_SET handler
    • Update remote controller payload
  4. New Status Fields: Extend StatusPayload
    • Add fields to struct
    • Update send_status_update()
    • Update remote controller handler

Dependencies

Dependency Version Purpose
ESP-IDF 5.x FreeRTOS, WiFi, ESP-NOW
TMC51x0 Driver Library Latest Motor control, homing
FreeRTOS (ESP-IDF) Task management, queues