Files
templates/tools/dsview/tests/check_pwm_native.py
T

144 lines
16 KiB
Python
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.
"""Manual native smoke test: synthetic 3-phase PWM, embedded DSView screenshot.
Run with a Python containing PySide6. No acquisition hardware is accessed.
"""
import argparse
import ctypes # Win32 WM_NULL проверяет отзывчивость реального потока интерфейса дочернего DSView.
import json
import math
import os
import re # Готовый HWND читается из штатного ответа host protocol 1.
from pathlib import Path
import subprocess
import time
import zipfile
def fixture(path, seconds=0.060032, carrier_hz=2000, layout='split', max_height='1X', show_text=False, text_only=False): # Видимость текстов и графиков воспроизводит пользовательскую компоновку и позволяет сравнить интервалы при разных высотах.
rate = 1000000 # Частота 1 МГц даёт 50 отсчётов на период при нагрузочной несущей 20 кГц.
if seconds <= 0 or not 0 < carrier_hz <= rate / 2: # Каждый период должен занимать хотя бы два отсчёта.
raise ValueError('Positive duration and carrier <= 500000 Hz are required.') # Некорректный сценарий не должен создавать повреждённую запись.
count, period = math.ceil(seconds * rate / 64) * 64, round(rate / carrier_hz) # Формат DSL требует выравнивания числа отсчётов по 64 битам.
decoders = [dict(id='pwm_waveform', version=2, label='PWM sine A / B / C',
view_index=3, channel=[{'a': 0}, {'b': 1}, {'c': 2}],
options=dict(active_a='high', active_b='high', active_c='high',
carrier_hz=0, smoothing_ms=0.0, waveform_layout=layout), # Используем штатный параметр Decoders, который также сохраняется в записи DSL.
**{'stacked decoders': [], 'show': {'pwm_waveform': True,
**{'pwm_waveform: '+ch+' duty / frequency': False for ch in 'ABC'}}})]
session = dict(Device='virtual-session', DeviceMode=0, Version=3, Title='DSView v1.3.2',
decoder=decoders, channel=[dict(index=i, enabled=True, name='D'+str(i),
colour='default', strigger=0, type=10000, view_index=i) for i in range(3)],
**{'Sample count': str(count), 'Sample rate': str(rate), 'Max Height': max_height}) # Большая высота дорожек не должна увеличивать фон числовых аннотаций.
if show_text or text_only: # Текстовые строки нужны для проверки расстояний и переключения видимых полей.
decoders[0]['show'].update({'pwm_waveform: ' + ch + ' duty / frequency': True for ch in 'ABC'}) # Показываем исходные числовые поля всех трёх каналов без изменения расчётных параметров.
if text_only: # Изолированная текстовая компоновка позволяет проверить независимость от масштаба графиков.
decoders[0]['show'].update({'pwm_waveform: ' + ch + ': -1 .. +1': False for ch in 'ABC'}) # Скрываем только графические строки, сохраняя их рассчитанные данные.
header = ('[version]\nversion = 3\n[header]\ndriver = virtual-session\n'
'device mode = 0\ncapturefile = data\n'
f'total samples = {count}\ntotal probes = 3\ntotal blocks = 1\n'
f'samplerate = {rate} Hz\ntrigger time = 0\ntrigger pos = 0\n'
'probe0 = D0\nprobe1 = D1\nprobe2 = D2\n')
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as archive:
archive.writestr('header', header)
archive.writestr('session', json.dumps(session))
archive.writestr('decoders', json.dumps(decoders))
for ch in range(3):
bits = bytearray(count // 8)
for i in range(0, count, period): # Обходим периоды вместо всех отсчётов, чтобы длинная фикстура создавалась быстро.
width = min(count - i, round(period * (0.5 + 0.4 * math.sin(2 * math.pi * i / rate * 50 + ch * 2 * math.pi / 3)))) # Синус 50 Гц задаёт заполнение от 10 до 90 процентов со сдвигом фаз 120 градусов.
if not width: # Очень короткий завершающий импульс может округлиться до нуля.
continue # Нулевая длительность сохраняет исходные нулевые биты.
first, last = i // 8, (i + width - 1) // 8 # Вычисляем крайние байты активного импульса.
if first == last: # Импульс внутри одного байта заполняется одной битовой маской.
bits[first] |= ((1 << width) - 1) << (i % 8) # Смещение сохраняет позицию фронта с точностью одного отсчёта.
else: # Длинный импульс состоит из двух неполных байтов и полного внутреннего диапазона.
bits[first] |= (255 << (i % 8)) & 255 # В первом байте активны биты начиная с фронта до конца байта.
bits[first + 1:last] = b'\xff' * (last - first - 1) # Внутренние полные байты заполняются без цикла по отдельным отсчётам.
bits[last] |= (1 << ((i + width - 1) % 8 + 1)) - 1 # Последний байт обрезается точно по спаду импульса.
archive.writestr(f'L-{ch}/0', bits)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--runtime', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--seconds', type=float, default=0.060032) # Прежний короткий сценарий остаётся значением по умолчанию.
parser.add_argument('--carrier-hz', type=int, default=2000) # Нагрузочная проверка использует 20 кГц и сотни тысяч периодов.
parser.add_argument('--timeout', type=float, default=30) # Ограничиваем время ожидания завершения декодирования.
parser.add_argument('--layout', choices=('split', 'combined'), default='split') # Один нативный сценарий проверяет оба пользовательских вида графиков.
parser.add_argument('--max-height', choices=('1X', '2X', '3X', '4X'), default='1X') # Проверка на увеличенной дорожке воспроизводит прежнее растягивание текстовых полей.
parser.add_argument('--show-text', action='store_true') # Включаем duty/frequency для проверки всей смешанной компоновки.
parser.add_argument('--text-only', action='store_true') # Фиксированные интервалы проверяются также при полностью скрытых графиках.
parser.add_argument('--check-display-cache', action='store_true') # Нативный сценарий принимает реальные диалоги видимости, неизменённых настроек и вида PWM без нового анализа.
args = parser.parse_args()
from PySide6.QtWidgets import QApplication, QWidget
runtime, output = args.runtime.resolve(), args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
capture = output / 'pwm-three-phase.dsl'
screenshot = output / 'pwm-native.png'
fixture(capture, args.seconds, args.carrier_hz, args.layout, args.max_height, args.show_text, args.text_only) # Все параметры восстанавливаются из обычного DSL, включая видимость строк и масштаб дорожек.
app = QApplication([])
window = QWidget()
window.resize(1400, 850)
window.setWindowTitle('PWM native rendering test')
window.show()
env = {k: v for k, v in os.environ.items() if not k.startswith(('PYTHON', 'QT_', 'QML'))}
env['PATH'] = str(runtime) + os.pathsep + str(Path(os.environ['SystemRoot']) / 'System32')
env['DSVIEW_HOST_SCREENSHOT'] = str(screenshot)
cache_report = output / 'display-cache.txt' # Отчёт нативных проверок отделён от журнала единственного декодирования.
if args.check_display_cache: # Обычный smoke-тест не открывает проверочные модальные диалоги.
cache_report.write_text('', encoding='utf-8') # Исключаем ложный успех от артефактов прежнего запуска.
env['DSVIEW_HOST_CHECK'] = str(cache_report) # Используем существующий механизм нативных проверок встроенного DSView.
env['DSVIEW_HOST_CHECK_DISPLAY_CACHE'] = '1' # Проверяем сохранение готовых результатов вместо удаления анализаторов.
began = time.time()
with (output / 'pwm-native.log').open('w', encoding='utf-8') as log:
process = subprocess.Popen([str(runtime / 'DSView.exe'), '--loglevel', '3', '--embed-parent', # Информационный журнал подтверждает фактическое завершение анализа всей записи.
str(int(window.winId())), str(capture)], cwd=runtime, env=env,
stdin=subprocess.PIPE, stdout=log, stderr=log, creationflags=subprocess.CREATE_NO_WINDOW)
try:
deadline = time.monotonic() + args.timeout # Длинная запись должна полностью обработаться до штатного закрытия.
completed, hwnd, response_times = False, None, [] # Фиксируем завершение и задержку обработки сообщений настоящим интерфейсом DSView.
user32 = ctypes.WinDLL('user32', use_last_error=True) # Системный API проверяет только дочерний процесс, запущенный этим тестом.
user32.SendMessageTimeoutW.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_size_t, ctypes.c_ssize_t, ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_size_t)] # Типы указателей учитывают 64-битный HWND.
while time.monotonic() < deadline and process.poll() is None:
app.processEvents()
report = (output / 'pwm-native.log').read_text(encoding='utf-8', errors='replace') # Чтение журнала не изменяет состояние декодера или прибора.
ready = re.search(r'DSVIEW_READY (\d+)', report) # Используем подтверждённый HWND из штатного протокола встраивания.
if ready: # До готовности нативного окна измерять его отзывчивость нельзя.
hwnd = int(ready.group(1)) # Дескриптор относится только к нашему тестовому экземпляру.
result, started = ctypes.c_size_t(), time.monotonic() # Монотонные часы измеряют обработку одного сообщения.
responded = user32.SendMessageTimeoutW(hwnd, 0, 0, 0, 2, 1000, ctypes.byref(result)) # WM_NULL с SMTO_ABORTIFHUNG ожидает не более одной секунды и не меняет приложение.
response_times.append(time.monotonic() - started) # Задержка показывает блокировку цикла событий во время анализа и рисования.
assert responded, 'DSView UI probe failed, Win32 error={}'.format(ctypes.get_last_error()) # Код Win32 отличает реальное зависание от запрета доступа к чужому HWND или смены рабочего стола.
cache = cache_report.read_text(encoding='utf-8') if args.check_display_cache else 'PASS' # Ожидаем завершения всех действий в реальном диалоге настроек.
assert 'FAIL' not in cache, cache # Ошибочное требование повторного анализа немедленно завершает сценарий с диагностикой фазы.
if 'Decoded sample count:' in report and screenshot.exists() and 'PASS' in cache: # Успех требует полного исходного анализа и подтверждённого сохранения кэша.
completed = True # В журнале есть подтверждение обработки всех отсчётов.
window.screen().grabWindow(hwnd).save(str(output / 'pwm-complete.png')) # Сохраняем итоговый экран после завершения длинного анализа.
break # Далее проверяем штатное закрытие завершённого экземпляра.
time.sleep(.02)
assert completed, 'PWM decode did not complete; inspect pwm-native.log' # Тайм-аут не должен маскироваться успешным принудительным закрытием.
print('UI max response ms:', round(max(response_times, default=0) * 1000, 3)) # Отчёт связывает нагрузочный сценарий с реальной задержкой интерфейса.
if process.poll() is None:
process.stdin.write(b'CLOSE\n')
process.stdin.flush()
deadline = time.monotonic() + 5
while time.monotonic() < deadline and process.poll() is None:
app.processEvents()
time.sleep(.02)
assert process.poll() == 0, 'DSView did not close cleanly; inspect pwm-native.log'
assert screenshot.exists() and screenshot.stat().st_mtime >= began, 'No fresh render artifact'
if args.check_display_cache: # Проверка по журналу дополняет результат принятия каждого настоящего диалога.
assert report.count('Decoded sample count:') == 1, 'Display changes triggered another decode' # Все переключения обязаны использовать результаты единственного анализа записи.
print(cache.strip()) # Отчёт показывает отдельный успех скрытия строки, неизменённого OK и изменения вида графиков.
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=5)
window.close()
print('Inspect three phase-shifted sine curves:', screenshot)
if __name__ == '__main__':
main()