Перейти к содержимому
k16

k16. Протоколы и интерфейсы: слои связи «для дронов»

k16. Протоколы и интерфейсы: слои связи «для дронов»

UAV Python Pro · Конспект K16 · Модуль M3. Протоколы и интерфейсы: слои связи «для дронов» · Редакция 1.0 · После CHECKPOINT-K15 · Модуль M2 закрыт

Модуль M2 завершён: вы умеете вычислять расстояния, направления и ориентацию — математический язык мира дронов освоен. Но числа должны дойти от аппарата до земли и обратно, и на этом пути работает целая цепочка соглашений, невидимая до тех пор, пока что-нибудь не сломается. Этим уроком начинается модуль M3 «Протоколы и интерфейсы». Его первый конспект отвечает на три вопроса: из каких слоёв состоит связь, где среди них живёт MAVLink и какие характеристики канала — задержка, джиттер, потери, пропускная способность — обязан контролировать программист наземной станции.

Протокол — это соглашение между двумя программами о том, как понимать передаваемые байты. Соглашение устроено слоями: физический уровень передаёт биты по проводу или радиоволне, транспорт доставляет блоки байтов, кадр выделяет сообщения внутри потока и проверяет их целостность контрольной суммой, сообщение несёт осмысленные поля, а приложение решает, что делать с полученным. MAVLink занимает ровно два средних слоя — кадр и сообщение — и не заботится о том, что течёт под ним: serial-поток, UDP-датаграммы или USB. Понимать эту структуру означает уметь быстро определить, на каком именно уровне возникла проблема со связью.

Содержание

1. Цели урока

После изучения этого урока вы будете уметь выполнять следующие действия.

  • Вы объясните, что такое протокол и почему соглашения о связи описывают слоями.
  • Вы назовёте пять слоёв модели связи «для дронов» и вопрос, на который отвечает каждый слой.
  • Вы отличите потоковую доставку байтов (serial-порт) от датаграммной (UDP) и объясните следствия для поиска границ сообщений.
  • Вы объясните назначение каждой части кадра: синхробайт, длина, порядковый номер, адрес отправителя, идентификатор сообщения, полезная нагрузка и контрольная сумма.
  • Вы объясните, что такое сериализация, и назовёте поля сообщения HEARTBEAT на уровне общего понимания.
  • Вы вычислите процент потерь, среднюю задержку и джиттер канала по формулам и интерпретируете результат вердиктом OK / DEGRADED / UNUSABLE.
  • Вы примените слоёную модель к диагностике: определите, на каком уровне искать проблему «телеметрия не отображается».
Связь с разработкой программного обеспечения для БПЛА. Модуль M3 идёт снизу вверх: K17 разберёт UART и serial-порты, K18 — UDP и TCP-сокеты, K19 — шины датчиков I2C и SPI, K20 — CAN и DroneCAN, K21 — ручной разбор бинарных кадров и контрольные суммы, а лабораторная работа K22 соберёт всё вместе: два процесса, передатчик фейковой телеметрии и приёмник-разборщик. После K22 сдаётся гейт G2. Этот урок даёт карту, на которую лягут все детали модуля.
↑ К оглавлению

2. Словарь урока

ТерминПростыми словами
Протокол Соглашение между двумя сторонами о формате и смысле передаваемых данных.
Слоёная модель Разделение связи на уровни: каждый слой решает свою задачу и пользуется услугами нижнего.
Физический уровень Способ передачи битов: электричество по проводам, радиоволны, свет.
Поток (stream) Доставка байтов непрерывной чередой без встроенных границ сообщений — так работает serial-порт и TCP.
Датаграмма Доставка самостоятельного блока байтов с сохранёнными границами — так работает UDP.
Кадр (frame) Структурированный блок байтов: начало, длина, содержимое и проверка целостности.
Полезная нагрузка (payload) Содержимое кадра без служебных полей — собственно данные сообщения.
Контрольная сумма (checksum, CRC) Короткое число, вычисленное из содержимого: не совпало при пересчёте — кадр повреждён и отбрасывается.
Сообщение Единица смысла: идентификатор плюс набор типизированных полей (например, HEARTBEAT).
Сериализация Превращение структуры данных программы в байты для передачи и обратно.
Задержка (latency) Время между отправкой и получением данных, миллисекунды.
Джиттер (jitter) Нестабильность задержки: насколько сильно время доставки прыгает от пакета к пакету.
Потери (packet loss) Доля отправленных пакетов, которые не дошли до получателя, в процентах.
Пропускная способность Сколько данных канал способен передать за секунду: биты или байты в секунду.
↑ К оглавлению

3. Зачем нужна слоёная модель: диагностика связи

Рассмотрим рабочую ситуацию. Вы запустили наземную станцию, подключились к аппарату — и телеметрия на экране не появляется. Начинающий разработчик в такой ситуации перебирает действия наугад: перезапускает программу, перетыкает кабель, меняет настройки. Инженер, который держит в голове слоёную модель, проверяет слои по порядку снизу вверх и на каждом слое задаёт свой вопрос.

  • Физический уровень и транспорт: кабель вставлен, порт существует, программа открыла именно тот порт? Байты из порта вообще читаются — пусть даже бессмысленные?
  • Кадры: удаётся ли в потоке байтов находить начала кадров, сходятся ли длины и контрольные суммы? Если кадры рассыпаются, проблема в скорости порта (baudrate) или в помехах.
  • Сообщения: приходят ли сообщения нужных типов — например, HEARTBEAT? Возможно, автопилот отправляет телеметрию на другой адрес или с другой частотой.
  • Приложение: не отфильтровала ли сама программа те сообщения, которые успешно дошли? Не ждёт ли она другого идентификатора системы?

Каждый слой имеет собственный инструмент проверки, и каждый инструмент вы напишете своими руками в этом модуле: чтение байтов из порта — в K17, UDP-обмен — в K18, разбор кадра и контрольная сумма — в K21, фильтрация сообщений — в модуле M5. Слоёная модель превращает диагностику из гадания в короткий перебор четырёх вопросов.

Откуда взялась эта модель. В учебниках сети описывают семиуровневой моделью OSI и четырёхуровневой TCP/IP. Для работы с беспилотником полная академическая схема избыточна: нам достаточно пяти слоёв, каждый из которых можно потрогать руками — провод, байты, кадр, сообщение, программа. Все термины этого урока соответствуют своим «взрослым» аналогам, но сгруппированы практично.
↑ К оглавлению

4. Пять слоёв связи «для дронов»

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

5. Приложение     что делать со знанием: Mission Planner, ваш скрипт
4. Сообщения      о чём именно речь: HEARTBEAT, ATTITUDE, COMMAND_LONG
3. Кадры          где границы и цело ли: синхробайт, длина, CRC
2. Транспорт      как доставляются блоки байтов: поток или датаграммы
1. Физический     как передаются биты: провод, радиоволна, оптика
Plain text

Бытовая аналогия — письмо. Физический уровень — дороги и транспорт: грузовик или самолёт, по которым едет почта. Транспорт — почтовая служба: она перевозит пачки корреспонденции и не читает содержимое. Кадр — конверт: на нём написан адрес, а печать позволяет получателю убедиться, что конверт не повреждён. Сообщение — текст письма, написанный на понятном обеим сторонам языке. Приложение — то решение, которое получатель примет после прочтения. Аналогия полезна, но помните, где она заканчивается: байты можно скопировать без износа, а «печать» кадра — контрольная сумма — проверяется автоматически и мгновенно.

Зафиксируем ту же модель в виде структуры данных — приёма, который пригодится, когда вы будете писать свой мониторинг связи:

Чистый код

# Пример: слоёная модель связи как словарь программы
layers = {
    "physical": "провод UART, радиоканал или Ethernet",
    "transport": "поток байтов serial или датаграммы UDP",
    "frame": "кадр MAVLink: границы и контрольная сумма",
    "message": "HEARTBEAT, ATTITUDE, COMMAND_LONG",
    "application": "Mission Planner или скрипт на companion",
}

for name, description in layers.items():
    print(f"{name}: {description}")
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    layers = {
        "physical": "провод UART, радиоканал или Ethernet",
        "transport": "поток байтов serial или датаграммы UDP",
        "frame": "кадр MAVLink: границы и контрольная сумма",
        "message": "HEARTBEAT, ATTITUDE, COMMAND_LONG",
        "application": "Mission Planner или скрипт на companion",
    }
    # → Словарь из урока K04: ключ — имя слоя (нижний слой первый),
    # → значение — короткое человеческое описание. Порядок вставки
    # → сохраняется, поэтому обход снизу вверх читается как модель.
    
    for name, description in layers.items():
        print(f"{name}: {description}")
    # → Метод items() даёт пары ключ-значение; f-строка печатает
    # → каждую пару как строку мини-справочника.
    Python

Главное свойство слоёной модели: каждый слой можно заменить, не трогая остальные. Та же самая телеметрия MAVLink летит по проводу UART в бортовой сети и по UDP в симуляторе SITL — потому что MAVLink живёт на двух верхних из средних слоёв и не зависит от того, что находится под ним. Это утверждение станет итоговой картой раздела 10.

↑ К оглавлению

5. Физический уровень и транспорт в мире БПЛА

Физический уровень отвечает на вопрос «как биты превращаются в физический процесс»: в электрические уровни напряжения на двух проводах UART, в радиоволны диапазона 433 или 915 МГц, в сигналы Ethernet и Wi-Fi. Его характеристики — разъёмы, уровни напряжений, частоты и скорость передачи в битах в секунду. Ошибка на этом уровне выглядит так: провод перепутан местами, скорость порта не совпала, радиоканал зашумлён.

Транспортный слой отвечает на вопрос «как блоки байтов доставляются от отправителя к получателю». Здесь важна не скорость, а форма доставки, и форм ровно две. Поток (stream) — непрерывная череда байтов без встроенных границ: так работают serial-порт и TCP. Получатель видит «реку» байтов и обязан сам искать в ней начала сообщений — эту задачу решает кадр из раздела 6. Датаграмма — самостоятельный блок с сохранёнными границами: так работает UDP. Каждый отправленный блок приходит отдельным пакетом (или не приходит вовсе), но если пришёл — его границы сохранены. Таблица ниже собирает каналы, которые встретятся в курсе.

КаналГде применяется в системе БПЛАФорма доставкиУрок
UART (serial) Бортовой компьютер-компаньон ↔ полётный контроллер; телеметрийные радиомодемы поток байтов K17
USB Наземный компьютер ↔ полётный контроллер: прошивка, калибровки, первый запуск поток (виден системе как виртуальный COM-порт) K17
UDP Симулятор SITL (порт 14550), телеметрия по Wi-Fi датаграммы K18
TCP Соединения наземных станций, сетевые мосты телеметрии поток поверх надёжного соединения K18
I2C, SPI Компаньон ↔ низкоскоростные датчики и периферия: OLED-экраны, АЦП, внешние компасы обмен байтами по шине K19
CAN, DroneCAN «Умная» периферия: GPS-приёмники, модули питания, сервоприводы сообщения по общей шине K20
Заметьте разницу масштабов. UART, USB, UDP и TCP соединяют компьютеры и контроллеры — по ним поедет MAVLink. I2C, SPI и CAN соединяют микросхемы внутри устройств — MAVLink по ним обычно не ходит, но знать их устройство программисту БПЛА необходимо: датчики и периферия подключаются именно так.
↑ К оглавлению

6. Кадр: границы сообщения и целостность

Допустим, транспорт доставляет байты. В потоке serial-порта их «река» не имеет ни пауз, ни отметок: получатель видит числа одно за другим и не знает, где начинается и заканчивается осмысленный блок. Эту задачу решает кадр — заранее согласованная структура блока байтов. Любая кадровая структура, включая MAVLink, содержит примерно одни и те же части, каждая из которых отвечает на свой вопрос.

Часть кадраВопрос, на который она отвечает
Синхробайт (стартовый маркер) Где начинается кадр? Получатель ищет в потоке особое число, с которого кадр обязан начинаться.
Длина Сколько байтов полезной нагрузки внутри? Зная длину, получатель понимает, где кадр заканчивается.
Порядковый номер (sequence) Не было ли пропусков? Номера идут подряд; разрыв номеров означает потерю кадров в канале.
Адрес отправителя Кто прислал кадр? В MAVLink это идентификаторы системы и компонента.
Идентификатор сообщения О чём этот кадр? Число, по которому получатель выбирает схему разбора полей.
Полезная нагрузка (payload) Собственно данные: поля сообщения в оговорённом порядке и размерах.
Контрольная сумма (checksum, CRC) Не исказился ли кадр в пути? Отправитель вычисляет короткое число из содержимого, получатель пересчитывает его сам; не совпало — кадр отбрасывается молча.

Контрольная сумма заслуживает отдельного пояснения, потому что именно она отделяет данные от шума. Радиоканал вносит искажения: отдельный бит может «перевернуться», и число в полезной нагрузке изменится. На глаз отличить искажённое значение от настоящего невозможно — оба выглядят правдоподобно. Контрольная сумма делает искажение видимым: пересчитанное значение не совпадёт с переданным, и приёмник отбросит кадр вместо того, чтобы поверить в ложные числа. Идею контрольных сумм вы соберёте руками на уроке K21, а точную анатомию кадра MAVLink 2 с шестнадцатеричным дампом настоящего HEARTBEAT разберёт урок K30.

↑ К оглавлению

7. Сообщение: идентификатор и поля

Полезная нагрузка кадра несёт сообщение — единицу смысла протокола. Сообщение состоит из идентификатора (какое это сообщение) и набора типизированных полей (какие именно числа внутри и что они означают). Превращение структуры данных программы в байты называется сериализацией, а обратный процесс — десериализацией; схема сообщения задаёт и порядок полей, и тип каждого поля, и единицы измерения.

Покажем на самом известном сообщении MAVLink — HEARTBEAT. Автопилот отправляет его примерно раз в секунду, и по одному факту приёма получатель понимает: система жива. Подробный разбор полей и работа с ними — уроки K29 и K32; сейчас достаточно общего знакомства.

ПолеЧто сообщает
type Класс аппарата: мультиротор, самолёт, VTOL, наземная станция
autopilot Семейство автопилота: ArduPilot, PX4 или «нет» для спутниковых компонентов
base_mode Флаги состояния: взведены ли моторы, включён ли режим управления от бортового компьютера
custom_mode Числовой идентификатор режима в нумерации автопилота: у ArduPilot это номера GUIDED, RTL, LOITER и других
system_status Стадия работы: ожидание, активный полёт, критическое состояние, отказ
mavlink_version Версия протокола MAVLink, которую использует отправитель

Обратите внимание на деталь, которая станет важной в модуле M5: единицы измерения и масштаб полей задаёт схема сообщения, а не здравый смысл. В одних сообщениях углы приходят в радианах, в других — в сантиградусах; координаты — в градусах, умноженных на 10⁷, как вы видели на уроке K15. Дисциплина одна: читать схему поля до того, как интерпретировать число.

↑ К оглавлению

8. Приложение: кто потребляет сообщения

Верхний слой — приложение: программа, ради которой вся передача и затевалась. В системе беспилотника приложений обычно несколько, и все они обмениваются одними и теми же сообщениями MAVLink.

  • Наземная станция управления (Mission Planner, QGroundControl) рисует карту, приборы и телеметрию, отправляет команды оператора и файлы миссий.
  • Бортовой компьютер-компаньон (наш будущий Raspberry Pi) выполняет «умные» задачи: сопровождение цели, облёт препятствий, передачу видео — и отправляет автопилоту команды верхнего уровня.
  • Сам автопилот — тоже потребитель: он принимает команды, параметры и миссии и отвечает телеметрией и подтверждениями.

Адресация на этом слое устроена просто: каждый участник обмена имеет идентификатор системы (sysid) — например, аппарат обычно получает номер 1, а наземная станция — 255, — и идентификатор компонента (compid) внутри системы: автопилот, камера, модуль питания. Получатель смотрит на адрес в кадре и решает, ему сообщение или другому участнику. По одному каналу телеметрию могут слушать несколько приложений одновременно — как это правильно организовать, расскажет урок K37.

Ваше место в этой картине. Весь курс готовит вас к роли автора прикладного слоя: скриптов наземной станции и сервисов на бортовом компьютере. Кадры и сообщения под вами будет разбирать библиотека pymavlink (модуль M5), но уверенность в работе библиотеки появляется только тогда, когда вы понимаете, что происходит на каждом слое ниже приложения.
↑ К оглавлению

9. Качество канала: задержка, джиттер, потери, пропускная способность

Канал связи характеризуется четырьмя числами, которые программист обязан уметь измерять и интерпретировать.

Первое — задержка (latency): время между отправкой данных и их получением, в миллисекундах. Различают одностороннюю задержку и время туда-обратно (RTT, round-trip time) — для команды с подтверждением важно второе. Задержка напрямую ограничивает управление: если команда «сместиться» доходит за полсекунды, аппарат будет «рыскать» вокруг цели, потому что контур управления реагирует на устаревшее состояние.

Второе — джиттер (jitter): нестабильность задержки. Канал со стабильными 100 миллисекундами часто лучше канала, у которого задержка прыгает от 20 до 200: предсказуемое запаздывание можно учесть, а случайные скачки ломают плавность управления. Учебная формула джиттера — средняя абсолютная разность соседних измерений задержки \(t_1, t_2, \ldots, t_n\):

\[ J = \frac{1}{n-1} \sum_{i=1}^{n-1} \left| t_{i+1} - t_i \right| \]

Третье — потери пакетов (packet loss): доля отправленных блоков, не дошедших до получателя. Если отправлено \(N_{sent}\) пакетов, а получено \(N_{recv}\), то процент потерь равен:

\[ L = \frac{N_{sent} - N_{recv}}{N_{sent}} \cdot 100\% \]

Небольшой процент потерь для радиоканала нормален, и протоколы это учитывают: телеметрия MAVLink передаётся периодическими потоками, и потерянный отсчёт просто заменится следующим через долю секунды. Опасны потери для команд: именно поэтому команды сопровождаются подтверждениями (COMMAND_ACK — разбор в модуле M5), а пропадание потока HEARTBEAT запускает защитную логику failsafe (урок K27).

Четвёртое — пропускная способность: сколько данных канал способен передать за секунду. Её измеряют в битах в секунду (бит/с) или в байтах в секунду (Б/с); между единицами перевод в восемь раз: 57600 бит/с — это примерно 7200 Б/с «грязными», а с учётом служебных полей кадров — примерно 4–6 КБ/с полезной телеметрии. Не путайте пропускную способность с задержкой: широкий канал не уменьшает время прохождения одного пакета.

Канал (типичный случай)ЗадержкаПропускная способностьПотери
UART 115200: компаньон ↔ полётный контроллер единицы миллисекунд и меньше около 11 КБ/с практически отсутствуют (провод)
Телеметрийный радиомодем 57600 бит/с десятки миллисекунд около 4–6 КБ/с доли процента и выше, зависят от дистанции и помех
UDP по Wi-Fi или локально к SITL единицы миллисекунд десятки МБ/с локально практически отсутствуют

Числа в таблице — ориентиры для интуиции, а не спецификация: реальные характеристики всегда измеряются на конкретном оборудовании. Именно этим занимается практика урока.

Чистый код

# Пример: метрики качества канала (фрагмент k16_task1_link_quality.py)
def loss_percent(sent: int, received: int) -> float:
    if sent <= 0:
        raise ValueError(f"число отправленных должно быть больше нуля: {sent}")
    if received < 0 or received > sent:
        raise ValueError(f"received должно быть от 0 до sent, получено: {received}")
    return (sent - received) / sent * 100.0

def jitter_ms(samples_ms) -> float:
    if len(samples_ms) < 2:
        raise ValueError("для джиттера нужно минимум два отсчёта")
    total = 0.0
    for i in range(1, len(samples_ms)):
        total += abs(samples_ms[i] - samples_ms[i - 1])
    return total / (len(samples_ms) - 1)
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    def loss_percent(sent: int, received: int) -> float:
    # → Чистая функция: отправили sent пакетов, дошло received —
    # → возвращает процент потерь по формуле из этого раздела.
    
        if sent <= 0:
            raise ValueError(f"число отправленных должно быть больше нуля: {sent}")
    # → Защита от деления на ноль: бессмысленный вход отклоняется
    # → исключением сразу (дисциплина уроков K08 и K12).
    
        if received < 0 or received > sent:
            raise ValueError(f"received должно быть от 0 до sent, получено: {received}")
    # → Получено не может быть больше, чем отправлено:
    # → такая комбинация означает ошибку учёта, а не чудо доставки.
    
        return (sent - received) / sent * 100.0
    # → Доля потерянного, умноженная на сто: ответ в процентах.
    
    def jitter_ms(samples_ms) -> float:
    # → На вход — список измерений задержки в миллисекундах,
    # → упорядоченный по времени прихода пакетов.
    
        if len(samples_ms) < 2:
            raise ValueError("для джиттера нужно минимум два отсчёта")
    # → Разность соседних отсчётов существует только при двух и более измерениях.
    
        total = 0.0
        for i in range(1, len(samples_ms)):
            total += abs(samples_ms[i] - samples_ms[i - 1])
    # → Цикл с урока K05: накапливаем абсолютные разности соседних отсчётов.
    # → abs убирает знак: важен размер скачка, а не его направление.
    
        return total / (len(samples_ms) - 1)
    # → Среднее арифметическое разностей — формула J из этого раздела.
    Python
↑ К оглавлению

Соберём картину в один рисунок. MAVLink занимает два слоя — кадры и сообщения; всё, что ниже, он оставляет на усмотрение системы, всё, что выше, — на усмотрение программиста.

Приложение    Mission Planner, скрипты на companion, логика автопилота
                                    ← ваш будущий Python
Сообщения     HEARTBEAT, ATTITUDE, COMMAND_LONG, GLOBAL_POSITION_INT
                                    ← MAVLink
Кадры         синхробайт, длина, seq, sysid/compid, msgid, payload, CRC
                                    ← MAVLink
Транспорт     serial-поток / UDP-датаграммы / TCP / USB
                                    ← уроки K17–K18
Физический    UART-провода, радиоканал модема, Ethernet, Wi-Fi
                                    ← уроки K17–K20
Plain text

Отсюда следует ключевое свойство MAVLink, которое вы увидите в модулях M4 и M5: транспортная независимость. Симулятор SITL отдаёт телеметрию датаграммами UDP на порт 14550, реальный полётный контроллер — потоком байтов по UART или USB, а сообщения и кадры при этом одинаковы. Поэтому библиотека pymavlink подключается к любому каналу одной строкой соединения: в модуле M5 вы будете писать и udp:127.0.0.1:14550 для симулятора, и имя COM-порта для настоящего контроллера — и код выше строки подключения меняться не будет.

Проверка понимания. Вернитесь к диагностической ситуации раздела 3 и сформулируйте её теперь на языке карты: «телеметрия не отображается» означает, что где-то в цепочке из пяти строк рисунка произошёл разрыв. Программа диагностики — это обход строк снизу вверх, и для каждой строки у вас появится свой инструмент в уроках K17–K22.
↑ К оглавлению

11. Типичные заблуждения

ЗаблуждениеПочему оно опасноКак правильно
«Протокол — это разъём или кабель» Подмена уровней: один и тот же MAVLink работает и по проводу, и по радиоканалу, и по UDP Протокол — соглашение о кадрах и сообщениях; физика и транспорт выбираются отдельно
«Байты из порта приходят готовыми сообщениями» Serial — поток без границ: сообщение может прийти по частям или склеиться с соседним Границы ищет кадровый уровень: синхробайт, длина, контрольная сумма
«Данные пришли — значит, они верные» Искажённый бит меняет число незаметно: ложная телеметрия опаснее отсутствующей Кадр без совпавшей контрольной суммы отбрасывается молча
«Потери пакетов — признак сломанной связи» Небольшой процент потерь нормален для радиоканала; паника ведёт к неверным решениям Следите за порогами: телеметрия переживёт потерю отсчёта, команды требуют подтверждений
«Маленькая задержка важнее стабильности» Канал 20–200 мс вразброс ломает плавность управления сильнее, чем ровные 100 мс Измеряйте и среднюю задержку, и джиттер; для контуров управления стабильность важнее
«Широкий канал означает маленькую задержку» Пропускная способность и задержка — разные характеристики: толстая труба не ускоряет первый байт Для команд важна задержка, для видео и логов — пропускная способность
↑ К оглавлению

12. Практика

Урок теоретический, поэтому практика компактная: один модуль метрик качества канала и один файл тестов. Модуль превращает формулы раздела 9 в чистые функции — потерю, среднюю задержку, джиттер и сводный вердикт, — а тесты закрепляют их поведение, включая защиту от бессмысленных входных данных. Новых пакетов не требуется: только стандартная библиотека и установленный pytest. Порядок работы: создайте файлы задач 16.1 и 16.2 в каталоге code/M01_python, выполните прогоны (задача 16.3) и сохраните исходники в Git (задача 16.4).

Задача 16.1. Метрики качества канала

Файл практики: k16_task1_link_quality.py.

Создайте модуль метрик: чистые функции loss_percent() с защитой от бессмысленных входов, mean_ms() для средней задержки, jitter_ms() для нестабильности задержки и сводный вердикт link_verdict() с тремя уровнями: OK, DEGRADED, UNUSABLE. Демо разбирает три сценария состояния канала.

Ожидаемый результат. Демо печатает вердикт OK для канала без потерь с задержкой 4.8 мс, DEGRADED для радиомодема с потерями 4 % и UNUSABLE для канала с потерями 25 %.

  • Полный текст файла k16_task1_link_quality.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k16_task1_link_quality.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k16_task1_link_quality.py
    # Цель: чистые функции для метрик качества канала связи —
    #       потери, средняя задержка, джиттер и сводный вердикт.
    # Запуск из каталога code/M01_python:
    #   python k16_task1_link_quality.py
    
    
    def loss_percent(sent: int, received: int) -> float:
        """Процент потерянных пакетов: сколько из отправленных не дошло."""
        if sent <= 0:
            raise ValueError(f"число отправленных должно быть больше нуля: {sent}")
        if received < 0 or received > sent:
            raise ValueError(f"received должно быть от 0 до sent, получено: {received}")
        return (sent - received) / sent * 100.0
    
    
    def mean_ms(samples_ms) -> float:
        """Среднее значение задержки по списку отсчётов, миллисекунды."""
        if len(samples_ms) == 0:
            raise ValueError("пустой список отсчётов: среднее не определено")
        return sum(samples_ms) / len(samples_ms)
    
    
    def jitter_ms(samples_ms) -> float:
        """Джиттер: средняя абсолютная разность соседних отсчётов задержки, мс.
    
        Характеризует не среднюю величину задержки, а её нестабильность.
        """
        if len(samples_ms) < 2:
            raise ValueError("для джиттера нужно минимум два отсчёта")
        total = 0.0
        for i in range(1, len(samples_ms)):
            total += abs(samples_ms[i] - samples_ms[i - 1])
        return total / (len(samples_ms) - 1)
    
    
    def link_verdict(loss_pct: float, mean_latency_ms: float,
                     max_loss_pct: float = 5.0,
                     max_latency_ms: float = 150.0) -> str:
        """Сводный вердикт качества канала: 'OK', 'DEGRADED' или 'UNUSABLE'.
    
        Учебные пороги по умолчанию: потери более max_loss_pct процентов
        или средняя задержка более max_latency_ms миллисекунд — канал
        непригоден; больше половины от любого порога — деградация.
        """
        if loss_pct > max_loss_pct or mean_latency_ms > max_latency_ms:
            return "UNUSABLE"
        if loss_pct > max_loss_pct / 2.0 or mean_latency_ms > max_latency_ms / 2.0:
            return "DEGRADED"
        return "OK"
    
    
    if __name__ == "__main__":
        scenarios = (
            ("хороший канал (SITL по Wi-Fi)", [4.0, 5.0, 4.0, 6.0, 5.0], 1000, 1000),
            ("радиомодем, умеренные помехи", [80.0, 120.0, 95.0, 140.0, 85.0, 110.0], 500, 480),
            ("канал с большими потерями", [90.0, 100.0], 200, 150),
        )
        for title, samples, sent, received in scenarios:
            loss = loss_percent(sent, received)
            average = mean_ms(samples)
            jitter = jitter_ms(samples)
            verdict = link_verdict(loss, average)
            print(
                f"{title}: потери {loss:5.1f} %, средняя задержка {average:6.1f} мс, "
                f"джиттер {jitter:5.1f} мс -> {verdict}"
            )
    Python

Задача 16.2. Тесты метрик

Файл практики: k16_task2_test_link_quality.py.

Создайте файл тестов: четыре проверки потерь (включая отказ при нуле отправленных и при received больше sent), две проверки среднего, три проверки джиттера и четыре проверки вердикта — всего тринадцать тестов с допусками pytest.approx.

Ожидаемый результат. Команда python -m pytest k16_task2_test_link_quality.py -v показывает тринадцать строк PASSED и итог «13 passed».

  • Полный текст файла k16_task2_test_link_quality.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k16_task2_test_link_quality.py в проекте PyCharm (виртуальное окружение) и запустите тесты командой python -m pytest k16_task2_test_link_quality.py -v из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k16_task2_test_link_quality.py
    # Тесты метрик качества канала K16: потери, задержка, джиттер, вердикт.
    # Запуск из каталога code/M01_python:
    #   python -m pytest k16_task2_test_link_quality.py -v
    
    import pytest
    
    from k16_task1_link_quality import jitter_ms, link_verdict, loss_percent, mean_ms
    
    
    # --- потери ------------------------------------------------------------------
    
    
    def test_loss_percent_basic():
        assert loss_percent(100, 95) == pytest.approx(5.0)
    
    
    def test_loss_percent_zero_loss():
        assert loss_percent(10, 10) == pytest.approx(0.0)
    
    
    def test_loss_percent_rejects_zero_sent():
        with pytest.raises(ValueError):
            loss_percent(0, 0)
    
    
    def test_loss_percent_rejects_received_more_than_sent():
        with pytest.raises(ValueError):
            loss_percent(10, 11)
    
    
    # --- средняя задержка ---------------------------------------------------------
    
    
    def test_mean_ms():
        assert mean_ms([10.0, 20.0, 30.0]) == pytest.approx(20.0)
    
    
    def test_mean_ms_rejects_empty():
        with pytest.raises(ValueError):
            mean_ms([])
    
    
    # --- джиттер -------------------------------------------------------------------
    
    
    def test_jitter_zero_for_stable_link():
        assert jitter_ms([50.0, 50.0, 50.0]) == pytest.approx(0.0)
    
    
    def test_jitter_alternating():
        assert jitter_ms([10.0, 20.0, 10.0, 20.0]) == pytest.approx(10.0)
    
    
    def test_jitter_rejects_single_sample():
        with pytest.raises(ValueError):
            jitter_ms([42.0])
    
    
    # --- сводный вердикт ------------------------------------------------------------
    
    
    def test_verdict_ok():
        assert link_verdict(0.0, 20.0) == "OK"
    
    
    def test_verdict_degraded():
        assert link_verdict(4.0, 60.0) == "DEGRADED"
    
    
    def test_verdict_unusable_on_loss():
        assert link_verdict(10.0, 20.0) == "UNUSABLE"
    
    
    def test_verdict_unusable_on_latency():
        assert link_verdict(0.0, 400.0) == "UNUSABLE"
    Python

Задача 16.3. Прогоны демо и тестов (обязательно выполнить)

Выполните команды по порядку из каталога code/M01_python в терминале с активным виртуальным окружением.

# Каталог: code/M01_python (venv активен, новых пакетов не требуется)
python k16_task1_link_quality.py
python -m pytest k16_task2_test_link_quality.py -v
Plain text

Ожидаемый результат. Демо печатает три сценария качества канала:

хороший канал (SITL по Wi-Fi): потери   0.0 %, средняя задержка    4.8 мс, джиттер   1.2 мс -> OK
радиомодем, умеренные помехи: потери   4.0 %, средняя задержка  105.0 мс, джиттер  38.0 мс -> DEGRADED
канал с большими потерями: потери  25.0 %, средняя задержка   95.0 мс, джиттер  10.0 мс -> UNUSABLE

tests: 13 passed
Plain text

Обратите внимание на третий сценарий: средняя задержка в нём приемлема, но вердикт вынесен по потерям. Сводный вердикт обязан учитывать обе метрики — в этом и состоит идея многосторонней оценки канала.

Задача 16.4. Сохранение в Git

Добавьте в репозиторий оба файла урока и сделайте коммит — первый в модуле M3.

# Из корня репозитория курса
git add code/M01_python/k16_task1_link_quality.py code/M01_python/k16_task2_test_link_quality.py
git commit -m "K16: link quality metrics - loss, latency, jitter, verdict; 13 tests"
Plain text

Ожидаемый результат. Команда git status показывает чистое рабочее дерево по файлам k16_*, в истории появился первый коммит модуля M3.

↑ К оглавлению

13. Проверьте себя

  1. Что такое протокол и почему соглашения о связи описывают слоями, а не одним текстом?
  2. Перечислите пять слоёв модели «для дронов» и вопрос, на который отвечает каждый слой.
  3. Чем потоковая доставка байтов отличается от датаграммной и какое следствие это имеет для поиска границ сообщений?
  4. Какую задачу решает каждая часть кадра: синхробайт, длина, порядковый номер, адрес, идентификатор сообщения, полезная нагрузка, контрольная сумма?
  5. Что такое сериализация и десериализация сообщений?
  6. Какие два слоя занимает MAVLink и почему он не зависит от того, что находится ниже?
  7. Чем задержка отличается от джиттера и почему стабильные 100 мс бывают лучше прыгающих 20–200 мс?
  8. Как вычисляется процент потерь и почему небольшой процент потерь нормален для радиоканала?
  9. Чем пропускная способность отличается от задержки? Приведите задачу, где критична каждая из них.
  10. Телеметрия не отображается в программе. Опишите порядок проверки по слоям снизу вверх.
↑ К оглавлению

14. Чек-лист самопроверки

  • Я могу назвать пять слоёв модели связи и вопрос каждого слоя без подсказки.
  • Я объясняю разницу потока и датаграммы на примере serial-порта и UDP.
  • Я объясняю назначение каждой части кадра и роль контрольной суммы.
  • Я могу пересказать, из чего состоит сообщение, на примере HEARTBEAT.
  • Я знаю, где MAVLink живёт в слоёной модели и почему он транспортно-независим.
  • Я создал оба файла практики в каталоге code/M01_python.
  • Демо напечатало вердикты OK, DEGRADED и UNUSABLE для трёх сценариев, и я могу объяснить каждый вердикт.
  • Команда python -m pytest k16_task2_test_link_quality.py -v завершается результатом 13 passed.
  • Я сделал git commit с файлами урока K16.
Результат. Когда все пункты закрыты, напишите ментору: K16 чек-лист закрыт. Следующий урок — K17 «UART / Serial / USB-UART»: первый взгляд на физический уровень и транспорт, первое чтение настоящих байтов из порта.
↑ К оглавлению

15. Что дальше

  • Предыдущий урок: K15 · Геокоординаты: lat/lon/alt, haversine, bearing (модуль M2 завершён)
  • Текущий урок: K16 · Слои связи «для дронов» — начало модуля M3 «Протоколы и интерфейсы»
  • Следующий урок: K17 · UART / Serial / USB-UART: TX/RX/GND, baudrate, COM-порты Windows, библиотека pyserial
  • Модуль M3 завершится лабораторной работой K22 (два процесса: передатчик фейковой телеметрии и приёмник-разборщик), после которой сдаётся гейт G2.
↑ К оглавлению

 

 

Вторник, 10 ноября 2026
k16. Протоколы и интерфейсы: слои связи «для дронов»