Rework radar system

This commit is contained in:
Ayzen
2026-10-06 12:32:26 +03:00
parent 552cdc688a
commit 5bb86b7165
299 changed files with 13537 additions and 27763 deletions
@@ -21,7 +21,7 @@
## Архитектура
```
[starter] ── TASK_ENABLE ──► плата кратко открыл порт, послал, закрыл
[starter] ── TASK_ENABLE ──► плата запускает вариацию
│ пишет session.json (target temp1/2, tolerance, variation_type)
▼
[monitor] ── TRANS_ENABLE ──► плата владеет портом всё время работы
@@ -32,19 +32,21 @@
сверяет temp1↔target_temp1 и temp2↔target_temp2, |Δ|>tol ─► WARNING в консоль
```
- **Порт лазера эксклюзивен.** `starter` трогает его кратко, затем `monitor`
владеет им всё время. `checker` порт не трогает вовсе — читает только файлы.
- **Порт лазера эксклюзивен.** При `--start` запуск и мониторинг используют одно
соединение. Штатный пайплайн закрывает порт после запуска вариации, и его
может открыть отдельный монитор. `checker` читает только файлы.
- **Связь через файлы** (JSONL + JSON), а не сокеты, — процессы стартуют,
останавливаются и перезапускаются независимо, без рукопожатия.
- **Сверяются оба лазера** по внутренним `temp1`/`temp2` (не по внешним
термисторам `temp_ext*`), каждый со своим допуском (по умолчанию `0.03 °C`).
термисторам `temp_ext*`), с общим допуском (по умолчанию `0.03 °C`).
## Быстрый старт (CLI, два процесса)
Терминал 1 — стартовать вариацию и мониторить температуру:
Команды запускаются из корня проекта. Терминал 1 — стартовать вариацию и
мониторить температуру:
```bash
python -m python_app.scripts.laser_temp_monitor \
.venv/bin/python -m python_app.scripts.laser_temp_monitor \
--config run_config.json \
--start
```
@@ -52,15 +54,15 @@ python -m python_app.scripts.laser_temp_monitor \
Терминал 2 — валидировать температуру и печатать предупреждения:
```bash
python -m python_app.scripts.laser_temp_checker
.venv/bin/python -m python_app.scripts.laser_temp_checker
```
Пример вывода чекера при расхождении и возврате в допуск:
```
WARNING laser_temp_checker: Laser 1 temperature off target: measured 28.100 °C,
WARNING python_app.hardware_full.laser_control.monitoring.checker: Laser 1 temperature off target: measured 28.100 °C,
target 28.000 °C, Δ=+0.100 °C exceeds tolerance ±0.030 °C [seq=1]
INFO laser_temp_checker: Laser 1 temperature back within tolerance:
INFO python_app.hardware_full.laser_control.monitoring.checker: Laser 1 temperature back within tolerance:
28.000 °C (target 28.000, |Δ|=0.000 ≤ 0.030) [seq=2]
```
@@ -69,7 +71,7 @@ INFO laser_temp_checker: Laser 1 temperature back within tolerance:
## Конфигурация
Параметры берутся из `run_config.json`, секция `radar.laser_control`. Мониторинг
использует блок `variation` и новое поле `temp_tolerance_c`:
использует блок `variation`, включая `temp_tolerance_c`:
```json
{
@@ -105,7 +107,7 @@ INFO laser_temp_checker: Laser 1 temperature back within tolerance:
| Поле | Смысл |
|---|---|
| `port` | Серийный порт лазерной платы (пусто → автоопределение) |
| `port` | Серийный порт лазерной платы; CLI-монитор допускает пустое значение для автоопределения, пайплайн требует явный порт |
| `static_temp1` / `static_temp2` | Целевые статичные температуры лазеров 1/2, °C |
| `min_value` / `max_value` / `step` | Диапазон и шаг свипа тока, мА — из них считается период свипа |
| `time_step` / `delay_time` | Тайминги точки (мкс / мс) — тоже входят в период свипа |
@@ -126,13 +128,13 @@ INFO laser_temp_checker: Laser 1 temperature back within tolerance:
Мониторить уже запущенную из GUI/пайплайна вариацию (без повторного старта):
```bash
python -m python_app.scripts.laser_temp_monitor --config run_config.json
.venv/bin/python -m python_app.scripts.laser_temp_monitor --config run_config.json
```
Фиксированный период вместо расчётного (напр. раз в 500 мс):
```bash
python -m python_app.scripts.laser_temp_monitor \
.venv/bin/python -m python_app.scripts.laser_temp_monitor \
--config run_config.json --strategy interval:500
```
@@ -150,10 +152,10 @@ python -m python_app.scripts.laser_temp_monitor \
```bash
# монитор
python -m python_app.scripts.laser_temp_monitor --config cfg.json --start \
.venv/bin/python -m python_app.scripts.laser_temp_monitor --config cfg.json --start \
--readings /tmp/run7.jsonl --session /tmp/run7.session.json
# чекер
python -m python_app.scripts.laser_temp_checker \
.venv/bin/python -m python_app.scripts.laser_temp_checker \
--readings /tmp/run7.jsonl --session /tmp/run7.session.json --reminder-every 20
```
@@ -161,9 +163,10 @@ python -m python_app.scripts.laser_temp_checker \
Когда вариацию стартует штатный пайплайн
([`apply_kamil_adc_laser_control`](../../kamil_adc/laser.py)), снимок сессии
`session.json` пишется автоматически. Достаточно запустить только чекер
(и, при желании, монитор без `--start`, чтобы он опрашивал плату). Так консоль
получит предупреждения о рассинхроне температуры прямо во время захвата.
`<tmp>/laser_variation_session.json` пишется автоматически. Для предупреждений
во время захвата нужны монитор без `--start`, который создаёт показания, и чекер.
Если монитор уже работает, достаточно добавить чекер. Он загружает сессию
один раз при запуске; после запуска новой вариации чекер нужно перезапустить.
## Встраивание в свой код (без CLI)
@@ -187,15 +190,22 @@ period = resolve_period_s("computed", min_value=33.0, max_value=60.0, step=0.05,
stop = threading.Event()
with LaserController(port="/dev/ttyUSB0") as ctrl, ReadingWriter("readings.jsonl") as w:
monitor = LaserTemperatureMonitor(ctrl, w, period_s=period)
threading.Thread(target=monitor.run, args=(stop,), daemon=True).start()
worker = threading.Thread(target=monitor.run, args=(stop,))
worker.start()
# 3. Чекер: тайлить показания и валидировать оба лазера
checker = LaserTemperatureChecker.from_session(session)
reader = ReadingReader("readings.jsonl")
while not stop.is_set():
for reading in reader.poll():
checker.process(reading) # печатает WARNING при |Δ| > tolerance
stop.wait(0.2)
try:
while not stop.is_set():
for reading in reader.poll():
checker.process(reading) # печатает WARNING при |Δ| > tolerance
stop.wait(0.2)
except KeyboardInterrupt:
pass
finally:
stop.set()
worker.join() # завершить опрос до закрытия порта и файла
```
`LaserTemperatureChecker.evaluate(reading)` возвращает список
@@ -216,7 +226,7 @@ with LaserController(port="/dev/ttyUSB0") as ctrl, ReadingWriter("readings.jsonl
}
```
`readings.jsonl` (по одной строке-объекту на свип):
`readings.jsonl` (по одной строке-объекту на успешный опрос):
```json
{"seq":0,"mono_ns":123456789,"temp1":28.0,"temp2":28.9,"temp_ext1":22.0,"temp_ext2":23.0,"current1":33.0,"current2":35.0}
@@ -232,14 +242,16 @@ per_point_s = delay_time / 1000 + time_step / 1_000_000
sweep_period = num_steps × per_point_s
```
Монитор публикует одно показание за такой период. Если нужен другой темп —
Монитор опрашивает плату сразу после запуска, затем ждёт такой период после
каждого опроса; это оценка, без синхронизации с границами аппаратных свипов.
Если нужен другой темп —
`--strategy interval:<ms>`. (Внутренний счётчик `TO6` платы существует, но его
семантика не гарантирована, поэтому для тайминга он не используется.)
## Тесты
```bash
python -m pytest python_app/tests/test_laser_temp_monitoring.py -q
.venv/bin/python -m unittest python_app.tests.test_laser_temp_monitoring
```
Покрыто: round-trip сессии, tail JSONL (включая усечённую последнюю строку),
@@ -23,10 +23,15 @@ from .readings_channel import ReadingWriter, TemperatureReading
logger = logging.getLogger(__name__)
class _Temperatures(Protocol):
temp1: float
temp2: float
class _MeasurementSource(Protocol):
"""Minimal controller surface the monitor depends on (eases testing)."""
def get_measurements(self) -> object: ...
def get_measurements(self) -> _Temperatures | None: ...
def compute_sweep_period_s(
@@ -138,5 +143,5 @@ class LaserTemperatureMonitor:
logger.info("Temperature monitor stopped after %d readings", seq)
def _opt(value: object) -> Optional[float]:
def _opt(value: float | None) -> Optional[float]:
return None if value is None else float(value)
@@ -58,7 +58,7 @@ class TemperatureReading:
)
def _opt_float(value: object) -> Optional[float]:
def _opt_float(value: str | float | None) -> Optional[float]:
return None if value is None else float(value)