# uCICMQTTClient — API Reference

## Overview

`uCICMQTTClient` implements an MQTT 5.0 client for Free Pascal / Lazarus.

Key characteristics:

- **Protocol**: MQTT 5.0 by default, with automatic fallback to MQTT 3.1.1 when the broker
  signals `0x84 Unsupported Protocol Version` in its CONNACK.
- **Transport**: Indy `TIdTCPClient` for raw TCP; `TIdSSLIOHandlerSocketOpenSSL` (TLS 1.2) for
  encrypted connections.
- **QoS**: Publishes and subscribes at QoS 0 and QoS 1. QoS 2 is declared in the type but not
  fully implemented (no PUBREC/PUBREL/PUBCOMP exchange).
- **No LCL dependency**: Safe to use in console applications, services, and non-visual threads.
- **Cross-platform**: Windows and Linux (tested on Raspberry Pi / arm-linux).
- **Reconnection**: Automatic exponential back-off reconnection on unexpected disconnection.
- **Keepalive**: Dedicated background thread sends PINGREQ and detects broker silence.

---

## Constructor and Destructor

### `constructor TCICMQTTClient.Create`

Creates a new client instance with the following defaults:

| Property | Default value |
|---|---|
| `BrokerPort` | `8883` |
| `UseSSL` | `True` |
| `KeepAliveSecs` | `60` |
| `CleanStart` | `True` |
| `SessionExpiryInterval` | `0` |
| `ConnectTimeoutSecs` | `10` |
| Protocol level | MQTT 5.0 |
| TLS method | TLSv1.2, client mode |

No TCP connection is opened during `Create`.

### `destructor TCICMQTTClient.Destroy`

If the client is not already disconnected, `Destroy` calls `Disconnect(mqrcSuccess)` before
freeing internal resources (background threads, Indy components, critical section, event).
Always free the client with `FreeAndNil` rather than direct assignment.

---

## Configuration Properties

All configuration properties are plain public fields — set them before calling `Connect`.
Changing them while connected has no effect until the next connection attempt.

| Property | Type | Default | Description |
|---|---|---|---|
| `BrokerHostName` | `String` | `''` | DNS name or IP address of the MQTT broker. |
| `BrokerPort` | `Integer` | `8883` | TCP port. Use `1883` for plain TCP, `8883` for TLS. |
| `UseSSL` | `Boolean` | `True` | When `True`, wraps the connection with `TIdSSLIOHandlerSocketOpenSSL` (TLS 1.2). |
| `ClientID` | `String` | `''` | MQTT client identifier. Must be unique per broker. Empty string asks the broker to assign one (MQTT 5.0). |
| `Username` | `String` | `''` | Optional broker username. Omit or leave empty if not required. |
| `Password` | `String` | `''` | Optional broker password. Included in the CONNECT payload only when `Username` is also non-empty. |
| `KeepAliveSecs` | `Integer` | `60` | MQTT keepalive interval in seconds. A PINGREQ is sent after half this interval of inactivity. |
| `CleanStart` | `Boolean` | `True` | MQTT 5.0 Clean Start flag (`CleanSession` in 3.1.1). `True` discards any prior session state. |
| `SessionExpiryInterval` | `UInt32` | `0` | MQTT 5.0 Session Expiry Interval property in seconds. `0` means the session ends on disconnect. Ignored when falling back to MQTT 3.1.1. |
| `ConnectTimeoutSecs` | `Integer` | `10` | Seconds to wait for a TCP connection and for CONNACK before raising an exception. |

---

## Methods

### `procedure Connect`

```pascal
procedure TCICMQTTClient.Connect;
```

Establishes a connection to the broker.

1. Sets status to `mqConnecting`.
2. Opens a TCP (and optionally TLS) connection to `BrokerHostName:BrokerPort`.
3. Starts the reader and keepalive background threads.
4. Sends an MQTT CONNECT packet (MQTT 5.0 format by default).
5. Blocks until a CONNACK is received or `ConnectTimeoutSecs` elapses.
6. If the broker returns reason code `0x84` (Unsupported Protocol Version), transparently
   reconnects using MQTT 3.1.1.
7. If the broker returns any other non-zero reason code, raises an `Exception`.
8. On success, sets status to `mqConnected`.

Raises `Exception` if the TCP connection fails, CONNACK is not received in time, or the broker
refuses the connection.

Does nothing if already in `mqConnecting` or `mqConnected` state.

---

### `procedure Disconnect`

```pascal
procedure TCICMQTTClient.Disconnect(AReasonCode: TCICMQTTReasonCode = mqrcSuccess);
```

Performs a clean shutdown.

1. Sets an internal flag (`FUserDisconnect`) to suppress automatic reconnection.
2. Terminates and waits for the reader and keepalive threads.
3. Sends an MQTT DISCONNECT packet with `AReasonCode`.
4. Closes the TCP connection.

**Parameters**

| Parameter | Type | Description |
|---|---|---|
| `AReasonCode` | `TCICMQTTReasonCode` | Reason code to include in the DISCONNECT packet. Default is `mqrcSuccess` ($00). |

Does not raise an exception if already disconnected.

---

### `function Publish`

```pascal
function TCICMQTTClient.Publish(const ATopic, APayload: String;
  AQoS: TCICMQTTQoS = mqQoS0): Word;
```

Publishes a message.

**Parameters**

| Parameter | Type | Description |
|---|---|---|
| `ATopic` | `String` | UTF-8 topic name. Must not be empty and must not contain wildcard characters. |
| `APayload` | `String` | Message payload, encoded as UTF-8 bytes on the wire. |
| `AQoS` | `TCICMQTTQoS` | Quality of service level. Default is `mqQoS0`. |

**Return value**: The Packet Identifier used for this PUBLISH. For QoS 0 the return value is
always `0`. For QoS 1 the identifier is tracked internally until a matching PUBACK is received.

Raises `Exception` with message `'MQTT client is not connected'` if called while not connected.

---

### `function Subscribe`

```pascal
function TCICMQTTClient.Subscribe(const ATopic: String;
  AQoS: TCICMQTTQoS = mqQoS0): Word;
```

Sends a SUBSCRIBE request.

**Parameters**

| Parameter | Type | Description |
|---|---|---|
| `ATopic` | `String` | Topic filter. May contain `+` (single-level) and `#` (multi-level) wildcards. |
| `AQoS` | `TCICMQTTQoS` | Maximum QoS level at which the broker will deliver matching messages. Default is `mqQoS0`. |

**Return value**: The Packet Identifier used in the SUBSCRIBE packet.

Raises `Exception` if not connected.

---

### `function Unsubscribe`

```pascal
function TCICMQTTClient.Unsubscribe(const ATopic: String): Word;
```

Sends an UNSUBSCRIBE request for the given topic filter.

**Parameters**

| Parameter | Type | Description |
|---|---|---|
| `ATopic` | `String` | Topic filter previously passed to `Subscribe`. |

**Return value**: The Packet Identifier used in the UNSUBSCRIBE packet.

Raises `Exception` if not connected.

---

### `function IsConnected`

```pascal
function TCICMQTTClient.IsConnected: Boolean;
```

Returns `True` when the internal status is exactly `mqConnected`. Thread-safe (reads a volatile
field; see Thread Safety section).

---

### `function ConnectionStatus`

```pascal
function TCICMQTTClient.ConnectionStatus: TCICMQTTConnectionStatus;
```

Returns the current `TCICMQTTConnectionStatus` value: `mqDisconnected`, `mqConnecting`,
`mqConnected`, or `mqDisconnecting`.

---

### `function LastConnectReasonCode`

```pascal
function TCICMQTTClient.LastConnectReasonCode: TCICMQTTReasonCode;
```

Returns the reason code from the most recently received CONNACK packet, cast to
`TCICMQTTReasonCode`. Returns `mqrcSuccess` ($00) when the last connection succeeded.

---

## Events

### `OnMessage`

```pascal
TCICMQTTMessageEvent = procedure(const ATopic, APayload: String;
                          AQoS: TCICMQTTQoS) of object;
property OnMessage: TCICMQTTMessageEvent;
```

Fired from the reader background thread whenever a PUBLISH packet arrives.

| Parameter | Type | Description |
|---|---|---|
| `ATopic` | `String` | The topic name decoded from the packet. |
| `APayload` | `String` | The message payload decoded as UTF-8. |
| `AQoS` | `TCICMQTTQoS` | The QoS level carried in the packet. |

**Important**: This callback runs on the reader thread. If you update VCL/LCL UI components,
synchronize with the main thread using `TThread.Synchronize` or `TThread.Queue`.

---

### `OnDisconnect`

```pascal
TCICMQTTDisconnectEvent = procedure(AReasonCode: TCICMQTTReasonCode;
                              const AReason: String) of object;
property OnDisconnect: TCICMQTTDisconnectEvent;
```

Fired when an unexpected disconnection is detected (broker closes the connection, keepalive
timeout, or network error). Not fired for disconnections initiated by calling `Disconnect`.

| Parameter | Type | Description |
|---|---|---|
| `AReasonCode` | `TCICMQTTReasonCode` | Reason code from the broker's DISCONNECT packet, or `mqrcUnspecifiedError` for local errors. |
| `AReason` | `String` | Human-readable description for logging. |

Called before the reconnection loop starts. The client will attempt to reconnect automatically
after this event fires.

---

### `OnLog`

```pascal
TCICMQTTLogEvent = procedure(const AMessage: String) of object;
property OnLog: TCICMQTTLogEvent;
```

Fired for diagnostic messages (connect/disconnect lifecycle, packet send/receive summaries,
reconnect attempts, errors). Useful for routing to a log file or memo.

| Parameter | Type | Description |
|---|---|---|
| `AMessage` | `String` | Formatted diagnostic text. |

May be called from the reader thread, keepalive thread, or the calling thread. Synchronize
with the main thread before updating UI.

---

## MQTT 5.0 Specifics

### Reason Codes

```pascal
TCICMQTTReasonCode = (
  mqrcSuccess                    = $00,
  mqrcNoMatchingSubscribers      = $10,
  mqrcUnspecifiedError           = $80,
  mqrcMalformedPacket            = $81,
  mqrcNotAuthorized              = $87,
  mqrcServerBusy                 = $89,
  mqrcTopicNameInvalid           = $90,
  mqrcPacketIdInUse              = $91,
  mqrcQuotaExceeded              = $97,
  mqrcUnsupportedProtocolVersion = $84
);
```

These values correspond to MQTT 5.0 reason codes. The `mqrcUnsupportedProtocolVersion` code
triggers the automatic 3.1.1 fallback. Other non-zero codes returned in CONNACK cause `Connect`
to raise an exception.

### 3.1.1 Fallback Behavior

When the broker responds to a MQTT 5.0 CONNECT with reason code `$84`:

1. The reader thread sets the internal protocol level to MQTT 3.1.1 (level byte `4`) and signals
   the CONNACK event.
2. `DoConnect` detects the fallback condition, closes the TCP socket, and calls itself recursively
   once with the updated protocol level.
3. The second CONNECT packet omits the Properties section entirely, matching the MQTT 3.1.1 wire
   format. PUBACK and DISCONNECT packets are also adjusted accordingly.
4. Subsequent publishes and subscribes use the 3.1.1 packet format for the remainder of the
   session. The protocol level resets to MQTT 5.0 if the client is fully disconnected and
   reconnected by calling `Connect` again.

---

## Reconnection

When an unexpected disconnection is detected (either by the reader thread catching a socket
exception, or by the keepalive thread detecting broker silence), the client enters an automatic
exponential back-off reconnection loop:

- Initial wait: 1 second.
- Each failed attempt doubles the wait: 1 s, 2 s, 4 s, 8 s, 16 s, 32 s, 60 s (capped).
- Maximum back-off: 60 seconds.
- The loop continues indefinitely until a connection succeeds or `Disconnect` is called.
- Only one reconnection loop runs at a time (guarded by `FReconnecting`).

Calling `Disconnect` from any thread sets the `FUserDisconnect` flag, which causes the
reconnection loop to exit on its next iteration.

After successful reconnection, subscriptions are not automatically re-sent. If the broker is
configured with `CleanStart = True` (the default), all prior subscriptions are lost and must be
re-established in the `OnLog` / reconnect notification path.

---

## Thread Safety

The client uses an internal `TCriticalSection` (`FLock`) and a `TEvent` (`FConnectEvent`).

| Operation | Thread-safe? | Notes |
|---|---|---|
| Setting configuration fields before `Connect` | Yes, from the main thread only. | These are plain fields with no lock; do not modify while connected. |
| `Connect` | Main thread only. | Blocks until CONNACK or timeout. |
| `Disconnect` | Safe to call from any thread. | Sets `FUserDisconnect` under the lock, then waits for threads to stop. |
| `Publish` | Safe to call from any thread while connected. | `SendBytes` acquires the lock around the socket write. |
| `Subscribe` | Safe to call from any thread while connected. | Same as `Publish`. |
| `Unsubscribe` | Safe to call from any thread while connected. | Same as `Publish`. |
| `IsConnected` / `ConnectionStatus` | Safe to read from any thread. | `FStatus` is a simple enumeration; reads are atomic on all supported platforms. |
| `OnMessage` callback | Called from reader thread. | Synchronize before touching UI. |
| `OnDisconnect` callback | Called from reader or keepalive thread. | Synchronize before touching UI. |
| `OnLog` callback | Called from any thread. | Synchronize before touching UI. |

---

## Migration from TMS MQTT

| TMS MQTT property / method | `uCICMQTTClient` equivalent |
|---|---|
| `TMSMQTTClient.Host` | `TCICMQTTClient.BrokerHostName` |
| `TMSMQTTClient.Port` | `TCICMQTTClient.BrokerPort` |
| `TMSMQTTClient.Username` | `TCICMQTTClient.Username` |
| `TMSMQTTClient.Password` | `TCICMQTTClient.Password` |
| `TMSMQTTClient.ClientID` | `TCICMQTTClient.ClientID` |
| `TMSMQTTClient.KeepAlive` | `TCICMQTTClient.KeepAliveSecs` |
| `TMSMQTTClient.CleanSession` | `TCICMQTTClient.CleanStart` |
| `TMSMQTTClient.UseTLS` | `TCICMQTTClient.UseSSL` |
| `TMSMQTTClient.Connect` | `TCICMQTTClient.Connect` |
| `TMSMQTTClient.Disconnect` | `TCICMQTTClient.Disconnect` |
| `TMSMQTTClient.Publish(topic, payload, qos, retain)` | `TCICMQTTClient.Publish(topic, payload, qos)` — retain flag not supported |
| `TMSMQTTClient.Subscribe(topic, qos)` | `TCICMQTTClient.Subscribe(topic, qos)` |
| `TMSMQTTClient.Unsubscribe(topic)` | `TCICMQTTClient.Unsubscribe(topic)` |
| `OnConnected` event | Poll `IsConnected` or use `OnLog` to detect the "MQTT session established" message |
| `OnDisconnect` event | `TCICMQTTClient.OnDisconnect` |
| `OnMQTTMessage` event | `TCICMQTTClient.OnMessage` |

Notable differences from TMS MQTT:

- No design-time component; `TCICMQTTClient` is a plain class, not a `TComponent`.
- No retain flag on publish.
- No will (Last Will and Testament) support.
- MQTT 5.0 is the default; TMS MQTT targets 3.1.1.
- Reconnection is built in; TMS MQTT requires the application to handle `OnDisconnect` and call
  `Connect` manually.

---

## Minimal Usage Example

```pascal
uses
  SysUtils, uCICMQTTClient;

var
  Client: TCICMQTTClient;

procedure OnMessage(const ATopic, APayload: String; AQoS: TCICMQTTQoS);
begin
  Writeln('Received [', ATopic, ']: ', APayload);
end;

procedure OnLog(const AMessage: String);
begin
  Writeln('LOG: ', AMessage);
end;

begin
  Client := TCICMQTTClient.Create;
  try
    Client.BrokerHostName     := 'mqtt.example.com';
    Client.BrokerPort         := 8883;
    Client.UseSSL             := True;
    Client.ClientID           := 'MyApp-001';
    Client.Username           := 'user';
    Client.Password           := 'secret';
    Client.OnMessage          := @OnMessage;
    Client.OnLog              := @OnLog;

    Client.Connect;
    try
      Client.Subscribe('sensors/#', mqQoS1);
      Client.Publish('status/MyApp-001', 'online', mqQoS1);

      Writeln('Press Enter to disconnect...');
      Readln;

      Client.Publish('status/MyApp-001', 'offline', mqQoS0);
    finally
      Client.Disconnect;
    end;
  finally
    Client.Free;
  end;
end.
```
