"""@file spectrum_demo.py @brief Демонстрационный источник спектра для проверки вкладки без прибора. Повторяет то, что передаёт прошивка: основной тон, несколько гармоник и шумовая подложка. Нужен, чтобы вкладку можно было открыть и убедиться в правильности отрисовки, не имея под рукой ни платы, ни датчика. Источник намеренно живёт рядом с разбором потока, а не внутри вкладки: он выдаёт те же самые строки, что и прибор, и проходит через тот же разбор. Иначе демонстрация проверяла бы отрисовку, но не формат. """ from __future__ import annotations import math import random from set_devices.spectrum_stream import ( Harmonic, HarmonicsFrame, build_harmonics_line, build_spectrum_line, ) #: Число элементов спектра в демонстрационном кадре. DEMO_BINS = 128 #: Частота выборки демонстрационного источника, Гц. DEMO_SAMPLE_RATE = 20000 #: Размер преобразования, которому соответствует спектр. DEMO_FFT_SIZE = 512 #: Верхняя граница шкалы уровней, дБ относительно милливольта. DEMO_TOP_DBMV = 60.0 #: Размах шкалы уровней, дБ. DEMO_RANGE_DB = 60.0 class SpectrumDemoSource: """@brief Выдаёт строки спектра и гармоник, как это делал бы прибор.""" def __init__(self, seed: int = 1) -> None: self._seq = 0 self._random = random.Random(seed) self._phase = 0.0 def next_lines(self) -> str: """@brief Формирует очередную пару строк. @return Строка спектра и строка гармоник, склеенные подряд. Основной тон медленно плывёт по частоте, чтобы на экране было видно движение и стало заметно, если отрисовка где-то подвисает. """ self._seq += 1 self._phase += 0.05 fundamental_hz = 1000.0 + 200.0 * math.sin(self._phase) fundamental_mv = 400.0 bin_width = (DEMO_SAMPLE_RATE / 2.0) / DEMO_BINS # Уровни: шумовая подложка около 3 мВ плюс узкие пики на основном # тоне и его гармониках. amplitudes = [self._random.uniform(1.5, 4.0) for _ in range(DEMO_BINS)] harmonics: list[Harmonic] = [] for order in range(1, 5): frequency = fundamental_hz * order if frequency >= DEMO_SAMPLE_RATE / 2.0: break amplitude = fundamental_mv / (order * order) index = int(round(frequency / bin_width)) if 0 <= index < DEMO_BINS: amplitudes[index] = amplitude # Соседние элементы приподняты: у реального окна энергия # тона всегда занимает несколько элементов, и совсем узкий # пик выглядел бы неправдоподобно. for neighbour in (index - 1, index + 1): if 0 <= neighbour < DEMO_BINS: amplitudes[neighbour] = max( amplitudes[neighbour], amplitude * 0.45 ) if order >= 2: harmonics.append( Harmonic( index=order, frequency_hz=frequency, amplitude_mv=amplitude, relative_db=20.0 * math.log10(amplitude / fundamental_mv), ) ) levels = [20.0 * math.log10(max(value, 1e-3)) for value in amplitudes] spectrum = build_spectrum_line( seq=self._seq, sample_rate_hz=DEMO_SAMPLE_RATE, fft_size=DEMO_FFT_SIZE, levels_db=levels, top_dbmv=DEMO_TOP_DBMV, range_db=DEMO_RANGE_DB, ) total = math.sqrt(sum(h.amplitude_mv**2 for h in harmonics)) frame = HarmonicsFrame( seq=self._seq, fundamental_hz=fundamental_hz, fundamental_mv=fundamental_mv, thd_percent=100.0 * total / fundamental_mv, snr_db=42.0, rms_mv=fundamental_mv / math.sqrt(2.0), harmonics=tuple(harmonics), ) return spectrum + build_harmonics_line(frame)