aboutsummaryrefslogtreecommitdiff
path: root/mqtt-wire-format.md
diff options
context:
space:
mode:
Diffstat (limited to 'mqtt-wire-format.md')
-rw-r--r--mqtt-wire-format.md1089
1 files changed, 1089 insertions, 0 deletions
diff --git a/mqtt-wire-format.md b/mqtt-wire-format.md
new file mode 100644
index 0000000..b4d4f65
--- /dev/null
+++ b/mqtt-wire-format.md
@@ -0,0 +1,1089 @@
1# MQTT Wire Format for Home Assistant
2
3This document covers the essential MQTT wire format details needed for implementing Home Assistant's MQTT integration.
4
5## Overview
6
7MQTT uses a binary protocol over TCP. Each message consists of:
81. **Fixed Header** (always present)
92. **Variable Header** (present in most message types)
103. **Payload** (optional, depends on message type)
11
12## Message Flow: When to Wait for Responses
13
14Understanding which messages require broker responses is critical for implementing the protocol correctly.
15
16### Client-Initiated Messages
17
18| Client Sends | Broker Responds | Must Wait? | Notes |
19| --------------------------------------- | --------------------------------------- | --------------------------------------- | --------------------------------------- |
20| CONNECT | CONNACK | ✅ Yes | Must wait before sending other messages |
21| PUBLISH QoS 0 | *(none)* | ❌ No | Fire and forget |
22| PUBLISH QoS 1 | PUBACK | ✅ Yes | Wait for acknowledgment |
23| PUBLISH QoS 2 | PUBREC → PUBREL → PUBCOMP | ✅ Yes | Four-way handshake |
24| SUBSCRIBE | SUBACK | ✅ Yes | Contains subscription result codes |
25| UNSUBSCRIBE | UNSUBACK | ✅ Yes | Confirms unsubscription |
26| PINGREQ | PINGRESP | ✅ Yes | Keep-alive mechanism |
27| DISCONNECT | *(none)* | ❌ No | Graceful shutdown |
28
29
30### Broker-Initiated Messages
31
32The broker can send these messages to the client at any time:
33
34| Broker Sends | When | Client Action |
35|--------------|------|---------------|
36| PUBLISH | When a message arrives on a subscribed topic | Send PUBACK if QoS 1, or PUBREC if QoS 2 |
37| PINGRESP | In response to PINGREQ | No further action needed |
38
39### Implementation Notes
40
411. **CONNECT**: Always the first message. Block until CONNACK received before sending anything else.
42
432. **PUBLISH QoS 0**: Most common for Home Assistant. Send and continue immediately.
44
453. **PUBLISH QoS 1**: For important messages (e.g., config updates). Wait for PUBACK with matching Packet ID.
46
474. **SUBSCRIBE**: Wait for SUBACK to confirm subscriptions were accepted before assuming you'll receive messages.
48
495. **PINGREQ**: Send when no other messages have been sent during keep-alive period. Wait for PINGRESP.
50
516. **Receiving PUBLISH**: When broker sends PUBLISH with QoS 1, you must respond with PUBACK containing the same Packet ID.
52
53## Fixed Header Format
54
55Every MQTT message starts with a 2+ byte fixed header:
56
57```
58Byte 1: Control Packet Type + Flags
59┌────────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
60│ Bit │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │
61├────────┼─────┴─────┴─────┴─────┼─────┴─────┴─────┴─────┤
62│ Field │ Message Type (4) │ Flags (4) │
63└────────┴───────────────────────┴───────────────────────┘
64
65Byte 2+: Remaining Length (1-4 bytes, variable length encoding)
66```
67
68### Message Types
69- `1` = CONNECT
70- `2` = CONNACK
71- `3` = PUBLISH
72- `4` = PUBACK
73- `8` = SUBSCRIBE
74- `9` = SUBACK
75- `10` = UNSUBSCRIBE
76- `11` = UNSUBACK
77- `12` = PINGREQ
78- `13` = PINGRESP
79- `14` = DISCONNECT
80
81### Fixed Header Flags (Bits 0-3)
82
83The meaning of the 4 flag bits depends on the message type.
84
85#### PUBLISH Flags (Message Type 3)
86
87For PUBLISH messages, all 4 flag bits are used:
88
89```
90Bit 3: DUP (Duplicate delivery)
91Bit 2-1: QoS level (2 bits)
92Bit 0: RETAIN
93```
94
95**Bit 3 - DUP (Duplicate)**:
96- `0` = First delivery attempt
97- `1` = Message is being re-delivered (only for QoS > 0)
98
99**Bits 2-1 - QoS Level**:
100- `00` (0) = At most once (fire and forget)
101- `01` (1) = At least once (acknowledged)
102- `10` (2) = Exactly once (four-way handshake)
103- `11` (3) = Reserved (invalid)
104
105**Bit 0 - RETAIN**:
106- `0` = Normal message
107- `1` = Broker should retain this message for new subscribers
108
109**Examples**:
110```
1110x30 = 0011 0000 = PUBLISH, DUP=0, QoS=0, RETAIN=0
1120x31 = 0011 0001 = PUBLISH, DUP=0, QoS=0, RETAIN=1 (retained message)
1130x32 = 0011 0010 = PUBLISH, DUP=0, QoS=1, RETAIN=0
1140x33 = 0011 0011 = PUBLISH, DUP=0, QoS=1, RETAIN=1
1150x34 = 0011 0100 = PUBLISH, DUP=0, QoS=2, RETAIN=0
1160x3A = 0011 1010 = PUBLISH, DUP=1, QoS=1, RETAIN=0 (re-delivery)
117```
118
119#### SUBSCRIBE Flags (Message Type 8)
120
121For SUBSCRIBE, flags MUST be `0010` (bit 1 = 1):
122
123```
124Fixed header byte 1: 0x82 = 1000 0010
125```
126
127This is mandated by the MQTT specification. Other flag values are protocol violations.
128
129#### UNSUBSCRIBE Flags (Message Type 10)
130
131For UNSUBSCRIBE, flags MUST be `0010` (bit 1 = 1):
132
133```
134Fixed header byte 1: 0xA2 = 1010 0010
135```
136
137#### All Other Message Types
138
139For all other message types (CONNECT, CONNACK, PUBACK, SUBACK, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT), the flags MUST be `0000`:
140
141```
142CONNECT: 0x10 = 0001 0000
143CONNACK: 0x20 = 0010 0000
144PUBACK: 0x40 = 0100 0000
145SUBACK: 0x90 = 1001 0000
146UNSUBACK: 0xB0 = 1011 0000
147PINGREQ: 0xC0 = 1100 0000
148PINGRESP: 0xD0 = 1101 0000
149DISCONNECT: 0xE0 = 1110 0000
150```
151
152Any other flag values for these message types are protocol violations.
153
154### Summary Table
155
156| Message Type | Required Flags | Flag Meaning |
157|--------------|----------------|--------------|
158| CONNECT (1) | `0000` | Reserved |
159| CONNACK (2) | `0000` | Reserved |
160| **PUBLISH (3)** | **`DQQR`** | **D=DUP, QQ=QoS, R=RETAIN** |
161| PUBACK (4) | `0000` | Reserved |
162| SUBSCRIBE (8) | `0010` | Fixed |
163| SUBACK (9) | `0000` | Reserved |
164| UNSUBSCRIBE (10) | `0010` | Fixed |
165| UNSUBACK (11) | `0000` | Reserved |
166| PINGREQ (12) | `0000` | Reserved |
167| PINGRESP (13) | `0000` | Reserved |
168| DISCONNECT (14) | `0000` | Reserved |
169
170**Key Point**: Only PUBLISH messages use flags meaningfully. SUBSCRIBE and UNSUBSCRIBE have fixed flag values, and all other message types must have flags set to 0.
171
172### Remaining Length Encoding
173
174The remaining length uses a variable-length encoding (1-4 bytes):
175- Each byte encodes 7 bits of data and 1 continuation bit
176- Bit 7 = continuation bit (1 = more bytes follow, 0 = last byte)
177- Bits 6-0 = length value
178
179Example decoding:
180```
1810x7F = 127 bytes
1820x80 0x01 = 128 bytes
1830xFF 0x7F = 16,383 bytes
1840xFF 0xFF 0x7F = 2,097,151 bytes
185```
186
187## Data Types
188
189### UTF-8 String
190
191**Important**: String length is a **fixed 2-byte unsigned integer (u16)**, NOT a variable-length integer like the Remaining Length field.
192
193#### Format
194```
195┌──────────────┬──────────────────┬─────────────────────────┐
196│ Length MSB │ Length LSB │ UTF-8 Encoded String │
197│ (byte 0) │ (byte 1) │ (N bytes) │
198└──────────────┴──────────────────┴─────────────────────────┘
199 ↑──── u16 big-endian ────↑
200
201Total size: 2 bytes + string length
202Maximum string length: 65,535 bytes (0xFFFF)
203```
204
205#### Encoding Details
206- **Length prefix**: 2 bytes, unsigned 16-bit integer
207- **Byte order**: Big-endian (MSB first, then LSB)
208- **String encoding**: UTF-8
209- **Maximum length**: 65,535 bytes (not 65,536 - zero-length strings are allowed)
210- This is **different** from the variable-length encoding used for "Remaining Length"
211
212#### Examples
213
214**Example 1: "MQTT" (4 bytes)**
215```
2160x00 0x04 'M' 'Q' 'T' 'T'
217
218Breakdown:
2190x00 0x04 = length 4 (u16 big-endian)
220'M' 'Q' 'T' 'T' = 0x4D 0x51 0x54 0x54
221```
222
223**Example 2: "ha-client" (9 bytes)**
224```
2250x00 0x09 'h' 'a' '-' 'c' 'l' 'i' 'e' 'n' 't'
226
227Breakdown:
2280x00 0x09 = length 9
229```
230
231**Example 3: "homeassistant/sensor/temperature/state" (39 bytes)**
232```
2330x00 0x27 'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' ...
234
235Breakdown:
2360x00 0x27 = length 39 (0x27 = 39 in decimal)
237```
238
239**Example 4: Empty string (0 bytes)**
240```
2410x00 0x00
242
243Breakdown:
2440x00 0x00 = length 0 (valid in MQTT)
245```
246
247**Example 5: Long string (300 bytes)**
248```
2490x01 0x2C ...300 bytes of UTF-8 data...
250
251Breakdown:
2520x01 0x2C = length 300 (256 + 44 = 300)
253```
254
255#### Parsing String in Rust
256```rust
257fn parse_mqtt_string(buf: &[u8]) -> Result<(&str, &[u8])> {
258 // Read u16 big-endian length
259 if buf.len() < 2 {
260 return Err(Error::InsufficientData);
261 }
262 let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
263
264 // Read string data
265 if buf.len() < 2 + len {
266 return Err(Error::InsufficientData);
267 }
268 let string_bytes = &buf[2..2 + len];
269 let string = core::str::from_utf8(string_bytes)?;
270 let remaining = &buf[2 + len..];
271
272 Ok((string, remaining))
273}
274```
275
276#### Writing String in Rust
277```rust
278fn write_mqtt_string(buf: &mut [u8], s: &str) -> Result<usize> {
279 let len = s.len();
280 if len > 65535 {
281 return Err(Error::StringTooLong);
282 }
283 if buf.len() < 2 + len {
284 return Err(Error::InsufficientBuffer);
285 }
286
287 // Write u16 big-endian length
288 buf[0] = (len >> 8) as u8; // MSB
289 buf[1] = (len & 0xFF) as u8; // LSB
290
291 // Write string bytes
292 buf[2..2 + len].copy_from_slice(s.as_bytes());
293
294 Ok(2 + len)
295}
296```
297
298### Binary Data
299Same format as UTF-8 string (2-byte u16 length prefix + data), but contains raw binary data instead of UTF-8 text. Used for passwords and message payloads.
300
301## Building MQTT Messages: Handling the Length Challenge
302
303### The Problem
304
305The fixed header contains the "Remaining Length" field which specifies the number of bytes following the fixed header. But you need to write the fixed header first, before you know how long the rest of the message will be.
306
307### Solution Approaches
308
309#### Approach 1: Calculate Length First (Recommended for Embedded)
310
311Calculate the message size before writing anything. This avoids buffering the entire message.
312
313```rust
314// Step 1: Calculate sizes
315let client_id = "ha-sensor";
316let topic = "homeassistant/status";
317
318let client_id_len = 2 + client_id.len(); // u16 prefix + string
319let topic_len = 2 + topic.len();
320
321// For CONNECT: protocol name + level + flags + keep-alive + payload
322let remaining_length =
323 (2 + 4) + // Protocol name "MQTT" (2 byte len + 4 bytes)
324 1 + // Protocol level
325 1 + // Connect flags
326 2 + // Keep-alive
327 client_id_len; // Client ID
328
329// Step 2: Write fixed header with calculated length
330let control_byte = 0x10; // CONNECT
331write_u8(buf, control_byte);
332write_varint(buf, remaining_length);
333
334// Step 3: Write variable header and payload
335write_mqtt_string(buf, "MQTT");
336write_u8(buf, 0x04); // Protocol level
337write_u8(buf, 0x02); // Connect flags
338write_u16_be(buf, 60); // Keep-alive
339write_mqtt_string(buf, client_id);
340```
341
342**Pros**:
343- No buffering needed (good for embedded)
344- Direct write to TCP socket
345- Minimal memory usage
346
347**Cons**:
348- Must calculate sizes carefully
349- Easy to make mistakes
350
351#### Approach 2: Reserve Space and Backfill
352
353Reserve maximum space for the header (5 bytes), write the message, then go back and fill in the actual length.
354
355```rust
356fn build_publish(buf: &mut [u8], topic: &str, payload: &[u8]) -> usize {
357 let mut pos = 0;
358
359 // Step 1: Reserve space for fixed header (max 5 bytes)
360 let header_start = pos;
361 pos += 5; // Maximum: 1 byte control + 4 bytes remaining length
362
363 // Step 2: Write variable header and payload
364 let payload_start = pos;
365 pos += write_mqtt_string(&mut buf[pos..], topic);
366 buf[pos..pos + payload.len()].copy_from_slice(payload);
367 pos += payload.len();
368
369 // Step 3: Calculate actual remaining length
370 let remaining_length = pos - payload_start;
371
372 // Step 4: Encode remaining length to temp buffer
373 let mut temp = [0u8; 4];
374 let varint_len = encode_varint(remaining_length as u32, &mut temp);
375
376 // Step 5: Backfill header (shift if needed)
377 let actual_header_len = 1 + varint_len;
378 let shift = 5 - actual_header_len;
379 if shift > 0 {
380 // Shift payload left to remove unused header bytes
381 buf.copy_within(payload_start..pos, payload_start - shift);
382 pos -= shift;
383 }
384
385 // Step 6: Write actual header
386 buf[0] = 0x30; // PUBLISH QoS 0
387 buf[1..1 + varint_len].copy_from_slice(&temp[..varint_len]);
388
389 pos
390}
391```
392
393**Pros**:
394- Single pass through data
395- No size calculation needed upfront
396
397**Cons**:
398- May need to shift data (costly on embedded)
399- Wastes up to 4 bytes initially
400- More complex
401
402#### Approach 3: Two-Buffer Strategy
403
404Write payload to one buffer, then construct final message.
405
406```rust
407fn build_connect(out: &mut [u8]) -> usize {
408 let mut payload_buf = [0u8; 256];
409 let mut payload_pos = 0;
410
411 // Step 1: Build variable header + payload in temp buffer
412 payload_pos += write_mqtt_string(&mut payload_buf[payload_pos..], "MQTT");
413 payload_buf[payload_pos] = 0x04; payload_pos += 1; // Protocol level
414 payload_buf[payload_pos] = 0x02; payload_pos += 1; // Connect flags
415 payload_pos += write_u16_be(&mut payload_buf[payload_pos..], 60); // Keep-alive
416 payload_pos += write_mqtt_string(&mut payload_buf[payload_pos..], "ha-client");
417
418 let remaining_length = payload_pos;
419
420 // Step 2: Write fixed header to output
421 let mut pos = 0;
422 out[pos] = 0x10; pos += 1; // CONNECT
423 pos += write_varint(&mut out[pos..], remaining_length as u32);
424
425 // Step 3: Copy payload to output
426 out[pos..pos + payload_pos].copy_from_slice(&payload_buf[..payload_pos]);
427 pos + payload_pos
428}
429```
430
431**Pros**:
432- Simple and clear
433- No backfilling needed
434
435**Cons**:
436- Uses 2x memory (bad for embedded)
437- Extra copy operation
438
439#### Approach 4: Builder Pattern with Deferred Write
440
441Build a message description, calculate size, then serialize.
442
443```rust
444struct ConnectMessage<'a> {
445 client_id: &'a str,
446 clean_session: bool,
447 keep_alive: u16,
448}
449
450impl<'a> ConnectMessage<'a> {
451 fn calculate_size(&self) -> usize {
452 let mut size = 0;
453 size += 2 + 4; // "MQTT"
454 size += 1; // Protocol level
455 size += 1; // Connect flags
456 size += 2; // Keep-alive
457 size += 2 + self.client_id.len(); // Client ID
458 size
459 }
460
461 fn serialize(&self, buf: &mut [u8]) -> usize {
462 let remaining_length = self.calculate_size();
463 let mut pos = 0;
464
465 // Write fixed header
466 buf[pos] = 0x10; pos += 1;
467 pos += write_varint(&mut buf[pos..], remaining_length as u32);
468
469 // Write variable header + payload
470 pos += write_mqtt_string(&mut buf[pos..], "MQTT");
471 buf[pos] = 0x04; pos += 1;
472 let flags = if self.clean_session { 0x02 } else { 0x00 };
473 buf[pos] = flags; pos += 1;
474 pos += write_u16_be(&mut buf[pos..], self.keep_alive);
475 pos += write_mqtt_string(&mut buf[pos..], self.client_id);
476
477 pos
478 }
479}
480```
481
482**Pros**:
483- Clean API
484- Size calculation is explicit and testable
485- No wasted space or copying
486
487**Cons**:
488- More code
489- Need to keep calculation in sync with serialization
490
491### Recommended Approach for Embedded Systems
492
493For embedded systems like RP2350 with Embassy:
494
495**Use Approach 1 (Calculate First) or Approach 4 (Builder Pattern)**
496
497```rust
498// Example: Helper to calculate MQTT string size
499fn mqtt_string_size(s: &str) -> usize {
500 2 + s.len()
501}
502
503// Example: Build PUBLISH message
504fn build_publish_qos0(
505 buf: &mut [u8],
506 topic: &str,
507 payload: &[u8]
508) -> usize {
509 // Calculate remaining length
510 let remaining_length =
511 mqtt_string_size(topic) + // Topic
512 payload.len(); // Payload
513
514 let mut pos = 0;
515
516 // Write fixed header
517 buf[pos] = 0x30; pos += 1; // PUBLISH QoS 0
518 pos += write_varint(&mut buf[pos..], remaining_length as u32);
519
520 // Write variable header
521 pos += write_mqtt_string(&mut buf[pos..], topic);
522
523 // Write payload
524 buf[pos..pos + payload.len()].copy_from_slice(payload);
525 pos += payload.len();
526
527 pos
528}
529```
530
531### Key Tips
532
5331. **Create size calculation helpers**: Make functions like `mqtt_string_size()`, `connect_size()` to avoid mistakes
534
5352. **Maximum varint size**: The remaining length varint can be 1-4 bytes. For small messages (<128 bytes), it's always 1 byte. This is predictable.
536
5373. **QoS 0 PUBLISH is simplest**: No packet identifier needed, making size calculation trivial
538
5394. **Test your calculations**: Write unit tests comparing calculated size vs actual serialized size
540
5415. **Consider const generics**: For fixed message types, you can calculate sizes at compile time
542
543## CONNECT Message
544
545Client initiates connection to broker.
546
547### Fixed Header
548```
549Byte 1: 0x10 (Message Type = 1, Flags = 0)
550Byte 2+: Remaining Length
551```
552
553### Variable Header
554```
555┌─────────────────────────────────────┐
556│ Protocol Name (UTF-8 String) │
557│ "MQTT" (0x00 0x04 0x4D 0x51 0x54 0x54) for MQTT 3.1.1
558├─────────────────────────────────────┤
559│ Protocol Level (1 byte) │
560│ 0x04 for MQTT 3.1.1 │
561│ 0x05 for MQTT 5.0 │
562├─────────────────────────────────────┤
563│ Connect Flags (1 byte) │
564│ Bit 7: User Name Flag │
565│ Bit 6: Password Flag │
566│ Bit 5: Will Retain │
567│ Bit 4-3: Will QoS (2 bits) │
568│ Bit 2: Will Flag │
569│ Bit 1: Clean Session (v3.1.1) │
570│ Clean Start (v5.0) │
571│ Bit 0: Reserved (must be 0) │
572├─────────────────────────────────────┤
573│ Keep Alive (2 bytes, MSB first) │
574│ Seconds, 0 = disabled │
575└─────────────────────────────────────┘
576```
577
578### Payload (in order)
5791. **Client ID** (UTF-8 String) - Required
5802. **Will Topic** (UTF-8 String) - If Will Flag = 1
5813. **Will Payload** (Binary Data) - If Will Flag = 1
5824. **User Name** (UTF-8 String) - If User Name Flag = 1
5835. **Password** (Binary Data) - If Password Flag = 1
584
585
586#### Connect Flags Breakdown
587
588The Connect Flags byte (byte 10 of CONNECT message) controls authentication and the Last Will and Testament.
589
590**Last Will and Testament (LWT)**: A message the broker will automatically publish if the client disconnects unexpectedly (network failure, crash, etc.). This is useful for Home Assistant to detect when a device goes offline.
591
592**Connect Flags bit layout:**
593```
594Bit: 7 6 5 4 3 2 1 0
595 ┌──────┬──────┬──────┬──────┬──────┬──────┬──────────────┬──────┐
596 │ User │ Pass │ Will │ Will QoS │ Will │ Clean │ Res. │
597 │ Name │ word │Retain│ │ Flag │ Session │ (0) │
598 └──────┴──────┴──────┴──────┴──────┴──────┴──────────────┴──────┘
599```
600
601**Bits 4-3: Will QoS** (only applies if Bit 2 Will Flag = 1):
602- `00` (0) = QoS 0 for Will message (fire and forget)
603- `01` (1) = QoS 1 for Will message (acknowledged)
604- `10` (2) = QoS 2 for Will message (exactly once)
605- `11` (3) = Invalid, must not be used
606
607**Important**: If Will Flag (bit 2) = 0, then Will QoS MUST be set to 00 and Will Retain (bit 5) MUST be 0.
608
609#### Connect Flags Examples
610
611**Example 1: Minimal connection (no Will, no auth)**
612```
613Binary: 0000 0010
614Hex: 0x02
615- User Name Flag: 0 (no username)
616- Password Flag: 0 (no password)
617- Will Retain: 0
618- Will QoS: 00 (not used, Will Flag = 0)
619- Will Flag: 0 (no Will message)
620- Clean Session: 1
621- Reserved: 0
622```
623
624**Example 2: With Last Will (QoS 0, no retain)**
625```
626Binary: 0000 0110
627Hex: 0x06
628- User Name Flag: 0
629- Password Flag: 0
630- Will Retain: 0 (don't retain Will message)
631- Will QoS: 00 (QoS 0 for Will)
632- Will Flag: 1 (Will message enabled)
633- Clean Session: 1
634- Reserved: 0
635
636Must include Will Topic and Will Payload in CONNECT payload.
637```
638
639**Example 3: With Last Will (QoS 1, retain)**
640```
641Binary: 0011 0110
642Hex: 0x36
643- User Name Flag: 0
644- Password Flag: 0
645- Will Retain: 1 (broker retains Will message)
646- Will QoS: 01 (QoS 1 for Will)
647- Will Flag: 1 (Will message enabled)
648- Clean Session: 1
649- Reserved: 0
650
651Will message will be retained and delivered with QoS 1.
652```
653
654**Example 4: With authentication and Last Will**
655```
656Binary: 1100 0110
657Hex: 0xC6
658- User Name Flag: 1 (username provided)
659- Password Flag: 1 (password provided)
660- Will Retain: 0
661- Will QoS: 00 (QoS 0 for Will)
662- Will Flag: 1 (Will message enabled)
663- Clean Session: 1
664- Reserved: 0
665
666Must include Username, Password, Will Topic, and Will Payload in payload.
667```
668
669### Example CONNECT (Simple)
670```
671Client ID: "ha-client"
672Clean Session: true
673Keep Alive: 60 seconds
674No Will, No Auth
675
6760x10 // Fixed header: CONNECT
6770x17 // Remaining length: 23 bytes
6780x00 0x04 'M' 'Q' 'T' 'T' // Protocol name
6790x04 // Protocol level: 3.1.1
6800x02 // Connect flags: 0000 0010 = Clean Session only
6810x00 0x3C // Keep Alive: 60 seconds
6820x00 0x09 'h' 'a' '-' 'c' 'l' 'i' 'e' 'n' 't' // Client ID: "ha-client"
683```
684
685### Example CONNECT (With Last Will)
686```
687Client ID: "sensor1"
688Clean Session: true
689Keep Alive: 60 seconds
690Will Topic: "homeassistant/sensor1/availability"
691Will Payload: "offline"
692Will QoS: 0
693Will Retain: true
694
6950x10 // Fixed header: CONNECT
6960x3C // Remaining length: 60 bytes
6970x00 0x04 'M' 'Q' 'T' 'T' // Protocol name: "MQTT"
6980x04 // Protocol level: 3.1.1
6990x26 // Connect flags: 0010 0110
700 // Bit 5: Will Retain = 1
701 // Bit 4-3: Will QoS = 00
702 // Bit 2: Will Flag = 1
703 // Bit 1: Clean Session = 1
7040x00 0x3C // Keep Alive: 60 seconds
7050x00 0x07 's' 'e' 'n' 's' 'o' 'r' '1' // Client ID: "sensor1"
7060x00 0x25 // Will Topic length: 37 bytes
707'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' 'a' 'n' 't' '/'
708's' 'e' 'n' 's' 'o' 'r' '1' '/'
709'a' 'v' 'a' 'i' 'l' 'a' 'b' 'i' 'l' 'i' 't' 'y'
7100x00 0x07 // Will Payload length: 7 bytes
711'o' 'f' 'f' 'l' 'i' 'n' 'e' // Will Payload: "offline"
712```
713
714When this client disconnects unexpectedly, the broker will publish:
715- Topic: `homeassistant/sensor1/availability`
716- Payload: `offline`
717- QoS: 0
718- Retained: Yes (so Home Assistant sees it even if it reconnects later)
719
720## DISCONNECT Message
721
722Client graceful disconnect (MQTT 3.1.1).
723
724### Fixed Header
725```
726Byte 1: 0xE0 (Message Type = 14, Flags = 0)
727Byte 2: 0x00 (Remaining Length = 0)
728```
729
730No Variable Header or Payload in MQTT 3.1.1.
731
732### Complete Message
733```
7340xE0 0x00
735```
736
737## PUBLISH Message
738
739Publish message to a topic.
740
741### Fixed Header
742```
743Byte 1: 0x3X (Message Type = 3, Flags in bits 0-3)
744 Bit 3: DUP flag (duplicate delivery)
745 Bit 2-1: QoS level (00, 01, or 10)
746 Bit 0: RETAIN flag
747
748Examples:
749 0x30 = PUBLISH, QoS 0, no retain
750 0x31 = PUBLISH, QoS 0, retain
751 0x32 = PUBLISH, QoS 1, no retain
752 0x34 = PUBLISH, QoS 2, no retain
753
754Byte 2+: Remaining Length
755```
756
757### Variable Header
758```
759┌─────────────────────────────────────┐
760│ Topic Name (UTF-8 String) │
761├─────────────────────────────────────┤
762│ Packet Identifier (2 bytes) │
763│ Only present if QoS > 0 │
764└─────────────────────────────────────┘
765```
766
767### Payload
768Raw message data (can be text or binary).
769
770### Example PUBLISH
771```
772Topic: "homeassistant/sensor/temp/state"
773Payload: "23.5"
774QoS: 0
775Retain: false
776
7770x30 // Fixed header: PUBLISH, QoS 0
7780x29 // Remaining length: 41 bytes
7790x00 0x23 // Topic length: 35 bytes
780'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' 'a' 'n' 't' '/'
781's' 'e' 'n' 's' 'o' 'r' '/' 't' 'e' 'm' 'p' '/'
782's' 't' 'a' 't' 'e'
783'2' '3' '.' '5' // Payload: "23.5"
784```
785
786### Example PUBLISH (QoS 1)
787```
788Topic: "homeassistant/switch/state"
789Payload: "ON"
790QoS: 1
791Packet ID: 1
792
7930x32 // Fixed header: PUBLISH, QoS 1
7940x1F // Remaining length
7950x00 0x1B // Topic length: 27 bytes
796'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' 'a' 'n' 't' '/'
797's' 'w' 'i' 't' 'c' 'h' '/' 's' 't' 'a' 't' 'e'
7980x00 0x01 // Packet Identifier: 1
799'O' 'N' // Payload
800```
801
802## SUBSCRIBE Message
803
804Subscribe to one or more topics.
805
806### Fixed Header
807```
808Byte 1: 0x82 (Message Type = 8, Flags = 0010)
809 Bit 1 MUST be 1 for SUBSCRIBE
810Byte 2+: Remaining Length
811```
812
813### Variable Header
814```
815┌─────────────────────────────────────┐
816│ Packet Identifier (2 bytes, MSB) │
817└─────────────────────────────────────┘
818```
819
820### Payload
821List of topic filters with QoS levels:
822```
823For each subscription:
824┌─────────────────────────────────────┐
825│ Topic Filter (UTF-8 String) │
826│ Supports wildcards: │
827│ + = single level wildcard │
828│ # = multi level wildcard │
829├─────────────────────────────────────┤
830│ QoS (1 byte) │
831│ Bits 7-2: Reserved (must be 0) │
832│ Bits 1-0: QoS level (0, 1, or 2) │
833└─────────────────────────────────────┘
834```
835
836### Example SUBSCRIBE
837```
838Subscribe to: "homeassistant/#" (all topics under homeassistant/)
839QoS: 0
840Packet ID: 2
841
8420x82 // Fixed header: SUBSCRIBE
8430x13 // Remaining length: 19 bytes
8440x00 0x02 // Packet Identifier: 2
8450x00 0x0F // Topic length: 15 bytes
846'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' 'a' 'n' 't' '/' '#'
8470x00 // QoS: 0
848```
849
850### Example SUBSCRIBE (Multiple Topics)
851```
852Subscribe to:
853 1. "homeassistant/status" QoS 0
854 2. "homeassistant/+/state" QoS 1
855Packet ID: 3
856
8570x82 // Fixed header
8580x37 // Remaining length
8590x00 0x03 // Packet Identifier: 3
8600x00 0x16 // Topic 1 length: 22 bytes
861'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' 'a' 'n' 't' '/'
862's' 't' 'a' 't' 'u' 's'
8630x00 // QoS: 0
8640x00 0x16 // Topic 2 length: 22 bytes
865'h' 'o' 'm' 'e' 'a' 's' 's' 'i' 's' 't' 'a' 'n' 't' '/'
866'+' '/' 's' 't' 'a' 't' 'e'
8670x01 // QoS: 1
868```
869
870## QoS Levels
871
872Home Assistant typically uses:
873- **QoS 0**: At most once delivery (fire and forget)
874- **QoS 1**: At least once delivery (requires PUBACK)
875- **QoS 2**: Exactly once delivery (requires PUBREC, PUBREL, PUBCOMP)
876
877For most Home Assistant integrations, QoS 0 is sufficient. Use QoS 1 for important state changes.
878
879## Topic Wildcards
880
881- `+` (single-level): Matches one topic level
882 - `homeassistant/+/state` matches `homeassistant/sensor/state` and `homeassistant/switch/state`
883- `#` (multi-level): Matches zero or more levels, must be last character
884 - `homeassistant/#` matches everything under `homeassistant/`
885
886## Home Assistant Topic Structure
887
888Common Home Assistant MQTT topics:
889```
890homeassistant/<component>/<node_id>/<object_id>/config
891homeassistant/<component>/<node_id>/<object_id>/state
892homeassistant/<component>/<node_id>/<object_id>/command
893homeassistant/status
894```
895
896Example:
897```
898homeassistant/sensor/living_room/temperature/config
899homeassistant/sensor/living_room/temperature/state
900homeassistant/switch/bedroom/light/config
901homeassistant/switch/bedroom/light/state
902homeassistant/switch/bedroom/light/command
903```
904
905## Typical Flow for Home Assistant Device
906
9071. **Connect** to broker
9082. **Subscribe** to:
909 - `homeassistant/status` (to detect HA restarts)
910 - Command topics for your devices (e.g., `homeassistant/switch/+/command`)
9113. **Publish** discovery configs to `homeassistant/.../config` with retain=true
9124. **Publish** state updates to `homeassistant/.../state`
9135. **Receive** commands on subscribed topics
9146. On disconnect, broker sends Will message if configured
915
916## Keep-Alive and PINGREQ/PINGRESP
917
918If no messages are sent within keep-alive period:
919- Client sends PINGREQ: `0xC0 0x00`
920- Broker responds with PINGRESP: `0xD0 0x00`
921
922If broker doesn't receive any message within 1.5x keep-alive, it disconnects the client.
923
924## Broker Response Messages
925
926These are messages the broker sends back to the client that you need to parse.
927
928### CONNACK (Connection Acknowledgment)
929
930Broker response to CONNECT.
931
932#### Fixed Header
933```
934Byte 1: 0x20 (Message Type = 2, Flags = 0)
935Byte 2: 0x02 (Remaining Length = 2)
936```
937
938#### Variable Header
939```
940┌─────────────────────────────────────┐
941│ Connect Acknowledge Flags (1 byte) │
942│ Bit 0: Session Present │
943│ Bits 7-1: Reserved (must be 0) │
944├─────────────────────────────────────┤
945│ Connect Return Code (1 byte) │
946│ 0x00 = Connection Accepted │
947│ 0x01 = Unacceptable protocol ver │
948│ 0x02 = Identifier rejected │
949│ 0x03 = Server unavailable │
950│ 0x04 = Bad username/password │
951│ 0x05 = Not authorized │
952└─────────────────────────────────────┘
953```
954
955#### Example CONNACK (Success)
956```
9570x20 0x02 // Fixed header
9580x00 // Session Present = 0
9590x00 // Return code = 0 (accepted)
960```
961
962#### Example CONNACK (Connection Refused)
963```
9640x20 0x02 // Fixed header
9650x00 // Session Present = 0
9660x05 // Return code = 5 (not authorized)
967```
968
969**Implementation**: Check return code is 0x00 before proceeding. If non-zero, connection failed.
970
971---
972
973### PUBACK (Publish Acknowledgment)
974
975Broker response to PUBLISH with QoS 1.
976
977#### Fixed Header
978```
979Byte 1: 0x40 (Message Type = 4, Flags = 0)
980Byte 2: 0x02 (Remaining Length = 2)
981```
982
983#### Variable Header
984```
985┌─────────────────────────────────────┐
986│ Packet Identifier (2 bytes, MSB) │
987│ Must match the PUBLISH packet ID │
988└─────────────────────────────────────┘
989```
990
991#### Example PUBACK
992```
993For PUBLISH with Packet ID = 42 (0x002A):
994
9950x40 0x02 // Fixed header
9960x00 0x2A // Packet Identifier: 42
997```
998
999**Implementation**: Match Packet ID with the PUBLISH you sent. Once received, the message is acknowledged.
1000
1001---
1002
1003### SUBACK (Subscribe Acknowledgment)
1004
1005Broker response to SUBSCRIBE.
1006
1007#### Fixed Header
1008```
1009Byte 1: 0x90 (Message Type = 9, Flags = 0)
1010Byte 2+: Remaining Length
1011```
1012
1013#### Variable Header
1014```
1015┌─────────────────────────────────────┐
1016│ Packet Identifier (2 bytes, MSB) │
1017│ Must match the SUBSCRIBE packet ID │
1018└─────────────────────────────────────┘
1019```
1020
1021#### Payload
1022One return code for each topic filter in the SUBSCRIBE request:
1023```
1024┌─────────────────────────────────────┐
1025│ Return Code (1 byte per topic) │
1026│ 0x00 = Success, QoS 0 │
1027│ 0x01 = Success, QoS 1 │
1028│ 0x02 = Success, QoS 2 │
1029│ 0x80 = Failure │
1030└─────────────────────────────────────┘
1031```
1032
1033#### Example SUBACK (Single Topic)
1034```
1035For SUBSCRIBE with Packet ID = 2, one topic requesting QoS 0:
1036
10370x90 0x03 // Fixed header, remaining = 3 bytes
10380x00 0x02 // Packet Identifier: 2
10390x00 // Return code: Success, QoS 0
1040```
1041
1042#### Example SUBACK (Multiple Topics)
1043```
1044For SUBSCRIBE with Packet ID = 3, two topics:
1045
10460x90 0x04 // Fixed header, remaining = 4 bytes
10470x00 0x03 // Packet Identifier: 3
10480x00 // Topic 1: Success, QoS 0
10490x01 // Topic 2: Success, QoS 1
1050```
1051
1052#### Example SUBACK (Failed Subscription)
1053```
10540x90 0x03 // Fixed header
10550x00 0x04 // Packet Identifier: 4
10560x80 // Return code: Failure
1057```
1058
1059**Implementation**: Check each return code. 0x80 means that topic subscription failed.
1060
1061---
1062
1063### UNSUBACK (Unsubscribe Acknowledgment)
1064
1065Broker response to UNSUBSCRIBE.
1066
1067#### Fixed Header
1068```
1069Byte 1: 0xB0 (Message Type = 11, Flags = 0)
1070Byte 2: 0x02 (Remaining Length = 2)
1071```
1072
1073#### Variable Header
1074```
1075┌─────────────────────────────────────┐
1076│ Packet Identifier (2 bytes, MSB) │
1077│ Must match UNSUBSCRIBE packet ID │
1078└─────────────────────────────────────┘
1079```
1080
1081#### Example UNSUBACK
1082```
1083For UNSUBSCRIBE with Packet ID = 5:
1084
10850xB0 0x02 // Fixed header
10860x00 0x05 // Packet Identifier: 5
1087```
1088
1089**Implementation**: Once received, unsubscription is confirmed.