--- 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 _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 // Меню каналов встраивается в существующую прокручиваемую панель управления. +#include // Флажки позволяют независимо добавить и убрать каждый канал осциллографа. #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 before; // Список позволяет обнаружить потерю или лишнее добавление любой дорожки. + _view->get_traces(ALL_VIEW, before); // Исходное число включает цифровые сигналы и уже добавленные декодеры. + const double scale = _view->scale(); // Включение осциллографа обязано сохранить выбранный масштаб времени. + QList channels; // Используем те же QAction, которые выбирает пользователь в меню осциллографа. + bool passed = true; // Любое несоответствие делает нативный сценарий неуспешным. + for (int channel = 1; channel <= 4; ++channel) { // Проверяем независимое включение всех четырёх источников Zynq. + auto action = _sampling_bar->findChild(QString("scope_channel_%1").arg(channel)); // Поиск идёт только в панели текущего экземпляра DSView. + if (!action) { passed = false; break; } // Отсутствие пользовательского элемента управления не маскируется проверкой только модели. + channels.append(action); // Сохраняем реальные действия для проверки удаления и повторного добавления. + action->setChecked(true); // Сигнал toggled выполняет производственный путь компоновки и обновления графика. + std::vector 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 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 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; }