From 4d23912aa19b92ed78312d3dd2768ac4f33b89c9 Mon Sep 17 00:00:00 2001 From: Gus Grubba Date: Tue, 25 Oct 2016 05:46:46 -0400 Subject: [PATCH] Receiving and writing logs. Forcing QGC to use Mavlink V2 if vehicle supports it. --- src/Vehicle/Vehicle.cc | 12 +- src/Vehicle/Vehicle.h | 2 +- src/comm/MAVLinkProtocol.cc | 21 +-- src/uas/MavlinkLogManager.cc | 178 ++++++++++++++++++------- src/uas/MavlinkLogManager.h | 6 + src/ui/preferences/MavlinkSettings.qml | 23 ++-- 6 files changed, 168 insertions(+), 74 deletions(-) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index ff154aa86..6a5044b6b 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -481,7 +481,7 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes _handleCommandAck(message); break; case MAVLINK_MSG_ID_AUTOPILOT_VERSION: - _handleAutopilotVersion(message); + _handleAutopilotVersion(link, message); break; case MAVLINK_MSG_ID_WIND_COV: _handleWindCov(message); @@ -508,11 +508,17 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes _uas->receiveMessage(message); } -void Vehicle::_handleAutopilotVersion(mavlink_message_t& message) +void Vehicle::_handleAutopilotVersion(LinkInterface *link, mavlink_message_t& message) { mavlink_autopilot_version_t autopilotVersion; mavlink_msg_autopilot_version_decode(&message, &autopilotVersion); + bool isMavlink2 = (autopilotVersion.capabilities & MAV_PROTOCOL_CAPABILITY_MAVLINK2) != 0; + if(isMavlink2) { + mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(link->mavlinkChannel()); + mavlinkStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1; + } + if (autopilotVersion.flight_sw_version != 0) { int majorVersion, minorVersion, patchVersion; FIRMWARE_VERSION_TYPE versionType; @@ -2002,7 +2008,6 @@ Vehicle::_ackMavlinkLogData(uint16_t sequence) void Vehicle::_handleMavlinkLoggingData(mavlink_message_t& message) { - qDebug() << "MAVLINK_MSG_ID_LOGGING_DATA"; mavlink_logging_data_t log; mavlink_msg_logging_data_decode(&message, &log); emit mavlinkLogData(this, log.target_system, log.target_component, log.sequence, log.length, log.first_message_offset, log.data, false); @@ -2012,7 +2017,6 @@ Vehicle::_handleMavlinkLoggingData(mavlink_message_t& message) void Vehicle::_handleMavlinkLoggingDataAcked(mavlink_message_t& message) { - qDebug() << "MAVLINK_MSG_ID_LOGGING_DATA_ACKED"; mavlink_logging_data_t log; mavlink_msg_logging_data_decode(&message, &log); _ackMavlinkLogData(log.sequence); diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 808d65092..372a848e5 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -692,7 +692,7 @@ private: void _handleVibration(mavlink_message_t& message); void _handleExtendedSysState(mavlink_message_t& message); void _handleCommandAck(mavlink_message_t& message); - void _handleAutopilotVersion(mavlink_message_t& message); + void _handleAutopilotVersion(LinkInterface* link, mavlink_message_t& message); void _handleHilActuatorControls(mavlink_message_t& message); void _missionManagerError(int errorCode, const QString& errorMsg); void _geoFenceManagerError(int errorCode, const QString& errorMsg); diff --git a/src/comm/MAVLinkProtocol.cc b/src/comm/MAVLinkProtocol.cc index 3e2381389..bf1ce9852 100644 --- a/src/comm/MAVLinkProtocol.cc +++ b/src/comm/MAVLinkProtocol.cc @@ -73,7 +73,7 @@ MAVLinkProtocol::MAVLinkProtocol(QGCApplication* app) MAVLinkProtocol::~MAVLinkProtocol() { storeSettings(); - + #ifndef __mobile__ _closeLogFile(); #endif @@ -162,7 +162,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) if (!_linkMgr->links()->contains(link)) { return; } - + // receiveMutex.lock(); mavlink_message_t message; mavlink_status_t status; @@ -214,6 +214,8 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) { decodedFirstPacket = true; + /* + * Handled in Vehicle.cc now. mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(mavlinkChannel); if (!(mavlinkStatus->flags & MAVLINK_STATUS_FLAG_IN_MAVLINK1) && (mavlinkStatus->flags & MAVLINK_STATUS_FLAG_OUT_MAVLINK1)) { qDebug() << "switch to mavlink 2.0" << mavlinkStatus << mavlinkChannel << mavlinkStatus->flags; @@ -222,6 +224,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) qDebug() << "switch to mavlink 1.0" << mavlinkStatus << mavlinkChannel << mavlinkStatus->flags; mavlinkStatus->flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1; } + */ if(message.msgid == MAVLINK_MSG_ID_RADIO_STATUS) { @@ -255,7 +258,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) #ifndef __mobile__ // Log data - + if (!_logSuspendError && !_logSuspendReplay && _tempLogFile.isOpen()) { uint8_t buf[MAVLINK_MAX_PACKET_LEN+sizeof(quint64)]; @@ -280,7 +283,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) _stopLogging(); _logSuspendError = true; } - + // Check for the vehicle arming going by. This is used to trigger log save. if (!_logPromptForSave && message.msgid == MAVLINK_MSG_ID_HEARTBEAT) { mavlink_heartbeat_t state; @@ -412,7 +415,7 @@ bool MAVLinkProtocol::_closeLogFile(void) return true; } } - + return false; } @@ -458,11 +461,11 @@ void MAVLinkProtocol::_stopLogging(void) void MAVLinkProtocol::checkForLostLogFiles(void) { QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - + QString filter(QString("*.%1").arg(_logFileExtension)); QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files); qDebug() << "Orphaned log file count" << fileInfoList.count(); - + foreach(const QFileInfo fileInfo, fileInfoList) { qDebug() << "Orphaned log file" << fileInfo.filePath(); if (fileInfo.size() == 0) { @@ -488,10 +491,10 @@ void MAVLinkProtocol::suspendLogForReplay(bool suspend) void MAVLinkProtocol::deleteTempLogFiles(void) { QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - + QString filter(QString("*.%1").arg(_logFileExtension)); QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files); - + foreach(const QFileInfo fileInfo, fileInfoList) { QFile::remove(fileInfo.filePath()); } diff --git a/src/uas/MavlinkLogManager.cc b/src/uas/MavlinkLogManager.cc index 9563dd1a4..17b1e8eca 100644 --- a/src/uas/MavlinkLogManager.cc +++ b/src/uas/MavlinkLogManager.cc @@ -30,7 +30,7 @@ static const char* kEnableAutoUploadKey = "EnableAutoUploadKey"; static const char* kEnableAutoStartKey = "EnableAutoStartKey"; //----------------------------------------------------------------------------- -MavlinkLogFiles::MavlinkLogFiles(MavlinkLogManager *manager, const QString& filePath) +MavlinkLogFiles::MavlinkLogFiles(MavlinkLogManager* manager, const QString& filePath) : _manager(manager) , _size(0) , _selected(false) @@ -76,6 +76,9 @@ MavlinkLogManager::MavlinkLogManager(QGCApplication* app) , _currentLogfile(NULL) , _vehicle(NULL) , _logRunning(false) + , _loggingDisabled(false) + , _currentSavingFileFd(NULL) + , _sequence(0) { //-- Get saved settings QSettings settings; @@ -90,14 +93,17 @@ MavlinkLogManager::MavlinkLogManager(QGCApplication* app) if(!QDir(_logPath).exists()) { if(QDir().mkpath(_logPath)) { qCCritical(MavlinkLogManagerLog) << "Could not create Mavlink log download path:" << _logPath; + _loggingDisabled = true; } } - //-- Load current list of logs - QDirIterator it(_logPath, QStringList() << "*.ulg", QDir::Files); - while(it.hasNext()) { - _logFiles.append(new MavlinkLogFiles(this, it.next())); + if(!_loggingDisabled) { + //-- Load current list of logs + QDirIterator it(_logPath, QStringList() << "*.ulg", QDir::Files); + while(it.hasNext()) { + _logFiles.append(new MavlinkLogFiles(this, it.next())); + } + qCDebug(MavlinkLogManagerLog) << "Mavlink logs directory:" << _logPath; } - qCDebug(MavlinkLogManagerLog) << "Mavlink logs directory:" << _logPath; } //----------------------------------------------------------------------------- @@ -108,19 +114,19 @@ MavlinkLogManager::~MavlinkLogManager() //----------------------------------------------------------------------------- void -MavlinkLogManager::setToolbox(QGCToolbox *toolbox) +MavlinkLogManager::setToolbox(QGCToolbox* toolbox) { - QGCTool::setToolbox(toolbox); - QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); - qmlRegisterUncreatableType("QGroundControl.MavlinkLogManager", 1, 0, "MavlinkLogManager", "Reference only"); - connect(toolbox->multiVehicleManager(), &MultiVehicleManager::activeVehicleChanged, this, &MavlinkLogManager::_activeVehicleChanged); - - // _uploadURL = "http://192.168.1.21/px4"; - // _uploadURL = "http://192.168.1.9:8080"; - // _emailAddress = "gus.grubba.com"; - // _description = "Test from QGroundControl - Discard"; - // _sendLog("/Users/gus/github/work/logs/simulator.ulg"); - + QGCTool::setToolbox(toolbox); + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + qmlRegisterUncreatableType("QGroundControl.MavlinkLogManager", 1, 0, "MavlinkLogManager", "Reference only"); + if(!_loggingDisabled) { + connect(toolbox->multiVehicleManager(), &MultiVehicleManager::activeVehicleChanged, this, &MavlinkLogManager::_activeVehicleChanged); + } + // _uploadURL = "http://192.168.1.21/px4"; + // _uploadURL = "http://192.168.1.9:8080"; + // _emailAddress = "gus.grubba.com"; + // _description = "Test from QGroundControl - Discard"; + // _sendLog("/Users/gus/github/work/logs/simulator.ulg"); } //----------------------------------------------------------------------------- @@ -190,7 +196,7 @@ MavlinkLogManager::uploadLog() if(_currentLogfile) { _currentLogfile->setUploading(false); } - for(int i = 0; i < _logFiles.count(); i++ ) { + for(int i = 0; i < _logFiles.count(); i++) { _currentLogfile = qobject_cast(_logFiles.get(i)); Q_ASSERT(_currentLogfile); if(_currentLogfile->selected()) { @@ -210,18 +216,49 @@ MavlinkLogManager::uploadLog() emit busyChanged(); } +//----------------------------------------------------------------------------- +int +MavlinkLogManager::_getFirstSelected() +{ + for(int i = 0; i < _logFiles.count(); i++) { + MavlinkLogFiles* f = qobject_cast(_logFiles.get(i)); + Q_ASSERT(f); + if(f->selected()) { + return i; + } + } + return -1; +} + //----------------------------------------------------------------------------- void MavlinkLogManager::deleteLog() { - //-- TODO + while (true) { + int idx = _getFirstSelected(); + if(idx < 0) { + break; + } + MavlinkLogFiles* f = qobject_cast(_logFiles.get(idx)); + QString filePath = _logPath; + filePath += "/"; + filePath += f->name(); + filePath += ".ulg"; + QFile gone(filePath); + if(!gone.remove()) { + qCWarning(MavlinkLogManagerLog) << "Could not delete Mavlink log file:" << _logPath; + } + _logFiles.removeAt(idx); + delete f; + emit logFilesChanged(); + } } //----------------------------------------------------------------------------- void MavlinkLogManager::cancelUpload() { - for(int i = 0; i < _logFiles.count(); i++ ) { + for(int i = 0; i < _logFiles.count(); i++) { MavlinkLogFiles* pLogFile = qobject_cast(_logFiles.get(i)); Q_ASSERT(pLogFile); if(pLogFile->selected() && pLogFile != _currentLogfile) { @@ -238,9 +275,11 @@ void MavlinkLogManager::startLogging() { if(_vehicle) { - _vehicle->startMavlinkLog(); - _logRunning = true; - emit logRunningChanged(); + if(_createNewLog()) { + _vehicle->startMavlinkLog(); + _logRunning = true; + emit logRunningChanged(); + } } } @@ -252,6 +291,17 @@ MavlinkLogManager::stopLogging() _vehicle->stopMavlinkLog(); _logRunning = false; emit logRunningChanged(); + if(_currentSavingFileFd) { + fclose(_currentSavingFileFd); + _logFiles.append(new MavlinkLogFiles(this, _currentSavingFileStr)); + emit logFilesChanged(); + _currentSavingFileFd = NULL; + _currentSavingFileStr.clear(); + //-- TODO: If auto upload is set, schedule it + if(_enableAutoUpload) { + //-- Queue log for auto upload + } + } } } @@ -287,21 +337,23 @@ MavlinkLogManager::_sendLog(const QString& logFile) qCCritical(MavlinkLogManagerLog) << "Log file missing:" << logFile; return false; } - QFile *file = new QFile(logFile); + QFile* file = new QFile(logFile); if(!file || !file->open(QIODevice::ReadOnly)) { - if (file) delete file; + if(file) { + delete file; + } qCCritical(MavlinkLogManagerLog) << "Could not open log file:" << logFile; return false; } if(!_nam) { - _nam = new QNetworkAccessManager(this); + _nam = new QNetworkAccessManager(this); } QNetworkProxy savedProxy = _nam->proxy(); QNetworkProxy tempProxy; tempProxy.setType(QNetworkProxy::DefaultProxy); _nam->setProxy(tempProxy); //-- Build POST request - QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart emailPart = create_form_part("email", _emailAddress); QHttpPart descriptionPart = create_form_part("description", _description); QHttpPart sourcePart = create_form_part("source", "QGroundControl"); @@ -319,7 +371,7 @@ MavlinkLogManager::_sendLog(const QString& logFile) file->setParent(multiPart); QNetworkRequest request(_uploadURL); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - QNetworkReply *reply = _nam->post(request, multiPart); + QNetworkReply* reply = _nam->post(request, multiPart); connect(reply, &QNetworkReply::finished, this, &MavlinkLogManager::_uploadFinished); connect(this, &MavlinkLogManager::abortUpload, reply, &QNetworkReply::abort); //connect(reply, &QNetworkReply::readyRead, this, &MavlinkLogManager::_dataAvailable); @@ -332,7 +384,7 @@ MavlinkLogManager::_sendLog(const QString& logFile) //----------------------------------------------------------------------------- bool -MavlinkLogManager::_processUploadResponse(int http_code, QByteArray &data) +MavlinkLogManager::_processUploadResponse(int http_code, QByteArray& data) { qCDebug(MavlinkLogManagerLog) << "Uploaded response:" << QString::fromUtf8(data); emit readyRead(data); @@ -343,7 +395,7 @@ MavlinkLogManager::_processUploadResponse(int http_code, QByteArray &data) void MavlinkLogManager::_dataAvailable() { - QNetworkReply *reply = qobject_cast(sender()); + QNetworkReply* reply = qobject_cast(sender()); if(!reply) { return; } @@ -355,7 +407,7 @@ MavlinkLogManager::_dataAvailable() void MavlinkLogManager::_uploadFinished() { - QNetworkReply *reply = qobject_cast(sender()); + QNetworkReply* reply = qobject_cast(sender()); if(!reply) { return; } @@ -379,8 +431,9 @@ MavlinkLogManager::_uploadProgress(qint64 bytesSent, qint64 bytesTotal) { if(bytesTotal) { qreal progress = (qreal)bytesSent / (qreal)bytesTotal; - if(_currentLogfile) + if(_currentLogfile) { _currentLogfile->setProgress(progress); + } } qCDebug(MavlinkLogManagerLog) << bytesSent << "of" << bytesTotal; } @@ -393,17 +446,15 @@ MavlinkLogManager::_activeVehicleChanged(Vehicle* vehicle) // connects/disconnects. In reality, if QGC is connected to multiple vehicles, // this is called each time the user switches from one vehicle to another. So // far, I'm working on the assumption that multiple vehicles is a rare exception. - // Disconnect the previous one (if any) - if (_vehicle) { + if(_vehicle) { disconnect(_vehicle, &Vehicle::armedChanged, this, &MavlinkLogManager::_armedChanged); disconnect(_vehicle, &Vehicle::mavlinkLogData, this, &MavlinkLogManager::_mavlinkLogData); _vehicle = NULL; emit canStartLogChanged(); } // Connect new system - if (vehicle) - { + if(vehicle) { _vehicle = vehicle; connect(_vehicle, &Vehicle::armedChanged, this, &MavlinkLogManager::_armedChanged); connect(_vehicle, &Vehicle::mavlinkLogData, this, &MavlinkLogManager::_mavlinkLogData); @@ -413,10 +464,47 @@ MavlinkLogManager::_activeVehicleChanged(Vehicle* vehicle) //----------------------------------------------------------------------------- void -MavlinkLogManager::_mavlinkLogData(Vehicle * /*vehicle*/, uint8_t /*target_system*/, uint8_t /*target_component*/, uint16_t sequence, uint8_t length, uint8_t first_message, const uint8_t* data, bool /*acked*/) +MavlinkLogManager::_mavlinkLogData(Vehicle* /*vehicle*/, uint8_t /*target_system*/, uint8_t /*target_component*/, uint16_t sequence, uint8_t length, uint8_t first_message, const uint8_t* data, bool /*acked*/) +{ + if(_currentSavingFileFd) { + if(sequence != _sequence) { + qCWarning(MavlinkLogManagerLog) << "Dropped Mavlink log data"; + if(first_message < 255) { + data += first_message; + length -= first_message; + } else { + return; + } + } + if(fwrite(data, 1, length, _currentSavingFileFd) != (size_t)length) { + fclose(_currentSavingFileFd); + _currentSavingFileFd = NULL; + qCCritical(MavlinkLogManagerLog) << "Error writing Mavlink log file:" << _currentSavingFileStr; + _logRunning = false; + _vehicle->stopMavlinkLog(); + emit logRunningChanged(); + } + } else { + qCWarning(MavlinkLogManagerLog) << "Mavlink log data received when not expected."; + } + _sequence = sequence + 1; +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogManager::_createNewLog() { - Q_UNUSED(data); - qDebug() << "Mavlink Log:" << sequence << length << first_message; + if(_currentSavingFileFd) { + fclose(_currentSavingFileFd); + } + _currentSavingFileStr.sprintf("%s/%03d-%s.ulg", _logPath.toLatin1().data(), _vehicle->id(), QDateTime::currentDateTime().toString("yyyy-MM-dd-hh-mm-ss-zzz").toLatin1().data()); + _currentSavingFileFd = fopen(_currentSavingFileStr.toLatin1().data(), "wb"); + if(!_currentSavingFileFd) { + qCCritical(MavlinkLogManagerLog) << "Could not create Mavlink log file:" << _currentSavingFileStr; + _currentSavingFileStr.clear(); + } + _sequence = 0; + return _currentSavingFileFd != NULL; } //----------------------------------------------------------------------------- @@ -426,17 +514,11 @@ MavlinkLogManager::_armedChanged(bool armed) if(_vehicle) { if(armed) { if(_enableAutoStart) { - _vehicle->startMavlinkLog(); - _logRunning = true; - emit logRunningChanged(); + startLogging(); } } else { if(_logRunning && _enableAutoStart) { - _vehicle->stopMavlinkLog(); - emit logRunningChanged(); - if(_enableAutoUpload) { - //-- TODO: Queue log for auto upload - } + stopLogging(); } } } diff --git a/src/uas/MavlinkLogManager.h b/src/uas/MavlinkLogManager.h index 3ef3dc585..7f5cf8594 100644 --- a/src/uas/MavlinkLogManager.h +++ b/src/uas/MavlinkLogManager.h @@ -131,6 +131,8 @@ private slots: private: bool _sendLog (const QString& logFile); bool _processUploadResponse (int http_code, QByteArray &data); + bool _createNewLog (); + int _getFirstSelected (); private: QString _description; @@ -144,6 +146,10 @@ private: MavlinkLogFiles* _currentLogfile; Vehicle* _vehicle; bool _logRunning; + bool _loggingDisabled; + FILE* _currentSavingFileFd; + QString _currentSavingFileStr; + uint16_t _sequence; }; #endif diff --git a/src/ui/preferences/MavlinkSettings.qml b/src/ui/preferences/MavlinkSettings.qml index b73fc5620..c5b34c976 100644 --- a/src/ui/preferences/MavlinkSettings.qml +++ b/src/ui/preferences/MavlinkSettings.qml @@ -37,12 +37,10 @@ Rectangle { var selected = 0 for(var i = 0; i < QGroundControl.mavlinkLogManager.logFiles.count; i++) { var logFile = QGroundControl.mavlinkLogManager.logFiles.get(i) - console.log(logFile.selected) if(logFile.selected) selected++ } _selectedCount = selected - console.log(_selectedCount) } } @@ -296,26 +294,27 @@ Rectangle { id: logFilesColumn spacing: ScreenTools.defaultFontPixelWidth anchors.centerIn: parent + width: ScreenTools.defaultFontPixelWidth * 68 Rectangle { - width: ScreenTools.defaultFontPixelWidth * 52 + width: ScreenTools.defaultFontPixelWidth * 64 height: ScreenTools.defaultFontPixelHeight * 10 anchors.horizontalCenter: parent.horizontalCenter - color: qgcPal.windowShade + color: qgcPal.window border.color: qgcPal.text border.width: 0.5 ListView { - width: ScreenTools.defaultFontPixelWidth * 50 - height: ScreenTools.defaultFontPixelHeight * 9 + width: ScreenTools.defaultFontPixelWidth * 56 + height: ScreenTools.defaultFontPixelHeight * 8.75 anchors.centerIn: parent orientation: ListView.Vertical model: QGroundControl.mavlinkLogManager.logFiles + clip: true delegate: Rectangle { - width: ScreenTools.defaultFontPixelWidth * 48 + width: ScreenTools.defaultFontPixelWidth * 52 height: ScreenTools.defaultFontPixelHeight * 1.25 - color: index % 2 == 0 ? qgcPal.window : qgcPal.windowShade - anchors.horizontalCenter: parent.horizontalCenter; + color: qgcPal.window Row { - width: ScreenTools.defaultFontPixelWidth * 46 + width: ScreenTools.defaultFontPixelWidth * 50 anchors.centerIn: parent spacing: ScreenTools.defaultFontPixelWidth QGCCheckBox { @@ -327,10 +326,10 @@ Rectangle { } QGCLabel { text: object.name - width: ScreenTools.defaultFontPixelWidth * 20 + width: ScreenTools.defaultFontPixelWidth * 28 } QGCLabel { - text: object.size + text: Number(object.size).toLocaleString(Qt.locale(), 'f', 0) visible: !object.uploading width: ScreenTools.defaultFontPixelWidth * 20; horizontalAlignment: Text.AlignRight -- 2.22.0