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(¤tPayload, 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 ...