QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCStateMachine.cc
Go to the documentation of this file.
1#include "QGCStateMachine.h"
5#include "QGCApplication.h"
6#include "AudioOutput.h"
8#include "Vehicle.h"
9
10#include <QtCore/QTimer>
11#include <QtStateMachine/QFinalState>
12#include <QtStateMachine/QSignalTransition>
13
14#include <memory>
15
16QGCStateMachine::QGCStateMachine(const QString& machineName, Vehicle *vehicle, QObject* parent)
17 : QStateMachine (parent)
18 , _vehicle (vehicle)
19{
20 setObjectName(machineName);
21
22 connect(this, &QGCStateMachine::started, this, [this] () {
23 qCDebug(QGCStateMachineLog) << "State machine started:" << objectName();
24 emit runningChanged();
25 });
26 connect(this, &QGCStateMachine::stopped, this, [this] () {
27 qCDebug(QGCStateMachineLog) << "State machine stopped:" << objectName();
28 emit runningChanged();
29 if (_deleteOnStop) {
30 deleteLater();
31 }
32 });
33 connect(this, &QGCStateMachine::finished, this, [this] () {
34 qCDebug(QGCStateMachineLog) << "State machine finished:" << objectName();
35 if (_progressTotalWeight > 0 && _progressLastEmitted < 1.0f) {
36 _progressLastEmitted = 1.0f;
37 emit progressUpdate(1.0f);
38 }
39 });
40
41 if (qEnvironmentVariableIsSet("QGC_STATEMACHINE_HISTORY")) {
42 setHistoryRecordingEnabled(true);
43 }
44 if (qEnvironmentVariableIsSet("QGC_STATEMACHINE_PROFILE")) {
45 setProfilingEnabled(true);
46 }
47 if (qEnvironmentVariableIsSet("QGC_STATEMACHINE_LOG")) {
48 setStructuredLoggingEnabled(true);
49 }
50}
51
52QString QGCStateMachine::currentStateName() const
53{
54 const auto activeStates = configuration();
55 if (activeStates.isEmpty()) {
56 return QString();
57 }
58 // Return the name of the first (deepest) active state
59 QAbstractState* active = *activeStates.begin();
60 return active ? active->objectName() : QString();
61}
62
64{
65 if (isRunning()) {
66 qCCritical(QGCStateMachineLog) << objectName() << "start() called but already running - check signal connections";
67 }
68 QStateMachine::start();
69}
70
71void QGCStateMachine::setGlobalErrorState(QAbstractState* errorState)
72{
73 _globalErrorState = errorState;
74}
75
76void QGCStateMachine::enablePropertyRestore()
77{
78 setGlobalRestorePolicy(QState::RestoreProperties);
79 qCDebug(QGCStateMachineLog) << objectName() << "property restore enabled";
80}
81
82bool QGCStateMachine::isPropertyRestoreEnabled() const
83{
84 return globalRestorePolicy() == QState::RestoreProperties;
85}
86
87void QGCStateMachine::registerState(QGCState* state)
88{
89 // Only add global error transition if no local error state is set
90 if (_globalErrorState && !state->localErrorState()) {
91 state->addTransition(state, &QGCState::error, _globalErrorState);
92 }
93}
94
95void QGCStateMachine::registerState(QGCAbstractState* state)
96{
97 // QGCAbstractState cannot have local error-state helpers, so wire to global.
98 if (_globalErrorState) {
99 state->addTransition(state, &QGCAbstractState::error, _globalErrorState);
100 }
101}
102
103FunctionState* QGCStateMachine::addFunctionState(const QString& stateName, std::function<void()> function)
104{
105 auto* state = new FunctionState(stateName, this, std::move(function));
106 registerState(state);
107 return state;
108}
109
110AsyncFunctionState* QGCStateMachine::addAsyncFunctionState(const QString& stateName,
112 int timeoutMsecs)
113{
114 auto* state = new AsyncFunctionState(stateName, this, std::move(setupFunction), timeoutMsecs);
115 registerState(state);
116 return state;
117}
118
119QGCState* QGCStateMachine::addErrorRecoveryState(const QString& stateName,
121 int maxRetries,
122 int retryDelayMsecs,
126 int timeoutMsecs)
127{
128 ErrorRecoveryBuilder builder(this, stateName);
129 builder.withAction(std::move(action))
130 .retry(maxRetries, retryDelayMsecs)
131 .onExhausted(exhaustedBehavior);
132
133 if (fallback) {
134 builder.withFallback(std::move(fallback));
135 }
136 if (rollback) {
137 builder.withRollback(std::move(rollback));
138 }
139 if (timeoutMsecs > 0) {
140 builder.withTimeout(timeoutMsecs);
141 }
142
143 return builder.build();
144}
145
146DelayState* QGCStateMachine::addDelayState(int delayMsecs)
147{
148 auto* state = new DelayState(this, delayMsecs);
149 registerState(state);
150 return state;
151}
152
153ParallelState* QGCStateMachine::addParallelState(const QString& stateName)
154{
155 auto* state = new ParallelState(stateName, this);
156 registerState(state);
157 return state;
158}
159
160void QGCStateMachine::postEvent(const QString& eventName, const QVariant& data, EventPriority priority)
161{
162 qCDebug(QGCStateMachineLog) << objectName() << "posting event:" << eventName
163 << (priority == HighPriority ? "(high priority)" : "");
164 emit machineEvent(eventName);
165 QStateMachine::postEvent(new QGCStateMachineEvent(eventName, data), priority);
166}
167
168int QGCStateMachine::postDelayedEvent(const QString& eventName, int delayMsecs, const QVariant& data)
169{
170 qCDebug(QGCStateMachineLog) << objectName() << "posting delayed event:" << eventName << "delay:" << delayMsecs << "ms";
171 return QStateMachine::postDelayedEvent(new QGCStateMachineEvent(eventName, data), delayMsecs);
172}
173
174bool QGCStateMachine::cancelDelayedEvent(int eventId)
175{
176 qCDebug(QGCStateMachineLog) << objectName() << "cancelling delayed event id:" << eventId;
177 return QStateMachine::cancelDelayedEvent(eventId);
178}
179
180// -----------------------------------------------------------------------------
181// State Configuration
182// -----------------------------------------------------------------------------
183
184void QGCStateMachine::setInitialState(QAbstractState* state, bool autoStart)
185{
186 QStateMachine::setInitialState(state);
187 if (autoStart) {
188 start();
189 }
190}
191
192QGCFinalState* QGCStateMachine::addFinalState(const QString& stateName)
193{
194 auto* state = new QGCFinalState(stateName.isEmpty() ? QStringLiteral("Final") : stateName, this);
195 return state;
196}
197
198ConditionalState* QGCStateMachine::addConditionalState(const QString& stateName,
201{
202 auto* state = new ConditionalState(stateName, this, std::move(predicate), std::move(action));
203 registerState(state);
204 return state;
205}
206
207SubMachineState* QGCStateMachine::addSubMachineState(const QString& stateName, SubMachineState::MachineFactory factory)
208{
209 auto* state = new SubMachineState(stateName, this, std::move(factory));
210 registerState(state);
211 return state;
212}
213
214EventQueuedState* QGCStateMachine::addEventQueuedState(const QString& stateName,
215 const QString& eventName,
216 int timeoutMsecs)
217{
218 auto* state = new EventQueuedState(stateName, this, eventName, timeoutMsecs);
219 registerState(state);
220 return state;
221}
222
223QState* QGCStateMachine::createTimedActionState(const QString& stateName,
224 int durationMsecs,
225 std::function<void()> onEntry,
226 std::function<void()> onExit)
227{
228 // Create the composite parent state
229 auto* parentState = new QState(this);
230 parentState->setObjectName(stateName);
231
232 // Create a timer owned by the parent state (auto-cleanup)
233 auto* timer = new QTimer(parentState);
234 timer->setInterval(durationMsecs);
235 timer->setSingleShot(true);
236
237 // Create internal timing state (active while timer runs)
238 auto* timingState = new QState(parentState);
239 timingState->setObjectName(stateName + QStringLiteral("_Timing"));
240
241 // Create internal final state (signals completion)
242 auto* doneState = new QFinalState(parentState);
243
244 // Wire up the timing state
245 if (onEntry) {
246 connect(timingState, &QState::entered, this, onEntry);
247 }
248 connect(timingState, &QState::entered, timer, qOverload<>(&QTimer::start));
249
250 if (onExit) {
251 connect(timingState, &QState::exited, this, onExit);
252 }
253
254 // Timer timeout transitions to done state
255 timingState->addTransition(timer, &QTimer::timeout, doneState);
256
257 // Set initial state
258 parentState->setInitialState(timingState);
259
260 qCDebug(QGCStateMachineLog) << "Created timed action state:" << stateName
261 << "duration:" << durationMsecs << "ms";
262
263 return parentState;
264}
265
266// -----------------------------------------------------------------------------
267// State Queries
268// -----------------------------------------------------------------------------
269
270bool QGCStateMachine::isStateActive(QAbstractState* state) const
271{
272 return configuration().contains(state);
273}
274
275QAbstractState* QGCStateMachine::findState(const QString& stateName) const
276{
277 // Use findChild with name parameter - more efficient than findChildren + loop
278 return findChild<QAbstractState*>(stateName);
279}
280
281// -----------------------------------------------------------------------------
282// Error Handling
283// -----------------------------------------------------------------------------
284
285bool QGCStateMachine::isInErrorState() const
286{
287 if (_globalErrorState) {
288 return configuration().contains(_globalErrorState);
289 }
290 return false;
291}
292
293FunctionState* QGCStateMachine::addLogAndContinueErrorState(const QString& stateName,
294 QAbstractState* nextState,
295 const QString& message)
296{
297 auto* state = ErrorHandlers::logAndContinue(this, stateName, nextState, message);
298 registerState(state);
299 return state;
300}
301
302FunctionState* QGCStateMachine::addLogAndStopErrorState(const QString& stateName,
303 const QString& message)
304{
305 auto* state = ErrorHandlers::logAndStop(this, stateName, message);
306 registerState(state);
307 return state;
308}
309
310void QGCStateMachine::clearError(bool restart)
311{
312 if (restart) {
313 stop();
314 start();
315 }
316}
317
318bool QGCStateMachine::resetToState(QAbstractState* state)
319{
320 if (!state) {
321 qCCritical(QGCStateMachineLog) << objectName() << "resetToState: null state";
322 return false;
323 }
324
325 // Verify the state belongs to this machine
326 if (state->parentState() != this && state->parent() != this) {
327 qCCritical(QGCStateMachineLog) << objectName() << "resetToState: state does not belong to this machine";
328 return false;
329 }
330
331 qCDebug(QGCStateMachineLog) << objectName() << "resetting to state:" << state->objectName();
332
333 // Stop the machine, change initial state, and restart
334 if (isRunning()) {
335 auto conn = std::make_shared<QMetaObject::Connection>();
336 *conn = connect(this, &QStateMachine::stopped, this, [this, state, conn]() {
337 disconnect(*conn);
338 setInitialState(state);
339 resetProgress();
340 start();
341 });
342 stop();
343 } else {
344 setInitialState(state);
345 resetProgress();
346 start();
347 }
348
349 return true;
350}
351
352bool QGCStateMachine::recoverFromError()
353{
354 if (!isInErrorState()) {
355 qCDebug(QGCStateMachineLog) << objectName() << "recoverFromError: not in error state";
356 return false;
357 }
358
359 qCDebug(QGCStateMachineLog) << objectName() << "recovering from error, restarting";
360 clearError(true);
361 return true;
362}
363
364bool QGCStateMachine::attemptRecovery()
365{
366 if (!_recoveryState) {
367 qCDebug(QGCStateMachineLog) << objectName() << "attemptRecovery: no recovery state set";
368 return false;
369 }
370
371 qCDebug(QGCStateMachineLog) << objectName() << "attempting recovery to:" << _recoveryState->objectName();
372 return resetToState(_recoveryState);
373}
374
375// -----------------------------------------------------------------------------
376// Lifecycle Helpers
377// -----------------------------------------------------------------------------
378
379void QGCStateMachine::ensureRunning()
380{
381 if (!isRunning()) {
382 start();
383 }
384}
385
386void QGCStateMachine::stopMachine(bool deleteOnStop)
387{
388 _deleteOnStop = deleteOnStop;
389 stop();
390}
391
392void QGCStateMachine::restart()
393{
394 if (isRunning()) {
395 // stop() is asynchronous, so connect to stopped signal to start again
396 auto conn = std::make_shared<QMetaObject::Connection>();
397 *conn = connect(this, &QStateMachine::stopped, this, [this, conn]() {
398 disconnect(*conn);
399 start();
400 });
401 stop();
402 } else {
403 start();
404 }
405}
406
407// -----------------------------------------------------------------------------
408// Transition Helpers
409// -----------------------------------------------------------------------------
410
411MachineEventTransition* QGCStateMachine::addEventTransition(QState* from, const QString& eventName, QAbstractState* to,
412 QAbstractAnimation* animation)
413{
414 auto* transition = new MachineEventTransition(eventName, to);
415 if (animation) {
416 transition->addAnimation(animation);
417 }
418 from->addTransition(transition);
419 return transition;
420}
421
422TimeoutTransition* QGCStateMachine::addTimeoutTransition(QState* from, int timeoutMsecs, QAbstractState* to,
423 QAbstractAnimation* animation)
424{
425 auto* transition = new TimeoutTransition(timeoutMsecs, to);
426 transition->attachToSourceState(from);
427 if (animation) {
428 transition->addAnimation(animation);
429 }
430 from->addTransition(transition);
431 return transition;
432}
433
434// -----------------------------------------------------------------------------
435// State Introspection
436// -----------------------------------------------------------------------------
437
438QList<QAbstractTransition*> QGCStateMachine::transitionsFrom(QAbstractState* state) const
439{
440 QList<QAbstractTransition*> result;
441 if (auto* qstate = qobject_cast<QState*>(state)) {
442 result = qstate->transitions();
443 }
444 return result;
445}
446
447QList<QAbstractTransition*> QGCStateMachine::transitionsTo(QAbstractState* state) const
448{
449 QList<QAbstractTransition*> result;
450 const auto allTransitions = findChildren<QAbstractTransition*>();
451 for (QAbstractTransition* transition : allTransitions) {
452 const auto targets = transition->targetStates();
453 if (targets.contains(state)) {
454 result.append(transition);
455 }
456 }
457 return result;
458}
459
460QList<QAbstractState*> QGCStateMachine::reachableFrom(QAbstractState* state) const
461{
462 QList<QAbstractState*> result;
463 for (QAbstractTransition* transition : transitionsFrom(state)) {
464 for (QAbstractState* target : transition->targetStates()) {
465 if (!result.contains(target)) {
466 result.append(target);
467 }
468 }
469 }
470 return result;
471}
472
473QList<QAbstractState*> QGCStateMachine::predecessorsOf(QAbstractState* state) const
474{
475 QList<QAbstractState*> result;
476 for (QAbstractTransition* transition : transitionsTo(state)) {
477 if (auto* source = transition->sourceState()) {
478 if (!result.contains(source)) {
479 result.append(source);
480 }
481 }
482 }
483 return result;
484}
485
486// -----------------------------------------------------------------------------
487// Debugging & Visualization
488// -----------------------------------------------------------------------------
489
490QString QGCStateMachine::dumpCurrentState() const
491{
492 QString result = objectName() + QStringLiteral(": ");
493
494 if (!isRunning()) {
495 result += QStringLiteral("(not running)");
496 return result;
497 }
498
499 const auto active = configuration();
500 if (active.isEmpty()) {
501 result += QStringLiteral("(no active state)");
502 } else {
503 QStringList stateNames;
504 for (QAbstractState* state : active) {
505 stateNames.append(state->objectName().isEmpty()
506 ? QStringLiteral("<unnamed>")
507 : state->objectName());
508 }
509 result += stateNames.join(QStringLiteral(", "));
510 result += QStringLiteral(" (running)");
511 }
512
513 return result;
514}
515
516QString QGCStateMachine::dumpConfiguration() const
517{
518 QString result;
519 result += QStringLiteral("State Machine: %1\n").arg(objectName());
520 result += QStringLiteral("================\n");
521
522 // Get all states
523 const auto allStates = findChildren<QAbstractState*>();
524 result += QStringLiteral("States (%1):\n").arg(allStates.size());
525
526 for (QAbstractState* state : allStates) {
527 QString stateName = state->objectName().isEmpty()
528 ? QStringLiteral("<unnamed>")
529 : state->objectName();
530
531 // Mark special states
532 QString marker;
533 if (state == initialState()) {
534 marker = QStringLiteral(" [initial]");
535 }
536 if (qobject_cast<QFinalState*>(state)) {
537 marker += QStringLiteral(" [final]");
538 }
539 if (state == _globalErrorState) {
540 marker += QStringLiteral(" [error]");
541 }
542 if (configuration().contains(state)) {
543 marker += QStringLiteral(" [active]");
544 }
545
546 result += QStringLiteral(" - %1%2\n").arg(stateName, marker);
547
548 // Show transitions from this state
549 auto transitions = transitionsFrom(state);
550 for (QAbstractTransition* transition : transitions) {
551 const auto targets = transition->targetStates();
552 for (QAbstractState* target : targets) {
553 QString targetName = target->objectName().isEmpty()
554 ? QStringLiteral("<unnamed>")
555 : target->objectName();
556 result += QStringLiteral(" -> %1\n").arg(targetName);
557 }
558 }
559 }
560
561 return result;
562}
563
564void QGCStateMachine::logCurrentState() const
565{
566 qCDebug(QGCStateMachineLog) << dumpCurrentState();
567}
568
569void QGCStateMachine::logConfiguration() const
570{
571 const auto lines = dumpConfiguration().split('\n');
572 for (const QString& line : lines) {
573 if (!line.isEmpty()) {
574 qCDebug(QGCStateMachineLog) << line;
575 }
576 }
577}
578
579void QGCStateMachine::setHistoryRecordingEnabled(bool enabled, int maxEntries)
580{
581 if (!_historyRecorder) {
582 _historyRecorder = new StateHistoryRecorder(this, maxEntries);
583 }
584 _historyRecorder->setMaxEntries(maxEntries);
585 _historyRecorder->setEnabled(enabled);
586}
587
588bool QGCStateMachine::historyRecordingEnabled() const
589{
590 return _historyRecorder && _historyRecorder->isEnabled();
591}
592
593QString QGCStateMachine::dumpRecordedHistory() const
594{
595 return _historyRecorder ? _historyRecorder->dumpHistory() : QString();
596}
597
598QJsonArray QGCStateMachine::recordedHistoryJson() const
599{
600 return _historyRecorder ? _historyRecorder->toJson() : QJsonArray();
601}
602
603void QGCStateMachine::setProfilingEnabled(bool enabled)
604{
605 if (!_profiler) {
606 _profiler = new StateMachineProfiler(this);
607 }
608 _profiler->setEnabled(enabled);
609}
610
611bool QGCStateMachine::profilingEnabled() const
612{
613 return _profiler && _profiler->isEnabled();
614}
615
616QString QGCStateMachine::profilingSummary() const
617{
618 return _profiler ? _profiler->summary() : QString();
619}
620
621void QGCStateMachine::setStructuredLoggingEnabled(bool enabled)
622{
623 if (!_logger) {
624 _logger = new StateMachineLogger(this, this);
625 }
626 _logger->setEnabled(enabled);
627}
628
629bool QGCStateMachine::structuredLoggingEnabled() const
630{
631 return _logger && _logger->isEnabled();
632}
633
634QString QGCStateMachine::exportAsDot() const
635{
636 QString dot;
637 dot += QStringLiteral("digraph \"%1\" {\n").arg(objectName());
638 dot += QStringLiteral(" rankdir=TB;\n");
639 dot += QStringLiteral(" node [shape=box, style=rounded];\n");
640 dot += QStringLiteral("\n");
641
642 const auto allStates = findChildren<QAbstractState*>();
643
644 // Define nodes with special styling
645 for (QAbstractState* state : allStates) {
646 QString name = state->objectName().isEmpty()
647 ? QStringLiteral("state_%1").arg(reinterpret_cast<quintptr>(state), 0, 16)
648 : state->objectName();
649
650 QStringList attrs;
651 if (state == initialState()) {
652 attrs << QStringLiteral("style=\"rounded,bold\"");
653 attrs << QStringLiteral("peripheries=2");
654 }
655 if (qobject_cast<QFinalState*>(state)) {
656 attrs << QStringLiteral("shape=doublecircle");
657 }
658 if (state == _globalErrorState) {
659 attrs << QStringLiteral("color=red");
660 }
661
662 if (attrs.isEmpty()) {
663 dot += QStringLiteral(" \"%1\";\n").arg(name);
664 } else {
665 dot += QStringLiteral(" \"%1\" [%2];\n").arg(name, attrs.join(", "));
666 }
667 }
668
669 dot += QStringLiteral("\n");
670
671 // Define edges
672 for (QAbstractState* state : allStates) {
673 QString fromName = state->objectName().isEmpty()
674 ? QStringLiteral("state_%1").arg(reinterpret_cast<quintptr>(state), 0, 16)
675 : state->objectName();
676
677 auto transitions = transitionsFrom(state);
678 for (QAbstractTransition* transition : transitions) {
679 const auto targets = transition->targetStates();
680 for (QAbstractState* target : targets) {
681 QString toName = target->objectName().isEmpty()
682 ? QStringLiteral("state_%1").arg(reinterpret_cast<quintptr>(target), 0, 16)
683 : target->objectName();
684
685 // Try to get transition label from object name
686 QString label = transition->objectName();
687 if (label.isEmpty()) {
688 dot += QStringLiteral(" \"%1\" -> \"%2\";\n").arg(fromName, toName);
689 } else {
690 dot += QStringLiteral(" \"%1\" -> \"%2\" [label=\"%3\"];\n").arg(fromName, toName, label);
691 }
692 }
693 }
694 }
695
696 dot += QStringLiteral("}\n");
697 return dot;
698}
699
700QList<QAbstractState*> QGCStateMachine::unreachableStates() const
701{
702 QList<QAbstractState*> unreachable;
703
704 if (!initialState()) {
705 return unreachable;
706 }
707
708 // BFS from initial state
709 QSet<QAbstractState*> visited;
710 QList<QAbstractState*> queue;
711 queue.append(initialState());
712 visited.insert(initialState());
713
714 while (!queue.isEmpty()) {
715 QAbstractState* current = queue.takeFirst();
716 auto reachable = reachableFrom(current);
717 for (QAbstractState* next : reachable) {
718 if (!visited.contains(next)) {
719 visited.insert(next);
720 queue.append(next);
721 }
722 }
723 }
724
725 // Find states not visited
726 const auto allStates = findChildren<QAbstractState*>();
727 for (QAbstractState* state : allStates) {
728 if (!visited.contains(state)) {
729 unreachable.append(state);
730 }
731 }
732
733 return unreachable;
734}
735
736int QGCStateMachine::maxPathLength() const
737{
738 if (!initialState()) {
739 return -1;
740 }
741
742 // BFS with depth tracking
743 QHash<QAbstractState*, int> depth;
744 QList<QAbstractState*> queue;
745
746 queue.append(initialState());
747 depth[initialState()] = 0;
748 int maxDepth = 0;
749
750 const int stateCount = findChildren<QAbstractState*>().size();
751 const int maxSearchDepth = qMax(stateCount * 2, 100);
752
753 while (!queue.isEmpty()) {
754 QAbstractState* current = queue.takeFirst();
755 int currentDepth = depth[current];
756
757 if (currentDepth >= maxSearchDepth) {
758 continue;
759 }
760
761 auto reachable = reachableFrom(current);
762 for (QAbstractState* next : reachable) {
763 int newDepth = currentDepth + 1;
764 // Only update if we found a longer path
765 if (!depth.contains(next) || depth[next] < newDepth) {
766 depth[next] = newDepth;
767 maxDepth = qMax(maxDepth, newDepth);
768 queue.append(next);
769 }
770 }
771 }
772
773 return maxDepth;
774}
775
776QList<QAbstractState*> QGCStateMachine::deadEndStates() const
777{
778 QList<QAbstractState*> deadEnds;
779
780 const auto allStates = findChildren<QAbstractState*>();
781 for (QAbstractState* state : allStates) {
782 // Skip final states - they're supposed to be dead ends
783 if (qobject_cast<QFinalState*>(state)) {
784 continue;
785 }
786
787 // Check if state has any outgoing transitions
788 auto transitions = transitionsFrom(state);
789 if (transitions.isEmpty()) {
790 deadEnds.append(state);
791 }
792 }
793
794 return deadEnds;
795}
796
797// -----------------------------------------------------------------------------
798// Progress Tracking
799// -----------------------------------------------------------------------------
800
801void QGCStateMachine::setProgressWeights(const QList<QPair<QAbstractState*, int>>& stateWeights)
802{
803 // Disconnect old connections before re-wiring
804 for (auto* state : _progressStates) {
805 disconnect(state, &QAbstractState::entered, this, &QGCStateMachine::_onStateEntered);
806 }
807
808 _progressStates.clear();
809 _progressWeights.clear();
810 _progressTotalWeight = 0;
811
812 for (const auto& pair : stateWeights) {
813 _progressStates.append(pair.first);
814 _progressWeights.append(pair.second);
815 _progressTotalWeight += pair.second;
816
817 // Connect to state's entered signal to auto-update progress index
818 connect(pair.first, &QAbstractState::entered, this, &QGCStateMachine::_onStateEntered);
819 }
820
821 qCDebug(QGCStateMachineLog) << objectName() << "progress tracking enabled for"
822 << _progressStates.size() << "states, total weight:" << _progressTotalWeight;
823}
824
825void QGCStateMachine::_onStateEntered()
826{
827 auto* state = qobject_cast<QAbstractState*>(sender());
828 if (!state) return;
829
830 // Update QML state history
831 QString stateName = state->objectName();
832 if (!stateName.isEmpty()) {
833 _stateHistory.append(stateName);
834 while (_stateHistory.size() > _stateHistoryLimit) {
835 _stateHistory.removeFirst();
836 }
837 emit stateHistoryChanged();
839 }
840
841 // Update progress tracking
842 int index = _progressStates.indexOf(state);
843 if (index >= 0 && index != _progressCurrentIndex) {
844 _progressCurrentIndex = index;
845 _progressSubProgress = 0.0f;
846
847 float newProgress = _calculateProgress();
848 if (newProgress > _progressLastEmitted) {
849 _progressLastEmitted = newProgress;
850 emit progressUpdate(newProgress);
851 }
852 }
853}
854
855void QGCStateMachine::setSubProgress(float subProgress)
856{
857 _progressSubProgress = qBound(0.0f, subProgress, 1.0f);
858
859 float newProgress = _calculateProgress();
860 if (newProgress > _progressLastEmitted) {
861 _progressLastEmitted = newProgress;
862 emit progressUpdate(newProgress);
863 }
864}
865
866float QGCStateMachine::progress() const
867{
868 return _progressLastEmitted;
869}
870
871void QGCStateMachine::resetProgress()
872{
873 _progressCurrentIndex = -1;
874 _progressSubProgress = 0.0f;
875 _progressLastEmitted = 0.0f;
876}
877
878float QGCStateMachine::_calculateProgress() const
879{
880 if (_progressTotalWeight <= 0 || _progressCurrentIndex < 0) {
881 return 0.0f;
882 }
883
884 int completedWeight = 0;
885 for (int i = 0; i < _progressCurrentIndex && i < _progressWeights.size(); ++i) {
886 completedWeight += _progressWeights[i];
887 }
888
889 int currentWeight = (_progressCurrentIndex < _progressWeights.size())
890 ? _progressWeights[_progressCurrentIndex]
891 : 1;
892
893 return (completedWeight + currentWeight * _progressSubProgress) / static_cast<float>(_progressTotalWeight);
894}
895
896// -----------------------------------------------------------------------------
897// Timeout Configuration
898// -----------------------------------------------------------------------------
899
900void QGCStateMachine::setTimeoutOverride(const QString& stateName, int timeoutMsecs)
901{
902 _timeoutOverrides[stateName] = timeoutMsecs;
903 qCDebug(QGCStateMachineLog) << objectName() << "timeout override set for" << stateName << ":" << timeoutMsecs << "ms";
904}
905
906void QGCStateMachine::removeTimeoutOverride(const QString& stateName)
907{
908 _timeoutOverrides.remove(stateName);
909}
910
911int QGCStateMachine::timeoutOverride(const QString& stateName) const
912{
913 return _timeoutOverrides.value(stateName, -1);
914}
915
916void QGCStateMachine::recordTimeout(const QString& stateName)
917{
918 _timeoutStats[stateName]++;
919 qCDebug(QGCStateMachineLog) << objectName() << "timeout recorded for" << stateName
920 << "total:" << _timeoutStats[stateName];
921}
922
923// -----------------------------------------------------------------------------
924// Entry/Exit Callbacks
925// -----------------------------------------------------------------------------
926
927void QGCStateMachine::setCallbacks(EntryCallback onEntry, ExitCallback onExit)
928{
929 _entryCallback = std::move(onEntry);
930 _exitCallback = std::move(onExit);
931}
932
933// -----------------------------------------------------------------------------
934// QStateMachine Overrides
935// -----------------------------------------------------------------------------
936
937void QGCStateMachine::onEntry(QEvent* event)
938{
939 QStateMachine::onEntry(event);
940
941 if (_entryCallback) {
942 _entryCallback();
943 }
944
945 onEnter();
946}
947
948void QGCStateMachine::onExit(QEvent* event)
949{
950 onLeave();
951
952 if (_exitCallback) {
953 _exitCallback();
954 }
955
956 QStateMachine::onExit(event);
957}
958
959bool QGCStateMachine::event(QEvent* event)
960{
961 // Only allow handler to intercept custom events, not internal state machine events
962 if (_eventHandler && event->type() >= QEvent::User) {
963 if (_eventHandler(event)) {
964 return true;
965 }
966 }
967
968 return QStateMachine::event(event);
969}
std::function< void(AsyncFunctionState *state)> SetupFunction
std::function< void()> Action
std::function< bool()> Predicate
Delays that state machine for the specified time in milliseconds.
Definition DelayState.h:9
ExhaustedBehavior
What to do when all recovery options are exhausted.
std::function< bool()> Action
std::function< void()> VoidAction
Final state for a QGCStateMachine with logging support.
Custom event for QGCStateMachine delayed/scheduled events.
void start()
Start the state machine with debug logging.
void progressUpdate(float progress)
void onExit(QEvent *event) override
void currentStateNameChanged()
Emitted when the current state changes (for QML binding)
void machineEvent(const QString &eventName)
bool event(QEvent *event) override
void onEntry(QEvent *event) override
virtual void onLeave()
Override to perform actions when machine stops.
void stateHistoryChanged()
Emitted when the state history changes (for QML binding)
virtual void onEnter()
Override to perform actions when machine starts.
QAbstractState * localErrorState() const
Get the per-state error state (nullptr if using global)
Definition QGCState.h:42
void setMaxEntries(int max)
Set the maximum number of entries to keep (circular buffer)
QString dumpHistory() const
Get a human-readable dump of the history.
QJsonArray toJson() const
Export history as JSON array.
void setEnabled(bool enabled)
Enable or disable recording.
void setEnabled(bool enabled)
QString summary() const
Get a human-readable summary.
void setEnabled(bool enabled)
Enable or disable profiling.
std::function< QGCStateMachine *(SubMachineState *parent)> MachineFactory
FunctionState * logAndContinue(QGCStateMachine *machine, const QString &stateName, QAbstractState *nextState, const QString &message)
FunctionState * logAndStop(QGCStateMachine *machine, const QString &stateName, const QString &message)