- Auto-scroll standard aktiv

- Tag-Filter wird gespeichert
- Tag Monitor kann bei einem Tag jetzt pro Key mehrere Werte in einer Tabelle anzeigen
- Bug für Clear behoben
This commit is contained in:
2026-06-23 23:19:06 +02:00
parent 92168ee5dc
commit d3229c7b64
29 changed files with 342 additions and 380 deletions

View File

@@ -58,7 +58,6 @@ target_link_libraries(uartscope PRIVATE
# Installation
install(TARGETS uartscope DESTINATION bin)
# Optional: create a .desktop file for Linux app launchers
configure_file(
${CMAKE_SOURCE_DIR}/uartscope.desktop.in
${CMAKE_BINARY_DIR}/uartscope.desktop

View File

@@ -10,19 +10,21 @@ Ein moderner UART-Monitor für Linux mit Qt6-Oberfläche. Gebaut als vollwertige
|---|---|
| **Unbegrenzter Verlauf** | Kein Zeilenlimit keine Daten gehen verloren, egal wie viel der Pi sendet |
| **Timestamps** | Jede Zeile im Raw-View bekommt automatisch einen `hh:mm:ss.zzz`-Timestamp |
| **Auto-Scroll** | Standardmäßig aktiv springt automatisch ans Ende neuer Ausgaben |
| **H + V Scrolling** | Kein Zeilenumbruch, voller horizontaler Scrollbalken |
| **Live-Logging** | Alle empfangenen Zeilen werden mit Timestamp in eine Datei geschrieben |
| **Tag-Monitor** | Zeilen mit `[TAG]` werden in eigenen Panels angezeigt und aktualisiert |
| **Tag-Filter** | Tags können aus dem Raw-View ausgeblendet werden (nur im Tag-Monitor sichtbar) |
| **Listen-Werte** | `;`-getrennte Werte in einem Tag werden als mehrzeilige Liste dargestellt |
| **Tag-Filter** | Tags können aus dem Raw-View ausgeblendet werden wird dauerhaft gespeichert |
| **Tabellenansicht** | CSV-formatierte UART-Zeilen werden in einer Tabelle dargestellt |
| **Suche** | Inkrementelle Volltextsuche im Raw-View |
| **Auto-Reconnect** | Bei Verbindungsabbruch wird automatisch neu verbunden (Intervall einstellbar) |
| **ANSI Clear-Screen** | `\033[2J\033[H` aus der Firmware leert alle Views gleichzeitig |
| **ANSI Clear-Screen** | `\033[2J\033[H` aus der Firmware leert Raw-View, Tabelle, Tag-Monitor und Video-Vorschau gleichzeitig |
| **Copy-Buttons** | Raw-View und jedes Tag-Panel haben einen „📋 Copy"-Button |
| **V4L2 Live Video** | HDMI-Grabber direkt eingebunden Live-Vorschau unter dem Tag-Monitor |
| **Screenshot** | Aktuellen Frame des HDMI-Grabbers als PNG/JPG speichern |
| **Format-Referenz** | Eingebauter Guide (mit Kopieren-Button) für KI-kompatible UART-Formatierung |
| **Fensterlayout** | Fenstergröße, Position und alle Splitter-Positionen werden beim Beenden gespeichert |
| **Fensterlayout** | Fenstergröße, Position, Splitter-Positionen und Tag-Filter werden beim Beenden gespeichert |
---
@@ -86,34 +88,48 @@ v4l2-ctl -d /dev/video0 --list-formats-ext # unterstützte Formate & Auflösung
Jede UART-Zeile erscheint im Raw-View mit Timestamp. Keine besondere Formatierung nötig.
**Screen leeren** von der Firmware aus alle Views gleichzeitig leeren:
**Screen leeren** von der Firmware aus Raw-View, Tabelle, Tag-Monitor und Video-Vorschau gleichzeitig leeren:
```c
uart_printf("\033[2J\033[H");
```
### Tag-Monitor
Zeilen mit `[TAGNAME]` werden im Tag-Monitor-Panel angezeigt **und** im Raw-View hervorgehoben (cyan). Über „Tag filter…" in der Toolbar können Tags vollständig aus dem Raw-View ausgeblendet werden, sodass sie nur noch im Tag-Monitor erscheinen.
Zeilen mit `[TAGNAME]` werden im Tag-Monitor-Panel angezeigt **und** im Raw-View hervorgehoben (cyan), sofern der Tag nicht über den Tag-Filter ausgeblendet wurde.
**Format:** `[TAGNAME] key1=value1 key2=value2 ...`
- Tag-Name: Buchstaben, Ziffern, Underscore z.B. `WDG`, `VIDEO`, `I2C_BUS`
- Tag-Name: Buchstaben, Ziffern, Underscore z.B. `WDG`, `VIDEO`, `KICKSTART`
- Key=Value-Paare: Leerzeichen-getrennt, Werte ohne Leerzeichen
- Ohne Key=Value-Paare wird der rohe String angezeigt
**Listen-Werte:** Ein Wert mit `;`-getrennten Einträgen wird als mehrzeilige Liste unter dem Key dargestellt:
```c
uart_printf("[KICKSTART] Devs=trackdisk.device;input.device\n");
```
Im Tag-Monitor erscheint das als:
```
Devs trackdisk.device
input.device
```
Jeder weitere Eintrag bekommt seine eigene Tabellenzeile, der Key wird nur einmal angezeigt.
**Beispiele:**
```c
uart_printf("[WDG] uptime=%lu free_heap=%lu temp=%d\n",
HAL_GetTick(), xPortGetFreeHeapSize(), core_temp);
uart_printf("[KICKSTART] Libs=graphics.library;intuition.library;dos.library\n");
uart_printf("[VIDEO] line=%d hblank=%d vblank=%d copper=%d\n",
scanline, hblank_ticks, vblank_ticks, copper_dma);
uart_printf("[AUDIO] ch=%d buf=%d underruns=%lu\n",
active_channels, buffer_fill, underruns);
```
Jeder einzigartige Tag bekommt automatisch ein eigenes Panel. Panels aktualisieren sich in-place kein Scrollen nötig.
Jeder einzigartige Tag bekommt automatisch ein eigenes Panel. Panels aktualisieren sich in-place kein Scrollen nötig. Listen-Längen können sich von Update zu Update ändern, die Tabelle passt sich automatisch an.
### Tabellenansicht
@@ -129,6 +145,12 @@ Delimiter per Dropdown umschaltbar: `,` `;` `\t` `|` `Space`
---
## Tag-Filter
Über „Tag filter…" in der Toolbar lassen sich Tags aus dem Raw-View ausblenden, sodass sie nur noch im Tag-Monitor erscheinen. Ein Tag pro Zeile, ohne Klammern (z.B. `WDG`). Groß-/Kleinschreibung spielt keine Rolle. Der Filter wird sofort beim Schließen des Dialogs gespeichert und beim nächsten Programmstart automatisch wiederhergestellt.
---
## Einstellungen
Beim Beenden werden folgende Einstellungen automatisch gespeichert und beim nächsten Start wiederhergestellt:
@@ -148,14 +170,16 @@ Gespeichert unter `~/.config/ChicaDev/UARTScope.conf` (via `QSettings`).
uartscope/
├── CMakeLists.txt
├── README.md
├── resources.qrc
├── UartscopeLogo.png
├── uartscope.desktop.in
├── include/
│ ├── mainwindow.h ← Hauptfenster, koordiniert alle Komponenten
│ ├── serialworker.h ← UART-Empfang im eigenen QThread, Auto-Reconnect
│ ├── serialworker.h ← UART-Empfang im eigenen QThread, Auto-Reconnect, ANSI-Erkennung
│ ├── rawview.h ← Unbegrenzter Log mit Timestamps, Suche, Copy
│ ├── tableview.h ← CSV-Parser → QTableWidget
│ ├── tagwidget.h ← Container für Tag-Panels
│ ├── tagpanel.h ← Ein Panel pro [TAG], Key=Value-Tabelle, Copy
│ ├── tagpanel.h ← Ein Panel pro [TAG], Key=Value-Tabelle mit Listen-Support, Copy
│ ├── connectdialog.h ← Port-Konfiguration (Port, Baud, Log-Datei)
│ ├── v4l2worker.h ← V4L2-Capture in std::thread, MJPEG/YUYV/NV12
│ └── videowidget.h ← Live-Vorschau, Freeze, Screenshot

View File

@@ -14,7 +14,7 @@ struct SerialConfig {
QSerialPort::Parity parity = QSerialPort::NoParity;
QSerialPort::StopBits stopBits = QSerialPort::OneStop;
QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl;
QString logFilePath; // empty = no logging
QString logFilePath;
};
class ConnectDialog : public QDialog
@@ -23,7 +23,6 @@ class ConnectDialog : public QDialog
public:
explicit ConnectDialog(QWidget *parent = nullptr);
SerialConfig config() const;
private slots:

View File

@@ -5,13 +5,13 @@
#include <QSplitter>
#include <QThread>
#include <QTabWidget>
#include <QSplitter>
#include <QStatusBar>
#include <QToolBar>
#include <QLabel>
#include <QAction>
#include <QSpinBox>
#include <QCheckBox>
#include <QSet>
#include "serialworker.h"
#include "rawview.h"
@@ -19,7 +19,6 @@
#include "tagwidget.h"
#include "videowidget.h"
#include "connectdialog.h"
#include <QSet>
class MainWindow : public QMainWindow
{
@@ -29,6 +28,9 @@ public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void onConnectClicked();
void onDisconnectClicked();
@@ -43,9 +45,6 @@ private slots:
void configureTagFilter();
void showAbout();
protected:
void closeEvent(QCloseEvent *event) override;
private:
void setupUi();
void setupToolBar();
@@ -55,26 +54,21 @@ private:
void saveSettings();
void restoreSettings();
// Worker & thread
QThread *m_thread = nullptr;
SerialWorker *m_worker = nullptr;
// Views
RawView *m_rawView = nullptr;
TableView *m_tableView = nullptr;
TagWidget *m_tagWidget = nullptr;
VideoWidget *m_videoWidget= nullptr;
// Status bar widgets
QLabel *m_portLabel = nullptr;
QLabel *m_stateLabel = nullptr;
QLabel *m_reconnectLabel = nullptr;
// Toolbar widgets
QCheckBox *m_autoReconnectCb = nullptr;
QSpinBox *m_reconnectIntervalSb= nullptr;
QSpinBox *m_reconnectIntervalSb = nullptr;
// Actions
QAction *m_connectAction = nullptr;
QAction *m_disconnectAction = nullptr;

View File

@@ -7,14 +7,12 @@
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QSpinBox>
#include <QSet>
#include <QRegularExpression>
// RawView shows the raw UART output in a QPlainTextEdit with:
// - Timestamp prefix on every line (hh:mm:ss.zzz)
// - Configurable tag suppression (lines whose tag is in the filter set are hidden)
// - Unlimited history, H+V scrolling, incremental search
// - Copy-to-clipboard and Clear buttons
// RawView shows the raw UART output with timestamps, search, tag suppression,
// unlimited history, H+V scrolling and a copy-to-clipboard button.
// Auto-scroll is ON by default.
class RawView : public QWidget
{
Q_OBJECT
@@ -23,7 +21,6 @@ public:
explicit RawView(QWidget *parent = nullptr);
public slots:
// suppressedTags: set of uppercase tag names whose lines should NOT appear here
void appendLine(const QString &line, const QSet<QString> &suppressedTags = {});
void clear();
@@ -43,7 +40,7 @@ private:
QPushButton *m_copyBtn = nullptr;
QCheckBox *m_autoScrollCb = nullptr;
QLabel *m_lineCountLbl = nullptr;
bool m_autoScroll = true;
bool m_autoScroll = true; // ON by default
int m_lineCount = 0;
static const QRegularExpression s_tagRe;

View File

@@ -10,9 +10,9 @@
// SerialWorker lives in its own QThread and owns the QSerialPort.
// It emits newLine() for every complete line received,
// tagDetected() when a line contains a recognised tag like [WDG],
// and clearScreen() when the ANSI clear-screen sequence \033[2J is received.
// Auto-reconnect: if the port drops unexpectedly (e.g. USB cable pulled),
// the worker retries every reconnectIntervalMs until success or closePort().
// 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().
class SerialWorker : public QObject
{
Q_OBJECT
@@ -21,7 +21,6 @@ public:
explicit SerialWorker(QObject *parent = nullptr);
~SerialWorker();
// Auto-reconnect configuration (call before openPort, or any time)
void setAutoReconnect(bool enabled) { m_autoReconnect = enabled; }
void setReconnectInterval(int ms) { m_reconnectIntervalMs = ms; }
@@ -31,17 +30,17 @@ public slots:
QSerialPort::Parity parity,
QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl);
void closePort(); // explicit user disconnect stops reconnect
void closePort();
void setLogFile(const QString &path);
void stopLogging();
signals:
void newLine(const QString &line);
void tagDetected(const QString &tag, const QString &value);
void clearScreen(); // emitted when \033[2J\033[H (or \033[2J alone) is received
void clearScreen();
void portOpened();
void portClosed();
void reconnecting(int attempt); // fired each retry attempt
void reconnecting(int attempt);
void errorOccurred(const QString &message);
private slots:
@@ -58,19 +57,18 @@ private:
QFile *m_logFile = nullptr;
QTextStream *m_logStream = nullptr;
QString m_buffer;
QByteArray m_scanTail; // carries partial ANSI sequences across reads
// Reconnect state
QTimer *m_reconnectTimer = nullptr;
bool m_autoReconnect = true;
bool m_userDisconnected = false; // true only after explicit closePort()
bool m_userDisconnected = false;
int m_reconnectIntervalMs = 2000;
int m_reconnectAttempt = 0;
// Saved config for reconnect
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;
QSerialPort::FlowControl m_flowControl = QSerialPort::NoFlowControl;
};

View File

@@ -10,13 +10,6 @@
#include <QComboBox>
// TableView interprets UART lines as delimited data and shows them in a table.
// The first line that matches the column count is used as the header row if it
// starts with '#'. Otherwise column names are auto-generated (Col 1, Col 2, …).
//
// Supported delimiters: comma, semicolon, tab, pipe, space
// To use: send lines like:
// #time,temp,voltage,current
// 1234,23.5,3.3,0.42
class TableView : public QWidget
{
Q_OBJECT
@@ -44,7 +37,7 @@ private:
QCheckBox *m_autoScrollCb = nullptr;
QStringList m_headers;
QList<QStringList> m_rows; // raw data for rebuild
QList<QStringList> m_rows;
int m_maxRows = 500;
char m_delimiter = ',';
bool m_autoScroll = true;

View File

@@ -6,11 +6,16 @@
#include <QPushButton>
#include <QTableWidget>
#include <QStringList>
#include <QMap>
// TagPanel displays the most recent data for a single tag (e.g. [WDG]).
// Key=value pairs are shown in a table that updates in-place.
// A "Copy" button copies the current values to the clipboard.
// No timestamp is shown here timestamps belong in the Raw view.
// If a value contains ';', it is treated as a LIST: the key is shown once
// and each list element gets its own row below it (key cell left empty
// for the continuation rows, like a merged-cell look).
// Example: Devs=trackdisk.device;input.device
// Devs trackdisk.device
// input.device
class TagPanel : public QGroupBox
{
Q_OBJECT
@@ -29,10 +34,16 @@ private slots:
private:
void parseKeyValue(const QString &value);
void showRaw(const QString &value);
void ensureRow(const QString &key);
// Replaces all rows belonging to `key` with one row per value in `values`.
void setKeyValues(const QString &key, const QStringList &values);
QString m_tag;
QTableWidget *m_table = nullptr;
QLabel *m_rawLabel = nullptr;
QStringList m_keys;
// Track which table rows belong to which key, so repeated updates can
// replace the previous rows instead of appending duplicates.
// Maps key -> (first row index, row count)
struct KeyRange { int firstRow; int count; };
QMap<QString, KeyRange> m_keyRanges;
};

View File

@@ -4,7 +4,7 @@
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QMap>
#include <QString>

View File

@@ -24,8 +24,8 @@ signals:
void errorOccurred(const QString &message);
private:
void stop(); // joins m_captureThread, closes device safe from any thread
void captureLoop(); // runs inside m_captureThread
void stop();
void captureLoop();
bool openDevice(const QString &device, int width, int height);
void closeDevice();
bool initMmap();
@@ -45,5 +45,5 @@ private:
int m_nBuffers = 0;
std::atomic<bool> m_running{false};
std::thread m_captureThread; // dedicated capture thread not the Qt event thread
std::thread m_captureThread;
};

View File

@@ -7,18 +7,9 @@
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QThread>
#include <QTimer>
#include "v4l2worker.h"
// VideoWidget shows a live preview of a V4L2 capture device.
// It lives in the right-side panel, below the Tag Monitor.
// Features:
// - Device selector (auto-enumerates /dev/video*)
// - Start / Stop capture
// - Freeze-frame toggle
// - Screenshot (saves current frame as PNG/JPG)
// - Frame is scaled to fit the widget while keeping aspect ratio
class VideoWidget : public QWidget
{
Q_OBJECT
@@ -28,12 +19,11 @@ public:
~VideoWidget();
public:
// Called by MainWindow::closeEvent to guarantee orderly shutdown before
// widget destruction. Safe to call multiple times.
void shutdown();
void shutdown(); // safe to call multiple times, called from MainWindow::closeEvent
public slots:
void onNewFrame(const QImage &frame);
void clearFrame(); // blanks the preview (used on ANSI clear-screen)
private slots:
void onStartStop();
@@ -51,21 +41,18 @@ private:
void stopCapture();
void updateScaled();
// Worker
QThread *m_thread = nullptr;
V4l2Worker *m_worker = nullptr;
// Current frame
QImage m_frame;
QImage m_scaled;
bool m_frozen = false;
bool m_running = false;
bool m_shutdownDone = false;
// UI
QComboBox *m_deviceCombo = nullptr;
QPushButton *m_startBtn = nullptr;
QPushButton *m_freezeBtn = nullptr;
QPushButton *m_screenshotBtn= nullptr;
QPushButton *m_screenshotBtn = nullptr;
QLabel *m_infoLabel = nullptr;
};

View File

@@ -2,7 +2,6 @@
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGroupBox>
#include <QFileDialog>
#include <QLabel>
@@ -16,19 +15,16 @@ ConnectDialog::ConnectDialog(QWidget *parent)
form->setRowWrapPolicy(QFormLayout::DontWrapRows);
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
// ── Port ──────────────────────────────────────────────────────────────────
auto *portRow = new QHBoxLayout();
m_portCombo = new QComboBox(this);
m_portCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto *refreshBtn = new QPushButton(tr(""), this);
refreshBtn->setMaximumWidth(30);
refreshBtn->setToolTip(tr("Refresh port list"));
connect(refreshBtn, &QPushButton::clicked, this, &ConnectDialog::refreshPorts);
portRow->addWidget(m_portCombo, 1);
portRow->addWidget(refreshBtn);
form->addRow(tr("Port:"), portRow);
// ── Baud rate ─────────────────────────────────────────────────────────────
m_baudCombo = new QComboBox(this);
const QList<int> bauds = {1200, 2400, 4800, 9600, 19200, 38400,
57600, 115200, 230400, 460800, 921600};
@@ -37,7 +33,6 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_baudCombo->setCurrentText("115200");
form->addRow(tr("Baud rate:"), m_baudCombo);
// ── Data bits ─────────────────────────────────────────────────────────────
m_dataBitsCombo = new QComboBox(this);
m_dataBitsCombo->addItem("5", QSerialPort::Data5);
m_dataBitsCombo->addItem("6", QSerialPort::Data6);
@@ -46,7 +41,6 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_dataBitsCombo->setCurrentText("8");
form->addRow(tr("Data bits:"), m_dataBitsCombo);
// ── Parity ────────────────────────────────────────────────────────────────
m_parityCombo = new QComboBox(this);
m_parityCombo->addItem(tr("None"), QSerialPort::NoParity);
m_parityCombo->addItem(tr("Even"), QSerialPort::EvenParity);
@@ -55,21 +49,18 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_parityCombo->addItem(tr("Mark"), QSerialPort::MarkParity);
form->addRow(tr("Parity:"), m_parityCombo);
// ── Stop bits ─────────────────────────────────────────────────────────────
m_stopBitsCombo = new QComboBox(this);
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);
// ── Flow control ──────────────────────────────────────────────────────────
m_flowCombo = new QComboBox(this);
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);
// ── Log file ──────────────────────────────────────────────────────────────
auto *logRow = new QHBoxLayout();
m_logPathEdit = new QLineEdit(this);
m_logPathEdit->setPlaceholderText(tr("(optional) path/to/output.log"));
@@ -80,7 +71,6 @@ ConnectDialog::ConnectDialog(QWidget *parent)
logRow->addWidget(browseBtn);
form->addRow(tr("Log file:"), logRow);
// ── Buttons ───────────────────────────────────────────────────────────────
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);

View File

@@ -1,5 +1,6 @@
#include <QApplication>
#include <QStyleFactory>
#include <QIcon>
#include "mainwindow.h"
int main(int argc, char *argv[])
@@ -9,10 +10,11 @@ int main(int argc, char *argv[])
app.setApplicationVersion("1.0.0");
app.setOrganizationName("ChicaDev");
// Prefer Fusion style for consistent cross-distro look
app.setStyle(QStyleFactory::create("Fusion"));
// Optional: dark palette (comment out if you prefer system theme)
const QIcon appIcon(":/icons/UartscopeLogo.png");
app.setWindowIcon(appIcon);
QPalette darkPalette;
darkPalette.setColor(QPalette::Window, QColor(0x2b, 0x2b, 0x2b));
darkPalette.setColor(QPalette::WindowText, QColor(0xdc, 0xdc, 0xdc));
@@ -29,10 +31,6 @@ int main(int argc, char *argv[])
darkPalette.setColor(QPalette::HighlightedText, Qt::white);
app.setPalette(darkPalette);
// Set application icon (embedded via Qt resource)
const QIcon appIcon(":/icons/UartscopeLogo.png");
app.setWindowIcon(appIcon);
MainWindow w;
w.show();
return app.exec();

View File

@@ -1,9 +1,9 @@
#include "mainwindow.h"
#include <QSettings>
#include <QApplication>
#include <QClipboard>
#include <QDialog>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
#include <QMetaObject>
@@ -14,6 +14,7 @@
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFont>
#include <QPixmap>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
@@ -26,7 +27,6 @@ MainWindow::MainWindow(QWidget *parent)
setupStatusBar();
restoreSettings();
// ── Worker thread ─────────────────────────────────────────────────────────
m_thread = new QThread(this);
m_worker = new SerialWorker();
m_worker->moveToThread(m_thread);
@@ -46,9 +46,6 @@ MainWindow::MainWindow(QWidget *parent)
MainWindow::~MainWindow()
{
// All real cleanup happens in closeEvent(). The destructor only runs if
// the window was destroyed without a close event (e.g. programmatically),
// so we guard against double-cleanup with m_shutdownDone.
doShutdown();
}
@@ -66,11 +63,9 @@ void MainWindow::doShutdown()
saveSettings();
// 1. Stop V4L2 capture first (joins the internal std::thread)
if (m_videoWidget)
m_videoWidget->shutdown();
// 2. Stop UART serial worker
if (m_thread && m_thread->isRunning()) {
QMetaObject::invokeMethod(m_worker, "closePort", Qt::BlockingQueuedConnection);
m_thread->quit();
@@ -82,12 +77,10 @@ void MainWindow::doShutdown()
void MainWindow::setupUi()
{
// ── Outer horizontal splitter: (Raw+Table tabs) | right panel ────────────
m_mainSplitter = new QSplitter(Qt::Horizontal, this);
m_mainSplitter->setHandleWidth(5);
auto *mainSplitter = m_mainSplitter;
mainSplitter->setHandleWidth(5);
// Left: tabs for Raw and Table views
auto *tabs = new QTabWidget(mainSplitter);
tabs->setTabPosition(QTabWidget::South);
@@ -97,7 +90,6 @@ void MainWindow::setupUi()
m_tableView = new TableView(tabs);
tabs->addTab(m_tableView, tr("Table view"));
// Right: vertical splitter Tag Monitor on top, Video below
m_rightSplitter = new QSplitter(Qt::Vertical, mainSplitter);
auto *rightSplitter = m_rightSplitter;
rightSplitter->setHandleWidth(5);
@@ -110,9 +102,8 @@ void MainWindow::setupUi()
rightSplitter->addWidget(m_tagWidget);
rightSplitter->addWidget(m_videoWidget);
// Initial split: ~30% tags, ~70% video user can drag to taste
rightSplitter->setStretchFactor(0, 1);
rightSplitter->setStretchFactor(1, 2);
rightSplitter->setStretchFactor(1, 1);
rightSplitter->setSizes({330, 330});
mainSplitter->addWidget(tabs);
@@ -143,7 +134,6 @@ void MainWindow::setupToolBar()
tb->addSeparator();
// ── Auto-reconnect controls ───────────────────────────────────────────────
m_autoReconnectCb = new QCheckBox(tr("Auto-reconnect"), tb);
m_autoReconnectCb->setChecked(true);
m_autoReconnectCb->setToolTip(tr("Automatically reconnect when the port drops"));
@@ -161,7 +151,6 @@ void MainWindow::setupToolBar()
m_reconnectIntervalSb->setSingleStep(500);
m_reconnectIntervalSb->setValue(2000);
m_reconnectIntervalSb->setSuffix(tr(" ms"));
m_reconnectIntervalSb->setToolTip(tr("Interval between reconnect attempts"));
connect(m_reconnectIntervalSb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this](int ms) {
QMetaObject::invokeMethod(m_worker, [this, ms] {
@@ -172,21 +161,18 @@ void MainWindow::setupToolBar()
tb->addSeparator();
// ── Tag filter ────────────────────────────────────────────────────────────
auto *filterAction = tb->addAction(tr("Tag filter…"));
filterAction->setToolTip(tr("Choose which tags are hidden from the Raw view"));
connect(filterAction, &QAction::triggered, this, &MainWindow::configureTagFilter);
tb->addSeparator();
// ── Format reference ──────────────────────────────────────────────────────
auto *helpAction = tb->addAction(tr("Format reference"));
helpAction->setToolTip(tr("Show UARTScope output format guide (copy for AI)"));
connect(helpAction, &QAction::triggered, this, &MainWindow::showFormatReference);
tb->addSeparator();
// ── About ─────────────────────────────────────────────────────────────────
auto *aboutAction = tb->addAction(tr("Über UARTScope"));
connect(aboutAction, &QAction::triggered, this, &MainWindow::showAbout);
}
@@ -197,7 +183,7 @@ void MainWindow::setupStatusBar()
m_reconnectLabel = new QLabel(this);
m_portLabel = new QLabel(this);
m_reconnectLabel->setStyleSheet("color: #e5a50a;"); // amber
m_reconnectLabel->setStyleSheet("color: #e5a50a;");
statusBar()->addWidget(m_stateLabel);
statusBar()->addWidget(m_reconnectLabel);
@@ -223,7 +209,6 @@ void MainWindow::onConnectClicked()
Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.logFilePath));
// Push current reconnect settings to worker before opening
const bool autoRec = m_autoReconnectCb->isChecked();
const int interval = m_reconnectIntervalSb->value();
QMetaObject::invokeMethod(m_worker, [this, autoRec, interval] {
@@ -277,13 +262,13 @@ void MainWindow::onReconnecting(int attempt)
void MainWindow::onClearScreen()
{
// ANSI clear-screen received from the firmware: clear ALL three views
// (raw output, table, AND the tag monitor) plus the video preview frame.
clearAllViews();
}
void MainWindow::onError(const QString &message)
{
// Only show a dialog for errors that arrive when we are NOT in reconnect
// mode otherwise the amber status text is sufficient feedback.
if (!m_autoReconnectCb->isChecked()) {
QMessageBox::critical(this, tr("Serial error"), message);
onPortClosed();
@@ -306,6 +291,8 @@ void MainWindow::clearAllViews()
m_rawView->clear();
m_tableView->clear();
m_tagWidget->clearAll();
if (m_videoWidget)
m_videoWidget->clearFrame();
}
// ── Format reference dialog ────────────────────────────────────────────────
@@ -322,12 +309,11 @@ three parallel ways. You can use any combination.
1. RAW OUTPUT (always active)
Every line sent over UART is shown verbatim in the "Raw output" tab.
No special formatting needed.
Every line sent over UART is shown verbatim (with a timestamp) in the
"Raw output" tab. No special formatting needed.
uart_printf("Boot complete. Version 1.0\n");
ANSI clear-screen clears ALL views simultaneously:
ANSI clear-screen clears ALL views simultaneously (raw output, table,
tag monitor, and the video preview frame):
uart_printf("\033[2J\033[H"); // standard VT100 clear-screen + cursor home
@@ -336,17 +322,25 @@ ANSI clear-screen → clears ALL views simultaneously:
Any line that contains [TAGNAME] is intercepted and displayed in a
dedicated panel in the Tag Monitor. The line is still shown in Raw output
(highlighted in cyan) so nothing is hidden.
(highlighted in cyan) unless the tag is added to the Tag Filter.
TAG FORMAT:
[TAGNAME] key1=value1 key2=value2 ...
Rules:
Tag name: uppercase letters, digits, underscore. Examples: WDG, VIDEO, I2C_BUS
Tag must start the meaningful content (leading whitespace is fine)
Key=value pairs are space-separated; values must not contain spaces
If no key=value pairs are present the raw string is shown as-is
LIST VALUES: separate multiple items in one value with ';' they are
shown as a multi-line list under the same key in the Tag Monitor:
uart_printf("[KICKSTART] Devs=trackdisk.device;input.device\n");
Tag Monitor shows:
Devs trackdisk.device
input.device
Examples:
uart_printf("[WDG] uptime=%lu free_heap=%lu temp=%d\n",
uptime_ms, heap_free, cpu_temp);
@@ -354,11 +348,7 @@ Examples:
uart_printf("[VIDEO] line=%d hblank=%d vblank=%d dma_buf=%d\n",
scanline, hblank_cycles, vblank_cycles, dma_remaining);
uart_printf("[AUDIO] buf=%d underruns=%lu vol=%d\n",
audio_buf_level, underrun_count, volume);
uart_printf("[I2C] addr=0x%02X ack=%d err=%d\n",
i2c_addr, ack_received, error_code);
uart_printf("[KICKSTART] Libs=graphics.library;intuition.library;dos.library\n");
Each unique tag gets its own panel. Panels update in-place so repeated
messages don't scroll past perfect for periodic watchdog output.
@@ -390,30 +380,25 @@ No special firmware changes needed.
5. COMPLETE EXAMPLE (Chica / Amiga hardware emulation)
void uart_status_update(void) {
// Watchdog heartbeat → Tag Monitor [WDG]
uart_printf("[WDG] uptime=%lu free=%lu load=%d temp=%d\n",
HAL_GetTick(), xPortGetFreeHeapSize(),
cpu_load_percent, core_temp_c);
// Video timing → Tag Monitor [VIDEO]
uart_printf("[VIDEO] line=%d hblank=%d vblank=%d copper=%d\n",
current_scanline, hblank_ticks, vblank_ticks, copper_dma);
// Audio state → Tag Monitor [AUDIO]
uart_printf("[AUDIO] ch=%d buf=%d underruns=%lu\n",
active_channels, buffer_fill, total_underruns);
uart_printf("[KICKSTART] Devs=trackdisk.device;input.device;console.device\n");
uart_printf("[KICKSTART] Libs=graphics.library;intuition.library\n");
// Structured CSV data → Table view
uart_printf("%lu,%.1f,%d,%d\n",
HAL_GetTick(), vcc_mv / 1000.0f, cpu_load_percent, core_temp_c);
}
// Call from your main loop or a timer ISR, e.g. every 500 ms
// Clear screen from firmware when you want a fresh start:
// uart_printf("\033[2J\033[H");
)";
// ── Dialog ────────────────────────────────────────────────────────────────
auto *dlg = new QDialog(this);
dlg->setWindowTitle(tr("UARTScope Format Reference"));
dlg->resize(720, 620);
@@ -475,7 +460,6 @@ void MainWindow::configureTagFilter()
QFont mono("Monospace");
mono.setStyleHint(QFont::Monospace);
edit->setFont(mono);
// Pre-populate with current filter
QStringList current(m_suppressedTags.values());
current.sort();
edit->setPlainText(current.join('\n'));
@@ -495,66 +479,12 @@ void MainWindow::configureTagFilter()
if (!tag.isEmpty())
m_suppressedTags.insert(tag);
}
// Persist immediately so the filter survives even an unexpected exit.
saveSettings();
}
dlg->deleteLater();
}
// ── Settings: save / restore ───────────────────────────────────────────────
void MainWindow::saveSettings()
{
QSettings s("ChicaDev", "UARTScope");
// Window geometry & state
s.setValue("window/geometry", saveGeometry());
s.setValue("window/state", saveState());
// Splitter sizes
if (m_mainSplitter)
s.setValue("splitter/main", m_mainSplitter->saveState());
if (m_rightSplitter)
s.setValue("splitter/right", m_rightSplitter->saveState());
// Toolbar settings
s.setValue("reconnect/enabled", m_autoReconnectCb->isChecked());
s.setValue("reconnect/interval", m_reconnectIntervalSb->value());
// Tag filter
QStringList tags(m_suppressedTags.values());
tags.sort();
s.setValue("tagfilter/suppressed", tags);
}
void MainWindow::restoreSettings()
{
QSettings s("ChicaDev", "UARTScope");
// Window geometry & state
if (s.contains("window/geometry"))
restoreGeometry(s.value("window/geometry").toByteArray());
if (s.contains("window/state"))
restoreState(s.value("window/state").toByteArray());
// Splitter sizes (only restore if we have saved data)
if (m_mainSplitter && s.contains("splitter/main"))
m_mainSplitter->restoreState(s.value("splitter/main").toByteArray());
if (m_rightSplitter && s.contains("splitter/right"))
m_rightSplitter->restoreState(s.value("splitter/right").toByteArray());
// Toolbar settings
if (s.contains("reconnect/enabled"))
m_autoReconnectCb->setChecked(s.value("reconnect/enabled").toBool());
if (s.contains("reconnect/interval"))
m_reconnectIntervalSb->setValue(s.value("reconnect/interval").toInt());
// Tag filter
const QStringList tags = s.value("tagfilter/suppressed").toStringList();
m_suppressedTags.clear();
for (const QString &t : tags)
if (!t.isEmpty())
m_suppressedTags.insert(t);
}
// ── About dialog ──────────────────────────────────────────────────────────
void MainWindow::showAbout()
@@ -567,7 +497,6 @@ void MainWindow::showAbout()
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// ── Header block (dark accent) ────────────────────────────────────────────
auto *header = new QWidget(dlg);
header->setFixedHeight(110);
header->setStyleSheet("background-color: #1e1e1e;");
@@ -575,14 +504,12 @@ void MainWindow::showAbout()
headerLayout->setContentsMargins(20, 14, 24, 14);
headerLayout->setSpacing(16);
// Logo
auto *logoLabel = new QLabel(header);
const QPixmap logoPix(":/icons/UartscopeLogo.png");
logoLabel->setPixmap(logoPix.scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation));
logoLabel->setStyleSheet("background: transparent;");
logoLabel->setFixedSize(72, 72);
// Title + subtitle
auto *titleBlock = new QVBoxLayout();
titleBlock->setSpacing(4);
auto *titleLabel = new QLabel("UARTScope", header);
@@ -598,7 +525,6 @@ void MainWindow::showAbout()
headerLayout->addLayout(titleBlock);
root->addWidget(header);
// ── Info grid ─────────────────────────────────────────────────────────────
auto *body = new QWidget(dlg);
body->setStyleSheet("background-color: #252526;");
auto *grid = new QGridLayout(body);
@@ -628,7 +554,6 @@ void MainWindow::showAbout()
root->addWidget(body, 1);
// ── Footer with close button ──────────────────────────────────────────────
auto *footer = new QWidget(dlg);
footer->setStyleSheet("background-color: #252526; border-top: 1px solid #3c3c3c;");
auto *footerLayout = new QHBoxLayout(footer);
@@ -646,3 +571,54 @@ void MainWindow::showAbout()
dlg->exec();
dlg->deleteLater();
}
// ── Settings: save / restore ───────────────────────────────────────────────
void MainWindow::saveSettings()
{
QSettings s("ChicaDev", "UARTScope");
s.setValue("window/geometry", saveGeometry());
s.setValue("window/state", saveState());
if (m_mainSplitter)
s.setValue("splitter/main", m_mainSplitter->saveState());
if (m_rightSplitter)
s.setValue("splitter/right", m_rightSplitter->saveState());
s.setValue("reconnect/enabled", m_autoReconnectCb->isChecked());
s.setValue("reconnect/interval", m_reconnectIntervalSb->value());
QStringList tags(m_suppressedTags.values());
tags.sort();
s.setValue("tagfilter/suppressed", tags);
s.sync(); // flush to disk immediately (important since we also call
// this mid-session from configureTagFilter())
}
void MainWindow::restoreSettings()
{
QSettings s("ChicaDev", "UARTScope");
if (s.contains("window/geometry"))
restoreGeometry(s.value("window/geometry").toByteArray());
if (s.contains("window/state"))
restoreState(s.value("window/state").toByteArray());
if (m_mainSplitter && s.contains("splitter/main"))
m_mainSplitter->restoreState(s.value("splitter/main").toByteArray());
if (m_rightSplitter && s.contains("splitter/right"))
m_rightSplitter->restoreState(s.value("splitter/right").toByteArray());
if (s.contains("reconnect/enabled"))
m_autoReconnectCb->setChecked(s.value("reconnect/enabled").toBool());
if (s.contains("reconnect/interval"))
m_reconnectIntervalSb->setValue(s.value("reconnect/interval").toInt());
const QStringList tags = s.value("tagfilter/suppressed").toStringList();
m_suppressedTags.clear();
for (const QString &t : tags)
if (!t.isEmpty())
m_suppressedTags.insert(t);
}

View File

@@ -24,7 +24,6 @@ void RawView::setupUi()
mainLayout->setContentsMargins(4, 4, 4, 4);
mainLayout->setSpacing(4);
// ── Toolbar ──────────────────────────────────────────────────────────────
auto *toolbar = new QHBoxLayout();
m_searchEdit = new QLineEdit(this);
@@ -33,7 +32,7 @@ void RawView::setupUi()
connect(m_searchEdit, &QLineEdit::textChanged, this, &RawView::onSearch);
m_autoScrollCb = new QCheckBox(tr("Auto-scroll"), this);
m_autoScrollCb->setChecked(true);
m_autoScrollCb->setChecked(true); // default ON
connect(m_autoScrollCb, &QCheckBox::toggled, this, &RawView::onAutoScrollToggled);
m_lineCountLbl = new QLabel(tr("Lines: 0"), this);
@@ -54,11 +53,10 @@ void RawView::setupUi()
toolbar->addWidget(m_clearBtn);
mainLayout->addLayout(toolbar);
// ── Text area ─────────────────────────────────────────────────────────────
m_textEdit = new QPlainTextEdit(this);
m_textEdit->setReadOnly(true);
m_textEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
m_textEdit->setMaximumBlockCount(0); // unlimited
m_textEdit->setMaximumBlockCount(0);
QFont monoFont("Monospace");
monoFont.setStyleHint(QFont::Monospace);
@@ -84,39 +82,27 @@ void RawView::applyColorScheme()
void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTags)
{
// Check whether this line belongs to a suppressed tag
if (!suppressedTags.isEmpty()) {
const auto match = s_tagRe.match(line);
if (match.hasMatch()) {
if (suppressedTags.contains(match.captured(1).toUpper()))
return; // silently drop it goes to the Tag Monitor only
}
if (match.hasMatch() && suppressedTags.contains(match.captured(1).toUpper()))
return;
}
++m_lineCount;
m_lineCountLbl->setText(tr("Lines: %1").arg(m_lineCount));
// Prepend timestamp
const QString ts = QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
const QString displayLine = QStringLiteral("[%1] %2").arg(ts, line);
// Colour tag lines cyan, everything else default
const bool hasTag = s_tagRe.match(line).hasMatch();
QTextCursor cursor(m_textEdit->document());
cursor.movePosition(QTextCursor::End);
QTextCharFormat fmt;
if (hasTag)
fmt.setForeground(QColor(0x56, 0xb6, 0xc2)); // cyan
else
fmt.setForeground(QColor(0xd4, 0xd4, 0xd4)); // default
// Dim the timestamp portion
QTextCharFormat tsFmt;
tsFmt.setForeground(QColor(0x60, 0x60, 0x60));
cursor.insertText(QStringLiteral("[%1] ").arg(ts), tsFmt);
QTextCharFormat fmt;
fmt.setForeground(hasTag ? QColor(0x56, 0xb6, 0xc2) : QColor(0xd4, 0xd4, 0xd4));
cursor.insertText(line + '\n', fmt);
if (m_autoScroll)

View File

@@ -20,7 +20,7 @@ SerialWorker::~SerialWorker()
closePort();
}
// ── Public slots ────────────────────────────────────────────────────────────
// ── public slots ──────────────────────────────────────────────────────────────
void SerialWorker::openPort(const QString &portName, qint32 baudRate,
QSerialPort::DataBits dataBits,
@@ -28,7 +28,6 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl)
{
// Save config for auto-reconnect
m_portName = portName;
m_baudRate = baudRate;
m_dataBits = dataBits;
@@ -55,6 +54,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
return;
}
m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0;
emit portOpened();
}
@@ -107,7 +107,7 @@ void SerialWorker::stopLogging()
}
}
// ── Private slots ───────────────────────────────────────────────────────────
// ── private slots ───────────────────────────────────────────────────────────
void SerialWorker::onReadyRead()
{
@@ -119,7 +119,6 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
if (err == QSerialPort::NoError)
return;
// ResourceError = device physically disconnected (USB pull, power loss)
const bool fatal = (err == QSerialPort::ResourceError ||
err == QSerialPort::DeviceNotFoundError);
@@ -132,8 +131,6 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
emit errorOccurred(m_port->errorString());
}
}
// Non-fatal errors (framing, parity, etc.) are silently ignored to avoid
// spamming the user during noisy serial sessions.
}
void SerialWorker::tryReconnect()
@@ -153,6 +150,7 @@ void SerialWorker::tryReconnect()
if (m_port->open(QIODevice::ReadOnly)) {
m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0;
emit portOpened();
} else {
@@ -160,7 +158,7 @@ void SerialWorker::tryReconnect()
}
}
// ── Private helpers ─────────────────────────────────────────────────────────
// ── private helpers ─────────────────────────────────────────────────────────
void SerialWorker::scheduleReconnect()
{
@@ -170,23 +168,48 @@ void SerialWorker::scheduleReconnect()
void SerialWorker::processRawData(const QByteArray &data)
{
// Scan raw bytes for ANSI clear-screen BEFORE UTF-8 conversion so we
// never miss a sequence that straddles a read boundary.
// We look for ESC[2J (optionally followed by ESC[H or ESC[1;1H)
// ESC = 0x1B
const QByteArray esc2j("\x1b[2J");
if (data.contains(esc2j)) {
emit clearScreen();
// Strip all ANSI escape sequences from the data before further processing
// so they don't clutter the raw view.
// ── ANSI clear-screen detection ────────────────────────────────────────
// We scan a buffer that includes a small tail from the PREVIOUS chunk so
// that a sequence split across two readyRead() calls (e.g. "...\x1b" +
// "[2J\x1b[H...") is still detected reliably. The clear sequence itself
// is also stripped from what reaches processLine()/the line buffer.
QByteArray combined = m_scanTail + data;
bool sawClear = false;
// Look for ESC[2J (the actual screen-clear command). ESC[H / ESC[1;1H
// (cursor-home) commonly follows but isn't required for us to clear.
const QByteArray clearSeq("\x1b[2J");
int clearPos = combined.indexOf(clearSeq);
while (clearPos != -1) {
sawClear = true;
// Remove the matched sequence so it doesn't leak into the text stream
combined.remove(clearPos, clearSeq.size());
clearPos = combined.indexOf(clearSeq, clearPos);
}
// Strip all ANSI escape sequences for display
// Sequence: ESC [ <params> <letter>
// Keep the last few bytes as the new tail (in case a sequence is cut off
// at the very end of this chunk); only forward the rest.
constexpr int kTailLen = 8; // longer than any escape sequence we match
QByteArray toProcess;
if (combined.size() > kTailLen) {
toProcess = combined.left(combined.size() - kTailLen);
m_scanTail = combined.right(kTailLen);
} else {
// Not enough data yet to safely flush hold everything as tail.
m_scanTail = combined;
}
if (sawClear)
emit clearScreen();
if (toProcess.isEmpty())
return;
// Strip remaining (non-clear) ANSI escape sequences for clean display.
static const QRegularExpression ansiRe(
"\x1b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])");
QString text = QString::fromUtf8(data);
QString text = QString::fromUtf8(toProcess);
text.remove(ansiRe);
m_buffer += text;

View File

@@ -10,7 +10,6 @@ TableView::TableView(QWidget *parent)
mainLayout->setContentsMargins(4, 4, 4, 4);
mainLayout->setSpacing(4);
// ── Toolbar ──────────────────────────────────────────────────────────────
auto *toolbar = new QHBoxLayout();
m_delimCombo = new QComboBox(this);
@@ -44,7 +43,6 @@ TableView::TableView(QWidget *parent)
toolbar->addWidget(m_clearBtn);
mainLayout->addLayout(toolbar);
// ── Table ─────────────────────────────────────────────────────────────────
m_table = new QTableWidget(this);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
@@ -66,7 +64,6 @@ void TableView::onDelimiterChanged(int index)
void TableView::onMaxRowsChanged()
{
m_maxRows = m_maxRowsEdit->text().toInt();
// Trim excess rows
while (m_rows.size() > m_maxRows)
m_rows.removeFirst();
rebuildTable();
@@ -87,14 +84,13 @@ void TableView::appendLine(const QString &line)
if (line.isEmpty())
return;
// Header row starts with '#'
if (!m_headersSet && line.startsWith('#')) {
parseHeaders(line.mid(1)); // strip leading '#'
parseHeaders(line.mid(1));
return;
}
if (!tryAppendRow(line))
return; // didn't match expected column count
return;
if (m_autoScroll)
m_table->scrollToBottom();
@@ -114,7 +110,6 @@ bool TableView::tryAppendRow(const QString &line)
{
const QStringList cells = line.split(m_delimiter);
// Auto-detect column count on the first data row if no header was given
if (!m_headersSet) {
m_headers.clear();
for (int i = 0; i < cells.size(); ++i)
@@ -125,9 +120,8 @@ bool TableView::tryAppendRow(const QString &line)
}
if (cells.size() != m_headers.size())
return false; // column mismatch skip row
return false;
// Enforce max rows
m_rows.append(cells);
if (m_rows.size() > m_maxRows)
m_rows.removeFirst();
@@ -140,7 +134,6 @@ bool TableView::tryAppendRow(const QString &line)
m_table->setItem(row, col, item);
}
// Remove oldest visual row if over limit
if (m_table->rowCount() > m_maxRows)
m_table->removeRow(0);

View File

@@ -12,25 +12,22 @@ TagPanel::TagPanel(const QString &tag, QWidget *parent)
layout->setContentsMargins(6, 14, 6, 6);
layout->setSpacing(4);
// ── Table for key=value pairs ─────────────────────────────────────────────
m_table = new QTableWidget(0, 2, this);
m_table->setHorizontalHeaderLabels({tr("Key"), tr("Value")});
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->verticalHeader()->hide();
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setAlternatingRowColors(true);
m_table->setMaximumHeight(200);
m_table->setMaximumHeight(220);
m_table->verticalHeader()->setDefaultSectionSize(22);
layout->addWidget(m_table);
// ── Fallback label for non-kv data ────────────────────────────────────────
m_rawLabel = new QLabel(this);
m_rawLabel->setWordWrap(true);
m_rawLabel->setStyleSheet("font-family: monospace;");
m_rawLabel->hide();
layout->addWidget(m_rawLabel);
// ── Copy button ───────────────────────────────────────────────────────────
auto *btnRow = new QHBoxLayout();
btnRow->addStretch();
auto *copyBtn = new QPushButton(tr("📋 Copy"), this);
@@ -61,10 +58,59 @@ void TagPanel::parseKeyValue(const QString &value)
auto it = kvRe.globalMatch(value);
while (it.hasNext()) {
const auto match = it.next();
ensureRow(match.captured(1));
const int row = m_keys.indexOf(match.captured(1));
m_table->item(row, 1)->setText(match.captured(2));
m_table->item(row, 1)->setBackground(QColor(0x2d, 0x5a, 0x2d)); // flash green
const QString key = match.captured(1);
const QString raw = match.captured(2);
// Split on ';' -> list of values. A plain (non-list) value yields
// a QStringList with exactly one element, which still goes through
// the same code path for consistency.
QStringList values = raw.split(';', Qt::SkipEmptyParts);
if (values.isEmpty())
values << raw; // preserve even an empty/odd value as-is
setKeyValues(key, values);
}
}
void TagPanel::setKeyValues(const QString &key, const QStringList &values)
{
const int newCount = values.size();
if (m_keyRanges.contains(key)) {
// Key already exists remove its current rows, then re-insert at
// the same starting position with the new row count.
const KeyRange old = m_keyRanges[key];
for (int i = 0; i < old.count; ++i)
m_table->removeRow(old.firstRow);
// Shift all OTHER key ranges that started after this one
for (auto it = m_keyRanges.begin(); it != m_keyRanges.end(); ++it) {
if (it.key() != key && it->firstRow > old.firstRow)
it->firstRow += (newCount - old.count);
}
int insertAt = old.firstRow;
for (int i = 0; i < newCount; ++i) {
m_table->insertRow(insertAt + i);
auto *keyItem = new QTableWidgetItem(i == 0 ? key : QString());
auto *valItem = new QTableWidgetItem(values[i]);
valItem->setBackground(QColor(0x2d, 0x5a, 0x2d)); // flash green on update
m_table->setItem(insertAt + i, 0, keyItem);
m_table->setItem(insertAt + i, 1, valItem);
}
m_keyRanges[key] = {insertAt, newCount};
} else {
// New key append at the bottom
const int insertAt = m_table->rowCount();
for (int i = 0; i < newCount; ++i) {
m_table->insertRow(insertAt + i);
auto *keyItem = new QTableWidgetItem(i == 0 ? key : QString());
auto *valItem = new QTableWidgetItem(values[i]);
valItem->setBackground(QColor(0x2d, 0x5a, 0x2d));
m_table->setItem(insertAt + i, 0, keyItem);
m_table->setItem(insertAt + i, 1, valItem);
}
m_keyRanges[key] = {insertAt, newCount};
}
}
@@ -73,27 +119,22 @@ void TagPanel::showRaw(const QString &value)
m_rawLabel->setText(value);
}
void TagPanel::ensureRow(const QString &key)
{
if (m_keys.contains(key))
return;
m_keys.append(key);
const int row = m_table->rowCount();
m_table->insertRow(row);
m_table->setItem(row, 0, new QTableWidgetItem(key));
m_table->setItem(row, 1, new QTableWidgetItem(QString()));
}
void TagPanel::copyToClipboard()
{
QStringList lines;
lines << QStringLiteral("[%1]").arg(m_tag);
if (m_table->isVisible()) {
QString lastKey;
for (int r = 0; r < m_table->rowCount(); ++r) {
const QString key = m_table->item(r, 0)->text();
const QString val = m_table->item(r, 1)->text();
if (!key.isEmpty()) {
lines << QStringLiteral(" %1 = %2").arg(key, val);
lastKey = key;
} else {
lines << QStringLiteral(" %1").arg(val); // continuation row
}
}
} else {
lines << QStringLiteral(" %1").arg(m_rawLabel->text());

View File

@@ -7,7 +7,6 @@ TagWidget::TagWidget(QWidget *parent)
outerLayout->setContentsMargins(4, 4, 4, 4);
outerLayout->setSpacing(4);
// Header row
auto *headerLayout = new QHBoxLayout();
auto *titleLabel = new QLabel(tr("<b>Tag Monitor</b>"), this);
auto *clearBtn = new QPushButton(tr("Clear all"), this);
@@ -18,7 +17,6 @@ TagWidget::TagWidget(QWidget *parent)
headerLayout->addWidget(clearBtn);
outerLayout->addLayout(headerLayout);
// Scrollable panel area
auto *scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
@@ -26,7 +24,7 @@ TagWidget::TagWidget(QWidget *parent)
m_panelLayout = new QVBoxLayout(container);
m_panelLayout->setContentsMargins(0, 0, 0, 0);
m_panelLayout->setSpacing(6);
m_panelLayout->addStretch(); // pushes panels to the top
m_panelLayout->addStretch();
scrollArea->setWidget(container);
outerLayout->addWidget(scrollArea, 1);
}
@@ -36,7 +34,6 @@ void TagWidget::handleTag(const QString &tag, const QString &value)
if (!m_panels.contains(tag)) {
auto *panel = new TagPanel(tag, this);
m_panels.insert(tag, panel);
// Insert before the stretch at the end
m_panelLayout->insertWidget(m_panelLayout->count() - 1, panel);
}
m_panels[tag]->update(value);

View File

@@ -10,7 +10,6 @@
#include <QBuffer>
#include <QImageReader>
#include <QThread>
static int xioctl(int fd, unsigned long req, void *arg)
{
@@ -19,16 +18,9 @@ static int xioctl(int fd, unsigned long req, void *arg)
return r;
}
V4l2Worker::V4l2Worker(QObject *parent)
: QObject(parent)
{}
V4l2Worker::V4l2Worker(QObject *parent) : QObject(parent) {}
V4l2Worker::~V4l2Worker()
{
stop();
}
// ── public slots ──────────────────────────────────────────────────────────────
V4l2Worker::~V4l2Worker() { stop(); }
void V4l2Worker::startCapture(const QString &device, int width, int height)
{
@@ -39,29 +31,19 @@ void V4l2Worker::startCapture(const QString &device, int width, int height)
return;
m_running.store(true);
// Run the blocking capture loop in a plain std::thread so the Qt event
// loop of this worker object stays responsive to stopCapture() signals.
m_captureThread = std::thread([this] { captureLoop(); });
}
void V4l2Worker::stopCapture()
{
stop();
}
// ── internal stop (safe to call from any thread) ──────────────────────────────
void V4l2Worker::stopCapture() { stop(); }
void V4l2Worker::stop()
{
m_running.store(false);
if (m_captureThread.joinable())
m_captureThread.join(); // wait for captureLoop() to exit
m_captureThread.join();
closeDevice();
}
// ── capture loop (runs in m_captureThread) ────────────────────────────────────
void V4l2Worker::captureLoop()
{
v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
@@ -75,7 +57,7 @@ void V4l2Worker::captureLoop()
fd_set fds;
FD_ZERO(&fds);
FD_SET(m_fd, &fds);
timeval tv{0, 100000}; // 100 ms timeout → checks m_running 10x/sec
timeval tv{0, 100000};
const int r = ::select(m_fd + 1, &fds, nullptr, nullptr, &tv);
if (r == 0) continue;
if (r == -1) {
@@ -108,8 +90,6 @@ void V4l2Worker::captureLoop()
emit captureStopped();
}
// ── device open / close ───────────────────────────────────────────────────────
bool V4l2Worker::openDevice(const QString &device, int width, int height)
{
m_width = width;
@@ -134,10 +114,7 @@ bool V4l2Worker::openDevice(const QString &device, int width, int height)
}
const uint32_t preferred[] = {
V4L2_PIX_FMT_MJPEG,
V4L2_PIX_FMT_YUYV,
V4L2_PIX_FMT_NV12,
0
V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_YUYV, V4L2_PIX_FMT_NV12, 0
};
m_pixFmt = 0;

View File

@@ -8,7 +8,6 @@
#include <QMetaObject>
#include <QResizeEvent>
// Enumerate /dev/video* entries
static QStringList enumerateVideoDevices()
{
QStringList devices;
@@ -19,8 +18,6 @@ static QStringList enumerateVideoDevices()
return devices;
}
// ── ctor / dtor ───────────────────────────────────────────────────────────────
VideoWidget::VideoWidget(QWidget *parent)
: QWidget(parent)
{
@@ -60,8 +57,6 @@ void VideoWidget::shutdown()
return;
m_shutdownDone = true;
// stopCapture() sets m_running=false and joins the std::thread internally.
// Must happen before we quit the Qt event thread.
m_worker->stopCapture();
if (m_thread && m_thread->isRunning()) {
@@ -77,8 +72,6 @@ VideoWidget::~VideoWidget()
shutdown();
}
// ── UI setup ─────────────────────────────────────────────────────────────────
void VideoWidget::setupUi()
{
setMinimumHeight(140);
@@ -87,7 +80,6 @@ void VideoWidget::setupUi()
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(3);
// ── Controls row ──────────────────────────────────────────────────────────
auto *ctrlRow = new QHBoxLayout();
ctrlRow->setSpacing(4);
@@ -97,7 +89,6 @@ void VideoWidget::setupUi()
auto *refreshBtn = new QPushButton("", this);
refreshBtn->setMaximumWidth(26);
refreshBtn->setToolTip(tr("Refresh device list"));
connect(refreshBtn, &QPushButton::clicked, this, &VideoWidget::refreshDeviceList);
m_startBtn = new QPushButton(tr("▶ Start"), this);
@@ -111,7 +102,6 @@ void VideoWidget::setupUi()
m_screenshotBtn = new QPushButton(tr("📷"), this);
m_screenshotBtn->setMaximumWidth(32);
m_screenshotBtn->setToolTip(tr("Save screenshot (PNG/JPG)"));
connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot);
ctrlRow->addWidget(m_deviceCombo, 1);
@@ -121,24 +111,17 @@ void VideoWidget::setupUi()
ctrlRow->addWidget(m_screenshotBtn);
layout->addLayout(ctrlRow);
// ── Info label ────────────────────────────────────────────────────────────
m_infoLabel = new QLabel(tr("No device started"), this);
m_infoLabel->setStyleSheet("color: gray; font-size: 9px;");
layout->addWidget(m_infoLabel);
// The video frame is drawn directly in paintEvent no QLabel needed.
// We leave stretch space for it.
layout->addStretch(1);
}
// ── Slots ─────────────────────────────────────────────────────────────────────
void VideoWidget::onStartStop()
{
if (m_running)
stopCapture();
else
startCapture();
if (m_running) stopCapture();
else startCapture();
}
void VideoWidget::onFreeze(bool frozen)
@@ -172,7 +155,14 @@ void VideoWidget::onNewFrame(const QImage &frame)
return;
m_frame = frame;
updateScaled();
update(); // trigger paintEvent
update();
}
void VideoWidget::clearFrame()
{
m_frame = QImage();
m_scaled = QImage();
update();
}
void VideoWidget::refreshDeviceList()
@@ -182,14 +172,11 @@ void VideoWidget::refreshDeviceList()
const auto devices = enumerateVideoDevices();
for (const auto &d : devices)
m_deviceCombo->addItem(d);
// Restore previous selection if still present
const int idx = m_deviceCombo->findText(current);
if (idx >= 0)
m_deviceCombo->setCurrentIndex(idx);
}
// ── Capture start/stop ────────────────────────────────────────────────────────
void VideoWidget::startCapture()
{
const QString dev = m_deviceCombo->currentText();
@@ -197,7 +184,6 @@ void VideoWidget::startCapture()
return;
m_frame = QImage();
m_scaled = QImage();
// startCapture blocks in the worker thread until stopCapture() is called
QMetaObject::invokeMethod(m_worker, "startCapture",
Qt::QueuedConnection,
Q_ARG(QString, dev),
@@ -210,11 +196,8 @@ void VideoWidget::stopCapture()
QMetaObject::invokeMethod(m_worker, "stopCapture", Qt::QueuedConnection);
}
// ── Paint ─────────────────────────────────────────────────────────────────────
void VideoWidget::updateScaled()
{
// Video area = widget height minus the two control rows (~55 px)
const QRect videoRect(0, 55, width(), height() - 55);
if (videoRect.width() < 10 || videoRect.height() < 10 || m_frame.isNull())
return;
@@ -226,8 +209,6 @@ void VideoWidget::updateScaled()
void VideoWidget::paintEvent(QPaintEvent *)
{
QPainter p(this);
// Background
p.fillRect(rect(), QColor(0x10, 0x10, 0x10));
if (m_scaled.isNull()) {
@@ -237,14 +218,12 @@ void VideoWidget::paintEvent(QPaintEvent *)
return;
}
// Centre the scaled frame in the video area
const QRect videoRect(0, 55, width(), height() - 55);
const QPoint origin(
videoRect.left() + (videoRect.width() - m_scaled.width()) / 2,
videoRect.top() + (videoRect.height() - m_scaled.height()) / 2);
p.drawImage(origin, m_scaled);
// Frozen overlay
if (m_frozen) {
p.setPen(QColor(0xff, 0xcc, 0x00));
p.setFont(QFont("Sans", 9, QFont::Bold));

View File

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

View File

@@ -1,11 +1,11 @@
format = 2
pkgname = uartscope
pkgbase = uartscope
pkgver = 1.0.0.r1.g2181f25-1
pkgver = 1.0.0.r2.g92168ee-1
pkgarch = x86_64
pkgbuild_sha256sum = 367d5f02c03027788d532bdeda7084f79008b9dcf902393b209ad7f2ae43859c
pkgbuild_sha256sum = ec7fe9ddf4a5966cba66edc3f38e169bdaeeffd2a0b4206e481794ce35a30737
packager = Unknown Packager
builddate = 1780988717
builddate = 1781039770
builddir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git
startdir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git
buildtool = makepkg
@@ -2790,7 +2790,7 @@ installed = ttf-ubuntu-font-family-1:0.83-2-any
installed = tumbler-4.20.1-1-x86_64
installed = twolame-0.4.0-4-x86_64
installed = tzdata-2026b-1-x86_64
installed = uartscope-1.0.0.r0.gcc102c9-1-x86_64
installed = uartscope-1.0.0.r1.g2181f25-1-x86_64
installed = uchardet-0.0.8-4-x86_64
installed = udisks2-2.11.1-2-x86_64
installed = unifdef-2.12-4-x86_64

Binary file not shown.

View File

@@ -3,12 +3,12 @@
pkgname = uartscope
pkgbase = uartscope
xdata = pkgtype=pkg
pkgver = 1.0.0.r1.g2181f25-1
pkgver = 1.0.0.r2.g92168ee-1
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
builddate = 1780988717
builddate = 1781039770
packager = Unknown Packager
size = 209170
size = 381553
arch = x86_64
license = MIT
conflict = uartscope

View File

@@ -2,7 +2,7 @@
Name=UARTScope
Comment=UART Serial Monitor
Exec=uartscope
Icon=utilities-terminal
Icon=uartscope
Terminal=false
Type=Application
Categories=Development;Utility;

Submodule uartscope-git/src/uartscope updated: 2181f254d2...92168ee5dc

View File

@@ -1,2 +1,2 @@
2181f254d29960cffe3b1f6158a97b3c65fb0d15 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
92168ee5dc7cfe0e6711207e314b6bd3acb8f291 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