--- a/DSView/pv/view/decodetrace.cpp +++ b/DSView/pv/view/decodetrace.cpp @@ -33,6 +33,7 @@ #include #include #include "decodetrace.h" +#include "pwm_waveform.h" // Индекс экстремумов сохраняет форму PWM при любом масштабе времени. #include "../sigsession.h" #include "../data/decoderstack.h" #include "../data/decode/decoder.h" @@ -212,6 +213,14 @@ } } +static bool pwm_combined(pv::data::decode::Decoder *decoder) // Режим берётся из штатных настроек конкретного экземпляра декодера. +{ // Старые записи без новой опции должны открываться с прежними отдельными графиками. + if (QString::fromUtf8(decoder->decoder()->id) != "pwm_waveform") return false; // Настройка PWM не меняет компоновку остальных анализаторов. + const auto option = decoder->options().find("waveform_layout"); // Ищем сохранённый параметр без добавления отсутствующего ключа в карту. + return option != decoder->options().end() && option->second && g_variant_is_of_type(option->second, G_VARIANT_TYPE_STRING) // Проверяем тип перед чтением, чтобы повреждённая настройка не завершила приложение. + && QString::fromUtf8(g_variant_get_string(option->second, nullptr)) == "combined"; // Только явно выбранный combined включает наложение цветных кривых. +} // Отсутствующее или неизвестное значение безопасно оставляет режим split. + void DecodeTrace::paint_mid(QPainter &p, int left, int right, QColor fore, QColor back) { using namespace pv::data::decode; @@ -261,6 +270,9 @@ for(auto dec :_decoder_stack->stack()) { if (dec->shown()) { + const bool combined = pwm_combined(dec); // Каждый экземпляр PWM может независимо отображаться совместно или раздельно. + int combined_y = y; // Общая область начинается на месте первой видимой кривой этого экземпляра. + bool wave_shown = false; // Скрытые и ещё не заполненные строки не должны резервировать высоту общего графика. const std::map rows = _decoder_stack->get_rows_gshow(); for (std::map::const_iterator i = rows.begin(); i != rows.end(); i++) { @@ -268,6 +280,70 @@ _decoder_stack->has_annotations((*i).first)) { if ((*i).second) { const Row &row = (*i).first; + // Для обзорного масштаба берём индексированные экстремумы, сохраняя кривую и короткие выбросы. + if (QString::fromUtf8(dec->decoder()->id) == "pwm_waveform" // Специальная геометрия применяется только к декодеру восстановления ШИМ. + && row.row() && QString::fromUtf8(row.row()->id).startsWith("wave_")) { // Числовые строки duty продолжают использовать штатные аннотации. + std::vector points; // Геометрия ограничивается шириной видимой области вместо числа периодов во всей записи. + _decoder_stack->get_pwm_waveform(points, row, start_sample, end_sample, std::max(1, right - left)); // Индекс экстремумов сохраняет короткие пики при уменьшении масштаба. + const QString row_id = QString::fromUtf8(row.row()->id); // Идентификатор отделяет измеренные фазы от дополнительных расчётных графиков. + const bool phase_wave = row_id == "wave_a" || row_id == "wave_b" || row_id == "wave_c"; // θ и несущая не должны совмещаться с фазами даже при общей компоновке A/B/C. + const bool row_combined = combined && phase_wave; // Наложение применяется только к основным трём осциллограммам. + const bool allocate = !row_combined || !wave_shown; // Каждому дополнительному расчётному графику всегда выделяется собственная область. + if (allocate) combined_y = y; // Раздельный режим начинает новую область для каждого канала. + const int wave_top = combined_y; // Следующие совмещённые фазы используют ту же вертикальную систему координат. + p.save(); // Локальные настройки пера и отсечения не затрагивают остальные строки анализатора. + const int wave_y = wave_top + annotation_height; // Нулевая линия располагается в середине собственной либо общей трёхстрочной области PWM. + p.setClipRect(QRectF(left, wave_top - annotation_height * 0.5, right - left, annotation_height * 3)); // Совмещённые фазы получают одинаковую область отсечения. + p.setPen(QPen(fore, 1, Qt::DotLine)); // Пунктирная линия задаёт уровень заполнения 50 процентов. + if (allocate) p.drawLine(QPointF(left, wave_y), QPointF(right, wave_y)); // Общая нулевая линия рисуется один раз и не перекрывает уже выведенные фазы. + p.setRenderHint(QPainter::Antialiasing, true); // Сглаживание применяется уже к ограниченному числу отрезков. + QColor curve_colour = fore; // Общие θ и несущая сохраняют нейтральное оформление, поскольку не относятся к отдельной фазе. + if (phase_wave) { // Цвет фазной кривой определяется назначенным входом A, B или C, а не порядком физических каналов. + const QString input_id = row_id.right(1) + (QString::fromUtf8(dec->decoder()->id) == "pwm_motor" ? QStringLiteral("_p") : QString()); // PWM waveform использует входы a/b/c, а модель токов двигателя — a_p/b_p/c_p; оба варианта связывают фазу с её основной верхней командой. + for (const auto *probe : dec->binded_probe_list()) { // Рассматриваем реальные назначения входов из сохранённых настроек анализатора. + if (QString::fromUtf8(probe->id) != input_id) continue; // Дополнительные затворы NPC и входы других фаз не задают цвет текущей кривой. + const int physical_index = dec->binded_probe_index(probe); // Индекс физического канала учитывает произвольное назначение A/B/C на входы прибора. + for (auto *signal : _session->get_signals()) { // Читаем актуальные цвета цифровых дорожек при каждой перерисовке, без повторного декодирования. + if (signal->signal_type() == SR_CHANNEL_LOGIC && signal->get_index() == physical_index) { // Аналоговые сигналы с тем же номером не являются исходным цифровым входом фазы. + curve_colour = signal->get_colour().isValid() ? signal->get_colour() : fore; // Повторяем правило цифровой дорожки: явный цвет входа либо цвет текста темы, если пользовательский цвет ещё не задан. + break; // Назначению соответствует только одна цифровая дорожка. + } // При отсутствии входной дорожки остаётся безопасный нейтральный цвет. + } // Изменения цвета входа сразу отражаются на обеих связанных дорожках. + break; // Другие назначенные входы больше не влияют на найденную фазу. + } // Поиск выполняется один раз на строку графика, а не на каждую аннотацию или отсчёт. + } // Режимы огибающей, коммутаций и совмещённых фаз используют одно правило цвета. + p.setPen(QPen(curve_colour, phase_wave ? 1.6 : 1.0)); // Толщина фазных и общих расчётных графиков сохраняется; меняется только согласование цвета с входом. + const auto topology = dec->options().find("input_mode"); // Схема NPC требует двух смещённых треугольников вместо одного двухуровневого. + const bool npc_carrier = row_id == "wave_carrier" && topology != dec->options().end() && topology->second && g_variant_is_of_type(topology->second, G_VARIANT_TYPE_STRING) && QString::fromUtf8(g_variant_get_string(topology->second, nullptr)) == "npc-three-level"; // Старые записи и обычный ШИМ сохраняют один треугольник −1…+1. + QVector lines; // Qt получает готовую геометрию одним пакетным вызовом. + lines.reserve(int(points.size())); // Единственное выделение памяти исключает рост буфера при заполнении. + for (const auto &point : points) { // В плотном масштабе здесь не более двух отрезков на экранный пиксель. + const double x1 = point.start / samples_per_pixel - pixels_offset, x2 = point.end / samples_per_pixel - pixels_offset; // Обе смещённые несущие используют одинаковые времена начала и конца сегмента. + const double a = npc_carrier ? (point.a + 1) / 2 : point.a, b = npc_carrier ? (point.b + 1) / 2 : point.b; // Верхний треугольник NPC занимает диапазон 0…+1, остальные кривые сохраняют исходную нормировку. + lines.append(QLineF(x1, wave_y - a * annotation_height * 1.20, x2, wave_y - b * annotation_height * 1.20)); // Основная геометрия использует прежнюю общую шкалу времени и высоту графика. + if (npc_carrier) lines.append(QLineF(x1, wave_y - (a - 1) * annotation_height * 1.20, x2, wave_y - (b - 1) * annotation_height * 1.20)); // Нижний синфазный треугольник NPC смещён на единицу в диапазон −1…0. + } // Полные исходные сегменты автоматически возвращаются при увеличении масштаба. + p.drawLines(lines); // Пакетная отрисовка не создаёт отдельный вызов QPainter на каждый период длинной записи. + p.restore(); // Цвет, сглаживание и отсечение не переходят на числовые строки. + if (allocate) { // Число заголовков и вертикальный шаг должны совпадать с rows_size(). + y += annotation_height * 3; // Область PWM занимает три высоты строки независимо от количества совмещённых фаз. + QString title = row.title(); // Раздельный график сохраняет прежнее название своего канала. + if (row_combined) { // Общая подпись перечисляет только основные видимые фазы, сохраняя собственные шкалы θ и несущей. + QStringList channels; // Список нужен для записи с одним, двумя или тремя видимыми входами. + for (const auto &entry : rows) { // Порядок совпадает с порядком строк и цветами A, B, C. + if (entry.first.decoder() == dec->decoder() && entry.second && entry.first.row() // Не включаем скрытые строки и строки других экземпляров стека. + && QString::fromUtf8(entry.first.row()->id).size() == 6 && QString::fromUtf8(entry.first.row()->id).startsWith("wave_") && _decoder_stack->has_annotations(entry.first)) // Из общей подписи исключены неподключённые фазы и дополнительные расчётные графики. + channels.append(QString::fromUtf8(entry.first.row()->id).right(1).toUpper()); // Суффикс wave_a…wave_c даёт обозначение фазы без разбора текста аннотаций. + } // Полученный список соответствует кривым, которые действительно будут нарисованы. + title = QString::fromUtf8(dec->decoder()->name) + ": " + channels.join(" / ") + ": -1 .. +1"; // Общая шкала остаётся нормированной для всех видимых каналов. + } // Значения PWM и числовые результаты не изменяются при переключении вида. + _cur_row_headings.push_back(title); // Единственный заголовок относится ко всей выделенной области. + _cur_row_headings.push_back(QString()); // Вторая строка резервирует место для верхней и нижней полуволн. + _cur_row_headings.push_back(QString()); // Третья строка завершает прежнюю высоту области PWM. + } // Повторная совмещённая фаза не сдвигает последующие аннотации. + wave_shown = wave_shown || phase_wave; // Отдельный θ или треугольник не заменяет первую видимую фазу общей области. + continue; // Числовой рендерер не должен рисовать служебный контракт PWM1 поверх кривой. + } // Остальные строки проходят штатную отрисовку текстовых аннотаций. const uint64_t min_annotation = _decoder_stack->get_min_annotation(row); @@ -328,6 +391,7 @@ double samples_per_pixel, double pixels_offset, int y, size_t base_colour, double min_annWidth, QColor fore, QColor back, double &last_x) { + h = std::min(h, p.fontMetrics().height() + 2); // Ограничиваем фон высотой шрифта плюс два экранных пикселя: по одному сверху и снизу, независимо от растягивания дорожки. const double start = max(a.start_sample() / samples_per_pixel - pixels_offset, (double)left); const double end = min(a.end_sample() / samples_per_pixel - @@ -579,13 +643,22 @@ for(auto dec : _decoder_stack->stack()) { if (dec->shown()) { auto rows = _decoder_stack->get_rows_gshow(); + const bool combined = pwm_combined(dec); // Раскладка использует ту же настройку, что и отрисовка кривых. + bool wave_shown = false; // Общая область учитывается один раз среди фактически видимых строк. for (auto i = rows.begin(); i != rows.end(); i++) { pv::data::decode::Row _row = (*i).first; if (_row.decoder() == dec->decoder() && _decoder_stack->has_annotations((*i).first) && (*i).second) - size++; + { // Учитываем только заполненные строки, разрешённые в настройках Decoders. + const bool waveform = QString::fromUtf8(dec->decoder()->id) == "pwm_waveform" // Особая высота относится только к кривым восстановления ШИМ. + && _row.row() && QString::fromUtf8(_row.row()->id).startsWith("wave_"); // Числовые duty/frequency строки сохраняют собственное место. + const bool phase_wave = waveform && QString::fromUtf8(_row.row()->id).size() == 6; // Только wave_a…wave_c относятся к общей области фаз; θ и несущая имеют собственную высоту. + if (phase_wave && combined && wave_shown) continue; // Следующая совмещённая основная фаза не увеличивает полную высоту дорожки. + size += waveform ? 3 : 1; // Кривая занимает три строки, текстовая аннотация — одну. + wave_shown = wave_shown || phase_wave; // Дополнительные кривые не влияют на учёт первой основной фазы. + } // Число строк совпадает с шагом y и количеством заголовков в paint_mid(). } } else { --- a/DSView/pv/dock/protocoldock.cpp +++ b/DSView/pv/dock/protocoldock.cpp @@ -1060,7 +1060,7 @@ { SearchComboBox *panel = new SearchComboBox(this); - const QStringList customIds = {"gate_driver_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart", "tms_uart", "plates_pbus", "plates_sbus"}; // При сборке пользовательский раздел включает анализаторы обеих шин 96-контактного интерфейса. + const QStringList customIds = {"gate_driver_timing", "transistor_pair", "pwm_waveform", "set_uart", "set_can", "pm35_uart", "tms_uart", "plates_pbus", "plates_sbus"}; // При сборке пользовательский раздел включает анализаторы обеих шин 96-контактного интерфейса. for (bool custom : {true, false}) { for (auto info : _decoderInfoList) { srd_decoder *dec = (srd_decoder *)(info->_data_handle); --- a/DSView/pv/data/decode/rowdata.h +++ b/DSView/pv/data/decode/rowdata.h @@ -27,6 +27,7 @@ #include #include "annotation.h" +#include "../../view/pwm_waveform.h" // Индекс PWM хранится рядом с исходными аннотациями и использует ту же блокировку. namespace pv { namespace data { @@ -35,7 +36,7 @@ class RowData { public: - RowData(); + RowData(bool pwm_waveform = false); // Только строки wave_a…wave_c создают индекс; остальные декодеры сохраняют прежнее поведение. ~RowData(); public: @@ -60,6 +61,8 @@ void get_annotation_subset(std::vector &dest, uint64_t start_sample, uint64_t end_sample); + void get_pwm_waveform(std::vector &dest, uint64_t start, uint64_t end, size_t pixels); // Возвращаем ограниченную шириной окна геометрию с обоими экстремумами. + void clear(); private: @@ -68,6 +71,8 @@ uint64_t _item_count; std::vector _annotations; static std::mutex _global_visitor_mutex; + bool _pwm_waveform; // Признак кривой исключает разбор текстов обычных протокольных аннотаций. + set_pwm::Envelope _pwm_envelope; // Полные сегменты и пирамида экстремумов принадлежат текущей строке. }; } --- a/DSView/pv/data/decode/rowdata.cpp +++ b/DSView/pv/data/decode/rowdata.cpp @@ -35,11 +35,12 @@ std::mutex RowData::_global_visitor_mutex; -RowData::RowData() : +RowData::RowData(bool pwm_waveform) : // Тип строки задаётся один раз при создании таблицы декодера. _max_annotation(0), _min_annotation(0) { _item_count = 0; + _pwm_waveform = pwm_waveform; // Индекс нужен только графическим строкам декодера PWM waveform. } RowData::~RowData() @@ -56,6 +57,7 @@ delete p; } _annotations.clear(); + _pwm_envelope.clear(); // Новое декодирование не должно использовать экстремумы предыдущего захвата. _item_count = 0; _min_annotation = 0; } @@ -96,6 +98,12 @@ } } +void RowData::get_pwm_waveform(std::vector &dest, uint64_t start, uint64_t end, size_t pixels) // Геометрия строится без полного перебора списка аннотаций. +{ // Чтение индекса синхронизируется с поступлением сегментов рабочего потока. + std::lock_guard lock(_global_visitor_mutex); // Та же блокировка защищает исходные аннотации и их индекс. + _pwm_envelope.lines(dest, start, end, pixels); // Сложность запроса зависит от экранной ширины, а не от длительности захвата. +} // После возврата интерфейс рисует собственную копию геометрии без блокировки декодирования. + uint64_t RowData::get_annotation_index(uint64_t start_sample) { std::lock_guard lock(_global_visitor_mutex); @@ -118,6 +126,11 @@ try { _annotations.push_back(a); + if (_pwm_waveform && !a->annotations().empty()) { // Только известный контракт PWM1 содержит нормированные концы кривой. + double first, last; // Значения разбираются один раз при поступлении вместо каждого кадра интерфейса. + if (set_pwm::segment(a->annotations().back(), first, last)) // Проверка контракта исключает повреждённые и нечисловые значения. + _pwm_envelope.append(a->start_sample(), a->end_sample(), first, last); // Пирамида экстремумов обновляется постепенно в рабочем потоке. + } // Таблица и экспорт по-прежнему получают каждую исходную аннотацию. _item_count = _annotations.size(); _max_annotation = max(_max_annotation, a->end_sample() - a->start_sample()); --- a/DSView/pv/data/decoderstack.h +++ b/DSView/pv/data/decoderstack.h @@ -30,6 +30,7 @@ #include #include +#include "../view/pwm_waveform.h" // Общий формат ограниченной геометрии связывает строку данных с нативным представлением. #include "decode/row.h" #include "../data/signaldata.h" #include "decode/decoderstatus.h" @@ -113,6 +114,8 @@ const decode::Row &row, uint64_t start_sample, uint64_t end_sample); + void get_pwm_waveform(std::vector &dest, const decode::Row &row, uint64_t start, uint64_t end, size_t pixels); // Запрос видимой кривой не копирует миллионы исходных аннотаций. + uint64_t get_annotation_index( const decode::Row &row, uint64_t start_sample); uint64_t get_max_annotation(const decode::Row &row); --- a/DSView/pv/data/decoderstack.cpp +++ b/DSView/pv/data/decoderstack.cpp @@ -188,7 +188,8 @@ const Row row(decc, ann_row, order); // Add a new empty row data object - _rows[row] = new decode::RowData(); + const bool waveform = QString::fromUtf8(decc->id) == "pwm_waveform" && QString::fromUtf8(ann_row->id).startsWith("wave_"); // Только графические строки PWM требуют многоуровневого индекса. + _rows[row] = new decode::RowData(waveform); // Индекс создаётся вместе со строкой и очищается при повторном анализе. std::map::const_iterator iter = _rows_gshow.find(row); if (iter == _rows_gshow.end()) { _rows_gshow[row] = true; @@ -227,6 +228,12 @@ start_sample, end_sample); } + +void DecoderStack::get_pwm_waveform(std::vector &dest, const decode::Row &row, uint64_t start, uint64_t end, size_t pixels) // Передаём запрос только строке выбранного канала PWM. +{ // Набор строк стабилен во время декодирования и отрисовки. + auto iter = _rows.find(row); // Находим данные по существующему ключу декодера и строки. + if (iter != _rows.end()) iter->second->get_pwm_waveform(dest, start, end, pixels); // Отсутствующая строка корректно возвращает пустую кривую. +} // Вектор содержит независимую геометрию без указателей на изменяемые аннотации. uint64_t DecoderStack::get_annotation_index( const Row &row, uint64_t start_sample)