// Самостоятельный осциллограф читает индекс готового декодера и не запускает повторный анализ записи. #ifndef SET_DECODER_SCOPE_H // Защита позволяет включать виджет из графика и нативной проверки одного процесса. #define SET_DECODER_SCOPE_H // Каждая единица трансляции получает единственное определение класса. #include // Обычное окно Qt остаётся частью того же DSView и может разворачиваться независимо. #include // Слабые ссылки предотвращают обращение к удалённым результатам или основному представлению. #include // Кривые и физические шкалы выводятся без копирования снимка основного окна. #include // Перетаскивание задаёт собственное положение временного окна. #include // Колесо изменяет временной масштаб относительно точки под курсором. #include // Панель команд сохраняет компактную горизонтальную компоновку. #include // Растягиваемая область под панелью принадлежит только выбранному графику. #include // Команды возвращают полную запись и подбирают вертикальный диапазон. #include // Полушкала задаётся в физических единицах без повторного декодирования. #include // Отдельные фазы можно временно скрывать в самостоятельном окне. #include // Подпись объясняет назначение вертикального масштаба. #include // Ограниченная частота обновления сохраняет отзывчивость при поступлении результатов. #include // Бесконечности используются только как исходные границы поиска экстремумов. #include // Преобразования шкал и увеличение выполняются в координатах отсчётов. #include // Ограничение области не допускает выхода за границы записи. #include "view.h" // Начальная область времени наследуется от текущего представления DSView. #include "../sigsession.h" // Цвет каждой фазы определяется её назначенным цифровым входом. #include "../data/decoderstack.h" // Существующий индекс выдаёт ограниченное числом пикселей количество отрезков. #include "../data/decode/decoder.h" // Параметры и назначения входов берутся из действующего экземпляра декодера. #include "../data/decode/row.h" // Искомые строки заново разрешаются после изменения настроек декодирования. #include "pwm_waveform.h" // Отдельное окно использует тот же контракт нормированных сегментов PWM1. namespace pv { namespace view { // Виджет относится к представлению DSView, не к приложению SETGUI. class DecoderScopeWindow : public QWidget { // Один экземпляр показывает только выбранную графическую область декодера. QPointer _stack; // Результаты принадлежат исходному декодеру и никогда не копируются целиком. QPointer _source; // Основное представление используется для начального диапазона и актуальных цветов входов. QString _decoder_id; // Имя декодера отделяет его строки от остальных уровней стека. QStringList _rows; // Выбранные идентификаторы строк сохраняются безопасно, без указателей на удаляемые RowData. QList _checks; // Каждый переключатель относится к одной выбранной кривой, например Ia, Ib или Ic. QDoubleSpinBox *_range; // Полушкала определяет только видимую амплитуду в этом окне. double _start = 0, _span = 2, _center = 0, _half = 1; // Время выражено в отсчётах; амплитуда — в физических единицах графика. double _drag_start = 0; // Начало временного окна запоминается один раз при захвате мыши. QPoint _drag_point; // Смещение курсора переводится в отсчёты относительно исходной позиции перетаскивания. bool _dragging = false; // Масштаб и положение меняются только при нажатой левой кнопке внутри графика. QRectF plot() const { return QRectF(86, 82, std::max(1, width() - 112), std::max(1, height() - 136)); } // Поля оставляют место для панели, значений вертикальной шкалы и времени в миллисекундах. double count() const { return _stack ? std::max(2.0, double(_stack->sample_count())) : 2.0; } // Минимальные два отсчёта исключают деление на ноль даже при очистке источника. void bound() { // Каждое изменение времени проверяется независимо от масштаба основного окна. _span = std::max(2.0, std::min(_span, count())); // Увеличение ограничено двумя отсчётами, уменьшение — полной длиной записи. _start = std::max(0.0, std::min(_start, count() - _span)); // Прокрутка не выходит за начало или конец цифрового захвата. setProperty("scope_start", _start); setProperty("scope_span", _span); update(); // Нативная проверка читает фактическую область; перерисовка не затрагивает исходное представление. } // Преобразования не вызывают begin_decode_work и не меняют опции анализатора. pv::data::decode::Decoder *decoder() const { // После изменения стека нельзя использовать старый указатель на конфигурацию декодера. if (_stack) for (auto *item : _stack->stack()) if (QString::fromUtf8(item->decoder()->id) == _decoder_id) return item; // Поиск ограничен исходным экземпляром стека, поэтому соседние анализаторы не попадают в окно. return nullptr; // Удалённая конфигурация отображается как отсутствие данных. } // Указатель используется только в текущем проходе GUI-потока. double physical(double value) const { // Нормированная геометрия преобразуется в единицы конкретной выбранной области. if (_rows.contains("wave_theta")) return (value + 1) * 180; // θ кодируется диапазоном −1…+1, соответствующим 0…360 электрическим градусам. auto *item = decoder(); // Параметр шкалы относится к тому же декодеру, который нормировал сегменты. if (item && _decoder_id == "pwm_motor") { // Только токовая модель имеет амперы вместо относительных уровней. const auto option = item->options().find("plot_range_a"); // Пользователь мог изменить исходную полушкалу после открытия окна. if (option != item->options().end() && option->second && g_variant_is_of_type(option->second, G_VARIANT_TYPE_DOUBLE)) return value * g_variant_get_double(option->second); // Умножение восстанавливает сохранённые физические амперы. return value * 100; // Значение 100 А совпадает со штатным значением адаптера при отсутствии сохранённой настройки. } // Напряжения и расчётная несущая остаются в относительных единицах. return value; // Внешнее окно не создаёт дополнительную фильтрацию или интерполяцию данных. } // На графике видны исходные пульсации и разрывы выбранного анализатора. QColor colour(const QString &row) const { // Цвет фазного результата следует за назначенным цифровым входом. auto *item = decoder(); // Текущие назначения берутся заново, чтобы не хранить удалённые указатели на входы. if (_source && item && (row == "wave_a" || row == "wave_b" || row == "wave_c")) { // Общие θ и несущая используют нейтральный цвет темы. const QString input = row.right(1) + (_decoder_id == "pwm_motor" ? "_p" : ""); // Верхняя команда фазы называется a/b/c либо a_p/b_p/c_p. for (auto *probe : item->binded_probe_list()) if (QString::fromUtf8(probe->id) == input) { // Остальные затворы не определяют цвет фазной кривой. const int index = item->binded_probe_index(probe); // Пользователь может назначить фазы в произвольном порядке физических каналов. for (auto *signal : _source->session().get_signals()) if (signal->signal_type() == SR_CHANNEL_LOGIC && signal->get_index() == index && signal->get_colour().isValid()) return signal->get_colour(); // Пользовательские цвета сохраняются при открытии и последующих изменениях. } // При отсутствии подходящего входа используется контрастный нейтральный цвет. } // Прямой доступ выполняется только пока исходное представление и стек существуют. return QColor(210, 225, 240); // Светлый цвет обеспечивает видимость общих графиков на тёмном фоне SETGUI. } // Цвет не зависит от порядка физического хранения входных каналов. std::vector segments(const QString &id) const { // Видимый диапазон запрашивается из существующего индекса минимумов и максимумов. std::vector result; // Размер ограничен шириной окна, а не длиной захвата. if (_stack && _source) for (const auto &entry : _stack->get_rows_gshow()) { // Наличие данных проверяется в действующем стеке после возможного повторного анализа. const auto &row = entry.first; // Копия карты действует только в текущем вызове и не переживает смену конфигурации. if (row.row() && QString::fromUtf8(row.decoder()->id) == _decoder_id && QString::fromUtf8(row.row()->id) == id) { // Выбирается ровно строка запрошенного графика, без цифровых дорожек и предупреждений. _stack->get_pwm_waveform(result, row, uint64_t(_start), uint64_t(std::ceil(_start + _span)), size_t(plot().width())); // Индекс сохраняет короткие выбросы и возвращает исходные сегменты при увеличении. break; // У конкретного экземпляра анализатора идентификатор строки уникален. } // Скрытие строки в основном окне не скрывает её в самостоятельном осциллографе. } // Пустой результат корректен во время очистки, загрузки или отсутствия данных в выбранном интервале. return result; // Владение исходными аннотациями остаётся у DecoderStack. } // Защиту данных от потока декодирования обеспечивает существующий get_pwm_waveform. public: // Конструктор и команды используются одинаково пользователем и нативной интеграционной проверкой. DecoderScopeWindow(pv::data::DecoderStack *stack, View *source, const QString &decoder_id, const QStringList &rows, const QString &title) : QWidget(source->window(), Qt::Window), _stack(stack), _source(source), _decoder_id(decoder_id), _rows(rows) { // Окно имеет собственную рамку ОС, но остаётся дочерним объектом текущего DSView. setObjectName("decoder_scope_window"); setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(title + QString::fromUtf8(" — отдельный осциллограф")); // Закрытие удаляет только дополнительное окно и освобождает его обработчики. setProperty("scope_decoder", decoder_id); setProperty("scope_rows", rows); resize(1100, 640); setMinimumSize(640, 360); // Идентификаторы позволяют проверить выбор конкретного нижнего графика; размеры заданы в пикселях интерфейса. auto *layout = new QVBoxLayout(this); auto *bar = new QHBoxLayout; layout->addLayout(bar); layout->addStretch(); // Панель остаётся сверху при любом размере отдельного окна. auto *all = new QPushButton(QString::fromUtf8("Вся запись"), this); all->setObjectName("scope_fit_time"); bar->addWidget(all); // Команда восстанавливает полный временной диапазон только этого осциллографа. auto *amplitude = new QPushButton(QString::fromUtf8("По амплитуде"), this); amplitude->setObjectName("scope_fit_amplitude"); bar->addWidget(amplitude); // Автомасштаб учитывает экстремумы всех включённых фаз в видимом интервале. bar->addWidget(new QLabel(QString::fromUtf8("Полушкала Y:"), this)); _range = new QDoubleSpinBox(this); _range->setObjectName("scope_y_range"); _range->setDecimals(6); _range->setRange(0.000001, 1e12); // Диапазон от микродоли единицы до 10¹² допускает малые пульсации и произвольные физические шкалы. _range->setSuffix(_decoder_id == "pwm_motor" ? " A" : rows.contains("wave_theta") ? QString::fromUtf8(" °") : QString::fromUtf8(" о.е.")); bar->addWidget(_range); // Единицы явно различают амперы, электрические градусы и нормированные уровни. _half = rows.contains("wave_theta") ? 180 : std::max(0.000001, std::abs(physical(1))); _center = rows.contains("wave_theta") ? 180 : 0; _range->setValue(_half); // Начальная вертикальная область совпадает с исходной шкалой токов, напряжения либо угла. for (const auto &row : rows) { // Для совмещённых токов или напряжений создаётся по независимому переключателю на фазу. auto *check = new QCheckBox(row.size() == 6 ? row.right(1).toUpper() : row == "wave_theta" ? QString::fromUtf8("θ") : QString::fromUtf8("Несущая"), this); check->setChecked(true); check->setObjectName("scope_" + row); bar->addWidget(check); _checks.append(check); // Имена A/B/C сохраняют смысл фаз при любом назначении физических каналов. connect(check, &QCheckBox::toggled, this, [this]() { update(); }); // Выбор фазы меняет только видимость текущего окна. } // Список цифровых каналов и остальные анализаторы сюда не добавляются. bar->addStretch(); setToolTip(QString::fromUtf8("Колесо — масштаб времени; левая кнопка и перемещение — прокрутка")); // Подсказка описывает независимое управление осциллографом. connect(all, &QPushButton::clicked, this, [this]() { _start = 0; _span = count(); bound(); }); // Полный захват доступен без изменения масштаба основной записи. connect(amplitude, &QPushButton::clicked, this, [this]() { fit_amplitude(); }); // Вертикальная подгонка использует текущие видимые данные без нового расчёта токов. connect(_range, static_cast(&QDoubleSpinBox::valueChanged), this, [this](double value) { _half = value; update(); }); // Ручная полушкала не меняет поток, токи или параметры двигателя. connect(stack, &QObject::destroyed, this, &QWidget::close); connect(source, &QObject::destroyed, this, &QWidget::close); // Удаление декодера или исходного представления безопасно закрывает связанное окно. auto *timer = new QTimer(this); timer->setInterval(150); connect(timer, &QTimer::timeout, this, [this]() { if (isVisible()) update(); }); timer->start(); // Обновление не чаще примерно семи раз в секунду ограничивает нагрузку и отражает новые данные и цвета. const double rate = std::max(1.0, stack->samplerate()); _start = std::max(0.0, double(source->offset()) * source->scale() * rate); _span = source->get_view_width() * source->scale() * rate; bound(); // Начальное окно времени совпадает с видимым участком исходного графика. } // QWidget владеет кнопками и таймером, отдельного процесса или копии захвата не создаётся. void fit_amplitude() { // Автомасштаб подходит и для пускового тока, и для малых установившихся пульсаций. double low = std::numeric_limits::infinity(), high = -low; // Начальные границы не влияют на экстремумы действительных сегментов. for (int i = 0; i < _rows.size(); ++i) if (_checks[i]->isChecked()) for (const auto &line : segments(_rows[i])) { // Скрытые в отдельном окне фазы не расширяют выбранную вертикальную шкалу. low = std::min(low, std::min(physical(line.a), physical(line.b))); high = std::max(high, std::max(physical(line.a), physical(line.b))); // Линейный сегмент достигает экстремумов на концах, включая пики индекса. } // Индекс обеспечивает ограниченное время работы даже для длинной записи. if (std::isfinite(low) && std::isfinite(high)) { _center = (low + high) / 2; _range->setValue(std::max(0.000001, (high - low) * 0.55)); update(); } // Десять процентов свободного места предотвращают касание кривой рамки; постоянный уровень остаётся видимым. } // При пустом интервале предыдущая ручная шкала сохраняется. protected: // Взаимодействия используют собственные координаты и не отправляют команды основному графику. void paintEvent(QPaintEvent *) override { // Вся доступная область отводится выбранным кривым и их измерительным шкалам. QPainter p(this); p.fillRect(rect(), QColor(16, 23, 32)); const QRectF box = plot(); const QColor text(210, 225, 240); p.setPen(text); // Цвета согласованы с тёмной темой SETGUI и сохраняют контраст подписей. const double rate = _stack ? std::max(1.0, _stack->samplerate()) : 1; // Координаты времени переводятся из исходных отсчётов в миллисекунды. for (int i = 0; i <= 10; ++i) { // Десять равных интервалов формируют привычную осциллографическую временную сетку. const double x = box.left() + box.width() * i / 10; p.setPen(QPen(QColor(53, 68, 82), 1, Qt::DotLine)); p.drawLine(QPointF(x, box.top()), QPointF(x, box.bottom())); p.setPen(text); // Вертикальные линии разделяют одинаковые интервалы времени. p.drawText(QRectF(x - 38, box.bottom() + 8, 76, 20), Qt::AlignCenter, QString::number((_start + _span * i / 10) * 1000 / rate, 'g', 6)); // Подпись показывает абсолютное время записи в миллисекундах. } // Масштаб и начало могут меняться независимо от основного DSView. for (int i = 0; i <= 8; ++i) { // Восемь делений обеспечивают читаемую вертикальную шкалу выбранной физической величины. const double y = box.top() + box.height() * i / 8; p.setPen(QPen(QColor(53, 68, 82), 1, Qt::DotLine)); p.drawLine(QPointF(box.left(), y), QPointF(box.right(), y)); p.setPen(text); // Горизонтальные уровни сохраняются при изменении размера окна. p.drawText(QRectF(2, y - 10, 76, 20), Qt::AlignRight | Qt::AlignVCenter, QString::number(_center + _half * (1 - i / 4.0), 'g', 6)); // Значения относятся к амперам, градусам либо относительным уровням, указанным на панели. } // Подгонка амплитуды меняет только центр и шаг данной шкалы. p.drawText(QRectF(box.left(), 54, box.width(), 22), Qt::AlignLeft, windowTitle().section(QString::fromUtf8(" — "), 0, 0)); p.drawText(QRectF(box.left(), height() - 24, box.width(), 20), Qt::AlignCenter, QString::fromUtf8("Время, мс · колесо: масштаб · перетаскивание: прокрутка")); // Название и подсказка относятся только к выбранному графику. p.save(); p.setClipRect(box); p.setRenderHint(QPainter::Antialiasing); size_t total = 0; // Отсечение не позволяет сильным токам перекрывать панель и числовые шкалы. for (int i = 0; i < _rows.size(); ++i) if (_checks[i]->isChecked()) { // Каждая видимая фаза выводится своим цветом без копирования остальных дорожек. const auto points = segments(_rows[i]); QVector lines; lines.reserve(int(points.size()) * 2); total += points.size(); // Резерв учитывает возможные две смещённые несущие NPC. bool npc_pair = false; auto *item = decoder(); // Признак дополнительного нижнего треугольника берётся из реальной топологии анализатора. if (item && _rows[i] == "wave_carrier") { const auto mode = item->options().find("input_mode"); npc_pair = mode != item->options().end() && mode->second && g_variant_is_of_type(mode->second, G_VARIANT_TYPE_STRING) && QString::fromUtf8(g_variant_get_string(mode->second, nullptr)) == "npc-three-level"; } // Для обычного ШИМ рисуется один треугольник −1…+1, для NPC — синфазные −1…0 и 0…+1. for (const auto &line : points) { // На обзорном масштабе индекс ограничивает число отрезков числом экранных пикселей. const double x1 = box.left() + (line.start - _start) / _span * box.width(), x2 = box.left() + (line.end - _start) / _span * box.width(); // Время каждого исходного сегмента переводится в локальные координаты отдельного окна. const double a = npc_pair ? (line.a + 1) / 2 : line.a, b = npc_pair ? (line.b + 1) / 2 : line.b; // Верхняя несущая NPC занимает только положительную половину шкалы. const auto y = [this, box](double value) { return box.center().y() - (physical(value) - _center) / _half * box.height() / 2; }; // Физическая полушкала применяется после восстановления ампер либо электрического угла. lines.append(QLineF(x1, y(a), x2, y(b))); if (npc_pair) lines.append(QLineF(x1, y(a - 1), x2, y(b - 1))); // Разрывы и импульсные фронты передаются непосредственно исходными сегментами. } // Дополнительное сглаживание сигнала между точками не выполняется. p.setPen(QPen(colour(_rows[i]), 1.6)); p.drawLines(lines); // Пакетный вывод сохраняет тонкие пульсации каждого фазного тока. } // Другие анализаторы текущего захвата не запрашиваются и не перерисовываются этим окном. p.restore(); setProperty("scope_segments", qulonglong(total)); if (!total) { p.setPen(text); p.drawText(box, Qt::AlignCenter, QString::fromUtf8("Нет данных в выбранном интервале")); } // Пустой участок либо повторный анализ отображается явно и безопасно. } // Исходный график и его временной масштаб остаются неизменными. void wheelEvent(QWheelEvent *event) override { // Увеличение привязано к месту интересующего перехода под курсором. if (!plot().contains(event->position())) { event->ignore(); return; } // Колесо над панелью сохраняет обычное поведение её элементов управления. const double fraction = (event->position().x() - plot().left()) / plot().width(), anchor = _start + fraction * _span; // Опорная временная точка должна остаться на том же месте экрана. _span = std::max(2.0, std::min(count(), _span / std::pow(1.25, event->angleDelta().y() / 120.0))); _start = anchor - fraction * _span; bound(); event->accept(); // Один обычный шаг колеса изменяет масштаб в 1,25 раза; 120 единиц соответствуют одному шагу Qt. } // В основном окне масштабирование не вызывается. void mousePressEvent(QMouseEvent *event) override { // Начало прокрутки фиксируется только для выбранной осциллограммы. if (event->button() == Qt::LeftButton && plot().contains(event->pos())) { _dragging = true; _drag_point = event->pos(); _drag_start = _start; setCursor(Qt::ClosedHandCursor); event->accept(); } // Перетаскивание сохраняет временную точку, захваченную пользователем. } // Кнопки панели продолжают получать свои обычные события. void mouseMoveEvent(QMouseEvent *event) override { // Смещение мыши пересчитывается в отсчёты текущего увеличенного интервала. if (_dragging) { _start = _drag_start - (event->pos().x() - _drag_point.x()) / plot().width() * _span; bound(); event->accept(); } // Перемещение вправо показывает более ранние данные, как в обычном осциллографе. } // Ограничение bound не позволяет уйти за границы исходного захвата. void mouseReleaseEvent(QMouseEvent *event) override { // Отпускание кнопки завершает жест без инерции и без изменения исходной записи. if (event->button() == Qt::LeftButton) { _dragging = false; unsetCursor(); event->accept(); } // Следующее движение без нажатой кнопки не прокручивает график. } // Виджет не захватывает мышь после окончания взаимодействия. }; // Все дочерние элементы и таймер уничтожаются вместе с отдельным осциллографом. } } // Пространства имён совпадают с остальными представлениями DSView. #endif // SET_DECODER_SCOPE_H предотвращает повторное определение виджета при включении в нативные проверки.