Skip to content

Commit 1d32f48

Browse files
jamesarichCopilot
andcommitted
feat(client): add MqttClient.probe() endpoint diagnostics API
Introduces a one-shot connectivity probe so consumer "Test Connection" affordances can classify endpoint failures without spinning up a full client lifecycle. API surface (commonMain): - `MqttClient.Companion.probe(endpoint, timeoutMs, configure)` — suspend extension that performs a CONNECT/CONNACK against a transient `MqttConnection`, then tears it down. Never throws for connectivity errors; cancellation is propagated. - `ProbeResult` sealed class — Success / Rejected / DnsFailure / TcpFailure / TlsFailure / Timeout / Other. - `ProbeServerInfo` — curated public projection of broker CONNACK properties (assignedClientIdentifier, serverKeepAliveSeconds, maximumQosOrdinal, retainAvailable, …) so we don't leak the internal MqttProperties shape. - `DEFAULT_PROBE_TIMEOUT_MS = 5_000`. Implementation notes: - Probe forces `autoReconnect = false` and defaults `clientId = ""` / `cleanStart = true` / `keepAliveSeconds = 0`. - Cleanup runs under `NonCancellable` so disconnect/transport.close fire even if the caller is cancelling. - Failure classifier matches on cause-chain class simpleName + message substrings (KMP-safe; no java.* / android.* references) and treats the internal `MqttConnectionException` (no cause) as a `Rejected` outcome carrying the broker's reason code and serverReference. - `MqttClient` companion promoted from `private` to `public` so the extension function can attach to it; companion constants stay private. Tests: 10 new cases in `ProbeApiTest` driving every ProbeResult arm via `FakeTransport` — including timeout, external cancellation, and the USE_ANOTHER_SERVER redirect path. Baseline: spotlessCheck detekt allTests apiCheck koverVerify all green; library.api regenerated with the new public surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 56de130 commit 1d32f48

6 files changed

Lines changed: 789 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
`state == ConnectionState.DISCONNECTED``state is ConnectionState.Disconnected`.
2020

2121
### Added
22+
- `MqttClient.probe(endpoint, timeoutMs, configure)` — one-shot connectivity diagnostic
23+
that performs a CONNECT/CONNACK handshake against an `MqttEndpoint`, classifies the
24+
outcome into a public `ProbeResult` sealed class (`Success`, `Rejected`, `DnsFailure`,
25+
`TcpFailure`, `TlsFailure`, `Timeout`, `Other`), and tears the transient connection
26+
back down. Designed for "Test Connection" affordances in consumer settings UIs.
27+
- `ProbeResult` and `ProbeServerInfo` public types — `Success.serverInfo` exposes a
28+
curated subset of broker CONNACK properties (`assignedClientIdentifier`,
29+
`serverKeepAliveSeconds`, `maximumQosOrdinal`, `retainAvailable`, etc.) for capability
30+
diagnostics without leaking the internal `MqttProperties` shape.
31+
- `DEFAULT_PROBE_TIMEOUT_MS` (5 000 ms) — public default for the probe wall-clock budget.
2232
- `ConnectionState.Disconnected.reason`: surfaces the failure that caused an unexpected
2333
disconnect — `MqttException.ConnectionRejected` for broker rejections,
2434
`MqttException.ConnectionLost` for transport-side / protocol-violation tear-downs,

library/api/jvm/library.api

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public final class org/meshtastic/mqtt/ConnectionState$Reconnecting : org/meshta
5959
}
6060

6161
public final class org/meshtastic/mqtt/MqttClient {
62+
public static final field Companion Lorg/meshtastic/mqtt/MqttClient$Companion;
6263
public fun <init> (Lorg/meshtastic/mqtt/MqttConfig;Lkotlinx/coroutines/CoroutineScope;)V
6364
public synthetic fun <init> (Lorg/meshtastic/mqtt/MqttConfig;Lkotlinx/coroutines/CoroutineScope;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
6465
public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -78,6 +79,9 @@ public final class org/meshtastic/mqtt/MqttClient {
7879
public final fun unsubscribe ([Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
7980
}
8081

82+
public final class org/meshtastic/mqtt/MqttClient$Companion {
83+
}
84+
8185
public final class org/meshtastic/mqtt/MqttClientKt {
8286
public static final fun MqttClient (Ljava/lang/String;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function1;)Lorg/meshtastic/mqtt/MqttClient;
8387
public static synthetic fun MqttClient$default (Ljava/lang/String;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/meshtastic/mqtt/MqttClient;
@@ -314,6 +318,132 @@ public final class org/meshtastic/mqtt/MqttMessage {
314318
public fun toString ()Ljava/lang/String;
315319
}
316320

321+
public final class org/meshtastic/mqtt/ProbeKt {
322+
public static final field DEFAULT_PROBE_TIMEOUT_MS J
323+
public static final fun probe (Lorg/meshtastic/mqtt/MqttClient$Companion;Lorg/meshtastic/mqtt/MqttEndpoint;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
324+
public static synthetic fun probe$default (Lorg/meshtastic/mqtt/MqttClient$Companion;Lorg/meshtastic/mqtt/MqttEndpoint;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
325+
}
326+
327+
public abstract class org/meshtastic/mqtt/ProbeResult {
328+
}
329+
330+
public final class org/meshtastic/mqtt/ProbeResult$DnsFailure : org/meshtastic/mqtt/ProbeResult {
331+
public fun <init> (Ljava/lang/Throwable;)V
332+
public final fun component1 ()Ljava/lang/Throwable;
333+
public final fun copy (Ljava/lang/Throwable;)Lorg/meshtastic/mqtt/ProbeResult$DnsFailure;
334+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$DnsFailure;Ljava/lang/Throwable;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$DnsFailure;
335+
public fun equals (Ljava/lang/Object;)Z
336+
public final fun getCause ()Ljava/lang/Throwable;
337+
public fun hashCode ()I
338+
public fun toString ()Ljava/lang/String;
339+
}
340+
341+
public final class org/meshtastic/mqtt/ProbeResult$Other : org/meshtastic/mqtt/ProbeResult {
342+
public fun <init> (Ljava/lang/Throwable;)V
343+
public final fun component1 ()Ljava/lang/Throwable;
344+
public final fun copy (Ljava/lang/Throwable;)Lorg/meshtastic/mqtt/ProbeResult$Other;
345+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$Other;Ljava/lang/Throwable;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$Other;
346+
public fun equals (Ljava/lang/Object;)Z
347+
public final fun getCause ()Ljava/lang/Throwable;
348+
public fun hashCode ()I
349+
public fun toString ()Ljava/lang/String;
350+
}
351+
352+
public final class org/meshtastic/mqtt/ProbeResult$Rejected : org/meshtastic/mqtt/ProbeResult {
353+
public fun <init> (Lorg/meshtastic/mqtt/ReasonCode;Ljava/lang/String;Ljava/lang/String;)V
354+
public synthetic fun <init> (Lorg/meshtastic/mqtt/ReasonCode;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
355+
public final fun component1 ()Lorg/meshtastic/mqtt/ReasonCode;
356+
public final fun component2 ()Ljava/lang/String;
357+
public final fun component3 ()Ljava/lang/String;
358+
public final fun copy (Lorg/meshtastic/mqtt/ReasonCode;Ljava/lang/String;Ljava/lang/String;)Lorg/meshtastic/mqtt/ProbeResult$Rejected;
359+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$Rejected;Lorg/meshtastic/mqtt/ReasonCode;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$Rejected;
360+
public fun equals (Ljava/lang/Object;)Z
361+
public final fun getMessage ()Ljava/lang/String;
362+
public final fun getReasonCode ()Lorg/meshtastic/mqtt/ReasonCode;
363+
public final fun getServerReference ()Ljava/lang/String;
364+
public fun hashCode ()I
365+
public fun toString ()Ljava/lang/String;
366+
}
367+
368+
public final class org/meshtastic/mqtt/ProbeResult$Success : org/meshtastic/mqtt/ProbeResult {
369+
public fun <init> (Lorg/meshtastic/mqtt/ProbeServerInfo;)V
370+
public final fun component1 ()Lorg/meshtastic/mqtt/ProbeServerInfo;
371+
public final fun copy (Lorg/meshtastic/mqtt/ProbeServerInfo;)Lorg/meshtastic/mqtt/ProbeResult$Success;
372+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$Success;Lorg/meshtastic/mqtt/ProbeServerInfo;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$Success;
373+
public fun equals (Ljava/lang/Object;)Z
374+
public final fun getServerInfo ()Lorg/meshtastic/mqtt/ProbeServerInfo;
375+
public fun hashCode ()I
376+
public fun toString ()Ljava/lang/String;
377+
}
378+
379+
public final class org/meshtastic/mqtt/ProbeResult$TcpFailure : org/meshtastic/mqtt/ProbeResult {
380+
public fun <init> (Ljava/lang/Throwable;)V
381+
public final fun component1 ()Ljava/lang/Throwable;
382+
public final fun copy (Ljava/lang/Throwable;)Lorg/meshtastic/mqtt/ProbeResult$TcpFailure;
383+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$TcpFailure;Ljava/lang/Throwable;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$TcpFailure;
384+
public fun equals (Ljava/lang/Object;)Z
385+
public final fun getCause ()Ljava/lang/Throwable;
386+
public fun hashCode ()I
387+
public fun toString ()Ljava/lang/String;
388+
}
389+
390+
public final class org/meshtastic/mqtt/ProbeResult$Timeout : org/meshtastic/mqtt/ProbeResult {
391+
public fun <init> (J)V
392+
public final fun component1 ()J
393+
public final fun copy (J)Lorg/meshtastic/mqtt/ProbeResult$Timeout;
394+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$Timeout;JILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$Timeout;
395+
public fun equals (Ljava/lang/Object;)Z
396+
public final fun getDurationMs ()J
397+
public fun hashCode ()I
398+
public fun toString ()Ljava/lang/String;
399+
}
400+
401+
public final class org/meshtastic/mqtt/ProbeResult$TlsFailure : org/meshtastic/mqtt/ProbeResult {
402+
public fun <init> (Ljava/lang/Throwable;)V
403+
public final fun component1 ()Ljava/lang/Throwable;
404+
public final fun copy (Ljava/lang/Throwable;)Lorg/meshtastic/mqtt/ProbeResult$TlsFailure;
405+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeResult$TlsFailure;Ljava/lang/Throwable;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeResult$TlsFailure;
406+
public fun equals (Ljava/lang/Object;)Z
407+
public final fun getCause ()Ljava/lang/Throwable;
408+
public fun hashCode ()I
409+
public fun toString ()Ljava/lang/String;
410+
}
411+
412+
public final class org/meshtastic/mqtt/ProbeServerInfo {
413+
public fun <init> ()V
414+
public fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)V
415+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
416+
public final fun component1 ()Ljava/lang/String;
417+
public final fun component10 ()Ljava/lang/Integer;
418+
public final fun component11 ()Ljava/lang/String;
419+
public final fun component12 ()Ljava/lang/String;
420+
public final fun component2 ()Ljava/lang/Integer;
421+
public final fun component3 ()Ljava/lang/Integer;
422+
public final fun component4 ()Ljava/lang/Boolean;
423+
public final fun component5 ()Ljava/lang/Boolean;
424+
public final fun component6 ()Ljava/lang/Boolean;
425+
public final fun component7 ()Ljava/lang/Boolean;
426+
public final fun component8 ()Ljava/lang/Long;
427+
public final fun component9 ()Ljava/lang/Integer;
428+
public final fun copy (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)Lorg/meshtastic/mqtt/ProbeServerInfo;
429+
public static synthetic fun copy$default (Lorg/meshtastic/mqtt/ProbeServerInfo;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/meshtastic/mqtt/ProbeServerInfo;
430+
public fun equals (Ljava/lang/Object;)Z
431+
public final fun getAssignedClientIdentifier ()Ljava/lang/String;
432+
public final fun getMaximumPacketSize ()Ljava/lang/Long;
433+
public final fun getMaximumQosOrdinal ()Ljava/lang/Integer;
434+
public final fun getReceiveMaximum ()Ljava/lang/Integer;
435+
public final fun getResponseInformation ()Ljava/lang/String;
436+
public final fun getRetainAvailable ()Ljava/lang/Boolean;
437+
public final fun getServerKeepAliveSeconds ()Ljava/lang/Integer;
438+
public final fun getServerReference ()Ljava/lang/String;
439+
public final fun getSharedSubscriptionAvailable ()Ljava/lang/Boolean;
440+
public final fun getSubscriptionIdentifiersAvailable ()Ljava/lang/Boolean;
441+
public final fun getTopicAliasMaximum ()Ljava/lang/Integer;
442+
public final fun getWildcardSubscriptionAvailable ()Ljava/lang/Boolean;
443+
public fun hashCode ()I
444+
public fun toString ()Ljava/lang/String;
445+
}
446+
317447
public final class org/meshtastic/mqtt/PublishProperties {
318448
public fun <init> ()V
319449
public fun <init> (Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Lkotlinx/io/bytestring/ByteString;ZLjava/util/List;Ljava/util/List;)V

library/src/commonMain/kotlin/org/meshtastic/mqtt/MqttClient.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,11 @@ public class MqttClient
927927
}
928928
}
929929

930-
private companion object {
930+
/**
931+
* Companion object — entry point for static helpers like [probe] (a one-shot
932+
* connectivity diagnostic that does not require constructing a long-lived client).
933+
*/
934+
public companion object {
931935
private const val MESSAGE_BUFFER_CAPACITY = 64
932936
private const val AUTH_BUFFER_CAPACITY = 8
933937
private const val MAX_REDIRECTS = 5

0 commit comments

Comments
 (0)