@annadata/capacitor-mqtt-quic 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/x86_64/lib/libwolfssl.a +0 -0
- package/android/src/main/cpp/ngtcp2_jni.cpp +14 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +4 -1
- package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +26 -21
- package/ios/Sources/MqttQuicPlugin/Client/MQTTClient.swift +99 -31
- package/ios/Sources/MqttQuicPlugin/MqttQuicPlugin.swift +18 -6
- package/ios/libs/libnghttp3.a +0 -0
- package/ios/libs/libngtcp2.a +0 -0
- package/ios/libs/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs/libwolfssl.a +0 -0
- package/ios/libs-simulator/libnghttp3.a +0 -0
- package/ios/libs-simulator/libngtcp2.a +0 -0
- package/ios/libs-simulator/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs-simulator/libwolfssl.a +0 -0
- package/ios/libs-simulator-x86_64/libnghttp3.a +0 -0
- package/ios/libs-simulator-x86_64/libngtcp2.a +0 -0
- package/ios/libs-simulator-x86_64/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs-simulator-x86_64/libwolfssl.a +0 -0
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -233,6 +233,20 @@ class QuicClient {
|
|
|
233
233
|
state.recv_buf.insert(state.recv_buf.end(), data, data + datalen);
|
|
234
234
|
LOGI("recv stream data stream_id=%" PRId64 " len=%zu recv_buf_total=%zu",
|
|
235
235
|
(int64_t)stream_id, datalen, state.recv_buf.size());
|
|
236
|
+
if (datalen > 2 && datalen <= 32) {
|
|
237
|
+
char hex[128];
|
|
238
|
+
size_t n = datalen < 16 ? datalen : 16u;
|
|
239
|
+
char *p = hex;
|
|
240
|
+
for (size_t i = 0; i < n && p < hex + sizeof(hex) - 4; i++)
|
|
241
|
+
p += snprintf(p, (size_t)(hex + sizeof(hex) - p), "%02x", data[i]);
|
|
242
|
+
LOGI("recv first bytes (type 0x%02x = %s) hex=%s",
|
|
243
|
+
(unsigned)data[0],
|
|
244
|
+
(data[0] & 0xF0) == 0x30 ? "PUBLISH" : (data[0] == (uint8_t)0xd0 ? "PINGRESP" : "other"),
|
|
245
|
+
hex);
|
|
246
|
+
} else if (datalen > 32) {
|
|
247
|
+
LOGI("recv large chunk len=%zu first_byte=0x%02x (PUBLISH=0x30)",
|
|
248
|
+
datalen, (unsigned)data[0]);
|
|
249
|
+
}
|
|
236
250
|
if (flags & NGTCP2_STREAM_DATA_FLAG_FIN) {
|
|
237
251
|
state.fin_received = true;
|
|
238
252
|
}
|
|
@@ -121,7 +121,10 @@ class MqttQuicPlugin : Plugin() {
|
|
|
121
121
|
} catch (_: Exception) {
|
|
122
122
|
Base64.encodeToString(payload, Base64.NO_WRAP)
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
// Ensure non-null strings so Capacitor bridge never receives undefined
|
|
125
|
+
val safeTopic = topic ?: ""
|
|
126
|
+
val safePayload = payloadStr ?: ""
|
|
127
|
+
val data = JSObject().put("topic", safeTopic).put("payload", safePayload)
|
|
125
128
|
Handler(Looper.getMainLooper()).post {
|
|
126
129
|
notifyListeners("message", data)
|
|
127
130
|
}
|
|
@@ -613,30 +613,35 @@ class MQTTClient {
|
|
|
613
613
|
}
|
|
614
614
|
MQTTMessageType.PUBLISH -> {
|
|
615
615
|
val qos = (msgType.toInt() shr 1) and 0x03
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
MQTTProtocol.parsePublish(rest, 0, qos)
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
val (cb, globalCb) = lock.withLock {
|
|
625
|
-
subscribedTopics[topic] to onPublish
|
|
626
|
-
}
|
|
627
|
-
globalCb?.invoke(topic, payload)
|
|
628
|
-
cb?.invoke(payload)
|
|
629
|
-
|
|
630
|
-
if (qos >= 1 && packetId != null) {
|
|
631
|
-
val w = lock.withLock { writer }
|
|
632
|
-
w?.let {
|
|
633
|
-
if (qos == 1) {
|
|
634
|
-
it.write(MQTTProtocol.buildPuback(packetId))
|
|
616
|
+
try {
|
|
617
|
+
val (topic, packetId, payload) = lock.withLock {
|
|
618
|
+
if (activeProtocolVersion == MQTTProtocolLevel.V5) {
|
|
619
|
+
MQTT5Protocol.parsePublishV5(rest, 0, qos, topicAliasMap)
|
|
635
620
|
} else {
|
|
636
|
-
|
|
621
|
+
MQTTProtocol.parsePublish(rest, 0, qos)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
val (cb, globalCb) = lock.withLock {
|
|
625
|
+
subscribedTopics[topic] to onPublish
|
|
626
|
+
}
|
|
627
|
+
globalCb?.invoke(topic, payload)
|
|
628
|
+
cb?.invoke(payload)
|
|
629
|
+
|
|
630
|
+
if (qos >= 1 && packetId != null) {
|
|
631
|
+
val w = lock.withLock { writer }
|
|
632
|
+
w?.let {
|
|
633
|
+
if (qos == 1) {
|
|
634
|
+
it.write(MQTTProtocol.buildPuback(packetId))
|
|
635
|
+
} else {
|
|
636
|
+
it.write(MQTTProtocol.buildPubrec(packetId))
|
|
637
|
+
}
|
|
638
|
+
it.drain()
|
|
637
639
|
}
|
|
638
|
-
it.drain()
|
|
639
640
|
}
|
|
641
|
+
} catch (e: Exception) {
|
|
642
|
+
// PUBLISH parse failed: log and skip this message (don't disconnect)
|
|
643
|
+
val hex = rest.take(64).joinToString("") { "%02x".format(it) }
|
|
644
|
+
Log.w("MQTTClient", "PUBLISH parse failed: ${e.message} restLen=${rest.size} hex=$hex", e)
|
|
640
645
|
}
|
|
641
646
|
}
|
|
642
647
|
MQTTMessageType.PUBREL -> {
|
|
@@ -46,6 +46,8 @@ public final class MQTTClient {
|
|
|
46
46
|
private var subackContinuation: CheckedContinuation<Data, Error>?
|
|
47
47
|
/// Handoff from message loop to unsubscribe() for UNSUBACK.
|
|
48
48
|
private var unsubackContinuation: CheckedContinuation<Data, Error>?
|
|
49
|
+
/// Pending PINGRESP for sendMqttPing(). Message loop completes when PINGRESP is read.
|
|
50
|
+
private var pendingPingresp: CheckedContinuation<Void, Error>?
|
|
49
51
|
private let lock = NSLock()
|
|
50
52
|
|
|
51
53
|
public init(protocolVersion: ProtocolVersion = .auto) {
|
|
@@ -345,6 +347,8 @@ public final class MQTTClient {
|
|
|
345
347
|
subackContinuation = nil
|
|
346
348
|
let unsubCont = unsubackContinuation
|
|
347
349
|
unsubackContinuation = nil
|
|
350
|
+
let pingCont = pendingPingresp
|
|
351
|
+
pendingPingresp = nil
|
|
348
352
|
let w = writer
|
|
349
353
|
let version = activeProtocolVersion
|
|
350
354
|
quicClient = nil
|
|
@@ -358,6 +362,7 @@ public final class MQTTClient {
|
|
|
358
362
|
lock.unlock()
|
|
359
363
|
subCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
|
|
360
364
|
unsubCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
|
|
365
|
+
pingCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
|
|
361
366
|
|
|
362
367
|
if let w = w {
|
|
363
368
|
let data: Data
|
|
@@ -403,6 +408,55 @@ public final class MQTTClient {
|
|
|
403
408
|
return (fixed[0], rem, fixed)
|
|
404
409
|
}
|
|
405
410
|
|
|
411
|
+
/// Send MQTT PINGREQ and wait for PINGRESP. Resets server's idle timer. Returns true if PINGRESP received within timeout.
|
|
412
|
+
public func sendMqttPing(timeoutMs: UInt64 = 5000) async -> Bool {
|
|
413
|
+
guard case .connected = getState() else { return false }
|
|
414
|
+
lock.lock()
|
|
415
|
+
if pendingPingresp != nil {
|
|
416
|
+
lock.unlock()
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
let w = writer
|
|
420
|
+
lock.unlock()
|
|
421
|
+
do {
|
|
422
|
+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
423
|
+
lock.lock()
|
|
424
|
+
pendingPingresp = cont
|
|
425
|
+
lock.unlock()
|
|
426
|
+
Task {
|
|
427
|
+
try? await Task.sleep(nanoseconds: timeoutMs * 1_000_000)
|
|
428
|
+
self.lock.lock()
|
|
429
|
+
if let c = self.pendingPingresp {
|
|
430
|
+
self.pendingPingresp = nil
|
|
431
|
+
c.resume(throwing: MQTTProtocolError.insufficientData("PINGRESP timeout"))
|
|
432
|
+
}
|
|
433
|
+
self.lock.unlock()
|
|
434
|
+
}
|
|
435
|
+
if let w = w {
|
|
436
|
+
Task {
|
|
437
|
+
do {
|
|
438
|
+
try await w.write(MQTTProtocol.buildPingreq())
|
|
439
|
+
try await w.drain()
|
|
440
|
+
} catch {
|
|
441
|
+
self.lock.lock()
|
|
442
|
+
if let c = self.pendingPingresp {
|
|
443
|
+
self.pendingPingresp = nil
|
|
444
|
+
c.resume(throwing: error)
|
|
445
|
+
}
|
|
446
|
+
self.lock.unlock()
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return true
|
|
452
|
+
} catch {
|
|
453
|
+
lock.lock()
|
|
454
|
+
pendingPingresp = nil
|
|
455
|
+
lock.unlock()
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
406
460
|
/// Send PINGREQ at effectiveKeepalive interval so server sees activity and does not close (idle/keepalive). [MQTT-3.1.2-20]
|
|
407
461
|
private func startKeepaliveLoop() {
|
|
408
462
|
keepaliveTask?.cancel()
|
|
@@ -493,44 +547,56 @@ public final class MQTTClient {
|
|
|
493
547
|
try await w.write(Data(pr))
|
|
494
548
|
try await w.drain()
|
|
495
549
|
}
|
|
550
|
+
case MQTTMessageType.PINGRESP.rawValue:
|
|
551
|
+
self.lock.lock()
|
|
552
|
+
let pingCont = self.pendingPingresp
|
|
553
|
+
self.pendingPingresp = nil
|
|
554
|
+
self.lock.unlock()
|
|
555
|
+
pingCont?.resume()
|
|
496
556
|
case MQTTMessageType.PUBLISH.rawValue:
|
|
497
557
|
let qos = (msgType >> 1) & 0x03
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
let version = self.activeProtocolVersion
|
|
503
|
-
if version == MQTTProtocolLevel.v5 {
|
|
504
|
-
var map = self.topicAliasMap
|
|
505
|
-
self.lock.unlock()
|
|
506
|
-
(topic, packetId, payload) = try MQTT5Protocol.parsePublishV5(Data(rest), offset: 0, qos: UInt8(qos), topicAliasMap: &map)
|
|
558
|
+
do {
|
|
559
|
+
let topic: String
|
|
560
|
+
let packetId: UInt16?
|
|
561
|
+
let payload: Data
|
|
507
562
|
self.lock.lock()
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
cb?(payload)
|
|
563
|
+
let version = self.activeProtocolVersion
|
|
564
|
+
if version == MQTTProtocolLevel.v5 {
|
|
565
|
+
var map = self.topicAliasMap
|
|
566
|
+
self.lock.unlock()
|
|
567
|
+
(topic, packetId, payload) = try MQTT5Protocol.parsePublishV5(Data(rest), offset: 0, qos: UInt8(qos), topicAliasMap: &map)
|
|
568
|
+
self.lock.lock()
|
|
569
|
+
self.topicAliasMap = map
|
|
570
|
+
self.lock.unlock()
|
|
571
|
+
} else {
|
|
572
|
+
self.lock.unlock()
|
|
573
|
+
let (t, pid, pl, _) = try MQTTProtocol.parsePublish(Data(rest), offset: 0, qos: UInt8(qos))
|
|
574
|
+
topic = t
|
|
575
|
+
packetId = pid
|
|
576
|
+
payload = pl
|
|
577
|
+
}
|
|
524
578
|
|
|
525
|
-
if qos >= 1, let pid = packetId {
|
|
526
579
|
self.lock.lock()
|
|
527
|
-
let
|
|
580
|
+
let cb = self.subscribedTopics[topic]
|
|
581
|
+
let globalCb = self.onPublish
|
|
528
582
|
self.lock.unlock()
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
583
|
+
globalCb?(topic, payload)
|
|
584
|
+
cb?(payload)
|
|
585
|
+
|
|
586
|
+
if qos >= 1, let pid = packetId {
|
|
587
|
+
self.lock.lock()
|
|
588
|
+
let wPuback = self.writer
|
|
589
|
+
self.lock.unlock()
|
|
590
|
+
if let wPuback = wPuback {
|
|
591
|
+
let puback = MQTTProtocol.buildPuback(packetId: pid)
|
|
592
|
+
try await wPuback.write(Data(puback))
|
|
593
|
+
try await wPuback.drain()
|
|
594
|
+
}
|
|
533
595
|
}
|
|
596
|
+
} catch {
|
|
597
|
+
// PUBLISH parse failed: log and skip this message (don't disconnect)
|
|
598
|
+
let hex = rest.prefix(64).map { String(format: "%02x", $0) }.joined()
|
|
599
|
+
print("[MqttQuic] PUBLISH parse failed: \(error) restLen=\(rest.count) hex=\(hex)")
|
|
534
600
|
}
|
|
535
601
|
default:
|
|
536
602
|
break
|
|
@@ -550,6 +616,8 @@ public final class MQTTClient {
|
|
|
550
616
|
subackContinuation = nil
|
|
551
617
|
unsubackContinuation?.resume(throwing: error)
|
|
552
618
|
unsubackContinuation = nil
|
|
619
|
+
pendingPingresp?.resume(throwing: error)
|
|
620
|
+
pendingPingresp = nil
|
|
553
621
|
state = .disconnected
|
|
554
622
|
lock.unlock()
|
|
555
623
|
}
|
|
@@ -15,6 +15,7 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
15
15
|
public let jsName = "MqttQuic"
|
|
16
16
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
17
17
|
CAPPluginMethod(name: "ping", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "sendKeepalive", returnType: CAPPluginReturnPromise),
|
|
18
19
|
CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
|
|
19
20
|
CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
|
|
20
21
|
CAPPluginMethod(name: "publish", returnType: CAPPluginReturnPromise),
|
|
@@ -27,6 +28,19 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
27
28
|
|
|
28
29
|
@objc override public func load() {}
|
|
29
30
|
|
|
31
|
+
@objc func sendKeepalive(_ call: CAPPluginCall) {
|
|
32
|
+
let timeoutMs = UInt64(call.getInt("timeoutMs") ?? 5000)
|
|
33
|
+
let clamped = min(max(timeoutMs, 1000), 15000)
|
|
34
|
+
Task {
|
|
35
|
+
do {
|
|
36
|
+
let ok = await client.sendMqttPing(timeoutMs: clamped)
|
|
37
|
+
DispatchQueue.main.async { call.resolve(["ok": ok]) }
|
|
38
|
+
} catch {
|
|
39
|
+
DispatchQueue.main.async { call.reject("\(error)") }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
@objc func ping(_ call: CAPPluginCall) {
|
|
31
45
|
let host = call.getString("host") ?? ""
|
|
32
46
|
let port = call.getInt("port") ?? 1884
|
|
@@ -120,12 +134,10 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
120
134
|
// Forward every incoming PUBLISH to JS so addListener('message', ...) receives topic + payload (matches Android)
|
|
121
135
|
client.onPublish = { [weak self] topic, payload in
|
|
122
136
|
guard let self = self else { return }
|
|
123
|
-
let payloadStr: String
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
payloadStr = payload.base64EncodedString()
|
|
128
|
-
}
|
|
137
|
+
let payloadStr: String = {
|
|
138
|
+
if let str = String(data: payload, encoding: .utf8) { return str }
|
|
139
|
+
return payload.base64EncodedString()
|
|
140
|
+
}()
|
|
129
141
|
DispatchQueue.main.async {
|
|
130
142
|
self.notifyListeners("message", data: ["topic": topic, "payload": payloadStr])
|
|
131
143
|
}
|
package/ios/libs/libnghttp3.a
CHANGED
|
Binary file
|
package/ios/libs/libngtcp2.a
CHANGED
|
Binary file
|
|
Binary file
|
package/ios/libs/libwolfssl.a
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annadata/capacitor-mqtt-quic",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MQTT-over-QUIC client for Capacitor (iOS, Android, Web). Native: ngtcp2+WolfSSL; Web: MQTT over WebSocket (WSS), same API.",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|