120 lines
4.9 KiB
Python
120 lines
4.9 KiB
Python
"""@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)
|