mirror of
https://github.com/Horizontal42/f1-telemetry.git
synced 2026-11-10 15:22:03 +03:00
58 lines
9.3 KiB
Markdown
58 lines
9.3 KiB
Markdown
[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, load_prompt, атомарная запись.
|
||
report_technique.py Отчёт о пилотировании (повороты, прямые, расход, трасса).
|
||
report_setup.py Отчёт о машине (сетап, баланс, шины, тормоза, подвеска, фазы).
|
||
report_compare.py Два+ круга рядом, дельта по поворотам и накопленная.
|
||
report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS.
|
||
rename.py Вставка типа сессии + номера круга (P1_L7); пропуск обработанных.
|
||
gui.py Окно tkinter; память последней папки по режиму; генерация/переименование в потоке.
|
||
prompts/ Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта.
|
||
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(…, lang)`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше. Затем вызывает **`load_prompt(mode, lang)`**, которая читает промпт из `prompts/<lang>/<mode>.md` и добавляет его после разделителя `---`. Финальный текст пишется через **`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 держится на ДВУХ токенах.** Файл считается обработанным только при наличии обоих: токена типа сессии (`P1`, `Q2`, `R`, … — совпадение по `_SESSION_PAT`) и токена `L<цифры>`. `session_token_present` и `lap_token_present` — совместный источник истины. Смена формата любого токена требует обновить и функцию вставки, и функцию проверки одновременно.
|
||
|
||
**Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*. Режим `race` кладёт отчёт внутрь папки сессии (`<race_dir>/reports/`). Перемещение инструмента не меняет, куда попадают отчёты.
|