Add option to export real-time console data (@AnnabellePundaky)

This commit is contained in:
Alex Spataru 2025-01-04 23:42:31 -05:00
parent 2c4bf6c68b
commit e05af1d830
11 changed files with 369 additions and 181 deletions

View File

@ -54,7 +54,6 @@ find_package(
Bluetooth
SerialPort
Positioning
PrintSupport
LinguistTools
QuickControls2
)
@ -98,6 +97,7 @@ set(SOURCES
src/IO/Checksum.cpp
src/IO/Console.cpp
src/IO/Manager.cpp
src/IO/ConsoleExport.cpp
src/IO/FileTransmission.cpp
src/IO/FrameReader.cpp
src/JSON/FrameParser.cpp
@ -145,6 +145,7 @@ set(HEADERS
src/IO/Manager.h
src/IO/HAL_Driver.h
src/IO/Checksum.h
src/IO/ConsoleExport.h
src/IO/CircularBuffer.h
src/IO/FileTransmission.h
src/IO/FrameReader.h
@ -223,7 +224,6 @@ target_link_libraries(
Qt6::Bluetooth
Qt6::SerialPort
Qt6::Positioning
Qt6::PrintSupport
Qt6::QuickControls2
simde

View File

@ -67,11 +67,11 @@ Widgets.Pane {
Settings {
category: "SetupPanel"
property alias tabIndex: tab.currentIndex
property alias csvExport: csvLogging.checked
property alias driver: driverCombo.currentIndex
property alias language: settings.language
property alias csvExport: csvLogging.checked
property alias tcpPlugins: settings.tcpPlugins
property alias consoleExport: consoleLogging.checked
property alias selectedDriver: driverCombo.currentIndex
}
//
@ -122,7 +122,7 @@ Widgets.Pane {
}
//
// Device type selector
// Driver selection
//
Label {
text: qsTr("Device Setup") + ":"
@ -140,23 +140,6 @@ Widgets.Pane {
}
}
//
// CSV generator
//
Switch {
id: csvLogging
Layout.leftMargin: -6
text: qsTr("Create CSV File")
Layout.alignment: Qt.AlignLeft
checked: Cpp_CSV_Export.exportEnabled
palette.highlight: Cpp_ThemeManager.colors["csv_switch"]
onCheckedChanged: {
if (Cpp_CSV_Export.exportEnabled !== checked)
Cpp_CSV_Export.exportEnabled = checked
}
}
//
// Spacer
//
@ -173,6 +156,7 @@ Widgets.Pane {
color: Cpp_ThemeManager.colors["pane_section_label"]
Component.onCompleted: font.capitalization = Font.AllUppercase
} RadioButton {
Layout.leftMargin: -6
Layout.maximumHeight: 18
Layout.maximumWidth: root.maxItemWidth
text: qsTr("No Parsing (Device Sends JSON Data)")
@ -183,6 +167,7 @@ Widgets.Pane {
Cpp_JSON_FrameBuilder.operationMode = SerialStudio.DeviceSendsJSON
}
} RadioButton {
Layout.leftMargin: -6
Layout.maximumHeight: 18
Layout.maximumWidth: root.maxItemWidth
text: qsTr("Quick Plot (Comma Separated Values)")
@ -193,6 +178,7 @@ Widgets.Pane {
Cpp_JSON_FrameBuilder.operationMode = SerialStudio.QuickPlot
}
} RadioButton {
Layout.leftMargin: -6
Layout.maximumHeight: 18
Layout.maximumWidth: root.maxItemWidth
text: qsTr("Parse via JSON Project File")
@ -225,6 +211,59 @@ Widgets.Pane {
implicitHeight: 4
}
//
// Data export switches
//
Label {
text: qsTr("Data Export") + ":"
font: Cpp_Misc_CommonFonts.customUiFont(0.8, true)
color: Cpp_ThemeManager.colors["pane_section_label"]
Component.onCompleted: font.capitalization = Font.AllUppercase
}
//
// CSV generator
//
CheckBox {
id: csvLogging
Layout.leftMargin: -6
Layout.maximumHeight: 18
text: qsTr("Create CSV File")
Layout.alignment: Qt.AlignLeft
checked: Cpp_CSV_Export.exportEnabled
Layout.maximumWidth: root.maxItemWidth
onCheckedChanged: {
if (Cpp_CSV_Export.exportEnabled !== checked)
Cpp_CSV_Export.exportEnabled = checked
}
}
//
// Console data export
//
CheckBox {
id: consoleLogging
Layout.leftMargin: -6
Layout.maximumHeight: 18
Layout.alignment: Qt.AlignLeft
text: qsTr("Export Console Data")
Layout.maximumWidth: root.maxItemWidth
checked: Cpp_IO_ConsoleExport.exportEnabled
onCheckedChanged: {
if (Cpp_IO_ConsoleExport.exportEnabled !== checked)
Cpp_IO_ConsoleExport.exportEnabled = checked
}
}
//
// Spacer
//
Item {
implicitHeight: 4
}
//
// Tab bar
//

View File

@ -34,6 +34,7 @@ Item {
//
QtSettings.Settings {
category: "DeviceSetup"
property alias serialDtr: serial.dtr
property alias serialParity: serial.parity
property alias serialBaudRate: serial.baudRate

View File

@ -152,23 +152,6 @@ Item {
text: qsTr("Clear")
opacity: enabled ? 1 : 0.5
onTriggered: root.clear()
enabled: Cpp_IO_Console.saveAvailable
}
MenuSeparator {}
MenuItem {
opacity: enabled ? 1 : 0.5
text: qsTr("Print")
enabled: Cpp_IO_Console.saveAvailable
onTriggered: Cpp_IO_Console.print()
}
MenuItem {
opacity: enabled ? 1 : 0.5
text: qsTr("Save as") + "..."
onTriggered: Cpp_IO_Console.save()
enabled: Cpp_IO_Console.saveAvailable
}
}
@ -421,30 +404,6 @@ Item {
}
}
Button {
icon.width: 18
icon.height: 18
implicitHeight: 24
Layout.maximumWidth: 32
opacity: enabled ? 1 : 0.5
onClicked: Cpp_IO_Console.save()
enabled: Cpp_IO_Console.saveAvailable
icon.source: "qrc:/rcc/icons/buttons/save.svg"
icon.color: Cpp_ThemeManager.colors["button_text"]
}
Button {
icon.width: 18
icon.height: 18
implicitHeight: 24
Layout.maximumWidth: 32
opacity: enabled ? 1 : 0.5
onClicked: Cpp_IO_Console.print()
enabled: Cpp_IO_Console.saveAvailable
icon.source: "qrc:/rcc/icons/buttons/print.svg"
icon.color: Cpp_ThemeManager.colors["button_text"]
}
Button {
icon.width: 18
icon.height: 18
@ -452,7 +411,6 @@ Item {
onClicked: root.clear()
Layout.maximumWidth: 32
opacity: enabled ? 1 : 0.5
enabled: Cpp_IO_Console.saveAvailable
icon.source: "qrc:/rcc/icons/buttons/clear.svg"
icon.color: Cpp_ThemeManager.colors["button_text"]
}

View File

@ -25,8 +25,8 @@
#include <QUrl>
#include <QFileInfo>
#include <QApplication>
#include <QDesktopServices>
#include <QStandardPaths>
#include <QDesktopServices>
#include "IO/Manager.h"
#include "CSV/Player.h"
@ -36,8 +36,8 @@
#include "JSON/FrameBuilder.h"
/**
* Connect JSON Parser & Serial Manager signals to begin registering JSON
* dataframes into JSON list.
* Constructor function, configures the path in which Serial Studio shall
* automatically write generated CSV files.
*/
CSV::Export::Export()
: m_exportEnabled(true)
@ -49,7 +49,7 @@ CSV::Export::Export()
}
/**
* Close file & finnish write-operations before destroying the class
* Close file & finnish write-operations before destroying the class.
*/
CSV::Export::~Export()
{
@ -57,7 +57,7 @@ CSV::Export::~Export()
}
/**
* Returns a pointer to the only instance of this class
* Returns a pointer to the only instance of this class.
*/
CSV::Export &CSV::Export::instance()
{
@ -66,7 +66,7 @@ CSV::Export &CSV::Export::instance()
}
/**
* Returns @c true if the CSV output file is open
* Returns @c true if the CSV output file is open.
*/
bool CSV::Export::isOpen() const
{
@ -74,25 +74,13 @@ bool CSV::Export::isOpen() const
}
/**
* Returns @c true if CSV export is enabled
* Returns @c true if CSV export is enabled.
*/
bool CSV::Export::exportEnabled() const
{
return m_exportEnabled;
}
/**
* Open the current CSV file in the Explorer/Finder window
*/
void CSV::Export::openCurrentCsv()
{
if (isOpen())
Misc::Utilities::revealFile(m_csvFile.fileName());
else
Misc::Utilities::showMessageBox(tr("CSV file not open"),
tr("Cannot find CSV export file!"));
}
/**
* Configures the signal/slot connections with the rest of the modules of the
* application.
@ -108,7 +96,7 @@ void CSV::Export::setupExternalConnections()
}
/**
* Enables or disables data export
* Enables or disables data export.
*/
void CSV::Export::setExportEnabled(const bool enabled)
{
@ -124,7 +112,7 @@ void CSV::Export::setExportEnabled(const bool enabled)
}
/**
* Write all remaining JSON frames & close the CSV file
* Write all remaining JSON frames & close the CSV file.
*/
void CSV::Export::closeFile()
{
@ -238,8 +226,8 @@ CSV::Export::createCsvFile(const CSV::TimestampFrame &frame)
const auto &rxTime = frame.rxDateTime;
// Get file name
const auto fileName
= rxTime.toString(QStringLiteral("yyyy_MMM_dd HH_mm_ss")) + ".csv";
const auto fileName = rxTime.toString(QStringLiteral("yyyy_MMM_dd HH_mm_ss"))
+ QStringLiteral(".csv");
// Get path
const QString path = QStringLiteral("%1/%2/").arg(m_csvPath, data.title());
@ -247,7 +235,7 @@ CSV::Export::createCsvFile(const CSV::TimestampFrame &frame)
// Generate file path if required
QDir dir(path);
if (!dir.exists())
dir.mkpath(".");
dir.mkpath(QStringLiteral("."));
// Open file
m_csvFile.setFileName(dir.filePath(fileName));
@ -314,7 +302,7 @@ CSV::Export::createCsvFile(const CSV::TimestampFrame &frame)
}
/**
* Appends the latest frame from the device to the output buffer
* Appends the latest frame from the device to the output buffer.
*/
void CSV::Export::registerFrame(const JSON::Frame &frame)
{

View File

@ -84,7 +84,6 @@ public:
public slots:
void closeFile();
void openCurrentCsv();
void setupExternalConnections();
void setExportEnabled(const bool enabled);

View File

@ -20,17 +20,11 @@
*/
#include <QFile>
#include <QPrinter>
#include <QDateTime>
#include <QFileDialog>
#include <QPrintDialog>
#include <QTextDocument>
#include "IO/Manager.h"
#include "IO/Console.h"
#include "Misc/Utilities.h"
#include "Misc/Translator.h"
#include "Misc/CommonFonts.h"
/**
* Generates a hexdump of the given data
@ -88,7 +82,7 @@ IO::Console::Console()
, m_showTimestamp(false)
, m_isStartingLine(true)
, m_lastCharWasCR(false)
, m_textBuffer(1024 * 1024)
, m_textBuffer(10 * 1024)
{
clear();
}
@ -110,14 +104,6 @@ bool IO::Console::echo() const
return m_echo;
}
/**
* Returns @c true if data buffer contains information that the user can export.
*/
bool IO::Console::saveAvailable() const
{
return m_textBuffer.size() > 0;
}
/**
* Returns @c true if a timestamp should be shown before each displayed data
* block.
@ -313,37 +299,6 @@ QByteArray IO::Console::hexToBytes(const QString &data)
return array;
}
/**
* Allows the user to export the information displayed on the console
*/
void IO::Console::save()
{
// No data buffer received, abort
if (!saveAvailable())
return;
// Get file name
auto path = QFileDialog::getSaveFileName(nullptr, tr("Export Console Data"),
QDir::homePath(),
tr("Text Files") + " (*.txt)");
// Create file
if (!path.isEmpty())
{
QFile file(path);
if (file.open(QFile::WriteOnly))
{
file.write(m_textBuffer.peek(m_textBuffer.size()));
file.close();
Misc::Utilities::revealFile(path);
}
else
Misc::Utilities::showMessageBox(tr("Error while exporting console data"),
file.errorString());
}
}
/**
* Deletes all the text displayed by the current QML text document
*/
@ -352,7 +307,6 @@ void IO::Console::clear()
m_textBuffer.clear();
m_isStartingLine = true;
m_lastCharWasCR = false;
Q_EMIT saveAvailableChanged();
}
/**
@ -451,35 +405,6 @@ void IO::Console::send(const QString &data)
Manager::instance().writeData(bin);
}
/**
* Creates a text document with current console output & prints it using native
* system libraries/toolkits.
*/
void IO::Console::print()
{
// Create text document
QTextDocument document;
document.setPlainText(
QString::fromUtf8(m_textBuffer.peek(m_textBuffer.size())));
// Set font
auto font = Misc::CommonFonts::instance().customMonoFont(0.8);
document.setDefaultFont(font);
// Create printer object
QPrinter printer(QPrinter::PrinterResolution);
printer.setFullPage(true);
printer.setDocName(qApp->applicationDisplayName());
printer.setPageOrientation(QPageLayout::Portrait);
// Show print dialog
QPrintDialog printDialog(&printer, nullptr);
if (printDialog.exec() == QDialog::Accepted)
{
document.print(&printer);
}
}
/**
* Enables/disables displaying a timestamp of each received data block.
*/
@ -544,9 +469,6 @@ void IO::Console::append(const QString &string, const bool addTimestamp)
if (string.isEmpty())
return;
// Check if we should update the save available feature
const bool previousSaveAvailable = saveAvailable();
// Omit leading \n if a trailing \r was already rendered from previous payload
auto data = string;
if (m_lastCharWasCR && data.startsWith('\n'))
@ -606,10 +528,6 @@ void IO::Console::append(const QString &string, const bool addTimestamp)
// Add data to saved text buffer
m_textBuffer.append(processedString.toUtf8());
// Update save avaialable
if (saveAvailable() != previousSaveAvailable)
Q_EMIT saveAvailableChanged();
// Update UI
QMetaObject::invokeMethod(
this, [=] { Q_EMIT displayString(processedString); },

View File

@ -48,9 +48,6 @@ class Console : public QObject
READ showTimestamp
WRITE setShowTimestamp
NOTIFY showTimestampChanged)
Q_PROPERTY(bool saveAvailable
READ saveAvailable
NOTIFY saveAvailableChanged)
Q_PROPERTY(IO::Console::DataMode dataMode
READ dataMode
WRITE setDataMode
@ -86,7 +83,6 @@ signals:
void historyItemChanged();
void textDocumentChanged();
void showTimestampChanged();
void saveAvailableChanged();
void displayString(const QString &text);
private:
@ -123,7 +119,6 @@ public:
static Console &instance();
[[nodiscard]] bool echo() const;
[[nodiscard]] bool saveAvailable() const;
[[nodiscard]] bool showTimestamp() const;
[[nodiscard]] DataMode dataMode() const;
@ -141,9 +136,7 @@ public:
static QByteArray hexToBytes(const QString &data);
public slots:
void save();
void clear();
void print();
void historyUp();
void historyDown();
void setupExternalConnections();

View File

@ -0,0 +1,209 @@
/*
* Serial Studio - https://serial-studio.github.io/
*
* Copyright (C) 2020-2025 Alex Spataru <https://aspatru.com>
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later OR Commercial
*/
#include "ConsoleExport.h"
#include <QDir>
#include <QUrl>
#include <QFileInfo>
#include <QApplication>
#include <QStandardPaths>
#include <QDesktopServices>
#include "IO/Console.h"
#include "IO/Manager.h"
#include "Misc/Utilities.h"
#include "Misc/TimerEvents.h"
/**
* Constructor function, configures the path in which Serial Studio shall
* automatically write generated console log files.
*/
IO::ConsoleExport::ConsoleExport()
: m_exportEnabled(true)
{
m_filePath = QStringLiteral("%1/%2/Console")
.arg(QStandardPaths::writableLocation(
QStandardPaths::DocumentsLocation),
qApp->applicationDisplayName());
}
/**
* Close file & finnish write-operations before destroying the class.
*/
IO::ConsoleExport::~ConsoleExport()
{
closeFile();
}
/**
* Returns a pointer to the only instance of this class.
*/
IO::ConsoleExport &IO::ConsoleExport::instance()
{
static ConsoleExport instance;
return instance;
}
/**
* Returns @c true if the console output file is open.
*/
bool IO::ConsoleExport::isOpen() const
{
return m_file.isOpen();
}
/**
* Returns @c true if console log export is enabled.
*/
bool IO::ConsoleExport::exportEnabled() const
{
return m_exportEnabled;
}
/**
* Write all remaining console data & close the output file.
*/
void IO::ConsoleExport::closeFile()
{
if (isOpen())
{
if (m_buffer.size() > 0)
writeData();
m_file.close();
m_textStream.setDevice(nullptr);
Q_EMIT openChanged();
}
}
/**
* Configures the signal/slot connections with the modules of the application
* that this module depends upon.
*/
void IO::ConsoleExport::setupExternalConnections()
{
connect(&IO::Console::instance(), &IO::Console::displayString, this,
&IO::ConsoleExport::registerData);
connect(&IO::Manager::instance(), &IO::Manager::connectedChanged, this,
&IO::ConsoleExport::closeFile);
connect(&Misc::TimerEvents::instance(), &Misc::TimerEvents::timeout1Hz, this,
&IO::ConsoleExport::writeData);
}
/**
* Enables or disables data export.
*/
void IO::ConsoleExport::setExportEnabled(const bool enabled)
{
m_exportEnabled = enabled;
Q_EMIT enabledChanged();
if (!exportEnabled() && isOpen())
{
m_buffer.clear();
closeFile();
}
}
/**
* Writes current buffer data to the output file, and creates a new file
* if needed.
*/
void IO::ConsoleExport::writeData()
{
// Device not connected, abort
if (!IO::Manager::instance().connected())
return;
// Export is disabled, abort
if (!exportEnabled())
return;
// Write data to the file
if (m_buffer.size() > 0)
{
// Create a new file if required
if (!isOpen())
createFile();
// Write data to hard disk
if (m_textStream.device())
{
m_textStream << m_buffer;
m_textStream.flush();
m_buffer.clear();
}
}
}
/**
* Creates a new console log output file based on the current date/time.
*/
void IO::ConsoleExport::createFile()
{
// Close current file (if any)
if (isOpen())
closeFile();
// Get filename
const auto dateTime = QDateTime::currentDateTime();
const auto fileName
= dateTime.toString(QStringLiteral("yyyy_MMM_dd HH_mm_ss"))
+ QStringLiteral(".txt");
// Generate file path if required
QDir dir(m_filePath);
if (!dir.exists())
dir.mkpath(QStringLiteral("."));
// Open file
m_file.setFileName(dir.filePath(fileName));
if (!m_file.open(QIODeviceBase::WriteOnly | QIODevice::Text))
{
Misc::Utilities::showMessageBox(tr("Console Output File Error"),
tr("Cannot open file for writing!"));
closeFile();
return;
}
// Configure text stream
m_textStream.setDevice(&m_file);
m_textStream.setGenerateByteOrderMark(true);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
m_textStream.setCodec("UTF-8");
#else
m_textStream.setEncoding(QStringConverter::Utf8);
#endif
// Emit signals
Q_EMIT openChanged();
}
/**
* Appends the given console data to the output buffer.
*/
void IO::ConsoleExport::registerData(const QString &data)
{
if (!data.isEmpty() && exportEnabled())
m_buffer.append(data);
}

View File

@ -0,0 +1,79 @@
/*
* Serial Studio - https://serial-studio.github.io/
*
* Copyright (C) 2020-2025 Alex Spataru <https://aspatru.com>
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later OR Commercial
*/
#pragma once
#include <QFile>
#include <QObject>
#include <QTextStream>
namespace IO
{
class ConsoleExport : public QObject
{
// clang-format off
Q_OBJECT
Q_PROPERTY(bool isOpen
READ isOpen
NOTIFY openChanged)
Q_PROPERTY(bool exportEnabled
READ exportEnabled
WRITE setExportEnabled
NOTIFY enabledChanged)
// clang-format on
signals:
void openChanged();
void enabledChanged();
private:
explicit ConsoleExport();
ConsoleExport(ConsoleExport &&) = delete;
ConsoleExport(const ConsoleExport &) = delete;
ConsoleExport &operator=(ConsoleExport &&) = delete;
ConsoleExport &operator=(const ConsoleExport &) = delete;
~ConsoleExport();
public:
static ConsoleExport &instance();
[[nodiscard]] bool isOpen() const;
[[nodiscard]] bool exportEnabled() const;
public slots:
void closeFile();
void setupExternalConnections();
void setExportEnabled(const bool enabled);
private slots:
void writeData();
void createFile();
void registerData(const QString &data);
private:
QFile m_file;
QString m_buffer;
QString m_filePath;
bool m_exportEnabled;
QTextStream m_textStream;
};
} // namespace IO

View File

@ -37,6 +37,7 @@
#include "IO/Manager.h"
#include "IO/Console.h"
#include "IO/ConsoleExport.h"
#include "IO/FileTransmission.h"
#include "IO/Drivers/Serial.h"
@ -255,6 +256,7 @@ void Misc::ModuleManager::initializeQmlInterface()
auto projectModel = &JSON::ProjectModel::instance();
auto miscTimerEvents = &Misc::TimerEvents::instance();
auto miscCommonFonts = &Misc::CommonFonts::instance();
auto ioConsoleExport = &IO::ConsoleExport::instance();
auto miscThemeManager = &Misc::ThemeManager::instance();
auto ioBluetoothLE = &IO::Drivers::BluetoothLE::instance();
auto ioFileTransmission = &IO::FileTransmission::instance();
@ -294,6 +296,7 @@ void Misc::ModuleManager::initializeQmlInterface()
c->setContextProperty("Cpp_JSON_FrameBuilder", frameBuilder);
c->setContextProperty("Cpp_Misc_TimerEvents", miscTimerEvents);
c->setContextProperty("Cpp_Misc_CommonFonts", miscCommonFonts);
c->setContextProperty("Cpp_IO_ConsoleExport", ioConsoleExport);
c->setContextProperty("Cpp_IO_FileTransmission", ioFileTransmission);
// Register app info with QML
@ -318,6 +321,7 @@ void Misc::ModuleManager::initializeQmlInterface()
ioManager->setupExternalConnections();
projectModel->setupExternalConnections();
frameBuilder->setupExternalConnections();
ioConsoleExport->setupExternalConnections();
// Install custom message handler to redirect qDebug output to console
qInstallMessageHandler(MessageHandler);