Embedded HandbookПриложения
Все разделы › XXIII. Приложение к прибору: Dart и Flutter
STM32F103, STM32F407, STM32G431, STM32L431 · уровень средний · 7 мин чтения
Открыть в интерактивном справочнике

188. Сеть приложения: HTTP, WebSocket и MQTT

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

Разбираем три способа на одном и том же приборе. Ничего не выдумано: HTTP и WebSocket поднимаются прямо в проверке, MQTT — настоящий брокер Mosquitto в контейнере (тот же, что в разделе 87).

Как это называется в документации

Оригинал Перевод
request-response запрос-ответ
polling опрос: приложение само спрашивает
push доставка по инициативе источника
WebSocket upgrade переход HTTP-соединения в двусторонний режим
broker, topic брокер, тема
publish / subscribe публикация и подписка
retained message сохранённое сообщение: последнее значение темы
last will and testament (LWT) завещание: что брокер опубликует при обрыве
QoS 0/1/2 уровень доставки
keepalive период проверки живости

188.1 Три способа и чем они отличаются

Способ Кто начинает обмен Чем хорош Чем плох
HTTP приложение просто, проходит везде новое значение узнаём только опросом
WebSocket обе стороны мгновенная доставка в обе стороны соединение надо держать и поднимать
MQTT обе стороны через брокера много приборов и много слушателей, есть «последнее значение» нужен брокер и его обслуживание

Правило выбора простое. Настройки и команды — HTTP: редко, по делу, с понятным кодом ответа. Живые показания на экране — WebSocket. Парк приборов, который смотрят несколько приложений сразу, — MQTT.

Три способа доставки показаний в приложение


188.2 HTTP: запрос-ответ с таймаутом и понятным отказом

Шлюз отдаёт последнее измерение в JSON, приложение его читает:

final HttpClientRequest request = await http.getUrl(url).timeout(timeout);
final HttpClientResponse response = await request.close().timeout(timeout);
if (response.statusCode != HttpStatus.ok) {
  await response.drain<void>();
  throw NetError('шлюз ответил отказом', code: response.statusCode);
}

Три вещи, без которых это не работает в поле:

  1. Срок ожидания на каждом шаге — соединение, ответ, чтение тела. Молчащий шлюз даёт TimeoutException за 100 мс, а не вечное ожидание.
  2. Код ответа — часть протокола. Отказ 503 превращается в ошибку с кодом внутри, и пользователь видит «связь: шлюз ответил отказом (код 503)», а не пустой экран.
  3. Тело дочитывается даже при отказе — иначе соединение остаётся висеть.

В проверке сервер поднимается в том же процессе (HttpServer.bind), и все три случая — нормальный ответ, отказ 503 и молчание — проверяются без единого настоящего сервера.


188.3 WebSocket: те же кадры, только по сети

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

Stream<FrameEvent> wsFrames(Stream<dynamic> socket) =>
    frames(socket.where((dynamic m) => m is List<int>).cast<List<int>>());

Кадры шлют только двоичными сообщениями: текст прошёл бы через UTF-8, и каждый байт от 0x80 превратился бы в пару — 0xC0 стал бы C3 80, и CRC не сошлась бы. Текстовое сообщение на таком канале — ошибка протокола, и разборщик его отбрасывает.

В проверке сервер шлёт кадр двумя сообщениями (первые два байта и остаток), приложение собирает его в один FrameReady и отвечает кадром команды — сервер получает ровно encodeFrame(<int>[0x01, 0x01]).

Отсюда следствие, которое экономит много работы: весь код разбора, таймаутов и переподключения из раздела 186 переносится на сеть без изменений. Меняется только источник байт.


188.4 MQTT: брокер, сохранённое значение и завещание

MQTT переворачивает схему: прибор публикует в тему, приложение подписывается, и они друг о друге не знают. Проверка работает с настоящим eclipse-mosquitto:2 в контейнере.

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

Что проверено Значение
опубликовано с retain в meter/last 3.356
получено новым подписчиком сразу после подписки 3.356

Это решает вечную проблему «экран пустой, пока прибор не пришлёт следующий кадр».

Завещание (LWT). Прибор при подключении говорит брокеру: если я пропаду — опубликуй в meter/state слово offline. Проверка делает это байтами, ровно как прошивка:

final body = <int>[
  ...str('MQTT'), 0x04,                     // уровень 3.1.1
  0x06,                                     // чистая сессия + завещание
  0x00, 0x3C,                               // keepalive 60 с
  ...str(id), ...str(willTopic), ...str(willMessage),
];

После обрыва соединения (сокет просто уничтожается, без прощания) подписчик получает offline — от брокера, а не от прибора. При прямом WebSocket обрыв видно по закрытию соединения и по молчанию на ping, а завещание решает другую задачу — сообщает об обрыве всем, кто с прибором напрямую не связан.

Флаги 0x06 — чистая сессия и завещание, но без сохранения. Подписчику, который уже ждёт, этого хватает, а приложение, открытое после обрыва, завещания не увидит: в meter/state для него нет ничего — или старое online. Поэтому в приборе завещание ставят с флагом сохранения — 0x26 (бит 0x20), а сразу после подключения прибор сам публикует в ту же тему online с retain. Опыт с тем же брокером: подписчик, пришедший после обрыва, при 0x06 получает online, при 0x26 — offline.

Когда брокер публикует завещание: сразу, если соединение закрылось без DISCONNECT, и только через полтора keepalive — 90 с при 60 с, — если прибор просто замолчал, например ушёл из зоны Wi-Fi (в опыте с keepalive 2 с — через 3–4 с). Keepalive выбирают по тому, как быстро нужно узнать об обрыве.

Прибор, брокер и два подписчика: прибор публикует meter/last равно 3.356 с retain и при подключении оставляет завещание meter/state равно offline с keepalive 60 секунд и флагами 0x06; приложение, подписавшееся позже, сразу получает 3.356 от брокера; после обрыва сокета подписчик meter/state получает offline — от брокера, а не от прибора


188.5 Что выбрать под задачу

Задача Способ Почему
прочитать настройки, записать калибровку HTTP редкие запросы, понятные коды ответа
живой график на экране WebSocket доставка без опроса, обе стороны
десять приборов и три приложения MQTT брокер разводит всех, retain даёт стартовое значение
«прибор пропал» на экране MQTT + LWT сообщение приходит от брокера
обновление прошивки HTTP файл, докачка, код ответа

Смешивать нормально: команды по HTTP, показания по MQTT — обычная раскладка для парка приборов.


188.6 Грабли

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

Нет срока ожидания. Пропавший шлюз подвешивает экран, и это единственное, что видит пользователь.

Код ответа проглочен. 503 и 200 с пустым телом для приложения одинаковы — и никто не знает, что сломалось.

Тело ответа не дочитано. Соединения копятся, через час шлюз отвечает отказом всем.

MQTT без retain. Экран пустой до первого сообщения прибора, то есть иногда минутами.

MQTT без завещания. Приложение показывает последнее значение и уверяет, что всё хорошо, хотя прибор отключён час назад.

Завещание без сохранения. Приложение, открытое после обрыва, его не видит: нужен бит 0x20 в CONNECT и своё online с retain.

Разбор кадров переписан под WebSocket. Сообщения режутся не так, как байты в порту, и второй разборщик ловит свои ошибки — тот же код работает и там, и там.


188.7 Итоги

Дальше: раздел 189 «BLE и USB-serial: транспорт до прибора, MTU, очередь записей, разрешения».

Открыть в интерактивном справочнике