Skip to content

feat(codec): add MQTT 3.1.1 protocol support with seamless version negotiation - #36

Merged
jamesarich merged 4 commits into
mainfrom
feat/mqtt-3.1.1-support
Apr 28, 2026
Merged

feat(codec): add MQTT 3.1.1 protocol support with seamless version negotiation#36
jamesarich merged 4 commits into
mainfrom
feat/mqtt-3.1.1-support

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

Summary

Add full MQTT 3.1.1 protocol support alongside the existing MQTT 5.0 implementation, with seamless version auto-negotiation so clients connect to any broker without manual configuration.

Changes

MQTT 3.1.1 Codec (feat(codec))

  • All 15 encoder/decoder functions accept a version parameter
  • 3.1.1 wire protocol differences: no properties, 1-byte CONNACK return codes, QoS-only SUBSCRIBE options, body-less DISCONNECT, no AUTH packet
  • Bidirectional CONNACK/SUBACK return code ↔ reason code mapping
  • New MqttProtocolVersion public enum (V3_1_1, V5_0)

Version Auto-Negotiation (feat(client))

  • negotiateVersion: Boolean = true config flag — tries V5.0 first, falls back to V3.1.1 on UNSUPPORTED_PROTOCOL_VERSION
  • negotiatedProtocolVersion public property on MqttClient
  • Comprehensive validateV311Compatibility() checks all V5.0-only config fields before fallback
  • Auto-reconnect preserves negotiated version

Critical Bug Fix (fix(codec))

  • When a 3.1.1-only broker rejects a V5.0 CONNECT, it sends a 3.1.1-format CONNACK (2 bytes), not V5.0 (≥3 bytes). The decoder now detects this and decodes correctly, enabling negotiation to work.

Hardened Validation (fix(codec))

  • Expanded validateV311Compatibility() to check: userProperties, maximumPacketSize, topicAliasMaximum, requestResponseInformation, receiveMaximum, and all will 5.0 properties
  • 3.1.1 CONNACK enforces exact 2-byte body + sessionPresent=0 on non-success
  • 3.1.1 SUBSCRIBE enforces reserved bits 2-7 = 0

Testing

Unit Tests (48+ new tests in Mqtt311Test.kt)

  • Encode/decode round-trips for all 3.1.1 packet types
  • Config validation (comprehensive V5.0-only field checks)
  • Connection state machine with FakeTransport
  • Version negotiation: fallback success, disabled, already V3.1.1, incompatible config

Live Broker Validation (6 brokers, 10 scenarios)

Broker Protocol Transport Result
mqtt.meshtastic.org V5.0 TLS:8883
mqtt.meshtastic.pt V5→V3.1.1 negotiation TCP:1883
mqtt.meshtastic.liamcottle.net V5.0 + V3.1.1 TCP:1883
mqtt.meshmap.app V5.0 + V3.1.1 TCP:1883
broker.emqx.io V5.0 TCP/TLS + V3.1.1 TCP:1883, TLS:8883
broker.hivemq.com V5.0 TCP:1883

All checks green

spotlessCheck · detekt · jvmTest · testAndroidHostTest · apiCheck · koverVerify (≥80%)

Competitor Comparison

  • KMQTT (292★): Separate class hierarchies per version, NO auto-negotiation
  • ktor-mqtt (55★): V5.0 only, explicitly doesn't support 3.1.1
  • Our approach: single hierarchy + version param + seamless negotiation — unique in the KMP ecosystem

jamesarich and others added 4 commits April 28, 2026 09:15
Thread MqttProtocolVersion enum through the codec and connection layers,
enabling the client to connect to MQTT 3.1.1 brokers while preserving
full MQTT 5.0 backward compatibility (default version remains V5_0).

Key changes:
- New public MqttProtocolVersion enum (V3_1_1, V5_0)
- MqttConfig gains protocolVersion field with 3.1.1 validation
- Encoder/decoder are version-aware: skip properties, use 3.1.1
  CONNACK return codes, QoS-only SUBSCRIBE options, body-less
  DISCONNECT, no AUTH packets
- MqttConnection threads version through all packet send/receive,
  skips topic aliases and flow control for 3.1.1
- FakeTransport accepts version for test helpers
- 41 new tests covering encode/decode round-trips, return code
  mapping, config validation, and connection state machine for 3.1.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add automatic protocol version fallback: the client tries MQTT 5.0
first and, if the broker rejects with UNSUPPORTED_PROTOCOL_VERSION,
retries with MQTT 3.1.1 on a fresh transport connection.

Key changes:
- Add negotiateVersion config flag (default: true)
- Add negotiatedProtocolVersion read-only property on MqttClient
- Extract validateV311Compatibility() for pre-fallback checks
- Fallback skipped when config uses 5.0-only features
- Auto-reconnect preserves the negotiated version
- Add 7 new tests for negotiation + fallback scenarios
- Update README with auto-negotiation docs and examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Compared against KMQTT (292★) and ktor-mqtt — identified gaps:

- validateV311Compatibility() now checks ALL 5.0-only config fields:
  userProperties, maximumPacketSize, topicAliasMaximum,
  requestResponseInformation, receiveMaximum, and will properties
  (willDelayInterval, messageExpiryInterval, contentType, responseTopic,
  correlationData, payloadFormatIndicator)
- negotiatedProtocolVersion only set AFTER successful fallback connect
  (prevents stale state if fallback fails)
- 3.1.1 CONNACK decoder enforces exact 2-byte body size and
  sessionPresent=0 on non-success (§3.2.2.3)
- 3.1.1 SUBSCRIBE decoder enforces reserved bits 2-7 = 0 (§3.8.3.1)
- Extended tests: 8 new validation cases for compatibility checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a 3.1.1-only broker receives a V5.0 CONNECT, it responds with a
3.1.1-format CONNACK (2 bytes, no properties section) containing return
code 0x01 (unacceptable protocol version). The decoder now detects this
case — when expecting V5.0 but receiving exactly 2 bytes — and decodes
as 3.1.1, allowing the version negotiation fallback to proceed.

Discovered testing against mqtt.meshtastic.pt (3.1.1-only broker):
- V5.0 CONNECT → 3.1.1 CONNACK (return code 1) → decoded correctly
- Fallback to V3.1.1 CONNECT → 3.1.1 CONNACK (return code 5) → NOT_AUTHORIZED
- The version negotiation worked; auth rejection is expected (different credentials)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich
jamesarich enabled auto-merge April 28, 2026 14:17
@jamesarich
jamesarich added this pull request to the merge queue Apr 28, 2026
@jamesarich jamesarich mentioned this pull request Apr 28, 2026
5 tasks
Merged via the queue into main with commit 96ea07f Apr 28, 2026
12 checks passed
@jamesarich
jamesarich deleted the feat/mqtt-3.1.1-support branch April 28, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant