f1-telemetry/ARCHITECTURE.ru.md
Andrew Pimenov 15bb552166 rename: sort by game — races/F1 22|ACC/<track>/<session>/
read_metadata now returns game_raw; _game_folder maps it to 'F1 22' or 'ACC';
dest_dir gains a game subfolder between races_dir and track
2026-06-19 16:40:02 +03:00

16 KiB
Raw Permalink Blame History

English

Архитектура

CSV-телеметрия (F1 22 или ACC) превращается в 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, game_of, load_prompt, атомарная запись.
  report_technique.py  Отчёт о пилотировании (повороты, прямые, расход, трасса).
  report_setup.py    Отчёт о машине (сетап, баланс, шины, тормоза, подвеска, фазы).
  report_compare.py  Два+ круга рядом, дельта по поворотам и накопленная.
  report_race.py     Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS. Дедуп дублей номеров кругов.
  report_profile.py  Папка кругов: дешёвые тенденции по поворотам (медиана на поворот, по доминирующему составу) + авто-выбор кругов для разбора. Без трассы.
  rename.py          Вставка типа сессии + номера круга (P1_L7) в имя; с races_dir — сортировка в races/<game>/<track>/<session>/ и рекурсия по подпапкам.
  gui.py             Окно tkinter с двумя вкладками (F1 22 / ACC); память последней папки по режиму; генерация/переименование в потоке. Предупреждает (без блокировки), если определённая игра не совпадает с активной вкладкой.
prompts/             Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта.
                     Переопределения для ACC лежат в prompts/<lang>/acc/<mode>.md; при отсутствии — откат к F1-промпту.
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, include_prompt=True) собирает Markdown из помощников report_common (header_block, md_table, legend) и данных выше. game_of(lap) читает поле Game из сессии и возвращает 'acc' (если значение начинается с "ACC") или 'f1'; генераторы отчётов используют это, чтобы убрать разделы DRS/ERS/гибрид для ACC и заменить таблицу сетапа пометкой, когда все значения нулевые (ACC всегда так экспортирует). При include_prompt вызывает load_prompt(mode, lang, game), которая для ACC сначала ищет prompts/<lang>/acc/<mode>.md и откатывается к prompts/<lang>/<mode>.md при его отсутствии, затем добавляет результат после разделителя --- (CLI --no-prompt / чекбокс GUI это отключают). Финальный текст пишется через write_report атомарно (временный файл + os.replace), возвращается (abs_path, tokens). Перед генерацией __main__ пропускает прогон через is_fresh(report, sources), если свежий отчёт уже есть (override — --force).
  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, задавай через конструктор.

Допущения и тюнящиеся константы

Два рода констант. Детекция — пороги в единицах телеметрии, одинаковы во всей F1 22: решают, где граница, а не вывод, поэтому не тюнингуются под трассу. Интерпретация — доменное суждение, которое реально варьируется; в отчётах рядом с любым флагом/вердиктом всегда печатается сырое число, так что спорный порог не прячет данные.

Константа Файл Род Основа / чувствительность
STEER_ON 6, BRAKE_ON 5, BRAKE_MARK 20, FULL_THROTTLE 98 segments / corner_metrics детекция % от диапазона ввода, одинаково в игре
MERGE_GAP_M 40 + merge_gap_for (clamp 28–60) segments детекция адаптивно: масштабируется по Tracklen вокруг якоря Zandvoort — шиканы/длинные трассы самотюнятся
MIN_LEN_M 15, _SAFE_SPEED_FLOOR 30, окно газа 0.3 с segments / corner_metrics детекция геометрия / защита деления / дебаунс
resample dense=median, sparse=8×median resample детекция самонастройка под частоту данных
LOCKUP −10 %, SPIN +8 % segments интерпретация флаги severe-only; сырые lock%/spin% всегда показаны
FUEL 0.03–0.05 с/кг report_compare интерпретация зависит от трассы; выдаётся диапазоном с печатью ставки
diff ±0.5 US/OS report_setup интерпретация относительно baseline скольжения круга; сырое front/rear показано
profile *_REL множители + *_FLOOR / TR поля report_profile интерпретация множители относительно медианы сессии; полы — шумовые гейты; медианы в таблице

Если будущая трасса потребует другой группировки поворотов — merge_gap_for уже адаптируется по длине; ручная правка нужна лишь на совсем нестандартной конфигурации.

Подводные камни

Файлы в 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 с races_dir распределяет по папкам и рекурсит. rename_unprocessed(targets, races_dir) когда races_dir установлен: (1) сканирует всё дерево подпапок (os.walk), (2) читает метаданные из CSV (read_metadata → lap, event, track, game), (3) вставляет токены если их нет, (4) создаёт races/F1 22|ACC/<track_safe>/Practice|Qualifying|Race|Sprint/ и перемещает туда файл, (5) даже уже переименованные файлы переносятся если лежат не там где надо, (6) пропускаются только файлы, которые уже находятся в нужной папке с нужным именем. Без races_dir работает как раньше — переименовывает в исходной папке.

Отчёты выходят рядом с входом, а не рядом с программой. report_path выводит папку reports/ из папки входного CSV. Режим race кладёт отчёт внутрь папки сессии (<race_dir>/reports/). Перемещение инструмента не меняет, куда попадают отчёты.

Игра определяется из поля Game в метаданных и передаётся сквозь всю генерацию отчёта. game_of(lap) читает lap.session['Game']; для Assetto Corsa Competizione поле начинается с "ACC", для F1 22 — иначе. Каждый генератор отчёта вызывает game_of, чтобы решить, какие секции выводить. Флаг CLI --game позволяет переопределить определение, патча поле сессии до диспатча.

Сетап ACC — всегда нули, и таблица параметров намеренно не рендерится. CSV-экспорт ACC записывает нули во все поля сетапа. report_setup проверяет, все ли значения нулевые, и, если да, заменяет таблицу параметров поясняющей пометкой вместо вывода бессмысленных нулей.

События сессии в ACC — полные слова; в F1 22 — короткие коды. F1 22 пишет коды вроде P1, Q1, R. ACC пишет Practice, Qualifying, Race. Модуль переименования нормализует полные слова ACC до компактных канонических кодов (Practice→P, Qualifying→Q, Race→R) перед вставкой в имя файла и выбором папки назначения. Вставляемый токен совпадает с тем, что ожидает распознаватель паттерна сессии, поэтому повторный запуск rename на уже обработанных файлах ACC идемпотентен (они пропускаются, а не получают двойной тег).