diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ed10e9..8cf9535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(UARTScope VERSION 1.1.0 LANGUAGES CXX) +project(UARTScope VERSION 1.2.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index 915a80d..488eb13 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,25 @@ Ein moderner UART-Monitor für Linux mit Qt6-Oberfläche. Gebaut als vollwertige | Feature | Beschreibung | |---|---| -| **Unbegrenzter Verlauf** | Kein Zeilenlimit – keine Daten gehen verloren, egal wie viel der Pi sendet | +| **Serieller UND Netzwerk-Input** | Verbindung wahlweise über einen echten/virtuellen seriellen Port **oder** eine reine TCP-Verbindung – praktisch für Emulatoren wie FS-UAE, deren Debug-UART per `socat` auf einen TCP-Port gelegt wird | +| **PTY-Erkennung** | Pseudo-Terminals unter `/dev/pts/` (z.B. von `qemu -serial pty`) werden im Port-Dropdown automatisch mitgelistet | +| **Unbegrenzter Verlauf** | Kein Zeilenlimit – keine Daten gehen verloren, egal wie viel gesendet wird | | **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-Monitor mit Verlauf** | Zeilen mit `[TAG]` werden in eigenen Panels angezeigt; jedes Panel hat einen „Aktuell"-Tab (Live-Stand) und einen „Verlauf"-Tab (alle je empfangenen Werte, timestamped) | | **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 | +| **Tag-Filter** | Tags können aus dem Raw-View ausgeblendet werden – wird dauerhaft gespeichert; auch Tag-Namen mit Bindestrich (z.B. `I2C-BUS`) werden korrekt erkannt | | **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) | +| **Suche mit Navigation** | Inkrementelle Volltextsuche im Raw-View mit Weiter/Zurück (Buttons, Enter, F3/Shift+F3) und Wrap-Around | +| **Auto-Reconnect** | Bei Verbindungsabbruch wird automatisch neu verbunden (Intervall einstellbar) – funktioniert für seriell und TCP gleichermaßen | | **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 (Aktuell- oder Verlauf-Tab) haben einen „📋 Copy"-Button | | **V4L2 Live Video** | HDMI-Grabber direkt eingebunden – Live-Vorschau unter dem Tag-Monitor | +| **Video-Aufzeichnung (MP4)** | Live-Vorschau per Knopfdruck als MP4 aufzeichnen (H.264, via `ffmpeg`) | | **Screenshot** | Aktuellen Frame des HDMI-Grabbers als PNG/JPG speichern | +| **Automatischer Screenshot-Trigger** | `[SCREENSHOT] dateiname` als Steuertag: der Frame wird exakt im Moment des Empfangs gespeichert – ideal um z.B. einen kurz angezeigten Memory-Dump zuverlässig zu erwischen | | **Format-Referenz** | Eingebauter Guide (mit Kopieren-Button) für KI-kompatible UART-Formatierung | | **Fensterlayout** | Fenstergröße, Position, Splitter-Positionen und Tag-Filter werden beim Beenden gespeichert | @@ -32,16 +36,18 @@ Ein moderner UART-Monitor für Linux mit Qt6-Oberfläche. Gebaut als vollwertige ```bash # Arch (empfohlen) -sudo pacman -S cmake qt6-base qt6-serialport +sudo pacman -S cmake qt6-base qt6-serialport ffmpeg # Ubuntu / Debian -sudo apt install cmake qt6-base-dev qt6-serialport-dev libqt6serialport6-dev +sudo apt install cmake qt6-base-dev qt6-serialport-dev libqt6serialport6-dev ffmpeg # Fedora -sudo dnf install cmake qt6-qtbase-devel qt6-qtserialport-devel +sudo dnf install cmake qt6-qtbase-devel qt6-qtserialport-devel ffmpeg ``` -V4L2 benötigt keine zusätzliche Library – `linux/videodev2.h` ist Teil der Standard-Kernel-Header. +V4L2 benötigt keine zusätzliche Library – `linux/videodev2.h` ist Teil der Standard-Kernel-Header. Das TCP-Netzwerk-Modul (`Qt6::Network`) ist üblicherweise Teil des `qt6-base`-Pakets und braucht keine separate Installation. + +`ffmpeg` ist **nur** für die Video-Aufzeichnung (MP4) nötig – ohne installiertes `ffmpeg` funktioniert der Rest von UARTScope normal, nur der „⏺ Record"-Button zeigt dann eine Fehlermeldung. --- @@ -62,6 +68,69 @@ sudo cmake --install build --- +## Verbindung: Seriell oder Netzwerk (TCP) + +Im Connect-Dialog gibt es zwei Tabs: + +### Seriell +Wie gewohnt: Port aus der Liste wählen (echte Geräte **und** Pseudo-Terminals unter `/dev/pts/` werden angezeigt), Baudrate/Parität/Stopbits/Flow Control einstellen. + +**Virtuelle serielle Geräte (z.B. QEMU):** +```bash +qemu-system-xxx -serial pty ... +# QEMU gibt im Log den zugewiesenen Pfad aus, z.B.: +# char device redirected to /dev/pts/4 +``` +Den ausgegebenen Pfad im Seriell-Tab auswählen (ggf. vorher ↻ zum Neuladen der Liste drücken). Baudrate & Co. werden von virtuellen PTYs ignoriert – das ist unschädlich. + +### Netzwerk (TCP) +Host/IP und Port angeben statt eines seriellen Ports. Gedacht für Firmware/Emulatoren, die ihre Debug-UART über TCP senden statt über ein echtes serielles Gerät – zum Beispiel FS-UAE, dessen serielle Schnittstelle per `socat` auf einen TCP-Port gelegt wird: + +```bash +# Beispiel: FS-UAEs serielle Ausgabe per socat auf TCP-Port 1234 legen +socat /tmp/fs-uae-serial TCP-LISTEN:1234,reuseaddr,fork +``` + +Danach im Netzwerk-Tab `localhost` und Port `1234` eintragen und verbinden. Auto-Reconnect funktioniert identisch zum seriellen Modus – bricht die TCP-Verbindung ab, wird automatisch neu verbunden. + +--- + +## Video-Aufzeichnung (MP4) + +Die Live-Vorschau im Video-Widget lässt sich per „⏺ Record"-Button als MP4-Datei aufzeichnen. Dazu wird intern ein `ffmpeg`-Prozess gestartet, dem die rohen Frames als BGRA über eine Pipe zugeführt werden; `ffmpeg` encodiert sie zu H.264/MP4. + +- **Voraussetzung:** `ffmpeg` muss installiert und im `PATH` sein (siehe [Voraussetzungen](#voraussetzungen)) +- Die Aufzeichnung läuft unabhängig vom Freeze-Status weiter – Freeze pausiert nur die Vorschau, nicht die Aufnahme +- Timestamps basieren auf der Systemuhr (`-use_wallclock_as_timestamps`), damit die Aufnahme auch bei schwankender Framerate des Grabbers die reale Dauer korrekt wiedergibt +- Ein Auflösungswechsel während der Aufnahme, ein Stopp der Capture oder ein Fehler beenden die Aufzeichnung automatisch sauber + +--- + +## Automatischer Screenshot-Trigger + +Manuelles Klicken auf den Screenshot-Button trifft bei schnellen Hardware-Ereignissen (z.B. einem kurz angezeigten Memory-Dump) selten genau den richtigen Moment – menschliche Reaktionszeit liegt bei mehreren hundert Millisekunden, in der Hardware-Welt passieren in dieser Zeit unzählige Instruktionen. Dafür gibt es den Steuertag `[SCREENSHOT]`: + +```c +uart_printf("[SCREENSHOT] memdump_%lu.png\n", HAL_GetTick()); +``` + +Sobald diese Zeile empfangen wird, speichert UARTScope **sofort** den Frame, der zu diesem Zeitpunkt im V4L2-Vorschaupuffer liegt – kein Dialog, keine Nutzerinteraktion nötig. Die Verzögerung besteht nur noch aus UART-Übertragungszeit und einem Event-Loop-Tick (typischerweise einstellige Millisekunden), statt aus menschlicher Reaktionszeit. + +**Einrichtung:** +1. In der Toolbar auf „Screenshot folder…" klicken und den Zielordner wählen +2. Die Einstellung wird sofort gespeichert und bei jedem Programmstart wiederhergestellt + +**Verhalten:** +- Ohne konfigurierten Ordner wird der Trigger zwar erkannt (erscheint im Tag-Monitor und Raw-View), aber es wird keine Datei geschrieben – das Ergebnis jedes Trigger-Versuchs (Erfolg oder Fehler, z.B. „kein Ordner konfiguriert" oder „noch kein Frame verfügbar") erscheint kurz in der Statusleiste **und** dauerhaft als `[UARTSCOPE] ...`-Zeile im Raw-View, direkt neben der eigentlichen `[SCREENSHOT]`-Zeile – so geht eine Fehlermeldung nie unbemerkt unter +- `` kann die Endung weglassen (Standard: `.png`); eventuelle Pfadanteile in der Firmware-Angabe werden ignoriert – die Datei landet immer direkt im konfigurierten Ordner (kein Path-Traversal möglich) +- Wird derselbe Dateiname mehrfach gesendet (z.B. immer `dump.png`), wird nichts überschrieben – UARTScope hängt automatisch `_1`, `_2`, … an +- Die Zeile erscheint zusätzlich ganz normal im Tag-Monitor unter `[SCREENSHOT]` – inklusive „Verlauf"-Tab, der eine timestamped Liste aller Trigger-Ereignisse **und** deren Ergebnis zeigt: zuerst der angeforderte Dateiname, direkt danach das Resultat („Screenshot saved: /pfad/..." oder eine Fehlermeldung) – beides chronologisch im selben Panel +- Funktioniert unabhängig vom Freeze-Status der Vorschau (wie der manuelle Screenshot-Button auch) + +**Falls kein Bild erscheint:** die häufigsten zwei Ursachen sind (1) der Screenshot-Ordner wurde noch nicht gesetzt, oder (2) die V4L2-Vorschau läuft nicht (▶ Start im Video-Widget noch nicht geklickt, es gibt also noch keinen Frame zum Speichern). In beiden Fällen steht die genaue Ursache als `[UARTSCOPE] ...`-Zeile im Raw-View. + +--- + ## Berechtigungen ```bash @@ -84,9 +153,11 @@ v4l2-ctl -d /dev/video0 --list-formats-ext # unterstützte Formate & Auflösung ## UART-Ausgabe formatieren +Alles Folgende gilt unabhängig davon, ob die Verbindung über einen seriellen Port oder über TCP läuft (siehe Abschnitt „Verbindung: Seriell oder Netzwerk (TCP)" weiter oben) – UARTScope interpretiert den empfangenen Text in beiden Fällen identisch. + ### Raw-View (immer aktiv) -Jede UART-Zeile erscheint im Raw-View mit Timestamp. Keine besondere Formatierung nötig. +Jede Zeile erscheint im Raw-View mit Timestamp. Keine besondere Formatierung nötig. **Screen leeren** – von der Firmware aus Raw-View, Tabelle, Tag-Monitor und Video-Vorschau gleichzeitig leeren: ```c @@ -99,10 +170,16 @@ Zeilen mit `[TAGNAME]` werden im Tag-Monitor-Panel angezeigt **und** im Raw-View **Format:** `[TAGNAME] key1=value1 key2=value2 ...` -- Tag-Name: Buchstaben, Ziffern, Underscore – z.B. `WDG`, `VIDEO`, `KICKSTART` +- Tag-Name: Buchstaben, Ziffern, Underscore **und Bindestrich** – z.B. `WDG`, `VIDEO`, `KICKSTART`, `I2C-BUS` - Key=Value-Paare: Leerzeichen-getrennt, Werte ohne Leerzeichen - Ohne Key=Value-Paare wird der rohe String angezeigt +Jedes Tag-Panel hat zwei Tabs: +- **Aktuell** – der letzte empfangene Stand, aktualisiert sich in-place (wie bisher) +- **Verlauf** – jeder je empfangene Wert dieses Tags, chronologisch mit Timestamp, mit eigenem „Verlauf leeren"-Button (löscht nur die Historie, das Panel bleibt bestehen) + +Der Copy-Button kopiert je nach aktivem Tab entweder den aktuellen Stand oder den kompletten Verlauf. + **Listen-Werte:** Ein Wert mit `;`-getrennten Einträgen wird als mehrzeilige Liste unter dem Key dargestellt: ```c @@ -143,6 +220,14 @@ Optionale Header-Zeile mit `#`, dann CSV-Datenzeilen: Delimiter per Dropdown umschaltbar: `,` `;` `\t` `|` `Space` +### Suche im Raw-View + +Suchbegriff eingeben – der erste Treffer wird automatisch markiert. Weitere Treffer: + +- **▼** / `Enter` / `F3` → nächster Treffer +- **▲** / `Shift+F3` → vorheriger Treffer +- Ist kein weiterer Treffer in Suchrichtung vorhanden, springt die Suche automatisch an den Anfang (bzw. bei Rückwärtssuche ans Ende) und macht dort weiter (Wrap-Around) + --- ## Tag-Filter @@ -159,6 +244,7 @@ Beim Beenden werden folgende Einstellungen automatisch gespeichert und beim näc - Splitter-Positionen (Haupt-Splitter und Tag/Video-Splitter) - Auto-Reconnect ein/aus und Intervall - Tag-Filter (ausgeblendete Tags) +- Screenshot-Ordner für den `[SCREENSHOT]`-Steuertag Gespeichert unter `~/.config/ChicaDev/UARTScope.conf` (via `QSettings`). @@ -174,15 +260,15 @@ uartscope/ ├── UartscopeLogo.png ├── uartscope.desktop.in ├── include/ -│ ├── mainwindow.h ← Hauptfenster, koordiniert alle Komponenten -│ ├── serialworker.h ← UART-Empfang im eigenen QThread, Auto-Reconnect, ANSI-Erkennung -│ ├── rawview.h ← Unbegrenzter Log mit Timestamps, Suche, Copy +│ ├── mainwindow.h ← Hauptfenster, koordiniert alle Komponenten, Screenshot-Ordner-Einstellung +│ ├── serialworker.h ← Empfang im eigenen QThread (seriell ODER TCP), Auto-Reconnect, ANSI-Erkennung, [SCREENSHOT]-Steuertag +│ ├── rawview.h ← Unbegrenzter Log mit Timestamps, Suche (Weiter/Zurück, Wrap-Around), Copy │ ├── tableview.h ← CSV-Parser → QTableWidget │ ├── tagwidget.h ← Container für Tag-Panels -│ ├── tagpanel.h ← Ein Panel pro [TAG], Key=Value-Tabelle mit Listen-Support, Copy -│ ├── connectdialog.h ← Port-Konfiguration (Port, Baud, Log-Datei) +│ ├── tagpanel.h ← Ein Panel pro [TAG]: Aktuell-Tab + Verlauf-Tab, Listen-Support, Copy +│ ├── connectdialog.h ← Seriell- und Netzwerk(TCP)-Konfiguration, PTY-Erkennung, Log-Datei │ ├── v4l2worker.h ← V4L2-Capture in std::thread, MJPEG/YUYV/NV12 -│ └── videowidget.h ← Live-Vorschau, Freeze, Screenshot +│ └── videowidget.h ← Live-Vorschau, Freeze, Screenshot (manuell + automatisch via Tag), MP4-Aufzeichnung (ffmpeg) └── src/ ├── main.cpp ├── mainwindow.cpp @@ -205,3 +291,25 @@ uartscope/ - **Hex-View**: rohe Bytes als Hex-Dump anzeigen - **Session-Replay**: gespeicherte Log-Dateien abspielen - **Regex-Filter**: Zeilen im Raw-View per regulärem Ausdruck ein-/ausblenden + +--- + +## Changelog + +**1.2.0** +- Automatischer Screenshot-Trigger via `[SCREENSHOT] dateiname`-Steuertag – speichert den Video-Frame exakt im Moment des Empfangs (z.B. für Memory-Dumps) +- Neue Toolbar-Option „Screenshot folder…" zur Konfiguration des Zielordners +- Format-Referenz-Dialog um Dokumentation des neuen Steuertags ergänzt +- Erfolg/Fehler eines Screenshot-Triggers wird zusätzlich zur Statusleiste dauerhaft als Zeile im Raw-View **und** direkt im `[SCREENSHOT]`-Tag-Panel selbst (Aktuell + Verlauf) protokolliert + +**1.1.0** +- Netzwerk-Input (TCP) als Alternative zum seriellen Port, inkl. Auto-Reconnect +- PTY-Erkennung (`/dev/pts/`) im Seriell-Tab, z.B. für QEMU +- Video-Aufzeichnung als MP4 (via `ffmpeg`) +- Tag-Panels: zweiter „Verlauf"-Tab mit vollständiger, timestamped Historie +- Suche im Raw-View: Weiter/Zurück-Navigation mit Wrap-Around +- Tag-Namen mit Bindestrich werden jetzt korrekt erkannt und gefiltert +- Bugfix: letzte Zeile ging bei einem ausbleibenden Folge-Byte-Strom manchmal verloren (Idle-Flush-Timer) + +**1.0.0** +- Erste Veröffentlichung diff --git a/include/mainwindow.h b/include/mainwindow.h index 5a086db..5a0b315 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -41,8 +41,10 @@ private slots: void onError(const QString &message); void onNewLine(const QString &line); void onTagDetected(const QString &tag, const QString &value); + void onScreenshotRequested(const QString &filename); void showFormatReference(); void configureTagFilter(); + void configureScreenshotDir(); void showAbout(); private: @@ -53,6 +55,7 @@ private: void doShutdown(); void saveSettings(); void restoreSettings(); + void updateScreenshotDirTooltip(); QThread *m_thread = nullptr; SerialWorker *m_worker = nullptr; @@ -71,6 +74,7 @@ private: QAction *m_connectAction = nullptr; QAction *m_disconnectAction = nullptr; + QAction *m_screenshotDirAction = nullptr; // Splitters (saved/restored via QSettings) QSplitter *m_mainSplitter = nullptr; @@ -78,5 +82,6 @@ private: SerialConfig m_lastConfig; QSet m_suppressedTags; // tags hidden from Raw view + QString m_screenshotDir; // target folder for [SCREENSHOT] tag captures bool m_shutdownDone = false; }; diff --git a/include/serialworker.h b/include/serialworker.h index 5e6f9f4..690cd78 100644 --- a/include/serialworker.h +++ b/include/serialworker.h @@ -48,6 +48,11 @@ public slots: signals: void newLine(const QString &line); void tagDetected(const QString &tag, const QString &value); + // Fired specifically for a [SCREENSHOT] control tag (in addition to the + // normal tagDetected() above, so it still shows up in the Tag Monitor + // history too). `filename` is whatever followed the tag, verbatim - + // the receiver is responsible for sanitizing/defaulting it. + void screenshotRequested(const QString &filename); void clearScreen(); void portOpened(); void portClosed(); diff --git a/include/videowidget.h b/include/videowidget.h index 34c5b6b..dc3a7b2 100644 --- a/include/videowidget.h +++ b/include/videowidget.h @@ -27,10 +27,27 @@ public: public: void shutdown(); // safe to call multiple times, called from MainWindow::closeEvent + // Folder that saveAutoScreenshot() writes into. Empty = not configured + // (saveAutoScreenshot() will report an error instead of silently + // dropping the request). + void setAutoScreenshotDir(const QString &dir) { m_autoScreenshotDir = dir; } public slots: void onNewFrame(const QImage &frame); void clearFrame(); // blanks the preview (used on ANSI clear-screen) + // Saves whichever frame is currently in m_frame under `filename` inside + // m_autoScreenshotDir. Triggered by a [SCREENSHOT] control tag from the + // firmware - the point is to grab the frame that's already in flight + // the instant the tag arrives (no dialog, no user reaction time), e.g. + // to reliably catch a video frame showing a memory dump. + void saveAutoScreenshot(const QString &filename); + +signals: + // Emitted after every saveAutoScreenshot() attempt so the caller (the + // main window) can surface a transient status message. Never blocks/ + // pops a dialog itself, since this can fire in rapid succession from + // firmware-driven triggers. + void autoScreenshotResult(bool success, const QString &message); private slots: void onStartStop(); @@ -76,4 +93,8 @@ private: QProcess *m_recordProcess = nullptr; bool m_recording = false; QSize m_recordSize; + + // Folder for saveAutoScreenshot() ([SCREENSHOT] tag). Set via + // setAutoScreenshotDir(), persisted/restored by MainWindow. + QString m_autoScreenshotDir; }; diff --git a/src/main.cpp b/src/main.cpp index 98e4b2f..7948e6e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,7 +7,7 @@ int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setApplicationName("UARTScope"); - app.setApplicationVersion("1.1.0"); + app.setApplicationVersion("1.2.0"); app.setOrganizationName("ChicaDev"); app.setStyle(QStyleFactory::create("Fusion")); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 77867cf..e7dada0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ MainWindow::MainWindow(QWidget *parent) connect(m_worker, &SerialWorker::newLine, this, &MainWindow::onNewLine); connect(m_worker, &SerialWorker::tagDetected, this, &MainWindow::onTagDetected); + connect(m_worker, &SerialWorker::screenshotRequested, this, &MainWindow::onScreenshotRequested); connect(m_worker, &SerialWorker::clearScreen, this, &MainWindow::onClearScreen); connect(m_worker, &SerialWorker::portOpened, this, &MainWindow::onPortOpened); connect(m_worker, &SerialWorker::portClosed, this, &MainWindow::onPortClosed); @@ -100,6 +102,30 @@ void MainWindow::setupUi() m_videoWidget = new VideoWidget(rightSplitter); m_videoWidget->setMinimumHeight(160); + connect(m_videoWidget, &VideoWidget::autoScreenshotResult, this, + [this](bool ok, const QString &message) { + statusBar()->showMessage(message, ok ? 4000 : 6000); + // The status bar message disappears after a few seconds and is + // easy to miss (especially right after triggering a capture + // and looking at the video preview, not the status bar). Also + // log it as a normal Raw View line - permanent, scrollable, + // and sitting right next to the [SCREENSHOT] tag line itself + // for context - so a failure (no folder configured, no frame + // yet, etc.) is never silently lost. + m_rawView->appendLine( + QStringLiteral("[UARTSCOPE] %1").arg(message), m_suppressedTags); + + // Also surface the outcome directly in the Tag Monitor's + // [SCREENSHOT] panel - both its "Aktuell" tab (so the latest + // status is visible at a glance) and its "Verlauf" history + // (so every past trigger's outcome, success or failure, stays + // reviewable with a timestamp). tagDetected() for the request + // itself is emitted before screenshotRequested() in + // SerialWorker, so this result entry always lands after the + // corresponding request entry. + m_tagWidget->handleTag(QStringLiteral("SCREENSHOT"), message); + }); + rightSplitter->addWidget(m_tagWidget); rightSplitter->addWidget(m_videoWidget); rightSplitter->setStretchFactor(0, 1); @@ -168,6 +194,12 @@ void MainWindow::setupToolBar() tb->addSeparator(); + m_screenshotDirAction = tb->addAction(tr("Screenshot folder…")); + connect(m_screenshotDirAction, &QAction::triggered, this, &MainWindow::configureScreenshotDir); + updateScreenshotDirTooltip(); + + tb->addSeparator(); + 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); @@ -309,6 +341,12 @@ void MainWindow::onTagDetected(const QString &tag, const QString &value) m_tagWidget->handleTag(tag, value); } +void MainWindow::onScreenshotRequested(const QString &filename) +{ + if (m_videoWidget) + m_videoWidget->saveAutoScreenshot(filename); +} + void MainWindow::clearAllViews() { m_rawView->clear(); @@ -403,7 +441,33 @@ Every received line is appended with a hh:mm:ss.zzz timestamp. No special firmware changes needed. ──────────────────────────────────────────────────────────── -5. COMPLETE EXAMPLE (Chica / Amiga hardware emulation) +5. AUTOMATIC SCREENSHOT TRIGGER (V4L2 / HDMI grabber tab) +──────────────────────────────────────────────────────────── +[SCREENSHOT] is a control tag: the instant this line is +received, UARTScope saves whatever video frame is currently in the V4L2 +preview under - no dialog, no user reaction time needed. This +is meant for catching a fast-changing signal (e.g. a memory dump flashed +on an emulated display for only a moment) at the exact instant the +firmware announces it, rather than relying on manually clicking the +screenshot button and missing the window. + + uart_printf("[SCREENSHOT] memdump_%lu.png\n", HAL_GetTick()); + +Notes: + • Configure the target folder once via the toolbar + ("Screenshot folder…") - if unset, the trigger is reported but no + file is written. + • may omit the extension (defaults to .png); any directory + components sent by the firmware are stripped for safety - the file + always lands directly in the configured folder. + • Reusing the same filename on every trigger is fine: UARTScope + automatically appends _1, _2, … instead of overwriting. + • The line is also still visible in the Tag Monitor's [SCREENSHOT] + panel (with its own "Verlauf" history tab), so you get a timestamped + log of every capture in addition to the saved image files. + +──────────────────────────────────────────────────────────── +6. COMPLETE EXAMPLE (Chica / Amiga hardware emulation) ──────────────────────────────────────────────────────────── void uart_status_update(void) { uart_printf("[WDG] uptime=%lu free=%lu load=%d temp=%d\n", @@ -421,6 +485,12 @@ void uart_status_update(void) { HAL_GetTick(), vcc_mv / 1000.0f, cpu_load_percent, core_temp_c); } +// Grab the exact video frame when a memory dump is about to be shown: +void trigger_memdump_capture(void) { + uart_printf("[SCREENSHOT] memdump_%lu.png\n", HAL_GetTick()); + show_memory_dump_on_screen(); +} + // Clear screen from firmware when you want a fresh start: // uart_printf("\033[2J\033[H"); )UARTSCOPE"; @@ -511,6 +581,35 @@ void MainWindow::configureTagFilter() dlg->deleteLater(); } +// ── Auto-screenshot folder ────────────────────────────────────────────────── + +void MainWindow::configureScreenshotDir() +{ + const QString dir = QFileDialog::getExistingDirectory( + this, tr("Auto-screenshot folder for [SCREENSHOT] tag"), m_screenshotDir); + if (dir.isEmpty()) + return; + + m_screenshotDir = dir; + if (m_videoWidget) + m_videoWidget->setAutoScreenshotDir(m_screenshotDir); + updateScreenshotDirTooltip(); + + // Persist immediately, consistent with the tag filter dialog above. + saveSettings(); +} + +void MainWindow::updateScreenshotDirTooltip() +{ + if (!m_screenshotDirAction) + return; + m_screenshotDirAction->setToolTip( + m_screenshotDir.isEmpty() + ? tr("No folder set yet - click to choose where [SCREENSHOT]-tag " + "captures are saved") + : tr("[SCREENSHOT] tag captures are saved to: %1").arg(m_screenshotDir)); +} + // ── About dialog ────────────────────────────────────────────────────────── void MainWindow::showAbout() @@ -572,7 +671,7 @@ void MainWindow::showAbout() grid->addWidget(v, row, 1); }; - addRow(0, tr("Version"), "1.1.0"); + addRow(0, tr("Version"), "1.2.0"); addRow(1, tr("Entwickler"), "Dany Thinnes"); addRow(2, tr("Projekt"), "Projekt Hirnfrei"); addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR)); @@ -619,6 +718,8 @@ void MainWindow::saveSettings() tags.sort(); s.setValue("tagfilter/suppressed", tags); + s.setValue("screenshot/autoDir", m_screenshotDir); + s.sync(); // flush to disk immediately (important since we also call // this mid-session from configureTagFilter()) } @@ -647,4 +748,9 @@ void MainWindow::restoreSettings() for (const QString &t : tags) if (!t.isEmpty()) m_suppressedTags.insert(t); + + m_screenshotDir = s.value("screenshot/autoDir").toString(); + if (m_videoWidget) + m_videoWidget->setAutoScreenshotDir(m_screenshotDir); + updateScreenshotDirTooltip(); } diff --git a/src/serialworker.cpp b/src/serialworker.cpp index 4cf4a18..67cfdfe 100644 --- a/src/serialworker.cpp +++ b/src/serialworker.cpp @@ -392,7 +392,24 @@ void SerialWorker::processLine(const QString &line) if (match.hasMatch()) { const QString tag = match.captured(1).toUpper(); const QString value = match.captured(2).trimmed(); + + // Emit tagDetected() first so the request itself (the filename the + // firmware sent) lands in the Tag Monitor's [SCREENSHOT] history + // before the save outcome does. Both signals are queued to the GUI + // thread; queued events are processed in the order they were + // posted, so emission order here directly determines the order + // the two entries appear in - swap it and the result would show + // up "before" the request that caused it. emit tagDetected(tag, value); + + // [SCREENSHOT] is a control tag: fire immediately, as + // close in time as possible to this line arriving, so the caller + // can grab whichever V4L2 frame is currently in flight - e.g. to + // capture the exact video frame showing a memory dump the moment + // the firmware announces it, rather than relying on a human's + // reaction time with the manual screenshot button. + if (tag == QLatin1String("SCREENSHOT")) + emit screenshotRequested(value); } emit newLine(line); diff --git a/src/videowidget.cpp b/src/videowidget.cpp index 6f613f5..6451128 100644 --- a/src/videowidget.cpp +++ b/src/videowidget.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -164,6 +165,55 @@ void VideoWidget::onScreenshot() m_frame.save(path); } +void VideoWidget::saveAutoScreenshot(const QString &filename) +{ + if (m_frame.isNull()) { + emit autoScreenshotResult(false, + tr("[SCREENSHOT] tag received, but no video frame is available yet.")); + return; + } + if (m_autoScreenshotDir.isEmpty()) { + emit autoScreenshotResult(false, + tr("[SCREENSHOT] tag received, but no auto-screenshot folder is " + "configured (toolbar → \"Screenshot folder…\").")); + return; + } + + // Only take the bare filename from what the firmware sent - this + // strips any directory components (incl. attempts at '../' path + // traversal), so the firmware can never dictate where on disk we + // write. Fall back to a timestamp if it sent nothing usable, and + // default to .png if it didn't specify an extension. + QString name = QFileInfo(filename.trimmed()).fileName(); + if (name.isEmpty()) { + name = QStringLiteral("screenshot_%1.png") + .arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz")); + } else if (QFileInfo(name).suffix().isEmpty()) { + name += QStringLiteral(".png"); + } + + const QDir dir(m_autoScreenshotDir); + QString path = dir.filePath(name); + + // Don't silently overwrite an earlier capture if the firmware reuses + // the same filename on every trigger (e.g. a generic "dump.png") - + // append a numeric suffix instead so nothing gets lost. + if (QFileInfo::exists(path)) { + const QString base = QFileInfo(name).completeBaseName(); + const QString suffix = QFileInfo(name).suffix(); + int n = 1; + do { + path = dir.filePath(QStringLiteral("%1_%2.%3").arg(base).arg(n).arg(suffix)); + ++n; + } while (QFileInfo::exists(path)); + } + + if (m_frame.save(path)) + emit autoScreenshotResult(true, tr("Screenshot saved: %1").arg(path)); + else + emit autoScreenshotResult(false, tr("Failed to save screenshot to %1").arg(path)); +} + void VideoWidget::onNewFrame(const QImage &frame) { // Feed the recorder regardless of freeze state, so pausing the preview diff --git a/uartscope-git/PKGBUILD b/uartscope-git/PKGBUILD index b3b860b..f2e62e9 100644 --- a/uartscope-git/PKGBUILD +++ b/uartscope-git/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: diabolus pkgname=uartscope -pkgver=1.0.0.r6.ga942f83 +pkgver=1.1.0.r0.g36923e1 pkgrel=1 pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect" arch=('x86_64' 'aarch64') diff --git a/uartscope-git/pkg/uartscope/.BUILDINFO b/uartscope-git/pkg/uartscope/.BUILDINFO index 459019b..fef3102 100644 --- a/uartscope-git/pkg/uartscope/.BUILDINFO +++ b/uartscope-git/pkg/uartscope/.BUILDINFO @@ -1,11 +1,11 @@ format = 2 pkgname = uartscope pkgbase = uartscope -pkgver = 1.0.0.r6.ga942f83-1 +pkgver = 1.1.0.r0.g36923e1-1 pkgarch = x86_64 -pkgbuild_sha256sum = 838e01d91a8df5c30e4be619acfc3fe42d6b58ec89f227d9df94874ebfbed8c0 +pkgbuild_sha256sum = 69ca8218ddee1c9dbe0ee82d052da7cad987dcc52773078209494a1ff8b3c487 packager = Unknown Packager -builddate = 1787005573 +builddate = 1787031398 builddir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git startdir = /home/diabolus/Arbeit/Projekt-Hirnfrei/uartscope/uartscope-git buildtool = makepkg @@ -242,7 +242,7 @@ installed = compiler-rt20-20.1.8-1-x86_64 installed = compiler-rt21-21.1.8-1-x86_64 installed = composefs-1.0.8-1-x86_64 installed = composer-2.10.2-1-any -installed = confuse-3.3-5-x86_64 +installed = confuse-3.4-1-x86_64 installed = containerd-2.3.4-1-x86_64 installed = convertlit-1.8-13-x86_64 installed = coolercontrol-4.3.1-2-x86_64 @@ -281,7 +281,7 @@ installed = ddcutil-2.2.7-1-x86_64 installed = debhelper-14.3-1-any installed = debtap-3.6.3-1-any installed = debugedit-5.3-2-x86_64 -installed = debuginfod-0.195-8-x86_64 +installed = debuginfod-0.196-1-x86_64 installed = default-cursors-3-1-any installed = dejagnu-1.6.3-21-any installed = deno-2.9.5-1-x86_64 @@ -303,7 +303,7 @@ installed = docbook-xml-4.5-11-any installed = docbook-xsl-1.79.2-9-any installed = docker-1:29.7.2-1-x86_64 installed = docker-buildx-0.36.1-1-x86_64 -installed = docker-compose-5.4.0-1-x86_64 +installed = docker-compose-5.5.0-1-x86_64 installed = dolphin-26.04.3-1-x86_64 installed = dos2unix-7.5.6-1-x86_64 installed = dosbox-0.74.3-4-x86_64 @@ -342,7 +342,7 @@ installed = eigen-5.0.1-2-x86_64 installed = electron-1:43-1-any installed = electron43-43.4.0-1-x86_64 installed = elementary-icon-theme-8.2.0-2-any -installed = elfutils-0.195-8-x86_64 +installed = elfutils-0.196-1-x86_64 installed = embree-4.4.1-1-x86_64 installed = enca-1.22-1-x86_64 installed = enchant-2.8.15-2-x86_64 @@ -393,7 +393,7 @@ installed = flashrom-1.7.0-1-x86_64 installed = flat-remix-gnome-20250926-1-any installed = flatpak-1:1.18.1-1-x86_64 installed = flex-2.6.4-6-x86_64 -installed = fltk-1.4.5-1-x86_64 +installed = fltk-1.4.5-2-x86_64 installed = fltk1.3-1.3.11-4-x86_64 installed = fluidsynth-2.6.0-1-x86_64 installed = fmt-12.2.0-1-x86_64 @@ -532,7 +532,7 @@ installed = gnupg-2.4.9-3-x86_64 installed = gnutls-3.8.13-2-x86_64 installed = go-2:1.26.6-1-x86_64 installed = go-md2man-2.0.7-2-x86_64 -installed = go-tools-4:0.48.0-1-x86_64 +installed = go-tools-4:0.49.0-1-x86_64 installed = gobject-introspection-1.86.0-2-x86_64 installed = gobject-introspection-runtime-1.86.0-2-x86_64 installed = gom-0.5.6-1-x86_64 @@ -635,7 +635,7 @@ installed = hidapi-0.15.0-1-x86_64 installed = highway-1.4.0-1-x86_64 installed = hiredis-1.3.0-1-x86_64 installed = hplip-1:3.26.4-1-x86_64 -installed = hspell-1.4-6-x86_64 +installed = hspell-1.4-7-x86_64 installed = htdig-3.2.0b6-11.1-x86_64 installed = htop-3.5.3-1-x86_64 installed = http-parser-2.9.4-2-x86_64 @@ -908,7 +908,7 @@ installed = lib32-libcups-2.4.19-1-x86_64 installed = lib32-libdatrie-0.2.14-1-x86_64 installed = lib32-libdrm-2.4.134-1-x86_64 installed = lib32-libdv-1.0.0-9-x86_64 -installed = lib32-libelf-0.195-1-x86_64 +installed = lib32-libelf-0.196-1-x86_64 installed = lib32-libepoxy-1.5.10-2-x86_64 installed = lib32-libffi-3.8.0-1-x86_64 installed = lib32-libgcrypt-1.12.2-1-x86_64 @@ -1104,7 +1104,7 @@ installed = libebml-1.4.5-3-x86_64 installed = libebur128-1.2.6-2-x86_64 installed = libedit-20260512_3.1-1-x86_64 installed = libei-1.6.0-1-x86_64 -installed = libelf-0.195-8-x86_64 +installed = libelf-0.196-1-x86_64 installed = libepoxy-1.5.10-3-x86_64 installed = libev-4.33-5-x86_64 installed = libevdev-1.13.6-1-x86_64 @@ -1267,7 +1267,7 @@ installed = libofx-0.10.9-2-x86_64 installed = libogg-1.3.6-1-x86_64 installed = liboggz-1.1.3-1-x86_64 installed = libomxil-bellagio-0.9.3-5-x86_64 -installed = libopenmpt-0.8.7-1-x86_64 +installed = libopenmpt-0.8.8-1-x86_64 installed = libosinfo-1.12.0-3-x86_64 installed = libp11-0.4.20-1-x86_64 installed = libp11-kit-0.26.5-1-x86_64 @@ -1459,7 +1459,7 @@ installed = lightdm-slick-greeter-2.2.7-1-x86_64 installed = lilv-0.28.0-1-x86_64 installed = linphone-desktop-appimage-6.1.2-1-x86_64 installed = linux-7.1.8.arch1-3-x86_64 -installed = linux-api-headers-7.1-1-x86_64 +installed = linux-api-headers-7.2-1-x86_64 installed = linux-firmware-20260810-2-any installed = linux-firmware-amdgpu-20260810-2-any installed = linux-firmware-atheros-20260810-2-any @@ -1625,7 +1625,7 @@ installed = nettle-4.0-1-x86_64 installed = network-manager-applet-1.36.0-2-x86_64 installed = networkmanager-1.58.0-1-x86_64 installed = networktablet-1.5-3-x86_64 -installed = nextcloud-34.0.2-1-any +installed = nextcloud-34.0.3-1-any installed = nextcloud-app-spreed-1:24.0.3-1-any installed = nextcloud-app-talk_matterbridge-1.33.1026000-1-any installed = nextcloud-client-2:34.0.1-3-x86_64 @@ -2001,7 +2001,7 @@ installed = python-html5lib-1.1-18-any installed = python-httpcore-1.0.9-3-any installed = python-httpx-0.28.1-7-any installed = python-huggingface-hub-1:1.27.0-1-any -installed = python-hypothesis-6.165.3-1-x86_64 +installed = python-hypothesis-6.165.5-1-x86_64 installed = python-idna-3.18-1-any installed = python-imagesize-2.0.0-1-any installed = python-importlib-metadata-9.0.0-1-any @@ -2021,8 +2021,8 @@ installed = python-jmespath-1.1.0-1-any installed = python-joblib-1.5.3-1-any installed = python-kiwisolver-1.5.0-1-x86_64 installed = python-ko-speech-tools-0.1.0-1-any -installed = python-ladybug-core-0.44.54-1-any -installed = python-ladybug-geometry-1.35.2-1-any +installed = python-ladybug-core-0.44.56-1-any +installed = python-ladybug-geometry-1.35.3-1-any installed = python-lark-parser-1.3.1-2-any installed = python-lazy-loader-0.5-1-any installed = python-legacy-cgi-2.6.4-2-any @@ -2074,7 +2074,7 @@ installed = python-pillow-12.3.0-1-x86_64 installed = python-pip-26.2.1-1-any installed = python-pipx-1.15.0-1-any installed = python-pkg_resources-81.0.0-1-any -installed = python-platformdirs-4.11.2-1-any +installed = python-platformdirs-4.11.3-1-any installed = python-pluggy-1.6.0-3-any installed = python-poetry-core-2.4.1-1-any installed = python-pooch-1.9.0-1-any @@ -2113,8 +2113,8 @@ installed = python-pytest-cov-7.1.0-1-any installed = python-pytest-freezer-0.4.9-2-any installed = python-pytest-subtests-0.15.0-2-any installed = python-pytest-sugar-1.1.1-2-any -installed = python-pytest-timeout-2.4.0-2-any -installed = python-python-discovery-1.5.1-1-any +installed = python-pytest-timeout-2.5.0-1-any +installed = python-python-discovery-1.5.2-1-any installed = python-pytokens-0.4.1-1-any installed = python-pytorch-cuda-2.13.0-4-x86_64 installed = python-pytz-2026.1-1-any @@ -2192,7 +2192,7 @@ installed = python-tzlocal-1:5.4.4-1-any installed = python-uc-micro-py-2.0.0-1-any installed = python-ujson-5.13.0-1-x86_64 installed = python-urllib3-2.7.0-1-any -installed = python-urwid-4.0.9-1-any +installed = python-urwid-4.0.10-1-any installed = python-userpath-1.9.2-4-any installed = python-uv-build-0.12.5-1-x86_64 installed = python-vcs-versioning-2.2.4-1-any @@ -2797,7 +2797,7 @@ installed = tinysparql-3.11.1-1-x86_64 installed = tinyxml-2.6.2-13-x86_64 installed = tinyxml2-11.0.0-2-x86_64 installed = tk-8.6.16-1-x86_64 -installed = tmux-3.7_b-1-x86_64 +installed = tmux-3.7_c-1-x86_64 installed = topgrade-17.9.0-1-x86_64 installed = tor-0.4.9.11-1-x86_64 installed = torsocks-2.5.0-1-x86_64 @@ -2820,7 +2820,7 @@ installed = ttf-ubuntu-font-family-1:0.83-2-any installed = tumbler-4.20.2-1-x86_64 installed = twolame-0.4.0-4-x86_64 installed = tzdata-2026c-1-x86_64 -installed = uartscope-1.0.0.r5.g1422efd-1-x86_64 +installed = uartscope-1.0.0.r6.ga942f83-1-x86_64 installed = uchardet-0.0.8-4-x86_64 installed = udisks2-2.11.2-1-x86_64 installed = unifdef-2.12-4-x86_64 diff --git a/uartscope-git/pkg/uartscope/.MTREE b/uartscope-git/pkg/uartscope/.MTREE index cf84c22..5cf152f 100644 Binary files a/uartscope-git/pkg/uartscope/.MTREE and b/uartscope-git/pkg/uartscope/.MTREE differ diff --git a/uartscope-git/pkg/uartscope/.PKGINFO b/uartscope-git/pkg/uartscope/.PKGINFO index 552012e..f727746 100644 --- a/uartscope-git/pkg/uartscope/.PKGINFO +++ b/uartscope-git/pkg/uartscope/.PKGINFO @@ -3,12 +3,12 @@ pkgname = uartscope pkgbase = uartscope xdata = pkgtype=pkg -pkgver = 1.0.0.r6.ga942f83-1 +pkgver = 1.1.0.r0.g36923e1-1 pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect url = https://git.projekt-hirnfrei.de/diabolus/uartscope -builddate = 1787005573 +builddate = 1787031398 packager = Unknown Packager -size = 406129 +size = 422641 arch = x86_64 license = MIT conflict = uartscope diff --git a/uartscope-git/pkg/uartscope/usr/bin/uartscope b/uartscope-git/pkg/uartscope/usr/bin/uartscope index d4383c6..c5c7173 100755 Binary files a/uartscope-git/pkg/uartscope/usr/bin/uartscope and b/uartscope-git/pkg/uartscope/usr/bin/uartscope differ diff --git a/uartscope-git/src/uartscope b/uartscope-git/src/uartscope index a942f83..36923e1 160000 --- a/uartscope-git/src/uartscope +++ b/uartscope-git/src/uartscope @@ -1 +1 @@ -Subproject commit a942f8385b8b05f5ec9a097ebbb3ba61ad0690f6 +Subproject commit 36923e16461bc15086c84187790dbcb5f0a5030f diff --git a/uartscope-git/uartscope/FETCH_HEAD b/uartscope-git/uartscope/FETCH_HEAD index 2587804..441ec37 100644 --- a/uartscope-git/uartscope/FETCH_HEAD +++ b/uartscope-git/uartscope/FETCH_HEAD @@ -1,2 +1,3 @@ -a942f8385b8b05f5ec9a097ebbb3ba61ad0690f6 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope +36923e16461bc15086c84187790dbcb5f0a5030f 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 +36923e16461bc15086c84187790dbcb5f0a5030f not-for-merge tag 'v1.1.0' of https://git.projekt-hirnfrei.de/diabolus/uartscope