- TAG [uartscopeclean] eingefügt, über den alle Logs gelöscht werden

- Pauseknopf und Eingabezeile hinzugefügt, um Kommandos an den Pi zu schicken
This commit is contained in:
2026-09-07 14:53:54 +02:00
parent ef335f2d9e
commit 934f32a3bd
14 changed files with 763 additions and 347 deletions

View File

@@ -38,11 +38,24 @@ MainWindow::MainWindow(QWidget *parent)
connect(m_worker, &SerialWorker::tagDetected, this, &MainWindow::onTagDetected);
connect(m_worker, &SerialWorker::screenshotRequested, this, &MainWindow::onScreenshotRequested);
connect(m_worker, &SerialWorker::clearScreen, this, &MainWindow::onClearScreen);
// [UARTSCOPECLEAR] control tag from the firmware: wipe everything, same
// as an ANSI clear-screen - lets a baremetal project guarantee a fresh
// UARTScope state right when it boots.
connect(m_worker, &SerialWorker::uartscopeClearRequested, this, &MainWindow::clearAllViews);
connect(m_worker, &SerialWorker::portOpened, this, &MainWindow::onPortOpened);
connect(m_worker, &SerialWorker::portClosed, this, &MainWindow::onPortClosed);
connect(m_worker, &SerialWorker::reconnecting, this, &MainWindow::onReconnecting);
connect(m_worker, &SerialWorker::errorOccurred, this, &MainWindow::onError);
// Send bar (Pause button + input line): forward whatever it wants sent
// to the worker thread. Queued since m_worker lives in m_thread.
connect(m_sendBar, &SendBar::sendRequested, this, [this](const QByteArray &data) {
QMetaObject::invokeMethod(m_worker, "writeData",
Qt::QueuedConnection,
Q_ARG(QByteArray, data));
});
connect(m_sendBar, &SendBar::configChanged, this, &MainWindow::saveSettings);
m_thread->start();
}
@@ -79,7 +92,16 @@ void MainWindow::doShutdown()
void MainWindow::setupUi()
{
m_mainSplitter = new QSplitter(Qt::Horizontal, this);
// Central widget: the existing raw/table/tag/video splitter on top,
// plus the send bar (Pause button + input line) spanning the full
// width underneath - below the log AND the HDMI preview, not tucked
// into just one side.
auto *central = new QWidget(this);
auto *centralLayout = new QVBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
m_mainSplitter = new QSplitter(Qt::Horizontal, central);
m_mainSplitter->setHandleWidth(5);
auto *mainSplitter = m_mainSplitter;
@@ -137,7 +159,12 @@ void MainWindow::setupUi()
mainSplitter->setStretchFactor(0, 3);
mainSplitter->setStretchFactor(1, 1);
setCentralWidget(mainSplitter);
centralLayout->addWidget(mainSplitter, 1);
m_sendBar = new SendBar(central);
centralLayout->addWidget(m_sendBar);
setCentralWidget(central);
}
void MainWindow::setupToolBar()
@@ -298,6 +325,8 @@ void MainWindow::onPortOpened()
.arg(m_lastConfig.baudRate));
}
statusBar()->clearMessage();
if (m_sendBar)
m_sendBar->setConnected(true);
}
void MainWindow::onPortClosed()
@@ -307,6 +336,8 @@ void MainWindow::onPortClosed()
m_stateLabel->setText(tr("○ Disconnected"));
m_stateLabel->setStyleSheet(QString());
m_portLabel->setText(QString());
if (m_sendBar)
m_sendBar->setConnected(false);
}
void MainWindow::onReconnecting(int attempt)
@@ -381,6 +412,13 @@ tag monitor, and the video preview frame):
uart_printf("\033[2J\033[H"); // standard VT100 clear-screen + cursor home
Alternative: the [UARTSCOPECLEAR] control tag does the exact same full
reset (raw output, table, tag monitor, video frame). No value needed -
handy to fire once right at boot so every run starts from an absolutely
clean UARTScope state, regardless of whatever the previous run left behind:
uart_printf("[UARTSCOPECLEAR]\n");
2. TAG MONITOR (side panel, one widget per tag)
@@ -467,7 +505,26 @@ Notes:
log of every capture in addition to the saved image files.
6. COMPLETE EXAMPLE (Chica / Amiga hardware emulation)
6. SENDING DATA BACK TO THE FIRMWARE (debugging)
UARTScope is no longer read-only. Below the Raw/Table view and the HDMI
preview there is a send bar:
A "Pause" button sends a fixed, user-configurable byte sequence every
time it's clicked (right-click the button in UARTScope to change the
label and the code - escape sequences like \n \r \t \xHH are
supported, e.g. a single control byte to pause/step your firmware).
An input line sends whatever is typed the instant Enter is pressed
(or the "Senden" button is clicked), with a selectable line ending
(none / LF / CRLF / CR) appended.
Both are only active while a port/connection is open. On the firmware
side this is just ordinary incoming UART data - read it the same way you
would read from any other serial console (e.g. a blocking or interrupt-
driven UART receive), there's no special protocol UARTScope expects here.
7. COMPLETE EXAMPLE (Chica / Amiga hardware emulation)
void uart_status_update(void) {
uart_printf("[WDG] uptime=%lu free=%lu load=%d temp=%d\n",
@@ -493,6 +550,16 @@ void trigger_memdump_capture(void) {
// Clear screen from firmware when you want a fresh start:
// uart_printf("\033[2J\033[H");
// or, equivalently:
// uart_printf("[UARTSCOPECLEAR]\n");
// Fire once right at boot, before anything else is printed, so every run
// starts from a completely clean UARTScope - log, table, tag monitor and
// video preview all wiped, no leftovers from a previous run/crash:
void uart_init(void) {
hal_uart_setup(115200);
uart_printf("[UARTSCOPECLEAR]\n");
}
)UARTSCOPE";
auto *dlg = new QDialog(this);
@@ -720,6 +787,12 @@ void MainWindow::saveSettings()
s.setValue("screenshot/autoDir", m_screenshotDir);
if (m_sendBar) {
s.setValue("sendbar/buttonLabel", m_sendBar->buttonLabel());
s.setValue("sendbar/buttonCode", m_sendBar->buttonCode());
s.setValue("sendbar/lineEnding", m_sendBar->lineEndingIndex());
}
s.sync(); // flush to disk immediately (important since we also call
// this mid-session from configureTagFilter())
}
@@ -753,4 +826,13 @@ void MainWindow::restoreSettings()
if (m_videoWidget)
m_videoWidget->setAutoScreenshotDir(m_screenshotDir);
updateScreenshotDirTooltip();
if (m_sendBar) {
if (s.contains("sendbar/buttonLabel"))
m_sendBar->setButtonLabel(s.value("sendbar/buttonLabel").toString());
if (s.contains("sendbar/buttonCode"))
m_sendBar->setButtonCode(s.value("sendbar/buttonCode").toString());
if (s.contains("sendbar/lineEnding"))
m_sendBar->setLineEndingIndex(s.value("sendbar/lineEnding").toInt());
}
}

223
src/sendbar.cpp Normal file
View File

@@ -0,0 +1,223 @@
#include "sendbar.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QMenu>
#include <QAction>
#include <QFont>
SendBar::SendBar(QWidget *parent)
: QWidget(parent)
{
setupUi();
}
void SendBar::setupUi()
{
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(6, 4, 6, 4);
m_pauseBtn = new QPushButton(m_buttonLabel, this);
m_pauseBtn->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_pauseBtn, &QPushButton::clicked, this, &SendBar::onPauseClicked);
connect(m_pauseBtn, &QPushButton::customContextMenuRequested,
this, &SendBar::onPauseContextMenu);
layout->addWidget(m_pauseBtn);
m_inputEdit = new QLineEdit(this);
m_inputEdit->setPlaceholderText(
tr("Text eingeben und Enter drücken, um über UART zu senden…"));
connect(m_inputEdit, &QLineEdit::returnPressed, this, &SendBar::sendInputLine);
layout->addWidget(m_inputEdit, 1);
m_sendBtn = new QPushButton(tr("Senden"), this);
connect(m_sendBtn, &QPushButton::clicked, this, &SendBar::onSendClicked);
layout->addWidget(m_sendBtn);
m_endingCombo = new QComboBox(this);
m_endingCombo->addItem(tr("kein Zeilenende"));
m_endingCombo->addItem(tr("\\n (LF)"));
m_endingCombo->addItem(tr("\\r\\n (CRLF)"));
m_endingCombo->addItem(tr("\\r (CR)"));
m_endingCombo->setCurrentIndex(1); // \n ist der übliche Default für UART-Konsolen
m_endingCombo->setToolTip(tr("Zeilenende, das an den über die Eingabezeile\ngesendeten Text angehängt wird"));
connect(m_endingCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SendBar::configChanged);
layout->addWidget(m_endingCombo);
updatePauseButtonUi();
// Solange nichts verbunden ist, gibt es nichts zu senden.
setConnected(false);
}
void SendBar::updatePauseButtonUi()
{
if (!m_pauseBtn)
return;
m_pauseBtn->setText(m_buttonLabel);
m_pauseBtn->setToolTip(
tr("Sendet über UART: %1\n(Rechtsklick zum Ändern)").arg(m_buttonCode));
}
// ── public accessors ────────────────────────────────────────────────────────
int SendBar::lineEndingIndex() const
{
return m_endingCombo ? m_endingCombo->currentIndex() : 1;
}
void SendBar::setButtonLabel(const QString &label)
{
m_buttonLabel = label.isEmpty() ? tr("Pause") : label;
updatePauseButtonUi();
}
void SendBar::setButtonCode(const QString &code)
{
m_buttonCode = code;
updatePauseButtonUi();
}
void SendBar::setLineEndingIndex(int index)
{
if (m_endingCombo && index >= 0 && index < m_endingCombo->count())
m_endingCombo->setCurrentIndex(index);
}
// ── slots ────────────────────────────────────────────────────────────────
void SendBar::setConnected(bool connected)
{
if (m_pauseBtn) m_pauseBtn->setEnabled(connected);
if (m_inputEdit) m_inputEdit->setEnabled(connected);
if (m_sendBtn) m_sendBtn->setEnabled(connected);
// m_endingCombo bleibt immer bedienbar - reine Konfiguration.
}
void SendBar::onPauseClicked()
{
emit sendRequested(unescape(m_buttonCode));
}
void SendBar::onPauseContextMenu(const QPoint &pos)
{
QMenu menu(this);
QAction *cfgAction = menu.addAction(tr("Code konfigurieren…"));
connect(cfgAction, &QAction::triggered, this, &SendBar::configureButton);
menu.exec(m_pauseBtn->mapToGlobal(pos));
}
void SendBar::onSendClicked()
{
sendInputLine();
}
void SendBar::sendInputLine()
{
const QString text = m_inputEdit->text();
if (text.isEmpty())
return;
QByteArray data = text.toUtf8();
switch (m_endingCombo->currentIndex()) {
case 1: data += "\n"; break;
case 2: data += "\r\n"; break;
case 3: data += "\r"; break;
default: break; // 0 = kein Zeilenende
}
emit sendRequested(data);
m_inputEdit->clear();
}
void SendBar::configureButton()
{
auto *dlg = new QDialog(this);
dlg->setWindowTitle(tr("Button konfigurieren"));
dlg->setMinimumWidth(380);
auto *layout = new QVBoxLayout(dlg);
auto *infoLabel = new QLabel(
tr("Der Code wird beim Klick als Rohbytes über UART gesendet.<br>"
"Escape-Sequenzen werden unterstützt: <tt>\\n \\r \\t \\\\ \\xHH</tt>"), dlg);
infoLabel->setWordWrap(true);
layout->addWidget(infoLabel);
auto *form = new QFormLayout();
auto *labelEdit = new QLineEdit(m_buttonLabel, dlg);
auto *codeEdit = new QLineEdit(m_buttonCode, dlg);
QFont mono("Monospace");
mono.setStyleHint(QFont::Monospace);
codeEdit->setFont(mono);
form->addRow(tr("Beschriftung:"), labelEdit);
form->addRow(tr("Code:"), codeEdit);
layout->addLayout(form);
auto *btnBox = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg);
connect(btnBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
connect(btnBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
layout->addWidget(btnBox);
if (dlg->exec() == QDialog::Accepted) {
const QString newLabel = labelEdit->text().trimmed();
m_buttonLabel = newLabel.isEmpty() ? tr("Pause") : newLabel;
m_buttonCode = codeEdit->text();
updatePauseButtonUi();
emit configChanged();
}
dlg->deleteLater();
}
// ── static helper ────────────────────────────────────────────────────────
QByteArray SendBar::unescape(const QString &text)
{
QByteArray out;
out.reserve(text.size());
int i = 0;
while (i < text.size()) {
const QChar c = text.at(i);
if (c == QLatin1Char('\\') && i + 1 < text.size()) {
const QChar next = text.at(i + 1);
bool handled = true;
switch (next.toLatin1()) {
case 'n': out.append('\n'); break;
case 'r': out.append('\r'); break;
case 't': out.append('\t'); break;
case '0': out.append('\0'); break;
case '\\': out.append('\\'); break;
case 'x':
if (i + 3 < text.size()) {
bool ok = false;
const int val = text.mid(i + 2, 2).toInt(&ok, 16);
if (ok) {
out.append(static_cast<char>(val));
i += 4;
continue;
}
}
handled = false;
break;
default:
handled = false;
break;
}
if (handled) {
i += 2;
continue;
}
}
// Kein erkanntes Escape (oder ein einzelnes '\' am Ende): das
// Zeichen unverändert als UTF-8 übernehmen.
out += QString(c).toUtf8();
++i;
}
return out;
}

View File

@@ -62,7 +62,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
m_port->setStopBits(stopBits);
m_port->setFlowControl(flowControl);
if (!m_port->open(QIODevice::ReadOnly)) {
if (!m_port->open(QIODevice::ReadWrite)) {
emit errorOccurred(tr("Cannot open %1: %2").arg(portName, m_port->errorString()));
if (m_autoReconnect)
scheduleReconnect();
@@ -164,6 +164,13 @@ void SerialWorker::stopLogging()
}
}
void SerialWorker::writeData(const QByteArray &data)
{
if (!m_device || data.isEmpty())
return;
m_device->write(data);
}
// ── private slots ───────────────────────────────────────────────────────────
void SerialWorker::onReadyRead()
@@ -256,7 +263,7 @@ void SerialWorker::tryReconnect()
m_port->setStopBits(m_stopBits);
m_port->setFlowControl(m_flowControl);
if (m_port->open(QIODevice::ReadOnly)) {
if (m_port->open(QIODevice::ReadWrite)) {
m_device = m_port;
m_idleFlushTimer->stop();
m_buffer.clear();
@@ -410,6 +417,13 @@ void SerialWorker::processLine(const QString &line)
// reaction time with the manual screenshot button.
if (tag == QLatin1String("SCREENSHOT"))
emit screenshotRequested(value);
// [UARTSCOPECLEAR] is a control tag (no value expected): fire
// immediately so a baremetal project can guarantee a completely
// fresh UARTScope state right as it boots, regardless of whatever
// was left over from a previous run/crash.
if (tag == QLatin1String("UARTSCOPECLEAR"))
emit uartscopeClearRequested();
}
emit newLine(line);