f1-telemetry/ARCHITECTURE.ru.md

9.3 KiB
Raw Permalink Blame History

English

Архитектура

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/). Перемещение инструмента не меняет, куда попадают отчёты.