From b806ed45d0e4ffb25809a06caf35def404a2391e Mon Sep 17 00:00:00 2001 From: Sami Salonen Date: Thu, 26 Nov 2020 19:07:49 +0200 Subject: [PATCH] [modbus] Modbus register array backed by bytes and other simplifications (#8865) Signed-off-by: Sami Salonen --- .../e3dc/internal/dto/DataConverter.java | 49 +- .../e3dc/internal/dto/EmergencyBlock.java | 4 +- .../modbus/e3dc/internal/dto/InfoBlock.java | 9 +- .../modbus/e3dc/internal/dto/PowerBlock.java | 25 +- .../modbus/e3dc/internal/dto/StringBlock.java | 17 +- .../modbus/e3dc/internal/modbus/Parser.java | 10 +- .../modbus/e3dc/dto/DataBlockTest.java | 33 +- .../handler/E3DCHandlerStateTest.java | 16 +- .../modbus/e3dc/util/DataConverterTest.java | 60 +- .../internal/HeliosEasyControlsHandler.java | 45 +- .../internal/PreparePayloadTest.java | 71 ++ .../handler/StiebelEltronHandler.java | 12 +- .../modbus/studer/internal/StuderHandler.java | 24 +- .../internal/parser/CommonModelParser.java | 10 +- .../transport/modbus/ModbusBitUtilities.java | 686 +++++++++++++----- .../io/transport/modbus/ModbusRegister.java | 113 --- .../transport/modbus/ModbusRegisterArray.java | 102 ++- .../io/transport/modbus/ValueBuffer.java | 331 +++++++++ .../modbus/internal/ModbusLibraryWrapper.java | 7 +- .../json/WriteRequestJsonUtilities.java | 5 +- .../BitUtilitiesCommandToRegistersTest.java | 4 +- .../test/BitUtilitiesExtractBitTest.java | 135 ++++ .../test/BitUtilitiesExtractFloat32Test.java | 80 ++ ...UtilitiesExtractIndividualMethodsTest.java | 266 +++++++ .../test/BitUtilitiesExtractInt8Test.java | 113 +++ ...tilitiesExtractStateFromRegistersTest.java | 14 +- ...ilitiesExtractStringFromRegistersTest.java | 96 --- .../test/BitUtilitiesExtractStringTest.java | 136 ++++ .../modbus/test/RegisterMatcher.java | 6 +- .../io/transport/modbus/test/SmokeTest.java | 103 ++- .../modbus/test/ValueBufferTest.java | 186 +++++ .../modbus/tests/ModbusDataHandlerTest.java | 23 +- 32 files changed, 2138 insertions(+), 653 deletions(-) create mode 100644 bundles/org.openhab.binding.modbus.helioseasycontrols/src/test/java/org/openhab/binding/modbus/helioseasycontrols/internal/PreparePayloadTest.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ValueBuffer.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractBitTest.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractFloat32Test.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractInt8Test.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringTest.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/ValueBufferTest.java diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/DataConverter.java b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/DataConverter.java index 00737b38f0..2f962be621 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/DataConverter.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/DataConverter.java @@ -12,11 +12,12 @@ */ package org.openhab.binding.modbus.e3dc.internal.dto; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.BitSet; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.io.transport.modbus.ModbusBitUtilities; +import org.openhab.io.transport.modbus.ValueBuffer; /** * The {@link DataConverter} Helper class to convert bytes from modbus into desired data format @@ -25,27 +26,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class DataConverter { - private static final long MAX_INT32 = (long) Math.pow(2, Integer.SIZE); - - /** - * Get unit16 value from 2 bytes - * - * @param wrap - * @return int - */ - public static int getUInt16Value(ByteBuffer wrap) { - return Short.toUnsignedInt(wrap.getShort()); - } - - /** - * Get unit32 value from 4 bytes - * - * @param wrap - * @return long - */ - public static long getLongValue(ByteBuffer wrap) { - return Integer.toUnsignedLong(wrap.getInt()); - } /** * Get double value from 2 bytes with correction factor @@ -53,28 +33,12 @@ public class DataConverter { * @param wrap * @return double */ - public static double getUDoubleValue(ByteBuffer wrap, double factor) { - return round(getUInt16Value(wrap) * factor, 2); - } - - /** - * Conversion done according to E3DC Modbus Specification V1.7 - * - * @param wrap - * @return decoded long value, Long.MIN_VALUE otherwise - */ - public static long getInt32Swap(ByteBuffer wrap) { - long a = getUInt16Value(wrap); - long b = getUInt16Value(wrap); - if (b < 32768) { - return b * 65536 + a; - } else { - return (MAX_INT32 - b * 65536 - a) * -1; - } + public static double getUDoubleValue(ValueBuffer wrap, double factor) { + return round(wrap.getUInt16() * factor, 2); } public static String getString(byte[] bArray) { - return new String(bArray, StandardCharsets.US_ASCII).trim(); + return ModbusBitUtilities.extractStringFromBytes(bArray, 0, bArray.length, StandardCharsets.US_ASCII).trim(); } public static int toInt(BitSet bitSet) { @@ -93,8 +57,7 @@ public class DataConverter { } long factor = (long) Math.pow(10, places); - value = value * factor; - long tmp = Math.round(value); + long tmp = Math.round(value * factor); return (double) tmp / factor; } } diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/EmergencyBlock.java b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/EmergencyBlock.java index ad72d09d30..7bb27cda31 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/EmergencyBlock.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/EmergencyBlock.java @@ -14,13 +14,13 @@ package org.openhab.binding.modbus.e3dc.internal.dto; import static org.openhab.binding.modbus.e3dc.internal.modbus.E3DCModbusConstans.*; -import java.nio.ByteBuffer; import java.util.BitSet; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.modbus.e3dc.internal.modbus.Data; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; +import org.openhab.io.transport.modbus.ModbusBitUtilities; /** * The {@link EmergencyBlock} Data object for E3DC Info Block @@ -55,7 +55,7 @@ public class EmergencyBlock implements Data { */ public EmergencyBlock(byte[] bArray) { // uint16 status register 40084 - possible Status Strings are defined in Constants above - int status = DataConverter.getUInt16Value(ByteBuffer.wrap(bArray)); + int status = ModbusBitUtilities.extractUInt16(bArray, 0); if (status >= 0 && status < 5) { epStatus = EP_STATUS_ARRAY[status]; } else { diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/InfoBlock.java b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/InfoBlock.java index 34a1e06618..658049478a 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/InfoBlock.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/InfoBlock.java @@ -12,13 +12,12 @@ */ package org.openhab.binding.modbus.e3dc.internal.dto; -import java.nio.ByteBuffer; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.modbus.e3dc.internal.modbus.Data; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.StringType; import org.openhab.core.util.HexUtils; +import org.openhab.io.transport.modbus.ValueBuffer; /** * The {@link InfoBlock} Data object for E3DC Info Block @@ -43,7 +42,7 @@ public class InfoBlock implements Data { */ public InfoBlock(byte[] bArray) { // index handling to calculate the correct start index - ByteBuffer wrapper = ByteBuffer.wrap(bArray); + ValueBuffer wrapper = ValueBuffer.wrap(bArray); // first uint16 = 2 bytes - decode magic byte byte[] magicBytes = new byte[2]; @@ -52,11 +51,11 @@ public class InfoBlock implements Data { // first uint16 = 2 bytes - decode magic byte // unit8 (Modbus Major Version) + uint8 Modbus minor Version - String modbusVersion = wrapper.get() + "." + wrapper.get(); + String modbusVersion = wrapper.getSInt8() + "." + wrapper.getSInt8(); this.modbusVersion = new StringType(modbusVersion); // unit16 - supported registers - short supportedRegisters = wrapper.getShort(); + short supportedRegisters = wrapper.getSInt16(); this.supportedRegisters = new DecimalType(supportedRegisters); byte[] buffer = new byte[32]; diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/PowerBlock.java b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/PowerBlock.java index 6a86ca5032..686ed0f0b2 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/PowerBlock.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/PowerBlock.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.modbus.e3dc.internal.dto; -import java.nio.ByteBuffer; - import javax.measure.quantity.Dimensionless; import javax.measure.quantity.Power; @@ -21,6 +19,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.modbus.e3dc.internal.modbus.Data; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SmartHomeUnits; +import org.openhab.io.transport.modbus.ValueBuffer; /** * The {@link PowerBlock} Data object for E3DC Info Block @@ -49,10 +48,10 @@ public class PowerBlock implements Data { */ public PowerBlock(byte[] bArray) { // index handling to calculate the correct start index - ByteBuffer wrap = ByteBuffer.wrap(bArray); + ValueBuffer wrap = ValueBuffer.wrap(bArray); // int32_swap value = 4 byte - long pvPowerSupplyL = DataConverter.getInt32Swap(wrap); + long pvPowerSupplyL = wrap.getSInt32Swap(); /* * int32_swap value don't provide negative values! @@ -60,7 +59,7 @@ public class PowerBlock implements Data { * Negative value - Battery is discharging = Power supplier */ pvPowerSupply = QuantityType.valueOf(pvPowerSupplyL, SmartHomeUnits.WATT); - long batteryPower = DataConverter.getInt32Swap(wrap); + long batteryPower = wrap.getSInt32Swap(); if (batteryPower > 0) { // Battery is charging so Power is consumed by Battery batteryPowerSupply = QuantityType.valueOf(0, SmartHomeUnits.WATT); @@ -72,7 +71,7 @@ public class PowerBlock implements Data { } // int32_swap value = 4 byte - long householdPowerConsumptionL = DataConverter.getInt32Swap(wrap); + long householdPowerConsumptionL = wrap.getSInt32Swap(); householdPowerConsumption = QuantityType.valueOf(householdPowerConsumptionL, SmartHomeUnits.WATT); /* @@ -80,7 +79,7 @@ public class PowerBlock implements Data { * Positive value - Power provided towards Grid = Power consumer * Negative value - Power requested from Grid = Power supplier */ - long gridPower = DataConverter.getInt32Swap(wrap); + long gridPower = wrap.getSInt32Swap(); if (gridPower > 0) { // Power is provided by Grid gridPowerSupply = QuantityType.valueOf(gridPower, SmartHomeUnits.WATT); @@ -92,19 +91,19 @@ public class PowerBlock implements Data { } // int32_swap value = 4 byte - externalPowerSupply = QuantityType.valueOf(DataConverter.getInt32Swap(wrap), SmartHomeUnits.WATT); + externalPowerSupply = QuantityType.valueOf(wrap.getSInt32Swap(), SmartHomeUnits.WATT); // int32_swap value = 4 byte - wallboxPowerConsumption = QuantityType.valueOf(DataConverter.getInt32Swap(wrap), SmartHomeUnits.WATT); + wallboxPowerConsumption = QuantityType.valueOf(wrap.getSInt32Swap(), SmartHomeUnits.WATT); // int32_swap value = 4 byte - wallboxPVPowerConsumption = QuantityType.valueOf(DataConverter.getInt32Swap(wrap), SmartHomeUnits.WATT); + wallboxPVPowerConsumption = QuantityType.valueOf(wrap.getSInt32Swap(), SmartHomeUnits.WATT); // unit8 + uint8 - one register with split value for Autarky & Self Consumption - autarky = QuantityType.valueOf(wrap.get(), SmartHomeUnits.PERCENT); - selfConsumption = QuantityType.valueOf(wrap.get(), SmartHomeUnits.PERCENT); + autarky = QuantityType.valueOf(wrap.getSInt8(), SmartHomeUnits.PERCENT); + selfConsumption = QuantityType.valueOf(wrap.getSInt8(), SmartHomeUnits.PERCENT); // uint16 for Battery State of Charge - batterySOC = QuantityType.valueOf(wrap.getShort(), SmartHomeUnits.PERCENT); + batterySOC = QuantityType.valueOf(wrap.getSInt16(), SmartHomeUnits.PERCENT); } } diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/StringBlock.java b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/StringBlock.java index 8476611616..e4a1eb572b 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/StringBlock.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/dto/StringBlock.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.modbus.e3dc.internal.dto; -import java.nio.ByteBuffer; - import javax.measure.quantity.ElectricCurrent; import javax.measure.quantity.ElectricPotential; import javax.measure.quantity.Power; @@ -22,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.modbus.e3dc.internal.modbus.Data; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SmartHomeUnits; +import org.openhab.io.transport.modbus.ValueBuffer; /** * The {@link StringBlock} Data object for E3DC Info Block @@ -46,17 +45,17 @@ public class StringBlock implements Data { * @param bArray - Modbus Registers as bytes from 40096 to 40104 */ public StringBlock(byte[] bArray) { - ByteBuffer wrap = ByteBuffer.wrap(bArray); + ValueBuffer wrap = ValueBuffer.wrap(bArray); // straight forward - for each String the values Volt, Ampere and then Watt. All unt16 = 2 bytes values - string1Volt = QuantityType.valueOf(DataConverter.getUInt16Value(wrap), SmartHomeUnits.VOLT); - string2Volt = QuantityType.valueOf(DataConverter.getUInt16Value(wrap), SmartHomeUnits.VOLT); - string3Volt = QuantityType.valueOf(DataConverter.getUInt16Value(wrap), SmartHomeUnits.VOLT); + string1Volt = QuantityType.valueOf(wrap.getUInt16(), SmartHomeUnits.VOLT); + string2Volt = QuantityType.valueOf(wrap.getUInt16(), SmartHomeUnits.VOLT); + string3Volt = QuantityType.valueOf(wrap.getUInt16(), SmartHomeUnits.VOLT); // E3DC Modbus Spec chapter 3.1.2, page 16 - Ampere values shall be handled with factor 0.01 string1Ampere = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.01), SmartHomeUnits.AMPERE); string2Ampere = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.01), SmartHomeUnits.AMPERE); string3Ampere = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.01), SmartHomeUnits.AMPERE); - string1Watt = QuantityType.valueOf(DataConverter.getUInt16Value(wrap), SmartHomeUnits.WATT); - string2Watt = QuantityType.valueOf(DataConverter.getUInt16Value(wrap), SmartHomeUnits.WATT); - string3Watt = QuantityType.valueOf(DataConverter.getUInt16Value(wrap), SmartHomeUnits.WATT); + string1Watt = QuantityType.valueOf(wrap.getUInt16(), SmartHomeUnits.WATT); + string2Watt = QuantityType.valueOf(wrap.getUInt16(), SmartHomeUnits.WATT); + string3Watt = QuantityType.valueOf(wrap.getUInt16(), SmartHomeUnits.WATT); } } diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/modbus/Parser.java b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/modbus/Parser.java index e1524f44c4..645f67686f 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/modbus/Parser.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/main/java/org/openhab/binding/modbus/e3dc/internal/modbus/Parser.java @@ -25,7 +25,6 @@ import org.openhab.binding.modbus.e3dc.internal.dto.StringBlock; import org.openhab.binding.modbus.e3dc.internal.dto.WallboxArray; import org.openhab.binding.modbus.e3dc.internal.modbus.Data.DataType; import org.openhab.io.transport.modbus.AsyncModbusReadResult; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,17 +58,10 @@ public class Parser { } public void handle(AsyncModbusReadResult result) { - byte[] newArray = new byte[size]; long startTime = System.currentTimeMillis(); Optional opt = result.getRegisters(); if (opt.isPresent()) { - ModbusRegisterArray registers = opt.get(); - int i = 0; - for (ModbusRegister reg : registers) { - System.arraycopy(reg.getBytes(), 0, newArray, i, 2); - i += 2; - } - setArray(newArray); + setArray(opt.get().getBytes()); long duration = System.currentTimeMillis() - startTime; avgDuration += duration; diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/dto/DataBlockTest.java b/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/dto/DataBlockTest.java index 460c9c9b04..00a8ada384 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/dto/DataBlockTest.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/dto/DataBlockTest.java @@ -35,14 +35,26 @@ import org.openhab.core.library.types.OnOffType; */ public class DataBlockTest { private Parser mc; + private Parser mcNegativePVSupply; @BeforeEach public void setup() { - byte[] dataBlock = new byte[] { 0, -14, 0, 0, -2, -47, -1, -1, 2, 47, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 99, 99, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, - 125, 2, 21, 0, 0, 0, 27, 0, 26, 0, 0, 0, 103, 0, -117, 0, 0 }; - mc = new Parser(DataType.DATA); - mc.setArray(dataBlock); + { + byte[] dataBlock = new byte[] { 0, -14, 0, 0, -2, -47, -1, -1, 2, 47, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 99, 99, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 125, 2, 21, 0, 0, 0, 27, 0, 26, 0, 0, 0, 103, 0, -117, 0, 0 }; + mc = new Parser(DataType.DATA); + mc.setArray(dataBlock); + } + { + // 65098 bytes [-2, 74] + // 65535 bytes [-1, -1] + byte[] dataBlock = new byte[] { -2, -74, -1, -1, -2, -47, -1, -1, 2, 47, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 99, 99, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 125, 2, 21, 0, 0, 0, 27, 0, 26, 0, 0, 0, 103, 0, -117, 0, 0 }; + mcNegativePVSupply = new Parser(DataType.DATA); + mcNegativePVSupply.setArray(dataBlock); + } } @Test @@ -56,6 +68,17 @@ public class DataBlockTest { assertEquals("303.0 W", b.batteryPowerSupply.toString(), "Battery Supply"); } + @Test + public void testValidPowerBlockNegativePVSupply() { + Optional dataOpt = mcNegativePVSupply.parse(DataType.POWER); + assertTrue(dataOpt.isPresent()); + PowerBlock b = (PowerBlock) dataOpt.get(); + assertEquals("-330.0 W", b.pvPowerSupply.toString(), "PV Supply"); + assertEquals("14.0 W", b.gridPowerSupply.toString(), "Grid Supply"); + assertEquals("0.0 W", b.gridPowerConsumpition.toString(), "Grid Consumption"); + assertEquals("303.0 W", b.batteryPowerSupply.toString(), "Battery Supply"); + } + @Test public void testValidWallboxBlock() { Optional wba = mc.parse(DataType.WALLBOX); diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/internal/handler/E3DCHandlerStateTest.java b/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/internal/handler/E3DCHandlerStateTest.java index dce81bf59f..ead7e6787c 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/internal/handler/E3DCHandlerStateTest.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/internal/handler/E3DCHandlerStateTest.java @@ -14,7 +14,6 @@ package org.openhab.binding.modbus.e3dc.internal.handler; import static org.mockito.Mockito.*; -import java.nio.ByteBuffer; import java.util.HashMap; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -31,7 +30,6 @@ import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.io.transport.modbus.AsyncModbusFailure; import org.openhab.io.transport.modbus.AsyncModbusReadResult; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; /** @@ -92,26 +90,16 @@ public class E3DCHandlerStateTest { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 78, 73, 78, 73, 84, 73, 65, 76, 73, 90, 69, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 49, 48, 95, 50, 48, 50, 48, 95, 48, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - ByteBuffer infoWrap = ByteBuffer.wrap(infoBlockBytes); - ModbusRegister[] infoBlock = new ModbusRegister[infoBlockBytes.length / 2]; - for (int i = 0; i < infoBlock.length; i++) { - infoBlock[i] = new ModbusRegister(infoWrap.get(), infoWrap.get()); - } ModbusReadRequestBlueprint readRequest = mock(ModbusReadRequestBlueprint.class); - return new AsyncModbusReadResult(readRequest, new ModbusRegisterArray(infoBlock)); + return new AsyncModbusReadResult(readRequest, new ModbusRegisterArray(infoBlockBytes)); } private AsyncModbusReadResult getDataResult() { byte[] dataBlockBytes = new byte[] { 0, -14, 0, 0, -2, -47, -1, -1, 2, 47, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 99, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 125, 2, 21, 0, 0, 0, 27, 0, 26, 0, 0, 0, 103, 0, -117, 0, 0 }; - ByteBuffer dataWrap = ByteBuffer.wrap(dataBlockBytes); - ModbusRegister[] dataBlock = new ModbusRegister[dataBlockBytes.length / 2]; - for (int i = 0; i < dataBlock.length; i++) { - dataBlock[i] = new ModbusRegister(dataWrap.get(), dataWrap.get()); - } ModbusReadRequestBlueprint readRequest = mock(ModbusReadRequestBlueprint.class); - return new AsyncModbusReadResult(readRequest, new ModbusRegisterArray(dataBlock)); + return new AsyncModbusReadResult(readRequest, new ModbusRegisterArray(dataBlockBytes)); } private AsyncModbusFailure getFailResult() { diff --git a/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/util/DataConverterTest.java b/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/util/DataConverterTest.java index bac8734812..bef20dc55c 100644 --- a/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/util/DataConverterTest.java +++ b/bundles/org.openhab.binding.modbus.e3dc/src/test/java/org/openhab/binding/modbus/e3dc/util/DataConverterTest.java @@ -14,12 +14,12 @@ package org.openhab.binding.modbus.e3dc.util; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.nio.ByteBuffer; import java.util.BitSet; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; import org.openhab.binding.modbus.e3dc.internal.dto.DataConverter; +import org.openhab.io.transport.modbus.ValueBuffer; /** * The {@link DataConverterTest} Test data conversions @@ -30,17 +30,58 @@ import org.openhab.binding.modbus.e3dc.internal.dto.DataConverter; public class DataConverterTest { @Test - public void testE3DCSwapValueNegative() { - // Reg 69 value 65098 bytes [-2, 74] - // Reg 70 value 65535 bytes [-1, -1] - byte[] b = new byte[] { -2, -74, -1, -1 }; - assertEquals(-330, DataConverter.getInt32Swap(ByteBuffer.wrap(b)), "Negative Value"); + public void testRoundPositive() { + assertEquals(2.3, DataConverter.round(2.34, 1), 0.01); } @Test - public void testBitset() { + public void testRoundPositive2() { + assertEquals(2.4, DataConverter.round(2.37, 1), 0.01); + } + + @Test + public void testRoundPositive3() { + assertEquals(2.4, DataConverter.round(2.35, 1), 0.01); + } + + @Test + public void testRoundNegative() { + assertEquals(-2.3, DataConverter.round(-2.34, 1), 0.01); + } + + @Test + public void testRoundNegative2() { + assertEquals(-2.4, DataConverter.round(-2.37, 1), 0.01); + } + + @Test + public void testRoundNegative3() { + // rounding towards positive infinity. Note difference to testRoundPositive3 + assertEquals(-2.3, DataConverter.round(-2.35, 1), 0.01); + } + + @Test + public void testUDoubleValue() { + assertEquals(0.5, DataConverter.getUDoubleValue(ValueBuffer.wrap(new byte[] { 0, 5 }), 0.1), 0.01); + } + + @Test + public void testUDoubleValue2() { + assertEquals(6159.9, + DataConverter.getUDoubleValue(ValueBuffer.wrap(new byte[] { (byte) 0xf0, (byte) 0x9f }), 0.1), 0.01); + } + + @Test + public void testUDoubleValue3() { + assertEquals(123198, + DataConverter.getUDoubleValue(ValueBuffer.wrap(new byte[] { (byte) 0xf0, (byte) 0x9f }), 2), 0.01); + } + + @Test + public void testBitsetToInt() { byte[] b = new byte[] { 3, 16 }; BitSet s = BitSet.valueOf(b); + // Bit0 is the least significant bit to DataConverter.toInt assertEquals(true, s.get(0), "Bit0"); assertEquals(true, s.get(1), "Bit1"); assertEquals(false, s.get(2), "Bit2"); @@ -57,5 +98,10 @@ public class DataConverterTest { assertEquals(false, s.get(13), "Bit13"); assertEquals(false, s.get(14), "Bit14"); assertEquals(false, s.get(15), "Bit15"); + + int bitsAsInt = DataConverter.toInt(s); + int expected = 0b0001000000000011; + assertEquals(Integer.toBinaryString(expected), Integer.toBinaryString(bitsAsInt)); + assertEquals(expected, bitsAsInt); } } diff --git a/bundles/org.openhab.binding.modbus.helioseasycontrols/src/main/java/org/openhab/binding/modbus/helioseasycontrols/internal/HeliosEasyControlsHandler.java b/bundles/org.openhab.binding.modbus.helioseasycontrols/src/main/java/org/openhab/binding/modbus/helioseasycontrols/internal/HeliosEasyControlsHandler.java index f15ba70418..f5092ef381 100644 --- a/bundles/org.openhab.binding.modbus.helioseasycontrols/src/main/java/org/openhab/binding/modbus/helioseasycontrols/internal/HeliosEasyControlsHandler.java +++ b/bundles/org.openhab.binding.modbus.helioseasycontrols/src/main/java/org/openhab/binding/modbus/helioseasycontrols/internal/HeliosEasyControlsHandler.java @@ -54,7 +54,6 @@ import org.openhab.io.transport.modbus.ModbusBitUtilities; import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; @@ -429,9 +428,8 @@ public class HeliosEasyControlsHandler extends BaseThingHandler { lock.acquire(); comms.submitOneTimeWrite( new ModbusWriteRegisterRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID, - HeliosEasyControlsBindingConstants.START_ADDRESS, - new ModbusRegisterArray(preparePayload(payload)), true, - HeliosEasyControlsBindingConstants.MAX_TRIES), + HeliosEasyControlsBindingConstants.START_ADDRESS, preparePayload(payload), + true, HeliosEasyControlsBindingConstants.MAX_TRIES), result -> { lock.release(); updateStatus(ThingStatus.ONLINE); @@ -477,8 +475,7 @@ public class HeliosEasyControlsHandler extends BaseThingHandler { String payload = v.getVariableString(); comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint( HeliosEasyControlsBindingConstants.UNIT_ID, HeliosEasyControlsBindingConstants.START_ADDRESS, - new ModbusRegisterArray(preparePayload(payload)), true, - HeliosEasyControlsBindingConstants.MAX_TRIES), result -> { + preparePayload(payload), true, HeliosEasyControlsBindingConstants.MAX_TRIES), result -> { comms.submitOneTimePoll( new ModbusReadRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, @@ -690,25 +687,21 @@ public class HeliosEasyControlsHandler extends BaseThingHandler { * @param payload The String representation of the payload * @return The Register representation of the payload */ - private ModbusRegister[] preparePayload(String payload) { - + private static ModbusRegisterArray preparePayload(String payload) { // determine number of registers - int l = (payload.length() + 1) / 2; // +1 because we need to include at least one termination symbol 0x00 - if ((payload.length() + 1) % 2 != 0) { - l++; - } - - ModbusRegister reg[] = new ModbusRegister[l]; - byte[] b = payload.getBytes(); - int ch = 0; - for (int i = 0; i < reg.length; i++) { - byte b1 = ch < b.length ? b[ch] : (byte) 0x00; // terminate with 0x00 if at the end of the payload - ch++; - byte b2 = ch < b.length ? b[ch] : (byte) 0x00; - ch++; - reg[i] = new ModbusRegister(b1, b2); - } - return reg; + byte[] asciiBytes = payload.getBytes(StandardCharsets.US_ASCII); + int bufferLength = asciiBytes.length // ascii characters + + 1 // NUL byte + + ((asciiBytes.length % 2 == 0) ? 1 : 0); // to have even number of bytes + assert bufferLength % 2 == 0; // Invariant, ensured above + + byte[] buffer = new byte[bufferLength]; + System.arraycopy(asciiBytes, 0, buffer, 0, asciiBytes.length); + // Fill in rest of bytes with NUL bytes + for (int i = asciiBytes.length; i < buffer.length; i++) { + buffer[i] = '\0'; + } + return new ModbusRegisterArray(buffer); } /** @@ -718,8 +711,8 @@ public class HeliosEasyControlsHandler extends BaseThingHandler { * @return The value or null if an error occurred */ private void processResponse(HeliosVariable v, ModbusRegisterArray registers) { - String r = ModbusBitUtilities - .extractStringFromRegisters(registers, 0, registers.size() * 2, StandardCharsets.US_ASCII).toString(); + String r = ModbusBitUtilities.extractStringFromRegisters(registers, 0, registers.size() * 2, + StandardCharsets.US_ASCII); String[] parts = r.split("=", 2); // remove the part "vXXXX=" from the string // making sure we have a proper response and the response matches the requested variable if ((parts.length == 2) && (v.getVariableString().equals(parts[0]))) { diff --git a/bundles/org.openhab.binding.modbus.helioseasycontrols/src/test/java/org/openhab/binding/modbus/helioseasycontrols/internal/PreparePayloadTest.java b/bundles/org.openhab.binding.modbus.helioseasycontrols/src/test/java/org/openhab/binding/modbus/helioseasycontrols/internal/PreparePayloadTest.java new file mode 100644 index 0000000000..a3d3d8d80b --- /dev/null +++ b/bundles/org.openhab.binding.modbus.helioseasycontrols/src/test/java/org/openhab/binding/modbus/helioseasycontrols/internal/PreparePayloadTest.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.helioseasycontrols.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.io.transport.modbus.ModbusRegisterArray; + +/** + * @author Sami Salonen - Initial contribution + */ +public class PreparePayloadTest { + + private Method preparePayloadMethod; + + public PreparePayloadTest() throws NoSuchMethodException, SecurityException { + preparePayloadMethod = HeliosEasyControlsHandler.class.getDeclaredMethod("preparePayload", String.class); + preparePayloadMethod.setAccessible(true); + } + + private ModbusRegisterArray preparePayload(String payload) { + try { + return (ModbusRegisterArray) preparePayloadMethod.invoke(null, payload); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + fail("Reflection failure:" + e.getMessage()); + throw new RuntimeException(); // to make compiler happy + } + } + + public static Collection data() { + return Collections.unmodifiableList(Stream + // Due to nul byte, full register full of nul bytes added + .of(new Object[] { "v00020=a", new ModbusRegisterArray(0x7630, 0x3030, 0x3230, 0x3d61, 0x0000) }, + new Object[] { "v00020=aa", new ModbusRegisterArray(0x7630, 0x3030, 0x3230, 0x3d61, 0x6100) }) + .collect(Collectors.toList())); + } + + @ParameterizedTest + @MethodSource("data") + public void testPreparePayload(String payload, ModbusRegisterArray expectedRegisters) { + ModbusRegisterArray actual = preparePayload(payload); + + assertEquals(actual.size(), expectedRegisters.size(), String.format("payload=%s", payload)); + for (int i = 0; i < expectedRegisters.size(); i++) { + int expectedRegisterDataUnsigned = expectedRegisters.getRegister(i); + int actualUnsigned = actual.getRegister(i); + + assertEquals(expectedRegisterDataUnsigned, actualUnsigned, + String.format("register index i=%d, payload=%s", i, payload)); + } + } +} diff --git a/bundles/org.openhab.binding.modbus.stiebeleltron/src/main/java/org/openhab/binding/modbus/stiebeleltron/internal/handler/StiebelEltronHandler.java b/bundles/org.openhab.binding.modbus.stiebeleltron/src/main/java/org/openhab/binding/modbus/stiebeleltron/internal/handler/StiebelEltronHandler.java index de5a7c63be..0913ae248a 100644 --- a/bundles/org.openhab.binding.modbus.stiebeleltron/src/main/java/org/openhab/binding/modbus/stiebeleltron/internal/handler/StiebelEltronHandler.java +++ b/bundles/org.openhab.binding.modbus.stiebeleltron/src/main/java/org/openhab/binding/modbus/stiebeleltron/internal/handler/StiebelEltronHandler.java @@ -14,8 +14,7 @@ package org.openhab.binding.modbus.stiebeleltron.internal.handler; import static org.openhab.binding.modbus.stiebeleltron.internal.StiebelEltronBindingConstants.*; import static org.openhab.core.library.unit.SIUnits.CELSIUS; -import static org.openhab.core.library.unit.SmartHomeUnits.KILOWATT_HOUR; -import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT; +import static org.openhab.core.library.unit.SmartHomeUnits.*; import java.util.Optional; @@ -52,7 +51,6 @@ import org.openhab.io.transport.modbus.AsyncModbusFailure; import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint; @@ -198,11 +196,9 @@ public class StiebelEltronHandler extends BaseThingHandler { throw new IllegalStateException("registerPollTask called without proper configuration"); } // big endian byte ordering - byte b1 = (byte) (shortValue >> 8); - byte b2 = (byte) shortValue; - - ModbusRegister register = new ModbusRegister(b1, b2); - ModbusRegisterArray data = new ModbusRegisterArray(new ModbusRegister[] { register }); + byte hi = (byte) (shortValue >> 8); + byte lo = (byte) shortValue; + ModbusRegisterArray data = new ModbusRegisterArray(hi, lo); ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data, false, myconfig.getMaxTries()); diff --git a/bundles/org.openhab.binding.modbus.studer/src/main/java/org/openhab/binding/modbus/studer/internal/StuderHandler.java b/bundles/org.openhab.binding.modbus.studer/src/main/java/org/openhab/binding/modbus/studer/internal/StuderHandler.java index 3a0d97412f..3e291b702c 100644 --- a/bundles/org.openhab.binding.modbus.studer/src/main/java/org/openhab/binding/modbus/studer/internal/StuderHandler.java +++ b/bundles/org.openhab.binding.modbus.studer/src/main/java/org/openhab/binding/modbus/studer/internal/StuderHandler.java @@ -27,6 +27,7 @@ import org.openhab.binding.modbus.studer.internal.StuderParser.ModeXtender; import org.openhab.binding.modbus.studer.internal.StuderParser.VSMode; import org.openhab.binding.modbus.studer.internal.StuderParser.VTMode; import org.openhab.binding.modbus.studer.internal.StuderParser.VTType; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; @@ -43,7 +44,9 @@ import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.ModbusBitUtilities; import org.openhab.io.transport.modbus.ModbusCommunicationInterface; +import org.openhab.io.transport.modbus.ModbusConstants.ValueType; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegisterArray; @@ -303,22 +306,21 @@ public class StuderHandler extends BaseThingHandler { * @param registers byte array read from the modbus slave */ protected void handlePolledData(int registerNumber, ModbusRegisterArray registers) { - String hexString = registers.toHexString().toString(); - Float quantity = parser.hexToFloat(hexString); - if (quantity != null) { + Optional quantity = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.FLOAT32); + quantity.ifPresent(value -> { if (type.equals(THING_TYPE_BSP)) { Unit unit = UNIT_CHANNELS_BSP.get(registerNumber); if (unit != null) { - internalUpdateState(CHANNELS_BSP.get(registerNumber), new QuantityType<>(quantity, unit)); + internalUpdateState(CHANNELS_BSP.get(registerNumber), new QuantityType<>(value, unit)); } } else if (type.equals(THING_TYPE_XTENDER)) { - handlePolledDataXtender(registerNumber, quantity); + handlePolledDataXtender(registerNumber, value); } else if (type.equals(THING_TYPE_VARIOTRACK)) { - handlePolledDataVarioTrack(registerNumber, quantity); + handlePolledDataVarioTrack(registerNumber, value); } else if (type.equals(THING_TYPE_VARIOSTRING)) { - handlePolledDataVarioString(registerNumber, quantity); + handlePolledDataVarioString(registerNumber, value); } - } + }); resetCommunicationError(); } @@ -327,7 +329,7 @@ public class StuderHandler extends BaseThingHandler { * The register array is first parsed, then each of the channels are updated * to the new values */ - protected void handlePolledDataVarioString(int registerNumber, Float quantity) { + protected void handlePolledDataVarioString(int registerNumber, DecimalType quantity) { switch (CHANNELS_VARIOSTRING.get(registerNumber)) { case CHANNEL_PV_OPERATING_MODE: case CHANNEL_PV1_OPERATING_MODE: @@ -356,7 +358,7 @@ public class StuderHandler extends BaseThingHandler { * The register array is first parsed, then each of the channels are updated * to the new values */ - protected void handlePolledDataVarioTrack(int registerNumber, Float quantity) { + protected void handlePolledDataVarioTrack(int registerNumber, DecimalType quantity) { switch (CHANNELS_VARIOTRACK.get(registerNumber)) { case CHANNEL_MODEL_VARIOTRACK: VTType type = StuderParser.getVTTypeByCode(quantity.intValue()); @@ -393,7 +395,7 @@ public class StuderHandler extends BaseThingHandler { * The register array is first parsed, then each of the channels are updated * to the new values */ - protected void handlePolledDataXtender(int registerNumber, Float quantity) { + protected void handlePolledDataXtender(int registerNumber, DecimalType quantity) { switch (CHANNELS_XTENDER.get(registerNumber)) { case CHANNEL_OPERATING_STATE: ModeXtender mode = StuderParser.getModeXtenderByCode(quantity.intValue()); diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/CommonModelParser.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/CommonModelParser.java index fb20e82271..17d1ddc2f7 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/CommonModelParser.java +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/CommonModelParser.java @@ -52,12 +52,10 @@ public class CommonModelParser extends AbstractBaseParser implements SunspecPars } // parse manufacturer, model and version - block.manufacturer = ModbusBitUtilities.extractStringFromRegisters(raw, 2, 32, Charset.forName("UTF-8")) - .toString(); - block.model = ModbusBitUtilities.extractStringFromRegisters(raw, 18, 32, Charset.forName("UTF-8")).toString(); - block.version = ModbusBitUtilities.extractStringFromRegisters(raw, 42, 16, Charset.forName("UTF-8")).toString(); - block.serialNumber = ModbusBitUtilities.extractStringFromRegisters(raw, 50, 32, Charset.forName("UTF-8")) - .toString(); + block.manufacturer = ModbusBitUtilities.extractStringFromRegisters(raw, 2, 32, Charset.forName("UTF-8")); + block.model = ModbusBitUtilities.extractStringFromRegisters(raw, 18, 32, Charset.forName("UTF-8")); + block.version = ModbusBitUtilities.extractStringFromRegisters(raw, 42, 16, Charset.forName("UTF-8")); + block.serialNumber = ModbusBitUtilities.extractStringFromRegisters(raw, 50, 32, Charset.forName("UTF-8")); block.deviceAddress = extractUInt16(raw, 66, 1); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java index 6980eb68a5..b55c4eeb7e 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java @@ -14,8 +14,6 @@ package org.openhab.io.transport.modbus; import java.math.BigDecimal; import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.nio.charset.Charset; import java.util.Optional; @@ -24,8 +22,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; -import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; +import org.openhab.io.transport.modbus.ModbusConstants.ValueType; /** * Utilities for working with binary data. @@ -101,165 +99,522 @@ public class ModbusBitUtilities { */ public static Optional extractStateFromRegisters(ModbusRegisterArray registers, int index, ModbusConstants.ValueType type) { - int endBitIndex = (type.getBits() >= 16 ? 16 * index : type.getBits() * index) + type.getBits() - 1; - // each register has 16 bits - int lastValidIndex = registers.size() * 16 - 1; - if (endBitIndex > lastValidIndex || index < 0) { - throw new IllegalArgumentException( - String.format("Index=%d with type=%s is out-of-bounds given registers of size %d", index, type, - registers.size())); - } + byte[] bytes = registers.getBytes(); switch (type) { case BIT: - return Optional - .of(new DecimalType((registers.getRegister(index / 16).toUnsignedShort() >> (index % 16)) & 1)); - case INT8: - return Optional.of(new DecimalType(registers.getRegister(index / 2).getBytes()[1 - (index % 2)])); - case UINT8: - return Optional.of(new DecimalType( - (registers.getRegister(index / 2).toUnsignedShort() >> (8 * (index % 2))) & 0xff)); - case INT16: { - ByteBuffer buff = ByteBuffer.allocate(2); - buff.put(registers.getRegister(index).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getShort(0))); + return Optional.of(new DecimalType(extractBit(bytes, index))); + case INT8: { + int registerIndex = index / 2; + boolean hiByte = index % 2 == 1; + return Optional.of(new DecimalType(extractSInt8(bytes, registerIndex, hiByte))); } - case UINT16: - return Optional.of(new DecimalType(registers.getRegister(index).toUnsignedShort())); - case INT32: { - ByteBuffer buff = ByteBuffer.allocate(4); - buff.put(registers.getRegister(index).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getInt(0))); - } - case UINT32: { - ByteBuffer buff = ByteBuffer.allocate(8); - buff.position(4); - buff.put(registers.getRegister(index).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0))); + case UINT8: { + int registerIndex = index / 2; + boolean hiByte = index % 2 == 1; + return Optional.of(new DecimalType(extractUInt8(bytes, registerIndex, hiByte))); } - case FLOAT32: { - ByteBuffer buff = ByteBuffer.allocate(4); - buff.put(registers.getRegister(index).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); + case INT16: + return Optional.of(new DecimalType(extractSInt16(bytes, index * 2))); + case UINT16: + return Optional.of(new DecimalType(extractUInt16(bytes, index * 2))); + case INT32: + return Optional.of(new DecimalType(extractSInt32(bytes, index * 2))); + case UINT32: + return Optional.of(new DecimalType(extractUInt32(bytes, index * 2))); + case FLOAT32: try { - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getFloat(0))); + return Optional.of(new DecimalType(extractFloat32(bytes, index * 2))); } catch (NumberFormatException e) { // floating point NaN or infinity encountered return Optional.empty(); } - } - case INT64: { - ByteBuffer buff = ByteBuffer.allocate(8); - buff.put(registers.getRegister(index).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index + 2).getBytes()); - buff.put(registers.getRegister(index + 3).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0))); - } - case UINT64: { - ByteBuffer buff = ByteBuffer.allocate(8); - buff.put(registers.getRegister(index).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index + 2).getBytes()); - buff.put(registers.getRegister(index + 3).getBytes()); - return Optional.of( - new DecimalType(new BigDecimal(new BigInteger(1, buff.order(ByteOrder.BIG_ENDIAN).array())))); - } - case INT32_SWAP: { - ByteBuffer buff = ByteBuffer.allocate(4); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getInt(0))); - } - case UINT32_SWAP: { - ByteBuffer buff = ByteBuffer.allocate(8); - buff.position(4); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0))); - } - case FLOAT32_SWAP: { - ByteBuffer buff = ByteBuffer.allocate(4); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index).getBytes()); + case INT64: + return Optional.of(new DecimalType(extractSInt64(bytes, index * 2))); + case UINT64: + return Optional.of(new DecimalType(new BigDecimal(extractUInt64(bytes, index * 2)))); + case INT32_SWAP: + return Optional.of(new DecimalType(extractSInt32Swap(bytes, index * 2))); + case UINT32_SWAP: + return Optional.of(new DecimalType(extractUInt32Swap(bytes, index * 2))); + case FLOAT32_SWAP: try { - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getFloat(0))); + return Optional.of(new DecimalType(extractFloat32Swap(bytes, index * 2))); } catch (NumberFormatException e) { // floating point NaN or infinity encountered return Optional.empty(); } - } - case INT64_SWAP: { - ByteBuffer buff = ByteBuffer.allocate(8); - buff.put(registers.getRegister(index + 3).getBytes()); - buff.put(registers.getRegister(index + 2).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index).getBytes()); - return Optional.of(new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0))); - } - case UINT64_SWAP: { - ByteBuffer buff = ByteBuffer.allocate(8); - buff.put(registers.getRegister(index + 3).getBytes()); - buff.put(registers.getRegister(index + 2).getBytes()); - buff.put(registers.getRegister(index + 1).getBytes()); - buff.put(registers.getRegister(index).getBytes()); - return Optional.of( - new DecimalType(new BigDecimal(new BigInteger(1, buff.order(ByteOrder.BIG_ENDIAN).array())))); - } + case INT64_SWAP: + return Optional.of(new DecimalType(extractSInt64Swap(bytes, index * 2))); + case UINT64_SWAP: + return Optional.of(new DecimalType(new BigDecimal(extractUInt64Swap(bytes, index * 2)))); default: throw new IllegalArgumentException(type.getConfigValue()); } } + private static void assertIndexAndType(byte[] bytes, int index, ValueType type) { + int typeBits = type.getBits(); + // for 8-bit types and larger, index specifies the index of the byte. For bits, index specifies the index of the + // bit (of the whole data) + int indexPositionAsBitIndex = Math.min(type.getBits(), 8) * index; + int endBitIndex = indexPositionAsBitIndex + typeBits - 1; + int lastValidIndex = bytes.length * 8 - 1; + if (endBitIndex > lastValidIndex || index < 0) { + throw new IllegalArgumentException( + String.format("Index=%d with type=%s is out-of-bounds given registers of size %d ", index, type, + bytes.length / 2)); + } + } + + /** + * Extract single bit from registers represented by sequence of bytes + * + * - indices between 0...15 (inclusive) represent bits of the first register + * - indices between 16...31 (inclusive) represent bits of the second register, etc. + * - index 0 refers to the least significant bit of the first register + * - index 1 refers to the second least significant bit of the first register, etc. + * + * @param bytes registers represented by sequence of bytes + * @param index index of bit + * @return 0 when bit is set, 1 otherwise + * @throws IllegalArgumentException when index is out of bounds + */ + public static int extractBit(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.BIT); + int registerIndex = index / 16; + int bitIndexWithinRegister = index % 16; + return extractBit(bytes, registerIndex, bitIndexWithinRegister); + } + + /** + * Extract single bit from registers represented by sequence of bytes + * + * bitIndexWithinRegister between 0...15 (inclusive) represent bits of the first register, where 0 refers to the + * least significant bit of the register, index 1 refers to the second least significant bit of the register, etc. + * + * @param bytes registers represented by sequence of bytes + * @param registerIndex index of register. First register has index of 0. + * @param bitIndexWithinRegister bit index within the register + * @return 0 when bit is set, 1 otherwise + * @throws IllegalArgumentException when registerIndex and/or bitIndexWithinRegister is out of bounds + */ + public static int extractBit(byte[] bytes, int registerIndex, int bitIndexWithinRegister) { + if (bitIndexWithinRegister < 0 || bitIndexWithinRegister > 15) { + throw new IllegalArgumentException( + String.format("bitIndexWithinRegister=%d is out-of-bounds (max 15)", bitIndexWithinRegister)); + } else if (registerIndex < 0) { + throw new IllegalArgumentException( + String.format("registerIndex=%d is out-of-bounds", bitIndexWithinRegister)); + } + boolean hiByte = bitIndexWithinRegister >= 8; + int indexWithinByte = bitIndexWithinRegister % 8; + int byteIndex = 2 * registerIndex + (hiByte ? 0 : 1); + if (byteIndex >= bytes.length) { + throw new IllegalArgumentException(String.format( + "registerIndex=%d, bitIndexWithinRegister=%d is out-of-bounds with registers of size %d", + registerIndex, bitIndexWithinRegister, bytes.length / 2)); + } + return ((bytes[byteIndex] >>> indexWithinByte) & 1); + } + + /** + * Extract signed 8-bit integer (byte) from registers represented by sequence of bytes + * + * @param bytes registers represented by sequence of bytes + * @param registerIndex index of register. First register has index of 0. + * @param hiByte whether to extract hi byte or lo byte + * @return 0 when bit is set, 1 otherwise + * @throws IllegalArgumentException when index is out of bounds + */ + public static byte extractSInt8(byte[] bytes, int registerIndex, boolean hiByte) { + int byteIndex = 2 * registerIndex + (hiByte ? 0 : 1); + byte signed = extractSInt8(bytes, byteIndex); + return signed; + } + + /** + * Extract signed 8-bit integer (byte) from registers represented by sequence of bytes + * + * - index 0 refers to low byte of the first register, 1 high byte of first register + * - index 2 refers to low byte of the second register, 3 high byte of second register, etc. + * - it is assumed that each high and low byte is encoded in most significant bit first order + * + * @param bytes registers represented by sequence of bytes + * @param registerIndex index of register. First register has index of 0. + * @param index index of the byte in registers + * @return 0 when bit is set, 1 otherwise + * @throws IllegalArgumentException when index is out of bounds + */ + public static byte extractSInt8(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.INT8); + byte signed = bytes[index]; + return signed; + } + + /** + * Extract unsigned 8-bit integer (byte) from registers represented by sequence of bytes + * + * @param bytes registers represented by sequence of bytes + * @param registerIndex index of register. First register has index of 0. + * @param hiByte whether to extract hi byte or lo byte + * @return 0 when bit is set, 1 otherwise + * @throws IllegalArgumentException when registerIndex is out of bounds + */ + public static short extractUInt8(byte[] bytes, int registerIndex, boolean hiByte) { + int byteIndex = 2 * registerIndex + (hiByte ? 0 : 1); + short unsigned = extractUInt8(bytes, byteIndex); + return unsigned; + } + + /** + * Extract unsigned 8-bit integer (byte) from registers represented by sequence of bytes + * + * - index 0 refers to low byte of the first register, 1 high byte of first register + * - index 2 refers to low byte of the second register, 3 high byte of second register, etc. + * - it is assumed that each high and low byte is encoded in most significant bit first order + * + * @param bytes registers represented by sequence of bytes + * @param registerIndex index of register. First register has index of 0. + * @param index index of the byte in registers + * @return 0 when bit is set, 1 otherwise + * @throws IllegalArgumentException when index is out of bounds + */ + public static short extractUInt8(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.UINT8); + int signed = extractSInt8(bytes, index); + short unsigned = (short) (signed & 0xff); + assert unsigned >= 0; + return unsigned; + } + + /** + * Extract signed 16-bit integer (short) from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order + * + * @param bytes registers represented by sequence of bytes + * @param index index of register. First register has index of 0. + * @return register with index interpreted as 16 bit signed integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static short extractSInt16(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.INT16); + int hi = (bytes[index] & 0xff); + int lo = (bytes[index + 1] & 0xff); + short signed = (short) ((hi << 8) | lo); + return signed; + } + + /** + * Extract unsigned 16-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order + * + * @param bytes registers represented by sequence of bytes + * @param index index of register. First register has index of 0. + * @return register with index interpreted as 16 bit unsigned integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static int extractUInt16(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.UINT16); + int signed = extractSInt16(bytes, index); + int unsigned = signed & 0xffff; + assert unsigned >= 0; + return unsigned; + } + + /** + * Extract signed 32-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index) and (index+1) interpreted as 32 bit signed integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static int extractSInt32(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.INT32); + int hi1 = bytes[index + 0] & 0xff; + int lo1 = bytes[index + 1] & 0xff; + int hi2 = bytes[index + 2] & 0xff; + int lo2 = bytes[index + 3] & 0xff; + int signed = (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; + return signed; + } + + /** + * Extract unsigned 32-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index) and (index+1) interpreted as 32 bit unsigned integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static long extractUInt32(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.UINT32); + long signed = extractSInt32(bytes, index); + long unsigned = signed & 0xffff_ffffL; + assert unsigned >= 0; + return unsigned; + } + + /** + * Extract signed 32-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * This is identical with extractSInt32, but with registers swapped. + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index+1), (index) interpreted as 32 bit signed integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static int extractSInt32Swap(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.INT32_SWAP); + // swapped order of registers, high 16 bits *follow* low 16 bits + int hi1 = bytes[index + 2] & 0xff; + int lo1 = bytes[index + 3] & 0xff; + int hi2 = bytes[index + 0] & 0xff; + int lo2 = bytes[index + 1] & 0xff; + int signed = (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; + return signed; + } + + /** + * Extract unsigned 32-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * This is identical with extractUInt32, but with registers swapped. + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index+1), (index) interpreted as 32 bit unsigned integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static long extractUInt32Swap(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.UINT32_SWAP); + long signed = extractSInt32Swap(bytes, index); + long unsigned = signed & 0xffff_ffffL; + assert unsigned >= 0; + return unsigned; + } + + /** + * Extract signed 64-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index), (index+1), (index+2), (index+3) interpreted as 64 bit signed integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static long extractSInt64(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.INT64); + byte hi1 = (byte) (bytes[index + 0] & 0xff); + byte lo1 = (byte) (bytes[index + 1] & 0xff); + byte hi2 = (byte) (bytes[index + 2] & 0xff); + byte lo2 = (byte) (bytes[index + 3] & 0xff); + byte hi3 = (byte) (bytes[index + 4] & 0xff); + byte lo3 = (byte) (bytes[index + 5] & 0xff); + byte hi4 = (byte) (bytes[index + 6] & 0xff); + byte lo4 = (byte) (bytes[index + 7] & 0xff); + return new BigInteger(new byte[] { hi1, lo1, hi2, lo2, hi3, lo3, hi4, lo4 }).longValue(); + } + /** - * Read data from registers and convert the result to StringType + * Extract unsigned 64-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index), (index+1), (index+2), (index+3) interpreted as 64 bit unsigned integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static BigInteger extractUInt64(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.UINT64); + byte hi1 = (byte) (bytes[index + 0] & 0xff); + byte lo1 = (byte) (bytes[index + 1] & 0xff); + byte hi2 = (byte) (bytes[index + 2] & 0xff); + byte lo2 = (byte) (bytes[index + 3] & 0xff); + byte hi3 = (byte) (bytes[index + 4] & 0xff); + byte lo3 = (byte) (bytes[index + 5] & 0xff); + byte hi4 = (byte) (bytes[index + 6] & 0xff); + byte lo4 = (byte) (bytes[index + 7] & 0xff); + return new BigInteger(1, new byte[] { hi1, lo1, hi2, lo2, hi3, lo3, hi4, lo4 }); + } + + /** + * Extract signed 64-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * This is identical with extractInt64, but with registers swapped (registers with higher index before lower index). + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index+3), (index+2), (index+1), (index) interpreted as 64 bit signed integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static long extractSInt64Swap(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.INT64_SWAP); + // Swapped order of registers + byte hi1 = (byte) (bytes[index + 6] & 0xff); + byte lo1 = (byte) (bytes[index + 7] & 0xff); + byte hi2 = (byte) (bytes[index + 4] & 0xff); + byte lo2 = (byte) (bytes[index + 5] & 0xff); + byte hi3 = (byte) (bytes[index + 2] & 0xff); + byte lo3 = (byte) (bytes[index + 3] & 0xff); + byte hi4 = (byte) (bytes[index + 0] & 0xff); + byte lo4 = (byte) (bytes[index + 1] & 0xff); + return new BigInteger(new byte[] { hi1, lo1, hi2, lo2, hi3, lo3, hi4, lo4 }).longValue(); + } + + /** + * Extract unsigned 64-bit integer from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * This is identical with extractUInt64, but with registers swapped (registers with higher index before lower + * index). + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index+3), (index+2), (index+1), (index) interpreted as 64 bit unsigned integer + * @throws IllegalArgumentException when index is out of bounds + */ + public static BigInteger extractUInt64Swap(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.UINT64_SWAP); + // Swapped order of registers + byte hi1 = (byte) (bytes[index + 6] & 0xff); + byte lo1 = (byte) (bytes[index + 7] & 0xff); + byte hi2 = (byte) (bytes[index + 4] & 0xff); + byte lo2 = (byte) (bytes[index + 5] & 0xff); + byte hi3 = (byte) (bytes[index + 2] & 0xff); + byte lo3 = (byte) (bytes[index + 3] & 0xff); + byte hi4 = (byte) (bytes[index + 0] & 0xff); + byte lo4 = (byte) (bytes[index + 1] & 0xff); + return new BigInteger(1, new byte[] { hi1, lo1, hi2, lo2, hi3, lo3, hi4, lo4 }); + } + + /** + * Extract single-precision 32-bit IEEE 754 floating point from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * Note that this method can return floating point NaN and floating point infinity. + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index), (index+1), (index+2), (index+3) interpreted as single-precision 32-bit IEEE 754 + * floating point + * @throws IllegalArgumentException when index is out of bounds + */ + public static float extractFloat32(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.FLOAT32); + int hi1 = bytes[index + 0] & 0xff; + int lo1 = bytes[index + 1] & 0xff; + int hi2 = bytes[index + 2] & 0xff; + int lo2 = bytes[index + 3] & 0xff; + int bits32 = (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; + return Float.intBitsToFloat(bits32); + } + + /** + * Extract single-precision 32-bit IEEE 754 floating point from registers represented by sequence of bytes + * + * It is assumed that each register is encoded in most significant bit first order. + * + * This is identical with extractFloat32, but with registers swapped (registers with higher index before lower + * index). + * + * Note that this method can return floating point NaN and floating point infinity. + * + * @param bytes registers represented by sequence of bytes + * @param index index of first register. First register has index of 0. + * @return registers (index+3), (index+2), (index+1), (index) interpreted as single-precision 32-bit IEEE 754 + * floating point + * @throws IllegalArgumentException when index is out of bounds + */ + public static float extractFloat32Swap(byte[] bytes, int index) { + assertIndexAndType(bytes, index, ValueType.FLOAT32_SWAP); + // swapped order of registers, high 16 bits *follow* low 16 bits + int hi1 = bytes[index + 2] & 0xff; + int lo1 = bytes[index + 3] & 0xff; + int hi2 = bytes[index + 0] & 0xff; + int lo2 = bytes[index + 1] & 0xff; + int bits32 = (hi1 << 24) | (lo1 << 16) | (hi2 << 8) | lo2; + return Float.intBitsToFloat(bits32); + } + + /** + * Read data from registers and convert the result to String * Strings should start the the first byte of a register, but could * have an odd number of characters. * Raw byte array values are converted using the charset parameter * and a maximum of length bytes are read. However reading stops at the first * NUL byte encountered. * + * Registers are read in big-endian order, i.e. two registers consisting 4 bytes (ab, cd) are parsed as sequence of + * bytes (a,b,c,d). + * * @param registers list of registers, each register represent 16bit of data - * @param index zero based register index. Registers are handled as 16bit registers, + * @param registerIndex zero based register index. Registers are handled as 16bit registers, * this parameter defines the starting register. - * @param length maximum length of string in 8bit characters. + * @param length maximum length of string in 8bit characters (number of bytes considered) * @param charset the character set used to construct the string. * @return string representation queried value * @throws IllegalArgumentException when index is out of bounds of registers */ - public static StringType extractStringFromRegisters(ModbusRegisterArray registers, int index, int length, + public static String extractStringFromRegisters(ModbusRegisterArray registers, int registerIndex, int length, Charset charset) { - if (index * 2 + length > registers.size() * 2) { + return extractStringFromBytes(registers.getBytes(), registerIndex * 2, length, charset); + } + + /** + * Read data from bytes and convert the result to String + * + * Raw byte array values are converted using the charset parameter + * and a maximum of length bytes are read. However reading stops at the first + * NUL byte encountered. + * + * @param bytes bytes representing the registers + * @param byteIndex zero based byte index + * @param length maximum length of string in 8bit characters (number of bytes considered) + * @param charset the character set used to construct the string. + * @return string representation queried value + * @throws IllegalArgumentException when index is out of bounds of registers + */ + public static String extractStringFromBytes(byte[] bytes, int byteIndex, int length, Charset charset) { + if (byteIndex + length > bytes.length) { throw new IllegalArgumentException( - String.format("Index=%d with length=%d is out-of-bounds given registers of size %d", index, length, - registers.size())); + String.format("byteIndex=%d with length=%d is out-of-bounds given registers of size %d", byteIndex, + length, bytes.length)); } - if (index < 0) { + if (byteIndex < 0) { throw new IllegalArgumentException("Negative index values are not supported"); } if (length < 0) { throw new IllegalArgumentException("Negative string length is not supported"); } - byte[] buff = new byte[length]; - int src = index; - int dest; - for (dest = 0; dest < length; dest++) { + int effectiveLength = length; - byte chr; - if (dest % 2 == 0) { - chr = (byte) ((registers.getRegister(src).getValue() >> 8)); - } else { - chr = (byte) (registers.getRegister(src).getValue() & 0xff); - src++; - } - if (chr == 0) { + // Find first zero byte in registers and call reduce length such that we stop before it + for (int i = 0; i < length; i++) { + if (bytes[byteIndex + i] == '\0') { + effectiveLength = i; break; } - buff[dest] = chr; } - return new StringType(new String(buff, 0, dest, charset)); + + return new String(bytes, byteIndex, effectiveLength, charset); } /** @@ -292,89 +647,80 @@ public class ModbusBitUtilities { case UINT16: { short shortValue = numericCommand.shortValue(); // big endian byte ordering - byte b1 = (byte) (shortValue >> 8); - byte b2 = (byte) shortValue; - - ModbusRegister register = new ModbusRegister(b1, b2); - return new ModbusRegisterArray(new ModbusRegister[] { register }); + byte hi = (byte) (shortValue >> 8); + byte lo = (byte) shortValue; + return new ModbusRegisterArray(new byte[] { hi, lo }); } case INT32: case UINT32: { int intValue = numericCommand.intValue(); // big endian byte ordering - byte b1 = (byte) (intValue >> 24); - byte b2 = (byte) (intValue >> 16); - byte b3 = (byte) (intValue >> 8); - byte b4 = (byte) intValue; - ModbusRegister register = new ModbusRegister(b1, b2); - ModbusRegister register2 = new ModbusRegister(b3, b4); - return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); + byte hi1 = (byte) (intValue >> 24); + byte lo1 = (byte) (intValue >> 16); + byte hi2 = (byte) (intValue >> 8); + byte lo2 = (byte) intValue; + return new ModbusRegisterArray(new byte[] { hi1, lo1, hi2, lo2 }); } case INT32_SWAP: case UINT32_SWAP: { int intValue = numericCommand.intValue(); // big endian byte ordering - byte b1 = (byte) (intValue >> 24); - byte b2 = (byte) (intValue >> 16); - byte b3 = (byte) (intValue >> 8); - byte b4 = (byte) intValue; - ModbusRegister register = new ModbusRegister(b3, b4); - ModbusRegister register2 = new ModbusRegister(b1, b2); - return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); + byte hi1 = (byte) (intValue >> 24); + byte lo1 = (byte) (intValue >> 16); + byte hi2 = (byte) (intValue >> 8); + byte lo2 = (byte) intValue; + // Swapped order of registers + return new ModbusRegisterArray(new byte[] { hi2, lo2, hi1, lo1 }); } case FLOAT32: { float floatValue = numericCommand.floatValue(); int intBits = Float.floatToIntBits(floatValue); // big endian byte ordering - byte b1 = (byte) (intBits >> 24); - byte b2 = (byte) (intBits >> 16); - byte b3 = (byte) (intBits >> 8); - byte b4 = (byte) intBits; - ModbusRegister register = new ModbusRegister(b1, b2); - ModbusRegister register2 = new ModbusRegister(b3, b4); - return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); + byte hi1 = (byte) (intBits >> 24); + byte lo1 = (byte) (intBits >> 16); + byte hi2 = (byte) (intBits >> 8); + byte lo2 = (byte) intBits; + return new ModbusRegisterArray(new byte[] { hi1, lo1, hi2, lo2 }); } case FLOAT32_SWAP: { float floatValue = numericCommand.floatValue(); int intBits = Float.floatToIntBits(floatValue); // big endian byte ordering - byte b1 = (byte) (intBits >> 24); - byte b2 = (byte) (intBits >> 16); - byte b3 = (byte) (intBits >> 8); - byte b4 = (byte) intBits; - ModbusRegister register = new ModbusRegister(b3, b4); - ModbusRegister register2 = new ModbusRegister(b1, b2); - return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); + byte hi1 = (byte) (intBits >> 24); + byte lo1 = (byte) (intBits >> 16); + byte hi2 = (byte) (intBits >> 8); + byte lo2 = (byte) intBits; + // Swapped order of registers + return new ModbusRegisterArray(new byte[] { hi2, lo2, hi1, lo1 }); } case INT64: case UINT64: { long longValue = numericCommand.longValue(); // big endian byte ordering - byte b1 = (byte) (longValue >> 56); - byte b2 = (byte) (longValue >> 48); - byte b3 = (byte) (longValue >> 40); - byte b4 = (byte) (longValue >> 32); - byte b5 = (byte) (longValue >> 24); - byte b6 = (byte) (longValue >> 16); - byte b7 = (byte) (longValue >> 8); - byte b8 = (byte) longValue; - return new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister(b1, b2), - new ModbusRegister(b3, b4), new ModbusRegister(b5, b6), new ModbusRegister(b7, b8) }); + byte hi1 = (byte) (longValue >> 56); + byte lo1 = (byte) (longValue >> 48); + byte hi2 = (byte) (longValue >> 40); + byte lo2 = (byte) (longValue >> 32); + byte hi3 = (byte) (longValue >> 24); + byte lo3 = (byte) (longValue >> 16); + byte hi4 = (byte) (longValue >> 8); + byte lo4 = (byte) longValue; + return new ModbusRegisterArray(new byte[] { hi1, lo1, hi2, lo2, hi3, lo3, hi4, lo4 }); } case INT64_SWAP: case UINT64_SWAP: { long longValue = numericCommand.longValue(); // big endian byte ordering - byte b1 = (byte) (longValue >> 56); - byte b2 = (byte) (longValue >> 48); - byte b3 = (byte) (longValue >> 40); - byte b4 = (byte) (longValue >> 32); - byte b5 = (byte) (longValue >> 24); - byte b6 = (byte) (longValue >> 16); - byte b7 = (byte) (longValue >> 8); - byte b8 = (byte) longValue; - return new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister(b7, b8), - new ModbusRegister(b5, b6), new ModbusRegister(b3, b4), new ModbusRegister(b1, b2) }); + byte hi1 = (byte) (longValue >> 56); + byte lo1 = (byte) (longValue >> 48); + byte hi2 = (byte) (longValue >> 40); + byte lo2 = (byte) (longValue >> 32); + byte hi3 = (byte) (longValue >> 24); + byte lo3 = (byte) (longValue >> 16); + byte hi4 = (byte) (longValue >> 8); + byte lo4 = (byte) longValue; + // Swapped order of registers + return new ModbusRegisterArray(new byte[] { hi4, lo4, hi3, lo3, hi2, lo2, hi1, lo1 }); } default: throw new NotImplementedException( diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java deleted file mode 100644 index 7b787b02bb..0000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.io.transport.modbus; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -import net.wimpi.modbus.procimg.SimpleInputRegister; - -/** - * Basic {@link ModbusRegister} implementation - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public class ModbusRegister { - - private final SimpleInputRegister wrapped; - - /** - * Constructs a new instance for bytes - * - * @param b1 the first (hi) byte of the word. - * @param b2 the second (low) byte of the word. - */ - public ModbusRegister(byte b1, byte b2) { - wrapped = new SimpleInputRegister(b1, b2); - } - - /** - * Construct register for at - * - * @param val value representing register data. The int will be downcasted to short. - */ - public ModbusRegister(int val) { - wrapped = new SimpleInputRegister(val); - } - - /** - * Get raw data represented by this register. Since register is 16 bits, array of length 2 will be returned. - * - * @return byte array of length 2, high byte first. - */ - public byte[] getBytes() { - return wrapped.toBytes(); - } - - /** - * Returns the value of this register as integer representing 16 bit data parsed as signed integer. - * - * @return the register content as unsigned integer - */ - public int getValue() { - return wrapped.getValue(); - } - - /** - * Returns the value of this register as integer representing 16 bit data parsed as unsigned integer. - * - * @return the register content as unsigned integer - */ - public int toUnsignedShort() { - return wrapped.toUnsignedShort(); - } - - @Override - public String toString() { - StringBuffer buffer = new StringBuffer("ModbusRegisterImpl("); - buffer.append("uint16=").append(toUnsignedShort()).append(", hex="); - return appendHexString(buffer).append(')').toString(); - } - - /** - * Returns the register value as hex string - * - * For example, 12 34 - * - * @return string representing the register data - */ - public String toHexString() { - StringBuffer buffer = new StringBuffer(5); - return appendHexString(buffer).toString(); - } - - /** - * Appends the register value as hex string to the given StringBuffer - * - */ - public StringBuffer appendHexString(StringBuffer buffer) { - byte[] bytes = getBytes(); - for (int i = 0; i < 2; i++) { - byte b = bytes[i]; - String byteHex = Long.toHexString(b & 0xff); - if ((b & 0xff) < 0x10) { - buffer.append('0'); - } - buffer.append(byteHex); - if (i == 0) { - buffer.append(' '); - } - } - return buffer; - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java index a6df40ea27..6246d34b2a 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java @@ -12,10 +12,10 @@ */ package org.openhab.io.transport.modbus; -import java.util.Iterator; -import java.util.stream.IntStream; +import java.util.Arrays; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.util.HexUtils; /** * Immutable {@link ModbusRegisterArray} implementation @@ -23,54 +23,60 @@ import org.eclipse.jdt.annotation.NonNullByDefault; * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public class ModbusRegisterArray implements Iterable { +public class ModbusRegisterArray { - private final ModbusRegister[] registers; + private final byte[] bytes; + + public ModbusRegisterArray(byte... bytes) { + if (bytes.length % 2 != 0) { + throw new IllegalArgumentException(); + } + this.bytes = Arrays.copyOf(bytes, bytes.length); + } /** - * Construct plain ModbusRegister[] array from register values + * Construct plain ModbusRegisterArray array from register values * * @param registerValues register values, each int corresponding to one register * @return */ - public static ModbusRegister[] registersFromValues(int... registerValues) { - ModbusRegister[] registers = new ModbusRegister[registerValues.length]; - for (int i = 0; i < registerValues.length; i++) { - registers[i] = new ModbusRegister(registerValues[i]); + public ModbusRegisterArray(int... registerValues) { + bytes = new byte[registerValues.length * 2]; + for (int registerIndex = 0; registerIndex < registerValues.length; registerIndex++) { + int register = registerValues[registerIndex] & 0xffff; + // hi-byte + bytes[registerIndex * 2] = (byte) (register >> 8); + // lo byte + bytes[registerIndex * 2 + 1] = (byte) register; } - return registers; } /** - * Construct ModbusRegisterArrayImpl from array of {@link ModbusRegister} + * Get register index i as unsigned integer * - * @param registers + * @param i register index + * @return register value interpreted as unsigned integer (big-endian byte ordering) */ - public ModbusRegisterArray(ModbusRegister[] registers) { - this.registers = registers; + public int getRegister(int i) { + int hi = bytes[i * 2] & 0xff; + int lo = bytes[i * 2 + 1] & 0xff; + return ((hi << 8) | lo) & 0xffff; } /** - * Construct plain ModbusRegisterArrayImpl array from register values + * Return bytes representing the registers * - * @param registerValues register values, each int corresponding to one register - * @return - */ - public ModbusRegisterArray(int... registerValues) { - this(registersFromValues(registerValues)); - } - - /** - * Return register at the given index * - * Index 0 matches first register (lowest register index). - *

+ * Index 0: hi-byte of 1st register + * Index 1: low-byte of 1st register + * Index 3: hi-byte of 2nd register + * Index 4: low-byte of 2nd register + * ... * - * @param index the index of the register to be returned. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @return set of bytes */ - public ModbusRegister getRegister(int index) { - return registers[index]; + public byte[] getBytes() { + return bytes; } /** @@ -79,24 +85,16 @@ public class ModbusRegisterArray implements Iterable { * @return */ public int size() { - return registers.length; + return bytes.length / 2; } @Override public String toString() { - if (registers.length == 0) { - return "ModbusRegisterArrayImpl()"; + if (bytes.length == 0) { + return "ModbusRegisterArray()"; } - StringBuffer buffer = new StringBuffer(registers.length * 2).append("ModbusRegisterArrayImpl("); - return appendHexString(buffer).append(')').toString(); - } - - /** - * Iterator over all the registers - */ - @Override - public Iterator iterator() { - return IntStream.range(0, size()).mapToObj(i -> getRegister(i)).iterator(); + return new StringBuilder(bytes.length).append("ModbusRegisterArray(").append(toHexString()).append(')') + .toString(); } /** @@ -110,22 +108,6 @@ public class ModbusRegisterArray implements Iterable { if (size() == 0) { return ""; } - // Initialize capacity to (n*2 + n-1), two chars per byte + spaces in between - StringBuffer buffer = new StringBuffer(size() * 2 + (size() - 1)); - return appendHexString(buffer).toString(); - } - - /** - * Appends the register data as hex string to the given StringBuffer - * - */ - public StringBuffer appendHexString(StringBuffer buffer) { - IntStream.range(0, size()).forEachOrdered(index -> { - getRegister(index).appendHexString(buffer); - if (index < size() - 1) { - buffer.append(' '); - } - }); - return buffer; + return HexUtils.bytesToHex(getBytes()); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ValueBuffer.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ValueBuffer.java new file mode 100644 index 0000000000..bcf5aa358e --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ValueBuffer.java @@ -0,0 +1,331 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus; + +import java.math.BigInteger; +import java.nio.BufferOverflowException; +import java.nio.InvalidMarkException; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * ByteBuffer-like interface for working with different types of data stored in byte arrays + * + * @author Sami Salonen - Initial contribution + */ +@NonNullByDefault +public class ValueBuffer { + private final byte[] bytes; + private final AtomicInteger byteIndex = new AtomicInteger(); + private volatile AtomicReference<@Nullable AtomicInteger> mark = new AtomicReference<>(); + + /** + * Wrap modbus registers and create a new instance of ValueBuffer + * + * The instance will have position of 0. + * + * @param array set of registers + * @return new instance of ValueBuffer referencing bytes represented by modbus register array + */ + public static ValueBuffer wrap(ModbusRegisterArray array) { + return new ValueBuffer(array.getBytes()); + } + + /** + * Wrap given bytes and create a new instance of ValueBuffer + * + * The instance will have position of 0. + * + * + * @param array set of bytes to wrap + * @return new instance of ValueBuffer referencing bytes + */ + public static ValueBuffer wrap(byte[] array) { + return new ValueBuffer(array); + } + + private ValueBuffer(byte[] bytes) { + this.bytes = bytes; + } + + /** + * Returns this buffer's position. + * + * @return The position of this buffer + */ + public int position() { + return byteIndex.get(); + } + + /** + * Sets this buffer's position. If the mark is defined and larger than the new position then it is discarded. + * + * @return this buffer + */ + public ValueBuffer position(int byteIndex) { + this.mark.getAndUpdate(curMark -> { + if (curMark == null) { + return null; + } else if (curMark.get() > byteIndex) { + return null; + } else { + return curMark; + } + }); + this.byteIndex.set(byteIndex); + return this; + } + + /** + * Sets this buffer's mark at its position. + * + * @return this buffer + */ + public ValueBuffer mark() { + mark = new AtomicReference<>(new AtomicInteger(byteIndex.get())); + return this; + } + + /** + * Resets this buffer's position to the previously-marked position. + * Invoking this method neither changes nor discards the mark's value. + * + * @return this buffer + * @throws InvalidMarkException If the mark has not been set + */ + public ValueBuffer reset() throws InvalidMarkException { + int mark = Optional.ofNullable(this.mark.get()).map(i -> i.get()).orElse(-1); + if (mark < 0) { + throw new InvalidMarkException(); + } + byteIndex.set(mark); + return this; + } + + /** + * Returns the number of bytes between the current position and the end. + * + * @return The number of bytes remaining in this buffer + */ + public int remaining() { + return bytes.length - byteIndex.get(); + } + + /** + * Returns underlying bytes + * + * @return reference to underlying bytes + */ + public byte[] array() { + return bytes; + } + + /** + * Tells whether there are any bytes left between current position and the end + * + * @return true if, and only if, there is at least one byte remaining in this buffer + */ + public boolean hasRemaining() { + return remaining() > 0; + } + + /** + * Starting from current position, read dst.length number of bytes and copy the data to dst + * + * @param dst copied bytes + * @return this buffer + * @throws BufferOverflowException If there is insufficient space in this buffer for the remaining bytes in the + * source buffer + */ + public ValueBuffer get(byte[] dst) { + int start = byteIndex.getAndAdd(dst.length); + try { + System.arraycopy(bytes, start, dst, 0, dst.length); + } catch (IndexOutOfBoundsException e) { + throw new BufferOverflowException(); + } + return this; + } + + /** + * Extract signed 8-bit integer at current position, and advance position. + * + * @return signed 8-bit integer (byte) + * @see ModbusBitUtilities.extractSInt8 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public byte getSInt8() { + return ModbusBitUtilities.extractSInt8(bytes, byteIndex.getAndAdd(1)); + } + + /** + * Extract unsigned 8-bit integer at current position, and advance position. + * + * @return unsigned 8-bit integer + * @see ModbusBitUtilities.extractUInt8 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public short getUInt8() { + return ModbusBitUtilities.extractUInt8(bytes, byteIndex.getAndAdd(1)); + } + + /** + * Extract signed 16-bit integer at current position, and advance position. + * + * @return signed 16-bit integer (short) + * @see ModbusBitUtilities.extractSInt16 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public short getSInt16() { + return ModbusBitUtilities.extractSInt16(bytes, byteIndex.getAndAdd(2)); + } + + /** + * Extract unsigned 16-bit integer at current position, and advance position. + * + * @return unsigned 16-bit integer + * @see ModbusBitUtilities.extractUInt16 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public int getUInt16() { + return ModbusBitUtilities.extractUInt16(bytes, byteIndex.getAndAdd(2)); + } + + /** + * Extract signed 32-bit integer at current position, and advance position. + * + * @return signed 32-bit integer + * @see ModbusBitUtilities.extractSInt32 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public int getSInt32() { + return ModbusBitUtilities.extractSInt32(bytes, byteIndex.getAndAdd(4)); + } + + /** + * Extract unsigned 32-bit integer at current position, and advance position. + * + * @return unsigned 32-bit integer + * @see ModbusBitUtilities.extractUInt32 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public long getUInt32() { + return ModbusBitUtilities.extractUInt32(bytes, byteIndex.getAndAdd(4)); + } + + /** + * Extract signed 32-bit integer at current position, and advance position. + * + * This is identical with getSInt32, but with registers swapped. + * + * @return signed 32-bit integer + * @see ModbusBitUtilities.extractSInt32Swap + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public int getSInt32Swap() { + return ModbusBitUtilities.extractSInt32Swap(bytes, byteIndex.getAndAdd(4)); + } + + /** + * Extract unsigned 32-bit integer at current position, and advance position. + * + * This is identical with getUInt32, but with registers swapped. + * + * @return unsigned 32-bit integer + * @see ModbusBitUtilities.extractUInt32Swap + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public long getUInt32Swap() { + return ModbusBitUtilities.extractUInt32Swap(bytes, byteIndex.getAndAdd(4)); + } + + /** + * Extract signed 64-bit integer at current position, and advance position. + * + * @return signed 64-bit integer + * @see ModbusBitUtilities.extractInt64 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public long getSInt64() { + return ModbusBitUtilities.extractSInt64(bytes, byteIndex.getAndAdd(8)); + } + + /** + * Extract unsigned 64-bit integer at current position, and advance position. + * + * @return unsigned 64-bit integer + * @see ModbusBitUtilities.extractUInt64 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public BigInteger getUInt64() { + return ModbusBitUtilities.extractUInt64(bytes, byteIndex.getAndAdd(8)); + } + + /** + * Extract signed 64-bit integer at current position, and advance position. + * + * This is identical with getSInt64, but with registers swapped. + * + * @return signed 64-bit integer + * @see ModbusBitUtilities.extractInt64Swap + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public long getSInt64Swap() { + return ModbusBitUtilities.extractSInt64Swap(bytes, byteIndex.getAndAdd(8)); + } + + /** + * Extract unsigned 64-bit integer at current position, and advance position. + * + * This is identical with getUInt64, but with registers swapped. + * + * @return unsigned 64-bit integer + * @see ModbusBitUtilities.extractUInt64Swap + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public BigInteger getUInt64Swap() { + return ModbusBitUtilities.extractUInt64Swap(bytes, byteIndex.getAndAdd(8)); + } + + /** + * Extract single-precision 32-bit IEEE 754 floating point at current position, and advance position. + * + * Note that this method can return floating point NaN and floating point infinity. + * + * @return single-precision 32-bit IEEE 754 floating point + * @see ModbusBitUtilities.extractFloat32 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public float getFloat32() { + return ModbusBitUtilities.extractFloat32(bytes, byteIndex.getAndAdd(4)); + } + + /** + * Extract single-precision 32-bit IEEE 754 floating point at current position, and advance position. + * + * This is identical with getFloat32, but with registers swapped. + * + * Note that this method can return floating point NaN and floating point infinity. + * + * @return single-precision 32-bit IEEE 754 floating point + * @see ModbusBitUtilities.extractFloat32 + * @throws IllegalArgumentException when there are not enough bytes in this ValueBuffer + */ + public float getFloat32Swap() { + return ModbusBitUtilities.extractFloat32Swap(bytes, byteIndex.getAndAdd(4)); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java index 2c47fdd95c..40fe719c3d 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java @@ -24,7 +24,6 @@ import org.openhab.io.transport.modbus.BitArray; import org.openhab.io.transport.modbus.ModbusReadCallback; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint; import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; @@ -87,9 +86,9 @@ public class ModbusLibraryWrapper { } private static ModbusRegisterArray modbusRegisterArrayFromInputRegisters(InputRegister[] inputRegisters) { - ModbusRegister[] registers = new ModbusRegister[inputRegisters.length]; + int[] registers = new int[inputRegisters.length]; for (int i = 0; i < inputRegisters.length; i++) { - registers[i] = new ModbusRegister(inputRegisters[i].getValue()); + registers[i] = inputRegisters[i].getValue(); } return new ModbusRegisterArray(registers); } @@ -270,7 +269,7 @@ public class ModbusLibraryWrapper { * @return */ public static Register[] convertRegisters(ModbusRegisterArray arr) { - return IntStream.range(0, arr.size()).mapToObj(i -> new SimpleInputRegister(arr.getRegister(i).getValue())) + return IntStream.range(0, arr.size()).mapToObj(i -> new SimpleInputRegister(arr.getRegister(i))) .collect(Collectors.toList()).toArray(new Register[0]); } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/json/WriteRequestJsonUtilities.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/json/WriteRequestJsonUtilities.java index a416e91e32..f0c9eab562 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/json/WriteRequestJsonUtilities.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/json/WriteRequestJsonUtilities.java @@ -22,7 +22,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.io.transport.modbus.BitArray; import org.openhab.io.transport.modbus.ModbusConstants; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint; import org.openhab.io.transport.modbus.ModbusWriteFunctionCode; @@ -197,7 +196,7 @@ public final class WriteRequestJsonUtilities { } // fall-through to WRITE_MULTIPLE_REGISTERS case WRITE_MULTIPLE_REGISTERS: { - ModbusRegister[] registers = new ModbusRegister[valuesElem.size()]; + int[] registers = new int[valuesElem.size()]; if (registers.length == 0) { throw new IllegalArgumentException("Must provide at least one register"); } else if (valuesElem.size() > ModbusConstants.MAX_REGISTERS_WRITE_COUNT) { @@ -206,7 +205,7 @@ public final class WriteRequestJsonUtilities { ModbusConstants.MAX_REGISTERS_WRITE_COUNT)); } for (int i = 0; i < valuesElem.size(); i++) { - registers[i] = new ModbusRegister(valuesElem.get(i).getAsInt()); + registers[i] = valuesElem.get(i).getAsInt(); } return new ModbusWriteRegisterRequestBlueprint(unitId, address, new ModbusRegisterArray(registers), !writeSingle.get(), maxTries); diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java index e5b0819b5f..f4e562df61 100644 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesCommandToRegistersTest.java @@ -322,9 +322,9 @@ public class BitUtilitiesCommandToRegistersTest { is(equalTo(expectedRegisters.length))); for (int i = 0; i < expectedRegisters.length; i++) { int expectedRegisterDataUnsigned = expectedRegisters[i] & 0xffff; - int actual = registers.getRegister(i).getValue(); + int actualUnsigned = registers.getRegister(i); - assertThat(String.format("register index i=%d, command=%s, type=%s", i, command, type), actual, + assertThat(String.format("register index i=%d, command=%s, type=%s", i, command, type), actualUnsigned, is(equalTo(expectedRegisterDataUnsigned))); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractBitTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractBitTest.java new file mode 100644 index 0000000000..8f74368241 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractBitTest.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus.test; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.openhab.io.transport.modbus.ModbusBitUtilities; + +/** + * + * Tests for extractBit + * + * @author Sami Salonen - Initial contribution + */ +public class BitUtilitiesExtractBitTest { + + @Test + public void testExtractBitWithRegisterIndexAndBitIndex() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + + { + int registerIndex = 0; + int[] expectedBitsFromLSBtoMSB = new int[] { // + 1, 0, 1, 0, 0, 1, 0, 0, // lo byte, with increasing significance + 1, 0, 0, 0, 0, 1, 0, 0 // hi byte, with increasing significance + }; + for (int bitIndex = 0; bitIndex < expectedBitsFromLSBtoMSB.length; bitIndex++) { + assertEquals(expectedBitsFromLSBtoMSB[bitIndex], + ModbusBitUtilities.extractBit(bytes, registerIndex, bitIndex), + String.format("bitIndex=%d", bitIndex)); + } + } + { + int registerIndex = 1; + int[] expectedBitsFromLSBtoMSB = new int[] { // + 1, 0, 0, 1, 0, 1, 0, 0, // lo byte, with increasing significance + 1, 0, 0, 0, 1, 1, 0, 0 // hi byte, with increasing significance + }; + for (int bitIndex = 0; bitIndex < expectedBitsFromLSBtoMSB.length; bitIndex++) { + assertEquals(expectedBitsFromLSBtoMSB[bitIndex], + ModbusBitUtilities.extractBit(bytes, registerIndex, bitIndex), + String.format("bitIndex=%d", bitIndex)); + } + } + } + + @Test + public void testExtractBitWithRegisterIndexAndBitIndexOOB() { + + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractBit(bytes, 3, 0)); + } + + @Test + public void testExtractBitWithRegisterIndexAndBitIndexOOB2() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractBit(bytes, 0, 17)); + } + + @Test + public void testExtractBitWithRegisterIndexAndBitIndexOOB3() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractBit(bytes, 0, -1)); + } + + @Test + public void testExtractBitWithRegisterIndexAndBitIndexOOB4() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractBit(bytes, -1, 0)); + } + + @Test + public void testExtractBitWithSingleIndex() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + int[] expectedBits = new int[] { // + 1, 0, 1, 0, 0, 1, 0, 0, // 1st register: lo byte, with increasing significance + 1, 0, 0, 0, 0, 1, 0, 0, // 1st register: hi byte, with increasing significance + 1, 0, 0, 1, 0, 1, 0, 0, // 2nd register: lo byte, with increasing significance + 1, 0, 0, 0, 1, 1, 0, 0 // 2nd register: hi byte, with increasing significance + }; + for (int bitIndex = 0; bitIndex < expectedBits.length; bitIndex++) { + assertEquals(expectedBits[bitIndex], ModbusBitUtilities.extractBit(bytes, bitIndex), + String.format("bitIndex=%d", bitIndex)); + assertEquals(expectedBits[bitIndex], ModbusBitUtilities.extractBit(bytes, bitIndex), + String.format("bitIndex=%d", bitIndex)); + } + } + + @Test + public void testExtractBitWithSingleIndexOOB() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractBit(bytes, 32)); + } + + @Test + public void testExtractBitWithSingleIndexOOB2() { + byte[] bytes = new byte[] { 0b00100001, // hi byte of 1st register + 0b00100101, // lo byte of 1st register + 0b00110001, // hi byte of 2nd register + 0b00101001 }; // lo byte of 2nd register + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractBit(bytes, -1)); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractFloat32Test.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractFloat32Test.java new file mode 100644 index 0000000000..388a9464f1 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractFloat32Test.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; +import org.openhab.io.transport.modbus.ModbusBitUtilities; + +/** + * + * Tests for 'special' float values such as infinity and NaN. These are not covered in detail in + * {@link BitUtilitiesExtractIndividualMethodsTest} and + * {@link BitUtilitiesExtractStateFromRegistersTest} + * + * @author Sami Salonen - Initial contribution + */ +public class BitUtilitiesExtractFloat32Test { + + /** + * Creates a byte array with byteOffset number of zeroes, followed by 32bit of data represented by data + * + * @param data actual data payload + * @param byteOffset number of zeros padded + * @return byte array of size 4 + byteOffset + */ + private static byte[] bytes(int data, int byteOffset) { + ByteBuffer buffer = ByteBuffer.allocate(4 + byteOffset); + for (int i = 0; i < byteOffset; i++) { + buffer.put((byte) 0); + } + buffer.putInt(data); + return buffer.array(); + } + + private static void testFloat(float number) { + int data = Float.floatToIntBits(number); + for (int byteOffset = 0; byteOffset < 5; byteOffset++) { + byte[] bytes = bytes(data, byteOffset); + float actual = ModbusBitUtilities.extractFloat32(bytes, byteOffset); + float expected = Float.intBitsToFloat(data); + // Strict comparison of the float values with the exception of NaN + assertTrue(Float.isNaN(expected) ? Float.isNaN(actual) : expected == actual, + String.format("Testing %f (%s) with offset %d, got %f (%s)", expected, Integer.toBinaryString(data), + byteOffset, actual, Integer.toBinaryString(Float.floatToRawIntBits(actual)))); + } + } + + @Test + public void testExtractFloat32Inf() { + testFloat(Float.POSITIVE_INFINITY); + } + + @Test + public void testExtractFloat32NegInf() { + testFloat(Float.NEGATIVE_INFINITY); + } + + @Test + public void testExtractFloat32NaN() { + testFloat(Float.NaN); + } + + @Test + public void testExtractFloat32Regular() { + testFloat(1.3f); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java new file mode 100644 index 0000000000..83f1a629a2 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractIndividualMethodsTest.java @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.Stream.Builder; + +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.core.library.types.DecimalType; +import org.openhab.io.transport.modbus.ModbusBitUtilities; +import org.openhab.io.transport.modbus.ModbusConstants.ValueType; +import org.openhab.io.transport.modbus.ModbusRegisterArray; + +/** + * @author Sami Salonen - Initial contribution + */ +public class BitUtilitiesExtractIndividualMethodsTest { + + public static Collection data() { + // We use test data from BitUtilitiesExtractStateFromRegistersTest + // In BitUtilitiesExtractStateFromRegistersTest the data is aligned to registers + // + // Here (in registerVariations) we generate offsetted variations of the byte data + // to test extractXX which can operate on data aligned on byte-level, not just data aligned on-register level + Collection data = BitUtilitiesExtractStateFromRegistersTest.data(); + return data.stream().flatMap(values -> { + Object expectedResult = values[0]; + ValueType type = (ValueType) values[1]; + ModbusRegisterArray registers = (ModbusRegisterArray) values[2]; + int index = (int) values[3]; + return registerVariations(expectedResult, type, registers, index); + }).collect(Collectors.toList()); + } + + public static Stream filteredTestData(ValueType type) { + return data().stream().filter(values -> (ValueType) values[1] == type); + } + + /** + * Generate register variations for extractXX functions + * + * + * @return entries of (byte[], byteIndex) + */ + private static Stream registerVariations(Object expectedResult, ValueType type, + ModbusRegisterArray registers, int index) { + byte[] origBytes = registers.getBytes(); + int origRegisterIndex = index; + int origByteIndex = origRegisterIndex * 2; + + Builder streamBuilder = Stream.builder(); + for (int offset = 0; offset < 5; offset++) { + int byteIndex = origByteIndex + offset; + byte[] bytesOffsetted = new byte[origBytes.length + offset]; + for (int i = 0; i < bytesOffsetted.length; i++) { + bytesOffsetted[i] = 99; + } + System.arraycopy(origBytes, 0, bytesOffsetted, offset, origBytes.length); + // offsetted: + streamBuilder.add(new Object[] { expectedResult, type, bytesOffsetted, byteIndex }); + + // offsetted, with no extra bytes following + // (this is only done for successfull cases to avoid copyOfRange padding with zeros + if (!(expectedResult instanceof Class)) { + byte[] bytesOffsettedCutExtra = Arrays.copyOfRange(bytesOffsetted, 0, byteIndex + type.getBits() / 8); + if (bytesOffsettedCutExtra.length != bytesOffsetted.length) { + streamBuilder.add(new Object[] { expectedResult, type, bytesOffsettedCutExtra, byteIndex }); + } + } + } + return streamBuilder.build(); + } + + private void testIndividual(Object expectedResult, ValueType type, byte[] bytes, int byteIndex, + Supplier methodUnderTest, Function expectedPrimitive) { + testIndividual(expectedResult, type, bytes, byteIndex, methodUnderTest, expectedPrimitive, null); + } + + @SuppressWarnings("unchecked") + private void testIndividual(Object expectedResult, ValueType type, byte[] bytes, int byteIndex, + Supplier methodUnderTest, Function expectedPrimitive, + @Nullable Number defaultWhenEmptyOptional) { + String testExplanation = String.format("bytes=%s, byteIndex=%d, type=%s", Arrays.toString(bytes), byteIndex, + type); + final Object expectedNumber; + if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { + assertThrows((Class) expectedResult, () -> methodUnderTest.get()); + } else if (expectedResult instanceof Optional) { + assertTrue(!((Optional) expectedResult).isPresent()); + if (defaultWhenEmptyOptional == null) { + fail("Should provide defaultWhenEmptyOptional"); + } + return; + } else { + DecimalType expectedDecimal = (DecimalType) expectedResult; + expectedNumber = expectedPrimitive.apply(expectedDecimal); + assertEquals(expectedNumber, methodUnderTest.get(), testExplanation); + } + } + + public static Stream filteredTestDataSInt16() { + return filteredTestData(ValueType.INT16); + } + + @ParameterizedTest + @MethodSource("filteredTestDataSInt16") + public void testExtractIndividualSInt16(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, () -> ModbusBitUtilities.extractSInt16(bytes, byteIndex), + decimal -> decimal.shortValue()); + } + + public static Stream filteredTestDataUInt16() { + return filteredTestData(ValueType.UINT16); + } + + @ParameterizedTest + @MethodSource("filteredTestDataUInt16") + public void testExtractIndividualUInt16(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, () -> ModbusBitUtilities.extractUInt16(bytes, byteIndex), + decimal -> decimal.intValue()); + } + + public static Stream filteredTestDataSInt32() { + return filteredTestData(ValueType.INT32); + } + + @ParameterizedTest + @MethodSource("filteredTestDataSInt32") + public void testExtractIndividualSInt32(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, () -> ModbusBitUtilities.extractSInt32(bytes, byteIndex), + decimal -> decimal.intValue()); + } + + public static Stream filteredTestDataUInt32() { + return filteredTestData(ValueType.UINT32); + } + + @ParameterizedTest + @MethodSource("filteredTestDataUInt32") + public void testExtractIndividualUInt32(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, () -> ModbusBitUtilities.extractUInt32(bytes, byteIndex), + decimal -> decimal.longValue()); + } + + public static Stream filteredTestDataSInt32Swap() { + return filteredTestData(ValueType.INT32_SWAP); + } + + @ParameterizedTest + @MethodSource("filteredTestDataSInt32Swap") + public void testExtractIndividualSInt32Swap(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, + () -> ModbusBitUtilities.extractSInt32Swap(bytes, byteIndex), decimal -> decimal.intValue()); + } + + public static Stream filteredTestDataUInt32Swap() { + return filteredTestData(ValueType.UINT32_SWAP); + } + + @ParameterizedTest + @MethodSource("filteredTestDataUInt32Swap") + public void testExtractIndividualUInt32Swap(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, + () -> ModbusBitUtilities.extractUInt32Swap(bytes, byteIndex), decimal -> decimal.longValue()); + } + + public static Stream filteredTestDataSInt64() { + return filteredTestData(ValueType.INT64); + } + + @ParameterizedTest + @MethodSource("filteredTestDataSInt64") + public void testExtractIndividualSInt64(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, () -> ModbusBitUtilities.extractSInt64(bytes, byteIndex), + decimal -> decimal.longValue()); + } + + public static Stream filteredTestDataUInt64() { + return filteredTestData(ValueType.UINT64); + } + + @ParameterizedTest + @MethodSource("filteredTestDataUInt64") + public void testExtractIndividualUInt64(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, () -> ModbusBitUtilities.extractUInt64(bytes, byteIndex), + decimal -> decimal.toBigDecimal().toBigIntegerExact()); + } + + public static Stream filteredTestDataSInt64Swap() { + return filteredTestData(ValueType.INT64_SWAP); + } + + @ParameterizedTest + @MethodSource("filteredTestDataSInt64Swap") + public void testExtractIndividualSInt64Swap(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, + () -> ModbusBitUtilities.extractSInt64Swap(bytes, byteIndex), decimal -> decimal.longValue()); + } + + public static Stream filteredTestDataUInt64Swap() { + return filteredTestData(ValueType.UINT64_SWAP); + } + + @ParameterizedTest + @MethodSource("filteredTestDataUInt64Swap") + public void testExtractIndividualUInt64Swap(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, + () -> ModbusBitUtilities.extractUInt64Swap(bytes, byteIndex), + decimal -> decimal.toBigDecimal().toBigIntegerExact()); + } + + public static Stream filteredTestDataFloat32() { + return filteredTestData(ValueType.FLOAT32); + } + + @ParameterizedTest + @MethodSource("filteredTestDataFloat32") + public void testExtractIndividualFloat32(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, + () -> ModbusBitUtilities.extractFloat32(bytes, byteIndex), decimal -> decimal.floatValue(), Float.NaN); + } + + public static Stream filteredTestDataFloat32Swap() { + return filteredTestData(ValueType.FLOAT32_SWAP); + } + + @ParameterizedTest + @MethodSource("filteredTestDataFloat32Swap") + public void testExtractIndividualFloat32Swap(Object expectedResult, ValueType type, byte[] bytes, int byteIndex) + throws InstantiationException, IllegalAccessException { + testIndividual(expectedResult, type, bytes, byteIndex, + () -> ModbusBitUtilities.extractFloat32Swap(bytes, byteIndex), decimal -> decimal.floatValue(), + Float.NaN); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractInt8Test.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractInt8Test.java new file mode 100644 index 0000000000..811b12d31d --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractInt8Test.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus.test; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.openhab.io.transport.modbus.ModbusBitUtilities; + +/** + * + * Tests for extractSInt8 and extractUInt8 + * + * @author Sami Salonen - Initial contribution + */ +public class BitUtilitiesExtractInt8Test { + + @Test + public void extractSInt8WithSingleIndex() { + byte[] bytes = new byte[] { -1, 2, 3 }; + assertEquals(-1, ModbusBitUtilities.extractSInt8(bytes, 0)); + assertEquals(2, ModbusBitUtilities.extractSInt8(bytes, 1)); + assertEquals(3, ModbusBitUtilities.extractSInt8(bytes, 2)); + } + + @Test + public void extractSInt8WithSingleIndexOOB() { + byte[] bytes = new byte[] { -1, 2, 3 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractSInt8(bytes, 3)); + } + + @Test + public void extractSInt8WithSingleIndexOOB2() { + byte[] bytes = new byte[] { -1, 2, 3 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractSInt8(bytes, -1)); + } + + @Test + public void extractSInt8WithRegisterIndexAndHiByte() { + byte[] bytes = new byte[] { -1, 2, 3, 4 }; + assertEquals(-1, ModbusBitUtilities.extractSInt8(bytes, 0, true)); + assertEquals(2, ModbusBitUtilities.extractSInt8(bytes, 0, false)); + assertEquals(3, ModbusBitUtilities.extractSInt8(bytes, 1, true)); + assertEquals(4, ModbusBitUtilities.extractSInt8(bytes, 1, false)); + } + + @Test + public void extractSInt8WithRegisterIndexAndHiByteOOB() { + byte[] bytes = new byte[] { -1, 2, 3, 4 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractSInt8(bytes, 2, true)); + } + + @Test + public void extractSInt8WithRegisterIndexAndHiByteOOB2() { + byte[] bytes = new byte[] { -1, 2, 3, 4 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractSInt8(bytes, -1, true)); + } + + // + // unsigned int8 follows + // + + @Test + public void extractUInt8WithSingleIndex() { + byte[] bytes = new byte[] { -1, 2, 3 }; + assertEquals(255, ModbusBitUtilities.extractUInt8(bytes, 0)); + assertEquals(2, ModbusBitUtilities.extractUInt8(bytes, 1)); + assertEquals(3, ModbusBitUtilities.extractUInt8(bytes, 2)); + } + + @Test + public void extractUInt8WithSingleIndexOOB() { + byte[] bytes = new byte[] { -1, 2, 3 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractUInt8(bytes, 3)); + } + + @Test + public void extractUInt8WithSingleIndexOOB2() { + byte[] bytes = new byte[] { -1, 2, 3 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractUInt8(bytes, -1)); + } + + @Test + public void extractUInt8WithRegisterIndexAndHiByte() { + byte[] bytes = new byte[] { -1, 2, 3, 4 }; + assertEquals(255, ModbusBitUtilities.extractUInt8(bytes, 0, true)); + assertEquals(2, ModbusBitUtilities.extractUInt8(bytes, 0, false)); + assertEquals(3, ModbusBitUtilities.extractUInt8(bytes, 1, true)); + assertEquals(4, ModbusBitUtilities.extractUInt8(bytes, 1, false)); + } + + @Test + public void extractUInt8WithRegisterIndexAndHiByteOOB() { + byte[] bytes = new byte[] { -1, 2, 3, 4 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractUInt8(bytes, 2, true)); + } + + @Test + public void extractUInt8WithRegisterIndexAndHiByteOOB2() { + byte[] bytes = new byte[] { -1, 2, 3, 4 }; + assertThrows(IllegalArgumentException.class, () -> ModbusBitUtilities.extractUInt8(bytes, 255, true)); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java index 8647903b24..195c68c8ac 100644 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStateFromRegistersTest.java @@ -16,12 +16,10 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.nio.ByteBuffer; import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNull; @@ -30,7 +28,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.openhab.core.library.types.DecimalType; import org.openhab.io.transport.modbus.ModbusBitUtilities; import org.openhab.io.transport.modbus.ModbusConstants.ValueType; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; /** @@ -39,12 +36,7 @@ import org.openhab.io.transport.modbus.ModbusRegisterArray; public class BitUtilitiesExtractStateFromRegistersTest { private static ModbusRegisterArray shortArrayToRegisterArray(int... arr) { - ModbusRegister[] tmp = new ModbusRegister[0]; - return new ModbusRegisterArray(IntStream.of(arr).mapToObj(val -> { - ByteBuffer buffer = ByteBuffer.allocate(2); - buffer.putShort((short) val); - return new ModbusRegister(buffer.get(0), buffer.get(1)); - }).collect(Collectors.toList()).toArray(tmp)); + return new ModbusRegisterArray(arr); } public static Collection data() { @@ -118,7 +110,7 @@ public class BitUtilitiesExtractStateFromRegistersTest { new Object[] { new DecimalType("64000"), ValueType.UINT16, shortArrayToRegisterArray(64000), 0 }, new Object[] { new DecimalType("64532"), ValueType.UINT16, shortArrayToRegisterArray(4, -1004), 1 }, new Object[] { new DecimalType("64532"), ValueType.UINT16, shortArrayToRegisterArray(-1004, 4), 0 }, - new Object[] { IllegalArgumentException.class, ValueType.INT16, shortArrayToRegisterArray(4, -1004), + new Object[] { IllegalArgumentException.class, ValueType.UINT16, shortArrayToRegisterArray(4, -1004), 2 }, // // INT32 @@ -364,7 +356,7 @@ public class BitUtilitiesExtractStateFromRegistersTest { @SuppressWarnings({ "unchecked", "rawtypes" }) @ParameterizedTest @MethodSource("data") - public void testCommandToRegisters(Object expectedResult, ValueType type, ModbusRegisterArray registers, + public void testextractStateFromRegisters(Object expectedResult, ValueType type, ModbusRegisterArray registers, int index) { if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { assertThrows((Class) expectedResult, diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java deleted file mode 100644 index 59b94885a2..0000000000 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.io.transport.modbus.test; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Collections; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.openhab.core.library.types.StringType; -import org.openhab.io.transport.modbus.ModbusBitUtilities; -import org.openhab.io.transport.modbus.ModbusRegister; -import org.openhab.io.transport.modbus.ModbusRegisterArray; - -import io.swagger.v3.oas.annotations.Parameters; - -/** - * @author Sami Salonen - Initial contribution - */ -public class BitUtilitiesExtractStringFromRegistersTest { - - private static ModbusRegisterArray shortArrayToRegisterArray(int... arr) { - ModbusRegister[] tmp = new ModbusRegister[0]; - return new ModbusRegisterArray(IntStream.of(arr).mapToObj(val -> { - ByteBuffer buffer = ByteBuffer.allocate(2); - buffer.putShort((short) val); - return new ModbusRegister(buffer.get(0), buffer.get(1)); - }).collect(Collectors.toList()).toArray(tmp)); - } - - @Parameters - public static Collection data() { - return Collections.unmodifiableList(Stream.of( - new Object[] { new StringType(""), shortArrayToRegisterArray(0), 0, 0, StandardCharsets.UTF_8 }, - new Object[] { new StringType("hello"), shortArrayToRegisterArray(0x6865, 0x6c6c, 0x6f00), 0, 5, - StandardCharsets.UTF_8 }, - new Object[] { new StringType("hello "), shortArrayToRegisterArray(0, 0, 0x6865, 0x6c6c, 0x6f20, 0, 0), - 2, 6, StandardCharsets.UTF_8 }, - new Object[] { new StringType("hello"), - shortArrayToRegisterArray(0x6865, 0x6c6c, 0x6f00, 0x0000, 0x0000), 0, 10, - StandardCharsets.UTF_8 }, - new Object[] { new StringType("árvíztűrő tükörfúrógép"), - shortArrayToRegisterArray(0xc3a1, 0x7276, 0xc3ad, 0x7a74, 0xc5b1, 0x72c5, 0x9120, 0x74c3, - 0xbc6b, 0xc3b6, 0x7266, 0xc3ba, 0x72c3, 0xb367, 0xc3a9, 0x7000), - 0, 32, StandardCharsets.UTF_8 }, - new Object[] { new StringType("árvíztűrő tükörfúrógép"), - shortArrayToRegisterArray(0xe172, 0x76ed, 0x7a74, 0xfb72, 0xf520, 0x74fc, 0x6bf6, 0x7266, - 0xfa72, 0xf367, 0xe970), - 0, 22, Charset.forName("ISO-8859-2") }, - - // Invalid values - new Object[] { IllegalArgumentException.class, shortArrayToRegisterArray(0, 0), 2, 4, - StandardCharsets.UTF_8 }, - new Object[] { IllegalArgumentException.class, shortArrayToRegisterArray(0, 0), 0, -1, - StandardCharsets.UTF_8 }, - new Object[] { IllegalArgumentException.class, shortArrayToRegisterArray(0, 0), 0, 5, - StandardCharsets.UTF_8 }) - .collect(Collectors.toList())); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @ParameterizedTest - @MethodSource("data") - public void testExtractStringFromRegisters(Object expectedResult, ModbusRegisterArray registers, int index, - int length, Charset charset) { - if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { - assertThrows((Class) expectedResult, - () -> ModbusBitUtilities.extractStringFromRegisters(registers, index, length, charset)); - return; - } - - StringType actualState = ModbusBitUtilities.extractStringFromRegisters(registers, index, length, charset); - assertThat(String.format("registers=%s, index=%d, length=%d", registers, index, length), actualState, - is(equalTo(expectedResult))); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringTest.java new file mode 100644 index 0000000000..bd762862f8 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringTest.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.Stream.Builder; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.io.transport.modbus.ModbusBitUtilities; +import org.openhab.io.transport.modbus.ModbusRegisterArray; + +/** + * @author Sami Salonen - Initial contribution + */ +public class BitUtilitiesExtractStringTest { + + private static ModbusRegisterArray shortArrayToRegisterArray(int... arr) { + return new ModbusRegisterArray(arr); + } + + public static Collection data() { + return Collections.unmodifiableList(Stream.of( + new Object[] { "", shortArrayToRegisterArray(0), 0, 0, StandardCharsets.UTF_8 }, + new Object[] { "hello", shortArrayToRegisterArray(0x6865, 0x6c6c, 0x6f00), 0, 5, + StandardCharsets.UTF_8 }, + new Object[] { "he", shortArrayToRegisterArray(0x6865, 0x6c6c, 0x6f00), 0, 2, StandardCharsets.UTF_8 }, // limited + // by + // count=2 + new Object[] { "hello ", shortArrayToRegisterArray(0, 0, 0x6865, 0x6c6c, 0x6f20, 0, 0), 2, 6, + StandardCharsets.UTF_8 }, + new Object[] { "hello", shortArrayToRegisterArray(0x6865, 0x6c6c, 0x6f00, 0x0000, 0x0000), 0, 10, + StandardCharsets.UTF_8 }, + new Object[] { "árvíztűrő tükörfúrógép", + shortArrayToRegisterArray(0xc3a1, 0x7276, 0xc3ad, 0x7a74, 0xc5b1, 0x72c5, 0x9120, 0x74c3, + 0xbc6b, 0xc3b6, 0x7266, 0xc3ba, 0x72c3, 0xb367, 0xc3a9, 0x7000), + 0, 32, StandardCharsets.UTF_8 }, + new Object[] { "你好,世界", + shortArrayToRegisterArray(0xe4bd, 0xa0e5, 0xa5bd, 0xefbc, 0x8ce4, 0xb896, 0xe795, 0x8c00), 0, + 16, StandardCharsets.UTF_8 }, + new Object[] { "árvíztűrő tükörfúrógép", + shortArrayToRegisterArray(0xe172, 0x76ed, 0x7a74, 0xfb72, 0xf520, 0x74fc, 0x6bf6, 0x7266, + 0xfa72, 0xf367, 0xe970), + 0, 22, Charset.forName("ISO-8859-2") }, + // Example where registers contain 0 byte in between -- only the data preceding zero byte is parsed + new Object[] { "hello", shortArrayToRegisterArray(0x6865, 0x6c6c, 0x6f00, 0x776f, 0x726c, 0x64), 0, 10, + StandardCharsets.UTF_8 }, + + // Invalid values + // 0xe4 = "ä" in extended ascii but not covered by US_ASCII. Will be replaced by � + new Object[] { "�", shortArrayToRegisterArray(0xe400), 0, 2, StandardCharsets.US_ASCII }, + // out of bounds + new Object[] { IllegalArgumentException.class, shortArrayToRegisterArray(0, 0), 2, 4, + StandardCharsets.UTF_8 }, + // negative index + new Object[] { IllegalArgumentException.class, shortArrayToRegisterArray(0, 0), 0, -1, + StandardCharsets.UTF_8 }, + // out of bounds + new Object[] { IllegalArgumentException.class, shortArrayToRegisterArray(0, 0), 0, 5, + StandardCharsets.UTF_8 }) + .collect(Collectors.toList())); + } + + public static Stream dataWithByteVariations() { + return data().stream().flatMap(vals -> { + Object expected = vals[0]; + ModbusRegisterArray registers = (ModbusRegisterArray) vals[1]; + int index = (int) vals[2]; + int length = (int) vals[3]; + Charset charset = (Charset) vals[4]; + + byte[] origBytes = registers.getBytes(); + int origRegisterIndex = index; + int origByteIndex = origRegisterIndex * 2; + + Builder streamBuilder = Stream.builder(); + for (int offset = 0; offset < 5; offset++) { + byte[] bytesOffsetted = new byte[origBytes.length + offset]; + System.arraycopy(origBytes, 0, bytesOffsetted, offset, origBytes.length); + streamBuilder.add( + new Object[] { expected, offset, bytesOffsetted, origByteIndex + offset, length, charset }); + } + Stream variations = streamBuilder.build(); + return variations; + }); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @ParameterizedTest + @MethodSource("data") + public void testExtractStringFromRegisters(Object expectedResult, ModbusRegisterArray registers, int index, + int length, Charset charset) { + if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { + assertThrows((Class) expectedResult, + () -> ModbusBitUtilities.extractStringFromRegisters(registers, index, length, charset)); + return; + } else { + String actualState = ModbusBitUtilities.extractStringFromRegisters(registers, index, length, charset); + assertEquals(actualState, expectedResult, + String.format("registers=%s, index=%d, length=%d", registers, index, length)); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @ParameterizedTest + @MethodSource("dataWithByteVariations") + public void testExtractStringFromBytes(Object expectedResult, int byteOffset, byte[] bytes, int byteIndex, + int length, Charset charset) { + if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) { + assertThrows((Class) expectedResult, + () -> ModbusBitUtilities.extractStringFromBytes(bytes, byteIndex, length, charset)); + return; + } else { + String actualState = ModbusBitUtilities.extractStringFromBytes(bytes, byteIndex, length, charset); + assertEquals(actualState, expectedResult, String.format("registers=%s, index=%d, length=%d, byteIndex=%d", + bytes, byteIndex, length, byteIndex)); + } + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/RegisterMatcher.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/RegisterMatcher.java index dee72dda33..2eb0c4a873 100644 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/RegisterMatcher.java +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/RegisterMatcher.java @@ -14,9 +14,11 @@ package org.openhab.io.transport.modbus.test; import java.util.Arrays; import java.util.Objects; +import java.util.stream.IntStream; import java.util.stream.StreamSupport; import org.hamcrest.Description; +import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusWriteFunctionCode; import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; @@ -42,7 +44,9 @@ class RegisterMatcher extends AbstractRequestComparer r.getValue()) + ModbusRegisterArray registers = item.getRegisters(); + Object[] actual = StreamSupport + .stream(IntStream.range(0, registers.size()).mapToObj(registers::getRegister).spliterator(), false) .toArray(); return Objects.deepEquals(actual, expectedRegisterValues); } diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java index 912175e4d5..f5c9973147 100644 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java @@ -19,11 +19,14 @@ import static org.junit.jupiter.api.Assumptions.*; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.Socket; import java.net.SocketImpl; import java.net.SocketImplFactory; +import java.net.SocketOption; +import java.net.StandardSocketOptions; import java.net.UnknownHostException; import java.util.BitSet; import java.util.Optional; @@ -120,16 +123,16 @@ public class SmokeTest extends IntegrationTestSupport { private void testHoldingValues(ModbusRegisterArray registers, int offsetInRegisters) { for (int i = 0; i < registers.size(); i++) { int expected = (i + offsetInRegisters) * HOLDING_REGISTER_MULTIPLIER; - assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i).toUnsignedShort(), - expected), registers.getRegister(i).toUnsignedShort(), is(equalTo(expected))); + assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i), expected), + registers.getRegister(i), is(equalTo(expected))); } } private void testInputValues(ModbusRegisterArray registers, int offsetInRegisters) { for (int i = 0; i < registers.size(); i++) { int expected = (i + offsetInRegisters) * INPUT_REGISTER_MULTIPLIER; - assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i).toUnsignedShort(), - expected), registers.getRegister(i).toUnsignedShort(), is(equalTo(expected))); + assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i), expected), + registers.getRegister(i), is(equalTo(expected))); } } @@ -945,11 +948,7 @@ public class SmokeTest extends IntegrationTestSupport { } catch (UnknownHostException e) { throw new RuntimeException(e); } - return socketSpy.sockets.stream().filter(socketImpl -> { - Socket socket = getSocketOfSocketImpl(socketImpl); - return socket.getPort() == tcpModbusPort && socket.isConnected() - && socket.getLocalAddress().equals(testServerAddress); - }).count(); + return socketSpy.sockets.stream().filter(this::isConnectedToTestServer).count(); } /** @@ -972,25 +971,87 @@ public class SmokeTest extends IntegrationTestSupport { private static SocketImpl newSocksSocketImpl() { try { - Class defaultSocketImpl = Class.forName("java.net.SocksSocketImpl"); - Constructor constructor = defaultSocketImpl.getDeclaredConstructor(); - constructor.setAccessible(true); - return (SocketImpl) constructor.newInstance(); + Class socksSocketImplClass = Class.forName("java.net.SocksSocketImpl"); + Class socketImplClass = SocketImpl.class; + + // // For Debugging + // for (Method method : socketImplClass.getDeclaredMethods()) { + // LoggerFactory.getLogger("foobar") + // .error("SocketImpl." + method.getName() + Arrays.toString(method.getParameters())); + // } + // for (Constructor constructor : socketImplClass.getDeclaredConstructors()) { + // LoggerFactory.getLogger("foobar") + // .error("SocketImpl." + constructor.getName() + Arrays.toString(constructor.getParameters())); + // } + // for (Method method : socksSocketImplClass.getDeclaredMethods()) { + // LoggerFactory.getLogger("foobar") + // .error("SocksSocketImpl." + method.getName() + Arrays.toString(method.getParameters())); + // } + // for (Constructor constructor : socksSocketImplClass.getDeclaredConstructors()) { + // LoggerFactory.getLogger("foobar").error( + // "SocksSocketImpl." + constructor.getName() + Arrays.toString(constructor.getParameters())); + // } + + try { + Constructor constructor = socksSocketImplClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return (SocketImpl) constructor.newInstance(); + } catch (NoSuchMethodException e) { + // Newer Javas (Java 14->) do not have default constructor 'SocksSocketImpl()' + // Instead we use "static SocketImpl.createPlatformSocketImpl" and "SocksSocketImpl(SocketImpl) + Method socketImplCreateMethod = socketImplClass.getDeclaredMethod("createPlatformSocketImpl", + boolean.class); + socketImplCreateMethod.setAccessible(true); + Object socketImpl = socketImplCreateMethod.invoke(/* null since we deal with static method */ null, + /* server */false); + + Constructor socksSocketImplConstructor = socksSocketImplClass + .getDeclaredConstructor(socketImplClass); + socksSocketImplConstructor.setAccessible(true); + return (SocketImpl) socksSocketImplConstructor.newInstance(socketImpl); + } } catch (Exception e) { throw new RuntimeException(e); } } - /** - * Get Socket corresponding to SocketImpl using reflection - */ - private static Socket getSocketOfSocketImpl(SocketImpl impl) { + private boolean isConnectedToTestServer(SocketImpl impl) { + final InetAddress testServerAddress; try { - Method getSocket = SocketImpl.class.getDeclaredMethod("getSocket"); - getSocket.setAccessible(true); - return (Socket) getSocket.invoke(impl); - } catch (Exception e) { + testServerAddress = localAddress(); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + + final int port; + boolean connected = true; + final InetAddress address; + try { + Method getPort = SocketImpl.class.getDeclaredMethod("getPort"); + getPort.setAccessible(true); + port = (int) getPort.invoke(impl); + + Method getInetAddressMethod = SocketImpl.class.getDeclaredMethod("getInetAddress"); + getInetAddressMethod.setAccessible(true); + address = (InetAddress) getInetAddressMethod.invoke(impl); + + // hacky (but java8-14 compatible) way to know if socket is open + // SocketImpl.getOption throws IOException when socket is closed + Method getOption = SocketImpl.class.getDeclaredMethod("getOption", SocketOption.class); + getOption.setAccessible(true); + try { + getOption.invoke(impl, StandardSocketOptions.SO_KEEPALIVE); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof IOException) { + connected = false; + } else { + throw e; + } + } + } catch (InvocationTargetException | SecurityException | IllegalArgumentException | IllegalAccessException + | NoSuchMethodException e) { throw new RuntimeException(e); } + return port == tcpModbusPort && connected && address.equals(testServerAddress); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/ValueBufferTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/ValueBufferTest.java new file mode 100644 index 0000000000..7f14c137ad --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/ValueBufferTest.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.transport.modbus.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.BufferOverflowException; +import java.nio.InvalidMarkException; + +import org.junit.jupiter.api.Test; +import org.openhab.io.transport.modbus.ValueBuffer; + +/** + * @author Sami Salonen - Initial contribution + */ +public class ValueBufferTest { + + @Test + public void testInt32Int8() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFC, 0x14, 3, -1, -2 }); + assertEquals(7, wrap.remaining()); + assertTrue(wrap.hasRemaining()); + + assertEquals(-1004, wrap.getSInt32()); + assertEquals(3, wrap.remaining()); + assertTrue(wrap.hasRemaining()); + + assertEquals(3, wrap.getSInt8()); + assertEquals(2, wrap.remaining()); + assertTrue(wrap.hasRemaining()); + + assertEquals(-1, wrap.getSInt8()); + assertEquals(1, wrap.remaining()); + assertTrue(wrap.hasRemaining()); + + assertEquals(254, wrap.getUInt8()); + assertEquals(0, wrap.remaining()); + assertFalse(wrap.hasRemaining()); + } + + @Test + public void testOutOfBounds() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFC, 0x14, 3, -1, -2 }); + wrap.position(7); + assertThrows(IllegalArgumentException.class, () -> wrap.getSInt8()); + } + + @Test + public void testOutOfBound2s() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFC, 0x14, 3, -1, -2 }); + wrap.position(6); + assertThrows(IllegalArgumentException.class, () -> wrap.getSInt16()); + } + + @Test + public void testMarkReset() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFC, 0x14, 3, -1, -2 }); + wrap.mark(); + assertEquals(-1004, wrap.getSInt32()); + wrap.reset(); + assertEquals(4294966292L, wrap.getUInt32()); + wrap.mark(); + assertEquals(3, wrap.getSInt8()); + wrap.reset(); + assertEquals(3, wrap.getSInt8()); + assertEquals(-1, wrap.getSInt8()); + assertEquals(254, wrap.getUInt8()); + } + + @Test + public void testMarkHigherThanPosition() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFC, 0x14, 3, -1, -2 }); + assertEquals(-1004, wrap.getSInt32()); + wrap.position(4); + wrap.mark(); + assertEquals(4, wrap.position()); + + // mark = position + wrap.position(4); + assertEquals(4, wrap.position()); + wrap.reset(); + assertEquals(4, wrap.position()); + + // position < mark + wrap.position(3); // Mark is removed here + assertEquals(3, wrap.position()); + boolean caughtException = false; + try { + wrap.reset(); + } catch (InvalidMarkException e) { + // OK, expected + caughtException = true; + } + assertTrue(caughtException); + assertEquals(3, wrap.position()); + + // Mark is removed. Reset unaccessible even with original position of 4 + wrap.position(4); + assertEquals(4, wrap.position()); + caughtException = false; + try { + wrap.reset(); + } catch (InvalidMarkException e) { + // OK, expected + caughtException = true; + } + assertTrue(caughtException); + } + + @Test + public void testMarkLowerThanPosition() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFC, 0x14, 3, -1, -2 }); + assertEquals(-1004, wrap.getSInt32()); + wrap.position(4); + wrap.mark(); + assertEquals(4, wrap.position()); + + // mark = position + wrap.position(4); + assertEquals(4, wrap.position()); + wrap.reset(); + assertEquals(4, wrap.position()); + + // mark > position + wrap.position(6); + assertEquals(6, wrap.position()); + wrap.reset(); + assertEquals(4, wrap.position()); + } + + @Test + public void testPosition() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { 0, 0, 0, 1, 3, -1, -2 }); + assertEquals(0, wrap.position()); + + wrap.position(4); + assertEquals(4, wrap.position()); + assertEquals(3, wrap.getSInt8()); + assertEquals(5, wrap.position()); + } + + @Test + public void testBulkGetBufferOverflow() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { 0, 0 }); + byte[] threeBytes = new byte[3]; + assertThrows(BufferOverflowException.class, () -> wrap.get(threeBytes)); + } + + @Test + public void testBulkGetAtCapacity() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { 1, 2 }); + byte[] twoBytes = new byte[2]; + wrap.get(twoBytes); + assertEquals(1, twoBytes[0]); + assertEquals(2, twoBytes[1]); + assertEquals(2, wrap.position()); + assertFalse(wrap.hasRemaining()); + } + + @Test + public void testBulkGet() { + ValueBuffer wrap = ValueBuffer.wrap(new byte[] { 1, 2, 3 }); + byte[] onebyte = new byte[1]; + wrap.get(onebyte); + assertEquals(1, onebyte[0]); + assertEquals(1, wrap.position()); + + // non-zero position + byte[] twoBytes = new byte[2]; + wrap.position(1); + wrap.get(twoBytes); + assertEquals(2, twoBytes[0]); + assertEquals(3, twoBytes[1]); + assertEquals(3, wrap.position()); + } +} diff --git a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java index 706b9f44e4..65e99e8640 100644 --- a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java +++ b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java @@ -68,7 +68,6 @@ import org.openhab.io.transport.modbus.ModbusConstants; import org.openhab.io.transport.modbus.ModbusConstants.ValueType; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusResponse; import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint; @@ -517,7 +516,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { public void testOnRegistersInt16StaticTransformation() { ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, "0", "-3", ModbusConstants.ValueType.INT16, null, - new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null); + new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class))); @@ -538,8 +537,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { mockTransformation("MULTIPLY", new MultiplyTransformation()); ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, "0", "MULTIPLY(10)", ModbusConstants.ValueType.INT16, null, - new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null, - bundleContext); + new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class))); @@ -561,8 +559,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, "0", "default", ModbusConstants.ValueType.FLOAT32, null, new ModbusRegisterArray( // equivalent of floating point NaN - new ModbusRegister[] { new ModbusRegister((byte) 0x7f, (byte) 0xc0), - new ModbusRegister((byte) 0x00, (byte) 0x00) }), + new byte[] { (byte) 0x7f, (byte) 0xc0, (byte) 0x00, (byte) 0x00 }), null, bundleContext); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); @@ -588,8 +585,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { }); ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, "0", "ONOFF(10)", ModbusConstants.ValueType.INT16, null, - new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null, - bundleContext); + new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class))); @@ -667,8 +663,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER))); assertThat(writeRequest.getReference(), is(equalTo(50))); assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0).getValue(), - is(equalTo(5))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0), is(equalTo(5))); } @Test @@ -702,11 +697,11 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS))); assertThat(writeRequest.getReference(), is(equalTo(5412))); assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(3))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0).getValue(), + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0), is(equalTo(1))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1).getValue(), + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1), is(equalTo(0))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2).getValue(), + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2), is(equalTo(5))); } { @@ -714,7 +709,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest { assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER))); assertThat(writeRequest.getReference(), is(equalTo(555))); assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0).getValue(), + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0), is(equalTo(3))); } } -- 2.47.3