Расширить DSView осциллографом и декодерами двигателя и сигма-дельта
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// Самостоятельный осциллограф читает индекс готового декодера и не запускает повторный анализ записи.
|
||||
#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 предотвращает повторное определение виджета при включении в нативные проверки.
|
||||
Reference in New Issue
Block a user