|
package io.github.J0B10.JSR385Demo; |
|
|
|
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException; |
|
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException; |
|
import com.intelligt.modbus.jlibmodbus.master.ModbusMaster; |
|
import com.intelligt.modbus.jlibmodbus.master.ModbusMasterFactory; |
|
import com.intelligt.modbus.jlibmodbus.msg.request.ReadHoldingRegistersRequest; |
|
import com.intelligt.modbus.jlibmodbus.msg.response.ReadHoldingRegistersResponse; |
|
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters; |
|
|
|
import java.io.Closeable; |
|
import java.math.BigInteger; |
|
import java.net.InetAddress; |
|
import java.net.UnknownHostException; |
|
import java.nio.ByteBuffer; |
|
import java.util.Arrays; |
|
import java.util.concurrent.CompletableFuture; |
|
import java.util.concurrent.ExecutorService; |
|
import java.util.concurrent.Executors; |
|
import java.util.concurrent.TimeUnit; |
|
|
|
/** |
|
* Basic Implementation of a PV inverter that allows reading metering values over |
|
* <a href="https://en.wikipedia.org/wiki/Modbus">Modbus</a>. |
|
* <p> |
|
* Use {@link #requestHoldingRegisters(int, int)} to request data. |
|
* |
|
* @author Jonas Blocher |
|
*/ |
|
public class PVInverter implements Closeable { |
|
|
|
private final int unitID; |
|
|
|
private final ModbusMaster modbus; |
|
private final ExecutorService executor; |
|
|
|
public PVInverter(String ip, int port, int unitId, int timeout, boolean keepAlive) throws UnknownHostException, ModbusIOException { |
|
this.unitID = unitId; |
|
TcpParameters tcpParameters = new TcpParameters(); |
|
tcpParameters.setHost(InetAddress.getByName(ip)); |
|
tcpParameters.setPort(port); |
|
tcpParameters.setKeepAlive(keepAlive); |
|
modbus = ModbusMasterFactory.createModbusMasterTCP(tcpParameters); |
|
modbus.setResponseTimeout(timeout); |
|
modbus.connect(); |
|
executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "PVInverterModbus")); |
|
} |
|
|
|
public PVInverter(String ip, int unitID) throws ModbusIOException, UnknownHostException { |
|
this(ip, 502, unitID, 1000, true); |
|
} |
|
|
|
/** |
|
* Request data in the given registers. |
|
* |
|
* @param register first register to request data from |
|
* @param amount amount of registers (123 at max due to packet size limitations) |
|
* @return Future that represents the response. use {@link CompletableFuture#join()} |
|
* to wait for the response and retrieve it |
|
* @throws ModbusNumberException if the starting register address is invalid |
|
*/ |
|
public CompletableFuture<Response> requestHoldingRegisters(int register, int amount) throws ModbusNumberException { |
|
ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(); |
|
request.setServerAddress(unitID); |
|
request.setStartAddress(register); |
|
request.setQuantity(amount); |
|
CompletableFuture<Response> future = new CompletableFuture<>(); |
|
executor.submit(() -> { |
|
try { |
|
modbus.processRequest(request); |
|
ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) request.getResponse(); |
|
future.complete(new Response(register, response.getHoldingRegisters().getBytes())); |
|
} catch (Exception e) { |
|
future.completeExceptionally(e); |
|
} |
|
}); |
|
return future; |
|
} |
|
|
|
@Override |
|
public synchronized void close() { |
|
executor.shutdownNow(); |
|
try { |
|
//noinspection ResultOfMethodCallIgnored |
|
executor.awaitTermination(5, TimeUnit.SECONDS); |
|
} catch (InterruptedException ignored) { |
|
} |
|
try { |
|
modbus.disconnect(); |
|
} catch (ModbusIOException e) { |
|
throw new RuntimeException(e); |
|
} |
|
} |
|
|
|
public static class Response { |
|
|
|
private final int start; |
|
private final byte[] data; |
|
|
|
public Response(int start, byte[] data) { |
|
this.start = start; |
|
this.data = data; |
|
} |
|
|
|
public int getUint16(int register) { |
|
return Short.toUnsignedInt(ByteBuffer.wrap(getRegisters(register, 1)).getShort()); |
|
} |
|
|
|
public int getUint16() { |
|
return getUint16(start); |
|
} |
|
|
|
public short getInt16(int register) { |
|
return ByteBuffer.wrap(getRegisters(register, 1)).getShort(); |
|
} |
|
|
|
public short getInt16() { |
|
return getInt16(start); |
|
} |
|
|
|
public long getUint32(int register) { |
|
return Integer.toUnsignedLong(ByteBuffer.wrap(getRegisters(register, 2)).getInt()); |
|
} |
|
|
|
public long getUint32() { |
|
return getUint32(start); |
|
} |
|
|
|
public int getInt32(int register) { |
|
return ByteBuffer.wrap(getRegisters(register, 2)).getInt(); |
|
} |
|
|
|
public int getInt32() { |
|
return getInt32(start); |
|
} |
|
|
|
public BigInteger getUint64(int register) { |
|
return new BigInteger(1, getRegisters(register, 4)); |
|
} |
|
|
|
public BigInteger getUint64() { |
|
return getUint64(start); |
|
} |
|
|
|
public long getInt64(int register) { |
|
return ByteBuffer.wrap(getRegisters(register, 4)).getLong(); |
|
} |
|
|
|
public long getInt64() { |
|
return getInt64(start); |
|
} |
|
|
|
public byte[] getRegisters(int register, int amount) { |
|
amount *= 2; //each register is 2 bytes |
|
if (register < start) throw new IndexOutOfBoundsException(register); |
|
else if (amount <= 0) throw new IllegalArgumentException("amount must be positive"); |
|
else if (register + amount > start + data.length) |
|
throw new IndexOutOfBoundsException(register + amount - 1); |
|
int i = (register - start) * 2, j = i + amount; |
|
return Arrays.copyOfRange(data, i, j); |
|
} |
|
|
|
public int getRegister() { |
|
return start; |
|
} |
|
|
|
public int length() { |
|
return data.length / 2; |
|
} |
|
} |
|
} |