/*
* 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.modbus.tcp.discovery;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
import org.apache.plc4x.java.api.messages.PlcDiscoveryItem;
import org.apache.plc4x.java.api.messages.PlcDiscoveryItemHandler;
import org.apache.plc4x.java.api.messages.PlcDiscoveryRequest;
import org.apache.plc4x.java.api.messages.PlcDiscoveryResponse;
import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.apache.plc4x.java.modbus.readwrite.*;
import org.apache.plc4x.java.spi.generation.*;
import org.apache.plc4x.java.spi.messages.DefaultPlcDiscoveryItem;
import org.apache.plc4x.java.spi.messages.DefaultPlcDiscoveryResponse;
import org.apache.plc4x.java.spi.messages.PlcDiscoverer;
import org.apache.plc4x.java.utils.rawsockets.netty.utils.ArpUtils;
import org.pcap4j.core.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class ModbusPlcDiscoverer implements PlcDiscoverer {
private final Logger logger = LoggerFactory.getLogger(ModbusPlcDiscoverer.class);
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
@Override
public CompletableFuture<PlcDiscoveryResponse> discover(PlcDiscoveryRequest discoveryRequest) {
return discoverWithHandler(discoveryRequest, null);
}
@Override
public CompletableFuture<PlcDiscoveryResponse> discoverWithHandler(PlcDiscoveryRequest discoveryRequest, PlcDiscoveryItemHandler handler) {
// Get a list of all reachable IP addresses from the current system.
// TODO: add an option to fine tune the network device or ip subnet to scan and maybe some timeouts and delays to prevent flooding.
final CompletableFuture<PlcDiscoveryResponse> future = new CompletableFuture<>();
Thread discoveryThread = new Thread(() -> {
executeDiscovery(future, discoveryRequest, handler);
});
discoveryThread.start();
return future;
}
private void executeDiscovery(CompletableFuture<PlcDiscoveryResponse> future, PlcDiscoveryRequest discoveryRequest, PlcDiscoveryItemHandler handler) {
List<InetAddress> possibleAddresses = new ArrayList<>();
try {
for (PcapNetworkInterface dev : Pcaps.findAllDevs()) {
logger.info("Scanning network {} for alive IP addresses", dev.getName());
final Set<InetAddress> inetAddresses = ArpUtils.scanNetworkDevice(dev);
logger.debug("Found {} addresses: {}", inetAddresses.size(), inetAddresses);
possibleAddresses.addAll(inetAddresses);
}
} catch (Throwable e) {
logger.error("Error collecting list of possible IP addresses", e);
future.complete(new DefaultPlcDiscoveryResponse(
discoveryRequest, PlcResponseCode.INTERNAL_ERROR, Collections.emptyList()));
return;
}
try {
possibleAddresses.add(InetAddress.getByName("localhost"));
} catch (UnknownHostException e) {
throw new PlcRuntimeException(e);
}
// Filter out duplicates.
possibleAddresses = possibleAddresses.stream().filter(
distinctByKey(InetAddress::getHostAddress)).collect(Collectors.toList());
Queue<PlcDiscoveryItem> discoveryItems = new ConcurrentLinkedQueue<>();
possibleAddresses.stream().parallel().forEach(possibleAddress -> {
try {
logger.info("Trying address: {}", possibleAddress);
// Try to get a connection to the given host and port.
Socket socket = new Socket(possibleAddress.getHostAddress(), ModbusConstants.MODBUSTCPDEFAULTPORT);
logger.info("Connected: {}", possibleAddress);
// Send the request to the target device
final OutputStream outputStream = socket.getOutputStream();
final InputStream inputStream = new BufferedInputStream(socket.getInputStream());
// As we not only need to provide the IP but also the unit-identifier, we need
// to iterate over all possible values until we find one that works.
// Unfortunately the way devices react to invalid requests differ:
// - My heating system doesn't sends an error response for an invalid uint-identifier
// - The Modbus Server on a S7 simply accepts any unit-identifier
// - Modbus pal only responds to correct unit-identifiers and doesn't send anything
// for invalid ones.
//
// So-far the only way I have found to reliably check if a Modbus device exists,
// was by trying to read a coil or register. Even if Modbus generally supports
// commands for diagnosing connections, it turns out none of these were actually
// supported by any device I came across. The spec is a bit unclear here, but
// it seems as if these are only supported on Serial (Modbus RTU)
// TODO: We should probably not only try to read a coil, but try any of the types and if one works, that's a match.
// Possibly we can fine tune this to speed up things.
int transactionIdentifier = 1;
for (short unitIdentifier = 1; unitIdentifier <= 247; unitIdentifier++) {
ModbusTcpADU packet = new ModbusTcpADU(transactionIdentifier++, unitIdentifier,
new ModbusPDUReadCoilsRequest(1, 1), false);
byte[] deviceIdentificationBytes = null;
try {
// Serialize the request to its byte form.
WriteBufferByteBased writeBuffer = new WriteBufferByteBased(packet.getLengthInBytes());
packet.serialize(writeBuffer);
deviceIdentificationBytes = writeBuffer.getBytes();
} catch (SerializationException e) {
logger.error("Error creating the device identification request", e);
}
if (deviceIdentificationBytes == null) {
future.complete(new DefaultPlcDiscoveryResponse(
discoveryRequest, PlcResponseCode.INTERNAL_ERROR, Collections.emptyList()));
return;
}
byte[] finalDeviceIdentificationBytes = deviceIdentificationBytes;
outputStream.write(finalDeviceIdentificationBytes);
outputStream.flush();
// Wait for a response.
byte[] responseBytes = null;
final long endTime = System.currentTimeMillis() + 100;
while (responseBytes == null) {
// If we've got enough bytes to find out the size of the packet, try to check this.
if (inputStream.available() >= 6) {
inputStream.mark(6);
inputStream.skip(4);
byte[] packetLengthBytes = new byte[2];
int bytesRead = inputStream.read(packetLengthBytes);
inputStream.reset();
// Only if we really read 2 bytes, does it make sense to continue using it.
if (bytesRead != 2) {
continue;
}
final short packetLength = (short) (ByteBuffer.wrap(packetLengthBytes).getShort() + 6);
if (inputStream.available() >= packetLength) {
responseBytes = new byte[packetLength];
bytesRead = inputStream.read(responseBytes);
if (bytesRead != packetLength) {
responseBytes = null;
break;
}
}
} else {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (System.currentTimeMillis() > endTime) {
break;
}
}
}
if (responseBytes != null) {
ReadBuffer readBuffer = new ReadBufferByteBased(responseBytes);
try {
ModbusTcpADU response = (ModbusTcpADU) ModbusTcpADU.staticParse(readBuffer, DriverType.MODBUS_TCP, true);
PlcDiscoveryItem discoveryItem;
boolean found = false;
// If we got a response telling us the address is unknown, we still know there's a
// Modbus device at the other side. In general ... as soon as we get a valid Modbus
// response, we should accept that we're talking to a Modbus device
if (response.getPdu().getErrorFlag()) {
ModbusPDUError errorPdu = (ModbusPDUError) response.getPdu();
if (errorPdu.getExceptionCode() == ModbusErrorCode.ILLEGAL_DATA_ADDRESS) {
found = true;
}
} else {
found = true;
}
if (found) {
discoveryItem = new DefaultPlcDiscoveryItem(
"modbus-tcp", "tcp", possibleAddress.getHostAddress(), Collections.singletonMap("unit-identifier", Integer.toString(unitIdentifier)), "unknown", Collections.emptyMap());
discoveryItems.add(discoveryItem);
// Give a handler the chance to react on the found device.
if (handler != null) {
handler.handle(discoveryItem);
}
break;
}
} catch (ParseException e) {
// Ignore.
}
}
}
} catch (IOException e) {
// Well this is actually sort of normal in case of us trying to connect to a non-existent device.
}
});
future.complete(new DefaultPlcDiscoveryResponse(discoveryRequest, PlcResponseCode.OK, Arrays.asList(discoveryItems.toArray(new PlcDiscoveryItem[0]))));
}
}
↑ V6046 Incorrect format. A different number of format items is expected. Arguments not used: 1.