├── .github
├── mosquitto
│ ├── ditchoom_mqtt_test_server.conf
│ └── mosquitto-osx.sh
└── workflows
│ ├── android_integration.yaml
│ ├── merged.yaml
│ └── review.yaml
├── .gitignore
├── .idea
├── artifacts
│ ├── base_models_jslegacy_10_0_0_SNAPSHOT.xml
│ ├── base_models_jvm_10_0_0_SNAPSHOT.xml
│ ├── common_desktop_1_0_SNAPSHOT.xml
│ ├── desktop_jvm_1_0_SNAPSHOT.xml
│ ├── models_base_jslegacy_10_0_0_SNAPSHOT.xml
│ ├── models_base_jvm_10_0_0_SNAPSHOT.xml
│ ├── models_v4_jslegacy_10_0_0_SNAPSHOT.xml
│ ├── models_v4_jvm_10_0_0_SNAPSHOT.xml
│ ├── models_v5_jslegacy_10_0_0_SNAPSHOT.xml
│ ├── models_v5_jvm_10_0_0_SNAPSHOT.xml
│ ├── mqtt_client_jslegacy_10_0_0_SNAPSHOT.xml
│ ├── mqtt_client_jvm_10_0_0_SNAPSHOT.xml
│ └── web_js.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── Readme.md
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── kotlin-js-store
└── yarn.lock
├── local.properties
├── models-base
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── androidMain
│ └── AndroidManifest.xml
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt
│ │ ├── Exception.kt
│ │ ├── InMemoryPersistence.kt
│ │ ├── MqttWarning.kt
│ │ ├── Persistence.kt
│ │ ├── connection
│ │ ├── MqttBroker.kt
│ │ └── MqttConnectionOptions.kt
│ │ └── controlpacket
│ │ ├── ControlPacket.kt
│ │ ├── ControlPacketFactory.kt
│ │ ├── IConnectionAcknowledgment.kt
│ │ ├── IConnectionRequest.kt
│ │ ├── IDisconnectNotification.kt
│ │ ├── IPingRequest.kt
│ │ ├── IPingResponse.kt
│ │ ├── IPublishAcknowledgment.kt
│ │ ├── IPublishComplete.kt
│ │ ├── IPublishMessage.kt
│ │ ├── IPublishReceived.kt
│ │ ├── IPublishRelease.kt
│ │ ├── IReserved.kt
│ │ ├── ISubscribeAcknowledgement.kt
│ │ ├── ISubscribeRequest.kt
│ │ ├── ISubscription.kt
│ │ ├── IUnsubscribeAcknowledgment.kt
│ │ ├── IUnsubscribeRequest.kt
│ │ ├── Level.kt
│ │ ├── PacketIdUtils.kt
│ │ ├── QualityOfService.kt
│ │ ├── String.kt
│ │ ├── Topic.kt
│ │ ├── Type.kt
│ │ └── format
│ │ ├── ReasonCode.kt
│ │ └── fixed
│ │ ├── DirectionOfFlow.kt
│ │ └── Flags.kt
│ └── commonTest
│ └── kotlin
│ └── com
│ └── ditchoom
│ └── mqtt
│ └── controlpacket
│ ├── StringTests.kt
│ ├── TopicTests.kt
│ └── VariableByteIntegerTests.kt
├── models-v4
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt3
│ │ └── persistence
│ │ ├── DefaultPersistence.kt
│ │ └── SqlDriver.kt
│ ├── appleMain
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt3
│ │ └── persistence
│ │ ├── DefaultPersistence.kt
│ │ └── SqlDriver.kt
│ ├── commonMain
│ ├── kotlin
│ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt3
│ │ │ ├── controlpacket
│ │ │ ├── ConnectionAcknowledgment.kt
│ │ │ ├── ConnectionRequest.kt
│ │ │ ├── ControlPacketV4.kt
│ │ │ ├── ControlPacketV4Factory.kt
│ │ │ ├── DisconnectNotification.kt
│ │ │ ├── PingRequest.kt
│ │ │ ├── PingResponse.kt
│ │ │ ├── PublishAcknowledgment.kt
│ │ │ ├── PublishComplete.kt
│ │ │ ├── PublishMessage.kt
│ │ │ ├── PublishReceived.kt
│ │ │ ├── PublishRelease.kt
│ │ │ ├── Reserved.kt
│ │ │ ├── SubscribeAcknowledgement.kt
│ │ │ ├── SubscribeRequest.kt
│ │ │ ├── UnsubscribeAcknowledgment.kt
│ │ │ └── UnsubscribeRequest.kt
│ │ │ └── persistence
│ │ │ ├── DefaultPersistence.kt
│ │ │ ├── SqlDatabasePersistence.kt
│ │ │ └── SqlPersistence.kt
│ └── sqldelight
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt3
│ │ └── persistence
│ │ ├── Broker.sq
│ │ ├── ConnectionRequest.sq
│ │ ├── PublishMessage.sq
│ │ ├── QoS2Messages.sq
│ │ ├── SocketConnection.sq
│ │ ├── Subscription.sq
│ │ ├── SubscriptionRequest.sq
│ │ └── UnsubscribeRequest.sq
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt3
│ │ ├── controlpacket
│ │ ├── ConnectionAcknowledgmentTests.kt
│ │ ├── ConnectionRequestTests.kt
│ │ ├── DisconnectTests.kt
│ │ ├── PingRequestTests.kt
│ │ ├── PingResponseTests.kt
│ │ ├── PublishAcknowledgementTest.kt
│ │ ├── PublishCompleteTests.kt
│ │ ├── PublishMessageTests.kt
│ │ ├── PublishReceivedTests.kt
│ │ ├── PublishReleaseTests.kt
│ │ ├── SubscribeAcknowledgementTests.kt
│ │ ├── SubscribeRequestTests.kt
│ │ ├── UnsubscribeAcknowledgmentTests.kt
│ │ └── UnsubscribeRequestTests.kt
│ │ └── persistence
│ │ └── PersistenceTests.kt
│ ├── jsMain
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt3
│ │ └── persistence
│ │ ├── DefaultPersistence.kt
│ │ ├── IDBObjects.kt
│ │ ├── IDBPersistence.kt
│ │ ├── Qos2Message.kt
│ │ └── SqlDriver.kt
│ └── jvmMain
│ └── kotlin
│ └── com
│ └── ditchoom
│ └── mqtt3
│ └── persistence
│ ├── DefaultPersistence.kt
│ └── SqlDriver.kt
├── models-v5
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt5
│ │ └── persistence
│ │ ├── DefaultPersistence.kt
│ │ └── SqlDriver.kt
│ ├── appleMain
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt5
│ │ └── persistence
│ │ ├── DefaultPersistence.kt
│ │ └── SqlDriver.kt
│ ├── commonMain
│ ├── kotlin
│ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt5
│ │ │ ├── controlpacket
│ │ │ ├── AuthenticationExchange.kt
│ │ │ ├── ConnectionAcknowledgment.kt
│ │ │ ├── ConnectionRequest.kt
│ │ │ ├── ControlPacketV5.kt
│ │ │ ├── ControlPacketV5Factory.kt
│ │ │ ├── DisconnectNotification.kt
│ │ │ ├── PingRequest.kt
│ │ │ ├── PingResponse.kt
│ │ │ ├── PublishAcknowledgment.kt
│ │ │ ├── PublishComplete.kt
│ │ │ ├── PublishMessage.kt
│ │ │ ├── PublishReceived.kt
│ │ │ ├── PublishRelease.kt
│ │ │ ├── Reserved.kt
│ │ │ ├── SubscribeAcknowledgement.kt
│ │ │ ├── SubscribeRequest.kt
│ │ │ ├── UnsubscribeAcknowledgment.kt
│ │ │ ├── UnsubscribeRequest.kt
│ │ │ └── properties
│ │ │ │ ├── AssignedClientIdentifier.kt
│ │ │ │ ├── Authentication.kt
│ │ │ │ ├── AuthenticationData.kt
│ │ │ │ ├── AuthenticationMethod.kt
│ │ │ │ ├── ContentType.kt
│ │ │ │ ├── CorrelationData.kt
│ │ │ │ ├── MaximumPacketSize.kt
│ │ │ │ ├── MaximumQos.kt
│ │ │ │ ├── MessageExpiryInterval.kt
│ │ │ │ ├── PayloadFormatIndicator.kt
│ │ │ │ ├── Property.kt
│ │ │ │ ├── ReasonString.kt
│ │ │ │ ├── ReceiveMaximum.kt
│ │ │ │ ├── RequestProblemInformation.kt
│ │ │ │ ├── RequestResponseInformation.kt
│ │ │ │ ├── ResponseInformation.kt
│ │ │ │ ├── ResponseTopic.kt
│ │ │ │ ├── RetainAvailable.kt
│ │ │ │ ├── ServerKeepAlive.kt
│ │ │ │ ├── ServerReference.kt
│ │ │ │ ├── SessionExpiryInterval.kt
│ │ │ │ ├── SharedSubscriptionAvailable.kt
│ │ │ │ ├── SubscriptionIdentifier.kt
│ │ │ │ ├── SubscriptionIdentifierAvailable.kt
│ │ │ │ ├── TopicAlias.kt
│ │ │ │ ├── TopicAliasMaximum.kt
│ │ │ │ ├── Type.kt
│ │ │ │ ├── UserProperty.kt
│ │ │ │ ├── WildcardSubscriptionAvailable.kt
│ │ │ │ └── WillDelayInterval.kt
│ │ │ └── persistence
│ │ │ ├── DefaultPersistence.kt
│ │ │ ├── SqlDatabasePersistence.kt
│ │ │ └── SqlPersistence.kt
│ └── sqldelight
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt5
│ │ └── persistence
│ │ ├── Broker.sq
│ │ ├── ConnectionRequest.sq
│ │ ├── PublishMessage.sq
│ │ ├── QoS2Messages.sq
│ │ ├── SocketConnection.sq
│ │ ├── Subscription.sq
│ │ ├── SubscriptionRequest.sq
│ │ ├── UnsubscribeRequest.sq
│ │ └── UserProperty.sq
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt5
│ │ ├── controlpacket
│ │ ├── AuthenticationExchangeTests.kt
│ │ ├── ConnectionAcknowledgmentTests.kt
│ │ ├── ConnectionRequestTests.kt
│ │ ├── DisconnectTests.kt
│ │ ├── FlagTests.kt
│ │ ├── PingRequestTests.kt
│ │ ├── PingResponseTests.kt
│ │ ├── PublishAcknowledgementTest.kt
│ │ ├── PublishCompleteTests.kt
│ │ ├── PublishMessageTests.kt
│ │ ├── PublishReceivedTests.kt
│ │ ├── PublishReleaseTests.kt
│ │ ├── ReasonCodeTests.kt
│ │ ├── SubscribeAcknowledgementTests.kt
│ │ ├── SubscribeRequestTest.kt
│ │ ├── TypeTests.kt
│ │ ├── UnsubscribeAcknowledgmentTests.kt
│ │ └── UnsubscribeRequestTests.kt
│ │ └── persistence
│ │ └── PersistenceTests.kt
│ ├── jsMain
│ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt5
│ │ └── persistence
│ │ ├── DefaultPersistence.kt
│ │ ├── IDBObjects.kt
│ │ ├── IDBPersistence.kt
│ │ ├── Qos2Message.kt
│ │ └── SqlDriver.kt
│ └── jvmMain
│ └── kotlin
│ └── com
│ └── ditchoom
│ └── mqtt5
│ └── persistence
│ ├── DefaultPersistence.kt
│ └── SqlDriver.kt
├── mqtt-client
├── build.gradle.kts
├── gradle.properties
├── karma.config.d
│ └── karma.conf.js
├── mqtt_client.podspec
├── src
│ ├── androidInstrumentedTest
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ ├── ipc
│ │ │ └── IPCTest.kt
│ │ │ └── net
│ │ │ └── Platform.mqtt.mqtt-client.unit.kt
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ ├── aidl
│ │ │ └── com
│ │ │ │ └── ditchoom
│ │ │ │ └── mqtt
│ │ │ │ └── client
│ │ │ │ └── ipc
│ │ │ │ ├── IPCMqttClient.aidl
│ │ │ │ ├── IPCMqttService.aidl
│ │ │ │ ├── MqttCompletionCallback.aidl
│ │ │ │ ├── MqttGetClientCallback.aidl
│ │ │ │ ├── MqttMessageCallback.aidl
│ │ │ │ └── MqttMessageTransferredCallback.aidl
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ ├── MqttServiceInitializer.kt
│ │ │ └── ipc
│ │ │ ├── AndroidMqttClientIPCServer.kt
│ │ │ ├── AndroidRemoteMqttClient.kt
│ │ │ ├── AndroidRemoteMqttServiceClient.kt
│ │ │ ├── AndroidRemoteMqttServiceWorker.kt
│ │ │ ├── AndroidRemoteServiceFactory.kt
│ │ │ ├── MqttManagerService.kt
│ │ │ ├── MqttServiceHelper.kt
│ │ │ └── SuspendingMqttCompletionCallback.kt
│ ├── androidUnitTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── net
│ │ │ └── Platform.mqtt.mqtt-client.unit.kt
│ ├── appleMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── ipc
│ │ │ └── RemoteServiceFactory.kt
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ ├── BufferedControlPacketReader.kt
│ │ │ ├── ConnectivityManager.kt
│ │ │ ├── ControlPacketHelper.kt
│ │ │ ├── ControlPacketOperation.kt
│ │ │ ├── ControlPacketProcessor.kt
│ │ │ ├── LocalMqttClient.kt
│ │ │ ├── LocalMqttService.kt
│ │ │ ├── MqttClient.kt
│ │ │ ├── MqttService.kt
│ │ │ ├── MqttSocketSession.kt
│ │ │ ├── Observer.kt
│ │ │ ├── UnavailableMqttServiceException.kt
│ │ │ └── ipc
│ │ │ ├── RemoteMqttClient.kt
│ │ │ ├── RemoteMqttClientWorker.kt
│ │ │ ├── RemoteMqttServiceClient.kt
│ │ │ ├── RemoteMqttServiceWorker.kt
│ │ │ └── RemoteServiceFactory.kt
│ ├── commonTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── net
│ │ │ ├── MqttClientTest.kt
│ │ │ ├── MqttSocketSessionTest.kt
│ │ │ └── Platform.kt
│ ├── jsMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── ipc
│ │ │ ├── JsRemoteMqttClient.kt
│ │ │ ├── JsRemoteMqttClientWorker.kt
│ │ │ ├── JsRemoteMqttServiceClient.kt
│ │ │ ├── JsRemoteMqttServiceWorker.kt
│ │ │ ├── MessageHelper.kt
│ │ │ ├── RemoteServiceFactory.kt
│ │ │ └── WorkerHelper.kt
│ ├── jsTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── net
│ │ │ └── Platform.mqtt.mqtt-client.js.kt
│ ├── jvmMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── ipc
│ │ │ └── RemoteServiceFactory.kt
│ ├── jvmTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── ditchoom
│ │ │ └── mqtt
│ │ │ └── client
│ │ │ └── net
│ │ │ └── Platform.com.ditchoom.mqtt.mqtt-client.jvm.kt
│ └── nativeTest
│ │ └── kotlin
│ │ └── com
│ │ └── ditchoom
│ │ └── mqtt
│ │ └── client
│ │ └── net
│ │ └── Platform.mqtt.mqtt-client.native.kt
└── webpack.config.d
│ └── patch.js
└── settings.gradle.kts
/.github/mosquitto/ditchoom_mqtt_test_server.conf:
--------------------------------------------------------------------------------
1 | listener 1883
2 | protocol mqtt
3 | allow_anonymous true
4 |
5 | listener 80
6 | protocol websockets
7 | allow_anonymous true
8 |
--------------------------------------------------------------------------------
/.github/mosquitto/mosquitto-osx.sh:
--------------------------------------------------------------------------------
1 | brew install mosquitto
2 | rm /usr/local/etc/mosquitto/mosquitto.conf
3 | cp "${GITHUB_WORKSPACE}/.github/mosquitto/ditchoom_mqtt_test_server.conf" /usr/local/etc/mosquitto/mosquitto.conf
4 | chmod 755 /usr/local/etc/mosquitto/mosquitto.conf
5 | /usr/local/opt/mosquitto/sbin/mosquitto -d -c "${GITHUB_WORKSPACE}/.github/mosquitto/ditchoom_mqtt_test_server.conf"
6 |
--------------------------------------------------------------------------------
/.github/workflows/android_integration.yaml:
--------------------------------------------------------------------------------
1 | name: "Android Emulator Integration Test"
2 | on:
3 | pull_request:
4 | paths-ignore:
5 | - '*.md'
6 | types:
7 | - synchronize
8 | - opened
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | api-level: [21, 27]
15 | steps:
16 | - name: checkout
17 | uses: actions/checkout@v4
18 | - name: Set up JDK 19
19 | uses: actions/setup-java@v4
20 | with:
21 | distribution: 'zulu'
22 | java-version: '19'
23 | cache: gradle
24 | - name: Enable KVM
25 | run: |
26 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
27 | sudo udevadm control --reload-rules
28 | sudo udevadm trigger --name-match=kvm
29 |
30 | - name: Gradle cache
31 | uses: gradle/actions/setup-gradle@v3
32 |
33 | - name: AVD cache
34 | uses: actions/cache@v4
35 | id: avd-cache
36 | with:
37 | path: |
38 | ~/.android/avd/*
39 | ~/.android/adb*
40 | key: avd-${{ matrix.api-level }}
41 |
42 | - name: create AVD and generate snapshot for caching
43 | if: steps.avd-cache.outputs.cache-hit != 'true'
44 | uses: reactivecircus/android-emulator-runner@v2
45 | with:
46 | api-level: ${{ matrix.api-level }}
47 | force-avd-creation: false
48 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
49 | disable-animations: false
50 | script: echo "Generated AVD snapshot for caching."
51 |
52 | - name: run tests
53 | uses: reactivecircus/android-emulator-runner@v2
54 | with:
55 | api-level: ${{ matrix.api-level }}
56 | force-avd-creation: false
57 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
58 | disable-animations: true
59 | script: ./gradlew connectedCheck
--------------------------------------------------------------------------------
/.github/workflows/review.yaml:
--------------------------------------------------------------------------------
1 | name: "Build and Test"
2 | on:
3 | pull_request:
4 | paths-ignore:
5 | - '*.md'
6 | types:
7 | - synchronize
8 | - opened
9 | jobs:
10 | review:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | os: [ macos-latest, ubuntu-latest ]
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Set up JDK 19
18 | uses: actions/setup-java@v4
19 | with:
20 | distribution: 'zulu'
21 | java-version: '19'
22 | cache: gradle
23 | - name: Setup Chrome
24 | uses: browser-actions/setup-chrome@v1
25 | - name: Gradle cache
26 | uses: gradle/actions/setup-gradle@v3
27 | - name: Tests with Gradle Major
28 | if: ${{ contains(github.event.pull_request.labels.*.name, 'major') }}
29 | run: ./gradlew ktlintCheck assemble build check allTests publishToMavenLocal -PincrementMajor=true
30 | - name: Tests with Gradle Minor
31 | if: ${{ contains(github.event.pull_request.labels.*.name, 'minor') }}
32 | run: ./gradlew ktlintCheck assemble build check allTests publishToMavenLocal -PincrementMinor=true
33 | - name: Tests with Gradle Patch
34 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'major') && !contains(github.event.pull_request.labels.*.name, 'minor') }}
35 | run: ./gradlew ktlintCheck assemble build check allTests publishToMavenLocal
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 |
7 | ### IntelliJ IDEA ###
8 | .idea
9 | !.idea/vcs.xml
10 | !.idea/runConfigurations
11 | !.idea/codeStyles
12 | *.iws
13 | *.iml
14 | *.ipr
15 | out/
16 | !**/src/main/**/out/
17 | !**/src/test/**/out/
18 |
19 | ### Eclipse ###
20 | .apt_generated
21 | .classpath
22 | .factorypath
23 | .project
24 | .settings
25 | .springBeans
26 | .sts4-cache
27 | bin/
28 | !**/src/main/**/bin/
29 | !**/src/test/**/bin/
30 |
31 | ### NetBeans ###
32 | /nbproject/private/
33 | /nbbuild/
34 | /dist/
35 | /nbdist/
36 | /.nb-gradle/
37 |
38 | ### VS Code ###
39 | .vscode/
40 |
41 | ### Mac OS ###
42 | .DS_Store
43 |
44 | ### Application DB
45 | mqtt-client/mqtt4.db
46 | mqtt-client/mqtt5.db
47 |
48 | .kotlin
--------------------------------------------------------------------------------
/.idea/artifacts/base_models_jslegacy_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/base-models/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/base_models_jvm_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/base-models/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/common_desktop_1_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/monitor/common/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/desktop_jvm_1_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/monitor/desktop/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/models_base_jslegacy_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/models-base/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/models_base_jvm_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/models-base/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/models_v4_jslegacy_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/models-v4/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/models_v4_jvm_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/models-v4/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/models_v5_jslegacy_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/models-v5/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/models_v5_jvm_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/models-v5/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/mqtt_client_jslegacy_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/mqtt-client/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/mqtt_client_jvm_10_0_0_SNAPSHOT.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/mqtt-client/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/web_js.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/monitor/web/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import groovy.util.Node
2 | import groovy.xml.XmlParser
3 | import java.net.URL
4 |
5 | val libraryVersionPrefix: String by project
6 | group "com.ditchoom"
7 | version "$libraryVersionPrefix.0-SNAPSHOT"
8 |
9 | allprojects {
10 | repositories {
11 | google()
12 | mavenCentral()
13 | mavenLocal()
14 | }
15 | }
16 |
17 | plugins {
18 | kotlin("multiplatform") apply false
19 | kotlin("android") apply false
20 | id("com.android.application") apply false
21 | id("com.android.library") apply false
22 | }
23 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | publishedGroupId=com.ditchoom
3 | libraryVersionPrefix=1.1.
4 | android.useAndroidX=true
5 | kotlin.version=2.0.0
6 | agp.version=8.4.0
7 | nexus-staging.version=0.30.0
8 | ktlint.version=12.1.1
9 | sqldelight.version=2.0.2
10 | buffer.version=1.4.1
11 | coroutines.version=1.8.1
12 | socket.version=1.1.14
13 | websocket.version=1.1.0
14 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
15 | kotlin.mpp.enableCInteropCommonization=true
16 | kotlin.native.ignoreIncorrectDependencies=true
17 | gnsp.disableApplyOnlyOnRootProjectEnforcement=true
18 | siteUrl=https://ditchoom.com
19 | gitUrl=git://github.com/DitchOOM/mqtt.git
20 | developerId=thebehera
21 | developerOrg=Ditchoom
22 | developerName=Rahul Behera
23 | developerEmail=rbehera@gmail.com
24 | licenseName=The Apache Software License, Version 2.0
25 | licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt
26 | allLicenses=["Apache-2.0"]
27 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DitchOoM/mqtt/eecf0ad09aa51db3b736716ce094f43dedc0bcec/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file must *NOT* be checked into Version Control Systems,
2 | # as it contains information specific to your local configuration.
3 | #
4 | # Location of the SDK. This is only used by Gradle.
5 | # For customization when using a Version Control System, please read the
6 | # header note.
7 | #Tue Jun 11 10:54:00 PDT 2024
8 | sdk.dir=/Users/thebehera/Library/Android/sdk
9 |
--------------------------------------------------------------------------------
/models-base/gradle.properties:
--------------------------------------------------------------------------------
1 | libraryName=MQTT Base Models
2 | libraryDescription=Defines the MQTT control packets and topics
3 | artifactName=mqtt-base-models
4 |
5 |
--------------------------------------------------------------------------------
/models-base/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/Exception.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt
2 |
3 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
4 |
5 | open class MqttException(msg: String, val reasonCode: UByte) : Exception(msg)
6 |
7 | open class MalformedPacketException(msg: String) : MqttException(msg, 0x81.toUByte())
8 |
9 | open class ProtocolError(msg: String) : MqttException(msg, 0x82.toUByte())
10 |
11 | class MalformedInvalidVariableByteInteger(value: Int) : MqttException(
12 | "Malformed Variable Byte Integer: This " +
13 | "property must be a number between 0 and %VARIABLE_BYTE_INT_MAX . Read value was: $value",
14 | ReasonCode.MALFORMED_PACKET.byte,
15 | )
16 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/MqttWarning.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt
2 |
3 | open class MqttWarning(mandatoryNormativeStatement: String, message: String) :
4 | Exception("$mandatoryNormativeStatement $message")
5 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/connection/MqttBroker.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.connection
2 |
3 | import com.ditchoom.mqtt.controlpacket.IConnectionRequest
4 |
5 | data class MqttBroker(
6 | val identifier: Int,
7 | val connectionOps: Collection,
8 | val connectionRequest: IConnectionRequest,
9 | ) {
10 | val brokerId = identifier
11 | val protocolVersion = connectionRequest.protocolVersion.toByte()
12 |
13 | override fun equals(other: Any?): Boolean {
14 | if (this === other) return true
15 | if (other == null || this::class != other::class) return false
16 |
17 | other as MqttBroker
18 |
19 | if (identifier != other.identifier) return false
20 | if (connectionOps != other.connectionOps) return false
21 | if (connectionRequest != other.connectionRequest) return false
22 |
23 | return true
24 | }
25 |
26 | override fun hashCode(): Int {
27 | var result = identifier
28 | result = 31 * result + connectionOps.hashCode()
29 | result = 31 * result + connectionRequest.hashCode()
30 | return result
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/connection/MqttConnectionOptions.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.connection
2 |
3 | import kotlin.time.Duration
4 | import kotlin.time.Duration.Companion.seconds
5 |
6 | sealed interface MqttConnectionOptions {
7 | val host: String
8 | val port: Int
9 | val tls: Boolean
10 | val readTimeout: Duration
11 | val writeTimeout: Duration
12 | val connectionTimeout: Duration
13 |
14 | fun copy(
15 | host: String = this.host,
16 | port: Int = this.port,
17 | tls: Boolean = this.tls,
18 | connectionTimeout: Duration = this.connectionTimeout,
19 | readTimeout: Duration = this.readTimeout,
20 | writeTimeout: Duration = this.writeTimeout,
21 | isWebsocket: Boolean = this is WebSocketConnectionOptions,
22 | websocketEndpoint: String = if (this is WebSocketConnectionOptions) this.websocketEndpoint else "/mqtt",
23 | protocols: List = if (this is WebSocketConnectionOptions) this.protocols else emptyList(),
24 | ): MqttConnectionOptions {
25 | return if (isWebsocket) {
26 | WebSocketConnectionOptions(
27 | host,
28 | port,
29 | tls,
30 | connectionTimeout,
31 | readTimeout,
32 | writeTimeout,
33 | websocketEndpoint,
34 | protocols,
35 | )
36 | } else {
37 | SocketConnection(host, port, tls, connectionTimeout, readTimeout, writeTimeout)
38 | }
39 | }
40 |
41 | data class SocketConnection(
42 | override val host: String,
43 | override val port: Int,
44 | override val tls: Boolean = port == 8883,
45 | override val connectionTimeout: Duration = 15.seconds,
46 | override val readTimeout: Duration = connectionTimeout,
47 | override val writeTimeout: Duration = connectionTimeout,
48 | ) : MqttConnectionOptions
49 |
50 | data class WebSocketConnectionOptions(
51 | override val host: String,
52 | override val port: Int,
53 | override val tls: Boolean = port == 443,
54 | override val connectionTimeout: Duration = 15.seconds,
55 | override val readTimeout: Duration = connectionTimeout,
56 | override val writeTimeout: Duration = connectionTimeout,
57 | val websocketEndpoint: String = "/",
58 | val protocols: List = listOf("mqtt"),
59 | ) : MqttConnectionOptions {
60 | internal fun buildUrl(): String {
61 | val prefix =
62 | if (tls) {
63 | "wss://"
64 | } else {
65 | "ws://"
66 | }
67 | val postfix = "$host:$port$websocketEndpoint"
68 | return prefix + postfix
69 | }
70 |
71 | companion object {
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/ControlPacketFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.mqtt.Persistence
5 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.readVariableByteInteger
6 | import com.ditchoom.mqtt.controlpacket.ISubscription.RetainHandling
7 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
8 |
9 | interface ControlPacketFactory {
10 | val protocolVersion: Int
11 |
12 | fun from(buffer: ReadBuffer): ControlPacket {
13 | val byte1 = buffer.readUnsignedByte()
14 | val remainingLength = buffer.readVariableByteInteger()
15 | return from(buffer, byte1, remainingLength)
16 | }
17 |
18 | fun from(
19 | buffer: ReadBuffer,
20 | byte1: UByte,
21 | remainingLength: Int,
22 | ): ControlPacket
23 |
24 | fun pingRequest(): IPingRequest
25 |
26 | fun pingResponse(): IPingResponse
27 |
28 | fun subscribe(
29 | topicFilter: Topic,
30 | maximumQos: QualityOfService = QualityOfService.AT_LEAST_ONCE,
31 | noLocal: Boolean = false,
32 | retainAsPublished: Boolean = false,
33 | retainHandling: RetainHandling = RetainHandling.SEND_RETAINED_MESSAGES_AT_TIME_OF_SUBSCRIBE,
34 | serverReference: String? = null,
35 | userProperty: List> = emptyList(),
36 | ): ISubscribeRequest
37 |
38 | fun subscribe(
39 | subscriptions: Set,
40 | serverReference: String? = null,
41 | userProperty: List> = emptyList(),
42 | ): ISubscribeRequest
43 |
44 | fun publish(
45 | dup: Boolean = false,
46 | qos: QualityOfService = QualityOfService.AT_MOST_ONCE,
47 | retain: Boolean = false,
48 | topicName: Topic,
49 | payload: ReadBuffer? = null,
50 | // MQTT 5 Properties
51 | payloadFormatIndicator: Boolean = false,
52 | messageExpiryInterval: Long? = null,
53 | topicAlias: Int? = null,
54 | responseTopic: Topic? = null,
55 | correlationData: ReadBuffer? = null,
56 | userProperty: List> = emptyList(),
57 | subscriptionIdentifier: Set = emptySet(),
58 | contentType: String? = null,
59 | ): IPublishMessage
60 |
61 | fun unsubscribe(
62 | topic: Topic,
63 | userProperty: List> = emptyList(),
64 | ) = unsubscribe(setOf(topic), userProperty)
65 |
66 | fun unsubscribe(
67 | topics: Set,
68 | userProperty: List> = emptyList(),
69 | ): IUnsubscribeRequest
70 |
71 | fun disconnect(
72 | reasonCode: ReasonCode = ReasonCode.NORMAL_DISCONNECTION,
73 | sessionExpiryIntervalSeconds: ULong? = null,
74 | reasonString: String? = null,
75 | userProperty: List> = emptyList(),
76 | ): IDisconnectNotification
77 |
78 | suspend fun defaultPersistence(
79 | androidContext: Any? = null,
80 | name: String = "mqtt$protocolVersion.db",
81 | inMemory: Boolean = false,
82 | ): Persistence
83 | }
84 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IConnectionAcknowledgment.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IConnectionAcknowledgment : ControlPacket {
4 | val isSuccessful: Boolean
5 | val connectionReason: String
6 | val sessionPresent: Boolean
7 |
8 | val sessionExpiryInterval: ULong get() = 0uL
9 | val receiveMaximum: Int get() = UShort.MAX_VALUE.toInt()
10 | val maximumQos: QualityOfService get() = QualityOfService.EXACTLY_ONCE
11 | val maxPacketSize: ULong get() = ULong.MAX_VALUE
12 | val assignedClientIdentifier: String? get() = null
13 | val serverKeepAlive: Int get() = -1
14 | }
15 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IConnectionRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 |
5 | interface IConnectionRequest : ControlPacket {
6 | val protocolName: String
7 | val protocolVersion: Int
8 | val hasUserName: Boolean
9 | val hasPassword: Boolean
10 | val willRetain: Boolean
11 | val willQos: QualityOfService
12 | val willFlag: Boolean
13 | val cleanStart: Boolean
14 | val keepAliveTimeoutSeconds: UShort
15 |
16 | // MQTT 5 Variable Header Properties
17 |
18 | // Null if unsupported, 0 if absent or set to 0.
19 | val sessionExpiryIntervalSeconds: ULong?
20 | get() = null
21 | val receiveMaximum: UShort
22 | get() = UShort.MAX_VALUE
23 |
24 | val maxPacketSize: ULong
25 | get() = ULong.MAX_VALUE
26 |
27 | // Null if unsupported, 0 if absent or set to 0.
28 | val topicAliasMax: UShort?
29 | get() = null
30 |
31 | // Mqtt Variable Header
32 | val clientIdentifier: String
33 | val willTopic: Topic?
34 | val willPayload: ReadBuffer?
35 | val userName: String?
36 | val password: String?
37 |
38 | // MQTT 5 Payload Will Properties
39 | val willDelayIntervalSeconds: Long get() = 0L
40 | val payloadFormatIndicator: Boolean get() = false
41 | val messageExpiryIntervalSeconds: Long? get() = null
42 | val contentType: String? get() = null
43 | val responseTopic: Topic? get() = null
44 | val correlationData: ReadBuffer? get() = null
45 | val userProperty: List> get() = emptyList()
46 | }
47 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IDisconnectNotification.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IDisconnectNotification : ControlPacket
4 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPingRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IPingRequest : ControlPacket
4 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPingResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IPingResponse : ControlPacket
4 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPublishAcknowledgment.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IPublishAcknowledgment : ControlPacket {
4 | companion object {
5 | const val CONTROL_PACKET_VALUE: Byte = 4
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPublishComplete.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IPublishComplete : ControlPacket {
4 | companion object {
5 | const val CONTROL_PACKET_VALUE: Byte = 7
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPublishMessage.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
5 |
6 | interface IPublishMessage : ControlPacket {
7 | val qualityOfService: QualityOfService
8 | val topic: Topic
9 | val payload: ReadBuffer?
10 |
11 | fun expectedResponse(
12 | reasonCode: ReasonCode = ReasonCode.SUCCESS,
13 | reasonString: String? = null,
14 | userProperty: List> = emptyList(),
15 | ): ControlPacket?
16 |
17 | fun setDupFlagNewPubMessage(): IPublishMessage
18 |
19 | fun maybeCopyWithNewPacketIdentifier(packetIdentifier: Int): IPublishMessage
20 |
21 | companion object {
22 | const val CONTROL_PACKET_VALUE: Byte = 3
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPublishReceived.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
4 |
5 | interface IPublishReceived : ControlPacket {
6 | fun expectedResponse(
7 | reasonCode: ReasonCode = ReasonCode.SUCCESS,
8 | reasonString: String? = null,
9 | userProperty: List> = emptyList(),
10 | ): IPublishRelease
11 |
12 | companion object {
13 | const val CONTROL_PACKET_VALUE: Byte = 5
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IPublishRelease.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
4 |
5 | interface IPublishRelease : ControlPacket {
6 | fun expectedResponse(
7 | reasonCode: ReasonCode = ReasonCode.SUCCESS,
8 | reasonString: String? = null,
9 | userProperty: List> = emptyList(),
10 | ): IPublishComplete
11 |
12 | companion object {
13 | const val CONTROL_PACKET_VALUE: Byte = 6
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IReserved.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IReserved : ControlPacket
4 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/ISubscribeAcknowledgement.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface ISubscribeAcknowledgement : ControlPacket {
4 | companion object {
5 | const val CONTROL_PACKET_VALUE: Byte = 9
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/ISubscribeRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface ISubscribeRequest : ControlPacket {
4 | fun expectedResponse(): ISubscribeAcknowledgement
5 |
6 | val subscriptions: Set
7 |
8 | fun copyWithNewPacketIdentifier(packetIdentifier: Int): ISubscribeRequest
9 |
10 | companion object {
11 | const val CONTROL_PACKET_VALUE: Byte = 8
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/ISubscription.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface ISubscription {
4 | val topicFilter: Topic
5 | val maximumQos: QualityOfService
6 |
7 | // mqtt 5
8 | val noLocal: Boolean
9 | get() = false
10 | val retainAsPublished: Boolean
11 | get() = false
12 |
13 | val retainHandling: RetainHandling
14 | get() = RetainHandling.SEND_RETAINED_MESSAGES_AT_TIME_OF_SUBSCRIBE
15 |
16 | enum class RetainHandling(val value: UByte) {
17 | SEND_RETAINED_MESSAGES_AT_TIME_OF_SUBSCRIBE(0.toUByte()),
18 | SEND_RETAINED_MESSAGES_AT_SUBSCRIBE_ONLY_IF_SUBSCRIBE_DOESNT_EXISTS(1.toUByte()),
19 | DO_NOT_SEND_RETAINED_MESSAGES(2.toUByte()),
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IUnsubscribeAcknowledgment.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IUnsubscribeAcknowledgment : ControlPacket {
4 | companion object {
5 | const val CONTROL_PACKET_VALUE: Byte = 11
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/IUnsubscribeRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | interface IUnsubscribeRequest : ControlPacket {
4 | val topics: Set
5 |
6 | fun copyWithNewPacketIdentifier(packetIdentifier: Int): IUnsubscribeRequest
7 |
8 | companion object {
9 | val controlPacketValue = 10.toByte()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/Level.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalJsExport::class)
2 |
3 | package com.ditchoom.mqtt.controlpacket
4 |
5 | import kotlin.js.ExperimentalJsExport
6 | import kotlin.js.JsExport
7 |
8 | @JsExport
9 | sealed class Level {
10 | abstract val value: String
11 | open val nextLevel: Level? = null
12 | var isPrefixedWithSlash = false
13 | var isPostfixedWithSlash = false
14 |
15 | abstract fun matches(other: Level): Boolean
16 |
17 | fun stringValue(): String {
18 | val next = nextLevel
19 | val postFix =
20 | if (next != null) {
21 | "/$next"
22 | } else {
23 | ""
24 | }
25 | return value + postFix
26 | }
27 |
28 | data class StringLevel(override val value: String, override val nextLevel: Level?) : Level() {
29 | override fun matches(other: Level): Boolean {
30 | val otherNextLevel = other.nextLevel
31 | return when (other) {
32 | MultiLevelWildcard -> true
33 | is SingleLevelWildcard ->
34 | nextLevel == null || (otherNextLevel != null && nextLevel.matches(otherNextLevel))
35 |
36 | is StringLevel -> {
37 | if (nextLevel == null && otherNextLevel == null) {
38 | value == other.value
39 | } else if (nextLevel != null && otherNextLevel != null) {
40 | value == other.value && nextLevel.matches(otherNextLevel)
41 | } else {
42 | (nextLevel is MultiLevelWildcard && otherNextLevel == null) ||
43 | (otherNextLevel is MultiLevelWildcard && nextLevel == null)
44 | }
45 | }
46 | }
47 | }
48 |
49 | override fun toString(): String = stringValue()
50 | }
51 |
52 | object MultiLevelWildcard : Level() {
53 | override val value: String = "#"
54 |
55 | override fun matches(other: Level): Boolean =
56 | when (other) {
57 | MultiLevelWildcard -> true
58 | is SingleLevelWildcard -> false
59 | is StringLevel -> other.matches(this)
60 | }
61 |
62 | override fun toString(): String = stringValue()
63 | }
64 |
65 | data class SingleLevelWildcard(override val nextLevel: Level? = null) : Level() {
66 | override val value: String = "+"
67 |
68 | override fun matches(other: Level): Boolean =
69 | when (other) {
70 | MultiLevelWildcard -> false
71 | is SingleLevelWildcard -> true
72 | is StringLevel -> other.matches(this)
73 | }
74 |
75 | override fun toString(): String = stringValue()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/PacketIdUtils.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | const val NO_PACKET_ID = 0
4 |
5 | val validControlPacketIdentifierRange = 1..UShort.MAX_VALUE.toInt()
6 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/QualityOfService.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.mqtt.MalformedPacketException
4 |
5 | enum class QualityOfService(val integerValue: Byte) {
6 | AT_MOST_ONCE(0),
7 | AT_LEAST_ONCE(1),
8 | EXACTLY_ONCE(2),
9 | ;
10 |
11 | fun isGreaterThan(otherQos: QualityOfService) = integerValue > otherQos.integerValue
12 |
13 | companion object {
14 | fun fromBooleans(
15 | bit2: Boolean,
16 | bit1: Boolean,
17 | ): QualityOfService {
18 | return if (bit2 && !bit1) {
19 | EXACTLY_ONCE
20 | } else if (!bit2 && bit1) {
21 | AT_LEAST_ONCE
22 | } else if (!bit2 && !bit1) {
23 | AT_MOST_ONCE
24 | } else {
25 | throw MalformedPacketException("Invalid flags received, 0x03. Double check QOS is not set to 0x03")
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/Type.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | enum class Type {
4 | BYTE,
5 | TWO_BYTE_INTEGER,
6 | FOUR_BYTE_INTEGER,
7 | UTF_8_ENCODED_STRING,
8 | BINARY_DATA,
9 | VARIABLE_BYTE_INTEGER,
10 | UTF_8_STRING_PAIR,
11 | }
12 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/format/ReasonCode.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket.format
2 |
3 | /**
4 | * A Reason Code is a one byte unsigned controlPacketValue that indicates the result of an operation. Reason Codes less than 0x80
5 | * indicate successful completion of an operation. The normal Reason Code for success is 0. Reason Code values of 0x80
6 | * or greater indicate failure.
7 | * The ConnectionAcknowledgment, PublishAcknowledgment, PublishReceived, PublishRelease, PublishCompleteNotification and AuthenticationExchange Control Packets have a single Reason Code as part
8 | * of the Variable Header. The SubscribeAcknowledgement and UnsubscribeAcknowledgment packets contain a list of one or more Reason Codes in the Payload.
9 | */
10 | enum class ReasonCode(val byte: UByte) {
11 | SUCCESS(0x00.toUByte()),
12 | NORMAL_DISCONNECTION(0x00.toUByte()),
13 | GRANTED_QOS_0(0x00.toUByte()),
14 | GRANTED_QOS_1(0x01.toUByte()),
15 | GRANTED_QOS_2(0x02.toUByte()),
16 | DISCONNECT_WITH_WILL_MESSAGE(0x04.toUByte()),
17 | NO_MATCHING_SUBSCRIBERS(0x10.toUByte()),
18 | NO_SUBSCRIPTIONS_EXISTED(0x11.toUByte()),
19 | CONTINUE_AUTHENTICATION(0x18.toUByte()),
20 | REAUTHENTICATE(0x19.toUByte()),
21 | UNSPECIFIED_ERROR(0x80.toUByte()),
22 | MALFORMED_PACKET(0x81.toUByte()),
23 | PROTOCOL_ERROR(0x82.toUByte()),
24 | IMPLEMENTATION_SPECIFIC_ERROR(0x83.toUByte()),
25 | UNSUPPORTED_PROTOCOL_VERSION(0x84.toUByte()),
26 | CLIENT_IDENTIFIER_NOT_VALID(0x85.toUByte()),
27 | BAD_USER_NAME_OR_PASSWORD(0x86.toUByte()),
28 | NOT_AUTHORIZED(0x87.toUByte()),
29 | SERVER_UNAVAILABLE(0x88.toUByte()),
30 | SERVER_BUSY(0x89.toUByte()),
31 | BANNED(0x8A.toUByte()),
32 | SERVER_SHUTTING_DOWN(0x8B.toUByte()),
33 | BAD_AUTHENTICATION_METHOD(0x8C.toUByte()),
34 | KEEP_ALIVE_TIMEOUT(0x8D.toUByte()),
35 | SESSION_TAKE_OVER(0x8E.toUByte()),
36 | TOPIC_FILTER_INVALID(0x8F.toUByte()),
37 | TOPIC_NAME_INVALID(0x90.toUByte()),
38 |
39 | /**
40 | * the response to this is either to try to fix the state, or to reset the Session state by connecting using Clean
41 | * Start set to 1, or to decide if the Client or Server implementations are defective.
42 | */
43 | PACKET_IDENTIFIER_IN_USE(0x91.toUByte()),
44 | PACKET_IDENTIFIER_NOT_FOUND(0x92.toUByte()),
45 | RECEIVE_MAXIMUM_EXCEEDED(0x93.toUByte()),
46 | TOPIC_ALIAS_INVALID(0x94.toUByte()),
47 | PACKET_TOO_LARGE(0x95.toUByte()),
48 | MESSAGE_RATE_TOO_HIGH(0x96.toUByte()),
49 | QUOTA_EXCEEDED(0x97.toUByte()),
50 | ADMINISTRATIVE_ACTION(0x98.toUByte()),
51 | PAYLOAD_FORMAT_INVALID(0x99.toUByte()),
52 | RETAIN_NOT_SUPPORTED(0x9A.toUByte()),
53 | QOS_NOT_SUPPORTED(0x9B.toUByte()),
54 | USE_ANOTHER_SERVER(0x9C.toUByte()),
55 | SERVER_MOVED(0x9D.toUByte()),
56 | SHARED_SUBSCRIPTIONS_NOT_SUPPORTED(0x9E.toUByte()),
57 | CONNECTION_RATE_EXCEEDED(0x9F.toUByte()),
58 | MAXIMUM_CONNECTION_TIME(0xA0.toUByte()),
59 | SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED(0xA1.toUByte()),
60 | WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED(0xA2.toUByte()),
61 | }
62 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/format/fixed/DirectionOfFlow.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket.format.fixed
2 |
3 | /**
4 | * Direction of the control packet
5 | * @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/cos02/mqtt-v5.0-cos02.html#_Toc1477322
6 | */
7 | enum class DirectionOfFlow {
8 | FORBIDDEN,
9 | CLIENT_TO_SERVER,
10 | SERVER_TO_CLIENT,
11 | BIDIRECTIONAL, // Client to Server or Server to Client
12 | }
13 |
--------------------------------------------------------------------------------
/models-base/src/commonMain/kotlin/com/ditchoom/mqtt/controlpacket/format/fixed/Flags.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket.format.fixed
2 |
3 | import kotlin.js.JsName
4 |
5 | /**
6 | * The remaining bits [7-0] of byte can be retrieved as a boolean
7 | * get the value at an index as a boolean
8 | */
9 | @JsName("ubyteGet")
10 | fun UByte.get(index: Int) = this.toInt().and(0b01.shl(index)) != 0
11 |
--------------------------------------------------------------------------------
/models-base/src/commonTest/kotlin/com/ditchoom/mqtt/controlpacket/StringTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import kotlin.js.JsName
4 | import kotlin.jvm.JvmName
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertFalse
8 | import kotlin.test.assertTrue
9 |
10 | /**
11 | * MQTT Conformance Character data in a UTF-8 Encoded String MUST be well-formed UTF-8 as defined by the Unicode specification Unicode and restated in RFC 3629
12 | */
13 | class StringTests {
14 | @Test
15 | fun invalidMqttString() = assertFalse("abc\u0001def".validateMqttUTF8String())
16 |
17 | @Test
18 | fun validMqttString() = assertTrue("abc\u002Fdef".validateMqttUTF8String())
19 |
20 | @Test
21 | fun invalidMqttStringPoint2() = assertFalse("abc\u007fdef".validateMqttUTF8String())
22 |
23 | @Test
24 | fun validMqttStringBasic() = assertTrue("abcdef".validateMqttUTF8String())
25 |
26 | @Test
27 | @JsName("utf8DoesNotHaveNull")
28 | @JvmName("utf8DoesNotHaveNull")
29 | fun `MQTT Conformance A UTF-8 Encoded String MUST NOT include an encoding of the null character U+0000`() {
30 | assertFalse { "\u0000".validateMqttUTF8String() }
31 | }
32 |
33 | // TODO: Fix this conformance test
34 | //
35 | // @Test
36 | // @JsName("zeroWidthNoBreakSpace")
37 | // fun `MQTT Conformance A UTF-8 encoded sequence 0xEF 0xBB 0xBF is always interpreted as U+FEFF ZERO WIDTH NO-BREAK SPACE wherever it appears in a string and MUST NOT be skipped over or stripped off by a packet receiver `() {
38 | // val bytes = PlatformBuffer.wrap(byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()))
39 | // val string = bytes.readString(3, Charset.UTF8)
40 | // assertEquals(0xFEFF.toChar().code, string[0].code)
41 | // }
42 |
43 | // The string AuD869uDED4 which is LATIN CAPITAL Letter A followed by the code point U+2A6D4
44 | // which represents a CJK IDEOGRAPH EXTENSION B character is encoded
45 | @Test
46 | @JsName("latinCaptialNoNormativeTest")
47 | @JvmName("latinCaptialNoNormativeTest")
48 | fun latinTest() {
49 | val string = "A\uD869\uDED4"
50 | assertFalse { string.validateMqttUTF8String() }
51 | assertEquals("A𪛔", "A\uD869\uDED4")
52 | }
53 |
54 | @Test
55 | fun controlCharacterU0001toU001F() {
56 | for (c in 0x0001..0x001F) {
57 | val string = c.toString()
58 | assertTrue { string.validateMqttUTF8String() }
59 | }
60 | }
61 |
62 | @Test
63 | fun stringLengthOverflow() {
64 | assertFalse { "a".repeat(65_536).validateMqttUTF8String() }
65 | }
66 |
67 | @Test
68 | fun controlCharacterUD800toUDFFF() {
69 | for (c in '\uD800'..'\uDFFF') {
70 | assertFalse { c.toString().validateMqttUTF8String() }
71 | }
72 | }
73 |
74 | @Test
75 | fun controlCharacterU007FtoU009F() {
76 | for (c in '\u007F'..'\u009F') {
77 | assertFalse { c.toString().validateMqttUTF8String() }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/models-base/src/commonTest/kotlin/com/ditchoom/mqtt/controlpacket/TopicTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.controlpacket
2 |
3 | import com.ditchoom.mqtt.ProtocolError
4 | import kotlin.test.Test
5 | import kotlin.test.assertEquals
6 | import kotlin.test.assertFailsWith
7 | import kotlin.test.assertFalse
8 | import kotlin.test.assertNotNull
9 | import kotlin.test.assertTrue
10 |
11 | class TopicTests {
12 | @Test
13 | fun multiLevelWildcard() {
14 | val topic = Topic.fromOrThrow("sport/tennis/player1/#", Topic.Type.Filter)
15 | assertEquals(topic.toString(), "sport/tennis/player1/#")
16 | assertTrue(validateMatchBothWays(topic, Topic.fromOrThrow("sport/tennis/player1", Topic.Type.Name)))
17 | assertTrue(validateMatchBothWays(topic, Topic.fromOrThrow("sport/tennis/player1/ranking", Topic.Type.Name)))
18 | assertTrue(
19 | validateMatchBothWays(
20 | topic,
21 | Topic.fromOrThrow("sport/tennis/player1/score/wimbledon", Topic.Type.Name),
22 | ),
23 | )
24 |
25 | assertTrue(validateMatchBothWays(topic, Topic.fromOrThrow("#", Topic.Type.Filter)))
26 | assertTrue(validateMatchBothWays(topic, Topic.fromOrThrow("sport/tennis/#", Topic.Type.Filter)))
27 | assertFailsWith(ProtocolError::class) {
28 | Topic.fromOrThrow("sport/tennis#", Topic.Type.Name)
29 | }
30 | assertFailsWith(ProtocolError::class) {
31 | Topic.fromOrThrow("sport/tennis/#/ranking", Topic.Type.Filter)
32 | }
33 | }
34 |
35 | private fun validateMatchBothWays(
36 | left: Topic?,
37 | right: Topic?,
38 | ): Boolean {
39 | val leftMatches = left?.matches(right) ?: false
40 | val rightMatches = right?.matches(left) ?: false
41 | return leftMatches && rightMatches
42 | }
43 |
44 | @Test
45 | fun singleLevelWildcard() {
46 | assertEquals(Topic.fromOrThrow("/test/hello/", Topic.Type.Filter).toString(), "/test/hello/")
47 | val shortTopic = checkNotNull(Topic.fromOrThrow("sport/+", Topic.Type.Filter))
48 | assertEquals(shortTopic.toString(), "sport/+")
49 | assertFalse(validateMatchBothWays(shortTopic, Topic.fromOrThrow("sport", Topic.Type.Name)))
50 | assertTrue(validateMatchBothWays(shortTopic, Topic.fromOrThrow("sport/", Topic.Type.Name)))
51 |
52 | val topic = checkNotNull(Topic.fromOrThrow("sport/tennis/+", Topic.Type.Filter))
53 | assertEquals(topic.toString(), "sport/tennis/+")
54 | assertTrue(validateMatchBothWays(topic, Topic.fromOrThrow("sport/tennis/player1", Topic.Type.Name)))
55 | assertTrue(validateMatchBothWays(topic, Topic.fromOrThrow("sport/tennis/player2", Topic.Type.Name)))
56 | assertFalse(validateMatchBothWays(topic, Topic.fromOrThrow("sport/tennis/player1/ranking", Topic.Type.Name)))
57 | assertNotNull(Topic.fromOrThrow("+", Topic.Type.Filter))
58 | assertNotNull(Topic.fromOrThrow("+/tennis/#", Topic.Type.Filter))
59 | assertFailsWith(ProtocolError::class) {
60 | Topic.fromOrThrow("sport+", Topic.Type.Filter)
61 | }
62 | assertNotNull(Topic.fromOrThrow("sport/+/player1", Topic.Type.Filter))
63 |
64 | val financeTopic = checkNotNull(Topic.fromOrThrow("/finance", Topic.Type.Name))
65 | assertEquals(financeTopic.toString(), "/finance")
66 | assertTrue(validateMatchBothWays(financeTopic, Topic.fromOrThrow("+/+", Topic.Type.Filter)))
67 | assertTrue(validateMatchBothWays(financeTopic, Topic.fromOrThrow("/+", Topic.Type.Filter)))
68 | assertFalse(validateMatchBothWays(financeTopic, Topic.fromOrThrow("+", Topic.Type.Filter)))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/models-v4/gradle.properties:
--------------------------------------------------------------------------------
1 | libraryName=MQTT 3 + 4 Models
2 | libraryDescription=Defines the MQTT 3 and 4 control packets
3 | artifactName=mqtt-4-models
--------------------------------------------------------------------------------
/models-v4/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/models-v4/src/androidMain/kotlin/com/ditchoom/mqtt3/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import com.ditchoom.mqtt.InMemoryPersistence
4 | import com.ditchoom.mqtt.Persistence
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.Dispatchers
7 |
8 | actual suspend fun newDefaultPersistence(
9 | androidContext: Any?,
10 | name: String,
11 | inMemory: Boolean,
12 | ): Persistence =
13 | try {
14 | SqlDatabasePersistence(sqlDriver(androidContext, name, inMemory)!!)
15 | } catch (t: Throwable) {
16 | InMemoryPersistence()
17 | }
18 |
19 | actual fun defaultDispatcher(
20 | nThreads: Int,
21 | name: String,
22 | ): CoroutineDispatcher = Dispatchers.IO
23 |
--------------------------------------------------------------------------------
/models-v4/src/androidMain/kotlin/com/ditchoom/mqtt3/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import android.content.Context
4 | import app.cash.sqldelight.db.SqlDriver
5 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver
6 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
7 | import com.ditchoom.Mqtt4
8 |
9 | actual fun sqlDriver(
10 | androidContext: Any?,
11 | name: String,
12 | inMemory: Boolean,
13 | ): SqlDriver? =
14 | if (androidContext != null) {
15 | AndroidSqliteDriver(
16 | Mqtt4.Schema,
17 | androidContext as Context,
18 | if (inMemory) {
19 | null
20 | } else {
21 | name
22 | },
23 | )
24 | } else {
25 | val driver =
26 | JdbcSqliteDriver(
27 | if (inMemory) {
28 | JdbcSqliteDriver.IN_MEMORY
29 | } else {
30 | name
31 | },
32 | )
33 | Mqtt4.Schema.create(driver)
34 | driver
35 | }
36 |
--------------------------------------------------------------------------------
/models-v4/src/appleMain/kotlin/com/ditchoom/mqtt3/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import com.ditchoom.mqtt.Persistence
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.newFixedThreadPoolContext
6 |
7 | actual suspend fun newDefaultPersistence(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): Persistence = SqlDatabasePersistence(sqlDriver(androidContext, name, inMemory)!!)
12 |
13 | actual fun defaultDispatcher(
14 | nThreads: Int,
15 | name: String,
16 | ): CoroutineDispatcher = newFixedThreadPoolContext(123, "mqtt")
17 |
--------------------------------------------------------------------------------
/models-v4/src/appleMain/kotlin/com/ditchoom/mqtt3/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 | import app.cash.sqldelight.driver.native.NativeSqliteDriver
5 | import com.ditchoom.Mqtt4
6 |
7 | actual fun sqlDriver(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): SqlDriver? =
12 | NativeSqliteDriver(
13 | Mqtt4.Schema,
14 | name,
15 | onConfiguration = {
16 | it.copy(inMemory = inMemory)
17 | },
18 | )
19 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/ControlPacketV4.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.mqtt.MalformedPacketException
5 | import com.ditchoom.mqtt.controlpacket.ControlPacket
6 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.readVariableByteInteger
7 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
8 |
9 | /**
10 | * The MQTT specification defines fifteen different types of MQTT Control Packet, for example the PublishMessage packet is
11 | * used to convey Application Messages.
12 | * @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/cos02/mqtt-v5.0-cos02.html#_Toc1477322
13 | * @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html#_Toc514847903
14 | * @param controlPacketValue Value defined under [MQTT 2.1.2]
15 | * @param direction Direction of Flow defined under [MQTT 2.1.2]
16 | */
17 | abstract class ControlPacketV4(
18 | override val controlPacketValue: Byte,
19 | override val direction: DirectionOfFlow,
20 | override val flags: Byte = 0b0,
21 | ) : ControlPacket {
22 | override val mqttVersion: Byte = 4
23 | override val controlPacketFactory = ControlPacketV4Factory
24 |
25 | companion object {
26 | inline fun fromTyped(buffer: ReadBuffer): ControlPacketV4 {
27 | val byte1 = buffer.readUnsignedByte()
28 | val remainingLength = buffer.readVariableByteInteger()
29 | return fromTyped(buffer, byte1, remainingLength)
30 | }
31 |
32 | fun from(buffer: ReadBuffer) = fromTyped(buffer)
33 |
34 | fun from(
35 | buffer: ReadBuffer,
36 | byte1: UByte,
37 | remainingLength: Int,
38 | ) = fromTyped(buffer, byte1, remainingLength)
39 |
40 | inline fun fromTyped(
41 | buffer: ReadBuffer,
42 | byte1: UByte,
43 | remainingLength: Int,
44 | ): ControlPacketV4 {
45 | val byte1AsUInt = byte1.toUInt()
46 | val packetValue = byte1AsUInt.shr(4).toInt()
47 | return when (packetValue) {
48 | 0 -> Reserved
49 | 1 -> ConnectionRequest.from(buffer)
50 | 2 -> ConnectionAcknowledgment.from(buffer)
51 | 3 -> PublishMessage.from(buffer, byte1, remainingLength)
52 | 4 -> PublishAcknowledgment.from(buffer)
53 | 5 -> PublishReceived.from(buffer)
54 | 6 -> PublishRelease.from(buffer)
55 | 7 -> PublishComplete.from(buffer)
56 | 8 -> SubscribeRequest.from(buffer, remainingLength)
57 | 9 -> SubscribeAcknowledgement.from(buffer, remainingLength)
58 | 10 -> UnsubscribeRequest.from(buffer, remainingLength)
59 | 11 -> UnsubscribeAcknowledgment.from(buffer)
60 | 12 -> PingRequest
61 | 13 -> PingResponse
62 | 14 -> DisconnectNotification
63 | else -> throw MalformedPacketException(
64 | "Invalid MQTT Control Packet Type: $packetValue Should be in range between 0 and 15 inclusive",
65 | )
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/ControlPacketV4Factory.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.mqtt.Persistence
5 | import com.ditchoom.mqtt.controlpacket.ControlPacketFactory
6 | import com.ditchoom.mqtt.controlpacket.IPublishMessage
7 | import com.ditchoom.mqtt.controlpacket.ISubscribeRequest
8 | import com.ditchoom.mqtt.controlpacket.ISubscription
9 | import com.ditchoom.mqtt.controlpacket.NO_PACKET_ID
10 | import com.ditchoom.mqtt.controlpacket.QualityOfService
11 | import com.ditchoom.mqtt.controlpacket.Topic
12 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
13 | import com.ditchoom.mqtt3.persistence.newDefaultPersistence
14 |
15 | object ControlPacketV4Factory : ControlPacketFactory {
16 | override val protocolVersion: Int = 4
17 |
18 | override suspend fun defaultPersistence(
19 | androidContext: Any?,
20 | name: String,
21 | inMemory: Boolean,
22 | ): Persistence = newDefaultPersistence(androidContext, name, inMemory)
23 |
24 | override fun from(
25 | buffer: ReadBuffer,
26 | byte1: UByte,
27 | remainingLength: Int,
28 | ) = ControlPacketV4.from(buffer, byte1, remainingLength)
29 |
30 | override fun pingRequest() = PingRequest
31 |
32 | override fun pingResponse() = PingResponse
33 |
34 | override fun publish(
35 | dup: Boolean,
36 | qos: QualityOfService,
37 | retain: Boolean,
38 | topicName: Topic,
39 | payload: ReadBuffer?,
40 | // MQTT 5 Properties, Should be ignored in this version
41 | payloadFormatIndicator: Boolean,
42 | messageExpiryInterval: Long?,
43 | topicAlias: Int?,
44 | responseTopic: Topic?,
45 | correlationData: ReadBuffer?,
46 | userProperty: List>,
47 | subscriptionIdentifier: Set,
48 | contentType: String?,
49 | ): IPublishMessage {
50 | val fixedHeader = PublishMessage.FixedHeader(dup, qos, retain)
51 | val variableHeader = PublishMessage.VariableHeader(topicName, NO_PACKET_ID)
52 | return PublishMessage(fixedHeader, variableHeader, payload)
53 | }
54 |
55 | override fun subscribe(
56 | topicFilter: Topic,
57 | maximumQos: QualityOfService,
58 | noLocal: Boolean,
59 | retainAsPublished: Boolean,
60 | retainHandling: ISubscription.RetainHandling,
61 | serverReference: String?,
62 | userProperty: List>,
63 | ): ISubscribeRequest {
64 | val subscription = Subscription(topicFilter, maximumQos)
65 | return subscribe(
66 | setOf(subscription),
67 | serverReference,
68 | userProperty,
69 | )
70 | }
71 |
72 | override fun subscribe(
73 | subscriptions: Set,
74 | serverReference: String?,
75 | userProperty: List>,
76 | ): ISubscribeRequest = SubscribeRequest(NO_PACKET_ID, subscriptions)
77 |
78 | override fun unsubscribe(
79 | topics: Set,
80 | userProperty: List>,
81 | ) = UnsubscribeRequest(NO_PACKET_ID, topics)
82 |
83 | override fun disconnect(
84 | reasonCode: ReasonCode,
85 | sessionExpiryIntervalSeconds: ULong?,
86 | reasonString: String?,
87 | userProperty: List>,
88 | ) = DisconnectNotification
89 | }
90 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/DisconnectNotification.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IDisconnectNotification
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | /**
7 | * The Server MUST validate that reserved bits are set to zero and disconnect the Client if they are not zero
8 | * [MQTT-3.14.1-1].
9 | *
10 | * 3.14.4 Response
11 | *
12 | * After sending a DISCONNECT Packet the Client:
13 | *
14 | * MUST close the Network Connection [MQTT-3.14.4-1].
15 | *
16 | * MUST NOT send any more Control Packets on that Network Connection [MQTT-3.14.4-2].
17 | *
18 | * On receipt of DISCONNECT the Server:
19 | *
20 | * MUST discard any Will Message associated with the current connection without publishing it, as described in Section
21 | * 3.1.2.5 [MQTT-3.14.4-3]
22 | *
23 | * SHOULD close the Network Connection if the Client has not already done so.
24 | */
25 | object DisconnectNotification :
26 | ControlPacketV4(14, DirectionOfFlow.BIDIRECTIONAL),
27 | IDisconnectNotification
28 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/PingRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IPingRequest
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | /**
7 | * 3.12 PINGREQ – PING request
8 | * The PINGREQ packet is sent from a Client to the Server. It can be used to:
9 | *
10 | * · Indicate to the Server that the Client is alive in the absence of any other MQTT Control Packets being
11 | * sent from the Client to the Server.
12 | *
13 | * · Request that the Server responds to confirm that it is alive.
14 | *
15 | * · Exercise the network to indicate that the Network Connection is active.
16 | *
17 | * This packet is used in Keep Alive processing. Refer to section 3.1.2.10 for more details.
18 | */
19 | object PingRequest : ControlPacketV4(12, DirectionOfFlow.CLIENT_TO_SERVER), IPingRequest
20 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/PingResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IPingResponse
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | object PingResponse : ControlPacketV4(13, DirectionOfFlow.SERVER_TO_CLIENT), IPingResponse
7 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/PublishAcknowledgment.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.controlpacket.IPublishAcknowledgment
6 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
7 |
8 | /**
9 | * 3.4 PUBACK – Publish acknowledgement
10 | *
11 | * A PUBACK packet is the response to a PUBLISH packet with QoS 1.
12 | */
13 | data class PublishAcknowledgment(override val packetIdentifier: Int) :
14 | ControlPacketV4(IPublishAcknowledgment.CONTROL_PACKET_VALUE, DirectionOfFlow.BIDIRECTIONAL), IPublishAcknowledgment {
15 | override fun remainingLength() = 2
16 |
17 | override fun variableHeader(writeBuffer: WriteBuffer) {
18 | writeBuffer.writeUShort(packetIdentifier.toUShort())
19 | }
20 |
21 | companion object {
22 | fun from(buffer: ReadBuffer) = PublishAcknowledgment(buffer.readUnsignedShort().toInt())
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/PublishComplete.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.controlpacket.IPublishComplete
6 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
7 |
8 | /**
9 | * 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3)
10 | *
11 | * The PUBCOMP packet is the response to a PUBREL packet. It is the fourth and final packet of the QoS 2 protocol exchange.
12 | */
13 | data class PublishComplete(override val packetIdentifier: Int) :
14 | ControlPacketV4(IPublishComplete.CONTROL_PACKET_VALUE, DirectionOfFlow.BIDIRECTIONAL),
15 | IPublishComplete {
16 | override fun variableHeader(writeBuffer: WriteBuffer) {
17 | writeBuffer.writeUShort(packetIdentifier.toUShort())
18 | }
19 |
20 | override fun remainingLength() = 2
21 |
22 | companion object {
23 | fun from(buffer: ReadBuffer) = PublishComplete(buffer.readUnsignedShort().toInt())
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/PublishReceived.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.controlpacket.IPublishReceived
6 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
7 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
8 |
9 | /**
10 | * 3.5 PUBREC – Publish received (QoS 2 delivery part 1)
11 | *
12 | * A PUBREC packet is the response to a PUBLISH packet with QoS 2. It is the second packet of the QoS 2 protocol exchange.
13 | */
14 | data class PublishReceived(override val packetIdentifier: Int) :
15 | ControlPacketV4(IPublishReceived.CONTROL_PACKET_VALUE, DirectionOfFlow.BIDIRECTIONAL),
16 | IPublishReceived {
17 | override fun variableHeader(writeBuffer: WriteBuffer) {
18 | writeBuffer.writeUShort(packetIdentifier.toUShort())
19 | }
20 |
21 | override fun remainingLength() = 2
22 |
23 | override fun expectedResponse(
24 | reasonCode: ReasonCode,
25 | reasonString: String?,
26 | userProperty: List>,
27 | ) = PublishRelease(packetIdentifier.toUShort().toInt())
28 |
29 | companion object {
30 | fun from(buffer: ReadBuffer) = PublishReceived(buffer.readUnsignedShort().toInt())
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/PublishRelease.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.controlpacket.IPublishRelease
6 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
7 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
8 |
9 | /**
10 | * 3.6 PUBREL – Publish release (QoS 2 delivery part 2)
11 | *
12 | * A PUBREL packet is the response to a PUBREC packet. It is the third packet of the QoS 2 protocol exchange.
13 | */
14 | data class PublishRelease(override val packetIdentifier: Int) :
15 | ControlPacketV4(IPublishRelease.CONTROL_PACKET_VALUE, DirectionOfFlow.BIDIRECTIONAL, 0b10),
16 | IPublishRelease {
17 | override fun variableHeader(writeBuffer: WriteBuffer) {
18 | writeBuffer.writeUShort(packetIdentifier.toUShort())
19 | }
20 |
21 | override fun remainingLength() = 2
22 |
23 | override fun expectedResponse(
24 | reasonCode: ReasonCode,
25 | reasonString: String?,
26 | userProperty: List>,
27 | ) = PublishComplete(packetIdentifier)
28 |
29 | companion object {
30 | fun from(buffer: ReadBuffer) = PublishRelease(buffer.readUnsignedShort().toInt())
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/Reserved.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IReserved
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | object Reserved : ControlPacketV4(0, DirectionOfFlow.FORBIDDEN), IReserved
7 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/SubscribeAcknowledgement.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.MalformedPacketException
6 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.variableByteSize
7 | import com.ditchoom.mqtt.controlpacket.ISubscribeAcknowledgement
8 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
9 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.GRANTED_QOS_0
10 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.GRANTED_QOS_1
11 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.GRANTED_QOS_2
12 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.UNSPECIFIED_ERROR
13 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
14 |
15 | /**
16 | * 3.9 SUBACK – Subscribe acknowledgement
17 | *
18 | * A SUBACK Packet is sent by the Server to the Client to confirm receipt and processing of a SUBSCRIBE Packet.
19 | *
20 | * A SUBACK Packet contains a list of return codes, that specify the maximum QoS level that was granted in each
21 | * Subscription that was requested by the SUBSCRIBE.
22 | */
23 | data class SubscribeAcknowledgement(
24 | override val packetIdentifier: Int,
25 | val payload: List,
26 | ) :
27 | ControlPacketV4(ISubscribeAcknowledgement.CONTROL_PACKET_VALUE, DirectionOfFlow.SERVER_TO_CLIENT),
28 | ISubscribeAcknowledgement {
29 | override fun remainingLength() = 2 + payload.size
30 |
31 | override fun variableHeader(writeBuffer: WriteBuffer) {
32 | writeBuffer.writeUShort(packetIdentifier.toUShort())
33 | }
34 |
35 | override fun payload(writeBuffer: WriteBuffer) {
36 | payload.forEach { writeBuffer.writeUByte(it.byte) }
37 | }
38 |
39 | companion object {
40 | fun from(
41 | buffer: ReadBuffer,
42 | remainingLength: Int,
43 | ): SubscribeAcknowledgement {
44 | val packetIdentifier = buffer.readUnsignedShort()
45 | val returnCodes = mutableListOf()
46 | while (returnCodes.size < remainingLength - variableByteSize(remainingLength) - 1) {
47 | val reasonCode =
48 | when (val reasonCodeByte = buffer.readUnsignedByte()) {
49 | GRANTED_QOS_0.byte -> GRANTED_QOS_0
50 | GRANTED_QOS_1.byte -> GRANTED_QOS_1
51 | GRANTED_QOS_2.byte -> GRANTED_QOS_2
52 | UNSPECIFIED_ERROR.byte -> UNSPECIFIED_ERROR
53 | else -> throw MalformedPacketException("Invalid return code $reasonCodeByte")
54 | }
55 | returnCodes += reasonCode
56 | }
57 | return SubscribeAcknowledgement(packetIdentifier.toInt(), returnCodes)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/UnsubscribeAcknowledgment.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.controlpacket.IUnsubscribeAcknowledgment
6 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
7 |
8 | data class UnsubscribeAcknowledgment(override val packetIdentifier: Int) :
9 | ControlPacketV4(IUnsubscribeAcknowledgment.CONTROL_PACKET_VALUE, DirectionOfFlow.SERVER_TO_CLIENT),
10 | IUnsubscribeAcknowledgment {
11 | override fun remainingLength() = 2
12 |
13 | override fun variableHeader(writeBuffer: WriteBuffer) {
14 | writeBuffer.writeUShort(packetIdentifier.toUShort())
15 | }
16 |
17 | companion object {
18 | fun from(buffer: ReadBuffer) = UnsubscribeAcknowledgment(buffer.readUnsignedShort().toInt())
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/controlpacket/UnsubscribeRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 | import com.ditchoom.mqtt.ProtocolError
6 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.readMqttUtf8StringNotValidatedSized
7 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.writeMqttUtf8String
8 | import com.ditchoom.mqtt.controlpacket.IUnsubscribeRequest
9 | import com.ditchoom.mqtt.controlpacket.Topic
10 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
11 | import com.ditchoom.mqtt.controlpacket.utf8Length
12 |
13 | /**
14 | * 3.10 UNSUBSCRIBE – Unsubscribe request
15 | * An UNSUBSCRIBE packet is sent by the Client to the Server, to unsubscribe from topics.
16 | */
17 | data class UnsubscribeRequest(
18 | override val packetIdentifier: Int,
19 | override val topics: Set,
20 | ) : ControlPacketV4(IUnsubscribeRequest.controlPacketValue, DirectionOfFlow.CLIENT_TO_SERVER, 0b10),
21 | IUnsubscribeRequest {
22 | constructor(packetIdentifier: Int, topicString: Collection) :
23 | this(packetIdentifier, topicString.map { Topic.fromOrThrow(it, Topic.Type.Filter) }.toSet())
24 |
25 | override fun remainingLength() = UShort.SIZE_BYTES + payloadSize()
26 |
27 | override fun variableHeader(writeBuffer: WriteBuffer) {
28 | writeBuffer.writeUShort(packetIdentifier.toUShort())
29 | }
30 |
31 | private fun payloadSize(): Int {
32 | var size = 0
33 | topics.forEach {
34 | size += UShort.SIZE_BYTES + it.toString().utf8Length()
35 | }
36 | return size
37 | }
38 |
39 | override fun payload(writeBuffer: WriteBuffer) {
40 | topics.forEach { writeBuffer.writeMqttUtf8String(it.toString()) }
41 | }
42 |
43 | init {
44 | if (topics.isEmpty()) {
45 | throw ProtocolError("An UNSUBSCRIBE packet with no Payload is a Protocol Error")
46 | }
47 | }
48 |
49 | override fun copyWithNewPacketIdentifier(packetIdentifier: Int): IUnsubscribeRequest = copy(packetIdentifier = packetIdentifier)
50 |
51 | companion object {
52 | fun from(
53 | buffer: ReadBuffer,
54 | remainingLength: Int,
55 | ): UnsubscribeRequest {
56 | val packetIdentifier = buffer.readUnsignedShort()
57 | val topics = mutableSetOf()
58 | var bytesRead = 0
59 | while (bytesRead < remainingLength - 2) {
60 | val pair = buffer.readMqttUtf8StringNotValidatedSized()
61 | bytesRead += 2 + pair.first
62 | topics += Topic.fromOrThrow(pair.second, Topic.Type.Filter)
63 | }
64 | return UnsubscribeRequest(packetIdentifier.toInt(), topics)
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import com.ditchoom.mqtt.Persistence
4 | import kotlinx.coroutines.CoroutineDispatcher
5 |
6 | expect suspend fun newDefaultPersistence(
7 | androidContext: Any? = null,
8 | name: String = "mqtt4.db",
9 | inMemory: Boolean = false,
10 | ): Persistence
11 |
12 | expect fun defaultDispatcher(
13 | nThreads: Int,
14 | name: String,
15 | ): CoroutineDispatcher
16 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/kotlin/com/ditchoom/mqtt3/persistence/SqlPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 |
5 | expect fun sqlDriver(
6 | androidContext: Any?,
7 | name: String = "mqtt4.db",
8 | inMemory: Boolean = false,
9 | ): SqlDriver?
10 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/Broker.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS Broker (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | next_packet_id INTEGER NOT NULL DEFAULT 1 CHECK(next_packet_id BETWEEN 0 AND 65535)
4 | );
5 |
6 | insertBroker:
7 | INSERT INTO Broker VALUES(NULL, 1);
8 |
9 | lastRowId:
10 | SELECT last_insert_rowid();
11 |
12 | deleteBroker:
13 | DELETE FROM Broker WHERE id = :id;
14 |
15 | allBrokers:
16 | SELECT * FROM Broker;
17 |
18 | incrementPacketId:
19 | UPDATE Broker SET next_packet_id = (next_packet_id % 65535) + 1 WHERE id = :id;
20 |
21 | nextPacketId:
22 | SELECT next_packet_id FROM Broker WHERE id = :id;
23 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/ConnectionRequest.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS ConnectionRequest (
2 | broker_id INTEGER NOT NULL UNIQUE,
3 | protocol_name TEXT NOT NULL DEFAULT 'MQTT',
4 | protocol_level INTEGER NOT NULL DEFAULT 4 CHECK(protocol_level BETWEEN 0 AND 255),
5 | will_retain INTEGER NOT NULL DEFAULT 0 CHECK(will_retain BETWEEN 0 AND 1),
6 | will_qos INTEGER NOT NULL DEFAULT 0 CHECK(will_qos BETWEEN 0 AND 2),
7 | will_flag INTEGER NOT NULL DEFAULT 0 CHECK(will_flag BETWEEN 0 AND 1),
8 | clean_session INTEGER NOT NULL DEFAULT 0 CHECK(clean_session BETWEEN 0 AND 1),
9 | keep_alive_seconds INTEGER NOT NULL DEFAULT 3600 CHECK(keep_alive_seconds BETWEEN 0 AND 65535),
10 | client_id TEXT NOT NULL,
11 | will_topic TEXT,
12 | will_payload BLOB,
13 | username TEXT,
14 | password TEXT,
15 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE
16 | );
17 |
18 | insertConnectionRequest:
19 | INSERT INTO ConnectionRequest
20 | (broker_id, protocol_name, protocol_level, will_retain, will_qos, will_flag, clean_session, keep_alive_seconds, client_id, will_topic, will_payload, username, password)
21 | VALUES
22 | (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
23 |
24 | connectionRequestByBrokerId:
25 | SELECT * FROM ConnectionRequest WHERE broker_id = :brokerId LIMIT 1;
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/PublishMessage.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS PublishMessage (
2 | broker_id INTEGER NOT NULL,
3 | incoming INTEGER NOT NULL DEFAULT 0 CHECK(incoming BETWEEN 0 AND 1),
4 | -- fixed header
5 | dup INTEGER NOT NULL DEFAULT 0 CHECK(dup BETWEEN 0 AND 1),
6 | qos INTEGER NOT NULL DEFAULT 1 CHECK(qos BETWEEN 0 AND 2), -- cannot be 0 if we are persisting
7 | retain INTEGER NOT NULL DEFAULT 0 CHECK(retain BETWEEN 0 AND 1),
8 | -- variable header
9 | topic_name TEXT NOT NULL,
10 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
11 | -- payload
12 | payload BLOB,
13 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
14 | PRIMARY KEY (broker_id, incoming, packet_id)
15 | );
16 |
17 | insertPublishMessage:
18 | INSERT INTO PublishMessage
19 | (broker_id, incoming, dup, qos, retain, topic_name, packet_id, payload)
20 | VALUES (?, ?, ?, ?, ?, ?, ?, ?);
21 |
22 | deletePublishMessage:
23 | DELETE FROM PublishMessage WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
24 |
25 | deleteAll:
26 | DELETE FROM PublishMessage WHERE broker_id = :brokerId;
27 |
28 | queuedPubMessages:
29 | SELECT * FROM PublishMessage WHERE broker_id = :brokerId AND incoming = 0;
30 |
31 | publishMessageCount:
32 | SELECT COUNT(broker_id) FROM PublishMessage WHERE broker_id = :brokerId;
33 |
34 | allMessages:
35 | SELECT * FROM PublishMessage WHERE broker_id = :brokerId;
36 |
37 | messageWithId:
38 | SELECT * FROM PublishMessage WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId LIMIT 1;
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/QoS2Messages.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS Qos2Messages (
2 | broker_id INTEGER NOT NULL,
3 | incoming INTEGER NOT NULL DEFAULT 0 CHECK(incoming BETWEEN 0 AND 1),
4 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
5 | type INTEGER NOT NULL CHECK(type BETWEEN 5 AND 7),
6 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
7 | PRIMARY KEY (broker_id, incoming, packet_id, type)
8 | );
9 |
10 | insertQos2Message:
11 | INSERT INTO Qos2Messages (broker_id, incoming, packet_id, type)
12 | VALUES (?, ?, ?, ?);
13 |
14 | updateQos2Message:
15 | UPDATE Qos2Messages
16 | SET type = :type
17 | WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
18 |
19 | deleteQos2Message:
20 | DELETE FROM Qos2Messages WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
21 |
22 | deleteAll:
23 | DELETE FROM Qos2Messages WHERE broker_id = :brokerId;
24 |
25 | queuedQos2Messages:
26 | SELECT * FROM Qos2Messages WHERE broker_id = :brokerId AND incoming = 0;
27 |
28 | queuedMessageCount:
29 | SELECT COUNT(broker_id) FROM Qos2Messages WHERE broker_id = :brokerId;
30 |
31 | allMessages:
32 | SELECT * FROM Qos2Messages WHERE broker_id = :brokerId;
33 |
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/SocketConnection.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS SocketConnection (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | broker_id INTEGER NOT NULL,
4 | type TEXT NOT NULL DEFAULT 'tcp' CHECK(type LIKE 'tcp' OR type LIKE 'websocket'),
5 | host TEXT NOT NULL,
6 | port INTEGER NOT NULL CHECK(port BETWEEN 1 AND 65535),
7 | tls INTEGER NOT NULL CHECK(tls BETWEEN 0 AND 1),
8 | connection_timeout_ms INTEGER NOT NULL CHECK(connection_timeout_ms > 0),
9 | read_timeout_ms INTEGER NOT NULL CHECK(read_timeout_ms > 0),
10 | write_timeout_ms INTEGER NOT NULL CHECK(write_timeout_ms > 0),
11 | websocket_endpoint TEXT,
12 | websocket_protocols TEXT,
13 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE
14 | );
15 |
16 |
17 | insertConnection:
18 | INSERT INTO SocketConnection
19 | (id,broker_id,type,host,port,tls,connection_timeout_ms,read_timeout_ms,write_timeout_ms,websocket_endpoint,websocket_protocols)
20 | VALUES
21 | (NULL,?,?,?,?,?,?,?,?,?,?);
22 |
23 |
24 | connectionsByBrokerId:
25 | SELECT * FROM SocketConnection WHERE broker_id = :brokerId;
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/Subscription.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS Subscription (
2 | broker_id INTEGER NOT NULL,
3 | subscribe_packet_id INTEGER NOT NULL CHECK(subscribe_packet_id BETWEEN 0 AND 65535),
4 | unsubscribe_packet_id INTEGER NOT NULL CHECK(unsubscribe_packet_id BETWEEN 0 AND 65535),
5 | topic_filter TEXT NOT NULL,
6 | qos INTEGER NOT NULL DEFAULT 0 CHECK(qos BETWEEN 0 AND 2),
7 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
8 | PRIMARY KEY (broker_id,subscribe_packet_id, topic_filter)
9 | );
10 |
11 | insertSubscription:
12 | INSERT INTO Subscription (broker_id, subscribe_packet_id,unsubscribe_packet_id, topic_filter, qos)
13 | VALUES (?, ?, 0,?, ?);
14 |
15 | allSubscriptions:
16 | SELECT * FROM Subscription WHERE broker_id = :brokerId;
17 |
18 | allSubscriptionsNotPendingUnsub:
19 | SELECT * FROM Subscription WHERE broker_id = :brokerId AND unsubscribe_packet_id = 0;
20 |
21 | addUnsubscriptionPacketId:
22 | UPDATE Subscription SET unsubscribe_packet_id = :unsubPacketId WHERE broker_id = :brokerId AND topic_filter = :topicFilter;
23 |
24 | deleteSubscription:
25 | DELETE FROM Subscription WHERE broker_id = :brokerId AND unsubscribe_packet_id = :unsubPacketId;
26 |
27 | deleteAll:
28 | DELETE FROM Subscription WHERE broker_id = :brokerId;
29 |
30 | queuedSubscriptions:
31 | SELECT * FROM Subscription WHERE broker_id = :brokerId AND subscribe_packet_id = :subPacketId AND unsubscribe_packet_id = 0;
32 |
33 | queuedUnsubscriptions:
34 | SELECT * FROM Subscription WHERE broker_id = :brokerId AND unsubscribe_packet_id = :unsubscribePacketId;
35 |
36 | queuedMessageCount:
37 | SELECT COUNT(broker_id) FROM Subscription WHERE broker_id = :brokerId;
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/SubscriptionRequest.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS SubscribeRequest(
2 | broker_id INTEGER NOT NULL,
3 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
4 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
5 | PRIMARY KEY (broker_id,packet_id)
6 | );
7 |
8 |
9 | insertSubscribeRequest:
10 | INSERT INTO SubscribeRequest (broker_id, packet_id)
11 | VALUES (?, ?);
12 |
13 | deleteSubscribeRequest:
14 | DELETE FROM SubscribeRequest
15 | WHERE broker_id = :brokerId AND packet_id = :packetId;
16 |
17 | queuedSubMessages:
18 | SELECT * FROM SubscribeRequest WHERE broker_id = :brokerId;
19 |
20 | deleteAll:
21 | DELETE FROM SubscribeRequest WHERE broker_id = :brokerId;
22 |
23 | queuedMessageCount:
24 | SELECT COUNT(broker_id) FROM SubscribeRequest WHERE broker_id = :brokerId;
25 |
26 | messageWithId:
27 | SELECT * FROM SubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId LIMIT 1;
--------------------------------------------------------------------------------
/models-v4/src/commonMain/sqldelight/com/ditchoom/mqtt3/persistence/UnsubscribeRequest.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS UnsubscribeRequest(
2 | broker_id INTEGER NOT NULL,
3 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
4 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
5 | PRIMARY KEY (broker_id,packet_id)
6 | );
7 |
8 |
9 | insertUnsubscribeRequest:
10 | INSERT INTO UnsubscribeRequest (broker_id, packet_id)
11 | VALUES (?, ?);
12 |
13 | deleteUnsubscribeRequest:
14 | DELETE FROM UnsubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId;
15 |
16 | queuedUnsubMessages:
17 | SELECT * FROM UnsubscribeRequest WHERE broker_id = :brokerId;
18 |
19 | deleteAll:
20 | DELETE FROM UnsubscribeRequest WHERE broker_id = :brokerId;
21 |
22 | queuedMessageCount:
23 | SELECT COUNT(broker_id) FROM UnsubscribeRequest WHERE broker_id = :brokerId;
24 |
25 | messageWithId:
26 | SELECT * FROM UnsubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId LIMIT 1;
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/ConnectionAcknowledgmentTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import com.ditchoom.mqtt.controlpacket.format.fixed.get
6 | import kotlin.test.Test
7 | import kotlin.test.assertEquals
8 | import kotlin.test.assertFalse
9 | import kotlin.test.assertTrue
10 |
11 | class ConnectionAcknowledgmentTests {
12 | @Test
13 | fun serializeDeserializeDefault() {
14 | val buffer = PlatformBuffer.allocate(4)
15 | val actual = ConnectionAcknowledgment()
16 | actual.serialize(buffer)
17 | buffer.resetForRead()
18 | val expected = ControlPacketV4.from(buffer)
19 | assertEquals(expected, actual)
20 | }
21 |
22 | @Test
23 | fun bit0SessionPresentFalseFlags() {
24 | val buffer = PlatformBuffer.allocate(4)
25 | val model = ConnectionAcknowledgment()
26 | model.header.serialize(buffer)
27 | buffer.resetForRead()
28 | val sessionPresentBit = buffer.readUnsignedByte().get(0)
29 | assertFalse(sessionPresentBit)
30 |
31 | val buffer2 = PlatformBuffer.allocate(4)
32 | model.serialize(buffer2)
33 | buffer2.resetForRead()
34 | val result = ControlPacketV4.from(buffer2) as ConnectionAcknowledgment
35 | assertFalse(result.header.sessionPresent)
36 | }
37 |
38 | @Test
39 | fun bit0SessionPresentFlags() {
40 | val buffer = PlatformBuffer.allocate(4)
41 | val model = ConnectionAcknowledgment(ConnectionAcknowledgment.VariableHeader(true))
42 | model.header.serialize(buffer)
43 | buffer.resetForRead()
44 | assertTrue(buffer.readUnsignedByte().get(0))
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/DisconnectTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class DisconnectTests {
9 | @Test
10 | fun serializeDeserialize() {
11 | val actual = DisconnectNotification
12 | val buffer = PlatformBuffer.allocate(2)
13 | actual.serialize(buffer)
14 | buffer.resetForRead()
15 | val expected = ControlPacketV4.from(buffer) as DisconnectNotification
16 | assertEquals(expected, actual)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/PingRequestTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PingRequestTests {
9 | @Test
10 | fun serializeDeserialize() {
11 | val ping = PingRequest
12 | val buffer = PlatformBuffer.allocate(4)
13 | ping.serialize(buffer)
14 | buffer.resetForRead()
15 | assertEquals(12.shl(4).toByte(), buffer.readByte())
16 | assertEquals(0, buffer.readByte())
17 |
18 | val buffer2 = PlatformBuffer.allocate(4)
19 | ping.serialize(buffer2)
20 | buffer2.resetForRead()
21 | val result = ControlPacketV4.from(buffer2)
22 | assertEquals(result, ping)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/PingResponseTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PingResponseTests {
9 | @Test
10 | fun serializeDeserialize() {
11 | val ping = PingResponse
12 | val buffer = PlatformBuffer.allocate(2)
13 | ping.serialize(buffer)
14 | buffer.resetForRead()
15 | assertEquals(13.shl(4).toByte(), buffer.readByte())
16 | assertEquals(0, buffer.readByte())
17 |
18 | val buffer2 = PlatformBuffer.allocate(2)
19 | ping.serialize(buffer2)
20 | buffer2.resetForRead()
21 | val result = ControlPacketV4.from(buffer2)
22 | assertEquals(result, ping)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/PublishAcknowledgementTest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PublishAcknowledgementTest {
9 | private val packetIdentifier = 2
10 |
11 | @Test
12 | fun packetIdentifier() {
13 | val buffer = PlatformBuffer.allocate(4)
14 | val puback = PublishAcknowledgment(packetIdentifier)
15 | assertEquals(4, puback.packetSize())
16 | puback.serialize(buffer)
17 | buffer.resetForRead()
18 | val pubackResult = ControlPacketV4.from(buffer) as PublishAcknowledgment
19 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
20 | }
21 |
22 | @Test
23 | fun packetIdentifierSendDefaults() {
24 | val buffer = PlatformBuffer.allocate(4)
25 | val puback = PublishAcknowledgment(packetIdentifier)
26 | assertEquals(4, puback.packetSize())
27 | puback.serialize(buffer)
28 | buffer.resetForRead()
29 | val pubackResult = ControlPacketV4.from(buffer) as PublishAcknowledgment
30 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/PublishCompleteTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PublishCompleteTests {
9 | private val packetIdentifier = 2
10 |
11 | @Test
12 | fun packetIdentifier() {
13 | val puback = PublishComplete(packetIdentifier)
14 | assertEquals(4, puback.packetSize())
15 | val buffer = PlatformBuffer.allocate(4)
16 | puback.serialize(buffer)
17 | buffer.resetForRead()
18 | val pubackResult = ControlPacketV4.from(buffer) as PublishComplete
19 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/PublishReceivedTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PublishReceivedTests {
9 | private val packetIdentifier = 2
10 |
11 | @Test
12 | fun packetIdentifier() {
13 | val puback = PublishReceived(packetIdentifier)
14 | assertEquals(4, puback.packetSize())
15 | val buffer = PlatformBuffer.allocate(4)
16 | puback.serialize(buffer)
17 | buffer.resetForRead()
18 | val pubackResult = ControlPacketV4.from(buffer) as PublishReceived
19 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
20 | }
21 |
22 | @Test
23 | fun packetIdentifierSendDefaults() {
24 | val puback = PublishReceived(packetIdentifier)
25 | assertEquals(4, puback.packetSize())
26 | val buffer = PlatformBuffer.allocate(4)
27 | puback.serialize(buffer)
28 | buffer.resetForRead()
29 | val pubackResult = ControlPacketV4.from(buffer) as PublishReceived
30 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/PublishReleaseTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PublishReleaseTests {
9 | private val packetIdentifier = 2
10 |
11 | @Test
12 | fun packetIdentifier() {
13 | val buffer = PlatformBuffer.allocate(4)
14 | val puback = PublishRelease(packetIdentifier)
15 | assertEquals(4, puback.packetSize())
16 | puback.serialize(buffer)
17 | buffer.resetForRead()
18 | val pubackResult = ControlPacketV4.from(buffer) as PublishRelease
19 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/SubscribeAcknowledgementTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.GRANTED_QOS_0
6 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.GRANTED_QOS_1
7 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.GRANTED_QOS_2
8 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode.UNSPECIFIED_ERROR
9 | import kotlin.test.Test
10 | import kotlin.test.assertEquals
11 |
12 | class SubscribeAcknowledgementTests {
13 | private val packetIdentifier = 2
14 |
15 | @Test
16 | fun successMaxQos0() {
17 | val buffer = PlatformBuffer.allocate(5)
18 | val payload = GRANTED_QOS_0
19 | val puback = SubscribeAcknowledgement(packetIdentifier, listOf(payload))
20 | puback.serialize(buffer)
21 | buffer.resetForRead()
22 | val pubackResult = ControlPacketV4.from(buffer) as SubscribeAcknowledgement
23 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
24 | assertEquals(pubackResult.payload, listOf(GRANTED_QOS_0))
25 | }
26 |
27 | @Test
28 | fun grantedQos1() {
29 | val payload = GRANTED_QOS_1
30 | val puback = SubscribeAcknowledgement(packetIdentifier, listOf(payload))
31 | val buffer = PlatformBuffer.allocate(5)
32 | puback.serialize(buffer)
33 | buffer.resetForRead()
34 | val pubackResult = ControlPacketV4.from(buffer) as SubscribeAcknowledgement
35 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
36 | assertEquals(pubackResult.payload, listOf(GRANTED_QOS_1))
37 | }
38 |
39 | @Test
40 | fun grantedQos2() {
41 | val payload = GRANTED_QOS_2
42 | val puback = SubscribeAcknowledgement(packetIdentifier, listOf(payload))
43 | val buffer = PlatformBuffer.allocate(5)
44 | puback.serialize(buffer)
45 | buffer.resetForRead()
46 | val pubackResult = ControlPacketV4.from(buffer) as SubscribeAcknowledgement
47 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
48 | assertEquals(pubackResult.payload, listOf(GRANTED_QOS_2))
49 | }
50 |
51 | @Test
52 | fun failure() {
53 | val payload = UNSPECIFIED_ERROR
54 | val puback = SubscribeAcknowledgement(packetIdentifier, listOf(payload))
55 | val buffer = PlatformBuffer.allocate(5)
56 | puback.serialize(buffer)
57 | buffer.resetForRead()
58 | val pubackResult = ControlPacketV4.from(buffer) as SubscribeAcknowledgement
59 | assertEquals(pubackResult.packetIdentifier, packetIdentifier)
60 | assertEquals(pubackResult.payload, listOf(UNSPECIFIED_ERROR))
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/UnsubscribeAcknowledgmentTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class UnsubscribeAcknowledgmentTests {
9 | private val packetIdentifier = 2
10 |
11 | @Test
12 | fun serializeDeserializeDefault() {
13 | val buffer = PlatformBuffer.allocate(4)
14 | val actual = UnsubscribeAcknowledgment(packetIdentifier)
15 | actual.serialize(buffer)
16 | buffer.resetForRead()
17 | val expected = ControlPacketV4.from(buffer)
18 | assertEquals(expected, actual)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/models-v4/src/commonTest/kotlin/com/ditchoom/mqtt3/controlpacket/UnsubscribeRequestTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class UnsubscribeRequestTests {
9 | private val packetIdentifier = 2
10 |
11 | @Test
12 | fun basicTest() {
13 | val buffer = PlatformBuffer.allocate(17)
14 | val unsub = UnsubscribeRequest(packetIdentifier, setOf("yolo", "yolo1"))
15 | unsub.serialize(buffer)
16 | buffer.resetForRead()
17 | val result = ControlPacketV4.from(buffer) as UnsubscribeRequest
18 | val topics = result.topics.sortedBy { it.toString() }
19 | assertEquals(topics.first().toString(), "yolo")
20 | assertEquals(topics[1].toString(), "yolo1")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/models-v4/src/jsMain/kotlin/com/ditchoom/mqtt3/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import com.ditchoom.mqtt.InMemoryPersistence
4 | import com.ditchoom.mqtt.Persistence
5 | import js.errors.ReferenceError
6 | import kotlinx.coroutines.CoroutineDispatcher
7 | import kotlinx.coroutines.Dispatchers
8 | import web.idb.IDBFactory
9 |
10 | actual suspend fun newDefaultPersistence(
11 | androidContext: Any?,
12 | name: String,
13 | inMemory: Boolean,
14 | ): Persistence {
15 | val indexedDb =
16 | try {
17 | js(
18 | "indexedDB || window.indexedDB || window.mozIndexedDB || " +
19 | "window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB",
20 | ) as IDBFactory
21 | } catch (e: ReferenceError) {
22 | console.warn(
23 | "Failed to reference indexedDB, defaulting to " +
24 | "InMemoryPersistence for mqtt 4",
25 | )
26 | return InMemoryPersistence()
27 | }
28 | return IDBPersistence.idbPersistence(indexedDb, name)
29 | }
30 |
31 | actual fun defaultDispatcher(
32 | nThreads: Int,
33 | name: String,
34 | ): CoroutineDispatcher = Dispatchers.Default
35 |
--------------------------------------------------------------------------------
/models-v4/src/jsMain/kotlin/com/ditchoom/mqtt3/persistence/Qos2Message.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | data class Qos2Message(
4 | val packetId: Int,
5 | val controlPacketValue: Byte,
6 | )
7 |
--------------------------------------------------------------------------------
/models-v4/src/jsMain/kotlin/com/ditchoom/mqtt3/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 |
5 | actual fun sqlDriver(
6 | androidContext: Any?,
7 | name: String,
8 | inMemory: Boolean,
9 | ): SqlDriver? = null
10 |
--------------------------------------------------------------------------------
/models-v4/src/jvmMain/kotlin/com/ditchoom/mqtt3/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import com.ditchoom.mqtt.Persistence
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.newSingleThreadContext
6 |
7 | actual suspend fun newDefaultPersistence(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): Persistence = SqlDatabasePersistence(sqlDriver(androidContext, name, inMemory)!!)
12 |
13 | actual fun defaultDispatcher(
14 | nThreads: Int,
15 | name: String,
16 | ): CoroutineDispatcher = newSingleThreadContext("Mqtt-SQL")
17 |
--------------------------------------------------------------------------------
/models-v4/src/jvmMain/kotlin/com/ditchoom/mqtt3/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt3.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
5 | import com.ditchoom.Mqtt4
6 |
7 | actual fun sqlDriver(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): SqlDriver? {
12 | val driver = JdbcSqliteDriver(if (inMemory) JdbcSqliteDriver.IN_MEMORY else "jdbc:sqlite:file:$name")
13 | Mqtt4.Schema.create(driver)
14 | return driver
15 | }
16 |
--------------------------------------------------------------------------------
/models-v5/gradle.properties:
--------------------------------------------------------------------------------
1 | libraryName=MQTT 5 Models
2 | libraryDescription=Defines the MQTT 5 control packets
3 | artifactName=mqtt-5-models
--------------------------------------------------------------------------------
/models-v5/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/models-v5/src/androidMain/kotlin/com/ditchoom/mqtt5/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import com.ditchoom.mqtt.InMemoryPersistence
4 | import com.ditchoom.mqtt.Persistence
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.Dispatchers
7 |
8 | actual suspend fun newDefaultPersistence(
9 | androidContext: Any?,
10 | name: String,
11 | inMemory: Boolean,
12 | ): Persistence =
13 | try {
14 | SqlDatabasePersistence(sqlDriver(androidContext, name, inMemory)!!)
15 | } catch (t: Throwable) {
16 | InMemoryPersistence()
17 | }
18 |
19 | actual fun defaultDispatcher(
20 | nThreads: Int,
21 | name: String,
22 | ): CoroutineDispatcher = Dispatchers.IO
23 |
--------------------------------------------------------------------------------
/models-v5/src/androidMain/kotlin/com/ditchoom/mqtt5/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import android.content.Context
4 | import app.cash.sqldelight.db.SqlDriver
5 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver
6 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
7 | import com.ditchoom.Mqtt5
8 |
9 | actual fun sqlDriver(
10 | androidContext: Any?,
11 | name: String,
12 | inMemory: Boolean,
13 | ): SqlDriver? =
14 | if (androidContext != null) {
15 | AndroidSqliteDriver(
16 | Mqtt5.Schema,
17 | androidContext as Context,
18 | if (inMemory) {
19 | null
20 | } else {
21 | name
22 | },
23 | )
24 | } else {
25 | val driver =
26 | JdbcSqliteDriver(
27 | if (inMemory) {
28 | JdbcSqliteDriver.IN_MEMORY
29 | } else {
30 | name
31 | },
32 | )
33 | Mqtt5.Schema.create(driver)
34 | driver
35 | }
36 |
--------------------------------------------------------------------------------
/models-v5/src/appleMain/kotlin/com/ditchoom/mqtt5/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import com.ditchoom.mqtt.Persistence
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.newFixedThreadPoolContext
6 |
7 | actual suspend fun newDefaultPersistence(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): Persistence = SqlDatabasePersistence(sqlDriver(androidContext, name, inMemory)!!)
12 |
13 | actual fun defaultDispatcher(
14 | nThreads: Int,
15 | name: String,
16 | ): CoroutineDispatcher = newFixedThreadPoolContext(1, "mqtt")
17 |
--------------------------------------------------------------------------------
/models-v5/src/appleMain/kotlin/com/ditchoom/mqtt5/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 | import app.cash.sqldelight.driver.native.NativeSqliteDriver
5 | import com.ditchoom.Mqtt5
6 |
7 | actual fun sqlDriver(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): SqlDriver? =
12 | NativeSqliteDriver(
13 | Mqtt5.Schema,
14 | name,
15 | onConfiguration = {
16 | it.copy(inMemory = inMemory)
17 | },
18 | )
19 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/ControlPacketV5.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.ReadBuffer
5 | import com.ditchoom.buffer.allocate
6 | import com.ditchoom.mqtt.MalformedPacketException
7 | import com.ditchoom.mqtt.controlpacket.ControlPacket
8 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.readVariableByteInteger
9 |
10 | /**
11 | * The MQTT specification defines fifteen different types of MQTT Control Packet, for example the PublishMessage packet is
12 | * used to convey Application Messages.
13 | * @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/cos02/mqtt-v5.0-cos02.html#_Toc1477322
14 | * @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html#_Toc514847903
15 | * @param controlPacketValue Value defined under [MQTT 2.1.2]
16 | * @param direction Direction of Flow defined under [MQTT 2.1.2]
17 | */
18 | abstract class ControlPacketV5(
19 | override val controlPacketValue: Byte,
20 | override val direction: com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow,
21 | override val flags: Byte = 0b0,
22 | ) : ControlPacket {
23 | override val mqttVersion: Byte = 5
24 |
25 | override val controlPacketFactory = ControlPacketV5Factory
26 |
27 | companion object {
28 | fun from(buffer: ReadBuffer) = fromTyped(buffer)
29 |
30 | fun fromTyped(buffer: ReadBuffer): ControlPacketV5 {
31 | val byte1 = buffer.readUnsignedByte()
32 | val remainingLength = buffer.readVariableByteInteger()
33 | val remainingBuffer =
34 | if (remainingLength > 1) {
35 | buffer.readBytes(remainingLength)
36 | } else {
37 | PlatformBuffer.allocate(0)
38 | }
39 | return fromTyped(remainingBuffer, byte1, remainingLength)
40 | }
41 |
42 | fun from(
43 | buffer: ReadBuffer,
44 | byte1: UByte,
45 | remainingLength: Int,
46 | ) = fromTyped(buffer, byte1, remainingLength)
47 |
48 | fun fromTyped(
49 | buffer: ReadBuffer,
50 | byte1: UByte,
51 | remainingLength: Int,
52 | ): ControlPacketV5 {
53 | val byte1AsUInt = byte1.toUInt()
54 | val packetValue = byte1AsUInt.shr(4).toInt()
55 | return when (packetValue) {
56 | 0 -> Reserved
57 | 1 -> ConnectionRequest.from(buffer)
58 | 2 -> ConnectionAcknowledgment.from(buffer, remainingLength)
59 | 3 -> PublishMessage.from(buffer, byte1, remainingLength)
60 | 4 -> PublishAcknowledgment.from(buffer, remainingLength)
61 | 5 -> PublishReceived.from(buffer, remainingLength)
62 | 6 -> PublishRelease.from(buffer, remainingLength)
63 | 7 -> PublishComplete.from(buffer, remainingLength)
64 | 8 -> SubscribeRequest.from(buffer, remainingLength)
65 | 9 -> SubscribeAcknowledgement.from(buffer, remainingLength)
66 | 10 -> UnsubscribeRequest.from(buffer, remainingLength)
67 | 11 -> UnsubscribeAcknowledgment.from(buffer, remainingLength)
68 | 12 -> PingRequest
69 | 13 -> PingResponse
70 | 14 -> DisconnectNotification.from(buffer)
71 | 15 -> AuthenticationExchange.from(buffer)
72 | else -> throw MalformedPacketException(
73 | "Invalid MQTT Control Packet Type: $packetValue Should be in range between 0 and 15 inclusive",
74 | )
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/PingRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IPingRequest
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | /**
7 | * 3.12 PINGREQ – PING request
8 | * The PINGREQ packet is sent from a Client to the Server. It can be used to:
9 | *
10 | * · Indicate to the Server that the Client is alive in the absence of any other MQTT Control Packets being
11 | * sent from the Client to the Server.
12 | *
13 | * · Request that the Server responds to confirm that it is alive.
14 | *
15 | * · Exercise the network to indicate that the Network Connection is active.
16 | *
17 | * This packet is used in Keep Alive processing. Refer to section 3.1.2.10 for more details.
18 | */
19 |
20 | object PingRequest : ControlPacketV5(12, DirectionOfFlow.CLIENT_TO_SERVER), IPingRequest
21 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/PingResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IPingResponse
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | object PingResponse : ControlPacketV5(13, DirectionOfFlow.SERVER_TO_CLIENT), IPingResponse
7 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/Reserved.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.mqtt.controlpacket.IReserved
4 | import com.ditchoom.mqtt.controlpacket.format.fixed.DirectionOfFlow
5 |
6 | object Reserved : ControlPacketV5(0, DirectionOfFlow.FORBIDDEN), IReserved
7 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/AssignedClientIdentifier.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class AssignedClientIdentifier(val value: String) :
6 | Property(0x12, Type.UTF_8_ENCODED_STRING) {
7 | override fun write(buffer: WriteBuffer): Int = write(buffer, value)
8 |
9 | override fun size(): Int = size(value)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/Authentication.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 |
5 | data class Authentication(
6 | /**
7 | * 3.1.2.11.9 Authentication Method
8 | *
9 | * 21 (0x15) Byte, Identifier of the Authentication Method.
10 | *
11 | * Followed by a UTF-8 Encoded String containing the name of the authentication method used for
12 | * extended authentication .It is a Protocol Error to include Authentication Method more than once.
13 | *
14 | * If Authentication Method is absent, extended authentication is not performed. Refer to section
15 | * 4.12.
16 | *
17 | * If a Client sets an Authentication Method in the CONNECT, the Client MUST NOT send any packets
18 | * other than AUTH or DISCONNECT packets until it has received a CONNACK packet [MQTT-3.1.2-30].
19 | *
20 | * @see
21 | * 3.1.2.11.9 Authentication Method
22 | * @see
23 | * Section 4.12 Enhanced Authentication
24 | */
25 | val method: String,
26 | /**
27 | * 3.1.2.11.10 Authentication Data
28 | *
29 | * 22 (0x16) Byte, Identifier of the Authentication Data.
30 | *
31 | * Followed by Binary Data containing authentication data. It is a Protocol Error to include
32 | * Authentication Data if there is no Authentication Method. It is a Protocol Error to include
33 | * Authentication Data more than once.
34 | *
35 | * The contents of this data are defined by the authentication method. Refer to section 4.12 for
36 | * more information about extended authentication.
37 | */
38 | val data: ReadBuffer,
39 | ) {
40 | override fun equals(other: Any?): Boolean {
41 | if (this === other) return true
42 | if (other == null || this::class != other::class) return false
43 |
44 | other as Authentication
45 |
46 | if (method != other.method) return false
47 | if (data != other.data) return false
48 |
49 | return true
50 | }
51 |
52 | override fun hashCode(): Int {
53 | var result = method.hashCode()
54 | result = 31 * result + data.hashCode()
55 | return result
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/AuthenticationData.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 |
6 | data class AuthenticationData(val data: ReadBuffer) : Property(0x16, Type.BINARY_DATA) {
7 | override fun size(): Int {
8 | data.position(0)
9 | return 1 + UShort.SIZE_BYTES + data.remaining()
10 | }
11 |
12 | override fun write(buffer: WriteBuffer): Int {
13 | buffer.writeByte(identifierByte)
14 | data.position(0)
15 | buffer.writeUShort(data.remaining().toUShort())
16 | buffer.write(data)
17 | return size()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/AuthenticationMethod.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class AuthenticationMethod(val value: String) :
6 | Property(0x15, Type.UTF_8_ENCODED_STRING) {
7 | override fun write(buffer: WriteBuffer): Int = write(buffer, value)
8 |
9 | override fun size(): Int = size(value)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ContentType.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class ContentType(val value: String) :
6 | Property(0x03, Type.UTF_8_ENCODED_STRING, willProperties = true) {
7 | override fun write(buffer: WriteBuffer): Int = write(buffer, value)
8 |
9 | override fun size(): Int = size(value)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/CorrelationData.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.WriteBuffer
5 |
6 | data class CorrelationData(val data: ReadBuffer) :
7 | Property(0x09, Type.BINARY_DATA, willProperties = true) {
8 | override fun size(): Int {
9 | data.position(0)
10 | return 1 + UShort.SIZE_BYTES + data.remaining()
11 | }
12 |
13 | override fun write(buffer: WriteBuffer): Int {
14 | buffer.writeByte(identifierByte)
15 | data.position(0)
16 | buffer.writeUShort(data.remaining().toUShort())
17 | buffer.write(data)
18 | return size()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/MaximumPacketSize.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class MaximumPacketSize(val packetSizeLimitationBytes: ULong) :
6 | Property(0x27, Type.FOUR_BYTE_INTEGER) {
7 | override fun size(): Int = size(packetSizeLimitationBytes)
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, packetSizeLimitationBytes)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/MaximumQos.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 | import com.ditchoom.mqtt.MalformedPacketException
5 | import com.ditchoom.mqtt.controlpacket.QualityOfService
6 | import com.ditchoom.mqtt.controlpacket.QualityOfService.AT_LEAST_ONCE
7 | import com.ditchoom.mqtt.controlpacket.QualityOfService.AT_MOST_ONCE
8 | import com.ditchoom.mqtt.controlpacket.QualityOfService.EXACTLY_ONCE
9 |
10 | data class MaximumQos(val qos: QualityOfService) : Property(0x24, Type.BYTE) {
11 | override fun size(): Int = 2
12 |
13 | override fun write(buffer: WriteBuffer): Int =
14 | when (qos) {
15 | AT_MOST_ONCE -> write(buffer, false)
16 | AT_LEAST_ONCE -> write(buffer, true)
17 | EXACTLY_ONCE -> throw MalformedPacketException(
18 | "Max QoS Cannot be >= 2 as defined https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html#_Toc514847957",
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/MessageExpiryInterval.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class MessageExpiryInterval(val seconds: Long) :
6 | Property(0x02, Type.FOUR_BYTE_INTEGER, willProperties = true) {
7 | override fun size(): Int = size(seconds.toUInt())
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, seconds.toUInt())
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/PayloadFormatIndicator.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class PayloadFormatIndicator(val willMessageIsUtf8: Boolean) : Property(
6 | 0x01,
7 | Type.BYTE,
8 | willProperties = true,
9 | ) {
10 | override fun size(): Int = 2
11 |
12 | override fun write(buffer: WriteBuffer): Int = write(buffer, willMessageIsUtf8)
13 | }
14 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ReasonString.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class ReasonString(val diagnosticInfoDontParse: String) :
6 | Property(0x1F, Type.UTF_8_ENCODED_STRING) {
7 | override fun write(buffer: WriteBuffer): Int = write(buffer, diagnosticInfoDontParse)
8 |
9 | override fun size(): Int = size(diagnosticInfoDontParse)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ReceiveMaximum.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class ReceiveMaximum(val maxQos1Or2ConcurrentMessages: Int) :
6 | Property(0x21, Type.TWO_BYTE_INTEGER) {
7 | override fun size(): Int = 3
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, maxQos1Or2ConcurrentMessages.toUShort())
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/RequestProblemInformation.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class RequestProblemInformation(val reasonStringOrUserPropertiesAreSentInFailures: Boolean) :
6 | Property(0x17, Type.BYTE) {
7 | override fun size(): Int = 2
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, reasonStringOrUserPropertiesAreSentInFailures)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/RequestResponseInformation.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class RequestResponseInformation(val requestServerToReturnInfoInConnack: Boolean) :
6 | Property(0x19, Type.BYTE) {
7 | override fun size(): Int = 2
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, requestServerToReturnInfoInConnack)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ResponseInformation.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class ResponseInformation(val requestResponseInformationInConnack: String) :
6 | Property(0x1A, Type.UTF_8_ENCODED_STRING) {
7 | override fun write(buffer: WriteBuffer): Int = write(buffer, requestResponseInformationInConnack)
8 |
9 | override fun size(): Int = size(requestResponseInformationInConnack)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ResponseTopic.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 | import com.ditchoom.mqtt.controlpacket.Topic
5 |
6 | data class ResponseTopic(val value: Topic) :
7 | Property(0x08, Type.UTF_8_ENCODED_STRING, willProperties = true) {
8 | override fun write(buffer: WriteBuffer): Int = write(buffer, value.toString())
9 |
10 | override fun size(): Int = size(value.toString())
11 | }
12 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/RetainAvailable.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class RetainAvailable(val serverSupported: Boolean) : Property(0x25, Type.BYTE) {
6 | override fun size(): Int = 2
7 |
8 | override fun write(buffer: WriteBuffer): Int = write(buffer, serverSupported)
9 | }
10 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ServerKeepAlive.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class ServerKeepAlive(val seconds: Int) : Property(0x13, Type.TWO_BYTE_INTEGER) {
6 | override fun size(): Int = 3
7 |
8 | override fun write(buffer: WriteBuffer): Int = write(buffer, seconds.toUShort())
9 | }
10 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/ServerReference.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class ServerReference(val otherServer: String) :
6 | Property(0x1C, Type.UTF_8_ENCODED_STRING) {
7 | override fun write(buffer: WriteBuffer): Int = write(buffer, otherServer)
8 |
9 | override fun size(): Int = size(otherServer)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/SessionExpiryInterval.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class SessionExpiryInterval(val seconds: ULong) : Property(0x11, Type.FOUR_BYTE_INTEGER) {
6 | constructor(seconds: Int) : this(seconds.toULong())
7 |
8 | override fun size(): Int = size(seconds)
9 |
10 | override fun write(buffer: WriteBuffer): Int = write(buffer, seconds)
11 | }
12 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/SharedSubscriptionAvailable.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class SharedSubscriptionAvailable(val serverSupported: Boolean) : Property(0x2A, Type.BYTE) {
6 | override fun size(): Int = 2
7 |
8 | override fun write(buffer: WriteBuffer): Int = write(buffer, serverSupported)
9 | }
10 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/SubscriptionIdentifier.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.variableByteSize
5 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.writeVariableByteInteger
6 |
7 | data class SubscriptionIdentifier(val value: Long) : Property(0x0B, Type.VARIABLE_BYTE_INTEGER) {
8 | override fun size(): Int = variableByteSize(value.toInt()) + 1
9 |
10 | override fun write(buffer: WriteBuffer): Int {
11 | buffer.writeByte(identifierByte)
12 | buffer.writeVariableByteInteger(value.toInt())
13 | return size()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/SubscriptionIdentifierAvailable.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class SubscriptionIdentifierAvailable(val serverSupported: Boolean) :
6 | Property(0x29, Type.BYTE) {
7 | override fun size(): Int = 2
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, serverSupported)
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/TopicAlias.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class TopicAlias(val value: Int) : Property(0x22, Type.TWO_BYTE_INTEGER) {
6 | override fun size(): Int = 3
7 |
8 | override fun write(buffer: WriteBuffer): Int = write(buffer, value.toUShort())
9 | }
10 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/TopicAliasMaximum.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class TopicAliasMaximum(val highestValueSupported: Int) :
6 | Property(0x23, Type.TWO_BYTE_INTEGER) {
7 | override fun size(): Int = 3
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, highestValueSupported.toUShort())
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/Type.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | enum class Type {
4 | BYTE,
5 | TWO_BYTE_INTEGER,
6 | FOUR_BYTE_INTEGER,
7 | UTF_8_ENCODED_STRING,
8 | BINARY_DATA,
9 | VARIABLE_BYTE_INTEGER,
10 | UTF_8_STRING_PAIR,
11 | }
12 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/UserProperty.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.writeMqttUtf8String
5 | import com.ditchoom.mqtt.controlpacket.utf8Length
6 |
7 | data class UserProperty(val key: String, val value: String) : Property(
8 | 0x26,
9 | Type.UTF_8_STRING_PAIR,
10 | willProperties = true,
11 | ) {
12 | override fun write(buffer: WriteBuffer): Int {
13 | buffer.writeByte(identifierByte)
14 | buffer.writeMqttUtf8String(key)
15 | buffer.writeMqttUtf8String(value)
16 | return size()
17 | }
18 |
19 | override fun size(): Int = 5 + key.utf8Length() + value.utf8Length()
20 | }
21 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/WildcardSubscriptionAvailable.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class WildcardSubscriptionAvailable(val serverSupported: Boolean) : Property(0x28, Type.BYTE) {
6 | override fun size(): Int = 2
7 |
8 | override fun write(buffer: WriteBuffer): Int = write(buffer, serverSupported)
9 | }
10 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/controlpacket/properties/WillDelayInterval.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket.properties
2 |
3 | import com.ditchoom.buffer.WriteBuffer
4 |
5 | data class WillDelayInterval(val seconds: Long) :
6 | Property(0x18, Type.FOUR_BYTE_INTEGER, willProperties = true) {
7 | override fun size(): Int = size(seconds.toUInt())
8 |
9 | override fun write(buffer: WriteBuffer): Int = write(buffer, seconds.toUInt())
10 | }
11 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import com.ditchoom.mqtt.Persistence
4 | import kotlinx.coroutines.CoroutineDispatcher
5 |
6 | expect suspend fun newDefaultPersistence(
7 | androidContext: Any? = null,
8 | name: String = "mqtt5.db",
9 | inMemory: Boolean = false,
10 | ): Persistence
11 |
12 | expect fun defaultDispatcher(
13 | nThreads: Int,
14 | name: String,
15 | ): CoroutineDispatcher
16 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/kotlin/com/ditchoom/mqtt5/persistence/SqlPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 |
5 | expect fun sqlDriver(
6 | androidContext: Any?,
7 | name: String = "mqtt5.db",
8 | inMemory: Boolean = false,
9 | ): SqlDriver?
10 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/Broker.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS Broker (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | next_packet_id INTEGER NOT NULL DEFAULT 1 CHECK(next_packet_id BETWEEN 0 AND 65535)
4 | );
5 |
6 | insertBroker:
7 | INSERT INTO Broker VALUES(NULL, 1);
8 |
9 | lastRowId:
10 | SELECT last_insert_rowid();
11 |
12 | deleteBroker:
13 | DELETE FROM Broker WHERE id = :id;
14 |
15 | allBrokers:
16 | SELECT * FROM Broker;
17 |
18 | incrementPacketId:
19 | UPDATE Broker SET next_packet_id = (next_packet_id % 65535) + 1 WHERE id = :id;
20 |
21 | nextPacketId:
22 | SELECT next_packet_id FROM Broker WHERE id = :id;
23 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/ConnectionRequest.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS ConnectionRequest (
2 | broker_id INTEGER NOT NULL UNIQUE,
3 | protocol_name TEXT NOT NULL DEFAULT 'MQTT',
4 | protocol_version INTEGER NOT NULL DEFAULT 5 CHECK(protocol_version BETWEEN 0 AND 255),
5 | will_retain INTEGER NOT NULL DEFAULT 0 CHECK(will_retain BETWEEN 0 AND 1),
6 | will_qos INTEGER NOT NULL DEFAULT 0 CHECK(will_qos BETWEEN 0 AND 2),
7 | will_flag INTEGER NOT NULL DEFAULT 0 CHECK(will_flag BETWEEN 0 AND 1),
8 | clean_start INTEGER NOT NULL DEFAULT 0 CHECK(clean_start BETWEEN 0 AND 1),
9 | keep_alive_seconds INTEGER NOT NULL DEFAULT 3600 CHECK(keep_alive_seconds BETWEEN 0 AND 65535),
10 | session_expiry_interval_seconds INTEGER,
11 | receive_maximum INTEGER CHECK(IFNULL(receive_maximum, 0) BETWEEN 0 AND 65535),
12 | maximum_packet_size INTEGER,
13 | topic_alias_maximum INTEGER CHECK(IFNULL(topic_alias_maximum, 0) BETWEEN 0 AND 65535),
14 | request_response_information INTEGER CHECK(IFNULL(request_response_information, 0) BETWEEN 0 AND 1),
15 | request_problem_information INTEGER CHECK(IFNULL(request_problem_information, 0) BETWEEN 0 AND 1),
16 | authentication_method TEXT,
17 | authentication_data BLOB,
18 | client_id TEXT NOT NULL,
19 | has_will_properties INTEGER NOT NULL DEFAULT 0 CHECK(has_will_properties BETWEEN 0 AND 1),
20 | will_topic TEXT,
21 | will_payload BLOB,
22 | username TEXT,
23 | password TEXT,
24 | will_property_will_delay_interval_seconds INTEGER NOT NULL CHECK(will_property_will_delay_interval_seconds BETWEEN 0 AND 4294967295),
25 | will_property_payload_format_indicator INTEGER CHECK(IFNULL(will_property_payload_format_indicator, 0) BETWEEN 0 AND 1),
26 | will_property_message_expiry_interval_seconds INTEGER CHECK(IFNULL(will_property_message_expiry_interval_seconds, 0) BETWEEN 0 AND 4294967295),
27 | will_property_content_type TEXT,
28 | will_property_response_topic TEXT,
29 | will_property_correlation_data BLOB,
30 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE
31 | );
32 |
33 | insertConnectionRequest:
34 | INSERT INTO ConnectionRequest
35 | (broker_id, protocol_name, protocol_version, will_retain, will_qos, will_flag, clean_start, keep_alive_seconds,
36 | session_expiry_interval_seconds, receive_maximum, maximum_packet_size, topic_alias_maximum,
37 | request_response_information, request_problem_information, authentication_method, authentication_data, client_id,
38 | has_will_properties, will_topic, will_payload, username, password,will_property_will_delay_interval_seconds,
39 | will_property_payload_format_indicator, will_property_message_expiry_interval_seconds, will_property_content_type,
40 | will_property_response_topic, will_property_correlation_data)
41 | VALUES
42 | (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
43 |
44 | connectionRequestByBrokerId:
45 | SELECT * FROM ConnectionRequest WHERE broker_id = :brokerId LIMIT 1;
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/PublishMessage.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS PublishMessage (
2 | broker_id INTEGER NOT NULL,
3 | incoming INTEGER NOT NULL DEFAULT 0 CHECK(incoming BETWEEN 0 AND 1),
4 | -- fixed header
5 | dup INTEGER NOT NULL DEFAULT 0 CHECK(dup BETWEEN 0 AND 1),
6 | qos INTEGER NOT NULL DEFAULT 1 CHECK(qos BETWEEN 0 AND 2), -- cannot be 0 if we are persisting
7 | retain INTEGER NOT NULL DEFAULT 0 CHECK(retain BETWEEN 0 AND 1),
8 | -- variable header
9 | topic_name TEXT NOT NULL,
10 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
11 | -- variable header props
12 | payload_format_indicator INTEGER NOT NULL DEFAULT 0 CHECK(payload_format_indicator BETWEEN 0 AND 1),
13 | message_expiry_interval INTEGER CHECK(IFNULL(message_expiry_interval, 0) BETWEEN 0 AND 4294967295),
14 | topic_alias INTEGER CHECK(IFNULL(topic_alias, 0) BETWEEN 0 AND 65535),
15 | response_topic TEXT,
16 | correlation_data BLOB,
17 | subscription_identifier TEXT,
18 | content_type TEXT,
19 | -- payload
20 | payload BLOB,
21 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
22 | PRIMARY KEY (broker_id, incoming, packet_id)
23 | );
24 |
25 | insertPublishMessage:
26 | INSERT INTO PublishMessage
27 | (broker_id, incoming, dup, qos, retain, topic_name, packet_id, payload_format_indicator, message_expiry_interval, topic_alias, response_topic, correlation_data, subscription_identifier, content_type, payload)
28 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
29 |
30 | deletePublishMessage {
31 | DELETE FROM PublishMessage WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
32 | DELETE FROM UserProperty WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
33 | }
34 |
35 | deleteAll:
36 | DELETE FROM PublishMessage WHERE broker_id = :brokerId;
37 |
38 | queuedPubMessages:
39 | SELECT * FROM PublishMessage WHERE broker_id = :brokerId AND incoming = 0;
40 |
41 | publishMessageCount:
42 | SELECT COUNT(broker_id) FROM PublishMessage WHERE broker_id = :brokerId;
43 |
44 | allMessages:
45 | SELECT * FROM PublishMessage WHERE broker_id = :brokerId;
46 |
47 | messageWithId:
48 | SELECT * FROM PublishMessage WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId LIMIT 1;
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/QoS2Messages.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS Qos2Messages (
2 | broker_id INTEGER NOT NULL,
3 | incoming INTEGER NOT NULL DEFAULT 0 CHECK(incoming BETWEEN 0 AND 1),
4 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
5 | reason_code INTEGER NOT NULL CHECK(reason_code BETWEEN 0 AND 162),
6 | reason_string TEXT,
7 | type INTEGER NOT NULL CHECK(type BETWEEN 5 AND 7),
8 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
9 | PRIMARY KEY (broker_id, incoming, packet_id, type)
10 | );
11 |
12 | insertQos2Message:
13 | INSERT INTO Qos2Messages (broker_id, incoming, packet_id, reason_code, reason_string, type)
14 | VALUES (?, ?, ?, ?, ?, ?);
15 |
16 | updateQos2Message {
17 | UPDATE Qos2Messages SET type = :type WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
18 | DELETE FROM UserProperty WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
19 | }
20 |
21 | deleteQos2Message {
22 | DELETE FROM Qos2Messages WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
23 | DELETE FROM UserProperty WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId;
24 | }
25 |
26 | deleteAll:
27 | DELETE FROM Qos2Messages WHERE broker_id = :brokerId;
28 |
29 | queuedQos2Messages:
30 | SELECT * FROM Qos2Messages WHERE broker_id = :brokerId AND incoming = 0;
31 |
32 | queuedMessageCount:
33 | SELECT COUNT(broker_id) FROM Qos2Messages WHERE broker_id = :brokerId;
34 |
35 | allMessages:
36 | SELECT * FROM Qos2Messages WHERE broker_id = :brokerId;
37 |
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/SocketConnection.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS SocketConnection (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | broker_id INTEGER NOT NULL,
4 | type TEXT NOT NULL DEFAULT 'tcp' CHECK(type LIKE 'tcp' OR type LIKE 'websocket'),
5 | host TEXT NOT NULL,
6 | port INTEGER NOT NULL CHECK(port BETWEEN 1 AND 65535),
7 | tls INTEGER NOT NULL CHECK(tls BETWEEN 0 AND 1),
8 | connection_timeout_ms INTEGER NOT NULL CHECK(connection_timeout_ms > 0),
9 | read_timeout_ms INTEGER NOT NULL CHECK(read_timeout_ms > 0),
10 | write_timeout_ms INTEGER NOT NULL CHECK(write_timeout_ms > 0),
11 | websocket_endpoint TEXT,
12 | websocket_protocols TEXT,
13 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE
14 | );
15 |
16 |
17 | insertConnection:
18 | INSERT INTO SocketConnection
19 | (id,broker_id,type,host,port,tls,connection_timeout_ms,read_timeout_ms,write_timeout_ms,websocket_endpoint,websocket_protocols)
20 | VALUES
21 | (NULL,?,?,?,?,?,?,?,?,?,?);
22 |
23 |
24 | connectionsByBrokerId:
25 | SELECT * FROM SocketConnection WHERE broker_id = :brokerId;
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/Subscription.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS Subscription (
2 | broker_id INTEGER NOT NULL,
3 | subscribe_packet_id INTEGER NOT NULL CHECK(subscribe_packet_id BETWEEN 0 AND 65535),
4 | unsubscribe_packet_id INTEGER NOT NULL CHECK(unsubscribe_packet_id BETWEEN 0 AND 65535),
5 | topic_filter TEXT NOT NULL,
6 | qos INTEGER NOT NULL DEFAULT 0 CHECK(qos BETWEEN 0 AND 2),
7 | no_local INTEGER NOT NULL DEFAULT 0 CHECK(no_local BETWEEN 0 AND 1),
8 | retain_as_published INTEGER NOT NULL DEFAULT 0 CHECK(retain_as_published BETWEEN 0 AND 1),
9 | retain_handling INTEGER NOT NULL DEFAULT 0 CHECK(retain_handling BETWEEN 0 AND 2),
10 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
11 | PRIMARY KEY (broker_id,subscribe_packet_id, topic_filter)
12 | );
13 |
14 | insertSubscription:
15 | INSERT INTO Subscription (broker_id, subscribe_packet_id,unsubscribe_packet_id, topic_filter, qos, no_local, retain_as_published, retain_handling)
16 | VALUES (?, ?, 0,?, ?, ?, ?, ?);
17 |
18 | allSubscriptions:
19 | SELECT * FROM Subscription WHERE broker_id = :brokerId;
20 |
21 | allSubscriptionsNotPendingUnsub:
22 | SELECT * FROM Subscription WHERE broker_id = :brokerId AND unsubscribe_packet_id = 0;
23 |
24 | addUnsubscriptionPacketId:
25 | UPDATE Subscription SET unsubscribe_packet_id = :unsubPacketId WHERE broker_id = :brokerId AND topic_filter = :topicFilter;
26 |
27 | deleteSubscription:
28 | DELETE FROM Subscription WHERE broker_id = :brokerId AND unsubscribe_packet_id = :unsubPacketId;
29 |
30 | deleteAll:
31 | DELETE FROM Subscription WHERE broker_id = :brokerId;
32 |
33 | queuedSubscriptions:
34 | SELECT * FROM Subscription WHERE broker_id = :brokerId AND subscribe_packet_id = :subPacketId AND unsubscribe_packet_id = 0;
35 |
36 | queuedUnsubscriptions:
37 | SELECT * FROM Subscription WHERE broker_id = :brokerId AND unsubscribe_packet_id = :unsubscribePacketId;
38 |
39 | queuedMessageCount:
40 | SELECT COUNT(broker_id) FROM Subscription WHERE broker_id = :brokerId;
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/SubscriptionRequest.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS SubscribeRequest(
2 | broker_id INTEGER NOT NULL,
3 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
4 | reason_string TEXT,
5 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
6 | PRIMARY KEY (broker_id,packet_id)
7 | );
8 |
9 |
10 | insertSubscribeRequest:
11 | INSERT INTO SubscribeRequest (broker_id, packet_id, reason_string)
12 | VALUES (?, ?, ?);
13 |
14 | deleteSubscribeRequest {
15 | DELETE FROM SubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId;
16 | DELETE FROM UserProperty WHERE broker_id = :brokerId AND incoming = 0 AND packet_id = :packetId;
17 | }
18 |
19 | queuedSubMessages:
20 | SELECT * FROM SubscribeRequest WHERE broker_id = :brokerId;
21 |
22 | deleteAll:
23 | DELETE FROM SubscribeRequest WHERE broker_id = :brokerId;
24 |
25 | queuedMessageCount:
26 | SELECT COUNT(broker_id) FROM SubscribeRequest WHERE broker_id = :brokerId;
27 |
28 | messageWithId:
29 | SELECT * FROM SubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId LIMIT 1;
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/UnsubscribeRequest.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS UnsubscribeRequest(
2 | broker_id INTEGER NOT NULL,
3 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN 0 AND 65535),
4 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
5 | PRIMARY KEY (broker_id,packet_id)
6 | );
7 |
8 |
9 | insertUnsubscribeRequest:
10 | INSERT INTO UnsubscribeRequest (broker_id, packet_id)
11 | VALUES (?, ?);
12 |
13 | deleteUnsubscribeRequest {
14 | DELETE FROM UnsubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId;
15 | DELETE FROM UserProperty WHERE broker_id = :brokerId AND incoming = 0 AND packet_id = :packetId;
16 | }
17 |
18 | queuedUnsubMessages:
19 | SELECT * FROM UnsubscribeRequest WHERE broker_id = :brokerId;
20 |
21 | deleteAll:
22 | DELETE FROM UnsubscribeRequest WHERE broker_id = :brokerId;
23 |
24 | queuedMessageCount:
25 | SELECT COUNT(broker_id) FROM UnsubscribeRequest WHERE broker_id = :brokerId;
26 |
27 | messageWithId:
28 | SELECT * FROM UnsubscribeRequest WHERE broker_id = :brokerId AND packet_id = :packetId LIMIT 1;
--------------------------------------------------------------------------------
/models-v5/src/commonMain/sqldelight/com/ditchoom/mqtt5/persistence/UserProperty.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS UserProperty(
2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | broker_id INTEGER NOT NULL,
4 | incoming INTEGER NOT NULL DEFAULT 0 CHECK(incoming BETWEEN 0 AND 1),
5 | packet_id INTEGER NOT NULL CHECK(packet_id BETWEEN -2 AND 65535),
6 | key TEXT NOT NULL,
7 | value TEXT NOT NULL,
8 | FOREIGN KEY(broker_id) REFERENCES Broker(id) ON DELETE CASCADE,
9 | -- FOREIGN KEY(broker_id, incoming, packet_id) REFERENCES PublishMessage(broker_id, incoming, packet_id) ON DELETE CASCADE,
10 | UNIQUE(id, broker_id, incoming, packet_id)
11 | );
12 |
13 | allProps:
14 | SELECT key, value FROM UserProperty WHERE broker_id = :brokerId AND incoming = :incoming AND packet_id = :packetId ORDER BY rowid ASC;
15 |
16 | addProp:
17 | INSERT INTO UserProperty (broker_id, incoming, packet_id, key, value)
18 | VALUES (?, ?, ?, ?, ?);
--------------------------------------------------------------------------------
/models-v5/src/commonTest/kotlin/com/ditchoom/mqtt5/controlpacket/PingRequestTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PingRequestTests {
9 | @Test
10 | fun serializeDeserialize() {
11 | val buffer = PlatformBuffer.allocate(2)
12 | val ping = PingRequest
13 | ping.serialize(buffer)
14 | buffer.resetForRead()
15 | assertEquals(12.shl(4).toByte(), buffer.readByte())
16 | assertEquals(0, buffer.readByte())
17 | buffer.resetForRead()
18 | val result = ControlPacketV5.from(buffer)
19 | assertEquals(result, ping)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/models-v5/src/commonTest/kotlin/com/ditchoom/mqtt5/controlpacket/PingResponseTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class PingResponseTests {
9 | @Test
10 | fun serializeDeserialize() {
11 | val buffer = PlatformBuffer.allocate(2)
12 | val ping = PingResponse
13 | ping.serialize(buffer)
14 | buffer.resetForRead()
15 | assertEquals(13.shl(4).toByte(), buffer.readByte())
16 | assertEquals(0, buffer.readByte())
17 | buffer.resetForRead()
18 | val result = ControlPacketV5.from(buffer)
19 | assertEquals(result, ping)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/models-v5/src/commonTest/kotlin/com/ditchoom/mqtt5/controlpacket/UnsubscribeRequestTests.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.controlpacket
2 |
3 | import com.ditchoom.buffer.PlatformBuffer
4 | import com.ditchoom.buffer.allocate
5 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.readMqttUtf8StringNotValidatedSized
6 | import com.ditchoom.mqtt.controlpacket.ControlPacket.Companion.readVariableByteInteger
7 | import com.ditchoom.mqtt.controlpacket.Topic
8 | import com.ditchoom.mqtt5.controlpacket.UnsubscribeRequest.VariableHeader
9 | import com.ditchoom.mqtt5.controlpacket.properties.UserProperty
10 | import kotlin.test.Test
11 | import kotlin.test.assertEquals
12 |
13 | class UnsubscribeRequestTests {
14 | private val packetIdentifier = 2
15 |
16 | @Test
17 | fun basicTest() {
18 | val buffer = PlatformBuffer.allocate(11)
19 | val unsub =
20 | UnsubscribeRequest(
21 | VariableHeader(packetIdentifier),
22 | setOf(Topic.fromOrThrow("yolo", Topic.Type.Filter)),
23 | )
24 | unsub.serialize(buffer)
25 | buffer.resetForRead()
26 | assertEquals(0b10100010.toByte(), buffer.readByte(), "fixed header byte 1")
27 | assertEquals(9, buffer.readVariableByteInteger(), "fixed header byte 2 remaining length")
28 | assertEquals(
29 | packetIdentifier.toUShort(),
30 | buffer.readUnsignedShort(),
31 | "variable header byte 1-2 packet identifier",
32 | )
33 | assertEquals(0, buffer.readVariableByteInteger(), "variable header byte 3 property length")
34 | assertEquals(
35 | "yolo",
36 | buffer.readMqttUtf8StringNotValidatedSized().second.toString(),
37 | "payload topic",
38 | )
39 | buffer.resetForRead()
40 | val result = ControlPacketV5.from(buffer) as UnsubscribeRequest
41 | assertEquals("yolo", result.topics.first().toString())
42 | assertEquals(unsub.toString(), result.toString())
43 | }
44 |
45 | @Test
46 | fun variableHeaderPropertyUserProperty() {
47 | val props = VariableHeader.Properties.from(setOf(UserProperty("key", "value")))
48 | val userPropertyResult = props.userProperty
49 | for ((key, value) in userPropertyResult) {
50 | assertEquals(key, "key")
51 | assertEquals(value, "value")
52 | }
53 | assertEquals(userPropertyResult.size, 1)
54 |
55 | val request =
56 | UnsubscribeRequest(
57 | VariableHeader(packetIdentifier, properties = props),
58 | setOf(Topic.fromOrThrow("test", Topic.Type.Filter)),
59 | )
60 | val buffer = PlatformBuffer.allocate(24)
61 | request.serialize(buffer)
62 | buffer.resetForRead()
63 | val requestRead = ControlPacketV5.from(buffer) as UnsubscribeRequest
64 | val (key, value) = requestRead.variable.properties.userProperty.first()
65 | assertEquals("key", key.toString())
66 | assertEquals("value", value.toString())
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/models-v5/src/jsMain/kotlin/com/ditchoom/mqtt5/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import com.ditchoom.mqtt.InMemoryPersistence
4 | import com.ditchoom.mqtt.Persistence
5 | import js.errors.ReferenceError
6 | import kotlinx.coroutines.CoroutineDispatcher
7 | import kotlinx.coroutines.Dispatchers
8 | import web.idb.IDBFactory
9 |
10 | actual suspend fun newDefaultPersistence(
11 | androidContext: Any?,
12 | name: String,
13 | inMemory: Boolean,
14 | ): Persistence {
15 | val indexedDb =
16 | try {
17 | js(
18 | "indexedDB || window.indexedDB || window.mozIndexedDB || " +
19 | "window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB",
20 | ) as IDBFactory
21 | } catch (e: ReferenceError) {
22 | console.warn(
23 | "Failed to reference indexedDB, defaulting to InMemoryPersistence " +
24 | "for mqtt 5",
25 | )
26 | return InMemoryPersistence()
27 | }
28 | return IDBPersistence.idbPersistence(indexedDb, name)
29 | }
30 |
31 | actual fun defaultDispatcher(
32 | nThreads: Int,
33 | name: String,
34 | ): CoroutineDispatcher = Dispatchers.Default
35 |
--------------------------------------------------------------------------------
/models-v5/src/jsMain/kotlin/com/ditchoom/mqtt5/persistence/Qos2Message.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | data class Qos2Message(
4 | val packetId: Int,
5 | val controlPacketValue: Byte,
6 | )
7 |
--------------------------------------------------------------------------------
/models-v5/src/jsMain/kotlin/com/ditchoom/mqtt5/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 |
5 | actual fun sqlDriver(
6 | androidContext: Any?,
7 | name: String,
8 | inMemory: Boolean,
9 | ): SqlDriver? = null
10 |
--------------------------------------------------------------------------------
/models-v5/src/jvmMain/kotlin/com/ditchoom/mqtt5/persistence/DefaultPersistence.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import com.ditchoom.mqtt.Persistence
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.newSingleThreadContext
6 |
7 | actual suspend fun newDefaultPersistence(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): Persistence = SqlDatabasePersistence(sqlDriver(androidContext, name, inMemory)!!)
12 |
13 | actual fun defaultDispatcher(
14 | nThreads: Int,
15 | name: String,
16 | ): CoroutineDispatcher = newSingleThreadContext("Mqtt5-SQL")
17 |
--------------------------------------------------------------------------------
/models-v5/src/jvmMain/kotlin/com/ditchoom/mqtt5/persistence/SqlDriver.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt5.persistence
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
5 | import com.ditchoom.Mqtt5
6 |
7 | actual fun sqlDriver(
8 | androidContext: Any?,
9 | name: String,
10 | inMemory: Boolean,
11 | ): SqlDriver? {
12 | val driver: SqlDriver = JdbcSqliteDriver(if (inMemory) JdbcSqliteDriver.IN_MEMORY else "jdbc:sqlite:file:$name")
13 | Mqtt5.Schema.create(driver)
14 | return driver
15 | }
16 |
--------------------------------------------------------------------------------
/mqtt-client/gradle.properties:
--------------------------------------------------------------------------------
1 | libraryName=MQTT Client
2 | libraryDescription=Coroutines based MQTT 4 + 5 Client
3 | artifactName=mqtt-client
--------------------------------------------------------------------------------
/mqtt-client/karma.config.d/karma.conf.js:
--------------------------------------------------------------------------------
1 | config.client = config.client || {}
2 | config.client.mocha = config.client.mocha || {}
3 | config.client.mocha.timeout = '10s'
4 | config.browserNoActivityTimeout = 10000
5 | config.browserDisconnectTimeout = 10000
--------------------------------------------------------------------------------
/mqtt-client/mqtt_client.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'mqtt_client'
3 | spec.version = '0.1.3'
4 | spec.homepage = ''
5 | spec.source = { :http=> ''}
6 | spec.authors = ''
7 | spec.license = ''
8 | spec.summary = ''
9 | spec.vendored_frameworks = 'build/cocoapods/framework/mqtt_client.framework'
10 | spec.libraries = 'c++'
11 | spec.ios.deployment_target = '13.0'
12 | spec.osx.deployment_target = '11.0'
13 | spec.tvos.deployment_target = '13.0'
14 | spec.watchos.deployment_target = '6.0'
15 | spec.dependency 'SocketWrapper'
16 |
17 | if !Dir.exist?('build/cocoapods/framework/mqtt_client.framework') || Dir.empty?('build/cocoapods/framework/mqtt_client.framework')
18 | raise "
19 |
20 | Kotlin framework 'mqtt_client' doesn't exist yet, so a proper Xcode project can't be generated.
21 | 'pod install' should be executed after running ':generateDummyFramework' Gradle task:
22 |
23 | ./gradlew :mqtt-client:generateDummyFramework
24 |
25 | Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
26 | end
27 |
28 | spec.xcconfig = {
29 | 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
30 | }
31 |
32 | spec.pod_target_xcconfig = {
33 | 'KOTLIN_PROJECT_PATH' => ':mqtt-client',
34 | 'PRODUCT_MODULE_NAME' => 'mqtt_client',
35 | }
36 |
37 | spec.script_phases = [
38 | {
39 | :name => 'Build mqtt_client',
40 | :execution_position => :before_compile,
41 | :shell_path => '/bin/sh',
42 | :script => <<-SCRIPT
43 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
44 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
45 | exit 0
46 | fi
47 | set -ev
48 | REPO_ROOT="$PODS_TARGET_SRCROOT"
49 | "$REPO_ROOT/../../../../../private/var/folders/pf/yv2h4kt547v0wlfdlclcwt300000gn/T/wrap10109loc/gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
50 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
51 | -Pkotlin.native.cocoapods.archs="$ARCHS" \
52 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
53 | SCRIPT
54 | }
55 | ]
56 |
57 | end
--------------------------------------------------------------------------------
/mqtt-client/src/androidInstrumentedTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidInstrumentedTest/kotlin/com/ditchoom/mqtt/client/ipc/IPCTest.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.test.filters.MediumTest
6 | import androidx.test.platform.app.InstrumentationRegistry
7 | import androidx.test.rule.ServiceTestRule
8 | import androidx.test.runner.AndroidJUnit4
9 | import com.ditchoom.mqtt.client.LocalMqttService
10 | import com.ditchoom.mqtt.client.MqttService
11 | import com.ditchoom.mqtt.client.net.sendAllMessageTypes
12 | import com.ditchoom.mqtt.connection.MqttConnectionOptions
13 | import com.ditchoom.mqtt.controlpacket.Topic
14 | import com.ditchoom.mqtt3.controlpacket.ConnectionRequest
15 | import kotlinx.coroutines.runBlocking
16 | import kotlinx.coroutines.suspendCancellableCoroutine
17 | import org.junit.Rule
18 | import org.junit.Test
19 | import org.junit.runner.RunWith
20 | import java.util.concurrent.TimeUnit
21 | import kotlin.random.Random
22 | import kotlin.random.nextUInt
23 | import kotlin.time.Duration.Companion.seconds
24 |
25 | @RunWith(AndroidJUnit4::class)
26 | @MediumTest
27 | class IPCTest {
28 | @get:Rule
29 | val serviceRule: ServiceTestRule = ServiceTestRule.withTimeout(1000, TimeUnit.MILLISECONDS)
30 | private val testWsMqttConnectionOptions =
31 | MqttConnectionOptions.WebSocketConnectionOptions(
32 | "10.0.2.2",
33 | 80,
34 | websocketEndpoint = "/mqtt",
35 | tls = false,
36 | protocols = listOf("mqttv3.1"),
37 | connectionTimeout = 10.seconds,
38 | )
39 | private val connectionRequestMqtt4 =
40 | ConnectionRequest(
41 | variableHeader = ConnectionRequest.VariableHeader(cleanSession = true, keepAliveSeconds = 1),
42 | payload = ConnectionRequest.Payload(clientId = "taco123Ipc-" + Random.nextUInt()),
43 | )
44 |
45 | @Test
46 | fun testIpcAllTypes() =
47 | runBlocking {
48 | val context = InstrumentationRegistry.getInstrumentation().context
49 | val i = Intent(context, MqttManagerService::class.java)
50 | val serviceBinder =
51 | suspendCancellableCoroutine {
52 | val connection = MqttServiceHelper.MqttServiceConnection(context, it)
53 | serviceRule.bindService(i, connection, Context.BIND_AUTO_CREATE)
54 | }
55 | // inMemory will not work because it's separate processes
56 | val service: MqttService = AndroidRemoteMqttServiceClient(serviceBinder, LocalMqttService.buildService(context))
57 | service.allBrokers().forEach { service.removeBroker(it.brokerId, it.protocolVersion) }
58 |
59 | val broker = service.addBroker(listOf(testWsMqttConnectionOptions), connectionRequestMqtt4)
60 | service.start(broker)
61 | val client = checkNotNull(service.getClient(broker))
62 | client.awaitConnectivity()
63 | sendAllMessageTypes(client, Topic.fromOrThrow("testIpc", Topic.Type.Name), "Test String")
64 | client.shutdown(true)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidInstrumentedTest/kotlin/com/ditchoom/mqtt/client/net/Platform.mqtt.mqtt-client.unit.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.net
2 |
3 | actual fun getPlatform(): Platform = Platform.Android
4 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/aidl/com/ditchoom/mqtt/client/ipc/IPCMqttClient.aidl:
--------------------------------------------------------------------------------
1 | // IPCMqttClient.aidl
2 | package com.ditchoom.mqtt.client.ipc;
3 |
4 | import com.ditchoom.mqtt.client.ipc.MqttMessageTransferredCallback;
5 | import com.ditchoom.mqtt.client.ipc.MqttMessageCallback;
6 | import com.ditchoom.mqtt.client.ipc.MqttCompletionCallback;
7 | import com.ditchoom.buffer.JvmBuffer;
8 |
9 | interface IPCMqttClient {
10 | void subscribeQueued(int packetIdentifier, MqttCompletionCallback cb);
11 | void publishQueued(int packetIdentifier, in JvmBuffer nullablleQos0Buffer, MqttCompletionCallback cb);
12 | void unsubscribeQueued(int packetIdentifier, MqttCompletionCallback cb);
13 |
14 | void registerObserver(MqttMessageTransferredCallback observer);
15 | void unregisterObserver(MqttMessageTransferredCallback observer);
16 |
17 | JvmBuffer currentConnectionAcknowledgmentOrNull();
18 | void awaitConnectivity(MqttMessageCallback cb);
19 |
20 | long pingCount();
21 | long pingResponseCount();
22 |
23 | long connectionCount();
24 | long connectionAttempts();
25 |
26 | void sendDisconnect(MqttCompletionCallback cb);
27 | void shutdown(boolean sendDisconnect, MqttCompletionCallback cb);
28 | }
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/aidl/com/ditchoom/mqtt/client/ipc/IPCMqttService.aidl:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc;
2 |
3 | import com.ditchoom.mqtt.client.ipc.MqttGetClientCallback;
4 | import com.ditchoom.mqtt.client.ipc.MqttCompletionCallback;
5 |
6 | interface IPCMqttService {
7 | void startAll(MqttCompletionCallback completion);
8 | void start(int brokerId, byte protocolVersion, MqttCompletionCallback completion);
9 |
10 | void stopAll(MqttCompletionCallback completion);
11 | void stop(int brokerId, byte protocolVersion, MqttCompletionCallback completion);
12 |
13 | void requestClientOrNull(int brokerId, byte protocolVersion, MqttGetClientCallback callback);
14 | }
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/aidl/com/ditchoom/mqtt/client/ipc/MqttCompletionCallback.aidl:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc;
2 |
3 | interface MqttCompletionCallback {
4 | void onSuccess();
5 | void onError(String messageOrNull);
6 | }
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/aidl/com/ditchoom/mqtt/client/ipc/MqttGetClientCallback.aidl:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc;
2 |
3 | import com.ditchoom.mqtt.client.ipc.IPCMqttClient;
4 |
5 | interface MqttGetClientCallback {
6 | void onClientReady(IPCMqttClient client, int brokerId, byte protocolVersion);
7 | void onClientNotFound();
8 | }
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/aidl/com/ditchoom/mqtt/client/ipc/MqttMessageCallback.aidl:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc;
2 |
3 | import com.ditchoom.buffer.JvmBuffer;
4 |
5 | interface MqttMessageCallback {
6 | void onMessage(in JvmBuffer buffer);
7 | }
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/aidl/com/ditchoom/mqtt/client/ipc/MqttMessageTransferredCallback.aidl:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc;
2 |
3 | import com.ditchoom.buffer.JvmBuffer;
4 |
5 | interface MqttMessageTransferredCallback {
6 | int id();
7 | void onControlPacketSent(in JvmBuffer controlPacket);
8 | void onControlPacketReceived(byte byte1, int remainingLength, in JvmBuffer controlPacket);
9 |
10 | }
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/MqttServiceInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import android.content.Context
4 | import androidx.startup.Initializer
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.runBlocking
7 |
8 | class MqttServiceInitializer : Initializer {
9 | override fun create(context: Context): LocalMqttService {
10 | return runBlocking(Dispatchers.Default) { LocalMqttService.buildService(context) }
11 | }
12 |
13 | override fun dependencies(): MutableList>> = mutableListOf()
14 | }
15 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/AndroidMqttClientIPCServer.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.buffer.AllocationZone
4 | import com.ditchoom.buffer.JvmBuffer
5 | import kotlinx.coroutines.launch
6 |
7 | class AndroidMqttClientIPCServer(private val clientServer: RemoteMqttClientWorker) : IPCMqttClient.Stub() {
8 | private val observers = HashMap()
9 |
10 | init {
11 | clientServer.observers += { incoming, byte1, remaining, buffer ->
12 | observers.values.forEach {
13 | if (incoming) {
14 | it.onControlPacketReceived(byte1.toByte(), remaining, buffer as JvmBuffer)
15 | buffer.resetForRead()
16 | } else {
17 | it.onControlPacketSent(buffer as JvmBuffer)
18 | }
19 | }
20 | }
21 | }
22 |
23 | override fun subscribeQueued(
24 | packetIdentifier: Int,
25 | callback: MqttCompletionCallback,
26 | ) = wrapResultWithCallback(callback) { clientServer.onSubscribeQueued(packetIdentifier) }
27 |
28 | override fun publishQueued(
29 | packetIdentifier: Int,
30 | nullablleQos0Buffer: JvmBuffer?,
31 | callback: MqttCompletionCallback,
32 | ) = wrapResultWithCallback(callback) { clientServer.onPublishQueued(packetIdentifier, nullablleQos0Buffer) }
33 |
34 | override fun unsubscribeQueued(
35 | packetIdentifier: Int,
36 | callback: MqttCompletionCallback,
37 | ) = wrapResultWithCallback(callback) { clientServer.onUnsubscribeQueued(packetIdentifier) }
38 |
39 | override fun registerObserver(observer: MqttMessageTransferredCallback) {
40 | observers[observer.id()] = observer
41 | }
42 |
43 | override fun unregisterObserver(observer: MqttMessageTransferredCallback) {
44 | observers.remove(observer.id())
45 | }
46 |
47 | override fun currentConnectionAcknowledgmentOrNull(): JvmBuffer? {
48 | return clientServer.currentConnectionAck()?.serialize(AllocationZone.SharedMemory) as? JvmBuffer
49 | }
50 |
51 | override fun awaitConnectivity(cb: MqttMessageCallback) {
52 | clientServer.scope.launch {
53 | cb.onMessage(clientServer.awaitConnectivity().serialize(AllocationZone.SharedMemory) as JvmBuffer)
54 | }
55 | }
56 |
57 | override fun pingCount(): Long {
58 | return clientServer.client.connectivityManager.processor.pingCount
59 | }
60 |
61 | override fun pingResponseCount(): Long {
62 | return clientServer.client.connectivityManager.processor.pingResponseCount
63 | }
64 |
65 | override fun connectionCount(): Long {
66 | return clientServer.client.connectivityManager.connectionCount
67 | }
68 |
69 | override fun connectionAttempts(): Long {
70 | return clientServer.client.connectivityManager.connectionAttempts
71 | }
72 |
73 | override fun sendDisconnect(cb: MqttCompletionCallback) = wrapResultWithCallback(cb) { clientServer.client.sendDisconnect() }
74 |
75 | override fun shutdown(
76 | sendDisconnect: Boolean,
77 | cb: MqttCompletionCallback,
78 | ) = wrapResultWithCallback(cb) {
79 | clientServer.shutdown(sendDisconnect)
80 | }
81 |
82 | private fun wrapResultWithCallback(
83 | callback: MqttCompletionCallback,
84 | block: suspend () -> Unit,
85 | ) {
86 | clientServer.scope.launch {
87 | try {
88 | block()
89 | callback.onSuccess()
90 | } catch (e: Exception) {
91 | callback.onError(e.message)
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/AndroidRemoteMqttServiceClient.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.os.IBinder
4 | import com.ditchoom.mqtt.client.LocalMqttService
5 | import com.ditchoom.mqtt.connection.MqttBroker
6 | import kotlin.coroutines.resume
7 | import kotlin.coroutines.suspendCoroutine
8 |
9 | class AndroidRemoteMqttServiceClient(binder: IBinder, service: LocalMqttService) :
10 | RemoteMqttServiceClient(service) {
11 | private val aidl = IPCMqttService.Stub.asInterface(binder)
12 | override val startAllCb: suspend () -> Unit =
13 | { suspendCoroutine { aidl.startAll(SuspendingMqttCompletionCallback("startAllCb", it)) } }
14 | override val startCb: suspend (Int, Byte) -> Unit = { brokerId, protocolVersion ->
15 | suspendCoroutine { aidl.start(brokerId, protocolVersion, SuspendingMqttCompletionCallback("startCb", it)) }
16 | }
17 | override val stopAllCb: suspend () -> Unit =
18 | { suspendCoroutine { aidl.stopAll(SuspendingMqttCompletionCallback("stopAllCb", it)) } }
19 | override val stopCb: suspend (Int, Byte) -> Unit = { brokerId, protocolVersion ->
20 | suspendCoroutine { aidl.stop(brokerId, protocolVersion, SuspendingMqttCompletionCallback("stopCb", it)) }
21 | }
22 |
23 | override suspend fun getClient(broker: MqttBroker): AndroidRemoteMqttClient? {
24 | val protocolVersion = broker.protocolVersion
25 | val brokerId = broker.brokerId
26 | val persistence = service.getPersistence(protocolVersion.toInt())
27 | persistence.brokerWithId(brokerId) ?: return null // validate broker still exists
28 | return suspendCoroutine {
29 | aidl.requestClientOrNull(
30 | brokerId,
31 | protocolVersion,
32 | object : MqttGetClientCallback.Stub() {
33 | override fun onClientReady(
34 | client: IPCMqttClient,
35 | brokerId: Int,
36 | protocolVersion: Byte,
37 | ) {
38 | it.resume(AndroidRemoteMqttClient(service.scope, client, broker, persistence))
39 | }
40 |
41 | override fun onClientNotFound() {
42 | it.resume(null)
43 | }
44 | },
45 | )
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/AndroidRemoteMqttServiceWorker.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.util.Log
4 | import com.ditchoom.mqtt.client.LocalMqttService
5 | import kotlinx.coroutines.launch
6 |
7 | class AndroidRemoteMqttServiceWorker(private val serviceServer: RemoteMqttServiceWorker) : IPCMqttService.Stub() {
8 | private val scope = serviceServer.service.scope
9 |
10 | constructor(service: LocalMqttService) : this(RemoteMqttServiceWorker(service))
11 |
12 | override fun startAll(callback: MqttCompletionCallback) =
13 | wrapResultWithCallback(callback) {
14 | serviceServer.startAll()
15 | }
16 |
17 | override fun start(
18 | brokerId: Int,
19 | protocolVersion: Byte,
20 | callback: MqttCompletionCallback,
21 | ) = wrapResultWithCallback(callback) { serviceServer.start(brokerId, protocolVersion) }
22 |
23 | override fun stopAll(callback: MqttCompletionCallback) =
24 | wrapResultWithCallback(callback) {
25 | serviceServer.stopAll()
26 | }
27 |
28 | override fun stop(
29 | brokerId: Int,
30 | protocolVersion: Byte,
31 | callback: MqttCompletionCallback,
32 | ) = wrapResultWithCallback(callback) { serviceServer.stop(brokerId, protocolVersion) }
33 |
34 | private fun wrapResultWithCallback(
35 | callback: MqttCompletionCallback,
36 | block: suspend () -> Unit,
37 | ) {
38 | scope.launch {
39 | try {
40 | block()
41 | callback.onSuccess()
42 | } catch (e: Exception) {
43 | Log.e("Remote Failure", "Failed to execute remote command: ", e)
44 | callback.onError(e.message)
45 | }
46 | }
47 | }
48 |
49 | override fun requestClientOrNull(
50 | brokerId: Int,
51 | protocolVersion: Byte,
52 | callback: MqttGetClientCallback,
53 | ) {
54 | scope.launch {
55 | val client = serviceServer.requestClientOrNull(brokerId, protocolVersion)
56 | client?.client?.allocateSharedMemory = true
57 | if (client != null) {
58 | callback.onClientReady(AndroidMqttClientIPCServer(client), brokerId, protocolVersion)
59 | } else {
60 | callback.onClientNotFound()
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/AndroidRemoteServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.content.Context
4 | import com.ditchoom.mqtt.client.MqttService
5 |
6 | actual suspend fun remoteMqttServiceWorkerClient(
7 | androidContextOrAbstractWorker: Any?,
8 | inMemory: Boolean,
9 | ): MqttService? = MqttServiceHelper.registerService(androidContextOrAbstractWorker as Context, inMemory)
10 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/MqttManagerService.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.app.Service
4 | import android.content.Intent
5 | import androidx.startup.AppInitializer
6 | import com.ditchoom.mqtt.client.LocalMqttService
7 | import com.ditchoom.mqtt.client.MqttServiceInitializer
8 | import kotlinx.coroutines.runBlocking
9 |
10 | class MqttManagerService : Service() {
11 | private lateinit var mqttService: LocalMqttService
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 | mqttService = AppInitializer.getInstance(this).initializeComponent(MqttServiceInitializer::class.java)
16 | }
17 |
18 | override fun onBind(intent: Intent) = AndroidRemoteMqttServiceWorker(mqttService)
19 |
20 | override fun onDestroy() {
21 | runBlocking {
22 | mqttService.shutdownAndCleanup()
23 | }
24 | super.onDestroy()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/MqttServiceHelper.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.ServiceConnection
7 | import android.os.IBinder
8 | import android.os.RemoteException
9 | import com.ditchoom.mqtt.MqttException
10 | import com.ditchoom.mqtt.client.LocalMqttService
11 | import com.ditchoom.mqtt.client.MqttService
12 | import com.ditchoom.mqtt.controlpacket.format.ReasonCode
13 | import kotlinx.coroutines.CancellableContinuation
14 | import kotlinx.coroutines.suspendCancellableCoroutine
15 | import kotlin.coroutines.resume
16 | import kotlin.coroutines.resumeWithException
17 |
18 | object MqttServiceHelper {
19 | private var serviceConnection: MqttServiceConnection? = null
20 | private var ipcClient: AndroidRemoteMqttServiceClient? = null
21 |
22 | suspend fun registerService(
23 | context: Context,
24 | inMemory: Boolean = false,
25 | ): MqttService {
26 | val ipcClient = ipcClient
27 | if (ipcClient != null) {
28 | return ipcClient
29 | }
30 | val i = Intent(context, MqttManagerService::class.java)
31 | context.startService(i)
32 | val serviceBinder =
33 | suspendCancellableCoroutine {
34 | val serviceConnection = MqttServiceConnection(context, it)
35 | if (!context.bindService(i, serviceConnection, Context.BIND_AUTO_CREATE)) {
36 | it.resumeWithException(RemoteException("Failed to allocate bind mqtt service"))
37 | }
38 | this.serviceConnection = serviceConnection
39 | }
40 | val c = AndroidRemoteMqttServiceClient(serviceBinder, LocalMqttService.buildService(context, inMemory))
41 | this.ipcClient = c
42 | return c
43 | }
44 |
45 | fun unregisterService(context: Context) {
46 | serviceConnection?.unbind(context)
47 | serviceConnection = null
48 | }
49 |
50 | class MqttServiceConnection(
51 | context: Context,
52 | private val cont: CancellableContinuation,
53 | ) : ServiceConnection {
54 | init {
55 | cont.invokeOnCancellation {
56 | unbind(context)
57 | }
58 | }
59 |
60 | override fun onServiceConnected(
61 | name: ComponentName,
62 | service: IBinder,
63 | ) {
64 | cont.resume(service)
65 | }
66 |
67 | override fun onServiceDisconnected(name: ComponentName) {
68 | cont.resumeWithException(
69 | MqttException(
70 | "Failed to connect to service $name",
71 | ReasonCode.NOT_AUTHORIZED.byte,
72 | ),
73 | )
74 | }
75 |
76 | fun unbind(context: Context) {
77 | context.unbindService(this)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidMain/kotlin/com/ditchoom/mqtt/client/ipc/SuspendingMqttCompletionCallback.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import android.os.RemoteException
4 | import kotlin.coroutines.Continuation
5 | import kotlin.coroutines.resume
6 | import kotlin.coroutines.resumeWithException
7 |
8 | class SuspendingMqttCompletionCallback(private val name: String, private val cont: Continuation) :
9 | MqttCompletionCallback.Stub() {
10 | override fun onSuccess() {
11 | cont.resume(Unit)
12 | }
13 |
14 | override fun onError(messageOrNull: String?) {
15 | cont.resumeWithException(RemoteException("Failed to run remote Mqtt Command $name $messageOrNull"))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/mqtt-client/src/androidUnitTest/kotlin/com/ditchoom/mqtt/client/net/Platform.mqtt.mqtt-client.unit.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.net
2 |
3 | actual fun getPlatform(): Platform = Platform.Android
4 |
--------------------------------------------------------------------------------
/mqtt-client/src/appleMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.mqtt.client.MqttService
4 |
5 | actual suspend fun remoteMqttServiceWorkerClient(
6 | androidContextOrAbstractWorker: Any?,
7 | inMemory: Boolean,
8 | ): MqttService? = null
9 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/BufferedControlPacketReader.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.buffer.ReadBuffer.Companion.EMPTY_BUFFER
5 | import com.ditchoom.data.Reader
6 | import com.ditchoom.mqtt.MalformedInvalidVariableByteInteger
7 | import com.ditchoom.mqtt.controlpacket.ControlPacket
8 | import com.ditchoom.mqtt.controlpacket.ControlPacketFactory
9 | import com.ditchoom.mqtt.controlpacket.IDisconnectNotification
10 | import com.ditchoom.socket.SuspendingSocketInputStream
11 | import kotlinx.coroutines.flow.flow
12 | import kotlin.experimental.and
13 | import kotlin.time.Duration
14 |
15 | class BufferedControlPacketReader(
16 | private val brokerId: Int,
17 | private val factory: ControlPacketFactory,
18 | readTimeout: Duration,
19 | private val reader: Reader,
20 | var observer: Observer? = null,
21 | private var incomingMessage: (UByte, Int, ReadBuffer) -> Unit,
22 | ) {
23 | private val inputStream = SuspendingSocketInputStream(readTimeout, reader)
24 | val incomingControlPackets =
25 | flow {
26 | try {
27 | while (reader.isOpen()) {
28 | try {
29 | val p = readControlPacket()
30 | emit(p)
31 | if (p is IDisconnectNotification) {
32 | return@flow
33 | }
34 | } catch (e: Exception) {
35 | return@flow
36 | }
37 | }
38 | } finally {
39 | incomingMessage = { _, _, _ -> }
40 | observer?.onReaderClosed(brokerId, factory.protocolVersion.toByte())
41 | }
42 | }
43 |
44 | fun isOpen() = reader.isOpen()
45 |
46 | internal suspend fun readControlPacket(): ControlPacket {
47 | val byte1 = inputStream.readUnsignedByte()
48 | observer?.readFirstByteFromStream(brokerId, factory.protocolVersion.toByte())
49 | val remainingLength = readVariableByteInteger()
50 | val buffer =
51 | if (remainingLength < 1) {
52 | EMPTY_BUFFER
53 | } else {
54 | inputStream.readBuffer(remainingLength)
55 | }
56 | val packet =
57 | factory.from(
58 | buffer,
59 | byte1,
60 | remainingLength,
61 | )
62 | buffer.resetForRead()
63 | incomingMessage(byte1, remainingLength, buffer)
64 | observer?.incomingPacket(brokerId, factory.protocolVersion.toByte(), packet)
65 | return packet
66 | }
67 |
68 | private suspend fun readVariableByteInteger(): Int {
69 | var digit: Byte
70 | var value = 0L
71 | var multiplier = 1L
72 | var count = 0L
73 | try {
74 | do {
75 | digit = inputStream.readByte()
76 | count++
77 | value += (digit and 0x7F).toLong() * multiplier
78 | multiplier *= 128
79 | } while ((digit and 0x80.toByte()).toInt() != 0)
80 | } catch (e: Exception) {
81 | throw MalformedInvalidVariableByteInteger(value.toInt())
82 | }
83 | val variableByteIntMax = 268435455L
84 | if (value < 0 || value > variableByteIntMax) {
85 | throw MalformedInvalidVariableByteInteger(value.toInt())
86 | }
87 | return value.toInt()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/ControlPacketHelper.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.buffer.AllocationZone
4 | import com.ditchoom.buffer.PlatformBuffer
5 | import com.ditchoom.buffer.allocate
6 | import com.ditchoom.mqtt.controlpacket.ControlPacket
7 |
8 | fun ControlPacket.toBuffer(zone: AllocationZone = AllocationZone.Direct) = listOf(this).toBuffer(zone)
9 |
10 | fun Collection.toBuffer(zone: AllocationZone = AllocationZone.Direct): PlatformBuffer {
11 | val packetSize =
12 | fold(0) { currentPacketSize, controlPacket ->
13 | currentPacketSize + controlPacket.packetSize()
14 | }
15 | return fold(PlatformBuffer.allocate(packetSize, zone)) { buffer, controlPacket ->
16 | controlPacket.serialize(buffer)
17 | buffer
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/ControlPacketOperation.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.mqtt.controlpacket.IPublishAcknowledgment
4 | import com.ditchoom.mqtt.controlpacket.IPublishComplete
5 | import com.ditchoom.mqtt.controlpacket.IPublishMessage
6 | import com.ditchoom.mqtt.controlpacket.IPublishReceived
7 | import com.ditchoom.mqtt.controlpacket.ISubscribeAcknowledgement
8 | import com.ditchoom.mqtt.controlpacket.ISubscription
9 | import com.ditchoom.mqtt.controlpacket.IUnsubscribeAcknowledgment
10 | import kotlinx.coroutines.Deferred
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.FlowCollector
13 | import kotlinx.coroutines.flow.combine
14 |
15 | sealed interface PublishOperation {
16 | object QoSAtMostOnceComplete : PublishOperation
17 |
18 | data class QoSAtLeastOnce(val packetId: Int, val pubAck: Deferred) : PublishOperation {
19 | override suspend fun awaitAll(): QoSAtLeastOnce {
20 | pubAck.await()
21 | return this
22 | }
23 | }
24 |
25 | data class QoSExactlyOnce(
26 | val packetId: Int,
27 | val pubRec: Deferred,
28 | val pubComp: Deferred,
29 | ) : PublishOperation {
30 | override suspend fun awaitAll(): QoSExactlyOnce {
31 | kotlinx.coroutines.awaitAll(pubRec, pubComp)
32 | return this
33 | }
34 | }
35 |
36 | suspend fun awaitAll(): PublishOperation = this
37 | }
38 |
39 | data class SubscribeOperation(
40 | val packetId: Int,
41 | val subscriptions: Map>,
42 | val subAck: Deferred,
43 | ) : Flow {
44 | override suspend fun collect(collector: FlowCollector) {
45 | combine(subscriptions.values.asIterable()) { array ->
46 | array.forEach { collector.emit(it) }
47 | }
48 | }
49 | }
50 |
51 | data class UnsubscribeOperation(val packetId: Int, val unsubAck: Deferred)
52 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/MqttClient.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.mqtt.connection.MqttBroker
5 | import com.ditchoom.mqtt.controlpacket.ControlPacketFactory
6 | import com.ditchoom.mqtt.controlpacket.IConnectionAcknowledgment
7 | import com.ditchoom.mqtt.controlpacket.IPublishMessage
8 | import com.ditchoom.mqtt.controlpacket.ISubscribeRequest
9 | import com.ditchoom.mqtt.controlpacket.ISubscription
10 | import com.ditchoom.mqtt.controlpacket.IUnsubscribeRequest
11 | import com.ditchoom.mqtt.controlpacket.QualityOfService
12 | import com.ditchoom.mqtt.controlpacket.Topic
13 | import kotlinx.coroutines.flow.Flow
14 |
15 | interface MqttClient {
16 | val packetFactory: ControlPacketFactory
17 | val broker: MqttBroker
18 |
19 | suspend fun currentConnectionAcknowledgment(): IConnectionAcknowledgment?
20 |
21 | suspend fun awaitConnectivity(): IConnectionAcknowledgment
22 |
23 | suspend fun pingCount(): Long
24 |
25 | suspend fun pingResponseCount(): Long
26 |
27 | suspend fun publish(
28 | topicName: String,
29 | qos: QualityOfService = QualityOfService.AT_MOST_ONCE,
30 | payload: ReadBuffer? = null,
31 | retain: Boolean = false,
32 | ): PublishOperation =
33 | publish(
34 | packetFactory.publish(
35 | topicName = Topic.fromOrThrow(topicName, Topic.Type.Name),
36 | qos = qos,
37 | retain = retain,
38 | payload = payload,
39 | ),
40 | )
41 |
42 | suspend fun publish(pub: IPublishMessage): PublishOperation
43 |
44 | fun observe(filter: Topic): Flow
45 |
46 | suspend fun subscribe(
47 | topicFilter: String,
48 | maxQos: QualityOfService,
49 | ): SubscribeOperation = subscribe(packetFactory.subscribe(Topic.fromOrThrow(topicFilter, Topic.Type.Filter), maxQos))
50 |
51 | suspend fun subscribe(subscriptions: Set): SubscribeOperation =
52 | subscribe(
53 | packetFactory.subscribe(subscriptions),
54 | )
55 |
56 | suspend fun subscribe(sub: ISubscribeRequest): SubscribeOperation
57 |
58 | suspend fun unsubscribe(topicFilter: String): UnsubscribeOperation =
59 | unsubscribe(packetFactory.unsubscribe(Topic.fromOrThrow(topicFilter, Topic.Type.Filter)))
60 |
61 | suspend fun unsubscribe(subscriptions: Set): UnsubscribeOperation =
62 | unsubscribe(
63 | packetFactory.unsubscribe(subscriptions),
64 | )
65 |
66 | suspend fun unsubscribe(unsub: IUnsubscribeRequest): UnsubscribeOperation
67 |
68 | suspend fun sendDisconnect()
69 |
70 | suspend fun shutdown(sendDisconnect: Boolean = true)
71 |
72 | suspend fun connectionCount(): Long
73 |
74 | suspend fun connectionAttempts(): Long
75 | }
76 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/MqttService.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.mqtt.client.ipc.remoteMqttServiceWorkerClient
4 | import com.ditchoom.mqtt.connection.MqttBroker
5 | import com.ditchoom.mqtt.connection.MqttConnectionOptions
6 | import com.ditchoom.mqtt.controlpacket.IConnectionRequest
7 |
8 | interface MqttService {
9 | suspend fun addBroker(
10 | connectionOps: Collection,
11 | connectionRequest: IConnectionRequest,
12 | ): MqttBroker
13 |
14 | suspend fun allBrokers(): Collection
15 |
16 | suspend fun removeBroker(
17 | brokerId: Int,
18 | protocolVersion: Byte,
19 | )
20 |
21 | suspend fun addBrokerAndStartClient(
22 | connectionOps: MqttConnectionOptions,
23 | connectionRequest: IConnectionRequest,
24 | ): MqttClient {
25 | val broker = addBroker(listOf(connectionOps), connectionRequest)
26 | start(broker)
27 | return checkNotNull(getClient(broker))
28 | }
29 |
30 | suspend fun addBrokerAndStartClient(
31 | connectionOps: Collection,
32 | connectionRequest: IConnectionRequest,
33 | ): MqttClient {
34 | val broker = addBroker(connectionOps, connectionRequest)
35 | start(broker)
36 | return checkNotNull(getClient(broker))
37 | }
38 |
39 | suspend fun getClient(broker: MqttBroker): MqttClient?
40 |
41 | suspend fun start(broker: MqttBroker)
42 |
43 | suspend fun start()
44 |
45 | suspend fun stop()
46 |
47 | suspend fun stop(broker: MqttBroker)
48 |
49 | companion object {
50 | suspend fun buildNewService(
51 | ipcEnabled: Boolean,
52 | androidContextOrAbstractWorker: Any? = null,
53 | inMemory: Boolean = false,
54 | ): MqttService {
55 | var serviceFound: MqttService? = null
56 | if (ipcEnabled) {
57 | serviceFound = remoteMqttServiceWorkerClient(androidContextOrAbstractWorker, inMemory)
58 | }
59 | if (serviceFound == null) {
60 | return LocalMqttService.buildService(androidContextOrAbstractWorker, inMemory)
61 | }
62 |
63 | return serviceFound
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/Observer.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.mqtt.connection.MqttConnectionOptions
4 | import com.ditchoom.mqtt.controlpacket.ControlPacket
5 | import com.ditchoom.mqtt.controlpacket.IConnectionRequest
6 | import kotlin.time.Duration
7 |
8 | // TODO: Reorganize and clean this up
9 | interface Observer {
10 | fun incomingPacket(
11 | brokerId: Int,
12 | protocolVersion: Byte,
13 | packet: ControlPacket,
14 | )
15 |
16 | fun wrotePackets(
17 | brokerId: Int,
18 | protocolVersion: Byte,
19 | controlPackets: Collection,
20 | )
21 |
22 | fun openSocketSession(
23 | brokerId: Int,
24 | protocolVersion: Byte,
25 | connectionRequest: IConnectionRequest,
26 | connectionOp: MqttConnectionOptions,
27 | )
28 |
29 | fun onReaderClosed(
30 | brokerId: Int,
31 | protocolVersion: Byte,
32 | )
33 |
34 | fun shutdown(
35 | brokerId: Int,
36 | protocolVersion: Byte,
37 | )
38 |
39 | // Ping timer
40 | fun resetPingTimer(
41 | brokerId: Int,
42 | protocolVersion: Byte,
43 | )
44 |
45 | fun sendingPing(
46 | brokerId: Int,
47 | protocolVersion: Byte,
48 | )
49 |
50 | fun delayPing(
51 | brokerId: Int,
52 | protocolVersion: Byte,
53 | delayDuration: Duration,
54 | )
55 |
56 | fun cancelPingTimer(
57 | brokerId: Int,
58 | protocolVersion: Byte,
59 | )
60 |
61 | // Reconnection
62 | fun stopReconnecting(
63 | brokerId: Int,
64 | protocolVersion: Byte,
65 | endReason: ConnectivityManager.ConnectionEndReason,
66 | )
67 |
68 | fun reconnectAndResetTimer(
69 | brokerId: Int,
70 | protocolVersion: Byte,
71 | endReason: ConnectivityManager.ConnectionEndReason,
72 | )
73 |
74 | fun reconnectIn(
75 | brokerId: Int,
76 | protocolVersion: Byte,
77 | currentDelay: Duration,
78 | endReason: ConnectivityManager.ConnectionEndReason,
79 | )
80 |
81 | // TODO: Delete?
82 | fun readFirstByteFromStream(
83 | brokerId: Int,
84 | protocolVersion: Byte,
85 | )
86 |
87 | // TODO: Delete?
88 | fun connectOnceWriteChannelReceiveException(
89 | brokerId: Int,
90 | protocolVersion: Byte,
91 | e: Exception,
92 | )
93 |
94 | // TODO: Delete?
95 | fun connectOnceSocketSessionWriteException(
96 | brokerId: Int,
97 | protocolVersion: Byte,
98 | e: Exception,
99 | )
100 | }
101 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/UnavailableMqttServiceException.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client
2 |
3 | import com.ditchoom.mqtt.connection.MqttConnectionOptions
4 |
5 | class UnavailableMqttServiceException(connectionOptions: Collection, cause: Throwable? = null) :
6 | Exception(
7 | connectionOptions.joinToString(
8 | prefix = "Failed to connect to services:",
9 | postfix = (" " + cause?.message),
10 | ),
11 | cause,
12 | )
13 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteMqttClientWorker.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.buffer.ReadBuffer
4 | import com.ditchoom.mqtt.client.LocalMqttClient
5 | import com.ditchoom.mqtt.client.MqttService
6 | import com.ditchoom.mqtt.controlpacket.IConnectionAcknowledgment
7 | import com.ditchoom.mqtt.controlpacket.IPublishMessage
8 |
9 | class RemoteMqttClientWorker(
10 | private val service: MqttService,
11 | internal val client: LocalMqttClient,
12 | ) {
13 | internal val scope = client.scope
14 | internal val factory = client.packetFactory
15 | internal val observers = ArrayList<(Boolean, UByte, Int, ReadBuffer) -> Unit>()
16 |
17 | suspend fun currentConnack(): IConnectionAcknowledgment? = client.currentConnectionAcknowledgment()
18 |
19 | fun currentConnectionAck(): IConnectionAcknowledgment? = client.connectivityManager.currentConnack()
20 |
21 | suspend fun awaitConnectivity(): IConnectionAcknowledgment = client.awaitConnectivity()
22 |
23 | suspend fun onSubscribeQueued(packetId: Int) {
24 | client.sendQueuedSubscribeMessage(packetId)
25 | }
26 |
27 | suspend fun onPublishQueued(
28 | packetId: Int,
29 | buffer: ReadBuffer?,
30 | ) {
31 | try {
32 | val pub0 =
33 | buffer?.let {
34 | it.resetForRead()
35 | factory.from(it) as? IPublishMessage
36 | }
37 | client.sendQueuedPublishMessage(packetId, pub0)
38 | } catch (e: Exception) {
39 | e.printStackTrace()
40 | }
41 | }
42 |
43 | suspend fun onPublishQueued(
44 | packetId: Int,
45 | pub0: IPublishMessage?,
46 | ) {
47 | client.sendQueuedPublishMessage(packetId, pub0)
48 | }
49 |
50 | suspend fun onUnsubscribeQueued(packetId: Int) {
51 | client.sendQueuedUnsubscribeMessage(packetId)
52 | }
53 |
54 | suspend fun shutdown(sendDisconnect: Boolean) {
55 | client.shutdown(sendDisconnect)
56 | service.stop(client.broker)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteMqttServiceClient.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.mqtt.client.LocalMqttService
4 | import com.ditchoom.mqtt.client.MqttService
5 | import com.ditchoom.mqtt.connection.MqttBroker
6 | import com.ditchoom.mqtt.connection.MqttConnectionOptions
7 | import com.ditchoom.mqtt.controlpacket.IConnectionRequest
8 |
9 | abstract class RemoteMqttServiceClient(
10 | protected val service: LocalMqttService,
11 | protected open val startAllCb: suspend () -> Unit = {},
12 | protected open val startCb: suspend (Int, Byte) -> Unit = { _, _ -> },
13 | protected open val stopAllCb: suspend () -> Unit = {},
14 | protected open val stopCb: suspend (Int, Byte) -> Unit = { _, _ -> },
15 | ) : MqttService {
16 | protected val scope = service.scope
17 |
18 | override suspend fun addBroker(
19 | connectionOps: Collection,
20 | connectionRequest: IConnectionRequest,
21 | ): MqttBroker {
22 | val persistence = service.getPersistence(connectionRequest)
23 | return persistence.addBroker(connectionOps, connectionRequest)
24 | }
25 |
26 | override suspend fun allBrokers() = service.allBrokers()
27 |
28 | override suspend fun removeBroker(
29 | brokerId: Int,
30 | protocolVersion: Byte,
31 | ) {
32 | service.removeBroker(brokerId, protocolVersion)
33 | }
34 |
35 | override suspend fun start() {
36 | startAllCb()
37 | }
38 |
39 | override suspend fun start(broker: MqttBroker) {
40 | startCb(broker.brokerId, broker.protocolVersion)
41 | }
42 |
43 | override suspend fun stop() {
44 | stopAllCb()
45 | }
46 |
47 | override suspend fun stop(broker: MqttBroker) {
48 | stopCb(broker.brokerId, broker.protocolVersion)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteMqttServiceWorker.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.mqtt.client.LocalMqttService
4 | import com.ditchoom.mqtt.connection.MqttBroker
5 |
6 | class RemoteMqttServiceWorker(
7 | internal val service: LocalMqttService,
8 | ) {
9 | private val clients = HashMap>()
10 |
11 | init {
12 | service.incomingMessages = { broker, byte1, remaining, buffer ->
13 | clients[broker.connectionRequest.protocolVersion.toByte()]
14 | ?.get(broker.identifier)
15 | ?.observers?.forEach {
16 | it(true, byte1, remaining, buffer)
17 | }
18 | }
19 | service.sentMessages = { broker, buffer ->
20 | clients[broker.connectionRequest.protocolVersion.toByte()]
21 | ?.get(broker.identifier)
22 | ?.observers?.forEach {
23 | it(false, 0u, 0, buffer)
24 | }
25 | }
26 | }
27 |
28 | private suspend fun findBroker(
29 | brokerId: Int,
30 | protocolVersion: Byte,
31 | ): MqttBroker? {
32 | val persistence = service.getPersistence(protocolVersion)
33 | return persistence.brokerWithId(brokerId)
34 | }
35 |
36 | suspend fun startAll() {
37 | service.start()
38 | }
39 |
40 | suspend fun start(
41 | brokerId: Int,
42 | protocolVersion: Byte,
43 | ) {
44 | val brokerFound = findBroker(brokerId, protocolVersion) ?: return
45 | service.start(brokerFound)
46 | }
47 |
48 | suspend fun stop(
49 | brokerId: Int,
50 | protocolVersion: Byte,
51 | ) {
52 | val brokerFound = findBroker(brokerId, protocolVersion) ?: return
53 | service.stop(brokerFound)
54 | clients[protocolVersion]?.remove(brokerId)
55 | }
56 |
57 | suspend fun stopAll() {
58 | service.stop()
59 | clients.clear()
60 | }
61 |
62 | suspend fun requestClientOrNull(
63 | brokerId: Int,
64 | protocolVersion: Byte,
65 | ): RemoteMqttClientWorker? {
66 | val cached = clients[protocolVersion]?.get(brokerId)
67 | if (cached != null && !cached.client.isStopped()) {
68 | return cached
69 | }
70 | val persistence = service.getPersistence(protocolVersion)
71 | val broker = persistence.brokerWithId(brokerId) ?: return null
72 | val client = service.getClient(broker) ?: return null
73 | val ipcClient = RemoteMqttClientWorker(service, client)
74 | clients.getOrPut(protocolVersion) { HashMap() }[brokerId] = ipcClient
75 | return ipcClient
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.mqtt.client.MqttService
4 |
5 | expect suspend fun remoteMqttServiceWorkerClient(
6 | androidContextOrAbstractWorker: Any?,
7 | inMemory: Boolean,
8 | ): MqttService?
9 |
--------------------------------------------------------------------------------
/mqtt-client/src/commonTest/kotlin/com/ditchoom/mqtt/client/net/Platform.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.net
2 |
3 | expect fun getPlatform(): Platform
4 |
5 | enum class Platform {
6 | Android,
7 | NonAndroid,
8 | }
9 |
--------------------------------------------------------------------------------
/mqtt-client/src/jsMain/kotlin/com/ditchoom/mqtt/client/ipc/JsRemoteMqttServiceWorker.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.buffer.JsBuffer
4 | import kotlinx.coroutines.launch
5 | import org.w3c.dom.MessageEvent
6 | import org.w3c.dom.MessagePort
7 |
8 | class JsRemoteMqttServiceWorker(private val serviceServer: RemoteMqttServiceWorker) {
9 | private val scope = serviceServer.service.scope
10 | internal val mqttService = serviceServer.service
11 |
12 | fun processIncomingMessage(m: MessageEvent): MessagePort? {
13 | if (m.data == MESSAGE_IPC_MQTT_SERVICE_REGISTRATION) {
14 | val messagePort = m.ports[0]
15 | messagePort.onmessage = {
16 | val data = it.data.asDynamic()
17 | if (data[MESSAGE_TYPE_KEY] == MESSAGE_TYPE_REGISTER_CLIENT) {
18 | scope.launch {
19 | requestClientAndPostMessage(data, it.ports[0])
20 | }
21 | } else {
22 | processIncomingMessage(it)
23 | }
24 | }
25 | messagePort.postMessage(MESSAGE_IPC_MQTT_SERVICE_REGISTRATION_ACK)
26 | return messagePort
27 | }
28 | val obj = m.data.asDynamic()
29 | val brokerIdProtocolPair = readBrokerIdProtocolVersionMessage(obj)
30 | if (obj[MESSAGE_TYPE_KEY] == MESSAGE_TYPE_SERVICE_START_ALL) {
31 | serviceServer.service.scope.launch { serviceServer.startAll() }
32 | } else if (obj[MESSAGE_TYPE_KEY] == MESSAGE_TYPE_SERVICE_START && brokerIdProtocolPair != null) {
33 | val (brokerId, protocolVersion) = brokerIdProtocolPair
34 | serviceServer.service.scope.launch { serviceServer.start(brokerId, protocolVersion) }
35 | (m.target as MessagePort).postMessage(MESSAGE_TYPE_SERVICE_START_RESPONSE)
36 | } else if (obj[MESSAGE_TYPE_KEY] == MESSAGE_TYPE_SERVICE_STOP && brokerIdProtocolPair != null) {
37 | val (brokerId, protocolVersion) = brokerIdProtocolPair
38 | serviceServer.service.scope.launch { serviceServer.stop(brokerId, protocolVersion) }
39 | (m.target as MessagePort).postMessage(MESSAGE_TYPE_SERVICE_STOP_RESPONSE)
40 | } else if (obj[MESSAGE_TYPE_KEY] == MESSAGE_TYPE_SERVICE_STOP_ALL) {
41 | serviceServer.service.scope.launch { serviceServer.stopAll() }
42 | (m.target as MessagePort).postMessage(MESSAGE_TYPE_SERVICE_STOP_ALL_RESPONSE)
43 | }
44 | return null
45 | }
46 |
47 | private suspend fun requestClientAndPostMessage(
48 | obj: dynamic,
49 | port: MessagePort,
50 | ) {
51 | val (brokerId, protocolVersion) = readBrokerIdProtocolVersionMessage(obj) ?: return
52 | val client = serviceServer.requestClientOrNull(brokerId, protocolVersion)
53 | if (client != null || client?.client?.isStopped() == true) {
54 | val ipcClientServer = JsRemoteMqttClientWorker(client, port)
55 | ipcClientServer.registerOnMessageObserver()
56 | port.postMessage(
57 | buildBrokerIdProtocolVersionMessage(
58 | MESSAGE_TYPE_REGISTER_CLIENT_SUCCESS,
59 | brokerId,
60 | protocolVersion,
61 | ),
62 | )
63 | client.observers += { incoming, byte1, remaining, buffer ->
64 |
65 | val packetMessage =
66 | if (incoming) {
67 | sendIncomingControlPacketMessage(byte1, remaining, buffer as JsBuffer)
68 | } else {
69 | buildOutgoingControlPacketMessage(buffer as JsBuffer)
70 | }
71 | port.postMessage(packetMessage)
72 | }
73 | } else {
74 | port.postMessage(buildSimpleMessage(MESSAGE_TYPE_REGISTER_CLIENT_NOT_FOUND))
75 | port.close()
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/mqtt-client/src/jsMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteServiceFactory.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
2 |
3 | package com.ditchoom.mqtt.client.ipc
4 |
5 | import com.ditchoom.mqtt.client.MqttService
6 | import com.ditchoom.socket.NetworkCapabilities
7 | import com.ditchoom.socket.getNetworkCapabilities
8 | import org.w3c.dom.AbstractWorker
9 |
10 | actual suspend fun remoteMqttServiceWorkerClient(
11 | androidContextOrAbstractWorker: Any?,
12 | inMemory: Boolean,
13 | ): MqttService? =
14 | if (getNetworkCapabilities() == NetworkCapabilities.WEBSOCKETS_ONLY) {
15 | sendAndAwaitRegistration(androidContextOrAbstractWorker as AbstractWorker)
16 | } else {
17 | null
18 | }
19 |
--------------------------------------------------------------------------------
/mqtt-client/src/jsMain/kotlin/com/ditchoom/mqtt/client/ipc/WorkerHelper.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.mqtt.client.LocalMqttService
4 | import kotlinx.coroutines.flow.first
5 | import kotlinx.coroutines.flow.receiveAsFlow
6 | import org.w3c.dom.AbstractWorker
7 | import org.w3c.dom.MessageChannel
8 | import org.w3c.dom.SharedWorker
9 | import org.w3c.dom.Worker
10 | import org.w3c.workers.ServiceWorker
11 |
12 | private var worker: JsRemoteMqttServiceWorker? = null
13 |
14 | suspend fun buildMqttServiceIPCServer(useSharedMemory: Boolean): JsRemoteMqttServiceWorker {
15 | val workerTmp = worker
16 | if (workerTmp != null) {
17 | return workerTmp
18 | }
19 | val service = LocalMqttService.buildService(null)
20 | service.useSharedMemory = useSharedMemory
21 | val serviceServer = RemoteMqttServiceWorker(service)
22 | val workerLocal = JsRemoteMqttServiceWorker(serviceServer)
23 | worker = workerLocal
24 | return workerLocal
25 | }
26 |
27 | internal const val MESSAGE_IPC_MQTT_SERVICE_REGISTRATION = "mqtt-ipc-service-registration"
28 | internal const val MESSAGE_IPC_MQTT_SERVICE_REGISTRATION_ACK = "mqtt-ipc-service-registration-ack"
29 |
30 | suspend fun sendAndAwaitRegistration(worker: AbstractWorker): JsRemoteMqttServiceClient {
31 | val messageChannel = MessageChannel()
32 | when (worker) {
33 | is Worker -> {
34 | worker.postMessage(MESSAGE_IPC_MQTT_SERVICE_REGISTRATION, arrayOf(messageChannel.port2))
35 | }
36 |
37 | is ServiceWorker -> {
38 | worker.postMessage(MESSAGE_IPC_MQTT_SERVICE_REGISTRATION, arrayOf(messageChannel.port2))
39 | }
40 |
41 | is SharedWorker -> {
42 | worker.onerror = {
43 | console.error("worker message error", it)
44 | }
45 | worker.port.onmessage = {
46 | console.log("incoming shared worker message", it)
47 | }
48 | worker.port.postMessage(MESSAGE_IPC_MQTT_SERVICE_REGISTRATION, arrayOf(messageChannel.port2))
49 | }
50 | }
51 | val ipcServer = buildMqttServiceIPCServer(false)
52 | val client = JsRemoteMqttServiceClient(ipcServer.mqttService, messageChannel.port1)
53 | client.channel.receiveAsFlow().first { it.data == MESSAGE_IPC_MQTT_SERVICE_REGISTRATION_ACK }
54 | return client
55 | }
56 |
--------------------------------------------------------------------------------
/mqtt-client/src/jsTest/kotlin/com/ditchoom/mqtt/client/net/Platform.mqtt.mqtt-client.js.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.net
2 |
3 | actual fun getPlatform(): Platform = Platform.NonAndroid
4 |
--------------------------------------------------------------------------------
/mqtt-client/src/jvmMain/kotlin/com/ditchoom/mqtt/client/ipc/RemoteServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.ipc
2 |
3 | import com.ditchoom.mqtt.client.MqttService
4 |
5 | actual suspend fun remoteMqttServiceWorkerClient(
6 | androidContextOrAbstractWorker: Any?,
7 | inMemory: Boolean,
8 | ): MqttService? = null
9 |
--------------------------------------------------------------------------------
/mqtt-client/src/jvmTest/kotlin/com/ditchoom/mqtt/client/net/Platform.com.ditchoom.mqtt.mqtt-client.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.net
2 |
3 | actual fun getPlatform(): Platform = Platform.NonAndroid
4 |
--------------------------------------------------------------------------------
/mqtt-client/src/nativeTest/kotlin/com/ditchoom/mqtt/client/net/Platform.mqtt.mqtt-client.native.kt:
--------------------------------------------------------------------------------
1 | package com.ditchoom.mqtt.client.net
2 |
3 | actual fun getPlatform(): Platform = Platform.NonAndroid
4 |
--------------------------------------------------------------------------------
/mqtt-client/webpack.config.d/patch.js:
--------------------------------------------------------------------------------
1 | config.resolve.alias = {
2 | "net": false,
3 | "util": false,
4 | "tls": false,
5 | "crypto": false,
6 | }
7 | if (config.devServer != null) {
8 | config.devServer.headers = {
9 | "Cross-Origin-Opener-Policy": "same-origin",
10 | "Cross-Origin-Embedder-Policy": "require-corp"
11 | }
12 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | gradlePluginPortal()
5 | mavenCentral()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | }
8 |
9 | plugins {
10 | val kotlinVersion = extra["kotlin.version"] as String
11 | val androidGradleVersion = extra["agp.version"] as String
12 | val sqldelightVersion = extra["sqldelight.version"] as String
13 | // val composePluginVersion = extra["compose.plugin.version"] as String
14 | kotlin("multiplatform").version(kotlinVersion)
15 | kotlin("android").version(kotlinVersion)
16 | kotlin("cocoapods").version(kotlinVersion)
17 | kotlin("native.cocoapods").version(kotlinVersion)
18 | id("com.android.application").version(androidGradleVersion)
19 | id("com.android.library").version(androidGradleVersion)
20 | id("io.codearte.nexus-staging").version(extra["nexus-staging.version"] as String)
21 | id("org.jlleitschuh.gradle.ktlint").version(extra["ktlint.version"] as String)
22 | id("app.cash.sqldelight").version(sqldelightVersion)
23 | // id("org.jetbrains.compose").version(composePluginVersion)
24 | }
25 | }
26 |
27 | rootProject.name = "mqtt"
28 |
29 | plugins {
30 | id("com.gradle.develocity") version ("3.17.3")
31 | }
32 | develocity {
33 | buildScan {
34 | uploadInBackground.set(System.getenv("CI") != null)
35 | termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use")
36 | termsOfUseAgree.set("yes")
37 | }
38 | }
39 |
40 |
41 | include(
42 | "models-base",
43 | "models-v4",
44 | "models-v5",
45 | "mqtt-client",
46 | )
47 |
--------------------------------------------------------------------------------