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