- 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 # Installation
install(TARGETS uartscope DESTINATION bin) install(TARGETS uartscope DESTINATION bin)
# Optional: create a .desktop file for Linux app launchers
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/uartscope.desktop.in ${CMAKE_SOURCE_DIR}/uartscope.desktop.in
${CMAKE_BINARY_DIR}/uartscope.desktop ${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 | | **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 | | **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 | | **H + V Scrolling** | Kein Zeilenumbruch, voller horizontaler Scrollbalken |
| **Live-Logging** | Alle empfangenen Zeilen werden mit Timestamp in eine Datei geschrieben | | **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-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 | | **Tabellenansicht** | CSV-formatierte UART-Zeilen werden in einer Tabelle dargestellt |
| **Suche** | Inkrementelle Volltextsuche im Raw-View | | **Suche** | Inkrementelle Volltextsuche im Raw-View |
| **Auto-Reconnect** | Bei Verbindungsabbruch wird automatisch neu verbunden (Intervall einstellbar) | | **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 | | **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 | | **V4L2 Live Video** | HDMI-Grabber direkt eingebunden Live-Vorschau unter dem Tag-Monitor |
| **Screenshot** | Aktuellen Frame des HDMI-Grabbers als PNG/JPG speichern | | **Screenshot** | Aktuellen Frame des HDMI-Grabbers als PNG/JPG speichern |
| **Format-Referenz** | Eingebauter Guide (mit Kopieren-Button) für KI-kompatible UART-Formatierung | | **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. 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 ```c
uart_printf("\033[2J\033[H"); uart_printf("\033[2J\033[H");
``` ```
### Tag-Monitor ### 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 ...` **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 - Key=Value-Paare: Leerzeichen-getrennt, Werte ohne Leerzeichen
- Ohne Key=Value-Paare wird der rohe String angezeigt - 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:** **Beispiele:**
```c ```c
uart_printf("[WDG] uptime=%lu free_heap=%lu temp=%d\n", uart_printf("[WDG] uptime=%lu free_heap=%lu temp=%d\n",
HAL_GetTick(), xPortGetFreeHeapSize(), core_temp); 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", uart_printf("[VIDEO] line=%d hblank=%d vblank=%d copper=%d\n",
scanline, hblank_ticks, vblank_ticks, copper_dma); 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 ### 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 ## Einstellungen
Beim Beenden werden folgende Einstellungen automatisch gespeichert und beim nächsten Start wiederhergestellt: 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/ uartscope/
├── CMakeLists.txt ├── CMakeLists.txt
├── README.md ├── README.md
├── resources.qrc
├── UartscopeLogo.png
├── uartscope.desktop.in ├── uartscope.desktop.in
├── include/ ├── include/
│ ├── mainwindow.h ← Hauptfenster, koordiniert alle Komponenten │ ├── 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 │ ├── rawview.h ← Unbegrenzter Log mit Timestamps, Suche, Copy
│ ├── tableview.h ← CSV-Parser → QTableWidget │ ├── tableview.h ← CSV-Parser → QTableWidget
│ ├── tagwidget.h ← Container für Tag-Panels │ ├── 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) │ ├── connectdialog.h ← Port-Konfiguration (Port, Baud, Log-Datei)
│ ├── v4l2worker.h ← V4L2-Capture in std::thread, MJPEG/YUYV/NV12 │ ├── v4l2worker.h ← V4L2-Capture in std::thread, MJPEG/YUYV/NV12
│ └── videowidget.h ← Live-Vorschau, Freeze, Screenshot │ └── videowidget.h ← Live-Vorschau, Freeze, Screenshot

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,13 +10,6 @@
#include <QComboBox> #include <QComboBox>
// TableView interprets UART lines as delimited data and shows them in a table. // 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 class TableView : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -44,7 +37,7 @@ private:
QCheckBox *m_autoScrollCb = nullptr; QCheckBox *m_autoScrollCb = nullptr;
QStringList m_headers; QStringList m_headers;
QList<QStringList> m_rows; // raw data for rebuild QList<QStringList> m_rows;
int m_maxRows = 500; int m_maxRows = 500;
char m_delimiter = ','; char m_delimiter = ',';
bool m_autoScroll = true; bool m_autoScroll = true;

View File

@@ -6,11 +6,16 @@
#include <QPushButton> #include <QPushButton>
#include <QTableWidget> #include <QTableWidget>
#include <QStringList> #include <QStringList>
#include <QMap>
// TagPanel displays the most recent data for a single tag (e.g. [WDG]). // 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. // Key=value pairs are shown in a table that updates in-place.
// A "Copy" button copies the current values to the clipboard. // If a value contains ';', it is treated as a LIST: the key is shown once
// No timestamp is shown here timestamps belong in the Raw view. // 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 class TagPanel : public QGroupBox
{ {
Q_OBJECT Q_OBJECT
@@ -29,10 +34,16 @@ private slots:
private: private:
void parseKeyValue(const QString &value); void parseKeyValue(const QString &value);
void showRaw(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; QString m_tag;
QTableWidget *m_table = nullptr; QTableWidget *m_table = nullptr;
QLabel *m_rawLabel = 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 <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPushButton> #include <QPushButton>
#include <QLineEdit> #include <QLabel>
#include <QMap> #include <QMap>
#include <QString> #include <QString>

View File

@@ -24,8 +24,8 @@ signals:
void errorOccurred(const QString &message); void errorOccurred(const QString &message);
private: private:
void stop(); // joins m_captureThread, closes device safe from any thread void stop();
void captureLoop(); // runs inside m_captureThread void captureLoop();
bool openDevice(const QString &device, int width, int height); bool openDevice(const QString &device, int width, int height);
void closeDevice(); void closeDevice();
bool initMmap(); bool initMmap();
@@ -45,5 +45,5 @@ private:
int m_nBuffers = 0; int m_nBuffers = 0;
std::atomic<bool> m_running{false}; 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 <QHBoxLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QThread> #include <QThread>
#include <QTimer>
#include "v4l2worker.h" #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 class VideoWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -28,12 +19,11 @@ public:
~VideoWidget(); ~VideoWidget();
public: public:
// Called by MainWindow::closeEvent to guarantee orderly shutdown before void shutdown(); // safe to call multiple times, called from MainWindow::closeEvent
// widget destruction. Safe to call multiple times.
void shutdown();
public slots: public slots:
void onNewFrame(const QImage &frame); void onNewFrame(const QImage &frame);
void clearFrame(); // blanks the preview (used on ANSI clear-screen)
private slots: private slots:
void onStartStop(); void onStartStop();
@@ -51,21 +41,18 @@ private:
void stopCapture(); void stopCapture();
void updateScaled(); void updateScaled();
// Worker
QThread *m_thread = nullptr; QThread *m_thread = nullptr;
V4l2Worker *m_worker = nullptr; V4l2Worker *m_worker = nullptr;
// Current frame
QImage m_frame; QImage m_frame;
QImage m_scaled; QImage m_scaled;
bool m_frozen = false; bool m_frozen = false;
bool m_running = false; bool m_running = false;
bool m_shutdownDone = false; bool m_shutdownDone = false;
// UI
QComboBox *m_deviceCombo = nullptr; QComboBox *m_deviceCombo = nullptr;
QPushButton *m_startBtn = nullptr; QPushButton *m_startBtn = nullptr;
QPushButton *m_freezeBtn = nullptr; QPushButton *m_freezeBtn = nullptr;
QPushButton *m_screenshotBtn= nullptr; QPushButton *m_screenshotBtn = nullptr;
QLabel *m_infoLabel = nullptr; QLabel *m_infoLabel = nullptr;
}; };

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
#include "mainwindow.h" #include "mainwindow.h"
#include <QSettings>
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QDialog> #include <QDialog>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QMetaObject> #include <QMetaObject>
@@ -14,6 +14,7 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QFont> #include <QFont>
#include <QPixmap>
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
@@ -26,7 +27,6 @@ MainWindow::MainWindow(QWidget *parent)
setupStatusBar(); setupStatusBar();
restoreSettings(); restoreSettings();
// ── Worker thread ─────────────────────────────────────────────────────────
m_thread = new QThread(this); m_thread = new QThread(this);
m_worker = new SerialWorker(); m_worker = new SerialWorker();
m_worker->moveToThread(m_thread); m_worker->moveToThread(m_thread);
@@ -46,9 +46,6 @@ MainWindow::MainWindow(QWidget *parent)
MainWindow::~MainWindow() 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(); doShutdown();
} }
@@ -66,11 +63,9 @@ void MainWindow::doShutdown()
saveSettings(); saveSettings();
// 1. Stop V4L2 capture first (joins the internal std::thread)
if (m_videoWidget) if (m_videoWidget)
m_videoWidget->shutdown(); m_videoWidget->shutdown();
// 2. Stop UART serial worker
if (m_thread && m_thread->isRunning()) { if (m_thread && m_thread->isRunning()) {
QMetaObject::invokeMethod(m_worker, "closePort", Qt::BlockingQueuedConnection); QMetaObject::invokeMethod(m_worker, "closePort", Qt::BlockingQueuedConnection);
m_thread->quit(); m_thread->quit();
@@ -82,12 +77,10 @@ void MainWindow::doShutdown()
void MainWindow::setupUi() void MainWindow::setupUi()
{ {
// ── Outer horizontal splitter: (Raw+Table tabs) | right panel ────────────
m_mainSplitter = new QSplitter(Qt::Horizontal, this); m_mainSplitter = new QSplitter(Qt::Horizontal, this);
m_mainSplitter->setHandleWidth(5);
auto *mainSplitter = m_mainSplitter; auto *mainSplitter = m_mainSplitter;
mainSplitter->setHandleWidth(5);
// Left: tabs for Raw and Table views
auto *tabs = new QTabWidget(mainSplitter); auto *tabs = new QTabWidget(mainSplitter);
tabs->setTabPosition(QTabWidget::South); tabs->setTabPosition(QTabWidget::South);
@@ -97,7 +90,6 @@ void MainWindow::setupUi()
m_tableView = new TableView(tabs); m_tableView = new TableView(tabs);
tabs->addTab(m_tableView, tr("Table view")); tabs->addTab(m_tableView, tr("Table view"));
// Right: vertical splitter Tag Monitor on top, Video below
m_rightSplitter = new QSplitter(Qt::Vertical, mainSplitter); m_rightSplitter = new QSplitter(Qt::Vertical, mainSplitter);
auto *rightSplitter = m_rightSplitter; auto *rightSplitter = m_rightSplitter;
rightSplitter->setHandleWidth(5); rightSplitter->setHandleWidth(5);
@@ -110,9 +102,8 @@ void MainWindow::setupUi()
rightSplitter->addWidget(m_tagWidget); rightSplitter->addWidget(m_tagWidget);
rightSplitter->addWidget(m_videoWidget); rightSplitter->addWidget(m_videoWidget);
// Initial split: ~30% tags, ~70% video user can drag to taste
rightSplitter->setStretchFactor(0, 1); rightSplitter->setStretchFactor(0, 1);
rightSplitter->setStretchFactor(1, 2); rightSplitter->setStretchFactor(1, 1);
rightSplitter->setSizes({330, 330}); rightSplitter->setSizes({330, 330});
mainSplitter->addWidget(tabs); mainSplitter->addWidget(tabs);
@@ -143,7 +134,6 @@ void MainWindow::setupToolBar()
tb->addSeparator(); tb->addSeparator();
// ── Auto-reconnect controls ───────────────────────────────────────────────
m_autoReconnectCb = new QCheckBox(tr("Auto-reconnect"), tb); m_autoReconnectCb = new QCheckBox(tr("Auto-reconnect"), tb);
m_autoReconnectCb->setChecked(true); m_autoReconnectCb->setChecked(true);
m_autoReconnectCb->setToolTip(tr("Automatically reconnect when the port drops")); m_autoReconnectCb->setToolTip(tr("Automatically reconnect when the port drops"));
@@ -161,7 +151,6 @@ void MainWindow::setupToolBar()
m_reconnectIntervalSb->setSingleStep(500); m_reconnectIntervalSb->setSingleStep(500);
m_reconnectIntervalSb->setValue(2000); m_reconnectIntervalSb->setValue(2000);
m_reconnectIntervalSb->setSuffix(tr(" ms")); m_reconnectIntervalSb->setSuffix(tr(" ms"));
m_reconnectIntervalSb->setToolTip(tr("Interval between reconnect attempts"));
connect(m_reconnectIntervalSb, QOverload<int>::of(&QSpinBox::valueChanged), connect(m_reconnectIntervalSb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this](int ms) { this, [this](int ms) {
QMetaObject::invokeMethod(m_worker, [this, ms] { QMetaObject::invokeMethod(m_worker, [this, ms] {
@@ -172,21 +161,18 @@ void MainWindow::setupToolBar()
tb->addSeparator(); tb->addSeparator();
// ── Tag filter ────────────────────────────────────────────────────────────
auto *filterAction = tb->addAction(tr("Tag filter…")); auto *filterAction = tb->addAction(tr("Tag filter…"));
filterAction->setToolTip(tr("Choose which tags are hidden from the Raw view")); filterAction->setToolTip(tr("Choose which tags are hidden from the Raw view"));
connect(filterAction, &QAction::triggered, this, &MainWindow::configureTagFilter); connect(filterAction, &QAction::triggered, this, &MainWindow::configureTagFilter);
tb->addSeparator(); tb->addSeparator();
// ── Format reference ──────────────────────────────────────────────────────
auto *helpAction = tb->addAction(tr("Format reference")); auto *helpAction = tb->addAction(tr("Format reference"));
helpAction->setToolTip(tr("Show UARTScope output format guide (copy for AI)")); helpAction->setToolTip(tr("Show UARTScope output format guide (copy for AI)"));
connect(helpAction, &QAction::triggered, this, &MainWindow::showFormatReference); connect(helpAction, &QAction::triggered, this, &MainWindow::showFormatReference);
tb->addSeparator(); tb->addSeparator();
// ── About ─────────────────────────────────────────────────────────────────
auto *aboutAction = tb->addAction(tr("Über UARTScope")); auto *aboutAction = tb->addAction(tr("Über UARTScope"));
connect(aboutAction, &QAction::triggered, this, &MainWindow::showAbout); connect(aboutAction, &QAction::triggered, this, &MainWindow::showAbout);
} }
@@ -197,7 +183,7 @@ void MainWindow::setupStatusBar()
m_reconnectLabel = new QLabel(this); m_reconnectLabel = new QLabel(this);
m_portLabel = 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_stateLabel);
statusBar()->addWidget(m_reconnectLabel); statusBar()->addWidget(m_reconnectLabel);
@@ -223,7 +209,6 @@ void MainWindow::onConnectClicked()
Qt::QueuedConnection, Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.logFilePath)); Q_ARG(QString, m_lastConfig.logFilePath));
// Push current reconnect settings to worker before opening
const bool autoRec = m_autoReconnectCb->isChecked(); const bool autoRec = m_autoReconnectCb->isChecked();
const int interval = m_reconnectIntervalSb->value(); const int interval = m_reconnectIntervalSb->value();
QMetaObject::invokeMethod(m_worker, [this, autoRec, interval] { QMetaObject::invokeMethod(m_worker, [this, autoRec, interval] {
@@ -277,13 +262,13 @@ void MainWindow::onReconnecting(int attempt)
void MainWindow::onClearScreen() 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(); clearAllViews();
} }
void MainWindow::onError(const QString &message) 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()) { if (!m_autoReconnectCb->isChecked()) {
QMessageBox::critical(this, tr("Serial error"), message); QMessageBox::critical(this, tr("Serial error"), message);
onPortClosed(); onPortClosed();
@@ -306,6 +291,8 @@ void MainWindow::clearAllViews()
m_rawView->clear(); m_rawView->clear();
m_tableView->clear(); m_tableView->clear();
m_tagWidget->clearAll(); m_tagWidget->clearAll();
if (m_videoWidget)
m_videoWidget->clearFrame();
} }
// ── Format reference dialog ──────────────────────────────────────────────── // ── Format reference dialog ────────────────────────────────────────────────
@@ -322,12 +309,11 @@ three parallel ways. You can use any combination.
1. RAW OUTPUT (always active) 1. RAW OUTPUT (always active)
Every line sent over UART is shown verbatim in the "Raw output" tab. Every line sent over UART is shown verbatim (with a timestamp) in the
No special formatting needed. "Raw output" tab. No special formatting needed.
uart_printf("Boot complete. Version 1.0\n"); ANSI clear-screen clears ALL views simultaneously (raw output, table,
tag monitor, and the video preview frame):
ANSI clear-screen clears ALL views simultaneously:
uart_printf("\033[2J\033[H"); // standard VT100 clear-screen + cursor home 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 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 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: TAG FORMAT:
[TAGNAME] key1=value1 key2=value2 ... [TAGNAME] key1=value1 key2=value2 ...
Rules: Rules:
Tag name: uppercase letters, digits, underscore. Examples: WDG, VIDEO, I2C_BUS 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 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 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: Examples:
uart_printf("[WDG] uptime=%lu free_heap=%lu temp=%d\n", uart_printf("[WDG] uptime=%lu free_heap=%lu temp=%d\n",
uptime_ms, heap_free, cpu_temp); uptime_ms, heap_free, cpu_temp);
@@ -354,11 +348,7 @@ Examples:
uart_printf("[VIDEO] line=%d hblank=%d vblank=%d dma_buf=%d\n", uart_printf("[VIDEO] line=%d hblank=%d vblank=%d dma_buf=%d\n",
scanline, hblank_cycles, vblank_cycles, dma_remaining); scanline, hblank_cycles, vblank_cycles, dma_remaining);
uart_printf("[AUDIO] buf=%d underruns=%lu vol=%d\n", uart_printf("[KICKSTART] Libs=graphics.library;intuition.library;dos.library\n");
audio_buf_level, underrun_count, volume);
uart_printf("[I2C] addr=0x%02X ack=%d err=%d\n",
i2c_addr, ack_received, error_code);
Each unique tag gets its own panel. Panels update in-place so repeated Each unique tag gets its own panel. Panels update in-place so repeated
messages don't scroll past perfect for periodic watchdog output. 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) 5. COMPLETE EXAMPLE (Chica / Amiga hardware emulation)
void uart_status_update(void) { void uart_status_update(void) {
// Watchdog heartbeat → Tag Monitor [WDG]
uart_printf("[WDG] uptime=%lu free=%lu load=%d temp=%d\n", uart_printf("[WDG] uptime=%lu free=%lu load=%d temp=%d\n",
HAL_GetTick(), xPortGetFreeHeapSize(), HAL_GetTick(), xPortGetFreeHeapSize(),
cpu_load_percent, core_temp_c); cpu_load_percent, core_temp_c);
// Video timing → Tag Monitor [VIDEO]
uart_printf("[VIDEO] line=%d hblank=%d vblank=%d copper=%d\n", uart_printf("[VIDEO] line=%d hblank=%d vblank=%d copper=%d\n",
current_scanline, hblank_ticks, vblank_ticks, copper_dma); current_scanline, hblank_ticks, vblank_ticks, copper_dma);
// Audio state → Tag Monitor [AUDIO] uart_printf("[KICKSTART] Devs=trackdisk.device;input.device;console.device\n");
uart_printf("[AUDIO] ch=%d buf=%d underruns=%lu\n", uart_printf("[KICKSTART] Libs=graphics.library;intuition.library\n");
active_channels, buffer_fill, total_underruns);
// Structured CSV data → Table view // Structured CSV data → Table view
uart_printf("%lu,%.1f,%d,%d\n", uart_printf("%lu,%.1f,%d,%d\n",
HAL_GetTick(), vcc_mv / 1000.0f, cpu_load_percent, core_temp_c); 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: // Clear screen from firmware when you want a fresh start:
// uart_printf("\033[2J\033[H"); // uart_printf("\033[2J\033[H");
)"; )";
// ── Dialog ────────────────────────────────────────────────────────────────
auto *dlg = new QDialog(this); auto *dlg = new QDialog(this);
dlg->setWindowTitle(tr("UARTScope Format Reference")); dlg->setWindowTitle(tr("UARTScope Format Reference"));
dlg->resize(720, 620); dlg->resize(720, 620);
@@ -475,7 +460,6 @@ void MainWindow::configureTagFilter()
QFont mono("Monospace"); QFont mono("Monospace");
mono.setStyleHint(QFont::Monospace); mono.setStyleHint(QFont::Monospace);
edit->setFont(mono); edit->setFont(mono);
// Pre-populate with current filter
QStringList current(m_suppressedTags.values()); QStringList current(m_suppressedTags.values());
current.sort(); current.sort();
edit->setPlainText(current.join('\n')); edit->setPlainText(current.join('\n'));
@@ -495,66 +479,12 @@ void MainWindow::configureTagFilter()
if (!tag.isEmpty()) if (!tag.isEmpty())
m_suppressedTags.insert(tag); m_suppressedTags.insert(tag);
} }
// Persist immediately so the filter survives even an unexpected exit.
saveSettings();
} }
dlg->deleteLater(); 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 ────────────────────────────────────────────────────────── // ── About dialog ──────────────────────────────────────────────────────────
void MainWindow::showAbout() void MainWindow::showAbout()
@@ -567,7 +497,6 @@ void MainWindow::showAbout()
root->setContentsMargins(0, 0, 0, 0); root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0); root->setSpacing(0);
// ── Header block (dark accent) ────────────────────────────────────────────
auto *header = new QWidget(dlg); auto *header = new QWidget(dlg);
header->setFixedHeight(110); header->setFixedHeight(110);
header->setStyleSheet("background-color: #1e1e1e;"); header->setStyleSheet("background-color: #1e1e1e;");
@@ -575,14 +504,12 @@ void MainWindow::showAbout()
headerLayout->setContentsMargins(20, 14, 24, 14); headerLayout->setContentsMargins(20, 14, 24, 14);
headerLayout->setSpacing(16); headerLayout->setSpacing(16);
// Logo
auto *logoLabel = new QLabel(header); auto *logoLabel = new QLabel(header);
const QPixmap logoPix(":/icons/UartscopeLogo.png"); const QPixmap logoPix(":/icons/UartscopeLogo.png");
logoLabel->setPixmap(logoPix.scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation)); logoLabel->setPixmap(logoPix.scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation));
logoLabel->setStyleSheet("background: transparent;"); logoLabel->setStyleSheet("background: transparent;");
logoLabel->setFixedSize(72, 72); logoLabel->setFixedSize(72, 72);
// Title + subtitle
auto *titleBlock = new QVBoxLayout(); auto *titleBlock = new QVBoxLayout();
titleBlock->setSpacing(4); titleBlock->setSpacing(4);
auto *titleLabel = new QLabel("UARTScope", header); auto *titleLabel = new QLabel("UARTScope", header);
@@ -598,7 +525,6 @@ void MainWindow::showAbout()
headerLayout->addLayout(titleBlock); headerLayout->addLayout(titleBlock);
root->addWidget(header); root->addWidget(header);
// ── Info grid ─────────────────────────────────────────────────────────────
auto *body = new QWidget(dlg); auto *body = new QWidget(dlg);
body->setStyleSheet("background-color: #252526;"); body->setStyleSheet("background-color: #252526;");
auto *grid = new QGridLayout(body); auto *grid = new QGridLayout(body);
@@ -628,7 +554,6 @@ void MainWindow::showAbout()
root->addWidget(body, 1); root->addWidget(body, 1);
// ── Footer with close button ──────────────────────────────────────────────
auto *footer = new QWidget(dlg); auto *footer = new QWidget(dlg);
footer->setStyleSheet("background-color: #252526; border-top: 1px solid #3c3c3c;"); footer->setStyleSheet("background-color: #252526; border-top: 1px solid #3c3c3c;");
auto *footerLayout = new QHBoxLayout(footer); auto *footerLayout = new QHBoxLayout(footer);
@@ -646,3 +571,54 @@ void MainWindow::showAbout()
dlg->exec(); dlg->exec();
dlg->deleteLater(); 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->setContentsMargins(4, 4, 4, 4);
mainLayout->setSpacing(4); mainLayout->setSpacing(4);
// ── Toolbar ──────────────────────────────────────────────────────────────
auto *toolbar = new QHBoxLayout(); auto *toolbar = new QHBoxLayout();
m_searchEdit = new QLineEdit(this); m_searchEdit = new QLineEdit(this);
@@ -33,7 +32,7 @@ void RawView::setupUi()
connect(m_searchEdit, &QLineEdit::textChanged, this, &RawView::onSearch); connect(m_searchEdit, &QLineEdit::textChanged, this, &RawView::onSearch);
m_autoScrollCb = new QCheckBox(tr("Auto-scroll"), this); 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); connect(m_autoScrollCb, &QCheckBox::toggled, this, &RawView::onAutoScrollToggled);
m_lineCountLbl = new QLabel(tr("Lines: 0"), this); m_lineCountLbl = new QLabel(tr("Lines: 0"), this);
@@ -54,11 +53,10 @@ void RawView::setupUi()
toolbar->addWidget(m_clearBtn); toolbar->addWidget(m_clearBtn);
mainLayout->addLayout(toolbar); mainLayout->addLayout(toolbar);
// ── Text area ─────────────────────────────────────────────────────────────
m_textEdit = new QPlainTextEdit(this); m_textEdit = new QPlainTextEdit(this);
m_textEdit->setReadOnly(true); m_textEdit->setReadOnly(true);
m_textEdit->setLineWrapMode(QPlainTextEdit::NoWrap); m_textEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
m_textEdit->setMaximumBlockCount(0); // unlimited m_textEdit->setMaximumBlockCount(0);
QFont monoFont("Monospace"); QFont monoFont("Monospace");
monoFont.setStyleHint(QFont::Monospace); monoFont.setStyleHint(QFont::Monospace);
@@ -84,39 +82,27 @@ void RawView::applyColorScheme()
void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTags) void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTags)
{ {
// Check whether this line belongs to a suppressed tag
if (!suppressedTags.isEmpty()) { if (!suppressedTags.isEmpty()) {
const auto match = s_tagRe.match(line); const auto match = s_tagRe.match(line);
if (match.hasMatch()) { if (match.hasMatch() && suppressedTags.contains(match.captured(1).toUpper()))
if (suppressedTags.contains(match.captured(1).toUpper())) return;
return; // silently drop it goes to the Tag Monitor only
}
} }
++m_lineCount; ++m_lineCount;
m_lineCountLbl->setText(tr("Lines: %1").arg(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 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(); const bool hasTag = s_tagRe.match(line).hasMatch();
QTextCursor cursor(m_textEdit->document()); QTextCursor cursor(m_textEdit->document());
cursor.movePosition(QTextCursor::End); 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; QTextCharFormat tsFmt;
tsFmt.setForeground(QColor(0x60, 0x60, 0x60)); tsFmt.setForeground(QColor(0x60, 0x60, 0x60));
cursor.insertText(QStringLiteral("[%1] ").arg(ts), tsFmt); 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); cursor.insertText(line + '\n', fmt);
if (m_autoScroll) if (m_autoScroll)

View File

@@ -20,7 +20,7 @@ SerialWorker::~SerialWorker()
closePort(); closePort();
} }
// ── Public slots ──────────────────────────────────────────────────────────── // ── public slots ──────────────────────────────────────────────────────────────
void SerialWorker::openPort(const QString &portName, qint32 baudRate, void SerialWorker::openPort(const QString &portName, qint32 baudRate,
QSerialPort::DataBits dataBits, QSerialPort::DataBits dataBits,
@@ -28,7 +28,6 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
QSerialPort::StopBits stopBits, QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl) QSerialPort::FlowControl flowControl)
{ {
// Save config for auto-reconnect
m_portName = portName; m_portName = portName;
m_baudRate = baudRate; m_baudRate = baudRate;
m_dataBits = dataBits; m_dataBits = dataBits;
@@ -55,6 +54,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
return; return;
} }
m_buffer.clear(); m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0; m_reconnectAttempt = 0;
emit portOpened(); emit portOpened();
} }
@@ -107,7 +107,7 @@ void SerialWorker::stopLogging()
} }
} }
// ── Private slots ─────────────────────────────────────────────────────────── // ── private slots ───────────────────────────────────────────────────────────
void SerialWorker::onReadyRead() void SerialWorker::onReadyRead()
{ {
@@ -119,7 +119,6 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
if (err == QSerialPort::NoError) if (err == QSerialPort::NoError)
return; return;
// ResourceError = device physically disconnected (USB pull, power loss)
const bool fatal = (err == QSerialPort::ResourceError || const bool fatal = (err == QSerialPort::ResourceError ||
err == QSerialPort::DeviceNotFoundError); err == QSerialPort::DeviceNotFoundError);
@@ -132,8 +131,6 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
emit errorOccurred(m_port->errorString()); 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() void SerialWorker::tryReconnect()
@@ -153,6 +150,7 @@ void SerialWorker::tryReconnect()
if (m_port->open(QIODevice::ReadOnly)) { if (m_port->open(QIODevice::ReadOnly)) {
m_buffer.clear(); m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0; m_reconnectAttempt = 0;
emit portOpened(); emit portOpened();
} else { } else {
@@ -160,7 +158,7 @@ void SerialWorker::tryReconnect()
} }
} }
// ── Private helpers ───────────────────────────────────────────────────────── // ── private helpers ─────────────────────────────────────────────────────────
void SerialWorker::scheduleReconnect() void SerialWorker::scheduleReconnect()
{ {
@@ -170,23 +168,48 @@ void SerialWorker::scheduleReconnect()
void SerialWorker::processRawData(const QByteArray &data) void SerialWorker::processRawData(const QByteArray &data)
{ {
// Scan raw bytes for ANSI clear-screen BEFORE UTF-8 conversion so we // ── ANSI clear-screen detection ────────────────────────────────────────
// never miss a sequence that straddles a read boundary. // We scan a buffer that includes a small tail from the PREVIOUS chunk so
// We look for ESC[2J (optionally followed by ESC[H or ESC[1;1H) // that a sequence split across two readyRead() calls (e.g. "...\x1b" +
// ESC = 0x1B // "[2J\x1b[H...") is still detected reliably. The clear sequence itself
const QByteArray esc2j("\x1b[2J"); // is also stripped from what reaches processLine()/the line buffer.
if (data.contains(esc2j)) { QByteArray combined = m_scanTail + data;
emit clearScreen(); bool sawClear = false;
// Strip all ANSI escape sequences from the data before further processing
// so they don't clutter the raw view. // 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 // Keep the last few bytes as the new tail (in case a sequence is cut off
// Sequence: ESC [ <params> <letter> // 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( static const QRegularExpression ansiRe(
"\x1b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])"); "\x1b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])");
QString text = QString::fromUtf8(data); QString text = QString::fromUtf8(toProcess);
text.remove(ansiRe); text.remove(ansiRe);
m_buffer += text; m_buffer += text;

View File

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

View File

@@ -12,25 +12,22 @@ TagPanel::TagPanel(const QString &tag, QWidget *parent)
layout->setContentsMargins(6, 14, 6, 6); layout->setContentsMargins(6, 14, 6, 6);
layout->setSpacing(4); layout->setSpacing(4);
// ── Table for key=value pairs ─────────────────────────────────────────────
m_table = new QTableWidget(0, 2, this); m_table = new QTableWidget(0, 2, this);
m_table->setHorizontalHeaderLabels({tr("Key"), tr("Value")}); m_table->setHorizontalHeaderLabels({tr("Key"), tr("Value")});
m_table->horizontalHeader()->setStretchLastSection(true); m_table->horizontalHeader()->setStretchLastSection(true);
m_table->verticalHeader()->hide(); m_table->verticalHeader()->hide();
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setAlternatingRowColors(true); m_table->setAlternatingRowColors(true);
m_table->setMaximumHeight(200); m_table->setMaximumHeight(220);
m_table->verticalHeader()->setDefaultSectionSize(22); m_table->verticalHeader()->setDefaultSectionSize(22);
layout->addWidget(m_table); layout->addWidget(m_table);
// ── Fallback label for non-kv data ────────────────────────────────────────
m_rawLabel = new QLabel(this); m_rawLabel = new QLabel(this);
m_rawLabel->setWordWrap(true); m_rawLabel->setWordWrap(true);
m_rawLabel->setStyleSheet("font-family: monospace;"); m_rawLabel->setStyleSheet("font-family: monospace;");
m_rawLabel->hide(); m_rawLabel->hide();
layout->addWidget(m_rawLabel); layout->addWidget(m_rawLabel);
// ── Copy button ───────────────────────────────────────────────────────────
auto *btnRow = new QHBoxLayout(); auto *btnRow = new QHBoxLayout();
btnRow->addStretch(); btnRow->addStretch();
auto *copyBtn = new QPushButton(tr("📋 Copy"), this); auto *copyBtn = new QPushButton(tr("📋 Copy"), this);
@@ -61,10 +58,59 @@ void TagPanel::parseKeyValue(const QString &value)
auto it = kvRe.globalMatch(value); auto it = kvRe.globalMatch(value);
while (it.hasNext()) { while (it.hasNext()) {
const auto match = it.next(); const auto match = it.next();
ensureRow(match.captured(1)); const QString key = match.captured(1);
const int row = m_keys.indexOf(match.captured(1)); const QString raw = match.captured(2);
m_table->item(row, 1)->setText(match.captured(2));
m_table->item(row, 1)->setBackground(QColor(0x2d, 0x5a, 0x2d)); // flash green // 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); 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() void TagPanel::copyToClipboard()
{ {
QStringList lines; QStringList lines;
lines << QStringLiteral("[%1]").arg(m_tag); lines << QStringLiteral("[%1]").arg(m_tag);
if (m_table->isVisible()) { if (m_table->isVisible()) {
QString lastKey;
for (int r = 0; r < m_table->rowCount(); ++r) { for (int r = 0; r < m_table->rowCount(); ++r) {
const QString key = m_table->item(r, 0)->text(); const QString key = m_table->item(r, 0)->text();
const QString val = m_table->item(r, 1)->text(); const QString val = m_table->item(r, 1)->text();
if (!key.isEmpty()) {
lines << QStringLiteral(" %1 = %2").arg(key, val); lines << QStringLiteral(" %1 = %2").arg(key, val);
lastKey = key;
} else {
lines << QStringLiteral(" %1").arg(val); // continuation row
}
} }
} else { } else {
lines << QStringLiteral(" %1").arg(m_rawLabel->text()); lines << QStringLiteral(" %1").arg(m_rawLabel->text());

View File

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

View File

@@ -10,7 +10,6 @@
#include <QBuffer> #include <QBuffer>
#include <QImageReader> #include <QImageReader>
#include <QThread>
static int xioctl(int fd, unsigned long req, void *arg) 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; return r;
} }
V4l2Worker::V4l2Worker(QObject *parent) V4l2Worker::V4l2Worker(QObject *parent) : QObject(parent) {}
: QObject(parent)
{}
V4l2Worker::~V4l2Worker() V4l2Worker::~V4l2Worker() { stop(); }
{
stop();
}
// ── public slots ──────────────────────────────────────────────────────────────
void V4l2Worker::startCapture(const QString &device, int width, int height) 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; return;
m_running.store(true); 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(); }); m_captureThread = std::thread([this] { captureLoop(); });
} }
void V4l2Worker::stopCapture() void V4l2Worker::stopCapture() { stop(); }
{
stop();
}
// ── internal stop (safe to call from any thread) ──────────────────────────────
void V4l2Worker::stop() void V4l2Worker::stop()
{ {
m_running.store(false); m_running.store(false);
if (m_captureThread.joinable()) if (m_captureThread.joinable())
m_captureThread.join(); // wait for captureLoop() to exit m_captureThread.join();
closeDevice(); closeDevice();
} }
// ── capture loop (runs in m_captureThread) ────────────────────────────────────
void V4l2Worker::captureLoop() void V4l2Worker::captureLoop()
{ {
v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
@@ -75,7 +57,7 @@ void V4l2Worker::captureLoop()
fd_set fds; fd_set fds;
FD_ZERO(&fds); FD_ZERO(&fds);
FD_SET(m_fd, &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); const int r = ::select(m_fd + 1, &fds, nullptr, nullptr, &tv);
if (r == 0) continue; if (r == 0) continue;
if (r == -1) { if (r == -1) {
@@ -108,8 +90,6 @@ void V4l2Worker::captureLoop()
emit captureStopped(); emit captureStopped();
} }
// ── device open / close ───────────────────────────────────────────────────────
bool V4l2Worker::openDevice(const QString &device, int width, int height) bool V4l2Worker::openDevice(const QString &device, int width, int height)
{ {
m_width = width; m_width = width;
@@ -134,10 +114,7 @@ bool V4l2Worker::openDevice(const QString &device, int width, int height)
} }
const uint32_t preferred[] = { const uint32_t preferred[] = {
V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_YUYV, V4L2_PIX_FMT_NV12, 0
V4L2_PIX_FMT_YUYV,
V4L2_PIX_FMT_NV12,
0
}; };
m_pixFmt = 0; m_pixFmt = 0;

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
format = 2 format = 2
pkgname = uartscope pkgname = uartscope
pkgbase = uartscope pkgbase = uartscope
pkgver = 1.0.0.r1.g2181f25-1 pkgver = 1.0.0.r2.g92168ee-1
pkgarch = x86_64 pkgarch = x86_64
pkgbuild_sha256sum = 367d5f02c03027788d532bdeda7084f79008b9dcf902393b209ad7f2ae43859c pkgbuild_sha256sum = ec7fe9ddf4a5966cba66edc3f38e169bdaeeffd2a0b4206e481794ce35a30737
packager = Unknown Packager packager = Unknown Packager
builddate = 1780988717 builddate = 1781039770
builddir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git builddir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git
startdir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git startdir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git
buildtool = makepkg 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 = tumbler-4.20.1-1-x86_64
installed = twolame-0.4.0-4-x86_64 installed = twolame-0.4.0-4-x86_64
installed = tzdata-2026b-1-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 = uchardet-0.0.8-4-x86_64
installed = udisks2-2.11.1-2-x86_64 installed = udisks2-2.11.1-2-x86_64
installed = unifdef-2.12-4-x86_64 installed = unifdef-2.12-4-x86_64

Binary file not shown.

View File

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

View File

@@ -2,7 +2,7 @@
Name=UARTScope Name=UARTScope
Comment=UART Serial Monitor Comment=UART Serial Monitor
Exec=uartscope Exec=uartscope
Icon=utilities-terminal Icon=uartscope
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Development;Utility; 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 cc102c93eb17f7b910d8e74c3505f198bed77f10 not-for-merge tag 'v1.0.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope