Skip to content

Commit c226208

Browse files
committed
Adding the s7-new adapter and tests
1 parent fd1b2dc commit c226208

23 files changed

+784
-919
lines changed

hivemq-edge/src/main/resources/hivemq-edge-configuration.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@
112112
"external" : true
113113
}
114114
},
115+
{
116+
"id" : "s7-new",
117+
"version" : "2024.9",
118+
"name" : "S7 to MQTT Protocol Adapter",
119+
"description" : "Connects HiveMQ Edge to S7-300, S7-400, S7-1200 & S7-1500 devices, reading data from the PLC into MQTT.",
120+
"author" : "HiveMQ",
121+
"documentationLink" : {
122+
"url" : "https://docs.hivemq.com/hivemq-edge/protocol-adapters.html#s7-adapter",
123+
"external" : true
124+
},
125+
"provisioningLink" : {
126+
"url" : "https://github.com/hivemq/hivemq-edge/releases",
127+
"external" : true
128+
},
129+
"logoUrl" : {
130+
"url" : "https://raw.githubusercontent.com/hivemq/hivemq-edge/master/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/s7-icon.png",
131+
"external" : true
132+
}
133+
},
115134
{
116135
"id" : "ads",
117136
"version" : "2024.7",

modules/hivemq-edge-module-s7/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ dependencies {
3838
compileOnly(libs.hivemq.edge.adapterSdk)
3939
compileOnly(libs.apache.commonsIO)
4040
compileOnly(libs.jackson.databind)
41-
compileOnly(libs.iot.communication)
4241
compileOnly(libs.slf4j.api)
42+
implementation(libs.iot.communication)
4343

4444
testImplementation(libs.jackson.databind)
4545
testImplementation(libs.junit.jupiter)
4646
testImplementation(libs.assertj)
4747
testImplementation(libs.mockito.junitJupiter)
48+
testImplementation(libs.iot.communication)
4849
testImplementation("com.hivemq:hivemq-edge")
4950

5051
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.hivemq.edge.adapters.s7;
2+
3+
import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
4+
import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
5+
import com.hivemq.adapter.sdk.api.data.DataPoint;
6+
import com.hivemq.adapter.sdk.api.factories.DataPointFactory;
7+
import com.hivemq.edge.adapters.s7.config.S7AdapterConfig;
8+
import com.hivemq.edge.adapters.s7.config.S7DataType;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
import java.util.List;
14+
import java.util.stream.Collectors;
15+
import java.util.stream.IntStream;
16+
17+
public class S7Client {
18+
19+
private static final Logger log = LoggerFactory.getLogger(S7Client.class);
20+
21+
private final S7PLC s7PLC;
22+
23+
private final DataPointFactory dataPointFactory;
24+
25+
public S7Client(final @NotNull EPlcType elpcType, final @NotNull String hostname, final int port, final int rack, final int slot, final int pduLength, final @NotNull DataPointFactory dataPointFactory) {
26+
s7PLC = new S7PLC(elpcType, hostname, port, rack, slot, pduLength);
27+
this.dataPointFactory = dataPointFactory;
28+
}
29+
30+
31+
public DataPoint readByte(final String address) {
32+
if(log.isTraceEnabled()) {
33+
log.trace("Reading bytes from address {} with count {}", address, 1);
34+
}
35+
return dataPointFactory.create(address, s7PLC.readByte(address, 1)[0]);
36+
}
37+
38+
39+
public DataPoint readBytes(final String address, final int count) {
40+
if(log.isTraceEnabled()) {
41+
log.trace("Reading bytes from address {} with count {}", address, count);
42+
}
43+
return dataPointFactory.create(address, s7PLC.readByte(address, count));
44+
}
45+
46+
public List<DataPoint> read(final @NotNull S7DataType type, final @NotNull List<String> addresses) {
47+
if(log.isTraceEnabled()) {
48+
log.trace("Reading data from addresses {} with type {}", addresses, type);
49+
}
50+
switch (type) {
51+
case BYTE: throw new IllegalArgumentException("Byte data type not supported by this method, use readBytes");
52+
case BOOL: return combine(dataPointFactory, addresses, s7PLC.readBoolean(addresses));
53+
case INT16: return combine(dataPointFactory, addresses, s7PLC.readInt16(addresses));
54+
case UINT16: return combine(dataPointFactory, addresses, s7PLC.readUInt16(addresses));
55+
case INT32: return combine(dataPointFactory, addresses, s7PLC.readInt32(addresses));
56+
case UINT32: return combine(dataPointFactory, addresses, s7PLC.readUInt32(addresses));
57+
case INT64: return combine(dataPointFactory, addresses, s7PLC.readInt64(addresses));
58+
case REAL: return combine(dataPointFactory, addresses, s7PLC.readFloat32(addresses));
59+
case LREAL: return combine(dataPointFactory, addresses, s7PLC.readFloat64(addresses));
60+
case STRING: return combine(dataPointFactory, addresses, addresses.stream().map(s7PLC::readString).collect(Collectors.toList()));
61+
case DATE: return combine(dataPointFactory, addresses, addresses.stream().map(s7PLC::readDate).collect(Collectors.toList()));
62+
case TIME_OF_DAY: return combine(dataPointFactory, addresses, addresses.stream().map(s7PLC::readTimeOfDay).collect(Collectors.toList()));
63+
case TIME: return combine(dataPointFactory, addresses, addresses.stream().map(s7PLC::readTime).collect(Collectors.toList()));
64+
case DATE_AND_TIME: return combine(dataPointFactory, addresses, addresses.stream().map(s7PLC::readDTL).collect(Collectors.toList()));
65+
default: {
66+
log.error("Unspported tag-type {} at address {}", type, addresses);
67+
throw new IllegalArgumentException("Unspported tag-type " + type + " at address " + addresses);
68+
}
69+
}
70+
}
71+
72+
public static List<DataPoint> combine(final @NotNull DataPointFactory dataPointFactory, final @NotNull List<String> addresses, final @NotNull List<?> values) {
73+
return IntStream
74+
.range(0, addresses.size())
75+
.mapToObj(i -> dataPointFactory.create(addresses.get(i), values.get(i)))
76+
.collect(Collectors.toList());
77+
}
78+
79+
public void connect() {
80+
s7PLC.connect();
81+
}
82+
83+
public void disconnect() {
84+
s7PLC.close();
85+
}
86+
87+
public static EPlcType getEplcType(final @NotNull S7AdapterConfig.ControllerType controllerType) {
88+
switch (controllerType) {
89+
case S7_200: return EPlcType.S200;
90+
case S7_200_SMART: return EPlcType.S200_SMART;
91+
case S7_300: return EPlcType.S300;
92+
case S7_400: return EPlcType.S400;
93+
case S7_1200: return EPlcType.S1200;
94+
case S7_1500: return EPlcType.S1500;
95+
default: throw new IllegalArgumentException("Unsupported controller type: " + controllerType);
96+
}
97+
}
98+
}

modules/hivemq-edge-module-s7/src/main/java/com/hivemq/edge/adapters/s7/S7ProtocolAdapter.java

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
package com.hivemq.edge.adapters.s7;
1717

1818
import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
19-
import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
2019
import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation;
21-
import com.hivemq.adapter.sdk.api.config.PollingContext;
20+
import com.hivemq.adapter.sdk.api.data.DataPoint;
2221
import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput;
2322
import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartInput;
2423
import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartOutput;
@@ -29,87 +28,127 @@
2928
import com.hivemq.adapter.sdk.api.polling.PollingProtocolAdapter;
3029
import com.hivemq.adapter.sdk.api.state.ProtocolAdapterState;
3130
import com.hivemq.edge.adapters.s7.config.S7AdapterConfig;
32-
import com.hivemq.edge.adapters.plc4x.config.Plc4xDataType;
33-
import com.hivemq.edge.adapters.plc4x.config.Plc4xToMqttMapping;
34-
import com.hivemq.edge.adapters.plc4x.impl.AbstractPlc4xAdapter;
35-
import com.hivemq.edge.adapters.plc4x.types.siemens.config.S7AdapterConfig;
31+
import com.hivemq.edge.adapters.s7.config.S7DataType;
32+
import com.hivemq.edge.adapters.s7.config.S7ToMqttConfig;
3633
import org.jetbrains.annotations.NotNull;
3734
import org.slf4j.Logger;
3835
import org.slf4j.LoggerFactory;
3936

40-
import java.util.HashMap;
4137
import java.util.List;
4238
import java.util.Map;
43-
import java.util.Set;
44-
import java.util.regex.Matcher;
45-
import java.util.regex.Pattern;
46-
47-
import static com.hivemq.edge.adapters.plc4x.config.Plc4xDataType.DATA_TYPE.*;
39+
import java.util.Objects;
40+
import java.util.concurrent.ConcurrentHashMap;
4841

4942
/**
5043
* @author HiveMQ Adapter Generator
5144
*/
52-
public class S7ProtocolAdapter implements PollingProtocolAdapter<S7AdapterConfig> {
45+
public class S7ProtocolAdapter implements PollingProtocolAdapter<S7ToMqttConfig> {
5346

5447
private static final Logger log = LoggerFactory.getLogger(S7ProtocolAdapter.class);
5548

5649
private final ProtocolAdapterInformation adapterInformation;
5750
private final S7AdapterConfig adapterConfig;
5851
private final ProtocolAdapterState protocolAdapterState;
59-
private final PollingContext
52+
private final S7Client s7Client;
53+
54+
private final Map<String, DataPoint> dataPoints;
6055

6156
public S7ProtocolAdapter(
6257
final @NotNull ProtocolAdapterInformation adapterInformation,
6358
final @NotNull ProtocolAdapterInput<S7AdapterConfig> input) {
6459
this.adapterInformation = adapterInformation;
6560
this.adapterConfig = input.getConfig();
6661
this.protocolAdapterState = input.getProtocolAdapterState();
67-
this.pollingContext = adapterConfig.getFileToMqttConfig().getMappings();
68-
}
69-
70-
@Override
71-
public void poll(
72-
@NotNull final PollingInput<S7AdapterConfig> pollingInput,
73-
@NotNull final PollingOutput pollingOutput) {
74-
S7PLC s7PLC = new S7PLC(EPlcType.S1200, "127.0.0.1");
75-
s7PLC.writeByte("DB2.1", (byte) 0x11);
76-
s7PLC.readByte("DB2.1");
77-
// close it manually, if you want to use it all the time, you do not need to close it
78-
s7PLC.close();
62+
final EPlcType eplcType = S7Client.getEplcType(adapterConfig.getControllerType());
63+
s7Client = new S7Client(
64+
eplcType,
65+
adapterConfig.getHost(),
66+
adapterConfig.getPort(),
67+
Objects.requireNonNullElse(adapterConfig.getRemoteRack(), eplcType.getRack()),
68+
Objects.requireNonNullElse(adapterConfig.getRemoteSlot(), eplcType.getSlot()),
69+
Objects.requireNonNullElse(adapterConfig.getPduLength(), eplcType.getPduLength()),
70+
input.adapterFactories().dataPointFactory());
71+
this.dataPoints = new ConcurrentHashMap<>();
7972
}
8073

8174
@Override
82-
public @NotNull List<S7AdapterConfig> getPollingContexts() {
83-
return List.of();
75+
public @NotNull List<S7ToMqttConfig> getPollingContexts() {
76+
return adapterConfig.getS7ToMqttMappings();
8477
}
8578

8679
@Override
8780
public int getPollingIntervalMillis() {
88-
return adapterInformation;
81+
return adapterConfig.getPollingIntervalMillis();
8982
}
9083

9184
@Override
9285
public int getMaxPollingErrorsBeforeRemoval() {
93-
return 0;
86+
return adapterConfig.getMaxPollingErrorsBeforeRemoval();
9487
}
9588

9689
@Override
9790
public @NotNull String getId() {
98-
return "s7-new";
91+
return adapterConfig.getId();
9992
}
10093

10194
@Override
10295
public void start(
10396
@NotNull final ProtocolAdapterStartInput input,
10497
@NotNull final ProtocolAdapterStartOutput output) {
105-
log.error("REPLACE WITH AN ACTUAL IMPLEMENTATION!");
106-
output.startedSuccessfully();
98+
log.info("Connecting to {}@{}:{}", adapterConfig.getControllerType(), adapterConfig.getHost(), adapterConfig.getPort());
99+
try {
100+
s7Client.connect();
101+
protocolAdapterState.setConnectionStatus(ProtocolAdapterState.ConnectionStatus.CONNECTED);
102+
output.startedSuccessfully();
103+
} catch (final Exception e) {
104+
String msg = "Unable to connect to " + adapterConfig.getControllerType() + "@" + adapterConfig.getHost() + ":" + adapterConfig.getPort();
105+
protocolAdapterState.setErrorConnectionStatus(e, msg);
106+
output.failStart(e, msg);
107+
}
107108
}
108109

109110
@Override
110111
public void stop(@NotNull final ProtocolAdapterStopInput input, @NotNull final ProtocolAdapterStopOutput output) {
111-
log.error("REPLACE WITH AN ACTUAL IMPLEMENTATION!");
112-
output.stoppedSuccessfully();
112+
log.info("Closing connection to {}@{}:{}", adapterConfig.getControllerType(), adapterConfig.getHost(), adapterConfig.getPort());
113+
try {
114+
s7Client.disconnect();
115+
protocolAdapterState.setConnectionStatus(ProtocolAdapterState.ConnectionStatus.DISCONNECTED);
116+
output.stoppedSuccessfully();
117+
} catch (final Exception e) {
118+
final String msg = "Unable to disconnect from " + adapterConfig.getControllerType() + "@" + adapterConfig.getHost() + ":" + adapterConfig.getPort();
119+
protocolAdapterState.setErrorConnectionStatus(e, msg);
120+
output.failStop(e, msg);
121+
}
122+
}
123+
124+
@Override
125+
public void poll(
126+
@NotNull final PollingInput<S7ToMqttConfig> pollingInput,
127+
@NotNull final PollingOutput pollingOutput) {
128+
final S7ToMqttConfig s7ToMqtt = pollingInput.getPollingContext();
129+
//Every S7 address starts with a % but the iot-communications lib doesn't like it so we are stripping it.
130+
final String tagAddress = s7ToMqtt.getTagAddress().replace("%","");
131+
final DataPoint dataPoint;
132+
133+
if(s7ToMqtt.getDataType() == S7DataType.BYTE) {
134+
dataPoint = s7Client.readByte(tagAddress);
135+
} else {
136+
dataPoint = s7Client.read(s7ToMqtt.getDataType(), List.of(tagAddress)).get(0);
137+
}
138+
139+
if(adapterConfig.getPublishChangedDataOnly() && dataPoints.containsKey(tagAddress)) {
140+
final DataPoint existingDataPoint = dataPoints.get(tagAddress);
141+
if(existingDataPoint != null && !existingDataPoint.equals(dataPoint)) {
142+
dataPoints.put(tagAddress, dataPoint);
143+
pollingOutput.addDataPoint(dataPoint);
144+
} else {
145+
log.debug("Skipping sending for {} because publishChangedDataOnly=true", tagAddress);
146+
}
147+
} else {
148+
pollingOutput.addDataPoint(dataPoint);
149+
}
150+
151+
pollingOutput.finish();
113152
}
114153

115154
@Override

modules/hivemq-edge-module-s7/src/main/java/com/hivemq/edge/adapters/s7/S7ProtocolAdapterFactory.java

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,12 @@
2121
import com.hivemq.adapter.sdk.api.config.ProtocolAdapterConfig;
2222
import com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory;
2323
import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput;
24-
import com.hivemq.edge.adapters.plc4x.config.Plc4xToMqttMapping;
25-
import com.hivemq.edge.adapters.plc4x.types.siemens.config.S7AdapterConfig;
26-
import com.hivemq.edge.adapters.plc4x.types.siemens.config.S7ToMqttConfig;
27-
import com.hivemq.edge.adapters.plc4x.types.siemens.config.legacy.LegacyS7AdapterConfig;
2824
import com.hivemq.edge.adapters.s7.config.S7AdapterConfig;
2925
import org.jetbrains.annotations.NotNull;
3026
import org.slf4j.Logger;
3127
import org.slf4j.LoggerFactory;
3228

33-
import java.util.List;
3429
import java.util.Map;
35-
import java.util.stream.Collectors;
3630

3731
/**
3832
* @author HiveMQ Adapter Generator
@@ -69,63 +63,7 @@ public S7ProtocolAdapterFactory(final boolean writingEnabled) {
6963
@Override
7064
public @NotNull ProtocolAdapterConfig convertConfigObject(
7165
final @NotNull ObjectMapper objectMapper, final @NotNull Map<String, Object> config) {
72-
try {
73-
return ProtocolAdapterFactory.super.convertConfigObject(objectMapper, config);
74-
} catch (final Exception currentConfigFailedException) {
75-
try {
76-
log.warn("Could not load '{}' configuration, trying to load legacy configuration. Because: '{}'. Support for the legacy configuration will be removed in the beginning of 2025.",
77-
S7ProtocolAdapterInformation.INSTANCE.getDisplayName(),
78-
currentConfigFailedException.getMessage());
79-
if (log.isDebugEnabled()) {
80-
log.debug("Original Exception:", currentConfigFailedException);
81-
}
82-
return tryConvertLegacyConfig(objectMapper, config);
83-
} catch (final Exception legacyConfigFailedException) {
84-
log.warn("Could not load legacy '{}' configuration. Because: '{}'",
85-
S7ProtocolAdapterInformation.INSTANCE.getDisplayName(),
86-
legacyConfigFailedException.getMessage());
87-
if (log.isDebugEnabled()) {
88-
log.debug("Original Exception:", legacyConfigFailedException);
89-
}
90-
//we rethrow the exception from the current config conversation, to have a correct rest response.
91-
throw currentConfigFailedException;
92-
}
93-
}
66+
return ProtocolAdapterFactory.super.convertConfigObject(objectMapper, config);
9467
}
9568

96-
private static @NotNull S7AdapterConfig tryConvertLegacyConfig(
97-
final @NotNull ObjectMapper objectMapper,
98-
final @NotNull Map<String, Object> config) {
99-
final LegacyS7AdapterConfig legacyS7AdapterConfig =
100-
objectMapper.convertValue(config, LegacyS7AdapterConfig.class);
101-
102-
final List<Plc4xToMqttMapping> plc4xToMqttMappings = legacyS7AdapterConfig.getSubscriptions()
103-
.stream()
104-
.map(subscription -> new Plc4xToMqttMapping(subscription.getMqttTopic(),
105-
subscription.getMqttQos(),
106-
subscription.getMessageHandlingOptions(),
107-
subscription.getIncludeTimestamp(),
108-
subscription.getIncludeTagNames(),
109-
subscription.getTagName(),
110-
subscription.getTagAddress(),
111-
subscription.getDataType(),
112-
subscription.getUserProperties()))
113-
.collect(Collectors.toList());
114-
115-
final S7ToMqttConfig s7ToMqttConfig = new S7ToMqttConfig(legacyS7AdapterConfig.getPollingIntervalMillis(),
116-
legacyS7AdapterConfig.getMaxPollingErrorsBeforeRemoval(),
117-
legacyS7AdapterConfig.getPublishChangedDataOnly(),
118-
plc4xToMqttMappings);
119-
120-
return new S7AdapterConfig(legacyS7AdapterConfig.getId(),
121-
legacyS7AdapterConfig.getPort(),
122-
legacyS7AdapterConfig.getHost(),
123-
legacyS7AdapterConfig.getControllerType(),
124-
legacyS7AdapterConfig.getRemoteRack(),
125-
legacyS7AdapterConfig.getRemoteRack2(),
126-
legacyS7AdapterConfig.getRemoteSlot(),
127-
legacyS7AdapterConfig.getRemoteSlot2(),
128-
legacyS7AdapterConfig.getRemoteTsap(),
129-
s7ToMqttConfig);
130-
}
13169
}

0 commit comments

Comments
 (0)