- Grafische Ausgabe kann nun als Video aufgezeichnet werden

- Verbindung über TCP/IP
This commit is contained in:
2026-08-18 07:34:20 +02:00
parent a942f8385b
commit 36923e1646
16 changed files with 724 additions and 311 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(UARTScope VERSION 1.0.0 LANGUAGES CXX) project(UARTScope VERSION 1.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -12,6 +12,7 @@ find_package(Qt6 REQUIRED COMPONENTS
Gui Gui
Widgets Widgets
SerialPort SerialPort
Network
) )
set(SOURCES set(SOURCES
@@ -49,6 +50,7 @@ target_link_libraries(uartscope PRIVATE
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::SerialPort Qt6::SerialPort
Qt6::Network
) )
# V4L2 uses Linux kernel headers directly (linux/videodev2.h) # V4L2 uses Linux kernel headers directly (linux/videodev2.h)

View File

@@ -4,16 +4,31 @@
#include <QLineEdit> #include <QLineEdit>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFormLayout> #include <QFormLayout>
#include <QTabWidget>
#include <QSpinBox>
#include <QSerialPort> #include <QSerialPort>
#include <QSerialPortInfo> #include <QSerialPortInfo>
struct SerialConfig { struct SerialConfig {
// Which transport to use: a real/virtual serial port, or a plain TCP
// connection (e.g. for emulators/firmware that stream their debug
// UART over the network, such as FS-UAE piped through socat).
enum class ConnectionType { Serial, Network };
ConnectionType connectionType = ConnectionType::Serial;
// Serial-specific
QString portName; QString portName;
qint32 baudRate = 115200; qint32 baudRate = 115200;
QSerialPort::DataBits dataBits = QSerialPort::Data8; QSerialPort::DataBits dataBits = QSerialPort::Data8;
QSerialPort::Parity parity = QSerialPort::NoParity; QSerialPort::Parity parity = QSerialPort::NoParity;
QSerialPort::StopBits stopBits = QSerialPort::OneStop; QSerialPort::StopBits stopBits = QSerialPort::OneStop;
QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl; QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl;
// Network-specific
QString networkHost;
quint16 networkPort = 0;
// Common
QString logFilePath; QString logFilePath;
}; };
@@ -32,11 +47,17 @@ private slots:
private: private:
void addPtyPorts(); void addPtyPorts();
QTabWidget *m_modeTabs = nullptr;
QComboBox *m_portCombo = nullptr; QComboBox *m_portCombo = nullptr;
QComboBox *m_baudCombo = nullptr; QComboBox *m_baudCombo = nullptr;
QComboBox *m_dataBitsCombo = nullptr; QComboBox *m_dataBitsCombo = nullptr;
QComboBox *m_parityCombo = nullptr; QComboBox *m_parityCombo = nullptr;
QComboBox *m_stopBitsCombo = nullptr; QComboBox *m_stopBitsCombo = nullptr;
QComboBox *m_flowCombo = nullptr; QComboBox *m_flowCombo = nullptr;
QLineEdit *m_hostEdit = nullptr;
QSpinBox *m_portSpin = nullptr;
QLineEdit *m_logPathEdit = nullptr; QLineEdit *m_logPathEdit = nullptr;
}; };

View File

@@ -2,17 +2,27 @@
#include <QObject> #include <QObject>
#include <QThread> #include <QThread>
#include <QSerialPort> #include <QSerialPort>
#include <QTcpSocket>
#include <QAbstractSocket>
#include <QFile> #include <QFile>
#include <QTextStream> #include <QTextStream>
#include <QString> #include <QString>
#include <QTimer> #include <QTimer>
// SerialWorker lives in its own QThread and owns the QSerialPort. // SerialWorker lives in its own QThread and owns the actual data source,
// which is either a QSerialPort (real/virtual serial device) or a
// QTcpSocket (for firmware/emulators that send their debug UART over a
// TCP connection, e.g. FS-UAE's serial-over-socat setup). Both are
// QIODevice subclasses, so the byte-parsing pipeline below (line
// buffering, ANSI stripping, tag detection, logging) is shared as-is
// between the two transports; only opening/closing/reconnect handling
// differs.
//
// It emits newLine() for every complete line received, // It emits newLine() for every complete line received,
// tagDetected() when a line contains a recognised tag like [WDG], // tagDetected() when a line contains a recognised tag like [WDG],
// and clearScreen() when an ANSI clear-screen sequence is received. // and clearScreen() when an ANSI clear-screen sequence is received.
// Auto-reconnect: if the port drops unexpectedly, the worker retries // Auto-reconnect: if the connection drops unexpectedly, the worker
// every reconnectIntervalMs until success or closePort(). // retries every reconnectIntervalMs until success or closePort().
class SerialWorker : public QObject class SerialWorker : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -30,6 +40,7 @@ public slots:
QSerialPort::Parity parity, QSerialPort::Parity parity,
QSerialPort::StopBits stopBits, QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl); QSerialPort::FlowControl flowControl);
void openNetwork(const QString &host, quint16 port);
void closePort(); void closePort();
void setLogFile(const QString &path); void setLogFile(const QString &path);
void stopLogging(); void stopLogging();
@@ -46,16 +57,28 @@ signals:
private slots: private slots:
void onReadyRead(); void onReadyRead();
void onPortError(QSerialPort::SerialPortError err); void onPortError(QSerialPort::SerialPortError err);
void onSocketConnected();
void onSocketDisconnected();
void onSocketError(QAbstractSocket::SocketError err);
void tryReconnect(); void tryReconnect();
void flushScanTail(); void flushScanTail();
private: private:
enum class ConnectionMode { Serial, Network };
void processRawData(const QByteArray &data); void processRawData(const QByteArray &data);
void appendToLineBuffer(const QByteArray &toProcess); void appendToLineBuffer(const QByteArray &toProcess);
void processLine(const QString &line); void processLine(const QString &line);
void scheduleReconnect(); void scheduleReconnect();
ConnectionMode m_mode = ConnectionMode::Serial;
QSerialPort *m_port = nullptr; QSerialPort *m_port = nullptr;
QTcpSocket *m_socket = nullptr;
// Points at whichever of m_port/m_socket is the currently active data
// source; onReadyRead() reads from this without caring which it is.
QIODevice *m_device = nullptr;
QFile *m_logFile = nullptr; QFile *m_logFile = nullptr;
QTextStream *m_logStream = nullptr; QTextStream *m_logStream = nullptr;
QString m_buffer; QString m_buffer;
@@ -77,10 +100,15 @@ private:
int m_reconnectIntervalMs = 2000; int m_reconnectIntervalMs = 2000;
int m_reconnectAttempt = 0; int m_reconnectAttempt = 0;
// Serial-specific connection parameters
QString m_portName; QString m_portName;
qint32 m_baudRate = 115200; qint32 m_baudRate = 115200;
QSerialPort::DataBits m_dataBits = QSerialPort::Data8; QSerialPort::DataBits m_dataBits = QSerialPort::Data8;
QSerialPort::Parity m_parity = QSerialPort::NoParity; QSerialPort::Parity m_parity = QSerialPort::NoParity;
QSerialPort::StopBits m_stopBits = QSerialPort::OneStop; QSerialPort::StopBits m_stopBits = QSerialPort::OneStop;
QSerialPort::FlowControl m_flowControl = QSerialPort::NoFlowControl; QSerialPort::FlowControl m_flowControl = QSerialPort::NoFlowControl;
// Network-specific connection parameters
QString m_networkHost;
quint16 m_networkPort = 0;
}; };

View File

@@ -1,15 +1,22 @@
#pragma once #pragma once
#include <QWidget> #include <QWidget>
#include <QImage> #include <QImage>
#include <QSize>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QComboBox> #include <QComboBox>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QThread> #include <QThread>
#include <QProcess>
#include "v4l2worker.h" #include "v4l2worker.h"
// VideoWidget shows the live V4L2 preview and can additionally record it
// to an MP4 file. Recording works by piping raw BGRA frames into an
// external `ffmpeg` process (via QProcess/stdin), which encodes them to
// H.264/MP4 - this avoids reimplementing video encoding and just needs
// ffmpeg to be installed and on PATH.
class VideoWidget : public QWidget class VideoWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -29,6 +36,7 @@ private slots:
void onStartStop(); void onStartStop();
void onFreeze(bool frozen); void onFreeze(bool frozen);
void onScreenshot(); void onScreenshot();
void onRecordToggled(bool checked);
void refreshDeviceList(); void refreshDeviceList();
protected: protected:
@@ -40,6 +48,9 @@ private:
void startCapture(); void startCapture();
void stopCapture(); void stopCapture();
void updateScaled(); void updateScaled();
void startRecording();
void stopRecording();
void writeRecordingFrame(const QImage &frame);
QThread *m_thread = nullptr; QThread *m_thread = nullptr;
V4l2Worker *m_worker = nullptr; V4l2Worker *m_worker = nullptr;
@@ -54,5 +65,15 @@ private:
QPushButton *m_startBtn = nullptr; QPushButton *m_startBtn = nullptr;
QPushButton *m_freezeBtn = nullptr; QPushButton *m_freezeBtn = nullptr;
QPushButton *m_screenshotBtn = nullptr; QPushButton *m_screenshotBtn = nullptr;
QPushButton *m_recordBtn = nullptr;
QLabel *m_infoLabel = nullptr; QLabel *m_infoLabel = nullptr;
// Recording state. m_recordProcess is the running ffmpeg instance (or
// nullptr when not recording); m_recordSize is the frame size the
// recording was started with, so a mid-stream resolution change (which
// ffmpeg's raw pipe can't handle) can be detected and the recording
// stopped cleanly instead of corrupting the output file.
QProcess *m_recordProcess = nullptr;
bool m_recording = false;
QSize m_recordSize;
}; };

View File

@@ -10,24 +10,28 @@
ConnectDialog::ConnectDialog(QWidget *parent) ConnectDialog::ConnectDialog(QWidget *parent)
: QDialog(parent) : QDialog(parent)
{ {
setWindowTitle(tr("Connect to UART")); setWindowTitle(tr("Connect to UARTScope input"));
setMinimumWidth(360); setMinimumWidth(380);
auto *form = new QFormLayout(); m_modeTabs = new QTabWidget(this);
// ── "Seriell" tab: real or virtual (PTY) serial port ────────────────
auto *serialTab = new QWidget(m_modeTabs);
auto *form = new QFormLayout(serialTab);
form->setRowWrapPolicy(QFormLayout::DontWrapRows); form->setRowWrapPolicy(QFormLayout::DontWrapRows);
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
auto *portRow = new QHBoxLayout(); auto *portRow = new QHBoxLayout();
m_portCombo = new QComboBox(this); m_portCombo = new QComboBox(serialTab);
m_portCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_portCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto *refreshBtn = new QPushButton(tr(""), this); auto *refreshBtn = new QPushButton(tr(""), serialTab);
refreshBtn->setMaximumWidth(30); refreshBtn->setMaximumWidth(30);
connect(refreshBtn, &QPushButton::clicked, this, &ConnectDialog::refreshPorts); connect(refreshBtn, &QPushButton::clicked, this, &ConnectDialog::refreshPorts);
portRow->addWidget(m_portCombo, 1); portRow->addWidget(m_portCombo, 1);
portRow->addWidget(refreshBtn); portRow->addWidget(refreshBtn);
form->addRow(tr("Port:"), portRow); form->addRow(tr("Port:"), portRow);
m_baudCombo = new QComboBox(this); m_baudCombo = new QComboBox(serialTab);
const QList<int> bauds = {1200, 2400, 4800, 9600, 19200, 38400, const QList<int> bauds = {1200, 2400, 4800, 9600, 19200, 38400,
57600, 115200, 230400, 460800, 921600}; 57600, 115200, 230400, 460800, 921600};
for (int b : bauds) for (int b : bauds)
@@ -35,7 +39,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_baudCombo->setCurrentText("115200"); m_baudCombo->setCurrentText("115200");
form->addRow(tr("Baud rate:"), m_baudCombo); form->addRow(tr("Baud rate:"), m_baudCombo);
m_dataBitsCombo = new QComboBox(this); m_dataBitsCombo = new QComboBox(serialTab);
m_dataBitsCombo->addItem("5", QSerialPort::Data5); m_dataBitsCombo->addItem("5", QSerialPort::Data5);
m_dataBitsCombo->addItem("6", QSerialPort::Data6); m_dataBitsCombo->addItem("6", QSerialPort::Data6);
m_dataBitsCombo->addItem("7", QSerialPort::Data7); m_dataBitsCombo->addItem("7", QSerialPort::Data7);
@@ -43,7 +47,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_dataBitsCombo->setCurrentText("8"); m_dataBitsCombo->setCurrentText("8");
form->addRow(tr("Data bits:"), m_dataBitsCombo); form->addRow(tr("Data bits:"), m_dataBitsCombo);
m_parityCombo = new QComboBox(this); m_parityCombo = new QComboBox(serialTab);
m_parityCombo->addItem(tr("None"), QSerialPort::NoParity); m_parityCombo->addItem(tr("None"), QSerialPort::NoParity);
m_parityCombo->addItem(tr("Even"), QSerialPort::EvenParity); m_parityCombo->addItem(tr("Even"), QSerialPort::EvenParity);
m_parityCombo->addItem(tr("Odd"), QSerialPort::OddParity); m_parityCombo->addItem(tr("Odd"), QSerialPort::OddParity);
@@ -51,18 +55,52 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_parityCombo->addItem(tr("Mark"), QSerialPort::MarkParity); m_parityCombo->addItem(tr("Mark"), QSerialPort::MarkParity);
form->addRow(tr("Parity:"), m_parityCombo); form->addRow(tr("Parity:"), m_parityCombo);
m_stopBitsCombo = new QComboBox(this); m_stopBitsCombo = new QComboBox(serialTab);
m_stopBitsCombo->addItem("1", QSerialPort::OneStop); m_stopBitsCombo->addItem("1", QSerialPort::OneStop);
m_stopBitsCombo->addItem("1.5", QSerialPort::OneAndHalfStop); m_stopBitsCombo->addItem("1.5", QSerialPort::OneAndHalfStop);
m_stopBitsCombo->addItem("2", QSerialPort::TwoStop); m_stopBitsCombo->addItem("2", QSerialPort::TwoStop);
form->addRow(tr("Stop bits:"), m_stopBitsCombo); form->addRow(tr("Stop bits:"), m_stopBitsCombo);
m_flowCombo = new QComboBox(this); m_flowCombo = new QComboBox(serialTab);
m_flowCombo->addItem(tr("None"), QSerialPort::NoFlowControl); m_flowCombo->addItem(tr("None"), QSerialPort::NoFlowControl);
m_flowCombo->addItem(tr("RTS/CTS"), QSerialPort::HardwareControl); m_flowCombo->addItem(tr("RTS/CTS"), QSerialPort::HardwareControl);
m_flowCombo->addItem(tr("XON/XOFF"), QSerialPort::SoftwareControl); m_flowCombo->addItem(tr("XON/XOFF"), QSerialPort::SoftwareControl);
form->addRow(tr("Flow control:"), m_flowCombo); form->addRow(tr("Flow control:"), m_flowCombo);
m_modeTabs->addTab(serialTab, tr("Seriell"));
// ── "Netzwerk (TCP)" tab: e.g. FS-UAE's serial-over-socat output ────
auto *networkTab = new QWidget(m_modeTabs);
auto *netForm = new QFormLayout(networkTab);
netForm->setRowWrapPolicy(QFormLayout::DontWrapRows);
netForm->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_hostEdit = new QLineEdit(networkTab);
m_hostEdit->setPlaceholderText(tr("z. B. localhost oder 192.168.1.50"));
m_hostEdit->setText("localhost");
netForm->addRow(tr("Host / IP:"), m_hostEdit);
m_portSpin = new QSpinBox(networkTab);
m_portSpin->setRange(1, 65535);
m_portSpin->setValue(1234);
netForm->addRow(tr("Port:"), m_portSpin);
auto *netInfoLabel = new QLabel(
tr("Für Emulatoren/Firmware, die die UART-Ausgabe über TCP senden "
"statt über ein echtes serielles Gerät (z. B. FS-UAE, dessen "
"serielle Schnittstelle per <tt>socat</tt> auf einen TCP-Port "
"gelegt wird): hier Host und Port statt eines seriellen Ports "
"angeben. Baudrate/Parität etc. spielen dabei keine Rolle."),
networkTab);
netInfoLabel->setWordWrap(true);
netInfoLabel->setStyleSheet("color: gray; font-size: 10px;");
netForm->addRow(netInfoLabel);
m_modeTabs->addTab(networkTab, tr("Netzwerk (TCP)"));
// ── shared: log file (applies to either transport) ──────────────────
auto *logForm = new QFormLayout();
logForm->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
auto *logRow = new QHBoxLayout(); auto *logRow = new QHBoxLayout();
m_logPathEdit = new QLineEdit(this); m_logPathEdit = new QLineEdit(this);
m_logPathEdit->setPlaceholderText(tr("(optional) path/to/output.log")); m_logPathEdit->setPlaceholderText(tr("(optional) path/to/output.log"));
@@ -71,7 +109,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
connect(browseBtn, &QPushButton::clicked, this, &ConnectDialog::browseLogFile); connect(browseBtn, &QPushButton::clicked, this, &ConnectDialog::browseLogFile);
logRow->addWidget(m_logPathEdit, 1); logRow->addWidget(m_logPathEdit, 1);
logRow->addWidget(browseBtn); logRow->addWidget(browseBtn);
form->addRow(tr("Log file:"), logRow); logForm->addRow(tr("Log file:"), logRow);
auto *buttons = new QDialogButtonBox( auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
@@ -79,7 +117,8 @@ ConnectDialog::ConnectDialog(QWidget *parent)
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *mainLayout = new QVBoxLayout(this); auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(form); mainLayout->addWidget(m_modeTabs);
mainLayout->addLayout(logForm);
mainLayout->addWidget(buttons); mainLayout->addWidget(buttons);
refreshPorts(); refreshPorts();
@@ -146,12 +185,20 @@ void ConnectDialog::browseLogFile()
SerialConfig ConnectDialog::config() const SerialConfig ConnectDialog::config() const
{ {
SerialConfig cfg; SerialConfig cfg;
cfg.connectionType = (m_modeTabs->currentIndex() == 1)
? SerialConfig::ConnectionType::Network
: SerialConfig::ConnectionType::Serial;
cfg.portName = m_portCombo->currentData().toString(); cfg.portName = m_portCombo->currentData().toString();
cfg.baudRate = m_baudCombo->currentData().toInt(); cfg.baudRate = m_baudCombo->currentData().toInt();
cfg.dataBits = static_cast<QSerialPort::DataBits>(m_dataBitsCombo->currentData().toInt()); cfg.dataBits = static_cast<QSerialPort::DataBits>(m_dataBitsCombo->currentData().toInt());
cfg.parity = static_cast<QSerialPort::Parity>(m_parityCombo->currentData().toInt()); cfg.parity = static_cast<QSerialPort::Parity>(m_parityCombo->currentData().toInt());
cfg.stopBits = static_cast<QSerialPort::StopBits>(m_stopBitsCombo->currentData().toInt()); cfg.stopBits = static_cast<QSerialPort::StopBits>(m_stopBitsCombo->currentData().toInt());
cfg.flowControl = static_cast<QSerialPort::FlowControl>(m_flowCombo->currentData().toInt()); cfg.flowControl = static_cast<QSerialPort::FlowControl>(m_flowCombo->currentData().toInt());
cfg.networkHost = m_hostEdit->text().trimmed();
cfg.networkPort = static_cast<quint16>(m_portSpin->value());
cfg.logFilePath = m_logPathEdit->text().trimmed(); cfg.logFilePath = m_logPathEdit->text().trimmed();
return cfg; return cfg;
} }

View File

@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
{ {
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("UARTScope"); app.setApplicationName("UARTScope");
app.setApplicationVersion("1.0.0"); app.setApplicationVersion("1.1.0");
app.setOrganizationName("ChicaDev"); app.setOrganizationName("ChicaDev");
app.setStyle(QStyleFactory::create("Fusion")); app.setStyle(QStyleFactory::create("Fusion"));

View File

@@ -117,6 +117,7 @@ void MainWindow::setupUi()
void MainWindow::setupToolBar() void MainWindow::setupToolBar()
{ {
auto *tb = addToolBar(tr("Main")); auto *tb = addToolBar(tr("Main"));
tb->setObjectName("mainToolBar"); // required by QMainWindow::saveState()
tb->setMovable(false); tb->setMovable(false);
m_connectAction = tb->addAction(tr("Connect…")); m_connectAction = tb->addAction(tr("Connect…"));
@@ -199,10 +200,18 @@ void MainWindow::onConnectClicked()
return; return;
m_lastConfig = dlg.config(); m_lastConfig = dlg.config();
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Serial) {
if (m_lastConfig.portName.isEmpty()) { if (m_lastConfig.portName.isEmpty()) {
QMessageBox::warning(this, tr("No port"), tr("Please select a valid serial port.")); QMessageBox::warning(this, tr("No port"), tr("Please select a valid serial port."));
return; return;
} }
} else {
if (m_lastConfig.networkHost.isEmpty()) {
QMessageBox::warning(this, tr("No host"), tr("Please enter a host/IP to connect to."));
return;
}
}
if (!m_lastConfig.logFilePath.isEmpty()) if (!m_lastConfig.logFilePath.isEmpty())
QMetaObject::invokeMethod(m_worker, "setLogFile", QMetaObject::invokeMethod(m_worker, "setLogFile",
@@ -216,6 +225,7 @@ void MainWindow::onConnectClicked()
m_worker->setReconnectInterval(interval); m_worker->setReconnectInterval(interval);
}, Qt::QueuedConnection); }, Qt::QueuedConnection);
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Serial) {
QMetaObject::invokeMethod(m_worker, "openPort", QMetaObject::invokeMethod(m_worker, "openPort",
Qt::QueuedConnection, Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.portName), Q_ARG(QString, m_lastConfig.portName),
@@ -224,6 +234,12 @@ void MainWindow::onConnectClicked()
Q_ARG(QSerialPort::Parity, m_lastConfig.parity), Q_ARG(QSerialPort::Parity, m_lastConfig.parity),
Q_ARG(QSerialPort::StopBits, m_lastConfig.stopBits), Q_ARG(QSerialPort::StopBits, m_lastConfig.stopBits),
Q_ARG(QSerialPort::FlowControl, m_lastConfig.flowControl)); Q_ARG(QSerialPort::FlowControl, m_lastConfig.flowControl));
} else {
QMetaObject::invokeMethod(m_worker, "openNetwork",
Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.networkHost),
Q_ARG(quint16, m_lastConfig.networkPort));
}
} }
void MainWindow::onDisconnectClicked() void MainWindow::onDisconnectClicked()
@@ -238,10 +254,17 @@ void MainWindow::onPortOpened()
m_stateLabel->setText(tr("● Connected")); m_stateLabel->setText(tr("● Connected"));
m_stateLabel->setStyleSheet("color: #4ec94e;"); m_stateLabel->setStyleSheet("color: #4ec94e;");
m_reconnectLabel->clear(); m_reconnectLabel->clear();
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Network) {
m_portLabel->setText(
QStringLiteral("%1:%2 (TCP)")
.arg(m_lastConfig.networkHost)
.arg(m_lastConfig.networkPort));
} else {
m_portLabel->setText( m_portLabel->setText(
QStringLiteral("%1 @ %2 baud") QStringLiteral("%1 @ %2 baud")
.arg(m_lastConfig.portName) .arg(m_lastConfig.portName)
.arg(m_lastConfig.baudRate)); .arg(m_lastConfig.baudRate));
}
statusBar()->clearMessage(); statusBar()->clearMessage();
} }
@@ -299,12 +322,15 @@ void MainWindow::clearAllViews()
void MainWindow::showFormatReference() void MainWindow::showFormatReference()
{ {
static const QString referenceText = R"( static const QString referenceText = R"UARTSCOPE(
# UARTScope Output Format Reference # UARTScope Output Format Reference
# Give this text to your AI assistant to generate compatible UART output. # Give this text to your AI assistant to generate compatible UART output.
UARTScope reads from a serial UART port and interprets the incoming text in UARTScope reads from either a serial UART port OR a plain TCP connection
three parallel ways. You can use any combination. (see the "Netzwerk (TCP)" tab in the Connect dialog - handy for emulators
like FS-UAE whose debug UART is piped through socat to a TCP port) and
interprets the incoming text in three parallel ways. You can use any
combination.
1. RAW OUTPUT (always active) 1. RAW OUTPUT (always active)
@@ -397,7 +423,7 @@ void uart_status_update(void) {
// Clear screen from firmware when you want a fresh start: // Clear screen from firmware when you want a fresh start:
// uart_printf("\033[2J\033[H"); // uart_printf("\033[2J\033[H");
)"; )UARTSCOPE";
auto *dlg = new QDialog(this); auto *dlg = new QDialog(this);
dlg->setWindowTitle(tr("UARTScope Format Reference")); dlg->setWindowTitle(tr("UARTScope Format Reference"));
@@ -546,7 +572,7 @@ void MainWindow::showAbout()
grid->addWidget(v, row, 1); grid->addWidget(v, row, 1);
}; };
addRow(0, tr("Version"), "1.0.0"); addRow(0, tr("Version"), "1.1.0");
addRow(1, tr("Entwickler"), "Dany Thinnes"); addRow(1, tr("Entwickler"), "Dany Thinnes");
addRow(2, tr("Projekt"), "Projekt Hirnfrei"); addRow(2, tr("Projekt"), "Projekt Hirnfrei");
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR)); addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));

View File

@@ -5,6 +5,7 @@
SerialWorker::SerialWorker(QObject *parent) SerialWorker::SerialWorker(QObject *parent)
: QObject(parent) : QObject(parent)
, m_port(new QSerialPort(this)) , m_port(new QSerialPort(this))
, m_socket(new QTcpSocket(this))
, m_idleFlushTimer(new QTimer(this)) , m_idleFlushTimer(new QTimer(this))
, m_reconnectTimer(new QTimer(this)) , m_reconnectTimer(new QTimer(this))
{ {
@@ -16,6 +17,11 @@ SerialWorker::SerialWorker(QObject *parent)
connect(m_port, &QSerialPort::readyRead, this, &SerialWorker::onReadyRead); connect(m_port, &QSerialPort::readyRead, this, &SerialWorker::onReadyRead);
connect(m_port, &QSerialPort::errorOccurred, this, &SerialWorker::onPortError); connect(m_port, &QSerialPort::errorOccurred, this, &SerialWorker::onPortError);
connect(m_socket, &QTcpSocket::readyRead, this, &SerialWorker::onReadyRead);
connect(m_socket, &QTcpSocket::connected, this, &SerialWorker::onSocketConnected);
connect(m_socket, &QTcpSocket::disconnected, this, &SerialWorker::onSocketDisconnected);
connect(m_socket, &QTcpSocket::errorOccurred, this, &SerialWorker::onSocketError);
} }
SerialWorker::~SerialWorker() SerialWorker::~SerialWorker()
@@ -32,6 +38,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
QSerialPort::StopBits stopBits, QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl) QSerialPort::FlowControl flowControl)
{ {
m_mode = ConnectionMode::Serial;
m_portName = portName; m_portName = portName;
m_baudRate = baudRate; m_baudRate = baudRate;
m_dataBits = dataBits; m_dataBits = dataBits;
@@ -41,6 +48,10 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
m_userDisconnected = false; m_userDisconnected = false;
m_reconnectAttempt = 0; m_reconnectAttempt = 0;
// Make sure any previous connection (of either transport) is torn down
// before switching to serial mode.
if (m_socket->state() != QAbstractSocket::UnconnectedState)
m_socket->abort();
if (m_port->isOpen()) if (m_port->isOpen())
m_port->close(); m_port->close();
@@ -57,6 +68,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
scheduleReconnect(); scheduleReconnect();
return; return;
} }
m_device = m_port;
m_idleFlushTimer->stop(); m_idleFlushTimer->stop();
m_buffer.clear(); m_buffer.clear();
m_scanTail.clear(); m_scanTail.clear();
@@ -64,15 +76,54 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
emit portOpened(); emit portOpened();
} }
void SerialWorker::openNetwork(const QString &host, quint16 port)
{
m_mode = ConnectionMode::Network;
m_networkHost = host;
m_networkPort = port;
m_userDisconnected = false;
m_reconnectAttempt = 0;
// Make sure any previous connection (of either transport) is torn down
// before switching to network mode.
if (m_port->isOpen())
m_port->close();
if (m_socket->state() != QAbstractSocket::UnconnectedState)
m_socket->abort();
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
// connectToHost() is asynchronous - the outcome (success or failure)
// arrives later via the connected()/errorOccurred() signals, handled
// in onSocketConnected()/onSocketError() below.
m_socket->connectToHost(host, port);
}
void SerialWorker::closePort() void SerialWorker::closePort()
{ {
m_userDisconnected = true; m_userDisconnected = true;
m_reconnectTimer->stop(); m_reconnectTimer->stop();
m_idleFlushTimer->stop(); m_idleFlushTimer->stop();
bool wasOpen = false;
if (m_port && m_port->isOpen()) { if (m_port && m_port->isOpen()) {
m_port->close(); m_port->close();
emit portClosed(); wasOpen = true;
} }
if (m_socket && m_socket->state() != QAbstractSocket::UnconnectedState) {
// abort() tears the connection down immediately (no graceful
// shutdown handshake needed for a debug/monitor connection like
// this one) and reliably triggers disconnected() right away.
m_socket->abort();
wasOpen = true;
}
m_device = nullptr;
if (wasOpen)
emit portClosed();
stopLogging(); stopLogging();
} }
@@ -117,11 +168,14 @@ void SerialWorker::stopLogging()
void SerialWorker::onReadyRead() void SerialWorker::onReadyRead()
{ {
processRawData(m_port->readAll()); if (m_device)
processRawData(m_device->readAll());
} }
void SerialWorker::onPortError(QSerialPort::SerialPortError err) void SerialWorker::onPortError(QSerialPort::SerialPortError err)
{ {
if (m_mode != ConnectionMode::Serial)
return;
if (err == QSerialPort::NoError) if (err == QSerialPort::NoError)
return; return;
@@ -130,6 +184,7 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
if (fatal) { if (fatal) {
m_port->close(); m_port->close();
m_device = nullptr;
emit portClosed(); emit portClosed();
if (m_autoReconnect && !m_userDisconnected) { if (m_autoReconnect && !m_userDisconnected) {
scheduleReconnect(); scheduleReconnect();
@@ -139,6 +194,52 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
} }
} }
void SerialWorker::onSocketConnected()
{
if (m_mode != ConnectionMode::Network)
return;
m_device = m_socket;
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0;
emit portOpened();
}
void SerialWorker::onSocketDisconnected()
{
if (m_mode != ConnectionMode::Network)
return;
m_device = nullptr;
// If this disconnect was requested by us (closePort()), that function
// has already emitted portClosed() and handles all cleanup - nothing
// more to do here, and reconnecting would be wrong.
if (m_userDisconnected)
return;
emit portClosed();
if (m_autoReconnect)
scheduleReconnect();
}
void SerialWorker::onSocketError(QAbstractSocket::SocketError)
{
if (m_mode != ConnectionMode::Network)
return;
const QString msg = m_socket->errorString();
m_device = nullptr;
if (m_autoReconnect && !m_userDisconnected) {
scheduleReconnect();
} else {
emit errorOccurred(msg);
}
}
void SerialWorker::tryReconnect() void SerialWorker::tryReconnect()
{ {
if (m_userDisconnected) if (m_userDisconnected)
@@ -147,6 +248,7 @@ void SerialWorker::tryReconnect()
++m_reconnectAttempt; ++m_reconnectAttempt;
emit reconnecting(m_reconnectAttempt); emit reconnecting(m_reconnectAttempt);
if (m_mode == ConnectionMode::Serial) {
m_port->setPortName(m_portName); m_port->setPortName(m_portName);
m_port->setBaudRate(m_baudRate); m_port->setBaudRate(m_baudRate);
m_port->setDataBits(m_dataBits); m_port->setDataBits(m_dataBits);
@@ -155,6 +257,7 @@ void SerialWorker::tryReconnect()
m_port->setFlowControl(m_flowControl); m_port->setFlowControl(m_flowControl);
if (m_port->open(QIODevice::ReadOnly)) { if (m_port->open(QIODevice::ReadOnly)) {
m_device = m_port;
m_idleFlushTimer->stop(); m_idleFlushTimer->stop();
m_buffer.clear(); m_buffer.clear();
m_scanTail.clear(); m_scanTail.clear();
@@ -163,6 +266,13 @@ void SerialWorker::tryReconnect()
} else { } else {
scheduleReconnect(); scheduleReconnect();
} }
} else {
// Network: connectToHost() is asynchronous. Success/failure is
// reported later via onSocketConnected()/onSocketError(), which
// either finish this attempt or schedule the next one.
m_socket->abort();
m_socket->connectToHost(m_networkHost, m_networkPort);
}
} }
// ── private helpers ───────────────────────────────────────────────────────── // ── private helpers ─────────────────────────────────────────────────────────

View File

@@ -7,6 +7,7 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <QMetaObject> #include <QMetaObject>
#include <QResizeEvent> #include <QResizeEvent>
#include <QMessageBox>
static QStringList enumerateVideoDevices() static QStringList enumerateVideoDevices()
{ {
@@ -40,12 +41,16 @@ VideoWidget::VideoWidget(QWidget *parent)
m_running = false; m_running = false;
m_startBtn->setText(tr("▶ Start")); m_startBtn->setText(tr("▶ Start"));
m_infoLabel->setText(tr("Stopped")); m_infoLabel->setText(tr("Stopped"));
if (m_recording)
stopRecording();
update(); update();
}); });
connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) { connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) {
m_infoLabel->setText(tr("Error: %1").arg(msg)); m_infoLabel->setText(tr("Error: %1").arg(msg));
m_running = false; m_running = false;
m_startBtn->setText(tr("▶ Start")); m_startBtn->setText(tr("▶ Start"));
if (m_recording)
stopRecording();
}); });
m_thread->start(); m_thread->start();
@@ -57,6 +62,9 @@ void VideoWidget::shutdown()
return; return;
m_shutdownDone = true; m_shutdownDone = true;
if (m_recording)
stopRecording();
m_worker->stopCapture(); m_worker->stopCapture();
if (m_thread && m_thread->isRunning()) { if (m_thread && m_thread->isRunning()) {
@@ -104,11 +112,18 @@ void VideoWidget::setupUi()
m_screenshotBtn->setMaximumWidth(32); m_screenshotBtn->setMaximumWidth(32);
connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot); connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot);
m_recordBtn = new QPushButton(tr("⏺ Record"), this);
m_recordBtn->setCheckable(true);
m_recordBtn->setMaximumWidth(80);
m_recordBtn->setToolTip(tr("Record the live preview to an MP4 file (requires ffmpeg)"));
connect(m_recordBtn, &QPushButton::toggled, this, &VideoWidget::onRecordToggled);
ctrlRow->addWidget(m_deviceCombo, 1); ctrlRow->addWidget(m_deviceCombo, 1);
ctrlRow->addWidget(refreshBtn); ctrlRow->addWidget(refreshBtn);
ctrlRow->addWidget(m_startBtn); ctrlRow->addWidget(m_startBtn);
ctrlRow->addWidget(m_freezeBtn); ctrlRow->addWidget(m_freezeBtn);
ctrlRow->addWidget(m_screenshotBtn); ctrlRow->addWidget(m_screenshotBtn);
ctrlRow->addWidget(m_recordBtn);
layout->addLayout(ctrlRow); layout->addLayout(ctrlRow);
m_infoLabel = new QLabel(tr("No device started"), this); m_infoLabel = new QLabel(tr("No device started"), this);
@@ -151,6 +166,14 @@ void VideoWidget::onScreenshot()
void VideoWidget::onNewFrame(const QImage &frame) void VideoWidget::onNewFrame(const QImage &frame)
{ {
// Feed the recorder regardless of freeze state, so pausing the preview
// doesn't also pause/corrupt the recording - recording tracks the real
// incoming stream, not what's currently shown on screen.
if (m_recording && m_recordProcess &&
m_recordProcess->state() == QProcess::Running) {
writeRecordingFrame(frame);
}
if (m_frozen) if (m_frozen)
return; return;
m_frame = frame; m_frame = frame;
@@ -237,3 +260,134 @@ void VideoWidget::resizeEvent(QResizeEvent *event)
QWidget::resizeEvent(event); QWidget::resizeEvent(event);
updateScaled(); updateScaled();
} }
// ── Recording (raw frames piped into ffmpeg, encoded to MP4) ──────────────
void VideoWidget::onRecordToggled(bool checked)
{
if (checked)
startRecording();
else
stopRecording();
}
void VideoWidget::startRecording()
{
if (m_frame.isNull()) {
QMessageBox::warning(this, tr("No signal"),
tr("Start the video capture first before recording."));
m_recordBtn->setChecked(false);
return;
}
const QString defaultName =
QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)
+ "/uartscope_"
+ QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")
+ ".mp4";
const QString path = QFileDialog::getSaveFileName(
this, tr("Save recording as"), defaultName,
tr("MP4 Video (*.mp4)"));
if (path.isEmpty()) {
m_recordBtn->setChecked(false);
return;
}
// Frames are piped in as raw BGRA (matching QImage::Format_RGB32's
// in-memory byte layout on little-endian systems) at whatever
// resolution capture is currently running at; a resolution change
// mid-recording would corrupt ffmpeg's raw input stream, so
// writeRecordingFrame() watches for that and stops the recording if
// it happens (see there).
m_recordSize = m_frame.size();
m_recordProcess = new QProcess(this);
connect(m_recordProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
m_infoLabel->setText(tr("Recording error: %1").arg(m_recordProcess->errorString()));
stopRecording();
});
connect(m_recordProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this](int exitCode, QProcess::ExitStatus) {
if (exitCode != 0 && m_recordProcess)
m_infoLabel->setText(tr("ffmpeg exited with code %1").arg(exitCode));
if (m_recordProcess) {
m_recordProcess->deleteLater();
m_recordProcess = nullptr;
}
});
// -use_wallclock_as_timestamps: frames don't arrive at a strictly
// constant rate (V4L2 delivers whatever the device/driver produces),
// so we let ffmpeg timestamp each frame using the system clock at the
// moment it's read from the pipe rather than assuming a fixed
// framerate. That keeps the resulting MP4's real-time duration
// correct even if the capture rate varies or briefly stalls.
const QStringList args = {
"-y",
"-f", "rawvideo",
"-pixel_format", "bgra",
"-video_size", QStringLiteral("%1x%2").arg(m_recordSize.width()).arg(m_recordSize.height()),
"-use_wallclock_as_timestamps", "1",
"-i", "-",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-preset", "veryfast",
"-movflags", "+faststart",
path
};
m_recordProcess->start("ffmpeg", args);
if (!m_recordProcess->waitForStarted(3000)) {
QMessageBox::critical(this, tr("Recording failed"),
tr("Could not start ffmpeg. Is it installed and available on PATH?"));
m_recordProcess->deleteLater();
m_recordProcess = nullptr;
m_recordBtn->setChecked(false);
return;
}
m_recording = true;
m_recordBtn->setText(tr("⏹ Stop"));
m_infoLabel->setText(tr("Recording to %1").arg(path));
}
void VideoWidget::stopRecording()
{
m_recording = false;
m_recordBtn->setChecked(false);
m_recordBtn->setText(tr("⏺ Record"));
if (m_recordProcess) {
// Closing stdin signals ffmpeg there's no more input; it then
// finishes encoding/muxing and exits on its own. The finished()
// handler connected in startRecording() cleans the QProcess up
// once that happens.
m_recordProcess->closeWriteChannel();
if (!m_recordProcess->waitForFinished(3000))
m_recordProcess->terminate();
}
}
void VideoWidget::writeRecordingFrame(const QImage &frame)
{
if (frame.size() != m_recordSize) {
// The capture device changed resolution mid-recording (shouldn't
// normally happen, but e.g. a device re-negotiating format could
// trigger it). Feeding ffmpeg mismatched frame sizes on a raw pipe
// would desync/corrupt the output, so stop cleanly instead.
stopRecording();
m_infoLabel->setText(tr("Recording stopped: frame size changed"));
return;
}
// Format_RGB32 stores each pixel as 0xffRRGGBB in host byte order,
// which on little-endian systems (the overwhelming majority of
// desktop/embedded targets) is byte-for-byte BGRA - matching the
// "-pixel_format bgra" we told ffmpeg to expect above.
const QImage bgra = frame.convertToFormat(QImage::Format_RGB32);
m_recordProcess->write(reinterpret_cast<const char *>(bgra.constBits()),
static_cast<qint64>(bgra.sizeInBytes()));
}

View File

@@ -1,6 +1,6 @@
# Maintainer: diabolus <your@email.com> # Maintainer: diabolus <your@email.com>
pkgname=uartscope pkgname=uartscope
pkgver=1.0.0.r5.g1422efd pkgver=1.0.0.r6.ga942f83
pkgrel=1 pkgrel=1
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect" pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -3,12 +3,12 @@
pkgname = uartscope pkgname = uartscope
pkgbase = uartscope pkgbase = uartscope
xdata = pkgtype=pkg xdata = pkgtype=pkg
pkgver = 1.0.0.r5.g1422efd-1 pkgver = 1.0.0.r6.ga942f83-1
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
url = https://git.projekt-hirnfrei.de/diabolus/uartscope url = https://git.projekt-hirnfrei.de/diabolus/uartscope
builddate = 1786450603 builddate = 1787005573
packager = Unknown Packager packager = Unknown Packager
size = 397937 size = 406129
arch = x86_64 arch = x86_64
license = MIT license = MIT
conflict = uartscope conflict = uartscope

Submodule uartscope-git/src/uartscope updated: 1422efdc2f...a942f8385b

View File

@@ -1,2 +1,2 @@
1422efdc2f14d71edf3c7accec15966f5aa81c07 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope a942f8385b8b05f5ec9a097ebbb3ba61ad0690f6 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
cc102c93eb17f7b910d8e74c3505f198bed77f10 not-for-merge tag 'v1.0.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope cc102c93eb17f7b910d8e74c3505f198bed77f10 not-for-merge tag 'v1.0.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope