Automatisierte Screenshot Aufnahme per [SCREENSHOT] datei implementiert

This commit is contained in:
2026-08-26 01:22:13 +02:00
parent 36923e1646
commit ef335f2d9e
16 changed files with 365 additions and 52 deletions

View File

@@ -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"));

View File

@@ -3,6 +3,7 @@
#include <QClipboard>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
@@ -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] <filename> is a control tag: the instant this line is
received, UARTScope saves whatever video frame is currently in the V4L2
preview under <filename> - 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.
<filename> 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();
}

View File

@@ -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] <filename> 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);

View File

@@ -2,6 +2,7 @@
#include <QPainter>
#include <QFileDialog>
#include <QFileInfo>
#include <QDir>
#include <QDateTime>
#include <QStandardPaths>
@@ -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