Embedded HandbookApps
All sections › XXIII. The device's companion app: Dart and Flutter
STM32F103, STM32F407, STM32G431, STM32L431 · level intermediate · 7 min read
Open in the interactive handbook

188. The app's networking: HTTP, WebSocket and MQTT

A cable and BLE reach a device that is nearby. Everything else goes over the network, and the choice of method decides what the app will be like: whether it polls, waits or subscribes.

We look at three methods on one and the same device. Nothing is made up: HTTP and WebSocket are brought up right inside the test, MQTT is a real Mosquitto broker in a container (the same one as in section 87).

Terms used in the documentation

Term Meaning
request-response a request and its answer
polling the app asks by itself
push delivery started by the source
WebSocket upgrade switching an HTTP connection to two-way mode
broker, topic the message hub; the named channel
publish / subscribe sending to a topic and listening to it
retained message the last value of a topic, kept by the broker
last will and testament (LWT) what the broker publishes when the link drops
QoS 0/1/2 delivery level
keepalive the period of liveness checks

188.1 Three methods and how they differ

Method Who starts the exchange Strength Weakness
HTTP the app simple, passes everywhere a new value is learned only by polling
WebSocket both sides instant delivery in both directions the connection has to be kept and re-established
MQTT both sides via the broker many devices and many listeners, there is a "last value" needs a broker and its upkeep

The rule of choice is simple. Settings and commands — HTTP: rarely, when needed, with a clear response code. Live readings on the screen — WebSocket. A fleet of devices watched by several apps at once — MQTT.

Three ways to deliver readings to the app


188.2 HTTP: request-response with a timeout and a clear refusal

The gateway serves the latest measurement as JSON, and the app reads it:

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);
}

Three things without which this does not work in the field:

  1. A timeout on every step — connection, response, reading the body. A silent gateway gives a TimeoutException in 100 ms, not an endless wait.
  2. The response code is part of the protocol. A 503 refusal turns into an error with the code inside, and the user sees "link: the gateway refused (code 503)", not an empty screen.
  3. The body is read to the end even on a refusal — otherwise the connection stays hanging.

In the test the server is brought up in the same process (HttpServer.bind), and all three cases — a normal answer, a 503 refusal and silence — are tested without a single real server.


188.3 WebSocket: the same frames, only over the network

WebSocket keeps message boundaries: what the gateway sent as one message is what arrives. But a gateway usually forwards the port bytes in the chunks in which it received them, so one frame ends up in two messages, and two frames in one. That is why the same parser as for the port runs on top of it:

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

Frames are sent only as binary messages: text would go through UTF-8, and every byte from 0x80 up would turn into a pair — 0xC0 would become C3 80, and the CRC would not match. A text message on such a channel is a protocol error, and the parser discards it.

In the test the server sends a frame as two messages (the first two bytes and the rest), the app assembles it into one FrameReady and answers with a command frame — the server receives exactly encodeFrame(<int>[0x01, 0x01]).

This leads to a consequence that saves a lot of work: all the parsing, timeout and reconnection code from section 186 moves to the network without changes. Only the byte source changes.


188.4 MQTT: the broker, the retained value and the will

MQTT turns the scheme around: the device publishes to a topic, the app subscribes, and they know nothing about each other. The test works with a real eclipse-mosquitto:2 in a container.

Retained value (retain). The device publishes a reading with the retain flag, and the broker remembers it. An app that connects an hour later gets the last value right on subscribing — before the device sends the next one:

What was tested Value
published with retain to meter/last 3.356
received by a new subscriber right after subscribing 3.356

This solves the eternal problem of "the screen is empty until the device sends the next frame".

The will (LWT). On connecting, the device tells the broker: if I disappear, publish the word offline to meter/state. The test does this in bytes, exactly as the firmware does:

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

After the connection drops (the socket is simply destroyed, with no goodbye) the subscriber gets offline — from the broker, not from the device. With a direct WebSocket a dropped link shows up as the connection closing and as silence to ping, while the will solves a different task — it reports the drop to everyone who is not connected to the device directly.

The flags 0x06 are clean session and will, but without retain. For a subscriber that is already waiting this is enough, but an app opened after the drop will not see the will: for it there is nothing in meter/state — or an old online. That is why in a device the will is set with the retain flag — 0x26 (bit 0x20), and right after connecting the device itself publishes online with retain to the same topic. An experiment with the same broker: a subscriber that arrives after the drop gets online with 0x06, and offline with 0x26.

When the broker publishes the will: immediately, if the connection closed without DISCONNECT, and only after one and a half keepalive periods — 90 s at 60 s — if the device simply went silent, for example left the Wi-Fi zone (in the experiment with a keepalive of 2 s — after 3–4 s). The keepalive is chosen by how quickly you need to learn about a drop.

A device, a broker and two subscribers: the device publishes meter/last equal to 3.356 with retain and on connecting leaves the will meter/state equal to offline with a keepalive of 60 seconds and flags 0x06; an app that subscribes later gets 3.356 from the broker at once; after the socket drops, the meter/state subscriber gets offline — from the broker, not from the device


188.5 What to choose for the task

Task Method Why
read the settings, write a calibration HTTP rare requests, clear response codes
a live chart on the screen WebSocket delivery without polling, both directions
ten devices and three apps MQTT the broker connects everyone, retain gives the starting value
"device gone" on the screen MQTT + LWT the message comes from the broker
firmware update HTTP a file, resumed download, a response code

Mixing is normal: commands over HTTP, readings over MQTT — a common layout for a fleet of devices.


188.6 Pitfalls

Polling instead of subscribing. The app asks once a second, the network and battery are used up, and the reading is still late.

No timeout. A vanished gateway hangs the screen, and that is the only thing the user sees.

The response code is swallowed. 503 and 200 with an empty body look the same to the app — and nobody knows what broke.

The response body is not read to the end. Connections pile up, and an hour later the gateway refuses everyone.

MQTT without retain. The screen is empty until the device's first message, which is sometimes minutes.

MQTT without a will. The app shows the last value and claims everything is fine, although the device went offline an hour ago.

A will without retain. An app opened after the drop does not see it: you need bit 0x20 in CONNECT and your own online with retain.

Frame parsing rewritten for WebSocket. Messages are cut differently from the bytes in the port, and a second parser catches its own bugs — the same code works in both places.


188.7 Summary

Next: section 189 "BLE and USB serial: the link to the device, MTU, write queue, permissions".

Open in the interactive handbook