<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Debugging on Igor Plotnikov</title><link>https://iplotnikov.com/tags/debugging/</link><description>Recent content in Debugging on Igor Plotnikov</description><generator>Hugo -- gohugo.io</generator><language>ru-RU</language><atom:link href="https://iplotnikov.com/tags/debugging/index.xml" rel="self" type="application/rss+xml"/><item><title>Как я искал звук в системе оповещения</title><link>https://iplotnikov.com/p/%D0%BA%D0%B0%D0%BA-%D1%8F-%D0%B8%D1%81%D0%BA%D0%B0%D0%BB-%D0%B7%D0%B2%D1%83%D0%BA-%D0%B2-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B5-%D0%BE%D0%BF%D0%BE%D0%B2%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D1%8F/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://iplotnikov.com/p/%D0%BA%D0%B0%D0%BA-%D1%8F-%D0%B8%D1%81%D0%BA%D0%B0%D0%BB-%D0%B7%D0%B2%D1%83%D0%BA-%D0%B2-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B5-%D0%BE%D0%BF%D0%BE%D0%B2%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D1%8F/</guid><description>&lt;img src="https://iplotnikov.com/p/%D0%BA%D0%B0%D0%BA-%D1%8F-%D0%B8%D1%81%D0%BA%D0%B0%D0%BB-%D0%B7%D0%B2%D1%83%D0%BA-%D0%B2-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B5-%D0%BE%D0%BF%D0%BE%D0%B2%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D1%8F/thumb.png" alt="Featured image of post Как я искал звук в системе оповещения" /&gt;&lt;h1 id=""&gt;&#10;&lt;/h1&gt;&lt;p&gt;Эта история про то, как два устройства говорили на одном протоколе, отправляли друг другу одни и те же данные, и всё равно не могли друг друга понять из-за четырёх байтов, записанных в другом порядке.&lt;/p&gt;&#10;&lt;h2 id="контекст"&gt;Контекст&#10;&lt;/h2&gt;&lt;p&gt;Я работал над системой оповещения, которая в случае ЧС включает сирены, отправляет голосовые сообщения и текстовые бегущие строки на оконечные устройства. Всё по ГОСТу 42.3.05-2023, с чёткими форматами пакетов, квитанциями и таймингами.&lt;/p&gt;&#10;&lt;p&gt;Архитектура: пульт оператора отправляет команды по TCP, а звук по UDP. На другом конце сервер принимает команды, запускает оповещение и ретранслирует звук на устройства. Между ними бинарный протокол с 8-байтными пакетами и аудио-датаграммами по 84 байта.&lt;/p&gt;&#10;&lt;p&gt;У меня была задача написать заглушку (эмулятор пульта), покрыть всё интеграционными тестами и добиться сквозной работы с реальным пультом от другого производителя.&lt;/p&gt;&#10;&lt;h2 id="заглушка-работает-реальный-пульт-нет"&gt;Заглушка работает, реальный пульт нет&#10;&lt;/h2&gt;&lt;p&gt;Первое что я сделал это написал заглушку на Node.js. Интерактивное меню, все команды по ГОСТу, UDP-поток с аудио, подключился к тестовому серверу, прогнал полный сценарий, квитанции приходят, звук записывается в файл, файл воспроизводится.&lt;/p&gt;&#10;&lt;p&gt;Потом подключили реальный пульт. TCP-команды ок. Квитанции ок. Запуск оповещения ок. А вот звука нет, файл вроде как создаётся, но внутри какой-то монотонный фон и щелчок в конце. Хотя поставщики пультов говорят что отправляли тестовое голосовое оповещение&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="первые-подозрения"&gt;Первые подозрения&#10;&lt;/h2&gt;&lt;p&gt;Сначала я подумал, что проблема в сети. Пульт стоял за NATом, подключался через внешний IP. TCP проходил, а UDP мог и не доходить. Подключился к серваку проверил &lt;code&gt;tcpdump&lt;/code&gt; , он показал, что UDP-пакеты приходят, 84 байта, правильный порт.&lt;/p&gt;&#10;&lt;p&gt;Сервер принимал UDP-пакеты и логировал счётчик каждого. По ГОСТу формат датаграммы такой:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Байты 0–3: Счётчик пакетов (32-битное целое)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Байты 4–83: Звуковые данные (80 байт, A-Law, 8 кГц)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Всего: 84 байта&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;Счётчик должен начинаться с нуля и увеличиваться на единицу с каждым пакетом: 0, 1, 2, 3&amp;hellip; Сервер использует его для обнаружения потерь и восстановления порядка.&lt;/p&gt;&#10;&lt;p&gt;Я посмотрел логи и увидел:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expected=0, received=838860801, gap=838860801&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expected=838860802, received=855638016, gap=16777215&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expected=855638017, received=872415232, gap=16777215&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;Вместо 0, 1, 2, 3 сервер видел 838860801, 855638016, 872415232. И разрыв между пакетами стабильные 16 777 215 вместо единицы.&lt;/p&gt;&#10;&lt;h2 id="16-777-215"&gt;16 777 215&#10;&lt;/h2&gt;&lt;p&gt;16 777 215 в шестнадцатеричной записи это &lt;code&gt;0x00FFFFFF&lt;/code&gt;. Ровно три байта, заполненных единицами, уже звоночек&lt;/p&gt;&#10;&lt;p&gt;Я перевёл первый счётчик в hex:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;838860801 = 0x32000001&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;Разложил по байтам: &lt;code&gt;[0x32, 0x00, 0x00, 0x01]&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;А потом перевернул: &lt;code&gt;[0x01, 0x00, 0x00, 0x32]&lt;/code&gt; и получил &lt;code&gt;0x01000032&lt;/code&gt; = число 50 в десятичной. Следующий пакет 51, потом 52, 53&amp;hellip; Счётчик оказывается шёл нормально, но в другом порядке байтов.&lt;/p&gt;&#10;&lt;h2 id="big-endian-vs-little-endian"&gt;Big Endian vs Little Endian&#10;&lt;/h2&gt;&lt;p&gt;Одно и то же 32-битное число можно записать двумя способами:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Big Endian&lt;/strong&gt; старший байт первый. Число 1 записывается как &lt;code&gt;[0x00, 0x00, 0x00, 0x01]&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Little Endian&lt;/strong&gt; младший байт первый. Число 1 записывается как &lt;code&gt;[0x01, 0x00, 0x00, 0x00]&lt;/code&gt;. Как Интел процессоры хранят данные в памяти.&lt;/p&gt;&#10;&lt;p&gt;Наш сервер читал счётчик как Big Endian. Пульт отправлял как Little Endian, и вместо нормального счётчика 50, 51, 52 сервер видел 838860801, 855638016, 872415232.&lt;/p&gt;&#10;&lt;h2 id="а-при-чём-тут-звук"&gt;А при чём тут звук?&#10;&lt;/h2&gt;&lt;p&gt;Сам по себе перевёрнутый счётчик не катастрофа, он логировал ошибки, но пакеты всё равно принимал, звуковые данные (байты 4–83) лежали на правильных позициях.&lt;/p&gt;&#10;&lt;p&gt;С мусорными значениями счётчика он решал, что пакеты приходят в неправильном порядке, пытался их переставить, и в результате аудио-поток рассыпался. На выходе данные есть, но перемешаны так, что декодер A-Law выдаёт постоянный уровень сигнала вместо речи, и получаем тишину с щелчком.&lt;/p&gt;&#10;&lt;h2 id="кто-прав-по-госту"&gt;Кто прав по ГОСТу?&#10;&lt;/h2&gt;&lt;p&gt;Я полез в стандарт искать, какой порядок байтов правильный. В пункте 5.9.3.2 который описывает формат датаграммы, порядок байтов не указывается явно. Просто указано «32-битный инкрементальный счётчик».&lt;/p&gt;&#10;&lt;p&gt;Зато в Приложении А, раздел А.1 нашлась фраза: «Передача начинается с младшего байта». Это &lt;strong&gt;Little Endian&lt;/strong&gt;. Формально пульт был прав.&lt;/p&gt;&#10;&lt;p&gt;Но сервер был написан под Big Endian, и все наши тесты (заглушка, интеграционные тесты) тоже использовали Big Endian. Выкатили фикс, поменяли на Little и всё заработало.&lt;/p&gt;&#10;&lt;h2 id="что-я-вынес"&gt;Что я вынес&#10;&lt;/h2&gt;&lt;p&gt;В очередной раз убедился, что в тупиковых ситуациях нужно десять раз перечитать логи, даже если они не показывают явных ошибок. Именно строка с gap равным 16777215 стала поворотной точкой всего расследования, и если бы сервер логировал только факт потери пакета без конкретного числа, я бы ковырялся в hex-дампах ещё очень долго.&lt;/p&gt;&#10;&lt;p&gt;Ещё раз подтвердилось, что собственная заглушка в таких проектах незаменима. Когда у тебя есть эталонный клиент, который гарантированно работает, любая проблема с реальным устройством сразу локализуется, достаточно сравнить логи двух подключений и посмотреть, где расходятся данные. В моём случае разница в порядке байтов стала очевидной буквально за секунду, когда я поставил рядом два лога и увидел, что счётчики отличаются на порядки.&lt;/p&gt;&#10;</description></item></channel></rss>