/* * This file is part of Skyward Hub. * * Skyward Hub is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * Skyward Hub is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * Skyward Hub. If not, see <https://www.gnu.org/licenses/>. * */ #include "OutgoingMessagesViewerModule.h" #include <Core/MessageBroker/MessageBroker.h> #include <Modules/SkywardHubStrings.h> #include <QDebug> #include <QTableWidgetItem> OutgoingMessagesViewerModule::OutgoingMessagesViewerModule() : Module(ModuleId::OUTGOING_MESSAGES_VIEWER) { setupUi(); customContextMenuActionSetup(); // Subscribe to commands, ACKs and NACKs MessageBroker::getInstance().subscribe( Filter::fromString(SkywardHubStrings::logCommandsTopic), this, [this](const Message& message, const Filter& filter) { handleMsg(message); }); MessageBroker::getInstance().subscribe( Filter::fromString(SkywardHubStrings::mavlink_received_msg_ACK_topic), this, [this](const Message& message, const Filter& filter) { handleAck(message); }); MessageBroker::getInstance().subscribe( Filter::fromString(SkywardHubStrings::mavlink_received_msg_NACK_topic), this, [this](const Message& message, const Filter& filter) { handleNack(message); }); MessageBroker::getInstance().subscribe( Filter::fromString(SkywardHubStrings::mavlink_received_msg_WACK_topic), this, [this](const Message& message, const Filter& filter) { handleWack(message); }); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &OutgoingMessagesViewerModule::updateElapsedTime); } XmlObject OutgoingMessagesViewerModule::toXmlObject() { XmlObject obj = Module::toXmlObject(); obj.addAttribute("timestamp", timestamp->isChecked()); obj.addAttribute("elapsed_time", elapsedTime->isChecked()); return obj; } void OutgoingMessagesViewerModule::fromXmlObject(const XmlObject& obj) { int timestampAtt; if (obj.getAttribute("timestamp", timestampAtt)) { timestamp->setChecked(timestampAtt != 0); } int elapsedTimeAtt; if (obj.getAttribute("elapsed_time", elapsedTimeAtt)) { elapsedTime->setChecked(elapsedTimeAtt != 0); } } void OutgoingMessagesViewerModule::setupUi() { QHBoxLayout* outerLayout = new QHBoxLayout; outerLayout->setContentsMargins(0, 0, 0, 0); table = new QTableWidget; outerLayout->addWidget(table); table->setColumnCount(3); // Hide elapsed time column table->setColumnHidden(1, true); // Hide headers table->verticalHeader()->setVisible(false); table->horizontalHeader()->setVisible(false); // Timestamp and elapse time columns resize to fit content while message // takes all the remaining space table->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::ResizeToContents); table->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::ResizeToContents); table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); // Hide vertical scrollbar table->verticalScrollBar()->setVisible(false); // Disable items selection table->setSelectionMode(QAbstractItemView::NoSelection); setLayout(outerLayout); } void OutgoingMessagesViewerModule::customContextMenuActionSetup() { timestamp = new QAction("Timestamp"); timestamp->setCheckable(true); timestamp->setChecked(true); connect(timestamp, &QAction::toggled, this, [this]() { table->setColumnHidden(0, !timestamp->isChecked()); }); customContextMenuActions.append(timestamp); elapsedTime = new QAction("Elapsed time"); elapsedTime->setCheckable(true); connect(elapsedTime, &QAction::toggled, this, [this]() { table->setColumnHidden(1, !elapsedTime->isChecked()); if (elapsedTime->isChecked() && !timer->isActive()) { timer->start(timerPeriod); } else { timer->stop(); } }); customContextMenuActions.append(elapsedTime); qDebug() << "Set context menu for outgoing messages viewer"; } void OutgoingMessagesViewerModule::resizeEvent(QResizeEvent* event) { if (table->rowCount() == 0) { return; } int totalHeight = table->viewport()->height(); int contentHeight = table->rowCount() * table->rowHeight(0); if (contentHeight <= totalHeight) { // Calculate how many rows can fit in the available space int rowsToAdd = (totalHeight - contentHeight) / table->rowHeight(0); // Calculate how many messages are available and not shown int rowsAvailable = messages.count() - table->rowCount(); // Add rows to fill the available space for (int i = 0; i < std::min(rowsToAdd, rowsAvailable); i++) { addMessageToTable(messages.at(table->rowCount()), table->rowCount()); } } else { // Calculate how many rows need to be removed int rowsToRemove = (contentHeight - totalHeight) / table->rowHeight(0) + 1; // Remove rows that don't fit int newRowCount = table->rowCount() - rowsToRemove; table->setRowCount(newRowCount); } } void OutgoingMessagesViewerModule::handleMsg(const Message& msg) { // First register the message into the list messages.prepend({QDateTime::currentDateTime(), msg}); // Check if the list is overgrowing if (messages.count() > maxListSize) { messages.removeLast(); } // Insert the message into the table addMessageToTable(messages.first()); // Update the table size resizeEvent(); } void OutgoingMessagesViewerModule::handleAck(const Message& ack) { // Color the message the ack is for with green background for (int i = 0; i < messages.count(); i++) { Message msg = messages.at(i).second; if (msg.getField("message_id") == ack.getField("recv_msgid") && msg.getField("sequence_number") == ack.getField("seq_ack")) { if (table->rowCount() > i) { table->item(i, 1)->setBackground(QBrush(QColor(0, 255, 0))); } } } } void OutgoingMessagesViewerModule::handleNack(const Message& nack) { // Color the message the nack is for with red background for (int i = 0; i < messages.count(); i++) { Message msg = messages.at(i).second; if (msg.getField("message_id") == nack.getField("recv_msgid") && msg.getField("sequence_number") == nack.getField("seq_ack")) { if (table->rowCount() > i) { table->item(i, 1)->setBackground(QBrush(QColor(255, 0, 0))); } } } } void OutgoingMessagesViewerModule::handleWack(const Message& nack) { // Color the message the wack is for with white background for (int i = 0; i < messages.count(); i++) { Message msg = messages.at(i).second; if (msg.getField("message_id") == nack.getField("recv_msgid") && msg.getField("sequence_number") == nack.getField("seq_ack")) { if (table->rowCount() > i) { table->item(i, 1)->setBackground(QBrush(QColor(255, 255, 255))); } } } } void OutgoingMessagesViewerModule::addMessageToTable( const QPair<QDateTime, Message>& msg, int row) { // Prepare the row items auto timestampTxt = new QTableWidgetItem(msg.first.toString("hh.mm.ss")); auto elapsedTimeTxt = new QTableWidgetItem("-00.00.00"); auto msgName = new QTableWidgetItem(msg.second.getField("name").toString()); msgName->setBackground(QBrush(QColor(80, 80, 80))); msgName->setForeground(QBrush(QColor(0, 0, 0))); // Add a new row table->insertRow(row); // Fill the row with data table->setItem(row, 0, timestampTxt); table->setItem(row, 1, elapsedTimeTxt); table->setItem(row, 2, msgName); } void OutgoingMessagesViewerModule::updateElapsedTime() { auto currentTime = QDateTime::currentDateTime(); for (int i = 0; i < std::min(table->rowCount(), messages.count()); i++) { auto date = messages[i].first; auto elapsedSeconds = date.secsTo(currentTime) % 60; auto elapsedMinutes = (date.secsTo(currentTime) / 60) % 60; auto elapsedHours = date.secsTo(currentTime) / 3600; auto elapsedTimeTxt = QString("-%1:%2:%3") .arg(elapsedHours, 2, 'f', 0, '0') .arg(elapsedMinutes, 2, 'f', 0, '0') .arg(elapsedSeconds, 2, 'f', 0, '0'); table->item(i, 1)->setText(elapsedTimeTxt); } }