Skip to content

Instantly share code, notes, and snippets.

@J0B10
Last active January 17, 2022 10:24
Show Gist options
  • Save J0B10/df38af7274f0be1aba1322dd6f03fe2e to your computer and use it in GitHub Desktop.
Save J0B10/df38af7274f0be1aba1322dd6f03fe2e to your computer and use it in GitHub Desktop.
Demonstrates how the Java Units of Measurements API defined in JSR385 can help to prevent conversion errors.
package io.github.J0B10.JSR385Demo;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException;
import javax.measure.Quantity;
import javax.measure.Unit;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import javax.measure.quantity.Time;
import java.net.UnknownHostException;
import static javax.measure.MetricPrefix.MILLI;
import static tech.units.indriya.quantity.Quantities.getQuantity;
import static tech.units.indriya.unit.Units.HOUR;
import static tech.units.indriya.unit.Units.SECOND;
import static tech.units.indriya.unit.Units.WATT;
public class JSR385Demo {
private static final int
REGISTER_PV_OUT_WH = 30529, //PV total power output in Wh
REGISTER_GRID_IN_WH = 30581, //Total power draw from grid in Wh
REGISTER_GRID_OUT_WH = 30583; //Grid feed-in power total in Wh
private static final long INTERVAL = 60_000; //intervall between readings in milliseconds
private static final Unit<Energy> WATT_HOURS = WATT.multiply(HOUR).asType(Energy.class);
public static void main(String[] args) {
//gracefully close on SIGTERM (Ctrl + C)
Thread mainThread = Thread.currentThread();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
mainThread.interrupt();
try {
mainThread.join();
} catch (InterruptedException ignored) {
}
}));
try (final PVInverter inverter = new PVInverter("192.168.0.31", 3)) {
try {
long lastMetering = -1L;
while (!Thread.interrupted()) {
//read specified metering from the pv inverter
long metering = inverter.requestHoldingRegisters(REGISTER_PV_OUT_WH, 4).join().getUint32();
System.out.printf("Meter Value: %s Wh%n", metering);
if (lastMetering != -1) {
//without jsr385 (but with conversion error 😉)
logAverage(lastMetering, metering);
//with jsr385
logAverageJSR385(getQuantity(lastMetering, WATT_HOURS), getQuantity(metering, WATT_HOURS));
}
System.out.println();
lastMetering = metering;
Thread.sleep(INTERVAL);
}
} catch (InterruptedException ignored) {
}
} catch (UnknownHostException | ModbusIOException | ModbusNumberException e) {
e.printStackTrace(System.err);
}
}
private static void logAverage(long lastMetering, long metering) {
long diff = metering - lastMetering; //in Wh
double avrg = (diff * 360000.0) / INTERVAL ; //in W
System.out.printf(" Average: %.2f W%n", avrg);
}
private static void logAverageJSR385(Quantity<Energy> lastMetering, Quantity<Energy> metering) {
Quantity<Time> interval = getQuantity(INTERVAL, MILLI(SECOND));
Quantity<Energy> diff = metering.subtract(lastMetering);
Quantity<Power> avrg = diff.divide(interval).asType(Power.class).to(WATT);
System.out.printf("(JSR385) Average: %s%n", avrg);
}
}
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;
}
}
}

About

JSR385Demo.java demonstrates how the Java Units of Measurements API defined in JSR385 can help to prevent conversion errors. It fetches meter readings from an PV-Inverter over Modbus, calculates the average power output, and logs it to console.

There are two implementations for calculating the average power draw:
logAverage(long, long) uses primitive data types and is therefore simpler but error prone.
logAverageJSR385(Quantity<Energy>, Quantity<Energy>) utilizes the API to create a more robust and type safe solution.

Looking at the log:

Meter Value: 3057583 Wh

Meter Value: 3057585 Wh
          Average: 72,00 W
(JSR385)  Average: 720 W

Meter Value: 3057586 Wh
          Average: 36,00 W
(JSR385)  Average: 360 W

It is now quite obvious that there is a unit conversion error in the logAverage(long, long) function:

    private static void logAverage(long lastMetering, long metering) {
        long diff = metering - lastMetering; //in Wh
-       double avrg = (diff * 360000.0) / INTERVAL ; //in W
+       double avrg = (diff * 3600000.0) / INTERVAL ; //in W
        System.out.printf("          Average: %.2f W%n", avrg);
    }

Looking at the code it is extremly hard to spot and thats what makes it so dangerous.
Just imagine an error like this hidden in the code of a cars emergency break assistent!

Useed Libraries

unitsofmeasurement/Indriya, JSR 385 - Reference Implementation licensed under custom license
kochedykov/jlibmodbus, MODBUS protocol implementation in pure java licensed under Apache-2.0 License

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment