10#include <QtCore/QTimer>
11#include <QtStateMachine/QFinalState>
12#include <QtStateMachine/QSignalTransition>
16QGCStateMachine::QGCStateMachine(
const QString& machineName,
Vehicle *vehicle, QObject* parent)
17 : QStateMachine (parent)
20 setObjectName(machineName);
22 connect(
this, &QGCStateMachine::started,
this, [
this] () {
23 qCDebug(QGCStateMachineLog) <<
"State machine started:" << objectName();
24 emit runningChanged();
26 connect(
this, &QGCStateMachine::stopped,
this, [
this] () {
27 qCDebug(QGCStateMachineLog) <<
"State machine stopped:" << objectName();
28 emit runningChanged();
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);
41 if (qEnvironmentVariableIsSet(
"QGC_STATEMACHINE_HISTORY")) {
42 setHistoryRecordingEnabled(
true);
44 if (qEnvironmentVariableIsSet(
"QGC_STATEMACHINE_PROFILE")) {
45 setProfilingEnabled(
true);
47 if (qEnvironmentVariableIsSet(
"QGC_STATEMACHINE_LOG")) {
48 setStructuredLoggingEnabled(
true);
52QString QGCStateMachine::currentStateName()
const
54 const auto activeStates = configuration();
55 if (activeStates.isEmpty()) {
59 QAbstractState* active = *activeStates.begin();
60 return active ? active->objectName() : QString();
66 qCCritical(QGCStateMachineLog) << objectName() <<
"start() called but already running - check signal connections";
68 QStateMachine::start();
71void QGCStateMachine::setGlobalErrorState(QAbstractState* errorState)
73 _globalErrorState = errorState;
76void QGCStateMachine::enablePropertyRestore()
78 setGlobalRestorePolicy(QState::RestoreProperties);
79 qCDebug(QGCStateMachineLog) << objectName() <<
"property restore enabled";
82bool QGCStateMachine::isPropertyRestoreEnabled()
const
84 return globalRestorePolicy() == QState::RestoreProperties;
87void QGCStateMachine::registerState(
QGCState* state)
98 if (_globalErrorState) {
103FunctionState* QGCStateMachine::addFunctionState(
const QString& stateName, std::function<
void()> function)
105 auto* state =
new FunctionState(stateName,
this, std::move(function));
106 registerState(state);
114 auto* state =
new AsyncFunctionState(stateName,
this, std::move(setupFunction), timeoutMsecs);
115 registerState(state);
119QGCState* QGCStateMachine::addErrorRecoveryState(
const QString& stateName,
129 builder.withAction(std::move(action))
130 .retry(maxRetries, retryDelayMsecs)
131 .onExhausted(exhaustedBehavior);
134 builder.withFallback(std::move(fallback));
137 builder.withRollback(std::move(rollback));
139 if (timeoutMsecs > 0) {
140 builder.withTimeout(timeoutMsecs);
143 return builder.build();
146DelayState* QGCStateMachine::addDelayState(
int delayMsecs)
148 auto* state =
new DelayState(
this, delayMsecs);
149 registerState(state);
153ParallelState* QGCStateMachine::addParallelState(
const QString& stateName)
156 registerState(state);
160void QGCStateMachine::postEvent(
const QString& eventName,
const QVariant& data, EventPriority priority)
162 qCDebug(QGCStateMachineLog) << objectName() <<
"posting event:" << eventName
163 << (priority == HighPriority ?
"(high priority)" :
"");
168int QGCStateMachine::postDelayedEvent(
const QString& eventName,
int delayMsecs,
const QVariant& data)
170 qCDebug(QGCStateMachineLog) << objectName() <<
"posting delayed event:" << eventName <<
"delay:" << delayMsecs <<
"ms";
174bool QGCStateMachine::cancelDelayedEvent(
int eventId)
176 qCDebug(QGCStateMachineLog) << objectName() <<
"cancelling delayed event id:" << eventId;
177 return QStateMachine::cancelDelayedEvent(eventId);
184void QGCStateMachine::setInitialState(QAbstractState* state,
bool autoStart)
186 QStateMachine::setInitialState(state);
192QGCFinalState* QGCStateMachine::addFinalState(
const QString& stateName)
194 auto* state =
new QGCFinalState(stateName.isEmpty() ? QStringLiteral(
"Final") : stateName, this);
198ConditionalState* QGCStateMachine::addConditionalState(
const QString& stateName,
202 auto* state =
new ConditionalState(stateName,
this, std::move(predicate), std::move(action));
203 registerState(state);
209 auto* state =
new SubMachineState(stateName,
this, std::move(factory));
210 registerState(state);
214EventQueuedState* QGCStateMachine::addEventQueuedState(
const QString& stateName,
215 const QString& eventName,
218 auto* state =
new EventQueuedState(stateName,
this, eventName, timeoutMsecs);
219 registerState(state);
223QState* QGCStateMachine::createTimedActionState(
const QString& stateName,
225 std::function<
void()> onEntry,
226 std::function<
void()> onExit)
229 auto* parentState =
new QState(
this);
230 parentState->setObjectName(stateName);
233 auto* timer =
new QTimer(parentState);
234 timer->setInterval(durationMsecs);
235 timer->setSingleShot(
true);
238 auto* timingState =
new QState(parentState);
239 timingState->setObjectName(stateName + QStringLiteral(
"_Timing"));
242 auto* doneState =
new QFinalState(parentState);
246 connect(timingState, &QState::entered,
this,
onEntry);
248 connect(timingState, &QState::entered, timer, qOverload<>(&QTimer::start));
251 connect(timingState, &QState::exited,
this,
onExit);
255 timingState->addTransition(timer, &QTimer::timeout, doneState);
258 parentState->setInitialState(timingState);
260 qCDebug(QGCStateMachineLog) <<
"Created timed action state:" << stateName
261 <<
"duration:" << durationMsecs <<
"ms";
270bool QGCStateMachine::isStateActive(QAbstractState* state)
const
272 return configuration().contains(state);
275QAbstractState* QGCStateMachine::findState(
const QString& stateName)
const
278 return findChild<QAbstractState*>(stateName);
285bool QGCStateMachine::isInErrorState()
const
287 if (_globalErrorState) {
288 return configuration().contains(_globalErrorState);
293FunctionState* QGCStateMachine::addLogAndContinueErrorState(
const QString& stateName,
294 QAbstractState* nextState,
295 const QString& message)
298 registerState(state);
302FunctionState* QGCStateMachine::addLogAndStopErrorState(
const QString& stateName,
303 const QString& message)
306 registerState(state);
310void QGCStateMachine::clearError(
bool restart)
318bool QGCStateMachine::resetToState(QAbstractState* state)
321 qCCritical(QGCStateMachineLog) << objectName() <<
"resetToState: null state";
326 if (state->parentState() !=
this && state->parent() !=
this) {
327 qCCritical(QGCStateMachineLog) << objectName() <<
"resetToState: state does not belong to this machine";
331 qCDebug(QGCStateMachineLog) << objectName() <<
"resetting to state:" << state->objectName();
335 auto conn = std::make_shared<QMetaObject::Connection>();
336 *conn = connect(
this, &QStateMachine::stopped,
this, [
this, state, conn]() {
338 setInitialState(state);
344 setInitialState(state);
352bool QGCStateMachine::recoverFromError()
354 if (!isInErrorState()) {
355 qCDebug(QGCStateMachineLog) << objectName() <<
"recoverFromError: not in error state";
359 qCDebug(QGCStateMachineLog) << objectName() <<
"recovering from error, restarting";
364bool QGCStateMachine::attemptRecovery()
366 if (!_recoveryState) {
367 qCDebug(QGCStateMachineLog) << objectName() <<
"attemptRecovery: no recovery state set";
371 qCDebug(QGCStateMachineLog) << objectName() <<
"attempting recovery to:" << _recoveryState->objectName();
372 return resetToState(_recoveryState);
379void QGCStateMachine::ensureRunning()
386void QGCStateMachine::stopMachine(
bool deleteOnStop)
388 _deleteOnStop = deleteOnStop;
392void QGCStateMachine::restart()
396 auto conn = std::make_shared<QMetaObject::Connection>();
397 *conn = connect(
this, &QStateMachine::stopped,
this, [
this, conn]() {
411MachineEventTransition* QGCStateMachine::addEventTransition(QState* from,
const QString& eventName, QAbstractState* to,
412 QAbstractAnimation* animation)
416 transition->addAnimation(animation);
418 from->addTransition(transition);
422TimeoutTransition* QGCStateMachine::addTimeoutTransition(QState* from,
int timeoutMsecs, QAbstractState* to,
423 QAbstractAnimation* animation)
426 transition->attachToSourceState(from);
428 transition->addAnimation(animation);
430 from->addTransition(transition);
438QList<QAbstractTransition*> QGCStateMachine::transitionsFrom(QAbstractState* state)
const
440 QList<QAbstractTransition*> result;
441 if (
auto* qstate = qobject_cast<QState*>(state)) {
442 result = qstate->transitions();
447QList<QAbstractTransition*> QGCStateMachine::transitionsTo(QAbstractState* state)
const
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);
460QList<QAbstractState*> QGCStateMachine::reachableFrom(QAbstractState* state)
const
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);
473QList<QAbstractState*> QGCStateMachine::predecessorsOf(QAbstractState* state)
const
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);
490QString QGCStateMachine::dumpCurrentState()
const
492 QString result = objectName() + QStringLiteral(
": ");
495 result += QStringLiteral(
"(not running)");
499 const auto active = configuration();
500 if (active.isEmpty()) {
501 result += QStringLiteral(
"(no active state)");
503 QStringList stateNames;
504 for (QAbstractState* state : active) {
505 stateNames.append(state->objectName().isEmpty()
506 ? QStringLiteral(
"<unnamed>")
507 : state->objectName());
509 result += stateNames.join(QStringLiteral(
", "));
510 result += QStringLiteral(
" (running)");
516QString QGCStateMachine::dumpConfiguration()
const
519 result += QStringLiteral(
"State Machine: %1\n").arg(objectName());
520 result += QStringLiteral(
"================\n");
523 const auto allStates = findChildren<QAbstractState*>();
524 result += QStringLiteral(
"States (%1):\n").arg(allStates.size());
526 for (QAbstractState* state : allStates) {
527 QString stateName = state->objectName().isEmpty()
528 ? QStringLiteral(
"<unnamed>")
529 : state->objectName();
533 if (state == initialState()) {
534 marker = QStringLiteral(
" [initial]");
536 if (qobject_cast<QFinalState*>(state)) {
537 marker += QStringLiteral(
" [final]");
539 if (state == _globalErrorState) {
540 marker += QStringLiteral(
" [error]");
542 if (configuration().contains(state)) {
543 marker += QStringLiteral(
" [active]");
546 result += QStringLiteral(
" - %1%2\n").arg(stateName, marker);
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);
564void QGCStateMachine::logCurrentState()
const
566 qCDebug(QGCStateMachineLog) << dumpCurrentState();
569void QGCStateMachine::logConfiguration()
const
571 const auto lines = dumpConfiguration().split(
'\n');
572 for (
const QString& line : lines) {
573 if (!line.isEmpty()) {
574 qCDebug(QGCStateMachineLog) << line;
579void QGCStateMachine::setHistoryRecordingEnabled(
bool enabled,
int maxEntries)
581 if (!_historyRecorder) {
588bool QGCStateMachine::historyRecordingEnabled()
const
590 return _historyRecorder && _historyRecorder->
isEnabled();
593QString QGCStateMachine::dumpRecordedHistory()
const
595 return _historyRecorder ? _historyRecorder->
dumpHistory() : QString();
598QJsonArray QGCStateMachine::recordedHistoryJson()
const
600 return _historyRecorder ? _historyRecorder->
toJson() : QJsonArray();
603void QGCStateMachine::setProfilingEnabled(
bool enabled)
611bool QGCStateMachine::profilingEnabled()
const
613 return _profiler && _profiler->
isEnabled();
616QString QGCStateMachine::profilingSummary()
const
618 return _profiler ? _profiler->
summary() : QString();
621void QGCStateMachine::setStructuredLoggingEnabled(
bool enabled)
629bool QGCStateMachine::structuredLoggingEnabled()
const
634QString QGCStateMachine::exportAsDot()
const
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");
642 const auto allStates = findChildren<QAbstractState*>();
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();
651 if (state == initialState()) {
652 attrs << QStringLiteral(
"style=\"rounded,bold\"");
653 attrs << QStringLiteral(
"peripheries=2");
655 if (qobject_cast<QFinalState*>(state)) {
656 attrs << QStringLiteral(
"shape=doublecircle");
658 if (state == _globalErrorState) {
659 attrs << QStringLiteral(
"color=red");
662 if (attrs.isEmpty()) {
663 dot += QStringLiteral(
" \"%1\";\n").arg(name);
665 dot += QStringLiteral(
" \"%1\" [%2];\n").arg(name, attrs.join(
", "));
669 dot += QStringLiteral(
"\n");
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();
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();
686 QString label = transition->objectName();
687 if (label.isEmpty()) {
688 dot += QStringLiteral(
" \"%1\" -> \"%2\";\n").arg(fromName, toName);
690 dot += QStringLiteral(
" \"%1\" -> \"%2\" [label=\"%3\"];\n").arg(fromName, toName, label);
696 dot += QStringLiteral(
"}\n");
700QList<QAbstractState*> QGCStateMachine::unreachableStates()
const
702 QList<QAbstractState*> unreachable;
704 if (!initialState()) {
709 QSet<QAbstractState*> visited;
710 QList<QAbstractState*> queue;
711 queue.append(initialState());
712 visited.insert(initialState());
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);
726 const auto allStates = findChildren<QAbstractState*>();
727 for (QAbstractState* state : allStates) {
728 if (!visited.contains(state)) {
729 unreachable.append(state);
736int QGCStateMachine::maxPathLength()
const
738 if (!initialState()) {
743 QHash<QAbstractState*, int> depth;
744 QList<QAbstractState*> queue;
746 queue.append(initialState());
747 depth[initialState()] = 0;
750 const int stateCount = findChildren<QAbstractState*>().size();
751 const int maxSearchDepth = qMax(stateCount * 2, 100);
753 while (!queue.isEmpty()) {
754 QAbstractState* current = queue.takeFirst();
755 int currentDepth = depth[current];
757 if (currentDepth >= maxSearchDepth) {
761 auto reachable = reachableFrom(current);
762 for (QAbstractState* next : reachable) {
763 int newDepth = currentDepth + 1;
765 if (!depth.contains(next) || depth[next] < newDepth) {
766 depth[next] = newDepth;
767 maxDepth = qMax(maxDepth, newDepth);
776QList<QAbstractState*> QGCStateMachine::deadEndStates()
const
778 QList<QAbstractState*> deadEnds;
780 const auto allStates = findChildren<QAbstractState*>();
781 for (QAbstractState* state : allStates) {
783 if (qobject_cast<QFinalState*>(state)) {
788 auto transitions = transitionsFrom(state);
789 if (transitions.isEmpty()) {
790 deadEnds.append(state);
801void QGCStateMachine::setProgressWeights(
const QList<QPair<QAbstractState*, int>>& stateWeights)
804 for (
auto* state : _progressStates) {
805 disconnect(state, &QAbstractState::entered,
this, &QGCStateMachine::_onStateEntered);
808 _progressStates.clear();
809 _progressWeights.clear();
810 _progressTotalWeight = 0;
812 for (
const auto& pair : stateWeights) {
813 _progressStates.append(pair.first);
814 _progressWeights.append(pair.second);
815 _progressTotalWeight += pair.second;
818 connect(pair.first, &QAbstractState::entered,
this, &QGCStateMachine::_onStateEntered);
821 qCDebug(QGCStateMachineLog) << objectName() <<
"progress tracking enabled for"
822 << _progressStates.size() <<
"states, total weight:" << _progressTotalWeight;
825void QGCStateMachine::_onStateEntered()
827 auto* state = qobject_cast<QAbstractState*>(sender());
831 QString stateName = state->objectName();
832 if (!stateName.isEmpty()) {
833 _stateHistory.append(stateName);
834 while (_stateHistory.size() > _stateHistoryLimit) {
835 _stateHistory.removeFirst();
842 int index = _progressStates.indexOf(state);
843 if (index >= 0 && index != _progressCurrentIndex) {
844 _progressCurrentIndex = index;
845 _progressSubProgress = 0.0f;
847 float newProgress = _calculateProgress();
848 if (newProgress > _progressLastEmitted) {
849 _progressLastEmitted = newProgress;
855void QGCStateMachine::setSubProgress(
float subProgress)
857 _progressSubProgress = qBound(0.0f, subProgress, 1.0f);
859 float newProgress = _calculateProgress();
860 if (newProgress > _progressLastEmitted) {
861 _progressLastEmitted = newProgress;
866float QGCStateMachine::progress()
const
868 return _progressLastEmitted;
871void QGCStateMachine::resetProgress()
873 _progressCurrentIndex = -1;
874 _progressSubProgress = 0.0f;
875 _progressLastEmitted = 0.0f;
878float QGCStateMachine::_calculateProgress()
const
880 if (_progressTotalWeight <= 0 || _progressCurrentIndex < 0) {
884 int completedWeight = 0;
885 for (
int i = 0; i < _progressCurrentIndex && i < _progressWeights.size(); ++i) {
886 completedWeight += _progressWeights[i];
889 int currentWeight = (_progressCurrentIndex < _progressWeights.size())
890 ? _progressWeights[_progressCurrentIndex]
893 return (completedWeight + currentWeight * _progressSubProgress) /
static_cast<float>(_progressTotalWeight);
900void QGCStateMachine::setTimeoutOverride(
const QString& stateName,
int timeoutMsecs)
902 _timeoutOverrides[stateName] = timeoutMsecs;
903 qCDebug(QGCStateMachineLog) << objectName() <<
"timeout override set for" << stateName <<
":" << timeoutMsecs <<
"ms";
906void QGCStateMachine::removeTimeoutOverride(
const QString& stateName)
908 _timeoutOverrides.remove(stateName);
911int QGCStateMachine::timeoutOverride(
const QString& stateName)
const
913 return _timeoutOverrides.value(stateName, -1);
916void QGCStateMachine::recordTimeout(
const QString& stateName)
918 _timeoutStats[stateName]++;
919 qCDebug(QGCStateMachineLog) << objectName() <<
"timeout recorded for" << stateName
920 <<
"total:" << _timeoutStats[stateName];
927void QGCStateMachine::setCallbacks(EntryCallback onEntry, ExitCallback onExit)
929 _entryCallback = std::move(
onEntry);
930 _exitCallback = std::move(
onExit);
939 QStateMachine::onEntry(
event);
941 if (_entryCallback) {
956 QStateMachine::onExit(
event);
962 if (_eventHandler &&
event->type() >= QEvent::User) {
963 if (_eventHandler(
event)) {
968 return QStateMachine::event(
event);
std::function< void(AsyncFunctionState *state)> SetupFunction
std::function< void()> Action
std::function< bool()> Predicate
Delays that state machine for the specified time in milliseconds.
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)
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)