Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ green locally. They are worth knowing before you spend a round trip on one:
releases format differently, and a newer analyser reports different findings; both make
a formatting job fail on code your editor just formatted.
* **CI builds against the minimum supported Qt**, not the newest. Code that compiles
against a newer Qt locally can still fail there — Qt's own macros differ between
versions.
against a newer Qt locally can still fail there — Qt's own macros and overload sets
differ between versions. A concrete one: the tree is built with
`QT_NO_CAST_FROM_ASCII`, and `QString::arg("text")` compiles against Qt 6.10 but not
against the 6.8 minimum. Wrap every string literal handed to a Qt API in
`QStringLiteral` or `tr()`.
* **libstdc++ and libc++ disagree about which headers include which.** A translation unit
that compiles on macOS can fail on Linux for a missing `<cstdint>`. Include what you use.

Expand Down
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ evaluates traces against CISPR/FCC limit lines and produces reproducible reports

| | |
|---|---|
| Status | Early development — the measurement suite, its instrument buses, reporting and the plugin bridge are in place (milestones M0–M7) |
| Status | Early development: the measurement suite, its instrument buses, reporting and the plugin bridge are in place (milestones M0–M7) |
| Language | C++23, Qt 6.8+ (developed on 6.10) |
| Platforms | Windows 10/11, Linux (Ubuntu 22.04+), macOS 13+ |
| Docs | [Contributing](CONTRIBUTING.md) · [Requirements](docs/requirements.md) · [Architecture](docs/architecture.md) · [Plugin API](docs/plugin-api.md) · [Tasklist](tasklist.md) |
Expand All @@ -32,7 +32,7 @@ evaluates traces against CISPR/FCC limit lines and produces reproducible reports
instrument cannot make is refused with a reason rather than sent and rejected.
* **Nothing is hidden.** Every SCPI exchange is visible in the console and in the
`--verbose` transcript. Limit lines and correction tables are documented CSV/JSON you can
write by hand, and sessions are versioned JSON — your measurements stay diffable,
write by hand, and sessions are versioned JSON, so your measurements stay diffable,
scriptable and readable without PeakEmi.
* **Nothing to plug in to start.** The simulated analyzer ships with the application, so the
full loop, including the report, can be tried before any hardware is on the bench.
Expand All @@ -41,7 +41,7 @@ evaluates traces against CISPR/FCC limit lines and produces reproducible reports
* **No licence server, no dongle, no seat count.** GPL-3.0-or-later; build it yourself and
run it offline.

The instrument list is short and the scope is deliberately narrow — pre-compliance
The instrument list is short and the scope is deliberately narrow: pre-compliance
measurement, not lab automation. Turntables, masts and LISNs are switched through the
[commands sent around a run](#what-works-today), not driven by PeakEmi.

Expand All @@ -58,11 +58,16 @@ verified Phase 2 points. Traces are decimated to a min/max envelope per pixel co

![Spectrum plot with limit overlay and flagged peaks](docs/images/spectrum-plot.png)

Every verified point lands in the results table with its full provenance — detector, resolution
bandwidth, dwell time, limit, margin and timestamp — coloured by verdict.
Every verified point lands in the results table with its full provenance (detector, resolution
bandwidth, dwell time, limit, margin and timestamp), coloured by verdict.

![Phase 2 result table](docs/images/results-table.png)

The interface follows the desktop colour scheme, and the verdict colours are chosen per scheme
so the table stays readable in either:

![Phase 2 result table in dark mode](docs/images/results-table-dark.png)

The log dock keeps the run narrative and the raw SCPI transcript side by side, so what the
application asked the instrument is always inspectable.

Expand All @@ -75,44 +80,45 @@ so they cannot drift from the application:
cmake --preset debug -DPEAKEMI_BUILD_TOOLS=ON
cmake --build --preset debug --target peakemi_screenshots
QT_QPA_PLATFORM=offscreen ./build/debug/bin/peakemi_screenshots docs/images
QT_QPA_PLATFORM=offscreen ./build/debug/bin/peakemi_screenshots docs/images --dark
```

## What works today

* **Two-phase automated run** — fast peak scan, peak flagging against the active limit
* **Two-phase automated run**: fast peak scan, peak flagging against the active limit
lines, then a quasi-peak dwell on every flagged peak with the CISPR-mandated RBW.
Pausable, resumable, abortable, with autosave after every verified point.
* **Simulated analyzer** — ships with the app, needs no hardware, and produces a
* **Simulated analyzer**: ships with the app, needs no hardware, and produces a
deterministic spectrum that fails CISPR 32 class B in a few places, so a new user can
go from launch to a finished run and a PDF report in a minute.
* **Instruments** — raw SCPI over TCP, VXI-11, USBTMC and serial, `*IDN?`-scored driver
* **Instruments**: raw SCPI over TCP, VXI-11, USBTMC and serial, `*IDN?`-scored driver
selection, bounded and opt-in LAN sweep, live USB hotplug detection, serial port
enumeration, an optional VISA path, and a raw SCPI console.
* **Supported analyzers** — Siglent SSA3021X/3032X/3075X and SVA1015X/1032X/1075X, Rigol
* **Supported analyzers**: Siglent SSA3021X/3032X/3075X and SVA1015X/1032X/1075X, Rigol
DSA705/710 and DSA815/832/875, each with its own frequency range, point count and
command dialect.
* **Python driver plugins** — in a build configured with `PEAKEMI_WITH_PYTHON=ON`, a
* **Python driver plugins**: in a build configured with `PEAKEMI_WITH_PYTHON=ON`, a
driver can be a single Python file loaded into the embedded interpreter, and the
measurement engine cannot tell it from one written in C++. A plugin is
imported only after you approve it, and the approval is recorded as a hash of the file's
contents, so an edited plugin is untrusted again until you say otherwise. See the
[plugin API](docs/plugin-api.md) and the worked
[example driver](plugins/drivers/example_sweeper.py).
* **Commands around a run** — a run can send operator-supplied commands when it starts and
* **Commands around a run**: a run can send operator-supplied commands when it starts and
when it ends, which is how a LISN, a relay box or a mast that speaks SCPI is switched.
The closing commands are sent whether the run finished, was aborted or failed. PeakEmi
drives no relays itself.
* **Limits and corrections** — built-in CISPR 32 / EN 55032 and FCC Part 15 B catalogue,
* **Limits and corrections**: built-in CISPR 32 / EN 55032 and FCC Part 15 B catalogue,
CSV/JSON import of custom limits and of antenna/cable/gain correction tables
(see [`resources/`](resources) for the documented file formats).
* **Sessions and exports** — versioned JSON session container written atomically, CSV and
* **Sessions and exports**: versioned JSON session container written atomically, CSV and
JSON export of traces and results, and a PDF report carrying the mandatory
pre-compliance disclaimer. Company, address, logo and the free-text sections come from
a report template that can be edited in the app, shared as a file and kept as the
default.

What is left is packaging: a notarised macOS disk image, signed Windows artifacts and a
Windows installer — see [tasklist.md](tasklist.md).
Windows installer, see [tasklist.md](tasklist.md).

### Instrument buses

Expand Down Expand Up @@ -171,6 +177,6 @@ any hardware attached. Logs are written to the platform application data directo

## Licence

GPL-3.0-or-later — the full text is in [LICENSE](LICENSE). The plugin API headers are
GPL-3.0-or-later, the full text is in [LICENSE](LICENSE). The plugin API headers are
dual-licensed so that proprietary in-house drivers remain possible; see
[requirements.md §1.4](docs/requirements.md#14-technology-stack-con-3).
Binary file added docs/images/about-dialog-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/about-dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/log-console-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/log-console.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/main-window-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/main-window.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/results-table-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/results-table.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/spectrum-plot-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/spectrum-plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions resources/peakemi.qrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/peakemi">
<file alias="icon.png">peakemi.png</file>
</qresource>
</RCC>
6 changes: 6 additions & 0 deletions src/app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <QApplication>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QIcon>
#include <QStandardPaths>
#include <QString>

Expand Down Expand Up @@ -63,6 +64,11 @@ int main(int argc, char* argv[])
peakemi::registerMetaTypes();
peakemi::drivers::registerBuiltInDrivers();

// Compiled in rather than loaded from disk, so the icon is there on every
// platform and in every packaging of the application. AboutDialog.cpp pulls
// the resource into the link.
QApplication::setWindowIcon(QIcon{QStringLiteral(":/peakemi/icon.png")});

peakemi::ui::MainWindow window;
window.show();

Expand Down
4 changes: 4 additions & 0 deletions src/ui/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
peakemi_add_module(ui
QT_AUTOMOC
QT_AUTORCC
SOURCES
"${PROJECT_SOURCE_DIR}/resources/peakemi.qrc"
dialogs/AboutDialog.cpp
dialogs/PluginManagerDialog.cpp
dialogs/ReportTemplateDialog.cpp
docks/InstrumentDock.cpp
Expand All @@ -12,6 +15,7 @@ peakemi_add_module(ui
plot/PainterPlotBackend.cpp
windows/MainWindow.cpp
HEADERS
include/peakemi/ui/AboutDialog.h
include/peakemi/ui/IPlotBackend.h
include/peakemi/ui/InstrumentDock.h
include/peakemi/ui/LogDock.h
Expand Down
151 changes: 151 additions & 0 deletions src/ui/dialogs/AboutDialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#include <peakemi/core/Disclaimer.h>
#include <peakemi/core/Version.h>
#include <peakemi/ui/AboutDialog.h>

#include <QApplication>
#include <QDialogButtonBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QPalette>
#include <QPushButton>
#include <QVBoxLayout>

#include <string_view>

/// Registers the compiled-in resources.
///
/// At global scope on purpose, and not in an anonymous namespace:
/// Q_INIT_RESOURCE declares the generated symbol in whatever namespace it is
/// expanded in, while the generated one lives in none. The call is needed at
/// all because the resource is compiled into a static library, from which the
/// linker drops anything that nothing references.
// NOLINTNEXTLINE(misc-use-anonymous-namespace)
static void initPeakEmiResources()
{
Q_INIT_RESOURCE(peakemi);
}

namespace peakemi::ui {
namespace {

constexpr int IconSize = 96;
constexpr int DialogWidth = 560;
constexpr int NoticeMargin = 10;

/// A link colour that stays readable on the background it is drawn on.
///
/// Several desktop dark themes leave QPalette::Link at its light-theme navy,
/// which all but disappears on a dark window, so the dialog picks its own.
[[nodiscard]] QColor linkColour(const QPalette& palette)
{
const bool dark = palette.color(QPalette::Window).lightness() < 128;
return dark ? QColor{0x7F, 0xB6, 0xFF} : QColor{0x11, 0x4E, 0xA8};
}

/// Lets the layout ask a wrapped label how tall it needs to be at a given
/// width. Without this the dialog is sized from the label's first guess and
/// the last line of a paragraph ends up cut off.
void wrapAndGrow(QLabel* label)
{
label->setWordWrap(true);
QSizePolicy policy = label->sizePolicy();
policy.setHeightForWidth(true);
label->setSizePolicy(policy);
}

[[nodiscard]] QString qs(std::string_view text)
{
return QString::fromUtf8(text.data(), static_cast<qsizetype>(text.size()));
}

} // namespace

QString AboutDialog::repositoryUrl()
{
return QStringLiteral("https://github.com/bitcrushtesting/peakemi");
}

QString AboutDialog::licenceUrl()
{
return repositoryUrl() + QStringLiteral("/blob/main/LICENSE");
}

AboutDialog::AboutDialog(QWidget* parent) : QDialog{parent}
{
initPeakEmiResources();

setWindowTitle(tr("About PeakEmi"));
setObjectName(QStringLiteral("aboutDialog"));

QPalette scheme = palette();
scheme.setColor(QPalette::Link, linkColour(scheme));
scheme.setColor(QPalette::LinkVisited, linkColour(scheme));
setPalette(scheme);

auto* layout = new QVBoxLayout{this};

auto* header = new QHBoxLayout;
header->setSpacing(16);
auto* icon = new QLabel{this};
icon->setObjectName(QStringLiteral("aboutIcon"));
icon->setPixmap(QIcon{QStringLiteral(":/peakemi/icon.png")}.pixmap(IconSize, IconSize));
icon->setFixedSize(IconSize, IconSize);
header->addWidget(icon);

auto* title = new QLabel{this};
title->setObjectName(QStringLiteral("aboutTitle"));
title->setTextFormat(Qt::RichText);
title->setText(tr("<h2 style='margin-bottom:2px'>PeakEmi %1</h2>"
"<p style='margin-top:0'>EMI pre-compliance measurement suite<br>"
"Developed by Bitcrush Testing</p>")
.arg(qs(ProjectVersion)));
header->addWidget(title, 1);
layout->addLayout(header);

auto* details = new QLabel{this};
details->setObjectName(QStringLiteral("aboutDetails"));
details->setTextFormat(Qt::RichText);
wrapAndGrow(details);
// Links open in the user's browser rather than inside a measurement tool.
details->setOpenExternalLinks(true);
details->setTextInteractionFlags(Qt::TextBrowserInteraction);
details->setText(tr("<p><b>Licence:</b> GPL-3.0-or-later — "
"<a href=\"%1\">read the licence</a>.<br>"
"PeakEmi is free software: you may use, study, share and modify it "
"under the terms of the GNU General Public License, version 3 or "
"any later version. It comes with no warranty.</p>"
"<p><b>Source:</b> <a href=\"%2\">%2</a></p>")
.arg(licenceUrl(), repositoryUrl()));
layout->addWidget(details);

// The notice reads as a notice: framed, and never quieter than the rest.
// The frame is a widget of its own rather than QLabel::setFrameShape so
// that its layout accounts for the border when the text wraps.
auto* notice = new QFrame{this};
notice->setFrameShape(QFrame::StyledPanel);
auto* noticeLayout = new QVBoxLayout{notice};
noticeLayout->setContentsMargins(NoticeMargin, NoticeMargin, NoticeMargin, NoticeMargin);

auto* disclaimer = new QLabel{notice};
disclaimer->setObjectName(QStringLiteral("aboutDisclaimer"));
wrapAndGrow(disclaimer);
disclaimer->setText(qs(ComplianceDisclaimer));
noticeLayout->addWidget(disclaimer);
layout->addWidget(notice);

auto* buttons = new QDialogButtonBox{QDialogButtonBox::Close, this};
auto* aboutQt = buttons->addButton(tr("About Qt"), QDialogButtonBox::ActionRole);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::accept);
connect(aboutQt, &QPushButton::clicked, qApp, &QApplication::aboutQt);
layout->addWidget(buttons);

// Fix the width first: a wrapped paragraph only knows how tall it is once
// it knows how wide it is, so the height has to be asked for at that width.
setMinimumWidth(DialogWidth);
layout->activate();
resize(DialogWidth, layout->totalHeightForWidth(DialogWidth));
}

} // namespace peakemi::ui
26 changes: 26 additions & 0 deletions src/ui/include/peakemi/ui/AboutDialog.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#include <QDialog>
#include <QString>

namespace peakemi::ui {

/// What PeakEmi is, who made it, what it may be used for and where to get it.
///
/// A dialog of its own rather than QMessageBox::about, because the links have
/// to be clickable: a licence a user cannot open is not much of a licence
/// notice, and a repository nobody can reach is not much of a source offer.
class AboutDialog : public QDialog
{
Q_OBJECT

public:
explicit AboutDialog(QWidget* parent = nullptr);

/// Where the source lives. Also the source offer the GPL expects to
/// accompany a distributed binary.
[[nodiscard]] static QString repositoryUrl();
[[nodiscard]] static QString licenceUrl();
};

} // namespace peakemi::ui
13 changes: 11 additions & 2 deletions src/ui/include/peakemi/ui/ResultsTableModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ namespace peakemi::ui {

/// Table model over the Phase 2 results (FR-VIS-4, FR-DAT-1).
///
/// Verdict colouring lives in the model's BackgroundRole so the view stays a
/// plain QTableView and sorting/filtering works through a proxy.
/// Verdict colouring lives in the model's Background and Foreground roles so
/// the view stays a plain QTableView and sorting/filtering works through a
/// proxy. Both colours are picked for the active colour scheme: a background
/// chosen for a light theme leaves the text unreadable on a dark one.
class ResultsTableModel : public QAbstractTableModel
{
Q_OBJECT
Expand Down Expand Up @@ -54,7 +56,14 @@ class ResultsTableModel : public QAbstractTableModel

[[nodiscard]] const MeasurementPoint* pointAt(int row) const;

protected:
/// Watches the application for theme changes; see refreshColours().
bool eventFilter(QObject* watched, QEvent* event) override;

private:
/// Repaint the verdict colours after a theme change.
void refreshColours();

std::vector<MeasurementPoint> m_points;
};

Expand Down
Loading
Loading