- Grafische Ausgabe kann nun als Video aufgezeichnet werden
- Verbindung über TCP/IP
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
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_REQUIRED ON)
|
||||
@@ -12,6 +12,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
Gui
|
||||
Widgets
|
||||
SerialPort
|
||||
Network
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
@@ -49,6 +50,7 @@ target_link_libraries(uartscope PRIVATE
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::SerialPort
|
||||
Qt6::Network
|
||||
)
|
||||
|
||||
# V4L2 uses Linux kernel headers directly (linux/videodev2.h)
|
||||
|
||||
@@ -4,16 +4,31 @@
|
||||
#include <QLineEdit>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QTabWidget>
|
||||
#include <QSpinBox>
|
||||
#include <QSerialPort>
|
||||
#include <QSerialPortInfo>
|
||||
|
||||
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;
|
||||
qint32 baudRate = 115200;
|
||||
QSerialPort::DataBits dataBits = QSerialPort::Data8;
|
||||
QSerialPort::Parity parity = QSerialPort::NoParity;
|
||||
QSerialPort::StopBits stopBits = QSerialPort::OneStop;
|
||||
QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl;
|
||||
|
||||
// Network-specific
|
||||
QString networkHost;
|
||||
quint16 networkPort = 0;
|
||||
|
||||
// Common
|
||||
QString logFilePath;
|
||||
};
|
||||
|
||||
@@ -32,11 +47,17 @@ private slots:
|
||||
private:
|
||||
void addPtyPorts();
|
||||
|
||||
QTabWidget *m_modeTabs = nullptr;
|
||||
|
||||
QComboBox *m_portCombo = nullptr;
|
||||
QComboBox *m_baudCombo = nullptr;
|
||||
QComboBox *m_dataBitsCombo = nullptr;
|
||||
QComboBox *m_parityCombo = nullptr;
|
||||
QComboBox *m_stopBitsCombo = nullptr;
|
||||
QComboBox *m_flowCombo = nullptr;
|
||||
|
||||
QLineEdit *m_hostEdit = nullptr;
|
||||
QSpinBox *m_portSpin = nullptr;
|
||||
|
||||
QLineEdit *m_logPathEdit = nullptr;
|
||||
};
|
||||
|
||||
@@ -2,17 +2,27 @@
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QSerialPort>
|
||||
#include <QTcpSocket>
|
||||
#include <QAbstractSocket>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QString>
|
||||
#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,
|
||||
// tagDetected() when a line contains a recognised tag like [WDG],
|
||||
// and clearScreen() when an ANSI clear-screen sequence is received.
|
||||
// Auto-reconnect: if the port drops unexpectedly, the worker retries
|
||||
// every reconnectIntervalMs until success or closePort().
|
||||
// Auto-reconnect: if the connection drops unexpectedly, the worker
|
||||
// retries every reconnectIntervalMs until success or closePort().
|
||||
class SerialWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -30,6 +40,7 @@ public slots:
|
||||
QSerialPort::Parity parity,
|
||||
QSerialPort::StopBits stopBits,
|
||||
QSerialPort::FlowControl flowControl);
|
||||
void openNetwork(const QString &host, quint16 port);
|
||||
void closePort();
|
||||
void setLogFile(const QString &path);
|
||||
void stopLogging();
|
||||
@@ -46,16 +57,28 @@ signals:
|
||||
private slots:
|
||||
void onReadyRead();
|
||||
void onPortError(QSerialPort::SerialPortError err);
|
||||
void onSocketConnected();
|
||||
void onSocketDisconnected();
|
||||
void onSocketError(QAbstractSocket::SocketError err);
|
||||
void tryReconnect();
|
||||
void flushScanTail();
|
||||
|
||||
private:
|
||||
enum class ConnectionMode { Serial, Network };
|
||||
|
||||
void processRawData(const QByteArray &data);
|
||||
void appendToLineBuffer(const QByteArray &toProcess);
|
||||
void processLine(const QString &line);
|
||||
void scheduleReconnect();
|
||||
|
||||
ConnectionMode m_mode = ConnectionMode::Serial;
|
||||
|
||||
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;
|
||||
QTextStream *m_logStream = nullptr;
|
||||
QString m_buffer;
|
||||
@@ -77,10 +100,15 @@ private:
|
||||
int m_reconnectIntervalMs = 2000;
|
||||
int m_reconnectAttempt = 0;
|
||||
|
||||
// Serial-specific connection parameters
|
||||
QString m_portName;
|
||||
qint32 m_baudRate = 115200;
|
||||
QSerialPort::DataBits m_dataBits = QSerialPort::Data8;
|
||||
QSerialPort::Parity m_parity = QSerialPort::NoParity;
|
||||
QSerialPort::StopBits m_stopBits = QSerialPort::OneStop;
|
||||
QSerialPort::FlowControl m_flowControl = QSerialPort::NoFlowControl;
|
||||
|
||||
// Network-specific connection parameters
|
||||
QString m_networkHost;
|
||||
quint16 m_networkPort = 0;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
#pragma once
|
||||
#include <QWidget>
|
||||
#include <QImage>
|
||||
#include <QSize>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QThread>
|
||||
#include <QProcess>
|
||||
|
||||
#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
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -29,6 +36,7 @@ private slots:
|
||||
void onStartStop();
|
||||
void onFreeze(bool frozen);
|
||||
void onScreenshot();
|
||||
void onRecordToggled(bool checked);
|
||||
void refreshDeviceList();
|
||||
|
||||
protected:
|
||||
@@ -40,6 +48,9 @@ private:
|
||||
void startCapture();
|
||||
void stopCapture();
|
||||
void updateScaled();
|
||||
void startRecording();
|
||||
void stopRecording();
|
||||
void writeRecordingFrame(const QImage &frame);
|
||||
|
||||
QThread *m_thread = nullptr;
|
||||
V4l2Worker *m_worker = nullptr;
|
||||
@@ -54,5 +65,15 @@ private:
|
||||
QPushButton *m_startBtn = nullptr;
|
||||
QPushButton *m_freezeBtn = nullptr;
|
||||
QPushButton *m_screenshotBtn = nullptr;
|
||||
QPushButton *m_recordBtn = 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;
|
||||
};
|
||||
|
||||
@@ -10,24 +10,28 @@
|
||||
ConnectDialog::ConnectDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Connect to UART"));
|
||||
setMinimumWidth(360);
|
||||
setWindowTitle(tr("Connect to UARTScope input"));
|
||||
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->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
auto *portRow = new QHBoxLayout();
|
||||
m_portCombo = new QComboBox(this);
|
||||
m_portCombo = new QComboBox(serialTab);
|
||||
m_portCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
auto *refreshBtn = new QPushButton(tr("↻"), this);
|
||||
auto *refreshBtn = new QPushButton(tr("↻"), serialTab);
|
||||
refreshBtn->setMaximumWidth(30);
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &ConnectDialog::refreshPorts);
|
||||
portRow->addWidget(m_portCombo, 1);
|
||||
portRow->addWidget(refreshBtn);
|
||||
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,
|
||||
57600, 115200, 230400, 460800, 921600};
|
||||
for (int b : bauds)
|
||||
@@ -35,7 +39,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
|
||||
m_baudCombo->setCurrentText("115200");
|
||||
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("6", QSerialPort::Data6);
|
||||
m_dataBitsCombo->addItem("7", QSerialPort::Data7);
|
||||
@@ -43,7 +47,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
|
||||
m_dataBitsCombo->setCurrentText("8");
|
||||
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("Even"), QSerialPort::EvenParity);
|
||||
m_parityCombo->addItem(tr("Odd"), QSerialPort::OddParity);
|
||||
@@ -51,18 +55,52 @@ ConnectDialog::ConnectDialog(QWidget *parent)
|
||||
m_parityCombo->addItem(tr("Mark"), QSerialPort::MarkParity);
|
||||
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.5", QSerialPort::OneAndHalfStop);
|
||||
m_stopBitsCombo->addItem("2", QSerialPort::TwoStop);
|
||||
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("RTS/CTS"), QSerialPort::HardwareControl);
|
||||
m_flowCombo->addItem(tr("XON/XOFF"), QSerialPort::SoftwareControl);
|
||||
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();
|
||||
m_logPathEdit = new QLineEdit(this);
|
||||
m_logPathEdit->setPlaceholderText(tr("(optional) path/to/output.log"));
|
||||
@@ -71,7 +109,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
|
||||
connect(browseBtn, &QPushButton::clicked, this, &ConnectDialog::browseLogFile);
|
||||
logRow->addWidget(m_logPathEdit, 1);
|
||||
logRow->addWidget(browseBtn);
|
||||
form->addRow(tr("Log file:"), logRow);
|
||||
logForm->addRow(tr("Log file:"), logRow);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
@@ -79,7 +117,8 @@ ConnectDialog::ConnectDialog(QWidget *parent)
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addLayout(form);
|
||||
mainLayout->addWidget(m_modeTabs);
|
||||
mainLayout->addLayout(logForm);
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
refreshPorts();
|
||||
@@ -146,12 +185,20 @@ void ConnectDialog::browseLogFile()
|
||||
SerialConfig ConnectDialog::config() const
|
||||
{
|
||||
SerialConfig cfg;
|
||||
cfg.connectionType = (m_modeTabs->currentIndex() == 1)
|
||||
? SerialConfig::ConnectionType::Network
|
||||
: SerialConfig::ConnectionType::Serial;
|
||||
|
||||
cfg.portName = m_portCombo->currentData().toString();
|
||||
cfg.baudRate = m_baudCombo->currentData().toInt();
|
||||
cfg.dataBits = static_cast<QSerialPort::DataBits>(m_dataBitsCombo->currentData().toInt());
|
||||
cfg.parity = static_cast<QSerialPort::Parity>(m_parityCombo->currentData().toInt());
|
||||
cfg.stopBits = static_cast<QSerialPort::StopBits>(m_stopBitsCombo->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();
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("UARTScope");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setApplicationVersion("1.1.0");
|
||||
app.setOrganizationName("ChicaDev");
|
||||
|
||||
app.setStyle(QStyleFactory::create("Fusion"));
|
||||
|
||||
@@ -117,6 +117,7 @@ void MainWindow::setupUi()
|
||||
void MainWindow::setupToolBar()
|
||||
{
|
||||
auto *tb = addToolBar(tr("Main"));
|
||||
tb->setObjectName("mainToolBar"); // required by QMainWindow::saveState()
|
||||
tb->setMovable(false);
|
||||
|
||||
m_connectAction = tb->addAction(tr("Connect…"));
|
||||
@@ -199,10 +200,18 @@ void MainWindow::onConnectClicked()
|
||||
return;
|
||||
|
||||
m_lastConfig = dlg.config();
|
||||
|
||||
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Serial) {
|
||||
if (m_lastConfig.portName.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("No port"), tr("Please select a valid serial port."));
|
||||
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())
|
||||
QMetaObject::invokeMethod(m_worker, "setLogFile",
|
||||
@@ -216,6 +225,7 @@ void MainWindow::onConnectClicked()
|
||||
m_worker->setReconnectInterval(interval);
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Serial) {
|
||||
QMetaObject::invokeMethod(m_worker, "openPort",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QString, m_lastConfig.portName),
|
||||
@@ -224,6 +234,12 @@ void MainWindow::onConnectClicked()
|
||||
Q_ARG(QSerialPort::Parity, m_lastConfig.parity),
|
||||
Q_ARG(QSerialPort::StopBits, m_lastConfig.stopBits),
|
||||
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()
|
||||
@@ -238,10 +254,17 @@ void MainWindow::onPortOpened()
|
||||
m_stateLabel->setText(tr("● Connected"));
|
||||
m_stateLabel->setStyleSheet("color: #4ec94e;");
|
||||
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(
|
||||
QStringLiteral("%1 @ %2 baud")
|
||||
.arg(m_lastConfig.portName)
|
||||
.arg(m_lastConfig.baudRate));
|
||||
}
|
||||
statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
@@ -299,12 +322,15 @@ void MainWindow::clearAllViews()
|
||||
|
||||
void MainWindow::showFormatReference()
|
||||
{
|
||||
static const QString referenceText = R"(
|
||||
static const QString referenceText = R"UARTSCOPE(
|
||||
# UARTScope – Output Format Reference
|
||||
# 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
|
||||
three parallel ways. You can use any combination.
|
||||
UARTScope reads from either a serial UART port OR a plain TCP connection
|
||||
(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)
|
||||
@@ -397,7 +423,7 @@ void uart_status_update(void) {
|
||||
|
||||
// Clear screen from firmware when you want a fresh start:
|
||||
// uart_printf("\033[2J\033[H");
|
||||
)";
|
||||
)UARTSCOPE";
|
||||
|
||||
auto *dlg = new QDialog(this);
|
||||
dlg->setWindowTitle(tr("UARTScope – Format Reference"));
|
||||
@@ -546,7 +572,7 @@ void MainWindow::showAbout()
|
||||
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(2, tr("Projekt"), "Projekt Hirnfrei");
|
||||
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
SerialWorker::SerialWorker(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_port(new QSerialPort(this))
|
||||
, m_socket(new QTcpSocket(this))
|
||||
, m_idleFlushTimer(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::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()
|
||||
@@ -32,6 +38,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
|
||||
QSerialPort::StopBits stopBits,
|
||||
QSerialPort::FlowControl flowControl)
|
||||
{
|
||||
m_mode = ConnectionMode::Serial;
|
||||
m_portName = portName;
|
||||
m_baudRate = baudRate;
|
||||
m_dataBits = dataBits;
|
||||
@@ -41,6 +48,10 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
|
||||
m_userDisconnected = false;
|
||||
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())
|
||||
m_port->close();
|
||||
|
||||
@@ -57,6 +68,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
m_device = m_port;
|
||||
m_idleFlushTimer->stop();
|
||||
m_buffer.clear();
|
||||
m_scanTail.clear();
|
||||
@@ -64,15 +76,54 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
|
||||
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()
|
||||
{
|
||||
m_userDisconnected = true;
|
||||
m_reconnectTimer->stop();
|
||||
m_idleFlushTimer->stop();
|
||||
|
||||
bool wasOpen = false;
|
||||
if (m_port && m_port->isOpen()) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -117,11 +168,14 @@ void SerialWorker::stopLogging()
|
||||
|
||||
void SerialWorker::onReadyRead()
|
||||
{
|
||||
processRawData(m_port->readAll());
|
||||
if (m_device)
|
||||
processRawData(m_device->readAll());
|
||||
}
|
||||
|
||||
void SerialWorker::onPortError(QSerialPort::SerialPortError err)
|
||||
{
|
||||
if (m_mode != ConnectionMode::Serial)
|
||||
return;
|
||||
if (err == QSerialPort::NoError)
|
||||
return;
|
||||
|
||||
@@ -130,6 +184,7 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
|
||||
|
||||
if (fatal) {
|
||||
m_port->close();
|
||||
m_device = nullptr;
|
||||
emit portClosed();
|
||||
if (m_autoReconnect && !m_userDisconnected) {
|
||||
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()
|
||||
{
|
||||
if (m_userDisconnected)
|
||||
@@ -147,6 +248,7 @@ void SerialWorker::tryReconnect()
|
||||
++m_reconnectAttempt;
|
||||
emit reconnecting(m_reconnectAttempt);
|
||||
|
||||
if (m_mode == ConnectionMode::Serial) {
|
||||
m_port->setPortName(m_portName);
|
||||
m_port->setBaudRate(m_baudRate);
|
||||
m_port->setDataBits(m_dataBits);
|
||||
@@ -155,6 +257,7 @@ void SerialWorker::tryReconnect()
|
||||
m_port->setFlowControl(m_flowControl);
|
||||
|
||||
if (m_port->open(QIODevice::ReadOnly)) {
|
||||
m_device = m_port;
|
||||
m_idleFlushTimer->stop();
|
||||
m_buffer.clear();
|
||||
m_scanTail.clear();
|
||||
@@ -163,6 +266,13 @@ void SerialWorker::tryReconnect()
|
||||
} else {
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QMetaObject>
|
||||
#include <QResizeEvent>
|
||||
#include <QMessageBox>
|
||||
|
||||
static QStringList enumerateVideoDevices()
|
||||
{
|
||||
@@ -40,12 +41,16 @@ VideoWidget::VideoWidget(QWidget *parent)
|
||||
m_running = false;
|
||||
m_startBtn->setText(tr("▶ Start"));
|
||||
m_infoLabel->setText(tr("Stopped"));
|
||||
if (m_recording)
|
||||
stopRecording();
|
||||
update();
|
||||
});
|
||||
connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) {
|
||||
m_infoLabel->setText(tr("Error: %1").arg(msg));
|
||||
m_running = false;
|
||||
m_startBtn->setText(tr("▶ Start"));
|
||||
if (m_recording)
|
||||
stopRecording();
|
||||
});
|
||||
|
||||
m_thread->start();
|
||||
@@ -57,6 +62,9 @@ void VideoWidget::shutdown()
|
||||
return;
|
||||
m_shutdownDone = true;
|
||||
|
||||
if (m_recording)
|
||||
stopRecording();
|
||||
|
||||
m_worker->stopCapture();
|
||||
|
||||
if (m_thread && m_thread->isRunning()) {
|
||||
@@ -104,11 +112,18 @@ void VideoWidget::setupUi()
|
||||
m_screenshotBtn->setMaximumWidth(32);
|
||||
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(refreshBtn);
|
||||
ctrlRow->addWidget(m_startBtn);
|
||||
ctrlRow->addWidget(m_freezeBtn);
|
||||
ctrlRow->addWidget(m_screenshotBtn);
|
||||
ctrlRow->addWidget(m_recordBtn);
|
||||
layout->addLayout(ctrlRow);
|
||||
|
||||
m_infoLabel = new QLabel(tr("No device started"), this);
|
||||
@@ -151,6 +166,14 @@ void VideoWidget::onScreenshot()
|
||||
|
||||
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)
|
||||
return;
|
||||
m_frame = frame;
|
||||
@@ -237,3 +260,134 @@ void VideoWidget::resizeEvent(QResizeEvent *event)
|
||||
QWidget::resizeEvent(event);
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: diabolus <your@email.com>
|
||||
pkgname=uartscope
|
||||
pkgver=1.0.0.r5.g1422efd
|
||||
pkgver=1.0.0.r6.ga942f83
|
||||
pkgrel=1
|
||||
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -3,12 +3,12 @@
|
||||
pkgname = uartscope
|
||||
pkgbase = uartscope
|
||||
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
|
||||
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||
builddate = 1786450603
|
||||
builddate = 1787005573
|
||||
packager = Unknown Packager
|
||||
size = 397937
|
||||
size = 406129
|
||||
arch = x86_64
|
||||
license = MIT
|
||||
conflict = uartscope
|
||||
|
||||
Binary file not shown.
Submodule uartscope-git/src/uartscope updated: 1422efdc2f...a942f8385b
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user