Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4938b80af6 | |||
| c7216e4e8e | |||
| 4ecb3f5ea5 | |||
| 9b5e39f3df | |||
| 99d4eb976f | |||
| d925a4ffaa | |||
| 07ffb31651 |
@ -1,145 +0,0 @@
|
|||||||
# Рефлектометр
|
|
||||||
|
|
||||||
Модуль представляет собой законченную встраиваемую систему рефлектометра, объединяющую:
|
|
||||||
|
|
||||||
- контроллер управления
|
|
||||||
- генератор импульсов (DAC path)
|
|
||||||
- сэмплер данных (ADC path)
|
|
||||||
- аккумулятор и обработчик данных
|
|
||||||
|
|
||||||
Система предназначена для формирования импульсов, синхронного сбора отраженного сигнала, накопления результатов и передачи обработанных данных во внешнюю систему.
|
|
||||||
|
|
||||||
Данный модуль является полноценным интегрируемым блоком, который может использоваться как самостоятельная аппаратная подсистема внутри более крупного проекта.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Назначение системы
|
|
||||||
|
|
||||||
Основная задача системы:
|
|
||||||
|
|
||||||
1. Получить параметры измерения через AXI Stream
|
|
||||||
2. Сформировать последовательность импульсов на DAC
|
|
||||||
3. Выполнить синходную выборку данных с ADC
|
|
||||||
4. Накопить и обработать результаты
|
|
||||||
5. Передать итоговые данные обратно через AXI Stream
|
|
||||||
|
|
||||||
Таким образом реализуется полный цикл измерения без необходимости внешнего управления отдельными блоками.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Состав системы
|
|
||||||
|
|
||||||
### Controller
|
|
||||||
|
|
||||||
Принимает входные команды по AXI Stream (Ethernet RX), декодирует параметры измерения и управляет всеми внутренними модулями системы.
|
|
||||||
|
|
||||||
Формирует:
|
|
||||||
|
|
||||||
- запуск генератора (`dac_start`)
|
|
||||||
- запуск аккумулятора (`adc_start`)
|
|
||||||
- параметры импульсов DAC
|
|
||||||
- параметры выборки ADC
|
|
||||||
- локальные reset-сигналы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Generator
|
|
||||||
|
|
||||||
Формирует последовательность импульсов на DAC с заданными:
|
|
||||||
|
|
||||||
- амплитудой
|
|
||||||
- длительностью
|
|
||||||
- периодом
|
|
||||||
- количеством повторений
|
|
||||||
|
|
||||||
Для каждого импульса инициирует запуск выборки в сэмплере.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Sampler
|
|
||||||
|
|
||||||
Выполняет синхронный сбор данных с ADC по запросу генератора.
|
|
||||||
|
|
||||||
Поддерживает:
|
|
||||||
|
|
||||||
- фильтрацию `out_of_range`
|
|
||||||
- упаковку данных
|
|
||||||
- преобразование типа кода ( прямой или дополнительный)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Accumulator
|
|
||||||
|
|
||||||
Получает поток данных от сэмплера, выполняет накопление, усреднение и оконную обработку, после чего формирует пакеты для передачи результата.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Управление системой
|
|
||||||
|
|
||||||
Пользователь взаимодействует только с контроллером через AXI Stream-интерфейс.
|
|
||||||
|
|
||||||
Прямое управление генератором, сэмплером и аккумулятором не требуется.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Clock Domain Crossing (CDC)
|
|
||||||
|
|
||||||
Система работает в нескольких тактовых доменах:
|
|
||||||
|
|
||||||
- Ethernet RX (`gmii_rx_clk`)
|
|
||||||
- Ethernet TX (`gmii_tx_clk`)
|
|
||||||
- DAC (`dac_clk`)
|
|
||||||
- ADC (`adc_clk`)
|
|
||||||
|
|
||||||
Для корректной синхронизации между DAC и ADC используются специальные CDC-регистры для сигналов:
|
|
||||||
|
|
||||||
- `sample_req`
|
|
||||||
- `sample_done`
|
|
||||||
|
|
||||||
Это обеспечивает безопасную передачу handshake-сигналов между тактовыми доменами.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Список параметров
|
|
||||||
|
|
||||||
### DAC_DATA_WIDTH
|
|
||||||
Ширина выходных данных отправляемых на ЦАП.
|
|
||||||
|
|
||||||
### ZERO_LEVEL
|
|
||||||
Уровень сигнала в состоянии отсутствия импульса (базовый уровень сигнала).
|
|
||||||
|
|
||||||
Типовые значения:
|
|
||||||
|
|
||||||
- `8192` — середина диапазона ЦАП
|
|
||||||
- `0` — нулевой уровень
|
|
||||||
|
|
||||||
### ADC_DATA_WIDTH
|
|
||||||
Ширина входных данных, получаемых с АЦП.
|
|
||||||
|
|
||||||
### PACK_FACTOR
|
|
||||||
Количество отсчетов, собираемых в один выходной пакет.
|
|
||||||
|
|
||||||
### PROCESS_MODE
|
|
||||||
Режим интерпретации входного кода:
|
|
||||||
|
|
||||||
- `0` — прямой код
|
|
||||||
- `1` — дополнительный код
|
|
||||||
|
|
||||||
### ACCUM_WIDTH
|
|
||||||
Размер данных для аккумуляции, должен быть степенью числа 2. По умолчанию - 32
|
|
||||||
|
|
||||||
### N_MAX
|
|
||||||
Максимальное число окон в последовательности. Должно быть степенью числа 2. Влияет на размер используемой памяти.
|
|
||||||
|
|
||||||
### WINDOW_SIZE
|
|
||||||
Размер окна усреднения
|
|
||||||
|
|
||||||
### PACKET_SIZE
|
|
||||||
Размер выходного пакета
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Сборка
|
|
||||||
```make all``` - собрать все до битстрима
|
|
||||||
|
|
||||||
```make vivado``` - открыть проект в Vivado
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
# Генератор
|
|
||||||
|
|
||||||
Модуль выполняет задачу формирования последовательности импульсов заданной амплитуды, длительности и периода.
|
|
||||||
Дополнительно реализован механизм синхронизации с модулем сэмплера через сигналы `sample_req` и `sample_done`, позволяющий запускать сбор данных для каждого импульса и ожидать подтверждения завершения выборки перед переходом к следующему импульсу.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Список параметров
|
|
||||||
|
|
||||||
### DATA_WIDTH
|
|
||||||
Ширина выходных данных генератора.
|
|
||||||
|
|
||||||
### ZERO_LEVEL
|
|
||||||
Уровень сигнала в состоянии отсутствия импульса (базовый уровень сигнала).
|
|
||||||
|
|
||||||
Типовые значения:
|
|
||||||
|
|
||||||
- `8192` — середина диапазона ЦАП
|
|
||||||
- `0` — нулевой уровень
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Список входных портов
|
|
||||||
|
|
||||||
### clk_in
|
|
||||||
Сигнал тактирования модуля.
|
|
||||||
|
|
||||||
### rst
|
|
||||||
Сброс модуля и остановка генерации.
|
|
||||||
|
|
||||||
### start
|
|
||||||
Сигнал запуска последовательности импульсов.
|
|
||||||
|
|
||||||
При его активации модуль фиксирует все входные параметры и начинает генерацию.
|
|
||||||
|
|
||||||
Повторный запуск во время активной генерации блокируется с помощью внутреннего сигнала `enable`.
|
|
||||||
|
|
||||||
### [31:0] pulse_width
|
|
||||||
Длительность активной части импульса (в тактах).
|
|
||||||
|
|
||||||
### [31:0] pulse_period
|
|
||||||
Полный период импульса (в тактах).
|
|
||||||
|
|
||||||
### [DATA_WIDTH-1:0] pulse_height
|
|
||||||
Амплитуда импульса.
|
|
||||||
|
|
||||||
### [15:0] pulse_num
|
|
||||||
Количество импульсов, которое необходимо сгенерировать.
|
|
||||||
|
|
||||||
### sample_done
|
|
||||||
Сигнал подтверждения от сэмплера о завершении выборки данных для текущего импульса.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Список выходных портов
|
|
||||||
|
|
||||||
pulse
|
|
||||||
Выходной сигнал разрешения записи сигнала
|
|
||||||
|
|
||||||
[DATA_WIDTH-1:0] pulse_height_out
|
|
||||||
Выходное значение амплитуды сигнала.
|
|
||||||
|
|
||||||
Во время активной части импульса равно `pulse_height`, вне импульса — `ZERO_LEVEL`.
|
|
||||||
|
|
||||||
sample_req
|
|
||||||
Сигнал запроса на запуск выборки в модуле сэмплера.
|
|
||||||
|
|
||||||
Поднимается в начале каждого нового импульса и снимается после получения `sample_done`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Логика работы
|
|
||||||
|
|
||||||
После прихода сигнала `start` модуль:
|
|
||||||
|
|
||||||
- фиксирует входные параметры генерации
|
|
||||||
- сбрасывает внутренние счетчики
|
|
||||||
- поднимает `enable = 1`
|
|
||||||
- формирует первый `sample_req`
|
|
||||||
|
|
||||||
После этого начинается последовательная генерация импульсов.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Симуляция
|
|
||||||
Тесты запускаются автоматически через make.
|
|
||||||
```
|
|
||||||
cd tests
|
|
||||||
make sim
|
|
||||||
```
|
|
||||||
При успешном завершении теста высвечивается "ALL PASSED".
|
|
||||||
@ -82,25 +82,17 @@ module generator_tb;
|
|||||||
|
|
||||||
repeat(40) @(posedge clk);
|
repeat(40) @(posedge clk);
|
||||||
|
|
||||||
pulse_width = 3;
|
pulse_width = 3;
|
||||||
pulse_period = 8;
|
pulse_period = 8;
|
||||||
pulse_num = 4;
|
pulse_num = 4;
|
||||||
pulse_height = 14'h3FF;
|
pulse_height = 14'h3FF;
|
||||||
start = 1;
|
start = 1;
|
||||||
|
|
||||||
repeat(1) @(posedge clk);
|
|
||||||
start = 0;
|
|
||||||
|
|
||||||
repeat(5) @(posedge clk);
|
|
||||||
start = 1;
|
|
||||||
pulse_height = 14'h155;
|
|
||||||
|
|
||||||
repeat(1) @(posedge clk);
|
repeat(1) @(posedge clk);
|
||||||
start = 0;
|
start = 0;
|
||||||
|
|
||||||
repeat(50) @(posedge clk);
|
repeat(50) @(posedge clk);
|
||||||
|
|
||||||
|
|
||||||
$display("\n=== TEST FINISHED ===");
|
$display("\n=== TEST FINISHED ===");
|
||||||
$finish;
|
$finish;
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,139 +1,23 @@
|
|||||||
# Сэмплер
|
# Сэмплер
|
||||||
|
Модуль выполняет задачу сбора данных с выхода АЦП, их обработку, упаковку, и передачу дальше с помощью AXI Stream интерфейса.
|
||||||
|
|
||||||
Модуль выполняет задачу сбора данных с выхода АЦП, их обработки, упаковки и передачи дальше с помощью AXI Stream интерфейса.
|
## Cписок параметров
|
||||||
Дополнительно реализован механизм синхронизации с внешним генератором через сигналы `sample_req` и `sample_done`, позволяющий запускать сбор строго по запросу и подтверждать завершение выборки.
|
DATA_WIDTH - ширина входных данных, получаемых с АЦП.
|
||||||
|
PACK_FACTOR - количество отсчетов, собираемых в один выходной пакет.
|
||||||
---
|
PROCESS_MODE - режим интерпретации входного кода. 0 - прямой код, 1 - дополнительный код.
|
||||||
|
|
||||||
## Список параметров
|
|
||||||
|
|
||||||
DATA_WIDTH
|
|
||||||
Ширина входных данных, получаемых с АЦП.
|
|
||||||
|
|
||||||
PACK_FACTOR
|
|
||||||
Количество отсчетов, собираемых в один выходной пакет.
|
|
||||||
|
|
||||||
PROCESS_MODE
|
|
||||||
Режим интерпретации входного кода:
|
|
||||||
|
|
||||||
- `0` — прямой код
|
|
||||||
- `1` — дополнительный код
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Список входных портов
|
## Список входных портов
|
||||||
|
clk_in - сигнал тактирования выходного интерфейса.
|
||||||
clk_in
|
rst - сброс модуля и остановка подачи импульсов.
|
||||||
Сигнал тактирования выходного интерфейса.
|
[DATA_WIDTH-1:0] data_in - входной сигнал с АЦП.
|
||||||
|
out_of_range - флаг выхода значений данных за допустимый диапазон. 0 - валидны, 1 - не валидны.
|
||||||
rst
|
|
||||||
Сброс модуля и остановка работы.
|
|
||||||
|
|
||||||
[DATA_WIDTH-1:0] data_in
|
|
||||||
Входной сигнал с АЦП.
|
|
||||||
|
|
||||||
out_of_range
|
|
||||||
Флаг выхода значений данных за допустимый диапазон:
|
|
||||||
|
|
||||||
- `0` — данные валидны
|
|
||||||
- `1` — данные невалидны и игнорируются
|
|
||||||
|
|
||||||
[31:0] smp_num
|
|
||||||
Количество валидных отсчетов, которое необходимо собрать после получения запроса на выборку.
|
|
||||||
|
|
||||||
sample_req
|
|
||||||
Сигнал запроса на запуск выборки.
|
|
||||||
При его активации модуль начинает сбор данных и переходит в активное состояние (`enable = 1`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Список выходных портов
|
## Список выходных портов
|
||||||
|
[DATA_WIDTH*PACK_FACTOR-1:0] m_axis_tdata - урезанный axis формат, выходные данные. Ширина шины считается исходя из битности данных и фактора упаковки.
|
||||||
[DATA_WIDTH*PACK_FACTOR-1:0] m_axis_tdata
|
m_axis_tvalid - урезанный axis формат, валидность выходных данных.
|
||||||
Урезанный AXI Stream формат, выходные данные.
|
|
||||||
Ширина шины определяется как произведение битности данных и фактора упаковки.
|
|
||||||
|
|
||||||
m_axis_tvalid
|
|
||||||
Урезанный AXI Stream формат, сигнал валидности выходных данных.
|
|
||||||
Формируется при готовности очередного пакета.
|
|
||||||
|
|
||||||
sample_done
|
|
||||||
Сигнал завершения выборки.
|
|
||||||
Поднимается после того, как модуль собрал количество валидных отсчетов, равное `smp_num`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Логика работы
|
## Логика работы
|
||||||
|
На каждом такте принимаются data_in (значение АЦП) и out_of_range (флаг выхода значений данных за допустимый диапазон). Если out_of_range = 1, то данные игнорируются и не попадают во внутренний буффер. В противном случае, модуль накапливает данные во внутреннем буффере, идет его заполнение до количества данных, равное PACK_FACTOR. Когда буффер оказывается заполненным, он выдает пакет упакованных данных, сопровождая их импульсом m_axis_tvalid (готовность пакета). Если PROCESS_MODE = 1, данные выдаются в дополнительном коде, если PROCESS_MODE = 0 - в прямом.
|
||||||
На каждом такте принимаются:
|
|
||||||
|
|
||||||
- `data_in` — значение АЦП
|
|
||||||
- `out_of_range` — флаг допустимости значения
|
|
||||||
|
|
||||||
Если `out_of_range = 1`, данные считаются невалидными, игнорируются и не попадают во внутренний буфер.
|
|
||||||
|
|
||||||
Если `out_of_range = 0`, данные считаются корректными и используются для дальнейшей обработки.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Преобразование данных
|
|
||||||
|
|
||||||
Если `PROCESS_MODE = 1`, входные данные интерпретируются как дополнительный код и преобразуются перед упаковкой.
|
|
||||||
|
|
||||||
Если `PROCESS_MODE = 0`, данные передаются без преобразования (прямой код).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Запуск выборки
|
|
||||||
|
|
||||||
Сбор данных начинается только после прихода сигнала `sample_req`.
|
|
||||||
|
|
||||||
При этом:
|
|
||||||
|
|
||||||
- фиксируется значение `smp_num`
|
|
||||||
- внутренний счетчик собранных отсчетов обнуляется
|
|
||||||
- модуль переходит в активное состояние (`enable = 1`)
|
|
||||||
|
|
||||||
Пока `enable = 1`, модуль принимает только валидные отсчеты и считает их.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Упаковка данных
|
|
||||||
|
|
||||||
Внутренний буфер заполняется до количества данных, равного `PACK_FACTOR`.
|
|
||||||
|
|
||||||
#### Если `PACK_FACTOR = 1`
|
|
||||||
|
|
||||||
Каждый валидный отсчет сразу формирует выходной пакет:
|
|
||||||
|
|
||||||
- данные передаются в `m_axis_tdata`
|
|
||||||
- формируется импульс `m_axis_tvalid`
|
|
||||||
|
|
||||||
#### Если `PACK_FACTOR > 1`
|
|
||||||
|
|
||||||
Данные последовательно накапливаются во внутреннем сдвиговом буфере.
|
|
||||||
|
|
||||||
Когда буфер полностью заполнен:
|
|
||||||
|
|
||||||
- формируется пакет упакованных данных
|
|
||||||
- поднимается `m_axis_tvalid`
|
|
||||||
|
|
||||||
После этого начинается сбор следующего пакета.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Завершение выборки
|
|
||||||
|
|
||||||
Когда количество собранных валидных отсчетов достигает значения `smp_num`:
|
|
||||||
|
|
||||||
- поднимается сигнал `sample_done`
|
|
||||||
- внутренние счетчики сбрасываются
|
|
||||||
- буфер очищается
|
|
||||||
- `enable` сбрасывается в `0`
|
|
||||||
|
|
||||||
Это означает полное завершение текущего цикла выборки.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Симуляция
|
## Симуляция
|
||||||
Тесты запускаются автоматически через make.
|
Тесты запускаются автоматически через make.
|
||||||
|
|||||||
736
software/gui.py
Normal file
736
software/gui.py
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
# shitpost
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from PyQt6 import uic
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from PyQt6.QtCore import QProcess, QTimer
|
||||||
|
from PyQt6.QtCore import QObject, QThread, pyqtSignal
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReflectometerConfig:
|
||||||
|
ip: str
|
||||||
|
send_port: int
|
||||||
|
recv_port: int
|
||||||
|
|
||||||
|
dac_bits: int
|
||||||
|
data_width: int
|
||||||
|
window_size: int
|
||||||
|
packet_size: int
|
||||||
|
|
||||||
|
pulse_width: int
|
||||||
|
pulse_period: int
|
||||||
|
pulse_height: int
|
||||||
|
pulse_num: int
|
||||||
|
|
||||||
|
adc_dac_ratio: float = 0.52
|
||||||
|
socket_timeout_sec: float = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
class ReflectometerWorker(QObject):
|
||||||
|
data_ready = pyqtSignal(list)
|
||||||
|
status = pyqtSignal(str)
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
finished = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, config: ReflectometerConfig):
|
||||||
|
super().__init__()
|
||||||
|
self.config = config
|
||||||
|
self._stop_requested = False
|
||||||
|
self._sock = None
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_requested = True
|
||||||
|
|
||||||
|
if self._sock is not None:
|
||||||
|
try:
|
||||||
|
self._sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
self._validate_config()
|
||||||
|
|
||||||
|
self.status.emit("Открытие UDP-сокета...")
|
||||||
|
|
||||||
|
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self._sock.settimeout(self.config.socket_timeout_sec)
|
||||||
|
self._sock.bind(("0.0.0.0", self.config.recv_port))
|
||||||
|
|
||||||
|
dest = (self.config.ip, self.config.send_port)
|
||||||
|
|
||||||
|
self.status.emit("Отправка soft reset...")
|
||||||
|
self._sock.sendto((0x0F00).to_bytes(2, "big"), dest)
|
||||||
|
|
||||||
|
self.status.emit("Отправка параметров...")
|
||||||
|
ctrl_data = self._format_ctrl_data()
|
||||||
|
self._sock.sendto(ctrl_data, dest)
|
||||||
|
|
||||||
|
self.status.emit("Отправка start...")
|
||||||
|
self._sock.sendto((0xF000).to_bytes(2, "big"), dest)
|
||||||
|
|
||||||
|
self.status.emit("Приём данных...")
|
||||||
|
data = self._recv_data()
|
||||||
|
|
||||||
|
if self._stop_requested:
|
||||||
|
self.status.emit("Операция остановлена")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.data_ready.emit(data)
|
||||||
|
self.status.emit(f"Получено samples: {len(data)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not self._stop_requested:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if self._sock is not None:
|
||||||
|
try:
|
||||||
|
self._sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.finished.emit()
|
||||||
|
|
||||||
|
def _format_ctrl_data(self) -> bytes:
|
||||||
|
output = bytearray()
|
||||||
|
|
||||||
|
output += 0b10001000.to_bytes(1, "little")
|
||||||
|
|
||||||
|
pulse_period_adc = (
|
||||||
|
int(self.config.pulse_period * self.config.adc_dac_ratio)
|
||||||
|
// self.config.window_size
|
||||||
|
) * self.config.window_size
|
||||||
|
|
||||||
|
output += self.config.pulse_width.to_bytes(4, "little")
|
||||||
|
output += self.config.pulse_period.to_bytes(4, "little")
|
||||||
|
output += self.config.pulse_num.to_bytes(2, "little")
|
||||||
|
output += self.config.pulse_height.to_bytes(2, "little")
|
||||||
|
output += pulse_period_adc.to_bytes(4, "little")
|
||||||
|
|
||||||
|
if len(output) != 17:
|
||||||
|
raise ValueError("Config data should be 128 bits + 8 bit header")
|
||||||
|
|
||||||
|
return bytes(output)
|
||||||
|
|
||||||
|
def _recv_data(self) -> list[int]:
|
||||||
|
packet_count = math.ceil(
|
||||||
|
(
|
||||||
|
self.config.adc_dac_ratio
|
||||||
|
* self.config.pulse_period
|
||||||
|
/ self.config.window_size
|
||||||
|
* self.config.data_width
|
||||||
|
)
|
||||||
|
/ self.config.packet_size
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_length = math.ceil(
|
||||||
|
self.config.adc_dac_ratio
|
||||||
|
* self.config.pulse_period
|
||||||
|
/ self.config.window_size
|
||||||
|
)
|
||||||
|
|
||||||
|
recv_buf = []
|
||||||
|
|
||||||
|
for pkt_cnt in range(packet_count):
|
||||||
|
if self._stop_requested:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
packet, _ = self._sock.recvfrom(65536)
|
||||||
|
except socket.timeout:
|
||||||
|
raise TimeoutError(f"Таймаут приёма UDP-пакета #{pkt_cnt + 1}")
|
||||||
|
|
||||||
|
if len(packet) % self.config.data_width != 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Некорректный размер UDP-пакета: {len(packet)} байт"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(0, len(packet), self.config.data_width):
|
||||||
|
sample = int.from_bytes(
|
||||||
|
packet[i:i + self.config.data_width],
|
||||||
|
"little",
|
||||||
|
)
|
||||||
|
recv_buf.append(sample)
|
||||||
|
|
||||||
|
if len(recv_buf) < expected_length:
|
||||||
|
raise ValueError(
|
||||||
|
f"Data underflow: получено {len(recv_buf)}, ожидалось {expected_length}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recv_buf[:expected_length - 1]
|
||||||
|
|
||||||
|
def _validate_config(self):
|
||||||
|
if self.config.pulse_period <= 0:
|
||||||
|
raise ValueError("pulse_period должен быть больше 0")
|
||||||
|
|
||||||
|
if self.config.pulse_num <= 0:
|
||||||
|
raise ValueError("pulse_num должен быть больше 0")
|
||||||
|
|
||||||
|
if self.config.window_size <= 0:
|
||||||
|
raise ValueError("window_size должен быть больше 0")
|
||||||
|
|
||||||
|
if self.config.packet_size <= 0:
|
||||||
|
raise ValueError("packet_size должен быть больше 0")
|
||||||
|
|
||||||
|
if self.config.data_width <= 0:
|
||||||
|
raise ValueError("data_width должен быть больше 0")
|
||||||
|
|
||||||
|
if self.config.pulse_period % self.config.window_size != 0:
|
||||||
|
raise ValueError("pulse_period должен быть кратен window_size")
|
||||||
|
|
||||||
|
if self.config.pulse_width >= 2**32 - 1:
|
||||||
|
raise ValueError("pulse_width слишком большой")
|
||||||
|
|
||||||
|
if self.config.pulse_period >= 2**32 - 1:
|
||||||
|
raise ValueError("pulse_period слишком большой")
|
||||||
|
|
||||||
|
if self.config.pulse_num >= 2**16 - 1:
|
||||||
|
raise ValueError("pulse_num слишком большой")
|
||||||
|
|
||||||
|
if self.config.pulse_height > 2**self.config.dac_bits - 1:
|
||||||
|
raise ValueError("pulse_height слишком большой")
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
uic.loadUi("reflectometer.ui", self)
|
||||||
|
|
||||||
|
self.ping_process = None
|
||||||
|
|
||||||
|
self.ping_timeout_timer = QTimer(self)
|
||||||
|
self.ping_timeout_timer.setSingleShot(True)
|
||||||
|
self.ping_timeout_timer.timeout.connect(self.on_ping_timeout)
|
||||||
|
|
||||||
|
self.button_ping.clicked.connect(self.check_ping)
|
||||||
|
|
||||||
|
# settings
|
||||||
|
self.pulse_period = 0
|
||||||
|
self.pulse_height = 0
|
||||||
|
self.pulse_width = 0
|
||||||
|
self.pulse_num = 0
|
||||||
|
|
||||||
|
self.dac_dw = 14
|
||||||
|
self.adc_dw = 12
|
||||||
|
self.nmax = 4096
|
||||||
|
self.packet_size = 1024
|
||||||
|
self.window_size = 65
|
||||||
|
self.adc_dac_ration = 0.52
|
||||||
|
self.accum_width = 32
|
||||||
|
|
||||||
|
# setup
|
||||||
|
|
||||||
|
self.setup_pulse_controls()
|
||||||
|
self.setup_global_settings()
|
||||||
|
|
||||||
|
self.update_pulse_limits()
|
||||||
|
|
||||||
|
self.data = []
|
||||||
|
|
||||||
|
self.adc_dac_ratio = 0.52
|
||||||
|
|
||||||
|
self.measurement_thread = None
|
||||||
|
self.measurement_worker = None
|
||||||
|
|
||||||
|
self.setup_graph()
|
||||||
|
self.setup_network_settings()
|
||||||
|
|
||||||
|
self.button_start.clicked.connect(self.run_measurement)
|
||||||
|
self.button_graph_autoscale.clicked.connect(self.reset_graph_autoscale)
|
||||||
|
|
||||||
|
# ping utils
|
||||||
|
|
||||||
|
def check_ping(self):
|
||||||
|
ip = self.line_ip.text().strip()
|
||||||
|
|
||||||
|
if not ip:
|
||||||
|
self.label_ping_status.setText("set ip!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "_" in self.line_ip.displayText():
|
||||||
|
self.label_ping_status.setText("IP invalid")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.ping_process is not None:
|
||||||
|
if self.ping_process.state() != QProcess.ProcessState.NotRunning:
|
||||||
|
self.label_ping_status.setText("Ping inflight")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.label_ping_status.setText("ping...")
|
||||||
|
self.button_ping.setEnabled(False)
|
||||||
|
|
||||||
|
self.ping_process = QProcess(self)
|
||||||
|
|
||||||
|
self.ping_process.finished.connect(self.on_ping_finished)
|
||||||
|
self.ping_process.errorOccurred.connect(self.on_ping_error)
|
||||||
|
|
||||||
|
system_name = platform.system().lower()
|
||||||
|
|
||||||
|
if system_name == "windows":
|
||||||
|
program = "ping"
|
||||||
|
arguments = ["-n", "1", "-w", "2000", ip]
|
||||||
|
else:
|
||||||
|
program = "ping"
|
||||||
|
arguments = ["-c", "1", "-W", "2", ip]
|
||||||
|
|
||||||
|
self.ping_process.start(program, arguments)
|
||||||
|
# fallback
|
||||||
|
self.ping_timeout_timer.start(2000)
|
||||||
|
|
||||||
|
def on_ping_finished(self, exit_code, exit_status):
|
||||||
|
self.ping_timeout_timer.stop()
|
||||||
|
self.button_ping.setEnabled(True)
|
||||||
|
|
||||||
|
if exit_code == 0:
|
||||||
|
self.label_ping_status.setText("алё✅")
|
||||||
|
else:
|
||||||
|
self.label_ping_status.setText("не алё❌")
|
||||||
|
|
||||||
|
def on_ping_error(self):
|
||||||
|
self.ping_timeout_timer.stop()
|
||||||
|
self.button_ping.setEnabled(True)
|
||||||
|
self.label_ping_status.setText("ping unavail")
|
||||||
|
|
||||||
|
def on_ping_timeout(self):
|
||||||
|
if self.ping_process is not None:
|
||||||
|
if self.ping_process.state() != QProcess.ProcessState.NotRunning:
|
||||||
|
self.ping_process.kill()
|
||||||
|
|
||||||
|
self.button_ping.setEnabled(True)
|
||||||
|
|
||||||
|
# pulse controls
|
||||||
|
def setup_pulse_controls(self):
|
||||||
|
self._bind_slider_and_spinbox(
|
||||||
|
name="pulse_period",
|
||||||
|
slider=self.slider_pulse_period,
|
||||||
|
box=self.box_pulse_period,
|
||||||
|
normalize_value=self.normalize_pulse_period,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_slider_and_spinbox(
|
||||||
|
name="pulse_height",
|
||||||
|
slider=self.slider_pulse_height,
|
||||||
|
box=self.box_pulse_height,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_slider_and_spinbox(
|
||||||
|
name="pulse_width",
|
||||||
|
slider=self.slider_pulse_width,
|
||||||
|
box=self.box_pulse_width,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_slider_and_spinbox(
|
||||||
|
name="pulse_num",
|
||||||
|
slider=self.slider_pulse_num,
|
||||||
|
box=self.box_pulse_num,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _bind_slider_and_spinbox(self, name, slider, box, normalize_value=None):
|
||||||
|
"""
|
||||||
|
Связывает QSlider и QSpinBox по значению.
|
||||||
|
Значение автоматически записывается в self.<name>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
minimum = min(slider.minimum(), box.minimum())
|
||||||
|
maximum = max(slider.maximum(), box.maximum())
|
||||||
|
|
||||||
|
slider.setRange(minimum, maximum)
|
||||||
|
box.setRange(minimum, maximum)
|
||||||
|
|
||||||
|
def normalize(value):
|
||||||
|
if normalize_value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return normalize_value(value)
|
||||||
|
|
||||||
|
value = normalize(box.value())
|
||||||
|
|
||||||
|
slider.setValue(value)
|
||||||
|
box.setValue(value)
|
||||||
|
setattr(self, name, value)
|
||||||
|
|
||||||
|
def update_value(new_value):
|
||||||
|
new_value = normalize(new_value)
|
||||||
|
|
||||||
|
if slider.value() != new_value:
|
||||||
|
slider.setValue(new_value)
|
||||||
|
|
||||||
|
if box.value() != new_value:
|
||||||
|
box.setValue(new_value)
|
||||||
|
|
||||||
|
setattr(self, name, new_value)
|
||||||
|
|
||||||
|
slider.valueChanged.connect(update_value)
|
||||||
|
box.valueChanged.connect(update_value)
|
||||||
|
|
||||||
|
def normalize_pulse_period(self, value):
|
||||||
|
step = max(1, getattr(self, "window_size",
|
||||||
|
self.box_window_size.value()))
|
||||||
|
|
||||||
|
snapped_value = round(value / step) * step
|
||||||
|
|
||||||
|
minimum = self.box_pulse_period.minimum()
|
||||||
|
maximum = self.box_pulse_period.maximum()
|
||||||
|
|
||||||
|
return max(minimum, min(snapped_value, maximum))
|
||||||
|
|
||||||
|
def _set_max_for_pair(self, slider, box, maximum):
|
||||||
|
slider.setMaximum(maximum)
|
||||||
|
box.setMaximum(maximum)
|
||||||
|
|
||||||
|
value = min(box.value(), maximum)
|
||||||
|
box.setValue(value)
|
||||||
|
slider.setValue(value)
|
||||||
|
|
||||||
|
def set_max_pulse_period(self, maximum):
|
||||||
|
self._set_max_for_pair(
|
||||||
|
slider=self.slider_pulse_period,
|
||||||
|
box=self.box_pulse_period,
|
||||||
|
maximum=maximum,
|
||||||
|
)
|
||||||
|
self.pulse_period = self.box_pulse_period.value()
|
||||||
|
|
||||||
|
def set_max_pulse_height(self, maximum):
|
||||||
|
self._set_max_for_pair(
|
||||||
|
slider=self.slider_pulse_height,
|
||||||
|
box=self.box_pulse_height,
|
||||||
|
maximum=maximum,
|
||||||
|
)
|
||||||
|
self.pulse_height = self.box_pulse_height.value()
|
||||||
|
|
||||||
|
def set_max_pulse_width(self, maximum):
|
||||||
|
self._set_max_for_pair(
|
||||||
|
slider=self.slider_pulse_width,
|
||||||
|
box=self.box_pulse_width,
|
||||||
|
maximum=maximum,
|
||||||
|
)
|
||||||
|
self.pulse_width = self.box_pulse_width.value()
|
||||||
|
|
||||||
|
def set_max_pulse_num(self, maximum):
|
||||||
|
self._set_max_for_pair(
|
||||||
|
slider=self.slider_pulse_num,
|
||||||
|
box=self.box_pulse_num,
|
||||||
|
maximum=maximum,
|
||||||
|
)
|
||||||
|
self.pulse_num = self.box_pulse_num.value()
|
||||||
|
|
||||||
|
# settings
|
||||||
|
|
||||||
|
def setup_global_settings(self):
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="dac_dw",
|
||||||
|
box=self.box_dac_dw,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="adc_dw",
|
||||||
|
box=self.box_adc_dw,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="nmax",
|
||||||
|
box=self.box_nmax,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="window_size",
|
||||||
|
box=self.box_window_size,
|
||||||
|
after_change=self.on_window_size_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="packet_size",
|
||||||
|
box=self.box_packet_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="adc_dac_ratio",
|
||||||
|
box=self.box_adc_dac_ratio,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="accum_width",
|
||||||
|
box=self.box_accum_width,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="recv_port",
|
||||||
|
box=self.box_recv_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="send_port",
|
||||||
|
box=self.box_send_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
# применяем шаг для pulse_period сразу при старте
|
||||||
|
self.update_pulse_period_step()
|
||||||
|
|
||||||
|
def _bind_spinbox_setting(self, name, box, after_change=None):
|
||||||
|
"""
|
||||||
|
Связывает QSpinBox с полем self.<name>.
|
||||||
|
Например:
|
||||||
|
box_dac_dw -> self.dac_dw
|
||||||
|
box_window_size -> self.window_size
|
||||||
|
"""
|
||||||
|
|
||||||
|
value = box.value()
|
||||||
|
setattr(self, name, value)
|
||||||
|
|
||||||
|
def on_value_changed(new_value):
|
||||||
|
setattr(self, name, new_value)
|
||||||
|
|
||||||
|
self.update_pulse_limits()
|
||||||
|
|
||||||
|
if after_change is not None:
|
||||||
|
after_change(new_value)
|
||||||
|
|
||||||
|
box.valueChanged.connect(on_value_changed)
|
||||||
|
|
||||||
|
def update_pulse_limits(self):
|
||||||
|
# re-calc limits
|
||||||
|
|
||||||
|
# nmax -> pulse_period limit
|
||||||
|
self.set_max_pulse_period(self.nmax * self.window_size)
|
||||||
|
self.set_max_pulse_width(self.nmax * self.window_size)
|
||||||
|
# accum_width + adc_width -> max pulse num
|
||||||
|
|
||||||
|
self.set_max_pulse_num(
|
||||||
|
2 ** (self.accum_width - self.adc_dw - math.ceil(math.log2(self.window_size))) - 1)
|
||||||
|
# dac_width -> max pulse height
|
||||||
|
self.set_max_pulse_height(2 ** self.dac_dw - 1)
|
||||||
|
|
||||||
|
self.slider_pulse_period.setMinimum(self.window_size)
|
||||||
|
self.box_pulse_period.setMinimum(self.window_size)
|
||||||
|
|
||||||
|
def on_window_size_changed(self, new_value):
|
||||||
|
self.update_pulse_period_step()
|
||||||
|
|
||||||
|
def update_pulse_period_step(self):
|
||||||
|
# set window_size step
|
||||||
|
|
||||||
|
step = max(1, self.window_size)
|
||||||
|
|
||||||
|
self.box_pulse_period.setSingleStep(step)
|
||||||
|
self.slider_pulse_period.setSingleStep(step)
|
||||||
|
self.slider_pulse_period.setPageStep(step)
|
||||||
|
|
||||||
|
self.snap_pulse_period_to_step(step)
|
||||||
|
|
||||||
|
def snap_pulse_period_to_step(self, step):
|
||||||
|
"""
|
||||||
|
Подгоняет текущее значение pulse_period к ближайшему кратному window_size.
|
||||||
|
|
||||||
|
Это нужно потому, что QSlider при перетаскивании мышкой
|
||||||
|
всё равно может дать любое промежуточное значение.
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_value = self.box_pulse_period.value()
|
||||||
|
|
||||||
|
snapped_value = round(current_value / step) * step
|
||||||
|
|
||||||
|
minimum = self.box_pulse_period.minimum()
|
||||||
|
maximum = self.box_pulse_period.maximum()
|
||||||
|
|
||||||
|
snapped_value = max(minimum, min(snapped_value, maximum))
|
||||||
|
|
||||||
|
self.box_pulse_period.setValue(snapped_value)
|
||||||
|
self.slider_pulse_period.setValue(snapped_value)
|
||||||
|
self.pulse_period = snapped_value
|
||||||
|
|
||||||
|
# graph
|
||||||
|
def setup_graph(self):
|
||||||
|
self.graph_widget = pg.PlotWidget()
|
||||||
|
self.graph_widget.setLabel("left", "ADC value")
|
||||||
|
self.graph_widget.setLabel("bottom", "Sample")
|
||||||
|
self.graph_widget.showGrid(x=True, y=True)
|
||||||
|
|
||||||
|
self.graph_curve = self.graph_widget.plot(
|
||||||
|
[],
|
||||||
|
name="Data",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.reference_curve = self.graph_widget.plot(
|
||||||
|
[],
|
||||||
|
name="Reference",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.graph_layout.addWidget(self.graph_widget)
|
||||||
|
self.graph_curve = self.graph_widget.plot(
|
||||||
|
[], pen=pg.mkPen(width=2, color="b"))
|
||||||
|
self.reference_curve = self.graph_widget.plot(
|
||||||
|
[], pen=pg.mkPen(style=Qt.PenStyle.DashLine, color="g"))
|
||||||
|
|
||||||
|
self.checkbox_draw_reference.stateChanged.connect(
|
||||||
|
self.update_reference_graph)
|
||||||
|
|
||||||
|
def setup_network_settings(self):
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="recv_port",
|
||||||
|
box=self.box_recv_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bind_spinbox_setting(
|
||||||
|
name="send_port",
|
||||||
|
box=self.box_send_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_measurement(self):
|
||||||
|
if self.measurement_thread is not None:
|
||||||
|
if self.measurement_thread.isRunning():
|
||||||
|
self.set_measurement_status("Измерение выполняется")
|
||||||
|
return
|
||||||
|
|
||||||
|
config = self.build_reflectometer_config()
|
||||||
|
|
||||||
|
self.data = []
|
||||||
|
self.graph_curve.setData([])
|
||||||
|
|
||||||
|
self.measurement_thread = QThread(self)
|
||||||
|
self.measurement_worker = ReflectometerWorker(config)
|
||||||
|
|
||||||
|
self.measurement_worker.moveToThread(self.measurement_thread)
|
||||||
|
|
||||||
|
self.measurement_thread.started.connect(self.measurement_worker.run)
|
||||||
|
|
||||||
|
self.measurement_worker.status.connect(self.set_measurement_status)
|
||||||
|
self.measurement_worker.error.connect(self.on_measurement_error)
|
||||||
|
self.measurement_worker.data_ready.connect(self.on_data_received)
|
||||||
|
|
||||||
|
self.measurement_worker.finished.connect(self.measurement_thread.quit)
|
||||||
|
self.measurement_worker.finished.connect(
|
||||||
|
self.measurement_worker.deleteLater)
|
||||||
|
|
||||||
|
self.measurement_thread.finished.connect(
|
||||||
|
self.measurement_thread.deleteLater)
|
||||||
|
self.measurement_thread.finished.connect(self.on_measurement_finished)
|
||||||
|
|
||||||
|
self.measurement_thread.start()
|
||||||
|
|
||||||
|
def build_reflectometer_config(self) -> ReflectometerConfig:
|
||||||
|
ip = self.line_ip.text().strip()
|
||||||
|
|
||||||
|
if not ip:
|
||||||
|
raise ValueError("IP адрес не задан")
|
||||||
|
|
||||||
|
data_width = self.accum_width // 8
|
||||||
|
|
||||||
|
return ReflectometerConfig(
|
||||||
|
ip=ip,
|
||||||
|
send_port=self.send_port,
|
||||||
|
recv_port=self.recv_port,
|
||||||
|
|
||||||
|
dac_bits=self.dac_dw,
|
||||||
|
data_width=data_width,
|
||||||
|
window_size=self.window_size,
|
||||||
|
packet_size=self.packet_size,
|
||||||
|
|
||||||
|
pulse_width=self.pulse_width,
|
||||||
|
pulse_period=self.pulse_period,
|
||||||
|
pulse_height=self.pulse_height,
|
||||||
|
pulse_num=self.pulse_num,
|
||||||
|
|
||||||
|
adc_dac_ratio=self.adc_dac_ratio,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_data_received(self, data: list[int]):
|
||||||
|
self.data = data
|
||||||
|
# normalize
|
||||||
|
for i in range(len(data)):
|
||||||
|
self.data[i] /= (self.window_size * self.pulse_num)
|
||||||
|
self.data[i] -= 2 ** (self.adc_dw - 1) + 1
|
||||||
|
|
||||||
|
self.draw_main_graph()
|
||||||
|
self.update_reference_graph()
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self.set_measurement_status(
|
||||||
|
f"Готово. smp: {len(data)}, min: {min(data)}, max: {max(data)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.set_measurement_status("Данные пустые")
|
||||||
|
|
||||||
|
def on_measurement_error(self, message: str):
|
||||||
|
self.set_measurement_status(f"Ошибка: {message}")
|
||||||
|
|
||||||
|
def on_measurement_finished(self):
|
||||||
|
self.measurement_worker = None
|
||||||
|
self.measurement_thread = None
|
||||||
|
|
||||||
|
def stop_measurement(self):
|
||||||
|
if self.measurement_worker is not None:
|
||||||
|
self.measurement_worker.stop()
|
||||||
|
|
||||||
|
def set_measurement_status(self, text: str):
|
||||||
|
self.label_status.setText(text)
|
||||||
|
|
||||||
|
def draw_main_graph(self):
|
||||||
|
if not self.data:
|
||||||
|
self.graph_curve.setData([])
|
||||||
|
return
|
||||||
|
|
||||||
|
x = list(range(len(self.data)))
|
||||||
|
self.graph_curve.setData(x, self.data)
|
||||||
|
|
||||||
|
def update_reference_graph(self):
|
||||||
|
"""
|
||||||
|
Рисует или очищает эталонный график.
|
||||||
|
Вызывается после получения данных и при переключении checkbox_draw_reference.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.checkbox_draw_reference.isChecked():
|
||||||
|
self.reference_curve.setData([])
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.data:
|
||||||
|
self.reference_curve.setData([])
|
||||||
|
return
|
||||||
|
|
||||||
|
reference_data = self.build_reference_data(len(self.data))
|
||||||
|
|
||||||
|
if not reference_data:
|
||||||
|
self.reference_curve.setData([])
|
||||||
|
return
|
||||||
|
|
||||||
|
x = list(range(len(reference_data)))
|
||||||
|
self.reference_curve.setData(x, reference_data)
|
||||||
|
|
||||||
|
def build_reference_data(self, length: int) -> list[int]:
|
||||||
|
reference = [0] * length
|
||||||
|
|
||||||
|
actual_pulse_width = round(
|
||||||
|
(self.pulse_width * self.adc_dac_ratio) / self.window_size)
|
||||||
|
|
||||||
|
reference[0:actual_pulse_width] = [
|
||||||
|
(self.pulse_height / 2 ** (self.dac_dw - self.adc_dw)) - 2 ** (self.adc_dw - 1), ] * (actual_pulse_width - 1)
|
||||||
|
|
||||||
|
return reference
|
||||||
|
|
||||||
|
def reset_graph_autoscale(self):
|
||||||
|
self.graph_widget.enableAutoRange(axis="xy", enable=True)
|
||||||
|
self.graph_widget.autoRange()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
505
software/reflectometer.ui
Normal file
505
software/reflectometer.ui
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1023</width>
|
||||||
|
<height>708</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Reflectometer PREMIUM</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout" stretch="4,2">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="graph_layout"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="settings_layout">
|
||||||
|
<item>
|
||||||
|
<widget class="QTabWidget" name="tabWidget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="tab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Настройки</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QScrollArea" name="scrollArea">
|
||||||
|
<property name="widgetResizable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>294</width>
|
||||||
|
<height>621</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>12</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Аппаратные параметры</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_dac_dw">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> bits</string>
|
||||||
|
</property>
|
||||||
|
<property name="prefix">
|
||||||
|
<string>DAC data width: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>8</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>32</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>14</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_adc_dw">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> bits</string>
|
||||||
|
</property>
|
||||||
|
<property name="prefix">
|
||||||
|
<string>ADC data width: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>8</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>32</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>12</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_accum_width">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> bits</string>
|
||||||
|
</property>
|
||||||
|
<property name="prefix">
|
||||||
|
<string>Accum width: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>16</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>64</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>8</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>32</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDoubleSpinBox" name="box_adc_dac_ratio">
|
||||||
|
<property name="prefix">
|
||||||
|
<string>ADC:DAC clk ratio: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>0.200000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>3.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<double>0.010000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<double>0.520000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_nmax">
|
||||||
|
<property name="prefix">
|
||||||
|
<string>N Max: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>512</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>65536</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>4096</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_window_size">
|
||||||
|
<property name="prefix">
|
||||||
|
<string>Window size: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1024</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>65</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_packet_size">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> bytes</string>
|
||||||
|
</property>
|
||||||
|
<property name="prefix">
|
||||||
|
<string>Packet size: </string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1572</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>1024</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="Line" name="line_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>12</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Подключение</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>IP устройства:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="line_ip">
|
||||||
|
<property name="inputMask">
|
||||||
|
<string>999.999.999.999</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>192.168.0.2</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string>Порт отправки:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_send_port">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>80</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>65536</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>8080</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Порт приёма:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_recv_port">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>80</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>65536</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>8080</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_6">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>12</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Тест</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_ping">
|
||||||
|
<property name="text">
|
||||||
|
<string>алё</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_ping_status">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_2">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Управление</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_7">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>12</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Импульс</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,1,2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="text">
|
||||||
|
<string>Период</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_pulse_period">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="slider_pulse_period">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,1,2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_9">
|
||||||
|
<property name="text">
|
||||||
|
<string>Ширина</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_pulse_width"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="slider_pulse_width">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,1,2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_10">
|
||||||
|
<property name="text">
|
||||||
|
<string>Высота</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_pulse_height"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="slider_pulse_height">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,1,2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_11">
|
||||||
|
<property name="text">
|
||||||
|
<string>Количество</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="box_pulse_num">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="slider_pulse_num">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_start">
|
||||||
|
<property name="text">
|
||||||
|
<string>start!</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_13">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Статус:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_status">
|
||||||
|
<property name="text">
|
||||||
|
<string>-</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkbox_draw_reference">
|
||||||
|
<property name="text">
|
||||||
|
<string>Отрисовка эталона</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_graph_autoscale">
|
||||||
|
<property name="text">
|
||||||
|
<string>Сброс масштаба</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenuBar" name="menubar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1023</width>
|
||||||
|
<height>30</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
Reference in New Issue
Block a user