9#include <QtCore/QTimer>
10#include <QtStateMachine/QFinalState>
11#include <QtStateMachine/QSignalTransition>
16 : QStateMachine (parent)
21 connect(
this, &QGCStateMachine::started,
this, [
this] () {
22 qCDebug(QGCStateMachineLog) <<
"State machine started:" << objectName();
25 connect(
this, &QGCStateMachine::stopped,
this, [
this] () {
26 qCDebug(QGCStateMachineLog) <<
"State machine stopped:" << objectName();
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;
40 if (qEnvironmentVariableIsSet(
"QGC_STATEMACHINE_HISTORY")) {
43 if (qEnvironmentVariableIsSet(
"QGC_STATEMACHINE_PROFILE")) {
46 if (qEnvironmentVariableIsSet(
"QGC_STATEMACHINE_LOG")) {
65 qCCritical(QGCStateMachineLog) << objectName() <<
"start() called but already running - check signal connections";
67 QStateMachine::start();
72 _globalErrorState = errorState;
77 setGlobalRestorePolicy(QState::RestoreProperties);
78 qCDebug(QGCStateMachineLog) << objectName() <<
"property restore enabled";
83 return globalRestorePolicy() == QState::RestoreProperties;
97 if (_globalErrorState) {
104 auto* state =
new FunctionState(stateName,
this, std::move(function));
113 auto* state =
new AsyncFunctionState(stateName,
this, std::move(setupFunction), timeoutMsecs);
129 .
retry(maxRetries, retryDelayMsecs)
138 if (timeoutMsecs > 0) {
142 return builder.
build();
147 auto* state =
new DelayState(
this, delayMsecs);
161 qCDebug(QGCStateMachineLog) << objectName() <<
"posting event:" << eventName
162 << (priority == HighPriority ?
"(high priority)" :
"");
169 qCDebug(QGCStateMachineLog) << objectName() <<
"posting delayed event:" << eventName <<
"delay:" << delayMsecs <<
"ms";
175 qCDebug(QGCStateMachineLog) << objectName() <<
"cancelling delayed event id:" << eventId;
176 return QStateMachine::cancelDelayedEvent(eventId);
185 QStateMachine::setInitialState(state);
193 auto* state =
new QGCFinalState(stateName.isEmpty() ? QStringLiteral(
"Final") : stateName,
this);
201 auto* state =
new ConditionalState(stateName,
this, std::move(predicate), std::move(action));
208 auto* state =
new SubMachineState(stateName,
this, std::move(factory));
214 const QString& eventName,
217 auto* state =
new EventQueuedState(stateName,
this, eventName, timeoutMsecs);
224 std::function<
void()> onEntry,
225 std::function<
void()> onExit)
228 auto* parentState =
new QState(
this);
229 parentState->setObjectName(stateName);
232 auto* timer =
new QTimer(parentState);
233 timer->setInterval(durationMsecs);
234 timer->setSingleShot(
true);
237 auto* timingState =
new QState(parentState);
238 timingState->setObjectName(stateName + QStringLiteral(
"_Timing"));
241 auto* doneState =
new QFinalState(parentState);
245 connect(timingState, &QState::entered,
this,
onEntry);
247 connect(timingState, &QState::entered, timer, qOverload<>(&QTimer::start));
250 connect(timingState, &QState::exited,
this,
onExit);
254 timingState->addTransition(timer, &QTimer::timeout, doneState);
257 parentState->setInitialState(timingState);
259 qCDebug(QGCStateMachineLog) <<
"Created timed action state:" << stateName
260 <<
"duration:" << durationMsecs <<
"ms";
271 return configuration().contains(state);
277 return findChild<QAbstractState*>(stateName);
286 if (_globalErrorState) {
287 return configuration().contains(_globalErrorState);
293 QAbstractState* nextState,
294 const QString& message)
302 const QString& message)
320 qCCritical(QGCStateMachineLog) << objectName() <<
"resetToState: null state";
325 if (state->parentState() !=
this && state->parent() !=
this) {
326 qCCritical(QGCStateMachineLog) << objectName() <<
"resetToState: state does not belong to this machine";
330 qCDebug(QGCStateMachineLog) << objectName() <<
"resetting to state:" << state->objectName();
334 auto conn = std::make_shared<QMetaObject::Connection>();
335 *conn = connect(
this, &QStateMachine::stopped,
this, [
this, state, conn]() {
354 qCDebug(QGCStateMachineLog) << objectName() <<
"recoverFromError: not in error state";
358 qCDebug(QGCStateMachineLog) << objectName() <<
"recovering from error, restarting";
365 if (!_recoveryState) {
366 qCDebug(QGCStateMachineLog) << objectName() <<
"attemptRecovery: no recovery state set";
370 qCDebug(QGCStateMachineLog) << objectName() <<
"attempting recovery to:" << _recoveryState->objectName();
387 _deleteOnStop = deleteOnStop;
395 auto conn = std::make_shared<QMetaObject::Connection>();
396 *conn = connect(
this, &QStateMachine::stopped,
this, [
this, conn]() {
411 QAbstractAnimation* animation)
415 transition->addAnimation(animation);
417 from->addTransition(transition);
422 QAbstractAnimation* animation)
425 transition->attachToSourceState(from);
427 transition->addAnimation(animation);
429 from->addTransition(transition);
439 QList<QAbstractTransition*> result;
440 if (
auto* qstate = qobject_cast<QState*>(state)) {
441 result = qstate->transitions();
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);
461 QList<QAbstractState*> result;
463 for (QAbstractState* target : transition->targetStates()) {
464 if (!result.contains(target)) {
465 result.append(target);
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);
491 QString result = objectName() + QStringLiteral(
": ");
494 result += QStringLiteral(
"(not running)");
498 const auto active = configuration();
500 result += QStringLiteral(
"(no active state)");
502 QStringList stateNames;
503 for (QAbstractState* state :
active) {
504 stateNames.append(state->objectName().isEmpty()
505 ? QStringLiteral(
"<unnamed>")
506 : state->objectName());
508 result += stateNames.join(QStringLiteral(
", "));
509 result += QStringLiteral(
" (running)");
518 result += QStringLiteral(
"State Machine: %1\n").arg(objectName());
519 result += QStringLiteral(
"================\n");
522 const auto allStates = findChildren<QAbstractState*>();
523 result += QStringLiteral(
"States (%1):\n").arg(allStates.size());
525 for (QAbstractState* state : allStates) {
526 QString stateName = state->objectName().isEmpty()
527 ? QStringLiteral(
"<unnamed>")
528 : state->objectName();
532 if (state == initialState()) {
533 marker = QStringLiteral(
" [initial]");
535 if (qobject_cast<QFinalState*>(state)) {
536 marker += QStringLiteral(
" [final]");
538 if (state == _globalErrorState) {
539 marker += QStringLiteral(
" [error]");
541 if (configuration().contains(state)) {
542 marker += QStringLiteral(
" [active]");
545 result += QStringLiteral(
" - %1%2\n").arg(stateName, marker);
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);
571 for (
const QString& line : lines) {
572 if (!line.isEmpty()) {
573 qCDebug(QGCStateMachineLog) << line;
580 if (!_historyRecorder) {
589 return _historyRecorder && _historyRecorder->
isEnabled();
594 return _historyRecorder ? _historyRecorder->
dumpHistory() : QString();
599 return _historyRecorder ? _historyRecorder->
toJson() : QJsonArray();
612 return _profiler && _profiler->
isEnabled();
617 return _profiler ? _profiler->
summary() : QString();
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");
641 const auto allStates = findChildren<QAbstractState*>();
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();
650 if (state == initialState()) {
651 attrs << QStringLiteral(
"style=\"rounded,bold\"");
652 attrs << QStringLiteral(
"peripheries=2");
654 if (qobject_cast<QFinalState*>(state)) {
655 attrs << QStringLiteral(
"shape=doublecircle");
657 if (state == _globalErrorState) {
658 attrs << QStringLiteral(
"color=red");
661 if (attrs.isEmpty()) {
662 dot += QStringLiteral(
" \"%1\";\n").arg(name);
664 dot += QStringLiteral(
" \"%1\" [%2];\n").arg(name, attrs.join(
", "));
668 dot += QStringLiteral(
"\n");
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();
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();
685 QString label = transition->objectName();
686 if (label.isEmpty()) {
687 dot += QStringLiteral(
" \"%1\" -> \"%2\";\n").arg(fromName, toName);
689 dot += QStringLiteral(
" \"%1\" -> \"%2\" [label=\"%3\"];\n").arg(fromName, toName, label);
695 dot += QStringLiteral(
"}\n");
701 QList<QAbstractState*> unreachable;
703 if (!initialState()) {
708 QSet<QAbstractState*> visited;
709 QList<QAbstractState*> queue;
710 queue.append(initialState());
711 visited.insert(initialState());
713 while (!queue.isEmpty()) {
714 QAbstractState* current = queue.takeFirst();
716 for (QAbstractState* next : reachable) {
717 if (!visited.contains(next)) {
718 visited.insert(next);
725 const auto allStates = findChildren<QAbstractState*>();
726 for (QAbstractState* state : allStates) {
727 if (!visited.contains(state)) {
728 unreachable.append(state);
737 if (!initialState()) {
742 QHash<QAbstractState*, int> depth;
743 QList<QAbstractState*> queue;
745 queue.append(initialState());
746 depth[initialState()] = 0;
749 const int stateCount = findChildren<QAbstractState*>().size();
750 const int maxSearchDepth = qMax(stateCount * 2, 100);
752 while (!queue.isEmpty()) {
753 QAbstractState* current = queue.takeFirst();
754 int currentDepth = depth[current];
756 if (currentDepth >= maxSearchDepth) {
761 for (QAbstractState* next : reachable) {
762 int newDepth = currentDepth + 1;
764 if (!depth.contains(next) || depth[next] < newDepth) {
765 depth[next] = newDepth;
766 maxDepth = qMax(maxDepth, newDepth);
777 QList<QAbstractState*> deadEnds;
779 const auto allStates = findChildren<QAbstractState*>();
780 for (QAbstractState* state : allStates) {
782 if (qobject_cast<QFinalState*>(state)) {
788 if (transitions.isEmpty()) {
789 deadEnds.append(state);
803 for (
auto* state : _progressStates) {
804 disconnect(state, &QAbstractState::entered,
this, &QGCStateMachine::_onStateEntered);
807 _progressStates.clear();
808 _progressWeights.clear();
809 _progressTotalWeight = 0;
811 for (
const auto& pair : stateWeights) {
812 _progressStates.append(pair.first);
813 _progressWeights.append(pair.second);
814 _progressTotalWeight += pair.second;
817 connect(pair.first, &QAbstractState::entered,
this, &QGCStateMachine::_onStateEntered);
820 qCDebug(QGCStateMachineLog) << objectName() <<
"progress tracking enabled for"
821 << _progressStates.size() <<
"states, total weight:" << _progressTotalWeight;
824void QGCStateMachine::_onStateEntered()
826 auto* state = qobject_cast<QAbstractState*>(sender());
830 QString stateName = state->objectName();
831 if (!stateName.isEmpty()) {
832 _stateHistory.append(stateName);
833 while (_stateHistory.size() > _stateHistoryLimit) {
834 _stateHistory.removeFirst();
841 int index = _progressStates.indexOf(state);
842 if (index >= 0 && index != _progressCurrentIndex) {
843 _progressCurrentIndex = index;
844 _progressSubProgress = 0.0f;
846 float newProgress = _calculateProgress();
847 if (newProgress > _progressLastEmitted) {
848 _progressLastEmitted = newProgress;
856 _progressSubProgress = qBound(0.0f, subProgress, 1.0f);
858 float newProgress = _calculateProgress();
859 if (newProgress > _progressLastEmitted) {
860 _progressLastEmitted = newProgress;
867 return _progressLastEmitted;
872 _progressCurrentIndex = -1;
873 _progressSubProgress = 0.0f;
874 _progressLastEmitted = 0.0f;
877float QGCStateMachine::_calculateProgress()
const
879 if (_progressTotalWeight <= 0 || _progressCurrentIndex < 0) {
883 int completedWeight = 0;
884 for (
int i = 0; i < _progressCurrentIndex && i < _progressWeights.size(); ++i) {
885 completedWeight += _progressWeights[i];
888 int currentWeight = (_progressCurrentIndex < _progressWeights.size())
889 ? _progressWeights[_progressCurrentIndex]
892 return (completedWeight + currentWeight * _progressSubProgress) /
static_cast<float>(_progressTotalWeight);
901 _timeoutOverrides[stateName] = timeoutMsecs;
902 qCDebug(QGCStateMachineLog) << objectName() <<
"timeout override set for" << stateName <<
":" << timeoutMsecs <<
"ms";
907 _timeoutOverrides.remove(stateName);
912 return _timeoutOverrides.value(stateName, -1);
917 _timeoutStats[stateName]++;
918 qCDebug(QGCStateMachineLog) << objectName() <<
"timeout recorded for" << stateName
919 <<
"total:" << _timeoutStats[stateName];
928 _entryCallback = std::move(
onEntry);
929 _exitCallback = std::move(
onExit);
938 QStateMachine::onEntry(
event);
940 if (_entryCallback) {
955 QStateMachine::onExit(
event);
961 if (_eventHandler &&
event->type() >= QEvent::User) {
962 if (_eventHandler(
event)) {
967 return QStateMachine::event(
event);
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.
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)
void enablePropertyRestore()
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)
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.
QAbstractState * localErrorState() const
Get the per-state error state (nullptr if using global)
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)