Files
templates/tools/dsview/native/decoder_scope.h
T

158 lines
32 KiB
C++
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.
// Самостоятельный осциллограф читает индекс готового декодера и не запускает повторный анализ записи.
#ifndef SET_DECODER_SCOPE_H // Защита позволяет включать виджет из графика и нативной проверки одного процесса.
#define SET_DECODER_SCOPE_H // Каждая единица трансляции получает единственное определение класса.
#include <QWidget> // Обычное окно Qt остаётся частью того же DSView и может разворачиваться независимо.
#include <QPointer> // Слабые ссылки предотвращают обращение к удалённым результатам или основному представлению.
#include <QPainter> // Кривые и физические шкалы выводятся без копирования снимка основного окна.
#include <QMouseEvent> // Перетаскивание задаёт собственное положение временного окна.
#include <QWheelEvent> // Колесо изменяет временной масштаб относительно точки под курсором.
#include <QHBoxLayout> // Панель команд сохраняет компактную горизонтальную компоновку.
#include <QVBoxLayout> // Растягиваемая область под панелью принадлежит только выбранному графику.
#include <QPushButton> // Команды возвращают полную запись и подбирают вертикальный диапазон.
#include <QDoubleSpinBox> // Полушкала задаётся в физических единицах без повторного декодирования.
#include <QCheckBox> // Отдельные фазы можно временно скрывать в самостоятельном окне.
#include <QLabel> // Подпись объясняет назначение вертикального масштаба.
#include <QTimer> // Ограниченная частота обновления сохраняет отзывчивость при поступлении результатов.
#include <limits> // Бесконечности используются только как исходные границы поиска экстремумов.
#include <cmath> // Преобразования шкал и увеличение выполняются в координатах отсчётов.
#include <algorithm> // Ограничение области не допускает выхода за границы записи.
#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<pv::data::DecoderStack> _stack; // Результаты принадлежат исходному декодеру и никогда не копируются целиком.
QPointer<View> _source; // Основное представление используется для начального диапазона и актуальных цветов входов.
QString _decoder_id; // Имя декодера отделяет его строки от остальных уровней стека.
QStringList _rows; // Выбранные идентификаторы строк сохраняются безопасно, без указателей на удаляемые RowData.
QList<QCheckBox*> _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<set_pwm::Line> segments(const QString &id) const { // Видимый диапазон запрашивается из существующего индекса минимумов и максимумов.
std::vector<set_pwm::Line> 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<void(QDoubleSpinBox::*)(double)>(&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<double>::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<QLineF> 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 предотвращает повторное определение виджета при включении в нативные проверки.