mqtt_message_pack A Dart package providing low-level MQTT 3.x and 5.0 packet construction and parsing utilities, along with a set of message‐type classes for building MQTT client payloads. It is a direct port of a PHP MQTT library into idiomatic Dart, supporting both TCP and WebSocket clients.


Table of Contents


Features

  • MQTT 3.1.1 & MQTT 5.0 Support Build and parse packets for both versions, with automatic handling of protocol‐level upgrades if MQTT 5.0 properties are present.
  • Low‐Level Packet Encoding/Decoding Classes to translate high‐level Dart objects into raw Uint8List byte buffers for publishing to a broker.
  • Message‐Type Abstractions Each MQTT control packet (CONNECT, PUBLISH, SUBACK, etc.) has a corresponding Dart class exposing typed getters/setters and a getContents({bool getAsMap = false}) method for structure or raw bytes.
  • Property Encoding for MQTT 5.0 Automatic encoding/decoding of variable‐length properties (e.g. sessionExpiryInterval, reasonString, userProperty, etc.).
  • Extensible & Framework‐Neutral No built‐in networking—use your preferred socket or WebSocket library to send/receive the byte buffers generated by Pack.*.

Getting Started

Prerequisites

  • Dart SDK ≥ 2.17.0 < 4.0.0
  • (Optional) A Dart HTTP/WebSocket or TCP client library to actually send/receive MQTT packets.

Installation

Add mqtt_basics_and_client to your pubspec.yaml dependencies:

dependencies:
  mqtt_basics_and_client:
    git:
      url: https://github.com/yourusername/mqtt_basics_and_client.git
      ref: main

Then run:

dart pub get

Project Structure

mqtt_basics_and_client/
├── lib/
│   ├── client/
│   │   ├── messages/
│   │   │   ├── abstract_message.dart
│   │   │   ├── auth.dart
│   │   │   ├── connack.dart
│   │   │   ├── disconnect.dart
│   │   │   ├── pingresp.dart
│   │   │   ├── puback.dart
│   │   │   ├── pubcomp.dart
│   │   │   ├── pubrec.dart
│   │   │   ├── pubrel.dart
│   │   │   ├── publish.dart
│   │   │   ├── suback.dart
│   │   │   ├── unsuback.dart
│   │   │   └── will.dart
│   │   └── packet/
│   │       └── pack.dart
│   ├── core/
│   │   ├── constants.dart
│   │   ├── exceptions/
│   │   │   ├── connect_exception.dart
│   │   │   ├── invalid_argument_exception.dart
│   │   │   ├── length_exception.dart
│   │   │   ├── protocol_exception.dart
│   │   │   └── runtime_exception.dart
│   │   ├── hex/
│   │   │   ├── property.dart
│   │   │   └── reason_code.dart
│   │   └── protocol/
│   │       ├── protocol_interface.dart
│   │       └── types.dart
│   └── utils/
│       ├── common.dart
│       └── pack_tool.dart
├── test/
│   └── mqtt_basics_and_client_test.dart
├── pubspec.yaml
└── README.md
  • lib/client/messages/: Dart classes for each MQTT packet type (CONNECT, PUBLISH, etc.)

  • lib/client/packet/pack.dart: High‐level static methods (Pack.connect, Pack.publish, etc.) that delegate to PackTool.encode*.

  • lib/core/:

    • constants.dart: MQTT protocol constants (QoS values, control packet types, etc.).
    • exceptions/: Custom exception types (e.g. ProtocolException, InvalidArgumentException).
    • hex/: Hex‐valued constants for MQTT 5.0 properties and reason codes.
    • protocol/: Core protocol constants and an abstract interface for future extension.
  • lib/utils/pack_tool.dart: Low‐level helpers for encoding strings, integers, variable‐byte integers, and MQTT 5.0 properties.

  • lib/utils/common.dart: Byte‐level debugging utility to print ASCII/hex representation of binary strings.

  • test/: Comprehensive unit tests covering utility functions, getContents(getAsMap: true), and raw packet encoding.


Usage

You can use mqtt_basics_and_client to build raw MQTT packets and send them over any socket or WebSocket. Below are common examples.

Constructing Packets

All message classes inherit from AbstractMessage and implement:

/// Returns either:
/// - a `Map<String, dynamic>` if `getAsMap == true`
/// - a `Uint8List` (raw bytes) if `getAsMap == false`
dynamic getContents({bool getAsMap = false});

When getAsMap == true, you receive a structured map—handy for debugging or custom serialization. When getAsMap == false, you receive a Uint8List ready to write directly to a TCP/WebSocket stream.

CONNECT

import 'dart:typed_data';
import 'package:mqtt_basics_and_client/client/packet/pack.dart';
import 'package:mqtt_basics_and_client/core/constants.dart';

void main() {
  // Build a CONNECT packet: MQTT 3.1.1, clientId="dartClient", clean session
  final connectMap = {
    'protocol_name': mqttProtocolName,
    'protocol_level': mqttProtocolLevel311,
    'clean_session': true,
    'client_id': 'dartClient',
    'keep_alive': 60,
    // For MQTT 5.0, you can add 'properties', 'will', 'user_name', 'password', etc.
  };

  // Get raw bytes
  Uint8List connectPacket = Pack.connect(connectMap);

  // Now send `connectPacket` to your TCP socket
}

PUBLISH

import 'dart:typed_data';
import 'package:mqtt_basics_and_client/client/messages/publish.dart';
import 'package:mqtt_basics_and_client/client/packet/pack.dart';

void main() {
  final publishMsg = Publish()
    ..setTopic('test/topic')
    ..setMessage('Hello from Dart!')
    ..setQos(1)
    ..setMessageId(42)
    ..setRetain(0)
    ..setDup(0)
    ..setProperty('payloadFormatIndicator', 1); // MQTT 5.0 only

  // Get raw bytes
  Uint8List publishPacket = Pack.publish(publishMsg.getContents(getAsMap: false));

  // Send `publishPacket` over your socket
}

QoS Flow (PUBREC, PUBREL, PUBCOMP)

// PUBREC (receiver → sender after QoS 2 PUBLISH)
final pubRecMsg = PubRec()
  ..setMessageId(42)
  ..setCode(0) // 0 = Success
  ..setProperty('reasonString', 'Received');

Uint8List pubRecPacket = Pack.pubRec(pubRecMsg.getContents(getAsMap: false));

// PUBREL (sender → receiver)
final pubRelMsg = PubRel()
  ..setMessageId(42)
  ..setCode(0); // Success
Uint8List pubRelPacket = Pack.pubRel(pubRelMsg.getContents(getAsMap: false));

// PUBCOMP (receiver → sender)
final pubCompMsg = PubComp()
  ..setMessageId(42)
  ..setCode(0); // Success
Uint8List pubCompPacket = Pack.pubComp(pubCompMsg.getContents(getAsMap: false));

SUBSCRIBE / SUBACK

// SUBSCRIBE at QoS 1: topic="cmd/#"
final subscribeMap = {
  'type': 8, // Types.subscribe = 8
  'message_id': 101,
  'topics': ['cmd/#'],  // currently assume one topic; extend as needed
  'qos': [1],           // parallel list of QoS values
  'properties': <String, dynamic>{} // MQTT 5.0 optional
};
// Encode via a custom helper or extend PackTool to support encodeSubscribe()

// SUBACK (broker → client)
final subAckMsg = SubAck()
  ..setMessageId(101)
  ..setCodes([0x01]) // Granted QoS 1
  ..setProperty('subscriptionIdentifier', 1);

Uint8List subAckPacket = Pack.subAck(subAckMsg.getContents(getAsMap: false));

Note: Subscribe encoding is not provided out of the box. You can adapt PackTool.encode* logic for SUBSCRIBE using string/topic lists and property mappings.

UNSUBSCRIBE / UNSUBACK

// UNSUBACK (broker → client)
final unsubAckMsg = UnSubAck()
  ..setMessageId(101)
  ..setCodes([0x00]) // Success (0x00 = Success, 0x80 = Failure)
  ..setProperty('reasonString', 'Unsubscribed');

Uint8List unsubAckPacket = Pack.unsubAck(unsubAckMsg.getContents(getAsMap: false));

PINGREQ / PINGRESP

  • PINGREQ: client sends [0xC0, 0x00] (fixed two‐byte header)
  • PINGRESP: built as:
// PINGRESP (broker → client)
Uint8List pingRespPacket = Pack.pingResp({});
// pingRespPacket == Uint8List.fromList([0xD0, 0x00]);

DISCONNECT

final disconnectMsg = DisConnect()
  ..setCode(0x00) // Normal disconnection
  ..setProperty('reasonString', 'Goodbye');
Uint8List disconnectPacket = Pack.disconnect(disconnectMsg.getContents(getAsMap: false));

AUTH (MQTT 5.0)

final authMsg = Auth()
  ..setCode(0x18) // Continue authentication
  ..setProperty('authenticationMethod', 'token');
Uint8List authPacket = Pack.auth(authMsg.getContents(getAsMap: false));

Decoding Incoming Packets

If you receive a raw Uint8List buffer from a broker, you can:

  1. Inspect the first byte to extract packet type:

    int packetType = data[0] >> 4; // Upper 4 bits of first byte
    
  2. Parse Remaining Length (variable‐byte integer) using PackTool.decodeVariableByteInteger(data, 1).

  3. Use your own UnPack or UnPackV5 logic (not included in this repo, but easily portable from PHP port) to extract fields.

This package focuses on packet construction; you can re‐implement UnPack classes similarly if you need a full decoder.


API Reference Overview

Core Utilities (PackTool)

  • encodeString(String str) → Uint8List Prefixes length (2 bytes) then UTF-8 bytes of str.

  • encodeStringPair(String key, String value) → Uint8List Two successive length‐prefixed strings (key, then value).

  • encodeInt16(int) → Uint8List & encodeInt32(int) → Uint8List Big‐endian 16‐bit or 32‐bit integer.

  • encodeVariableByteInteger(int) → Uint8List MQTT’s VB‐I format (up to four bytes).

  • decodeVariableByteInteger(Uint8List, int offset) → Map<String, dynamic> Decodes a VB‐I starting at offset, returns { 'value': int, 'offset': newOffset }.

  • encodeProperties(Map<String, dynamic>) → Uint8List Prepend VB‐I length, then [ keyLength, keyBytes, valueLength, valueBytes ]… for each property.

  • (Future) Add encodeWill, encodeSub, etc., if you want to unify PUBLISH/SUBSCRIBE encoding under one tool.

Packet Builders (Pack)

Each static method in Pack produces the variable header + payload bytes. You will still need to prepend the appropriate fixed header (control packet type + flags, remaining length) before sending over TCP/WebSocket. Typical flow:

  1. Build variable header & payload with Pack.*(map).

  2. Determine remaining length (remainingLength = variableHeaderBytes.length).

  3. Build fixed header:

    int packetTypeAndFlags = (controlPacketType << 4) | flags;  
    Uint8List remLenBytes = PackTool.encodeVariableByteInteger(remainingLength);
    
  4. Concatenate [packetTypeAndFlags] + remLenBytes + variableHeaderBytes.

For example, for a QoS 1 PUBLISH (controlPacketType = 3, flags = (qos << 1)):

final varHeaderPayload = Pack.publish(publishMap);
final remaining = PackTool.encodeVariableByteInteger(varHeaderPayload.length);
final fixedHeader = Uint8List.fromList([ (3 << 4) | (1 << 1), ...remaining ]);
Uint8List fullPacket = Uint8List.fromList([...fixedHeader, ...varHeaderPayload]);

Message Classes (AbstractMessage & subclasses)

Every message class extends AbstractMessage and must override:

dynamic getContents({bool getAsMap = false});
  • Returns a Map<String, dynamic> structure if getAsMap == true.
  • Returns a Uint8List by calling the corresponding Pack.* method if getAsMap == false.

Subclasses:

  • Auth (MQTT 5.0 only)
  • ConnAck
  • DisConnect
  • PingResp
  • PubAck, PubRec, PubRel, PubComp (QoS flows)
  • Publish
  • SubAck, UnSubAck
  • Will (embedded in CONNECT payload)

Testing

A comprehensive test suite is provided in test/mqtt_basics_and_client_test.dart. It includes:

  • PackTool Utility Tests

    • encodeString, encodeVariableByteInteger, encodeProperties, and now decodeVariableByteInteger.
  • Message Class Structure Tests

    • Verifying that getContents(getAsMap: true) returns the correct map keys/values for each message type.
  • Pack Encoding Smoke Tests

    • Ensuring Pack.pubAck, Pack.disconnect, Pack.publish, Pack.connAck, and Pack.pingResp each produce non-empty or properly structured bytes.

Run all tests with:

dart test

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/your-feature)
  3. Implement your changes, ensuring new code is covered by tests
  4. Run dart test to verify
  5. Submit a Pull Request with a clear description and relevant issue references

Please follow the existing coding style:

  • Use two‐space indentation
  • Keep line length around 80–100 characters
  • Add doc comments (/// …) to any new public API

License

This project is licensed under the MIT License. See LICENSE for details.


Happy MQTT’ing with Dart!