ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
mqtt.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file mqtt.h
6 * @brief The MQTT Client (PROTOCORE_ENABLE_MQTT).
7 *
8 * The governing document is "MQTT Version 3.1.1", an **OASIS Standard** dated 29 October 2014,
9 * amended by "MQTT Version 3.1.1 Plus Errata 01" (OASIS Standard Incorporating Approved Errata 01,
10 * 10 December 2015). MQTT is not an IETF protocol and carries no RFC number; every section cited
11 * below is a section of that OASIS document. The same text is published as ISO/IEC 20922:2016.
12 *
13 * The Client and the Server exchange MQTT Control Packets over a Network Connection (sec 4.2). Each
14 * packet is a fixed header (sec 2.2) carrying the Control Packet type (sec 2.2.1), four type-specific
15 * flag bits (sec 2.2.2) and a Remaining Length (sec 2.2.3), then an optional variable header and
16 * payload.
17 *
18 * Two halves behind one handle:
19 *
20 * - The codec builds and reads packet octets in the caller's buffers and holds nothing, so it is
21 * unit-tested on the host.
22 * - The transport drives one Network Connection over the outbound TCP client, and `mqtts://` over
23 * the shared persistent client TLS session. No heap; one Server at a time.
24 *
25 * QoS 2 runs the four-packet PUBLISH / PUBREC / PUBREL / PUBCOMP exchange (sec 4.3.3). Outbound
26 * QoS 1 and QoS 2 messages sit in a fixed in-flight pool and are re-delivered with DUP set
27 * (sec 3.3.1.1) until acknowledged. An inbound QoS 2 Packet Identifier is held from PUBREC until
28 * PUBCOMP so a repeat delivers the Application Message once (sec 4.3.3).
29 *
30 * The module exports one symbol, @ref Mqtt. Everything in mqtt.c has internal linkage.
31 *
32 * @author Douglas Quigg (dstroy0)
33 * @date 2026
34 */
35
36#ifndef PROTOCORE_MQTT_H
37#define PROTOCORE_MQTT_H
38
39#include "protocore_config.h" // the entry point: protocore_types.h for the widths
40
41#if PROTOCORE_ENABLE_MQTT
42
44
45// ---------------------------------------------------------------------------
46// Literals
47// ---------------------------------------------------------------------------
48
49/** @brief The largest Remaining Length a four-octet field encodes (MQTT 3.1.1 sec 2.2.3). */
50#define PROTOCORE_MQTT_REMAINING_LENGTH_MAX 268435455u
51
52/** @brief The Protocol Level a 3.1.1 CONNECT carries (MQTT 3.1.1 sec 3.1.2.2). */
53#define PROTOCORE_MQTT_PROTOCOL_LEVEL 0x04
54
55/** @brief The SUBACK return code that reports a failed subscription (MQTT 3.1.1 sec 3.9.3). */
56#define PROTOCORE_MQTT_SUBACK_FAILURE 0x80
57
58// ---------------------------------------------------------------------------
59// Typedefs
60// ---------------------------------------------------------------------------
61
62/** @brief MQTT Control Packet type, byte 1 bits 7-4 (MQTT 3.1.1 sec 2.2.1, Table 2.1). */
63typedef enum PROTO_ENUM_PACKED
64{
65 MQTT_CONNECT = 1, ///< Client request to connect to a Server
66 MQTT_CONNACK = 2, ///< Connect acknowledgment
67 MQTT_PUBLISH = 3, ///< Publish message
68 MQTT_PUBACK = 4, ///< Publish acknowledgment
69 MQTT_PUBREC = 5, ///< Publish received (QoS 2 publish received, part 1)
70 MQTT_PUBREL = 6, ///< Publish release (QoS 2 publish received, part 2)
71 MQTT_PUBCOMP = 7, ///< Publish complete (QoS 2 publish received, part 3)
72 MQTT_SUBSCRIBE = 8, ///< Client subscribe request
73 MQTT_SUBACK = 9, ///< Subscribe acknowledgment
74 MQTT_UNSUBSCRIBE = 10, ///< Unsubscribe request
75 MQTT_UNSUBACK = 11, ///< Unsubscribe acknowledgment
76 MQTT_PINGREQ = 12, ///< PING request
77 MQTT_PINGRESP = 13, ///< PING response
78 MQTT_DISCONNECT = 14, ///< Client is disconnecting
79} MqttType;
80
81/** @brief Where an inbound PUBLISH's Topic Name and Payload are delivered (MQTT 3.1.1 sec 3.3). */
82typedef void (*MqttMessageCb)(const char *topic_name, const uint8_t *payload, size_t payload_len);
83
84/** @brief MQTT 3.1.1 sec 4.2: the Network Connection this Client opens to a Server. */
85typedef struct
86{
87 const char *host; ///< the Server's name; read on every connect step, so it outlives the call
88 uint16_t port; ///< its TCP port
89 proto_bool use_tls; ///< the Network Connection runs over TLS (`mqtts://`)
90} MqttServerArgs;
91
92/**
93 * @brief MQTT 3.1.1 sec 3.1: the CONNECT variable header and payload, less the Will.
94 *
95 * A null @c user_name clears the User Name flag (sec 3.1.2.8) and a null @c password clears the
96 * Password flag (sec 3.1.2.9), so neither field is written into the payload.
97 */
98typedef struct
99{
100 const char *client_id; ///< Client Identifier (sec 3.1.3.1); "" asks the Server to assign one
101 const char *user_name; ///< User Name (sec 3.1.3.4), or null for none
102 const char *password; ///< Password (sec 3.1.3.5), or null for none
103 uint16_t keep_alive; ///< Keep Alive seconds (sec 3.1.2.10); 0 turns the mechanism off
104 proto_bool clean_session; ///< Clean Session (sec 3.1.2.4)
105} MqttSessionArgs;
106
107/**
108 * @brief MQTT 3.1.1 sec 3.1.2.5 - sec 3.1.2.7, sec 3.1.3.2, sec 3.1.3.3: the Will a CONNECT carries.
109 *
110 * A null @c topic clears the Will Flag, and with it Will QoS and Will Retain (sec 3.1.2.5).
111 */
112typedef struct
113{
114 const char *topic; ///< Will Topic (sec 3.1.3.2), or null for no Will
115 const uint8_t *message; ///< Will Message (sec 3.1.3.3); may be null when @c message_len is 0
116 size_t message_len; ///< its octet count
117 uint8_t qos; ///< Will QoS, 0 to 2 (sec 3.1.2.6)
118 proto_bool retain; ///< Will Retain (sec 3.1.2.7)
119} MqttWillArgs;
120
121/**
122 * @brief MQTT 3.1.1 sec 3.3: a PUBLISH's Topic Name, Payload and fixed-header flags.
123 *
124 * @c topic_name is what a build writes; @c topic_out is where a parse copies the Topic Name it read,
125 * NUL terminated, with @c topic_len reporting its octet count.
126 */
127typedef struct
128{
129 const char *topic_name; ///< Topic Name a build writes (sec 3.3.2.1); no wildcards (MQTT-3.3.2-2)
130 char *topic_out; ///< where a parse copies the Topic Name it read
131 size_t topic_cap; ///< its room, the NUL included
132 size_t topic_len; ///< the Topic Name octets a parse copied
133 const uint8_t *payload; ///< Payload a build writes, or where a parse found it inside @c in (sec 3.3.3)
134 size_t payload_len; ///< its octet count
135 uint8_t qos; ///< QoS level, 0 to 2 (sec 3.3.1.2)
136 proto_bool retain; ///< RETAIN (sec 3.3.1.3)
137 proto_bool dup; ///< DUP, set on a re-delivery (sec 3.3.1.1)
138} MqttPublishArgs;
139
140/** @brief MQTT 3.1.1 sec 3.8.3, sec 3.10.3: the Topic Filter a SUBSCRIBE or UNSUBSCRIBE names. */
141typedef struct
142{
143 const char *topic_filter; ///< Topic Filter (sec 4.7); wildcards are allowed here
144 uint8_t qos; ///< Requested QoS, 0 to 2 (sec 3.8.3.1)
145} MqttFilterArgs;
146
147/** @brief MQTT 3.1.1 sec 2.2, sec 2.3.1: the fixed header a build stamps or a parse reads. */
148typedef struct
149{
150 MqttType type; ///< MQTT Control Packet type (sec 2.2.1)
151 uint8_t flags; ///< the type-specific flags, byte 1 bits 3-0 (sec 2.2.2)
152 uint32_t remaining_length; ///< Remaining Length (sec 2.2.3)
153 uint16_t packet_id; ///< Packet Identifier (sec 2.3.1); never 0 on the wire
154} MqttPacketArgs;
155
156/**
157 * @brief The octets a codec call writes or reads.
158 *
159 * A build assembles the variable header and payload in @c body, because the fixed header's Remaining
160 * Length is not known until that part is finished, then composes the whole Control Packet into
161 * @c out. The codec declares no storage, so the caller lends both.
162 */
163typedef struct
164{
165 uint8_t *out; ///< where a build writes the whole Control Packet
166 size_t cap; ///< its room
167 uint8_t *body; ///< scratch the variable header and payload assemble in
168 size_t body_cap; ///< its room
169 const uint8_t *in; ///< the octets a parse reads
170 size_t avail; ///< how many are readable there
171} MqttBufArgs;
172
173/** @brief Where the Client hands an inbound Application Message on (MQTT 3.1.1 sec 3.3). */
174typedef struct
175{
176 MqttMessageCb on_message; ///< the Topic Name and Payload sink; null delivers nowhere
177} MqttDeliveryArgs;
178
179/**
180 * @brief The MQTT Client (OASIS MQTT Version 3.1.1).
181 *
182 * A caller sets the members a call takes, invokes it through ::Mqtt, and reads the outcome off the
183 * same handle. The session, its in-flight window and its buffers are behind @ref internal.
184 *
185 * No slot member: this Client holds one Network Connection at a time (sec 4.2), so no call names a
186 * row.
187 *
188 * @var MqttNs::server the Network Connection this Client opens (sec 4.2)
189 * @var MqttNs::session the CONNECT variable header and payload (sec 3.1)
190 * @var MqttNs::will the Will a CONNECT carries (sec 3.1.2.5, sec 3.1.3.2, sec 3.1.3.3)
191 * @var MqttNs::message a PUBLISH's Topic Name, Payload and flags (sec 3.3)
192 * @var MqttNs::filter the Topic Filter a SUBSCRIBE or UNSUBSCRIBE names (sec 4.7)
193 * @var MqttNs::packet the fixed header a build stamps or a parse reads (sec 2.2, sec 2.3.1)
194 * @var MqttNs::buf the octets a codec call writes or reads
195 * @var MqttNs::delivery where an inbound Application Message is handed on (sec 3.3)
196 * @var MqttNs::ok a call's true/false outcome
197 * @var MqttNs::n
198 * An octet count: the Control Packet a build wrote, the fixed header a parse read, or the Remaining
199 * Length field an encode or decode covered. 0 when a build did not fit @c cap or @c body_cap.
200 * @var MqttNs::i32 the Connect Return code a CONNACK carried (sec 3.2.2.3), or -1 if malformed
201 * @var MqttNs::u8 the first return code a SUBACK carried (sec 3.9.3)
202 * @var MqttNs::session_present the Session Present flag a CONNACK carried (sec 3.2.2.2)
203 * @var MqttNs::encode_remaining_length
204 * Write @c packet.remaining_length into @c out as a Remaining Length field of 1 to 4 octets
205 * (sec 2.2.3). @c n reports the octets written, 0 above ::PROTOCORE_MQTT_REMAINING_LENGTH_MAX or
206 * when the field would not fit @c cap.
207 * @var MqttNs::decode_remaining_length
208 * Read a Remaining Length field from @c in into @c packet.remaining_length, @c n reporting the octets
209 * it consumed. False when the field is incomplete or runs past four octets (sec 2.2.3).
210 * @var MqttNs::build_connect
211 * Build a CONNECT from @c session and @c will: Protocol Name "MQTT", Protocol Level 4, Connect Flags,
212 * Keep Alive, then the payload's Client Identifier, Will Topic, Will Message, User Name and Password
213 * (sec 3.1).
214 * @var MqttNs::build_publish
215 * Build a PUBLISH from @c message, its Packet Identifier taken from @c packet.packet_id when
216 * @c message.qos is above 0 (sec 3.3). A Topic Name holding `+` or `#` is refused (MQTT-3.3.2-2).
217 * @var MqttNs::build_subscribe
218 * Build a SUBSCRIBE carrying @c packet.packet_id and one Topic Filter at @c filter.qos, with the
219 * fixed-header flags the spec reserves as 0,0,1,0 (sec 3.8.1, sec 3.8.3).
220 * @var MqttNs::build_unsubscribe
221 * Build an UNSUBSCRIBE carrying @c packet.packet_id and one Topic Filter, with the fixed-header flags
222 * the spec reserves as 0,0,1,0 (sec 3.10.1, sec 3.10.3).
223 * @var MqttNs::build_ack
224 * Build the four-octet PUBACK, PUBREC, PUBREL or PUBCOMP named by @c packet.type carrying
225 * @c packet.packet_id (sec 3.4 - sec 3.7). PUBREL takes the reserved flags 0,0,1,0 (sec 3.6.1).
226 * @var MqttNs::build_pingreq build the two-octet PINGREQ (sec 3.12)
227 * @var MqttNs::build_disconnect build the two-octet DISCONNECT (sec 3.14)
228 * @var MqttNs::parse_fixed_header
229 * Read the fixed header at @c in into @c packet.type, @c packet.flags and @c packet.remaining_length,
230 * @c n reporting its size (sec 2.2). False until @c avail holds the whole header.
231 * @var MqttNs::parse_publish
232 * Read the @c packet.remaining_length octets after a PUBLISH fixed header: copy the Topic Name into
233 * @c message.topic_out, point @c message.payload at the Payload, and take the Packet Identifier into
234 * @c packet.packet_id when @c packet.flags carries a QoS above 0 (sec 3.3). False on a malformed
235 * Topic Name (sec 1.5.3) or on both QoS bits set (MQTT-3.3.1-4).
236 * @var MqttNs::parse_ack
237 * Read the Packet Identifier from a PUBACK, PUBREC, PUBREL, PUBCOMP or UNSUBACK body into
238 * @c packet.packet_id (sec 2.3.1). 0 reports a malformed body, since no real identifier is 0.
239 * @var MqttNs::parse_connack
240 * Read a CONNACK body into @c session_present (sec 3.2.2.2) and @c i32, the Connect Return code
241 * (sec 3.2.2.3). @c i32 is -1 when the body is malformed.
242 * @var MqttNs::parse_suback
243 * Read a SUBACK body into @c packet.packet_id and @c u8, the first return code of the payload list
244 * (sec 3.9.2, sec 3.9.3). ::PROTOCORE_MQTT_SUBACK_FAILURE there is a refused subscription.
245 * @var MqttNs::on_message record @c delivery.on_message; call it before @ref MqttNs::connect
246 * @var MqttNs::connect
247 * Open the Network Connection to @c server and frame the CONNECT built from @c session and @c will.
248 * Returns straight away: @ref MqttNs::loop steps the transport, then the handshake, then the CONNACK,
249 * and gives the whole thing up past ::PROTOCORE_MQTT_CONNECT_MS. @c session and @c will are read only
250 * during this call.
251 * @var MqttNs::publish
252 * Send @c message as a PUBLISH (sec 3.3). QoS 0 goes out and is forgotten; QoS 1 and QoS 2 take an
253 * in-flight slot and are re-delivered with DUP until acknowledged (sec 4.3.2, sec 4.3.3).
254 * @var MqttNs::subscribe send a SUBSCRIBE for @c filter (sec 3.8)
255 * @var MqttNs::unsubscribe send an UNSUBSCRIBE for @c filter.topic_filter (sec 3.10)
256 * @var MqttNs::loop
257 * Pump the Network Connection: read inbound Control Packets, deliver PUBLISH to
258 * @c delivery.on_message and run the QoS 1 and QoS 2 acknowledgement flows, re-deliver unacknowledged
259 * in-flight messages, and send PINGREQ when Keep Alive is due (sec 3.1.2.10). Call once per loop().
260 * False once the Network Connection is gone.
261 * @var MqttNs::connected true while the Server has accepted the CONNECT
262 * @var MqttNs::disconnect
263 * Send DISCONNECT and close the Network Connection (sec 3.14).
264 */
265typedef struct
266{
267 MqttServerArgs server; ///< the Network Connection this Client opens
268 MqttSessionArgs session; ///< what a CONNECT states about the session
269 MqttWillArgs will; ///< the Will a CONNECT carries
270 MqttPublishArgs message; ///< a PUBLISH's Topic Name, Payload and flags
271 MqttFilterArgs filter; ///< the Topic Filter a SUBSCRIBE or UNSUBSCRIBE names
272 MqttPacketArgs packet; ///< the fixed header a build stamps or a parse reads
273 MqttBufArgs buf; ///< the octets a codec call moves
274 MqttDeliveryArgs delivery; ///< where an inbound Application Message is handed on
275 proto_bool ok;
276 size_t n;
277 int32_t i32;
278 uint8_t u8;
279 proto_bool session_present;
280} MqttVars;
281
282/** @brief The operands and the outcome. */
283extern MqttVars MqttV;
284
285/** @brief The entries. */
286typedef struct
287{
288 void (*const encode_remaining_length)(uint8_t *work);
289 void (*const decode_remaining_length)(uint8_t *work);
290 void (*const build_connect)(uint8_t *work);
291 void (*const build_publish)(uint8_t *work);
292 void (*const build_subscribe)(uint8_t *work);
293 void (*const build_unsubscribe)(uint8_t *work);
294 void (*const build_ack)(uint8_t *work);
295 void (*const build_pingreq)(uint8_t *work);
296 void (*const build_disconnect)(uint8_t *work);
297 void (*const parse_fixed_header)(uint8_t *work);
298 void (*const parse_publish)(uint8_t *work);
299 void (*const parse_ack)(uint8_t *work);
300 void (*const parse_connack)(uint8_t *work);
301 void (*const parse_suback)(uint8_t *work);
302 void (*const on_message)(uint8_t *work);
303 void (*const connect)(uint8_t *work);
304 void (*const publish)(uint8_t *work);
305 void (*const subscribe)(uint8_t *work);
306 void (*const unsubscribe)(uint8_t *work);
307 void (*const loop)(uint8_t *work);
308 void (*const connected)(uint8_t *work);
309 void (*const disconnect)(uint8_t *work);
310} MqttNs;
311
312// What the table binds, defined once in the .c and taking one parameter each: everything
313// else an entry needs is an operand in MqttV or a region of the borrow at a fixed offset.
314void protocore_mqtt_encode_remaining_length(uint8_t *work);
315void protocore_mqtt_decode_remaining_length(uint8_t *work);
316void protocore_mqtt_build_connect(uint8_t *work);
317void protocore_mqtt_build_publish(uint8_t *work);
318void protocore_mqtt_build_subscribe(uint8_t *work);
319void protocore_mqtt_build_unsubscribe(uint8_t *work);
320void protocore_mqtt_build_ack(uint8_t *work);
321void protocore_mqtt_build_pingreq(uint8_t *work);
322void protocore_mqtt_build_disconnect(uint8_t *work);
323void protocore_mqtt_parse_fixed_header(uint8_t *work);
324void protocore_mqtt_parse_publish(uint8_t *work);
325void protocore_mqtt_parse_ack(uint8_t *work);
326void protocore_mqtt_parse_connack(uint8_t *work);
327void protocore_mqtt_parse_suback(uint8_t *work);
328void protocore_mqtt_on_message(uint8_t *work);
329void protocore_mqtt_connect(uint8_t *work);
330void protocore_mqtt_publish(uint8_t *work);
331void protocore_mqtt_subscribe(uint8_t *work);
332void protocore_mqtt_unsubscribe(uint8_t *work);
333void protocore_mqtt_loop(uint8_t *work);
334void protocore_mqtt_connected(uint8_t *work);
335void protocore_mqtt_disconnect(uint8_t *work);
336
337// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
338// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
339// `Mqtt.encode_remaining_length(work)` resolves to a named function and becomes a DIRECT call. An extern table
340// leaves the call indirect and the symbol live at every level, -O2 -flto included.
341static const MqttNs Mqtt __attribute__((unused)) = {
342 .encode_remaining_length = protocore_mqtt_encode_remaining_length,
343 .decode_remaining_length = protocore_mqtt_decode_remaining_length,
344 .build_connect = protocore_mqtt_build_connect,
345 .build_publish = protocore_mqtt_build_publish,
346 .build_subscribe = protocore_mqtt_build_subscribe,
347 .build_unsubscribe = protocore_mqtt_build_unsubscribe,
348 .build_ack = protocore_mqtt_build_ack,
349 .build_pingreq = protocore_mqtt_build_pingreq,
350 .build_disconnect = protocore_mqtt_build_disconnect,
351 .parse_fixed_header = protocore_mqtt_parse_fixed_header,
352 .parse_publish = protocore_mqtt_parse_publish,
353 .parse_ack = protocore_mqtt_parse_ack,
354 .parse_connack = protocore_mqtt_parse_connack,
355 .parse_suback = protocore_mqtt_parse_suback,
356 .on_message = protocore_mqtt_on_message,
357 .connect = protocore_mqtt_connect,
358 .publish = protocore_mqtt_publish,
359 .subscribe = protocore_mqtt_subscribe,
360 .unsubscribe = protocore_mqtt_unsubscribe,
361 .loop = protocore_mqtt_loop,
362 .connected = protocore_mqtt_connected,
363 .disconnect = protocore_mqtt_disconnect,
364};
365
366/**
367 * @brief The PROTOCORE_MQTT_BORROW bytes this module's state lives in.
368 *
369 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
370 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
371 * walks, so the state lasts the life of the program.
372 *
373 * @return the span.
374 */
375#if PROTOCORE_HAS_NET_STACK
376uint8_t *protocore_mqtt_span(void);
377#endif // PROTOCORE_HAS_NET_STACK
378
380
381#endif // PROTOCORE_ENABLE_MQTT
382
383#endif // PROTOCORE_MQTT_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97