Files
templates/tools/dsview/decoder-scope.patch
T

189 lines
25 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- a/DSView/pv/mainwindow.cpp
+++ b/DSView/pv/mainwindow.cpp
@@ -84,6 +84,8 @@
#include "dock/protocoldock.h"
#include "view/view.h"
+#include "view/decoder_scope.h" // Нативная проверка открывает и проверяет отдельный осциллограф в реальном процессе DSView.
+#include "view/decodetrace.h" // Тест использует настоящий нижний анализатор и его контекстное действие без подмены данных.
#include "view/trace.h"
#include "view/signal.h"
#include "view/dsosignal.h"
@@ -652,6 +654,51 @@
void MainWindow::checkHostScope() // Проверяет добавление аналоговых дорожек отдельно от сценариев диалогов декодеров.
{ // Отдельная точка запуска не требует Ethernet-патча и доступна в базовой сборке DSView.
+ if (qEnvironmentVariableIsSet("DSVIEW_HOST_CHECK_DECODER_SCOPE")) { // Дополнительный сценарий выполняется только при явном запуске проверки и не влияет на пользовательскую сессию.
+ QTimer::singleShot(1500, _view, [this]() { // Пауза даёт трём анализаторам короткой тестовой записи завершить полный расчёт перед проверкой окна.
+ bool passed = !_session->get_decode_signals().empty(); // Без исходного анализатора положительный результат невозможен.
+ auto *trace = passed ? _session->get_decode_signals().back() : nullptr; // Тестовая запись заканчивается именно графиком токов Ia/Ib/Ic.
+ const double scale = _view->scale(); const int64_t offset = _view->offset(); // Независимое увеличение не должно менять область основной записи.
+ const size_t digital_count = _session->get_signals().size(); const size_t decoder_count = _session->get_decode_signals().size(); // Данные и соседние анализаторы сохраняются при открытии и закрытии дополнительного окна.
+ auto *stack = trace ? trace->decoder() : nullptr; const uint64_t results = stack ? stack->get_result_count() : 0; // Повторный анализ не требуется, поэтому число готовых результатов должно оставаться неизменным.
+ QString diagnostic = QString("target_y=%1 source_results=%2 running=%3").arg(trace ? trace->get_y() : -1).arg(results).arg(stack ? stack->IsRunning() : false); // Диагностика отличает ошибку выбора области от незавершённого исходного анализа.
+ auto mark = [&diagnostic, &passed](const char *stage) { if (!passed) diagnostic += QString(" failed_after=") + stage; }; // Первая неуспешная стадия остаётся видимой без повторного запуска отладочного сценария.
+ _view->get_time_view()->repaint(); _view->get_time_view()->grab().save(QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK")) + ".source.png"); // Перерисовывается настоящий временной Viewport, расположенный внутри общего QScrollArea::viewport.
+ auto find_scope = []() -> QWidget* { for (auto *widget : QApplication::topLevelWidgets()) if (widget->objectName() == "decoder_scope_window") return widget; return nullptr; }; // Проверка ищет настоящее отдельное окно, а не его миниатюру или область основного вида.
+ if (trace) { QMouseEvent click(QEvent::MouseButtonDblClick, QPointF(_view->get_view_width() / 2, trace->get_y() - trace->text_row_height() / 2), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(_view->get_time_view(), &click); } // Событие получает именно временной график, а не его контейнер QScrollArea; точка лежит выше строки предупреждений.
+ QPointer<QWidget> scope = find_scope(); // Слабая ссылка проверяет фактическое удаление при закрытии окна.
+ passed = passed && scope && scope->isWindow() && scope->property("scope_decoder").toString() == "pwm_motor" && scope->property("scope_rows").toStringList() == QStringList({"wave_a", "wave_b", "wave_c"}); // В отдельном окне должны оказаться ровно три фазных тока без соседних графиков.
+ mark("open_selected"); // Проверяется выбор нижнего графика до остальных взаимодействий.
+ if (scope) { // Последующие действия выполняются только при успешном создании настоящего окна.
+ scope->resize(1100, 640); scope->grab(); passed = passed && scope->property("scope_segments").toULongLong() > 0; mark("render"); // QWidget::grab принудительно рисует новое окно даже до первого события его экспозиции в Windows.
+ const double span = scope->property("scope_span").toDouble(); const QPointF point(600, 300); // Внутренняя точка гарантированно находится в области графика после изменения размера.
+ QWheelEvent wheel(point, scope->mapToGlobal(point.toPoint()), QPoint(), QPoint(0, 120), Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false); QApplication::sendEvent(scope, &wheel); // Один реальный шаг колеса увеличивает временной масштаб только отдельного осциллографа.
+ passed = passed && scope->property("scope_span").toDouble() < span && _view->scale() == scale && _view->offset() == offset; // Проверяется независимость времени от основного DSView.
+ mark("zoom"); // Уменьшение видимого интервала подтверждается собственными координатами отдельного окна.
+ const double begin = scope->property("scope_start").toDouble(); QMouseEvent press(QEvent::MouseButtonPress, point, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(scope, &press); // Начальная точка жеста принадлежит текущему увеличенному интервалу.
+ QMouseEvent move(QEvent::MouseMove, point - QPointF(100, 0), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(scope, &move); // Перемещение влево должно показать более поздний участок той же записи.
+ QMouseEvent release(QEvent::MouseButtonRelease, point - QPointF(100, 0), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); QApplication::sendEvent(scope, &release); passed = passed && scope->property("scope_start").toDouble() > begin; // Отпускание завершает жест после реального изменения временного положения.
+ mark("pan"); // Прокрутка должна менять начало собственного видимого интервала.
+ auto *fit = scope->findChild<QPushButton*>("scope_fit_time"); if (fit) fit->click(); passed = passed && fit && std::abs(scope->property("scope_span").toDouble() - double(stack->sample_count())) < 0.5; mark("fit_time"); // Полная запись сравнивается с числом исходных отсчётов, а не с округлённым начальным масштабом основного окна.
+ auto *range = scope->findChild<QDoubleSpinBox*>("scope_y_range"); if (range) range->setValue(50); passed = passed && range && range->value() == 50; // Ручная шкала ±50 А не меняет параметры расчётной модели двигателя.
+ auto *fit_y = scope->findChild<QPushButton*>("scope_fit_amplitude"); if (fit_y) fit_y->click(); passed = passed && fit_y && range && range->value() > 50; mark("fit_amplitude"); // Автомасштаб должен вместить пусковые токи около 80 А после ручного выбора ±50 А.
+ scope->repaint(); const QString screenshot = QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK")) + ".png"; passed = scope->grab().save(screenshot) && passed; // Снимок содержит только отдельный выбранный график и служит визуальным артефактом проверки.
+ passed = passed && stack->get_result_count() == results && _session->get_signals().size() == digital_count && _session->get_decode_signals().size() == decoder_count; // Все взаимодействия используют кэш и сохраняют исходную цифровую запись.
+ mark("cached_results"); // Открытие и перемещение графика не должны запускать повторное декодирование.
+ scope->close(); QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); passed = passed && scope.isNull(); // Закрытие дополнительного осциллографа должно действительно освобождать его объект.
+ mark("close"); // Исчезновение окна проверяется слабой ссылкой на настоящий QWidget.
+ trace->show_scope_menu(_view->mapToGlobal(QPoint(100, 100))); auto *menu = _view->findChild<QMenu*>("decoder_scope_menu"); // Повторное открытие проверяет альтернативный пользовательский путь через контекстное меню имени.
+ passed = passed && menu && menu->actions().size() == 1; if (menu && !menu->actions().empty()) { menu->actions().front()->trigger(); menu->close(); } // Нижний совмещённый график должен предлагаться одним действием A/B/C.
+ QPointer<QWidget> reopened = find_scope(); passed = passed && reopened; // Закрытие предыдущего окна не мешает повторному открытию того же анализатора.
+ mark("menu_reopen"); // Проверяется наличие одного контекстного действия и его связь с теми же готовыми данными.
+ _session->remove_decoder(int(decoder_count - 1)); QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); // Удаление тестового анализатора воспроизводит смену источника и проверяет слабую связь окна с данными.
+ passed = passed && reopened.isNull() && _session->get_signals().size() == digital_count && _session->get_decode_signals().size() == decoder_count - 1; // Окно закрывается вместе с источником, а цифровые каналы и два соседних анализатора сохраняются.
+ mark("source_removal"); // Удаление исходного стека не должно оставлять окно с недействительными указателями.
+ } // В случае отсутствия окна журнал получит явный FAIL без обращения по нулевому указателю.
+ QFile report(QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK"))); if (report.open(QIODevice::WriteOnly | QIODevice::Truncate)) report.write(passed ? "PASS selected decoder scope / render / independent zoom and pan / range / cached results / reopen / source removal\n" : (QString("FAIL detached decoder scope ") + diagnostic + "\n").toUtf8()); // Отчёт проверяет полный жизненный цикл и при ошибке сохраняет координаты выбора и состояние исходного анализа.
+ }); // Проверка не блокирует главный цикл событий DSView во время ожидания результата.
+ return; // Демонстрационные CH1…CH4 в этом отдельном сценарии не добавляются.
+ } // В обычной работе ветка проверки отключена.
if (qEnvironmentVariableIsSet("DSVIEW_HOST_CHECK_SCOPE")) { // Явный проверочный режим использует реальные действия меню, не подключая USB или Ethernet.
const auto original_signals = _session->get_signals(); // Копия указателей проверяет сохранность существующей цифровой записи.
std::vector<pv::view::Trace*> before; // Список позволяет обнаружить потерю или лишнее добавление любой дорожки.
--- a/DSView/pv/view/decodetrace.cpp
+++ b/DSView/pv/view/decodetrace.cpp
@@ -34,6 +34,7 @@
#include <QApplication>
#include "decodetrace.h"
#include "pwm_waveform.h" // Индекс экстремумов сохраняет форму PWM при любом масштабе времени.
+#include "decoder_scope.h" // Самостоятельное окно использует тот же индекс выбранной графической области анализатора.
#include "../sigsession.h"
#include "../data/decoderstack.h"
#include "../data/decode/decoder.h"
@@ -222,6 +223,7 @@
_cur_row_headings.clear();
_cur_row_heading_offsets.clear(); // Координаты и подписи пересоздаются вместе после изменения видимых строк декодера.
+ _scope_regions.clear(); // Старые области выбора не должны оставаться после смены раскладки, очистки или нового декодирования.
// Show sample rate as 1Hz when it is unknown
if (samplerate == 0.0)
@@ -278,6 +280,8 @@
const bool allocate = !row_combined || !wave_shown; // Каждому дополнительному расчётному графику всегда выделяется собственная область.
if (allocate) combined_y = y; // Раздельный режим начинает новую область для каждого канала.
const int wave_top = combined_y; // Следующие совмещённые фазы используют ту же вертикальную систему координат.
+ if (allocate) _scope_regions.push_back({wave_top - annotation_height * 0.5, wave_top + annotation_height * 2.5, QString::fromUtf8(dec->decoder()->id), QStringList{row_id}, get_name() + QStringLiteral(" · ") + (row_combined ? QStringLiteral("A / B / C") : row.title())}); // Координаты совпадают с областью отсечения нарисованного графика, а совмещённые фазы открываются вместе.
+ else _scope_regions.back().rows.append(row_id); // Вторая и третья фазы той же области добавляются в одно отдельное окно без предупреждений и других анализаторов.
p.save(); // Локальные настройки пера и отсечения не затрагивают остальные строки анализатора.
const int wave_y = wave_top + annotation_height; // Нулевая линия располагается в середине собственной либо общей трёхстрочной области PWM.
p.setClipRect(QRectF(left, wave_top - annotation_height * 0.5, right - left, annotation_height * 3)); // Совмещённые фазы получают одинаковую область отсечения.
@@ -786,6 +790,26 @@
}
//to show decoder's property setting dialog
+bool DecodeTrace::open_scope_at(int y) // Обработчик вызывается только для двойного щелчка в области данных DSView.
+{ // Текстовые аннотации и цифровые сигналы сохраняют прежнее поведение мыши.
+ for (const auto &region : _scope_regions) if (y >= region.top && y < region.bottom) { // Попадание учитывает актуальные высоты областей и объединение фаз текущего кадра.
+ auto *window = new DecoderScopeWindow(_decoder_stack, _view, region.decoder, region.rows, region.title); window->show(); // Новое окно разделяет готовые данные только выбранного анализатора и имеет собственную временную шкалу.
+ return true; // Исходный обработчик двойного щелчка больше не добавляет курсор поверх открытого графика.
+ } // Все области принадлежат только данному экземпляру декодера.
+ return false; // При отсутствии графика исходное взаимодействие DSView продолжается.
+} // Удаление источника автоматически закрывает созданное окно через слабую ссылку DecoderScopeWindow.
+
+void DecodeTrace::show_scope_menu(const QPoint &position) // Альтернативный способ открытия доступен через название анализатора слева.
+{ // Меню содержит только реально отображаемые графические области текущего анализатора.
+ if (_scope_regions.empty()) return; // Обычные текстовые декодеры не получают пустого контекстного меню.
+ auto *menu = new QMenu(_view); menu->setObjectName("decoder_scope_menu"); menu->setAttribute(Qt::WA_DeleteOnClose); // Меню принадлежит текущему представлению и освобождается при закрытии.
+ for (const auto &region : _scope_regions) { // Раздельные фазы, общий A/B/C, θ и несущая перечисляются как отдельные варианты.
+ auto *action = menu->addAction(QString::fromUtf8("Открыть в отдельном окне: ") + region.title); // Название явно указывает, какой именно график будет вынесен.
+ connect(action, &QAction::triggered, this, [this, region]() { auto *window = new DecoderScopeWindow(_decoder_stack, _view, region.decoder, region.rows, region.title); window->show(); }); // Контекст DecodeTrace отменяет обработчик, если исходный анализатор удалён.
+ } // Выбор одной области не открывает соседние анализаторы или цифровые дорожки.
+ menu->popup(position); // Неблокирующее меню сохраняет обработку потока декодирования и команд SETGUI.
+} // Позиция передаётся в глобальных координатах мыши.
+
bool DecodeTrace::create_popup(bool isnew)
{
(void)isnew;
--- a/DSView/pv/view/decodetrace.h
+++ b/DSView/pv/view/decodetrace.h
@@ -30,6 +30,7 @@
#include <QFormLayout>
#include <QWidget>
#include <QString>
+#include <QStringList> // Безопасные идентификаторы фаз и дополнительных графиков не содержат указателей на результаты декодирования.
#include "trace.h"
#include "../prop/binding/decoderoptions.h"
@@ -143,6 +144,8 @@
void* get_key_handel();
bool create_popup(bool isnew);
+ bool open_scope_at(int y); // Двойной щелчок открывает только графическую область под указанной координатой представления.
+ void show_scope_menu(const QPoint &position); // Контекстное меню имени анализатора перечисляет его отдельные графики.
protected:
void paint_type_options(QPainter &p, int right, const QPoint pt, QColor fore);
@@ -196,6 +199,8 @@
std::vector<QString> _cur_row_headings;
std::vector<int> _cur_row_heading_offsets; // Относительные координаты заголовков учитывают разную высоту текстовых и графических строк.
+ struct ScopeRegion { double top, bottom; QString decoder; QStringList rows; QString title; }; // Одна область связывает фактические координаты кадра с безопасными идентификаторами выбранных кривых.
+ std::vector<ScopeRegion> _scope_regions; // Список пересоздаётся при рисовании, учитывая совмещение фаз, скрытие строк и ручную высоту графиков.
};
--- a/DSView/pv/view/header.cpp
+++ b/DSView/pv/view/header.cpp
@@ -25,6 +25,7 @@
#include <QColorDialog>
#include <QInputDialog>
#include <QMenu>
+#include <QContextMenuEvent> // Положение контекстного меню определяется текущим нажатием, без зависимости от последнего наведения мыши.
#include <QMouseEvent>
#include <QPainter>
#include <QRect>
@@ -544,14 +545,11 @@
void Header::contextMenuEvent(QContextMenuEvent *event)
{
- (void)event;
-
- int action;
-
- const auto t = get_mTrace(action, _mouse_point);
-
- if (!t || !t->selected() || action != Trace::LABEL)
- return;
+ int action = 0; // Поиск использует штатные области имени, метки и цвета дорожки.
+ auto *trace = get_mTrace(action, event->pos()); // Правый щелчок выбирает конкретный анализатор под курсором, включая ещё не выделенный.
+ if (trace && trace->get_type() == SR_CHANNEL_DECODER) { // Контекстное открытие графиков не изменяет меню цифровых и аппаратных аналоговых каналов.
+ static_cast<DecodeTrace*>(trace)->show_scope_menu(event->globalPos()); event->accept(); // Пользователь выбирает одну осциллограмму этого анализатора в неблокирующем меню.
+ } // Для текстового декодера без графических строк отдельное меню не появляется.
}
void Header::on_action_set_name_triggered()
--- a/DSView/pv/view/viewport.cpp
+++ b/DSView/pv/view/viewport.cpp
@@ -1260,6 +1260,10 @@
if (!_view.get_view_rect().contains(event->pos()))
return;
+ if (event->button() == Qt::LeftButton) for (auto *trace : _view.session().get_decode_signals()) { // Двойной щелчок по графику анализатора обрабатывается раньше добавления обычных измерительных курсоров.
+ if (trace->open_scope_at(event->pos().y())) { event->accept(); return; } // Проверяются фактические области кадра; новое окно содержит только выбранную осциллограмму.
+ } // Щелчки по цифровым сигналам, текстам и остальным видам сохраняют прежнюю обработку DSView.
+
int mode = _view.session().get_device()->get_work_mode();
if (mode == LOGIC)