QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
StateMachineLogger.cc
Go to the documentation of this file.
2#include "QGCStateMachine.h"
4
5#include <QtCore/QJsonArray>
6#include <QtCore/QJsonDocument>
7#include <QtStateMachine/QAbstractState>
8#include <QtStateMachine/QAbstractTransition>
9#include <QtStateMachine/QSignalTransition>
10
11// ANSI color codes
12namespace Colors {
13 const char* Reset = "\033[0m";
14 const char* Red = "\033[31m";
15 const char* Green = "\033[32m";
16 const char* Yellow = "\033[33m";
17 const char* Blue = "\033[34m";
18 const char* Magenta = "\033[35m";
19 const char* Cyan = "\033[36m";
20 const char* White = "\033[37m";
21 const char* BoldRed = "\033[1;31m";
22 const char* Gray = "\033[90m";
23}
24
25// -----------------------------------------------------------------------------
26// LogEntry
27// -----------------------------------------------------------------------------
28
29QString StateMachineLogger::LogEntry::toString(bool colored, bool showTiming, bool indent) const
30{
31 QString result;
32
33 // Timing prefix
34 if (showTiming) {
35 QString timeStr = QStringLiteral("[+%1]").arg(elapsedMs / 1000.0, 7, 'f', 3);
36 if (colored) {
37 result += QStringLiteral("%1%2%3 ").arg(Colors::Gray, timeStr, Colors::Reset);
38 } else {
39 result += timeStr + " ";
40 }
41 }
42
43 // Indentation
44 if (indent && depth > 0) {
45 result += QString(depth * 2, ' ');
46 }
47
48 // Event-specific formatting
49 QString eventStr;
50 QString colorCode = Colors::White;
51
52 switch (event) {
54 eventStr = QStringLiteral("▶ Entered %1").arg(state);
55 colorCode = Colors::Green;
56 break;
57
59 eventStr = QStringLiteral("◀ Exited %1").arg(state);
60 if (stateDurationMs > 0) {
61 eventStr += QStringLiteral(" (duration: %1ms)").arg(stateDurationMs);
62 }
63 colorCode = Colors::Blue;
64 break;
65
67 eventStr = QStringLiteral("→ Transition %1 -> %2").arg(previousState, state);
68 if (!transitionReason.isEmpty()) {
69 eventStr += QStringLiteral(" [%1]").arg(transitionReason);
70 }
71 colorCode = Colors::Cyan;
72 break;
73
75 eventStr = QStringLiteral("⏱ Timeout in %1").arg(state);
76 colorCode = Colors::Yellow;
77 break;
78
80 eventStr = QStringLiteral("✖ Error in %1").arg(state);
81 if (!message.isEmpty()) {
82 eventStr += QStringLiteral(": %1").arg(message);
83 }
84 colorCode = Colors::BoldRed;
85 break;
86
88 eventStr = QStringLiteral("▷ Machine started: %1").arg(machine);
89 colorCode = Colors::Green;
90 break;
91
93 eventStr = QStringLiteral("□ Machine stopped: %1").arg(machine);
94 colorCode = Colors::Blue;
95 break;
96
98 eventStr = QStringLiteral("↻ Retry in %1").arg(state);
99 if (!message.isEmpty()) {
100 eventStr += QStringLiteral(": %1").arg(message);
101 }
102 colorCode = Colors::Yellow;
103 break;
104
106 eventStr = QStringLiteral("⚡ Signal: %1").arg(message);
107 colorCode = Colors::Magenta;
108 break;
109
111 eventStr = QStringLiteral("◐ Progress: %1").arg(message);
112 colorCode = Colors::Cyan;
113 break;
114
115 default:
116 eventStr = message;
117 break;
118 }
119
120 if (colored) {
121 result += QStringLiteral("%1%2%3").arg(colorCode, eventStr, Colors::Reset);
122 } else {
123 result += eventStr;
124 }
125
126 return result;
127}
128
130{
131 QJsonObject obj;
132 obj["timestamp"] = timestamp.toString(Qt::ISODateWithMs);
133 obj["elapsed_ms"] = elapsedMs;
134 obj["level"] = static_cast<int>(level);
135 obj["event"] = static_cast<int>(event);
136 obj["machine"] = machine;
137
138 if (!state.isEmpty()) {
139 obj["state"] = state;
140 }
141 if (!previousState.isEmpty()) {
142 obj["previous_state"] = previousState;
143 }
144 if (!message.isEmpty()) {
145 obj["message"] = message;
146 }
147 if (!transitionReason.isEmpty()) {
148 obj["transition_reason"] = transitionReason;
149 }
150 if (stateDurationMs > 0) {
151 obj["state_duration_ms"] = stateDurationMs;
152 }
153 if (!context.isEmpty()) {
154 obj["context"] = context;
155 }
156 if (depth > 0) {
157 obj["depth"] = depth;
158 }
159
160 return obj;
161}
162
163// -----------------------------------------------------------------------------
164// StateMachineLogger
165// -----------------------------------------------------------------------------
166
168 : QObject(parent ? parent : machine)
169 , _machine(machine)
170{
171}
172
177
179{
180 if (_enabled == enabled) {
181 return;
182 }
183
184 _enabled = enabled;
185
186 if (_enabled) {
187 // Connect to machine events
188 _connections.append(connect(_machine, &QStateMachine::started,
189 this, &StateMachineLogger::_onMachineStarted));
190 _connections.append(connect(_machine, &QStateMachine::stopped,
191 this, &StateMachineLogger::_onMachineStopped));
192
193 // Connect to all existing states
194 const auto states = _machine->findChildren<QAbstractState*>();
195 for (QAbstractState* state : states) {
196 _connectToState(state);
197 }
198
199 _machineTimer.start();
200 } else {
201 // Disconnect all
202 for (const auto& conn : _connections) {
203 disconnect(conn);
204 }
205 _connections.clear();
206 }
207}
208
209void StateMachineLogger::_connectToState(QAbstractState* state)
210{
211 auto entryConn = connect(state, &QAbstractState::entered,
212 this, &StateMachineLogger::_onStateEntered);
213 auto exitConn = connect(state, &QAbstractState::exited,
214 this, &StateMachineLogger::_onStateExited);
215 _connections.append(entryConn);
216 _connections.append(exitConn);
217}
218
219void StateMachineLogger::setStateLogLevel(const QString& stateName, LogLevel level)
220{
221 _stateLogLevels[stateName] = level;
222}
223
224void StateMachineLogger::clearStateLogLevel(const QString& stateName)
225{
226 _stateLogLevels.remove(stateName);
227}
228
229void StateMachineLogger::excludeState(const QString& stateName)
230{
231 _excludedStates.insert(stateName);
232}
233
234void StateMachineLogger::includeState(const QString& stateName)
235{
236 _excludedStates.remove(stateName);
237}
238
239bool StateMachineLogger::setLogFile(const QString& filePath)
240{
241 closeLogFile();
242
243 _logFile = new QFile(filePath, this);
244 if (!_logFile->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
245 delete _logFile;
246 _logFile = nullptr;
247 return false;
248 }
249
250 _logStream = new QTextStream(_logFile);
251 return true;
252}
253
255{
256 if (_logStream) {
257 delete _logStream;
258 _logStream = nullptr;
259 }
260 if (_logFile) {
261 _logFile->close();
262 delete _logFile;
263 _logFile = nullptr;
264 }
265}
266
268{
269 _crashLogMaxEntries = maxEntries;
270 _crashLog.clear();
271}
272
274{
275 _crashLogMaxEntries = 0;
276 _crashLog.clear();
277}
278
280{
281 QStringList lines;
282 lines << QStringLiteral("=== Crash Log: %1 ===").arg(_machine->objectName());
283 lines << QStringLiteral("Entries: %1").arg(_crashLog.size());
284 lines << QString();
285
286 for (const auto& entry : _crashLog) {
287 lines << entry.toString(false, true, true);
288 }
289
290 return lines.join('\n');
291}
292
293void StateMachineLogger::log(LogLevel level, const QString& message, const QJsonObject& context)
294{
295 if (!_enabled || level > _logLevel) {
296 return;
297 }
298
299 LogEntry entry;
300 entry.timestamp = QDateTime::currentDateTime();
301 entry.elapsedMs = _machineTimer.elapsed();
302 entry.level = level;
303 entry.event = EventCustom;
304 entry.machine = _machine->objectName();
305 entry.message = message;
306 entry.context = context;
307 entry.depth = _currentDepth;
308
309 _log(entry);
310}
311
312void StateMachineLogger::logEvent(LogEvent event, const QString& message, const QJsonObject& context)
313{
314 if (!_enabled || !(_logFilter & event)) {
315 return;
316 }
317
318 LogLevel requiredLevel = Normal;
319 if (event == EventSignal || event == EventProgress) {
320 requiredLevel = Verbose;
321 }
322
323 if (requiredLevel > _logLevel) {
324 return;
325 }
326
327 LogEntry entry;
328 entry.timestamp = QDateTime::currentDateTime();
329 entry.elapsedMs = _machineTimer.elapsed();
330 entry.level = requiredLevel;
331 entry.event = event;
332 entry.machine = _machine->objectName();
333 entry.message = message;
334 entry.context = context;
335 entry.depth = _currentDepth;
336
337 _log(entry);
338}
339
340void StateMachineLogger::_log(const LogEntry& entry)
341{
342 // Update statistics
343 _eventCounts[entry.event]++;
344
345 // Add to crash log if enabled
346 if (_crashLogMaxEntries > 0) {
347 _crashLog.append(entry);
348 while (_crashLog.size() > _crashLogMaxEntries) {
349 _crashLog.removeFirst();
350 }
351 }
352
353 // Format the message
354 QString formatted = entry.toString(_coloredOutput, _logTimings, _logIndent);
355
356 // Output to console
357 qCDebug(QGCStateMachineLog).noquote() << formatted;
358
359 // Output to file
360 if (_logStream) {
361 *_logStream << entry.toString(false, _logTimings, _logIndent) << "\n";
362 _logStream->flush();
363 }
364
365 // Output to custom handler
366 if (_customHandler) {
367 _customHandler(entry);
368 }
369}
370
371StateMachineLogger::LogLevel StateMachineLogger::_effectiveLogLevel(const QString& stateName) const
372{
373 if (_stateLogLevels.contains(stateName)) {
374 return _stateLogLevels[stateName];
375 }
376 return _logLevel;
377}
378
379void StateMachineLogger::_onMachineStarted()
380{
381 if (!_enabled || !(_logFilter & EventMachineStart)) {
382 return;
383 }
384
385 _machineTimer.restart();
386 _currentDepth = 0;
387 _previousState.clear();
388 _stateEntryTimes.clear();
389
390 LogEntry entry;
391 entry.timestamp = QDateTime::currentDateTime();
392 entry.elapsedMs = 0;
393 entry.level = Normal;
394 entry.event = EventMachineStart;
395 entry.machine = _machine->objectName();
396
397 _log(entry);
398}
399
400void StateMachineLogger::_onMachineStopped()
401{
402 if (!_enabled || !(_logFilter & EventMachineStop)) {
403 return;
404 }
405
406 LogEntry entry;
407 entry.timestamp = QDateTime::currentDateTime();
408 entry.elapsedMs = _machineTimer.elapsed();
409 entry.level = Normal;
410 entry.event = EventMachineStop;
411 entry.machine = _machine->objectName();
412
413 _log(entry);
414}
415
416void StateMachineLogger::_onStateEntered()
417{
418 auto* state = qobject_cast<QAbstractState*>(sender());
419 if (!state) return;
420
421 QString stateName = state->objectName();
422
423 // Check exclusions
424 if (_excludedStates.contains(stateName)) {
425 return;
426 }
427
428 // Check log level
429 if (_effectiveLogLevel(stateName) < Normal) {
430 return;
431 }
432
433 if (!(_logFilter & EventStateEntry)) {
434 // Still track for duration calculation
435 _stateEntryTimes[stateName] = _machineTimer.elapsed();
436 _previousState = stateName;
437 return;
438 }
439
440 qint64 now = _machineTimer.elapsed();
441 _stateEntryTimes[stateName] = now;
442
443 LogEntry entry;
444 entry.timestamp = QDateTime::currentDateTime();
445 entry.elapsedMs = now;
446 entry.level = Normal;
447 entry.event = EventStateEntry;
448 entry.machine = _machine->objectName();
449 entry.state = stateName;
450 entry.previousState = _previousState;
451 entry.depth = _currentDepth;
452
453 // Try to determine transition reason
454 if (_logTransitionReasons && !_previousState.isEmpty()) {
455 entry.transitionReason = _determineTransitionReason();
456 }
457
458 _log(entry);
459
460 _previousState = stateName;
461 _currentDepth++;
462}
463
464void StateMachineLogger::_onStateExited()
465{
466 auto* state = qobject_cast<QAbstractState*>(sender());
467 if (!state) return;
468
469 QString stateName = state->objectName();
470
471 // Check exclusions
472 if (_excludedStates.contains(stateName)) {
473 return;
474 }
475
476 _currentDepth = qMax(0, _currentDepth - 1);
477
478 // Check log level
479 if (_effectiveLogLevel(stateName) < Normal) {
480 return;
481 }
482
483 if (!(_logFilter & EventStateExit)) {
484 return;
485 }
486
487 qint64 now = _machineTimer.elapsed();
488 qint64 duration = 0;
489 if (_stateEntryTimes.contains(stateName)) {
490 duration = now - _stateEntryTimes[stateName];
491 _stateEntryTimes.remove(stateName);
492 }
493
494 LogEntry entry;
495 entry.timestamp = QDateTime::currentDateTime();
496 entry.elapsedMs = now;
497 entry.level = Normal;
498 entry.event = EventStateExit;
499 entry.machine = _machine->objectName();
500 entry.state = stateName;
501 entry.stateDurationMs = duration;
502 entry.depth = _currentDepth;
503
504 _log(entry);
505}
506
507QString StateMachineLogger::_determineTransitionReason() const
508{
509 // This is a simplified version - in practice you'd need to track
510 // which signal/event triggered the transition
511 return QString(); // Would need signal tracking infrastructure
512}
513
514QString StateMachineLogger::_colorize(const QString& text, const QString& colorCode) const
515{
516 if (!_coloredOutput) {
517 return text;
518 }
519 return colorCode + text + Colors::Reset;
520}
521
522QString StateMachineLogger::_eventColor(LogEvent event) const
523{
524 switch (event) {
525 case EventStateEntry:
527 return Colors::Green;
528 case EventStateExit:
529 case EventMachineStop:
530 return Colors::Blue;
531 case EventTransition:
532 return Colors::Cyan;
533 case EventSignal:
534 return Colors::Magenta;
535 case EventTimeout:
536 case EventRetry:
537 return Colors::Yellow;
538 case EventError:
539 return Colors::BoldRed;
540 default:
541 return Colors::White;
542 }
543}
QGroundControl specific state machine with enhanced error handling.
void setEnabled(bool enabled)
void excludeState(const QString &stateName)
Exclude specific states from logging.
StateMachineLogger(QGCStateMachine *machine, QObject *parent=nullptr)
void enableCrashLog(int maxEntries)
Enable crash log buffer (circular buffer for post-mortem analysis)
void setStateLogLevel(const QString &stateName, LogLevel level)
Set log level override for a specific state.
void log(LogLevel level, const QString &message, const QJsonObject &context=QJsonObject())
Log a custom message.
void includeState(const QString &stateName)
void logEvent(LogEvent event, const QString &message, const QJsonObject &context=QJsonObject())
Log with specific event type.
LogLevel
Log verbosity levels.
@ Normal
Errors + state changes.
@ Verbose
Normal + signals/transitions.
QString dumpCrashLog() const
void clearStateLogLevel(const QString &stateName)
LogEvent
Types of log events.
bool setLogFile(const QString &filePath)
Log to a file (in addition to console)
const char * Yellow
const char * Red
const char * Magenta
const char * Blue
const char * Reset
const char * Green
const char * BoldRed
const char * White
const char * Gray
const char * Cyan
QString toString(bool colored=false, bool showTiming=true, bool indent=true) const