├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------