QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
InitialConnectStateMachine.cc
Go to the documentation of this file.
2#include "MAVLinkLib.h"
3#include "Vehicle.h"
5#include "QGCCorePlugin.h"
6#include "QGCOptions.h"
7#include "FirmwarePlugin.h"
8#include "ParameterManager.h"
10#include "MissionManager.h"
11#include "StandardModes.h"
12#include "GeoFenceManager.h"
13#include "RallyPointManager.h"
14#include "QGCLoggingCategory.h"
15#include "SettingsManager.h"
16#include "MavlinkSettings.h"
17
18#include <cstring>
19
20QGC_LOGGING_CATEGORY(InitialConnectStateMachineLog, "Vehicle.InitialConnectStateMachine")
21
22// ============================================================================
23// InitialConnectStateMachine Implementation
24// ============================================================================
25
27 : QGCStateMachine(QStringLiteral("InitialConnectStateMachine"), vehicle, parent)
28{
29 _createStates();
30 _wireTransitions();
31 _wireProgressTracking();
32
33 setInitialState(_stateAutopilotVersion);
34}
35
39
45
46void InitialConnectStateMachine::_createStates()
47{
48 // State 0: Request autopilot version
49 _stateAutopilotVersion = new RetryableRequestMessageState(
50 QStringLiteral("RequestAutopilotVersion"),
51 this,
52 MAVLINK_MSG_ID_AUTOPILOT_VERSION,
53 [this](Vehicle*, const mavlink_message_t& message) {
54 _handleAutopilotVersionSuccess(message);
55 },
56 _autopilotVersionMaxRetries,
57 MAV_COMP_ID_AUTOPILOT1
58 );
59 _stateAutopilotVersion->setSkipPredicate([this]() {
60 return _shouldSkipAutopilotVersionRequest();
61 });
62 _stateAutopilotVersion->setFailureHandler([this](auto, auto) {
63 _handleAutopilotVersionFailure();
64 });
65
66 // State 1: Request standard modes
67 // No timeout: download duration varies too much with link speed and mode count.
68 // The standard modes protocol handles all timeouts internally and always signals completion.
69 _stateStandardModes = new AsyncFunctionState(
70 QStringLiteral("RequestStandardModes"),
71 this,
72 [this](AsyncFunctionState* state) { _requestStandardModes(state); }
73 );
74
75 // State 2: Request component information
76 // No timeout: ComponentInformationManager's nested state machines have per-state
77 // timeouts on every step and always signal completion.
78 _stateCompInfo = new AsyncFunctionState(
79 QStringLiteral("RequestCompInfo"),
80 this,
81 [this](AsyncFunctionState* state) { _requestCompInfo(state); }
82 );
83
84 // State 3: Request parameters (skippable)
85 // No timeout: download duration varies too much with link speed and param count.
86 // ParameterManager handles all timeouts internally and always terminates via
87 // parametersReadyChanged or initialParametersRequestFailed.
88 _stateParameters = new SkippableAsyncState(
89 QStringLiteral("RequestParameters"), this,
90 [this]() {
91 if (_shouldSkipForArmed()) {
92 // PX4 can try a lightweight hash-check cache load
93 if (vehicle()->px4Firmware()) {
94 return false;
95 }
96 _lastSkipReason = QStringLiteral("(vehicle is armed)");
97 return true;
98 }
99 return false;
100 },
101 [this](SkippableAsyncState* state) { _requestParameters(state); },
102 [this]() {
103 qCDebug(InitialConnectStateMachineLog) << "Skipping parameter download" << _lastSkipReason;
104 vehicle()->_parameterManager->setParameterDownloadSkipped(true);
105 });
106
107 // State 4: Request mission (skippable)
108 // No timeout: PlanManager handles all timeouts/retries internally and always signals completion.
109 _stateMission = new SkippableAsyncState(
110 QStringLiteral("RequestMission"),
111 this,
112 [this]() { return _shouldSkipForPlanLoad(); },
113 [this](SkippableAsyncState* state) { _requestMission(state); },
114 [this]() {
115 qCDebug(InitialConnectStateMachineLog) << "Skipping mission load" << _lastSkipReason;
116 }
117 );
118
119 // State 5: Request geofence (skippable)
120 // No timeout: PlanManager handles all timeouts/retries internally and always signals completion.
121 _stateGeoFence = new SkippableAsyncState(
122 QStringLiteral("RequestGeoFence"),
123 this,
124 [this]() {
125 if (_shouldSkipForPlanLoad()) {
126 return true;
127 }
128 if (!vehicle()->_geoFenceManager->supported()) {
129 _lastSkipReason = QStringLiteral("(not supported by vehicle)");
130 return true;
131 }
132 return false;
133 },
134 [this](SkippableAsyncState* state) { _requestGeoFence(state); },
135 [this]() {
136 qCDebug(InitialConnectStateMachineLog) << "Skipping geofence load" << _lastSkipReason;
137 }
138 );
139
140 // State 6: Request rally points (skippable)
141 // No timeout: PlanManager handles all timeouts/retries internally and always signals completion.
142 _stateRallyPoints = new SkippableAsyncState(
143 QStringLiteral("RequestRallyPoints"),
144 this,
145 [this]() {
146 if (_shouldSkipForPlanLoad()) {
147 return true;
148 }
149 if (!vehicle()->_rallyPointManager->supported()) {
150 _lastSkipReason = QStringLiteral("(not supported by vehicle)");
151 return true;
152 }
153 return false;
154 },
155 [this](SkippableAsyncState* state) { _requestRallyPoints(state); },
156 [this]() {
157 qCDebug(InitialConnectStateMachineLog) << "Skipping rally points load" << _lastSkipReason;
158 // Mark plan request complete when skipping
159 vehicle()->_initialPlanRequestComplete = true;
161 }
162 );
163
164 // State 7: Signal completion
165 // Use RetryState with zero retries so completion participates in the unified
166 // retry/error state family while preserving immediate success behavior.
167 _stateComplete = new RetryState(
168 QStringLiteral("SignalComplete"),
169 this,
170 [this]() {
171 _signalComplete();
172 return true;
173 },
174 0,
175 0,
177 );
178
179 // Final state
180 _stateFinal = new QGCFinalState(QStringLiteral("Final"), this);
181}
182
183void InitialConnectStateMachine::_wireTransitions()
184{
185 // Linear progression - use completed() for WaitStateBase-derived states (more semantic)
186 _stateAutopilotVersion->addTransition(_stateAutopilotVersion, &WaitStateBase::completed, _stateStandardModes);
187 _stateStandardModes->addTransition(_stateStandardModes, &WaitStateBase::completed, _stateCompInfo);
188 _stateCompInfo->addTransition(_stateCompInfo, &WaitStateBase::completed, _stateParameters);
189
190 // SkippableAsyncStates: both completed and skipped go to next state
191
192 _stateParameters->addTransition(_stateParameters, &WaitStateBase::completed, _stateMission);
193 _stateParameters->addTransition(_stateParameters, &SkippableAsyncState::skipped, _stateMission);
194
195 _stateMission->addTransition(_stateMission, &WaitStateBase::completed, _stateGeoFence);
196 _stateMission->addTransition(_stateMission, &SkippableAsyncState::skipped, _stateGeoFence);
197
198 _stateGeoFence->addTransition(_stateGeoFence, &WaitStateBase::completed, _stateRallyPoints);
199 _stateGeoFence->addTransition(_stateGeoFence, &SkippableAsyncState::skipped, _stateRallyPoints);
200
201 _stateRallyPoints->addTransition(_stateRallyPoints, &WaitStateBase::completed, _stateComplete);
202 _stateRallyPoints->addTransition(_stateRallyPoints, &SkippableAsyncState::skipped, _stateComplete);
203
204 // Complete -> Final (RetryState emits advance() on success)
205 _stateComplete->addTransition(_stateComplete, &QGCState::advance, _stateFinal);
206}
207
208void InitialConnectStateMachine::_wireProgressTracking()
209{
210 // Use QGCStateMachine's built-in weighted progress tracking
211 // Progress is automatically updated when states are entered
213 {_stateAutopilotVersion, 1},
214 {_stateStandardModes, 1},
215 {_stateCompInfo, 5},
216 {_stateParameters, 5},
217 {_stateMission, 2},
218 {_stateGeoFence, 1},
219 {_stateRallyPoints, 1},
220 {_stateComplete, 1}
221 });
222}
223
224void InitialConnectStateMachine::_onSubProgressUpdate(double progressValue)
225{
226 setSubProgress(static_cast<float>(progressValue));
227}
228
229// ============================================================================
230// Skip Predicates
231// ============================================================================
232
233bool InitialConnectStateMachine::_shouldSkipAutopilotVersionRequest() const
234{
236 if (!sharedLink) {
237 qCDebug(InitialConnectStateMachineLog) << "Skipping AUTOPILOT_VERSION: no primary link";
238 return true;
239 }
240 if (sharedLink->linkConfiguration()->isHighLatency() || sharedLink->isLogReplay()) {
241 qCDebug(InitialConnectStateMachineLog) << "Skipping AUTOPILOT_VERSION: high latency or log replay";
242 return true;
243 }
244 return false;
245}
246
247bool InitialConnectStateMachine::_shouldSkipForArmed() const
248{
249 if (!SettingsManager::instance()->mavlinkSettings()->noInitialDownloadWhenArmed()->rawValue().toBool()) {
250 return false;
251 }
252 return vehicle()->armed();
253}
254
255bool InitialConnectStateMachine::_shouldSkipForLinkType() const
256{
258 if (!sharedLink) {
259 return true;
260 }
261 return sharedLink->linkConfiguration()->isHighLatency() || sharedLink->isLogReplay();
262}
263
264bool InitialConnectStateMachine::_hasPrimaryLink() const
265{
267 return sharedLink != nullptr;
268}
269
270bool InitialConnectStateMachine::_shouldSkipForPlanLoad()
271{
272 if (_shouldSkipForArmed()) {
273 _lastSkipReason = QStringLiteral("(vehicle is armed)");
274 return true;
275 }
276 if (!_hasPrimaryLink()) {
277 _lastSkipReason = QStringLiteral("(no primary link)");
278 return true;
279 }
280 if (_shouldSkipForLinkType()) {
281 _lastSkipReason = QStringLiteral("(high latency or log replay link)");
282 return true;
283 }
284 return false;
285}
286
287// ============================================================================
288// State Callbacks
289// ============================================================================
290
291void InitialConnectStateMachine::_handleAutopilotVersionSuccess(const mavlink_message_t& message)
292{
293 qCDebug(InitialConnectStateMachineLog) << "AUTOPILOT_VERSION received";
294
295 mavlink_autopilot_version_t autopilotVersion;
296 mavlink_msg_autopilot_version_decode(&message, &autopilotVersion);
297
298 vehicle()->_uid = (quint64)autopilotVersion.uid;
299 vehicle()->_firmwareBoardVendorId = autopilotVersion.vendor_id;
300 vehicle()->_firmwareBoardProductId = autopilotVersion.product_id;
301 emit vehicle()->vehicleUIDChanged();
302
303 if (autopilotVersion.flight_sw_version != 0) {
304 int majorVersion, minorVersion, patchVersion;
305 FIRMWARE_VERSION_TYPE versionType;
306
307 majorVersion = (autopilotVersion.flight_sw_version >> (8*3)) & 0xFF;
308 minorVersion = (autopilotVersion.flight_sw_version >> (8*2)) & 0xFF;
309 patchVersion = (autopilotVersion.flight_sw_version >> (8*1)) & 0xFF;
310 versionType = (FIRMWARE_VERSION_TYPE)((autopilotVersion.flight_sw_version >> (8*0)) & 0xFF);
311 vehicle()->setFirmwareVersion(majorVersion, minorVersion, patchVersion, versionType);
312 }
313
314 if (vehicle()->px4Firmware()) {
315 // Lower 3 bytes is custom version
316 int majorVersion, minorVersion, patchVersion;
317 majorVersion = autopilotVersion.flight_custom_version[2];
318 minorVersion = autopilotVersion.flight_custom_version[1];
319 patchVersion = autopilotVersion.flight_custom_version[0];
320 vehicle()->setFirmwareCustomVersion(majorVersion, minorVersion, patchVersion);
321
322 // PX4 Firmware stores the first 16 characters of the git hash as binary, with the individual bytes in reverse order
323 vehicle()->_gitHash = "";
324 for (int i = 7; i >= 0; i--) {
325 vehicle()->_gitHash.append(QString("%1").arg(autopilotVersion.flight_custom_version[i], 2, 16, QChar('0')));
326 }
327 } else {
328 // APM Firmware stores the first 8 characters of the git hash as an ASCII character string
329 char nullStr[9];
330 strncpy(nullStr, (char*)autopilotVersion.flight_custom_version, 8);
331 nullStr[8] = 0;
332 vehicle()->_gitHash = nullStr;
333 }
334
335 if (QGCCorePlugin::instance()->options()->checkFirmwareVersion() && !vehicle()->_checkLatestStableFWDone) {
336 vehicle()->_checkLatestStableFWDone = true;
337 vehicle()->_firmwarePlugin->checkIfIsLatestStable(vehicle());
338 }
339 emit vehicle()->gitHashChanged(vehicle()->_gitHash);
340
341 vehicle()->_setCapabilities(autopilotVersion.capabilities);
342}
343
344void InitialConnectStateMachine::_handleAutopilotVersionFailure()
345{
346 qCDebug(InitialConnectStateMachineLog) << "AUTOPILOT_VERSION request failed, setting assumed capabilities";
347
348 uint64_t assumedCapabilities = MAV_PROTOCOL_CAPABILITY_MAVLINK2;
349 if (vehicle()->px4Firmware() || vehicle()->apmFirmware()) {
350 // We make some assumptions for known firmware
351 assumedCapabilities |= MAV_PROTOCOL_CAPABILITY_MISSION_INT | MAV_PROTOCOL_CAPABILITY_COMMAND_INT |
352 MAV_PROTOCOL_CAPABILITY_MISSION_FENCE | MAV_PROTOCOL_CAPABILITY_MISSION_RALLY;
353 }
354 vehicle()->_setCapabilities(assumedCapabilities);
355}
356
357void InitialConnectStateMachine::_requestStandardModes(AsyncFunctionState* state)
358{
359 qCDebug(InitialConnectStateMachineLog) << "_stateRequestStandardModes";
360
363}
364
365void InitialConnectStateMachine::_requestCompInfo(AsyncFunctionState* state)
366{
367 qCDebug(InitialConnectStateMachineLog) << "_stateRequestCompInfo";
368
369 connect(vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate,
370 this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection);
371
372 // Ensure progress tracking is always cleaned up, including timeout/skip paths.
373 state->setOnExit([this]() {
374 disconnect(vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate,
375 this, &InitialConnectStateMachine::_onSubProgressUpdate);
376 });
377
378 state->connectToCompletion(vehicle()->_componentInformationManager, &ComponentInformationManager::requestAllComplete);
379 vehicle()->_componentInformationManager->requestAllComponentInformation(nullptr, nullptr);
380}
381
382void InitialConnectStateMachine::_requestParameters(SkippableAsyncState* state)
383{
384 qCDebug(InitialConnectStateMachineLog) << "_stateRequestParameters";
385
386 const bool cacheOnly = _shouldSkipForArmed();
387 QMetaObject::Connection cacheFailedConn;
388 if (cacheOnly) {
389 // If cache-only check fails (miss/timeout/non-PX4), complete the state without params
390 cacheFailedConn =
391 connect(vehicle()->_parameterManager, &ParameterManager::cacheCheckOnlyFailed, state, [state, this]() {
392 qCDebug(InitialConnectStateMachineLog)
393 << "Parameter cache check failed while armed, advancing without parameters";
394 vehicle()->_parameterManager->setParameterDownloadSkipped(true);
395 state->complete();
396 });
397 }
398
399 connect(vehicle()->_parameterManager, &ParameterManager::loadProgressChanged,
400 this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection);
401
402 // If the vehicle never answers PARAM_REQUEST_LIST, advance without parameters
403 const QMetaObject::Connection requestFailedConn = connect(vehicle()->_parameterManager, &ParameterManager::initialParametersRequestFailed,
404 state, [state]() {
405 qCDebug(InitialConnectStateMachineLog) << "Initial parameter request failed, advancing without parameters";
406 state->complete();
407 });
408
410 [this](bool parametersReady) {
411 _onParametersReady(parametersReady);
412 });
413
414 // Ensure progress tracking is always cleaned up, including failure/skip paths.
415 state->setOnExit([this, cacheFailedConn, requestFailedConn]() {
416 disconnect(vehicle()->_parameterManager, &ParameterManager::loadProgressChanged,
417 this, &InitialConnectStateMachine::_onSubProgressUpdate);
418 disconnect(requestFailedConn);
419 if (cacheFailedConn) {
420 disconnect(cacheFailedConn);
421 }
422 });
423
424 if (cacheOnly) {
425 vehicle()->_parameterManager->tryHashCheckCacheLoad();
426 } else {
427 vehicle()->_parameterManager->refreshAllParameters(MAV_COMP_ID_ALL);
428 }
429}
430
431void InitialConnectStateMachine::_onParametersReady(bool parametersReady)
432{
433 qCDebug(InitialConnectStateMachineLog) << "_onParametersReady" << parametersReady;
434
435 // Disconnect progress tracking from parameter manager
436 disconnect(vehicle()->_parameterManager, &ParameterManager::loadProgressChanged,
437 this, &InitialConnectStateMachine::_onSubProgressUpdate);
438
439 if (parametersReady) {
440 // Send time to vehicle (twice for reliability on noisy links)
441 vehicle()->_sendQGCTimeToVehicle();
442 vehicle()->_sendQGCTimeToVehicle();
443
444 // Set up auto-disarm signalling
445 vehicle()->_setupAutoDisarmSignalling();
446
447 // Note: Speed limits are handled by Vehicle::_parametersReady
448
449 if (_stateParameters) {
450 _stateParameters->complete();
451 }
452 }
453}
454
455void InitialConnectStateMachine::_requestMission(SkippableAsyncState* state)
456{
457 qCDebug(InitialConnectStateMachineLog) << "_stateRequestMission";
458
459 connect(vehicle()->_missionManager, &MissionManager::progressPctChanged,
460 this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection);
461
463
464 // Disconnect progress tracking on exit
465 state->setOnExit([this]() {
466 disconnect(vehicle()->_missionManager, &MissionManager::progressPctChanged,
467 this, &InitialConnectStateMachine::_onSubProgressUpdate);
468 });
469
471}
472
473void InitialConnectStateMachine::_requestGeoFence(SkippableAsyncState* state)
474{
475 qCDebug(InitialConnectStateMachineLog) << "_stateRequestGeoFence";
476
477 connect(vehicle()->_geoFenceManager, &GeoFenceManager::progressPctChanged,
478 this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection);
479
480 state->connectToCompletion(vehicle()->_geoFenceManager, &GeoFenceManager::loadComplete);
481
482 // Disconnect progress tracking on exit
483 state->setOnExit([this]() {
484 disconnect(vehicle()->_geoFenceManager, &GeoFenceManager::progressPctChanged,
485 this, &InitialConnectStateMachine::_onSubProgressUpdate);
486 });
487
489}
490
491void InitialConnectStateMachine::_requestRallyPoints(SkippableAsyncState* state)
492{
493 qCDebug(InitialConnectStateMachineLog) << "_stateRequestRallyPoints";
494
495 connect(vehicle()->_rallyPointManager, &RallyPointManager::progressPctChanged,
496 this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection);
497
498 state->connectToCompletion(vehicle()->_rallyPointManager, &RallyPointManager::loadComplete,
499 [this]() {
500 // Mark initial plan request complete
501 vehicle()->_initialPlanRequestComplete = true;
503
504 if (_stateRallyPoints) {
505 _stateRallyPoints->complete();
506 }
507 });
508
509 // Always clean up progress tracking when leaving this state.
510 state->setOnExit([this]() {
511 disconnect(vehicle()->_rallyPointManager, &RallyPointManager::progressPctChanged,
512 this, &InitialConnectStateMachine::_onSubProgressUpdate);
513 });
514
516}
517
518void InitialConnectStateMachine::_signalComplete()
519{
520 qCDebug(InitialConnectStateMachineLog) << "Signalling initialConnectComplete";
521 connect(this, &QStateMachine::finished, vehicle(), [this]() {
523 }, Qt::SingleShotConnection);
524}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Calls a function when entered and waits for an external trigger to advance.
void connectToCompletion(typename QtPrivate::FunctionPointer< Func >::Object *sender, Func signal)
void complete()
Call this to signal that the async operation has completed successfully.
void progressUpdate(float progress)
void requestAllComponentInformation(RequestAllCompleteFn requestAllCompletFn, void *requestAllCompleteFnData)
virtual void checkIfIsLatestStable(Vehicle *vehicle) const
Used to check if running firmware is latest stable version.
void loadComplete(void)
State machine for initial vehicle connection sequence.
void setParameterDownloadSkipped(bool skipped)
void initialParametersRequestFailed()
Vehicle never responded to PARAM_REQUEST_LIST, all retries exhausted.
void parametersReadyChanged(bool parametersReady)
void refreshAllParameters(uint8_t componentID)
Re-request the full set of parameters from the autopilot.
void loadProgressChanged(float value)
void cacheCheckOnlyFailed()
void newMissionItemsAvailable(bool removeAllRequested)
void loadFromVehicle(void)
void progressPctChanged(double progressPercentPct)
void setOnExit(ExitCallback callback)
Set a callback to be invoked when the state is exited.
static QGCCorePlugin * instance()
Final state for a QGCStateMachine with logging support.
QGroundControl specific state machine with enhanced error handling.
void start()
Start the state machine with debug logging.
void setSubProgress(float subProgress)
Vehicle * vehicle() const
void setProgressWeights(const QList< QPair< QAbstractState *, int > > &stateWeights)
void resetProgress()
Reset progress tracking (call when restarting the machine)
void loadComplete(void)
A state that retries an action and chooses behavior when retries are exhausted.
Definition RetryState.h:15
@ EmitError
Emit error() after retries exhausted.
Definition RetryState.h:23
Requests a MAVLink message with built-in retry logic.
void setSkipPredicate(SkipPredicate predicate)
void setFailureHandler(FailureHandler handler)
Set handler called when all retries fail.
static SettingsManager * instance()
Combines skip condition checking with async operation setup in a single state.
void connectToCompletion(typename QtPrivate::FunctionPointer< Func >::Object *sender, Func signal)
void skipped()
Emitted when skip predicate returns true and the state is skipped.
void complete()
Call this to signal that the async operation has completed successfully.
void requestCompleted()
WeakLinkInterfacePtr primaryLink() const
MissionManager * _missionManager
Definition Vehicle.h:1148
void initialConnectComplete()
void initialPlanRequestCompleteChanged(bool initialPlanRequestComplete)
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:597
void setFirmwareCustomVersion(int majorVersion, int minorVersion, int patchVersion)
Definition Vehicle.cc:2311
void vehicleUIDChanged()
GeoFenceManager * _geoFenceManager
Definition Vehicle.h:1149
void gitHashChanged(QString hash)
void setFirmwareVersion(int majorVersion, int minorVersion, int patchVersion, FIRMWARE_VERSION_TYPE versionType=FIRMWARE_VERSION_TYPE_OFFICIAL)
Definition Vehicle.cc:2302
RallyPointManager * _rallyPointManager
Definition Vehicle.h:1150
bool armed() const
Definition Vehicle.h:467
StandardModes * _standardModes
Definition Vehicle.h:1156
void completed()