Featured image of post Как я искал звук в системе оповещения

Как я искал звук в системе оповещения

Пакеты приходят, а звук пустой. Разбираюсь и нахожу ответ в порядке четырёх байтов

Эта история про то, как два устройства говорили на одном протоколе, отправляли друг другу одни и те же данные, и всё равно не могли друг друга понять из-за четырёх байтов, записанных в другом порядке.

Контекст

Я работал над системой оповещения, которая в случае ЧС включает сирены, отправляет голосовые сообщения и текстовые бегущие строки на оконечные устройства. Всё по ГОСТу 42.3.05-2023, с чёткими форматами пакетов, квитанциями и таймингами.

Архитектура: пульт оператора отправляет команды по TCP, а звук по UDP. На другом конце сервер принимает команды, запускает оповещение и ретранслирует звук на устройства. Между ними бинарный протокол с 8-байтными пакетами и аудио-датаграммами по 84 байта.

У меня была задача написать заглушку (эмулятор пульта), покрыть всё интеграционными тестами и добиться сквозной работы с реальным пультом от другого производителя.

Заглушка работает, реальный пульт нет

Первое что я сделал это написал заглушку на Node.js. Интерактивное меню, все команды по ГОСТу, UDP-поток с аудио, подключился к тестовому серверу, прогнал полный сценарий, квитанции приходят, звук записывается в файл, файл воспроизводится.

Потом подключили реальный пульт. TCP-команды ок. Квитанции ок. Запуск оповещения ок. А вот звука нет, файл вроде как создаётся, но внутри какой-то монотонный фон и щелчок в конце. Хотя поставщики пультов говорят что отправляли тестовое голосовое оповещение…

Первые подозрения

Сначала я подумал, что проблема в сети. Пульт стоял за NATом, подключался через внешний IP. TCP проходил, а UDP мог и не доходить. Подключился к серваку проверил tcpdump , он показал, что UDP-пакеты приходят, 84 байта, правильный порт.

Сервер принимал UDP-пакеты и логировал счётчик каждого. По ГОСТу формат датаграммы такой:

1
2
3
Байты 0–3:  Счётчик пакетов (32-битное целое)
Байты 4–83: Звуковые данные (80 байт, A-Law, 8 кГц)
Всего:      84 байта

Счётчик должен начинаться с нуля и увеличиваться на единицу с каждым пакетом: 0, 1, 2, 3… Сервер использует его для обнаружения потерь и восстановления порядка.

Я посмотрел логи и увидел:

1
2
3
expected=0, received=838860801, gap=838860801
expected=838860802, received=855638016, gap=16777215
expected=855638017, received=872415232, gap=16777215

Вместо 0, 1, 2, 3 сервер видел 838860801, 855638016, 872415232. И разрыв между пакетами стабильные 16 777 215 вместо единицы.

16 777 215

16 777 215 в шестнадцатеричной записи это 0x00FFFFFF. Ровно три байта, заполненных единицами, уже звоночек

Я перевёл первый счётчик в hex:

1
838860801 = 0x32000001

Разложил по байтам: [0x32, 0x00, 0x00, 0x01].

А потом перевернул: [0x01, 0x00, 0x00, 0x32] и получил 0x01000032 = число 50 в десятичной. Следующий пакет 51, потом 52, 53… Счётчик оказывается шёл нормально, но в другом порядке байтов.

Big Endian vs Little Endian

Одно и то же 32-битное число можно записать двумя способами:

Big Endian старший байт первый. Число 1 записывается как [0x00, 0x00, 0x00, 0x01].

Little Endian младший байт первый. Число 1 записывается как [0x01, 0x00, 0x00, 0x00]. Как Интел процессоры хранят данные в памяти.

Наш сервер читал счётчик как Big Endian. Пульт отправлял как Little Endian, и вместо нормального счётчика 50, 51, 52 сервер видел 838860801, 855638016, 872415232.

А при чём тут звук?

Сам по себе перевёрнутый счётчик не катастрофа, он логировал ошибки, но пакеты всё равно принимал, звуковые данные (байты 4–83) лежали на правильных позициях.

С мусорными значениями счётчика он решал, что пакеты приходят в неправильном порядке, пытался их переставить, и в результате аудио-поток рассыпался. На выходе данные есть, но перемешаны так, что декодер A-Law выдаёт постоянный уровень сигнала вместо речи, и получаем тишину с щелчком.

Кто прав по ГОСТу?

Я полез в стандарт искать, какой порядок байтов правильный. В пункте 5.9.3.2 который описывает формат датаграммы, порядок байтов не указывается явно. Просто указано «32-битный инкрементальный счётчик».

Зато в Приложении А, раздел А.1 нашлась фраза: «Передача начинается с младшего байта». Это Little Endian. Формально пульт был прав.

Но сервер был написан под Big Endian, и все наши тесты (заглушка, интеграционные тесты) тоже использовали Big Endian. Выкатили фикс, поменяли на Little и всё заработало.

Что я вынес

В очередной раз убедился, что в тупиковых ситуациях нужно десять раз перечитать логи, даже если они не показывают явных ошибок. Именно строка с gap равным 16777215 стала поворотной точкой всего расследования, и если бы сервер логировал только факт потери пакета без конкретного числа, я бы ковырялся в hex-дампах ещё очень долго.

Ещё раз подтвердилось, что собственная заглушка в таких проектах незаменима. Когда у тебя есть эталонный клиент, который гарантированно работает, любая проблема с реальным устройством сразу локализуется, достаточно сравнить логи двух подключений и посмотреть, где расходятся данные. В моём случае разница в порядке байтов стала очевидной буквально за секунду, когда я поставил рядом два лога и увидел, что счётчики отличаются на порядки.

Создано при помощи Hugo
Тема Stack, дизайн Jimmy