You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by cd...@apache.org on 2024/04/04 19:35:24 UTC

(plc4x-extras) 01/01: Initial Commit (Continuing to move the build next)

This is an automated email from the ASF dual-hosted git repository.

cdutz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/plc4x-extras.git

commit e513a12ebdbb3631704ff8947a98e780a79f9ea4
Author: Christofer Dutz <cd...@apache.org>
AuthorDate: Thu Apr 4 21:35:16 2024 +0200

    Initial Commit (Continuing to move the build next)
---
 plc4c/examples/CMakeLists.txt                      |   22 +
 plc4c/examples/hello-subscriptions/CMakeLists.txt  |   41 +
 .../hello-subscriptions/src/hello_subscriptions.c  |  285 ++++
 plc4c/examples/hello-world-modbus/CMakeLists.txt   |   44 +
 .../hello-world-modbus/src/hello_world_modbus.c    |  355 +++++
 plc4c/examples/hello-world-s7/CMakeLists.txt       |   69 +
 plc4c/examples/hello-world-s7/src/hello_world_s7.c |  381 +++++
 .../hello-world-s7/src/hello_world_s7_loopback.c   |  350 +++++
 plc4c/integrations/CMakeLists.txt                  |   20 +
 plc4c/integrations/apache-mynewt/CMakeLists.txt    |   23 +
 plc4go/examples/ads/browse/Browse.go               |   44 +
 plc4go/examples/ads/discovery/Discovery.go         |   36 +
 plc4go/examples/ads/read/Read.go                   |   72 +
 plc4go/examples/ads/subscribe/Subscribe.go         |   70 +
 plc4go/examples/ads/write/Write.go                 |  113 ++
 .../hello_world_plc4go_bacnet_discovery.go         |   91 ++
 plc4go/examples/doc.go                             |   21 +
 .../discovery/hello_world_plc4go_knx_discovery.go  |  204 +++
 .../hello_world_plc4go_knx_read_group_address.go   |   95 ++
 .../hello_world_plc4go_knx_subscription.go         |  123 ++
 plc4go/examples/read/hello_world_plc4go_read.go    |   80 ++
 plc4go/examples/write/hello_world_plc4go_write.go  |   77 +
 plc4go/tools/plc4xbrowser/doc.go                   |   26 +
 plc4go/tools/plc4xbrowser/main.go                  |   35 +
 plc4go/tools/plc4xbrowser/ui/actions.go            |  159 +++
 plc4go/tools/plc4xbrowser/ui/commands.go           |  803 +++++++++++
 plc4go/tools/plc4xbrowser/ui/common.go             |   85 ++
 plc4go/tools/plc4xbrowser/ui/config.go             |  186 +++
 plc4go/tools/plc4xbrowser/ui/ui.go                 |  300 ++++
 plc4go/tools/plc4xpcapanalyzer/cmd/analyze.go      |   82 ++
 plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go       |   63 +
 plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go         |   73 +
 plc4go/tools/plc4xpcapanalyzer/cmd/extract.go      |   66 +
 plc4go/tools/plc4xpcapanalyzer/cmd/root.go         |  128 ++
 plc4go/tools/plc4xpcapanalyzer/cmd/ui.go           |   71 +
 .../plc4xpcapanalyzer/config/AnalyzeConfig.go      |   31 +
 .../tools/plc4xpcapanalyzer/config/BacnetConfig.go |   31 +
 .../tools/plc4xpcapanalyzer/config/CBusConfig.go   |   41 +
 .../plc4xpcapanalyzer/config/ExtractConfig.go      |   39 +
 .../tools/plc4xpcapanalyzer/config/PcapConfig.go   |   34 +
 .../tools/plc4xpcapanalyzer/config/RootConfig.go   |   30 +
 plc4go/tools/plc4xpcapanalyzer/doc.go              |   26 +
 .../internal/analyzer/analyzer.go                  |  271 ++++
 .../internal/bacnetanalyzer/analyzer.go            |   52 +
 .../internal/cbusanalyzer/analyzer.go              |  340 +++++
 .../plc4xpcapanalyzer/internal/common/common.go    |   72 +
 .../internal/common/mock_FilteredPackage_test.go   |  703 +++++++++
 .../internal/extractor/extractor.go                |  180 +++
 .../internal/pcaphandler/pcaphandler.go            |   86 ++
 plc4go/tools/plc4xpcapanalyzer/main.go             |   26 +
 plc4go/tools/plc4xpcapanalyzer/ui/actions.go       |  153 ++
 plc4go/tools/plc4xpcapanalyzer/ui/commands.go      |  724 ++++++++++
 plc4go/tools/plc4xpcapanalyzer/ui/common.go        |   88 ++
 plc4go/tools/plc4xpcapanalyzer/ui/config.go        |  203 +++
 plc4go/tools/plc4xpcapanalyzer/ui/ui.go            |  318 +++++
 plc4j/examples/hello-ads-telemetry/pom.xml         |   74 +
 .../helloads/telemetry/DeviceManagerConstants.java |   55 +
 .../helloads/telemetry/EtherCatDevice.java         |   56 +
 .../telemetry/EtherCatMasterConstants.java         |  100 ++
 .../telemetry/HelloAdsEtherCatTelemetry.java       |  175 +++
 .../helloads/telemetry/HelloAdsTelemetry.java      |   96 ++
 .../src/main/resources/logback.xml                 |   34 +
 plc4j/examples/hello-cloud-azure/pom.xml           |   59 +
 .../java/examples/cloud/azure/CliOptions.java      |   98 ++
 .../cloud/azure/S7PlcToAzureIoTHubSample.java      |   97 ++
 plc4j/examples/hello-cloud-google/README.md        |   61 +
 plc4j/examples/hello-cloud-google/pom.xml          |   89 ++
 .../java/examples/cloud/google/CliOptions.java     |  221 +++
 .../cloud/google/S7PlcToGoogleIoTCoreSample.java   |  286 ++++
 .../src/main/resources/logback.xml                 |   34 +
 .../hello-connectivity-mqtt/mqtt-connector.yml     |   35 +
 plc4j/examples/hello-connectivity-mqtt/pom.xml     |  102 ++
 .../examples/connectivity/mqtt/MqttConnector.java  |  155 ++
 .../connectivity/mqtt/model/Configuration.java     |   56 +
 .../connectivity/mqtt/model/MqttConfig.java        |   56 +
 .../connectivity/mqtt/model/PlcConfig.java         |   57 +
 .../connectivity/mqtt/model/PlcMemoryBlock.java    |   41 +
 .../connectivity/mqtt/model/PlcTagConfig.java      |   69 +
 .../src/main/resources/logback.xml                 |   34 +
 plc4j/examples/hello-discovery/pom.xml             |   80 ++
 .../java/examples/hellodiscovery/CliOptions.java   |   72 +
 .../examples/hellodiscovery/HelloDiscovery.java    |   69 +
 .../hello-discovery/src/main/resources/logback.xml |   36 +
 plc4j/examples/hello-docker/Dockerfile             |  116 ++
 .../hello-influx-data-collection/README.adoc       |   36 +
 .../example.properties                             |   32 +
 .../examples/hello-influx-data-collection/pom.xml  |   72 +
 .../java/examples/helloinflux/HelloInflux.java     |  160 +++
 .../src/main/resources/logback.xml                 |   34 +
 plc4j/examples/hello-integration-iotdb/pom.xml     |  106 ++
 .../examples/integration/iotdb/CliOptions.java     |  206 +++
 .../examples/integration/iotdb/IIoTDBWriter.java   |   26 +
 .../integration/iotdb/IoTDBWriterWithJDBC.java     |   86 ++
 .../integration/iotdb/IoTDBWriterWithSession.java  |   88 ++
 .../java/examples/integration/iotdb/PlcLogger.java |  124 ++
 .../src/main/resources/logback.xml                 |   34 +
 .../hello-kafka-connect-elasticsearch/README.md    |   61 +
 .../elasticsearch-sink.properties                  |   24 +
 .../elasticsearch-worker.properties                |   29 +
 .../kafka-connect-config/plc4x-source.properties   |   53 +
 .../kafka-connect-config/plc4x-worker.properties   |   29 +
 plc4j/examples/hello-nats/pom.xml                  |   83 ++
 .../plc4x/java/examples/hellonats/CliOptions.java  |   98 ++
 .../plc4x/java/examples/hellonats/HelloNats.java   |  106 ++
 .../hello-nats/src/main/resources/logback.xml      |   34 +
 plc4j/examples/hello-opm/pom.xml                   |   81 ++
 .../plc4x/java/examples/helloopm/HelloOpm.java     |  141 ++
 .../hello-opm/src/main/resources/logback.xml       |   34 +
 plc4j/examples/hello-webservice/README.adoc        |   24 +
 plc4j/examples/hello-webservice/pom.xml            |   91 ++
 .../java/examples/hellowebservice/CliOptions.java  |   98 ++
 .../examples/hellowebservice/HelloWebservice.java  |  169 +++
 .../src/main/resources/logback-test.xml            |   36 +
 .../plc4j-examples-hello-world-kotlin.iml          |   13 +
 plc4j/examples/hello-world-kotlin/pom.xml          |  167 +++
 .../java/examples/hellokotlin/ReadModbusPal.kt     |   63 +
 .../hello-world-plc4x-discover-and-browse/pom.xml  |   81 ++
 .../HelloPlc4xDiscoverAndBrowse.java               |   87 ++
 .../src/main/resources/logback.xml                 |   41 +
 plc4j/examples/hello-world-plc4x-read/pom.xml      |   80 ++
 .../java/examples/helloplc4x/read/CliOptions.java  |   83 ++
 .../examples/helloplc4x/read/HelloPlc4xRead.java   |  118 ++
 .../src/main/resources/logback.xml                 |   36 +
 plc4j/examples/hello-world-plc4x-subscribe/pom.xml |   86 ++
 .../examples/helloplc4x/subscribe/CliOptions.java  |   83 ++
 .../subscribe/HelloPlc4xSubscription.java          |  131 ++
 .../src/main/resources/logback.xml                 |   36 +
 plc4j/examples/hello-world-plc4x-write/pom.xml     |   81 ++
 .../java/examples/helloplc4x/write/CliOptions.java |   98 ++
 .../examples/helloplc4x/write/HelloPlc4xWrite.java |   95 ++
 .../src/main/resources/logback.xml                 |   36 +
 plc4j/examples/plc4j-s7event/pom.xml               |  104 ++
 .../plc4j/s7event/PLcSetAndGetRtClock.java         |   30 +
 .../plc4j/s7event/PlcCycSubscriptionS7400H.java    |  351 +++++
 .../plc4j/s7event/PlcEventSubscriptionS7400H.java  |  116 ++
 .../plc4j/s7event/PlcReadAndWriteVIPA.java         |   38 +
 .../examples/plc4j/s7event/PlcReadDataS7400H.java  |  300 ++++
 .../examples/plc4j/s7event/PlcReadS71200.java      |  265 ++++
 .../plc4j/s7event/PlcReadWriteStrings.java         |  434 ++++++
 .../plc4j/s7event/PlcSzlRequestS7400H.java         |   89 ++
 .../examples/plc4j/s7event/PlcWriteDataS7400H.java |   31 +
 .../examples/plc4j/s7event/PlcWriteS71200.java     |   31 +
 plc4j/examples/poll-loop/pom.xml                   |   71 +
 .../apache/plc4x/java/examples/pollloop/Main.java  |   44 +
 .../plc4x/java/examples/pollloop/PollLoop.java     |  227 +++
 plc4j/examples/pom.xml                             |  142 ++
 plc4j/integrations/apache-calcite/pom.xml          |  110 ++
 .../main/java/org/apache/plc4x/Plc4xBaseTable.java |  198 +++
 .../main/java/org/apache/plc4x/Plc4xSchema.java    |  105 ++
 .../java/org/apache/plc4x/Plc4xSchemaFactory.java  |   69 +
 .../java/org/apache/plc4x/Plc4xStreamTable.java    |   45 +
 .../src/main/java/org/apache/plc4x/Plc4xTable.java |   39 +
 .../java/org/apache/plc4x/DriverManagerTest.java   |   87 ++
 .../java/org/apache/plc4x/Plc4XBaseTableTest.java  |   52 +
 .../org/apache/plc4x/Plc4xSchemaFactoryTest.java   |   35 +
 .../apache-calcite/src/test/resources/example.yml  |   32 +
 .../src/test/resources/logback-test.xml            |   36 +
 .../apache-calcite/src/test/resources/model.json   |   24 +
 plc4j/integrations/apache-kafka/README.md          |  229 +++
 .../apache-kafka/config/plc4x-sink.properties      |   77 +
 .../apache-kafka/config/plc4x-source.properties    |   83 ++
 plc4j/integrations/apache-kafka/pom.xml            |  195 +++
 .../org/apache/plc4x/kafka/Plc4xSinkConnector.java |  122 ++
 .../java/org/apache/plc4x/kafka/Plc4xSinkTask.java |  268 ++++
 .../apache/plc4x/kafka/Plc4xSourceConnector.java   |  141 ++
 .../org/apache/plc4x/kafka/Plc4xSourceTask.java    |  333 +++++
 .../org/apache/plc4x/kafka/config/Constants.java   |   92 ++
 .../java/org/apache/plc4x/kafka/config/Job.java    |   95 ++
 .../apache/plc4x/kafka/config/JobReference.java    |   64 +
 .../java/org/apache/plc4x/kafka/config/Sink.java   |  133 ++
 .../org/apache/plc4x/kafka/config/SinkConfig.java  |   84 ++
 .../java/org/apache/plc4x/kafka/config/Source.java |  139 ++
 .../apache/plc4x/kafka/config/SourceConfig.java    |  133 ++
 .../java/org/apache/plc4x/kafka/config/Tag.java    |   59 +
 .../org/apache/plc4x/kafka/util/VersionUtil.java   |   32 +
 .../apache-kafka/src/remote-resources/LICENSE      |    7 +
 .../apache-kafka/src/remote-resources/NOTICE       |    4 +
 .../apache/plc4x/kafka/config/SinkConfigTest.java  |  130 ++
 .../apache/plc4x/kafka/config/SinkTaskTest.java    |  145 ++
 .../plc4x/kafka/config/SourceConfigTest.java       |  232 +++
 .../apache/plc4x/kafka/config/SourceTaskTest.java  |  101 ++
 .../kafka/properties/sink_task_no_error.properties |   62 +
 .../properties/source_task_no_error.properties     |   62 +
 .../src/test/resources/logback-test.xml            |   36 +
 plc4j/integrations/apache-nifi/README.md           |  180 +++
 .../apache-nifi/nifi-plc4x-nar/bin/.gitignore      |    2 +
 .../apache-nifi/nifi-plc4x-nar/pom.xml             |   89 ++
 .../apache-nifi/nifi-plc4x-processors/pom.xml      |  179 +++
 .../org/apache/plc4x/nifi/BasePlc4xProcessor.java  |  388 +++++
 .../plc4x/nifi/Plc4xListenRecordProcessor.java     |  308 ++++
 .../org/apache/plc4x/nifi/Plc4xSinkProcessor.java  |  109 ++
 .../plc4x/nifi/Plc4xSinkRecordProcessor.java       |  195 +++
 .../apache/plc4x/nifi/Plc4xSourceProcessor.java    |  118 ++
 .../plc4x/nifi/Plc4xSourceRecordProcessor.java     |  203 +++
 .../nifi/address/AddressesAccessStrategy.java      |   47 +
 .../plc4x/nifi/address/AddressesAccessUtils.java   |   96 ++
 .../plc4x/nifi/address/BaseAccessStrategy.java     |  124 ++
 .../address/DynamicPropertyAccessStrategy.java     |   70 +
 .../nifi/address/FilePropertyAccessStrategy.java   |   79 ++
 .../nifi/address/TextPropertyAccessStrategy.java   |   70 +
 .../record/Plc4xFullReadResponseRowCallback.java   |   32 +
 .../nifi/record/Plc4xReadResponseRecordSet.java    |  191 +++
 .../nifi/record/Plc4xReadResponseRowCallback.java  |   27 +
 .../org/apache/plc4x/nifi/record/Plc4xWriter.java  |   83 ++
 .../plc4x/nifi/record/RecordPlc4xWriter.java       |  196 +++
 .../org/apache/plc4x/nifi/record/SchemaCache.java  |  139 ++
 .../nifi/subscription/Plc4xListenerDispatcher.java |  152 ++
 .../nifi/subscription/Plc4xSubscriptionType.java   |   25 +
 .../org/apache/plc4x/nifi/util/Plc4xCommon.java    |  199 +++
 .../services/org.apache.nifi.processor.Processor   |   19 +
 .../plc4x/nifi/Plc4xListenRecordProcessorTest.java |   80 ++
 .../apache/plc4x/nifi/Plc4xSinkProcessorTest.java  |   93 ++
 .../plc4x/nifi/Plc4xSinkRecordProcessorTest.java   |  110 ++
 .../plc4x/nifi/Plc4xSourceProcessorTest.java       |   88 ++
 .../plc4x/nifi/Plc4xSourceRecordProcessorTest.java |   98 ++
 .../plc4x/nifi/address/AccessStrategyTest.java     |  206 +++
 .../apache/plc4x/nifi/record/SchemaCacheTest.java  |  174 +++
 .../apache/plc4x/nifi/util/Plc4xCommonTest.java    |  214 +++
 .../src/test/resources/logback-test.xml            |   36 +
 .../integrations/apache-nifi/plc4j-apache-nifi.iml |    8 +
 plc4j/integrations/apache-nifi/pom.xml             |  159 +++
 plc4j/integrations/plc4j-integrations.iml          |    8 +
 plc4j/integrations/pom.xml                         |   46 +
 plc4j/tools/opcua-server/pom.xml                   |  208 +++
 .../apache/plc4x/java/opcuaserver/OPCUAServer.java |  382 +++++
 .../opcuaserver/backend/Plc4xCommunication.java    |  281 ++++
 .../java/opcuaserver/backend/Plc4xNamespace.java   |  221 +++
 .../opcuaserver/configuration/Configuration.java   |   76 +
 .../configuration/DeviceConfiguration.java         |   49 +
 .../configuration/PasswordConfiguration.java       |   87 ++
 .../plc4x/java/opcuaserver/configuration/Tag.java  |   41 +
 .../plc4x/java/opcuaserver/configuration/User.java |   96 ++
 .../opcuaserver/context/CertificateGenerator.java  |  127 ++
 .../opcuaserver/context/CertificateKeyPair.java    |   37 +
 .../opcua-server/src/main/resources/config.yml     |  103 ++
 .../opcua-server/src/main/resources/logback.xml    |   34 +
 .../plc4x/java/opcuaserver/OpcuaPlcDriverTest.java |  244 ++++
 .../opcua-server/src/test/resources/config.yml     |  103 ++
 .../src/test/resources/logback-test.xml            |   36 +
 plc4j/tools/plc4x-server/pom.xml                   |  161 +++
 .../plc4x/java/tools/plc4xserver/Plc4xServer.java  |  175 +++
 .../plc4xserver/protocol/Plc4xServerAdapter.java   |  176 +++
 .../java/tools/plc4xserver/Plc4xServerTest.java    |  131 ++
 plc4j/tools/ui/application/pom.xml                 |  179 +++
 .../java/tools/ui/Plc4xUiBackendApplication.java   |   34 +
 .../src/main/resources/application-dev.properties  |   18 +
 .../src/main/resources/application.properties      |   31 +
 .../ui/application/src/main/resources/logback.xml  |   39 +
 .../tools/ui/Plc4xUiBackendApplicationTests.java   |   34 +
 .../src/test/resources/application-test.properties |   19 +
 plc4j/tools/ui/backend/pom.xml                     |  161 +++
 .../tools/ui/configuration/Plc4xConfiguration.java |   34 +
 .../tools/ui/configuration/WebSocketConfig.java    |   43 +
 .../java/tools/ui/controller/DeviceController.java |   64 +
 .../java/tools/ui/controller/DriverController.java |   51 +
 .../plc4x/java/tools/ui/event/DeviceEvent.java     |   30 +
 .../plc4x/java/tools/ui/event/EventType.java       |   28 +
 .../java/tools/ui/event/UiApplicationEvent.java    |   42 +
 .../java/tools/ui/handler/WebSocketHandler.java    |  107 ++
 .../java/tools/ui/model/ConfigurationOption.java   |   52 +
 .../apache/plc4x/java/tools/ui/model/Device.java   |  109 ++
 .../apache/plc4x/java/tools/ui/model/Driver.java   |   49 +
 .../plc4x/java/tools/ui/model/Transport.java       |   49 +
 .../java/tools/ui/repository/DeviceRepository.java |   30 +
 .../plc4x/java/tools/ui/service/DeviceService.java |   82 ++
 .../plc4x/java/tools/ui/service/DriverService.java |  119 ++
 plc4j/tools/ui/backend/ws.html                     |   36 +
 plc4j/tools/ui/frontend/pom.xml                    |  222 +++
 plc4j/tools/ui/frontend/project/.eslintrc.cjs      |   37 +
 plc4j/tools/ui/frontend/project/.gitignore         |   25 +
 plc4j/tools/ui/frontend/project/README.md          |   49 +
 plc4j/tools/ui/frontend/project/index.html         |   32 +
 plc4j/tools/ui/frontend/project/package.json       |   51 +
 .../ui/frontend/project/public/plc4x-favicon.png   |  Bin 0 -> 6065 bytes
 plc4j/tools/ui/frontend/project/src/App.css        |   19 +
 plc4j/tools/ui/frontend/project/src/App.tsx        |  106 ++
 .../ui/frontend/project/src/assets/plc4x-logo.svg  | 1494 ++++++++++++++++++++
 .../tools/ui/frontend/project/src/assets/react.svg |   23 +
 .../project/src/components/DeviceDialog.tsx        |  184 +++
 .../project/src/components/NavigationTree.tsx      |  221 +++
 .../project/src/components/PlcConnection.tsx       |   45 +
 .../src/generated/plc4j-tools-ui-frontend.ts       |  208 +++
 plc4j/tools/ui/frontend/project/src/index.css      |  102 ++
 .../ui/frontend/project/src/layouts/MainLayout.tsx |   73 +
 plc4j/tools/ui/frontend/project/src/main.tsx       |   37 +
 .../ui/frontend/project/src/model/TreeItemData.ts  |   37 +
 .../tools/ui/frontend/project/src/pages/About.tsx  |   24 +
 .../ui/frontend/project/src/pages/Inspect.tsx      |  140 ++
 plc4j/tools/ui/frontend/project/src/pages/Mqtt.tsx |   24 +
 .../tools/ui/frontend/project/src/pages/OpcUa.tsx  |   24 +
 .../ui/frontend/project/src/pages/Settings.tsx     |   24 +
 plc4j/tools/ui/frontend/project/src/store/index.ts |   91 ++
 .../tools/ui/frontend/project/src/utils/Counter.ts |   28 +
 plc4j/tools/ui/frontend/project/src/vite-env.d.ts  |   20 +
 plc4j/tools/ui/frontend/project/tsconfig.json      |   25 +
 plc4j/tools/ui/frontend/project/tsconfig.node.json |   10 +
 plc4j/tools/ui/frontend/project/vite.config.ts     |   26 +
 plc4j/tools/ui/plc4j-tools-ui.iml                  |    8 +
 plc4j/tools/ui/pom.xml                             |  113 ++
 299 files changed, 32797 insertions(+)

diff --git a/plc4c/examples/CMakeLists.txt b/plc4c/examples/CMakeLists.txt
new file mode 100644
index 0000000..879651b
--- /dev/null
+++ b/plc4c/examples/CMakeLists.txt
@@ -0,0 +1,22 @@
+#[[
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+]]
+
+add_subdirectory(hello-subscriptions)
+add_subdirectory(hello-world-modbus)
+add_subdirectory(hello-world-s7)
diff --git a/plc4c/examples/hello-subscriptions/CMakeLists.txt b/plc4c/examples/hello-subscriptions/CMakeLists.txt
new file mode 100644
index 0000000..67d72a8
--- /dev/null
+++ b/plc4c/examples/hello-subscriptions/CMakeLists.txt
@@ -0,0 +1,41 @@
+#[[
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+]]
+
+file(GLOB sources "src/*.c")
+
+add_executable(plc4c-examples-hello-subscriptions ${sources} src/hello_subscriptions.c)
+
+target_include_directories(plc4c-examples-hello-subscriptions
+        PUBLIC
+        "../../api/include"
+        "../../drivers/simulated/include"
+        "../../transports/dummy/include")
+
+IF (NOT WIN32)
+    target_link_libraries(plc4c-examples-hello-subscriptions
+            plc4c-spi plc4c-driver-simulated
+            plc4c-transport-dummy
+            m
+    )
+ELSE()
+    target_link_libraries(plc4c-examples-hello-subscriptions
+            plc4c-spi plc4c-driver-simulated
+            plc4c-transport-dummy
+    )
+ENDIF()
diff --git a/plc4c/examples/hello-subscriptions/src/hello_subscriptions.c b/plc4c/examples/hello-subscriptions/src/hello_subscriptions.c
new file mode 100644
index 0000000..e95f778
--- /dev/null
+++ b/plc4c/examples/hello-subscriptions/src/hello_subscriptions.c
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#include <plc4c/driver_simulated.h>
+#include <plc4c/plc4c.h>
+#include <plc4c/transport_dummy.h>
+#include <plc4c/utils/list.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "../../../spi/include/plc4c/spi/types_private.h"
+
+enum plc4c_connection_state_t {
+  CONNECTING,
+  CONNECTED,
+  SUBSCRIPTION_REQUEST_SENT,
+  SUBSCRIPTION_RESPONSE_RECEIVED,
+  READING_EVENTS,
+  DISCONNECTING,
+  DISCONNECTED
+};
+typedef enum plc4c_connection_state_t plc4c_connection_state;
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "hicpp-multiway-paths-covered"
+
+int main() {
+  bool loop = true;
+  plc4c_system *system = NULL;
+  plc4c_connection *connection = NULL;
+  plc4c_subscription_request *subscription_request = NULL;
+  plc4c_subscription_request_execution *subscription_request_execution = NULL;
+  int num_events = 0;
+  void *subscription_handle = NULL;
+
+  // Create a new uninitialized plc4c_system
+  printf("Creating new PLC4C System (Initializing inner data-structures) ... ");
+  plc4c_return_code result = plc4c_system_create(&system);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Manually register the "simulated" driver with the system.
+  printf("Registering driver for the 'simulated' protocol ... ");
+  plc4c_driver *simulated_driver = plc4c_driver_simulated_create();
+  result = plc4c_system_add_driver(system, simulated_driver);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  printf("Registering transport for the 'dummy' transport ... ");
+  plc4c_transport *dummy_transport = plc4c_transport_dummy_create();
+  result = plc4c_system_add_transport(system, dummy_transport);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Initialize the plc4c_system (loading of drivers, setting up other stuff,
+  // ...)
+  printf(
+      "Initializing the PLC4C system (Loading of drivers and transports) ... ");
+  result = plc4c_system_init(system);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Establish connections to remote devices
+  // you may or may not care about the connection handle
+  printf("Connecting to 'simulated://foo' ... ");
+  result = plc4c_system_connect(system, "simulated://foo", &connection);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+
+  // Central program loop ...
+  plc4c_connection_state state = CONNECTING;
+  while (loop) {
+    printf("* ");
+
+    // Give plc4c a chance to do something.
+    // This is where all I/O is being done.
+    if (plc4c_system_loop(system) != OK) {
+      printf("ERROR in the system loop\n");
+      break;
+    }
+
+    // Depending on the current state, implement some logic.
+    switch (state) {
+      case CONNECTING: {
+        // Check if the connection is established:
+        if (plc4c_connection_get_connected(connection)) {
+          printf("SUCCESS\n");
+          state = CONNECTED;
+        } else if (plc4c_connection_has_error(connection)) {
+          printf("FAILED\n");
+          return -1;
+        }
+        break;
+      }
+      case CONNECTED: {
+        // Create a new subscription-request.
+        printf("Preparing a subscription-request ... ");
+        result = plc4c_connection_create_subscription_request(
+            connection, &subscription_request);
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        printf("SUCCESS\n");
+
+        printf("Adding cyclic item for 'RANDOM/foo:INTEGER' ... ");
+        result = plc4c_subscription_request_add_cyclic_item(
+            subscription_request, "RANDOM/foo:INTEGER", 500);
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        printf("SUCCESS\n");
+
+        // Execute the subscription-request.
+        printf("Executing a subscription-request ... ");
+        result = plc4c_subscription_request_execute(
+            subscription_request, &subscription_request_execution);
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        } else {
+          state = SUBSCRIPTION_REQUEST_SENT;
+        }
+        break;
+      }
+        // Wait until the read-request execution is finished.
+      case SUBSCRIPTION_REQUEST_SENT: {
+        if (plc4c_subscription_request_execution_check_finished_successfully(
+                subscription_request_execution)) {
+          printf("SUCCESS\n");
+          state = SUBSCRIPTION_RESPONSE_RECEIVED;
+        } else if (
+            plc4c_subscription_request_execution_check_finished_with_error(
+                subscription_request_execution)) {
+          printf("FAILED\n");
+          return -1;
+        }
+        break;
+      }
+      case SUBSCRIPTION_RESPONSE_RECEIVED: {
+        // Get the response for the given subscription-request.
+        plc4c_subscription_response *subscription_response =
+            plc4c_subscription_request_execution_get_response(
+                subscription_request_execution);
+        if (subscription_response == NULL) {
+          printf("FAILED (No Response)\n");
+          return -1;
+        }
+
+        // Save the subsciption handle.
+        plc4c_list_element *cur_element =
+            plc4c_utils_list_head(subscription_response->response_items);
+        plc4c_response_subscription_item *subscription_item_item =
+            cur_element->value;
+        printf("Value %s (%s):", subscription_item_item->item->name,
+               plc4c_response_code_to_message(
+                   subscription_item_item->response_code));
+        // Check if the response was ok.
+        if (subscription_item_item->response_code == OK) {
+          // Save the subscription handles...
+          subscription_handle = subscription_item_item->subscription_handle;
+        } else {
+          printf("FAILED (No Response)\n");
+          return -1;
+        }
+
+        // Clean up the subscription..
+        plc4c_subscription_response_destroy(subscription_response);
+        plc4c_subscription_request_execution_destroy(
+            subscription_request_execution);
+        plc4c_subscription_request_destroy(subscription_request);
+
+        state = READING_EVENTS;
+        break;
+      }
+        // Wait for 10 incoming events.
+      case READING_EVENTS: {
+        // Check if an event is available ...
+        printf("Checking Events ... ");
+        if (plc4c_subscription_check_data_available(subscription_handle)) {
+          printf("New events available.\n");
+
+          printf("Getting Events ... ");
+          plc4c_list *events = NULL;
+          result = plc4c_subscription_get_subscription_events(
+              subscription_handle, &events);
+          if (result != OK) {
+            printf("FAILED\n");
+            return -1;
+          }
+          printf("SUCCESS\n");
+
+          plc4c_list_element *cur_element = plc4c_utils_list_head(events);
+          while (cur_element != NULL) {
+            plc4c_response_subscription_item *subscription_event_item =
+                cur_element->value;
+
+            // TODO: Do something with the event ...
+            printf("Got Event %s", subscription_event_item->item->name);
+
+            // Increment the number of processed events.
+            num_events++;
+
+            cur_element = cur_element->next;
+          }
+
+          // If at least 10 events have been processed, disconnect.
+          if (num_events > 10) {
+            // Disconnect.
+            printf("Disconnecting ... ");
+            result = plc4c_connection_disconnect(connection);
+            if (result != OK) {
+              printf("FAILED");
+              return -1;
+            }
+            state = DISCONNECTING;
+          }
+        } else {
+          printf("No events.\n");
+        }
+        break;
+      }
+        // Wait until the connection is disconnected
+      case DISCONNECTING: {
+        if (!plc4c_connection_get_connected(connection)) {
+          printf("SUCCESS\n");
+          // we could let the system shut this down,
+          // or do it ourselves
+          plc4c_system_remove_connection(system, connection);
+          plc4c_connection_destroy(connection);
+          state = DISCONNECTED;
+
+          // Terminate the main program loop.
+          loop = false;
+        }
+        break;
+      }
+      case DISCONNECTED: {
+        // End the loop.
+        loop = false;
+        break;
+      }
+    }
+  }
+
+  // Make sure everything is cleaned up correctly.
+  plc4c_system_shutdown(system);
+
+  // Finally destroy the plc4c_system, freeing up all memory allocated by plc4c.
+  plc4c_system_destroy(system);
+
+  return 0;
+}
+
+#pragma clang diagnostic pop
diff --git a/plc4c/examples/hello-world-modbus/CMakeLists.txt b/plc4c/examples/hello-world-modbus/CMakeLists.txt
new file mode 100644
index 0000000..8d73360
--- /dev/null
+++ b/plc4c/examples/hello-world-modbus/CMakeLists.txt
@@ -0,0 +1,44 @@
+#[[
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+]]
+
+add_executable(plc4c-examples-hello-world-modbus
+        src/hello_world_modbus.c
+        )
+
+target_include_directories(plc4c-examples-hello-world-modbus
+        PUBLIC
+        "../../api/include"
+        "../../spi/include"
+        "../../drivers/modbus/include"
+        "../../transports/tcp/include")
+
+IF (NOT WIN32)
+    target_link_libraries(plc4c-examples-hello-world-modbus
+            plc4c-spi
+            plc4c-driver-modbus
+            plc4c-transport-tcp
+            m
+    )
+ELSE()
+    target_link_libraries(plc4c-examples-hello-world-modbus
+            plc4c-spi
+            plc4c-driver-modbus
+            plc4c-transport-tcp
+    )
+ENDIF()
diff --git a/plc4c/examples/hello-world-modbus/src/hello_world_modbus.c b/plc4c/examples/hello-world-modbus/src/hello_world_modbus.c
new file mode 100644
index 0000000..91b5d26
--- /dev/null
+++ b/plc4c/examples/hello-world-modbus/src/hello_world_modbus.c
@@ -0,0 +1,355 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#include <plc4c/driver_modbus.h>
+#include <plc4c/plc4c.h>
+#include <plc4c/transport_tcp.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "../../../spi/include/plc4c/spi/types_private.h"
+
+int numOpenConnections = 0;
+
+/**
+ * Here we could implement something that keeps track of all open connections.
+ * For example on embedded devices using the W5100 SPI Network device, this can
+ * only handle 4 simultaneous connections.
+ *
+ * @param connection the connection that was just established
+ */
+void onGlobalConnect(plc4c_connection *cur_connection) {
+  printf("Connected to %s",
+         plc4c_connection_get_connection_string(cur_connection));
+  numOpenConnections++;
+}
+
+void onGlobalDisconnect(plc4c_connection *cur_connection) {
+  printf("Disconnected from %s",
+         plc4c_connection_get_connection_string(cur_connection));
+  numOpenConnections--;
+}
+
+enum plc4c_connection_state_t {
+  CONNECTING,
+  CONNECTED,
+  READ_REQUEST_SENT,
+  READ_RESPONSE_RECEIVED,
+  DISCONNECTING,
+  DISCONNECTED
+};
+typedef enum plc4c_connection_state_t plc4c_connection_state;
+
+//#pragma clang diagnostic push
+//#pragma ide diagnostic ignored "hicpp-multiway-paths-covered"
+
+int main() {
+  bool loop = true;
+  plc4c_system *system = NULL;
+  plc4c_connection *connection = NULL;
+  plc4c_read_request *read_request = NULL;
+  plc4c_read_request_execution *read_request_execution = NULL;
+
+  // Create a new uninitialized plc4c_system
+  printf("Creating new PLC4C System (Initializing inner data-structures) ... ");
+  plc4c_return_code result = plc4c_system_create(&system);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Manually register the "simulated" driver with the system.
+  printf("Registering driver for the 'modbus' protocol ... ");
+  plc4c_driver *modbus_driver = plc4c_driver_modbus_create();
+  result = plc4c_system_add_driver(system, modbus_driver);
+  if (result != OK) {
+    printf("FAILED adding modbus driver\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  plc4c_transport *tcp_transport = plc4c_transport_tcp_create();
+  result = plc4c_system_add_transport(system, tcp_transport);
+  if (result != OK) {
+    printf("FAILED adding tcp transport\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Initialize the plc4c_system
+  // (loading of drivers, setting up other stuff ...)
+  printf(
+      "Initializing the PLC4C system (Loading of drivers and transports) ... ");
+  result = plc4c_system_init(system);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Register the global callbacks.
+  plc4c_system_set_on_connect_success_callback(system, &onGlobalConnect);
+  plc4c_system_set_on_disconnect_success_callback(system, &onGlobalDisconnect);
+
+  // Establish connections to remote devices
+  // you may or may not care about the connection handle
+  printf("Connecting to 'modbus-tcp://192.168.23.30' ... ");
+  result = plc4c_system_connect(system, "modbus-tcp://192.168.23.30:502", &connection);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+
+  // Central program loop ...
+  plc4c_connection_state state = CONNECTING;
+  while (loop) {
+    printf("* ");
+
+    // Give plc4c a chance to do something.
+    // This is where all I/O is being done.
+    if (plc4c_system_loop(system) != OK) {
+      printf("ERROR in the system loop\n");
+      break;
+    }
+
+    // Depending on the current state, implement some logic.
+    switch (state) {
+      case CONNECTING: {
+        // Check if the connection is established:
+        if (plc4c_connection_get_connected(connection)) {
+          printf("SUCCESS\n");
+          state = CONNECTED;
+        } else if (plc4c_connection_has_error(connection)) {
+          printf("FAILED\n");
+          return -1;
+        }
+        break;
+      }
+      case CONNECTED: {
+        // Create a new read-request.
+        printf("Preparing a read-request ... ");
+        result =
+            plc4c_connection_create_read_request(connection, &read_request);
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        printf("SUCCESS\n");
+
+        result = plc4c_read_request_add_tag_address(read_request, "BOOL",
+                                                    "holding-register:1:BOOL");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "BYTE",
+                                                    "holding-register:2:BYTE");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "WORD",
+                                                    "holding-register:3:WORD");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "DWORD",
+                                                    "holding-register:4:DWORD");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "LWORD",
+                                                    "holding-register:6:LWORD");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "SINT",
+                                                    "holding-register:10:SINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(
+            read_request, "USINT", "holding-register:11:USINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "INT",
+                                                    "holding-register:12:INT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "UINT",
+                                                    "holding-register:13:UINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "DINT",
+                                                    "holding-register:14:DINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(
+            read_request, "UDINT", "holding-register:16:UDINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "LINT",
+                                                    "holding-register:18:LINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(
+            read_request, "ULINT", "holding-register:22:ULINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "REAL",
+                                                    "holding-register:26:REAL");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(
+            read_request, "REAL", "holding-register:28:LREAL");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        // UTF-16 doesn't work well in a portable way
+        printf("SUCCESS\n");
+
+        // Execute the read-request.
+        printf("Executing a read-request ... ");
+        result =
+            plc4c_read_request_execute(read_request, &read_request_execution);
+
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        } else {
+          state = READ_REQUEST_SENT;
+        }
+        break;
+      }
+      // Wait until the read-request execution is finished.
+      case READ_REQUEST_SENT: {
+        if (plc4c_read_request_execution_check_finished_successfully(
+                read_request_execution)) {
+          printf("SUCCESS\n");
+          state = READ_RESPONSE_RECEIVED;
+        } else if (plc4c_read_request_execution_check_finished_with_error(
+                       read_request_execution)) {
+          printf("FAILED\n");
+          return -1;
+        }
+        break;
+      }
+      case READ_RESPONSE_RECEIVED: {
+        // Get the response for the given read-request.
+        plc4c_read_response *read_response =
+            plc4c_read_request_execution_get_response(read_request_execution);
+        if (read_response == NULL) {
+          printf("FAILED (No Response)\n");
+          return -1;
+        }
+
+        // Iterate over all returned items.
+        plc4c_list_element *cur_element =
+            plc4c_utils_list_tail(read_response->items);
+        while (cur_element != NULL) {
+          plc4c_response_value_item *value_item = cur_element->value;
+
+          printf("Value %s (%s): ", value_item->item->name,
+                 plc4c_response_code_to_message(value_item->response_code));
+          plc4c_data_printf(value_item->value);
+          if (strcmp(value_item->item->name, "TIME") == 0) {
+            printf(" (Duration in ms)");
+          } else if (strcmp(value_item->item->name, "DATE") == 0) {
+            printf(" (Days since 1990-01-01)");
+          } else if ((strcmp(value_item->item->name, "TIME_OF_DAY") == 0) ||
+                     (strcmp(value_item->item->name, "TOD") == 0)) {
+            printf(" (Number of ms since midnight)");
+          }
+          printf("\n");
+
+          cur_element = cur_element->next;
+        }
+
+        // Clean up.
+        plc4c_read_response_destroy(read_response);
+        plc4c_read_request_execution_destroy(read_request_execution);
+        plc4c_read_request_destroy(read_request);
+
+        // Disconnect.
+        printf("Disconnecting ... ");
+        result = plc4c_connection_disconnect(connection);
+        if (result != OK) {
+          printf("FAILED");
+          return -1;
+        }
+        state = DISCONNECTING;
+
+        break;
+      }
+      // Wait until the connection is disconnected
+      case DISCONNECTING: {
+        if (!plc4c_connection_get_connected(connection)) {
+          printf("SUCCESS\n");
+          // we could let the system shut this down,
+          // or do it ourselves
+          plc4c_system_remove_connection(system, connection);
+          plc4c_connection_destroy(connection);
+          state = DISCONNECTED;
+
+          // Terminate the main program loop.
+          loop = false;
+        }
+        break;
+      }
+      case DISCONNECTED: {
+        // End the loop.
+        loop = false;
+        break;
+      }
+      default: {
+      }
+    }
+  }
+
+  // Make sure everything is cleaned up correctly.
+  plc4c_system_shutdown(system);
+
+  // Finally destroy the plc4c_system, freeing up all memory allocated by plc4c.
+  plc4c_system_destroy(system);
+
+  return 0;
+}
+
+//#pragma clang diagnostic pop
diff --git a/plc4c/examples/hello-world-s7/CMakeLists.txt b/plc4c/examples/hello-world-s7/CMakeLists.txt
new file mode 100644
index 0000000..cebad4f
--- /dev/null
+++ b/plc4c/examples/hello-world-s7/CMakeLists.txt
@@ -0,0 +1,69 @@
+#[[
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+]]
+
+add_executable(plc4c-examples-hello-world-s7
+        src/hello_world_s7.c
+        )
+
+target_include_directories(
+        plc4c-examples-hello-world-s7
+        PUBLIC
+        "../../api/include"
+        "../../spi/include"
+        "../../drivers/s7/include"
+        "../../transports/tcp/include" )
+
+add_executable(plc4c-examples-loopback-s7
+		src/hello_world_s7_loopback.c
+		)
+
+target_include_directories(
+        plc4c-examples-loopback-s7
+        PUBLIC
+        "../../api/include"
+        "../../spi/include"
+        "../../drivers/s7/include"
+        "../../transports/tcp/include" )
+
+IF (NOT WIN32)
+    target_link_libraries(plc4c-examples-hello-world-s7
+            plc4c-spi
+            plc4c-driver-s7
+            plc4c-transport-tcp
+            m
+    )
+    target_link_libraries(plc4c-examples-loopback-s7
+            plc4c-spi
+            plc4c-driver-s7
+            plc4c-transport-tcp
+            m
+    )
+ELSE()
+    target_link_libraries(plc4c-examples-hello-world-s7
+            plc4c-spi
+            plc4c-driver-s7
+            plc4c-transport-tcp
+    )
+    target_link_libraries(plc4c-examples-loopback-s7
+            plc4c-spi
+            plc4c-driver-s7
+            plc4c-transport-tcp
+    )
+ENDIF()
+
diff --git a/plc4c/examples/hello-world-s7/src/hello_world_s7.c b/plc4c/examples/hello-world-s7/src/hello_world_s7.c
new file mode 100644
index 0000000..dd8f4f4
--- /dev/null
+++ b/plc4c/examples/hello-world-s7/src/hello_world_s7.c
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#include <plc4c/driver_s7.h>
+#include <plc4c/plc4c.h>
+#include <plc4c/transport_tcp.h>
+#include <stdio.h>
+#include <string.h>
+
+
+#include "../../../spi/include/plc4c/spi/types_private.h"
+
+#define DEFAULT_CONNECTION_TEST_STRING "s7:tcp://192.168.23.30:102"
+
+int numOpenConnections = 0;
+
+/**
+ * Here we could implement something that keeps track of all open connections.
+ * For example on embedded devices using the W5100 SPI Network device, this can
+ * only handle 4 simultaneous connections.
+ *
+ * @param connection the connection that was just established
+ */
+void onGlobalConnect(plc4c_connection *cur_connection) {
+  printf("Connected to %s",
+         plc4c_connection_get_connection_string(cur_connection));
+  numOpenConnections++;
+}
+
+void onGlobalDisconnect(plc4c_connection *cur_connection) {
+  printf("Disconnected from %s",
+         plc4c_connection_get_connection_string(cur_connection));
+  numOpenConnections--;
+}
+
+enum plc4c_connection_state_t {
+  CONNECTING,
+  CONNECTED,
+  READ_REQUEST_SENT,
+  READ_RESPONSE_RECEIVED,
+  DISCONNECTING,
+  DISCONNECTED
+};
+typedef enum plc4c_connection_state_t plc4c_connection_state;
+
+//#pragma clang diagnostic push
+//#pragma ide diagnostic ignored "hicpp-multiway-paths-covered"
+
+int main(int argc, char** argv) {
+  
+  char* connection_test_string;
+  bool loop = true;
+  plc4c_system *system = NULL;
+  plc4c_connection *connection = NULL;
+  plc4c_read_request *read_request = NULL;
+  plc4c_read_request_execution *read_request_execution = NULL;
+
+  if (argc == 1)
+    connection_test_string = DEFAULT_CONNECTION_TEST_STRING;
+  else
+    connection_test_string = argv[1];
+
+  // Create a new uninitialized plc4c_system
+  printf("Creating new PLC4C System (Initializing inner data-structures) ... ");
+  plc4c_return_code result = plc4c_system_create(&system);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Manually register the "s7" driver with the system.
+  printf("Registering driver for the 's7' protocol ... ");
+  plc4c_driver *s7_driver = plc4c_driver_s7_create();
+  result = plc4c_system_add_driver(system, s7_driver);
+  if (result != OK) {
+    printf("FAILED adding s7 driver\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  printf("Registering driver for 'tcp' transport ... ");
+  plc4c_transport *tcp_transport = plc4c_transport_tcp_create();
+  result = plc4c_system_add_transport(system, tcp_transport);
+  if (result != OK) {
+    printf("FAILED adding tcp transport\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Initialize the plc4c_system
+  // (loading of drivers, setting up other stuff ...)
+  printf(
+      "Initializing the PLC4C system (Loading of drivers and transports) ... ");
+  result = plc4c_system_init(system);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+  printf("SUCCESS\n");
+
+  // Register the global callbacks.
+  plc4c_system_set_on_connect_success_callback(system, &onGlobalConnect);
+  plc4c_system_set_on_disconnect_success_callback(system, &onGlobalDisconnect);
+
+  // Establish connections to remote devices
+  // you may or may not care about the connection handle
+
+  printf("Connecting to '%s' ... ", connection_test_string);
+  result = plc4c_system_connect(system, connection_test_string, &connection);
+  if (result != OK) {
+    printf("FAILED\n");
+    return -1;
+  }
+
+  // Central program loop ...
+  plc4c_connection_state state = CONNECTING;
+  while (loop) {
+    printf("* ");
+
+    // Give plc4c a chance to do something.
+    // This is where all I/O is being done.
+    if (plc4c_system_loop(system) != OK) {
+      printf("ERROR in the system loop\n");
+      break;
+    }
+
+    // Depending on the current state, implement some logic.
+    switch (state) {
+      case CONNECTING: {
+        // Check if the connection is established:
+        if (plc4c_connection_get_connected(connection)) {
+          printf("SUCCESS\n");
+          state = CONNECTED;
+        } else if (plc4c_connection_has_error(connection)) {
+          printf("FAILED\n");
+          return -1;
+        }
+        break;
+      }
+      case CONNECTED: {
+        // Create a new read-request.
+        printf("Preparing a read-request ... ");
+        result =
+            plc4c_connection_create_read_request(connection, &read_request);
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        printf("SUCCESS\n");
+
+        result = plc4c_read_request_add_tag_address(read_request, "BOOL",
+                                                    "%DB4:0.0:BOOL");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "BYTE",
+                                                    "%DB4.1:BYTE");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "WORD",
+                                                    "%DB4.2:WORD");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "DWORD",
+                                                    "%DB4.4:DWORD");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "SINT",
+                                                    "%DB4.16:SINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "USINT",
+                                                    "%DB4.17:USINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "INT",
+                                                    "%DB4.18:INT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "UINT",
+                                                    "%DB4.20:UINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "DINT",
+                                                    "%DB4.22:DINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "UDINT",
+                                                    "%DB4.26:UDINT");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_tag_address(read_request, "REAL",
+                                                    "%DB4.46:REAL");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+/*        result =
+            plc4c_read_request_add_item(read_request, "TIME", "%DB4:58:TIME");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result =
+            plc4c_read_request_add_item(read_request, "DATE", "%DB4:70:DATE");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result = plc4c_read_request_add_item(read_request, "TIME_OF_DAY",
+                                             "%DB4:72:TIME_OF_DAY");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result =
+            plc4c_read_request_add_item(read_request, "TOD", "%DB4:76:TOD");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        result =
+            plc4c_read_request_add_item(read_request, "CHAR", "%DB4:136:CHAR");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }
+        // UTF-16 doesn't work well in a portable way
+        result = plc4c_read_request_add_tag_address(read_request, "STRING",
+                                             "%DB4:140:STRING(10)");
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        }*/
+        // UTF-16 doesn't work well in a portable way
+        printf("SUCCESS\n");
+
+        // Execute the read-request.
+        printf("Executing a read-request ... ");
+        result =
+            plc4c_read_request_execute(read_request, &read_request_execution);
+
+        if (result != OK) {
+          printf("FAILED\n");
+          return -1;
+        } else {
+          state = READ_REQUEST_SENT;
+        }
+        break;
+      }
+      // Wait until the read-request execution is finished.
+      case READ_REQUEST_SENT: {
+        if (plc4c_read_request_execution_check_finished_successfully(
+                read_request_execution)) {
+          printf("SUCCESS\n");
+          state = READ_RESPONSE_RECEIVED;
+        } else if (plc4c_read_request_execution_check_finished_with_error(
+                       read_request_execution)) {
+          printf("FAILED\n");
+          return -1;
+        }
+        break;
+      }
+      case READ_RESPONSE_RECEIVED: {
+        // Get the response for the given read-request.
+        plc4c_read_response *read_response =
+            plc4c_read_request_execution_get_response(read_request_execution);
+        if (read_response == NULL) {
+          printf("FAILED (No Response)\n");
+          return -1;
+        }
+
+        // Iterate over all returned items.
+        plc4c_list_element *cur_element =
+            plc4c_utils_list_tail(read_response->items);
+        while (cur_element != NULL) {
+          plc4c_response_value_item *value_item = cur_element->value;
+
+          printf("Value %s (%s): ", value_item->item->name,
+                 plc4c_response_code_to_message(value_item->response_code));
+          plc4c_data_printf(value_item->value);
+          if (strcmp(value_item->item->name, "TIME") == 0) {
+            printf(" (Duration in ms)");
+          } else if (strcmp(value_item->item->name, "DATE") == 0) {
+            printf(" (Days since 1990-01-01)");
+          } else if ((strcmp(value_item->item->name, "TIME_OF_DAY") == 0) ||
+                (strcmp(value_item->item->name, "TOD") == 0)) {
+            printf(" (Number of ms since midnight)");
+          }
+          printf("\n");
+
+          cur_element = cur_element->next;
+        }
+
+        // Clean up.
+        plc4c_read_response_destroy(read_response);
+        plc4c_read_request_execution_destroy(read_request_execution);
+        plc4c_read_request_destroy(read_request);
+
+        // Disconnect.
+        printf("Disconnecting ... ");
+        result = plc4c_connection_disconnect(connection);
+        if (result != OK) {
+          printf("FAILED");
+          return -1;
+        }
+        state = DISCONNECTING;
+
+        break;
+      }
+      // Wait until the connection is disconnected
+      case DISCONNECTING: {
+        if (!plc4c_connection_get_connected(connection)) {
+          printf("SUCCESS\n");
+          // we could let the system shut this down,
+          // or do it ourselves
+          plc4c_system_remove_connection(system, connection);
+          plc4c_connection_destroy(connection);
+          state = DISCONNECTED;
+
+          // Terminate the main program loop.
+          loop = false;
+        }
+        break;
+      }
+      case DISCONNECTED: {
+        // End the loop.
+        loop = false;
+        break;
+      }
+      default: {
+      }
+    }
+
+  }
+
+  // Make sure everything is cleaned up correctly.
+  plc4c_system_shutdown(system);
+
+  // Finally destroy the plc4c_system, freeing up all memory allocated by plc4c.
+  plc4c_system_destroy(system);
+
+  return 0;
+}
+
+//#pragma clang diagnostic pop
diff --git a/plc4c/examples/hello-world-s7/src/hello_world_s7_loopback.c b/plc4c/examples/hello-world-s7/src/hello_world_s7_loopback.c
new file mode 100644
index 0000000..8e5ff5b
--- /dev/null
+++ b/plc4c/examples/hello-world-s7/src/hello_world_s7_loopback.c
@@ -0,0 +1,350 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#define _GNU_SOURCE
+/*#ifndef CLOCK_MONOTONIC
+#define CLOCK_MONOTONIC 1
+#endif*/
+
+
+#include <plc4c/driver_s7.h>
+#include <plc4c/plc4c.h>
+#include <plc4c/transport_tcp.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "../../../spi/include/plc4c/spi/types_private.h"
+
+#define DEFAULT_CONNECTION_TEST_STRING "s7:tcp://0.0.0.0:102"
+#define TEST_DOUBLE_INT
+int numOpenConnections = 0;
+
+#ifndef _WIN32
+  #include <unistd.h>
+  #include <time.h>
+  #define S7_LOOPBACK_TIME_IO
+#endif
+
+/**
+ * Here we could implement something that keeps track of all open connections.
+ * For example on embedded devices using the W5100 SPI Network device, this can
+ * only handle 4 simultaneous connections.
+ *
+ * @param connection the connection that was just established
+ */
+void onGlobalConnect(plc4c_connection *cur_connection) {
+  printf("Connected to %s",
+         plc4c_connection_get_connection_string(cur_connection));
+  numOpenConnections++;
+}
+
+void onGlobalDisconnect(plc4c_connection *cur_connection) {
+  printf("Disconnected from %s",
+         plc4c_connection_get_connection_string(cur_connection));
+  numOpenConnections--;
+}
+
+
+bool syncBoolLoop(plc4c_connection *conn, 
+  bool (passCallback)(plc4c_connection* conn),
+  bool (failCallback)(plc4c_connection *conn)  ) {
+
+  plc4c_system *system;
+  system = plc4c_connection_get_system(conn);
+  while (true) {
+    if (passCallback(conn))
+      return EXIT_SUCCESS;
+    else if (failCallback(conn))
+      return EXIT_FAILURE;
+    if (plc4c_system_loop(system) != OK)
+      return EXIT_FAILURE;
+  }
+}
+
+//#pragma clang diagnostic push
+//#pragma ide diagnostic ignored "hicpp-multiway-paths-covered"
+
+#define CHECK_RESULT(chk, ret, fs) do {if (chk) {printf(fs); return(ret);}} while(0)
+
+void get_user_loopback_values(int argc, char **argv, long *value) {
+  int count = 0;
+  while (count < argc)
+    value[count++] = atol(argv[count]);
+}
+
+int main(int argc, char** argv) {
+  
+#ifdef S7_LOOPBACK_TIME_IO
+  struct timespec start, finish;
+  long delta_us, diff_s, diff_ns;
+#endif
+
+  char* connection_test_string;
+  plc4c_return_code result;
+  bool errorFlag = false;
+  int idx = 0;
+  plc4c_system *system = NULL;
+  plc4c_connection *connection = NULL;
+  
+  plc4c_read_request *read_request = NULL;
+  plc4c_read_request_execution *read_request_execution = NULL;
+  plc4c_read_response *read_response;
+
+  plc4c_write_request *write_request = NULL;
+  plc4c_write_request_execution *write_request_execution = NULL;
+  plc4c_write_response *write_response;
+
+  plc4c_list_element *cur_element;
+  plc4c_response_value_item *value_item;
+
+  long loopback_value[7] = {0,0,0,0,0};
+  plc4c_data *loopback_data;
+
+  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  bool doRead = 1;
+  bool doWrite = 1;
+  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+  // Connection string argument and do_write test arg (defaults to off as
+  // not currently working)
+  connection_test_string = DEFAULT_CONNECTION_TEST_STRING;
+
+  // Initialisation and startup sequence
+  result = plc4c_system_create(&system);
+  CHECK_RESULT(result != OK, result, "plc4c_system_create failed\n");
+  plc4c_driver *s7_driver = plc4c_driver_s7_create();
+  result = plc4c_system_add_driver(system, s7_driver);
+  CHECK_RESULT(result != OK, result, "plc4c_system_add_driver failed\n");
+  plc4c_transport *tcp_transport = plc4c_transport_tcp_create();
+  result = plc4c_system_add_transport(system, tcp_transport);
+  CHECK_RESULT(result != OK, result, "plc4c_system_add_transport failed\n");
+  result = plc4c_system_init(system);
+  CHECK_RESULT(result != OK, result, "plc4c_system_init failed\n");
+
+  // Register the global callbacks
+  plc4c_system_set_on_connect_success_callback(system, &onGlobalConnect);
+  plc4c_system_set_on_disconnect_success_callback(system, &onGlobalDisconnect);
+  
+  // Establish connections to remote devices
+  result = plc4c_system_connect(system, connection_test_string, &connection);
+  CHECK_RESULT(result != OK, result, "plc4c_system_connect failed\n");
+
+  unsigned int loopTimes;
+  if (argc >= 2)
+    loopTimes = (unsigned int) atoi(argv[1]);
+  else
+    loopTimes = 1;
+
+  unsigned int loopCount = 0;
+  #define NREAD 3
+  float valuetowrite[NREAD] = {1.1 ,2.2, 3.3};//, 5.5};
+
+  #define ITEM_STR "%DB2:4.0:REAL[3]"
+
+  // Central program loop ...
+  syncBoolLoop(connection, plc4c_connection_get_connected, plc4c_connection_has_error);
+  bool rtMalloc = false;
+  
+  while ( (!errorFlag) && (loopCount++ < loopTimes)) {
+    if (doWrite) {
+      { // Write request create scope
+        if (loopCount == 1 || rtMalloc) {
+          result = plc4c_connection_create_write_request(connection, &write_request);
+          CHECK_RESULT(result != OK, result,"plc4c_connection_create_write_request failed\n");
+          loopback_data = plc4c_data_create_real_array(valuetowrite,NREAD);
+          valuetowrite[(loopCount-1)%NREAD]++;
+          result = plc4c_write_request_add_item(write_request, ITEM_STR, loopback_data);
+        } else {
+          plc4c_data_update_values(loopback_data, valuetowrite);
+          valuetowrite[(loopCount-1)%NREAD]++;
+          //valuetowrite++;
+        }
+        
+
+        #ifdef S7_LOOPBACK_TIME_IO
+          clock_gettime(CLOCK_MONOTONIC,&start);
+        #endif
+
+        result = plc4c_write_request_execute(write_request, &write_request_execution);
+        
+        CHECK_RESULT(result != OK, result,"plc4c_write_request_execute failed\n");
+        
+        if (plc4c_system_loop(system) != OK) {
+          printf("ERROR in the system loop\n");
+          break;
+        }
+      }
+
+      { // Write request sent scope
+        while(1) {
+          if (plc4c_write_request_check_finished_successfully(write_request_execution)) {
+            break;
+          } else if (plc4c_write_request_execution_check_completed_with_error(write_request_execution)) {
+            printf("FAILED\n");
+            errorFlag = true;
+            break;
+          }
+          if (plc4c_system_loop(system) != OK) {
+            printf("ERROR in the system loop\n");
+            errorFlag = true;
+            break;
+          }
+        }
+      }
+
+      { // WRITE_RESPONSE_RECEIVED scope
+        write_response = plc4c_write_request_execution_get_response(write_request_execution);
+        CHECK_RESULT(write_response == NULL, -1,"plc4c_write_request_execution_get_response failed (no response)\n");
+        cur_element = plc4c_utils_list_tail(write_response->response_items);
+        idx = 0;
+        while (cur_element != NULL) {
+          plc4c_response_item *checker = (plc4c_response_item*) cur_element->value;
+          printf("Write item %d status: '%s'\n", idx++,
+              plc4c_response_code_to_message(checker->response_code));
+          cur_element = cur_element->next;
+        }
+
+        #ifdef S7_LOOPBACK_TIME_IO
+          clock_gettime(CLOCK_MONOTONIC,&finish);
+          diff_s = finish.tv_sec - start.tv_sec;
+          diff_ns = finish.tv_nsec - start.tv_nsec;
+          delta_us = (diff_s * 1000000L) + (diff_ns / 1000L);
+          printf("Write %ldus%c", delta_us, doRead ? '\t' : '\n');
+        #endif
+        
+        if (rtMalloc)
+          plc4c_write_request_destroy(write_request);
+  
+        plc4c_write_response_destroy(write_response);
+        plc4c_write_request_execution_destroy(write_request_execution);
+
+        if (plc4c_system_loop(system) != OK) {
+          printf("ERROR in the system loop\n");
+          break;
+        }
+      }
+    } // end of doWrite
+
+
+    if (doRead) {
+      { // READ_REQUEST_CREATE scope
+        
+        result = plc4c_connection_create_read_request(connection, &read_request);
+        CHECK_RESULT(result != OK, result, "plc4c_connection_create_read_request failed\n");
+        
+        result = plc4c_read_request_add_tag_address(read_request, "A_REQUEST",
+                                                    ITEM_STR);
+        CHECK_RESULT(result != OK, result, "plc4c_read_request_add_tag_address failed\n");
+
+        result = plc4c_read_request_execute(read_request, &read_request_execution);
+        CHECK_RESULT(result != OK, result, "plc4c_read_request_execute failed\n");
+        
+        #ifdef S7_LOOPBACK_TIME_IO
+          clock_gettime(CLOCK_MONOTONIC,&start);
+        #endif
+
+        if (plc4c_system_loop(system) != OK) {
+          printf("ERROR in the system loop\n");
+          break;
+        }
+      }
+
+
+      { // READ_REQUEST_SENT scope
+        while(true) {
+          if (plc4c_read_request_execution_check_finished_successfully(read_request_execution)) {
+            #ifdef S7_LOOPBACK_TIME_IO
+              clock_gettime(CLOCK_MONOTONIC,&finish);
+              diff_s = finish.tv_sec - start.tv_sec;
+              diff_ns = finish.tv_nsec - start.tv_nsec;
+              delta_us = (diff_s * 1000000L) + (diff_ns / 1000L);
+              printf("Read %ldus\n", delta_us);
+            #endif
+            break;
+          } else if (plc4c_read_request_execution_check_finished_with_error(read_request_execution)) {
+            printf("plc4c_read_request_execution_check_finished_with_error FAILED\n");
+            errorFlag= true;
+            break;
+          }
+          if (plc4c_system_loop(system) != OK) {
+            printf("ERROR in the system loop\n");
+            errorFlag = true;
+            break;
+          }
+        }
+      }
+
+
+      { // READ_RESPONSE_RECEIVED scope
+
+        read_response = plc4c_read_request_execution_get_response(read_request_execution);
+        CHECK_RESULT(read_response == NULL, -1, "plc4c_read_request_execution_get_response failed (No Response)\n");
+        
+        // Iterate over all returned items.
+        cur_element = plc4c_utils_list_tail(read_response->items);
+        while (cur_element != NULL) {
+          value_item = cur_element->value;
+          printf("Value %s (%s): ", value_item->item->name,
+                  plc4c_response_code_to_message(value_item->response_code));
+          plc4c_data_printf(value_item->value);
+          printf("\n");
+          cur_element = cur_element->next;
+        }
+
+        plc4c_read_response_destroy(read_response);
+        plc4c_read_request_execution_destroy(read_request_execution);
+        plc4c_read_request_destroy(read_request);
+
+        if (plc4c_system_loop(system) != OK) {
+          printf("ERROR in the system loop\n");
+          errorFlag = true;
+          break;
+        }
+      }
+    } // end of do read
+  } 
+
+  if (!rtMalloc && doWrite) 
+    plc4c_write_request_destroy(write_request);
+  
+  // Start disconnecting, break on error or dis-connection
+  result = plc4c_connection_disconnect(connection);
+  CHECK_RESULT(result != OK, -1,"plc4c_connection_disconnect failed\n");
+
+  while (true) {
+    if (plc4c_system_loop(system) != OK) {
+      printf("ERROR in the system loop\n");
+      return EXIT_FAILURE;
+    } else if (!plc4c_connection_get_connected(connection)) {
+      plc4c_system_remove_connection(system, connection);
+      plc4c_connection_destroy(connection);
+      break;
+    }
+  }
+
+  // Make sure everything is cleaned up correctly then destroy the 
+  // plc4c_system, freeing up all memory allocated by plc4
+  plc4c_system_shutdown(system);
+  plc4c_system_destroy(system);
+  return 0;
+}
+
+//#pragma clang diagnostic pop
diff --git a/plc4c/integrations/CMakeLists.txt b/plc4c/integrations/CMakeLists.txt
new file mode 100644
index 0000000..646d39b
--- /dev/null
+++ b/plc4c/integrations/CMakeLists.txt
@@ -0,0 +1,20 @@
+#[[
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+]]
+
+add_subdirectory(apache-mynewt)
diff --git a/plc4c/integrations/apache-mynewt/CMakeLists.txt b/plc4c/integrations/apache-mynewt/CMakeLists.txt
new file mode 100644
index 0000000..daa62aa
--- /dev/null
+++ b/plc4c/integrations/apache-mynewt/CMakeLists.txt
@@ -0,0 +1,23 @@
+#[[
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+]]
+
+add_library(plc4c-integrations-apache-mynewt
+        )
+
+target_link_libraries(plc4c-integrations-apache-mynewt ${CMAKE_DL_LIBS})
\ No newline at end of file
diff --git a/plc4go/examples/ads/browse/Browse.go b/plc4go/examples/ads/browse/Browse.go
new file mode 100644
index 0000000..c7c3bf2
--- /dev/null
+++ b/plc4go/examples/ads/browse/Browse.go
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+)
+
+func main() {
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterAdsDriver(driverManager)
+	connectionChan := driverManager.GetConnection("ads:tcp://192.168.23.20?sourceAmsNetId=192.168.23.200.1.1&sourceAmsPort=65534&targetAmsNetId=192.168.23.20.1.1&targetAmsPort=851")
+	connection := <-connectionChan
+	browseRequest, err := connection.GetConnection().BrowseRequestBuilder().AddQuery("all", "MAIN.rivianTest01.RotationalPosition").Build()
+	if err != nil {
+		panic(err)
+	}
+	browseResponseChannel := browseRequest.Execute()
+	browseResponse := <-browseResponseChannel
+	print(browseResponse)
+}
diff --git a/plc4go/examples/ads/discovery/Discovery.go b/plc4go/examples/ads/discovery/Discovery.go
new file mode 100644
index 0000000..e19cc19
--- /dev/null
+++ b/plc4go/examples/ads/discovery/Discovery.go
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"context"
+	"time"
+
+	"github.com/apache/plc4x/plc4go/internal/ads"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+)
+
+func main() {
+	discoverer := ads.NewDiscoverer()
+	_ = discoverer.Discover(context.Background(), func(event apiModel.PlcDiscoveryItem) {
+		print(event)
+	})
+	time.Sleep(time.Second * 5)
+}
diff --git a/plc4go/examples/ads/read/Read.go b/plc4go/examples/ads/read/Read.go
new file mode 100644
index 0000000..96d82af
--- /dev/null
+++ b/plc4go/examples/ads/read/Read.go
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+)
+
+func main() {
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterAdsDriver(driverManager)
+	connectionChan := driverManager.GetConnection("ads:tcp://192.168.23.20?sourceAmsNetId=192.168.23.200.1.1&sourceAmsPort=65534&targetAmsNetId=192.168.23.20.1.1&targetAmsPort=851")
+	connection := <-connectionChan
+	readRequest, err := connection.GetConnection().ReadRequestBuilder().
+		AddTagAddress("value-bool", "MAIN.hurz_BOOL").
+		AddTagAddress("value-byte", "MAIN.hurz_BYTE").
+		AddTagAddress("value-word", "MAIN.hurz_WORD").
+		AddTagAddress("value-dword", "MAIN.hurz_DWORD").
+		AddTagAddress("value-lword", "MAIN.hurz_LWORD").
+		AddTagAddress("value-sint", "MAIN.hurz_SINT").
+		AddTagAddress("value-usint", "MAIN.hurz_USINT").
+		AddTagAddress("value-int", "MAIN.hurz_INT").
+		AddTagAddress("value-uint", "MAIN.hurz_UINT").
+		AddTagAddress("value-dint", "MAIN.hurz_DINT").
+		AddTagAddress("value-udint", "MAIN.hurz_UDINT").
+		AddTagAddress("value-lint", "MAIN.hurz_LINT").
+		AddTagAddress("value-ulint", "MAIN.hurz_ULINT").
+		AddTagAddress("value-real", "MAIN.hurz_REAL").
+		AddTagAddress("value-lreal", "MAIN.hurz_LREAL").
+		AddTagAddress("value-string", "MAIN.hurz_STRING").
+		AddTagAddress("value-wstring", "MAIN.hurz_WSTRING").
+		AddTagAddress("value-time", "MAIN.hurz_TIME").
+		AddTagAddress("value-ltime", "MAIN.hurz_LTIME").
+		AddTagAddress("value.date", "MAIN.hurz_DATE").
+		AddTagAddress("value-time-of-day", "MAIN.hurz_TIME_OF_DAY").
+		AddTagAddress("value-date-and-time", "MAIN.hurz_DATE_AND_TIME").
+		AddTagAddress("value-struct", "MAIN.hurz_Struct").
+		Build()
+	if err != nil {
+		panic(err)
+	}
+	readResponseChannel := readRequest.Execute()
+	readResult := <-readResponseChannel
+	readResponse := readResult.GetResponse()
+	for _, tagName := range readResponse.GetTagNames() {
+		plcValue := readResponse.GetValue(tagName)
+		print(plcValue)
+	}
+}
diff --git a/plc4go/examples/ads/subscribe/Subscribe.go b/plc4go/examples/ads/subscribe/Subscribe.go
new file mode 100644
index 0000000..c39eb30
--- /dev/null
+++ b/plc4go/examples/ads/subscribe/Subscribe.go
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"time"
+
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+)
+
+func main() {
+	zerolog.SetGlobalLevel(zerolog.InfoLevel)
+
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterAdsDriver(driverManager)
+	connectionChan := driverManager.GetConnection("ads:tcp://192.168.23.20?sourceAmsNetId=192.168.23.200.1.1&sourceAmsPort=65534&targetAmsNetId=192.168.23.20.1.1&targetAmsPort=851")
+	connection := <-connectionChan
+
+	subscriptionRequest, err := connection.GetConnection().SubscriptionRequestBuilder().
+		AddChangeOfStateTagAddress("value-int", "MAIN.rivianTest01.HorizontalPosition").
+		AddPreRegisteredConsumer("value-int", func(event apiModel.PlcSubscriptionEvent) {
+			value := event.GetValue("value-int")
+			log.Info().Uint16("value", value.GetUint16()).Msg("Got value")
+		}).
+		Build()
+	if err != nil {
+		panic(err)
+	}
+	subscriptionResponseChannel := subscriptionRequest.Execute()
+	subscriptionResult := <-subscriptionResponseChannel
+	if subscriptionResult.GetErr() != nil {
+		log.Error().Err(subscriptionResult.GetErr()).Msg("error in response")
+		return
+	}
+	subscriptionResponse := subscriptionResult.GetResponse()
+	for _, tagName := range subscriptionResponse.GetTagNames() {
+		responseCode := subscriptionResponse.GetResponseCode(tagName)
+		print(responseCode)
+	}
+
+	time.Sleep(200 * time.Second)
+
+}
diff --git a/plc4go/examples/ads/write/Write.go b/plc4go/examples/ads/write/Write.go
new file mode 100644
index 0000000..a14eb53
--- /dev/null
+++ b/plc4go/examples/ads/write/Write.go
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"time"
+
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	apiValues "github.com/apache/plc4x/plc4go/pkg/api/values"
+	spiValues "github.com/apache/plc4x/plc4go/spi/values"
+
+	"github.com/rs/zerolog/log"
+)
+
+func main() {
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterAdsDriver(driverManager)
+	connectionChan := driverManager.GetConnection("ads:tcp://192.168.23.20?sourceAmsNetId=192.168.23.200.1.1&sourceAmsPort=65534&targetAmsNetId=192.168.23.20.1.1&targetAmsPort=851")
+	connection := <-connectionChan
+
+	duration, _ := time.ParseDuration("1.234S")
+	lduration, _ := time.ParseDuration("24015H23M12.034002044S")
+	date, _ := time.Parse("2014-11-12", "1978-03-28")
+	timeOfDay, _ := time.Parse("11:45:26.371Z", "15:36:30.123")
+	dateAndTime, _ := time.Parse("2014-11-12T11:45:26.371Z", "1996-05-06T15:36:30")
+
+	// Prepare the data structure for a custom type.
+	children := map[string]apiValues.PlcValue{}
+	children["hurz_BOOL"] = spiValues.NewPlcBOOL(true)
+	children["hurz_BYTE"] = spiValues.NewPlcBYTE(1)
+	children["hurz_WORD"] = spiValues.NewPlcWORD(2)
+	children["hurz_DWORD"] = spiValues.NewPlcDWORD(3)
+	children["hurz_LWORD"] = spiValues.NewPlcLWORD(4)
+	children["hurz_SINT"] = spiValues.NewPlcSINT(5)
+	children["hurz_USINT"] = spiValues.NewPlcUSINT(6)
+	children["hurz_INT"] = spiValues.NewPlcINT(7)
+	children["hurz_UINT"] = spiValues.NewPlcUINT(8)
+	children["hurz_DINT"] = spiValues.NewPlcDINT(9)
+	children["hurz_UDINT"] = spiValues.NewPlcUDINT(10)
+	children["hurz_LINT"] = spiValues.NewPlcLINT(11)
+	children["hurz_ULINT"] = spiValues.NewPlcULINT(12)
+	children["hurz_REAL"] = spiValues.NewPlcREAL(13.0)
+	children["hurz_LREAL"] = spiValues.NewPlcLREAL(14.0)
+	children["hurz_STRING"] = spiValues.NewPlcSTRING("hurz")
+	children["hurz_WSTRING"] = spiValues.NewPlcWSTRING("wolf")
+	children["hurz_TIME"] = spiValues.NewPlcTIME(duration)
+	children["hurz_LTIME"] = spiValues.NewPlcLTIME(lduration)
+	children["hurz_DATE"] = spiValues.NewPlcDATE(date)
+	children["hurz_TIME_OF_DAY"] = spiValues.NewPlcTIME_OF_DAY(timeOfDay)
+	children["hurz_DATE_AND_TIME"] = spiValues.NewPlcDATE_AND_TIME(dateAndTime)
+	writeRequest, err := connection.GetConnection().WriteRequestBuilder().
+		AddTagAddress("value-bool", "MAIN.hurz_BOOL", spiValues.NewPlcBOOL(true)).                   // 1
+		AddTagAddress("value-byte", "MAIN.hurz_BYTE", spiValues.NewPlcBYTE(42)).                     // 1
+		AddTagAddress("value-word", "MAIN.hurz_WORD", spiValues.NewPlcWORD(42424)).                  // 2
+		AddTagAddress("value-dword", "MAIN.hurz_DWORD", spiValues.NewPlcDWORD(4242442424)).          // 4
+		AddTagAddress("value-lword", "MAIN.hurz_LWORD", spiValues.NewPlcLWORD(4242442424242424242)). // 8
+		AddTagAddress("value-sint", "MAIN.hurz_SINT", spiValues.NewPlcSINT(-42)).                    // 1
+		AddTagAddress("value-usint", "MAIN.hurz_USINT", spiValues.NewPlcUSINT(42)).                  // 1
+		AddTagAddress("value-int", "MAIN.hurz_INT", spiValues.NewPlcINT(-2424)).                     // 2
+		AddTagAddress("value-uint", "MAIN.hurz_UINT", spiValues.NewPlcUINT(42424)).                  // 2
+		AddTagAddress("value-dint", "MAIN.hurz_DINT", spiValues.NewPlcDINT(-242442424)).             // 4
+		AddTagAddress("value-udint", "MAIN.hurz_UDINT", spiValues.NewPlcUDINT(4242442424)).          // 4
+		AddTagAddress("value-lint", "MAIN.hurz_LINT", spiValues.NewPlcLINT(-4242442424242424242)).   // 8
+		AddTagAddress("value-ulint", "MAIN.hurz_ULINT", spiValues.NewPlcULINT(4242442424242424242)). // 8
+		AddTagAddress("value-real", "MAIN.hurz_REAL", spiValues.NewPlcREAL(3.14159265359)).          // 4
+		AddTagAddress("value-lreal", "MAIN.hurz_LREAL", spiValues.NewPlcLREAL(2.71828182846)).       // 8
+		AddTagAddress("value-string", "MAIN.hurz_STRING", spiValues.NewPlcSTRING("hurz")).           // 4
+		AddTagAddress("value-wstring", "MAIN.hurz_WSTRING", spiValues.NewPlcWSTRING("wolf")).        // 8
+		AddTagAddress("value-time", "MAIN.hurz_TIME", spiValues.NewPlcTIME(duration)).
+		AddTagAddress("value-ltime", "MAIN.hurz_LTIME", spiValues.NewPlcLTIME(lduration)).
+		AddTagAddress("value.date", "MAIN.hurz_DATE", spiValues.NewPlcDATE(date)).
+		AddTagAddress("value-time-of-day", "MAIN.hurz_TIME_OF_DAY", spiValues.NewPlcTIME_OF_DAY(timeOfDay)).
+		AddTagAddress("value-date-and-time", "MAIN.hurz_DATE_AND_TIME", spiValues.NewPlcDATE_AND_TIME(dateAndTime)).
+		AddTagAddress("value-struct", "MAIN.hurz_Struct", spiValues.NewPlcStruct(children)).
+		Build()
+	if err != nil {
+		panic(err)
+	}
+	writeResponseChannel := writeRequest.Execute()
+	writeResult := <-writeResponseChannel
+	if writeResult.GetErr() != nil {
+		log.Error().Err(writeResult.GetErr()).Msg("error in response")
+		return
+	}
+	writeResponse := writeResult.GetResponse()
+	for _, tagName := range writeResponse.GetTagNames() {
+		responseCode := writeResponse.GetResponseCode(tagName)
+		print(responseCode)
+	}
+}
diff --git a/plc4go/examples/bacnet/discovery/hello_world_plc4go_bacnet_discovery.go b/plc4go/examples/bacnet/discovery/hello_world_plc4go_bacnet_discovery.go
new file mode 100644
index 0000000..4e2c77b
--- /dev/null
+++ b/plc4go/examples/bacnet/discovery/hello_world_plc4go_bacnet_discovery.go
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"fmt"
+	"github.com/apache/plc4x/plc4go/spi/options"
+	"os"
+	"time"
+
+	"github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+
+	"github.com/rs/zerolog/log"
+)
+
+func main() {
+	logger := log.With().Str("myCustomLogger", "example").Logger()
+
+	driverManager := plc4go.NewPlcDriverManager(
+		options.WithCustomLogger(logger),
+		options.WithTraceTransactionManagerTransactions(true),
+	)
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterBacnetDriver(driverManager)
+
+	var connectionStrings []string
+	if len(os.Args) < 2 {
+		// Try to auto-find bacnet devices via broadcast-message discovery
+		if err := driverManager.Discover(func(event apiModel.PlcDiscoveryItem) {
+			connStr := event.GetProtocolCode() + "://" + event.GetTransportUrl().Host
+			log.Info().Str("connection string", connStr).Stringer("event", event.(fmt.Stringer)).Msg("Found Bacnet Gateway")
+
+			connectionStrings = append(connectionStrings, connStr)
+		},
+			plc4go.WithDiscoveryOptionProtocolSpecific("who-is-low-limit", 0),
+			plc4go.WithDiscoveryOptionProtocolSpecific("who-is-high-limit", "255"),
+			plc4go.WithDiscoveryOptionProtocolSpecific("who-has-device-instance-range-low-limit", "0"),
+			plc4go.WithDiscoveryOptionProtocolSpecific("who-has-device-instance-range-high-limit", "255"),
+			// Either use name or the 2 identifier tuple below
+			plc4go.WithDiscoveryOptionProtocolSpecific("who-has-object-name", "john"),
+			//plc4go.WithDiscoveryOptionProtocolSpecific("who-has-object-identifier-type", "DEVICE"),
+			//plc4go.WithDiscoveryOptionProtocolSpecific("who-has-object-identifier-instance", "133"),
+		); err != nil {
+			panic(err)
+		}
+		// Wait for 5 seconds for incoming responses
+		time.Sleep(time.Second * 50)
+	} else {
+		connStr := "bacnet-ip://" + os.Args[1] + ":47808"
+		log.Info().Str("connection string", connStr).Msg("Using manually provided bacnet gateway")
+		connectionStrings = append(connectionStrings, connStr)
+	}
+
+	for _, connStr := range connectionStrings {
+		log.Info().Str("connection string", connStr).Msg("Connecting")
+		crc := driverManager.GetConnection(connStr)
+
+		// Wait for the driver to connect (or not)
+		connectionResult := <-crc
+		if connectionResult.GetErr() != nil {
+			log.Error().Err(connectionResult.GetErr()).Msg("error connecting to PLC")
+			return
+		}
+		log.Info().Str("connection string", connStr).Msg("Connected")
+		connection := connectionResult.GetConnection()
+		connection.BlockingClose()
+	}
+}
diff --git a/plc4go/examples/doc.go b/plc4go/examples/doc.go
new file mode 100644
index 0000000..e37a632
--- /dev/null
+++ b/plc4go/examples/doc.go
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Package examples contains some examples how to use plc4go (go part of plc4x)
+package examples
diff --git a/plc4go/examples/knx/discovery/hello_world_plc4go_knx_discovery.go b/plc4go/examples/knx/discovery/hello_world_plc4go_knx_discovery.go
new file mode 100644
index 0000000..2ccd9ee
--- /dev/null
+++ b/plc4go/examples/knx/discovery/hello_world_plc4go_knx_discovery.go
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	"github.com/apache/plc4x/plc4go/pkg/api/logging"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+	"github.com/apache/plc4x/plc4go/spi/utils"
+
+	"github.com/rs/zerolog/log"
+)
+
+func main() {
+	// Set logging to INFO
+	logging.InfoLevel()
+
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterKnxDriver(driverManager)
+
+	var connectionStrings []string
+	if len(os.Args) < 2 {
+		// Try to auto-find KNX gateways via broadcast-message discovery
+		_ = driverManager.Discover(func(event apiModel.PlcDiscoveryItem) {
+			connStr := event.GetProtocolCode() + "://" + event.GetTransportUrl().Host
+			log.Info().Str("connection string", connStr).Msg("Found KNX Gateway")
+
+			connectionStrings = append(connectionStrings, connStr)
+		})
+		// Wait for 5 seconds for incoming responses
+		time.Sleep(time.Second * 5)
+	} else {
+		connStr := "knxnet-ip://" + os.Args[1] + ":3671"
+		log.Info().Str("connection string", connStr).Msg("Using manually provided KNX Gateway")
+		connectionStrings = append(connectionStrings, connStr)
+	}
+
+	for _, connStr := range connectionStrings {
+		log.Info().Str("connection string", connStr).Msg("Connecting")
+		crc := driverManager.GetConnection(connStr)
+
+		// Wait for the driver to connect (or not)
+		connectionResult := <-crc
+		if connectionResult.GetErr() != nil {
+			log.Error().Err(connectionResult.GetErr()).Msg("error connecting to PLC")
+			return
+		}
+		log.Info().Str("connection string", connStr).Msg("Connected")
+		connection := connectionResult.GetConnection()
+		connection.BlockingClose()
+
+		// Try to find all KNX devices on the current network
+		browseRequest, err := connection.BrowseRequestBuilder().
+			AddQuery("allDevices", "[1-15].[1-15].[0-255]").
+			//AddQuery("allMyDevices", "[1-3].[1-6].[0-60]").
+			//AddQuery("onlyOneDevice", "1.1.20")
+			Build()
+		if err != nil {
+			log.Error().Err(err).Msg("error creating browse request")
+			return
+		}
+		brr := browseRequest.ExecuteWithInterceptor(func(result apiModel.PlcBrowseItem) bool {
+			knxTag := result.GetTag()
+			knxAddress := knxTag.GetAddressString()
+			log.Info().Str("knxAddress", knxAddress).Msg("Inspecting detected Device at KNX Address")
+
+			// Try to get all the com-objects and the group addresses they are attached to.
+			browseRequest, err := connection.BrowseRequestBuilder().
+				AddQuery("comObjects", knxAddress+"#com-obj").
+				Build()
+			if err != nil {
+				log.Error().Err(err).Msg("error creating read request")
+				return false
+			}
+			brr := browseRequest.Execute()
+			browseResult := <-brr
+			if browseResult.GetErr() != nil {
+				log.Error().Err(browseResult.GetErr()).Msg("error executing the browse request for com-objects")
+				return false
+			}
+			for _, result := range browseResult.GetResponse().GetQueryResults("comObjects") {
+				permissions := ""
+				if result.IsReadable() {
+					permissions += "R"
+				} else {
+					permissions += " "
+				}
+				if result.IsWritable() {
+					permissions += "W"
+				} else {
+					permissions += " "
+				}
+				if result.IsSubscribable() {
+					permissions += "S"
+				} else {
+					permissions += " "
+				}
+				log.Info().
+					Str("addressString", fmt.Sprintf("%15s", result.GetTag().GetAddressString())).
+					Str("permissions", permissions).
+					Str("name", result.GetName()).
+					Msg(" - addressString (permissions) name")
+			}
+
+			readRequest, err := connection.ReadRequestBuilder().
+				AddTagAddress("applicationProgramVersion", knxAddress+"#3/13").
+				AddTagAddress("interfaceProgramVersion", knxAddress+"#4/13").
+				Build()
+			if err != nil {
+				log.Error().Str("knxAddress", knxAddress).Msg("Error creating read request for scanning")
+				return false
+			}
+
+			rrr := readRequest.Execute()
+			readRequestResult := <-rrr
+
+			if readRequestResult.GetErr() != nil {
+				log.Error().Str("knxAddress", knxAddress).Msg("Error executing read request for reading device identification information")
+				return false
+			}
+			readResponse := readRequestResult.GetResponse()
+			var programVersion []byte
+			if readResponse.GetResponseCode("applicationProgramVersion") == apiModel.PlcResponseCode_OK {
+				programVersion = utils.PlcValueUint8ListToByteArray(readResponse.GetValue("applicationProgramVersion"))
+			} else if readResponse.GetResponseCode("interfaceProgramVersion") == apiModel.PlcResponseCode_OK {
+				programVersion = utils.PlcValueUint8ListToByteArray(readResponse.GetValue("interfaceProgramVersion"))
+			}
+			rb := utils.NewReadBufferByteBased(programVersion)
+			manufacturerId := uint16(0)
+			applicationId := uint16(0)
+			applicationVersionMajor := uint8(0)
+			applicationVersionMinor := uint8(0)
+			if rb.GetTotalBytes() == 5 {
+				manufacturerId, err = rb.ReadUint16("manufacturerId", 16)
+				if err != nil {
+					log.Error().Err(err).Msg("Error reading manufacturer id from")
+					return false
+				}
+				applicationId, err = rb.ReadUint16("applicationId", 16)
+				if err != nil {
+					log.Error().Err(err).Msg("Error reading application id from")
+					return false
+				}
+				applicationVersionMajor, err = rb.ReadUint8("applicationVersionMajor", 4)
+				if err != nil {
+					log.Error().Err(err).Str("knxAddress", knxAddress).Msg("Error reading application version major from knxAddress")
+					return false
+				}
+				applicationVersionMinor, err = rb.ReadUint8("applicationVersionMinor", 4)
+				if err != nil {
+					log.Error().Err(err).Str("knxAddress", knxAddress).Msg("Error reading application version minor from knxAddress")
+					return false
+				}
+			}
+
+			log.Info().
+				Uint16("manufacturerId", manufacturerId).
+				Msg("     manufacturer id: manufacturerId")
+			log.Info().
+				Uint16("applicationId", applicationId).
+				Uint8("applicationVersionMajor", applicationVersionMajor).
+				Uint8("applicationVersionMinor", applicationVersionMinor).
+				Msg("     program id: applicationId version applicationVersionMajor.applicationVersionMinor")
+
+			return true
+		})
+		if brr == nil {
+			log.Error().Msg("error executing browse request")
+			return
+		}
+		select {
+		case browseRequestResult := <-brr:
+			log.Info().Stringer("browseRequestResult", browseRequestResult).Msg("Browse Request Result")
+		}
+		return
+	}
+}
diff --git a/plc4go/examples/knx/groupAddressRead/hello_world_plc4go_knx_read_group_address.go b/plc4go/examples/knx/groupAddressRead/hello_world_plc4go_knx_read_group_address.go
new file mode 100644
index 0000000..9572ed5
--- /dev/null
+++ b/plc4go/examples/knx/groupAddressRead/hello_world_plc4go_knx_read_group_address.go
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	"github.com/apache/plc4x/plc4go/pkg/api/logging"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+)
+
+func main() {
+	// Set logging to INFO
+	logging.InfoLevel()
+
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterKnxDriver(driverManager)
+
+	// Get a connection to a remote PLC
+	crc := driverManager.GetConnection("knxnet-ip:udp://192.168.42.11")
+
+	// Wait for the driver to connect (or not)
+	connectionResult := <-crc
+	if connectionResult.GetErr() != nil {
+		fmt.Printf("error connecting to PLC: %s", connectionResult.GetErr().Error())
+		return
+	}
+	connection := connectionResult.GetConnection()
+
+	// Make sure the connection is closed at the end
+	defer connection.BlockingClose()
+
+	// Prepare a read-request
+	readRequest, err := connection.ReadRequestBuilder().
+		AddTagAddress("firstFlorTemperatures", "2/[1,2,4,6]/10:DPT_Value_Temp").
+		AddTagAddress("secondFlorTemperatures", "3/[2,3,4,6]/10:DPT_Value_Temp").
+		Build()
+	if err != nil {
+		fmt.Printf("error preparing read-request: %s", connectionResult.GetErr().Error())
+		return
+	}
+
+	// Execute a read-request
+	rrc := readRequest.Execute()
+
+	// Wait for the response to finish
+	rrr := <-rrc
+	if rrr.GetErr() != nil {
+		fmt.Printf("error executing read-request: %s", rrr.GetErr().Error())
+		return
+	}
+
+	// Do something with the response
+	for _, tagName := range rrr.GetResponse().GetTagNames() {
+		if rrr.GetResponse().GetResponseCode(tagName) != apiModel.PlcResponseCode_OK {
+			fmt.Printf("error an non-ok return code for tag %s: %s\n", tagName, rrr.GetResponse().GetResponseCode(tagName).GetName())
+			continue
+		}
+
+		value := rrr.GetResponse().GetValue(tagName)
+		if value == nil {
+			fmt.Printf("Got nil for tag %s\n", tagName)
+		} else if value.GetStruct() != nil {
+			for address, structValue := range value.GetStruct() {
+				fmt.Printf("Got result for tag %s with address: %s: %s °C\n", tagName, address, structValue.GetString())
+			}
+		} else {
+			fmt.Printf("Got result for tag %s: %s °C\n", tagName, value.GetString())
+		}
+	}
+}
diff --git a/plc4go/examples/knx/subscribe/hello_world_plc4go_knx_subscription.go b/plc4go/examples/knx/subscribe/hello_world_plc4go_knx_subscription.go
new file mode 100644
index 0000000..8a0dafe
--- /dev/null
+++ b/plc4go/examples/knx/subscribe/hello_world_plc4go_knx_subscription.go
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	"github.com/apache/plc4x/plc4go/pkg/api/logging"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+	apiValues "github.com/apache/plc4x/plc4go/pkg/api/values"
+)
+
+func main() {
+	// Set logging to INFO
+	logging.InfoLevel()
+
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterKnxDriver(driverManager)
+
+	// Get a connection to a remote PLC
+	crc := driverManager.GetConnection("knxnet-ip:udp://192.168.42.11")
+
+	// Wait for the driver to connect (or not)
+	connectionResult := <-crc
+	if connectionResult.GetErr() != nil {
+		fmt.Printf("error connecting to PLC: %s", connectionResult.GetErr().Error())
+		return
+	}
+	connection := connectionResult.GetConnection()
+
+	// Make sure the connection is closed at the end
+	defer connection.BlockingClose()
+
+	// Prepare a subscription-request
+	if subscriptionRequest, err := connection.SubscriptionRequestBuilder().
+		// Intentionally catching all without datatype and the temperature apiValues of the first floor with type
+		AddChangeOfStateTagAddress("all", "*/*/*").
+		AddChangeOfStateTagAddress("firstFlorTemperatures", "2/[1,2,4,6]/10:DPT_Value_Temp").
+		AddPreRegisteredConsumer("all", func(event apiModel.PlcSubscriptionEvent) {
+			// Iterate over all tags that were triggered in the current event.
+			for _, tagName := range event.GetTagNames() {
+				if event.GetResponseCode(tagName) == apiModel.PlcResponseCode_OK {
+					address := event.GetAddress(tagName)
+					value := event.GetValue(tagName)
+					// If the plc-value was a raw-plcValue, we will try lazily decode the value
+					// In my installation all group addresses ending with "/10" are temperature apiValues
+					// and ending on "/0" are light switch actions.
+					// So if I find a group address ending on that, decode it with a given type name,
+					// If not, simply output it as array of USINT apiValues.
+					switch value.(type) {
+					case apiValues.RawPlcValue:
+						rawValue := value.(apiValues.RawPlcValue)
+						datatypeName := "USINT"
+						if strings.HasSuffix(address, "/10") {
+							datatypeName = "DPT_Value_Temp"
+						} else if strings.HasSuffix(address, "/0") {
+							datatypeName = "BOOL"
+						}
+						fmt.Printf("Got raw-value event for address %s: ", address)
+						if !rawValue.RawHasMore() {
+							fmt.Printf("nothing")
+						}
+						for rawValue.RawHasMore() {
+							value = rawValue.RawDecodeValue(datatypeName)
+							fmt.Printf(" '%s'", value.GetString())
+						}
+						fmt.Printf("\n")
+					default:
+						fmt.Printf("Got event for address %s: %s\n", address, value.GetString())
+					}
+				}
+			}
+		}).Build(); err == nil {
+		// Execute a subscription-request
+		rrc := subscriptionRequest.Execute()
+
+		// Wait for the response to finish
+		rrr := <-rrc
+		if rrr.GetErr() != nil {
+			fmt.Printf("error executing subscription-request: %s", rrr.GetErr().Error())
+			return
+		}
+
+		// Do something with the response
+		for _, tagName := range rrr.GetResponse().GetTagNames() {
+			if rrr.GetResponse().GetResponseCode(tagName) != apiModel.PlcResponseCode_OK {
+				fmt.Printf("error an non-ok return code for tag %s: %s\n", tagName, rrr.GetResponse().GetResponseCode(tagName).GetName())
+				continue
+			}
+		}
+
+		time.Sleep(time.Minute * 5)
+	} else {
+		fmt.Printf("error preparing subscription-request: %s", connectionResult.GetErr().Error())
+		return
+	}
+}
diff --git a/plc4go/examples/read/hello_world_plc4go_read.go b/plc4go/examples/read/hello_world_plc4go_read.go
new file mode 100644
index 0000000..dfe89e4
--- /dev/null
+++ b/plc4go/examples/read/hello_world_plc4go_read.go
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+)
+
+func main() {
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterModbusTcpDriver(driverManager)
+
+	// Get a connection to a remote PLC
+	crc := driverManager.GetConnection("modbus-tcp://192.168.23.30")
+
+	// Wait for the driver to connect (or not)
+	connectionResult := <-crc
+	if connectionResult.GetErr() != nil {
+		fmt.Printf("error connecting to PLC: %s", connectionResult.GetErr().Error())
+		return
+	}
+	connection := connectionResult.GetConnection()
+
+	// Make sure the connection is closed at the end
+	defer connection.BlockingClose()
+
+	// Prepare a read-request
+	readRequest, err := connection.ReadRequestBuilder().
+		AddTagAddress("tag", "holding-register:26:REAL").
+		Build()
+	if err != nil {
+		fmt.Printf("error preparing read-request: %s", connectionResult.GetErr().Error())
+		return
+	}
+
+	// Execute a read-request
+	rrc := readRequest.Execute()
+
+	// Wait for the response to finish
+	rrr := <-rrc
+	if rrr.GetErr() != nil {
+		fmt.Printf("error executing read-request: %s", rrr.GetErr().Error())
+		return
+	}
+
+	// Do something with the response
+	if rrr.GetResponse().GetResponseCode("tag") != apiModel.PlcResponseCode_OK {
+		fmt.Printf("error an non-ok return code: %s", rrr.GetResponse().GetResponseCode("tag").GetName())
+		return
+	}
+
+	value := rrr.GetResponse().GetValue("tag")
+	fmt.Printf("Got result %f", value.GetFloat32())
+}
diff --git a/plc4go/examples/write/hello_world_plc4go_write.go b/plc4go/examples/write/hello_world_plc4go_write.go
new file mode 100644
index 0000000..6f5765c
--- /dev/null
+++ b/plc4go/examples/write/hello_world_plc4go_write.go
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/drivers"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+)
+
+func main() {
+	driverManager := plc4go.NewPlcDriverManager()
+	defer func() {
+		if err := driverManager.Close(); err != nil {
+			panic(err)
+		}
+	}()
+	drivers.RegisterModbusTcpDriver(driverManager)
+
+	// Get a connection to a remote PLC
+	crc := driverManager.GetConnection("modbus-tcp://192.168.23.30")
+
+	// Wait for the driver to connect (or not)
+	connectionResult := <-crc
+	if connectionResult.GetErr() != nil {
+		fmt.Printf("error connecting to PLC: %s", connectionResult.GetErr().Error())
+		return
+	}
+	connection := connectionResult.GetConnection()
+
+	// Make sure the connection is closed at the end
+	defer connection.BlockingClose()
+
+	// Prepare a write-request
+	writeRequest, err := connection.WriteRequestBuilder().
+		AddTagAddress("tag", "holding-register:26:REAL", 2.7182818284).
+		Build()
+	if err != nil {
+		fmt.Printf("error preparing read-request: %s", connectionResult.GetErr().Error())
+		return
+	}
+
+	// Execute a read-request
+	wrc := writeRequest.Execute()
+
+	// Wait for the response to finish
+	wrr := <-wrc
+	if wrr.GetErr() != nil {
+		fmt.Printf("error executing write-request: %s", wrr.GetErr().Error())
+		return
+	}
+
+	if wrr.GetResponse().GetResponseCode("tag") != apiModel.PlcResponseCode_OK {
+		fmt.Printf("error an non-ok return code: %s", wrr.GetResponse().GetResponseCode("tag").GetName())
+		return
+	}
+	fmt.Print("Result: SUCCESS\n")
+}
diff --git a/plc4go/tools/plc4xbrowser/doc.go b/plc4go/tools/plc4xbrowser/doc.go
new file mode 100644
index 0000000..3f26eb2
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/doc.go
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+	Package main contains the code for the plc4xbrowser
+
+The plc4xbrowser can be used to interactively talk to a plc using the plc4x api. Therefor it can be used by users to
+evaluate different API requests in a REPL style format.
+*/
+package main
diff --git a/plc4go/tools/plc4xbrowser/main.go b/plc4go/tools/plc4xbrowser/main.go
new file mode 100644
index 0000000..0427d9c
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/main.go
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+	"github.com/apache/plc4x/plc4go/tools/plc4xbrowser/ui"
+)
+
+func main() {
+	ui.LoadConfig()
+	application := ui.SetupApplication()
+	ui.InitSubsystem()
+
+	if err := application.Run(); err != nil {
+		panic(err)
+	}
+	ui.Shutdown()
+}
diff --git a/plc4go/tools/plc4xbrowser/ui/actions.go b/plc4go/tools/plc4xbrowser/ui/actions.go
new file mode 100644
index 0000000..2a9b442
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/ui/actions.go
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/apache/plc4x/plc4go/internal/ads"
+	"github.com/apache/plc4x/plc4go/internal/bacnetip"
+	"github.com/apache/plc4x/plc4go/internal/cbus"
+	"github.com/apache/plc4x/plc4go/internal/opcua"
+	"github.com/apache/plc4x/plc4go/internal/s7"
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/pkg/api/transports"
+	"github.com/pkg/errors"
+	"github.com/rivo/tview"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"strings"
+)
+
+func InitSubsystem() {
+	logLevel := zerolog.InfoLevel
+	if configuredLevel := config.LogLevel; configuredLevel != "" {
+		if parsedLevel, err := zerolog.ParseLevel(configuredLevel); err != nil {
+			panic(err)
+		} else {
+			logLevel = parsedLevel
+		}
+	}
+
+	log.Logger = log.
+		//// Enable below if you want to see the filenames
+		//With().Caller().Logger().
+		Output(zerolog.NewConsoleWriter(
+			func(w *zerolog.ConsoleWriter) {
+				w.Out = tview.ANSIWriter(consoleOutput)
+			},
+			func(w *zerolog.ConsoleWriter) {
+				w.FormatFieldValue = func(i interface{}) string {
+					if aString, ok := i.(string); ok && strings.Contains(aString, "\\n") {
+						return fmt.Sprintf("\x1b[%dm%v\x1b[0m", 31, "see below")
+					}
+					return fmt.Sprintf("%s", i)
+				}
+				w.FormatExtra = func(m map[string]interface{}, buffer *bytes.Buffer) error {
+					for key, i := range m {
+						if aString, ok := i.(string); ok && strings.Contains(aString, "\n") {
+							buffer.WriteString("\n")
+							buffer.WriteString(fmt.Sprintf("\x1b[%dm%v\x1b[0m", 32, "field "+key))
+							buffer.WriteString(":\n" + aString)
+						}
+					}
+					return nil
+				}
+			},
+		),
+		).
+		Level(logLevel)
+
+	driverManager = plc4go.NewPlcDriverManager()
+
+	// We offset the commands executed with the last commands
+	commandsExecuted = len(config.History.Last10Commands)
+	outputCommandHistory()
+
+	for _, driver := range config.AutoRegisterDrivers {
+		log.Info().Str("driver", driver).Msg("Auto register driver")
+		if err := validateDriverParam(driver); err != nil {
+			log.Err(err).Msg("Invalid configuration")
+			continue
+		}
+		_ = registerDriver(driver)
+	}
+}
+
+func outputCommandHistory() {
+	_, _ = fmt.Fprintln(commandOutput, "[#0000ff]Last 10 commands[white]")
+	for i, command := range config.History.Last10Commands {
+		_, _ = fmt.Fprintf(commandOutput, "   [#00ff00]%d[white]: [\"%d\"]%s[\"\"]\n", i, i, tview.Escape(command))
+	}
+}
+
+func validateDriverParam(driver string) error {
+	for _, protocol := range protocolList {
+		if protocol == driver {
+			return nil
+		}
+	}
+	return errors.Errorf("protocol %s not found", driver)
+}
+
+var tcpRegistered, udpRegistered bool
+
+func registerDriver(driverId string) error {
+	if _, ok := registeredDrivers[driverId]; ok {
+		return errors.Errorf("%s already registered", driverId)
+	}
+	var driver plc4go.PlcDriver
+	switch driverId {
+	case "ads":
+		driver = ads.NewDriver()
+		driverManager.RegisterDriver(driver)
+		if !tcpRegistered {
+			transports.RegisterTcpTransport(driverManager)
+			tcpRegistered = true
+		}
+	case "bacnetip":
+		driver = bacnetip.NewDriver()
+		driverManager.RegisterDriver(driver)
+		if !udpRegistered {
+			transports.RegisterUdpTransport(driverManager)
+			udpRegistered = true
+		}
+	case "c-bus":
+		driver = cbus.NewDriver()
+		driverManager.RegisterDriver(driver)
+		if !tcpRegistered {
+			transports.RegisterTcpTransport(driverManager)
+			tcpRegistered = true
+		}
+	case "s7":
+		driver = s7.NewDriver()
+		driverManager.RegisterDriver(driver)
+		if !tcpRegistered {
+			transports.RegisterTcpTransport(driverManager)
+			tcpRegistered = true
+		}
+	case "opcua":
+		driver = opcua.NewDriver()
+		driverManager.RegisterDriver(driver)
+		if !tcpRegistered {
+			transports.RegisterTcpTransport(driverManager)
+			tcpRegistered = true
+		}
+	default:
+		return errors.Errorf("Unknown driver %s", driverId)
+	}
+	registeredDrivers[driverId] = driver
+	go driverAdded(driver)
+	return nil
+}
diff --git a/plc4go/tools/plc4xbrowser/ui/commands.go b/plc4go/tools/plc4xbrowser/ui/commands.go
new file mode 100644
index 0000000..0d13f30
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/ui/commands.go
@@ -0,0 +1,803 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"fmt"
+	"net/url"
+	"runtime/debug"
+	"strings"
+	"time"
+
+	plc4xConfig "github.com/apache/plc4x/plc4go/pkg/api/config"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+
+	"github.com/pkg/errors"
+	"github.com/rivo/tview"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+)
+
+const rootCommandIndicator = "rootCommand"
+
+var commands = map[inputMode]Command{
+	normalMode:        rootCommand,
+	readEditMode:      rootCommand,
+	writeEditMode:     rootCommand,
+	subscribeEditMode: rootCommand,
+}
+
+var rootCommand = Command{
+	Name: rootCommandIndicator,
+	subCommands: []Command{
+		{
+			Name:        "discover",
+			Description: "Discovers devices",
+			action: func(_ Command, driverId string) error {
+				if driver, ok := registeredDrivers[driverId]; ok {
+					if !driver.SupportsDiscovery() {
+						return errors.Errorf("%s doesn't support discovery", driverId)
+					}
+					return driver.Discover(func(event apiModel.PlcDiscoveryItem) {
+						_, _ = fmt.Fprintf(messageOutput, "%v\n", event)
+					})
+				} else {
+					return errors.Errorf("%s not registered", driverId)
+				}
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for _, protocol := range protocolList {
+					entries = append(entries, protocol)
+				}
+				return
+			},
+		},
+		{
+			Name:        "connect",
+			Description: "Connects to a device",
+			action: func(_ Command, connectionString string) error {
+				log.Info().Str("connectionString", connectionString).Msg("connect connectionString")
+				connectionUrl, err := url.Parse(connectionString)
+				if err != nil {
+					return errors.Wrapf(err, "can't parse connection url %s", connectionString)
+				}
+				addHostHistoryEntry(connectionUrl.Host)
+				connectionId := fmt.Sprintf("%s://%s", connectionUrl.Scheme, connectionUrl.Host)
+				if _, ok := connections[connectionId]; ok {
+					return errors.Errorf("%s already connected", connectionId)
+				}
+				connectionResult := <-driverManager.GetConnection(connectionString)
+				if err := connectionResult.GetErr(); err != nil {
+					return errors.Wrapf(err, "%s can't connect to", connectionUrl.Host)
+				}
+				log.Info().Str("connectionId", connectionId).Msg("connected")
+				connections[connectionId] = connectionResult.GetConnection()
+				connectionsChanged()
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for _, protocol := range protocolList {
+					if strings.HasPrefix(currentText, protocol) {
+						for _, host := range config.History.Last10Hosts {
+							entries = append(entries, protocol+"://"+host)
+						}
+						entries = append(entries, currentText)
+					} else {
+						entries = append(entries, protocol)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "disconnect",
+			Description: "Disconnect a connection",
+			action: func(_ Command, connectionString string) error {
+				if connection, ok := connections[connectionString]; !ok {
+					return errors.Errorf("%s not connected", connectionString)
+				} else {
+					closeResult := <-connection.Close()
+					log.Info().Str("connectionString", connectionString).Msg("connectionString disconnected")
+					delete(connections, connectionString)
+					connectionsChanged()
+					if err := closeResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s can't close", connectionString)
+					}
+				}
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					entries = append(entries, connectionsString)
+				}
+				return
+			},
+		},
+		{
+			Name:        "read",
+			Description: "Starts a read request (switched mode to read edit)",
+			action: func(_ Command, connectionsString string) error {
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					return errors.Errorf("%s mode switch not yet implemented", connection)
+				}
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					entries = append(entries, connectionsString)
+				}
+				return
+			},
+		},
+		{
+			Name:        "read-direct",
+			Description: "Builds a read request with the supplied field",
+			action: func(c Command, connectionsStringAndFieldQuery string) error {
+				split := strings.Split(connectionsStringAndFieldQuery, " ")
+				if len(split) != 2 {
+					return errors.Errorf("%s expects exactly two arguments [connection url] [fieldQuery]", c)
+				}
+				connectionsString := split[0]
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					start := time.Now()
+					readRequest, err := connection.ReadRequestBuilder().
+						AddTagAddress("readField", split[1]).
+						Build()
+					if err != nil {
+						return errors.Wrapf(err, "%s can't read", connectionsString)
+					}
+					readRequestResult := <-readRequest.Execute()
+					if err := readRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s can't read", connectionsString)
+					}
+					plc4xBrowserLog.Debug().TimeDiff("runtime", time.Now(), start).Msg("read took runtime")
+					if err := readRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s error reading", connectionsString)
+					}
+					numberOfMessagesReceived++
+					messageReceived(numberOfMessagesReceived, time.Now(), readRequestResult.GetResponse())
+				}
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					if strings.HasPrefix(currentText, connectionsString+"") {
+						parse, _ := url.Parse(connectionsString)
+						switch parse.Scheme {
+						// TODO: add to protocol suggestor so it can be reused.
+
+						}
+					} else {
+						entries = append(entries, connectionsString)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "write",
+			Description: "Starts a write request (switched mode to write edit)",
+			action: func(_ Command, connectionsString string) error {
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					return errors.Errorf("%s mode switch not yet implemented", connection)
+				}
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					entries = append(entries, connectionsString)
+				}
+				return
+			},
+		},
+		{
+			Name:        "write-direct",
+			Description: "Builds a write request with the supplied field",
+			action: func(c Command, connectionsStringAndFieldQuery string) error {
+				split := strings.Split(connectionsStringAndFieldQuery, " ")
+				if len(split) != 3 {
+					return errors.Errorf("%s expects exactly three arguments [connection url] [fieldQuery] [value]", c)
+				}
+				connectionsString := split[0]
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					start := time.Now()
+					writeRequest, err := connection.WriteRequestBuilder().
+						AddTagAddress("writeField", split[1], split[2]).
+						Build()
+					if err != nil {
+						return errors.Wrapf(err, "%s can't write", connectionsString)
+					}
+					writeRequestResult := <-writeRequest.Execute()
+					if err := writeRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s can't write", connectionsString)
+					}
+					plc4xBrowserLog.Debug().TimeDiff("runtime", time.Now(), start).Msg("write took runtime")
+					if err := writeRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s error writing", connectionsString)
+					}
+					numberOfMessagesReceived++
+					messageReceived(numberOfMessagesReceived, time.Now(), writeRequestResult.GetResponse())
+				}
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					if strings.HasPrefix(currentText, connectionsString+"") {
+						parse, _ := url.Parse(connectionsString)
+						switch parse.Scheme {
+						// TODO: add to protocol suggestor so it can be reused.
+
+						}
+					} else {
+						entries = append(entries, connectionsString)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "browse",
+			Description: "Starts a browse request (switched mode to browse edit)",
+			action: func(_ Command, connectionsString string) error {
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					return errors.Errorf("%s mode switch not yet implemented", connection)
+				}
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					entries = append(entries, connectionsString)
+				}
+				return
+			},
+		},
+		{
+			Name:        "browse-direct",
+			Description: "Builds a browse request with the supplied field",
+			action: func(c Command, connectionsStringAndFieldQuery string) error {
+				split := strings.Split(connectionsStringAndFieldQuery, " ")
+				if len(split) != 2 {
+					return errors.Errorf("%s expects exactly three arguments [connection url] [fieldQuery]", c)
+				}
+				connectionsString := split[0]
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					start := time.Now()
+					browseRequest, err := connection.BrowseRequestBuilder().
+						AddQuery("browseField", split[1]).
+						Build()
+					if err != nil {
+						return errors.Wrapf(err, "%s can't browse", connectionsString)
+					}
+					browseRequestResult := <-browseRequest.ExecuteWithInterceptor(func(result apiModel.PlcBrowseItem) bool {
+						// TODO: Disabled for now ... not quite sure what this is for ...
+						//numberOfMessagesReceived++
+						//messageReceived(numberOfMessagesReceived, time.Now(), result)
+						return true
+					})
+					if err := browseRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s can't browse", connectionsString)
+					}
+					plc4xBrowserLog.Debug().TimeDiff("runtime", time.Now(), start).Msg("write took runtime")
+					if err := browseRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s error browse", connectionsString)
+					}
+					numberOfMessagesReceived++
+					messageReceived(numberOfMessagesReceived, time.Now(), browseRequestResult.GetResponse())
+				}
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					if strings.HasPrefix(currentText, connectionsString+"") {
+						parse, _ := url.Parse(connectionsString)
+						switch parse.Scheme {
+						// TODO: add to protocol suggestor so it can be reused.
+						case "c-bus":
+							entries = append(entries, connectionsString+" info/*/*")
+						}
+					} else {
+						entries = append(entries, connectionsString)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "register",
+			Description: "register a driver in the subsystem",
+			action: func(_ Command, driver string) error {
+				return registerDriver(driver)
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for _, protocol := range protocolList {
+					if strings.HasPrefix(protocol, currentText) {
+						entries = append(entries, protocol)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "subscribe",
+			Description: "Starts a subscription request (switched mode to subscribe edit)",
+			action: func(_ Command, connectionsString string) error {
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					return errors.Errorf("%s mode switch not yet implemented", connection)
+				}
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					entries = append(entries, connectionsString)
+				}
+				return
+			},
+		},
+		{
+			Name:        "subscribe-direct",
+			Description: "Builds a subscriptions request with the supplied field",
+			action: func(c Command, connectionsStringAndFieldQuery string) error {
+				split := strings.Split(connectionsStringAndFieldQuery, " ")
+				if len(split) != 2 {
+					return errors.Errorf("%s expects exactly two arguments [connection url] [fieldQuery]", c)
+				}
+				connectionsString := split[0]
+				if connection, ok := connections[connectionsString]; !ok {
+					return errors.Errorf("%s not connected", connectionsString)
+				} else {
+					subscriptionRequest, err := connection.SubscriptionRequestBuilder().
+						AddEventTagAddress("subscriptionField", split[1]).
+						AddPreRegisteredConsumer("subscriptionField", func(event apiModel.PlcSubscriptionEvent) {
+							numberOfMessagesReceived++
+							messageReceived(numberOfMessagesReceived, time.Now(), event)
+						}).
+						Build()
+					if err != nil {
+						return errors.Wrapf(err, "%s can't subscribe", connectionsString)
+					}
+					subscriptionRequestResult := <-subscriptionRequest.Execute()
+					if err := subscriptionRequestResult.GetErr(); err != nil {
+						return errors.Wrapf(err, "%s can't subscribe", connectionsString)
+					}
+					log.Info().Stringer("response", subscriptionRequestResult.GetResponse()).Msg("subscription result")
+				}
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for connectionsString := range connections {
+					entries = append(entries, connectionsString)
+					if strings.HasPrefix(currentText, connectionsString) {
+						parse, _ := url.Parse(connectionsString)
+						switch parse.Scheme {
+						// TODO: add to protocol suggestor so it can be reused.
+						case "c-bus":
+							entries = append(entries, connectionsString+" salmonitor/*/*")
+							entries = append(entries, connectionsString+" mmimonitor/*/*")
+						}
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "quit",
+			Description: "Quits the application",
+		},
+		{
+			Name:        "log",
+			Description: "Log related operations",
+			subCommands: []Command{
+				{
+					Name:        "get",
+					Description: "Get a log level",
+					action: func(_ Command, _ string) error {
+						_, _ = fmt.Fprintf(commandOutput, "Current log level %s", log.Logger.GetLevel())
+						return nil
+					},
+				},
+				{
+					Name:        "set",
+					Description: "Sets a log level",
+					action: func(_ Command, level string) error {
+						parseLevel, err := zerolog.ParseLevel(level)
+						if err != nil {
+							return errors.Wrapf(err, "Error setting log level")
+						}
+						setLevel(parseLevel)
+						log.Logger = log.Logger.Level(parseLevel)
+						return nil
+					},
+					parameterSuggestions: func(currentText string) (entries []string) {
+						levels := []string{
+							zerolog.LevelTraceValue,
+							zerolog.LevelDebugValue,
+							zerolog.LevelInfoValue,
+							zerolog.LevelWarnValue,
+							zerolog.LevelErrorValue,
+							zerolog.LevelFatalValue,
+							zerolog.LevelPanicValue,
+						}
+						for _, level := range levels {
+							entries = append(entries, level)
+						}
+						return
+					},
+				},
+			},
+		},
+		{
+			Name:        "plc4x-conf",
+			Description: "plc4x related settings",
+			subCommands: []Command{
+				{
+					Name:        "TraceTransactionManagerWorkers",
+					Description: "print information about transaction manager workers",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "trace on",
+							action: func(_ Command, _ string) error {
+								plc4xConfig.TraceTransactionManagerWorkers = true
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "trace off",
+							action: func(_ Command, _ string) error {
+								plc4xConfig.TraceTransactionManagerWorkers = false
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "TraceTransactionManagerTransactions",
+					Description: "print information about transaction manager transactions",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "trace on",
+							action: func(_ Command, _ string) error {
+								plc4xConfig.TraceTransactionManagerTransactions = true
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "trace off",
+							action: func(_ Command, _ string) error {
+								plc4xConfig.TraceTransactionManagerTransactions = false
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "TraceDefaultMessageCodecWorker",
+					Description: "print information about message codec workers",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "trace on",
+							action: func(_ Command, _ string) error {
+								plc4xConfig.TraceDefaultMessageCodecWorker = true
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "trace off",
+							action: func(_ Command, _ string) error {
+								plc4xConfig.TraceDefaultMessageCodecWorker = false
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "plc4xbrowser-debug",
+					Description: "Prints out debug information of the browser itself",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "debug on",
+							action: func(_ Command, _ string) error {
+								plc4xBrowserLog = zerolog.New(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)})
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "debug off",
+							action: func(_ Command, _ string) error {
+								plc4xBrowserLog = zerolog.Nop()
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "auto-register",
+					Description: "autoregister driver at startup",
+					subCommands: []Command{
+						{
+							Name: "list",
+							action: func(currentCommand Command, argument string) error {
+								_, _ = fmt.Fprintf(commandOutput, "Auto-register enabled drivers:\n  %s\n", strings.Join(config.AutoRegisterDrivers, "\n  "))
+								return nil
+							},
+						},
+						{
+							Name: "enable",
+							action: func(_ Command, argument string) error {
+								return enableAutoRegister(argument)
+							},
+							parameterSuggestions: func(currentText string) (entries []string) {
+								for _, protocol := range protocolList {
+									if strings.HasPrefix(protocol, currentText) {
+										entries = append(entries, protocol)
+									}
+								}
+								return
+							},
+						},
+						{
+							Name: "disable",
+							action: func(_ Command, argument string) error {
+								return disableAutoRegister(argument)
+							},
+							parameterSuggestions: func(currentText string) (entries []string) {
+								for _, protocol := range protocolList {
+									if strings.HasPrefix(protocol, currentText) {
+										entries = append(entries, protocol)
+									}
+								}
+								return
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			Name:        "history",
+			Description: "outputs the last commands",
+			action: func(_ Command, _ string) error {
+				outputCommandHistory()
+				return nil
+			},
+		},
+		{
+			Name:        "clear",
+			Description: "clear all outputs",
+			action: func(_ Command, _ string) error {
+				messageOutputClear()
+				consoleOutputClear()
+				commandOutputClear()
+				return nil
+			},
+			subCommands: []Command{
+				{
+					Name:        "message",
+					Description: "clears message output",
+					action: func(_ Command, _ string) error {
+						messageOutputClear()
+						return nil
+					},
+				},
+				{
+					Name:        "console",
+					Description: "clears console output",
+					action: func(_ Command, _ string) error {
+						consoleOutputClear()
+						return nil
+					},
+				},
+				{
+					Name:        "command",
+					Description: "clears command output",
+					action: func(_ Command, _ string) error {
+						commandOutputClear()
+						return nil
+					},
+				},
+			},
+		},
+	},
+}
+
+func init() {
+	// Because of the cycle we need to define the help command here as it needs access to the to command
+	rootCommand.subCommands = append(rootCommand.subCommands, Command{
+		Name:        "help",
+		Description: "prints out this help",
+		action: func(_ Command, _ string) error {
+			_, _ = fmt.Fprintf(commandOutput, "[#0000ff]Available commands[white]\n")
+			rootCommand.visit(0, func(currentIndent int, command Command) {
+				indentString := strings.Repeat("  ", currentIndent)
+				description := command.Description
+				if description == "" {
+					description = command.Name + "s"
+				}
+				_, _ = fmt.Fprintf(commandOutput, "%s [#00ff00]%s[white]: %s\n", indentString, command.Name, description)
+			})
+			return nil
+		},
+	})
+}
+
+var NotDirectlyExecutable = errors.New("Not directly executable")
+
+type Command struct {
+	Name                 string
+	Description          string
+	action               func(currentCommand Command, argument string) error
+	subCommands          []Command
+	parameterSuggestions func(currentText string) (entries []string)
+}
+
+func (c Command) Completions(currentCommandText string) (entries []string) {
+	if c.Name == rootCommandIndicator && len(currentCommandText) == 0 {
+		// We don't return anything here to not pollute the command text by default
+		return
+	}
+	if c.acceptsCurrentText(currentCommandText) {
+		currentCommandPrefix := c.currentCommandPrefix()
+		doesCommandTextTargetSubCommand := c.doesCommandTextTargetSubCommand(currentCommandPrefix)
+		if c.hasDirectExecution() && !doesCommandTextTargetSubCommand {
+			if c.parameterSuggestions != nil {
+				preparedForParameters := c.prepareForParameters(currentCommandText)
+				for _, parameterSuggestion := range c.parameterSuggestions(preparedForParameters) {
+					entries = append(entries, currentCommandPrefix+parameterSuggestion)
+				}
+			} else {
+				entries = append(entries, c.Name)
+			}
+		}
+		if doesCommandTextTargetSubCommand {
+			remainder := c.prepareForSubCommand(currentCommandText)
+			for _, command := range c.subCommands {
+				for _, subCommandCompletions := range command.Completions(remainder) {
+					entries = append(entries, currentCommandPrefix+subCommandCompletions)
+				}
+			}
+		}
+	} else if strings.HasPrefix(c.Name, currentCommandText) {
+		// Suggest ourselves if we start with the current letter
+		entries = append(entries, c.Name)
+	}
+	return
+}
+
+func (c Command) acceptsCurrentText(currentCommandText string) bool {
+	if c.Name == rootCommandIndicator {
+		return true
+	}
+	hasThePrefix := strings.HasPrefix(currentCommandText, c.Name)
+	hasNoMatchingAlternative := !strings.HasPrefix(currentCommandText, c.Name+"-")
+	accepts := hasThePrefix && hasNoMatchingAlternative
+	plc4xBrowserLog.Debug().
+		Stringer("c", c).
+		Bool("accepts", accepts).
+		Msg("c accepts")
+	return accepts
+}
+
+func (c Command) doesCommandTextTargetSubCommand(currentCommandText string) bool {
+	if c.Name == rootCommandIndicator {
+		return true
+	}
+	if len(c.subCommands) == 0 {
+		return false
+	}
+	return strings.HasPrefix(currentCommandText, c.currentCommandPrefix())
+}
+
+func (c Command) prepareForParameters(currentCommandText string) string {
+	return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix())
+}
+func (c Command) prepareForSubCommand(currentCommandText string) string {
+	return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix())
+}
+
+func (c Command) currentCommandPrefix() string {
+	if c.Name == rootCommandIndicator {
+		return ""
+	}
+	return c.Name + " "
+}
+
+func (c Command) hasDirectExecution() bool {
+	return c.action != nil
+}
+
+func Execute(commandText string) error {
+	err := rootCommand.Execute(commandText)
+	if err == nil {
+		addCommandHistoryEntry(commandText)
+	}
+	return err
+}
+
+func (c Command) Execute(commandText string) (err error) {
+	defer func() {
+		if recoveredErr := recover(); recoveredErr != nil {
+			if log.Debug().Enabled() {
+				log.Error().
+					Str("stack", string(debug.Stack())).
+					Interface("err", err).
+					Msg("panic-ed")
+			}
+			err = errors.Errorf("panic occurred: %v.", recoveredErr)
+		}
+	}()
+	plc4xBrowserLog.Debug().
+		Stringer("c", c).
+		Str("commandText", commandText).
+		Msg("%s executes %s")
+	if !c.acceptsCurrentText(commandText) {
+		return errors.Errorf("%s doesn't understand %s", c.Name, commandText)
+	}
+	if c.doesCommandTextTargetSubCommand(commandText) {
+		prepareForSubCommandForSubCommand := c.prepareForSubCommand(commandText)
+		for _, command := range c.subCommands {
+			if command.acceptsCurrentText(prepareForSubCommandForSubCommand) {
+				plc4xBrowserLog.Debug().
+					Stringer("c", c).
+					Stringer("command", command).
+					Msg("c delegates to sub command")
+				return command.Execute(prepareForSubCommandForSubCommand)
+			}
+		}
+		return errors.Errorf("%s not accepted by any subcommands of %s", commandText, c.Name)
+	} else {
+		if c.action == nil {
+			return NotDirectlyExecutable
+		}
+		plc4xBrowserLog.Debug().
+			Stringer("c", c).
+			Str("commandText", commandText).
+			Msg("c executes commandText directly")
+		preparedForParameters := c.prepareForParameters(commandText)
+		return c.action(c, preparedForParameters)
+	}
+}
+
+func (c Command) visit(i int, f func(currentIndent int, command Command)) {
+	f(i, c)
+	for _, subCommand := range c.subCommands {
+		subCommand.visit(i+1, f)
+	}
+}
+
+func (c Command) String() string {
+	return c.Name
+}
diff --git a/plc4go/tools/plc4xbrowser/ui/common.go b/plc4go/tools/plc4xbrowser/ui/common.go
new file mode 100644
index 0000000..c052901
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/ui/common.go
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	plc4goModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+	"github.com/rs/zerolog"
+	"io"
+	"strings"
+	"sync"
+	"time"
+)
+
+const protocols = "ads,bacnetip,c-bus,opcua,s7"
+
+var protocolList = strings.Split(protocols, ",")
+
+var plc4xBrowserLog = zerolog.Nop()
+
+var driverManager plc4go.PlcDriverManager
+var registeredDrivers map[string]plc4go.PlcDriver
+var driverAdded func(driver plc4go.PlcDriver)
+var connections map[string]plc4go.PlcConnection
+var connectionsChanged func()
+
+var messageReceived func(messageNumber int, receiveTime time.Time, message plc4goModel.PlcMessage)
+var numberOfMessagesReceived int
+var messageOutput io.Writer
+var messageOutputClear func()
+
+var consoleOutput io.Writer
+var consoleOutputClear func()
+
+var commandsExecuted int
+var commandOutput io.Writer
+var commandOutputClear func()
+
+type inputMode int
+
+const (
+	normalMode inputMode = iota
+	readEditMode
+	writeEditMode
+	subscribeEditMode
+)
+
+func init() {
+	hasShutdown = false
+	registeredDrivers = make(map[string]plc4go.PlcDriver)
+	connections = make(map[string]plc4go.PlcConnection)
+}
+
+var shutdownMutex sync.Mutex
+var hasShutdown bool
+
+func Shutdown() {
+	shutdownMutex.Lock()
+	defer shutdownMutex.Unlock()
+	if hasShutdown {
+		return
+	}
+	for _, connection := range connections {
+		connection.Close()
+	}
+	hasShutdown = true
+	saveConfig()
+}
diff --git a/plc4go/tools/plc4xbrowser/ui/config.go b/plc4go/tools/plc4xbrowser/ui/config.go
new file mode 100644
index 0000000..47a325b
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/ui/config.go
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"gopkg.in/yaml.v3"
+	"os"
+	"path"
+	"sync"
+	"time"
+)
+
+var plc4xBrowserConfigDir string
+var configFile string
+var config = Config{
+	MaxConsoleLines: 500,
+	MaxOutputLines:  500,
+}
+
+type Config struct {
+	History struct {
+		Last10Hosts    []string `yaml:"last_hosts"`
+		Last10Commands []string `yaml:"last_commands"`
+	}
+	AutoRegisterDrivers []string  `yaml:"auto_register_driver"`
+	LastUpdated         time.Time `yaml:"last_updated"`
+	LogLevel            string    `yaml:"log_level"`
+	MaxConsoleLines     int       `yaml:"max_console_lines"`
+	MaxOutputLines      int       `yaml:"max_output_lines"`
+}
+
+func init() {
+	userConfigDir, err := os.UserConfigDir()
+	if err != nil {
+		panic(err)
+	}
+	plc4xBrowserConfigDir = path.Join(userConfigDir, "plc4xbrowser")
+	if _, err := os.Stat(plc4xBrowserConfigDir); os.IsNotExist(err) {
+		err := os.Mkdir(plc4xBrowserConfigDir, os.ModeDir|os.ModePerm)
+		if err != nil {
+			panic(err)
+		}
+	}
+	configFile = path.Join(plc4xBrowserConfigDir, "config.yml")
+}
+
+func LoadConfig() {
+	f, err := os.Open(configFile)
+	if err != nil {
+		log.Info().Err(err).Msg("No config file found")
+		return
+	}
+	defer func(f *os.File) {
+		err := f.Close()
+		if err != nil {
+			log.Error().Err(err).Msg("Error closing config file")
+		}
+	}(f)
+
+	decoder := yaml.NewDecoder(f)
+	if err = decoder.Decode(&config); err != nil {
+		log.Warn().Err(err).Msg("Can't decode config file")
+		return
+	}
+}
+
+var saveMutex sync.Mutex
+
+func saveConfig() {
+	saveMutex.Lock()
+	defer saveMutex.Unlock()
+	config.LastUpdated = time.Now()
+	f, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0755)
+	if err != nil {
+		log.Warn().Err(err).Msg("Can't save config file")
+		return
+	}
+	encoder := yaml.NewEncoder(f)
+	defer func(encoder *yaml.Encoder) {
+		err := encoder.Close()
+		if err != nil {
+			log.Error().Err(err).Msg("Error closing config file")
+		}
+	}(encoder)
+	if err := encoder.Encode(config); err != nil {
+		log.Warn().Err(err).Msg("Can't encode config file")
+		panic(err)
+	}
+}
+
+func addHostHistoryEntry(host string) {
+	existingIndex := -1
+	for i, lastHost := range config.History.Last10Hosts {
+		if lastHost == host {
+			existingIndex = i
+			break
+		}
+	}
+	if existingIndex >= 0 {
+		config.History.Last10Hosts = append(config.History.Last10Hosts[:existingIndex], config.History.Last10Hosts[existingIndex+1:]...)
+	}
+	if len(config.History.Last10Hosts) >= 10 {
+		config.History.Last10Hosts = config.History.Last10Hosts[1:]
+	}
+	config.History.Last10Hosts = append(config.History.Last10Hosts, host)
+}
+
+func addCommandHistoryEntry(command string) {
+	switch command {
+	case "clear":
+		return
+	case "history":
+		return
+	}
+	existingIndex := -1
+	for i, lastCommand := range config.History.Last10Commands {
+		if lastCommand == command {
+			existingIndex = i
+			break
+		}
+	}
+	if existingIndex >= 0 {
+		config.History.Last10Commands = append(config.History.Last10Commands[:existingIndex], config.History.Last10Commands[existingIndex+1:]...)
+	}
+	if len(config.History.Last10Commands) >= 10 {
+		config.History.Last10Commands = config.History.Last10Commands[1:]
+	}
+	config.History.Last10Commands = append(config.History.Last10Commands, command)
+}
+
+func setLevel(level zerolog.Level) {
+	config.LogLevel = level.String()
+}
+
+func enableAutoRegister(driver string) error {
+	if err := validateDriverParam(driver); err != nil {
+		return err
+	}
+	for _, autoRegisterDriver := range config.AutoRegisterDrivers {
+		if autoRegisterDriver == driver {
+			return errors.Errorf("%s already registered for auto register", driver)
+		}
+	}
+	config.AutoRegisterDrivers = append(config.AutoRegisterDrivers, driver)
+	log.Info().Str("driver", driver).Msg("Auto register enabled")
+	return nil
+}
+
+func disableAutoRegister(driver string) error {
+	if err := validateDriverParam(driver); err != nil {
+		return err
+	}
+	index := -1
+	for i, autoRegisterDriver := range config.AutoRegisterDrivers {
+		if autoRegisterDriver == driver {
+			index = i
+			break
+		}
+	}
+	if index < 0 {
+		return errors.Errorf("%s not registered for auto register", driver)
+	}
+	config.AutoRegisterDrivers = append(config.AutoRegisterDrivers[:index], config.AutoRegisterDrivers[index+1:]...)
+	log.Info().Str("driver", driver).Msg("Auto register disabled")
+	return nil
+}
diff --git a/plc4go/tools/plc4xbrowser/ui/ui.go b/plc4go/tools/plc4xbrowser/ui/ui.go
new file mode 100644
index 0000000..92de8f4
--- /dev/null
+++ b/plc4go/tools/plc4xbrowser/ui/ui.go
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"time"
+
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+
+	"github.com/gdamore/tcell/v2"
+	"github.com/pkg/errors"
+	"github.com/rivo/tview"
+)
+
+func SetupApplication() *tview.Application {
+	application := tview.NewApplication()
+
+	newPrimitive := func(text string) tview.Primitive {
+		return tview.NewTextView().
+			SetTextAlign(tview.AlignCenter).
+			SetText(text)
+	}
+	connectionArea := buildConnectionArea(newPrimitive, application)
+	outputArea := buildOutputArea(newPrimitive, application)
+	commandArea := buildCommandArea(newPrimitive, application)
+
+	grid := tview.NewGrid().
+		SetRows(1, 0, 1).
+		SetColumns(30, 0, 30).
+		SetBorders(true).
+		AddItem(newPrimitive("PLC4X Browser"), 0, 0, 1, 3, 0, 0, false).
+		AddItem(newPrimitive("https://github.com/apache/plc4x"), 2, 0, 1, 3, 0, 0, false)
+
+	// Layout for screens narrower than 100 cells (connectionArea and side bar are hidden).
+	grid.AddItem(connectionArea, 0, 0, 0, 0, 0, 0, false).
+		AddItem(outputArea, 1, 0, 1, 3, 0, 0, false).
+		AddItem(commandArea, 0, 0, 0, 0, 0, 0, true)
+
+	// Layout for screens wider than 100 cells.
+	grid.AddItem(connectionArea, 1, 0, 1, 1, 0, 100, false).
+		AddItem(outputArea, 1, 1, 1, 1, 0, 100, false).
+		AddItem(commandArea, 1, 2, 1, 1, 0, 100, false)
+
+	application.SetRoot(grid, true).EnableMouse(true)
+
+	return application
+}
+
+func buildConnectionArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
+	connectionAreaHeader := newPrimitive("Connections")
+	connectionArea := tview.NewGrid().
+		SetRows(3, 0, 10).
+		SetColumns(0).
+		AddItem(connectionAreaHeader, 0, 0, 1, 1, 0, 0, false)
+	{
+		connectionList := tview.NewList()
+		connectionsChanged = func() {
+			application.QueueUpdateDraw(func() {
+				connectionList.Clear()
+				for connectionString, connection := range connections {
+					connectionList.AddItem(connectionString, "", 0x0, func() {
+						//TODO: disconnect popup
+						_ = connection
+					})
+				}
+			})
+		}
+		connectionArea.AddItem(connectionList, 1, 0, 1, 1, 0, 0, false)
+		{
+			registeredDriverAreaHeader := newPrimitive("Registered drivers")
+			registeredDriverArea := tview.NewGrid().
+				SetRows(3, 0).
+				SetColumns(0).
+				AddItem(registeredDriverAreaHeader, 0, 0, 1, 1, 0, 0, false)
+			{
+				driverList := tview.NewList()
+				driverAdded = func(driver plc4go.PlcDriver) {
+					application.QueueUpdateDraw(func() {
+						driverList.AddItem(driver.GetProtocolCode(), tview.Escape(fmt.Sprintf("%s", driver)), 0x0, func() {
+							//TODO: disconnect popup
+						})
+					})
+				}
+				registeredDriverArea.AddItem(driverList, 1, 0, 1, 1, 0, 0, false)
+			}
+			connectionArea.AddItem(registeredDriverArea, 2, 0, 1, 1, 0, 0, false)
+		}
+
+	}
+	return connectionArea
+}
+
+func buildCommandArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
+	commandAreaHeader := newPrimitive("Commands")
+	commandArea := tview.NewGrid().
+		SetRows(3, 0, 3).
+		SetColumns(0).
+		AddItem(commandAreaHeader, 0, 0, 1, 1, 0, 0, false)
+	{
+		enteredCommandsView := tview.NewTextView().
+			SetDynamicColors(true).
+			SetRegions(true).
+			SetWordWrap(true).
+			SetChangedFunc(func() {
+				application.Draw()
+			})
+		commandOutput = enteredCommandsView
+		commandOutputClear = func() {
+			enteredCommandsView.SetText("")
+		}
+
+		commandArea.AddItem(enteredCommandsView, 1, 0, 1, 1, 0, 0, false)
+
+		commandInputField := tview.NewInputField().
+			SetLabel("$").
+			SetFieldWidth(30)
+		application.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+			switch event.Key() {
+			case tcell.KeyCtrlC:
+				commandInputField.SetText("")
+				application.SetFocus(commandInputField)
+				return nil
+			case tcell.KeyCtrlD:
+				// TODO: maybe add a modal here
+				application.Stop()
+				return nil
+			}
+			return event
+		})
+		commandInputField.
+			SetDoneFunc(func(key tcell.Key) {
+				commandText := commandInputField.GetText()
+				if commandText == "quit" {
+					// TODO: maybe add a modal here
+					application.Stop()
+					return
+				}
+				commandsExecuted++
+				go func() {
+					commandHistoryShortcut, _ := regexp.Compile("^[0-9]$")
+					if commandHistoryShortcut.MatchString(commandText) {
+						atoi, _ := strconv.Atoi(commandHistoryShortcut.FindString(commandText))
+						if atoi < len(config.History.Last10Commands) {
+							commandText = config.History.Last10Commands[atoi]
+						} else {
+							_, _ = fmt.Fprintf(enteredCommandsView, "[#ff0000]%s %s[white]\n", time.Now().Format("04:05"), errors.Errorf("No such elements %d in command history", atoi))
+							return
+						}
+					}
+					_, _ = fmt.Fprintf(enteredCommandsView, "%s [\"%d\"]%s[\"\"]\n", time.Now().Format("04:05"), commandsExecuted, commandText)
+					if err := Execute(commandText); err != nil {
+						_, _ = fmt.Fprintf(enteredCommandsView, "[#ff0000]%s %s[white]\n", time.Now().Format("04:05"), err)
+						return
+					}
+					application.QueueUpdateDraw(func() {
+						commandInputField.SetText("")
+					})
+				}()
+			})
+		commandInputField.SetAutocompleteFunc(rootCommand.Completions)
+
+		enteredCommandsView.SetDoneFunc(func(key tcell.Key) {
+			currentSelection := enteredCommandsView.GetHighlights()
+			if key == tcell.KeyEnter {
+				if len(currentSelection) > 0 {
+					enteredCommandsView.Highlight()
+				} else {
+					enteredCommandsView.Highlight("0").ScrollToHighlight()
+				}
+				if len(currentSelection) == 1 {
+					commandInputField.SetText(enteredCommandsView.GetRegionText(currentSelection[0]))
+					application.SetFocus(commandInputField)
+				}
+			} else if len(currentSelection) > 0 {
+				index, _ := strconv.Atoi(currentSelection[0])
+				if key == tcell.KeyTab {
+					index = (index + 1) % commandsExecuted
+				} else if key == tcell.KeyBacktab {
+					index = (index - 1 + commandsExecuted) % commandsExecuted
+				} else {
+					return
+				}
+				enteredCommandsView.Highlight(strconv.Itoa(index)).ScrollToHighlight()
+			}
+		})
+
+		commandArea.AddItem(commandInputField, 2, 0, 1, 1, 0, 0, true)
+	}
+	return commandArea
+}
+
+func buildOutputArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) *tview.Grid {
+	outputAreaHeader := newPrimitive("Output")
+	outputArea := tview.NewGrid().
+		SetRows(3, 0, 10).
+		SetColumns(0, 30).
+		AddItem(outputAreaHeader, 0, 0, 1, 1, 0, 0, false)
+	{
+		var jumpToMessageItem func(messageNumber int) bool
+		{
+			outputView := tview.NewTextView().
+				SetDynamicColors(true).
+				SetRegions(true).
+				SetWordWrap(false).
+				SetWrap(false).
+				SetChangedFunc(func() {
+					application.Draw()
+				})
+			jumpToMessageItem = func(messageNumber int) bool {
+				regionId := strconv.Itoa(messageNumber)
+				if outputView.GetRegionText(regionId) == "" {
+					return false
+				}
+				outputView.Highlight(regionId).ScrollToHighlight()
+				return true
+			}
+			messageOutput = outputView
+			messageOutputClear = func() {
+				outputView.SetText("")
+			}
+
+			outputView.SetDoneFunc(func(key tcell.Key) {
+				currentSelection := outputView.GetHighlights()
+				if key == tcell.KeyEnter {
+					if len(currentSelection) > 0 {
+						outputView.Highlight()
+					} else {
+						outputView.Highlight("0").ScrollToHighlight()
+					}
+				} else if len(currentSelection) > 0 {
+					index, _ := strconv.Atoi(currentSelection[0])
+					if key == tcell.KeyTab {
+						index = (index + 1) % numberOfMessagesReceived
+					} else if key == tcell.KeyBacktab {
+						index = (index - 1 + numberOfMessagesReceived) % numberOfMessagesReceived
+					} else {
+						return
+					}
+					outputView.Highlight(strconv.Itoa(index)).ScrollToHighlight()
+				}
+			})
+			outputView.SetBorder(false)
+			outputArea.AddItem(outputView, 1, 0, 1, 1, 0, 0, false)
+		}
+
+		{
+			consoleView := tview.NewTextView().
+				SetDynamicColors(true).
+				SetMaxLines(config.MaxConsoleLines).
+				SetChangedFunc(func() {
+					application.Draw()
+				})
+			consoleOutput = consoleView
+			consoleOutputClear = func() {
+				consoleView.SetText("")
+			}
+
+			consoleView.SetBorder(false)
+			outputArea.AddItem(consoleView, 2, 0, 1, 1, 0, 0, false)
+		}
+
+		{
+			receivedMessagesList := tview.NewList()
+			messageReceived = func(messageNumber int, receiveTime time.Time, message apiModel.PlcMessage) {
+				application.QueueUpdateDraw(func() {
+					receivedMessagesList.AddItem(fmt.Sprintf("No %d @%s", messageNumber, receiveTime.Format("15:04:05.999999")), "", 0x0, func() {
+						if ok := jumpToMessageItem(messageNumber); !ok {
+							plc4xBrowserLog.Debug().Msg("Adding new message to console output")
+							_, _ = fmt.Fprintf(messageOutput, "Message nr: %d\n[\"%d\"]%s[\"\"]\n", messageNumber, messageNumber, message)
+							jumpToMessageItem(messageNumber)
+						}
+					})
+				})
+			}
+			outputArea.AddItem(receivedMessagesList, 0, 1, 3, 1, 0, 0, false)
+		}
+	}
+	return outputArea
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/analyze.go b/plc4go/tools/plc4xpcapanalyzer/cmd/analyze.go
new file mode 100644
index 0000000..fa0c119
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/analyze.go
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
+	"github.com/pkg/errors"
+	"math"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+var validProtocolType = map[string]any{
+	"bacnet": nil,
+	"c-bus":  nil,
+}
+
+// analyzeCmd represents the analyze command
+var analyzeCmd = &cobra.Command{
+	Use:   "analyze [protocolType] [pcapfile]",
+	Short: "analyzes a pcap file using a driver supplied driver",
+	Long: `Analyzes a pcap file using a driver
+TODO: document me
+`,
+	Args: func(cmd *cobra.Command, args []string) error {
+		if len(args) < 2 {
+			return errors.New("requires exactly two arguments")
+		}
+		if _, ok := validProtocolType[args[0]]; !ok {
+			return errors.Errorf("Only following protocols are supported %v", validProtocolType)
+		}
+		pcapFile := args[1]
+		if _, err := os.Stat(pcapFile); errors.Is(err, os.ErrNotExist) {
+			return errors.Errorf("Pcap file not found %s", pcapFile)
+		}
+		return nil
+	},
+	Run: func(cmd *cobra.Command, args []string) {
+		protocolType := args[0]
+		pcapFile := args[1]
+		if err := analyzer.Analyze(pcapFile, protocolType); err != nil {
+			panic(err)
+		}
+		println("Done")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(analyzeCmd)
+
+	addAnalyzeFlags(analyzeCmd)
+}
+
+func addAnalyzeFlags(command *cobra.Command) {
+	command.Flags().StringVarP(&config.AnalyzeConfigInstance.Filter, "filter", "f", "", "BFF filter to apply")
+	command.Flags().BoolVarP(&config.AnalyzeConfigInstance.NoFilter, "no-filter", "n", false, "disable filter")
+	command.Flags().BoolVarP(&config.AnalyzeConfigInstance.OnlyParse, "only-parse", "o", false, "only parse messaged")
+	command.Flags().BoolVarP(&config.AnalyzeConfigInstance.NoBytesCompare, "no-bytes-compare", "b", false, "don't compare original bytes with serialized bytes")
+	command.Flags().BoolVarP(&config.AnalyzeConfigInstance.NoCustomMapping, "no-custom-mapping", "", false, "don't use the custom mapper for protocols")
+	command.Flags().StringVarP(&config.AnalyzeConfigInstance.Client, "client", "c", "", "The client ip (this is useful for protocols where request/response is different e.g. modbus, cbus)")
+	command.Flags().UintVarP(&config.AnalyzeConfigInstance.StartPackageNumber, "start-package-umber", "s", 0, "Defines with what package number should be started")
+	command.Flags().UintVarP(&config.AnalyzeConfigInstance.PackageNumberLimit, "package-number-limit", "l", math.MaxUint, "Defines how many packages should be parsed")
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go b/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
new file mode 100644
index 0000000..b268414
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
+	"github.com/pkg/errors"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// bacnetCmd represents the bacnet command
+var bacnetCmd = &cobra.Command{
+	Use:   "bacnet [pcapfile]",
+	Short: "analyzes a pcap file using a bacnet driver",
+	Long: `Analyzes a pcap file using a bacnet driver
+TODO: document me
+`,
+	Args: func(cmd *cobra.Command, args []string) error {
+		if len(args) < 1 {
+			return errors.New("requires exactly one arguments")
+		}
+		pcapFile := args[0]
+		if _, err := os.Stat(pcapFile); errors.Is(err, os.ErrNotExist) {
+			return errors.Errorf("Pcap file not found %s", pcapFile)
+		}
+		return nil
+	},
+	Run: func(cmd *cobra.Command, args []string) {
+		pcapFile := args[0]
+		if err := analyzer.Analyze(pcapFile, "bacnet"); err != nil {
+			panic(err)
+		}
+		println("Done")
+	},
+}
+
+func init() {
+	analyzeCmd.AddCommand(bacnetCmd)
+
+	bacnetCmd.PersistentFlags().StringVarP(&config.BacnetConfigInstance.BacnetFilter, "default-bacnet-filter", "", "udp port 47808 and udp[4:2] > 29", "Defines the default filter when bacnet is selected")
+
+	addAnalyzeFlags(bacnetCmd)
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go b/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go
new file mode 100644
index 0000000..5c76e98
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
+	"github.com/pkg/errors"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// cbusCmd represents the cbus command
+var cbusCmd = &cobra.Command{
+	Use:   "c-bus [pcapfile]",
+	Short: "analyzes a pcap file using a c-bus driver",
+	Long: `Analyzes a pcap file using a c-bus driver
+TODO: document me
+`,
+	Args: func(cmd *cobra.Command, args []string) error {
+		if len(args) < 1 {
+			return errors.New("requires exactly one arguments")
+		}
+		pcapFile := args[0]
+		if _, err := os.Stat(pcapFile); errors.Is(err, os.ErrNotExist) {
+			return errors.Errorf("Pcap file not found %s", pcapFile)
+		}
+		return nil
+	},
+	Run: func(cmd *cobra.Command, args []string) {
+		pcapFile := args[0]
+		if err := analyzer.Analyze(pcapFile, "c-bus"); err != nil {
+			panic(err)
+		}
+		println("Done")
+	},
+}
+
+func init() {
+	analyzeCmd.AddCommand(cbusCmd)
+
+	cbusCmd.PersistentFlags().StringVarP(&config.CBusConfigInstance.CBusFilter, "default-cbus-filter", "", "tcp port 10001", "Defines the default filter when c-bus is selected")
+
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Connect, "cbus-connect", "", false, "Defines that SAL messages can occur at any time")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Smart, "cbus-smart", "", false, "Disable echo of characters. When used with connect SAL have a long option. Select long from of most CAL replies")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Idmon, "cbus-idmon", "", false, "only works with smart. Select long form of CAL messages")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Exstat, "cbus-exstat", "", false, "useful with smart. Select long form, extended format for all monitored and initiated status requests")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Monitor, "cbus-monitor", "", false, "monitors all traffic for status requests. Status requests will be returned as CAL. Replies are modified by exstat. Usually used in conjunction with connect.")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Monall, "cbus-monall", "", false, "Same as connect. In addition it will return remote network SAL")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Pun, "cbus-pun", "", false, "Serial interface will emit a power up notification")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Pcn, "cbus-pcn", "", false, "causes parameter change notifications to be emitted.")
+	cbusCmd.Flags().BoolVarP(&config.CBusConfigInstance.Srchk, "cbus-srchk", "", false, "forces the checksum checks from the beginning")
+
+	addAnalyzeFlags(cbusCmd)
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go b/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go
new file mode 100644
index 0000000..441d1a9
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/extractor"
+	"github.com/pkg/errors"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// extractCmd represents the extract command
+var extractCmd = &cobra.Command{
+	Use:   "extract [protocolType] [pcapfile]",
+	Short: "extract a pcap file using a driver supplied driver",
+	Long: `Extract a pcap file using a driver
+TODO: document me
+`,
+	Args: func(cmd *cobra.Command, args []string) error {
+		if len(args) < 2 {
+			return errors.New("requires exactly two arguments")
+		}
+		if _, ok := validProtocolType[args[0]]; !ok {
+			return errors.Errorf("Only following protocols are supported %v", validProtocolType)
+		}
+		pcapFile := args[1]
+		if _, err := os.Stat(pcapFile); errors.Is(err, os.ErrNotExist) {
+			return errors.Errorf("Pcap file not found %s", pcapFile)
+		}
+		return nil
+	},
+	Run: func(cmd *cobra.Command, args []string) {
+		protocolType := args[0]
+		pcapFile := args[1]
+		if err := extractor.Extract(pcapFile, protocolType); err != nil {
+			panic(err)
+		}
+		println("Done")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(extractCmd)
+
+	extractCmd.Flags().StringVarP(&config.ExtractConfigInstance.Client, "client", "c", "", "The client ip (this is useful for protocols where request/response is different e.g. modbus, cbus)")
+	extractCmd.Flags().BoolVarP(&config.ExtractConfigInstance.ShowDirectionalIndicators, "show-directional-indicators", "", true, "Indicates if directional markers should be printed")
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/root.go b/plc4go/tools/plc4xpcapanalyzer/cmd/root.go
new file mode 100644
index 0000000..4132f48
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/root.go
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cmd
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"github.com/rs/zerolog/pkgerrors"
+	"os"
+	"strings"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "plc4xpcapanalyzer",
+	Short: "analyzes pcaps with plc4x",
+	Long:  `todo describe me`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	cobra.OnInitialize(initConfig)
+
+	rootCmd.PersistentFlags().StringVar(&config.RootConfigInstance.CfgFile, "config", "", "config file (default is $HOME/.plc4xpcapanalyzer.yaml)")
+	rootCmd.PersistentFlags().StringVar(&config.RootConfigInstance.LogType, "log-type", "text", "define how the log will be evaluated")
+	rootCmd.PersistentFlags().StringVar(&config.RootConfigInstance.LogLevel, "log-level", "error", "define the log Level")
+	rootCmd.PersistentFlags().CountVarP(&config.RootConfigInstance.Verbosity, "verbose", "v", "counted verbosity")
+	rootCmd.PersistentFlags().BoolVarP(&config.RootConfigInstance.HideProgressBar, "hide-progress-bar", "", false, "hides the progress bar")
+
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+	if config.RootConfigInstance.CfgFile != "" {
+		// Use config file from the flag.
+		viper.SetConfigFile(config.RootConfigInstance.CfgFile)
+	} else {
+		// Find user config directory.
+		home, err := os.UserConfigDir()
+		cobra.CheckErr(err)
+
+		viper.AddConfigPath(home)
+		viper.SetConfigType("yaml")
+		viper.SetConfigName("plc4xpcapanalyzer-viper")
+	}
+
+	viper.AutomaticEnv() // read in environment variables that match
+
+	// If a config file is found, read it in.
+	if err := viper.ReadInConfig(); err == nil {
+		_, _ = fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
+	}
+
+	zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
+	if config.RootConfigInstance.LogType == "text" {
+		log.Logger = log.
+			//// Enable below if you want to see the filenames
+			//With().Caller().Logger().
+			Output(zerolog.NewConsoleWriter(
+				func(w *zerolog.ConsoleWriter) {
+					w.Out = os.Stderr
+				},
+				func(w *zerolog.ConsoleWriter) {
+					w.FormatFieldValue = func(i interface{}) string {
+						if aString, ok := i.(string); ok && strings.Contains(aString, "\\n") {
+							return fmt.Sprintf("\x1b[%dm%v\x1b[0m", 31, "see below")
+						}
+						return fmt.Sprintf("%s", i)
+					}
+					w.FormatExtra = func(m map[string]interface{}, buffer *bytes.Buffer) error {
+						for key, i := range m {
+							if aString, ok := i.(string); ok && strings.Contains(aString, "\n") {
+								buffer.WriteString("\n")
+								buffer.WriteString(fmt.Sprintf("\x1b[%dm%v\x1b[0m", 32, "field "+key))
+								buffer.WriteString(":\n" + aString)
+							}
+						}
+						return nil
+					}
+				},
+			),
+			).
+			Level(parseLogLevel())
+	}
+}
+
+func parseLogLevel() zerolog.Level {
+	level, err := zerolog.ParseLevel(config.RootConfigInstance.LogLevel)
+	if err != nil {
+		log.Fatal().Err(err).Str("level", config.RootConfigInstance.LogLevel).Msg("Unknown log level")
+	}
+	return level
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/ui.go b/plc4go/tools/plc4xpcapanalyzer/cmd/ui.go
new file mode 100644
index 0000000..3d3f83a
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/ui.go
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/ui"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// uiCmd represents the ui command
+var uiCmd = &cobra.Command{
+	Use:   "ui [pcapfile]",
+	Short: "Start the ui with optional pcapfile",
+	Long: `Analyzes a pcap file using a bacnet driver
+TODO: document me
+`,
+	Args: func(cmd *cobra.Command, args []string) error {
+		if len(args) < 1 {
+			return nil
+		}
+		pcapFile := args[0]
+		if _, err := os.Stat(pcapFile); errors.Is(err, os.ErrNotExist) {
+			return errors.Errorf("Pcap file not found %s", pcapFile)
+		}
+		return nil
+	},
+	Run: func(cmd *cobra.Command, args []string) {
+		ui.LoadConfig()
+		application := ui.SetupApplication()
+		ui.InitSubsystem()
+		if len(args) > 0 {
+			pcapFile := args[0]
+			go func() {
+				err := ui.OpenFile(pcapFile)
+				if err != nil {
+					log.Error().Err(err).Msg("Error opening argument file")
+				}
+			}()
+		}
+
+		defer ui.Shutdown()
+		if err := application.Run(); err != nil {
+			panic(err)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(uiCmd)
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/config/AnalyzeConfig.go b/plc4go/tools/plc4xpcapanalyzer/config/AnalyzeConfig.go
new file mode 100644
index 0000000..1c6f253
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/config/AnalyzeConfig.go
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+type AnalyzeConfig struct {
+	*PcapConfig                                          `json:"-"`
+	NoFilter, OnlyParse, NoBytesCompare, NoCustomMapping bool
+}
+
+var AnalyzeConfigInstance = AnalyzeConfig{}
+
+func init() {
+	AnalyzeConfigInstance.PcapConfig = &PcapConfigInstance
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/config/BacnetConfig.go b/plc4go/tools/plc4xpcapanalyzer/config/BacnetConfig.go
new file mode 100644
index 0000000..b662daf
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/config/BacnetConfig.go
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+type BacnetConfig struct {
+	*AnalyzeConfig `json:"-"`
+	BacnetFilter   string
+}
+
+var BacnetConfigInstance = BacnetConfig{}
+
+func init() {
+	BacnetConfigInstance.AnalyzeConfig = &AnalyzeConfigInstance
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/config/CBusConfig.go b/plc4go/tools/plc4xpcapanalyzer/config/CBusConfig.go
new file mode 100644
index 0000000..42bbd4a
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/config/CBusConfig.go
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+type CBusConfig struct {
+	*AnalyzeConfig `json:"-"`
+	CBusFilter     string
+
+	Connect bool
+	Smart   bool
+	Idmon   bool
+	Exstat  bool
+	Monitor bool
+	Monall  bool
+	Pun     bool
+	Pcn     bool
+	Srchk   bool
+}
+
+var CBusConfigInstance = CBusConfig{}
+
+func init() {
+	CBusConfigInstance.AnalyzeConfig = &AnalyzeConfigInstance
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/config/ExtractConfig.go b/plc4go/tools/plc4xpcapanalyzer/config/ExtractConfig.go
new file mode 100644
index 0000000..6f101aa
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/config/ExtractConfig.go
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+type ExtractType uint
+
+const (
+	HEX ExtractType = iota
+	ASCII
+)
+
+type ExtractConfig struct {
+	*PcapConfig               `json:"-"`
+	ShowDirectionalIndicators bool
+	ExtractType               ExtractType
+}
+
+var ExtractConfigInstance = ExtractConfig{}
+
+func init() {
+	ExtractConfigInstance.PcapConfig = &PcapConfigInstance
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/config/PcapConfig.go b/plc4go/tools/plc4xpcapanalyzer/config/PcapConfig.go
new file mode 100644
index 0000000..c399c63
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/config/PcapConfig.go
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+type PcapConfig struct {
+	*RootConfig        `json:"-"`
+	Filter             string
+	Client             string
+	StartPackageNumber uint
+	PackageNumberLimit uint
+}
+
+var PcapConfigInstance = PcapConfig{}
+
+func init() {
+	PcapConfigInstance.RootConfig = &RootConfigInstance
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/config/RootConfig.go b/plc4go/tools/plc4xpcapanalyzer/config/RootConfig.go
new file mode 100644
index 0000000..35b1730
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/config/RootConfig.go
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+type RootConfig struct {
+	CfgFile         string
+	LogType         string
+	LogLevel        string
+	Verbosity       int
+	HideProgressBar bool
+}
+
+var RootConfigInstance = RootConfig{}
diff --git a/plc4go/tools/plc4xpcapanalyzer/doc.go b/plc4go/tools/plc4xpcapanalyzer/doc.go
new file mode 100644
index 0000000..7be8134
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/doc.go
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+	Package main contains the code for the plc4xpcapanalyzer
+
+The plc4xpcapanalyzer is meant to be used for internal plc4x development. It can be used to analyze pcap files using the
+parser/serializers of plc4x.
+*/
+package main
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go b/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go
new file mode 100644
index 0000000..de49729
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package analyzer
+
+import (
+	"bytes"
+	"context"
+	"encoding/hex"
+	"fmt"
+	"github.com/apache/plc4x/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/common"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler"
+	"github.com/gopacket/gopacket"
+	"github.com/gopacket/gopacket/layers"
+	"github.com/k0kubun/go-ansi"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"github.com/schollz/progressbar/v3"
+	"io"
+	"net"
+	"os"
+	"time"
+)
+
+func Analyze(pcapFile, protocolType string) error {
+	return AnalyzeWithOutput(pcapFile, protocolType, os.Stdout, os.Stderr)
+}
+
+func AnalyzeWithOutput(pcapFile, protocolType string, stdout, stderr io.Writer) error {
+	return AnalyzeWithOutputAndCallback(context.TODO(), pcapFile, protocolType, stdout, stderr, nil)
+}
+
+func AnalyzeWithOutputAndCallback(ctx context.Context, pcapFile, protocolType string, stdout, stderr io.Writer, messageCallback func(parsed spi.Message)) error {
+	var filterExpression = config.AnalyzeConfigInstance.Filter
+	if filterExpression != "" {
+		log.Info().Str("filterExpression", filterExpression).Msg("Using global filter")
+	}
+	var mapPackets = func(in chan gopacket.Packet, packetInformationCreator func(packet gopacket.Packet) common.PacketInformation) chan gopacket.Packet {
+		return in
+	}
+	var packageParse func(common.PacketInformation, []byte) (spi.Message, error)
+	var serializePackage func(spi.Message) ([]byte, error)
+	var prettyPrint = func(item spi.Message) {
+		_, _ = fmt.Fprintf(stdout, "%v\n", item)
+	}
+	var byteOutput = hex.Dump
+	switch protocolType {
+	case "bacnetip":
+		if !config.AnalyzeConfigInstance.NoFilter {
+			if config.AnalyzeConfigInstance.Filter == "" && config.BacnetConfigInstance.BacnetFilter != "" {
+				log.Debug().Str("filter", config.BacnetConfigInstance.Filter).Msg("Setting bacnet filter")
+				filterExpression = config.BacnetConfigInstance.BacnetFilter
+			}
+		} else {
+			log.Info().Msg("All filtering disabled")
+		}
+		packageParse = bacnetanalyzer.PackageParse
+		serializePackage = bacnetanalyzer.SerializePackage
+	case "c-bus":
+		if !config.AnalyzeConfigInstance.NoFilter {
+			if config.AnalyzeConfigInstance.Filter == "" && config.CBusConfigInstance.CBusFilter != "" {
+				log.Debug().Str("filter", config.CBusConfigInstance.Filter).Msg("Setting cbus filter")
+				filterExpression = config.CBusConfigInstance.CBusFilter
+			}
+		} else {
+			log.Info().Msg("All filtering disabled")
+		}
+		analyzer := cbusanalyzer.Analyzer{Client: net.ParseIP(config.AnalyzeConfigInstance.Client)}
+		analyzer.Init()
+		packageParse = analyzer.PackageParse
+		serializePackage = analyzer.SerializePackage
+		mapPackets = analyzer.MapPackets
+		if !config.AnalyzeConfigInstance.NoCustomMapping {
+			byteOutput = analyzer.ByteOutput
+		} else {
+			log.Info().Msg("Custom mapping disabled")
+		}
+	default:
+		return errors.Errorf("Unsupported protocol type %s", protocolType)
+	}
+
+	log.Info().
+		Str("pcapFile", pcapFile).
+		Str("protocolType", protocolType).
+		Str("filterExpression", filterExpression).
+		Msg("Analyzing pcap file pcapFile with protocolType and filter filterExpression now")
+	handle, numberOfPackage, timestampToIndexMap, err := pcaphandler.GetIndexedPcapHandle(pcapFile, filterExpression)
+	if err != nil {
+		return errors.Wrap(err, "Error getting handle")
+	}
+	log.Info().Int("numberOfPackage", numberOfPackage).Msg("Starting to analyze numberOfPackage packages")
+	defer handle.Close()
+	log.Debug().Interface("handle", handle).Int("numberOfPackage", numberOfPackage).Msg("got handle")
+	source := pcaphandler.GetPacketSource(handle)
+	bar := progressbar.NewOptions(numberOfPackage, progressbar.OptionSetWriter(ansi.NewAnsiStderr()),
+		progressbar.OptionSetVisibility(!config.RootConfigInstance.HideProgressBar),
+		progressbar.OptionEnableColorCodes(true),
+		progressbar.OptionShowBytes(false),
+		progressbar.OptionSetWidth(15),
+		progressbar.OptionSetDescription("[cyan][1/3][reset] Analyzing packages..."),
+		progressbar.OptionSetTheme(progressbar.Theme{
+			Saucer:        "[green]=[reset]",
+			SaucerHead:    "[green]>[reset]",
+			SaucerPadding: " ",
+			BarStart:      "[",
+			BarEnd:        "]",
+		}))
+	currentPackageNum := uint(0)
+	parseFails := 0
+	serializeFails := 0
+	compareFails := 0
+	for packet := range mapPackets(source.Packets(), func(packet gopacket.Packet) common.PacketInformation {
+		return createPacketInformation(pcapFile, packet, timestampToIndexMap)
+	}) {
+		if err := ctx.Err(); err != nil {
+			log.Info().Err(err).Uint("currentPackageNum", currentPackageNum).Msg("Aborted after currentPackageNum packages")
+			break
+		}
+		currentPackageNum++
+		if currentPackageNum < config.AnalyzeConfigInstance.StartPackageNumber {
+			log.Debug().
+				Uint("currentPackageNum", currentPackageNum).
+				Uint("startPackageNum", config.AnalyzeConfigInstance.StartPackageNumber).
+				Msg("Skipping package number currentPackageNum (till no. startPackageNum)")
+			continue
+		}
+		if currentPackageNum > config.AnalyzeConfigInstance.PackageNumberLimit {
+			log.Warn().
+				Uint("PackageNumberLimit", config.AnalyzeConfigInstance.PackageNumberLimit).
+				Msg("Aborting reading packages because we hit the limit of packageNumberLimit")
+			break
+		}
+		if packet == nil {
+			log.Debug().Msg("Done reading packages. (nil returned)")
+			break
+		}
+		if err := bar.Add(1); err != nil {
+			log.Warn().Err(err).Msg("Error updating progressBar")
+		}
+		packetInformation := createPacketInformation(pcapFile, packet, timestampToIndexMap)
+		realPacketNumber := packetInformation.PacketNumber
+		if filteredPackage, ok := packet.(common.FilteredPackage); ok {
+			log.Info().Err(filteredPackage.FilterReason()).
+				Int("realPacketNumber", realPacketNumber).Msg("No.[realPacketNumber] was filtered")
+			continue
+		}
+
+		applicationLayer := packet.ApplicationLayer()
+		if applicationLayer == nil {
+			log.Info().Stringer("packetInformation", packetInformation).
+				Int("realPacketNumber", realPacketNumber).
+				Msg("No.[realPacketNumber] No application layer")
+			continue
+		}
+		payload := applicationLayer.Payload()
+		if parsed, err := packageParse(packetInformation, payload); err != nil {
+			switch err {
+			case common.ErrUnterminatedPackage:
+				log.Info().Stringer("packetInformation", packetInformation).
+					Int("realPacketNumber", realPacketNumber).
+					Msg("No.[realPacketNumber] is unterminated")
+			case common.ErrEmptyPackage:
+				log.Info().Stringer("packetInformation", packetInformation).
+					Int("realPacketNumber", realPacketNumber).
+					Msg("No.[realPacketNumber] is empty")
+			case common.ErrEcho:
+				log.Info().Stringer("packetInformation", packetInformation).
+					Int("realPacketNumber", realPacketNumber).
+					Msg("No.[realPacketNumber] is echo")
+			default:
+				parseFails++
+				// TODO: write report to xml or something
+				log.Error().
+					Err(err).
+					Stringer("packetInformation", packetInformation).
+					Int("realPacketNumber", realPacketNumber).
+					Str("byteOutput", byteOutput(payload)).
+					Msg("No.[realPacketNumber] Error parsing package.")
+			}
+			continue
+		} else {
+			if messageCallback != nil {
+				messageCallback(parsed)
+			}
+			log.Info().
+				Stringer("packetInformation", packetInformation).
+				Int("realPacketNumber", realPacketNumber).
+				Msg("No.[realPacketNumber] Parsed")
+			if config.AnalyzeConfigInstance.Verbosity > 1 {
+				prettyPrint(parsed)
+			}
+			if config.AnalyzeConfigInstance.OnlyParse {
+				log.Trace().Msg("only parsing")
+				continue
+			}
+			serializedBytes, err := serializePackage(parsed)
+			if err != nil {
+				serializeFails++
+				// TODO: write report to xml or something
+				log.Warn().
+					Err(err).
+					Stringer("packetInformation", packetInformation).
+					Int("realPacketNumber", realPacketNumber).
+					Msg("No.[realPacketNumber] Error serializing")
+				continue
+			}
+			if config.AnalyzeConfigInstance.NoBytesCompare {
+				log.Trace().Msg("not comparing bytes")
+				continue
+			}
+			if compareResult := bytes.Compare(payload, serializedBytes); compareResult != 0 {
+				compareFails++
+				// TODO: write report to xml or something
+				log.Warn().
+					Stringer("packetInformation", packetInformation).
+					Int("realPacketNumber", realPacketNumber).
+					Str("byteOutputPayload", byteOutput(payload)).
+					Str("byteSerializedBytes", byteOutput(serializedBytes)).
+					Msg("No.[realPacketNumber] Bytes don't match.")
+				if config.AnalyzeConfigInstance.Verbosity > 0 {
+					_, _ = fmt.Fprintf(stdout, "Original bytes\n%s\n%s\n", hex.Dump(payload), hex.Dump(serializedBytes))
+				}
+			}
+		}
+	}
+
+	log.Info().
+		Uint("currentPackageNum", currentPackageNum).
+		Int("numberOfPackage", numberOfPackage).
+		Int("parseFails", parseFails).
+		Int("serializeFails", serializeFails).
+		Int("compareFails", compareFails).
+		Msg("Done evaluating currentPackageNum of numberOfPackage packages (parseFails failed to parse, serializeFails failed to serialize and compareFails failed in byte comparison)")
+	return nil
+}
+
+func createPacketInformation(pcapFile string, packet gopacket.Packet, timestampToIndexMap map[time.Time]int) common.PacketInformation {
+	packetTimestamp := packet.Metadata().Timestamp
+	realPacketNumber := timestampToIndexMap[packetTimestamp]
+	description := fmt.Sprintf("No.[%d] timestamp: %v, %s", realPacketNumber, packetTimestamp, pcapFile)
+	packetInformation := common.PacketInformation{
+		PacketNumber:    realPacketNumber,
+		PacketTimestamp: packetTimestamp,
+		Description:     description,
+	}
+	if networkLayer, ok := packet.NetworkLayer().(*layers.IPv4); ok {
+		packetInformation.SrcIp = networkLayer.SrcIP
+		packetInformation.DstIp = networkLayer.DstIP
+	}
+	return packetInformation
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go b/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go
new file mode 100644
index 0000000..f7a5d8b
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package bacnetanalyzer
+
+import (
+	"context"
+	"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+	"github.com/apache/plc4x/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/common"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+)
+
+func PackageParse(packetInformation common.PacketInformation, payload []byte) (spi.Message, error) {
+	log.Debug().Stringer("packetInformation", packetInformation).Msg("Parsing")
+	parse, err := model.BVLCParse(context.TODO(), payload)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error parsing bvlc")
+	}
+	log.Debug().Stringer("parse", parse).Msg("Parsed bvlc")
+	return parse, nil
+}
+
+func SerializePackage(bvlc spi.Message) ([]byte, error) {
+	if bvlc, ok := bvlc.(model.BVLC); !ok {
+		log.Fatal().Type("bvlc", bvlc).Msg("Unsupported type supplied")
+		panic("unreachable statement")
+	} else {
+		theBytes, err := bvlc.Serialize()
+		if err != nil {
+			return nil, errors.Wrap(err, "Error serializing")
+		}
+		return theBytes, nil
+	}
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go b/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go
new file mode 100644
index 0000000..bef46f4
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package cbusanalyzer
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"reflect"
+
+	"github.com/apache/plc4x/plc4go/internal/cbus"
+	"github.com/apache/plc4x/plc4go/protocols/cbus/readwrite/model"
+	"github.com/apache/plc4x/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/common"
+	"github.com/gopacket/gopacket"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+)
+
+type Analyzer struct {
+	Client                          net.IP
+	requestContext                  model.RequestContext
+	cBusOptions                     model.CBusOptions
+	initialized                     bool
+	currentInboundPayloads          map[string][]byte
+	currentPrefilterInboundPayloads map[string][]byte
+	mappedPacketChan                chan gopacket.Packet
+
+	lastParsePayload []byte
+	lastMapPayload   []byte
+}
+
+func (a *Analyzer) Init() {
+	if a.initialized {
+		return
+	}
+	a.requestContext = model.NewRequestContext(false)
+	a.cBusOptions = model.NewCBusOptions(config.CBusConfigInstance.Connect, config.CBusConfigInstance.Smart, config.CBusConfigInstance.Idmon, config.CBusConfigInstance.Exstat, config.CBusConfigInstance.Monitor, config.CBusConfigInstance.Monall, config.CBusConfigInstance.Pun, config.CBusConfigInstance.Pcn, config.CBusConfigInstance.Srchk)
+	a.currentInboundPayloads = make(map[string][]byte)
+	a.currentPrefilterInboundPayloads = make(map[string][]byte)
+	a.initialized = true
+}
+
+func (a *Analyzer) PackageParse(packetInformation common.PacketInformation, payload []byte) (spi.Message, error) {
+	if !a.initialized {
+		log.Warn().Msg("Not initialized... doing that now")
+		a.Init()
+	}
+	cBusOptions := a.cBusOptions
+	log.Debug().
+		Stringer("packetInformation", packetInformation).
+		Stringer("requestContext", a.requestContext).
+		Stringer("cBusOptions", cBusOptions).
+		Bytes("payload", payload).
+		Msg("Parsing")
+	isResponse := a.isResponse(packetInformation)
+	if isResponse {
+		// Responses should have a checksum
+		cBusOptions = model.NewCBusOptions(
+			cBusOptions.GetConnect(),
+			cBusOptions.GetSmart(),
+			cBusOptions.GetIdmon(),
+			cBusOptions.GetExstat(),
+			cBusOptions.GetMonitor(),
+			cBusOptions.GetMonall(),
+			cBusOptions.GetPun(),
+			cBusOptions.GetPcn(),
+			true,
+		)
+	}
+	mergeCallback := func(index int) {
+		log.Warn().Stringer("packetInformation", packetInformation).Int("index", index).Msg("we have a split at index")
+	}
+	currentPayload, err := a.getCurrentPayload(packetInformation, payload, mergeCallback, a.currentInboundPayloads, &a.lastParsePayload)
+	if err != nil {
+		return nil, err
+	}
+	if reflect.DeepEqual(currentPayload, a.lastParsePayload) {
+		return nil, common.ErrEcho
+	}
+	a.lastParsePayload = currentPayload
+	parse, err := model.CBusMessageParse(context.TODO(), currentPayload, isResponse, a.requestContext, cBusOptions)
+	if err != nil {
+		if secondParse, err := model.CBusMessageParse(context.TODO(), currentPayload, isResponse, model.NewRequestContext(false), model.NewCBusOptions(false, false, false, false, false, false, false, false, false)); err != nil {
+			log.Debug().Err(err).Msg("Second parse failed too")
+			return nil, errors.Wrap(err, "Error parsing CBusCommand")
+		} else {
+			log.Warn().
+				Stringer("packetInformation", packetInformation).
+				Stringer("secondParse", secondParse).
+				Msg("package got overridden by second parse... probably a MMI")
+			parse = secondParse
+		}
+	}
+	a.requestContext = cbus.CreateRequestContextWithInfoCallback(parse, func(infoString string) {
+		log.Debug().
+			Int("packetNumber", packetInformation.PacketNumber).
+			Str("infoString", infoString).
+			Msg("No.[packetNumber] infoString")
+	})
+	log.Debug().Stringer("parse", parse).Msg("Parsed c-bus command")
+	return parse, nil
+}
+
+func (a *Analyzer) isResponse(packetInformation common.PacketInformation) bool {
+	isResponse := packetInformation.DstIp.Equal(a.Client)
+	log.Debug().Stringer("packetInformation", packetInformation).Bool("isResponse", isResponse).Msg("isResponse")
+	return isResponse
+}
+
+func (a *Analyzer) getCurrentPayload(packetInformation common.PacketInformation, payload []byte, mergeCallback func(int), currentInboundPayloads map[string][]byte, lastPayload *[]byte) ([]byte, error) {
+	srcUip := packetInformation.SrcIp.String()
+	payload = filterXOnXOff(payload)
+	if len(payload) == 0 {
+		return nil, common.ErrEmptyPackage
+	}
+	currentPayload := currentInboundPayloads[srcUip]
+	if currentPayload != nil {
+		log.Debug().Func(func(e *zerolog.Event) {
+			e.
+				Bytes("currentPayload", currentPayload).
+				Bytes("actualPayload", payload).
+				Interface("allPayload", append(currentPayload, payload...)).
+				Msg("Prepending current payload currentPayload to actual payload actualPayload: allPayload")
+		})
+		currentPayload = append(currentPayload, payload...)
+	} else {
+		currentPayload = payload
+	}
+	if len(currentPayload) == 1 && currentPayload[0] == '!' {
+		// This is an errormessage from the server
+		return currentPayload, nil
+	}
+	containsError := false
+	// We ensure that there are no random ! in the string
+	currentPayload, containsError = filterOneServerError(currentPayload)
+	if containsError {
+		// Save the current inbound payload for the next try
+		currentInboundPayloads[srcUip] = currentPayload
+		return []byte{'!'}, nil
+	}
+	// Check if we have a termination in the middle
+	isMergedMessage, shouldClearInboundPayload := mergeCheck(&currentPayload, srcUip, mergeCallback, currentInboundPayloads, lastPayload)
+	if !isMergedMessage {
+		// When we have a merge message we already set the current payload to the tail
+		currentInboundPayloads[srcUip] = currentPayload
+	} else {
+		log.Debug().Stringer("packetInformation", packetInformation).
+			Interface("remainder", currentInboundPayloads[srcUip]).
+			Msg("Remainder %+q")
+	}
+	if lastElement := currentPayload[len(currentPayload)-1]; (lastElement != '\r') && (lastElement != '\n') {
+		return nil, common.ErrUnterminatedPackage
+	} else {
+		log.Debug().Stringer("packetInformation", packetInformation).
+			Uint8("lastElement", lastElement).
+			Msg("Last element")
+		if shouldClearInboundPayload {
+			if currentSavedPayload := currentInboundPayloads[srcUip]; currentSavedPayload != nil {
+				// We remove our current payload from the beginning of the cache
+				for i, b := range currentPayload {
+					if currentSavedPayload[i] != b {
+						panic("programming error... at this point they should start with the identical bytes")
+					}
+				}
+			}
+			currentInboundPayloads[srcUip] = nil
+		}
+	}
+	log.Debug().Stringer("packetInformation", packetInformation).
+		Bytes("currentPayload", currentPayload).
+		Msg("Returning payload")
+	return currentPayload, nil
+}
+
+func mergeCheck(currentPayload *[]byte, srcUip string, mergeCallback func(int), currentInboundPayloads map[string][]byte, lastPayload *[]byte) (isMergedMessage, shouldClearInboundPayload bool) {
+	// Check if we have a merged message
+	for i, b := range *currentPayload {
+		if i == 0 {
+			// we ignore the first byte as this is typical for reset etc... so maybe this is good or bad we will see
+			continue
+		}
+		switch b {
+		case 0x0D:
+			if i+1 < len(*currentPayload) && (*currentPayload)[i+1] == 0x0A {
+				// If we know the next is a newline we jump to that index...
+				i++
+			}
+			// ... other than that the logic is the same
+			fallthrough
+		case 0x0A:
+			// We have a merged message if we are not at the end
+			if i < len(*currentPayload)-1 {
+				headPayload := (*currentPayload)[:i+1]
+				tailPayload := (*currentPayload)[i+1:]
+				if reflect.DeepEqual(headPayload, *lastPayload) {
+					// This means that we have a merge where the last payload is an echo. In that case we discard that here to not offset all numbers
+					*currentPayload = tailPayload
+					log.Debug().Bytes("headPayload", headPayload).Msg("We cut the echo message %s out of the response to keep numbering")
+					return mergeCheck(currentPayload, srcUip, mergeCallback, currentInboundPayloads, lastPayload)
+				} else {
+					if mergeCallback != nil {
+						mergeCallback(i)
+					}
+					// In this case we need to put the tail into our "buffer"
+					currentInboundPayloads[srcUip] = tailPayload
+					// and use the beginning as current payload
+					*currentPayload = headPayload
+					return true, false
+				}
+			}
+		}
+	}
+	return false, true
+}
+
+func filterXOnXOff(payload []byte) []byte {
+	n := 0
+	for i, b := range payload {
+		switch b {
+		case 0x11: // Filter XON
+			fallthrough
+		case 0x13: // Filter XOFF
+			log.Trace().
+				Uint8("b", b).
+				Int("i", i).
+				Bytes("payload", payload).
+				Msg("Filtering b at i for payload")
+		default:
+			payload[n] = b
+			n++
+		}
+	}
+	return payload[:n]
+}
+
+func filterOneServerError(unfilteredPayload []byte) (filteredPayload []byte, containsError bool) {
+	for i, b := range unfilteredPayload {
+		if b == '!' {
+			return append(unfilteredPayload[:i], unfilteredPayload[i+1:]...), true
+
+		}
+	}
+	return unfilteredPayload, false
+}
+
+func (a *Analyzer) SerializePackage(message spi.Message) ([]byte, error) {
+	if message, ok := message.(model.CBusMessage); !ok {
+		log.Fatal().Type("message", message).Msg("Unsupported type supplied")
+		panic("unreachable statement")
+	} else {
+		theBytes, err := message.Serialize()
+		if err != nil {
+			return nil, errors.Wrap(err, "Error serializing")
+		}
+		return theBytes, nil
+	}
+}
+
+// MapPackets reorders the packages as they were not split
+func (a *Analyzer) MapPackets(in chan gopacket.Packet, packetInformationCreator func(packet gopacket.Packet) common.PacketInformation) chan gopacket.Packet {
+	if a.mappedPacketChan == nil {
+		a.mappedPacketChan = make(chan gopacket.Packet)
+		go func() {
+			defer close(a.mappedPacketChan)
+		mappingLoop:
+			for packet := range in {
+				switch {
+				case packet == nil:
+					log.Debug().Msg("Done reading packages. (nil returned)")
+					a.mappedPacketChan <- nil
+					break mappingLoop
+				case packet.ApplicationLayer() == nil:
+					a.mappedPacketChan <- packet
+				default:
+					packetInformation := packetInformationCreator(packet)
+					mergeCallback := func(index int) {
+						log.Warn().Stringer("packetInformation", packetInformation).
+							Int("index", index).
+							Msg("we have a split at index")
+					}
+					if payload, err := a.getCurrentPayload(packetInformation, packet.ApplicationLayer().Payload(), mergeCallback, a.currentPrefilterInboundPayloads, &a.lastMapPayload); err != nil {
+						log.Debug().Err(err).Stringer("packetInformation", packetInformation).Msg("Filtering message")
+						a.mappedPacketChan <- common.NewFilteredPackage(err, packet)
+					} else {
+						currentApplicationLayer := packet.ApplicationLayer()
+						newPayload := gopacket.Payload(payload)
+						if !reflect.DeepEqual(currentApplicationLayer.Payload(), payload) {
+							log.Debug().
+								Bytes("currentPayload", currentApplicationLayer.Payload()).
+								Bytes("payload", payload).
+								Msg("Replacing payload currentPayload with payload")
+							packet = &manipulatedPackage{Packet: packet, newApplicationLayer: newPayload}
+						}
+						a.lastMapPayload = payload
+						a.mappedPacketChan <- packet
+					}
+				}
+			}
+		}()
+	}
+	return a.mappedPacketChan
+}
+
+// ByteOutput returns the string representation as usually this is ASCII over serial... so this output is much more useful in that context
+func (a *Analyzer) ByteOutput(data []byte) string {
+	return fmt.Sprintf("%+q\n", data)
+}
+
+type manipulatedPackage struct {
+	gopacket.Packet
+	newApplicationLayer gopacket.ApplicationLayer
+}
+
+func (p *manipulatedPackage) SetApplicationLayer(l gopacket.ApplicationLayer) {
+	p.newApplicationLayer = l
+}
+
+func (p *manipulatedPackage) ApplicationLayer() gopacket.ApplicationLayer {
+	return p.newApplicationLayer
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/common/common.go b/plc4go/tools/plc4xpcapanalyzer/internal/common/common.go
new file mode 100644
index 0000000..afab92d
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/common/common.go
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package common
+
+import (
+	"fmt"
+	"github.com/gopacket/gopacket"
+	"github.com/pkg/errors"
+	"net"
+	"time"
+)
+
+type PacketInformation struct {
+	PacketNumber    int
+	PacketTimestamp time.Time
+	Description     string
+	SrcIp           net.IP
+	DstIp           net.IP
+}
+
+func (p PacketInformation) String() string {
+	return fmt.Sprintf("%s (SrcIp:%v, DstIp:%v)", p.Description, p.SrcIp, p.DstIp)
+}
+
+// ErrUnterminatedPackage is used when a transmission is incomplete (usually when package is split)
+var ErrUnterminatedPackage = errors.New("ErrUnterminatedPackage")
+
+// ErrEmptyPackage is used when there is no payload
+var ErrEmptyPackage = errors.New("ErrEmptyPackage")
+
+// ErrEcho is used when the package is a echo from the previous
+var ErrEcho = errors.New("ErrEcho")
+
+type FilteredPackage interface {
+	gopacket.Packet
+	IsFilteredPackage() bool
+	FilterReason() error
+}
+
+func NewFilteredPackage(err error, packet gopacket.Packet) FilteredPackage {
+	return &filteredPackage{Packet: packet, err: err}
+}
+
+type filteredPackage struct {
+	gopacket.Packet
+	err error
+}
+
+func (f *filteredPackage) IsFilteredPackage() bool {
+	return true
+}
+
+func (f *filteredPackage) FilterReason() error {
+	return f.err
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/common/mock_FilteredPackage_test.go b/plc4go/tools/plc4xpcapanalyzer/internal/common/mock_FilteredPackage_test.go
new file mode 100644
index 0000000..e7cb049
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/common/mock_FilteredPackage_test.go
@@ -0,0 +1,703 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Code generated by mockery v2.32.4. DO NOT EDIT.
+
+package common
+
+import (
+	gopacket "github.com/gopacket/gopacket"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MockFilteredPackage is an autogenerated mock type for the FilteredPackage type
+type MockFilteredPackage struct {
+	mock.Mock
+}
+
+type MockFilteredPackage_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockFilteredPackage) EXPECT() *MockFilteredPackage_Expecter {
+	return &MockFilteredPackage_Expecter{mock: &_m.Mock}
+}
+
+// ApplicationLayer provides a mock function with given fields:
+func (_m *MockFilteredPackage) ApplicationLayer() gopacket.ApplicationLayer {
+	ret := _m.Called()
+
+	var r0 gopacket.ApplicationLayer
+	if rf, ok := ret.Get(0).(func() gopacket.ApplicationLayer); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.ApplicationLayer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_ApplicationLayer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplicationLayer'
+type MockFilteredPackage_ApplicationLayer_Call struct {
+	*mock.Call
+}
+
+// ApplicationLayer is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) ApplicationLayer() *MockFilteredPackage_ApplicationLayer_Call {
+	return &MockFilteredPackage_ApplicationLayer_Call{Call: _e.mock.On("ApplicationLayer")}
+}
+
+func (_c *MockFilteredPackage_ApplicationLayer_Call) Run(run func()) *MockFilteredPackage_ApplicationLayer_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_ApplicationLayer_Call) Return(_a0 gopacket.ApplicationLayer) *MockFilteredPackage_ApplicationLayer_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_ApplicationLayer_Call) RunAndReturn(run func() gopacket.ApplicationLayer) *MockFilteredPackage_ApplicationLayer_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Data provides a mock function with given fields:
+func (_m *MockFilteredPackage) Data() []byte {
+	ret := _m.Called()
+
+	var r0 []byte
+	if rf, ok := ret.Get(0).(func() []byte); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]byte)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_Data_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Data'
+type MockFilteredPackage_Data_Call struct {
+	*mock.Call
+}
+
+// Data is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) Data() *MockFilteredPackage_Data_Call {
+	return &MockFilteredPackage_Data_Call{Call: _e.mock.On("Data")}
+}
+
+func (_c *MockFilteredPackage_Data_Call) Run(run func()) *MockFilteredPackage_Data_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_Data_Call) Return(_a0 []byte) *MockFilteredPackage_Data_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_Data_Call) RunAndReturn(run func() []byte) *MockFilteredPackage_Data_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Dump provides a mock function with given fields:
+func (_m *MockFilteredPackage) Dump() string {
+	ret := _m.Called()
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_Dump_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dump'
+type MockFilteredPackage_Dump_Call struct {
+	*mock.Call
+}
+
+// Dump is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) Dump() *MockFilteredPackage_Dump_Call {
+	return &MockFilteredPackage_Dump_Call{Call: _e.mock.On("Dump")}
+}
+
+func (_c *MockFilteredPackage_Dump_Call) Run(run func()) *MockFilteredPackage_Dump_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_Dump_Call) Return(_a0 string) *MockFilteredPackage_Dump_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_Dump_Call) RunAndReturn(run func() string) *MockFilteredPackage_Dump_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ErrorLayer provides a mock function with given fields:
+func (_m *MockFilteredPackage) ErrorLayer() gopacket.ErrorLayer {
+	ret := _m.Called()
+
+	var r0 gopacket.ErrorLayer
+	if rf, ok := ret.Get(0).(func() gopacket.ErrorLayer); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.ErrorLayer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_ErrorLayer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ErrorLayer'
+type MockFilteredPackage_ErrorLayer_Call struct {
+	*mock.Call
+}
+
+// ErrorLayer is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) ErrorLayer() *MockFilteredPackage_ErrorLayer_Call {
+	return &MockFilteredPackage_ErrorLayer_Call{Call: _e.mock.On("ErrorLayer")}
+}
+
+func (_c *MockFilteredPackage_ErrorLayer_Call) Run(run func()) *MockFilteredPackage_ErrorLayer_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_ErrorLayer_Call) Return(_a0 gopacket.ErrorLayer) *MockFilteredPackage_ErrorLayer_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_ErrorLayer_Call) RunAndReturn(run func() gopacket.ErrorLayer) *MockFilteredPackage_ErrorLayer_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// FilterReason provides a mock function with given fields:
+func (_m *MockFilteredPackage) FilterReason() error {
+	ret := _m.Called()
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_FilterReason_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterReason'
+type MockFilteredPackage_FilterReason_Call struct {
+	*mock.Call
+}
+
+// FilterReason is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) FilterReason() *MockFilteredPackage_FilterReason_Call {
+	return &MockFilteredPackage_FilterReason_Call{Call: _e.mock.On("FilterReason")}
+}
+
+func (_c *MockFilteredPackage_FilterReason_Call) Run(run func()) *MockFilteredPackage_FilterReason_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_FilterReason_Call) Return(_a0 error) *MockFilteredPackage_FilterReason_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_FilterReason_Call) RunAndReturn(run func() error) *MockFilteredPackage_FilterReason_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// IsFilteredPackage provides a mock function with given fields:
+func (_m *MockFilteredPackage) IsFilteredPackage() bool {
+	ret := _m.Called()
+
+	var r0 bool
+	if rf, ok := ret.Get(0).(func() bool); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(bool)
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_IsFilteredPackage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsFilteredPackage'
+type MockFilteredPackage_IsFilteredPackage_Call struct {
+	*mock.Call
+}
+
+// IsFilteredPackage is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) IsFilteredPackage() *MockFilteredPackage_IsFilteredPackage_Call {
+	return &MockFilteredPackage_IsFilteredPackage_Call{Call: _e.mock.On("IsFilteredPackage")}
+}
+
+func (_c *MockFilteredPackage_IsFilteredPackage_Call) Run(run func()) *MockFilteredPackage_IsFilteredPackage_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_IsFilteredPackage_Call) Return(_a0 bool) *MockFilteredPackage_IsFilteredPackage_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_IsFilteredPackage_Call) RunAndReturn(run func() bool) *MockFilteredPackage_IsFilteredPackage_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Layer provides a mock function with given fields: _a0
+func (_m *MockFilteredPackage) Layer(_a0 gopacket.LayerType) gopacket.Layer {
+	ret := _m.Called(_a0)
+
+	var r0 gopacket.Layer
+	if rf, ok := ret.Get(0).(func(gopacket.LayerType) gopacket.Layer); ok {
+		r0 = rf(_a0)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.Layer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_Layer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Layer'
+type MockFilteredPackage_Layer_Call struct {
+	*mock.Call
+}
+
+// Layer is a helper method to define mock.On call
+//   - _a0 gopacket.LayerType
+func (_e *MockFilteredPackage_Expecter) Layer(_a0 interface{}) *MockFilteredPackage_Layer_Call {
+	return &MockFilteredPackage_Layer_Call{Call: _e.mock.On("Layer", _a0)}
+}
+
+func (_c *MockFilteredPackage_Layer_Call) Run(run func(_a0 gopacket.LayerType)) *MockFilteredPackage_Layer_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(gopacket.LayerType))
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_Layer_Call) Return(_a0 gopacket.Layer) *MockFilteredPackage_Layer_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_Layer_Call) RunAndReturn(run func(gopacket.LayerType) gopacket.Layer) *MockFilteredPackage_Layer_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// LayerClass provides a mock function with given fields: _a0
+func (_m *MockFilteredPackage) LayerClass(_a0 gopacket.LayerClass) gopacket.Layer {
+	ret := _m.Called(_a0)
+
+	var r0 gopacket.Layer
+	if rf, ok := ret.Get(0).(func(gopacket.LayerClass) gopacket.Layer); ok {
+		r0 = rf(_a0)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.Layer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_LayerClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LayerClass'
+type MockFilteredPackage_LayerClass_Call struct {
+	*mock.Call
+}
+
+// LayerClass is a helper method to define mock.On call
+//   - _a0 gopacket.LayerClass
+func (_e *MockFilteredPackage_Expecter) LayerClass(_a0 interface{}) *MockFilteredPackage_LayerClass_Call {
+	return &MockFilteredPackage_LayerClass_Call{Call: _e.mock.On("LayerClass", _a0)}
+}
+
+func (_c *MockFilteredPackage_LayerClass_Call) Run(run func(_a0 gopacket.LayerClass)) *MockFilteredPackage_LayerClass_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(gopacket.LayerClass))
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_LayerClass_Call) Return(_a0 gopacket.Layer) *MockFilteredPackage_LayerClass_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_LayerClass_Call) RunAndReturn(run func(gopacket.LayerClass) gopacket.Layer) *MockFilteredPackage_LayerClass_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Layers provides a mock function with given fields:
+func (_m *MockFilteredPackage) Layers() []gopacket.Layer {
+	ret := _m.Called()
+
+	var r0 []gopacket.Layer
+	if rf, ok := ret.Get(0).(func() []gopacket.Layer); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]gopacket.Layer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_Layers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Layers'
+type MockFilteredPackage_Layers_Call struct {
+	*mock.Call
+}
+
+// Layers is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) Layers() *MockFilteredPackage_Layers_Call {
+	return &MockFilteredPackage_Layers_Call{Call: _e.mock.On("Layers")}
+}
+
+func (_c *MockFilteredPackage_Layers_Call) Run(run func()) *MockFilteredPackage_Layers_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_Layers_Call) Return(_a0 []gopacket.Layer) *MockFilteredPackage_Layers_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_Layers_Call) RunAndReturn(run func() []gopacket.Layer) *MockFilteredPackage_Layers_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// LinkLayer provides a mock function with given fields:
+func (_m *MockFilteredPackage) LinkLayer() gopacket.LinkLayer {
+	ret := _m.Called()
+
+	var r0 gopacket.LinkLayer
+	if rf, ok := ret.Get(0).(func() gopacket.LinkLayer); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.LinkLayer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_LinkLayer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkLayer'
+type MockFilteredPackage_LinkLayer_Call struct {
+	*mock.Call
+}
+
+// LinkLayer is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) LinkLayer() *MockFilteredPackage_LinkLayer_Call {
+	return &MockFilteredPackage_LinkLayer_Call{Call: _e.mock.On("LinkLayer")}
+}
+
+func (_c *MockFilteredPackage_LinkLayer_Call) Run(run func()) *MockFilteredPackage_LinkLayer_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_LinkLayer_Call) Return(_a0 gopacket.LinkLayer) *MockFilteredPackage_LinkLayer_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_LinkLayer_Call) RunAndReturn(run func() gopacket.LinkLayer) *MockFilteredPackage_LinkLayer_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Metadata provides a mock function with given fields:
+func (_m *MockFilteredPackage) Metadata() *gopacket.PacketMetadata {
+	ret := _m.Called()
+
+	var r0 *gopacket.PacketMetadata
+	if rf, ok := ret.Get(0).(func() *gopacket.PacketMetadata); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*gopacket.PacketMetadata)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_Metadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Metadata'
+type MockFilteredPackage_Metadata_Call struct {
+	*mock.Call
+}
+
+// Metadata is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) Metadata() *MockFilteredPackage_Metadata_Call {
+	return &MockFilteredPackage_Metadata_Call{Call: _e.mock.On("Metadata")}
+}
+
+func (_c *MockFilteredPackage_Metadata_Call) Run(run func()) *MockFilteredPackage_Metadata_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_Metadata_Call) Return(_a0 *gopacket.PacketMetadata) *MockFilteredPackage_Metadata_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_Metadata_Call) RunAndReturn(run func() *gopacket.PacketMetadata) *MockFilteredPackage_Metadata_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NetworkLayer provides a mock function with given fields:
+func (_m *MockFilteredPackage) NetworkLayer() gopacket.NetworkLayer {
+	ret := _m.Called()
+
+	var r0 gopacket.NetworkLayer
+	if rf, ok := ret.Get(0).(func() gopacket.NetworkLayer); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.NetworkLayer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_NetworkLayer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NetworkLayer'
+type MockFilteredPackage_NetworkLayer_Call struct {
+	*mock.Call
+}
+
+// NetworkLayer is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) NetworkLayer() *MockFilteredPackage_NetworkLayer_Call {
+	return &MockFilteredPackage_NetworkLayer_Call{Call: _e.mock.On("NetworkLayer")}
+}
+
+func (_c *MockFilteredPackage_NetworkLayer_Call) Run(run func()) *MockFilteredPackage_NetworkLayer_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_NetworkLayer_Call) Return(_a0 gopacket.NetworkLayer) *MockFilteredPackage_NetworkLayer_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_NetworkLayer_Call) RunAndReturn(run func() gopacket.NetworkLayer) *MockFilteredPackage_NetworkLayer_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// String provides a mock function with given fields:
+func (_m *MockFilteredPackage) String() string {
+	ret := _m.Called()
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_String_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'String'
+type MockFilteredPackage_String_Call struct {
+	*mock.Call
+}
+
+// String is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) String() *MockFilteredPackage_String_Call {
+	return &MockFilteredPackage_String_Call{Call: _e.mock.On("String")}
+}
+
+func (_c *MockFilteredPackage_String_Call) Run(run func()) *MockFilteredPackage_String_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_String_Call) Return(_a0 string) *MockFilteredPackage_String_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_String_Call) RunAndReturn(run func() string) *MockFilteredPackage_String_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// TransportLayer provides a mock function with given fields:
+func (_m *MockFilteredPackage) TransportLayer() gopacket.TransportLayer {
+	ret := _m.Called()
+
+	var r0 gopacket.TransportLayer
+	if rf, ok := ret.Get(0).(func() gopacket.TransportLayer); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(gopacket.TransportLayer)
+		}
+	}
+
+	return r0
+}
+
+// MockFilteredPackage_TransportLayer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TransportLayer'
+type MockFilteredPackage_TransportLayer_Call struct {
+	*mock.Call
+}
+
+// TransportLayer is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) TransportLayer() *MockFilteredPackage_TransportLayer_Call {
+	return &MockFilteredPackage_TransportLayer_Call{Call: _e.mock.On("TransportLayer")}
+}
+
+func (_c *MockFilteredPackage_TransportLayer_Call) Run(run func()) *MockFilteredPackage_TransportLayer_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_TransportLayer_Call) Return(_a0 gopacket.TransportLayer) *MockFilteredPackage_TransportLayer_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFilteredPackage_TransportLayer_Call) RunAndReturn(run func() gopacket.TransportLayer) *MockFilteredPackage_TransportLayer_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// VerifyChecksums provides a mock function with given fields:
+func (_m *MockFilteredPackage) VerifyChecksums() (error, []gopacket.ChecksumMismatch) {
+	ret := _m.Called()
+
+	var r0 error
+	var r1 []gopacket.ChecksumMismatch
+	if rf, ok := ret.Get(0).(func() (error, []gopacket.ChecksumMismatch)); ok {
+		return rf()
+	}
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	if rf, ok := ret.Get(1).(func() []gopacket.ChecksumMismatch); ok {
+		r1 = rf()
+	} else {
+		if ret.Get(1) != nil {
+			r1 = ret.Get(1).([]gopacket.ChecksumMismatch)
+		}
+	}
+
+	return r0, r1
+}
+
+// MockFilteredPackage_VerifyChecksums_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyChecksums'
+type MockFilteredPackage_VerifyChecksums_Call struct {
+	*mock.Call
+}
+
+// VerifyChecksums is a helper method to define mock.On call
+func (_e *MockFilteredPackage_Expecter) VerifyChecksums() *MockFilteredPackage_VerifyChecksums_Call {
+	return &MockFilteredPackage_VerifyChecksums_Call{Call: _e.mock.On("VerifyChecksums")}
+}
+
+func (_c *MockFilteredPackage_VerifyChecksums_Call) Run(run func()) *MockFilteredPackage_VerifyChecksums_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFilteredPackage_VerifyChecksums_Call) Return(_a0 error, _a1 []gopacket.ChecksumMismatch) *MockFilteredPackage_VerifyChecksums_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFilteredPackage_VerifyChecksums_Call) RunAndReturn(run func() (error, []gopacket.ChecksumMismatch)) *MockFilteredPackage_VerifyChecksums_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockFilteredPackage creates a new instance of MockFilteredPackage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMockFilteredPackage(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockFilteredPackage {
+	mock := &MockFilteredPackage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go b/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go
new file mode 100644
index 0000000..03ffa92
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package extractor
+
+import (
+	"context"
+	"fmt"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/common"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler"
+	"github.com/fatih/color"
+	"github.com/gopacket/gopacket/layers"
+	"github.com/k0kubun/go-ansi"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"github.com/schollz/progressbar/v3"
+	"io"
+	"net"
+)
+
+func Extract(pcapFile, protocolType string) error {
+	return ExtractWithOutput(context.TODO(), pcapFile, protocolType, ansi.NewAnsiStdout(), ansi.NewAnsiStderr())
+}
+
+func ExtractWithOutput(ctx context.Context, pcapFile, protocolType string, stdout, stderr io.Writer) error {
+	var printPayload = func(packetInformation common.PacketInformation, item []byte) {
+		_, _ = fmt.Fprintf(stdout, "%x\n", item)
+	}
+	switch protocolType {
+	case "bacnet":
+		// nothing special as this is byte based
+	case "c-bus":
+		// c-bus is string based so we consume the string and print it
+		clientIp := net.ParseIP(config.ExtractConfigInstance.Client)
+		serverResponseWriter := color.New(color.FgRed)
+		serverResponseIndicatorWriter := color.New(color.FgHiRed)
+		clientRequestWriter := color.New(color.FgGreen)
+		clientRequestIndicatorWriter := color.New(color.FgHiGreen)
+		printPayload = func(packetInformation common.PacketInformation, payload []byte) {
+			payloadString := ""
+			suffix := ""
+			extraInformation := ""
+			if config.ExtractConfigInstance.Verbosity > 2 {
+				extraInformation = fmt.Sprintf("(No.[%d])", packetInformation.PacketNumber)
+			}
+			if len(payload) > 0 {
+				if properTerminated := payload[len(payload)-1] == 0x0D || payload[len(payload)-1] == 0x0A; properTerminated {
+					suffix = "\n"
+				}
+				quotedPayload := fmt.Sprintf("%+q", payload)
+				unquotedPayload := quotedPayload[1 : len(quotedPayload)-1]
+				payloadString = unquotedPayload
+			}
+			if isResponse := packetInformation.DstIp.Equal(clientIp); isResponse {
+				if config.ExtractConfigInstance.ShowDirectionalIndicators {
+					_, _ = serverResponseIndicatorWriter.Fprintf(stderr, "%s(<--pci)", extraInformation)
+				}
+				_, _ = serverResponseWriter.Fprintf(stdout, "%s%s", payloadString, suffix)
+			} else {
+				if config.ExtractConfigInstance.ShowDirectionalIndicators {
+					_, _ = clientRequestIndicatorWriter.Fprintf(stderr, "%s(-->pci)", extraInformation)
+				}
+				_, _ = clientRequestWriter.Fprintf(stdout, "%s%s", payloadString, suffix)
+			}
+		}
+	}
+	filterExpression := config.ExtractConfigInstance.Filter
+	log.Info().
+		Str("pcapFile", pcapFile).
+		Str("protocolType", protocolType).
+		Str("filterExpression", filterExpression).
+		Msg("Analyzing pcap file pcapFile with protocolType protocolType and filter filterExpression now")
+
+	handle, numberOfPackage, timestampToIndexMap, err := pcaphandler.GetIndexedPcapHandle(pcapFile, filterExpression)
+	if err != nil {
+		return errors.Wrap(err, "Error getting handle")
+	}
+	log.Info().Int("numberOfPackage", numberOfPackage).Msg("Starting to analyze numberOfPackage packages")
+	defer handle.Close()
+	log.Debug().Interface("handle", handle).Int("numberOfPackage", numberOfPackage).Msg("got handle")
+	source := pcaphandler.GetPacketSource(handle)
+	bar := progressbar.NewOptions(numberOfPackage, progressbar.OptionSetWriter(ansi.NewAnsiStderr()),
+		progressbar.OptionSetVisibility(!config.RootConfigInstance.HideProgressBar),
+		progressbar.OptionEnableColorCodes(true),
+		progressbar.OptionShowBytes(false),
+		progressbar.OptionSetWidth(15),
+		progressbar.OptionSetDescription("[cyan][1/3][reset] Analyzing packages..."),
+		progressbar.OptionSetTheme(progressbar.Theme{
+			Saucer:        "[green]=[reset]",
+			SaucerHead:    "[green]>[reset]",
+			SaucerPadding: " ",
+			BarStart:      "[",
+			BarEnd:        "]",
+		}))
+	currentPackageNum := uint(0)
+	parseFails := 0
+	serializeFails := 0
+	compareFails := 0
+	for packet := range source.Packets() {
+		if errors.Is(ctx.Err(), context.Canceled) {
+			log.Info().
+				Uint("currentPackageNum", currentPackageNum).
+				Msg("Aborted after currentPackageNum packages")
+			break
+		}
+		currentPackageNum++
+		if currentPackageNum < config.ExtractConfigInstance.StartPackageNumber {
+			log.Debug().
+				Uint("currentPackageNum", currentPackageNum).
+				Uint("startPackageNumber", config.ExtractConfigInstance.StartPackageNumber).
+				Msg("Skipping package number currentPackageNum (till no. startPackageNumber)")
+			continue
+		}
+		if currentPackageNum > config.ExtractConfigInstance.PackageNumberLimit {
+			log.Warn().
+				Uint("packageNumberLimit", config.ExtractConfigInstance.PackageNumberLimit).
+				Msg("Aborting reading packages because we hit the limit of packageNumberLimit")
+			break
+		}
+		if packet == nil {
+			log.Debug().Msg("Done reading packages. (nil returned)")
+			break
+		}
+		if err := bar.Add(1); err != nil {
+			log.Warn().Err(err).Msg("Error updating progressBar")
+		}
+		packetTimestamp := packet.Metadata().Timestamp
+		realPacketNumber := timestampToIndexMap[packetTimestamp]
+		description := fmt.Sprintf("No.[%d] timestamp: %v, %s", realPacketNumber, packetTimestamp, pcapFile)
+		packetInformation := common.PacketInformation{
+			PacketNumber:    realPacketNumber,
+			PacketTimestamp: packetTimestamp,
+			Description:     description,
+		}
+		if networkLayer, ok := packet.NetworkLayer().(*layers.IPv4); ok {
+			packetInformation.SrcIp = networkLayer.SrcIP
+			packetInformation.DstIp = networkLayer.DstIP
+		}
+
+		var payload []byte
+		applicationLayer := packet.ApplicationLayer()
+		if applicationLayer == nil {
+			log.Info().Stringer("packetInformation", packetInformation).Int("realPacketNumber", realPacketNumber).Msg("No.[realPacketNumber] No application layer")
+		} else {
+			payload = applicationLayer.Payload()
+		}
+
+		log.Debug().Hex("payload", payload).Msg("Got payload")
+		if config.ExtractConfigInstance.Verbosity > 1 {
+			printPayload(packetInformation, payload)
+		}
+	}
+	_, _ = fmt.Fprintf(stdout, "\n")
+
+	log.Info().
+		Uint("currentPackageNum", currentPackageNum).
+		Int("numberOfPackage", numberOfPackage).
+		Int("parseFails", parseFails).
+		Int("serializeFails", serializeFails).
+		Int("compareFails", compareFails).
+		Msg("Done evaluating currentPackageNum of numberOfPackage packages (parseFails failed to parse, serializeFails failed to serialize and compareFails failed in byte comparison)")
+	return nil
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go b/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go
new file mode 100644
index 0000000..2cd9896
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package pcaphandler
+
+import (
+	"github.com/gopacket/gopacket"
+	"github.com/gopacket/gopacket/pcap"
+	"github.com/pkg/errors"
+	"time"
+)
+
+// GetPacketSource gets a packet source from a handle
+func GetPacketSource(handle *pcap.Handle) *gopacket.PacketSource {
+	return gopacket.NewPacketSource(handle, handle.LinkType())
+}
+
+// GetIndexedPcapHandle returns a *pcap.Handle, the number of packages found and an index which maps timestamp to
+// absolute package number
+func GetIndexedPcapHandle(file, filterExpression string) (handle *pcap.Handle, numberOfPackages int, timestampToIndexMap map[time.Time]int, err error) {
+	timestampToIndexMap = make(map[time.Time]int)
+	// Count absolute packages and set timestamp map
+	temporaryHandle, err := GetPcapHandle(file, "")
+	if err != nil {
+		return nil, 0, nil, err
+	}
+	defer temporaryHandle.Close()
+	packetSource := GetPacketSource(temporaryHandle)
+	packages := 0
+	for packet := range packetSource.Packets() {
+		if packet == nil {
+			break
+		}
+		packages++
+		timestampToIndexMap[packet.Metadata().Timestamp] = packages
+	}
+	// Just count filtered packages
+	temporaryFilteredHandle, err := GetPcapHandle(file, filterExpression)
+	if err != nil {
+		return nil, 0, nil, err
+	}
+	defer temporaryFilteredHandle.Close()
+	filteredPacketSource := GetPacketSource(temporaryFilteredHandle)
+	packages = 0
+	for packet := range filteredPacketSource.Packets() {
+		if packet == nil {
+			break
+		}
+		packages++
+	}
+	pcapHandle, err := GetPcapHandle(file, filterExpression)
+	if err != nil {
+		return nil, 0, nil, err
+	}
+	return pcapHandle, packages, timestampToIndexMap, nil
+}
+
+// GetPcapHandle returns a *pcap.Handle and panics if an error occurs
+func GetPcapHandle(file, filterExpression string) (*pcap.Handle, error) {
+	handle, err := pcap.OpenOffline(file)
+	if err != nil {
+		return nil, errors.Wrap(err, "error open offline")
+	}
+	if filterExpression != "" {
+		if err := handle.SetBPFFilter(filterExpression); err != nil {
+			return nil, errors.Wrap(err, "error setting BPF filter")
+		}
+	}
+	return handle, nil
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/main.go b/plc4go/tools/plc4xpcapanalyzer/main.go
new file mode 100644
index 0000000..cb3fd52
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/main.go
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/cmd"
+
+func main() {
+	cmd.Execute()
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/actions.go b/plc4go/tools/plc4xpcapanalyzer/ui/actions.go
new file mode 100644
index 0000000..085558c
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/actions.go
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/apache/plc4x/plc4go/internal/ads"
+	"github.com/apache/plc4x/plc4go/internal/bacnetip"
+	"github.com/apache/plc4x/plc4go/internal/cbus"
+	"github.com/apache/plc4x/plc4go/internal/s7"
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	"github.com/apache/plc4x/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/spi/transports/pcap"
+	"github.com/pkg/errors"
+	"github.com/rivo/tview"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"os"
+	"path"
+	"strings"
+)
+
+func InitSubsystem() {
+	logLevel := zerolog.InfoLevel
+	if configuredLevel := config.LogLevel; configuredLevel != "" {
+		if parsedLevel, err := zerolog.ParseLevel(configuredLevel); err != nil {
+			panic(err)
+		} else {
+			logLevel = parsedLevel
+		}
+	}
+	driverManager = plc4go.NewPlcDriverManager()
+
+	log.Logger = log.
+		//// Enable below if you want to see the filenames
+		//With().Caller().Logger().
+		Output(zerolog.NewConsoleWriter(
+			func(w *zerolog.ConsoleWriter) {
+				w.Out = tview.ANSIWriter(consoleOutput)
+			},
+			func(w *zerolog.ConsoleWriter) {
+				w.FormatFieldValue = func(i interface{}) string {
+					if aString, ok := i.(string); ok && strings.Contains(aString, "\\n") {
+						return fmt.Sprintf("\x1b[%dm%v\x1b[0m", 31, "see below")
+					}
+					return fmt.Sprintf("%s", i)
+				}
+				w.FormatExtra = func(m map[string]interface{}, buffer *bytes.Buffer) error {
+					for key, i := range m {
+						if aString, ok := i.(string); ok && strings.Contains(aString, "\n") {
+							buffer.WriteString("\n")
+							buffer.WriteString(fmt.Sprintf("\x1b[%dm%v\x1b[0m", 32, "field "+key))
+							buffer.WriteString(":\n" + aString)
+						}
+					}
+					return nil
+				}
+			},
+		)).
+		Level(logLevel)
+
+	// We offset the commands executed with the last commands
+	commandsExecuted = len(config.History.Last10Commands)
+	outputCommandHistory()
+
+	for _, driver := range config.AutoRegisterDrivers {
+		log.Info().Str("driver", driver).Msg("Auto register driver")
+		if err := validateDriverParam(driver); err != nil {
+			log.Err(err).Msg("Invalid configuration")
+			continue
+		}
+		_ = registerDriver(driver)
+	}
+}
+
+func OpenFile(pcapFile string) error {
+	if !strings.HasPrefix(pcapFile, string(os.PathSeparator)) {
+		pcapFile = path.Join(currentDir, pcapFile)
+	}
+	var name string
+	if stat, err := os.Stat(pcapFile); err != nil {
+		return err
+	} else if stat.IsDir() {
+		stat.Name()
+		return errors.Errorf("%s is a dir", pcapFile)
+	} else {
+		name = stat.Name()
+	}
+	for _, loadedPcapFile := range loadedPcapFiles {
+		if loadedPcapFile.path == pcapFile {
+			return errors.Errorf("%s already loaded", name)
+		}
+	}
+	addRecentFilesEntry(pcapFile)
+	loadedPcapFiles = append(loadedPcapFiles, loadedPcapFile{
+		name: name,
+		path: pcapFile,
+	})
+	loadedPcapFilesChanged()
+	return nil
+}
+
+func outputCommandHistory() {
+	_, _ = fmt.Fprintln(commandOutput, "[#0000ff]Last 10 commands[white]")
+	for i, command := range config.History.Last10Commands {
+		_, _ = fmt.Fprintf(commandOutput, "   [#00ff00]%d[white]: [\"%d\"]%s[\"\"]\n", i, i, tview.Escape(command))
+	}
+}
+
+func validateDriverParam(driver string) error {
+	for _, protocol := range protocolList {
+		if protocol == driver {
+			return nil
+		}
+	}
+	return errors.Errorf("protocol %s not found", driver)
+}
+
+func registerDriver(driver string) error {
+	switch driver {
+	case "ads":
+		driverManager.RegisterDriver(ads.NewDriver())
+	case "bacnetip":
+		driverManager.RegisterDriver(bacnetip.NewDriver())
+	case "c-bus":
+		driverManager.RegisterDriver(cbus.NewDriver())
+	case "s7":
+		driverManager.RegisterDriver(s7.NewDriver())
+	default:
+		return errors.Errorf("Unknown driver %s", driver)
+	}
+	driverManager.(spi.TransportAware).RegisterTransport(pcap.NewTransport())
+	go driverAdded(driver)
+	return nil
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/commands.go b/plc4go/tools/plc4xpcapanalyzer/ui/commands.go
new file mode 100644
index 0000000..b6b752d
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/commands.go
@@ -0,0 +1,724 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"context"
+	"fmt"
+	plc4x_config "github.com/apache/plc4x/plc4go/pkg/api/config"
+	"github.com/apache/plc4x/plc4go/spi"
+	cliConfig "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
+	"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/extractor"
+	"github.com/pkg/errors"
+	"github.com/rivo/tview"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"os"
+	"path"
+	"reflect"
+	"runtime/debug"
+	"strings"
+	"time"
+)
+
+const rootCommandIndicator = "rootCommand"
+
+var rootCommand = Command{
+	Name: rootCommandIndicator,
+	subCommands: []Command{
+		{
+			Name:        "ls",
+			Description: "list directories",
+			action: func(_ context.Context, _ Command, dir string) error {
+				if dir == "" {
+					dir = currentDir
+				}
+				_, _ = fmt.Fprintf(commandOutput, "dir cotents of %s\n", dir)
+				readDir, err := os.ReadDir(dir)
+				if err != nil {
+					return err
+				}
+				for _, dirEntry := range readDir {
+					isDir := dirEntry.IsDir()
+					name := dirEntry.Name()
+					name = strings.TrimPrefix(name, dir)
+					if isDir {
+						name = fmt.Sprintf("[#0000ff]%s[white]", name)
+					} else if strings.HasSuffix(name, ".pcap") || strings.HasSuffix(name, ".pcapng") {
+						name = fmt.Sprintf("[#00ff00]%s[white]", name)
+					}
+					_, _ = fmt.Fprintf(commandOutput, "%s\n", name)
+				}
+				return nil
+			},
+			// TODO: add parameter suggestions
+		},
+		{
+			Name:        "cd",
+			Description: "changes directory",
+			action: func(_ context.Context, _ Command, newDir string) error {
+				var proposedCurrentDir string
+				if newDir == "" {
+					var err error
+					proposedCurrentDir, err = os.UserHomeDir()
+					if err != nil {
+						return err
+					}
+				} else if strings.HasPrefix(newDir, "."+string(os.PathSeparator)) {
+					proposedCurrentDir = currentDir + strings.TrimPrefix(newDir, ".")
+				} else if strings.HasPrefix(newDir, ""+string(os.PathSeparator)) {
+					proposedCurrentDir = newDir
+				} else {
+					proposedCurrentDir = currentDir + string(os.PathSeparator) + newDir
+				}
+				stat, err := os.Stat(proposedCurrentDir)
+				if err != nil {
+					return err
+				}
+				if !stat.IsDir() {
+					return errors.Errorf("%s is not a dir", newDir)
+				}
+				currentDir = proposedCurrentDir
+				_, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir)
+				return nil
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				if strings.HasPrefix(currentText, string(os.PathSeparator)) {
+					dirEntries, err := os.ReadDir(currentText)
+					if err != nil {
+						plc4xpcapanalyzerLog.Warn().Err(err).Msg("Error suggesting directories")
+						return
+					}
+					for _, dirEntry := range dirEntries {
+						entry := path.Join(currentText, dirEntry.Name())
+						entries = append(entries, entry)
+					}
+				} else {
+					dirEntries, err := os.ReadDir(currentDir)
+					if err != nil {
+						plc4xpcapanalyzerLog.Warn().Err(err).Msg("Error suggesting directories")
+						return
+					}
+					for _, dirEntry := range dirEntries {
+						entry := path.Join(".", dirEntry.Name())
+						entries = append(entries, entry)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "pwd",
+			Description: "shows current directory",
+			action: func(_ context.Context, _ Command, _ string) error {
+				_, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir)
+				return nil
+			},
+		},
+		{
+			Name:        "open",
+			Description: "open file",
+			action: func(_ context.Context, _ Command, pcapFile string) error {
+				return OpenFile(pcapFile)
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				entries = append(entries, config.History.Last10Files...)
+				readDir, err := os.ReadDir(currentDir)
+				if err != nil {
+					return
+				}
+				for _, dirEntry := range readDir {
+					name := dirEntry.Name()
+					name = strings.TrimPrefix(name, currentDir)
+					if strings.HasSuffix(dirEntry.Name(), ".cap") || strings.HasSuffix(dirEntry.Name(), ".pcap") || strings.HasSuffix(name, ".pcapng") {
+						entries = append(entries, name)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "analyze",
+			Description: "Analyzes a pcap file using a driver",
+			action: func(ctx context.Context, _ Command, protocolTypeAndPcapFile string) error {
+				split := strings.Split(protocolTypeAndPcapFile, " ")
+				if len(split) != 2 {
+					return errors.Errorf("expect protocol and pcapfile")
+				}
+				protocolType := split[0]
+				pcapFile := strings.TrimPrefix(protocolTypeAndPcapFile, protocolType+" ")
+				cliConfig.PcapConfigInstance.Client = config.HostIp
+				cliConfig.RootConfigInstance.HideProgressBar = true
+				// disabled as we get this output anyway with the message call back
+				//cliConfig.RootConfigInstance.Verbosity = 4
+				return analyzer.AnalyzeWithOutputAndCallback(ctx, pcapFile, protocolType, tview.ANSIWriter(messageOutput), tview.ANSIWriter(messageOutput), func(parsed spi.Message) {
+					spiNumberOfMessagesReceived++
+					spiMessageReceived(spiNumberOfMessagesReceived, time.Now(), parsed)
+				})
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for _, file := range loadedPcapFiles {
+					for _, protocol := range protocolList {
+						entries = append(entries, protocol+" "+file.path)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "extract",
+			Description: "Extract a pcap file using a driver",
+			action: func(ctx context.Context, _ Command, protocolTypeAndPcapFile string) error {
+				split := strings.Split(protocolTypeAndPcapFile, " ")
+				if len(split) != 2 {
+					return errors.Errorf("expect protocol and pcapfile")
+				}
+				protocolType := split[0]
+				pcapFile := strings.TrimPrefix(protocolTypeAndPcapFile, protocolType+" ")
+				cliConfig.PcapConfigInstance.Client = config.HostIp
+				cliConfig.RootConfigInstance.HideProgressBar = true
+				cliConfig.RootConfigInstance.Verbosity = 4
+				return extractor.ExtractWithOutput(ctx, pcapFile, protocolType, tview.ANSIWriter(messageOutput), tview.ANSIWriter(messageOutput))
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for _, file := range loadedPcapFiles {
+					for _, protocol := range protocolList {
+						entries = append(entries, protocol+" "+file.path)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "host",
+			Description: "The host which is assumed to be the sender (important for protocols that are directional)",
+			subCommands: []Command{
+				{
+					Name: "set",
+					action: func(_ context.Context, _ Command, host string) error {
+						config.HostIp = host
+						return nil
+					},
+				},
+				{
+					Name: "get",
+					action: func(_ context.Context, _ Command, host string) error {
+						_, _ = fmt.Fprintf(commandOutput, "current set host %s", config.HostIp)
+						return nil
+					},
+				},
+			},
+		},
+		{
+			Name:        "register",
+			Description: "register a driver in the subsystem",
+			action: func(_ context.Context, _ Command, driver string) error {
+				return registerDriver(driver)
+			},
+			parameterSuggestions: func(currentText string) (entries []string) {
+				for _, protocol := range protocolList {
+					if strings.HasPrefix(protocol, currentText) {
+						entries = append(entries, protocol)
+					}
+				}
+				return
+			},
+		},
+		{
+			Name:        "quit",
+			Description: "Quits the application",
+		},
+		{
+			Name:        "log",
+			Description: "Log related operations",
+			subCommands: []Command{
+				{
+					Name:        "get",
+					Description: "Get a log level",
+					action: func(_ context.Context, _ Command, _ string) error {
+						_, _ = fmt.Fprintf(commandOutput, "Current log level %s", log.Logger.GetLevel())
+						return nil
+					},
+				},
+				{
+					Name:        "set",
+					Description: "Sets a log level",
+					action: func(_ context.Context, _ Command, level string) error {
+						parseLevel, err := zerolog.ParseLevel(level)
+						if err != nil {
+							return errors.Wrapf(err, "Error setting log level")
+						}
+						setLevel(parseLevel)
+						log.Logger = log.Logger.Level(parseLevel)
+						return nil
+					},
+					parameterSuggestions: func(currentText string) (entries []string) {
+						levels := []string{
+							zerolog.LevelTraceValue,
+							zerolog.LevelDebugValue,
+							zerolog.LevelInfoValue,
+							zerolog.LevelWarnValue,
+							zerolog.LevelErrorValue,
+							zerolog.LevelFatalValue,
+							zerolog.LevelPanicValue,
+						}
+						for _, level := range levels {
+							entries = append(entries, level)
+						}
+						return
+					},
+				},
+			},
+		},
+		{
+			Name:        "conf",
+			Description: "Various settings for plc4xpcapanalyzer",
+			subCommands: []Command{
+				{
+					Name:        "list",
+					Description: "list config values with their current settings",
+					action: func(_ context.Context, _ Command, _ string) error {
+						allCliConfigsValue := reflect.ValueOf(allCliConfigsInstances)
+						for i := 0; i < allCliConfigsValue.NumField(); i++ {
+							allConfigField := allCliConfigsValue.Field(i)
+							allConfigFieldType := allCliConfigsValue.Type().Field(i)
+							_, _ = fmt.Fprintf(commandOutput, "%s:\n", allConfigFieldType.Name)
+							configInstanceReflectValue := reflect.ValueOf(allConfigField.Interface())
+							if configInstanceReflectValue.Kind() == reflect.Ptr {
+								configInstanceReflectValue = configInstanceReflectValue.Elem()
+							}
+							for j := 0; j < configInstanceReflectValue.NumField(); j++ {
+								configField := configInstanceReflectValue.Field(j)
+								configFieldType := configInstanceReflectValue.Type().Field(j)
+								if configFieldType.Tag.Get("json") == "-" {
+									// Ignore those
+									continue
+								}
+								_, _ = fmt.Fprintf(commandOutput, "  %s: %s\t= %v\n", configFieldType.Name, configFieldType.Type, configField.Interface())
+							}
+						}
+						return nil
+					},
+				},
+				{
+					Name:        "set",
+					Description: "sets a config value",
+					subCommands: func() []Command {
+						var configCommand []Command
+						allCliConfigsValue := reflect.ValueOf(allCliConfigsInstances)
+						for i := 0; i < allCliConfigsValue.NumField(); i++ {
+							allConfigField := allCliConfigsValue.Field(i)
+							allConfigFieldType := allCliConfigsValue.Type().Field(i)
+							configCommand = append(configCommand, Command{
+								Name:        allConfigFieldType.Name,
+								Description: fmt.Sprintf("Setting for %s", allConfigFieldType.Name),
+								subCommands: func() []Command {
+									var configElementCommands []Command
+									configInstanceReflectValue := reflect.ValueOf(allConfigField.Interface())
+									if configInstanceReflectValue.Kind() == reflect.Ptr {
+										configInstanceReflectValue = configInstanceReflectValue.Elem()
+									}
+									for i := 0; i < configInstanceReflectValue.NumField(); i++ {
+										field := configInstanceReflectValue.Field(i)
+										fieldOfType := configInstanceReflectValue.Type().Field(i)
+										if fieldOfType.Tag.Get("json") == "-" {
+											// Ignore those
+											continue
+										}
+										configElementCommands = append(configElementCommands, Command{
+											Name:        fieldOfType.Name,
+											Description: fmt.Sprintf("Sets value for %s", fieldOfType.Name),
+											action: func(_ context.Context, _ Command, argument string) error {
+												field.SetString(argument)
+												return nil
+											},
+										})
+									}
+									return configElementCommands
+								}(),
+							})
+						}
+						return configCommand
+					}(),
+				},
+				{
+					Name:        "plc4xpcapanalyzer-debug",
+					Description: "Prints out debug information of the pcap analyzer itself",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "debug on",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4xpcapanalyzerLog = zerolog.New(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)})
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "debug off",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4xpcapanalyzerLog = zerolog.Nop()
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "auto-register",
+					Description: "autoregister driver at startup",
+					subCommands: []Command{
+						{
+							Name: "list",
+							action: func(_ context.Context, currentCommand Command, argument string) error {
+								_, _ = fmt.Fprintf(commandOutput, "Auto-register enabled drivers:\n  %s\n", strings.Join(config.AutoRegisterDrivers, "\n  "))
+								return nil
+							},
+						},
+						{
+							Name: "enable",
+							action: func(_ context.Context, _ Command, argument string) error {
+								return enableAutoRegister(argument)
+							},
+							parameterSuggestions: func(currentText string) (entries []string) {
+								for _, protocol := range protocolList {
+									if strings.HasPrefix(protocol, currentText) {
+										entries = append(entries, protocol)
+									}
+								}
+								return
+							},
+						},
+						{
+							Name: "disable",
+							action: func(_ context.Context, _ Command, argument string) error {
+								return disableAutoRegister(argument)
+							},
+							parameterSuggestions: func(currentText string) (entries []string) {
+								for _, protocol := range protocolList {
+									if strings.HasPrefix(protocol, currentText) {
+										entries = append(entries, protocol)
+									}
+								}
+								return
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			Name:        "plc4x-conf",
+			Description: "plc4x related settings",
+			subCommands: []Command{
+				{
+					Name:        "TraceTransactionManagerWorkers",
+					Description: "print information about transaction manager workers",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "trace on",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4x_config.TraceTransactionManagerWorkers = true
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "trace off",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4x_config.TraceTransactionManagerWorkers = false
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "TraceTransactionManagerTransactions",
+					Description: "print information about transaction manager transactions",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "trace on",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4x_config.TraceTransactionManagerTransactions = true
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "trace off",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4x_config.TraceTransactionManagerTransactions = false
+								return nil
+							},
+						},
+					},
+				},
+				{
+					Name:        "TraceDefaultMessageCodecWorker",
+					Description: "print information about message codec workers",
+					subCommands: []Command{
+						{
+							Name:        "on",
+							Description: "trace on",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4x_config.TraceDefaultMessageCodecWorker = true
+								return nil
+							},
+						},
+						{
+							Name:        "off",
+							Description: "trace off",
+							action: func(_ context.Context, _ Command, _ string) error {
+								plc4x_config.TraceDefaultMessageCodecWorker = false
+								return nil
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			Name:        "history",
+			Description: "outputs the last commands",
+			action: func(_ context.Context, _ Command, _ string) error {
+				outputCommandHistory()
+				return nil
+			},
+		},
+		{
+			Name:        "clear",
+			Description: "clear all outputs",
+			action: func(_ context.Context, _ Command, _ string) error {
+				messageOutputClear()
+				consoleOutputClear()
+				commandOutputClear()
+				return nil
+			},
+			subCommands: []Command{
+				{
+					Name:        "message",
+					Description: "clears message output",
+					action: func(_ context.Context, _ Command, _ string) error {
+						messageOutputClear()
+						return nil
+					},
+				},
+				{
+					Name:        "console",
+					Description: "clears console output",
+					action: func(_ context.Context, _ Command, _ string) error {
+						consoleOutputClear()
+						return nil
+					},
+				},
+				{
+					Name:        "command",
+					Description: "clears command output",
+					action: func(_ context.Context, _ Command, _ string) error {
+						commandOutputClear()
+						return nil
+					},
+				},
+			},
+		},
+		{
+			Name:        "abort",
+			Description: "abort currently running jobs",
+			action: func(_ context.Context, _ Command, _ string) error {
+				for _, cancelFunc := range cancelFunctions {
+					cancelFunc()
+				}
+				return nil
+			},
+		},
+	},
+}
+
+func init() {
+	// Because of the cycle we need to define the help command here as it needs access to the to command
+	rootCommand.subCommands = append(rootCommand.subCommands, Command{
+		Name:        "help",
+		Description: "prints out this help",
+		action: func(_ context.Context, _ Command, _ string) error {
+			_, _ = fmt.Fprintf(commandOutput, "[#0000ff]Available commands[white]\n")
+			rootCommand.visit(0, func(currentIndent int, command Command) {
+				indentString := strings.Repeat("  ", currentIndent)
+				description := command.Description
+				if description == "" {
+					description = command.Name + "s"
+				}
+				_, _ = fmt.Fprintf(commandOutput, "%s [#00ff00]%s[white]: %s\n", indentString, command.Name, description)
+			})
+			return nil
+		},
+	})
+}
+
+var NotDirectlyExecutable = errors.New("Not directly executable")
+
+type Command struct {
+	Name                 string
+	Description          string
+	action               func(ctx context.Context, currentCommand Command, argument string) error
+	subCommands          []Command
+	parameterSuggestions func(currentText string) (entries []string)
+}
+
+func (c Command) Completions(currentCommandText string) (entries []string) {
+	if c.Name == rootCommandIndicator && len(currentCommandText) == 0 {
+		// We don't return anything here to not pollute the command text by default
+		return
+	}
+	if c.acceptsCurrentText(currentCommandText) {
+		currentCommandPrefix := c.currentCommandPrefix()
+		doesCommandTextTargetSubCommand := c.doesCommandTextTargetSubCommand(currentCommandPrefix)
+		if c.hasDirectExecution() && !doesCommandTextTargetSubCommand {
+			if c.parameterSuggestions != nil {
+				preparedForParameters := c.prepareForParameters(currentCommandText)
+				for _, parameterSuggestion := range c.parameterSuggestions(preparedForParameters) {
+					entries = append(entries, currentCommandPrefix+parameterSuggestion)
+				}
+			} else if currentCommandText == "" {
+				entries = append(entries, c.Name)
+			}
+		}
+		if doesCommandTextTargetSubCommand {
+			remainder := c.prepareForSubCommand(currentCommandText)
+			for _, command := range c.subCommands {
+				for _, subCommandCompletions := range command.Completions(remainder) {
+					entries = append(entries, currentCommandPrefix+subCommandCompletions)
+				}
+			}
+		}
+	} else if strings.HasPrefix(c.Name, currentCommandText) {
+		// Suggest ourselves if we start with the current letter
+		entries = append(entries, c.Name)
+	}
+	return
+}
+
+func (c Command) acceptsCurrentText(currentCommandText string) bool {
+	if c.Name == rootCommandIndicator {
+		return true
+	}
+	hasThePrefix := strings.HasPrefix(currentCommandText, c.Name)
+	hasNoMatchingAlternative := !strings.HasPrefix(currentCommandText, c.Name+"-")
+	accepts := hasThePrefix && hasNoMatchingAlternative
+	plc4xpcapanalyzerLog.Debug().
+		Stringer("c", c).
+		Bool("accepts", accepts).
+		Msg("c accepts accepts")
+	return accepts
+}
+
+func (c Command) doesCommandTextTargetSubCommand(currentCommandText string) bool {
+	if c.Name == rootCommandIndicator {
+		return true
+	}
+	if len(c.subCommands) == 0 {
+		return false
+	}
+	return strings.HasPrefix(currentCommandText, c.currentCommandPrefix())
+}
+
+func (c Command) prepareForParameters(currentCommandText string) string {
+	if currentCommandText == c.Name {
+		return ""
+	}
+	return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix())
+}
+func (c Command) prepareForSubCommand(currentCommandText string) string {
+	return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix())
+}
+
+func (c Command) currentCommandPrefix() string {
+	if c.Name == rootCommandIndicator {
+		return ""
+	}
+	return c.Name + " "
+}
+
+func (c Command) hasDirectExecution() bool {
+	return c.action != nil
+}
+
+func Execute(ctx context.Context, commandText string) error {
+	err := rootCommand.Execute(ctx, commandText)
+	if err == nil {
+		addCommandHistoryEntry(commandText)
+	}
+	return err
+}
+
+func (c Command) Execute(ctx context.Context, commandText string) (err error) {
+	defer func() {
+		if recoveredErr := recover(); recoveredErr != nil {
+			if log.Debug().Enabled() {
+				log.Error().
+					Str("stack", string(debug.Stack())).
+					Interface("err", err).
+					Msg("panic-ed")
+			}
+			err = errors.Errorf("panic occurred: %v.", recoveredErr)
+		}
+	}()
+	plc4xpcapanalyzerLog.Debug().
+		Stringer("c", c).Str("commandText", commandText).
+		Msg("c executes commandText")
+	if !c.acceptsCurrentText(commandText) {
+		return errors.Errorf("%s doesn't understand %s", c.Name, commandText)
+	}
+	if c.doesCommandTextTargetSubCommand(commandText) {
+		prepareForSubCommandForSubCommand := c.prepareForSubCommand(commandText)
+		for _, command := range c.subCommands {
+			if command.acceptsCurrentText(prepareForSubCommandForSubCommand) {
+				plc4xpcapanalyzerLog.Debug().
+					Stringer("c", c).
+					Str("commandText", commandText).
+					Msg("c delegates to sub command")
+				return command.Execute(ctx, prepareForSubCommandForSubCommand)
+			}
+		}
+		return errors.Errorf("%s not accepted by any subcommands of %s", commandText, c.Name)
+	} else {
+		if c.action == nil {
+			return NotDirectlyExecutable
+		}
+		plc4xpcapanalyzerLog.Debug().
+			Stringer("c", c).
+			Str("commandText", commandText).
+			Msg("c executes commandText directly")
+		preparedForParameters := c.prepareForParameters(commandText)
+		return c.action(ctx, c, preparedForParameters)
+	}
+}
+
+func (c Command) visit(i int, f func(currentIndent int, command Command)) {
+	f(i, c)
+	for _, subCommand := range c.subCommands {
+		subCommand.visit(i+1, f)
+	}
+}
+
+func (c Command) String() string {
+	return c.Name
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/common.go b/plc4go/tools/plc4xpcapanalyzer/ui/common.go
new file mode 100644
index 0000000..58e170b
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/common.go
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"context"
+	"io"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+	"github.com/apache/plc4x/plc4go/spi"
+
+	"github.com/rs/zerolog"
+)
+
+const protocols = "ads,bacnetip,c-bus,s7"
+
+var protocolList = strings.Split(protocols, ",")
+
+var plc4xpcapanalyzerLog = zerolog.Nop()
+
+var driverManager plc4go.PlcDriverManager
+var driverAdded func(string)
+
+type loadedPcapFile struct {
+	name string
+	path string
+}
+
+var loadedPcapFiles []loadedPcapFile
+var loadedPcapFilesChanged func()
+
+var messageReceived func(messageNumber int, receiveTime time.Time, message apiModel.PlcMessage)
+var numberOfMessagesReceived int
+var spiMessageReceived func(messageNumber int, receiveTime time.Time, message spi.Message)
+var spiNumberOfMessagesReceived int
+var messageOutput io.Writer
+var messageOutputClear func()
+
+var consoleOutput io.Writer
+var consoleOutputClear func()
+
+var commandsExecuted int
+var commandOutput io.Writer
+var commandOutputClear func()
+
+var currentDirChanged func()
+var currentDir = func() string {
+	dir, _ := os.Getwd()
+	return dir
+}()
+
+var rootContext = context.Background()
+var cancelFunctions = make(map[uint32]context.CancelFunc)
+
+var shutdownMutex sync.Mutex
+var hasShutdown bool
+
+func Shutdown() {
+	shutdownMutex.Lock()
+	defer shutdownMutex.Unlock()
+	if hasShutdown {
+		return
+	}
+	hasShutdown = true
+	saveConfig()
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/config.go b/plc4go/tools/plc4xpcapanalyzer/ui/config.go
new file mode 100644
index 0000000..dc96f61
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/config.go
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	cliConfig "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"gopkg.in/yaml.v3"
+	"os"
+	"path"
+	"time"
+)
+
+var plc4xpcapanalyzerConfigDir string
+var configFile string
+var config = Config{
+	MaxConsoleLines: 500,
+	MaxOutputLines:  500,
+	CliConfigs:      allCliConfigsInstances,
+}
+
+type Config struct {
+	HostIp  string `yaml:"host_ip"`
+	History struct {
+		Last10Files    []string `yaml:"last_hosts"`
+		Last10Commands []string `yaml:"last_commands"`
+	}
+	AutoRegisterDrivers []string      `yaml:"auto_register_driver"`
+	LastUpdated         time.Time     `yaml:"last_updated"`
+	LogLevel            string        `yaml:"log_level"`
+	MaxConsoleLines     int           `yaml:"max_console_lines"`
+	MaxOutputLines      int           `yaml:"max_output_lines"`
+	CliConfigs          allCliConfigs `yaml:"cli_configs"`
+}
+
+type allCliConfigs struct {
+	RootConfig    *cliConfig.RootConfig
+	AnalyzeConfig *cliConfig.AnalyzeConfig
+	ExtractConfig *cliConfig.ExtractConfig
+	BacnetConfig  *cliConfig.BacnetConfig
+	CBusConfig    *cliConfig.CBusConfig
+	PcapConfig    *cliConfig.PcapConfig
+}
+
+var allCliConfigsInstances = allCliConfigs{
+	&cliConfig.RootConfigInstance,
+	&cliConfig.AnalyzeConfigInstance,
+	&cliConfig.ExtractConfigInstance,
+	&cliConfig.BacnetConfigInstance,
+	&cliConfig.CBusConfigInstance,
+	&cliConfig.PcapConfigInstance,
+}
+
+func init() {
+	userConfigDir, err := os.UserConfigDir()
+	if err != nil {
+		panic(err)
+	}
+	plc4xpcapanalyzerConfigDir = path.Join(userConfigDir, "plc4xpcapanalyzer")
+	if _, err := os.Stat(plc4xpcapanalyzerConfigDir); os.IsNotExist(err) {
+		err := os.Mkdir(plc4xpcapanalyzerConfigDir, os.ModeDir|os.ModePerm)
+		if err != nil {
+			panic(err)
+		}
+	}
+	configFile = path.Join(plc4xpcapanalyzerConfigDir, "config.yml")
+}
+
+func LoadConfig() {
+	f, err := os.Open(configFile)
+	if err != nil {
+		log.Info().Err(err).Msg("No config file found")
+		return
+	}
+	defer func(f *os.File) {
+		err := f.Close()
+		if err != nil {
+			log.Error().Err(err).Msg("Error closing config file")
+		}
+	}(f)
+
+	decoder := yaml.NewDecoder(f)
+	if err = decoder.Decode(&config); err != nil {
+		log.Warn().Err(err).Msg("Can't decode config file")
+		return
+	}
+}
+
+func saveConfig() {
+	config.LastUpdated = time.Now()
+	f, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0755)
+	if err != nil {
+		log.Warn().Err(err).Msg("Can't save config file")
+		return
+	}
+	encoder := yaml.NewEncoder(f)
+	defer func(encoder *yaml.Encoder) {
+		err := encoder.Close()
+		if err != nil {
+			log.Error().Err(err).Msg("Error closing config file")
+		}
+	}(encoder)
+	if err := encoder.Encode(config); err != nil {
+		log.Warn().Err(err).Msg("Can't encode config file")
+		panic(err)
+	}
+}
+
+func addRecentFilesEntry(pcapFile string) {
+	existingIndex := -1
+	for i, lastPcapFile := range config.History.Last10Files {
+		if lastPcapFile == pcapFile {
+			existingIndex = i
+			break
+		}
+	}
+	if existingIndex >= 0 {
+		config.History.Last10Files = append(config.History.Last10Files[:existingIndex], config.History.Last10Files[existingIndex+1:]...)
+	}
+	if len(config.History.Last10Files) >= 10 {
+		config.History.Last10Files = config.History.Last10Files[1:]
+	}
+	config.History.Last10Files = append(config.History.Last10Files, pcapFile)
+}
+
+func addCommandHistoryEntry(command string) {
+	switch command {
+	case "clear":
+		return
+	case "history":
+		return
+	}
+	existingIndex := -1
+	for i, lastCommand := range config.History.Last10Commands {
+		if lastCommand == command {
+			existingIndex = i
+			break
+		}
+	}
+	if existingIndex >= 0 {
+		config.History.Last10Commands = append(config.History.Last10Commands[:existingIndex], config.History.Last10Commands[existingIndex+1:]...)
+	}
+	if len(config.History.Last10Commands) >= 10 {
+		config.History.Last10Commands = config.History.Last10Commands[1:]
+	}
+	config.History.Last10Commands = append(config.History.Last10Commands, command)
+}
+
+func setLevel(level zerolog.Level) {
+	config.LogLevel = level.String()
+}
+
+func enableAutoRegister(driver string) error {
+	if err := validateDriverParam(driver); err != nil {
+		return err
+	}
+	for _, autoRegisterDriver := range config.AutoRegisterDrivers {
+		if autoRegisterDriver == driver {
+			return errors.Errorf("%s already registered for auto register", driver)
+		}
+	}
+	config.AutoRegisterDrivers = append(config.AutoRegisterDrivers, driver)
+	log.Info().Str("driver", driver).Msg("Auto register enabled")
+	return nil
+}
+
+func disableAutoRegister(driver string) error {
+	if err := validateDriverParam(driver); err != nil {
+		return err
+	}
+	index := -1
+	for i, autoRegisterDriver := range config.AutoRegisterDrivers {
+		if autoRegisterDriver == driver {
+			index = i
+			break
+		}
+	}
+	if index < 0 {
+		return errors.Errorf("%s not registered for auto register", driver)
+	}
+	config.AutoRegisterDrivers = append(config.AutoRegisterDrivers[:index], config.AutoRegisterDrivers[index+1:]...)
+	log.Info().Str("driver", driver).Msg("Auto register disabled")
+	return nil
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/ui.go b/plc4go/tools/plc4xpcapanalyzer/ui/ui.go
new file mode 100644
index 0000000..e5f8843
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/ui.go
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package ui
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+	"regexp"
+	"strconv"
+	"time"
+
+	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+	"github.com/apache/plc4x/plc4go/spi"
+
+	"github.com/gdamore/tcell/v2"
+	"github.com/pkg/errors"
+	"github.com/rivo/tview"
+)
+
+func SetupApplication() *tview.Application {
+	application := tview.NewApplication()
+
+	newPrimitive := func(text string) tview.Primitive {
+		return tview.NewTextView().
+			SetTextAlign(tview.AlignCenter).
+			SetText(text)
+	}
+	fileArea := buildFileArea(newPrimitive, application)
+	outputArea := buildOutputArea(newPrimitive, application)
+	commandArea := buildCommandArea(newPrimitive, application)
+
+	grid := tview.NewGrid().
+		SetRows(1, 0, 1).
+		SetColumns(30, 0, 30).
+		SetBorders(true).
+		AddItem(newPrimitive("PLC4X PCAP Analyzer"), 0, 0, 1, 3, 0, 0, false).
+		AddItem(newPrimitive("https://github.com/apache/plc4x"), 2, 0, 1, 3, 0, 0, false)
+
+	// Layout for screens narrower than 100 cells (fileArea and side bar are hidden).
+	grid.AddItem(fileArea, 0, 0, 0, 0, 0, 0, false).
+		AddItem(outputArea, 1, 0, 1, 3, 0, 0, false).
+		AddItem(commandArea, 0, 0, 0, 0, 0, 0, true)
+
+	// Layout for screens wider than 100 cells.
+	grid.AddItem(fileArea, 1, 0, 1, 1, 0, 100, false).
+		AddItem(outputArea, 1, 1, 1, 1, 0, 100, false).
+		AddItem(commandArea, 1, 2, 1, 1, 0, 100, false)
+
+	application.SetRoot(grid, true).EnableMouse(true)
+
+	return application
+}
+
+func buildFileArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
+	connectionAreaHeader := newPrimitive("Files")
+	connectionArea := tview.NewGrid().
+		SetRows(3, 0, 10).
+		SetColumns(0).
+		AddItem(connectionAreaHeader, 0, 0, 1, 1, 0, 0, false)
+	{
+		fileList := tview.NewList()
+		loadedPcapFilesChanged = func() {
+			application.QueueUpdateDraw(func() {
+				fileList.Clear()
+				for _, pcapFile := range loadedPcapFiles {
+					fileList.AddItem(pcapFile.name, pcapFile.path, 0x0, func() {
+						//TODO: disconnect popup
+						_ = pcapFile
+					})
+				}
+			})
+		}
+		connectionArea.AddItem(fileList, 1, 0, 1, 1, 0, 0, false)
+		{
+			registeredDriverAreaHeader := newPrimitive("Registered drivers")
+			registeredDriverArea := tview.NewGrid().
+				SetRows(3, 0).
+				SetColumns(0).
+				AddItem(registeredDriverAreaHeader, 0, 0, 1, 1, 0, 0, false)
+			{
+				driverList := tview.NewList()
+				driverAdded = func(driver string) {
+					application.QueueUpdateDraw(func() {
+						driverList.AddItem(driver, "", 0x0, func() {
+							//TODO: disconnect popup
+						})
+					})
+				}
+				registeredDriverArea.AddItem(driverList, 1, 0, 1, 1, 0, 0, false)
+			}
+			connectionArea.AddItem(registeredDriverArea, 2, 0, 1, 1, 0, 0, false)
+		}
+
+	}
+	return connectionArea
+}
+
+func buildCommandArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
+	commandAreaHeader := newPrimitive("Commands")
+	commandArea := tview.NewGrid().
+		SetRows(3, 0, 3).
+		SetColumns(0).
+		AddItem(commandAreaHeader, 0, 0, 1, 1, 0, 0, false)
+	{
+		enteredCommandsView := tview.NewTextView().
+			SetDynamicColors(true).
+			SetRegions(true).
+			SetWordWrap(true).
+			SetChangedFunc(func() {
+				application.Draw()
+			})
+		commandOutput = enteredCommandsView
+		commandOutputClear = func() {
+			enteredCommandsView.SetText("")
+		}
+
+		commandArea.AddItem(enteredCommandsView, 1, 0, 1, 1, 0, 0, false)
+
+		commandInputField := tview.NewInputField().
+			SetLabel("$").
+			SetFieldWidth(30)
+		application.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+			switch event.Key() {
+			case tcell.KeyCtrlC:
+				commandInputField.SetText("")
+				application.SetFocus(commandInputField)
+				return nil
+			case tcell.KeyCtrlD:
+				// TODO: maybe add a modal here
+				application.Stop()
+				return nil
+			}
+			return event
+		})
+		commandInputField.
+			SetDoneFunc(func(key tcell.Key) {
+				commandText := commandInputField.GetText()
+				if commandText == "quit" {
+					// TODO: maybe add a modal here
+					application.Stop()
+					return
+				}
+				commandsExecuted++
+				go func() {
+					commandHistoryShortcut, _ := regexp.Compile("^[0-9]$")
+					if commandHistoryShortcut.MatchString(commandText) {
+						atoi, _ := strconv.Atoi(commandHistoryShortcut.FindString(commandText))
+						if atoi < len(config.History.Last10Commands) {
+							commandText = config.History.Last10Commands[atoi]
+						} else {
+							_, _ = fmt.Fprintf(enteredCommandsView, "[#ff0000]%s %s[white]\n", time.Now().Format("04:05"), errors.Errorf("No such elements %d in command history", atoi))
+							return
+						}
+					}
+					_, _ = fmt.Fprintf(enteredCommandsView, "%s [\"%d\"]%s[\"\"]\n", time.Now().Format("04:05"), commandsExecuted, commandText)
+					ctx, cancelFunc := context.WithCancel(rootContext)
+					randomId := rand.Uint32()
+					cancelFunctions[randomId] = cancelFunc
+					defer delete(cancelFunctions, randomId)
+
+					if err := Execute(ctx, commandText); err != nil {
+						_, _ = fmt.Fprintf(enteredCommandsView, "[#ff0000]%s %s[white]\n", time.Now().Format("04:05"), err)
+						return
+					}
+					application.QueueUpdateDraw(func() {
+						commandInputField.SetText("")
+					})
+				}()
+			})
+		commandInputField.SetAutocompleteFunc(rootCommand.Completions)
+
+		enteredCommandsView.SetDoneFunc(func(key tcell.Key) {
+			currentSelection := enteredCommandsView.GetHighlights()
+			if key == tcell.KeyEnter {
+				if len(currentSelection) > 0 {
+					enteredCommandsView.Highlight()
+				} else {
+					enteredCommandsView.Highlight("0").ScrollToHighlight()
+				}
+				if len(currentSelection) == 1 {
+					commandInputField.SetText(enteredCommandsView.GetRegionText(currentSelection[0]))
+					application.SetFocus(commandInputField)
+				}
+			} else if len(currentSelection) > 0 {
+				index, _ := strconv.Atoi(currentSelection[0])
+				if key == tcell.KeyTab {
+					index = (index + 1) % commandsExecuted
+				} else if key == tcell.KeyBacktab {
+					index = (index - 1 + commandsExecuted) % commandsExecuted
+				} else {
+					return
+				}
+				enteredCommandsView.Highlight(strconv.Itoa(index)).ScrollToHighlight()
+			}
+		})
+
+		commandArea.AddItem(commandInputField, 2, 0, 1, 1, 0, 0, true)
+	}
+	return commandArea
+}
+
+func buildOutputArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) *tview.Grid {
+	outputAreaHeader := newPrimitive("Output")
+	outputArea := tview.NewGrid().
+		SetRows(3, 0, 10).
+		SetColumns(0, 30).
+		AddItem(outputAreaHeader, 0, 0, 1, 1, 0, 0, false)
+	{
+		var jumpToMessageItem func(messageNumber int) bool
+		{
+			outputView := tview.NewTextView().
+				SetDynamicColors(true).
+				SetRegions(true).
+				SetWordWrap(false).
+				SetWrap(false).
+				SetChangedFunc(func() {
+					application.Draw()
+				})
+			jumpToMessageItem = func(messageNumber int) bool {
+				regionId := strconv.Itoa(messageNumber)
+				if outputView.GetRegionText(regionId) == "" {
+					return false
+				}
+				outputView.Highlight(regionId).ScrollToHighlight()
+				return true
+			}
+			messageOutput = outputView
+			messageOutputClear = func() {
+				outputView.SetText("")
+			}
+
+			outputView.SetDoneFunc(func(key tcell.Key) {
+				currentSelection := outputView.GetHighlights()
+				if key == tcell.KeyEnter {
+					if len(currentSelection) > 0 {
+						outputView.Highlight()
+					} else {
+						outputView.Highlight("0").ScrollToHighlight()
+					}
+				} else if len(currentSelection) > 0 {
+					index, _ := strconv.Atoi(currentSelection[0])
+					if key == tcell.KeyTab {
+						index = (index + 1) % numberOfMessagesReceived
+					} else if key == tcell.KeyBacktab {
+						index = (index - 1 + numberOfMessagesReceived) % numberOfMessagesReceived
+					} else {
+						return
+					}
+					outputView.Highlight(strconv.Itoa(index)).ScrollToHighlight()
+				}
+			})
+			outputView.SetBorder(false)
+			outputArea.AddItem(outputView, 1, 0, 1, 1, 0, 0, false)
+		}
+
+		{
+			consoleView := tview.NewTextView().
+				SetDynamicColors(true).
+				SetMaxLines(config.MaxConsoleLines).
+				SetChangedFunc(func() {
+					application.Draw()
+				})
+			consoleOutput = consoleView
+			consoleOutputClear = func() {
+				consoleView.SetText("")
+			}
+
+			consoleView.SetBorder(false)
+			outputArea.AddItem(consoleView, 2, 0, 1, 1, 0, 0, false)
+		}
+
+		{
+			receivedMessagesList := tview.NewList()
+			messageReceived = func(messageNumber int, receiveTime time.Time, message apiModel.PlcMessage) {
+				application.QueueUpdateDraw(func() {
+					receivedMessagesList.AddItem(fmt.Sprintf("No %d @%s (api)", messageNumber, receiveTime.Format("15:04:05.999999")), "", 0x0, func() {
+						if ok := jumpToMessageItem(messageNumber); !ok {
+							plc4xpcapanalyzerLog.Debug().Msg("Adding new message to console output")
+							_, _ = fmt.Fprintf(messageOutput, "Message nr: %d\n[\"%d\"]%s[\"\"]\n", messageNumber, messageNumber, message)
+							jumpToMessageItem(messageNumber)
+						}
+					})
+				})
+			}
+			spiMessageReceived = func(messageNumber int, receiveTime time.Time, message spi.Message) {
+				application.QueueUpdateDraw(func() {
+					receivedMessagesList.AddItem(fmt.Sprintf("No %d @%s (spi)", messageNumber, receiveTime.Format("15:04:05.999999")), "", 0x0, func() {
+						if ok := jumpToMessageItem(messageNumber); !ok {
+							plc4xpcapanalyzerLog.Debug().Msg("Adding new spi message to console output")
+							_, _ = fmt.Fprintf(messageOutput, "Message nr: %d\n[\"%d\"]%s[\"\"]\n", messageNumber, messageNumber, message)
+							jumpToMessageItem(messageNumber)
+						}
+					})
+				})
+			}
+			outputArea.AddItem(receivedMessagesList, 0, 1, 3, 1, 0, 0, false)
+		}
+	}
+	return outputArea
+}
diff --git a/plc4j/examples/hello-ads-telemetry/pom.xml b/plc4j/examples/hello-ads-telemetry/pom.xml
new file mode 100644
index 0000000..661eff0
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/pom.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.plc4x.examples</groupId>
+    <artifactId>plc4j-examples</artifactId>
+    <version>0.13.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>plc4j-examples-hello-ads-telemetry</artifactId>
+  <name>PLC4J: Examples: Hello-ADS telemetry</name>
+  <description>Hello world application using PLC4X to read ADS device telemetry.</description>
+
+  <properties>
+    <project.build.outputTimestamp>2024-02-16T14:53:02Z</project.build.outputTimestamp>
+    <app.main.class>org.apache.plc4x.java.examples.helloads.telemetry.HelloAdsTelemetry</app.main.class>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.plc4x</groupId>
+      <artifactId>plc4j-api</artifactId>
+      <version>0.13.0-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>log4j-over-slf4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <configuration>
+          <usedDependencies combine.children="append">
+            <usedDependency>org.slf4j:log4j-over-slf4j</usedDependency>
+          </usedDependencies>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
\ No newline at end of file
diff --git a/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/DeviceManagerConstants.java b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/DeviceManagerConstants.java
new file mode 100644
index 0000000..e463b2e
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/DeviceManagerConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.helloads.telemetry;
+
+// Extracted from https://infosys.beckhoff.com/index.php?content=../content/1031/devicemanager/262982923.html
+
+public enum DeviceManagerConstants {
+    NIC(0x00000002),
+    Time(0x00000003),
+    UserManagement(0x00000004),
+    RAS(0x00000005),
+    FTP(0x00000006),
+    SMB(0x00000007),
+    TwinCat(0x00000008),
+    Software(0x0000000A),
+    CPU(0x0000000B),
+    Memory(0x0000000C),
+    FirewallWinCE(0x0000000E),
+    FileSystemObject(0x00000010),
+    DisplayDevice(0x00000013),
+    EWF(0x00000014),
+    FBWF(0x00000015),
+    OS(0x00000018),
+    RAID(0x00000019),
+    Fan(0x0000001B),
+    Mainboard(0x0000001C),
+    DiskManagement(0x0000001D),
+    UPS(0x0000001E),
+    PhysicalDrive(0x0000001F),
+    MassStorageDrive(0x00000020),
+    UnifiedWriteFilter(0x00000021),
+    IO(0x00000022),
+    Misc(0x00000100);
+
+    final int typeNumber;
+    DeviceManagerConstants(int typeNumber) {
+        this.typeNumber = typeNumber;
+    }
+}
diff --git a/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/EtherCatDevice.java b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/EtherCatDevice.java
new file mode 100644
index 0000000..094a0ff
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/EtherCatDevice.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.helloads.telemetry;
+
+public class EtherCatDevice {
+
+    private final int etherCatAddress;
+    private final String name;
+    private final int vendorId;
+    private final int productCode;
+    private final int revisionNumber;
+
+    public EtherCatDevice(int etherCatAddress, String name, int vendorId, int productCode, int revisionNumber) {
+        this.etherCatAddress = etherCatAddress;
+        this.name = name;
+        this.vendorId = vendorId;
+        this.productCode = productCode;
+        this.revisionNumber = revisionNumber;
+    }
+
+    public int getEtherCatAddress() {
+        return etherCatAddress;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getVendorId() {
+        return vendorId;
+    }
+
+    public int getProductCode() {
+        return productCode;
+    }
+
+    public int getRevisionNumber() {
+        return revisionNumber;
+    }
+}
diff --git a/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/EtherCatMasterConstants.java b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/EtherCatMasterConstants.java
new file mode 100644
index 0000000..174070b
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/EtherCatMasterConstants.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.helloads.telemetry;
+
+// Extracted from https://infosys.beckhoff.com/index.php?content=../content/1031/devicemanager/262982923.html
+
+import org.apache.plc4x.java.api.types.PlcValueType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum EtherCatMasterConstants {
+    DeviceType(0x10000000, PlcValueType.UDINT),
+    ManufacturerDeviceName(0x10080000, PlcValueType.STRING),
+    HardwareVersion(0x10090000, PlcValueType.UINT),
+    SoftwareVersion(0x100A0000, PlcValueType.STRING),
+    IdentityObjectNum(0x10180000, PlcValueType.USINT),
+    IdentityObjectVendorId(0x10180001, PlcValueType.UDINT),
+    IdentityObjectProductCode(0x10180002, PlcValueType.UDINT),
+    IdentityObjectRevisionNumber(0x10180003, PlcValueType.UDINT),
+    IdentityObjectSerialNumber(0x10180004, PlcValueType.UDINT),
+    ConfigurationDataNum(0x80000000, PlcValueType.USINT), // 0x8xxx0000 xxx = device index (starting with 0)
+    ConfigurationDataAddress(0x80000001, PlcValueType.UINT),
+    ConfigurationDataType(0x80000002, PlcValueType.STRING), // Not sure ...
+    ConfigurationDataName(0x80000003, PlcValueType.STRING),
+    ConfigurationDataDeviceType(0x80000004, PlcValueType.UDINT),
+    ConfigurationDataVendorId(0x80000005, PlcValueType.UDINT),
+    ConfigurationDataProductCode(0x80000006, PlcValueType.UDINT),
+    ConfigurationDataRevisionNumber(0x80000007, PlcValueType.UDINT),
+    ConfigurationDataSerialNumber(0x80000008, PlcValueType.UDINT),
+    ConfigurationDataMailboxOutSize(0x80000021, PlcValueType.UINT),
+    ConfigurationDataMailboxInSize(0x80000022, PlcValueType.UINT),
+    ConfigurationDataLinkStatus(0x80000023, PlcValueType.USINT),
+    ConfigurationDataLinkPreset(0x80000024, PlcValueType.USINT),
+    ConfigurationDataFlags(0x80000025, PlcValueType.UINT),
+    StateMachineNum(0xA0000000, PlcValueType.USINT),      // 0xAxxx0000 xxx = device index (starting with 0)
+    StateMachineAlStatus(0xA0000001, PlcValueType.UINT),
+    StateMachineAlControl(0xA0000002, PlcValueType.UINT),
+    StateMachineLastAlStatusCode(0xA0000003, PlcValueType.UINT),
+    StateMachineLinkConnectionStatus(0xA0000004, PlcValueType.USINT),
+    StateMachineLinkControl(0xA0000005, PlcValueType.USINT),
+    StateMachineFixedAddressPort0(0xA0000006, PlcValueType.UINT),
+    StateMachineFixedAddressPort1(0xA0000007, PlcValueType.UINT),
+    StateMachineFixedAddressPort2(0xA0000008, PlcValueType.UINT),
+    StateMachineFixedAddressPort3(0xA0000009, PlcValueType.UINT),
+    StateMachineCrcErrorCountPort0(0xA000000A, PlcValueType.UDINT),
+    StateMachineCrcErrorCountPort1(0xA000000B, PlcValueType.UDINT),
+    StateMachineCrcErrorCountPort2(0xA000000C, PlcValueType.UDINT),
+    StateMachineCrcErrorCountPort3(0xA000000D, PlcValueType.UDINT),
+    StateMachineCyclicWcErrorCount(0xA000000E, PlcValueType.UDINT),
+    StateMachineSlaveNotPresentCount(0xA000000F, PlcValueType.UDINT),
+    StateMachineAbnormalStateChangeCount(0xA0000010, PlcValueType.UDINT),
+    //StateMachineDisableAutomaticLinkControl(0xA0000011, PlcValueType.BOOL),
+    //ScanSlavesNum(0xF002000, PlcValueType.),
+    //ScanSlavesScanCommand(0xF002001, PlcValueType.),
+    ScanSlavesScanStatus(0xF002002, PlcValueType.USINT),
+    //ScanSlavesScanReply(0xF002003, PlcValueType.),
+    ConfiguredSlavesNum(0xF0200000, PlcValueType.USINT), // Get the EtherCAT address by looping through the modules
+    //    FrameStatisticsNum(0xF1200000, PlcValueType.);
+    FrameStatisticsCyclicLostFramesCount(0xF1200001, PlcValueType.UDINT),
+    FrameStatisticsAcyclicLostFramesCount(0xF1200002, PlcValueType.UDINT);
+    //DiagnosticNum(0xF2000000, PlcValueType.),
+    //DiagnosticResetDiagnosticCounters(0xF2000001, PlcValueType.);
+
+    private static final Map<Integer, EtherCatMasterConstants> map;
+    static {
+        map = new HashMap<>();
+        for (EtherCatMasterConstants value : EtherCatMasterConstants.values()) {
+            map.put(value.offset, value);
+        }
+    }
+
+    final int offset;
+    final PlcValueType plcValueType;
+    EtherCatMasterConstants(int offset, PlcValueType plcValueType) {
+        this.offset = offset;
+        this.plcValueType = plcValueType;
+    }
+
+    public static EtherCatMasterConstants enumForValue(int offset) {
+        return map.get(offset);
+    }
+
+}
diff --git a/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/HelloAdsEtherCatTelemetry.java b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/HelloAdsEtherCatTelemetry.java
new file mode 100644
index 0000000..38d8e8a
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/HelloAdsEtherCatTelemetry.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.helloads.telemetry;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.api.types.PlcValueType;
+import org.apache.plc4x.java.api.value.PlcValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+// Extracted from https://infosys.beckhoff.com/index.php?content=../content/1031/devicemanager/262982923.html
+
+public class HelloAdsEtherCatTelemetry {
+
+    private static final Logger logger = LoggerFactory.getLogger(HelloAdsTelemetry.class);
+
+    private static final int AoEGroupIndex = 0x0000F302;
+
+    protected void outputEtherCatData(String remoteIp, String localAmsNetId, String remoteAmdNetId) {
+        Map<Integer, EtherCatDevice> devices = new HashMap<>();
+        // The AmsNetId of the PLC usually is {ip}.1.1 and that of the EtherCAT master is {ip}.3.1
+        // The port number equals the EtherCAT address. For the EtherCAT master, this port is 0xFFFF = 65535
+        try (PlcConnection connection = PlcDriverManager.getDefault().getConnectionManager().getConnection(String.format("ads:tcp://%s?targetAmsNetId=%s&targetAmsPort=65535&sourceAmsNetId=%s&sourceAmsPort=65534&load-symbol-and-data-type-tables=false", remoteIp, remoteAmdNetId, localAmsNetId))) {
+            String manufacturerDeviceName = connection.readRequestBuilder().addTagAddress("manufacturerDeviceName", getAddress(EtherCatMasterConstants.ManufacturerDeviceName)).build().execute().get().getString("manufacturerDeviceName");
+            int hardwareVersion = connection.readRequestBuilder().addTagAddress("hardwareVersion", getAddress(EtherCatMasterConstants.HardwareVersion)).build().execute().get().getInteger("hardwareVersion");
+            String softwareVersion = connection.readRequestBuilder().addTagAddress("softwareVersion", getAddress(EtherCatMasterConstants.SoftwareVersion)).build().execute().get().getString("softwareVersion");
+            logger.info("Found Device: {} Hardware Version {}, Software Version {}", manufacturerDeviceName, hardwareVersion, softwareVersion);
+
+            logger.info("Identity Object:");
+            outputEtherCatSection(connection, EtherCatMasterConstants.IdentityObjectNum.offset, EtherCatMasterConstants.IdentityObjectNum.offset);
+
+            // Load the number of EtherCAT slaves:
+            int numSlaves = connection.readRequestBuilder().addTagAddress("numberOfSlaves", "0x0000F302/0xF0200000:USINT").build().execute().get().getInteger("numberOfSlaves");
+
+            // Load the number of slaves and their etherCatAddresses
+            // NOTE: We need to do this without using multi-item-requests as it seems that this part of the system doesn't support this.
+            for(int i = 0; i < numSlaves; i++) {
+                logger.info("Slave {}", i);
+                int etherCatAddressOffset = EtherCatMasterConstants.ConfiguredSlavesNum.offset | (i + 1);
+                int configDataOffset = EtherCatMasterConstants.ConfigurationDataNum.offset | (i << 16);
+                int stateMachineOffset = EtherCatMasterConstants.StateMachineNum.offset | (i << 16);
+
+                String etherCatAddressAddress = String.format("0x%08X/0x%08X:%s", AoEGroupIndex, etherCatAddressOffset, PlcValueType.UINT.name());
+                int etherCatAddress = connection.readRequestBuilder().addTagAddress("etherCatAddress", etherCatAddressAddress).build().execute().get().getInteger("etherCatAddress");
+                logger.info(" - EtherCat Address: {}", etherCatAddress);
+
+                logger.info(" - Configuration Data:");
+                EtherCatDevice device = outputEtherCatSection(connection, configDataOffset, EtherCatMasterConstants.ConfigurationDataNum.offset);
+                //logger.info(" - State Machine Data");
+                //outputEtherCatSection(connection, stateMachineOffset, EtherCatConstant.StateMachineNum.offset);
+                devices.put(i, device);
+
+                //String manufacturerDeviceName = connection.readRequestBuilder().addTagAddress("lalala", getAddress(EtherCatConstant.ManufacturerDeviceName)).build().execute().get().getString("manufacturerDeviceName");
+
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        logger.info("Loading EtherCAT device information:");
+        for (Map.Entry<Integer, EtherCatDevice> device : devices.entrySet()) {
+            int deviceIndex = device.getKey();
+            EtherCatDevice etherCatDevice = device.getValue();
+            logger.info(" - Connecting with device {} on EtherCAT address {}", deviceIndex, 1001);
+            try (PlcConnection etherCatConnection = PlcDriverManager.getDefault().getConnectionManager().getConnection(String.format("ads:tcp://%s?targetAmsNetId=%s&targetAmsPort=%d&sourceAmsNetId=%s&sourceAmsPort=65534&load-symbol-and-data-type-tables=false", remoteIp, remoteAmdNetId, etherCatDevice.getEtherCatAddress(), localAmsNetId))) {
+                String etherCatAddressAddress = String.format("0x%08X/0x%08X:%s", AoEGroupIndex, 0x60000001, PlcValueType.BOOL.name());
+                PlcReadRequest build = etherCatConnection.readRequestBuilder()
+                    .addTagAddress("Channel 1", etherCatAddressAddress)
+                    .build();
+                PlcReadResponse plcReadResponse = build.execute().get();
+                System.out.println(plcReadResponse);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+    
+    protected EtherCatDevice outputEtherCatSection(PlcConnection connection, int baseOffset, int baseTypeOffset) throws Exception {
+        String sectionNumAddress = String.format("0x%08X/0x%08X:%s", AoEGroupIndex, baseOffset, PlcValueType.USINT.name());
+
+        int etherCatAddress = 0;
+        String deviceName = null;
+        int vendorId = 0;
+        int productCode = 0;
+        int revisionNumber = 0;
+
+        int identityObjectNum = connection.readRequestBuilder().addTagAddress("num", sectionNumAddress).build().execute().get().getInteger("num");
+        for (int i = 1; i < identityObjectNum; i++) {
+            int offset = baseOffset | i;
+            int typeOffset = baseTypeOffset | i;
+            EtherCatMasterConstants etherCatConstantAddress = EtherCatMasterConstants.enumForValue(typeOffset);
+            PlcValueType etherCatConstantType = (etherCatConstantAddress != null) ? etherCatConstantAddress.plcValueType : PlcValueType.USINT;
+            String address = String.format("0x%08X/0x%08X:%s", AoEGroupIndex, offset, etherCatConstantType.name());
+            if(etherCatConstantType == PlcValueType.STRING) {
+                address += "(255)";
+            }
+            try {
+                PlcReadResponse readResponse = connection.readRequestBuilder().addTagAddress("value", address).build().execute().get();
+                if (readResponse.getResponseCode("value") == PlcResponseCode.OK) {
+                    PlcValue value = readResponse.getPlcValue("value");
+                    if (etherCatConstantAddress != null) {
+                        switch (etherCatConstantAddress) {
+                            case ConfigurationDataAddress:
+                                etherCatAddress = value.getInteger();
+                                break;
+                            case ConfigurationDataName:
+                                deviceName = value.getString();
+                                break;
+                            case ConfigurationDataVendorId:
+                                vendorId = value.getInteger();
+                                break;
+                            case ConfigurationDataProductCode:
+                                productCode = value.getInteger();
+                                break;
+                            case ConfigurationDataRevisionNumber:
+                                revisionNumber = value.getInteger();
+                                break;
+                        }
+                        logger.info("    - {}: {}", etherCatConstantAddress.name(), value.toString());
+                    } else {
+                        logger.info("    - Unknown: {}", value.toString());
+                    }
+                }
+            } catch (Exception e) {
+                // Ignore this ...
+            }
+        }
+        return new EtherCatDevice(etherCatAddress, deviceName, vendorId, productCode, revisionNumber);
+    }
+    
+    
+    protected String getAddress(EtherCatMasterConstants variable) {
+        String dataTypeName = variable.plcValueType.name();
+        if (variable.plcValueType == PlcValueType.STRING) {
+            dataTypeName += "(255)";
+        }
+        return String.format("0x%08X/0x%08X:%s", AoEGroupIndex, variable.offset, dataTypeName);
+    }
+
+    public static void main(String[] args) {
+        if(args.length != 3) {
+            logger.error("Usage: HelloAdsTelemetry {remote ip-address} {local-ams-net-id} {remote-ams-net-id}");
+            System.exit(1);
+        }
+
+        String remoteIp = args[0];
+        String localAmsNetId = args[1];
+        String remoteAmsNetId = args[2];
+        new HelloAdsEtherCatTelemetry().outputEtherCatData(remoteIp, localAmsNetId, remoteAmsNetId);
+    }
+    
+}
\ No newline at end of file
diff --git a/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/HelloAdsTelemetry.java b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/HelloAdsTelemetry.java
new file mode 100644
index 0000000..da11b05
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/src/main/java/org/apache/plc4x/java/examples/helloads/telemetry/HelloAdsTelemetry.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.helloads.telemetry;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HelloAdsTelemetry {
+
+    private static final Logger logger = LoggerFactory.getLogger(HelloAdsTelemetry.class);
+    public static void main(String[] args) {
+        if(args.length != 2) {
+            logger.error("Usage: HelloAdsTelemetry {ip-address of PLC} {local ip-address}");
+            System.exit(1);
+        }
+
+        String remoteIp = args[0];
+        String localIp = args[1];
+        try (PlcConnection connection = PlcDriverManager.getDefault().getConnectionManager().getConnection(String.format("ads:tcp://%s?targetAmsNetId=%s.1.1&targetAmsPort=10000&sourceAmsNetId=%s.1.1&sourceAmsPort=65534&load-symbol-and-data-type-tables=false", remoteIp, remoteIp, localIp))) {
+            // Load the number of modules:
+            int numModules = connection.readRequestBuilder().addTagAddress("numberOfModules", "0x0000F302/0xF0200000:UINT").build().execute().get().getInteger("numberOfModules");
+
+            // Load the mdp type and index for each module
+            // NOTE: We need to do this without using multi-item-requests as it seems that this part of the system doesn't support this.
+            Map<Integer, Integer> moduleTypeIdMap = new HashMap<>();
+            for(int i = 1; i < numModules; i++) {
+                String name = "module-" + i;
+                String address = String.format("0x0000F302/0xF020%04X:UDINT", i);
+                PlcReadResponse plcReadResponse = connection.readRequestBuilder().addTagAddress(name, address).build().execute().get();
+                if (plcReadResponse.getResponseCode(name) == PlcResponseCode.OK) {
+                    int value = plcReadResponse.getInteger(name);
+                    int mdpType = ((value & 0xFFFF0000) >> 16);
+                    int mdpId = value & 0x0000FFFF;
+                    logger.info("Module {} has mdp type {} and mdp id {}", i, mdpType, mdpId);
+                    moduleTypeIdMap.put(mdpType, mdpId);
+                }
+            }
+
+            // Read the ADS Version information.
+            if(moduleTypeIdMap.containsKey(DeviceManagerConstants.TwinCat.typeNumber)) {
+                Integer mdpId = moduleTypeIdMap.get(DeviceManagerConstants.TwinCat.typeNumber);
+                int addrAdsTypeMain = (mdpId << 20) | 0x80010001;
+                int addrAdsTypeMinor = (mdpId << 20) | 0x80010002;
+                int addrAdsTypeBuild = (mdpId << 20) | 0x80010003;
+                int twinCatMainVersion = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UINT", addrAdsTypeMain)).build().execute().get().getInteger("value");
+                int twinCatMinorVersion = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UINT", addrAdsTypeMinor)).build().execute().get().getInteger("value");
+                int twinCatBuildVersion = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UINT", addrAdsTypeBuild)).build().execute().get().getInteger("value");
+                logger.info("TwinCat Version: {}.{}.{}", twinCatMainVersion, twinCatMinorVersion, twinCatBuildVersion);
+            }
+            // Read the CPU Frequency and Utilization.
+            if(moduleTypeIdMap.containsKey(DeviceManagerConstants.CPU.typeNumber)) {
+                Integer mdpId = moduleTypeIdMap.get(DeviceManagerConstants.CPU.typeNumber);
+                int addrCpuFrequency = (mdpId << 20) | 0x80010001;
+                int addrCpuUsage = (mdpId << 20) | 0x80010002;
+                int cpuFrequency = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UDINT", addrCpuFrequency)).build().execute().get().getInteger("value");
+                int cpuUsage = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UINT", addrCpuUsage)).build().execute().get().getInteger("value");
+                logger.info("CPU: Frequency: {}MHz Usage: {}%", cpuFrequency, cpuUsage);
+            }
+            // Read the Memory usage.
+            if(moduleTypeIdMap.containsKey(DeviceManagerConstants.Memory.typeNumber)) {
+                Integer mdpId = moduleTypeIdMap.get(DeviceManagerConstants.Memory.typeNumber);
+                int addrMemoryAllocated = (mdpId << 20) | 0x80010001;
+                int addrMemoryAvailable = (mdpId << 20) | 0x80010002;
+                int memoryAllocated = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UDINT", addrMemoryAllocated)).build().execute().get().getInteger("value");
+                int memoryAvailable = connection.readRequestBuilder().addTagAddress("value", String.format("0x0000F302/0x%8X:UDINT", addrMemoryAvailable)).build().execute().get().getInteger("value");
+                logger.info("Memory: Allocated: {}MB, Available: {}MB, Usage {}%", memoryAllocated / (1024 * 1024), memoryAvailable / (1024 * 1024), ((float) 100 / memoryAllocated) * memoryAvailable);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/plc4j/examples/hello-ads-telemetry/src/main/resources/logback.xml b/plc4j/examples/hello-ads-telemetry/src/main/resources/logback.xml
new file mode 100644
index 0000000..16b83b4
--- /dev/null
+++ b/plc4j/examples/hello-ads-telemetry/src/main/resources/logback.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+  -->
+<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
+               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="warn">
+    <appender-ref ref="STDOUT" />
+  </root>
+
+</configuration>
\ No newline at end of file
diff --git a/plc4j/examples/hello-cloud-azure/pom.xml b/plc4j/examples/hello-cloud-azure/pom.xml
new file mode 100644
index 0000000..cba8fed
--- /dev/null
+++ b/plc4j/examples/hello-cloud-azure/pom.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.plc4x.examples</groupId>
+    <artifactId>plc4j-examples</artifactId>
+    <version>0.13.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>plc4j-examples-hello-cloud-azure</artifactId>
+  <name>PLC4J: Examples: Cloud: Azure IoT Hub</name>
+  <description>
+    Implementation of a PLC4X application gathering information from
+    a PLC and making that available in an Azure IoT Hub.
+  </description>
+
+  <properties>
+    <project.build.outputTimestamp>2024-02-16T14:53:02Z</project.build.outputTimestamp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.plc4x</groupId>
+      <artifactId>plc4j-api</artifactId>
+      <version>0.13.0-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.microsoft.azure.sdk.iot</groupId>
+      <artifactId>iot-device-client</artifactId>
+      <version>2.4.1</version>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/plc4j/examples/hello-cloud-azure/src/main/java/org/apache/plc4x/java/examples/cloud/azure/CliOptions.java b/plc4j/examples/hello-cloud-azure/src/main/java/org/apache/plc4x/java/examples/cloud/azure/CliOptions.java
new file mode 100644
index 0000000..ed60e91
--- /dev/null
+++ b/plc4j/examples/hello-cloud-azure/src/main/java/org/apache/plc4x/java/examples/cloud/azure/CliOptions.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.cloud.azure;
+
+import org.apache.commons.cli.*;
+
+public class CliOptions {
+
+    private static Options options;
+
+    private final String plc4xConnectionString;
+    private final String plc4xTagAddress;
+    private final String iotHubConnectionString;
+
+    public static CliOptions fromArgs(String[] args) {
+        options = new Options();
+        // Required arguments
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("plc4x-connection-string")
+                .hasArg()
+                .desc("Connection String")
+                .required()
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("plc4x-tag-address")
+                .hasArg()
+                .desc("Tag Address.")
+                .required()
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("iot-hub-connection-string")
+                .hasArg()
+                .desc("IoT Hub Connection String.")
+                .required()
+                .build());
+
+        CommandLineParser parser = new DefaultParser();
+        CommandLine commandLine;
+        try {
+            commandLine = parser.parse(options, args);
+
+            String plc4xConnectionString = commandLine.getOptionValue("plc4x-connection-string");
+            String plc4xTagAddress = commandLine.getOptionValue("plc4x-tag-address");
+            String iotHubConnectionString = commandLine.getOptionValue("iot-hub-connection-string");
+
+            return new CliOptions(plc4xConnectionString, plc4xTagAddress, iotHubConnectionString);
+        } catch (ParseException e) {
+            System.err.println(e.getMessage());
+            return null;
+        }
+    }
+
+    public static void printHelp() {
+        HelpFormatter formatter = new HelpFormatter();
+        formatter.printHelp("S7PlcToAzureIoTHubSample", options);
+    }
+
+    public CliOptions(String plc4xConnectionString, String plc4xTagAddress, String iotHubConnectionString) {
+        this.plc4xConnectionString = plc4xConnectionString;
+        this.plc4xTagAddress = plc4xTagAddress;
+        this.iotHubConnectionString = iotHubConnectionString;
+    }
+
+    public String getPlc4xConnectionString() {
+        return plc4xConnectionString;
+    }
+
+    public String getPlc4xTagAddress() {
+        return plc4xTagAddress;
+    }
+
+    protected String getIotHubConnectionString() {
+        return iotHubConnectionString;
+    }
+
+}
diff --git a/plc4j/examples/hello-cloud-azure/src/main/java/org/apache/plc4x/java/examples/cloud/azure/S7PlcToAzureIoTHubSample.java b/plc4j/examples/hello-cloud-azure/src/main/java/org/apache/plc4x/java/examples/cloud/azure/S7PlcToAzureIoTHubSample.java
new file mode 100644
index 0000000..1c97a84
--- /dev/null
+++ b/plc4j/examples/hello-cloud-azure/src/main/java/org/apache/plc4x/java/examples/cloud/azure/S7PlcToAzureIoTHubSample.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.cloud.azure;
+
+import com.microsoft.azure.sdk.iot.device.DeviceClient;
+import com.microsoft.azure.sdk.iot.device.IotHubClientProtocol;
+import com.microsoft.azure.sdk.iot.device.Message;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+public class S7PlcToAzureIoTHubSample {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(S7PlcToAzureIoTHubSample.class);
+
+    private static final String TAG_NAME = "value";
+
+    /**
+     * Example code do demonstrate sending events from an S7 device to Microsoft Azure IoT Hub
+     *
+     * @param args Expected: [plc4x connection string, plc4x tag address, IoT-Hub connection string].
+     */
+    public static void main(String[] args) throws Exception {
+        CliOptions options = CliOptions.fromArgs(args);
+        if (options == null) {
+            CliOptions.printHelp();
+            // Could not parse.
+            System.exit(1);
+        }
+
+        LOGGER.info("Connecting {}, {}, {}", options.getPlc4xConnectionString(), options.getPlc4xTagAddress(),
+            options.getIotHubConnectionString());
+
+        // Open both a connection to the remote PLC and the cloud service.
+        DeviceClient client = new DeviceClient(options.getIotHubConnectionString(), IotHubClientProtocol.MQTT);
+        try (PlcConnection plcConnection = PlcDriverManager.getDefault().getConnectionManager().getConnection(options.getPlc4xConnectionString())) {
+
+            LOGGER.info("Connected");
+
+            client.open(true);
+
+            // Prepare a read request.
+            PlcReadRequest request = plcConnection.readRequestBuilder()
+                .addTagAddress(TAG_NAME, options.getPlc4xTagAddress()).build();
+
+            while (!Thread.currentThread().isInterrupted()) {
+                // Simulate telemetry.
+                PlcReadResponse response = request.execute().get();
+                response.getAllLongs(TAG_NAME)
+                    .forEach(longValue -> {
+                            String result = Long.toBinaryString(longValue);
+                            LOGGER.info("Outputs {}", result);
+                            Message msg = new Message("{ \"bits\" : \"" + result + "\"}");
+
+                            // Send the message.
+                            client.sendEventAsync(msg,
+                                (sentMessage, clientException, callbackContext) -> {
+                                    if(clientException != null) {
+                                        LOGGER.info("Received exception: ", clientException);
+                                    } else {
+                                        LOGGER.info("Sent successfully");
+                                    }
+                                },
+                                msg);
+                        }
+                    );
+
+                // Wait a second.
+                TimeUnit.SECONDS.sleep(1);
+            }
+        } finally {
+            client.close();
+        }
+    }
+
+}
diff --git a/plc4j/examples/hello-cloud-google/README.md b/plc4j/examples/hello-cloud-google/README.md
new file mode 100644
index 0000000..3e857bc
--- /dev/null
+++ b/plc4j/examples/hello-cloud-google/README.md
@@ -0,0 +1,61 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+  -->
+# Plc to Google IoT Core Adapter
+
+This Adapter opens a MQTT Bridge to Google Cloud IoT Core.
+
+To run this code, you need an account to the google cloud and a project.
+You need to create a device registry and add a device to it, as described here:
+https://cloud.google.com/iot/docs/how-tos/devices
+
+Then, you can run the PlcToGoogleIoTCoreSample to connect tho google and send some values into the cloud.
+Some sample arguments:
+
+    -project-id=myprojectname
+    -registry-id=plc4x-test
+    -cloud-region=europe-west1
+    -device-id=plc4x-test-device
+    -private-key-file=../../../rsa_private_pkcs8
+    -algorithm=RS256
+
+Some documentation can be found here:
+https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/iot/api-client/manager#cloud-iot-core-java-mqtt-example
+This code was adapted from:
+https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/iot/api-client/manager#cloud-iot-core-java-mqtt-example
+
+To retrieve the messages, you can subscribe to Cloud Pub/Sub.
+For example, you could install the google cloud sdk (gcloud) and run:
+
+    gcloud auth login
+    gcloud config set myprojectname
+    gcloud iot devices configs list --project=myprojectname \\
+        --region=europe-west1 \\
+        --registry=myplc4x-test-registry \\
+        --device=myplc4x-test-device \\
+        --limit=5
+    gcloud pubsub subscriptions create --topic plc4x-test-events plc4x-test-subscription
+    gcloud pubsub subscriptions pull --auto-ack plc4x-test-subscription
+
+
+To pull more than one message, use the option --limit [number]
+
+For further reference to the Cloud Pub/Sub, see:
+https://cloud.google.com/pubsub/docs/quickstart-cli
+https://cloud.google.com/sdk/gcloud/reference/alpha/pubsub/subscriptions/pull
+
diff --git a/plc4j/examples/hello-cloud-google/pom.xml b/plc4j/examples/hello-cloud-google/pom.xml
new file mode 100644
index 0000000..8aa05eb
--- /dev/null
+++ b/plc4j/examples/hello-cloud-google/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.plc4x.examples</groupId>
+    <artifactId>plc4j-examples</artifactId>
+    <version>0.13.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>plc4j-examples-hello-cloud-google</artifactId>
+  <name>PLC4J: Examples: Cloud: Google Cloud IoT Core</name>
+  <description>
+    Implementation of a PLC4X application gathering information from
+    a PLC and making that available in an the Google Cloud IoT Core.
+  </description>
+
+  <properties>
+    <project.build.outputTimestamp>2024-02-16T14:53:02Z</project.build.outputTimestamp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.plc4x</groupId>
+      <artifactId>plc4j-api</artifactId>
+      <version>0.13.0-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.paho</groupId>
+      <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+      <version>1.2.5</version>
+    </dependency>
+    <dependency>
+      <groupId>joda-time</groupId>
+      <artifactId>joda-time</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.jsonwebtoken</groupId>
+      <artifactId>jjwt-api</artifactId>
+      <version>0.12.5</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>log4j-over-slf4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <configuration>
+          <usedDependencies combine.children="append">
+            <usedDependency>org.slf4j:log4j-over-slf4j</usedDependency>
+          </usedDependencies>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
\ No newline at end of file
diff --git a/plc4j/examples/hello-cloud-google/src/main/java/org/apache/plc4x/java/examples/cloud/google/CliOptions.java b/plc4j/examples/hello-cloud-google/src/main/java/org/apache/plc4x/java/examples/cloud/google/CliOptions.java
new file mode 100644
index 0000000..d36476e
--- /dev/null
+++ b/plc4j/examples/hello-cloud-google/src/main/java/org/apache/plc4x/java/examples/cloud/google/CliOptions.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.examples.cloud.google;
+
+import org.apache.commons.cli.*;
+
+/** Command line options for the MQTT example. */
+public class CliOptions {
+
+    private static Options options;
+
+    private final String projectId;
+    private final String registryId;
+    private final String deviceId;
+    private final String privateKeyFile;
+    private final String algorithm;
+    private final String cloudRegion;
+    private final int tokenExpMins;
+    private final String mqttBridgeHostname;
+    private final short mqttBridgePort;
+    private final String messageType;
+
+    public static CliOptions fromArgs(String[] args) {
+        options = new Options();
+        // Required arguments
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("project-id")
+                .hasArg()
+                .desc("GCP cloud project name.")
+                .required()
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("registry-id")
+                .hasArg()
+                .desc("Cloud IoT Core registry id.")
+                .required()
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("device-id")
+                .hasArg()
+                .desc("Cloud IoT Core device id.")
+                .required()
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("private-key-file")
+                .hasArg()
+                .desc("Path to private key file.")
+                .required()
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("algorithm")
+                .hasArg()
+                .desc("Encryption algorithm to use to generate the JWT. Either 'RS256' or 'ES256'.")
+                .required()
+                .build());
+
+        // Optional arguments.
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("cloud-region")
+                .hasArg()
+                .desc("GCP cloud region.")
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("mqtt-bridge-hostname")
+                .hasArg()
+                .desc("MQTT bridge hostname.")
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(Number.class)
+                .longOpt("token-exp-minutes")
+                .hasArg()
+                .desc("Minutes to JWT token refresh (token expiration time).")
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(Number.class)
+                .longOpt("mqtt-bridge-port")
+                .hasArg()
+                .desc("MQTT bridge port.")
+                .build());
+        options.addOption(
+            Option.builder()
+                .type(String.class)
+                .longOpt("message-type")
+                .hasArg()
+                .desc("Indicates whether the message is a telemetry event or a device state message")
+                .build());
+
+        CommandLineParser parser = new DefaultParser();
+        CommandLine commandLine;
+        try {
+            commandLine = parser.parse(options, args);
+
+            String projectId = commandLine.getOptionValue("project-id");
+            String registryId = commandLine.getOptionValue("registry-id");
+            String deviceId = commandLine.getOptionValue("device-id");
+            String privateKeyFile = commandLine.getOptionValue("private-key-file");
+            String algorithm = commandLine.getOptionValue("algorithm");
+            String cloudRegion = "europe-west1";
+            if (commandLine.hasOption("cloud-region")) {
+                cloudRegion = commandLine.getOptionValue("cloud-region");
+            }
+            int tokenExpMins = 20;
+            if (commandLine.hasOption("token-exp-minutes")) {
+                tokenExpMins =
+                    ((Number) commandLine.getParsedOptionValue("token-exp-minutes")).intValue();
+            }
+            String mqttBridgeHostname = "mqtt.googleapis.com";
+            if (commandLine.hasOption("mqtt-bridge-hostname")) {
+                mqttBridgeHostname = commandLine.getOptionValue("mqtt-bridge-hostname");
+            }
+            short mqttBridgePort = 8883;
+            if (commandLine.hasOption("mqtt-bridge-port")) {
... 24959 lines suppressed ...