Über... hinzugefügt, Icon hinzugefügt
This commit is contained in:
@@ -29,6 +29,10 @@ int main(int argc, char *argv[])
|
||||
darkPalette.setColor(QPalette::HighlightedText, Qt::white);
|
||||
app.setPalette(darkPalette);
|
||||
|
||||
// Set application icon (embedded via Qt resource)
|
||||
const QIcon appIcon(":/icons/UartscopeLogo.png");
|
||||
app.setWindowIcon(appIcon);
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
return app.exec();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDialog>
|
||||
@@ -23,6 +24,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
setupUi();
|
||||
setupToolBar();
|
||||
setupStatusBar();
|
||||
restoreSettings();
|
||||
|
||||
// ── Worker thread ─────────────────────────────────────────────────────────
|
||||
m_thread = new QThread(this);
|
||||
@@ -44,16 +46,48 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
QMetaObject::invokeMethod(m_worker, "closePort", Qt::BlockingQueuedConnection);
|
||||
m_thread->quit();
|
||||
m_thread->wait(3000);
|
||||
// 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();
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
doShutdown();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void MainWindow::doShutdown()
|
||||
{
|
||||
if (m_shutdownDone)
|
||||
return;
|
||||
m_shutdownDone = true;
|
||||
|
||||
saveSettings();
|
||||
|
||||
// 1. Stop V4L2 capture first (joins the internal std::thread)
|
||||
if (m_videoWidget)
|
||||
m_videoWidget->shutdown();
|
||||
|
||||
// 2. Stop UART serial worker
|
||||
if (m_thread && m_thread->isRunning()) {
|
||||
QMetaObject::invokeMethod(m_worker, "closePort", Qt::BlockingQueuedConnection);
|
||||
m_thread->quit();
|
||||
m_thread->wait(5000);
|
||||
if (m_thread->isRunning())
|
||||
m_thread->terminate();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setupUi()
|
||||
{
|
||||
auto *mainSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
// ── Outer horizontal splitter: (Raw+Table tabs) | right panel ────────────
|
||||
m_mainSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
auto *mainSplitter = m_mainSplitter;
|
||||
mainSplitter->setHandleWidth(5);
|
||||
|
||||
// Left: tabs for Raw and Table views
|
||||
auto *tabs = new QTabWidget(mainSplitter);
|
||||
tabs->setTabPosition(QTabWidget::South);
|
||||
|
||||
@@ -63,12 +97,26 @@ void MainWindow::setupUi()
|
||||
m_tableView = new TableView(tabs);
|
||||
tabs->addTab(m_tableView, tr("Table view"));
|
||||
|
||||
m_tagWidget = new TagWidget(mainSplitter);
|
||||
m_tagWidget->setMinimumWidth(220);
|
||||
m_tagWidget->setMaximumWidth(420);
|
||||
// Right: vertical splitter – Tag Monitor on top, Video below
|
||||
m_rightSplitter = new QSplitter(Qt::Vertical, mainSplitter);
|
||||
auto *rightSplitter = m_rightSplitter;
|
||||
rightSplitter->setHandleWidth(5);
|
||||
rightSplitter->setMinimumWidth(240);
|
||||
rightSplitter->setMaximumWidth(480);
|
||||
|
||||
m_tagWidget = new TagWidget(rightSplitter);
|
||||
m_videoWidget = new VideoWidget(rightSplitter);
|
||||
m_videoWidget->setMinimumHeight(160);
|
||||
|
||||
rightSplitter->addWidget(m_tagWidget);
|
||||
rightSplitter->addWidget(m_videoWidget);
|
||||
// Initial split: ~30% tags, ~70% video – user can drag to taste
|
||||
rightSplitter->setStretchFactor(0, 1);
|
||||
rightSplitter->setStretchFactor(1, 2);
|
||||
rightSplitter->setSizes({330, 330});
|
||||
|
||||
mainSplitter->addWidget(tabs);
|
||||
mainSplitter->addWidget(m_tagWidget);
|
||||
mainSplitter->addWidget(rightSplitter);
|
||||
mainSplitter->setStretchFactor(0, 3);
|
||||
mainSplitter->setStretchFactor(1, 1);
|
||||
|
||||
@@ -135,6 +183,12 @@ void MainWindow::setupToolBar()
|
||||
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);
|
||||
|
||||
tb->addSeparator();
|
||||
|
||||
// ── About ─────────────────────────────────────────────────────────────────
|
||||
auto *aboutAction = tb->addAction(tr("Über UARTScope"));
|
||||
connect(aboutAction, &QAction::triggered, this, &MainWindow::showAbout);
|
||||
}
|
||||
|
||||
void MainWindow::setupStatusBar()
|
||||
@@ -444,3 +498,151 @@ void MainWindow::configureTagFilter()
|
||||
}
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::showAbout()
|
||||
{
|
||||
auto *dlg = new QDialog(this);
|
||||
dlg->setWindowTitle(tr("Über UARTScope"));
|
||||
dlg->setFixedSize(400, 280);
|
||||
|
||||
auto *root = new QVBoxLayout(dlg);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
root->setSpacing(0);
|
||||
|
||||
// ── Header block (dark accent) ────────────────────────────────────────────
|
||||
auto *header = new QWidget(dlg);
|
||||
header->setFixedHeight(110);
|
||||
header->setStyleSheet("background-color: #1e1e1e;");
|
||||
auto *headerLayout = new QHBoxLayout(header);
|
||||
headerLayout->setContentsMargins(20, 14, 24, 14);
|
||||
headerLayout->setSpacing(16);
|
||||
|
||||
// Logo
|
||||
auto *logoLabel = new QLabel(header);
|
||||
const QPixmap logoPix(":/icons/UartscopeLogo.png");
|
||||
logoLabel->setPixmap(logoPix.scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
logoLabel->setStyleSheet("background: transparent;");
|
||||
logoLabel->setFixedSize(72, 72);
|
||||
|
||||
// Title + subtitle
|
||||
auto *titleBlock = new QVBoxLayout();
|
||||
titleBlock->setSpacing(4);
|
||||
auto *titleLabel = new QLabel("UARTScope", header);
|
||||
titleLabel->setStyleSheet(
|
||||
"color: #ffffff; font-size: 26px; font-weight: bold; background: transparent;");
|
||||
auto *subtitleLabel = new QLabel(tr("UART-Monitor & HDMI-Viewer für Baremetal-Entwicklung"), header);
|
||||
subtitleLabel->setStyleSheet(
|
||||
"color: #56b6c2; font-size: 11px; background: transparent;");
|
||||
titleBlock->addWidget(titleLabel);
|
||||
titleBlock->addWidget(subtitleLabel);
|
||||
|
||||
headerLayout->addWidget(logoLabel);
|
||||
headerLayout->addLayout(titleBlock);
|
||||
root->addWidget(header);
|
||||
|
||||
// ── Info grid ─────────────────────────────────────────────────────────────
|
||||
auto *body = new QWidget(dlg);
|
||||
body->setStyleSheet("background-color: #252526;");
|
||||
auto *grid = new QGridLayout(body);
|
||||
grid->setContentsMargins(24, 20, 24, 20);
|
||||
grid->setVerticalSpacing(10);
|
||||
grid->setHorizontalSpacing(16);
|
||||
grid->setColumnMinimumWidth(0, 90);
|
||||
|
||||
const QString labelStyle = "color: #888888; font-weight: bold; background: transparent;";
|
||||
const QString valueStyle = "color: #d4d4d4; background: transparent;";
|
||||
|
||||
auto addRow = [&](int row, const QString &label, const QString &value) {
|
||||
auto *l = new QLabel(label, body);
|
||||
auto *v = new QLabel(value, body);
|
||||
l->setStyleSheet(labelStyle);
|
||||
v->setStyleSheet(valueStyle);
|
||||
l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
grid->addWidget(l, row, 0);
|
||||
grid->addWidget(v, row, 1);
|
||||
};
|
||||
|
||||
addRow(0, tr("Version"), "1.0.0");
|
||||
addRow(1, tr("Entwickler"), "Dany Thinnes");
|
||||
addRow(2, tr("Projekt"), "Projekt Hirnfrei");
|
||||
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));
|
||||
addRow(4, tr("Sprache"), "C++17");
|
||||
|
||||
root->addWidget(body, 1);
|
||||
|
||||
// ── Footer with close button ──────────────────────────────────────────────
|
||||
auto *footer = new QWidget(dlg);
|
||||
footer->setStyleSheet("background-color: #252526; border-top: 1px solid #3c3c3c;");
|
||||
auto *footerLayout = new QHBoxLayout(footer);
|
||||
footerLayout->setContentsMargins(16, 10, 16, 10);
|
||||
|
||||
auto *closeBtn = new QPushButton(tr("Schließen"), footer);
|
||||
closeBtn->setDefault(true);
|
||||
closeBtn->setMinimumWidth(90);
|
||||
connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept);
|
||||
|
||||
footerLayout->addStretch();
|
||||
footerLayout->addWidget(closeBtn);
|
||||
root->addWidget(footer);
|
||||
|
||||
dlg->exec();
|
||||
dlg->deleteLater();
|
||||
}
|
||||
|
||||
288
src/v4l2worker.cpp
Normal file
288
src/v4l2worker.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "v4l2worker.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QImageReader>
|
||||
#include <QThread>
|
||||
|
||||
static int xioctl(int fd, unsigned long req, void *arg)
|
||||
{
|
||||
int r;
|
||||
do { r = ::ioctl(fd, req, arg); } while (r == -1 && errno == EINTR);
|
||||
return r;
|
||||
}
|
||||
|
||||
V4l2Worker::V4l2Worker(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
V4l2Worker::~V4l2Worker()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
// ── public slots ──────────────────────────────────────────────────────────────
|
||||
|
||||
void V4l2Worker::startCapture(const QString &device, int width, int height)
|
||||
{
|
||||
if (m_running.load())
|
||||
stop();
|
||||
|
||||
if (!openDevice(device, width, height))
|
||||
return;
|
||||
|
||||
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(); });
|
||||
}
|
||||
|
||||
void V4l2Worker::stopCapture()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
// ── internal stop (safe to call from any thread) ──────────────────────────────
|
||||
|
||||
void V4l2Worker::stop()
|
||||
{
|
||||
m_running.store(false);
|
||||
if (m_captureThread.joinable())
|
||||
m_captureThread.join(); // wait for captureLoop() to exit
|
||||
closeDevice();
|
||||
}
|
||||
|
||||
// ── capture loop (runs in m_captureThread) ────────────────────────────────────
|
||||
|
||||
void V4l2Worker::captureLoop()
|
||||
{
|
||||
v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(m_fd, VIDIOC_STREAMON, &type) == -1) {
|
||||
emit errorOccurred(tr("VIDIOC_STREAMON failed: %1").arg(strerror(errno)));
|
||||
m_running.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
while (m_running.load()) {
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(m_fd, &fds);
|
||||
timeval tv{0, 100000}; // 100 ms timeout → checks m_running 10x/sec
|
||||
const int r = ::select(m_fd + 1, &fds, nullptr, nullptr, &tv);
|
||||
if (r == 0) continue;
|
||||
if (r == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
emit errorOccurred(tr("select() failed: %1").arg(strerror(errno)));
|
||||
break;
|
||||
}
|
||||
|
||||
v4l2_buffer buf{};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
if (xioctl(m_fd, VIDIOC_DQBUF, &buf) == -1) {
|
||||
if (errno == EAGAIN) continue;
|
||||
emit errorOccurred(tr("VIDIOC_DQBUF failed: %1").arg(strerror(errno)));
|
||||
break;
|
||||
}
|
||||
|
||||
const QImage img = convertToImage(
|
||||
m_buffers[buf.index].start, buf.bytesused, m_width, m_height);
|
||||
if (!img.isNull())
|
||||
emit newFrame(img);
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
emit errorOccurred(tr("VIDIOC_QBUF failed: %1").arg(strerror(errno)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
xioctl(m_fd, VIDIOC_STREAMOFF, &type);
|
||||
emit captureStopped();
|
||||
}
|
||||
|
||||
// ── device open / close ───────────────────────────────────────────────────────
|
||||
|
||||
bool V4l2Worker::openDevice(const QString &device, int width, int height)
|
||||
{
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
|
||||
m_fd = ::open(device.toLocal8Bit().constData(), O_RDWR | O_NONBLOCK);
|
||||
if (m_fd == -1) {
|
||||
emit errorOccurred(tr("Cannot open %1: %2").arg(device, strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
|
||||
v4l2_capability cap{};
|
||||
if (xioctl(m_fd, VIDIOC_QUERYCAP, &cap) == -1) {
|
||||
emit errorOccurred(tr("VIDIOC_QUERYCAP failed: %1").arg(strerror(errno)));
|
||||
closeDevice();
|
||||
return false;
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
|
||||
emit errorOccurred(tr("%1 is not a video capture device").arg(device));
|
||||
closeDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t preferred[] = {
|
||||
V4L2_PIX_FMT_MJPEG,
|
||||
V4L2_PIX_FMT_YUYV,
|
||||
V4L2_PIX_FMT_NV12,
|
||||
0
|
||||
};
|
||||
|
||||
m_pixFmt = 0;
|
||||
for (int i = 0; preferred[i]; ++i) {
|
||||
v4l2_format fmt{};
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.fmt.pix.width = static_cast<uint32_t>(width);
|
||||
fmt.fmt.pix.height = static_cast<uint32_t>(height);
|
||||
fmt.fmt.pix.pixelformat = preferred[i];
|
||||
fmt.fmt.pix.field = V4L2_FIELD_ANY;
|
||||
if (xioctl(m_fd, VIDIOC_S_FMT, &fmt) == 0) {
|
||||
m_pixFmt = fmt.fmt.pix.pixelformat;
|
||||
m_width = static_cast<int>(fmt.fmt.pix.width);
|
||||
m_height = static_cast<int>(fmt.fmt.pix.height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!m_pixFmt) {
|
||||
emit errorOccurred(tr("No supported pixel format found on %1").arg(device));
|
||||
closeDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString fmtName;
|
||||
switch (m_pixFmt) {
|
||||
case V4L2_PIX_FMT_MJPEG: fmtName = "MJPEG"; break;
|
||||
case V4L2_PIX_FMT_YUYV: fmtName = "YUYV"; break;
|
||||
case V4L2_PIX_FMT_NV12: fmtName = "NV12"; break;
|
||||
default: fmtName = "?"; break;
|
||||
}
|
||||
|
||||
if (!initMmap()) {
|
||||
closeDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
emit captureStarted(device, m_width, m_height, fmtName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void V4l2Worker::closeDevice()
|
||||
{
|
||||
freeMmap();
|
||||
if (m_fd != -1) {
|
||||
::close(m_fd);
|
||||
m_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool V4l2Worker::initMmap()
|
||||
{
|
||||
v4l2_requestbuffers req{};
|
||||
req.count = MMAP_BUFFERS;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
if (xioctl(m_fd, VIDIOC_REQBUFS, &req) == -1) {
|
||||
emit errorOccurred(tr("VIDIOC_REQBUFS failed: %1").arg(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
m_nBuffers = static_cast<int>(req.count);
|
||||
|
||||
for (int i = 0; i < m_nBuffers; ++i) {
|
||||
v4l2_buffer buf{};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = static_cast<uint32_t>(i);
|
||||
if (xioctl(m_fd, VIDIOC_QUERYBUF, &buf) == -1) {
|
||||
emit errorOccurred(tr("VIDIOC_QUERYBUF failed: %1").arg(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
m_buffers[i].length = buf.length;
|
||||
m_buffers[i].start = ::mmap(nullptr, buf.length,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||
m_fd, buf.m.offset);
|
||||
if (m_buffers[i].start == MAP_FAILED) {
|
||||
emit errorOccurred(tr("mmap failed: %1").arg(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
if (xioctl(m_fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
emit errorOccurred(tr("VIDIOC_QBUF failed: %1").arg(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void V4l2Worker::freeMmap()
|
||||
{
|
||||
for (int i = 0; i < m_nBuffers; ++i) {
|
||||
if (m_buffers[i].start && m_buffers[i].start != MAP_FAILED) {
|
||||
::munmap(m_buffers[i].start, m_buffers[i].length);
|
||||
m_buffers[i].start = nullptr;
|
||||
}
|
||||
}
|
||||
m_nBuffers = 0;
|
||||
}
|
||||
|
||||
QImage V4l2Worker::convertToImage(const void *data, size_t size, int width, int height)
|
||||
{
|
||||
switch (m_pixFmt) {
|
||||
case V4L2_PIX_FMT_MJPEG: {
|
||||
QByteArray ba(static_cast<const char *>(data), static_cast<int>(size));
|
||||
QBuffer buf(&ba);
|
||||
QImageReader reader(&buf, "JPEG");
|
||||
return reader.read();
|
||||
}
|
||||
case V4L2_PIX_FMT_YUYV:
|
||||
return yuyv2Rgb(static_cast<const uchar *>(data), width, height);
|
||||
case V4L2_PIX_FMT_NV12:
|
||||
return nv122Rgb(static_cast<const uchar *>(data), width, height);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QImage V4l2Worker::yuyv2Rgb(const uchar *src, int width, int height)
|
||||
{
|
||||
QImage img(width, height, QImage::Format_RGB32);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const uchar *row = src + y * width * 2;
|
||||
QRgb *dst = reinterpret_cast<QRgb *>(img.scanLine(y));
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
const int Y0 = row[0], U = row[1]-128, Y1 = row[2], V = row[3]-128;
|
||||
row += 4;
|
||||
auto clamp = [](int v) { return static_cast<uchar>(v<0?0:v>255?255:v); };
|
||||
dst[x] = qRgb(clamp(Y0+1.402f*V), clamp(Y0-0.344f*U-0.714f*V), clamp(Y0+1.772f*U));
|
||||
dst[x+1] = qRgb(clamp(Y1+1.402f*V), clamp(Y1-0.344f*U-0.714f*V), clamp(Y1+1.772f*U));
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
QImage V4l2Worker::nv122Rgb(const uchar *src, int width, int height)
|
||||
{
|
||||
QImage img(width, height, QImage::Format_RGB32);
|
||||
const uchar *uvPlane = src + width * height;
|
||||
auto clamp = [](int v) { return static_cast<uchar>(v<0?0:v>255?255:v); };
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const uchar *yRow = src + y * width;
|
||||
const uchar *uvRow = uvPlane + (y/2) * width;
|
||||
QRgb *dst = reinterpret_cast<QRgb *>(img.scanLine(y));
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const int Y = yRow[x], U = uvRow[x&~1]-128, V = uvRow[(x&~1)+1]-128;
|
||||
dst[x] = qRgb(clamp(Y+1.402f*V), clamp(Y-0.344f*U-0.714f*V), clamp(Y+1.772f*U));
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
260
src/videowidget.cpp
Normal file
260
src/videowidget.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
#include "videowidget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QFileDialog>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
#include <QStandardPaths>
|
||||
#include <QMetaObject>
|
||||
#include <QResizeEvent>
|
||||
|
||||
// Enumerate /dev/video* entries
|
||||
static QStringList enumerateVideoDevices()
|
||||
{
|
||||
QStringList devices;
|
||||
const QDir dev("/dev");
|
||||
const auto entries = dev.entryList({"video*"}, QDir::System, QDir::Name);
|
||||
for (const auto &e : entries)
|
||||
devices << "/dev/" + e;
|
||||
return devices;
|
||||
}
|
||||
|
||||
// ── ctor / dtor ───────────────────────────────────────────────────────────────
|
||||
|
||||
VideoWidget::VideoWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
|
||||
m_thread = new QThread(this);
|
||||
m_worker = new V4l2Worker();
|
||||
m_worker->moveToThread(m_thread);
|
||||
|
||||
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
|
||||
|
||||
connect(m_worker, &V4l2Worker::newFrame, this, &VideoWidget::onNewFrame,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_worker, &V4l2Worker::captureStarted, this, [this](const QString &dev, int w, int h, const QString &fmt) {
|
||||
m_infoLabel->setText(tr("%1 %2×%3 %4").arg(dev).arg(w).arg(h).arg(fmt));
|
||||
m_running = true;
|
||||
m_startBtn->setText(tr("■ Stop"));
|
||||
});
|
||||
connect(m_worker, &V4l2Worker::captureStopped, this, [this] {
|
||||
m_running = false;
|
||||
m_startBtn->setText(tr("▶ Start"));
|
||||
m_infoLabel->setText(tr("Stopped"));
|
||||
update();
|
||||
});
|
||||
connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) {
|
||||
m_infoLabel->setText(tr("Error: %1").arg(msg));
|
||||
m_running = false;
|
||||
m_startBtn->setText(tr("▶ Start"));
|
||||
});
|
||||
|
||||
m_thread->start();
|
||||
}
|
||||
|
||||
void VideoWidget::shutdown()
|
||||
{
|
||||
if (m_shutdownDone)
|
||||
return;
|
||||
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();
|
||||
|
||||
if (m_thread && m_thread->isRunning()) {
|
||||
m_thread->quit();
|
||||
m_thread->wait(5000);
|
||||
if (m_thread->isRunning())
|
||||
m_thread->terminate();
|
||||
}
|
||||
}
|
||||
|
||||
VideoWidget::~VideoWidget()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
// ── UI setup ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::setupUi()
|
||||
{
|
||||
setMinimumHeight(140);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(2, 2, 2, 2);
|
||||
layout->setSpacing(3);
|
||||
|
||||
// ── Controls row ──────────────────────────────────────────────────────────
|
||||
auto *ctrlRow = new QHBoxLayout();
|
||||
ctrlRow->setSpacing(4);
|
||||
|
||||
m_deviceCombo = new QComboBox(this);
|
||||
m_deviceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
refreshDeviceList();
|
||||
|
||||
auto *refreshBtn = new QPushButton("↻", this);
|
||||
refreshBtn->setMaximumWidth(26);
|
||||
refreshBtn->setToolTip(tr("Refresh device list"));
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &VideoWidget::refreshDeviceList);
|
||||
|
||||
m_startBtn = new QPushButton(tr("▶ Start"), this);
|
||||
m_startBtn->setMaximumWidth(70);
|
||||
connect(m_startBtn, &QPushButton::clicked, this, &VideoWidget::onStartStop);
|
||||
|
||||
m_freezeBtn = new QPushButton(tr("❚❚ Freeze"), this);
|
||||
m_freezeBtn->setCheckable(true);
|
||||
m_freezeBtn->setMaximumWidth(80);
|
||||
connect(m_freezeBtn, &QPushButton::toggled, this, &VideoWidget::onFreeze);
|
||||
|
||||
m_screenshotBtn = new QPushButton(tr("📷"), this);
|
||||
m_screenshotBtn->setMaximumWidth(32);
|
||||
m_screenshotBtn->setToolTip(tr("Save screenshot (PNG/JPG)"));
|
||||
connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot);
|
||||
|
||||
ctrlRow->addWidget(m_deviceCombo, 1);
|
||||
ctrlRow->addWidget(refreshBtn);
|
||||
ctrlRow->addWidget(m_startBtn);
|
||||
ctrlRow->addWidget(m_freezeBtn);
|
||||
ctrlRow->addWidget(m_screenshotBtn);
|
||||
layout->addLayout(ctrlRow);
|
||||
|
||||
// ── Info label ────────────────────────────────────────────────────────────
|
||||
m_infoLabel = new QLabel(tr("No device started"), this);
|
||||
m_infoLabel->setStyleSheet("color: gray; font-size: 9px;");
|
||||
layout->addWidget(m_infoLabel);
|
||||
|
||||
// The video frame is drawn directly in paintEvent – no QLabel needed.
|
||||
// We leave stretch space for it.
|
||||
layout->addStretch(1);
|
||||
}
|
||||
|
||||
// ── Slots ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::onStartStop()
|
||||
{
|
||||
if (m_running)
|
||||
stopCapture();
|
||||
else
|
||||
startCapture();
|
||||
}
|
||||
|
||||
void VideoWidget::onFreeze(bool frozen)
|
||||
{
|
||||
m_frozen = frozen;
|
||||
m_freezeBtn->setText(frozen ? tr("▶ Live") : tr("❚❚ Freeze"));
|
||||
}
|
||||
|
||||
void VideoWidget::onScreenshot()
|
||||
{
|
||||
if (m_frame.isNull())
|
||||
return;
|
||||
|
||||
const QString defaultName =
|
||||
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
|
||||
+ "/uartscope_"
|
||||
+ QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")
|
||||
+ ".png";
|
||||
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this, tr("Save screenshot"), defaultName,
|
||||
tr("PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)"));
|
||||
|
||||
if (!path.isEmpty())
|
||||
m_frame.save(path);
|
||||
}
|
||||
|
||||
void VideoWidget::onNewFrame(const QImage &frame)
|
||||
{
|
||||
if (m_frozen)
|
||||
return;
|
||||
m_frame = frame;
|
||||
updateScaled();
|
||||
update(); // trigger paintEvent
|
||||
}
|
||||
|
||||
void VideoWidget::refreshDeviceList()
|
||||
{
|
||||
const QString current = m_deviceCombo->currentText();
|
||||
m_deviceCombo->clear();
|
||||
const auto devices = enumerateVideoDevices();
|
||||
for (const auto &d : devices)
|
||||
m_deviceCombo->addItem(d);
|
||||
// Restore previous selection if still present
|
||||
const int idx = m_deviceCombo->findText(current);
|
||||
if (idx >= 0)
|
||||
m_deviceCombo->setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
// ── Capture start/stop ────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::startCapture()
|
||||
{
|
||||
const QString dev = m_deviceCombo->currentText();
|
||||
if (dev.isEmpty())
|
||||
return;
|
||||
m_frame = QImage();
|
||||
m_scaled = QImage();
|
||||
// startCapture blocks in the worker thread until stopCapture() is called
|
||||
QMetaObject::invokeMethod(m_worker, "startCapture",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QString, dev),
|
||||
Q_ARG(int, 1920),
|
||||
Q_ARG(int, 1080));
|
||||
}
|
||||
|
||||
void VideoWidget::stopCapture()
|
||||
{
|
||||
QMetaObject::invokeMethod(m_worker, "stopCapture", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
// ── Paint ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::updateScaled()
|
||||
{
|
||||
// Video area = widget height minus the two control rows (~55 px)
|
||||
const QRect videoRect(0, 55, width(), height() - 55);
|
||||
if (videoRect.width() < 10 || videoRect.height() < 10 || m_frame.isNull())
|
||||
return;
|
||||
|
||||
m_scaled = m_frame.scaled(videoRect.size(), Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
void VideoWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
|
||||
// Background
|
||||
p.fillRect(rect(), QColor(0x10, 0x10, 0x10));
|
||||
|
||||
if (m_scaled.isNull()) {
|
||||
p.setPen(QColor(0x44, 0x44, 0x44));
|
||||
p.drawText(rect(), Qt::AlignCenter,
|
||||
m_running ? tr("Waiting for frames…") : tr("No signal"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Centre the scaled frame in the video area
|
||||
const QRect videoRect(0, 55, width(), height() - 55);
|
||||
const QPoint origin(
|
||||
videoRect.left() + (videoRect.width() - m_scaled.width()) / 2,
|
||||
videoRect.top() + (videoRect.height() - m_scaled.height()) / 2);
|
||||
p.drawImage(origin, m_scaled);
|
||||
|
||||
// Frozen overlay
|
||||
if (m_frozen) {
|
||||
p.setPen(QColor(0xff, 0xcc, 0x00));
|
||||
p.setFont(QFont("Sans", 9, QFont::Bold));
|
||||
p.drawText(videoRect.adjusted(4, 4, -4, -4),
|
||||
Qt::AlignTop | Qt::AlignRight, tr("FREEZE"));
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateScaled();
|
||||
}
|
||||
Reference in New Issue
Block a user