Техническая сторона
Как это устроено
Всё, что обычно прячут за словом «профессиональный»: путь одного времени от калитки до протокола, что летит по радио, чем закрыт файл с фамилиями детей и где у комплекса SkiSport границы. Без цифр такой рассказ ничего не стоит, поэтому цифры здесь есть.
1. Путь одного времени
Время рождается дважды: в момент, когда спортсмен раскрывает калитку, и в момент, когда пересекает створ финиша. Всё остальное — доставка. Вопрос только в том, что происходит с точностью по дороге, и ответ зависит от того, как соединены старт и финиш.
Проводом
≈0,3 мс
Импульс старта идёт по витой паре, и оба фронта — старт и финиш — размечает один кварц финишного узла. Ошибка здесь масштабная: она растягивает время всем одинаково и никого не может переставить местами.
По радио
1–3 мс
Двое часов, которые подводятся друг под друга каждые десять минут. Ошибка случайная и своя у каждого — сумма рассинхронизации и ухода между сверками. Двое, стоящие в сотой доле, действительно могут поменяться местами.
Рукой
десятые
Человек и секундомер — или человек, печатающий время в таблицу. Исправленное руками время понижается до этой категории, даже если сначала его измерил провод.
Разница между ними — два порядка, но важнее, что она разная по природе. Поэтому происхождение времени хранится вместе с самим временем и печатается в протоколе. Беспроводная детская гонка — это нормально. Беспроводное время, приехавшее в протокол под видом проводного, — нет.
2. Что летит по радио
18 байт
Не «пакет данных», а конкретная структура, в которой каждое поле пришлось заслужить:
| Поле | Байт | Зачем |
|---|---|---|
| magic, version | 2 | Чужой передатчик на той же частоте не должен быть принят за свой |
| type | 1 | Результат, время старта, отзыв старта, подтверждение, пульс узла… |
| run | 1 | Первый заезд или второй |
| bib | 2 | Номер участника; у пульса узла здесь напряжение батареи |
| seq | 2 | Номер пакета — по нему отзывают конкретный старт, а не «последний» |
| value | 4 | Время в миллисекундах; в сверке часов — отметки t0…t3 |
| speed_cms | 2 | Скорость в створе, см/с |
| src | 1 | Кто отправил: старт, финиш, база, табло |
| hops | 1 | Сколько раз пакет ретранслировали; дальше двух не живёт |
| crc | 2 | CRC-16/CCITT-FALSE поверх всего предыдущего |
Своя контрольная сумма — при том, что у радио есть собственная. Радийная проверяет кадр в эфире; эта проверяет пакет и ловит то, что случилось с ним по дороге через ретранслятор и очереди. Стоит два байта.
Отправитель в пакете появился не сразу: до третьей версии приёмник угадывал его по типу — результат шлёт финиш, время старта шлёт старт. С подтверждениями так уже нельзя, надо знать, кому отвечать, а ретранслятору нужно ещё и отличать свой пакет от собственного эха. Два байта на отправителя и счётчик пересылок — и петли стали невозможны.
Радио
LoRa, 868 МГц, SF7, полоса 125 кГц. Не самый дальний из возможных режимов — самый быстрый из достаточных: пакет уходит за десятки миллисекунд, а не за полсекунды, и склон он перекрывает.
Часы подводятся сами
Раз в десять минут. Обмен из четырёх отметок (t0…t3) — тот же приём, что у сетевого протокола времени: путь туда и обратно симметричен, поэтому половина оборота и есть поправка.
Ретранслятор
Один на склоне — стартовая калитка между ноутбуком и финишем. Дальше двух пересылок пакет не живёт: счётчик в пакете и есть то, что не даёт эху ходить по кругу.
3. Кто это проехал
Спортсмена узнаёт метка — маленький BLE-маячок, который едет на нём. Узлы
читают номер прямо из рекламного кадра, и форматов ровно два: свой,
шестибайтовый, и обычный iBeacon — тот, что продаётся
готовым за две сотни рублей, не требует ни пайки, ни прошивки, а номер
спортсмена кладётся в поле major из приложения.
- Свой UUID обязателен. Без него любой чужой маячок — витрина магазина, телефон, соседняя команда — становится спортсменом с правдоподобным номером, и ошибка эта молчаливая: время припишется тому, кого на трассе нет. Принимаются только метки с нашим UUID.
- Разбор один на два узла. Старт и финиш читают метку одним и тем же кодом. Две копии разбора означали бы, что калитка и финиш по-разному понимают, кто едет, — и разошлись бы они однажды на морозе, а не на столе.
- Номер живёт в метке, а не в таблице. Поэтому подменить его через программу нельзя: узлы адреса метки вообще не видят. Таблица «Учёт меток» отвечает на другой вопрос — «Петров потерял метку, какая ему выдана взамен», — и маячки в ней перечислены по спортсменам, а не по заводским адресам.
4. Старт по правилам
Стартовые часы отсчитывают пять секунд: низкий тон на каждой из последних пяти, высокий — в ноль. Так это описано в Timing Booklet FIS, и так это здесь и сделано. Окно, в котором старт считается действительным, — пять секунд по обе стороны от нуля (ICR 613.7).
- Пять секунд блокировки. Калитка, открытая раньше этого срока после предыдущего старта, — это не старт, а тот же спортсмен, выпутывающийся из створа. Такой импульс не заводит новый отсчёт.
-
Задел, но не поехал. Стартовый узел смотрит, ушла ли
метка: восемь секунд на уход, три секунды тишины подряд считаются
подтверждением. Не ушла — уходит отзыв, и отзывается ровно тот пакет, чей
seqназван. Задели калитку, через полминуты поехали по-настоящему — в очереди уже два старта одного номера, и отозвать не тот значило бы выбросить настоящий заезд. - Опознание раньше отзыва. Кто едет, финиш должен узнать сразу, иначе он не знает, чьё время меряет. Поэтому отзыв приходит позже — спортсмену до финиша ещё полминуты, и отзыв успевает.
5. Тренировка без ноутбука
Разворачивать ноутбук ради вторничной прикидки никто не станет, поэтому
базовая станция умеет работать одна. Она держит журнал на 250 записей в
собственной файловой системе, раздаёт точку доступа и отдаёт день по
адресу /api/today; телефон тренера открывает страницу без
интернета вовсе.
Когда связь появляется, день уезжает на сайт — по ключу тренера и идемпотентно: повторная выгрузка того же дня не создаёт вторых копий, потому что днём и служит ключ. Это не мелочь: сеть на склоне рвётся ровно в тот момент, когда выгрузка почти прошла, и «отправить ещё раз» — первое, что делает человек.
Метки на тренировке именные и постоянные: у каждого своя, привязанная раз, — поэтому тренеру не нужно каждый вторник раздавать номера и записывать, кому какой достался. Соревновательные метки живут иначе, вшитыми в майку с номером, и это разные роли одного и того же железа. Ростер команды хранится на сайте и служит источником правды: программа сверяет с ним номер, а расхождение не проглатывает, а пишет в журнал.
В конце тренировки коробка раздаёт времена сама. Спортсмен подключается к
её точке доступа, вводит на странице /mine свой номер — тот,
что на груди, — и уезжает со своими спусками: сколько их, лучший, разброс
между лучшей и худшей попыткой и список по порядку, как ехал. Ждать, пока
тренер найдёт сеть и выгрузит день, ему больше не нужно; он уже в машине.
Фамилий коробка при этом не знает и не узнает: она возит номера, а
подписывает их сайт.
6. Свой сервер на ноутбуке
Пока идёт соревнование, программа секретаря сама раздаёт страницы по локальной сети — без интернета, которого на склоне нет. Один ноутбук обслуживает всех, кому нужно смотреть, и каждому даёт свою страницу вместо общей «таблицы для всех».
Экран склона
Для телевизора в кафе и монитора на финише: приехавший появляется крупно и подсвечивается, экран сам поворачивается к его группе, рядом — кого ждут на трассе и какое время ему надо побить. В углу код: тот же экран уносят в телефоне. Такой же экран отдаёт и сайт, когда на объекте есть интернет.
Табло и протоколы
Живые результаты, стартовый протокол, альтернативный вид. Колонки настраиваются отдельно для каждого режима — стартовый список, первый заезд, второй, итог, — потому что в этих четырёх случаях нужны разные колонки.
Оглавление
Адрес ноутбука открывает список всех страниц сервера — с подписью, кому они и нужен ли вход, и QR к каждой. Один лист, распечатанный и повешенный у судейского домика, заменяет вопрос «а какой там адрес» на весь день.
Судья с телефона
Отдельный вход под паролем: посмотреть спортсмена по номеру, ввести время. Сессия, ограничение попыток входа, и запись, сделанная с телефона, приходит в ту же очередь, что и любая другая, — модель меняется только в главном потоке.
Комментатор
Своя выдача, собранная вокруг человека, а не вокруг строки таблицы: кто сейчас на трассе и кто следующий, клуб, тренер, возраст, как едет относительно лидера и относительно своей первой попытки — и заметка секретаря, которой нет больше нигде. У комментатора примерно четыре секунды, чтобы сказать о человеке что-то правдивое.
Голос по громкой связи
Программа объявляет приехавшего вслух: номер, фамилия, время, а если мерили — и скорость на выкате. Синтез системный, поэтому на горе он работает так же, как в судейской: облачный там просто молчит. Место не называется — пока группа едет, объявленное третье через минуту окажется пятым, и это услышит вся поляна. Шаблон выбирается: от «номер и время» до «на старте такой-то, на трассе такой-то из такого-то клуба, результат такого-то». Тот же голос и те же шаблоны умеет экран на сайте, уже средствами браузера.
Титры для трансляции
Страница на однотонном фоне, который видеомикшер вырезает по цвету, — не отчёт, а видеоисточник. Плашка сама следует за тем, кто только что финишировал, держится заданное число секунд и уходит; номер можно приколоть вручную для интервью или пьедестала.
Титры устроены хитрее, чем кажется, и вот в чём соль: страница следит только за собственным временем каждого спортсмена. Место и отставание нарочно исключены — новый лидер переписывает место всему полю, и плашка побежала бы за тем, кто просто оказался последним в сортировке, ровно в тот момент, когда гонка становится интересной.
7. Связь: боты вместо почты
Сайт не отправляет ни одного письма — и это решение, а не недоделка. Транзакционная почта с маленького сервера уезжает в спам, а подтверждение, которое не пришло, выглядит как сломанная регистрация. Вместо неё три независимых канала — Telegram, MAX и Matrix, — каждый включается отдельно в админке.
- Решения принимаются из чата. Администратор открывает доступ тренеру, организатор принимает заявку — командой в мессенджере, с телефона, стоя у финиша. Код приклеен к команде через подчёркивание, и это не косметика: так мессенджер делает ссылкой всю команду целиком, и нажатие одно, а не «перенаберите код руками с экрана».
- Право даёт привязка чата, а не текст сообщения. Команда исполнится, только если этот чат привязан к учётной записи с нужным правом, а привязка подтверждена одноразовым кодом из кабинета.
- Тридцать детей — одно уведомление. Заявка, поданная одним нажатием, получает один код пачки и уходит владельцу соревнования, а не всем администраторам сразу.
- Молчащий бот виден. Каждый успешный опрос отмечается, и в админке видно, принимает ли бот коды на самом деле. Раньше настроенный токен при неработающем процессе выглядел точно так же, как рабочий, — человек отправлял код и не получал ничего.
8. Чем закрыты данные
В файле соревнования лежат фамилии, даты рождения и разряды несовершеннолетних. Забытый в машине ноутбук должен означать украденный ноутбук, а не украденную заявку.
Тело файла закрыто AES-256-GCM — тем же алгоритмом, каким закрыт обмен между судейскими ноутбуками. Выбран он не за громкое имя: 256-битный ключ подобрать перебором нельзя никакими вычислениями, а режим GCM, в отличие от простого шифрования, отвечает ещё и за целостность. Изменённый в файле байт не превратится в другую фамилию — файл просто не откроется, и это будет сказано вслух. Открытый заголовок входит в шифрование как связанные данные, поэтому подменить в нём список судей, оставив тело прежним, тоже не выйдет.
Вся стойкость этой схемы держится на двух правилах, и оба они — про то, как алгоритм применён, а не про сам алгоритм: одноразовый номер (nonce) не должен повторяться дважды под одним ключом, а ключ должен быть надёжно сохранён. Повторённый nonce в GCM — это не «чуть слабее»: он выдаёт разницу двух сообщений и позволяет подделывать подписи к следующим. Ниже — что здесь сделано ради каждого из двух.
Файл соревнования
AES-256-GCM на тело файла. Ключ у файла свой и случайный, а под пароль каждого судьи заворачивается отдельная его копия — поэтому судью можно добавить или убрать, не перешифровывая файл и не меняя пароль остальным.
Номер, который не повторяется
12 случайных байт на каждое сохранение и на каждый пакет между ноутбуками — из системного источника случайности, а не из счётчика. Счётчик пришлось бы хранить и переносить вместе с файлом, а файл копируют на флешку, восстанавливают из истории сохранений и открывают на втором ноутбуке — то есть счётчик рано или поздно откатился бы назад.
Где лежит ключ
Ключ файла в открытом виде не лежит нигде — ни в файле, ни в настройках. В заголовке хранятся только его копии, завёрнутые под пароли судей через scrypt; чтобы развернуть любую, нужен пароль. Поэтому украденный файл сам по себе не даёт ничего, а забытый пароль означает потерянный файл — по-настоящему, а не «мы восстановим».
Пароль в ключ
scrypt, N = 215, r = 8, p = 1. Около ста миллисекунд на судейском ноутбуке — и ровно столько же на каждую попытку у того, кто подбирает пароль по украденному файлу. Дорого намеренно.
Пароль не покидает браузер
Программе секретаря пароль от сайта не нужен: она показывает шестизначный код, судья вводит его на сайте — там, где уже вошёл, — и сайт выдаёт программе ключ. Пароль открывает все соревнования организации, а ноутбук в конце дня передают другому. Разрешение отзывается в кабинете, и там же видно, когда каждая программа обращалась в последний раз.
Капча своя, а не чужая
Форма обратной связи открыта всем, а значит, видна рассылкам. Проверка «человек ли это» рисуется самим сайтом: чужой скрипт сюда не загрузится — политика содержимого пускает только свой домен, — да и отдавать рекламной сети каждого, кто зашёл посмотреть результаты ребёнка, ради формы обратной связи незачем.
Что сайт не хранит
Год рождения вместо полной даты. Ни кодов FIS, ни кодов РУС: как сквозной идентификатор человека они опаснее всего остального — по ним две чужие базы связываются в одну. Фотографий спортсменов нет.
Согласия
Хранится не галочка, а версия текста, его отпечаток, кто согласился, о чьих данных речь и когда согласие отозвали. Галочка помнит, что её поставили; она не помнит, что было написано над ней.
9. Протокол по правилам
Официальный протокол результатов обязан содержать тринадцать позиций — пункт 33.3.4 Правил вида спорта «горнолыжный спорт», он же 617.3.4 ICR. Российские правила здесь дословный перевод международных: пункты идут парами, и требования к содержанию у них одни.
Программа печатает все тринадцать: проводящую организацию, дисциплину, категорию и пол, судейскую бригаду целиком, постановщиков и открывающих по каждому заезду, время старта заездов, данные трассы с номером омологации, погоду, состояние снега и температуру воздуха на старте и финише, коды и очки участников, списки не стартовавших, не финишировавших и снятых, номер из календаря и коэффициент F, расчёт константы соревнований и место под подпись технического делегата.
Форма протокола — не то же самое, что допуск системы хронометража. Первое здесь сделано целиком; про второе честно сказано ниже, в разделе «Где границы».
Расчёт, а не итог
Надбавку правила требуют не назвать, а показать: суммы A, B и C и частное. Она считается и печатается рядом с применённой — применяет надбавку жюри, и расхождение должно быть видно, а не спрятано.
Код, а не фамилия
Код участника стоит и в строке результата, и в списках снятых. По фамилии человека в базе не находят, по коду находят.
Место под подпись
Технический делегат протокол подписывает. Напечатанная фамилия подписью не является, поэтому под ней линия.
10. Температура — с табло
Температура воздуха в зоне старта и в зоне финиша — одна из тринадцати позиций, и единственная, которую нельзя просто напечатать: её надо измерить. Поэтому термодатчик стоит в стартовом и финишном табло, а показания приходят в программу по нажатию и сразу попадают в соревнование.
Датчик снаружи
DS18B20 на проводе, вынесенном за корпус в тень. Внутренний датчик процессора меряет кристалл, а он в работающем табло теплее воздуха на десяток градусов; табло на солнце показало бы солнце.
По запросу, а не потоком
Радио занято временами, а температура нужна дважды за гонку. И та, что приезжает сама, измерена неизвестно когда — тогда как в протокол пишут температуру на момент заезда.
Ноль — это ноль
Ноль градусов бывает, и подменять им «не измеряли» нельзя. Отказ датчика — отсутствие ответа, а не ноль; показание старше четверти часа видно серым.
11. Три проверки за день
Соревнование можно испортить трижды, и вопросы в этих трёх точках разные. Поэтому проверок тоже три, и каждая спрашивает своё.
Перед стартом
Можно ли вообще ехать: одинаковые номера, участник без группы, молчащая станция, садящаяся батарея, выключенное автосохранение.
Перед второй попыткой
Цел ли результат первой и верен ли составленный из него список. Нет времени первой попытки — этого человека во второй не окажется, и узнает он об этом в калитке.
После финиша
Можно ли подписывать: незаконченные вторые попытки, отсутствующие места, снятия без причины, ручные времена к сверке с бумагой, группы с неполным пьедесталом.
12. Порядок второй попытки
Вторая попытка едет тем же порядком, что и первая, — по стартовым номерам. Меняется только голова списка: лучшие по результату первой попытки стартуют развёрнуто, n-й первым, победитель первой попытки n-м. Сколько мест разворачивать, решает жюри (п. 36.11.3 Правил разрешает уменьшить это число), а без разворота вторая попытка повторяет первую.
Номер называют один раз
Порядок печатают и вывешивают на старте. Ввод очередного времени его не двигает: если бы двигал, узнавали бы об этом не в программе, а в калитке.
Не допущен — значит без номера
Снятый, сошедший и не стартовавший во второй попытке не участвуют. Номер для них — не «поедет последним», а неправда, произнесённая стартёром вслух.
Равные времена — по номеру
В протокол результатов при равном времени первым вносится больший стартовый номер (п. 33.3.3). Разворот переставляет их обратно, и на границе первым едет меньший — как требует п. 36.11.2.
13. Где границы
Комплекс SkiSport говорит на языке профессиональных систем — понимает построчный вывод ALGE Timy и совместимых приборов TAG Heuer, — и умеет работать рядом с ними третьим каналом. Своё железо честно называет свои пределы: для тренировок, прикидок и детских стартов оно годится, а для разрядных и FIS-стартов нужна гомологированная система.
Это не оговорка мелким шрифтом, а часть устройства: везде, где можно было выбрать между тихой ошибкой и громкой, выбрана громкая. Перерезанный кабель узел замечает сам и кричит. Задетая калитка отзывается. Расхождение номера с протоколом пишется в журнал. Время, измеренное по радио, приходит в протокол с пометкой, что оно измерено по радио.