HF-TMC51x0 Driver (TMC5130 & TMC5160) 0.1.0-dev
Hardware Agnostic C++ Driver for the TMC51x0 (TMC5130 & TMC5160)
Loading...
Searching...
No Matches
fatigue_motion_impl.hpp
Go to the documentation of this file.
1
11#pragma once
12// When included from header, use conditional include; when compiled directly, include header
13#ifdef FATIGUE_MOTION_HEADER_INCLUDED
14// Already included from header - the class definition is available in the current context
15// We're inside the namespace, so we can access the class
16// No need to include header or open namespace
17#else
18// Not included from header (shouldn't happen for template implementation)
19#include "fatigue_motion.hpp"
20#endif
21
22#include "esp_log.h"
23#include "esp_timer.h"
25#include <algorithm>
26#include <cmath>
27
28static const char* TAG_MOTION = "FatigueMotion";
29
30// When included from header, namespace is already closed in header
31// When compiled standalone, we need to open the namespace
32#ifdef FATIGUE_MOTION_HEADER_INCLUDED
33// Included from header - namespace was closed before this include
34// Use 'using namespace' to bring namespace into scope (matches tmc51x0.cpp pattern)
35using namespace FatigueTest;
36#else
37// Standalone compilation - open namespace
38namespace FatigueTest {
39#endif
40
41// Note: Esp32TmcMutex and TmcMutexGuard are included by fatigue_motion.hpp
42
43// Implementation
44
46 : driver_(driver), global_min_bound_(0.0f), global_max_bound_(0.0f), local_min_bound_(0.0f), local_max_bound_(0.0f),
47 home_position_(0.0f), bounded_(false), amplitude_(1000.0F),
48 vmax_rpm_(60.0f), amax_rev_s2_(10.0f), // Default motion parameters
49 dwell_at_min_ms_(0), dwell_at_max_ms_(0), running_(false), start_time_us_(0),
50 target_cycles_(0), current_cycles_(0), cycle_complete_(false),
51 state_(MotionState::STOPPED), dwell_start_time_ms_(0),
52 estimated_frequency_hz_(0.0f),
53 driver_mutex_(driver_mutex) {
54 // Note: Initialization order matches member declaration order in header
55}
56
57FatigueTestMotion::~FatigueTestMotion() noexcept = default;
58
59
60void FatigueTestMotion::SetGlobalBounds(float min_bound_degrees, float max_bound_degrees) noexcept {
61 {
62 TmcMutexGuard guard(driver_mutex_);
63 global_min_bound_ = min_bound_degrees;
64 global_max_bound_ = max_bound_degrees;
65 bounded_ = true;
66 }
67 ESP_LOGI(TAG_MOTION, "Global bounds set: min=%.2f°, max=%.2f°", global_min_bound_, global_max_bound_);
68
69 // Clip local bounds to global bounds if they exist
70 {
71 TmcMutexGuard guard(driver_mutex_);
72 if (local_min_bound_ != 0.0f || local_max_bound_ != 0.0f) {
73 guard.unlock();
74 ClipLocalBoundsToGlobal();
75 return;
76 }
77 }
78}
79
80void FatigueTestMotion::GetGlobalBoundsDegrees(float& min_degrees, float& max_degrees) const noexcept {
81 TmcMutexGuard guard(driver_mutex_);
82 min_degrees = global_min_bound_;
83 max_degrees = global_max_bound_;
84}
85
86void FatigueTestMotion::SetUnbounded(float current_position_degrees, float default_range_degrees) noexcept {
87 {
88 TmcMutexGuard guard(driver_mutex_);
89 bounded_ = false;
90 home_position_ = current_position_degrees;
91 global_min_bound_ = current_position_degrees - default_range_degrees / 2.0f;
92 global_max_bound_ = current_position_degrees + default_range_degrees / 2.0f;
93 }
94 // Establish home/zero at current position
95 // IMPORTANT: After this point, use ABSOLUTE positioning (SetTargetPosition)
96 // Before this, we would use RELATIVE positioning (MoveRelative) if needed
97 driver_->rampControl.SetCurrentPosition(0.0f, tmc51x0::Unit::Deg);
98 driver_->rampControl.SetTargetPosition(0.0f, tmc51x0::Unit::Deg);
99 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
100 ESP_LOGW(TAG_MOTION, "Unbounded mode: No mechanical stops found");
101 ESP_LOGI(TAG_MOTION, "Using current position as home: %.2f°", current_position_degrees);
102 ESP_LOGI(TAG_MOTION, "Default global range: [%.2f°, %.2f°]", global_min_bound_, global_max_bound_);
103}
104
105bool FatigueTestMotion::SetLocalBoundsFromCenterDegrees(float min_degrees_from_center, float max_degrees_from_center,
106 float edge_backoff_deg) noexcept {
107 float min_deg, max_deg;
108 bool is_bounded;
109 float global_min, global_max;
110 {
111 TmcMutexGuard guard(driver_mutex_);
112 min_deg = min_degrees_from_center;
113 max_deg = max_degrees_from_center;
114 is_bounded = bounded_;
115 global_min = global_min_bound_;
116 global_max = global_max_bound_;
117 }
118
119 // Clip to global bounds (all in degrees)
120 if (is_bounded) {
121 min_deg = std::max(min_deg, global_min);
122 max_deg = std::min(max_deg, global_max);
123 }
124
125 // Apply edge backoff to stay inside mechanical bounds during oscillation
126 // This prevents repeatedly "kissing" the endpoints and accounts for any residual error
127 if (edge_backoff_deg > 0.0f) {
128 float backoff_min = min_deg + edge_backoff_deg;
129 float backoff_max = max_deg - edge_backoff_deg;
130
131 // Only apply backoff if it leaves a valid range
132 if (backoff_min < backoff_max) {
133 ESP_LOGI(TAG_MOTION, "Applying edge backoff: %.2f° (min: %.2f° -> %.2f°, max: %.2f° -> %.2f°)",
134 edge_backoff_deg, min_deg, backoff_min, max_deg, backoff_max);
135 min_deg = backoff_min;
136 max_deg = backoff_max;
137 } else {
138 ESP_LOGW(TAG_MOTION, "Edge backoff %.2f° too large for range [%.2f°, %.2f°] - using full range",
139 edge_backoff_deg, min_deg, max_deg);
140 }
141 }
142
143 {
144 TmcMutexGuard guard(driver_mutex_);
145 local_min_bound_ = min_deg;
146 local_max_bound_ = max_deg;
147 home_position_ = (local_min_bound_ + local_max_bound_) / 2.0f;
148 amplitude_ = (local_max_bound_ - local_min_bound_) / 2.0f;
149 }
150
151 ESP_LOGI(TAG_MOTION, "Local bounds set: min=%.2f°, max=%.2f° (with %.2f° edge backoff)",
152 min_deg, max_deg, edge_backoff_deg);
153
154 // Recalculate estimated frequency with new bounds
155 {
156 TmcMutexGuard guard(driver_mutex_);
157 RecalculateEstimatedFrequency();
158 }
159 return true;
160}
161
162void FatigueTestMotion::GetLocalBoundsFromCenterDegrees(float& min_degrees, float& max_degrees) const noexcept {
163 TmcMutexGuard guard(driver_mutex_);
164 min_degrees = local_min_bound_;
165 max_degrees = local_max_bound_;
166}
167
168// Velocity/Acceleration safety limits (conservative for fatigue testing)
169static constexpr float VMAX_MIN_RPM = 5.0f; // Minimum useful velocity
170static constexpr float VMAX_MAX_RPM = 120.0f; // Maximum safe velocity for fatigue testing
171static constexpr float AMAX_MIN_REV_S2 = 0.5f; // Minimum acceleration
172static constexpr float AMAX_MAX_REV_S2 = 30.0f; // Maximum acceleration
173
174bool FatigueTestMotion::SetMaxVelocity(float vmax_rpm) noexcept {
175 // Clamp to safe limits
176 float clamped = std::clamp(vmax_rpm, VMAX_MIN_RPM, VMAX_MAX_RPM);
177 if (clamped != vmax_rpm) {
178 ESP_LOGW(TAG_MOTION, "VMAX clamped: %.1f -> %.1f RPM (limits: %.1f-%.1f)",
179 vmax_rpm, clamped, VMAX_MIN_RPM, VMAX_MAX_RPM);
180 }
181 {
182 TmcMutexGuard guard(driver_mutex_);
183 vmax_rpm_ = clamped;
184 RecalculateEstimatedFrequency();
185
186 // Always update the driver immediately - keeps registers in sync with settings
187 // This ensures changes take effect whether motion is running, paused, or stopped
188 driver_->rampControl.SetMaxSpeed(vmax_rpm_, tmc51x0::Unit::RPM);
189 }
190 ESP_LOGI(TAG_MOTION, "Max velocity set: %.1f RPM", clamped);
191 return true;
192 }
193
194float FatigueTestMotion::GetMaxVelocity() const noexcept {
196 return vmax_rpm_;
197}
198
199bool FatigueTestMotion::SetAcceleration(float amax_rev_s2) noexcept {
200 // Clamp to safe limits
201 float clamped = std::clamp(amax_rev_s2, AMAX_MIN_REV_S2, AMAX_MAX_REV_S2);
202 if (clamped != amax_rev_s2) {
203 ESP_LOGW(TAG_MOTION, "AMAX clamped: %.2f -> %.2f rev/s² (limits: %.2f-%.2f)",
204 amax_rev_s2, clamped, AMAX_MIN_REV_S2, AMAX_MAX_REV_S2);
205 }
206 {
207 TmcMutexGuard guard(driver_mutex_);
208 amax_rev_s2_ = clamped;
209 RecalculateEstimatedFrequency();
210
211 // Always update the driver immediately - keeps registers in sync with settings
212 // This ensures changes take effect whether motion is running, paused, or stopped
213 driver_->rampControl.SetAcceleration(amax_rev_s2_, tmc51x0::Unit::RevPerSec);
214 driver_->rampControl.SetDeceleration(amax_rev_s2_, tmc51x0::Unit::RevPerSec);
215 }
216 ESP_LOGI(TAG_MOTION, "Acceleration set: %.2f rev/s²", clamped);
217 return true;
218}
219
222 return amax_rev_s2_;
223}
224
229
230bool FatigueTestMotion::SetDwellTimes(uint32_t dwell_at_min_ms, uint32_t dwell_at_max_ms) noexcept {
231 // Accept dwell times as-is (no minimum enforcement - remote controller controls this)
232 {
233 TmcMutexGuard guard(driver_mutex_);
234 dwell_at_min_ms_ = dwell_at_min_ms;
235 dwell_at_max_ms_ = dwell_at_max_ms;
236 RecalculateEstimatedFrequency();
237 }
238 ESP_LOGI(TAG_MOTION, "Dwell times updated: min=%lu ms, max=%lu ms",
239 dwell_at_min_ms, dwell_at_max_ms);
240 return true;
241}
242
243void FatigueTestMotion::GetDwellTimes(uint32_t& dwell_at_min_ms, uint32_t& dwell_at_max_ms) const noexcept {
244 TmcMutexGuard guard(driver_mutex_);
245 dwell_at_min_ms = dwell_at_min_ms_;
246 dwell_at_max_ms = dwell_at_max_ms_;
247}
248
249bool FatigueTestMotion::SetTargetCycles(uint32_t cycles) noexcept {
250 {
251 TmcMutexGuard guard(driver_mutex_);
252 target_cycles_ = cycles;
253 }
254 ESP_LOGI(TAG_MOTION, "Target cycles set: %lu (0 = infinite)", target_cycles_);
255 return true;
256}
257
258uint32_t FatigueTestMotion::GetCurrentCycles() const noexcept {
260 return current_cycles_;
261}
262
263uint32_t FatigueTestMotion::GetTargetCycles() const noexcept {
265 return target_cycles_;
266}
267
270 return cycle_complete_;
271}
272
274 {
276 current_cycles_ = 0;
277 cycle_complete_ = false;
278 }
279 ESP_LOGI(TAG_MOTION, "Cycle count reset");
280}
281
282bool FatigueTestMotion::IsBounded() const noexcept {
284 return bounded_;
285}
286
288 uint32_t current_cycles, target_cycles;
289
290 {
292 // CRITICAL: Verify bounds are set before allowing motion to start
293 if (fabsf(local_min_bound_) < 0.01f && fabsf(local_max_bound_) < 0.01f) {
294 ESP_LOGE(TAG_MOTION, "Cannot start: local bounds not set!");
295 return false;
296 }
297
298 if (cycle_complete_) {
299 ESP_LOGW(TAG_MOTION, "Cycle count reached. Reset cycles or set new target to continue.");
300 return false;
301 }
302 }
303
304 // CRITICAL: Verify motor is stopped before starting new motion
305 // This prevents parameter changes during active motion
306 // Check outside mutex to avoid holding lock during driver calls
307 auto standstill_result = driver_->rampControl.IsStandstill();
308 if (!standstill_result || !standstill_result.Value()) {
309 ESP_LOGW(TAG_MOTION, "Motor not at standstill, stopping before start...");
310 driver_->rampControl.Stop();
311 vTaskDelay(pdMS_TO_TICKS(200));
312 // Wait for standstill
313 uint32_t checks = 0;
314 while (checks < 20) {
315 checks++;
316 auto check_result = driver_->rampControl.IsStandstill();
317 if (check_result && check_result.Value()) {
318 break;
319 }
320 vTaskDelay(pdMS_TO_TICKS(100));
321 }
322 }
323
324 {
326
327 // Update estimated frequency before starting (informational)
329
330 // =====================================================================
331 // RAMP SPEED CONFIGURATION
332 // Use motor config defaults for VSTART/VSTOP (don't override)
333 // These are tuned per motor and should be used as-is
334 // =====================================================================
335 // Get motor config defaults for VSTART/VSTOP
337 const float vstart_rpm = MotorConfig::RAMP_VSTART_RPM;
338 const float vstop_rpm = MotorConfig::RAMP_VSTOP_RPM;
339
340 // =====================================================================
341 // TMC5160 DATASHEET COMPLIANCE: Correct order for starting motion
342 // Per datasheet Section 12 (Ramp Generator):
343 // 1. Stay in HOLD mode while configuring
344 // 2. Set VMAX, AMAX, DMAX (safe in HOLD - no motion occurs)
345 // 3. Set XTARGET to desired position (safe in HOLD - no motion occurs)
346 // 4. THEN switch to POSITIONING mode → motor starts moving
347 //
348 // WRONG order was: Set POSITIONING first, then target (causes brief wrong-direction motion)
349 // =====================================================================
350
351 // Ensure we're in HOLD mode before changing parameters
352 // This prevents any motion until we explicitly switch to POSITIONING
353 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
354
355 // Configure ramp parameters (safe to do in HOLD mode - no motion yet)
356 driver_->rampControl.SetMaxSpeed(vmax_rpm_, tmc51x0::Unit::RPM);
357 driver_->rampControl.SetAcceleration(amax_rev_s2_, tmc51x0::Unit::RevPerSec);
358 driver_->rampControl.SetDeceleration(amax_rev_s2_, tmc51x0::Unit::RevPerSec);
359 driver_->rampControl.SetRampSpeeds(vstart_rpm, vstop_rpm, 0.0f, tmc51x0::Unit::RPM);
360
361 // CRITICAL: Ensure StallGuard stop-on-stall is disabled for normal motion
362 // This prevents false stall detection during oscillation
363 (void)driver_->stallGuard.EnableStopOnStall(false);
364
365 // CRITICAL: Enable StealthChop for smooth, quiet motion during oscillations
366 // StealthChop provides smooth motion without the vibration of SpreadCycle
367 (void)driver_->motorControl.SetStealthChopEnabled(true);
368
369 // Check if resuming from pause
370 bool resuming_from_pause = (state_ == MotionState::PAUSED);
371
372 if (resuming_from_pause) {
373 // Resume from pause - restore motion from current position
374 running_ = true;
375 // Don't reset start_time_us_ - keep the original start time for cycle counting
376 ESP_LOGI(TAG_MOTION, "Resuming from pause - continuing motion from current position");
377 } else {
378 // Fresh start - reset everything
379 running_ = true;
380 start_time_us_ = esp_timer_get_time();
381 }
382
383 // Determine target based on current position (for both fresh start and resume)
384 auto current_pos_result = driver_->rampControl.GetCurrentPosition(tmc51x0::Unit::Deg);
385 float current_pos_deg = current_pos_result.IsOk() ? current_pos_result.Value() : 0.0f;
386 float min_pos_deg = local_min_bound_;
387 float max_pos_deg = local_max_bound_;
388 float home_pos = home_position_;
389
390 // Determine which direction to move based on current position
391 float dist_to_home = fabsf(current_pos_deg - home_pos);
392 float dist_to_min = fabsf(current_pos_deg - min_pos_deg);
393 float dist_to_max = fabsf(current_pos_deg - max_pos_deg);
394
395 // SET TARGET FIRST (while still in HOLD mode - no motion yet)
396 // This ensures XTARGET is correct BEFORE we switch to POSITIONING
397 float target_pos;
398 constexpr float NEAR_THRESHOLD_DEG = 2.0f;
399 if (dist_to_home < NEAR_THRESHOLD_DEG) {
400 // At center, start cycle by moving to MAX
402 target_pos = max_pos_deg;
403 } else if (dist_to_max < NEAR_THRESHOLD_DEG) {
404 // At MAX, move to MIN
406 target_pos = min_pos_deg;
407 } else if (dist_to_min < NEAR_THRESHOLD_DEG) {
408 // At MIN, this completes a cycle - move to MAX for next cycle
410 target_pos = max_pos_deg;
411 } else {
412 // Somewhere in between - move toward nearest endpoint
413 state_ = (dist_to_min <= dist_to_max) ? MotionState::MOVING_TO_MIN : MotionState::MOVING_TO_MAX;
414 target_pos = (dist_to_min <= dist_to_max) ? min_pos_deg : max_pos_deg;
415 }
416
417 // Set target position WHILE STILL IN HOLD MODE (no motion yet)
418 driver_->rampControl.SetTargetPosition(target_pos, tmc51x0::Unit::Deg);
419
420 // NOW switch to POSITIONING mode - motor starts moving toward the already-set target
421 // This is the correct order per TMC5160 datasheet
422 driver_->rampControl.SetRampMode(tmc51x0::RampMode::POSITIONING);
423
424 current_cycles = current_cycles_;
425 target_cycles = target_cycles_;
426 }
427
428 ESP_LOGI(TAG_MOTION, "Starting fatigue test (cycles: %lu/%lu)", current_cycles,
429 target_cycles == 0 ? 0xFFFFFFFF : target_cycles);
430 ESP_LOGI(TAG_MOTION, " Motion: Point-to-point mode (center->MAX->MIN), VMAX=%.1f RPM, AMAX=%.2f rev/s², Est.Freq=%.2f Hz",
432 ESP_LOGI(TAG_MOTION, " Dwell: min=%lu ms, max=%lu ms", (unsigned long)dwell_at_min_ms_, (unsigned long)dwell_at_max_ms_);
433 ESP_LOGI(TAG_MOTION, " Cycle counting: one cycle = center -> MAX -> MIN (counted at MIN)");
434
435
436 return true;
437}
438
440 // Simple approach: Just set state to PAUSED
441 // Update() will stop commanding new targets, but current move will finish naturally
442 {
445 // running_ stays true - we can resume
446 // Don't stop the motor - let it finish the current move to its target
447 }
448 ESP_LOGI(TAG_MOTION, "Paused fatigue test motion - current move will finish, then hold");
449}
450
451void FatigueTestMotion::Stop() noexcept {
452 uint32_t cycles;
453 {
455 running_ = false;
456 state_ = MotionState::STOPPED; // Set state first to stop Update() from setting new targets
457 cycles = current_cycles_;
458 }
459
460 // Stop the ramp generator (outside mutex)
461 driver_->rampControl.Stop();
462
463 // Wait for standstill
464 uint32_t checks = 0;
465 while (checks < 50) { // Max 5 seconds
466 checks++;
467 auto standstill_result = driver_->rampControl.IsStandstill();
468 if (standstill_result.IsOk() && standstill_result.Value()) {
469 break;
470 }
471 vTaskDelay(pdMS_TO_TICKS(100));
472 }
473
474 // Clear target and set HOLD mode
475 {
477 auto current_pos_result = driver_->rampControl.GetCurrentPosition(tmc51x0::Unit::Deg);
478 if (current_pos_result.IsOk()) {
479 float current_pos = current_pos_result.Value();
480 driver_->rampControl.SetTargetPosition(current_pos, tmc51x0::Unit::Deg);
481 }
482 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
483 }
484
485 ESP_LOGI(TAG_MOTION, "Stopped fatigue test motion (cycles completed: %lu)", cycles);
486}
487
488bool FatigueTestMotion::IsRunning() const noexcept {
491}
492
494 // =========================================================================
495 // ESTIMATED FREQUENCY CALCULATION
496 // Calculate the estimated cycle frequency based on user-set VMAX, AMAX,
497 // and travel distance. This is informational - the user directly controls
498 // VMAX and AMAX, so the frequency is a derived result.
499 // =========================================================================
500
501 // Calculate total travel distance (one way) in degrees
502 float distance_deg = fabsf(local_max_bound_ - local_min_bound_);
503 if (distance_deg < 0.1f || vmax_rpm_ <= 0.0f || amax_rev_s2_ <= 0.0f) {
505 ESP_LOGW(TAG_MOTION, "Cannot estimate frequency (dist=%.2f°, vmax=%.1f RPM, amax=%.2f rev/s²)",
506 distance_deg, vmax_rpm_, amax_rev_s2_);
507 return;
508 }
509
510 // Convert VMAX to deg/s: RPM * (360 deg/rev) / (60 s/min) = deg/s
511 float vmax_deg_per_s = vmax_rpm_ * 360.0f / 60.0f;
512
513 // Convert AMAX to deg/s²: rev/s² * 360 deg/rev = deg/s²
514 float amax_deg_per_s2 = amax_rev_s2_ * 360.0f;
515
516 // Time to accelerate to VMAX: t_accel = vmax / amax
517 float time_to_vmax_s = vmax_deg_per_s / amax_deg_per_s2;
518
519 // Distance covered during acceleration: d_accel = 0.5 * a * t²
520 float distance_during_accel_deg = 0.5f * amax_deg_per_s2 * time_to_vmax_s * time_to_vmax_s;
521
522 float leg_time_s;
523
524 if (2.0f * distance_during_accel_deg >= distance_deg) {
525 // Triangular profile: can't reach VMAX
526 // d = 0.5 * a * t² * 2 (accel + decel)
527 // t_leg = sqrt(d / a)
528 leg_time_s = 2.0f * sqrtf(distance_deg / amax_deg_per_s2);
529 } else {
530 // Trapezoidal profile: reaches VMAX
531 // Cruise distance = total - 2*accel_dist
532 float cruise_distance_deg = distance_deg - 2.0f * distance_during_accel_deg;
533 float cruise_time_s = cruise_distance_deg / vmax_deg_per_s;
534 leg_time_s = 2.0f * time_to_vmax_s + cruise_time_s;
535 }
536
537 // Total dwell time per cycle
538 float total_dwell_s = (dwell_at_min_ms_ + dwell_at_max_ms_) / 1000.0f;
539
540 // Cycle period = 2 legs + dwell
541 float cycle_period_s = 2.0f * leg_time_s + total_dwell_s;
542
543 estimated_frequency_hz_ = (cycle_period_s > 0.0f) ? (1.0f / cycle_period_s) : 0.0f;
544
545 ESP_LOGI(TAG_MOTION, "Estimated Frequency: %.2f Hz (dist=%.1f°, leg=%.3fs, dwell=%.1fs)",
546 estimated_frequency_hz_, distance_deg, leg_time_s, total_dwell_s);
547 ESP_LOGI(TAG_MOTION, " Using VMAX=%.1f RPM (%.1f°/s), AMAX=%.2f rev/s² (%.1f°/s²)",
548 vmax_rpm_, vmax_deg_per_s, amax_rev_s2_, amax_deg_per_s2);
549}
550
552 if (!bounded_)
553 return;
554
555 float old_min = local_min_bound_;
556 float old_max = local_max_bound_;
557
558 // Clip local bounds to global bounds (all in degrees)
561
562 // Update home position
565
566 if (fabsf(old_min - local_min_bound_) > 0.01f || fabsf(old_max - local_max_bound_) > 0.01f) {
567 ESP_LOGW(TAG_MOTION, "Local bounds clipped to global bounds");
568 ESP_LOGI(TAG_MOTION, "Clipped local bounds: min=%.2f°, max=%.2f°", local_min_bound_, local_max_bound_);
569 }
570}
571
573 // Check if running
574 {
576 // Don't update if stopped
578 return;
579 }
580
581 // If paused, allow current move to finish but don't command next target
583 // Check if current move has finished - if so, set to HOLD mode
584 auto target_reached = driver_->rampControl.IsTargetReached();
585 if (target_reached.IsOk() && target_reached.Value()) {
586 // Current move finished - set to HOLD mode to maintain position
587 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
588 }
589 return; // Don't command next target while paused
590 }
591
592 // Check if cycle count reached
594 if (!cycle_complete_) {
595 cycle_complete_ = true;
596 uint32_t cycles = current_cycles_;
598 running_ = false;
599
600 // Stop the ramp generator
601 driver_->rampControl.Stop();
602
603 // CRITICAL: Clear target by setting XTARGET = XACTUAL to prevent lingering targets
604 auto current_pos = driver_->rampControl.GetCurrentPosition(tmc51x0::Unit::Deg);
605 if (current_pos.IsOk()) {
606 driver_->rampControl.SetTargetPosition(current_pos.Value(), tmc51x0::Unit::Deg);
607 }
608
609 // Set HOLD mode
610 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
611
612 guard.unlock();
613 ESP_LOGI(TAG_MOTION, "Target cycle count reached: %lu cycles. Stopped and holding.", cycles);
614 }
615 return;
616 }
617 }
618
619 // Get current state and bounds
620 MotionState current_state;
621 float min_bound, max_bound;
622 uint32_t dwell_min, dwell_max;
623 uint32_t target_cycles;
624
625 {
627 current_state = state_;
628 min_bound = local_min_bound_;
629 max_bound = local_max_bound_;
630 dwell_min = dwell_at_min_ms_;
631 dwell_max = dwell_at_max_ms_;
632 target_cycles = target_cycles_;
633 }
634
635 // Check if target reached (for moving states)
636 bool target_reached = false;
637 if (current_state == MotionState::MOVING_TO_MAX || current_state == MotionState::MOVING_TO_MIN) {
638 auto reached_result = driver_->rampControl.IsTargetReached();
639 target_reached = reached_result.IsOk() && reached_result.Value();
640 }
641
642 // Get current position for cycle counting
643 auto current_pos_result = driver_->rampControl.GetCurrentPosition(tmc51x0::Unit::Deg);
644 float current_pos_deg = current_pos_result.IsOk() ? current_pos_result.Value() : 0.0f;
645 float dist_to_min = fabsf(current_pos_deg - min_bound);
646 float dist_to_max = fabsf(current_pos_deg - max_bound);
647 constexpr float NEAR_THRESHOLD_DEG = 2.0f;
648
649 // State machine: point-to-point motion (like bounds_finding_test.cpp)
650 switch (current_state) {
652 // Paused state - do nothing, Update() already returned early
653 // This case should never be reached, but included for completeness
654 return;
656 if (target_reached || dist_to_max < NEAR_THRESHOLD_DEG) {
657 // Reached MAX - enter dwell (which includes settle time) or move to MIN
659 // Check if paused - if so, don't transition, just wait
661 // Paused - set to HOLD and wait for resume
662 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
663 return;
664 }
665 // Enter dwell if requested (no minimum enforced - remote controller controls this)
666 if (dwell_max > 0) {
668 dwell_start_time_ms_ = esp_timer_get_time() / 1000;
669 } else {
670 // No dwell, immediately move to MIN (dwell time already provides settle)
672 driver_->rampControl.SetTargetPosition(min_bound, tmc51x0::Unit::Deg);
673 }
674 }
675 break;
676
678 {
679 uint32_t current_time_ms = esp_timer_get_time() / 1000;
680 uint32_t dwell_start;
681 {
683 dwell_start = dwell_start_time_ms_;
684 // Check if paused - if so, don't transition
686 return;
687 }
688 }
689 if (current_time_ms - dwell_start >= dwell_max) {
690 // Dwell complete, move to MIN
692 // Double-check not paused (could have been paused during dwell)
694 return;
695 }
697 driver_->rampControl.SetTargetPosition(min_bound, tmc51x0::Unit::Deg);
698 }
699 }
700 break;
701
703 if (target_reached || dist_to_min < NEAR_THRESHOLD_DEG) {
704 // Reached MIN - this completes a cycle! Count it.
705 uint32_t new_cycles;
706 bool should_stop = false;
707 {
709 // Check if paused - if so, don't transition, just wait
711 // Paused - set to HOLD and wait for resume
712 driver_->rampControl.SetRampMode(tmc51x0::RampMode::HOLD);
713 return;
714 }
715
716 // This completes a cycle! Count it.
718 new_cycles = current_cycles_;
719
720 // Check if target reached
721 if (target_cycles > 0 && new_cycles >= target_cycles) {
722 cycle_complete_ = true;
723 running_ = false;
725 driver_->rampControl.Stop();
726 should_stop = true;
727 } else {
728 // Enter dwell or move to MAX for next cycle
729 // Dwell time is controlled by remote controller (no minimum enforced)
730 if (dwell_min > 0) {
732 dwell_start_time_ms_ = esp_timer_get_time() / 1000;
733 } else {
734 // No dwell, immediately move to MAX for next cycle
736 driver_->rampControl.SetTargetPosition(max_bound, tmc51x0::Unit::Deg);
737 }
738 }
739 }
740
741 if (should_stop) {
742 ESP_LOGI(TAG_MOTION, "Target cycle count reached: %lu cycles. Stopping.", new_cycles);
743 return;
744 } else {
745 ESP_LOGI(TAG_MOTION, "Cycle %lu completed at MIN position (target: %lu)",
746 new_cycles, target_cycles == 0 ? 0xFFFFFFFF : target_cycles);
747 }
748 }
749 break;
750
752 {
753 uint32_t current_time_ms = esp_timer_get_time() / 1000;
754 uint32_t dwell_start;
755 {
757 dwell_start = dwell_start_time_ms_;
758 // Check if paused - if so, don't transition
760 return;
761 }
762 }
763 if (current_time_ms - dwell_start >= dwell_min) {
764 // Dwell complete, move to MAX for next cycle
766 // Double-check not paused (could have been paused during dwell)
768 return;
769 }
771 driver_->rampControl.SetTargetPosition(max_bound, tmc51x0::Unit::Deg);
772 }
773 }
774 break;
775
777 // Do nothing
778 break;
779 }
780}
781
783 Status status{};
784 {
786 status.running = running_ && state_ != MotionState::STOPPED;
787 status.bounded = bounded_;
788 status.vmax_rpm = vmax_rpm_;
789 status.amax_rev_s2 = amax_rev_s2_;
790 status.estimated_frequency_hz = estimated_frequency_hz_;
791 status.current_cycles = current_cycles_;
792 status.target_cycles = target_cycles_;
793 status.dwell_min_ms = dwell_at_min_ms_;
794 status.dwell_max_ms = dwell_at_max_ms_;
795 status.min_degrees_from_center = local_min_bound_;
796 status.max_degrees_from_center = local_max_bound_;
797 status.global_min_degrees = global_min_bound_;
798 status.global_max_degrees = global_max_bound_;
799 }
800
801 return status;
802}
803
804// Close namespace only if we opened it (standalone compilation)
805#ifndef FATIGUE_MOTION_HEADER_INCLUDED
806} // namespace FatigueTest
807#endif
ESP32 FreeRTOS mutex wrapper for TMC51x0 driver.
Definition esp32_tmc_mutex.hpp:24
Unified fatigue test motion controller.
Definition fatigue_motion.hpp:26
void Pause() noexcept
Pause active motion (stops movement but keeps motor energized for resume).
Definition fatigue_motion_impl.hpp:439
void GetLocalBoundsFromCenterDegrees(float &min_degrees, float &max_degrees) const noexcept
Read local bounds (degrees).
Definition fatigue_motion_impl.hpp:162
bool bounded_
Definition fatigue_motion.hpp:252
uint32_t GetCurrentCycles() const noexcept
Get current completed cycles.
Definition fatigue_motion_impl.hpp:258
void RecalculateEstimatedFrequency() noexcept
Definition fatigue_motion_impl.hpp:493
float local_max_bound_
Definition fatigue_motion.hpp:250
uint32_t GetTargetCycles() const noexcept
Get configured target cycles.
Definition fatigue_motion_impl.hpp:263
Esp32TmcMutex & driver_mutex_
Definition fatigue_motion.hpp:277
uint32_t target_cycles_
Definition fatigue_motion.hpp:264
uint32_t dwell_at_min_ms_
Definition fatigue_motion.hpp:258
MotionState state_
Definition fatigue_motion.hpp:270
void Update() noexcept
Periodic update tick.
Definition fatigue_motion_impl.hpp:572
bool SetDwellTimes(uint32_t dwell_at_min_ms, uint32_t dwell_at_max_ms) noexcept
Set dwell times at local bounds.
Definition fatigue_motion_impl.hpp:230
tmc51x0::TMC51x0< Esp32SPI > * driver_
Definition fatigue_motion.hpp:244
~FatigueTestMotion() noexcept
Destructor.
void ClipLocalBoundsToGlobal() noexcept
Definition fatigue_motion_impl.hpp:551
float GetMaxVelocity() const noexcept
Get configured maximum velocity (RPM).
Definition fatigue_motion_impl.hpp:194
float GetAcceleration() const noexcept
Get configured acceleration (rev/s²).
Definition fatigue_motion_impl.hpp:220
void GetGlobalBoundsDegrees(float &min_degrees, float &max_degrees) const noexcept
Read current global bounds (degrees).
Definition fatigue_motion_impl.hpp:80
bool SetAcceleration(float amax_rev_s2) noexcept
Set acceleration for oscillation (direct TMC5160 AMAX control).
Definition fatigue_motion_impl.hpp:199
void ResetCycles() noexcept
Reset cycle counter and completion flags.
Definition fatigue_motion_impl.hpp:273
void SetUnbounded(float current_position_degrees, float default_range_degrees=175.0f) noexcept
Mark the system as unbounded and establish a default travel window.
Definition fatigue_motion_impl.hpp:86
float vmax_rpm_
Definition fatigue_motion.hpp:256
uint32_t current_cycles_
Definition fatigue_motion.hpp:265
bool IsRunning() const noexcept
Whether the motion controller is currently active.
Definition fatigue_motion_impl.hpp:488
bool Start() noexcept
Start active motion.
Definition fatigue_motion_impl.hpp:287
float amplitude_
Definition fatigue_motion.hpp:255
bool IsBounded() const noexcept
Whether the controller is in bounded mode (mechanical stops found).
Definition fatigue_motion_impl.hpp:282
float local_min_bound_
Definition fatigue_motion.hpp:249
FatigueTestMotion(tmc51x0::TMC51x0< Esp32SPI > *driver, Esp32TmcMutex &driver_mutex) noexcept
Construct a motion controller bound to a TMC51x0 driver instance.
Definition fatigue_motion_impl.hpp:45
uint32_t dwell_start_time_ms_
Definition fatigue_motion.hpp:271
MotionState
Definition fatigue_motion.hpp:269
float home_position_
Definition fatigue_motion.hpp:251
bool IsCycleComplete() const noexcept
Check whether the configured target cycle count has been reached.
Definition fatigue_motion_impl.hpp:268
Status GetStatus() const noexcept
Snapshot current controller status.
Definition fatigue_motion_impl.hpp:782
void Stop() noexcept
Stop active motion and command driver stop.
Definition fatigue_motion_impl.hpp:451
float global_min_bound_
Definition fatigue_motion.hpp:247
uint64_t start_time_us_
Definition fatigue_motion.hpp:261
float estimated_frequency_hz_
Definition fatigue_motion.hpp:274
void GetDwellTimes(uint32_t &dwell_at_min_ms, uint32_t &dwell_at_max_ms) const noexcept
Get dwell times at local bounds.
Definition fatigue_motion_impl.hpp:243
bool SetMaxVelocity(float vmax_rpm) noexcept
Set maximum velocity for oscillation (direct TMC5160 VMAX control).
Definition fatigue_motion_impl.hpp:174
uint32_t dwell_at_max_ms_
Definition fatigue_motion.hpp:259
bool SetLocalBoundsFromCenterDegrees(float min_degrees_from_center, float max_degrees_from_center, float edge_backoff_deg=3.5f) noexcept
Set local oscillation bounds (degrees) relative to the center.
Definition fatigue_motion_impl.hpp:105
float amax_rev_s2_
Definition fatigue_motion.hpp:257
bool SetTargetCycles(uint32_t cycles) noexcept
Set target cycle count (0 = infinite).
Definition fatigue_motion_impl.hpp:249
float global_max_bound_
Definition fatigue_motion.hpp:248
bool running_
Definition fatigue_motion.hpp:260
bool cycle_complete_
Definition fatigue_motion.hpp:266
float GetEstimatedCycleFrequency() const noexcept
Get estimated cycle frequency based on current velocity/accel/distance.
Definition fatigue_motion_impl.hpp:225
RAII mutex guard for automatic lock/unlock.
Definition esp32_tmc_mutex.hpp:63
void unlock() noexcept
Manually unlock mutex.
Definition esp32_tmc_mutex.hpp:85
Main class representing a TMC51x0 stepper motor driver (TMC5130 & TMC5160)
Definition tmc51x0.hpp:119
ESP32 GPIO pin configuration and compile-time configuration for TMC51x0 driver (TMC5130 & TMC5160)
Unified fatigue test motion controller.
static const char * TAG_MOTION
Definition fatigue_motion_impl.hpp:28
Definition fatigue_motion.hpp:18
static constexpr float AMAX_MAX_REV_S2
Definition fatigue_motion_impl.hpp:172
static constexpr float AMAX_MIN_REV_S2
Definition fatigue_motion_impl.hpp:171
static constexpr float VMAX_MIN_RPM
Definition fatigue_motion_impl.hpp:169
static constexpr float VMAX_MAX_RPM
Definition fatigue_motion_impl.hpp:170
@ RevPerSec
Revolutions per Second (recommended default for velocity)
@ Deg
Degrees (per second for velocity, per second^2 for accel)
@ RPM
Revolutions per Minute (Velocity only, typically)
@ HOLD
Velocity remains unchanged, unless stop event occurs.
@ POSITIONING
Positioning mode using all A, D and V parameters.
Status structure containing current motion controller state.
Definition fatigue_motion.hpp:31
Base template for test rig configuration (undefined - forces explicit specialization)
Definition esp32_tmc51x0_test_config.hpp:996