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