diff --git a/streampipes-extensions/pom.xml b/streampipes-extensions/pom.xml index b14c2471b2..0e89e64f6c 100644 --- a/streampipes-extensions/pom.xml +++ b/streampipes-extensions/pom.xml @@ -63,6 +63,7 @@ streampipes-sinks-databases-jvm streampipes-sinks-internal-jvm streampipes-sinks-notifications-jvm + streampipes-connectors-plc diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/pom.xml b/streampipes-extensions/streampipes-connect-adapters-iiot/pom.xml index 39d51cf868..8d5bff6371 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/pom.xml +++ b/streampipes-extensions/streampipes-connect-adapters-iiot/pom.xml @@ -59,6 +59,11 @@ streampipes-connectors-opcua 0.93.0-SNAPSHOT + + org.apache.streampipes + streampipes-connectors-plc + 0.93.0-SNAPSHOT + org.apache.streampipes streampipes-connectors-rocketmq @@ -138,30 +143,6 @@ org.apache.commons commons-text - - org.apache.plc4x - plc4j-api - - - org.apache.plc4x - plc4j-connection-pool - - - org.apache.plc4x - plc4j-driver-s7 - runtime - - - com.fasterxml.woodstox - woodstox-core - - - - - org.apache.plc4x - plc4j-driver-modbus - runtime - org.bouncycastle bcprov-jdk15on diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/ConnectAdapterIiotInit.java b/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/ConnectAdapterIiotInit.java index fa651ea92d..dd48df49be 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/ConnectAdapterIiotInit.java +++ b/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/ConnectAdapterIiotInit.java @@ -19,8 +19,6 @@ package org.apache.streampipes.connect.iiot; import org.apache.streampipes.connect.iiot.adapters.iolink.IfmAlMqttAdapter; -import org.apache.streampipes.connect.iiot.adapters.plc4x.modbus.Plc4xModbusAdapter; -import org.apache.streampipes.connect.iiot.adapters.plc4x.s7.Plc4xS7Adapter; import org.apache.streampipes.connect.iiot.adapters.ros.RosBridgeAdapter; import org.apache.streampipes.connect.iiot.adapters.simulator.machine.MachineDataSimulatorAdapter; import org.apache.streampipes.connect.iiot.protocol.stream.FileReplayAdapter; @@ -31,6 +29,9 @@ import org.apache.streampipes.extensions.connectors.nats.adapter.NatsProtocol; import org.apache.streampipes.extensions.connectors.opcua.adapter.OpcUaAdapter; import org.apache.streampipes.extensions.connectors.opcua.migration.OpcUaAdapterMigrationV1; +import org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xS7AdapterMigrationV1; +import org.apache.streampipes.extensions.connectors.plc.adapter.modbus.Plc4xModbusAdapter; +import org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter; import org.apache.streampipes.extensions.connectors.pulsar.adapter.PulsarProtocol; import org.apache.streampipes.extensions.connectors.rocketmq.adapter.RocketMQProtocol; import org.apache.streampipes.extensions.connectors.tubemq.adapter.TubeMQProtocol; @@ -65,7 +66,10 @@ public SpServiceDefinition provideServiceDefinition() { .registerAdapter(new HttpServerProtocol()) .registerAdapter(new TubeMQProtocol()) - .registerMigrators(new OpcUaAdapterMigrationV1()) + .registerMigrators( + new OpcUaAdapterMigrationV1(), + new Plc4xS7AdapterMigrationV1() + ) .build(); } } diff --git a/streampipes-extensions/streampipes-connectors-plc/pom.xml b/streampipes-extensions/streampipes-connectors-plc/pom.xml new file mode 100644 index 0000000000..e41573b065 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-plc/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + org.apache.streampipes + streampipes-extensions + 0.93.0-SNAPSHOT + + + streampipes-connectors-plc + + + + org.apache.streampipes + streampipes-extensions-api + 0.93.0-SNAPSHOT + + + org.apache.streampipes + streampipes-extensions-management + 0.93.0-SNAPSHOT + + + org.apache.streampipes + streampipes-sdk + 0.93.0-SNAPSHOT + + + + org.apache.plc4x + plc4j-api + + + org.apache.plc4x + plc4j-connection-pool + + + org.apache.plc4x + plc4j-driver-s7 + runtime + + + com.fasterxml.woodstox + woodstox-core + + + + + org.apache.plc4x + plc4j-driver-modbus + runtime + + + + junit + junit + test + + + + diff --git a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7AdapterMigrationV1.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7AdapterMigrationV1.java new file mode 100644 index 0000000000..c88d3f9a5b --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7AdapterMigrationV1.java @@ -0,0 +1,87 @@ +/* + * 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 + * + * http://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.streampipes.extensions.connectors.plc.adapter.migration; + +import org.apache.streampipes.extensions.api.extractor.IStaticPropertyExtractor; +import org.apache.streampipes.extensions.api.migration.IAdapterMigrator; +import org.apache.streampipes.model.connect.adapter.AdapterDescription; +import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceTagPrefix; +import org.apache.streampipes.model.migration.MigrationResult; +import org.apache.streampipes.model.migration.ModelMigratorConfig; +import org.apache.streampipes.model.staticproperty.CollectionStaticProperty; +import org.apache.streampipes.model.staticproperty.StaticProperty; +import org.apache.streampipes.sdk.StaticProperties; +import org.apache.streampipes.sdk.helpers.Alternatives; +import org.apache.streampipes.sdk.helpers.CodeLanguage; +import org.apache.streampipes.sdk.helpers.Labels; + +import java.util.List; + +import static org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter.CODE_TEMPLATE; +import static org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter.PLC_CODE_BLOCK; +import static org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter.PLC_NODES; +import static org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter.PLC_NODE_INPUT_ALTERNATIVES; +import static org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter.PLC_NODE_INPUT_CODE_BLOCK_ALTIVE; +import static org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter.PLC_NODE_INPUT_COLLECTION_ALTERNATIVE; + +public class Plc4xS7AdapterMigrationV1 implements IAdapterMigrator { + @Override + public ModelMigratorConfig config() { + return new ModelMigratorConfig( + "org.apache.streampipes.connect.iiot.adapters.plc4x.s7", + SpServiceTagPrefix.ADAPTER, + 0, + 1 + ); + } + + @Override + public MigrationResult migrate(AdapterDescription element, + IStaticPropertyExtractor extractor) throws RuntimeException { + var newConfigs = new java.util.ArrayList<>(element.getConfig().stream().map(config -> { + if (isCollectionConfig(config)) { + return modifyCollection((CollectionStaticProperty) config); + } else { + return config; + } + }).toList()); + + newConfigs.removeIf(c -> c.getInternalName().equals(PLC_NODES)); + element.setConfig(newConfigs); + + return MigrationResult.success(element); + } + + private StaticProperty modifyCollection(CollectionStaticProperty collectionConfig) { + + var alternatives = List.of( + Alternatives.from(Labels.withId(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE), + collectionConfig, + true), + Alternatives.from(Labels.withId(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE), + StaticProperties.codeStaticProperty(Labels.withId(PLC_CODE_BLOCK), CodeLanguage.None, CODE_TEMPLATE)) + ); + + return StaticProperties.alternatives(Labels.withId(PLC_NODE_INPUT_ALTERNATIVES), alternatives); + } + + private boolean isCollectionConfig(StaticProperty config) { + return config.getInternalName().equals(PLC_NODES); + } +} diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/modbus/ModbusConfigFile.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/ModbusConfigFile.java similarity index 95% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/modbus/ModbusConfigFile.java rename to streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/ModbusConfigFile.java index fbda391ecc..f6e3961126 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/modbus/ModbusConfigFile.java +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/ModbusConfigFile.java @@ -16,7 +16,7 @@ * */ -package org.apache.streampipes.connect.iiot.adapters.plc4x.modbus; +package org.apache.streampipes.extensions.connectors.plc.adapter.modbus; public class ModbusConfigFile { @@ -58,4 +58,4 @@ public String getLogicalAddress() { public void setLogicalAddress(String logicalAddress) { this.logicalAddress = logicalAddress; } -} \ No newline at end of file +} diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/modbus/Plc4xModbusAdapter.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java similarity index 99% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/modbus/Plc4xModbusAdapter.java rename to streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java index c755901d58..6938ead546 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/modbus/Plc4xModbusAdapter.java +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java @@ -16,7 +16,7 @@ * */ -package org.apache.streampipes.connect.iiot.adapters.plc4x.modbus; +package org.apache.streampipes.extensions.connectors.plc.adapter.modbus; import org.apache.streampipes.commons.exceptions.connect.AdapterException; diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/s7/Plc4xS7Adapter.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java similarity index 64% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/s7/Plc4xS7Adapter.java rename to streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java index 5913480d80..103e428cb1 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/s7/Plc4xS7Adapter.java +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java @@ -16,10 +16,11 @@ * */ -package org.apache.streampipes.connect.iiot.adapters.plc4x.s7; +package org.apache.streampipes.extensions.connectors.plc.adapter.s7; import org.apache.streampipes.commons.exceptions.connect.AdapterException; +import org.apache.streampipes.extensions.connectors.plc.adapter.s7.config.ConfigurationParser; import org.apache.streampipes.extensions.api.connect.IAdapterConfiguration; import org.apache.streampipes.extensions.api.connect.IEventCollector; import org.apache.streampipes.extensions.api.connect.IPullAdapter; @@ -33,6 +34,7 @@ import org.apache.streampipes.model.AdapterType; import org.apache.streampipes.model.connect.guess.GuessSchema; import org.apache.streampipes.model.schema.EventProperty; +import org.apache.streampipes.model.schema.EventPropertyList; import org.apache.streampipes.model.staticproperty.CollectionStaticProperty; import org.apache.streampipes.model.staticproperty.StaticProperty; import org.apache.streampipes.model.staticproperty.StaticPropertyGroup; @@ -41,11 +43,12 @@ import org.apache.streampipes.sdk.builder.adapter.AdapterConfigurationBuilder; import org.apache.streampipes.sdk.builder.adapter.GuessSchemaBuilder; import org.apache.streampipes.sdk.extractor.StaticPropertyExtractor; +import org.apache.streampipes.sdk.helpers.Alternatives; +import org.apache.streampipes.sdk.helpers.CodeLanguage; import org.apache.streampipes.sdk.helpers.Labels; import org.apache.streampipes.sdk.helpers.Locales; import org.apache.streampipes.sdk.helpers.Options; import org.apache.streampipes.sdk.utils.Assets; -import org.apache.streampipes.sdk.utils.Datatypes; import org.apache.plc4x.java.PlcDriverManager; import org.apache.plc4x.java.api.PlcConnection; @@ -53,6 +56,7 @@ 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.value.PlcValue; import org.apache.plc4x.java.utils.connectionpool.PooledPlcDriverManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,17 +82,33 @@ public class Plc4xS7Adapter implements StreamPipesAdapter, IPullAdapter, PlcRead */ private static final String PLC_IP = "plc_ip"; private static final String PLC_POLLING_INTERVAL = "plc_polling_interval"; - private static final String PLC_NODES = "plc_nodes"; + public static final String PLC_NODES = "plc_nodes"; private static final String PLC_NODE_NAME = "plc_node_name"; private static final String PLC_NODE_RUNTIME_NAME = "plc_node_runtime_name"; private static final String PLC_NODE_TYPE = "plc_node_type"; + public static final String PLC_NODE_INPUT_CODE_BLOCK_ALTIVE = "plc_node_input_code_block_altive"; + public static final String PLC_CODE_BLOCK = "plc_code_block"; + public static final String PLC_NODE_INPUT_ALTERNATIVES = "plc_node_input_alternatives"; + public static final String PLC_NODE_INPUT_COLLECTION_ALTERNATIVE = "plc_node_input_collection_alternative"; + + public static final String CODE_TEMPLATE = """ + // This code block can be used to manually specify the addresses of the PLC registers. + // The syntax is based on the PLC4X syntax, see [1]. + // Address Pattern: + // propertyName=%{Memory-Area}{start-address}:{Data-Type}[{array-size}] + + temperature=%I0.0:INT + + // [1] https://plc4x.apache.org/users/protocols/s7.html + """; + /** * Values of user configuration parameters */ private String ip; private int pollingInterval; - private List> nodes; + private Map nodes; private PlcDriverManager driverManager; @@ -101,10 +121,8 @@ public Plc4xS7Adapter() { /** * This method is executed when the adapter is started. A connection to the PLC is initialized - * - * @throws AdapterException */ - private void before(IStaticPropertyExtractor extractor) throws AdapterException { + private void before(IStaticPropertyExtractor extractor) { // Extract user input getConfigurations(extractor); @@ -134,17 +152,18 @@ public void pullData() { } } - private PlcReadRequest makeReadRequest(PlcConnection plcConnection) throws PlcConnectionException { + private PlcReadRequest makeReadRequest(PlcConnection plcConnection) { PlcReadRequest.Builder builder = plcConnection.readRequestBuilder(); - for (Map node : this.nodes) { - builder.addItem(node.get(PLC_NODE_NAME), - node.get(PLC_NODE_NAME) + ":" + node.get(PLC_NODE_TYPE).toUpperCase().replaceAll(" ", "_")); + + for (Map.Entry entry : this.nodes.entrySet()) { + builder.addItem(entry.getKey(), entry.getValue()); } + return builder.build(); } private void readPlcData( - PlcConnection plcConnection, PlcReadResponseHandler handler) throws PlcConnectionException { + PlcConnection plcConnection, PlcReadResponseHandler handler) { var readRequest = makeReadRequest(plcConnection); // Execute the request CompletableFuture asyncResponse = readRequest.execute(); @@ -175,44 +194,28 @@ public PollingSettings getPollingInterval() { * * @param extractor StaticPropertyExtractor */ - private void getConfigurations(IStaticPropertyExtractor extractor) throws AdapterException { + private void getConfigurations(IStaticPropertyExtractor extractor) { this.ip = extractor.singleValueParameter(PLC_IP, String.class); this.pollingInterval = extractor.singleValueParameter(PLC_POLLING_INTERVAL, Integer.class); - this.nodes = new ArrayList<>(); - CollectionStaticProperty sp = (CollectionStaticProperty) extractor.getStaticPropertyByName(PLC_NODES); - - for (StaticProperty member : sp.getMembers()) { - StaticPropertyExtractor memberExtractor = - StaticPropertyExtractor.from(((StaticPropertyGroup) member).getStaticProperties(), new ArrayList<>()); - Map map = new HashMap<>(); - map.put(PLC_NODE_RUNTIME_NAME, memberExtractor.textParameter(PLC_NODE_RUNTIME_NAME)); - map.put(PLC_NODE_NAME, memberExtractor.textParameter(PLC_NODE_NAME)); - map.put(PLC_NODE_TYPE, memberExtractor.selectedSingleValue(PLC_NODE_TYPE, String.class)); - this.nodes.add(map); - } - } + this.nodes = new HashMap<>(); - /** - * Transforms PLC4X data types to datatypes supported in StreamPipes - * - * @param plcType String - * @return Datatypes - */ - private Datatypes getStreamPipesDataType(String plcType) throws AdapterException { + var selectedAlternative = extractor.selectedAlternativeInternalId(PLC_NODE_INPUT_ALTERNATIVES); - String type = plcType.substring(plcType.lastIndexOf(":") + 1); + if (selectedAlternative.equals(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE)) { + // Alternative Simple + var csp = (CollectionStaticProperty) extractor.getStaticPropertyByName(PLC_NODES); + this.nodes = getNodeInformationFromCollectionStaticProperty(csp); - return switch (type) { - case "BOOL" -> Datatypes.Boolean; - case "BYTE", "REAL" -> Datatypes.Float; - case "INT" -> Datatypes.Integer; - case "WORD", "TIME_OF_DAY", "DATE", "DATE_AND_TIME", "STRING", "CHAR" -> Datatypes.String; - default -> throw new AdapterException("Datatype " + plcType + " is not supported"); - }; + } else { + // Alternative Advanced + var codePropertyInput = extractor.codeblockValue(PLC_CODE_BLOCK); + this.nodes = new ConfigurationParser().getNodeInformationFromCodeProperty(codePropertyInput); + } } + @Override public void onReadResult(PlcReadResponse response, Throwable throwable) { if (throwable != null) { @@ -226,13 +229,26 @@ public void onReadResult(PlcReadResponse response, Throwable throwable) { } private Map makeEvent(PlcReadResponse response) { - Map event = new HashMap<>(); - for (Map node : this.nodes) { - if (response.getResponseCode(node.get(PLC_NODE_NAME)) == PlcResponseCode.OK) { - event.put(node.get(PLC_NODE_RUNTIME_NAME), response.getObject(node.get(PLC_NODE_NAME))); + var event = new HashMap(); + + for (String key : this.nodes.keySet()) { + if (response.getResponseCode(key) == PlcResponseCode.OK) { + + // if the response is a list, add each element to the result + if (response.getObject(key) instanceof List) { + event.put(key, + response.getAsPlcValue() + .getValue(key) + .getList().stream() + .map(PlcValue::getObject) + .toList() + .toArray()); + } else { + event.put(key, response.getObject(key)); + } } else { - LOG.error("Error[" + node.get(PLC_NODE_NAME) + "]: " - + response.getResponseCode(node.get(PLC_NODE_NAME)).name()); + LOG.error("Error[" + key + "]: " + + response.getResponseCode(key).name()); } } return event; @@ -246,18 +262,24 @@ private Map makeEvent(PlcReadResponse response) { */ @Override public IAdapterConfiguration declareConfig() { - return AdapterConfigurationBuilder.create(ID, Plc4xS7Adapter::new) + return AdapterConfigurationBuilder.create(ID, 1, Plc4xS7Adapter::new) .withLocales(Locales.EN) .withAssets(Assets.DOCUMENTATION, Assets.ICON) .withCategory(AdapterType.Manufacturing) .requiredTextParameter(Labels.withId(PLC_IP)) .requiredIntegerParameter(Labels.withId(PLC_POLLING_INTERVAL), 1000) - .requiredCollection(Labels.withId(PLC_NODES), - StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME)), - StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_NAME)), - StaticProperties.singleValueSelection(Labels.withId(PLC_NODE_TYPE), - Options.from("Bool", "Byte", "Int", "Word", "Real", "Char", "String", "Date", "Time of day", - "Date and Time"))) + .requiredAlternatives( + Labels.withId(PLC_NODE_INPUT_ALTERNATIVES), + Alternatives.from(Labels.withId(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE), + StaticProperties.collection(Labels.withId(PLC_NODES), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME)), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_NAME)), + StaticProperties.singleValueSelection(Labels.withId(PLC_NODE_TYPE), + Options.from("Bool", "Byte", "Int", "Word", "Real", "Char", "String", "Date", "Time of day", + "Date and Time"))), + true), + Alternatives.from(Labels.withId(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE), + StaticProperties.codeStaticProperty(Labels.withId(PLC_CODE_BLOCK), CodeLanguage.None, CODE_TEMPLATE))) .buildConfiguration(); } @@ -291,17 +313,27 @@ public GuessSchema onSchemaRequested(IAdapterParameterExtractor extractor, GuessSchemaBuilder builder = GuessSchemaBuilder.create(); List allProperties = new ArrayList<>(); - for (Map node : this.nodes) { - Datatypes datatype = getStreamPipesDataType(node.get(PLC_NODE_TYPE) - .toUpperCase() - .replaceAll(" ", "_")); - - allProperties.add( - PrimitivePropertyBuilder - .create(datatype, node.get(PLC_NODE_RUNTIME_NAME)) - .label(node.get(PLC_NODE_RUNTIME_NAME)) - .description("") - .build()); + for (Map.Entry entry : this.nodes.entrySet()) { + var datatype = new ConfigurationParser().getStreamPipesDataType(entry.getValue()); + + var primitiveProperty = PrimitivePropertyBuilder + .create(datatype, entry.getKey()) + .label(entry.getKey()) + .description("") + .build(); + + // Check if the address configuration is an array + var isArray = new ConfigurationParser().isPLCArray(entry.getValue()); + + if (isArray) { + var propertyList = new EventPropertyList(); + propertyList.setRuntimeName(entry.getKey()); + propertyList.setLabel(entry.getKey()); + propertyList.setEventProperty(primitiveProperty); + allProperties.add(propertyList); + } else { + allProperties.add(primitiveProperty); + } } this.before(extractor.getStaticPropertyExtractor()); @@ -315,4 +347,36 @@ public GuessSchema onSchemaRequested(IAdapterParameterExtractor extractor, throw new AdapterException(e.getMessage(), e); } } + + + private Map getNodeInformationFromCollectionStaticProperty(CollectionStaticProperty csp) { + var result = new HashMap(); + + for (StaticProperty member : csp.getMembers()) { + StaticPropertyExtractor memberExtractor = + StaticPropertyExtractor.from(((StaticPropertyGroup) member).getStaticProperties(), new ArrayList<>()); + + result.put( + memberExtractor.textParameter(PLC_NODE_RUNTIME_NAME), + getNodeAddress(memberExtractor)); + + } + + return result; + } + + /** + * Takes the members of the static property collection from the UI and creates the PLC4X node address + * + * @param memberExtractor member of the static property node collection + * @return string in the format of PLC4X node address + */ + private String getNodeAddress(StaticPropertyExtractor memberExtractor) { + return "%s:%s".formatted( + memberExtractor.textParameter(PLC_NODE_NAME), + memberExtractor.selectedSingleValue(PLC_NODE_TYPE, String.class) + .toUpperCase() + .replaceAll(" ", "_")); + } + } diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/s7/PlcReadResponseHandler.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/PlcReadResponseHandler.java similarity index 93% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/s7/PlcReadResponseHandler.java rename to streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/PlcReadResponseHandler.java index b9b8f1bfc2..2a4a44c762 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/adapters/plc4x/s7/PlcReadResponseHandler.java +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/PlcReadResponseHandler.java @@ -16,7 +16,7 @@ * */ -package org.apache.streampipes.connect.iiot.adapters.plc4x.s7; +package org.apache.streampipes.extensions.connectors.plc.adapter.s7; import org.apache.plc4x.java.api.messages.PlcReadResponse; diff --git a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/config/ConfigurationParser.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/config/ConfigurationParser.java new file mode 100644 index 0000000000..f45b4dbc40 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/config/ConfigurationParser.java @@ -0,0 +1,101 @@ +/* + * 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 + * + * http://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.streampipes.extensions.connectors.plc.adapter.s7.config; + +import org.apache.streampipes.commons.exceptions.connect.AdapterException; +import org.apache.streampipes.sdk.utils.Datatypes; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * This is a helper class to parse the user input for the PLC4X S7 adapter + */ +public class ConfigurationParser { + + /** + * This method takes a string with the PLC configuration and parses the configuration accorting to this pattern: + * variableName=value + * + * @param codePropertyValue code block with the PLC configuration + * @return returns a list of Maps, with the variable name as the Key and value as value + */ + public Map getNodeInformationFromCodeProperty(String codePropertyValue) { + var result = new HashMap(); + + var lines = codePropertyValue.split("\n"); + + // pattern to match "variableName=value" + var pattern = Pattern.compile("(\\w+)=(.+)"); + + for (String line : lines) { + // Remove leading and trailing whitespace + line = line.trim(); + + // Skip comments + if (line.startsWith("//")) { + continue; + } + + // Check if macher matches and add to results list + var matcher = pattern.matcher(line); + if (matcher.find()) { + result.put(matcher.group(1), matcher.group(2)); + } + } + + return result; + } + + + /** + * Transforms PLC4X data types to datatypes supported in StreamPipes + * + * @param plcType String + * @return Datatypes + */ + public Datatypes getStreamPipesDataType(String plcType) throws AdapterException { + + String type = plcType.substring(plcType.lastIndexOf(":") + 1); + + // replace array information from type + type = type.replaceAll("\\[.*?\\]", ""); + + return switch (type) { + case "BOOL" -> Datatypes.Boolean; + case "BYTE", "REAL" -> Datatypes.Float; + case "INT" -> Datatypes.Integer; + case "WORD", "TIME_OF_DAY", "DATE", "DATE_AND_TIME", "STRING", "CHAR" -> Datatypes.String; + default -> throw new AdapterException("Datatype " + plcType + " is not supported"); + }; + } + + /** + * Takes the PLC4X address description and validates if it describes an array + * + * @param plcType address of the register that should be read + * @return whether the address describes an array or not + */ + public boolean isPLCArray(String plcType) { + return plcType.matches(".*\\[.*\\].*"); + } + + +} diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md similarity index 100% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md rename to streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/icon.png b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/icon.png similarity index 100% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/icon.png rename to streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/icon.png diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/strings.en b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/strings.en similarity index 100% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/strings.en rename to streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/strings.en diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md similarity index 100% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md rename to streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/icon.png b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/icon.png similarity index 100% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/icon.png rename to streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/icon.png diff --git a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/strings.en b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/strings.en similarity index 74% rename from streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/strings.en rename to streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/strings.en index 705c36e030..5aebb1485d 100644 --- a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/strings.en +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/strings.en @@ -41,4 +41,16 @@ plc_node_name.title=Node Name plc_node_name.description=example: %Q0.4 plc_node_type.title=Data Type -plc_node_type.description=example: bool \ No newline at end of file +plc_node_type.description=example: bool + +plc_node_input_code_block_altive.title=Advanced +plc_node_input_code_block_altive.description= + +plc_code_block.title=Advanced +plc_code_block.description=Enter the nodes in the code block below, acording to the described format + +plc_node_input_alternatives.title=Register Input +plc_node_input_alternatives.description=The information about the registers can be entered in simple or advanced mode + +plc_node_input_collection_alternative.title=Simple +plc_node_input_collection_alternative.description= diff --git a/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/config/ConfigurationParserTest.java b/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/config/ConfigurationParserTest.java new file mode 100644 index 0000000000..20a31073e7 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/config/ConfigurationParserTest.java @@ -0,0 +1,106 @@ +/* + * 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 + * + * http://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.streampipes.extensions.connectors.plc.adapter.s7.config; + +import org.apache.streampipes.commons.exceptions.connect.AdapterException; +import org.apache.streampipes.sdk.utils.Datatypes; + +import org.junit.Test; + +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ConfigurationParserTest { + + @Test + public void testGetNodeInformationFromCodePropertyWithComments() { + var configBlock = """ + // This code block can be used to manually specify the addresses of the PLC registers. + // The syntax is based on the PLC4X syntax, see [1]. + // Address Pattern: + // propertyName=%{Memory-Area}{start-address}:{Data-Type}[{array-size}] + // + // Sample: + temperature=%I0.0:INT + + + // [1] https://plc4x.apache.org/users/protocols/s7.html + """; + var result = new ConfigurationParser().getNodeInformationFromCodeProperty(configBlock); + + assertEquals(1, result.size()); + assertEquals(Set.of("temperature"), result.keySet()); + assertEquals("%I0.0:INT", result.get("temperature")); + } + + @Test + public void testGetNodeInformationFromCodePropertyMultipleEntries() { + var configBlock = """ + v1=%I0.0:INT + v2=%I0.0:BOOL + """; + var result = new ConfigurationParser().getNodeInformationFromCodeProperty(configBlock); + + assertEquals(2, result.size()); + assertEquals(Set.of("v1", "v2"), result.keySet()); + assertEquals("%I0.0:INT", result.get("v1")); + assertEquals("%I0.0:BOOL", result.get("v2")); + } + + @Test + public void testGetStreamPipesDataTypeArray() throws AdapterException { + var plcType = "INT[100]"; + var result = new ConfigurationParser().getStreamPipesDataType(plcType); + + assertEquals(Datatypes.Integer, result); + } + + @Test + public void testGetStreamPipesDataTypeBasic() throws AdapterException { + var plcType = "INT"; + var result = new ConfigurationParser().getStreamPipesDataType(plcType); + + assertEquals(Datatypes.Integer, result); + } + + + @Test + public void testGetNodeInformationFromCodePropertyNoEntries() { + var configBlock = ""; + var result = new ConfigurationParser().getNodeInformationFromCodeProperty(configBlock); + + assertEquals(0, result.size()); + } + + @Test + public void testIsPLCArray() { + var result = new ConfigurationParser().isPLCArray("%DB3.DB0:BOOL[100]"); + assertTrue(result); + } + + + @Test + public void testIsNoPLCArray() { + var result = new ConfigurationParser().isPLCArray("%DB3.DB0:BOOL"); + assertFalse(result); + } +} diff --git a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/StaticProperties.java b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/StaticProperties.java index 6707617e84..bd9c595cf4 100644 --- a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/StaticProperties.java +++ b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/StaticProperties.java @@ -19,6 +19,7 @@ package org.apache.streampipes.sdk; import org.apache.streampipes.model.schema.PropertyScope; +import org.apache.streampipes.model.staticproperty.CodeInputStaticProperty; import org.apache.streampipes.model.staticproperty.CollectionStaticProperty; import org.apache.streampipes.model.staticproperty.FileStaticProperty; import org.apache.streampipes.model.staticproperty.FreeTextStaticProperty; @@ -32,9 +33,11 @@ import org.apache.streampipes.model.staticproperty.SecretStaticProperty; import org.apache.streampipes.model.staticproperty.SelectionStaticProperty; import org.apache.streampipes.model.staticproperty.StaticProperty; +import org.apache.streampipes.model.staticproperty.StaticPropertyAlternative; import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives; import org.apache.streampipes.model.staticproperty.StaticPropertyGroup; import org.apache.streampipes.model.staticproperty.SupportedProperty; +import org.apache.streampipes.sdk.helpers.CodeLanguage; import org.apache.streampipes.sdk.helpers.Filetypes; import org.apache.streampipes.sdk.helpers.Label; import org.apache.streampipes.sdk.helpers.RequirementsSelector; @@ -47,6 +50,23 @@ public class StaticProperties { + public static StaticPropertyAlternatives alternatives(Label label, StaticPropertyAlternative... alternatives) { + return alternatives(label, Arrays.asList(alternatives)); + } + + public static StaticPropertyAlternatives alternatives(Label label, List alternatives) { + StaticPropertyAlternatives alternativesContainer = + new StaticPropertyAlternatives(label.getInternalId(), label.getLabel(), label.getDescription()); + + for (int i = 0; i < alternatives.size(); i++) { + alternatives.get(i).setIndex(i); + } + + alternativesContainer.setAlternatives(alternatives); + + return alternativesContainer; + } + public static MappingPropertyUnary mappingPropertyUnary(Label label, RequirementsSelector requirementsSelector, PropertyScope propertyScope) { MappingPropertyUnary mp = new MappingPropertyUnary(label.getInternalId(), label @@ -134,7 +154,6 @@ public static RuntimeResolvableAnyStaticProperty multiValueSelectionFromContaine } - public static RuntimeResolvableTreeInputStaticProperty runtimeResolvableTreeInput(Label label, List dependsOn, boolean resolveDynamically, @@ -209,6 +228,16 @@ public static CollectionStaticProperty collection(Label label, StaticProperty... } } + public static CodeInputStaticProperty codeStaticProperty(Label label, + CodeLanguage codeLanguage, + String defaultSkeleton) { + var codeInputStaticProperty = new CodeInputStaticProperty(label.getInternalId(), + label.getLabel(), label.getDescription()); + codeInputStaticProperty.setLanguage(codeLanguage.name()); + codeInputStaticProperty.setCodeTemplate(defaultSkeleton); + return codeInputStaticProperty; + } + private static StaticProperty setHorizontalRendering(StaticProperty sp) { if (sp instanceof StaticPropertyGroup) { ((StaticPropertyGroup) sp).setHorizontalRendering(true); diff --git a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/builder/AbstractConfigurablePipelineElementBuilder.java b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/builder/AbstractConfigurablePipelineElementBuilder.java index 64d7a05ae4..f0979282f5 100644 --- a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/builder/AbstractConfigurablePipelineElementBuilder.java +++ b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/builder/AbstractConfigurablePipelineElementBuilder.java @@ -977,13 +977,7 @@ public K requiredFile(Label label, String... requiredFiletypes) { public K requiredAlternatives(Label label, StaticPropertyAlternative... alternatives) { StaticPropertyAlternatives alternativesContainer = - new StaticPropertyAlternatives(label.getInternalId(), label.getLabel(), label.getDescription()); - - for (int i = 0; i < alternatives.length; i++) { - alternatives[i].setIndex(i); - } - - alternativesContainer.setAlternatives(Arrays.asList(alternatives)); + StaticProperties.alternatives(label, alternatives); this.staticProperties.add(alternativesContainer); return me(); diff --git a/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/StaticPropertiesTest.java b/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/StaticPropertiesTest.java new file mode 100644 index 0000000000..532ed187ee --- /dev/null +++ b/streampipes-sdk/src/test/java/org/apache/streampipes/sdk/StaticPropertiesTest.java @@ -0,0 +1,43 @@ +/* + * 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 + * + * http://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.streampipes.sdk; + +import org.apache.streampipes.sdk.helpers.CodeLanguage; +import org.apache.streampipes.sdk.helpers.Label; +import org.apache.streampipes.sdk.helpers.Labels; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class StaticPropertiesTest { + + private static final String TEST_PROPERTY_LABEL = "test-property-id"; + private static final Label defaultLabel = Labels.from("", TEST_PROPERTY_LABEL, ""); + + @Test + public void codeStaticProperty() { + String codeTemplate = "// This is a test"; + var result = StaticProperties.codeStaticProperty(defaultLabel, CodeLanguage.Javascript, codeTemplate); + + assertEquals(TEST_PROPERTY_LABEL, result.getLabel()); + assertEquals(CodeLanguage.Javascript.name(), result.getLanguage()); + assertEquals(codeTemplate, result.getCodeTemplate()); + } +} \ No newline at end of file