Filtern nach Suchbegriff im ganzen Log implementiert
This commit is contained in:
1 parent
a0c50345d4
commit
4c14e7cb13
13 files changed
+848
-559
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(UARTScope VERSION 1.3.0 LANGUAGES CXX)
|
||||
project(UARTScope VERSION 1.4.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -296,6 +296,12 @@ uartscope/
|
||||
|
||||
## Changelog
|
||||
|
||||
**1.4.0**
|
||||
- Raw-View: neues Treffer-Panel („☰ Matches“, Strg+Umschalt+F). Listet alle Zeilen auf, die den Suchbegriff enthalten (live beim Tippen, optional Groß-/Kleinschreibung beachten)
|
||||
- Treffer lassen sich mit Strg/Umschalt mehrfach auswählen (Strg+A bzw. „Select all“ für alle) und mit Strg+C oder „Copy selection“ kopieren – standardmäßig mit Zeitstempel, optional „Copy without timestamp“
|
||||
- Neue Such-Optionen neben dem Suchfeld: „Aa“ (Groß-/Kleinschreibung beachten) und „Regex“ (regulärer Ausdruck, z.B. `error|fail|timeout`). Sie gelten für F3/Umschalt+F3 und für die Trefferliste; ungültige Muster (und solche, die leeren Text matchen) färben das Suchfeld rot und werden im Panel gemeldet. Hinweis: Zeilen enthalten den Zeitstempel, `^` bezieht sich also auf dessen Anfang
|
||||
- Doppelklick auf einen Treffer springt zur Zeile im Log; „Refresh“ sucht erneut und übernimmt neue Zeilen (die Liste ist bewusst ein Schnappschuss, damit die Auswahl bei laufendem Log nicht verrutscht)
|
||||
|
||||
**1.3.0**
|
||||
- Toolbar aufgeräumt: nur noch Connect/Disconnect/Clear all sind direkt sichtbar
|
||||
- Auto-reconnect, Reconnect-Intervall, Tag-Filter, Screenshot-Ordner, Format-Referenz und Über UARTScope in die neue Menüleiste (Einstellungen/Hilfe) verschoben
|
||||
|
||||
@@ -10,10 +10,16 @@
|
||||
#include <QSet>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextDocument>
|
||||
#include <QListWidget>
|
||||
#include <QSplitter>
|
||||
#include <QTimer>
|
||||
|
||||
// RawView shows the raw UART output with timestamps, search (with next/
|
||||
// previous navigation and wrap-around), tag suppression, unlimited history,
|
||||
// H+V scrolling and a copy-to-clipboard button.
|
||||
// A "Matches" panel (Ctrl+Shift+F) lists every line containing the search
|
||||
// text; entries can be multi-selected and copied (with timestamps by default)
|
||||
// or double-clicked to jump to the line in the log.
|
||||
// Auto-scroll is ON by default.
|
||||
class RawView : public QWidget
|
||||
{
|
||||
@@ -33,6 +39,11 @@ private slots:
|
||||
void copyToClipboard();
|
||||
void findNext();
|
||||
void findPrevious();
|
||||
void toggleMatchPanel(bool show);
|
||||
void refreshMatches();
|
||||
void updateMatchLabel();
|
||||
void jumpToMatch(QListWidgetItem *item);
|
||||
void copySelectedMatches();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
@@ -42,6 +53,12 @@ private:
|
||||
// `wrapAllowed` is true, retries once from the start/end of the
|
||||
// document so search wraps around instead of dead-ending.
|
||||
bool searchAndSelect(const QString &text, QTextDocument::FindFlags flags, bool wrapAllowed);
|
||||
// Regex helpers: makeRegex() honours the "Aa" (match case) checkbox;
|
||||
// regexError() returns an empty string if the pattern is usable, else
|
||||
// a short reason (invalid syntax, or it matches empty text which would
|
||||
// make "find next" stall and match every line in the list).
|
||||
QRegularExpression makeRegex(const QString &text) const;
|
||||
static QString regexError(const QRegularExpression &re);
|
||||
|
||||
QPlainTextEdit *m_textEdit = nullptr;
|
||||
QLineEdit *m_searchEdit = nullptr;
|
||||
@@ -51,6 +68,22 @@ private:
|
||||
QPushButton *m_copyBtn = nullptr;
|
||||
QCheckBox *m_autoScrollCb = nullptr;
|
||||
QLabel *m_lineCountLbl = nullptr;
|
||||
// Matches panel (list of all lines containing the search text)
|
||||
QPushButton *m_matchBtn = nullptr;
|
||||
QWidget *m_matchPanel = nullptr;
|
||||
QListWidget *m_matchList = nullptr;
|
||||
QLabel *m_matchCountLbl = nullptr;
|
||||
QCheckBox *m_caseCb = nullptr; // toolbar: "Aa" match case
|
||||
QCheckBox *m_regexCb = nullptr; // toolbar: "Regex"
|
||||
QString m_matchError; // non-empty = invalid regex
|
||||
QCheckBox *m_noTsCb = nullptr;
|
||||
QPushButton *m_refreshBtn = nullptr;
|
||||
QPushButton *m_selectAllBtn = nullptr;
|
||||
QPushButton *m_copyMatchBtn = nullptr;
|
||||
QTimer *m_matchTimer = nullptr;
|
||||
int m_matchTotal = 0;
|
||||
bool m_matchTruncated = false;
|
||||
|
||||
bool m_autoScroll = true; // ON by default
|
||||
int m_lineCount = 0;
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("UARTScope");
|
||||
app.setApplicationVersion("1.3.0");
|
||||
app.setApplicationVersion("1.4.0");
|
||||
app.setOrganizationName("ChicaDev");
|
||||
|
||||
app.setStyle(QStyleFactory::create("Fusion"));
|
||||
|
||||
+1
-1
@@ -763,7 +763,7 @@ void MainWindow::showAbout()
|
||||
grid->addWidget(v, row, 1);
|
||||
};
|
||||
|
||||
addRow(0, tr("Version"), "1.3.0");
|
||||
addRow(0, tr("Version"), "1.4.0");
|
||||
addRow(1, tr("Entwickler"), "Dany Thinnes");
|
||||
addRow(2, tr("Projekt"), "Projekt Hirnfrei");
|
||||
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));
|
||||
|
||||
+247
-3
@@ -9,6 +9,9 @@
|
||||
#include <QDateTime>
|
||||
#include <QShortcut>
|
||||
#include <QKeySequence>
|
||||
#include <QTextBlock>
|
||||
#include <QListWidgetItem>
|
||||
#include <algorithm>
|
||||
|
||||
// Tag names may contain letters, digits, underscore AND hyphen (e.g.
|
||||
// [I2C-BUS]) - keep this in sync with the tagRe in serialworker.cpp, which
|
||||
@@ -72,7 +75,25 @@ void RawView::setupUi()
|
||||
toolbar->addWidget(new QLabel(tr("Search:"), this));
|
||||
toolbar->addWidget(m_searchEdit, 1);
|
||||
toolbar->addWidget(m_findPrevBtn);
|
||||
m_matchBtn = new QPushButton(tr("☰ Matches"), this);
|
||||
m_matchBtn->setCheckable(true);
|
||||
m_matchBtn->setToolTip(tr("List all lines containing the search text (Ctrl+Shift+F)"));
|
||||
connect(m_matchBtn, &QPushButton::toggled, this, &RawView::toggleMatchPanel);
|
||||
auto *matchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_F), this);
|
||||
connect(matchShortcut, &QShortcut::activated, m_matchBtn, &QPushButton::toggle);
|
||||
|
||||
m_caseCb = new QCheckBox(tr("Aa"), this);
|
||||
m_caseCb->setToolTip(tr("Match case"));
|
||||
m_regexCb = new QCheckBox(tr("Regex"), this);
|
||||
m_regexCb->setToolTip(tr("Treat the search text as a regular expression (e.g. error|fail|timeout)"));
|
||||
// Re-run the search whenever an option changes (also refreshes the matches list)
|
||||
connect(m_caseCb, &QCheckBox::toggled, this, [this] { onSearch(m_searchEdit->text()); });
|
||||
connect(m_regexCb, &QCheckBox::toggled, this, [this] { onSearch(m_searchEdit->text()); });
|
||||
|
||||
toolbar->addWidget(m_findNextBtn);
|
||||
toolbar->addWidget(m_caseCb);
|
||||
toolbar->addWidget(m_regexCb);
|
||||
toolbar->addWidget(m_matchBtn);
|
||||
toolbar->addWidget(m_autoScrollCb);
|
||||
toolbar->addWidget(m_lineCountLbl);
|
||||
toolbar->addWidget(m_copyBtn);
|
||||
@@ -95,7 +116,62 @@ void RawView::setupUi()
|
||||
connect(m_textEdit->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, &RawView::onScrollValueChanged);
|
||||
|
||||
mainLayout->addWidget(m_textEdit, 1);
|
||||
// --- Matches panel (hidden until toggled) ---
|
||||
m_matchPanel = new QWidget(this);
|
||||
auto *panelLayout = new QVBoxLayout(m_matchPanel);
|
||||
panelLayout->setContentsMargins(0, 0, 0, 0);
|
||||
panelLayout->setSpacing(4);
|
||||
|
||||
auto *panelBar = new QHBoxLayout();
|
||||
m_matchCountLbl = new QLabel(tr("Matches: 0"), m_matchPanel);
|
||||
m_noTsCb = new QCheckBox(tr("Copy without timestamp"), m_matchPanel);
|
||||
m_refreshBtn = new QPushButton(tr("↻ Refresh"), m_matchPanel);
|
||||
m_refreshBtn->setToolTip(tr("Search the log again (picks up new lines)"));
|
||||
m_selectAllBtn = new QPushButton(tr("Select all"), m_matchPanel);
|
||||
m_copyMatchBtn = new QPushButton(tr("📋 Copy selection"), m_matchPanel);
|
||||
m_copyMatchBtn->setToolTip(tr("Copy selected lines to clipboard (Ctrl+C)"));
|
||||
panelBar->addWidget(m_matchCountLbl, 1);
|
||||
panelBar->addWidget(m_noTsCb);
|
||||
panelBar->addWidget(m_refreshBtn);
|
||||
panelBar->addWidget(m_selectAllBtn);
|
||||
panelBar->addWidget(m_copyMatchBtn);
|
||||
panelLayout->addLayout(panelBar);
|
||||
|
||||
m_matchList = new QListWidget(m_matchPanel);
|
||||
m_matchList->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_matchList->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_matchList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_matchList->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_matchList->setWordWrap(false);
|
||||
m_matchList->setUniformItemSizes(true);
|
||||
m_matchList->setFont(monoFont);
|
||||
m_matchList->setToolTip(tr("Ctrl/Shift to select several lines, double-click to jump to the line in the log"));
|
||||
panelLayout->addWidget(m_matchList, 1);
|
||||
|
||||
// Re-run the search shortly after the user stops typing / changes options
|
||||
m_matchTimer = new QTimer(this);
|
||||
m_matchTimer->setSingleShot(true);
|
||||
m_matchTimer->setInterval(150);
|
||||
connect(m_matchTimer, &QTimer::timeout, this, &RawView::refreshMatches);
|
||||
connect(m_refreshBtn, &QPushButton::clicked, this, &RawView::refreshMatches);
|
||||
connect(m_selectAllBtn, &QPushButton::clicked, m_matchList, &QListWidget::selectAll);
|
||||
connect(m_copyMatchBtn, &QPushButton::clicked, this, &RawView::copySelectedMatches);
|
||||
connect(m_matchList, &QListWidget::itemDoubleClicked, this, &RawView::jumpToMatch);
|
||||
connect(m_matchList, &QListWidget::itemSelectionChanged, this, &RawView::updateMatchLabel);
|
||||
|
||||
auto *copyShortcut = new QShortcut(QKeySequence::Copy, m_matchList);
|
||||
copyShortcut->setContext(Qt::WidgetShortcut);
|
||||
connect(copyShortcut, &QShortcut::activated, this, &RawView::copySelectedMatches);
|
||||
|
||||
m_matchPanel->setVisible(false);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Vertical, this);
|
||||
splitter->setChildrenCollapsible(false);
|
||||
splitter->addWidget(m_textEdit);
|
||||
splitter->addWidget(m_matchPanel);
|
||||
splitter->setStretchFactor(0, 3);
|
||||
splitter->setStretchFactor(1, 2);
|
||||
mainLayout->addWidget(splitter, 1);
|
||||
}
|
||||
|
||||
void RawView::applyColorScheme()
|
||||
@@ -104,6 +180,8 @@ void RawView::applyColorScheme()
|
||||
p.setColor(QPalette::Base, QColor(0x1e, 0x1e, 0x1e));
|
||||
p.setColor(QPalette::Text, QColor(0xd4, 0xd4, 0xd4));
|
||||
m_textEdit->setPalette(p);
|
||||
if (m_matchList)
|
||||
m_matchList->setPalette(p);
|
||||
}
|
||||
|
||||
void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTags)
|
||||
@@ -139,6 +217,12 @@ void RawView::appendLine(const QString &line, const QSet<QString> &suppressedTag
|
||||
void RawView::clear()
|
||||
{
|
||||
m_textEdit->clear();
|
||||
if (m_matchList) { // block numbers in the list are invalid after a clear
|
||||
m_matchList->clear();
|
||||
m_matchTotal = 0;
|
||||
m_matchTruncated = false;
|
||||
updateMatchLabel();
|
||||
}
|
||||
m_lineCount = 0;
|
||||
m_lineCountLbl->setText(tr("Lines: 0"));
|
||||
}
|
||||
@@ -154,7 +238,23 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
|
||||
return false;
|
||||
|
||||
QTextDocument *doc = m_textEdit->document();
|
||||
QTextCursor cursor = doc->find(text, m_textEdit->textCursor(), flags);
|
||||
|
||||
QTextDocument::FindFlags f = flags;
|
||||
if (m_caseCb->isChecked())
|
||||
f |= QTextDocument::FindCaseSensitively;
|
||||
|
||||
const bool useRegex = m_regexCb->isChecked();
|
||||
QRegularExpression re;
|
||||
if (useRegex) {
|
||||
re = makeRegex(text);
|
||||
if (!regexError(re).isEmpty())
|
||||
return false;
|
||||
}
|
||||
auto doFind = [&](const QTextCursor &from) {
|
||||
return useRegex ? doc->find(re, from, f) : doc->find(text, from, f);
|
||||
};
|
||||
|
||||
QTextCursor cursor = doFind(m_textEdit->textCursor());
|
||||
|
||||
if (cursor.isNull() && wrapAllowed) {
|
||||
// No further match in the search direction from here - wrap around
|
||||
@@ -163,7 +263,7 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
|
||||
QTextCursor wrapCursor(doc);
|
||||
if (flags.testFlag(QTextDocument::FindBackward))
|
||||
wrapCursor.movePosition(QTextCursor::End);
|
||||
cursor = doc->find(text, wrapCursor, flags);
|
||||
cursor = doFind(wrapCursor);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
@@ -175,6 +275,10 @@ bool RawView::searchAndSelect(const QString &text, QTextDocument::FindFlags flag
|
||||
|
||||
void RawView::onSearch(const QString &text)
|
||||
{
|
||||
// Keep the matches list in sync with the search text while it is open
|
||||
if (m_matchPanel && !m_matchPanel->isHidden())
|
||||
m_matchTimer->start();
|
||||
|
||||
if (text.isEmpty()) {
|
||||
m_searchEdit->setStyleSheet(QString());
|
||||
return;
|
||||
@@ -223,3 +327,143 @@ void RawView::onAutoScrollToggled(bool enabled)
|
||||
m_textEdit->verticalScrollBar()->setValue(
|
||||
m_textEdit->verticalScrollBar()->maximum());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Matches panel: list every log line containing the search text
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void RawView::toggleMatchPanel(bool show)
|
||||
{
|
||||
m_matchPanel->setVisible(show);
|
||||
if (show)
|
||||
refreshMatches();
|
||||
}
|
||||
|
||||
void RawView::refreshMatches()
|
||||
{
|
||||
// The list is a snapshot of the log at the time of searching, so new
|
||||
// incoming lines don't shift the user's selection. "Refresh" re-runs it.
|
||||
constexpr int kMaxMatches = 100000;
|
||||
|
||||
m_matchList->clear();
|
||||
m_matchTotal = 0;
|
||||
m_matchTruncated = false;
|
||||
m_matchError.clear();
|
||||
|
||||
const QString needle = m_searchEdit->text();
|
||||
if (!needle.isEmpty()) {
|
||||
const Qt::CaseSensitivity cs =
|
||||
m_caseCb->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive;
|
||||
|
||||
const bool useRegex = m_regexCb->isChecked();
|
||||
QRegularExpression re;
|
||||
if (useRegex) {
|
||||
re = makeRegex(needle);
|
||||
m_matchError = regexError(re);
|
||||
if (!m_matchError.isEmpty()) {
|
||||
updateMatchLabel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_matchList->setUpdatesEnabled(false);
|
||||
for (QTextBlock b = m_textEdit->document()->begin(); b.isValid(); b = b.next()) {
|
||||
const QString text = b.text();
|
||||
const bool hit = useRegex ? re.match(text).hasMatch()
|
||||
: text.contains(needle, cs);
|
||||
if (!hit)
|
||||
continue;
|
||||
if (m_matchTotal >= kMaxMatches) {
|
||||
m_matchTruncated = true;
|
||||
break;
|
||||
}
|
||||
auto *item = new QListWidgetItem(text, m_matchList);
|
||||
item->setData(Qt::UserRole, b.blockNumber());
|
||||
++m_matchTotal;
|
||||
}
|
||||
m_matchList->setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
void RawView::updateMatchLabel()
|
||||
{
|
||||
const int selected = m_matchList->selectedItems().size();
|
||||
if (!m_matchError.isEmpty()) {
|
||||
m_matchCountLbl->setText(tr("Invalid regex: %1").arg(m_matchError));
|
||||
return;
|
||||
}
|
||||
QString text = tr("Matches: %1").arg(m_matchTotal);
|
||||
if (m_matchTruncated)
|
||||
text += tr(" (limited)");
|
||||
if (selected > 0)
|
||||
text += tr(" – %1 selected").arg(selected);
|
||||
m_matchCountLbl->setText(text);
|
||||
}
|
||||
|
||||
void RawView::jumpToMatch(QListWidgetItem *item)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
const QTextBlock block = m_textEdit->document()->findBlockByNumber(
|
||||
item->data(Qt::UserRole).toInt());
|
||||
if (!block.isValid())
|
||||
return;
|
||||
|
||||
QTextCursor cursor(block);
|
||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
m_textEdit->setTextCursor(cursor);
|
||||
m_textEdit->centerCursor();
|
||||
}
|
||||
|
||||
void RawView::copySelectedMatches()
|
||||
{
|
||||
QList<QListWidgetItem *> items = m_matchList->selectedItems();
|
||||
if (items.isEmpty())
|
||||
return;
|
||||
|
||||
// selectedItems() is in selection order - copy in log order instead
|
||||
std::sort(items.begin(), items.end(),
|
||||
[this](QListWidgetItem *a, QListWidgetItem *b) {
|
||||
return m_matchList->row(a) < m_matchList->row(b);
|
||||
});
|
||||
|
||||
static const QRegularExpression tsRe(R"(^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\s+)");
|
||||
const bool stripTs = m_noTsCb->isChecked();
|
||||
|
||||
QStringList lines;
|
||||
lines.reserve(items.size());
|
||||
for (QListWidgetItem *it : items) {
|
||||
QString text = it->text();
|
||||
if (stripTs)
|
||||
text.remove(tsRe);
|
||||
lines << text;
|
||||
}
|
||||
|
||||
QApplication::clipboard()->setText(lines.join('\n'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Regex support
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QRegularExpression RawView::makeRegex(const QString &text) const
|
||||
{
|
||||
return QRegularExpression(
|
||||
text,
|
||||
m_caseCb->isChecked() ? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
|
||||
QString RawView::regexError(const QRegularExpression &re)
|
||||
{
|
||||
if (!re.isValid())
|
||||
return re.errorString();
|
||||
// A pattern like "a*" or "^" matches the empty string: it would match every
|
||||
// line in the list and make "find next" stall on a zero-length selection.
|
||||
if (re.match(QString()).hasMatch())
|
||||
return tr("pattern matches empty text");
|
||||
return QString();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: diabolus <your@email.com>
|
||||
pkgname=uartscope
|
||||
pkgver=1.1.0.r6.g98f6f1c
|
||||
pkgver=1.1.0.r7.ga0c5034
|
||||
pkgrel=1
|
||||
pkgdesc="Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect"
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -3,10 +3,10 @@
|
||||
pkgname = uartscope
|
||||
pkgbase = uartscope
|
||||
xdata = pkgtype=pkg
|
||||
pkgver = 1.1.0.r6.g98f6f1c-1
|
||||
pkgver = 1.1.0.r7.ga0c5034-1
|
||||
pkgdesc = Qt6-based UART serial monitor with tag monitoring, table view and auto-reconnect
|
||||
url = https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||
builddate = 1791306653
|
||||
builddate = 1792095702
|
||||
packager = Unknown Packager
|
||||
size = 484465
|
||||
arch = x86_64
|
||||
|
||||
Binary file not shown.
Submodule uartscope-git/src/uartscope updated: 98f6f1c903...a0c50345d4.
@@ -1,3 +1,3 @@
|
||||
98f6f1c903d808cd169405560139e8cfaa90a815 not-for-merge branch 'main' of https://git.projekt-hirnfrei.de/diabolus/uartscope
|
||||
a0c50345d40f9910f0e99dedf58c7bd5c21d41ef 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
|
||||
Reference in new issue
Block a user