DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
mqtt.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file mqtt.cpp
6 * @brief MQTT 3.1.1 packet codec (host-testable) + the raw-lwIP / mbedTLS
7 * persistent client transport (ESP32 only).
8 */
9
10#include "services/mqtt/mqtt.h"
11
12#if DETWS_ENABLE_MQTT
13
15#include <string.h>
16
17// ---------------------------------------------------------------------------
18// Pure codec (host-testable)
19// ---------------------------------------------------------------------------
20
21// Big-endian 16-bit helpers and a length-prefixed UTF-8 string writer.
22static inline void put_u16(uint8_t *p, uint16_t v)
23{
24 p[0] = (uint8_t)(v >> 8);
25 p[1] = (uint8_t)(v & 0xFF);
26}
27static inline uint16_t get_u16(const uint8_t *p)
28{
29 return (uint16_t)((p[0] << 8) | p[1]);
30}
31// Write a 2-byte length + the bytes; returns total bytes written (2 + len).
32static size_t put_field(uint8_t *p, const uint8_t *data, size_t len)
33{
34 put_u16(p, (uint16_t)len);
35 if (len)
36 memcpy(p + 2, data, len);
37 return 2 + len;
38}
39static inline size_t put_str(uint8_t *p, const char *s)
40{
41 return put_field(p, (const uint8_t *)s, s ? strnlen(s, DETWS_MQTT_BUF_SIZE) : 0);
42}
43
44size_t mqtt_encode_remlen(uint8_t *out, uint32_t len)
45{
46 if (len > 268435455u) // 4 * 7 bits
47 return 0;
48 size_t n = 0;
49 do
50 {
51 uint8_t byte = (uint8_t)(len % 128);
52 len /= 128;
53 if (len > 0)
54 byte |= 0x80;
55 out[n++] = byte;
56 } while (len > 0);
57 return n;
58}
59
60bool mqtt_decode_remlen(const uint8_t *buf, size_t avail, uint32_t *value, size_t *used)
61{
62 uint32_t v = 0;
63 uint32_t mult = 1;
64 size_t i = 0;
65 for (; i < 4; i++)
66 {
67 if (i >= avail)
68 return false; // incomplete
69 uint8_t b = buf[i];
70 v += (uint32_t)(b & 0x7F) * mult;
71 if ((b & 0x80) == 0)
72 {
73 *value = v;
74 *used = i + 1;
75 return true;
76 }
77 mult *= 128;
78 }
79 return false; // malformed (5th continuation byte)
80}
81
82// Assemble a packet: fixed header byte0 + remaining-length, then a pre-built
83// variable-header+payload body of vlen bytes already placed at out + (header).
84// Returns total length or 0 if it will not fit cap. The body must be written by
85// the caller into a scratch first; here we shift it after the header is known.
86// To avoid a second buffer we build the body at a fixed offset (max 4-byte
87// remlen) and memmove it tight - simpler: build body in `body`, then compose.
88static size_t compose(uint8_t *out, size_t cap, uint8_t byte0, const uint8_t *body, size_t blen)
89{
90 uint8_t rl[4];
91 // Every caller pre-builds body in body[DETWS_MQTT_BUF_SIZE] (1024), so blen is bounded far below
92 // the 2^28 remaining-length limit and mqtt_encode_remlen never rejects here; the len > 256MB reject
93 // is covered directly on the public mqtt_encode_remlen.
94 size_t rln = mqtt_encode_remlen(rl, (uint32_t)blen);
95 if (rln == 0)
96 return 0; // GCOVR_EXCL_LINE unreachable: blen <= DETWS_MQTT_BUF_SIZE << 2^28 via compose's bounded callers
97 size_t total = 1 + rln + blen;
98 if (total > cap)
99 return 0;
100 out[0] = byte0;
101 memcpy(out + 1, rl, rln);
102 if (blen)
103 memcpy(out + 1 + rln, body, blen);
104 return total;
105}
106
107size_t mqtt_build_connect(uint8_t *out, size_t cap, const MqttConnectOpts *opts)
108{
109 if (!out || !opts || !opts->client_id)
110 return 0;
111 uint8_t body[DETWS_MQTT_BUF_SIZE];
112 size_t n = 0;
113 // Variable header: protocol name + level + flags + keep-alive.
114 n += put_str(body + n, "MQTT");
115 body[n++] = 0x04; // protocol level 4 (MQTT 3.1.1)
116
117 uint8_t flags = 0;
118 if (opts->clean_session)
119 flags |= 0x02;
120 if (opts->will_topic)
121 {
122 flags |= 0x04; // will flag
123 flags |= (uint8_t)((opts->will_qos & 0x03) << 3); // will QoS
124 if (opts->will_retain)
125 flags |= 0x20;
126 }
127 if (opts->user)
128 flags |= 0x80;
129 if (opts->pass)
130 flags |= 0x40;
131 body[n++] = flags;
132 put_u16(body + n, opts->keepalive_s);
133 n += 2;
134
135 // Payload: client id, [will topic, will msg], [user], [pass]. Bounds-check
136 // each field against the body scratch as we go.
137 size_t need = 2 + strnlen(opts->client_id, DETWS_MQTT_BUF_SIZE);
138 if (opts->will_topic)
139 need += 2 + strnlen(opts->will_topic, DETWS_MQTT_BUF_SIZE) + 2 + opts->will_len;
140 if (opts->user)
141 need += 2 + strnlen(opts->user, DETWS_MQTT_BUF_SIZE);
142 if (opts->pass)
143 need += 2 + strnlen(opts->pass, DETWS_MQTT_BUF_SIZE);
144 if (n + need > sizeof(body))
145 return 0;
146
147 n += put_str(body + n, opts->client_id);
148 if (opts->will_topic)
149 {
150 n += put_str(body + n, opts->will_topic);
151 n += put_field(body + n, opts->will_msg, opts->will_len);
152 }
153 if (opts->user)
154 n += put_str(body + n, opts->user);
155 if (opts->pass)
156 n += put_str(body + n, opts->pass);
157
158 return compose(out, cap, (uint8_t)((uint8_t)MqttType::MQTT_CONNECT << 4), body, n);
159}
160
161size_t mqtt_build_publish(uint8_t *out, size_t cap, const char *topic, const uint8_t *payload, size_t payload_len,
162 uint8_t qos, uint16_t packet_id, bool retain, bool dup)
163{
164 if (!out || !topic || qos > 2)
165 return 0;
166 // MQTT-3.3.2-2: a PUBLISH Topic Name MUST NOT contain wildcard characters
167 // (subscribe topic *filters* may, so this check is publish-only).
168 for (const char *t = topic; *t; t++)
169 if (*t == '+' || *t == '#')
170 return 0;
171 size_t tlen = strnlen(topic, DETWS_MQTT_BUF_SIZE);
172 size_t blen = 2 + tlen + (qos > 0 ? 2 : 0) + payload_len;
173 uint8_t body[DETWS_MQTT_BUF_SIZE];
174 if (blen > sizeof(body))
175 return 0;
176 size_t n = 0;
177 n += put_field(body + n, (const uint8_t *)topic, tlen);
178 if (qos > 0)
179 {
180 put_u16(body + n, packet_id);
181 n += 2;
182 }
183 if (payload_len)
184 memcpy(body + n, payload, payload_len);
185 n += payload_len;
186
187 uint8_t f = (uint8_t)((qos & 0x03) << 1);
188 if (retain)
189 f |= 0x01;
190 if (dup)
191 f |= 0x08;
192 return compose(out, cap, (uint8_t)(((uint8_t)MqttType::MQTT_PUBLISH << 4) | f), body, n);
193}
194
195size_t mqtt_build_subscribe(uint8_t *out, size_t cap, uint16_t packet_id, const char *topic, uint8_t qos)
196{
197 if (!out || !topic || qos > 2)
198 return 0;
199 size_t tlen = strnlen(topic, DETWS_MQTT_BUF_SIZE);
200 uint8_t body[DETWS_MQTT_BUF_SIZE];
201 size_t blen = 2 + 2 + tlen + 1;
202 if (blen > sizeof(body))
203 return 0;
204 size_t n = 0;
205 put_u16(body + n, packet_id);
206 n += 2;
207 n += put_field(body + n, (const uint8_t *)topic, tlen);
208 body[n++] = (uint8_t)(qos & 0x03);
209 return compose(out, cap, (uint8_t)(((uint8_t)MqttType::MQTT_SUBSCRIBE << 4) | 0x02), body,
210 n); // SUBSCRIBE flags = 0010
211}
212
213size_t mqtt_build_unsubscribe(uint8_t *out, size_t cap, uint16_t packet_id, const char *topic)
214{
215 if (!out || !topic)
216 return 0;
217 size_t tlen = strnlen(topic, DETWS_MQTT_BUF_SIZE);
218 uint8_t body[DETWS_MQTT_BUF_SIZE];
219 size_t blen = 2 + 2 + tlen;
220 if (blen > sizeof(body))
221 return 0;
222 size_t n = 0;
223 put_u16(body + n, packet_id);
224 n += 2;
225 n += put_field(body + n, (const uint8_t *)topic, tlen);
226 return compose(out, cap, (uint8_t)(((uint8_t)MqttType::MQTT_UNSUBSCRIBE << 4) | 0x02), body,
227 n); // UNSUBSCRIBE flags = 0010
228}
229
230size_t mqtt_build_ack(uint8_t *out, size_t cap, MqttType type, uint16_t packet_id)
231{
232 if (!out || cap < 4)
233 return 0;
234 uint8_t f = (type == MqttType::MQTT_PUBREL) ? 0x02 : 0x00; // PUBREL requires flags 0010
235 out[0] = (uint8_t)(((uint8_t)type << 4) | f);
236 out[1] = 0x02;
237 put_u16(out + 2, packet_id);
238 return 4;
239}
240
241size_t mqtt_build_pingreq(uint8_t *out, size_t cap)
242{
243 if (!out || cap < 2)
244 return 0;
245 out[0] = (uint8_t)((uint8_t)MqttType::MQTT_PINGREQ << 4);
246 out[1] = 0x00;
247 return 2;
248}
249
250size_t mqtt_build_disconnect(uint8_t *out, size_t cap)
251{
252 if (!out || cap < 2)
253 return 0;
254 out[0] = (uint8_t)((uint8_t)MqttType::MQTT_DISCONNECT << 4);
255 out[1] = 0x00;
256 return 2;
257}
258
259bool mqtt_parse_fixed_header(const uint8_t *buf, size_t avail, uint8_t *type, uint8_t *flags, uint32_t *remaining_len,
260 size_t *header_len)
261{
262 if (avail < 2)
263 return false;
264 uint32_t rl;
265 size_t used;
266 if (!mqtt_decode_remlen(buf + 1, avail - 1, &rl, &used))
267 return false;
268 *type = (uint8_t)(buf[0] >> 4);
269 *flags = (uint8_t)(buf[0] & 0x0F);
270 *remaining_len = rl;
271 *header_len = 1 + used;
272 return true;
273}
274
275bool mqtt_parse_publish(const uint8_t *buf, uint32_t remaining_len, uint8_t flags, char *topic_out, size_t topic_cap,
276 size_t *topic_len, const uint8_t **payload, size_t *payload_len, uint16_t *packet_id)
277{
278 if (!buf || remaining_len < 2)
279 return false;
280 uint16_t tlen = get_u16(buf);
281 size_t off = 2;
282 if ((uint32_t)off + tlen > remaining_len)
283 return false;
284 if ((size_t)tlen + 1 > topic_cap)
285 return false; // topic + NUL must fit
286 // MQTT 1.5.3: a UTF-8 string must be well-formed and must not contain U+0000.
287 if (!det_utf8_valid(buf + off, tlen) || memchr(buf + off, 0x00, tlen))
288 return false;
289 memcpy(topic_out, buf + off, tlen);
290 topic_out[tlen] = '\0';
291 *topic_len = tlen;
292 off += tlen;
293
294 uint8_t qos = (uint8_t)((flags >> 1) & 0x03);
295 if (qos == 3)
296 return false; // MQTT-3.3.1-4: a PUBLISH MUST NOT have both QoS bits set (malformed)
297 *packet_id = 0;
298 if (qos > 0)
299 {
300 if ((uint32_t)off + 2 > remaining_len)
301 return false;
302 *packet_id = get_u16(buf + off);
303 off += 2;
304 }
305 *payload = buf + off;
306 *payload_len = remaining_len - off;
307 return true;
308}
309
310uint16_t mqtt_parse_ack(const uint8_t *buf, uint32_t remaining_len)
311{
312 if (!buf || remaining_len < 2)
313 return 0;
314 return get_u16(buf);
315}
316
317int mqtt_parse_connack(const uint8_t *buf, uint32_t remaining_len, bool *session_present)
318{
319 if (!buf || remaining_len < 2)
320 return -1;
321 if (session_present)
322 *session_present = (buf[0] & 0x01) != 0;
323 return buf[1];
324}
325
326bool mqtt_parse_suback(const uint8_t *buf, uint32_t remaining_len, uint16_t *packet_id, uint8_t *return_code)
327{
328 if (!buf || remaining_len < 3)
329 return false;
330 if (packet_id)
331 *packet_id = get_u16(buf);
332 if (return_code)
333 *return_code = buf[2];
334 return true;
335}
336
337// ---------------------------------------------------------------------------
338// Transport (ESP32 only): persistent raw-lwIP TCP client + QoS state machine,
339// with mqtts:// over a persistent client TLS session (det_tls csess).
340// ---------------------------------------------------------------------------
341#if defined(ARDUINO)
342
343#include "network_drivers/transport/client.h" // shared outbound TCP client (L4)
344#include <Arduino.h>
345
346#if DETWS_ENABLE_MQTT_TLS
347#include "network_drivers/tls/tls.h" // persistent client TLS session (csess)
348#include <mbedtls/ssl.h> // MBEDTLS_ERR_SSL_WANT_* for the BIO callbacks
349#endif
350
351#ifdef DETWS_MQTT_DEBUG
352#define MQ_DBG(...) printf(__VA_ARGS__)
353#else
354#define MQ_DBG(...) ((void)0)
355#endif
356
357// Outbound QoS 1/2 in-flight (held for DUP retransmit until acknowledged).
358struct MqttInflight
359{
360 uint16_t pid;
361 uint8_t state; // 0 free, 1 awaiting PUBACK(qos1)/PUBREC(qos2), 2 awaiting PUBCOMP(qos2)
362 uint32_t sent_ms;
363 uint16_t len;
364 uint8_t pkt[DETWS_MQTT_INFLIGHT_BUF];
365};
366
367// All MQTT connection state, owned by one instance (internal linkage): one broker at a time,
368// all static / no heap. Grouped so it is one named owner, unreachable from any other TU.
369struct MqttCtx
370{
371 MqttMessageCb cb;
372 int cid = -1; // outbound connection id (det_client pool)
373 volatile bool closed; // peer closed / error (set when the pump sees it)
374
375 // Inbound plaintext byte ring (consumer = process_rx). It is fed by a pump in
376 // process_rx: for plain TCP from det_client_read, for MQTTS from the TLS session
377 // (det_tls_csess_read), whose BIO in turn reads ciphertext from det_client.
378 uint8_t rx[DETWS_MQTT_BUF_SIZE];
379 volatile size_t rx_head;
380 volatile size_t rx_tail;
381
382 uint8_t pkt[DETWS_MQTT_BUF_SIZE]; // contiguous scratch a packet is copied into to parse
383 uint8_t tx[DETWS_MQTT_BUF_SIZE]; // outgoing packet scratch
384 bool use_tls; // mqtts:// mode
385
386 bool mqtt_up;
387 uint16_t keepalive_s;
388 uint32_t last_tx_ms;
389 bool ping_pending;
390 uint32_t ping_sent_ms;
391 uint16_t next_pid = 1;
392 int connack_code; // set by process_rx during the connect handshake
393
394 MqttInflight inflight[DETWS_MQTT_MAX_INFLIGHT];
395 // Inbound QoS 2 packet ids that have been PUBREC'd and await PUBREL (0 = empty).
396 uint16_t rx_qos2[DETWS_MQTT_RX_QOS2_SLOTS];
397};
398static MqttCtx s_mqtt;
399
400static uint16_t next_pid()
401{
402 uint16_t p = s_mqtt.next_pid++;
403 if (s_mqtt.next_pid == 0)
404 s_mqtt.next_pid = 1;
405 return p;
406}
407
408// --- ring helpers (single-producer/single-consumer) ---
409static inline size_t ring_avail()
410{
411 return (s_mqtt.rx_head + sizeof(s_mqtt.rx) - s_mqtt.rx_tail) % sizeof(s_mqtt.rx);
412}
413static inline uint8_t ring_peek(size_t i)
414{
415 return s_mqtt.rx[(s_mqtt.rx_tail + i) % sizeof(s_mqtt.rx)];
416}
417static void ring_copy(uint8_t *dst, size_t n)
418{
419 for (size_t i = 0; i < n; i++)
420 dst[i] = s_mqtt.rx[(s_mqtt.rx_tail + i) % sizeof(s_mqtt.rx)];
421}
422static inline void ring_advance(size_t n)
423{
424 s_mqtt.rx_tail = (s_mqtt.rx_tail + n) % sizeof(s_mqtt.rx);
425}
426
427// --- transport over the shared outbound client (det_client) ---
428
429// Send raw plaintext bytes to the broker.
430static bool mq_tx_plain(const uint8_t *data, size_t len)
431{
432 return det_client_send(s_mqtt.cid, data, len);
433}
434
435// Drain plaintext wire bytes from the client into the s_mqtt.rx ring (plain TCP).
436// det_client's own ring applies lossless backpressure to the peer when s_mqtt.rx is
437// full and we stop draining.
438static void mq_pump_plain()
439{
440 uint8_t tmp[256];
441 for (;;)
442 {
443 size_t freey = (sizeof(s_mqtt.rx) - 1) - ring_avail();
444 if (freey == 0)
445 break;
446 size_t want = freey < sizeof(tmp) ? freey : sizeof(tmp);
447 size_t n = det_client_read(s_mqtt.cid, tmp, want);
448 if (n == 0)
449 {
450 if (det_client_is_closed(s_mqtt.cid))
451 s_mqtt.closed = true;
452 break;
453 }
454 for (size_t i = 0; i < n; i++)
455 {
456 s_mqtt.rx[s_mqtt.rx_head] = tmp[i];
457 s_mqtt.rx_head = (s_mqtt.rx_head + 1) % sizeof(s_mqtt.rx);
458 }
459 }
460}
461
462#if DETWS_ENABLE_MQTT_TLS
463// TLS BIO over the shared client: write ciphertext through the pool, read
464// ciphertext by draining the client's wire ring.
465static int mq_tls_send(void *ctx, const unsigned char *buf, size_t len)
466{
467 (void)ctx;
468 size_t cap = len > 0xFFFF ? 0xFFFF : len;
469 return det_client_send(s_mqtt.cid, buf, cap) ? (int)cap : MBEDTLS_ERR_SSL_WANT_WRITE;
470}
471static int mq_tls_recv(void *ctx, unsigned char *buf, size_t len)
472{
473 (void)ctx;
474 size_t n = det_client_read(s_mqtt.cid, buf, len);
475 if (n == 0)
476 return det_client_is_closed(s_mqtt.cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
477 return (int)n;
478}
479// Drain decrypted plaintext from the TLS session into the s_mqtt.rx ring (main loop).
480static void mq_pump_tls()
481{
482 uint8_t tmp[256];
483 for (;;)
484 {
485 size_t freey = (sizeof(s_mqtt.rx) - 1) - ring_avail();
486 if (freey == 0)
487 break;
488 size_t want = freey < sizeof(tmp) ? freey : sizeof(tmp);
489 int n = det_tls_csess_read(tmp, want);
490 if (n <= 0)
491 {
492 if (n < 0)
493 s_mqtt.closed = true;
494 break;
495 }
496 for (int i = 0; i < n; i++)
497 {
498 s_mqtt.rx[s_mqtt.rx_head] = tmp[i];
499 s_mqtt.rx_head = (s_mqtt.rx_head + 1) % sizeof(s_mqtt.rx);
500 }
501 }
502}
503#endif // DETWS_ENABLE_MQTT_TLS
504
505// Send a complete MQTT packet (plaintext or TLS-encrypted per the mode).
506static bool mq_tx(const uint8_t *data, size_t len)
507{
508 bool ok;
509#if DETWS_ENABLE_MQTT_TLS
510 if (s_mqtt.use_tls)
511 ok = det_tls_csess_write(data, len) == (int)len;
512 else
513#endif
514 ok = mq_tx_plain(data, len);
515 if (ok)
516 s_mqtt.last_tx_ms = millis();
517 return ok;
518}
519
520static void mq_close()
521{
522#if DETWS_ENABLE_MQTT_TLS
523 if (s_mqtt.use_tls)
524 det_tls_csess_end();
525#endif
526 if (s_mqtt.cid >= 0)
527 det_client_close(s_mqtt.cid);
528 s_mqtt.cid = -1;
529 s_mqtt.mqtt_up = false;
530}
531
532static int inflight_find(uint16_t pid)
533{
534 for (int i = 0; i < DETWS_MQTT_MAX_INFLIGHT; i++)
535 if (s_mqtt.inflight[i].state != 0 && s_mqtt.inflight[i].pid == pid)
536 return i;
537 return -1;
538}
539
540static void rxqos2_add(uint16_t pid)
541{
542 for (int i = 0; i < DETWS_MQTT_RX_QOS2_SLOTS; i++)
543 if (s_mqtt.rx_qos2[i] == 0)
544 {
545 s_mqtt.rx_qos2[i] = pid;
546 return;
547 }
548}
549static bool rxqos2_has(uint16_t pid)
550{
551 for (int i = 0; i < DETWS_MQTT_RX_QOS2_SLOTS; i++)
552 if (s_mqtt.rx_qos2[i] == pid)
553 return true;
554 return false;
555}
556static void rxqos2_del(uint16_t pid)
557{
558 for (int i = 0; i < DETWS_MQTT_RX_QOS2_SLOTS; i++)
559 if (s_mqtt.rx_qos2[i] == pid)
560 s_mqtt.rx_qos2[i] = 0;
561}
562
563// Handle one fully-received packet sitting in s_mqtt.pkt (length plen).
564static void handle_packet(uint8_t type, uint8_t flags, const uint8_t *body, uint32_t rl)
565{
566 switch ((MqttType)type) // wire byte -> typed control-packet dispatch
567 {
568 case MqttType::MQTT_CONNACK:
569 s_mqtt.connack_code = mqtt_parse_connack(body, rl, nullptr);
570 if (s_mqtt.connack_code == 0)
571 s_mqtt.mqtt_up = true;
572 MQ_DBG("[mqtt] CONNACK code=%d\n", s_mqtt.connack_code);
573 break;
574 case MqttType::MQTT_PUBLISH: {
575 char topic[DETWS_MQTT_MAX_TOPIC];
576 size_t tlen;
577 size_t plen;
578 const uint8_t *payload;
579 uint16_t pid;
580 if (!mqtt_parse_publish(body, rl, flags, topic, sizeof(topic), &tlen, &payload, &plen, &pid))
581 {
582 mq_close(); // MQTT-4.8.0-1: a malformed PUBLISH (incl. QoS=3) MUST close the connection
583 break;
584 }
585 uint8_t qos = (uint8_t)((flags >> 1) & 0x03);
586 if (qos < 2)
587 {
588 if (s_mqtt.cb)
589 s_mqtt.cb(topic, payload, plen);
590 if (qos == 1)
591 {
592 size_t n = mqtt_build_ack(s_mqtt.tx, sizeof(s_mqtt.tx), MqttType::MQTT_PUBACK, pid);
593 mq_tx(s_mqtt.tx, n);
594 }
595 }
596 else // QoS 2: dispatch once, dedup by id until PUBREL completes
597 {
598 if (!rxqos2_has(pid))
599 {
600 if (s_mqtt.cb)
601 s_mqtt.cb(topic, payload, plen);
602 rxqos2_add(pid);
603 }
604 size_t n = mqtt_build_ack(s_mqtt.tx, sizeof(s_mqtt.tx), MqttType::MQTT_PUBREC, pid);
605 mq_tx(s_mqtt.tx, n);
606 }
607 break;
608 }
609 case MqttType::MQTT_PUBACK: // our QoS 1 publish acknowledged
610 case MqttType::MQTT_PUBCOMP: // our QoS 2 publish completed
611 {
612 int s = inflight_find(mqtt_parse_ack(body, rl));
613 if (s >= 0)
614 s_mqtt.inflight[s].state = 0;
615 break;
616 }
617 case MqttType::MQTT_PUBREC: // our QoS 2 publish: reply PUBREL, await PUBCOMP
618 {
619 uint16_t pid = mqtt_parse_ack(body, rl);
620 int s = inflight_find(pid);
621 if (s >= 0)
622 {
623 s_mqtt.inflight[s].state = 2;
624 s_mqtt.inflight[s].sent_ms = millis();
625 }
626 size_t n = mqtt_build_ack(s_mqtt.tx, sizeof(s_mqtt.tx), MqttType::MQTT_PUBREL, pid);
627 mq_tx(s_mqtt.tx, n);
628 break;
629 }
630 case MqttType::MQTT_PUBREL: // broker releasing an inbound QoS 2 message: reply PUBCOMP
631 {
632 uint16_t pid = mqtt_parse_ack(body, rl);
633 rxqos2_del(pid);
634 size_t n = mqtt_build_ack(s_mqtt.tx, sizeof(s_mqtt.tx), MqttType::MQTT_PUBCOMP, pid);
635 mq_tx(s_mqtt.tx, n);
636 break;
637 }
638 case MqttType::MQTT_PINGRESP:
639 s_mqtt.ping_pending = false;
640 break;
641 case MqttType::MQTT_SUBACK:
642 case MqttType::MQTT_UNSUBACK:
643 default:
644 break; // acknowledgements we do not need to act on
645 }
646}
647
648// Drain complete packets from the rx ring (copies each into s_mqtt.pkt to parse).
649static void process_rx()
650{
651#if DETWS_ENABLE_MQTT_TLS
652 if (s_mqtt.use_tls)
653 mq_pump_tls(); // decrypt ciphertext into the plaintext ring first
654 else
655#endif
656 mq_pump_plain(); // drain plaintext wire bytes into the ring
657 for (;;)
658 {
659 size_t avail = ring_avail();
660 if (avail < 2)
661 return;
662 // Peek the fixed header (byte0 + 1-4 remlen bytes) without advancing.
663 uint8_t hdr[5];
664 size_t hn = avail < 5 ? avail : 5;
665 for (size_t i = 0; i < hn; i++)
666 hdr[i] = ring_peek(i);
667 uint8_t type;
668 uint8_t flags;
669 uint32_t rl;
670 size_t hl;
671 if (!mqtt_parse_fixed_header(hdr, hn, &type, &flags, &rl, &hl))
672 return; // incomplete header
673 size_t total = hl + rl;
674 if (avail < total)
675 return; // packet not fully arrived yet
676 if (total > sizeof(s_mqtt.pkt))
677 {
678 ring_advance(total); // oversized: drop it
679 continue;
680 }
681 ring_copy(s_mqtt.pkt, total);
682 ring_advance(total);
683 handle_packet(type, flags, s_mqtt.pkt + hl, rl);
684 }
685}
686
687void mqtt_on_message(MqttMessageCb cb)
688{
689 s_mqtt.cb = cb;
690}
691
692bool mqtt_connect(const char *host, uint16_t port, bool use_tls, const MqttConnectOpts *opts)
693{
694 if (!host || !opts)
695 return false;
696#if !DETWS_ENABLE_MQTT_TLS
697 if (use_tls)
698 return false; // built without MQTTS support
699#endif
700
701 // Reset all session state.
702 memset(s_mqtt.inflight, 0, sizeof(s_mqtt.inflight));
703 memset(s_mqtt.rx_qos2, 0, sizeof(s_mqtt.rx_qos2));
704 s_mqtt.rx_head = s_mqtt.rx_tail = 0;
705 s_mqtt.closed = s_mqtt.mqtt_up = s_mqtt.ping_pending = false;
706 s_mqtt.connack_code = -1;
707 s_mqtt.keepalive_s = opts->keepalive_s;
708 s_mqtt.use_tls = use_tls;
709
710 uint32_t deadline = millis() + 8000;
711
712 // Open the TCP connection (DNS + connect) via the shared client transport.
713 s_mqtt.cid = det_client_open(host, port, 8000);
714 if (s_mqtt.cid < 0)
715 return false;
716
717#if DETWS_ENABLE_MQTT_TLS
718 if (s_mqtt.use_tls)
719 {
720 if (!det_tls_csess_begin(host, mq_tls_send, mq_tls_recv))
721 {
722 mq_close();
723 return false;
724 }
725 int h;
726 while ((h = det_tls_csess_handshake()) == 0 && !s_mqtt.closed && (int32_t)(deadline - millis()) > 0)
727 delay(5);
728 if (h != 1)
729 {
730 MQ_DBG("[mqtt] TLS handshake failed (%d)\n", h);
731 mq_close();
732 return false;
733 }
734 }
735#endif
736
737 size_t n = mqtt_build_connect(s_mqtt.tx, sizeof(s_mqtt.tx), opts);
738 if (n == 0 || !mq_tx(s_mqtt.tx, n))
739 {
740 mq_close();
741 return false;
742 }
743
744 // Wait for CONNACK.
745 while (!s_mqtt.mqtt_up && s_mqtt.connack_code < 0 && !s_mqtt.closed && (int32_t)(deadline - millis()) > 0)
746 {
747 process_rx();
748 delay(5);
749 }
750 if (!s_mqtt.mqtt_up)
751 {
752 mq_close();
753 return false;
754 }
755 s_mqtt.last_tx_ms = millis();
756 return true;
757}
758
759bool mqtt_publish(const char *topic, const uint8_t *payload, size_t len, uint8_t qos, bool retain)
760{
761 if (!s_mqtt.mqtt_up || qos > 2)
762 return false;
763 if (qos == 0)
764 {
765 size_t n = mqtt_build_publish(s_mqtt.tx, sizeof(s_mqtt.tx), topic, payload, len, 0, 0, retain, false);
766 return n && mq_tx(s_mqtt.tx, n);
767 }
768 // QoS 1/2: take an in-flight slot, store the serialized packet for retransmit.
769 int slot = inflight_find(0);
770 if (slot < 0)
771 for (int i = 0; i < DETWS_MQTT_MAX_INFLIGHT; i++)
772 if (s_mqtt.inflight[i].state == 0)
773 {
774 slot = i;
775 break;
776 }
777 if (slot < 0)
778 return false; // in-flight window full
779 uint16_t pid = next_pid();
780 size_t n = mqtt_build_publish(s_mqtt.inflight[slot].pkt, sizeof(s_mqtt.inflight[slot].pkt), topic, payload, len,
781 qos, pid, retain, false);
782 if (n == 0)
783 return false; // too large for an in-flight slot
784 s_mqtt.inflight[slot].pid = pid;
785 s_mqtt.inflight[slot].state = 1;
786 s_mqtt.inflight[slot].len = (uint16_t)n;
787 s_mqtt.inflight[slot].sent_ms = millis();
788 return mq_tx(s_mqtt.inflight[slot].pkt, n);
789}
790
791bool mqtt_subscribe(const char *topic, uint8_t qos)
792{
793 if (!s_mqtt.mqtt_up)
794 return false;
795 size_t n = mqtt_build_subscribe(s_mqtt.tx, sizeof(s_mqtt.tx), next_pid(), topic, qos);
796 return n && mq_tx(s_mqtt.tx, n);
797}
798
799bool mqtt_unsubscribe(const char *topic)
800{
801 if (!s_mqtt.mqtt_up)
802 return false;
803 size_t n = mqtt_build_unsubscribe(s_mqtt.tx, sizeof(s_mqtt.tx), next_pid(), topic);
804 return n && mq_tx(s_mqtt.tx, n);
805}
806
807bool mqtt_loop()
808{
809 if (!s_mqtt.mqtt_up)
810 return false;
811 process_rx();
812 if (s_mqtt.closed)
813 {
814 mq_close();
815 return false;
816 }
817
818 uint32_t now = millis();
819
820 // Keep-alive: send PINGREQ when idle; drop the link if no PINGRESP comes back.
821 if (s_mqtt.keepalive_s)
822 {
823 uint32_t ka = (uint32_t)s_mqtt.keepalive_s * 1000u;
824 if (s_mqtt.ping_pending && (now - s_mqtt.ping_sent_ms) > ka)
825 {
826 mq_close();
827 return false;
828 }
829 if (!s_mqtt.ping_pending && (now - s_mqtt.last_tx_ms) >= ka)
830 {
831 size_t n = mqtt_build_pingreq(s_mqtt.tx, sizeof(s_mqtt.tx));
832 if (mq_tx(s_mqtt.tx, n))
833 {
834 s_mqtt.ping_pending = true;
835 s_mqtt.ping_sent_ms = now;
836 }
837 }
838 }
839
840 // Retransmit unacked in-flight QoS 1/2 messages.
841 for (int i = 0; i < DETWS_MQTT_MAX_INFLIGHT; i++)
842 {
843 if (s_mqtt.inflight[i].state == 0)
844 continue;
845 if ((now - s_mqtt.inflight[i].sent_ms) < DETWS_MQTT_RETRANSMIT_MS)
846 continue;
847 if (s_mqtt.inflight[i].state == 1)
848 {
849 s_mqtt.inflight[i].pkt[0] |= 0x08; // set DUP on the stored PUBLISH
850 mq_tx(s_mqtt.inflight[i].pkt, s_mqtt.inflight[i].len);
851 }
852 else // state 2: re-send PUBREL
853 {
854 size_t n = mqtt_build_ack(s_mqtt.tx, sizeof(s_mqtt.tx), MqttType::MQTT_PUBREL, s_mqtt.inflight[i].pid);
855 mq_tx(s_mqtt.tx, n);
856 }
857 s_mqtt.inflight[i].sent_ms = now;
858 }
859 return true;
860}
861
862bool mqtt_connected()
863{
864 return s_mqtt.mqtt_up;
865}
866
867void mqtt_disconnect()
868{
869 if (s_mqtt.cid >= 0 && s_mqtt.mqtt_up)
870 {
871 size_t n = mqtt_build_disconnect(s_mqtt.tx, sizeof(s_mqtt.tx));
872 mq_tx(s_mqtt.tx, n);
873 }
874 mq_close();
875}
876
877#else // host build: transport is a stub
878
879void mqtt_on_message(MqttMessageCb)
880{
881}
882bool mqtt_connect(const char *, uint16_t, bool, const MqttConnectOpts *)
883{
884 return false;
885}
886bool mqtt_publish(const char *, const uint8_t *, size_t, uint8_t, bool)
887{
888 return false;
889}
890bool mqtt_subscribe(const char *, uint8_t)
891{
892 return false;
893}
894bool mqtt_unsubscribe(const char *)
895{
896 return false;
897}
898bool mqtt_loop()
899{
900 return false;
901}
902bool mqtt_connected()
903{
904 return false;
905}
906void mqtt_disconnect()
907{
908}
909
910#endif // ARDUINO
911
912#endif // DETWS_ENABLE_MQTT
#define DETWS_MQTT_RX_QOS2_SLOTS
Inbound QoS 2 packet-id de-duplication ring depth (PUBREC-acknowledged, awaiting PUBREL).
#define DETWS_MQTT_BUF_SIZE
MQTT packet buffer size in bytes (bounds one outgoing/incoming packet).
#define DETWS_MQTT_INFLIGHT_BUF
Stored-packet size per in-flight QoS 1/2 slot (caps a retransmittable PUBLISH).
#define DETWS_MQTT_RETRANSMIT_MS
Retransmit timeout (ms) for an unacknowledged in-flight QoS 1/2 message.
#define DETWS_MQTT_MAX_INFLIGHT
Outbound QoS 1/2 in-flight slots (unacknowledged messages held for DUP retransmit).
#define DETWS_MQTT_MAX_TOPIC
Maximum inbound MQTT topic length (including NUL) delivered to the callback.
bool det_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:318
int det_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:310
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:330
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:334
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:322
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Zero-heap MQTT 3.1.1 publish/subscribe client (DETWS_ENABLE_MQTT).
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).
Strict UTF-8 validation (RFC 3629), one shared copy.
bool det_utf8_valid(const uint8_t *s, size_t n)
True if [s, s+n) is well-formed UTF-8.
Definition utf8.h:29