Liu Song’s Projects


~/Projects/mqtt-ios

git clone https://code.lsong.org/mqtt-ios

Commit

Commit
71778bd3aebddb43992987eca2f247d6aced7b7a
Author
Philipp Arndt <[email protected]>
Date
2022-04-16 10:03:45 +0200 +0200
Diffstat
 .github/workflows/screenshots.yml | 27 
 .github/workflows/swift.yml | 33 
 .github/workflows/test.yml | 46 
 .gitignore | 4 
 Docs/examples/client-certs/README.md | 46 
 Docs/examples/client-certs/config/mosquitto/mosquitto.conf | 16 
 Docs/examples/client-certs/docker-compose.yml | 11 
 ci/prepare-screenshots.mjs | 4 
 ci/start-mosquitto.mjs | 1 
 create-screenshots.sh | 2 
 mqtt-stub-service/.gitignore | 3 
 mqtt-stub-service/config/mosquitto-noauth/config/mosquitto.conf | 8 
 mqtt-stub-service/config/mosquitto-userpass/config/mosquitto.conf | 11 
 mqtt-stub-service/config/mosquitto-userpass/config/passwd | 1 
 mqtt-stub-service/config/mosquitto/config/mosquitto.conf | 10 
 mqtt-stub-service/docker-compose.yaml | 75 
 publish.sh | 24 
 src/MQTTAnalyzer.xcodeproj/project.pbxproj | 148 
 src/MQTTAnalyzer.xcodeproj/xcshareddata/xcschemes/MQTTAnalyzer.xcscheme | 14 
 src/MQTTAnalyzer.xcworkspace/contents.xcworkspacedata | 16 
 src/MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved | 24 
 src/MQTTAnalyzer/AppDelegate.swift | 1 
 src/MQTTAnalyzer/Info.plist | 7 
 src/MQTTAnalyzer/extensions/Array+Move.swift | 45 
 src/MQTTAnalyzer/extensions/Array+Remove.swift | 18 
 src/MQTTAnalyzer/extensions/String+RegExp.swift | 33 
 src/MQTTAnalyzer/model/persistence/HostSettingExamples.swift | 15 
 src/MQTTAnalyzer/model/persistence/RealmPersistence.swift | 8 
 src/MQTTAnalyzer/model/persistence/StubPersistence.swift | 1 
 src/MQTTAnalyzer/model/v2/TopicTree.swift | 2 
 src/MQTTAnalyzer/mqtt/MqttClientSharedUtils.swift | 26 
 src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient+ErrorMessage.swift | 34 
 src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient.swift | 82 
 src/MQTTAnalyzer/views/about/AboutView.swift | 2 
 src/MQTTAnalyzer/views/common/CustomListView.swift | 2 
 src/MQTTAnalyzer/views/common/QuestionBox.swift | 22 
 src/MQTTAnalyzer/views/host/form/DisconnectedView.swift | 2 
 src/MQTTAnalyzer/views/host/form/LoginView.swift | 1 
 src/MQTTAnalyzer/views/host/form/auth/AuthenticationTypePicker.swift | 3 
 src/MQTTAnalyzer/views/host/form/auth/UsernamePasswordAuthenticationView.swift | 4 
 src/MQTTAnalyzer/views/host/form/aws-iot/AWSIOTHelpView.swift | 8 
 src/MQTTAnalyzer/views/host/form/aws-iot/HostFormModel+AWS.swift | 2 
 src/MQTTAnalyzer/views/host/form/client-certs/ClientCertsHelpView.swift | 47 
 src/MQTTAnalyzer/views/host/form/client-certs/HostFormModel+ClientCerts.swift | 29 
 src/MQTTAnalyzer/views/host/form/server/ProtocolPicker.swift | 3 
 src/MQTTAnalyzer/views/host/form/server/ServerFormView.swift | 8 
 src/MQTTAnalyzer/views/host/form/topic/SubscriptionDetailsView.swift | 3 
 src/MQTTAnalyzer/views/host/form/topic/TopicsFormView.swift | 6 
 src/MQTTAnalyzer/views/login/LoginDialog.swift | 2 
 src/MQTTAnalyzer/views/message-publish/PublishMessageFormView.swift | 4 
 src/MQTTAnalyzer/views/topic/FolderNavigationView.swift | 3 
 src/MQTTAnalyzerTests/AWSIOTPresetTests.swift | 7 
 src/MQTTAnalyzerTests/CocoaMQTTRegression.swift | 2 
 src/MQTTAnalyzerTests/Info.plist | 1 
 src/MQTTAnalyzerTests/MqttClientCocoaMQTTTests.swift | 35 
 src/MQTTAnalyzerTests/TreeModelTests.swift | 12 
 src/MQTTAnalyzerUITests/AbstractConfigurationTests.swift | 39 
 src/MQTTAnalyzerUITests/BrokerTests.swift | 2 
 src/MQTTAnalyzerUITests/ConfigurationMQTTTests.swift | 60 
 src/MQTTAnalyzerUITests/ConfigurationWebSocketTests.swift | 63 
 src/MQTTAnalyzerUITests/FlatViewTests.swift | 4 
 src/MQTTAnalyzerUITests/Info.plist | 1 
 src/MQTTAnalyzerUITests/PublishTests.swift | 20 
 src/MQTTAnalyzerUITests/ReadStateTests.swift | 20 
 src/MQTTAnalyzerUITests/ScreenshotTests.swift | 4 
 src/MQTTAnalyzerUITests/SearchTests.swift | 8 
 src/MQTTAnalyzerUITests/SubscriptionTests.swift | 28 
 src/MQTTAnalyzerUITests/TestServer.swift | 15 
 src/MQTTAnalyzerUITests/extensions/XCTestCase+Disappear.swift | 3 
 src/MQTTAnalyzerUITests/extensions/XCUIElement+ClearText.swift | 61 
 src/MQTTAnalyzerUITests/utils/Broker.swift | 17 
 src/MQTTAnalyzerUITests/utils/Brokers.swift | 153 
 src/MQTTAnalyzerUITests/utils/ExampleMessages.swift | 43 
 src/MQTTAnalyzerUITests/utils/Navigation.swift | 9 
 src/MQTTAnalyzerUITests/utils/PublishDialog.swift | 2 
 src/Podfile | 12 
 src/Podfile.lock | 36 
 src/ReleaseTestPlan.xctestplan | 31 
 src/TestPlan.xctestplan | 42 
 src/TestPlanThreads.xctestplan | 35 
 src/UnitTestPlan.xctestplan | 24 
 src/fastlane/Fastfile | 6 
 src/fastlane/Scanfile | 2 
 src/fastlane/Snapfile | 3 
 src/fastlane/SnapshotHelper.swift | 2 

Apple network (#137)

* test on iPad mini

* CocoaMQTT update

* error handling

* Package.resolve v2 fix

* mac first as it requires user interaction

* fix test failure with new CocoaMQTT version

* Create scan.yml

* naming

* task name

* update dependency list

* Update scan.yml

* Update screenshots.yml

* Update scan.yml

* Update scan.yml

* fix test case on older iOS version

* rename action

* websocket test case

* clean-up

* improve error message

* support topics with empty path names

https://github.com/philipparndt/mqtt-analyzer/issues/138

* support topics with empty path names

https://github.com/philipparndt/mqtt-analyzer/issues/138

* start mosquitto

* fix indent

* fix indent

* test with more configurations

* use test server instead of localhost

* clean-up connection view

* more test cases / delete dead code

* split test cases to run more tests in parallel

* fix connect data race

* remove topic limit for test cases

* run only unit tests, integration tests are running during deployment

* add more output

* specify test target

* improve screenshot performance

* use local container for integration tests

* fix typo

* introduce test plans

* topic # is not a good suggestion

* introduce test plans

* introduce test plans

* fixed font

* introduce test plans

* pod update

* introduce test plans

* update new test cases for macOS

* add client certificates docu

* fix test case


diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml
index bee5e96b9f1258fe29651af7e6c5142e73d1b6c3..f36acf1d3f4a7bb50bb680b78e523a9d3822384e 100644
--- a/.github/workflows/screenshots.yml
+++ b/.github/workflows/screenshots.yml
@@ -10,21 +11,19 @@     steps:
     - uses: actions/checkout@master
 
 name: 'Create screenshots'
+  screenshots:
 name: 'Create screenshots'
-name: 'Create screenshots'
+    runs-on: macOS-latest
+        
-        npm install -g zx
+    # No support for Package.resolve v2 on GitHub
-name: 'Create screenshots'
 on: 
+    steps:
 name: 'Create screenshots'
-  workflow_dispatch:
+    - uses: actions/checkout@master
       run: |
-        ci/start-mosquitto.mjs
+        rm ./MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved
 
 name: 'Create screenshots'
-  screenshots:
-      run: sudo xcode-select -s /Applications/Xcode_13.2.app
-    
-name: 'Create screenshots'
     steps:
       working-directory: src
       run: |
@@ -39,15 +38,16 @@         brew update 
         brew install imagemagick
       shell: bash
 
-    - name: Create screenshots
+    # No support for Package.resolve v2 on GitHub
+    - name: Remove Package.resolve 
+      working-directory: src
       run: |
-        bash create-screenshots.sh
+        rm ./MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved
 
 
-    steps:
-      if: always()
+  screenshots:
       run: |
-        ci/stop-mosquitto.mjs
+        bash create-screenshots.sh
 
     - uses: actions/upload-artifact@v3
       with:




diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
index 4280d1d48c74d345e168e5c4618b1b3534ba3f8d..d6dc248fbdb786e105a58198056e901ac65d2368 100644
--- a/.github/workflows/swift.yml
+++ b/.github/workflows/swift.yml
@@ -14,33 +14,37 @@     steps:
     - uses: actions/checkout@master
     
 name: 'Build'
-  workflow_dispatch:
+  test:
+      run: sudo xcode-select -s /Applications/Xcode_13.2.app
-      run: |
+
+
 name: 'Build'
-    paths:
 
-    - name: Install Mosquitto
+
       run: |
-        ci/start-mosquitto.mjs
+        pod repo update
 
-    - name: Set XCode Version
+  workflow_dispatch:
 
+  push:
     
+  workflow_dispatch:
 
-name: 'Build'
+    - name: Remove Package.resolve 
       working-directory: src
       run: |
-        pod repo update
-
+  workflow_dispatch:
   workflow_dispatch:
 
-  push:
-    
     - name: iOS Tests
       working-directory: src
       run: |
-
+        xcodebuild test -enableCodeCoverage YES \
+        -workspace MQTTAnalyzer.xcworkspace \
+  workflow_dispatch:
       - 'src/**'
+        -testPlan UnitTestPlan \
+        -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.2'
 
     - name: Upload test results
       uses: actions/upload-artifact@v3
@@ -52,8 +57,3 @@     #- name: macOS Tests
     #  working-directory: src
     #  run: |
     #    fastlane mac tests
-
-    - name: Stop Mosquitto
-      if: always()
-      run: |
-        ci/stop-mosquitto.mjs




diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5c7eef5c66cd3e631a544cd3c7c8dd2896e532dd..5563d9b2ab5c7a8d6ac0288f15bf5ca5ca295685 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,42 +3,48 @@ on:
    workflow_dispatch:
 
 jobs:
+  test:
+on:
    test:
-     runs-on: macOS-latest
+
-
+    steps:
+on:
      steps:
+on:
      - uses: actions/checkout@master
+    - name: Set XCode Version
+      run: sudo xcode-select -s /Applications/Xcode_13.2.app
 
      # No support for Package.resolve v2 on GitHub
-     - name: Remove Package.resolve 
-name: 'Test'
+   workflow_dispatch:
 on:
-name: 'Test'
+   workflow_dispatch:
    workflow_dispatch:
-name: 'Test'
+   workflow_dispatch:
 
-
-name: 'Test'
+   workflow_dispatch:
 jobs:
-       working-directory: src
+
-name: 'Test'
    workflow_dispatch:
-name: 'Test'
    test:
-name: 'Test'
+      working-directory: src
+      run: |
+   workflow_dispatch:
      runs-on: macOS-latest
-name: 'Test'
+   workflow_dispatch:
      steps:
+      shell: bash
 
-     - name: Test
+    - name: Test
-name: 'Test'
+   workflow_dispatch:
    workflow_dispatch:
-         cd src
+      run: |
-on:
+
 name: 'Test'
-on:
+
 on:
-on:
+
    workflow_dispatch:
-on:
 
+
+        -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.2'




diff --git a/.gitignore b/.gitignore
index ac9e6d8911e254030e213296c4f920c5ac5c052c..e9e558a2952d183bc616060841924fe7555e2baf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,5 +33,9 @@ *.hmap
 *.ipa
 
 ### Xcode ###
+### Xcode ###
+.idea/
+
+### Xcode ###
 # Created by http://www.gitignore.io
 Pods
\ No newline at end of file




diff --git a/Docs/examples/client-certs/README.md b/Docs/examples/client-certs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..dcebe71686692e1b62990393053aa2016e49a9e7
--- /dev/null
+++ b/Docs/examples/client-certs/README.md
@@ -0,0 +1,46 @@
+# Connect with client certificates
+
+This is an example on how to set-up client certificates.
+To use this example you need to create self-singed certificates first.
+Open the ./config/mosquitto folder in your terminal and run the following commands:
+
+### Create root CA:
+```sh
+openssl genrsa -des3 -out ca.key 2048 # password
+openssl req -new -x509 -days 1826 -key ca.key -out ca.crt
+```
+
+### Create broker certificate:
+```sh
+openssl genrsa -out broker.key 2048
+openssl req -new -out broker.csr -key broker.key
+openssl x509 -req -in broker.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out broker.crt -days 360
+```
+
+### Create client certificate:
+```sh
+openssl genrsa -out client.key 2048
+openssl req -new -out client.csr -key client.key
+openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 360
+```
+
+### Create P12 file:
+```sh
+openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12
+```
+
+### Start the container:
+```sh
+docker compose up -d
+``` 
+
+### In MQTTAnalyzer:
+Use the P12 file in MQTTAnalyzer to connect to the broker.
+The settings are:
+
+- Port: 8883
+- SSL: `true`
+- Allow untrusted: `true`
+- Authentication: `Certificate`
+- Client PKCS#12: `client.p12`
+- Password: `password`
\ No newline at end of file




diff --git a/Docs/examples/client-certs/config/mosquitto/mosquitto.conf b/Docs/examples/client-certs/config/mosquitto/mosquitto.conf
new file mode 100644
index 0000000000000000000000000000000000000000..5128c2fedeb321a43c5cba832bf922a4d9a90645
--- /dev/null
+++ b/Docs/examples/client-certs/config/mosquitto/mosquitto.conf
@@ -0,0 +1,16 @@
+# Per Listener Settings
+per_listener_settings true
+# log_type error
+# log_type warning
+# log_type notice
+log_type all
+
+# Secured Listener
+listener 8883
+cafile /mosquitto/config/ca.crt
+keyfile /mosquitto/config/broker.key
+certfile /mosquitto/config/broker.crt
+require_certificate true
+use_identity_as_username true
+allow_anonymous false
+#log_type info




diff --git a/Docs/examples/client-certs/docker-compose.yml b/Docs/examples/client-certs/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1cc2d625e0d79db979e3d71803b2a253b76fb0aa
--- /dev/null
+++ b/Docs/examples/client-certs/docker-compose.yml
@@ -0,0 +1,11 @@
+version: '3'
+services:
+  mosquitto:
+    image: eclipse-mosquitto
+    hostname: mosquitto
+    expose:
+      - "8883"
+    ports:
+      - "8883:8883"
+    volumes:
+      - ./config/mosquitto:/mosquitto/config




diff --git a/ci/prepare-screenshots.mjs b/ci/prepare-screenshots.mjs
index 68f85338bbd8500255bbda6f051fe2a85cdd5015..d8b106d91fb078fe594f18bd2defed0500ccd79f 100755
--- a/ci/prepare-screenshots.mjs
+++ b/ci/prepare-screenshots.mjs
@@ -2,11 +2,11 @@ #!/usr/bin/env zx
 
 const devices = [
     "iPhone 13 Pro", 
-    "iPhone SE (3rd generation)"", 
+    "iPhone SE (3rd generation)",
     "iPad Pro (11-inch) (3rd generation)"
     //, "iPad Air (3rd generation)"
 ]
-const papp = process.env["APPEARENCE"]
+const papp = process.env["APPEARANCE"]
 const appearance=papp ?? "dark" //  'dark' or 'light'
 
 console.log(`---- Setting appearance to ${papp} ----`)




diff --git a/ci/start-mosquitto.mjs b/ci/start-mosquitto.mjs
index 4d0c514e29e9d8db669c5befb3da84ea96015489..35616a9850aa8ec27a52642e316bed415c012c39 100755
--- a/ci/start-mosquitto.mjs
+++ b/ci/start-mosquitto.mjs
@@ -1,6 +1,7 @@
 #!/usr/bin/env zx
 try {
     await $`brew install mosquitto`
+    await $`cp ./mqtt-stub-service/config/mosquitto/config/mosquitto.conf /usr/local/etc/mosquitto/mosquitto.conf`
     await $`brew services start mosquitto`
 }
 catch (e) {




diff --git a/create-screenshots.sh b/create-screenshots.sh
index 514e48da75c046d929a9934c088a300dab489882..fa484ab0dd032341842c919a2ff196b5e4fecbc3 100755
--- a/create-screenshots.sh
+++ b/create-screenshots.sh
@@ -11,7 +11,7 @@ array=( dark light )
 for i in "${array[@]}"
 do
     mkdir "./screenshots/$i"
-#!/bin/bash
+cd "$(dirname)"
 cd "$(dirname)"
     ./ci/prepare-screenshots.mjs
 




diff --git a/mqtt-stub-service/.gitignore b/mqtt-stub-service/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..8e57aa04825441958f88bf3f4fb3eb82bc91a72c
--- /dev/null
+++ b/mqtt-stub-service/.gitignore
@@ -0,0 +1,3 @@
+.env
+traefik/
+acme.json
\ No newline at end of file




diff --git a/mqtt-stub-service/config/mosquitto/config/mosquitto.conf b/mqtt-stub-service/config/mosquitto/config/mosquitto.conf
deleted file mode 100644
index b588b8e976547b118a863980e62ed10d5dccf970..0000000000000000000000000000000000000000
--- a/mqtt-stub-service/config/mosquitto/config/mosquitto.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-log_type all
-log_facility 5
-
-port 1883
-
-allow_anonymous true
-#password_file /mosquitto/config/mosquitto.password
-
-listener 9001
-protocol websockets
\ No newline at end of file




diff --git a/mqtt-stub-service/config/mosquitto-noauth/config/mosquitto.conf b/mqtt-stub-service/config/mosquitto-noauth/config/mosquitto.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2a2d49758f5d9b9cffd733087683658fef1a19b0
--- /dev/null
+++ b/mqtt-stub-service/config/mosquitto-noauth/config/mosquitto.conf
@@ -0,0 +1,8 @@
+log_type all
+log_facility 5
+allow_anonymous true
+
+listener 1883
+
+listener 9001
+protocol websockets




diff --git a/mqtt-stub-service/config/mosquitto-userpass/config/mosquitto.conf b/mqtt-stub-service/config/mosquitto-userpass/config/mosquitto.conf
new file mode 100644
index 0000000000000000000000000000000000000000..70519f59170941336791bacf6b1aa3f76f5bb3b6
--- /dev/null
+++ b/mqtt-stub-service/config/mosquitto-userpass/config/mosquitto.conf
@@ -0,0 +1,11 @@
+log_type all
+log_facility 5
+
+allow_anonymous false
+password_file /mosquitto/config/passwd
+
+listener 1884
+protocol mqtt
+
+listener 9002
+protocol websockets
\ No newline at end of file




diff --git a/mqtt-stub-service/config/mosquitto-userpass/config/passwd b/mqtt-stub-service/config/mosquitto-userpass/config/passwd
new file mode 100644
index 0000000000000000000000000000000000000000..ef153a901a501072810099b830db7b36e460f5dc
--- /dev/null
+++ b/mqtt-stub-service/config/mosquitto-userpass/config/passwd
@@ -0,0 +1 @@
+admin:$6$ZSu9Ickl6AHFMax7$lvXOnS+Hjx1UBScqAup1O3WcReuQcyV1ZnL5svFXvWGxkUnzE8pquy4iuxdOMg3MACPDwiDXpwlNYDldPKeDgA==




diff --git a/mqtt-stub-service/docker-compose.yaml b/mqtt-stub-service/docker-compose.yaml
index d79ed739f34c4261f71f133dd124dcaa97a889c9..4f0590216444939aaec9878d83236c37c0e8530f 100644
--- a/mqtt-stub-service/docker-compose.yaml
+++ b/mqtt-stub-service/docker-compose.yaml
@@ -1,8 +1,83 @@
 version: '3'
 services:
+  traefik:
+    image: traefik:v2.6
+    restart: "no"
+
+version: '3'
   mosquitto:
+version: '3'
     image: eclipse-mosquitto
+version: '3'
     volumes:
+version: '3'
       - ./config/mosquitto/config:/mosquitto/config:rw    
+version: '3'
     ports:
+version: '3'
       - "1883:1883"
+
+      # Entrypoints
+      - "--entrypoints.websecure.address=:443"
+      - "--entrypoints.mqtts.address=:8883"
+
+      # Redirections
+      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
+      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
+
+      # TLS
+      - "--entrypoints.websecure.http.tls.certResolver=letsencrypt"
+      - "--certificatesResolvers.letsencrypt.acme.email=${MAIL}"
+      - "--certificatesresolvers.letsencrypt.acme.preferredChain='ISRG Root X1'"
+      - "--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json"
+      - "--certificatesResolvers.letsencrypt.acme.keyType=EC384"
+      - "--certificatesResolvers.letsencrypt.acme.dnsChallenge.provider=ionos"
+
+      # Providers
+      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
+      - "--providers.docker.exposedByDefault=false"
+      - "--providers.file.directory=/configurations"
+      - "--providers.file.watch=true"
+
+    ports:
+      # Web server
+      - 443:443
+      # MQTT
+      - 8883:8883
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+      - ./config/traefik/configs:/configurations
+      - ./config/traefik/acme:/etc/traefik/acme
+
+    environment:
+      - IONOS_API_KEY=${IONOS_API_KEY}
+
+  mosquitto-no-auth:
+    image: eclipse-mosquitto
+    volumes:
+      - ./config/mosquitto-noauth/config:/mosquitto/config:rw
+    ports:
+      - "1883:1883"
+      - "9001:9001"
+
+    labels:
+      - traefik.enable=true
+
+      - "traefik.http.routers.websecure.rule=Host(`${MQTT_HOST}`)"
+      - "traefik.http.routers.websecure.entrypoints=websecure"
+      - "traefik.http.routers.websecure.tls.certresolver=letsencrypt"
+      - "traefik.http.services.websecure.loadbalancer.server.port=9001"
+
+      - traefik.tcp.routers.mqtts.rule=HostSNI(`${MQTT_HOST}`)
+      - traefik.tcp.routers.mqtts.entrypoints=mqtts
+      - traefik.tcp.routers.mqtts.tls.certresolver=letsencrypt
+      - traefik.tcp.routers.mqtts.service=mqtts
+      - traefik.tcp.services.mqtts.loadBalancer.server.port=1883
+
+  mosquitto-user-apssword-auth:
+    image: eclipse-mosquitto
+    volumes:
+      - ./config/mosquitto-userpass/config:/mosquitto/config:rw
+    ports:
+      - "1884:1884"
+      - "9002:9002"




diff --git a/publish.sh b/publish.sh
index 35fe54139816a10d1fb976aa8714450c66d48a4b..7b283cdaf6babb302b1e33c98ffc01085fab4d01 100755
--- a/publish.sh
+++ b/publish.sh
@@ -7,20 +7,19 @@ pushd ci
     zx realm-headers.mjs undo
 popd 
 
-### Create macOS Archive
-pushd src
+### Test Env
-    pod install
+pushd mqtt-stub-service
-#!/bin/bash
 set -e
+popd 
 popd
 
-## iOS ##################################
+
-### Create iOS Archive
+### Install / build number
 pushd src
 #!/bin/bash
-pushd ci
+#!/bin/bash
 #!/bin/bash
-    zx realm-headers.mjs undo
+set -e
 popd
 
 ## macOS ################################ 
@@ -43,5 +42,12 @@
 ### Undo prepare Realm for macOS
 pushd ci
     zx realm-headers.mjs undo
-set -e
+popd 
+
+## iOS ##################################
+#!/bin/bash
 # ~/Library/Developer/Xcode/Archives
+pushd src
+    rm -f MQTTAnalyzer.ipa MQTTAnalyzer.pkg
+    fastlane ios publish
+popd




diff --git a/src/MQTTAnalyzer/AppDelegate.swift b/src/MQTTAnalyzer/AppDelegate.swift
index 91ef51c0426827a7e88f3a7f9e2220c8f27c5122..eda09e7c33c8cfb521a5fddbed7076014e0d00fb 100644
--- a/src/MQTTAnalyzer/AppDelegate.swift
+++ b/src/MQTTAnalyzer/AppDelegate.swift
@@ -10,7 +10,6 @@ import UIKit
 import IceCream
 import CloudKit
 import CocoaMQTT
-import Starscream
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate {




diff --git a/src/MQTTAnalyzer/Info.plist b/src/MQTTAnalyzer/Info.plist
index 3af66e141ead7ebce718e93d151d0ae1df824977..9087781a3129b6fb029a7b6ca9a7e2fadc9ea1cb 100644
--- a/src/MQTTAnalyzer/Info.plist
+++ b/src/MQTTAnalyzer/Info.plist
@@ -17,7 +17,7 @@ 	$(PRODUCT_BUNDLE_PACKAGE_TYPE)
 	<key>CFBundleShortVersionString</key>
 	<string>$(MARKETING_VERSION)</string>
 	<key>CFBundleVersion</key>
-	<string>120</string>
+	<string>139</string>
 	<key>ITSAppUsesNonExemptEncryption</key>
 	<false/>
 	<key>LSApplicationCategoryType</key>
@@ -80,5 +80,10 @@ 		UIInterfaceOrientationPortraitUpsideDown
 		<string>UIInterfaceOrientationLandscapeLeft</string>
 		<string>UIInterfaceOrientationLandscapeRight</string>
 	</array>
+	<key>NSAppTransportSecurity</key>
+    <dict>
+        <key>NSAllowsArbitraryLoads</key>
+        <true/>
+  	</dict>	
 </dict>
 </plist>




diff --git a/src/MQTTAnalyzer/extensions/Array+Move.swift b/src/MQTTAnalyzer/extensions/Array+Move.swift
deleted file mode 100644
index a6de830370634acd182413691d0ac190412a1f61..0000000000000000000000000000000000000000
--- a/src/MQTTAnalyzer/extensions/Array+Move.swift
+++ /dev/null
@@ -1,45 +0,0 @@
-//
-//  ArrayUtils.swift
-//  MQTTAnalyzer
-//
-//  Created by Philipp Arndt on 2019-11-16.
-//  Copyright © 2019 Philipp Arndt. All rights reserved.
-//
-
-import Foundation
-
-extension Array {
-	mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) {
-		let suffixStart = halfStablePartition { index, _ in
-			return source.contains(index)
-		}
-		let suffix = self[suffixStart...]
-		removeSubrange(suffixStart...)
-		insert(contentsOf: suffix, at: destination)
-	}
-
-	mutating func halfStablePartition(isSuffixElement predicate: (Index, Element) -> Bool) -> Index {
-		guard var i = firstIndex(where: predicate) else {
-			return endIndex
-		}
-
-		var j = index(after: i)
-		while j != endIndex {
-			if !predicate(j, self[j]) {
-				swapAt(i, j)
-				formIndex(after: &i)
-			}
-			formIndex(after: &j)
-		}
-		return i
-	}
-
-	func firstIndex(where predicate: (Index, Element) -> Bool) -> Index? {
-		for (index, element) in self.enumerated() {
-			if predicate(index, element) {
-				return index
-			}
-		}
-		return nil
-	}
-}




diff --git a/src/MQTTAnalyzer/extensions/Array+Remove.swift b/src/MQTTAnalyzer/extensions/Array+Remove.swift
deleted file mode 100644
index a706cee22e8fcf29b6a3aa8f9d294d0ace9f036b..0000000000000000000000000000000000000000
--- a/src/MQTTAnalyzer/extensions/Array+Remove.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-//  Array+Remove.swift
-//  MQTTAnalyzer
-//
-//  Created by Philipp Arndt on 2022-01-31.
-//  Copyright © 2022 Philipp Arndt. All rights reserved.
-//
-
-import Foundation
-
-extension Array {
-	mutating func remove(atOffsets offsets: IndexSet) {
-		let suffixStart = halfStablePartition { index, _ in
-			return offsets.contains(index)
-		}
-		removeSubrange(suffixStart...)
-	}
-}




diff --git a/src/MQTTAnalyzer/extensions/String+RegExp.swift b/src/MQTTAnalyzer/extensions/String+RegExp.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1e5db0b7bc139b42f48e9d87b63c12cebf47a1fc
--- /dev/null
+++ b/src/MQTTAnalyzer/extensions/String+RegExp.swift
@@ -0,0 +1,33 @@
+//
+//  String+RegExp.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 08.04.22.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+
+// https://stackoverflow.com/questions/42789953/swift-3-how-do-i-extract-captured-groups-in-regular-expressions
+extension String {
+	func groups(for regexPattern: String) -> [[String]] {
+	do {
+		let text = self
+		let regex = try NSRegularExpression(pattern: regexPattern)
+		let matches = regex.matches(in: text,
+									range: NSRange(text.startIndex..., in: text))
+		return matches.map { match in
+			return (0..<match.numberOfRanges).map {
+				let rangeBounds = match.range(at: $0)
+				guard let range = Range(rangeBounds, in: text) else {
+					return ""
+				}
+				return String(text[range])
+			}
+		}
+	} catch let error {
+		print("invalid regex: \(error.localizedDescription)")
+		return []
+	}
+}
+}




diff --git a/src/MQTTAnalyzer/model/persistence/HostSettingExamples.swift b/src/MQTTAnalyzer/model/persistence/HostSettingExamples.swift
index b55a1c16c3d7107ff1e7a1e5fa60a5335e3cbd64..651d90943e974bb1b9b07037c8ff3f41c6df4e96 100644
--- a/src/MQTTAnalyzer/model/persistence/HostSettingExamples.swift
+++ b/src/MQTTAnalyzer/model/persistence/HostSettingExamples.swift
@@ -47,14 +47,23 @@ 		]
 		return result
 	}
 	
+//  Copyright © 2019 Philipp Arndt. All rights reserved.
 //  MQTTAnalyzer
-
 		let result = Host()
 		result.alias = "Example"
+		result.hostname = "test.mqtt.rnd7.de"
+		result.limitTopic = 0
 //  MQTTAnalyzer
-import RealmSwift
+class HostSettingExamples {
+		return result
+	}
+	
+	class func exampleLocalhost() -> Host {
 //  HostSettingExamples.swift
-import Foundation
+//  Created by Philipp Arndt on 2019-11-23.
+		result.alias = "localhost"
+		result.hostname = "localhost"
+		result.limitTopic = 0
 		result.subscriptions = [TopicSubscription(topic: "#", qos: 0)]
 		return result
 	}




diff --git a/src/MQTTAnalyzer/model/persistence/RealmPersistence.swift b/src/MQTTAnalyzer/model/persistence/RealmPersistence.swift
index 344a958f1c9d1b6d03cc7e0328f7034228b61045..934e5a98c26dc4b930aa12f39f91c669231e5c9b 100644
--- a/src/MQTTAnalyzer/model/persistence/RealmPersistence.swift
+++ b/src/MQTTAnalyzer/model/persistence/RealmPersistence.swift
@@ -8,6 +8,7 @@ //
 
 import Foundation
 import RealmSwift
+import SwiftUI
 
 public class RealmPersistence: Persistence {
 	let model: HostsModel
@@ -18,7 +19,7 @@ 	init?(model: HostsModel) {
 		self.model = model
 		
 //
-import Foundation
+			}
 			self.realm = realm
 		}
 		else {
@@ -26,13 +27,14 @@ 			return nil
 		}
 	}
 	
-//  HostModelPersistence.swift
+//
 //  MQTTAnalyzer
+//  Copyright © 2019 Philipp Arndt. All rights reserved.
 		do {
 			return try Realm()
 		}
 		catch {
-			NSLog("Unable to initialize persistence, using stub persistence.")
+			NSLog("Unable to initialize persistence, using stub persistence. \(error)")
 			return nil
 		}
 		




diff --git a/src/MQTTAnalyzer/model/persistence/StubPersistence.swift b/src/MQTTAnalyzer/model/persistence/StubPersistence.swift
index 338aa64b25efe67b0ab6159ab240d8836529057e..544912dd912c6ebbbff49230b80b6abd85f6eff0 100644
--- a/src/MQTTAnalyzer/model/persistence/StubPersistence.swift
+++ b/src/MQTTAnalyzer/model/persistence/StubPersistence.swift
@@ -27,6 +27,7 @@ 	func initExamples() {
 		hosts = [
 			HostSettingExamples.example1(),
 			HostSettingExamples.example2(),
+			HostSettingExamples.exampleRnd7(),
 			HostSettingExamples.exampleLocalhost()
 		]
 	}




diff --git a/src/MQTTAnalyzer/model/v2/TopicTree.swift b/src/MQTTAnalyzer/model/v2/TopicTree.swift
index 5513441d8f1a7ccccf3ac20898e578aa42bf9ca8..f94ae59fa232a2320f260e6b7ee9d4bd33e6245c 100644
--- a/src/MQTTAnalyzer/model/v2/TopicTree.swift
+++ b/src/MQTTAnalyzer/model/v2/TopicTree.swift
@@ -144,7 +144,7 @@ 		return addTopic(topic: topic, create: false)
 	}
 	
 	func addTopic(topic: String, create: Bool = true) -> TopicTree? {
-		let segments = topic.split(separator: "/").map { String($0) }
+		let segments = topic.split(separator: "/", omittingEmptySubsequences: false).map { String($0) }
 		
 		var current = findRoot()
 		var created = false




diff --git a/src/MQTTAnalyzer/mqtt/MqttClientSharedUtils.swift b/src/MQTTAnalyzer/mqtt/MqttClientSharedUtils.swift
deleted file mode 100644
index b3d1d611ed72b9b62dda78aebae412cd0eac9a7f..0000000000000000000000000000000000000000
--- a/src/MQTTAnalyzer/mqtt/MqttClientSharedUtils.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-//  MqttClientSharedUtils.swift
-//  MQTTAnalyzer
-//
-//  Created by Philipp Arndt on 2020-04-13.
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
-//
-
-import Foundation
-
-class MqttClientSharedUtils {
-	func waitFor(predicate: @escaping () -> Bool) -> DispatchTimeoutResult {
-		let group = DispatchGroup()
-		group.enter()
-
-		DispatchQueue.global().async {
-			while !predicate() {
-				usleep(useconds_t(500))
-			}
-			group.leave()
-		}
-
-		return group.wait(timeout: .now() + 10)
-	}
-	
-}




diff --git a/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient+ErrorMessage.swift b/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient+ErrorMessage.swift
new file mode 100644
index 0000000000000000000000000000000000000000..60febfcaab9f022affa5ae5b8c3519bfa36da5fc
--- /dev/null
+++ b/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient+ErrorMessage.swift
@@ -0,0 +1,34 @@
+//
+//  CocoaMQTTClient+ErrorMessage.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 08.04.22.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+import CocoaMQTT
+
+extension MqttClientCocoaMQTT {
+	class func extractErrorMessage(error: Error) -> String {
+		let nsError = error as NSError
+		let code = nsError.code
+		
+		if code == 8 {
+			return "Invalid hostname.\n\(error.localizedDescription)"
+		}
+		else if nsError.domain == "Network.NWError" {
+			if nsError.description.starts(with: "-9808") {
+				return "Bad certificate format, check all properties, like SAN, ... (-9808)"
+			}
+			else {
+				let groups = nsError.description.groups(for: ".*\\(rawValue:.(\\d+)\\):.(.*)")
+				if groups.count == 1 && groups[0].count == 3 {
+					return "\(groups[0][2]) (NW: \(groups[0][1]))"
+				}
+			}
+		}
+		
+		return "\(nsError.domain): \(nsError.description)"
+	}
+}




diff --git a/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient.swift b/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient.swift
index 5d70b57c9bad205ebc89bb50d0e069c613df8b3a..f5fc1607ada2eba803600ff8e741a38cef5c9c6c 100644
--- a/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient.swift
+++ b/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTClient.swift
@@ -8,13 +8,15 @@ //
 
 import Foundation
 import CocoaMQTT
-import Starscream
+import Combine
+//  MqttClientCocoaMQTT.swift
 //
+//  Created by Philipp Arndt on 2020-04-13.
 
 class MqttClientCocoaMQTT: MqttClient {
+	private let connectionStateQueue = DispatchQueue(label: "connection.state.lock.queue")
 	
 	let delgate = MQTTDelegate()
-	let utils = MqttClientSharedUtils()
 	
 	let sessionNum: Int
 	let model: TopicTree
@@ -65,8 +68,7 @@ 		}
 		
 		mqtt.enableSSL = host.ssl
 		mqtt.allowUntrustCACertificate = host.untrustedSSL
-		mqtt.sslSettings = [kCFStreamSSLPeerName as String: host.hostname as NSObject]
-		
+
 		if host.auth == .usernamePassword {
 			mqtt.username = host.usernameNonpersistent ?? host.username
 			mqtt.password = host.passwordNonpersistent ?? host.password
@@ -92,9 +94,6 @@ 		mqtt.delegate = self.delgate
 		mqtt.didReceiveMessage = self.didReceiveMessage
 		mqtt.didDisconnect = self.didDisconnect
 		mqtt.didConnectAck = self.didConnect
-		mqtt.didChangeState = { _, state in
-			print(state)
-		}
 		
 		if !mqtt.connect() {
 			failConnection(reason: "Connection to port \(host.port) failed")
@@ -123,19 +122,21 @@
 		DispatchQueue.global().async {
 			var i = 10
 			
+			var connecting = true
 import CocoaMQTT
-import Starscream
+import CocoaMQTT
+			while connecting && i > 0 {
 				print("CONNECTION: waiting... \(self.sessionNum) \(i) \(self.host.hostname)")
 				sleep(1)
 				
 import Starscream
-//  MQTTAnalyzer
+
 import Starscream
-//  Created by Philipp Arndt on 2020-04-13.
+//  MqttClientCocoaMQTT.swift
-				}
+				self.connectionStateQueue.sync {
-
+					connecting = self.connectionState.state == .connecting
 import Starscream
-
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 			}
 			group.leave()
 		}
@@ -179,9 +180,10 @@ 	func disconnect() {
 		print("CONNECTION: disconnect \(sessionNum) \(host.hostname)")
 
 		messageSubject.cancel()
-//
+		self.connectionStateQueue.async {
+	var connectionState = ConnectionState()
 //
-import Starscream
+		}
 		
 		if let mqtt = self.mqtt {
 			DispatchQueue.global(qos: .background).async {
@@ -217,8 +219,10 @@ 		}
 	}
 	
 	func setDisconnected() {
+		self.connectionStateQueue.async {
+	var connectionState = ConnectionState()
 //
-	var connectionAlive: Bool {
+		}
 
 		DispatchQueue.main.async {
 			self.host.state = .disconnected
@@ -241,7 +245,7 @@ 			return
 		}
 		
 		for message in messages {
-			if self.model.totalTopicCounter >= host.limitTopic {
+			if host.limitTopic > 0 && self.model.totalTopicCounter >= host.limitTopic {
 				// Limit exceeded
 				self.model.topicLimitExceeded = true
 			}
@@ -262,43 +266,25 @@ 		}
 	}
 	
 //
+import Foundation
 
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
 //
-		mqtt.didDisconnect = self.didDisconnect
-		
-	let model: TopicTree
+import Foundation
 import Foundation
-//
 
-import CocoaMQTT
-			return "Invalid hostname.\n\(error.localizedDescription)"
 	var host: Host
-			if host.protocolMethod == .mqtt {
+import CocoaMQTT
-	var host: Host
 //  MqttClientCocoaMQTT.swift
-			}
-	var host: Host
+//  MqttClientCocoaMQTT.swift
 //  MQTTAnalyzer
-	var host: Host
+			
+	var connectionState = ConnectionState()
 //  Created by Philipp Arndt on 2020-04-13.
+	var connectionState = ConnectionState()
 //  Copyright © 2020 Philipp Arndt. All rights reserved.
-
-		default:
-	var host: Host
 //  Copyright © 2020 Philipp Arndt. All rights reserved.
-		}
-	}
 
 //
-		messageSubject.cancellable = messageSubject.subject.eraseToAnyPublisher()
-		print("CONNECTION: onDisconnect \(sessionNum) \(host.hostname)")
-
-		if err != nil {
-			let messgae = extractErrorMessage(error: err!)
-			
-			connectionState.message = messgae
-//
 
 				self.host.usernameNonpersistent = nil
 				self.host.passwordNonpersistent = nil
@@ -317,8 +303,10 @@ 	
 	func didConnect(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
 		if ack == .accept {
 			print("CONNECTION: onConnect \(sessionNum) \(host.hostname)")
-			connectionState.state = .connected
+			self.connectionStateQueue.async {
-			
+				self.connectionState.state = .connected
+			}
+
 			NSLog("Connected. Return Code is \(ack.description)")
 			DispatchQueue.main.async {
 				self.host.state = .connected
@@ -353,9 +341,12 @@ 	
 	func failConnection(reason: String) {
 		NSLog("Connection failed: " + reason)
 //  MqttClientCocoaMQTT.swift
+//  MqttClientCocoaMQTT.swift
-import CocoaMQTT
-import Starscream
+//  MqttClientCocoaMQTT.swift
 //  MqttClientCocoaMQTT.swift
+import Foundation
+		}
+
 		self.setDisconnected()
 
 		DispatchQueue.main.async {




diff --git a/src/MQTTAnalyzer/views/about/AboutView.swift b/src/MQTTAnalyzer/views/about/AboutView.swift
index 2e81b3f545e0c46fed6f758a466f91b3abc314b4..8bb02270a174581ae611852b5bc1b3c03e5e1ca5 100644
--- a/src/MQTTAnalyzer/views/about/AboutView.swift
+++ b/src/MQTTAnalyzer/views/about/AboutView.swift
@@ -37,7 +37,7 @@ [Ulrich Frank](https://github.com/UlrichFrank), [Ricardo Pereira](https://github.com/visnaut), [AndreCouture](https://github.com/AndreCouture), [RoSchmi](https://github.com/RoSchmi),
 [Xploder](https://github.com/Xploder), [Ed Gauthier](https://github.com/edgauthier)
 
 **Dependencies**
-[CocoaMQTT](https://github.com/emqx/CocoaMQTT), [CocoaAsyncSocket](https://github.com/robbiehanson/CocoaAsyncSocket), [Starscream](https://github.com/daltoniam/Starscream), [RealmSwift](https://realm.io/docs/swift/latest/), [IceCream](https://github.com/caiyue1993/IceCream), [Highlightr](https://github.com/raspu/Highlightr), [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON), [swift-petitparser](https://github.com/philipparndt/swift-petitparser)
+[CocoaMQTT](https://github.com/emqx/CocoaMQTT), [RealmSwift](https://realm.io/docs/swift/latest/), [IceCream](https://github.com/caiyue1993/IceCream), [Highlightr](https://github.com/raspu/Highlightr), [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON), [swift-petitparser](https://github.com/philipparndt/swift-petitparser) [GRDB](https://github.com/groue/GRDB.swift)
 
 """).foregroundColor(.secondary)
 					.font(.footnote)




diff --git a/src/MQTTAnalyzer/views/common/CustomListView.swift b/src/MQTTAnalyzer/views/common/CustomListView.swift
index 188c052206a72548e669e195c4372d0ab693560f..d47b3d9fa578faa746fc59fa0b00dafbcdefbe8a 100644
--- a/src/MQTTAnalyzer/views/common/CustomListView.swift
+++ b/src/MQTTAnalyzer/views/common/CustomListView.swift
@@ -20,7 +20,7 @@ 	
 	var body: some View {
 		HStack {
 			VStack {
-				ForEach(views.indices) { index in
+				ForEach(views.indices, id: \.self) { index in
 					views[index]
 					if index < views.count - 1 {
 						Divider()




diff --git a/src/MQTTAnalyzer/views/common/QuestionBox.swift b/src/MQTTAnalyzer/views/common/QuestionBox.swift
deleted file mode 100644
index 08ffe7a9fcbb55536482240a750c40443d125bc7..0000000000000000000000000000000000000000
--- a/src/MQTTAnalyzer/views/common/QuestionBox.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-//  MenuButton.swift
-//  MQTTAnalyzer
-//
-//  Created by Philipp Arndt on 2020-01-04.
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
-//
-
-import SwiftUI
-
-struct QuestionBox: View {
-	let text: String
-
-	var body: some View {
-		FillingText(text: text,
-		imageName: "questionmark.circle.fill")
-		.padding()
-			.font(.body)
-			.background(Color.green.opacity(0.1))
-			.cornerRadius(10)
-	}
-}




diff --git a/src/MQTTAnalyzer/views/host/form/DisconnectedView.swift b/src/MQTTAnalyzer/views/host/form/DisconnectedView.swift
index 5adffa45b3c85c1d4b1b49fdf4b8dc3a79a84500..ae3c1607715fe90ced36bd1dca1ff064fe83c770 100644
--- a/src/MQTTAnalyzer/views/host/form/DisconnectedView.swift
+++ b/src/MQTTAnalyzer/views/host/form/DisconnectedView.swift
@@ -13,7 +13,7 @@ 	var host: Host
 
 	var body: some View {
 		HStack {
-			Text(host.connectionMessage ?? "Disconnected...")
+			Text(host.connectionMessage ?? "Disconnected")
 			
 			Spacer()
 			




diff --git a/src/MQTTAnalyzer/views/host/form/LoginView.swift b/src/MQTTAnalyzer/views/host/form/LoginView.swift
index b534dc82892f670787c2639a1b8e329eea8b9ec8..9b45738e9fa8581a26081edeb0e159c947cba544 100644
--- a/src/MQTTAnalyzer/views/host/form/LoginView.swift
+++ b/src/MQTTAnalyzer/views/host/form/LoginView.swift
@@ -27,6 +27,7 @@ 					
 					Text("Authenticate")
 				}
 			}
+			.accessibilityLabel("Play")
 		}
 		.padding()
 	}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/AuthenticationTypePicker.swift b/src/MQTTAnalyzer/views/host/form/auth/AuthenticationTypePicker.swift
index 2f61fc194decf9f49ecf31ae8ede1a64457fb2ca..d5bcacc81f7fd7c88068fc67fa994aa92fbe71f8 100644
--- a/src/MQTTAnalyzer/views/host/form/auth/AuthenticationTypePicker.swift
+++ b/src/MQTTAnalyzer/views/host/form/auth/AuthenticationTypePicker.swift
@@ -25,7 +26,10 @@ 	var body: some View {
 		Picker(selection: $type, label: Text("Auth")) {
 			Text("None").tag(HostAuthenticationType.none)
 //  AuthPicker.swift
+//  MQTTAnalyzer
+			Text("User/password").tag(HostAuthenticationType.usernamePassword).accessibilityLabel("userPassword-auth")
 			Text("Certificate").tag(HostAuthenticationType.certificate)
+				.accessibilityLabel("certificate-auth")
 		}.pickerStyle(SegmentedPickerStyle())
 	}
 }




diff --git a/src/MQTTAnalyzer/views/host/form/auth/UsernamePasswordAuthenticationView.swift b/src/MQTTAnalyzer/views/host/form/auth/UsernamePasswordAuthenticationView.swift
index 960280cdfce4cb78443523ccb1c5f73183976003..08d8a7be6ddb059572da0455beba230cd8ebfa06 100644
--- a/src/MQTTAnalyzer/views/host/form/auth/UsernamePasswordAuthenticationView.swift
+++ b/src/MQTTAnalyzer/views/host/form/auth/UsernamePasswordAuthenticationView.swift
@@ -26,6 +26,7 @@ 					.disableAutocorrection(true)
 					.autocapitalization(.none)
 					.multilineTextAlignment(.trailing)
 					.font(.body)
+					.accessibilityLabel("your username")
 			}
 			
 			HStack {
@@ -34,11 +35,12 @@ 					.font(.headline)
 				
 					Spacer()
 				
-				SecureField("password", text: $host.password)
+				SecureField("your password", text: $host.password)
 					.disableAutocorrection(true)
 					.autocapitalization(.none)
 					.multilineTextAlignment(.trailing)
 					.font(.body)
+					.accessibilityLabel("password")
 			}
 			
 			InfoBox(text: "Leave username and/or password empty. In order to not persist them. You will get a login dialog.")




diff --git a/src/MQTTAnalyzer/views/host/form/aws-iot/AWSIOTHelpView.swift b/src/MQTTAnalyzer/views/host/form/aws-iot/AWSIOTHelpView.swift
index 9ce133720ef587b82fe5ab3f43bae1ef7a5bab91..00f0c8455fa1c7d174d3d0530f795b0daae8d82e 100644
--- a/src/MQTTAnalyzer/views/host/form/aws-iot/AWSIOTHelpView.swift
+++ b/src/MQTTAnalyzer/views/host/form/aws-iot/AWSIOTHelpView.swift
@@ -15,15 +15,15 @@     var body: some View {
 		HStack {
 			VStack {
 				HStack {
-					Text("[AWS IoT documentation](https://github.com/philipparndt/mqtt-analyzer/blob/master/Docs/AWS-IoT.md#connect-to-aws-iot)")
+					Text("[AWS IoT documentation](https://github.com/philipparndt/mqtt-analyzer/blob/master/Docs/examples/aws/README.md)")
 						.foregroundColor(.blue)
 					Spacer()
 				}
 				
-				if host.suggestAWSIOTCHanges() {
+				if host.suggestAWSIOTChanges() {
 					Text("") // Space
 					HStack {
-						Button(action: self.updateSettingsForAWSIOT) {
+						Button(action: self.updateSettings) {
 							Text("Apply default values")
 						}
 						Spacer()
@@ -41,7 +41,7 @@ 		.background(Color.green.opacity(0.1))
 		.cornerRadius(10)
     }
 	
-	func updateSettingsForAWSIOT() {
+	func updateSettings() {
 		self.host.updateSettingsForAWSIOT()
 	}
 }




diff --git a/src/MQTTAnalyzer/views/host/form/aws-iot/HostFormModel+AWS.swift b/src/MQTTAnalyzer/views/host/form/aws-iot/HostFormModel+AWS.swift
index 06d2fd248e67cf1ad6a8b951aaddf53ae447019f..d4978b068ccac5725709816cd6c83d0ff5aecd03 100644
--- a/src/MQTTAnalyzer/views/host/form/aws-iot/HostFormModel+AWS.swift
+++ b/src/MQTTAnalyzer/views/host/form/aws-iot/HostFormModel+AWS.swift
@@ -14,7 +14,7 @@ 		return hostname.lowercased().hasSuffix("amazonaws.com")
 		&& hostname.lowercased().contains(".iot.")
 	}
 	
-	func suggestAWSIOTCHanges() -> Bool {
+	func suggestAWSIOTChanges() -> Bool {
 		if isAWS() {
 			// mqtt not ws
 			// cocoa not moscapsule




diff --git a/src/MQTTAnalyzer/views/host/form/client-certs/ClientCertsHelpView.swift b/src/MQTTAnalyzer/views/host/form/client-certs/ClientCertsHelpView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..948b437308fa0063510ad5eb46d0d2e87fcab89c
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/client-certs/ClientCertsHelpView.swift
@@ -0,0 +1,47 @@
+//
+//  AWSIoTHelpView.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2022-02-20.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import SwiftUI
+
+struct ClientCertsHelpView: View {
+	@Binding var host: HostFormModel
+	
+    var body: some View {
+		HStack {
+			VStack {
+				HStack {
+					Text("[Client certificates documentation](https://github.com/philipparndt/mqtt-analyzer/blob/master/Docs/examples/client-certs/README.md)")
+						.foregroundColor(.blue)
+					Spacer()
+				}
+				
+				if host.suggestClientCertsTLSChanges() {
+					Text("") // Space
+					HStack {
+						Button(action: self.updateSettings) {
+							Text("Apply default values")
+						}
+						Spacer()
+					}
+				}
+			}
+			
+			Spacer()
+			
+			Image(systemName: "questionmark.circle.fill")
+		}
+		.padding()
+		.font(.body)
+		.background(Color.green.opacity(0.1))
+		.cornerRadius(10)
+    }
+	
+	func updateSettings() {
+		self.host.updateSettingsForClientCertsTLS()
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/client-certs/HostFormModel+ClientCerts.swift b/src/MQTTAnalyzer/views/host/form/client-certs/HostFormModel+ClientCerts.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3533cd4aace1ab3b358ad243b601a3f14b4bfbbe
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/client-certs/HostFormModel+ClientCerts.swift
@@ -0,0 +1,29 @@
+//
+//  HostFormModel+ClientCerts.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 16.04.22.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+
+extension HostFormModel {
+	func isClientCerts() -> Bool {
+		return authType == .certificate
+	}
+	
+	func suggestClientCertsTLSChanges() -> Bool {
+		if isClientCerts() {
+			if !ssl {
+				return true
+			}
+		}
+		return false
+	}
+	
+	mutating func updateSettingsForClientCertsTLS() {
+		self.ssl = true
+		self.untrustedSSL = true
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/server/ProtocolPicker.swift b/src/MQTTAnalyzer/views/host/form/server/ProtocolPicker.swift
index 43f2fde8a76341e34b67328895cfe8e58772c579..e021785a0f7efef02e292d33f2834241563588bf 100644
--- a/src/MQTTAnalyzer/views/host/form/server/ProtocolPicker.swift
+++ b/src/MQTTAnalyzer/views/host/form/server/ProtocolPicker.swift
@@ -24,8 +24,9 @@ 	@Binding var type: HostProtocol
 
 	var body: some View {
 		Picker(selection: $type, label: Text("Protocol")) {
-			Text("MQTT").tag(HostProtocol.mqtt)
+			Text("MQTT").tag(HostProtocol.mqtt).accessibilityLabel("mqtt")
 //  ConnectionTypePicker.swift
+//  MQTTAnalyzer
 		}.pickerStyle(SegmentedPickerStyle())
 	}
 }




diff --git a/src/MQTTAnalyzer/views/host/form/server/ServerFormView.swift b/src/MQTTAnalyzer/views/host/form/server/ServerFormView.swift
index 78946e27aab777c8a1492da275afcd84e84e43bd..afa9b76023b044778140279c29127b9b82a91c1d 100644
--- a/src/MQTTAnalyzer/views/host/form/server/ServerFormView.swift
+++ b/src/MQTTAnalyzer/views/host/form/server/ServerFormView.swift
@@ -55,6 +55,9 @@ 			
 			if host.isAWS() {
 				AWSIoTHelpView(host: $host)
 			}
+			else if host.isClientCerts() {
+				ClientCertsHelpView(host: $host)
+			}
 			
 			HStack {
 				FormFieldInvalidMark(invalid: portInvalid)
@@ -64,9 +67,10 @@ 					.font(.headline)
 
 				Spacer()
 
-				TextField("1883", text: $host.port)
+				TextField("e.g. 1883", text: $host.port)
 					.multilineTextAlignment(.trailing)
 					.disableAutocorrection(true)
+					.accessibilityLabel("port")
 					.font(.body)
 			}
 			
@@ -98,7 +102,7 @@ 			
 			Toggle(isOn: $host.ssl) {
 				Text("SSL")
 					.font(.headline)
-			}
+			}.accessibilityLabel("tls")
 
 			if host.ssl {
 				Toggle(isOn: $host.untrustedSSL) {




diff --git a/src/MQTTAnalyzer/views/host/form/topic/SubscriptionDetailsView.swift b/src/MQTTAnalyzer/views/host/form/topic/SubscriptionDetailsView.swift
index b82d4df638b451d9e4aaef494799f8f0f0fdfe67..6da92682b383e30eaf87f1d65c8ceab4c569dede 100644
--- a/src/MQTTAnalyzer/views/host/form/topic/SubscriptionDetailsView.swift
+++ b/src/MQTTAnalyzer/views/host/form/topic/SubscriptionDetailsView.swift
@@ -20,11 +20,12 @@ 					Text("Topic")
 						.font(.headline)
 
 					Spacer()
-					TextField("#", text: $subscription.topic)
+					TextField("e.g. #", text: $subscription.topic)
 						.multilineTextAlignment(.trailing)
 						.disableAutocorrection(true)
 						.autocapitalization(.none)
 						.font(.body)
+						.accessibilityLabel("subscription-topic")
 				}
 
 				HStack {




diff --git a/src/MQTTAnalyzer/views/host/form/topic/TopicsFormView.swift b/src/MQTTAnalyzer/views/host/form/topic/TopicsFormView.swift
index 4949ea32cb70dffc6386c3931a16ba487c7c2fb2..08a107b6714bd18f0abf106e286207f3c17836a9 100644
--- a/src/MQTTAnalyzer/views/host/form/topic/TopicsFormView.swift
+++ b/src/MQTTAnalyzer/views/host/form/topic/TopicsFormView.swift
@@ -33,7 +33,9 @@ 				.onDelete(perform: self.delete)
 				
 				Button(action: addSubscription) {
 					Text("Add subscription")
-				}.font(.body)
+				}
+				.font(.body)
+				.accessibilityLabel("add-subscription")
 			}
 		}
 	}
@@ -48,7 +50,7 @@ 	}
 	
 	func addSubscription() {
 //  Created by Philipp Arndt on 2020-04-14.
-//
+struct TopicsFormView: View {
 		host.subscriptions.append(model)
 		
 		ViewSelection.update(newValue: model.id) { id in




diff --git a/src/MQTTAnalyzer/views/login/LoginDialog.swift b/src/MQTTAnalyzer/views/login/LoginDialog.swift
index b9dad5b1711e3cb9d589387ef531cc5f8f495aa1..d2e02c6feb28420d17b2a88be7c092b556b57de9 100644
--- a/src/MQTTAnalyzer/views/login/LoginDialog.swift
+++ b/src/MQTTAnalyzer/views/login/LoginDialog.swift
@@ -57,6 +57,7 @@ 							.disableAutocorrection(true)
 							.autocapitalization(.none)
 							.multilineTextAlignment(.trailing)
 							.font(.body)
+							.accessibilityLabel("username")
 					}
 					
 					HStack {
@@ -70,6 +71,7 @@ 							.disableAutocorrection(true)
 							.autocapitalization(.none)
 							.multilineTextAlignment(.trailing)
 							.font(.body)
+							.accessibilityLabel("password")
 					}
 				}
 				




diff --git a/src/MQTTAnalyzer/views/message-publish/PublishMessageFormView.swift b/src/MQTTAnalyzer/views/message-publish/PublishMessageFormView.swift
index 0d1deef2b765e4f6bd9feddf8045c5391b9f9a16..bb1fdca8e605121228b4675b17f9545caf6b1e8b 100644
--- a/src/MQTTAnalyzer/views/message-publish/PublishMessageFormView.swift
+++ b/src/MQTTAnalyzer/views/message-publish/PublishMessageFormView.swift
@@ -143,7 +143,7 @@
 	var body: some View {
 		Form {
 			Section(header: Text("Topic")) {
-				TextField("#", text: $message.topic)
+				TextField("", text: $message.topic)
 					.disableAutocorrection(true)
 					.autocapitalization(.none)
 					.font(.body)
@@ -213,7 +213,7 @@ 	
 	var body: some View {
 		Group {
 //
-	}
+	let id: String = NSUUID().uuidString
 				HStack {
 					Text(self.message.properties[index].pathName)
 					Spacer()




diff --git a/src/MQTTAnalyzer/views/topic/FolderNavigationView.swift b/src/MQTTAnalyzer/views/topic/FolderNavigationView.swift
index 0640da85af05a63cd204bfe659c08540d96b23fb..37351bb6b7cd416f3690658e37dae2990833eb4e 100644
--- a/src/MQTTAnalyzer/views/topic/FolderNavigationView.swift
+++ b/src/MQTTAnalyzer/views/topic/FolderNavigationView.swift
@@ -45,7 +45,8 @@ 	var body: some View {
 		HStack {
 			FolderReadMarkerView(read: model.readState)
 			
-			Text(model.name)
+			Text(model.name.isBlank ? "<empty>" : model.name)
+				.foregroundColor(model.name.isBlank ? .gray : .primary)
 			
 			Spacer()
 




diff --git a/src/MQTTAnalyzer.xcodeproj/project.pbxproj b/src/MQTTAnalyzer.xcodeproj/project.pbxproj
index fc75645507f7a8fb773537fcc59076cb5193d695..e35350915915a4fd57f0962ce993e2f19bbb02fc 100644
--- a/src/MQTTAnalyzer.xcodeproj/project.pbxproj
+++ b/src/MQTTAnalyzer.xcodeproj/project.pbxproj
@@ -25,19 +25,23 @@ 		22061AC327CA6BD300FFF915 /* XCUIElement+Checked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22061AC227CA6BD300FFF915 /* XCUIElement+Checked.swift */; };
 		22061AC527CA79FE00FFF915 /* DispatchQueue+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22061AC427CA79FE00FFF915 /* DispatchQueue+Background.swift */; };
 		2209C86C23B720E7007C1D93 /* HostValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2209C86B23B720E7007C1D93 /* HostValidator.swift */; };
 		220CCD632477F12300E8CA39 /* DataMigrationCertificateFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220CCD622477F12300E8CA39 /* DataMigrationCertificateFiles.swift */; };
+// !$*UTF8*$!
 {
+// !$*UTF8*$!
 
+// !$*UTF8*$!
 {
+// !$*UTF8*$!
 /* Begin PBXBuildFile section */
-		221C571C2466847800C0DD02 /* QuestionBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571B2466847800C0DD02 /* QuestionBox.swift */; };
+		2215118D27C923EB0000E385 /* TopicTree+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2215118C27C923EB0000E385 /* TopicTree+Search.swift */; };
+		2217BA3C27A3D2F900699428 /* AwaitMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2217BA3B27A3D2F900699428 /* AwaitMessagesView.swift */; };
 		221C571E2466C9CD00C0DD02 /* HostFormModel+AWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571D2466C9CD00C0DD02 /* HostFormModel+AWS.swift */; };
 		221C57202466CC2800C0DD02 /* AWSIOTPresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571F2466CC2800C0DD02 /* AWSIOTPresetTests.swift */; };
+		222AC3302803350600D66334 /* TestServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222AC32F2803350600D66334 /* TestServer.swift */; };
 		222EAAB1279FC289000E37AF /* CocoaMQTTRegression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222EAAB0279FC289000E37AF /* CocoaMQTTRegression.swift */; };
 		222EAAB327A31849000E37AF /* PropertyImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222EAAB227A31849000E37AF /* PropertyImageProvider.swift */; };
 		222EAAB527A31CCB000E37AF /* PropertyImageProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222EAAB427A31CCB000E37AF /* PropertyImageProviderTest.swift */; };
 		222EAAB727A3CDEB000E37AF /* ConnectBrokerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222EAAB627A3CDEB000E37AF /* ConnectBrokerView.swift */; };
-		2230FEF427A4340400D4327F /* NavigationModeFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF327A4340400D4327F /* NavigationModeFormView.swift */; };
-		2230FEF627A435A000D4327F /* NavigationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF527A435A000D4327F /* NavigationPicker.swift */; };
 		2230FEF827A4434800D4327F /* DataMigrationNavigationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF727A4434800D4327F /* DataMigrationNavigationMode.swift */; };
 		2230FEFA27A50C8000D4327F /* TreeModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF927A50C8000D4327F /* TreeModelTests.swift */; };
 		2230FEFE27A50E9000D4327F /* Date+Iso.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEFD27A50E9000D4327F /* Date+Iso.swift */; };
@@ -61,7 +65,6 @@ 		2230FF2927A6BBB800D4327F /* TitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2827A6BBB800D4327F /* TitleView.swift */; };
 		2230FF2B27A6BC1C00D4327F /* InformationDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2A27A6BC1C00D4327F /* InformationDetailView.swift */; };
 		2230FF2D27A6BC8900D4327F /* InformationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2C27A6BC8900D4327F /* InformationContainerView.swift */; };
 		2230FF2F27A6CDB800D4327F /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2E27A6CDB800D4327F /* Welcome.swift */; };
-		2230FF3127A8304E00D4327F /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF3027A8304E00D4327F /* Array+Remove.swift */; };
 		2230FF3327A830C900D4327F /* DataProtocol+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF3227A830C900D4327F /* DataProtocol+Hex.swift */; };
 		2231894B27F08BAB001177F4 /* ViewSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2231894A27F08BAB001177F4 /* ViewSelection.swift */; };
 		223AF5D12477D575009810E6 /* FileLister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D02477D575009810E6 /* FileLister.swift */; };
@@ -94,8 +97,6 @@ 		2263C1D924813B0C00FF14E8 /* CertificateFilesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2263C1D824813B0C00FF14E8 /* CertificateFilesModel.swift */; };
 		226A6B50244457E100ACDFC3 /* MqttClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B4F244457E100ACDFC3 /* MqttClient.swift */; };
 		226A6B5224445BA400ACDFC3 /* CocoaMQTTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B5124445BA400ACDFC3 /* CocoaMQTTClient.swift */; };
 /* Begin PBXBuildFile section */
-	classes = {
-/* Begin PBXBuildFile section */
 	};
 		226A6B5D2445748500ACDFC3 /* HostFormModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B5C2445748500ACDFC3 /* HostFormModel.swift */; };
 		226A6B5F2445754D00ACDFC3 /* ServerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B5E2445754D00ACDFC3 /* ServerFormView.swift */; };
@@ -141,6 +142,9 @@ 		2291284924685AE1006F8256 /* DataMigrationLimits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2291284824685AE0006F8256 /* DataMigrationLimits.swift */; };
 		2291424C23BF78000086C251 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2291424B23BF78000086C251 /* AboutView.swift */; };
 		2291424F23C0C0370086C251 /* MenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2291424E23C0C0370086C251 /* MenuButton.swift */; };
 // !$*UTF8*$!
+		2209C86B23B720E7007C1D93 /* HostValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostValidator.swift; sourceTree = "<group>"; };
+		22953B33280A92D8000F8F37 /* ClientCertsHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22953B32280A92D8000F8F37 /* ClientCertsHelpView.swift */; };
+// !$*UTF8*$!
 		2230FEF827A4434800D4327F /* DataMigrationNavigationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF727A4434800D4327F /* DataMigrationNavigationMode.swift */; };
 		229FCAEF247D6E2700490628 /* ImportCertificatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229FCAEE247D6E2700490628 /* ImportCertificatePicker.swift */; };
 		22A351DE24A76463001B8AEE /* PublishMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A351DD24A76463001B8AEE /* PublishMessageModel.swift */; };
@@ -151,6 +155,11 @@ 		22A386FD2409440F00DF8F94 /* InfoBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A386FC2409440F00DF8F94 /* InfoBox.swift */; };
 		22A387052409768100DF8F94 /* HostModelPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A387042409768100DF8F94 /* HostModelPersistenceTests.swift */; };
 		22A38707240BD9E600DF8F94 /* TimeSeriesValueUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A38706240BD9E600DF8F94 /* TimeSeriesValueUtil.swift */; };
 // !$*UTF8*$!
+		2215118C27C923EB0000E385 /* TopicTree+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TopicTree+Search.swift"; sourceTree = "<group>"; };
+		22AB2C112800771700E88875 /* CocoaMQTTClient+ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AB2C102800771700E88875 /* CocoaMQTTClient+ErrorMessage.swift */; };
+		22AB2C132800854700E88875 /* MqttClientCocoaMQTTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AB2C122800854700E88875 /* MqttClientCocoaMQTTTests.swift */; };
+		22AB2C152801D8A400E88875 /* ConfigurationMQTTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AB2C142801D8A400E88875 /* ConfigurationMQTTTests.swift */; };
+// !$*UTF8*$!
 		2230FF1027A6511F00D4327F /* TreeUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0F27A6511F00D4327F /* TreeUtilsTests.swift */; };
 		22AE64362412637A00C2C4FE /* TimeSeriesValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AE64352412637A00C2C4FE /* TimeSeriesValue.swift */; };
 		22AE643824126A7500C2C4FE /* DiagramPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AE643724126A7500C2C4FE /* DiagramPathTests.swift */; };
@@ -166,6 +175,8 @@ 		22B46BD927CA652B0077FB83 /* Broker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B46BD227CA652B0077FB83 /* Broker.swift */; };
 		22B46BDA27CA652B0077FB83 /* Brokers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B46BD327CA652B0077FB83 /* Brokers.swift */; };
 		22B46BDF27CA65750077FB83 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B46BDE27CA65750077FB83 /* SearchTests.swift */; };
 // !$*UTF8*$!
+		221C571F2466CC2800C0DD02 /* AWSIOTPresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSIOTPresetTests.swift; sourceTree = "<group>"; };
+// !$*UTF8*$!
 		2230FF3127A8304E00D4327F /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF3027A8304E00D4327F /* Array+Remove.swift */; };
 		22C386A322CB84900054C385 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C386A222CB84900054C385 /* RootView.swift */; };
 		22C9F72F23B7486E00892C4B /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 22C9F72E23B7486E00892C4B /* .swiftlint.yml */; };
@@ -180,7 +191,6 @@ 		22D236FB23FF00E10003D87F /* StringUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D236FA23FF00E10003D87F /* StringUtilsTests.swift */; };
 		22D50F9722CE4C4300F37EAF /* Multimap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D50F9622CE4C4300F37EAF /* Multimap.swift */; };
 		22E469D5237FBEC500D72BD6 /* ReadMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E469D4237FBEC500D72BD6 /* ReadMarkerView.swift */; };
 		22E469D7237FC03500D72BD6 /* Readstate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E469D6237FC03500D72BD6 /* Readstate.swift */; };
-		22E469DB23801CA000D72BD6 /* Array+Move.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E469DA23801CA000D72BD6 /* Array+Move.swift */; };
 		22E8971722CFBFED00A4B8A3 /* TimeSeriesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E8971622CFBFED00A4B8A3 /* TimeSeriesModel.swift */; };
 		22E914DF25A8420700BEC599 /* HostFormModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E914DE25A8420700BEC599 /* HostFormModelTests.swift */; };
 		22F3AB1324A77B6600B2CF92 /* DataMigrationMoscapsuleDeprecation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F3AB1224A77B6600B2CF92 /* DataMigrationMoscapsuleDeprecation.swift */; };
@@ -235,22 +245,23 @@ 		22061AC227CA6BD300FFF915 /* XCUIElement+Checked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Checked.swift"; sourceTree = ""; };
 		22061AC427CA79FE00FFF915 /* DispatchQueue+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Background.swift"; sourceTree = "<group>"; };
 		2209C86B23B720E7007C1D93 /* HostValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostValidator.swift; sourceTree = "<group>"; };
 		220CCD622477F12300E8CA39 /* DataMigrationCertificateFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataMigrationCertificateFiles.swift; sourceTree = "<group>"; };
+// !$*UTF8*$!
 		2205E5E1238A7EE2001638DF /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2205E5E0238A7EE2001638DF /* ButtonStyle.swift */; };
-	archiveVersion = 1;
+
+// !$*UTF8*$!
 		2205E5E1238A7EE2001638DF /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2205E5E0238A7EE2001638DF /* ButtonStyle.swift */; };
-	classes = {
+/* Begin PBXBuildFile section */
 		2205E5E1238A7EE2001638DF /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2205E5E0238A7EE2001638DF /* ButtonStyle.swift */; };
-	};
+	archiveVersion = 1;
+		2217BA3B27A3D2F900699428 /* AwaitMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwaitMessagesView.swift; sourceTree = "<group>"; };
 		221C571D2466C9CD00C0DD02 /* HostFormModel+AWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HostFormModel+AWS.swift"; sourceTree = "<group>"; };
 		221C571F2466CC2800C0DD02 /* AWSIOTPresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSIOTPresetTests.swift; sourceTree = "<group>"; };
+		222AC32F2803350600D66334 /* TestServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestServer.swift; sourceTree = "<group>"; };
 		222EAAB0279FC289000E37AF /* CocoaMQTTRegression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaMQTTRegression.swift; sourceTree = "<group>"; };
 		222EAAB227A31849000E37AF /* PropertyImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyImageProvider.swift; sourceTree = "<group>"; };
 		222EAAB427A31CCB000E37AF /* PropertyImageProviderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyImageProviderTest.swift; sourceTree = "<group>"; };
 		222EAAB627A3CDEB000E37AF /* ConnectBrokerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectBrokerView.swift; sourceTree = "<group>"; };
 		22061AC127CA682F00FFF915 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22061AC027CA682F00FFF915 /* Search.swift */; };
-{
-		2230FEF527A435A000D4327F /* NavigationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationPicker.swift; sourceTree = "<group>"; };
-		22061AC127CA682F00FFF915 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22061AC027CA682F00FFF915 /* Search.swift */; };
 	classes = {
 		2230FEF927A50C8000D4327F /* TreeModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeModelTests.swift; sourceTree = "<group>"; };
 		2230FEFB27A50C9700D4327F /* TopicTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicTree.swift; sourceTree = "<group>"; };
@@ -274,7 +285,6 @@ 		2230FF2827A6BBB800D4327F /* TitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleView.swift; sourceTree = ""; };
 		2230FF2A27A6BC1C00D4327F /* InformationDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationDetailView.swift; sourceTree = "<group>"; };
 		2230FF2C27A6BC8900D4327F /* InformationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationContainerView.swift; sourceTree = "<group>"; };
 		2230FF2E27A6CDB800D4327F /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; };
-		2230FF3027A8304E00D4327F /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
 		2230FF3227A830C900D4327F /* DataProtocol+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataProtocol+Hex.swift"; sourceTree = "<group>"; };
 		2231894A27F08BAB001177F4 /* ViewSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewSelection.swift; sourceTree = "<group>"; };
 		223AF5D02477D575009810E6 /* FileLister.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLister.swift; sourceTree = "<group>"; };
@@ -316,7 +326,6 @@ 		2256BDA9246FBE5100F92EFE /* DataMigrationEmptyTopic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrationEmptyTopic.swift; sourceTree = ""; };
 		2263C1D824813B0C00FF14E8 /* CertificateFilesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateFilesModel.swift; sourceTree = "<group>"; };
 		226A6B4F244457E100ACDFC3 /* MqttClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClient.swift; sourceTree = "<group>"; };
 		226A6B5124445BA400ACDFC3 /* CocoaMQTTClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaMQTTClient.swift; sourceTree = "<group>"; };
-		226A6B5324448ECB00ACDFC3 /* MqttClientSharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientSharedUtils.swift; sourceTree = "<group>"; };
 		226A6B5524449F5400ACDFC3 /* ProtocolPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolPicker.swift; sourceTree = "<group>"; };
 		226A6B5C2445748500ACDFC3 /* HostFormModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostFormModel.swift; sourceTree = "<group>"; };
 		226A6B5E2445754D00ACDFC3 /* ServerFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerFormView.swift; sourceTree = "<group>"; };
@@ -361,6 +370,8 @@ 		2291284624685A92006F8256 /* DataMigrationAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrationAuth.swift; sourceTree = ""; };
 		2291284824685AE0006F8256 /* DataMigrationLimits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrationLimits.swift; sourceTree = "<group>"; };
 		2291424B23BF78000086C251 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
 		2291424E23C0C0370086C251 /* MenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuButton.swift; sourceTree = "<group>"; };
+		22953B30280A9206000F8F37 /* HostFormModel+ClientCerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HostFormModel+ClientCerts.swift"; sourceTree = "<group>"; };
+		22953B32280A92D8000F8F37 /* ClientCertsHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCertsHelpView.swift; sourceTree = "<group>"; };
 		229D30C923197D8200632896 /* CocoaAsyncSocket.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CocoaAsyncSocket.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		229D30CA23197D8200632896 /* CocoaMQTT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CocoaMQTT.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		229FCAEC247D67BC00490628 /* DocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerView.swift; sourceTree = "<group>"; };
@@ -372,6 +383,10 @@ 		22A386FA240941B600DF8F94 /* CertificateFilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateFilePickerView.swift; sourceTree = ""; };
 		22A386FC2409440F00DF8F94 /* InfoBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBox.swift; sourceTree = "<group>"; };
 		22A387042409768100DF8F94 /* HostModelPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostModelPersistenceTests.swift; sourceTree = "<group>"; };
 		22A38706240BD9E600DF8F94 /* TimeSeriesValueUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSeriesValueUtil.swift; sourceTree = "<group>"; };
+		22AB2C0E280076CB00E88875 /* String+RegExp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+RegExp.swift"; sourceTree = "<group>"; };
+		22AB2C102800771700E88875 /* CocoaMQTTClient+ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CocoaMQTTClient+ErrorMessage.swift"; sourceTree = "<group>"; };
+		22AB2C122800854700E88875 /* MqttClientCocoaMQTTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientCocoaMQTTTests.swift; sourceTree = "<group>"; };
+		22AB2C142801D8A400E88875 /* ConfigurationMQTTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationMQTTTests.swift; sourceTree = "<group>"; };
 		22AE64332412636300C2C4FE /* DiagramPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramPath.swift; sourceTree = "<group>"; };
 		22AE64352412637A00C2C4FE /* TimeSeriesValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSeriesValue.swift; sourceTree = "<group>"; };
 		22AE643724126A7500C2C4FE /* DiagramPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramPathTests.swift; sourceTree = "<group>"; };
@@ -386,6 +401,7 @@ 		22B46BD127CA652B0077FB83 /* MessageTopicUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTopicUtils.swift; sourceTree = ""; };
 		22B46BD227CA652B0077FB83 /* Broker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Broker.swift; sourceTree = "<group>"; };
 		22B46BD327CA652B0077FB83 /* Brokers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Brokers.swift; sourceTree = "<group>"; };
 		22B46BDE27CA65750077FB83 /* SearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTests.swift; sourceTree = "<group>"; };
+		22B90E872805B2DE0083C6E5 /* SubscriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionTests.swift; sourceTree = "<group>"; };
 		22C2856124759FD40000C1E8 /* CertificateLocationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateLocationPicker.swift; sourceTree = "<group>"; };
 		22C386A222CB84900054C385 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
 		22C7F0D02416A16600534880 /* MQTTAnalyzerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MQTTAnalyzerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -402,7 +418,6 @@ 		22D236FA23FF00E10003D87F /* StringUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilsTests.swift; sourceTree = ""; };
 		22D50F9622CE4C4300F37EAF /* Multimap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Multimap.swift; sourceTree = "<group>"; };
 		22E469D4237FBEC500D72BD6 /* ReadMarkerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerView.swift; sourceTree = "<group>"; };
 		22E469D6237FC03500D72BD6 /* Readstate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readstate.swift; sourceTree = "<group>"; };
-		22E469DA23801CA000D72BD6 /* Array+Move.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Move.swift"; sourceTree = "<group>"; };
 		22E8971622CFBFED00A4B8A3 /* TimeSeriesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSeriesModel.swift; sourceTree = "<group>"; };
 		22E914DE25A8420700BEC599 /* HostFormModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostFormModelTests.swift; sourceTree = "<group>"; };
 		22F3AB1224A77B6600B2CF92 /* DataMigrationMoscapsuleDeprecation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrationMoscapsuleDeprecation.swift; sourceTree = "<group>"; };
@@ -488,22 +503,13 @@ 			children = (
 				226A6B5124445BA400ACDFC3 /* CocoaMQTTClient.swift */,
 				2203571E2445852800A98CD3 /* CocoaMQTTCertificateFiles.swift */,
 				220357202445B2ED00A98CD3 /* MQTTDelegate.swift */,
-	classes = {
 // !$*UTF8*$!
-	objects = {
-		2230FF0627A5668200D4327F /* TopicTree+Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0527A5668200D4327F /* TopicTree+Counter.swift */; };
 {
-	classes = {
 	archiveVersion = 1;
-		};
-		2230FEF227A433F300D4327F /* navigation */ = {
-			isa = PBXGroup;
-			children = (
-				2230FEF527A435A000D4327F /* NavigationPicker.swift */,
-				2230FEF327A4340400D4327F /* NavigationModeFormView.swift */,
+
 			);
 		2230FF0627A5668200D4327F /* TopicTree+Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0527A5668200D4327F /* TopicTree+Counter.swift */; };
-	objectVersion = 52;
+{
 			sourceTree = "<group>";
 		};
 		2230FF0027A5206400D4327F /* v2 */ = {
@@ -650,42 +656,44 @@ 			isa = PBXGroup;
 			children = (
 				2253F8F122C8C008007E35A2 /* Info.plist */,
 	};
-		2230FEF627A435A000D4327F /* NavigationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF527A435A000D4327F /* NavigationPicker.swift */; };
+		2230FF0E27A650CE00D4327F /* TreeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0D27A650CE00D4327F /* TreeUtils.swift */; };
 	};
-		2230FEF827A4434800D4327F /* DataMigrationNavigationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF727A4434800D4327F /* DataMigrationNavigationMode.swift */; };
+		2230FF1627A6669C00D4327F /* MsgMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1527A6669C00D4327F /* MsgMetadata.swift */; };
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
+	objectVersion = 52;
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
-// !$*UTF8*$!
+
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
-{
+// !$*UTF8*$!
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
-	archiveVersion = 1;
+	};
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
-	classes = {
+/* Begin PBXBuildFile section */
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
-	};
+	archiveVersion = 1;
 	};
-		2230FF0C27A57DF500D4327F /* TopicTree+ReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0B27A57DF400D4327F /* TopicTree+ReadState.swift */; };
 	};
-		2230FF0E27A650CE00D4327F /* TreeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0D27A650CE00D4327F /* TreeUtils.swift */; };
+	archiveVersion = 1;
 	};
-	classes = {
+	archiveVersion = 1;
 
-		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
+		2285C81C27847786008DA37D /* MetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2285C81B27847786008DA37D /* MetadataView.swift */; };
 /* Begin PBXBuildFile section */
 	};
+		2230FEF827A4434800D4327F /* DataMigrationNavigationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF727A4434800D4327F /* DataMigrationNavigationMode.swift */; };
 	};
+		2230FF0627A5668200D4327F /* TopicTree+Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF0527A5668200D4327F /* TopicTree+Counter.swift */; };
 		2230FF2227A6AEC800D4327F /* TopicTree+Flatten.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2127A6AEC800D4327F /* TopicTree+Flatten.swift */; };
-// !$*UTF8*$!
 	};
-		2230FF1827A666C600D4327F /* MsgMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1727A666C600D4327F /* MsgMessage.swift */; };
+		2230FEFA27A50C8000D4327F /* TreeModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF927A50C8000D4327F /* TreeModelTests.swift */; };
 		2230FF2227A6AEC800D4327F /* TopicTree+Flatten.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2127A6AEC800D4327F /* TopicTree+Flatten.swift */; };
-	archiveVersion = 1;
+	objectVersion = 52;
-	};
 		2230FF1E27A6981900D4327F /* MessageLimitReachedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF1D27A6981900D4327F /* MessageLimitReachedView.swift */; };
+{
 				2230FF2327A6B80900D4327F /* TopicLimitTests.swift */,
 		2230FF2227A6AEC800D4327F /* TopicTree+Flatten.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2127A6AEC800D4327F /* TopicTree+Flatten.swift */; };
-	objectVersion = 52;
+{
+				2230FF0F27A6511F00D4327F /* TreeUtilsTests.swift */,
 			);
 			path = MQTTAnalyzerTests;
 			sourceTree = "<group>";
@@ -691,6 +700,7 @@ 		};
 		226A6B5B2445740C00ACDFC3 /* form */ = {
 			isa = PBXGroup;
 			children = (
+				22953B2F280A91DC000F8F37 /* client-certs */,
 				2200D97927C2381100E63E89 /* aws-iot */,
 				2285C81927842176008DA37D /* DisconnectedView.swift */,
 				222EAAB627A3CDEB000E37AF /* ConnectBrokerView.swift */,
@@ -703,7 +713,6 @@ 				2285C80A2781E89B008DA37D /* TopicLimitReachedView.swift */,
 				226A6B682445761200ACDFC3 /* server */,
 				226A6B692445763A00ACDFC3 /* auth */,
 				226A6B6A2445765C00ACDFC3 /* topic */,
-				2230FEF227A433F300D4327F /* navigation */,
 				226A6B6B2445767800ACDFC3 /* more */,
 				226A6B5C2445748500ACDFC3 /* HostFormModel.swift */,
 				22FD7CF922C8D2650078795F /* EditHostFormView.swift */,
@@ -777,14 +786,12 @@ 		22810479238170A200112F24 /* extensions */ = {
 			isa = PBXGroup;
 			children = (
 		2230FF2F27A6CDB800D4327F /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2E27A6CDB800D4327F /* Welcome.swift */; };
-	classes = {
-				2230FF3027A8304E00D4327F /* Array+Remove.swift */,
-		2230FF2F27A6CDB800D4327F /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FF2E27A6CDB800D4327F /* Welcome.swift */; };
 	objectVersion = 52;
 				22F8BEE623C24A5800422BFF /* String+Utils.swift */,
 				2230FEFD27A50E9000D4327F /* Date+Iso.swift */,
 				224AFEA627AE88DC00DFD09F /* Color+SystemColors.swift */,
 				22061AC427CA79FE00FFF915 /* DispatchQueue+Background.swift */,
+				22AB2C0E280076CB00E88875 /* String+RegExp.swift */,
 			);
 			path = extensions;
 			sourceTree = "<group>";
@@ -887,8 +894,6 @@ 			children = (
 				228104902381770000112F24 /* FillingText.swift */,
 				22A386FC2409440F00DF8F94 /* InfoBox.swift */,
 		223AF5D52477D5F5009810E6 /* FileItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D42477D5F5009810E6 /* FileItemView.swift */; };
-	};
-		223AF5D52477D5F5009810E6 /* FileItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D42477D5F5009810E6 /* FileItemView.swift */; };
 	objectVersion = 52;
 				224AFEAA27AEF98700DFD09F /* CustomListView.swift */,
 				224AFEAC27AEF9BB00DFD09F /* TupleView+GetChildren.swift */,
@@ -896,6 +901,15 @@ 			);
 			path = common;
 			sourceTree = "<group>";
 		};
+		22953B2F280A91DC000F8F37 /* client-certs */ = {
+			isa = PBXGroup;
+			children = (
+				22953B30280A9206000F8F37 /* HostFormModel+ClientCerts.swift */,
+				22953B32280A92D8000F8F37 /* ClientCertsHelpView.swift */,
+			);
+			path = "client-certs";
+			sourceTree = "<group>";
+		};
 		22B46BCB27CA640A0077FB83 /* Recovered References */ = {
 			isa = PBXGroup;
 			children = (
@@ -935,8 +949,13 @@ 				22B46BDE27CA65750077FB83 /* SearchTests.swift */,
 				227D987E27B7A0EB00E45C4C /* AboutDialogTests.swift */,
 				227D987827B7A01300E45C4C /* ReadStateTests.swift */,
 				2200D96527C0C3BF00E63E89 /* PublishTests.swift */,
+				22B90E872805B2DE0083C6E5 /* SubscriptionTests.swift */,
 				227D987A27B7A05C00E45C4C /* ScreenshotTests.swift */,
+				22117B6E2805C93300D0117D /* AbstractConfigurationTests.swift */,
+				22AB2C142801D8A400E88875 /* ConfigurationMQTTTests.swift */,
+				22117B702805C95A00D0117D /* ConfigurationWebSocketTests.swift */,
 				22C7F0D42416A16600534880 /* Info.plist */,
+				222AC32F2803350600D66334 /* TestServer.swift */,
 			);
 			path = MQTTAnalyzerUITests;
 			sourceTree = "<group>";
@@ -973,7 +992,6 @@ 			children = (
 				2203571D2445850F00A98CD3 /* cocoamqtt */,
 				22FD7CF822C8D2650078795F /* MQTTSessionController.swift */,
 				226A6B4F244457E100ACDFC3 /* MqttClient.swift */,
-				226A6B5324448ECB00ACDFC3 /* MqttClientSharedUtils.swift */,
 			);
 			path = mqtt;
 			sourceTree = "<group>";
@@ -1313,8 +1331,6 @@ 				22A386FB240941B600DF8F94 /* CertificateFilePickerView.swift in Sources */,
 				22F3AB1324A77B6600B2CF92 /* DataMigrationMoscapsuleDeprecation.swift in Sources */,
 				22FD7D0222C8D2660078795F /* RootModel.swift in Sources */,
 		2256BDAA246FBE5100F92EFE /* DataMigrationEmptyTopic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2256BDA9246FBE5100F92EFE /* DataMigrationEmptyTopic.swift */; };
-	classes = {
-		2256BDAA246FBE5100F92EFE /* DataMigrationEmptyTopic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2256BDA9246FBE5100F92EFE /* DataMigrationEmptyTopic.swift */; };
 	};
 				224AFEAF27B0600D00DFD09F /* StubPersistence.swift in Sources */,
 				22C2856224759FD40000C1E8 /* CertificateLocationPicker.swift in Sources */,
@@ -1328,7 +1344,6 @@ 				223AF5D72477D60A009810E6 /* PKCS12HelpView.swift in Sources */,
 				2281048A2381740000112F24 /* MessageView.swift in Sources */,
 				2253F8D822C8C007007E35A2 /* SceneDelegate.swift in Sources */,
 				2256BDAA246FBE5100F92EFE /* DataMigrationEmptyTopic.swift in Sources */,
-				2230FEF627A435A000D4327F /* NavigationPicker.swift in Sources */,
 				2230FEF827A4434800D4327F /* DataMigrationNavigationMode.swift in Sources */,
 				22E469D5237FBEC500D72BD6 /* ReadMarkerView.swift in Sources */,
 				2291283C24681CAD006F8256 /* SubscriptionDetailsView.swift in Sources */,
@@ -1345,13 +1360,10 @@ 				2230FF2927A6BBB800D4327F /* TitleView.swift in Sources */,
 				22C9F73123B78F9700892C4B /* PublishMessageFormView.swift in Sources */,
 				2253F8DD22C8C007007E35A2 /* TopicsView.swift in Sources */,
 		226A6B5224445BA400ACDFC3 /* CocoaMQTTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B5124445BA400ACDFC3 /* CocoaMQTTClient.swift */; };
-	classes = {
-		226A6B5224445BA400ACDFC3 /* CocoaMQTTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B5124445BA400ACDFC3 /* CocoaMQTTClient.swift */; };
 	};
 				228104852381727B00112F24 /* MessageDetailsPlainTextView.swift in Sources */,
 				2230FF2D27A6BC8900D4327F /* InformationContainerView.swift in Sources */,
 				2285C82527887F79008DA37D /* TopicSuffixPickerView.swift in Sources */,
-				221C571C2466847800C0DD02 /* QuestionBox.swift in Sources */,
 				229FCAED247D67BC00490628 /* DocumentPickerView.swift in Sources */,
 				221C571E2466C9CD00C0DD02 /* HostFormModel+AWS.swift in Sources */,
 				22FD7D0322C8D2660078795F /* HostsView.swift in Sources */,
@@ -1359,7 +1371,6 @@ 				22A386F724093EA200DF8F94 /* UsernamePasswordAuthenticationView.swift in Sources */,
 				2285C81A27842176008DA37D /* DisconnectedView.swift in Sources */,
 				22A386F92409404600DF8F94 /* CertificateAuthenticationView.swift in Sources */,
 				2291284324685959006F8256 /* MigrationHelper.swift in Sources */,
-				2230FF3127A8304E00D4327F /* Array+Remove.swift in Sources */,
 				2291284724685A92006F8256 /* DataMigrationAuth.swift in Sources */,
 				2263C1D924813B0C00FF14E8 /* CertificateFilesModel.swift in Sources */,
 				22FD7D0522C8D2820078795F /* DataSeriesDetailsView.swift in Sources */,
@@ -1375,6 +1386,7 @@ 				228104932381773100112F24 /* TopicCellView.swift in Sources */,
 				228104832381723D00112F24 /* MessageDetailsJsonView.swift in Sources */,
 				2230FF2F27A6CDB800D4327F /* Welcome.swift in Sources */,
 				22E469D7237FC03500D72BD6 /* Readstate.swift in Sources */,
+				22AB2C112800771700E88875 /* CocoaMQTTClient+ErrorMessage.swift in Sources */,
 				2231894B27F08BAB001177F4 /* ViewSelection.swift in Sources */,
 				2285C80C2781E89B008DA37D /* LimitReachedView.swift in Sources */,
 				2291283E24682494006F8256 /* TopicCell.swift in Sources */,
@@ -1384,6 +1396,7 @@ 				222EAAB327A31849000E37AF /* PropertyImageProvider.swift in Sources */,
 				22FD7CFE22C8D2660078795F /* MessageDetailsView.swift in Sources */,
 				22AF3AEB23891267001D9F87 /* HostSettingExamples.swift in Sources */,
 				226A6B50244457E100ACDFC3 /* MqttClient.swift in Sources */,
+				22953B33280A92D8000F8F37 /* ClientCertsHelpView.swift in Sources */,
 				22F6057B23D4911000E6338B /* DataMigration.swift in Sources */,
 				2285C81C27847786008DA37D /* MetadataView.swift in Sources */,
 				2291284524685A13006F8256 /* DataMigrationClientImpl.swift in Sources */,
@@ -1407,8 +1420,10 @@ 				22C9F73723B79A1300892C4B /* MessageTextView.swift in Sources */,
 				2285C81227841C72008DA37D /* ResumeConnectionView.swift in Sources */,
 				223AF5D92477D620009810E6 /* NoFilesHelpView.swift in Sources */,
 				223AF5D52477D5F5009810E6 /* FileItemView.swift in Sources */,
-		226A6B632445759600ACDFC3 /* ClientIDFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A6B622445759600ACDFC3 /* ClientIDFormView.swift */; };
+		2200D96B27C1123000E63E89 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200D96A27C1123000E63E89 /* Logger.swift */; };
 	};
+	archiveVersion = 1;
+				22953B31280A9206000F8F37 /* HostFormModel+ClientCerts.swift in Sources */,
 				226A6B632445759600ACDFC3 /* ClientIDFormView.swift in Sources */,
 				226A6B67244575DB00ACDFC3 /* AuthFormView.swift in Sources */,
 				2230FF3327A830C900D4327F /* DataProtocol+Hex.swift in Sources */,
@@ -1433,6 +1448,8 @@ 				221C57202466CC2800C0DD02 /* AWSIOTPresetTests.swift in Sources */,
 				22A387052409768100DF8F94 /* HostModelPersistenceTests.swift in Sources */,
 				2230FF2427A6B80900D4327F /* TopicLimitTests.swift in Sources */,
 // !$*UTF8*$!
+		2230FF2C27A6BC8900D4327F /* InformationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationContainerView.swift; sourceTree = "<group>"; };
+// !$*UTF8*$!
 		2200D96D27C1149300E63E89 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200D96C27C1149300E63E89 /* LoggerTests.swift */; };
 				2230FF1027A6511F00D4327F /* TreeUtilsTests.swift in Sources */,
 				22E914DF25A8420700BEC599 /* HostFormModelTests.swift in Sources */,
@@ -1457,6 +1474,8 @@ 			buildActionMask = 2147483647;
 			files = (
 				22B46BDA27CA652B0077FB83 /* Brokers.swift in Sources */,
 // !$*UTF8*$!
+		2230FF2E27A6CDB800D4327F /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; };
+// !$*UTF8*$!
 		221C571C2466847800C0DD02 /* QuestionBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571B2466847800C0DD02 /* QuestionBox.swift */; };
 				2240834927AC533A00AA4A42 /* SnapshotHelper.swift in Sources */,
 				22061AC327CA6BD300FFF915 /* XCUIElement+Checked.swift in Sources */,
@@ -1466,10 +1485,16 @@ 				22B46BDF27CA65750077FB83 /* SearchTests.swift in Sources */,
 				22B46BD627CA652B0077FB83 /* ExampleMessages.swift in Sources */,
 				227D987B27B7A05C00E45C4C /* ScreenshotTests.swift in Sources */,
 // !$*UTF8*$!
+		2230FF3027A8304E00D4327F /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
+				22B90E882805B2DE0083C6E5 /* SubscriptionTests.swift in Sources */,
+// !$*UTF8*$!
 		2230FEF627A435A000D4327F /* NavigationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF527A435A000D4327F /* NavigationPicker.swift */; };
 				2200D97B27C2675300E63E89 /* AbstractUITests.swift in Sources */,
 // !$*UTF8*$!
+		2231894A27F08BAB001177F4 /* ViewSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewSelection.swift; sourceTree = "<group>"; };
+// !$*UTF8*$!
 	classes = {
+		2285C82327887CDC008DA37D /* PublishMessageFormModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2285C82227887CDC008DA37D /* PublishMessageFormModelTests.swift */; };
 				22061AC127CA682F00FFF915 /* Search.swift in Sources */,
 				224AFEB727B2C67900DFD09F /* XCUIElement+ClearText.swift in Sources */,
 				22B46BD727CA652B0077FB83 /* ScreenshotIds.swift in Sources */,
@@ -1635,8 +1660,8 @@ 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_ENTITLEMENTS = MQTTAnalyzer/MQTTAnalyzer.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 // !$*UTF8*$!
+		2209C86C23B720E7007C1D93 /* HostValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2209C86B23B720E7007C1D93 /* HostValidator.swift */; };
 // !$*UTF8*$!
-		2230FEFA27A50C8000D4327F /* TreeModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF927A50C8000D4327F /* TreeModelTests.swift */; };
 				DEAD_CODE_STRIPPING = NO;
 				DEVELOPMENT_ASSET_PATHS = "MQTTAnalyzer/Preview\\ Content";
 				DEVELOPMENT_TEAM = 643R6YSRER;
@@ -1648,7 +1673,7 @@ 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
 // !$*UTF8*$!
-		22AE64362412637A00C2C4FE /* TimeSeriesValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AE64352412637A00C2C4FE /* TimeSeriesValue.swift */; };
+		223AF5D42477D5F5009810E6 /* FileItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileItemView.swift; sourceTree = "<group>"; };
 				PRODUCT_BUNDLE_IDENTIFIER = de.rnd7.MQTTAnalyzer;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SUPPORTS_MACCATALYST = YES;
@@ -1665,8 +1690,8 @@ 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_ENTITLEMENTS = MQTTAnalyzer/MQTTAnalyzer.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 // !$*UTF8*$!
+		2209C86C23B720E7007C1D93 /* HostValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2209C86B23B720E7007C1D93 /* HostValidator.swift */; };
 // !$*UTF8*$!
-		2230FEFA27A50C8000D4327F /* TreeModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2230FEF927A50C8000D4327F /* TreeModelTests.swift */; };
 				DEAD_CODE_STRIPPING = NO;
 				DEVELOPMENT_ASSET_PATHS = "MQTTAnalyzer/Preview\\ Content";
 				DEVELOPMENT_TEAM = 643R6YSRER;
@@ -1678,7 +1703,7 @@ 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
 // !$*UTF8*$!
-		22AE64362412637A00C2C4FE /* TimeSeriesValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AE64352412637A00C2C4FE /* TimeSeriesValue.swift */; };
+		223AF5D42477D5F5009810E6 /* FileItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileItemView.swift; sourceTree = "<group>"; };
 				PRODUCT_BUNDLE_IDENTIFIER = de.rnd7.MQTTAnalyzer;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SUPPORTS_MACCATALYST = YES;




diff --git a/src/MQTTAnalyzer.xcodeproj/xcshareddata/xcschemes/MQTTAnalyzer.xcscheme b/src/MQTTAnalyzer.xcodeproj/xcshareddata/xcschemes/MQTTAnalyzer.xcscheme
index af2e35898d54d0a5b27d2967ba21664924590c76..a7b947968ab35626a1362db02694c58dd8f49f04 100644
--- a/src/MQTTAnalyzer.xcodeproj/xcshareddata/xcschemes/MQTTAnalyzer.xcscheme
+++ b/src/MQTTAnalyzer.xcodeproj/xcshareddata/xcschemes/MQTTAnalyzer.xcscheme
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
    LastUpgradeVersion = "1300"
-   version = "1.3">
+   version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"
       buildImplicitDependencies = "YES">
@@ -42,6 +42,18 @@             value = ""
             isEnabled = "YES">
          </AdditionalOption>
       </AdditionalOptions>
+      <TestPlans>
+         <TestPlanReference
+            reference = "container:TestPlanThreads.xctestplan">
+         </TestPlanReference>
+         <TestPlanReference
+            reference = "container:UnitTestPlan.xctestplan">
+         </TestPlanReference>
+         <TestPlanReference
+            reference = "container:ReleaseTestPlan.xctestplan"
+            default = "YES">
+         </TestPlanReference>
+      </TestPlans>
       <Testables>
          <TestableReference
             skipped = "NO">




diff --git a/src/MQTTAnalyzer.xcworkspace/contents.xcworkspacedata b/src/MQTTAnalyzer.xcworkspace/contents.xcworkspacedata
index 4375d9683343d39af80577b61cc59e24c3254249..1a32435bbbb4f7f0bf04a07d402401f58a30a112 100644
--- a/src/MQTTAnalyzer.xcworkspace/contents.xcworkspacedata
+++ b/src/MQTTAnalyzer.xcworkspace/contents.xcworkspacedata
@@ -7,4 +7,20 @@    
    <FileRef
       location = "group:Pods/Pods.xcodeproj">
    </FileRef>
+   <Group
+      location = "container:"
+      name = "TestPlans">
+      <FileRef
+         location = "group:/Users/philipparndt/dev/smarthome/projects/mqtt-analyzer/src/TestPlan.xctestplan">
+      </FileRef>
+      <FileRef
+         location = "group:TestPlanThreads.xctestplan">
+      </FileRef>
+      <FileRef
+         location = "group:UnitTestPlan.xctestplan">
+      </FileRef>
+      <FileRef
+         location = "group:ReleaseTestPlan.xctestplan">
+      </FileRef>
+   </Group>
 </Workspace>




diff --git a/src/MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/src/MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved
index a2ea5c3ba8f5daeca43afa5b290962f8bba36e69..b868ca01e763648997a8318129eaaa14c48cae5c 100644
--- a/src/MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/src/MQTTAnalyzer.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,22 +1,24 @@
 {
-  "object": {
-    "pins": [
-      {
-        "package": "Dynamic",
-        "repositoryURL": "https://github.com/mhdhejazi/Dynamic.git",
+{
         "state": {
+{
           "branch": null,
+{
           "revision": "ab9a2570862d54aed2663691bb767f881226a12f",
-{
+  "object": {
-{
+  "object": {
 {
-{
+  "object": {
   "object": {
-{
+  "object": {
     "pins": [
-{
+  "object": {
       {
 {
+  "object": {
+  "object": {
         "package": "Dynamic",
-{
+  "object": {
         "repositoryURL": "https://github.com/mhdhejazi/Dynamic.git",
+  "version" : 2
+}




diff --git a/src/MQTTAnalyzerTests/AWSIOTPresetTests.swift b/src/MQTTAnalyzerTests/AWSIOTPresetTests.swift
index 416f2206e66f46884e064b52b9662b3aa34a0fd6..d74f22c14a1efd839f5d489ea843d3a8611cd8e6 100644
--- a/src/MQTTAnalyzerTests/AWSIOTPresetTests.swift
+++ b/src/MQTTAnalyzerTests/AWSIOTPresetTests.swift
@@ -15,25 +15,26 @@
 	func testNoSuggestChangeForOtherHosts() {
 		var model = HostFormModel()
 		model.hostname = "piiot"
+//  MQTTAnalyzerTests
 //
-//  Created by Philipp Arndt on 2020-05-09.
 		model.hostname = "test.mosquitto.org"
+//  MQTTAnalyzerTests
 //
-//  Created by Philipp Arndt on 2020-05-09.
 	}
 	
 	func testSuggestChange() {
 		var model = HostFormModel()
 		model.hostname = "1234-ats.iot.some.amazonaws.com"
+//  MQTTAnalyzerTests
 //  AWSIOTPresetTests.swift
 	}
 	
 	func testNoSuggestChangeAfterApply() {
 		var model = HostFormModel()
 		model.hostname = "1234-ats.iot.some.amazonaws.com"
 		model.updateSettingsForAWSIOT()
+//  MQTTAnalyzerTests
 //
-//  Created by Philipp Arndt on 2020-05-09.
 	}
 	
 	func testSettingsAfterApply() {




diff --git a/src/MQTTAnalyzerTests/CocoaMQTTRegression.swift b/src/MQTTAnalyzerTests/CocoaMQTTRegression.swift
index 770672e1b4c094f3fa6cb63c320fe9fad9208c94..806dd137f6cb957f895cfda54955805125165405 100644
--- a/src/MQTTAnalyzerTests/CocoaMQTTRegression.swift
+++ b/src/MQTTAnalyzerTests/CocoaMQTTRegression.swift
@@ -12,7 +12,7 @@
 class CocoaMQTTRegressionTests: XCTestCase {
 	func testDecodeBinary789c8d() {
 //
-//
+class CocoaMQTTRegressionTests: XCTestCase {
 		
 		let publish = MqttDecodePublish()
 		publish.decodePublish(fixedHeader: 49, publishData:




diff --git a/src/MQTTAnalyzerTests/Info.plist b/src/MQTTAnalyzerTests/Info.plist
index be5b286e45ac2d540e98c5fec4c906a8b066dbcc..ce04f3fd7a74c403c42526948ec988648d63616d 100644
--- a/src/MQTTAnalyzerTests/Info.plist
+++ b/src/MQTTAnalyzerTests/Info.plist
@@ -17,5 +18,6 @@ 	CFBundleShortVersionString
 	<string>1.0</string>
 	<key>CFBundleVersion</key>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
 </dict>
 </plist>




diff --git a/src/MQTTAnalyzerTests/MqttClientCocoaMQTTTests.swift b/src/MQTTAnalyzerTests/MqttClientCocoaMQTTTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6f65598cda013f3d71be720939ed571c85dc2413
--- /dev/null
+++ b/src/MQTTAnalyzerTests/MqttClientCocoaMQTTTests.swift
@@ -0,0 +1,35 @@
+//
+//  MqttClientCocoaMQTTTests.swift
+//  MQTTAnalyzerTests
+//
+//  Created by Philipp Arndt on 08.04.22.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import XCTest
+import Network
+@testable import MQTTAnalyzer
+
+class MqttClientCocoaMQTTTests: XCTestCase {
+	 
+	func testInvalidHostname() throws {
+		let msg = MqttClientCocoaMQTT.extractErrorMessage(error: NSError(domain: "", code: 8))
+		XCTAssertEqual("Invalid hostname.\nThe operation couldn’t be completed. ( error 8.)", msg)
+	}
+	
+    func testConnectionRefused() throws {
+		let msg = MqttClientCocoaMQTT.extractErrorMessage(error: NWError.posix(POSIXErrorCode.ECONNREFUSED))
+		XCTAssertTrue(msg.contains("Connection refused"), msg)
+	}
+	
+	func testConnectionTLS() throws {
+		let msg = MqttClientCocoaMQTT.extractErrorMessage(error: NWError.tls(-9407))
+		XCTAssertEqual("Network.NWError: -9407: Optional(OSStatus -9407)", msg)
+	}
+	
+	func testConnectionTLSBadCertificate() throws {
+		let msg = MqttClientCocoaMQTT.extractErrorMessage(error: NWError.tls(-9808))
+		XCTAssertEqual("Bad certificate format, check all properties, like SAN, ... (-9808)", msg)
+	}
+
+}




diff --git a/src/MQTTAnalyzerTests/TreeModelTests.swift b/src/MQTTAnalyzerTests/TreeModelTests.swift
index 4884fea6a76f07bc71eb7f78a3145936aaf2f4ec..cffb6cd5d94d2fc64d8bb1299e2b7790d3555f61 100644
--- a/src/MQTTAnalyzerTests/TreeModelTests.swift
+++ b/src/MQTTAnalyzerTests/TreeModelTests.swift
@@ -156,4 +156,16 @@
 		XCTAssertFalse(msg.topic.readStateCombined)
 		XCTAssertFalse(root.readStateCombined)
 	}
+	
+	func testRegressionTopicStartsWithSlash() throws {
+		let root = TopicTree()
+		let withSlash = root.addTopic(topic: "/test")!
+		let withoutSlash = root.addTopic(topic: "test")!
+		let withMultipleSlashes = root.addTopic(topic: "///test")!
+		XCTAssertEqual("/test", withSlash.nameQualified)
+		XCTAssertEqual("test", withoutSlash.nameQualified)
+		XCTAssertEqual("///test", withMultipleSlashes.nameQualified)
+		XCTAssertEqual(2, root.children.count)
+	}
+	
 }




diff --git a/src/MQTTAnalyzerUITests/AbstractConfigurationTests.swift b/src/MQTTAnalyzerUITests/AbstractConfigurationTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..13f4eff5f960ad3c24166647ed6df8e296c1120a
--- /dev/null
+++ b/src/MQTTAnalyzerUITests/AbstractConfigurationTests.swift
@@ -0,0 +1,39 @@
+//
+//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
+//
+//  Created by Philipp Arndt on 2022-02-12.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import XCTest
+import RealmSwift
+
+class AbstractConfigurationTests: AbstractUITests {
+	let hostname = TestServer.getTestServer()
+
+	func assertWithBroker(_ broker: Broker, credentials: Credentials? = nil) {
+		let id = Navigation.id()
+		let brokers = Brokers(app: app)
+
+		app.launch()
+		
+		let nav = Navigation(app: app, alias: broker.alias!)
+		
+		brokers.create(broker: broker)
+		brokers.start(alias: broker.alias!, waitConnected: false)
+		
+		if let credentials = credentials {
+			brokers.login(credentials: credentials)
+		}
+		
+		brokers.waitUntilConnected()
+
+		let dialog = PublishDialog(app: app)
+		dialog.open()
+		dialog.fill(topic: "\(id)topic", message: "msg")
+		dialog.apply()
+		
+		nav.navigate(to: "\(id)topic")
+	}
+}




diff --git a/src/MQTTAnalyzerUITests/BrokerTests.swift b/src/MQTTAnalyzerUITests/BrokerTests.swift
index 0eea5540cfa51c1d42847123471c0a7b1d5cad2e..c0e71fea478c0a3347d52349bbf8c052a64e2596 100644
--- a/src/MQTTAnalyzerUITests/BrokerTests.swift
+++ b/src/MQTTAnalyzerUITests/BrokerTests.swift
@@ -36,7 +36,7 @@ 		awaitAppear(element: example)
 		brokers.delete(alias: alias)
 		brokers.cancelDelete(alias: alias)
 		XCTAssertFalse(app.buttons["Delete"].exists)
-		XCTAssertTrue(example.exists)
+		XCTAssertTrue(example.exists, "Expected example to be still there")
 	}
 	
 	func testRenameBroker() {




diff --git a/src/MQTTAnalyzerUITests/ConfigurationMQTTTests.swift b/src/MQTTAnalyzerUITests/ConfigurationMQTTTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b944a414f292c1ea5d277ae6f2c3e84d8bfc44f0
--- /dev/null
+++ b/src/MQTTAnalyzerUITests/ConfigurationMQTTTests.swift
@@ -0,0 +1,60 @@
+//
+//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
+//
+//  Created by Philipp Arndt on 2022-02-12.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import XCTest
+import RealmSwift
+
+class ConfigurationMQTTTests: AbstractConfigurationTests {
+	func testMQTTNoAuth() {
+		assertWithBroker(
+			Broker(
+				alias: "1883",
+				hostname: hostname,
+				port: 1883
+			)
+		)
+	}
+	
+	func testMQTTPersistedAuth() {
+		assertWithBroker(
+			Broker(
+				alias: "1884",
+				hostname: hostname,
+				port: 1884,
+				authType: .userPassword,
+				username: "admin",
+				password: "password"
+			)
+		)
+	}
+	
+	func testMQTTPersistedUsername() {
+		assertWithBroker(
+			Broker(
+				alias: "1884",
+				hostname: hostname,
+				port: 1884,
+				authType: .userPassword,
+				username: "admin"
+			),
+			credentials: Credentials(username: nil, password: "password")
+		)
+	}
+	
+	func testMQTTLetsEncryptTraefik() {
+		assertWithBroker(
+			Broker(
+				alias: "LE MQTTS",
+				hostname: hostname,
+				port: 8883,
+				connectionProtocol: .mqtt,
+				tls: true
+			)
+		)
+	}
+}




diff --git a/src/MQTTAnalyzerUITests/ConfigurationWebSocketTests.swift b/src/MQTTAnalyzerUITests/ConfigurationWebSocketTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9981dd16c301a376c096dbd0c3e9f40774a1a401
--- /dev/null
+++ b/src/MQTTAnalyzerUITests/ConfigurationWebSocketTests.swift
@@ -0,0 +1,63 @@
+//
+//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
+//
+//  Created by Philipp Arndt on 2022-02-12.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import XCTest
+import RealmSwift
+
+class ConfigurationWebSocketTests: AbstractConfigurationTests {
+	func testWebSocketLetsEncryptTraefik() {
+		assertWithBroker(
+			Broker(
+				alias: "LE WSS",
+				hostname: hostname,
+				port: 443,
+				connectionProtocol: .websocket,
+				tls: true
+			)
+		)
+	}
+	
+	func testWebSocket() {
+		assertWithBroker(
+			Broker(
+				alias: "WebSocket",
+				hostname: hostname,
+				port: 9001,
+				connectionProtocol: .websocket
+			)
+		)
+	}
+	
+	func testWebSocketPersistedAuth() {
+		assertWithBroker(
+			Broker(
+				alias: "9002",
+				hostname: hostname,
+				port: 9002,
+				connectionProtocol: .websocket,
+				authType: .userPassword,
+				username: "admin",
+				password: "password"
+			)
+		)
+	}
+	
+	func testWebSocketPersistedUsername() {
+		assertWithBroker(
+			Broker(
+				alias: "9002",
+				hostname: hostname,
+				port: 9002,
+				connectionProtocol: .websocket,
+				authType: .userPassword,
+				username: "admin"
+			),
+			credentials: Credentials(username: nil, password: "password")
+		)
+	}
+}




diff --git a/src/MQTTAnalyzerUITests/FlatViewTests.swift b/src/MQTTAnalyzerUITests/FlatViewTests.swift
index 2f219cd2353edbfe254e936149f367c936717099..75314aa6ee3d40477cf78cbbab4686260d7b3689 100644
--- a/src/MQTTAnalyzerUITests/FlatViewTests.swift
+++ b/src/MQTTAnalyzerUITests/FlatViewTests.swift
@@ -12,12 +12,12 @@ class FlatViewTests: AbstractUITests {
 	func testFlatView() {
 		let brokers = Brokers(app: app)
 		
-//
 //  MQTTAnalyzerUITests+Broker.swift
+import XCTest
 		let alias = "Example"
 		let id = Navigation.id()
 		
-		let examples = ExampleMessages(hostname: hostname)
+		let examples = ExampleMessages(broker: Broker(alias: nil, hostname: hostname))
 		app.launch()
 		brokers.start(alias: alias)
 		examples.publish(prefix: id)




diff --git a/src/MQTTAnalyzerUITests/Info.plist b/src/MQTTAnalyzerUITests/Info.plist
index be5b286e45ac2d540e98c5fec4c906a8b066dbcc..ce04f3fd7a74c403c42526948ec988648d63616d 100644
--- a/src/MQTTAnalyzerUITests/Info.plist
+++ b/src/MQTTAnalyzerUITests/Info.plist
@@ -17,5 +18,6 @@ 	CFBundleShortVersionString
 	<string>1.0</string>
 	<key>CFBundleVersion</key>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
 </dict>
 </plist>




diff --git a/src/MQTTAnalyzerUITests/PublishTests.swift b/src/MQTTAnalyzerUITests/PublishTests.swift
index 6021e9771ed6391ccd1879b476ffa1db75976e82..834366f142422535a81b0cdd0040dee6ba192b53 100644
--- a/src/MQTTAnalyzerUITests/PublishTests.swift
+++ b/src/MQTTAnalyzerUITests/PublishTests.swift
@@ -13,12 +13,12 @@ 	
 	func testPublish() {
 		let brokers = Brokers(app: app)
 		
-//
 //  MQTTAnalyzerUITests
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
 		let alias = "Example"
 		let id = Navigation.id()
 
-//
+//  MQTTAnalyzerUITests
 
 		app.launch()
 
@@ -29,29 +29,29 @@ 		let nav = Navigation(app: app, alias: alias)
 		nav.navigate(to: id)
 		
 		XCTAssertTrue(app.staticTexts["hue"]
-//  MQTTAnalyzerUITests+Broker.swift
 //  MQTTAnalyzerUITests
+import XCTest
 
 		let dialog = PublishDialog(app: app)
 		dialog.open()
 		dialog.fill(topic: "\(id)topic", message: "msg")
 		dialog.apply()
 		
-//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
 class PublishTests: AbstractUITests {
-//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
 	
 	}
 	
 	func testPublishWhileWait() {
 		let brokers = Brokers(app: app)
 		
-//
 //  MQTTAnalyzerUITests
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
 		let alias = "Example"
 		let id = Navigation.id()
 
-//
+//  MQTTAnalyzerUITests
 
 		app.launch()
 
@@ -63,15 +63,15 @@ 		nav.navigate(to: id)
 		
 		MessageTopicUtils.clearAll(app: app)
 		
-		XCTAssertTrue(app.staticTexts["Waiting for messages"].waitForExistence(timeout: 4))
+		XCTAssertTrue(app.staticTexts["Waiting for messages"].waitForExistence(timeout: 4), "Expected waiting for messages to be there")
 		
 		let dialog = PublishDialog(app: app)
 		dialog.open()
 		dialog.fill(topic: "\(id)topic", message: "msg")
 		dialog.apply()
 		
-		XCTAssertTrue(app.staticTexts["Inherited Message Groups"].waitForExistence(timeout: 4))
+		XCTAssertTrue(app.staticTexts["Inherited Message Groups"].waitForExistence(timeout: 4), "Expected inherited messages groups to be there")
-//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
 	
 	}
 }




diff --git a/src/MQTTAnalyzerUITests/ReadStateTests.swift b/src/MQTTAnalyzerUITests/ReadStateTests.swift
index 7ab77325891223a36a6f9253c95f7d70c3693ae1..52f8849baa43c93b2cd6fe4b186bf20bcd31ff9b 100644
--- a/src/MQTTAnalyzerUITests/ReadStateTests.swift
+++ b/src/MQTTAnalyzerUITests/ReadStateTests.swift
@@ -13,11 +13,11 @@ 	
 	func testMarkRead() {
 		let brokers = Brokers(app: app)
 		
-		let hostname = "localhost"
+		let hostname = TestServer.getTestServer()
 		let alias = "Example"
 		let id = Navigation.id()
 
-		let examples = ExampleMessages(hostname: hostname)
+		let examples = ExampleMessages(broker: Broker(alias: nil, hostname: hostname))
 		app.launch()
 
 		brokers.start(alias: alias)
@@ -34,8 +34,8 @@ 		nav.navigate(to: "\(id)hue/light")
 		let office = nav.getReadMarker(topic: "\(id)hue/light/office")
 		let kitchen = nav.getReadMarker(topic: "\(id)hue/light/office")
 		
-		XCTAssertTrue(office.firstMatch.exists)
+		XCTAssertTrue(office.firstMatch.exists, "Expected office to be there")
-		XCTAssertTrue(kitchen.firstMatch.exists)
+		XCTAssertTrue(kitchen.firstMatch.exists, "Expected kitche to be there")
 		
 		MessageTopicUtils.markAllAsRead(app: app)
 
@@ -47,19 +47,19 @@ 		let light = nav.getReadMarker(topic: "\(id)hue/light")
 		XCTAssertFalse(light.exists)
 		
 		nav.navigate(to: "\(id)")
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
 //  MQTTAnalyzerUITests
-import XCTest
-		XCTAssertFalse(hue.exists)
+		XCTAssertFalse(hue.exists, "Expected hue to be not there")
 	}
 	
 	func testClear() {
 		let brokers = Brokers(app: app)
 		
-		let hostname = "localhost"
+		let hostname = TestServer.getTestServer()
 		let alias = "Example"
 		let id = Navigation.id()
 
-		let examples = ExampleMessages(hostname: hostname)
+		let examples = ExampleMessages(broker: Broker(alias: nil, hostname: hostname))
 		app.launch()
 
 		brokers.start(alias: alias)
@@ -75,7 +75,7 @@ 		MessageTopicUtils.clearAll(app: app)
 		
 		nav.navigate(to: id)
 		let home = nav.getReadMarker(topic: "\(id)home")
-		XCTAssertTrue(homeFolder.staticTexts["0/0"].exists)
+		XCTAssertTrue(homeFolder.staticTexts["0/0"].exists, "Expected 0/0 to be there")
-			XCTAssertFalse(home.exists)
+		XCTAssertFalse(home.exists, "Expected brokerCell home to be not there")
 	}
 }




diff --git a/src/MQTTAnalyzerUITests/ScreenshotTests.swift b/src/MQTTAnalyzerUITests/ScreenshotTests.swift
index 031b32c1934d8e8cc178092d2fa1065794d0b41f..303533a11cf594244d9e93a8dbe508f228c32a80 100644
--- a/src/MQTTAnalyzerUITests/ScreenshotTests.swift
+++ b/src/MQTTAnalyzerUITests/ScreenshotTests.swift
@@ -11,11 +11,11 @@
 class ScreenshotTests: AbstractUITests {
 	//  ~/Library/Containers/de.rnd7.MQTTAnalyzerUITests.xctrunner/Data/screenshots
 	func testFullRoundtripScreenshots() {
-		let hostname = "localhost"
+		let hostname = TestServer.getTestServer()
 		let alias = "Example"
 		let id = Navigation.idSmall()
 		
-		let examples = ExampleMessages(hostname: hostname)
+		let examples = ExampleMessages(broker: Broker(alias: nil, hostname: hostname))
 		let brokers = Brokers(app: app)
 		
 		app.launch()




diff --git a/src/MQTTAnalyzerUITests/SearchTests.swift b/src/MQTTAnalyzerUITests/SearchTests.swift
index a191a78392941040c09b68dab715e4020c4cfc43..563d4d5e35b004eca019bcf41fefd5e816bb5bdb 100644
--- a/src/MQTTAnalyzerUITests/SearchTests.swift
+++ b/src/MQTTAnalyzerUITests/SearchTests.swift
@@ -12,10 +12,10 @@ class SearchTests: AbstractUITests {
 	func startSearch(id: String) -> Navigation {
 		let brokers = Brokers(app: app)
 		
-		let hostname = "localhost"
+		let hostname = TestServer.getTestServer()
 		let alias = "Example"
 		
-//
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
 //  Created by Philipp Arndt on 2022-02-12.
 		app.launch()
 
@@ -62,11 +62,11 @@ 	
 	func testSearchIsUpdated() {
 		let brokers = Brokers(app: app)
 		
-		let hostname = "localhost"
+		let hostname = TestServer.getTestServer()
 		let alias = "Example"
 		let id = Navigation.id()
 		
-//
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
 //  Created by Philipp Arndt on 2022-02-12.
 		app.launch()
 		




diff --git a/src/MQTTAnalyzerUITests/SubscriptionTests.swift b/src/MQTTAnalyzerUITests/SubscriptionTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b7869ad36e5ad831c0d213543abb5aff32b968f1
--- /dev/null
+++ b/src/MQTTAnalyzerUITests/SubscriptionTests.swift
@@ -0,0 +1,28 @@
+//
+//  MQTTAnalyzerUITests+Broker.swift
+//  MQTTAnalyzerUITests
+//
+//  Created by Philipp Arndt on 2022-02-12.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import XCTest
+
+class SubscriptionTests: AbstractUITests {
+	func testSubscribeToSYS() {
+		let brokers = Brokers(app: app)
+		
+		let alias = "Example"
+		app.launch()
+
+		brokers.startEdit(alias: alias)
+		brokers.addSubscriptionToCurrentBroker(topic: "$SYS/#")
+		brokers.deleteSubscriptionFromCurrentBroker(topic: "#")
+		brokers.save()
+		
+		brokers.start(alias: alias)
+
+		let nav = Navigation(app: app, alias: alias)
+		nav.navigate(to: "$SYS")
+	}
+}




diff --git a/src/MQTTAnalyzerUITests/TestServer.swift b/src/MQTTAnalyzerUITests/TestServer.swift
new file mode 100644
index 0000000000000000000000000000000000000000..915ee684196eebe7eccc0011c447cd9528c9ba6c
--- /dev/null
+++ b/src/MQTTAnalyzerUITests/TestServer.swift
@@ -0,0 +1,15 @@
+//
+//  TestServer.swift
+//  MQTTAnalyzerUITests
+//
+//  Created by Philipp Arndt on 10.04.22.
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+
+class TestServer {
+	static func getTestServer() -> String {
+		return "test.mqtt.rnd7.de"
+	}
+}




diff --git a/src/MQTTAnalyzerUITests/extensions/XCTestCase+Disappear.swift b/src/MQTTAnalyzerUITests/extensions/XCTestCase+Disappear.swift
index c6cd9f6a4695475a6086f72109b0ac4cb1d76cee..bf90aa567f8c3da9e880ec93477b5dbb40f1077a 100644
--- a/src/MQTTAnalyzerUITests/extensions/XCTestCase+Disappear.swift
+++ b/src/MQTTAnalyzerUITests/extensions/XCTestCase+Disappear.swift
@@ -12,7 +12,7 @@
 extension XCTestCase {
 	
 	func awaitAppear(element: XCUIElement) {
-//
+//  XCTestCase+Disappear.swift
 //  XCTestCase+Disappear.swift
 	}
 	
@@ -22,5 +23,6 @@ 					handler: nil)
 		waitForExpectations(timeout: 4, handler: nil)
 		
 //  XCTestCase+Disappear.swift
+//  MQTTAnalyzerUITests
 	}
 }




diff --git a/src/MQTTAnalyzerUITests/extensions/XCUIElement+ClearText.swift b/src/MQTTAnalyzerUITests/extensions/XCUIElement+ClearText.swift
index b4f69665afba975016dcaffc0d2dec90b8cb4f59..a44d5f86598af171ebe98bb9ce253eef1ec4bb62 100644
--- a/src/MQTTAnalyzerUITests/extensions/XCUIElement+ClearText.swift
+++ b/src/MQTTAnalyzerUITests/extensions/XCUIElement+ClearText.swift
@@ -28,12 +28,30 @@ 		
 		return false
 	}
 	
+	func isPlaceholderEqValue() -> Bool {
+		guard let stringValue = self.value as? String else {
+			XCTFail("Tried to clear and enter text into a non string value")
+			return false
+		}
+		
+		if stringValue.isEmpty {
+			return false
+		}
+		
+		// workaround for apple bug
+		if let placeholderString = self.placeholderValue, placeholderString == stringValue {
+			return true
+		}
+		
+		return false
+	}
+	
 	func clear() {
 		#if targetEnvironment(macCatalyst)
 		self.tap()
 		#endif
 		
-		if alreadyClearStrategy() || clearWithDeleteKeyStrategy() || clearWithSelectAllStrategy() {
+		if fastClearStrategy() || clearWithDeleteKeyStrategy() || clearWithSelectAllStrategy() {
 			return
 		}
 		
@@ -44,6 +62,44 @@ 	func clearAndEnterText(text: String) {
 		clear()
 		typeText(text)
 	}
+	
+	func enterTextIfNotAlreadySame(text: String) {
+		guard let stringValue = self.value as? String else {
+			XCTFail("Tried to clear and enter text into a non string value")
+			return
+		}
+		
+		if stringValue != text {
+			clearAndEnterText(text: text)
+		}
+	}
+}
+
+extension XCUIElement {
+	/*
+	 Try a fast clear by typing a single character, verify that it is the
+	 single character and delete it again.
+	 
+	 This is necessary as we cannot distinct between a placeholder value
+	 and the same value entered.
+	 */
+	func fastClearStrategy() -> Bool {
+		if isPlaceholderEqValue() {
+			self.typeText(".")
+			
+			guard let stringValue = self.value as? String else {
+				XCTFail("Tried to clear and enter text into a non string value")
+				return false
+			}
+			
+			if stringValue == "." {
+				self.typeText(XCUIKeyboardKey.delete.rawValue)
+			}
+			return isEmpty()
+		}
+		
+		return false
+	}
 }
 
 extension XCUIElement {
@@ -62,8 +118,9 @@ 		let lowerRightCorner = self.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9))
 		lowerRightCorner.tap()
 		#endif
 		
-//  MQTTAnalyzerUITests
+
 	
+		let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count + sometimesCharactersMissing)
 		self.typeText(deleteString)
 		
 		return isEmpty()




diff --git a/src/MQTTAnalyzerUITests/utils/Broker.swift b/src/MQTTAnalyzerUITests/utils/Broker.swift
index aa10ba9efdc61cd60656f715e7a49a3be2ead948..27ecd17080cf04e17778347c3877b325a77b540f 100644
--- a/src/MQTTAnalyzerUITests/utils/Broker.swift
+++ b/src/MQTTAnalyzerUITests/utils/Broker.swift
@@ -8,7 +8,24 @@ //
 
 import Foundation
 
+enum ConnectionProtocol {
+	case mqtt
+	case websocket
+}
+
+enum AuthType {
+	case none
+	case userPassword
+	case certificate
+}
+
 struct Broker {
 	let alias: String?
 	let hostname: String?
+	var port: UInt16?
+	var connectionProtocol: ConnectionProtocol?
+	var authType: AuthType?
+	var username: String?
+	var password: String?
+	var tls: Bool?
 }




diff --git a/src/MQTTAnalyzerUITests/utils/Brokers.swift b/src/MQTTAnalyzerUITests/utils/Brokers.swift
index 795229f448f4556979b4b9d6f611c40b14fb3c69..da489ad524c1f2ee1cd2c7c99ccae36793b08dd2 100644
--- a/src/MQTTAnalyzerUITests/utils/Brokers.swift
+++ b/src/MQTTAnalyzerUITests/utils/Brokers.swift
@@ -9,6 +9,11 @@
 import Foundation
 import XCTest
 
+struct Credentials {
+	var username: String?
+	var password: String?
+}
+
 class Brokers {
 	let app: XCUIApplication
 	init(app: XCUIApplication) {
@@ -45,33 +50,88 @@ 	func create(broker: Broker) {
 		app.buttons["Add Broker"].tap()
 		
 		if let alias = broker.alias {
+			let field = app.textFields["alias"]
+			field.tap()
+			field.typeText("\(alias)\n")
+		}
+		
+		if let hostname = broker.hostname {
+import XCTest
 //  MQTTAnalyzerUITests
+			field.tap()
+			field.typeText("\(hostname)\n")
+		}
+
+		if let port = broker.port {
+			if port != 1883 {
+import XCTest
 import Foundation
+				field.tap()
+				field.clearAndEnterText(text: "\(port)")
+			}
+		}
+		
+		if let proto = broker.connectionProtocol {
+			let field = app.buttons["\(proto)"]
+			field.tap()
+		}
+		
+		if let tls = broker.tls {
+class Brokers {
 //  MQTTAnalyzerUITests
+				#if targetEnvironment(macCatalyst)
+				let field = app.checkBoxes["tls"]
+				field.click()
+				#else
+				let field = app.switches["tls"]
+import XCTest
 import XCTest
-//  MQTTAnalyzerUITests
+class Brokers {
 class Brokers {
+			}
 		}
 		
-//  Created by Philipp Arndt on 2022-02-04.
+	let app: XCUIApplication
-//  Created by Philipp Arndt on 2022-02-04.
+	let app: XCUIApplication
 //
-//  Created by Philipp Arndt on 2022-02-04.
+			field.tap()
+			
+	let app: XCUIApplication
 //  BrokerForm.swift
+				if let username = broker.username {
+	let app: XCUIApplication
 //  Created by Philipp Arndt on 2022-02-04.
-//  MQTTAnalyzerUITests
+					field.tap()
+					field.typeText(username)
+				}
+				
+				if let password = broker.password {
+					let field = app.secureTextFields["password"]
+					field.tap()
+					field.typeText(password)
+				}
+			}
 		}
 		
 		snapshot(ScreenshotIds.CONFIG)
 		app.buttons["Save"].tap()
 	}
 	
-	func edit(alias oldName: String, broker: Broker) {
+	func startEdit(alias oldName: String) {
 		app.launchMenuAction(
 			on: brokerCell(of: oldName),
 			label: "Edit"
 		)
 //
+//  MQTTAnalyzerUITests
+	
+	func save() {
+		app.buttons["Save"].tap()
+	}
+	
+	func edit(alias oldName: String, broker: Broker) {
+		startEdit(alias: oldName)
+//
 class Brokers {
 		if let alias = broker.alias {
 			let aliasField = app.textFields["alias"]
@@ -83,8 +143,32 @@ 			let hostField = app.textFields["hostname"]
 			hostField.clearAndEnterText(text: "\(hostname)\n")
 		}
 		
+	init(app: XCUIApplication) {
 //  Created by Philipp Arndt on 2022-02-04.
+	}
+	
+	init(app: XCUIApplication) {
 //  Copyright © 2022 Philipp Arndt. All rights reserved.
+		startEdit(alias: alias)
+		addSubscriptionToCurrentBroker(topic: topic)
+		app.buttons["Save"].tap()
+		save()
+	}
+	
+	func addSubscriptionToCurrentBroker(topic: String) {
+		app.buttons["add-subscription"].tap()
+		let field = app.textFields["subscription-topic"]
+		XCTAssertTrue(field.waitForExistence(timeout: 4), "Expected add-subscription button to be there")
+		field.tap()
+		field.clearAndEnterText(text: topic)
+		app.buttons["Edit broker"].tap()
+	}
+	
+	func deleteSubscriptionFromCurrentBroker(topic: String) {
+		app.buttons[topic].tap()
+		let button = app.buttons["Delete"]
+		XCTAssertTrue(button.waitForExistence(timeout: 4), "Expected delete button to be there")
+		button.tap()
 	}
 	
 	func createBasedOn(alias oldName: String, broker: Broker) {
@@ -120,40 +204,65 @@ 		app.buttons["Play"].tap()
 		#endif
 		
 		if waitConnected {
-
+			waitUntilConnected()
+		}
+	}
+	
+	func waitUntilConnected() {
+	}
 //  BrokerForm.swift
-
+		#if targetEnvironment(macCatalyst)
+	}
 //  MQTTAnalyzerUITests
-
+		#else
+	}
 //  Created by Philipp Arndt on 2022-02-04.
-
+		#endif
+	}
 //  Copyright © 2022 Philipp Arndt. All rights reserved.
+	}
 
-
+				return
+			}
-
+	}
 import Foundation
-
+				.waitForExistence(timeout: 2) {
+				return
+			}
+//
 import XCTest
-
+	}
+	
+	}
 class Brokers {
-import Foundation
+	
-import Foundation
 //
+class Brokers {
-import Foundation
+		if let username = credentials.username {
+	
 //  BrokerForm.swift
-import Foundation
+			field.tap()
+	
 //  MQTTAnalyzerUITests
-					return
+		}
+
-import Foundation
 //
+		snapshot(ScreenshotIds.CONFIG)
+	
 //  Copyright © 2022 Philipp Arndt. All rights reserved.
-//  Copyright © 2022 Philipp Arndt. All rights reserved.
+			field.tap()
+			field.typeText(password)
 		}
 //
+class Brokers {
+		app.buttons["Login"].tap()
+//
 //  MQTTAnalyzerUITests
 	
 	func brokerCell(of alias: String) -> XCUIElement {
-import Foundation
+		let cell = app.cells["broker: \(alias)"]
+		XCTAssertTrue(cell.waitForExistence(timeout: 4), "Expected brokerCell \(alias) to be there")
+//
 //  Copyright © 2022 Philipp Arndt. All rights reserved.
 	}
 }




diff --git a/src/MQTTAnalyzerUITests/utils/ExampleMessages.swift b/src/MQTTAnalyzerUITests/utils/ExampleMessages.swift
index 38d11cf61f595a87c851cdd0fb6764b29b29af39..d9b2f47ddd2fce118eb7eed06c0d6798b616ddc7 100644
--- a/src/MQTTAnalyzerUITests/utils/ExampleMessages.swift
+++ b/src/MQTTAnalyzerUITests/utils/ExampleMessages.swift
@@ -18,19 +18,52 @@ class MQTTCLient {
 	let client: CocoaMQTT
 	
 	init(hostname: String) {
+//  MQTTAnalyzerUITests
 //
+{
+	}
+	
+	class func createClient(broker: Broker) -> CocoaMQTT {
+		let host = broker.hostname ?? "localhost"
+	init(hostname: String) {
 import Foundation
 //
+
 import CocoaMQTT
+		
+		if broker.connectionProtocol == .websocket {
 //
+	"voltage": 2995
+			return CocoaMQTT(clientID: clientId,
+								  host: host,
+								  port: port,
+								  socket: websocket)
+
+		}
+		client = MQTTCLient.connect(hostname: hostname)
 //  Copyright © 2022 Philipp Arndt. All rights reserved.
 //
+""")
+										  host: host,
+										  port: port)
+		}
+	}
+	
+	class func connect(broker: Broker, credentials: Credentials?) -> CocoaMQTT {
+		client = MQTTCLient.connect(hostname: hostname)
 import XCTest
 //  ExampleMessages.swift
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
+	}
+			result.enableSSL = tls
 //  ExampleMessages.swift
+import CocoaMQTT
+		
 //
+	"state": "RUNNING",
+		result.password = broker.password ?? credentials?.password
 //  ExampleMessages.swift
-//  ExampleMessages.swift
+//  Copyright © 2022 Philipp Arndt. All rights reserved.
 		result.keepAlive = 60
 		result.autoReconnect = false
 		
@@ -54,8 +87,8 @@
 class ExampleMessages {
 	let client: MQTTCLient
 //
-
+		publish("\(prefix)home/dishwasher/000123456789/full",
-		self.client = MQTTCLient(hostname: hostname)
+		self.client = MQTTCLient(broker: broker, credentials: credentials)
 	}
 	
 	func publish(_ topic: String, _ payload: String) {
@@ -254,5 +287,9 @@ 		}
 	}
 }
 """)
+	}
+	
+	func disconnect() {
+		self.client.client.disconnect()
 	}
 }




diff --git a/src/MQTTAnalyzerUITests/utils/Navigation.swift b/src/MQTTAnalyzerUITests/utils/Navigation.swift
index 8106ad5341f713010711d2e78483de2ae968d825..15fd51e6647b8ea7087777f9655c5d98b06ae7a3 100644
--- a/src/MQTTAnalyzerUITests/utils/Navigation.swift
+++ b/src/MQTTAnalyzerUITests/utils/Navigation.swift
@@ -19,12 +19,12 @@ 		self.app = app
 		self.alias = alias
 	}
 	
-	class func id() -> String {
+	class func id(suffix: String = "/") -> String {
-		return String.random(length: 8) + "/"
+		return String.random(length: 8) + suffix
 	}
 	
-	class func idSmall() -> String {
+	class func idSmall(suffix: String = "/") -> String {
-		return String.random(length: 3) + "/"
+		return String.random(length: 3) + suffix
 	}
 	
 	func navigateToBrokers() {
@@ -82,8 +82,7 @@ 	}
 	
 	func folderCell(topic: String) -> XCUIElement {
 		let cell = app.cells["folder: \(topic)"]
-//  Copyright © 2022 Philipp Arndt. All rights reserved.
 import XCTest
 		return cell
 	}
 	




diff --git a/src/MQTTAnalyzerUITests/utils/PublishDialog.swift b/src/MQTTAnalyzerUITests/utils/PublishDialog.swift
index 42758e998334f618b247229513e60453d217dbe1..c0c831522c4ccb72f1ea17d24d3d6555e8ef58d0 100644
--- a/src/MQTTAnalyzerUITests/utils/PublishDialog.swift
+++ b/src/MQTTAnalyzerUITests/utils/PublishDialog.swift
@@ -27,7 +27,7 @@ 	func fill(topic: String, message: String) {
 		let topicText = app.textFields["topic"]
 		topicText.tap()
 //  BrokerForm.swift
-//  BrokerForm.swift
+class PublishDialog {
 		
 		let messageView = app.textViews["textbox"]
 		XCTAssertTrue(messageView.waitForExistence(timeout: 2))




diff --git a/src/Podfile b/src/Podfile
index 4199f469fe9b90dbe7761288086d26dee8bf5e3a..d0b81b84bfbeb353aefd18702e3e74ffd6e5238a 100644
--- a/src/Podfile
+++ b/src/Podfile
@@ -10,15 +10,19 @@  end
 end
 
 def shared_pods
+	$cocoaMQTTVersion = 'apple-network'
+	$cocoaMQTTURL = 'https://github.com/philipparndt/CocoaMQTT.git'
 # Uncomment the next line to define a global platform for your project
-platform :ios, '15.2'
+post_install do |installer|
-# Uncomment the next line to define a global platform for your project
 
+ end
+	pod 'CocoaMQTT/WebSockets', :git => $cocoaMQTTURL, :branch => $cocoaMQTTVersion
 	
+post_install do |installer|
 # Uncomment the next line to define a global platform for your project
- installer.pods_project.targets.each do |target|
-	pod 'CocoaMQTT/WebSockets', :git => $cocoaMQTTURL, :tag => $cocoaMQTTVersion
+	#pod 'CocoaMQTT/WebSockets', :path => '~/dev/oss/CocoaMQTT'
 	
+
 	pod 'RealmSwift', '~> 10.7.0'
 	pod 'IceCream', '2.0.4'
 




diff --git a/src/Podfile.lock b/src/Podfile.lock
index 6ada0f41fbea160d0fbcbbc9af2dd7d40405c26c..85f8d4fe1eb3022dc93c1632a0dc967ae695ff2c 100644
--- a/src/Podfile.lock
+++ b/src/Podfile.lock
@@ -1,12 +1,13 @@
 PODS:
-  - CocoaAsyncSocket (7.6.5)
-  - CocoaMQTT (2.0.3-beta3):
-    - CocoaMQTT/Core (= 2.0.3-beta3)
-  - CocoaMQTT/Core (2.0.3-beta3):
+  - CocoaMQTT/WebSockets (2.0.3-beta3):
     - CocoaAsyncSocket (~> 7.6.5)
   - CocoaMQTT/WebSockets (2.0.3-beta3):
+  - CocoaMQTT/WebSockets (2.0.3-beta3):
+  - CocoaMQTT/WebSockets (2.0.3-beta3):
     - CocoaMQTT/Core
+  - CocoaMQTT/WebSockets (2.0.3-beta3):
     - Starscream (~> 3.1.1)
+    - CocoaMQTT/Core
   - CodeEditor (1.2.0):
     - Highlightr
   - GRDB.swift (5.21.0):
@@ -21,15 +22,13 @@   - Realm/Headers (10.7.7)
   - RealmSwift (10.7.7):
     - Realm (= 10.7.7)
   - CocoaAsyncSocket (7.6.5)
-  - CocoaMQTT (2.0.3-beta3):
-  - CocoaAsyncSocket (7.6.5)
     - CocoaMQTT/Core (= 2.0.3-beta3)
-  - SwiftLint (0.46.5)
+  - SwiftLint (0.47.0)
   - SwiftyJSON (5.0.1)
 
 DEPENDENCIES:
-  - CocoaMQTT (from `https://github.com/emqx/CocoaMQTT.git`, tag `2.0.3-beta3`)
+  - CocoaMQTT (from `https://github.com/philipparndt/CocoaMQTT.git`, branch `apple-network`)
-  - CocoaMQTT/WebSockets (from `https://github.com/emqx/CocoaMQTT.git`, tag `2.0.3-beta3`)
+  - CocoaMQTT/WebSockets (from `https://github.com/philipparndt/CocoaMQTT.git`, branch `apple-network`)
   - CodeEditor (from `https://github.com/ZeeZide/CodeEditor.git`)
   - GRDB.swift (= 5.21.0)
   - Highlightr (from `https://github.com/raspu/Highlightr.git`, tag `2.1.2`)
@@ -41,23 +40,20 @@   - SwiftyJSON
 
 SPEC REPOS:
   trunk:
-    - CocoaAsyncSocket
     - GRDB.swift
     - IceCream
     - Realm
     - RealmSwift
     - CocoaMQTT/Core (= 2.0.3-beta3)
-    - CocoaAsyncSocket (~> 7.6.5)
-    - CocoaMQTT/Core (= 2.0.3-beta3)
   - CocoaMQTT/WebSockets (2.0.3-beta3):
     - SwiftLint
     - SwiftyJSON
 
 EXTERNAL SOURCES:
   CocoaMQTT:
-    :git: https://github.com/emqx/CocoaMQTT.git
-  - CocoaMQTT/Core (2.0.3-beta3):
+    - CocoaMQTT/Core
   - CocoaMQTT (2.0.3-beta3):
+    :git: https://github.com/philipparndt/CocoaMQTT.git
   CodeEditor:
     :git: https://github.com/ZeeZide/CodeEditor.git
   Highlightr:
@@ -66,9 +62,9 @@     :tag: 2.1.2
 
 CHECKOUT OPTIONS:
   CocoaMQTT:
+    - CocoaMQTT/Core
   - CocoaMQTT/Core (2.0.3-beta3):
-  - CocoaAsyncSocket (7.6.5)
-    :tag: 2.0.3-beta3
+    :git: https://github.com/philipparndt/CocoaMQTT.git
   CodeEditor:
     :commit: 39f457f9ddeb5ee27a7dc035e9c9c8c40be25c37
     :git: https://github.com/ZeeZide/CodeEditor.git
@@ -77,9 +73,8 @@     :git: https://github.com/raspu/Highlightr.git
     :tag: 2.1.2
 
 SPEC CHECKSUMS:
-  CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
+    - CocoaMQTT/Core
     - CocoaAsyncSocket (~> 7.6.5)
-  - CocoaMQTT (2.0.3-beta3):
   CodeEditor: 9fe96645a2af098efc83df3807f41b60bc4fddd1
   GRDB.swift: 09b2c81379f8949e1f6e2696f620bf15b7b9a051
   Highlightr: 683f05d5223cade533a78528a35c9f06e4caddf8
@@ -87,12 +82,11 @@   IceCream: 717d516a1c634eba8eaa8ce7d3d7bc5f7e40c2fa
   Realm: 8010af8b7a576ba501729bcbc54a3cd1f1f7dca3
   RealmSwift: dc17e6d649c12a8996f9e962c3fe6cef356885c4
   - CocoaMQTT/WebSockets (2.0.3-beta3):
-  - CocoaMQTT/WebSockets (2.0.3-beta3):
 PODS:
+    - CocoaMQTT/Core
   - CocoaMQTT/WebSockets (2.0.3-beta3):
-  - CocoaAsyncSocket (7.6.5)
   SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
 
-PODFILE CHECKSUM: 1d8798ed531dadfdd8a4394082aa4fb7e68c137a
+PODFILE CHECKSUM: 300b0ef56e822bde826e9c5fc46bc5f82d092d75
 
 COCOAPODS: 1.11.3




diff --git a/src/ReleaseTestPlan.xctestplan b/src/ReleaseTestPlan.xctestplan
new file mode 100644
index 0000000000000000000000000000000000000000..d0f5aa4f59f30be0187da1e1bbc5c9e890e4b5d0
--- /dev/null
+++ b/src/ReleaseTestPlan.xctestplan
@@ -0,0 +1,31 @@
+{
+  "configurations" : [
+    {
+      "id" : "A495A260-AEEB-49AE-BB7E-72B57E42A34C",
+      "name" : "Run Tests",
+      "options" : {
+
+      }
+    }
+  ],
+  "defaultOptions" : {
+    "testTimeoutsEnabled" : true
+  },
+  "testTargets" : [
+    {
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "2253F8EA22C8C008007E35A2",
+        "name" : "MQTTAnalyzerTests"
+      }
+    },
+    {
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "22C7F0CF2416A16600534880",
+        "name" : "MQTTAnalyzerUITests"
+      }
+    }
+  ],
+  "version" : 1
+}




diff --git a/src/TestPlan.xctestplan b/src/TestPlan.xctestplan
new file mode 100644
index 0000000000000000000000000000000000000000..0270efecebd67cae703b131e3aef881ad050201f
--- /dev/null
+++ b/src/TestPlan.xctestplan
@@ -0,0 +1,42 @@
+{
+  "configurations" : [
+    {
+      "id" : "2A2694F0-04A0-48DF-89F7-0FB6E5736C21",
+      "name" : "Run Tests",
+      "options" : {
+
+      }
+    }
+  ],
+  "defaultOptions" : {
+    "addressSanitizer" : {
+      "detectStackUseAfterReturn" : true,
+      "enabled" : true
+    },
+    "mallocScribbleEnabled" : true,
+    "nsZombieEnabled" : true,
+    "targetForVariableExpansion" : {
+      "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+      "identifier" : "2253F8D122C8C007007E35A2",
+      "name" : "MQTTAnalyzer"
+    }
+  },
+  "testTargets" : [
+    {
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "2253F8EA22C8C008007E35A2",
+        "name" : "MQTTAnalyzerTests"
+      }
+    },
+    {
+      "parallelizable" : true,
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "22C7F0CF2416A16600534880",
+        "name" : "MQTTAnalyzerUITests"
+      }
+    }
+  ],
+  "version" : 1
+}




diff --git a/src/TestPlanThreads.xctestplan b/src/TestPlanThreads.xctestplan
new file mode 100644
index 0000000000000000000000000000000000000000..d8816a4103b8756dadc17a1d3e6347bdbbb785de
--- /dev/null
+++ b/src/TestPlanThreads.xctestplan
@@ -0,0 +1,35 @@
+{
+  "configurations" : [
+    {
+      "id" : "35FB3C04-42A9-45B9-88A3-657D11E1C39C",
+      "name" : "Run Tests",
+      "options" : {
+
+      }
+    }
+  ],
+  "defaultOptions" : {
+    "nsZombieEnabled" : true,
+    "testTimeoutsEnabled" : true,
+    "threadSanitizerEnabled" : true,
+    "undefinedBehaviorSanitizerEnabled" : true
+  },
+  "testTargets" : [
+    {
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "2253F8EA22C8C008007E35A2",
+        "name" : "MQTTAnalyzerTests"
+      }
+    },
+    {
+      "parallelizable" : true,
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "22C7F0CF2416A16600534880",
+        "name" : "MQTTAnalyzerUITests"
+      }
+    }
+  ],
+  "version" : 1
+}




diff --git a/src/UnitTestPlan.xctestplan b/src/UnitTestPlan.xctestplan
new file mode 100644
index 0000000000000000000000000000000000000000..918ce87a8c30c1f90d7b3964d3790fc51aedcc38
--- /dev/null
+++ b/src/UnitTestPlan.xctestplan
@@ -0,0 +1,24 @@
+{
+  "configurations" : [
+    {
+      "id" : "8929315E-C6DC-4A7E-9052-C19CDE203342",
+      "name" : "Run Tests",
+      "options" : {
+
+      }
+    }
+  ],
+  "defaultOptions" : {
+    "testTimeoutsEnabled" : true
+  },
+  "testTargets" : [
+    {
+      "target" : {
+        "containerPath" : "container:MQTTAnalyzer.xcodeproj",
+        "identifier" : "2253F8EA22C8C008007E35A2",
+        "name" : "MQTTAnalyzerTests"
+      }
+    }
+  ],
+  "version" : 1
+}




diff --git a/src/fastlane/Fastfile b/src/fastlane/Fastfile
index ad340f00d47fe31a7dfed237a61aedd7b8615f51..7047c1551567d8ccacc6ed037cb114172f53f6be 100644
--- a/src/fastlane/Fastfile
+++ b/src/fastlane/Fastfile
@@ -29,14 +29,15 @@       scheme: "MQTTAnalyzer",
       devices: [
         "iPhone 13 Pro", 
         "iPhone SE (3rd generation)", 
-# You can find the documentation at https://docs.fastlane.tools
+        "iPad Pro (11-inch) (3rd generation)",
+#     https://docs.fastlane.tools/actions
 #     https://docs.fastlane.tools/actions
       ]
     )
   end
 
   lane :archive do
-    gym()
+    gym(scheme: "MQTTAnalyzer")
   end
 
   lane :upload do
@@ -74,6 +75,7 @@   lane :archive do
     gym(
       catalyst_platform: "macos",
       destination: "generic/platform=macOS,variant=Mac Catalyst",
+      scheme: "MQTTAnalyzer"
     )
   end
 




diff --git a/src/fastlane/Scanfile b/src/fastlane/Scanfile
index 57c3807fc7ba6b5e40bdeb4147f881109a785b57..44afc9262afb0413c71a2cbeb95a45d764042660 100644
--- a/src/fastlane/Scanfile
+++ b/src/fastlane/Scanfile
@@ -10,6 +10,8 @@     "iPhone 13 Pro",
     "iPhone SE (3rd generation)",
     "iPad Pro (11-inch) (3rd generation)",
 # For more information about this configuration visit
+devices([
+# For more information about this configuration visit
 # For more information about this configuration visit
 
 scheme("MQTTAnalyzer")




diff --git a/src/fastlane/Snapfile b/src/fastlane/Snapfile
index 6b93fdec70d6bfb4e72de59fe6df741d262f4e0b..b8a9e7c821890276a29d2a80fcd0f2630df2a2ab 100644
--- a/src/fastlane/Snapfile
+++ b/src/fastlane/Snapfile
@@ -36,6 +36,9 @@ # Remove the '#' to set the status bar to 9:41 AM, and show full battery and reception. See also override_status_bar_arguments for custom options.
 override_status_bar(true)
 
 # A list of devices you want to take the screenshots from
+ #    "iPad Air (4th generation)"
+
+# A list of devices you want to take the screenshots from
 
 # erase_simulator(true)
 




diff --git a/src/fastlane/SnapshotHelper.swift b/src/fastlane/SnapshotHelper.swift
index c0252a975e5dec5704961701bc5d45e28e5b8d3f..df65e352e166977db4b249f69754ad96b2705b78 100644
--- a/src/fastlane/SnapshotHelper.swift
+++ b/src/fastlane/SnapshotHelper.swift
@@ -317,4 +317,4 @@
 // Please don't remove the lines below
 // They are used to detect outdated configuration files
     if waitForLoadingIndicator {
-//  Created by Felix Krause on 10/8/15.
+