Расширить DSView осциллографом и декодерами двигателя и сигма-дельта

This commit is contained in:
2026-11-12 15:00:41 +03:00
parent fa5ea166a9
commit 1fc88684d5
41 changed files with 4147 additions and 151 deletions
+157
View File
@@ -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 предотвращает повторное определение виджета при включении в нативные проверки.
+103
View File
@@ -5,8 +5,111 @@
#include <QPainter>
#include <QStringList>
#include <cmath>
#include <algorithm> // Двоичный поиск ограничивает обработку видимой областью записи.
#include <cstdint> // Координаты отсчётов хранятся без потери точности в 64 битах.
#include <vector> // Пирамида экстремумов строится постепенно в потоке декодирования.
namespace set_pwm {
struct Line { // Отрезок кривой хранит координаты отсчётов и нормированные значения.
double start, end, a, b; // Время переводится в пиксели только при окончательной отрисовке.
}; // Одна колонка обзорного графика требует не более двух таких отрезков.
class Envelope { // Индекс позволяет рисовать длинную запись за время, зависящее от ширины окна.
struct Sample { // Исходные сегменты сохраняются для точного отображения при увеличении.
uint64_t start, end; // Целые номера отсчётов определяют границы сегмента PWM1.
double a, b; // Нормированные значения на концах сегмента лежат в диапазоне −1…+1.
}; // Сегменты добавляются в хронологическом порядке и могут иметь разрывы.
struct Range { // Узел пирамиды содержит оба экстремума своего диапазона.
double low, high; // Минимум и максимум сохраняют короткие выбросы при уменьшении масштаба.
}; // Усреднение здесь не применяется, поскольку оно скрывало бы пики.
std::vector<Sample> samples; // Полные данные нужны только для видимых сегментов крупного масштаба.
std::vector<std::vector<Range>> levels; // Уровень k объединяет блоки из 2^(k+1) сегментов.
static Range bounds(const Sample &s) { // Линейный сегмент достигает экстремумов на своих концах.
return {std::min(s.a, s.b), std::max(s.a, s.b)}; // Порядок значений не зависит от направления наклона.
} // Промежуточные точки линейного сегмента не расширяют этот диапазон.
static Range merge(Range a, Range b) { // Объединение узлов сохраняет экстремумы обеих частей.
return {std::min(a.low, b.low), std::max(a.high, b.high)}; // Операция применяется при добавлении и запросах.
} // Пирамида не изменяет исходные значения PWM.
Range range(size_t first, size_t last) const { // Полуоткрытый диапазон разбивается на готовые двоичные блоки.
Range result = {1.0, -1.0}; // Обратные границы нейтральны для допустимых значений −1…+1.
size_t level = 0; // Нулевой шаг работает с исходными сегментами.
while (first < last) { // Каждый проход поднимается на один уровень вместо обхода всех периодов.
if (first & 1) { // Левый непарный узел нельзя объединить с соседом за пределами запроса.
result = merge(result, level ? levels[level - 1][first] : bounds(samples[first])); // Готовый узел заменяет целый блок периодов.
++first; // Обработанный узел исключается из оставшегося диапазона.
} // Левая граница теперь выровнена на пару.
if (last & 1) { // Правый непарный узел также обрабатывается отдельно.
--last; // Правая граница полуоткрытого диапазона перемещается к последнему узлу.
result = merge(result, level ? levels[level - 1][last] : bounds(samples[last])); // Сохраняются оба экстремума правого блока.
} // После обработки края оставшиеся узлы можно объединить попарно.
first /= 2; // Индекс левой границы переводится на уровень родителей.
last /= 2; // Правая граница переводится на тот же уровень.
++level; // Следующий проход использует вдвое более крупные блоки.
} // Запрос требует лишь логарифмического числа объединений.
return result; // Диапазон не зависит от количества скрытых за пикселем периодов.
} // Вызывающая сторона передаёт только непустые диапазоны.
static double value(const Sample &s, double time) { // Значение на границе пикселя получается линейной интерполяцией.
return s.a + (s.b - s.a) * ((time - s.start) / (s.end - s.start)); // Длительность положительна благодаря проверке append.
} // Обрезка не переносит экстремум соседнего пикселя в текущий.
public: // Потокобезопасность обеспечивает существующая блокировка RowData.
void clear() { // Повторное декодирование должно удалить и сегменты, и индекс предыдущей записи.
samples.clear(); // Старые координаты больше не участвуют в поиске.
levels.clear(); // Старые экстремумы не должны попадать в новый график.
} // Память исходных сегментов может повторно использоваться в следующем захвате.
void append(uint64_t start, uint64_t end, double a, double b) { // Один PWM1-сегмент индексируется сразу при поступлении аннотации.
if (end <= start || (!samples.empty() && start < samples.back().end)) return; // Пустые и перекрывающиеся сегменты нарушили бы двоичный поиск.
samples.push_back({start, end, a, b}); // Точные значения остаются доступны при любом увеличении.
size_t index = samples.size() - 1; // Пересчитывается только путь нового сегмента к корню.
Range current = bounds(samples.back()); // Начальный диапазон содержит концы нового сегмента.
for (size_t level = 0; index || level < levels.size(); ++level) { // Рост дерева требует O(log N) действий на сегмент.
if (level == levels.size()) levels.emplace_back(); // Новый уровень появляется только при увеличении высоты дерева.
if (index & 1) current = merge(level ? levels[level - 1][index - 1] : bounds(samples[index - 1]), current); // Правый потомок объединяется с уже готовым левым.
index /= 2; // Переходим к индексу родительского узла.
if (index == levels[level].size()) levels[level].push_back(current); // Новый блок добавляется без перестройки предыдущих блоков.
else levels[level][index] = current; // Незавершённый блок обновляется после появления правого потомка.
} // Остальные узлы пирамиды остаются неизменными.
} // Индексация выполняется в рабочем потоке, а не во время перерисовки интерфейса.
void lines(std::vector<Line> &dest, uint64_t start, uint64_t end, size_t pixels) const { // Выборка возвращает геометрию только для видимой области.
if (samples.empty() || end <= start || !pixels) return; // Пустая область не требует вычислений или рисования.
const auto first = std::upper_bound(samples.begin(), samples.end(), start, [](double t, const Sample &s) { return t < s.end; }); // Исключаем сегменты, завершившиеся слева от окна.
const auto last = std::lower_bound(first, samples.end(), end, [](const Sample &s, double t) { return s.start < t; }); // Исключаем сегменты, начинающиеся справа от окна.
if (size_t(last - first) <= pixels * 2) { // При двух сегментах на пиксель точная геометрия уже достаточно мала.
for (auto i = first; i != last; ++i) { // Крупный масштаб сохраняет исходные отрезки и мгновенные переходы между смежными интервалами.
if (i != samples.begin() && i->start >= start) { // Предыдущий сегмент нужен также при сбросе θ точно на левой границе видимого окна.
const auto previous = i - 1; // Соседний сегмент содержит уровень непосредственно перед текущим интервалом.
if (previous->end == i->start && previous->b != i->a) dest.push_back({double(i->start), double(i->start), previous->b, i->a}); // Совпадающие границы означают вертикальный фронт, включая 360°↔0°; через отсутствие данных соединение не проводится.
} // Реальные промежутки без измерений остаются разрывами графика.
dest.push_back({double(i->start), double(i->end), i->a, i->b}); // Наклон и значения самого измеренного сегмента сохраняются без изменений.
} // Дополнительная вертикаль не расширяет временной диапазон и не создаёт ложного наклонного участка θ.
return; // Пирамида нужна только для плотного обзорного графика.
} // Для плотной записи число отрезков ограничивается шириной окна.
double previous_time = -1, previous_x = 0, previous_value = 0; // Состояние соединяет соседние непустые колонки, сохраняя разрывы.
for (size_t pixel = 0; pixel < pixels; ++pixel) { // Одна итерация соответствует одной экранной колонке.
const double left = start + double(end - start) * pixel / pixels; // Левая граница колонки выражена в отсчётах.
const double right = start + double(end - start) * (pixel + 1) / pixels; // Правая граница не накапливает ошибку округления.
const auto begin = std::upper_bound(first, last, left, [](double t, const Sample &s) { return t < s.end; }); // Двоичный поиск пропускает невидимые периоды.
const auto finish = std::lower_bound(begin, last, right, [](const Sample &s, double t) { return s.start < t; }); // В колонку входят только пересекающие её сегменты.
if (begin == finish) { previous_time = -1; continue; } // Пустая колонка разрывает кривую вместо выдуманного соединения.
const double begin_time = std::max(left, double(begin->start)); // Первый сегмент обрезается по реальному началу данных.
const double finish_time = std::min(right, double((finish - 1)->end)); // Последний сегмент обрезается по реальному концу данных.
const double a = value(*begin, begin_time), b = value(*(finish - 1), finish_time); // Концы нужны для соединения соседних колонок.
Range extent = {std::min(a, b), std::max(a, b)}; // Начальные экстремумы учитывают обрезанные границы колонки.
if (finish - begin > 1) { // Внутренние концы крайних сегментов тоже могут содержать экстремумы.
extent = merge(extent, {std::min(begin->b, (finish - 1)->a), std::max(begin->b, (finish - 1)->a)}); // Значения за внешними границами колонки не включаются.
if (finish - begin > 2) extent = merge(extent, range(size_t(begin - samples.begin() + 1), size_t(finish - samples.begin() - 1))); // Пирамида заменяет обход всех внутренних периодов.
} // Все экстремумы текущей колонки теперь учтены.
const double x = (left + right) / 2; // Обзорная вертикаль располагается в центре соответствующего пикселя.
if (previous_time == begin_time) dest.push_back({previous_x, x, previous_value, a}); // Соединяем только непрерывные данные соседних колонок.
dest.push_back({x, x, extent.low, extent.high}); // Вертикальный отрезок сохраняет минимум и максимум даже одиночного выброса.
previous_time = finish_time; // Следующая колонка проверит непрерывность по границе данных.
previous_x = x; // Координата предыдущей колонки нужна для соединительного отрезка.
previous_value = b; // Сохраняется последнее измеренное значение текущей колонки.
} // На экран выдаётся не более двух отрезков на пиксель независимо от длительности записи.
} // Полные аннотации при этом остаются доступны таблице, экспорту и увеличению масштаба.
}; // Индекс принадлежит строке аннотаций и освобождается вместе с ней.
inline bool segment(const QString &text, double &a, double &b)
{
const QStringList fields = text.split(' ');
+29
View File
@@ -0,0 +1,29 @@
#pragma once // Единственное определение переносимой модели при включении в несколько компонентов DSView.
#include <algorithm> // Перестановка границ и сравнение напряжений нужны для точной огибающей.
#include <cmath> // Синус и операции над фазой повторяют идеальный источник осциллографа Zynq.
namespace set_scope { // Модель не зависит от Qt, аппаратного захвата и состояния цифровых каналов.
static const double hz[] = {1000.0, 500.3, 1999.7, 750.2}; // Частоты CH1…CH4 в Гц перенесены из zynq/demo/hdmi_usb_menu/scope_signal.h.
static const double peak[] = {0.80, 0.60, 0.70, 0.60}; // Амплитуды в вольтах взяты из scope_peak_v того же демонстрационного источника Zynq.
static const unsigned rgb[] = {0xf9ce4b, 0x33d2ed, 0x72e08a, 0xdb86ee}; // Цвета RGB888 сохраняют соответствие каналам осциллографа Zynq.
static const double tau = 6.2831853071795864769; // Полный оборот 2π в радианах переводит фазу периода в аргумент синуса.
inline double value(unsigned channel, double seconds) { // Напряжение рассчитывается по абсолютному времени общей записи, выраженному в секундах.
if (channel >= 4 || !std::isfinite(seconds)) return 0; // Неверный канал либо время не должны приводить к чтению за пределами таблиц.
const double cycles = seconds * hz[channel], phase = cycles - std::floor(cycles); // Фаза остаётся в диапазоне 0…1 также для отрицательного времени.
return peak[channel] * ((channel & 1) ? (phase < 0.5 ? 1.0 : -1.0) : std::sin(tau * phase)); // CH1/CH3 дают синус, CH2/CH4 — меандр с заполнением 50%, как после scope_init Zynq.
} // Расчёт не читает АЦП и не выдаёт демонстрационный сигнал за измеренный.
struct Range { double low, high; }; // Минимум и максимум одного временного интервала задаются в вольтах.
inline Range extrema(unsigned channel, double first, double last) { // Аналитическая огибающая сохраняет быстрые колебания при уменьшении масштаба.
if (channel >= 4 || !std::isfinite(first) || !std::isfinite(last)) return {0, 0}; // Повреждённые параметры не поступают в вычисления фазы.
if (first > last) std::swap(first, last); // Обратное направление просмотра не меняет физический диапазон напряжения.
const double a = value(channel, first), b = value(channel, last); // Значения концов учитываются даже у интервала короче одного периода.
Range result = {std::min(a, b), std::max(a, b)}; // Начальный диапазон расширяется только при наличии внутреннего экстремума.
const double x = first * hz[channel], z = last * hz[channel]; // Время переводится в периоды соответствующего канала.
if (channel & 1) { // Для меандра скачок возможен на каждой границе половины периода.
if (std::floor(2 * x) != std::floor(2 * z)) result = {-peak[channel], peak[channel]}; // Наличие фронта включает оба уровня вместо ложной медленной волны.
} else { // Для синуса внутренние экстремумы приходятся на четверть и три четверти периода.
if (std::ceil(x - 0.25) <= std::floor(z - 0.25)) result.high = peak[channel]; // Фаза 1/4 периода задаёт положительный пик в вольтах.
if (std::ceil(x - 0.75) <= std::floor(z - 0.75)) result.low = -peak[channel]; // Фаза 3/4 периода задаёт отрицательный пик в вольтах.
} // Для длинного интервала расчёт остаётся постоянным по времени и не перебирает все периоды.
return result; // Оба экстремума используются при рисовании одного экранного пикселя.
} // Модель повторяет scope_value_v и scope_interval_extrema исходного проекта Zynq.
} // Пространство имён исключает совпадения с функциями аппаратного осциллографа DSView.
+55
View File
@@ -0,0 +1,55 @@
#pragma once // Заголовочная реализация добавляет дорожку без отдельного списка исходников CMake и генерации moc.
#include "trace.h" // Базовая дорожка обеспечивает выбор, название, цвет и размещение на общей временной шкале.
#include "view.h" // Масштаб и смещение берутся из того же View, который рисует цифровые каналы.
#include "scope_demo.h" // Переносимая модель повторяет демонстрационные сигналы осциллографа Zynq.
#include <QPainterPath> // Непрерывная кривая передаётся Qt одним пакетом вместо вызова для каждого отсчёта.
namespace pv { namespace view { // Дорожка принадлежит тому же пространству имён, что остальные графики DSView.
class ScopeTrace : public Trace { // Виртуальный осциллограф дополняет цифровую сессию без подмены устройства и его каналов.
unsigned _channel; // Индекс 0…3 выбирает частоту, амплитуду, форму и цвет исходного эмулятора Zynq.
bool _active = false; // Канал появляется только после явного выбора в меню осциллографа.
public: // Управление доступно только владельцу View и общему рендереру дорожек.
enum { Type = 100 }; // Локальный тип отделён от SR_CHANNEL_* библиотеки, чтобы аппаратные приведения типов не затрагивали эмулятор.
explicit ScopeTrace(unsigned channel) : Trace(QString::fromUtf8("A%1 · Zynq DEMO").arg(channel + 1), channel, Type), _channel(channel) { // Номер A1…A4 явно отличает эмуляцию от входов D0…D15.
_colour = QColor::fromRgb(set_scope::rgb[channel]); // Цвет трассы совпадает с исходным каналом Zynq.
_v_offset = 0; // До первой компоновки координата определена и не участвует в переполнении сортировки.
} // Родитель QObject назначается владельцем после создания дорожки.
bool enabled() override { return _active; } // Общий рендерер обрабатывает только выбранные каналы.
void set_active(bool active) { _active = active; } // Повторный выбор скрывает ту же дорожку, сохраняя её имя и цвет.
int get_zero_vpos() override { return get_y(); } // Маркер нуля следует за дорожкой при вертикальной прокрутке цифровой записи.
int rows_size() override { return 6; } // Шесть обычных строк дают начальную высоту 120 пикселей при масштабе 1X DSView.
QRect get_view_rect() override { return QRect(0, get_y() - _totalHeight / 2, _view->get_view_width(), _totalHeight); } // Область графика учитывает прокрутку и реальную высоту дорожки.
void paint_back(QPainter &, int, int, QColor, QColor) override {} // Сетка рисуется вместе с кривой и не меняет общий фон цифровых дорожек.
void paint_mid(QPainter &, int, int, QColor, QColor) override {} // Эмулятор рисуется передним слоем также до первого аппаратного захвата.
void paint_fore(QPainter &p, int left, int right, QColor fore, QColor) override { // Общий слой Viewport обеспечивает показ эмуляции при пустой или остановленной записи.
if (!_active || !_view || right <= left || !std::isfinite(_view->scale()) || _view->scale() <= 0) return; // Неполная геометрия при запуске и изменении размера безопасно пропускается.
const QRect area = get_view_rect(); // Вертикальные границы принадлежат исключительно этой дорожке.
const double top = area.top() + 22, bottom = area.bottom() - 12, zero = (top + bottom) / 2, gain = (bottom - top) / 2; // Верхние 22 и нижние 12 пикселей оставляют место подписи; полный диапазон равен −1…+1 В.
if (gain <= 0) return; // Сжатая до нуля дорожка не создаёт отрицательный масштаб напряжения.
p.save(); // Перо, сглаживание и область отсечения других каналов сохраняются.
p.setClipRect(area.intersected(QRect(left, area.top(), right - left, area.height()))); // Кривая не перекрывает соседние сигналы и заголовки.
p.setPen(QPen(fore, 1, Qt::DotLine)); // Пунктирная сетка имеет постоянную толщину одного экранного пикселя.
for (int division = -4; division <= 4; ++division) p.drawLine(QPointF(left, zero - gain * division / 4), QPointF(right, zero - gain * division / 4)); // Восемь делений охватывают 2 В, то есть 0,25 В/дел.
p.drawText(QPointF(left + 4, top - 5), QString::fromUtf8("Zynq DEMO · %1 · %2 Гц · ±%3 В · 0,25 В/дел").arg((_channel & 1) ? QString::fromUtf8("меандр") : QString::fromUtf8("синус")).arg(set_scope::hz[_channel]).arg(set_scope::peak[_channel])); // Единицы и источник остаются видимыми рядом с осциллограммой.
p.drawText(QPointF(left + 4, zero - 3), QString::fromUtf8("0 В")); // Нулевой уровень исключает неоднозначность вертикального положения сигнала.
p.setPen(QPen(_colour, 1.5)); // Толщина в экранных пикселях и цвет сохраняются при масштабировании времени.
p.setBrush(Qt::NoBrush); // Наследованная кисть маркеров не должна заливать область между началом и концом осциллограммы.
p.setRenderHint(QPainter::Antialiasing, true); // Сглаживание применяется к геометрии, ограниченной шириной экрана.
QPainterPath path; // Путь содержит не более нескольких точек на экранный пиксель независимо от длины записи.
const double scale = _view->scale(), offset = _view->offset(); // Секунды на пиксель и смещение в пикселях совпадают с цифровыми сигналами DSView.
const bool envelope = scale * set_scope::hz[_channel] >= 0.25; // При четверти периода на пиксель и плотнее рисуем точную огибающую, предотвращая алиасинг.
for (int x = left; x <= right; ++x) { // Линейная сложность по ширине окна не зависит от частоты эмулятора или длительности захвата.
const double first = (x + offset) * scale, last = first + scale; // Абсолютное время сохраняет фазу при прокрутке и увеличении записи.
if (envelope) { // Каждый пиксель представляет диапазон напряжений за соответствующий интервал времени.
const auto range = set_scope::extrema(_channel, first, last); // Аналитический расчёт сохраняет оба пика даже при множестве периодов внутри пикселя.
path.moveTo(x, zero - range.low * gain); // Нижний экстремум переводится из вольт в вертикальные экранные координаты.
path.lineTo(x, zero - range.high * gain); // Верхний экстремум завершает вертикальный отрезок огибающей.
} else { // При увеличении масштаба показываем форму исходного синуса или меандра.
const double y = zero - set_scope::value(_channel, first) * gain; // Напряжение рассчитывается в тот же момент времени, что и цифровая дорожка.
if (x == left) path.moveTo(x, y); else path.lineTo(x, y); // Последовательные точки образуют непрерывную осциллограмму без соединения разных дорожек.
} // Значение фазы зависит только от времени записи, поэтому перерисовка не запускает генератор заново.
} // Объём временной геометрии остаётся ограниченным даже у часового захвата.
p.drawPath(path); // Одним вызовом выводим готовую кривую или огибающую.
p.restore(); // Последующие каналы получают исходные настройки QPainter.
} // Эмуляция не записывается в аппаратные данные и не меняет результаты декодеров.
}; // Жизненным циклом дорожки управляет родительский View через QObject.
} } // Завершаем пространства имён компонентов отображения DSView.