f1-telemetry/ARCHITECTURE.ru.md
2026-06-13 18:53:52 +03:00

58 lines
8.8 KiB
Markdown
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.

[English](ARCHITECTURE.md)
# Архитектура
CSV-телеметрия превращается в Markdown-отчёт через короткий линейный конвейер. Всё на стандартной библиотеке Python; ни фреймворка, ни глобального состояния.
## Файлы
```
telemetry/
__main__.py Точка входа CLI: разбор аргументов, диспатч режима, stdout. Без аргументов -> GUI.
parser.py CSV -> Lap (frozen dataclass: session, track, setup, ch, n).
segments.py Lap -> повороты и прямые. Детекция зон + нумерация.
corner_metrics.py Подсчёт по зоне: точка торможения, lock/spin, накат, температуры.
resample.py Адаптивная сетка по дистанции + интерполяция каналов для трассы.
report_common.py Шапка, строки легенды, md_table, token_estimate, атомарная запись.
report_technique.py Отчёт о пилотировании (повороты, прямые, расход, трасса).
report_setup.py Отчёт о машине (сетап, баланс, шины, тормоза, подвеска, фазы).
report_compare.py Два+ круга рядом, дельта по поворотам и накопленная.
report_race.py Папка кругов: лаптаймы, стинты, износ, тепло, ERS.
rename.py Вставка номера круга в имена файлов; пропуск обработанных.
gui.py Окно tkinter; генерация/переименование в рабочем потоке.
prompts/ Русские промпты для LLM, по одному на режим.
tests/ Набор pytest + фикстуры-круги.
```
## Как CSV становится отчётом
1. **`__main__.main`** разбирает `<режим> <файлы…>`, проверяет количество (technique/setup = 1 файл, compare = 2+, race = 1 папка) и диспатчит.
2. **`parser.load_lap`** читает файл как UTF-8 и делит на четыре мета-блока (session, track, setup) плюс таблицу телеметрии на 108 каналов. Имена каналов нормализуются срезанием суффикса ` [unit]`. Каждая колонка, целиком состоящая из нулей, отбрасывается — игра пишет много мёртвых каналов (см. «Подводные камни»).
3. **`segments.detect_corners`** проходит по сэмплам, помечает сэмпл как «в повороте», если `|Steer| > STEER_ON` или `Brake > BRAKE_ON`, сливает соседние зоны с зазором меньше `MERGE_GAP_M`, отбрасывает зоны короче `MIN_LEN_M` и нумерует оставшиеся T1..Tn по дистанции. Для каждой зоны вызывается **`corner_metrics.corner_from_zone`**, которая извлекает точку торможения, апекс, передачу, точку полного газа, пик тормоза/gLat, относительные lock%/spin%, дистанцию наката и пик температуры задней шины. `straights` заполняет промежутки между поворотами.
4. **`resample.adaptive_points`** строит сетку по дистанции — плотную внутри поворотов (и на подходе) и редкую на прямых; шаги по умолчанию берутся из медианного интервала сэмплов круга через `auto_steps`, так что более плотная сырая телеметрия даёт более плотную трассу. **`sample_at`** интерполирует каждый канал на эту сетку (ближайший сэмпл для дискретных вроде Gear/DRS).
5. Функция **`report_<режим>.generate`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше, затем вызывает **`report_common.write_report`**, которая пишет атомарно (временный файл + `os.replace`) и возвращает `(abs_path, tokens)`.
6. **`__main__`** печатает путь и `~N tokens (budget 60k)`. **GUI** показывает тот же кортеж в логе вместо печати.
## Как добавлять
**Новый режим отчёта.** Создай `telemetry/report_x.py` с `generate(lap, out_path) -> tuple[str, int]` (или `generate(laps, out_path)` для multi-lap). Собери текст из помощников `report_common` и заверши `return write_report(out_path, text)`. В `__main__.py` добавь `'x'` в список режимов и ветку диспатча, которая считает путь вывода (`report_path(input, 'x')`) и вызывает твой `generate`. Добавь режим в радиокнопки GUI и `_worker_generate`, если нужен в интерфейсе.
**Новый канал трассы.** Сэмплируй его в построителе трассы через `sample_at(lap, 'ChannelName', dists)`, добавь в заголовок трассы и `resample.format_trace_row`, и заведи строку `LEGEND_*` в `report_common.py`, чтобы колонка была задокументирована.
**Новая метрика поворота.** Добавь поле в dataclass `Corner` в `segments.py` и посчитай его в `corner_metrics.corner_from_zone`. Поскольку `Corner` — frozen, задавай через конструктор.
## Подводные камни
**Файлы в UTF-8, а кодировка ОС здесь — cp1251.** `parser.load_lap` всегда передаёт `encoding='utf-8'`; открытие файла телеметрии дефолтным кодеком падает с `UnicodeDecodeError` на первом не-ASCII байте. `rename.read_lap_number` читает только первые пять строк, но обязан использовать ту же кодировку.
**Многие каналы в этой версии игры целиком нулевые** — `Torque`, `IcePower`, высоты подвески, нагрузки, посекционные температуры шин, `Clutch`, `Handbrake` и другие. Парсер отбрасывает любую полностью нулевую колонку, поэтому код ниже должен трактовать отсутствие канала как «не записан», а не «ошибка».
**`WheelSlip*` — это разница скоростей в км/ч, не отношение.** Переднее значение −4.5 при торможении — норма. Поэтому lock/spin считаются как *относительное* скольжение (`slip / max(speed, 30) * 100`) с порогами −10 % / +8 %; сырые значения слипа несравнимы между скоростями, и базовые уровни передней/задней оси различаются.
**`Steer` — это процент (−100..100), не доля.** Малые значения на прямой — всё равно проценты.
**Строка сетапа заканчивается лишней запятой** (пустой хвост `FuelLoad` в части экспортов). Парсер отбрасывает пустые хвостовые поля, чтобы в словаре сетапа не было ключа с пустой строкой.
**Идемпотентность rename держится на токене `L<цифры>`.** `lap_token_present` — единственный источник истины для «уже обработан». Смена формата токена требует менять `insert_lap_token` и `lap_token_present` вместе, иначе rename перестаёт быть безопасным для повторного запуска.
**Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*; режим `race` считает её от родителя папки гонки. Перемещение инструмента не меняет, куда попадают отчёты.