Расширить DSView осциллографом и декодерами двигателя и сигма-дельта

This commit is contained in:
2026-11-12 15:00:41 +03:00
parent fa5ea166a9
commit 1fc88684d5
41 changed files with 4147 additions and 151 deletions
+152
View File
@@ -0,0 +1,152 @@
--- a/DSView/pv/view/view.h
+++ b/DSView/pv/view/view.h
@@ -64,4 +64,5 @@
namespace view {
+class ScopeTrace; // Виртуальные аналоговые дорожки принадлежат представлению и не меняют аппаратный захват.
class Header;
class DevMode;
@@ -439,4 +440,6 @@
pv::toolbars::SamplingBar *_sampling_bar;
+ std::vector<ScopeTrace*> _scope_traces; // Четыре демонстрационных канала сохраняют настройки видимости в текущей сессии.
+ QAction *_scope_action = nullptr; // Доступ к меню зависит от цифрового режима текущего прибора.
QWidget *_viewcenter;
ViewStatus *_viewbottom;
--- a/DSView/pv/view/view.cpp
+++ b/DSView/pv/view/view.cpp
@@ -42,4 +42,7 @@
#include "lissajoustrace.h"
#include "analogsignal.h"
+#include "scope_trace.h" // Аналоговая эмуляция Zynq отображается вместе с цифровыми каналами.
+#include <QToolButton> // Меню каналов встраивается в существующую прокручиваемую панель управления.
+#include <QMenu> // Флажки позволяют независимо добавить и убрать каждый канал осциллографа.
#include "../sigsession.h"
@@ -202,4 +205,28 @@
connect(_header, SIGNAL(header_updated()),this, SLOT(header_updated()));
+ _trace_view_map[ScopeTrace::Type] = TIME_VIEW; // Виртуальный осциллограф использует ту же шкалу времени и прокрутку, что цифровые сигналы.
+ auto scope_button = new QToolButton(_sampling_bar); // Кнопка принадлежит штатной панели DSView и работает также без SETGUI.
+ scope_button->setObjectName("scope_channel_button"); // Постоянное имя позволяет проверять настоящий интерфейс в нативном тесте.
+ scope_button->setText(QString::fromUtf8("Осциллограф +")); // Название явно обозначает добавление аналоговой дорожки к существующим сигналам.
+ scope_button->setToolTip(QString::fromUtf8("Добавить канал осциллографа: эмуляция Zynq на общей шкале времени")); // Пользователь видит происхождение данных до включения канала.
+ scope_button->setPopupMode(QToolButton::InstantPopup); // Одно нажатие сразу открывает выбор каналов.
+ auto scope_menu = new QMenu(scope_button); // Меню и его действия удаляются вместе с панелью управления.
+ scope_button->setMenu(scope_menu); // Выбор канала выполняется внутри существующего DSView без отдельного окна генератора.
+ for (unsigned channel = 0; channel < 4; ++channel) { // Эмулятор Zynq предоставляет четыре независимых канала CH1…CH4.
+ auto trace = new ScopeTrace(channel); // Сохраняем точные частоты, формы и амплитуды исходного эмулятора.
+ trace->setParent(this); // QObject освобождает дорожку при уничтожении представления без участия аппаратной сессии.
+ _scope_traces.push_back(trace); // Постоянный объект сохраняет имя, цвет и высоту при скрытии и повторном включении.
+ auto action = scope_menu->addAction(QString::fromUtf8("CH%1 · %2 · %3 Гц · ±%4 В").arg(channel + 1).arg((channel & 1) ? QString::fromUtf8("меандр") : QString::fromUtf8("синус")).arg(set_scope::hz[channel]).arg(set_scope::peak[channel])); // Подпись показывает параметры до добавления канала.
+ action->setObjectName(QString("scope_channel_%1").arg(channel + 1)); // Идентификатор связывает действие с конкретным каналом при проверке интерфейса.
+ action->setCheckable(true); // Флажок одновременно показывает состояние и позволяет убрать дорожку повторным нажатием.
+ connect(action, &QAction::toggled, this, [this, trace](bool active) { // Меняем только выбранный аналоговый канал, сохраняя цифровые сигналы и декодеры.
+ trace->set_active(active); // Источник не запускает отдельный захват и не открывает сетевое соединение.
+ signals_changed(nullptr); // Пересчитываем высоту дорожек и диапазон общей вертикальной прокрутки.
+ set_all_update(true); // Обновляем кэш диаграммы после изменения набора дорожек.
+ viewport_update(); // Новый канал появляется сразу, в том числе до первого аппаратного захвата.
+ }); // Контекст View отключает обработчик при закрытии его окна.
+ } // Каждый канал можно включать независимо от остальных трёх.
+ _scope_action = _sampling_bar->addWidget(scope_button); // Кнопка доступна рядом с обычными настройками захвата, включая отдельный запуск DSView.
+ _scope_action->setEnabled(_device_agent->have_instance() && _device_agent->get_work_mode() == LOGIC); // Добавление доступно для цифровых источников, без вмешательства в штатный DSO.
ADD_UI(this);
}
@@ -393,4 +420,8 @@
traces.push_back(t);
}
+
+ if (_device_agent->get_work_mode() == LOGIC && (type == ALL_VIEW || type == TIME_VIEW)) { // Виртуальные аналоги дополняют только цифровую временную область.
+ for (auto trace : _scope_traces) if (trace->enabled()) traces.push_back(trace); // Скрытые каналы не занимают место и не участвуют в обработке мыши.
+ } // Данные устройства, спектр и аппаратный осциллограф сохраняют собственные списки каналов.
auto lissajous = _session->get_lissajous_trace();
@@ -706,4 +737,5 @@
void View::signals_changed(const Trace* eventTrace)
{
+ if (_scope_action) _scope_action->setEnabled(_device_agent->have_instance() && _device_agent->get_work_mode() == LOGIC); // Меню следует за сменой прибора или режима без удаления выбранных демо-каналов.
double actualMargin = SignalMargin;
int total_rows = 0;
@@ -841,4 +873,9 @@
time_traces.push_back(t);
}
+
+ for (auto trace : _scope_traces) { // Аналоговые дорожки участвуют в общей сортировке после перемещения мышью.
+ if (!trace->enabled()) continue; // Скрытый демо-канал не занимает места в компоновке.
+ if (trace->get_view_index() == -1) time_traces.push_back(trace); else all_traces.push_back(trace); // Новый канал появляется сверху, перемещённый сохраняет выбранный порядок среди цифровых сигналов.
+ } // Переупорядочивание не требует добавлять эмуляцию в аппаратный список каналов.
sort(all_traces.begin(), all_traces.end(), compare_trace_view_index);
--- a/DSView/pv/mainwindow.cpp
+++ b/DSView/pv/mainwindow.cpp
@@ -612,4 +612,38 @@
}
+ void MainWindow::checkHostScope() // Проверяет добавление аналоговых дорожек отдельно от сценариев диалогов декодеров.
+ { // Отдельная точка запуска не требует Ethernet-патча и доступна в базовой сборке DSView.
+ if (qEnvironmentVariableIsSet("DSVIEW_HOST_CHECK_SCOPE")) { // Явный проверочный режим использует реальные действия меню, не подключая USB или Ethernet.
+ const auto original_signals = _session->get_signals(); // Копия указателей проверяет сохранность существующей цифровой записи.
+ std::vector<pv::view::Trace*> before; // Список позволяет обнаружить потерю или лишнее добавление любой дорожки.
+ _view->get_traces(ALL_VIEW, before); // Исходное число включает цифровые сигналы и уже добавленные декодеры.
+ const double scale = _view->scale(); // Включение осциллографа обязано сохранить выбранный масштаб времени.
+ QList<QAction*> channels; // Используем те же QAction, которые выбирает пользователь в меню осциллографа.
+ bool passed = true; // Любое несоответствие делает нативный сценарий неуспешным.
+ for (int channel = 1; channel <= 4; ++channel) { // Проверяем независимое включение всех четырёх источников Zynq.
+ auto action = _sampling_bar->findChild<QAction*>(QString("scope_channel_%1").arg(channel)); // Поиск идёт только в панели текущего экземпляра DSView.
+ if (!action) { passed = false; break; } // Отсутствие пользовательского элемента управления не маскируется проверкой только модели.
+ channels.append(action); // Сохраняем реальные действия для проверки удаления и повторного добавления.
+ action->setChecked(true); // Сигнал toggled выполняет производственный путь компоновки и обновления графика.
+ std::vector<pv::view::Trace*> current; // Новый список должен содержать ровно одну дополнительную дорожку на выбранный канал.
+ _view->get_traces(ALL_VIEW, current); // Проверяем состояние того же списка, который рисует Viewport.
+ passed = passed && current.size() == before.size() + channel && _view->scale() == scale; // Добавление не должно терять цифровые каналы или сбрасывать время.
+ } // Все четыре канала одновременно участвуют в итоговой компоновке.
+ for (auto action : channels) action->setChecked(false); // Убираем все добавленные дорожки через пользовательские действия.
+ std::vector<pv::view::Trace*> after; // Повторное чтение обнаруживает оставшиеся скрытые трассы в активном списке.
+ _view->get_traces(ALL_VIEW, after); // Исходная сессия должна восстановиться без перезагрузки захвата.
+ passed = passed && after == before && _session->get_signals() == original_signals; // Сравниваем идентичность объектов, а не только количество каналов.
+ if (channels.size() == 4) { channels[0]->setChecked(true); channels[1]->setChecked(true); } // Итоговый снимок показывает синус и меандр рядом с исходными цифровыми сигналами.
+ _view->set_scale_offset(2e-6, 0); // Две микросекунды на пиксель позволяют визуально проверить форму синуса 1 кГц и меандра 500,3 Гц.
+ QTimer::singleShot(200, this, [this, passed]() { // Даём нативному циклу событий завершить компоновку и перерисовку всех дорожек.
+ const QString screenshot = QString::fromUtf8(qgetenv("DSVIEW_HOST_SCREENSHOT")); // Артефакт сохраняется только по явно указанному тестом пути.
+ const bool rendered = _view->grab().save(screenshot); // Проверяем фактический рендер настоящего Qt-представления, включая цифровые каналы.
+ QFile report(QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK"))); // Тест получает результат через существующий диагностический канал DSView.
+ if (report.open(QIODevice::WriteOnly | QIODevice::Truncate)) report.write(passed && rendered ? "PASS scope add/remove/digital preservation/render\n" : "FAIL scope integration\n"); // Успех требует корректного поведения меню и сохранённого изображения.
+ }); // Захват и дочерний процесс продолжают работать до штатной команды CLOSE от теста.
+ return; // Проверка осциллографа не запускает посторонние сценарии диалогов декодеров.
+ } // В обычном запуске эта ветка полностью отключена.
+ } // В обычном запуске проверочная функция не вызывается.
+
void MainWindow::on_protocol(bool visible)
{
--- a/DSView/pv/view/header.cpp
+++ b/DSView/pv/view/header.cpp
@@ -316,11 +316,5 @@
std::vector<Trace*> traces;
- for (auto s : _view.session().get_decode_signals()){
- traces.push_back(s);
- }
-
- for (auto s : _view.session().get_signals()){
- traces.push_back(s);
- }
+ _view.get_traces(TIME_VIEW, traces); // Индексы перемещения включают также виртуальные аналоговые дорожки, отсутствующие в аппаратной сессии.
sort(traces.begin(), traces.end(), View::compare_trace_y);
--- a/DSView/pv/mainwindow.h
+++ b/DSView/pv/mainwindow.h
@@ -98,4 +98,5 @@
const char *openHostFile(const QString &path);
void checkHostDecoders();
+ void checkHostScope(); // Нативный сценарий проверяет реальные действия меню и сохранность цифровой записи.
public slots:
--- a/DSView/pv/mainframe.cpp
+++ b/DSView/pv/mainframe.cpp
@@ -769,5 +769,5 @@
QFrame::show();
if (!qgetenv("DSVIEW_HOST_CHECK").isEmpty())
- QTimer::singleShot(3500, _mainWindow, &MainWindow::checkHostDecoders);
+ QTimer::singleShot(3500, _mainWindow, qEnvironmentVariableIsSet("DSVIEW_HOST_CHECK_SCOPE") ? &MainWindow::checkHostScope : &MainWindow::checkHostDecoders); // Явный параметр выбирает проверку осциллографа, сохраняя прежние сценарии декодеров.
return true;
}