Moved modbus transport from addons to core (#1892)

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer 2020-12-08 12:55:52 +01:00 committed by GitHub
parent 0abf1aa987
commit e075df30f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 10215 additions and 0 deletions

View File

@ -148,6 +148,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.modbus</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.serial</artifactId>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.io.transport.modbus</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,20 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-core
== Third-party Content
jsoup
* License: MIT License
* Project: https://jsoup.org/
* Source: https://github.com/jhy/jsoup

View File

@ -0,0 +1,10 @@
Bundle-SymbolicName: ${project.artifactId}
Automatic-Module-Name: ${def;bsn}
Import-Package: \
org.eclipse.jdt.annotation.*;resolution:=optional,\
org.openhab.*;version=!,\
!net.sf.cglib.proxy,\
gnu.io;version="[3.12,6)",\
*
-sources: false
-contract: *

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.io.transport.modbus</artifactId>
<name>openHAB Core :: Bundles :: Modbus Transport</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.wimpi</groupId>
<artifactId>jamod</artifactId>
<version>1.2.4.OH</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>embed-dependencies</id>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<includeTypes>jar</includeTypes>
<excludeGroupIds>commons-pool2,org.openhab.core.bundles</excludeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<excludeTransitive>true</excludeTransitive>
<type>jar</type>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,65 @@
/**
* 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.core.io.transport.modbus;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Encapsulates result of modbus read operations
*
* @author Nagy Attila Gabor - Initial contribution
*/
@NonNullByDefault
public class AsyncModbusFailure<R> {
private final R request;
private final Exception cause;
public AsyncModbusFailure(R request, Exception cause) {
Objects.requireNonNull(request, "Request must not be null!");
Objects.requireNonNull(cause, "Cause must not be null!");
this.request = request;
this.cause = cause;
}
/**
* Get request matching this response
*
* @return request object
*/
public R getRequest() {
return request;
}
/**
* Get cause of error
*
* @return exception representing error
*/
public Exception getCause() {
return cause;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("AsyncModbusReadResult(");
builder.append("request = ");
builder.append(request);
builder.append(", error = ");
builder.append(cause);
builder.append(")");
return builder.toString();
}
}

View File

@ -0,0 +1,93 @@
/**
* 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.core.io.transport.modbus;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Encapsulates result of modbus read operations
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class AsyncModbusReadResult {
private final ModbusReadRequestBlueprint request;
private final Optional<BitArray> bits;
private final Optional<ModbusRegisterArray> registers;
public AsyncModbusReadResult(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
Objects.requireNonNull(request, "Request must not be null!");
Objects.requireNonNull(registers, "Registers must not be null!");
this.request = request;
this.registers = Optional.of(registers);
this.bits = Optional.empty();
}
public AsyncModbusReadResult(ModbusReadRequestBlueprint request, BitArray bits) {
Objects.requireNonNull(request, "Request must not be null!");
Objects.requireNonNull(bits, "Bits must not be null!");
this.request = request;
this.registers = Optional.empty();
this.bits = Optional.of(bits);
}
/**
* Get request matching this response
*
* @return request object
*/
public ModbusReadRequestBlueprint getRequest() {
return request;
}
/**
* Get "coil" or "discrete input" bit data in the case of no errors
*
* @return bit data
*/
public Optional<BitArray> getBits() {
return bits;
}
/**
* Get "input register" or "holding register" data in the case of no errors
*
* @return register data
*/
public Optional<ModbusRegisterArray> getRegisters() {
return registers;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("AsyncModbusReadResult(");
builder.append("request = ");
builder.append(request);
bits.ifPresent(bits -> {
builder.append(", bits = ");
builder.append(bits);
});
registers.ifPresent(registers -> {
builder.append(", registers = ");
builder.append(registers);
});
builder.append(")");
return builder.toString();
}
}

View File

@ -0,0 +1,66 @@
/**
* 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.core.io.transport.modbus;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Encapsulates result of modbus write operations
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class AsyncModbusWriteResult {
private final ModbusWriteRequestBlueprint request;
private final ModbusResponse response;
public AsyncModbusWriteResult(ModbusWriteRequestBlueprint request, ModbusResponse response) {
Objects.requireNonNull(request, "Request must not be null!");
Objects.requireNonNull(response, "Response must not be null!");
this.request = request;
this.response = response;
}
/**
* Get request matching this response
*
* @return request object
*/
public ModbusWriteRequestBlueprint getRequest() {
return request;
}
/**
* Get response
*
* @return response
*/
public ModbusResponse getResponse() {
return response;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("AsyncModbusWriteResult(");
builder.append("request = ");
builder.append(request);
builder.append(", response = ");
builder.append(response);
builder.append(")");
return builder.toString();
}
}

View File

@ -0,0 +1,141 @@
/**
* 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.core.io.transport.modbus;
import java.util.BitSet;
import java.util.Iterator;
import java.util.stream.IntStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Class that implements a collection for
* bits
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class BitArray implements Iterable<Boolean> {
private final BitSet wrapped;
private final int length;
public BitArray(int nbits) {
this(new BitSet(nbits), nbits);
}
public BitArray(boolean... bits) {
this(bitSetFromBooleans(bits), bits.length);
}
public BitArray(BitSet wrapped, int length) {
this.wrapped = wrapped;
this.length = length;
}
private static BitSet bitSetFromBooleans(boolean... bits) {
BitSet bitSet = new BitSet(bits.length);
for (int i = 0; i < bits.length; i++) {
bitSet.set(i, bits[i]);
}
return bitSet;
}
private boolean sizeAndValuesEquals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof BitArray)) {
return false;
}
BitArray other = (BitArray) obj;
if (this.size() != other.size()) {
return false;
}
for (int i = 0; i < this.size(); i++) {
if (this.getBit(i) != other.getBit(i)) {
return false;
}
}
return true;
}
/**
* Returns the state of the bit at the given index
*
* Index 0 matches LSB (rightmost) bit
* <p>
*
* @param index the index of the bit to be returned.
* @return true if the bit at the specified index is set,
* false otherwise.
* @throws IndexOutOfBoundsException if the index is out of bounds.
*/
public boolean getBit(int index) {
if (index >= size()) {
throw new IndexOutOfBoundsException();
}
return this.wrapped.get(index);
}
public void setBit(int index, boolean value) {
if (value) {
this.wrapped.set(index);
} else {
this.wrapped.clear(index);
}
}
/**
* Get number of bits stored in this instance
*
* @return
*/
public int size() {
return length;
}
@Override
public String toString() {
return new StringBuilder("BitArray(bits=").append(length == 0 ? "<empty>" : toBinaryString()).append(")")
.toString();
}
@Override
public Iterator<Boolean> iterator() {
return IntStream.range(0, size()).mapToObj(i -> getBit(i)).iterator();
}
@Override
public boolean equals(@Nullable Object obj) {
return sizeAndValuesEquals(obj);
}
/**
* Get data as binary string
*
* For example, 0010
*
* @return string representing the data
*/
public String toBinaryString() {
final StringBuilder buffer = new StringBuilder(size());
IntStream.range(0, size()).mapToObj(i -> getBit(i) ? '1' : '0').forEach(buffer::append);
return buffer.toString();
}
}

View File

@ -0,0 +1,759 @@
/**
* 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.core.io.transport.modbus;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.Optional;
import org.apache.commons.lang.NotImplementedException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
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.types.Command;
/**
* Utilities for working with binary data.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusBitUtilities {
/**
* Read data from registers and convert the result to DecimalType
* Interpretation of <tt>index</tt> goes as follows depending on type
*
* BIT:
* - a single bit is read from the registers
* - 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.
* INT8:
* - a byte (8 bits) from the registers is interpreted as signed integer
* - 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
* UINT8:
* - same as INT8 except value is interpreted as unsigned integer
* INT16:
* - register with index (counting from zero) is interpreted as 16 bit signed integer.
* - it is assumed that each register is encoded in most significant bit first order
* UINT16:
* - same as INT16 except value is interpreted as unsigned integer
* INT32:
* - registers (index) and (index + 1) are interpreted as signed 32bit integer.
* - it assumed that the first register contains the most significant 16 bits
* - it is assumed that each register is encoded in most significant bit first order
* INT32_SWAP:
* - Same as INT32 but registers swapped
* UINT32:
* - same as INT32 except value is interpreted as unsigned integer
* UINT32_SWAP:
* - same as INT32_SWAP except value is interpreted as unsigned integer
* FLOAT32:
* - registers (index) and (index + 1) are interpreted as signed 32bit floating point number.
* - it assumed that the first register contains the most significant 16 bits
* - it is assumed that each register is encoded in most significant bit first order
* - floating point NaN and infinity will return as empty optional
* FLOAT32_SWAP:
* - Same as FLOAT32 but registers swapped
* INT64:
* - registers (index), (index + 1), (index + 2), (index + 3) are interpreted as signed 64bit integer.
* - it assumed that the first register contains the most significant 16 bits
* - it is assumed that each register is encoded in most significant bit first order
* INT64_SWAP:
* - same as INT64 but registers swapped, that is, registers (index + 3), (index + 2), (index + 1), (index + 1) are
* interpreted as signed 64bit integer
* UINT64:
* - same as INT64 except value is interpreted as unsigned integer
* UINT64_SWAP:
* - same as INT64_SWAP except value is interpreted as unsigned integer
*
* @param registers list of registers, each register represent 16bit of data
* @param index zero based item index. Interpretation of this depends on type, see examples above.
* With type larger or equal to 16 bits, the index tells the register index to start reading
* from.
* With type less than 16 bits, the index tells the N'th item to read from the registers.
* @param type item type, e.g. unsigned 16bit integer (<tt>ModbusBindingProvider.ValueType.UINT16</tt>)
* @return number representation queried value, <tt>DecimalType</tt>. Empty optional is returned
* with NaN and infinity floating point values
* @throws NotImplementedException in cases where implementation is lacking for the type. This can be considered a
* bug
* @throws IllegalArgumentException when <tt>index</tt> is out of bounds of registers
*
*/
public static Optional<DecimalType> extractStateFromRegisters(ModbusRegisterArray registers, int index,
ModbusConstants.ValueType type) {
byte[] bytes = registers.getBytes();
switch (type) {
case BIT:
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 UINT8: {
int registerIndex = index / 2;
boolean hiByte = index % 2 == 1;
return Optional.of(new DecimalType(extractUInt8(bytes, registerIndex, hiByte)));
}
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(extractFloat32(bytes, index * 2)));
} catch (NumberFormatException e) {
// floating point NaN or infinity encountered
return Optional.empty();
}
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(extractFloat32Swap(bytes, index * 2)));
} catch (NumberFormatException e) {
// floating point NaN or infinity encountered
return Optional.empty();
}
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();
}
/**
* 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 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 (number of bytes considered)
* @param charset the character set used to construct the string.
* @return string representation queried value
* @throws IllegalArgumentException when <tt>index</tt> is out of bounds of registers
*/
public static String extractStringFromRegisters(ModbusRegisterArray registers, int registerIndex, int length,
Charset charset) {
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 <tt>index</tt> 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("byteIndex=%d with length=%d is out-of-bounds given registers of size %d", byteIndex,
length, bytes.length));
}
if (byteIndex < 0) {
throw new IllegalArgumentException("Negative index values are not supported");
}
if (length < 0) {
throw new IllegalArgumentException("Negative string length is not supported");
}
int effectiveLength = length;
// 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;
}
}
return new String(bytes, byteIndex, effectiveLength, charset);
}
/**
* Convert command to array of registers using a specific value type
*
* @param command command to be converted
* @param type value type to use in conversion
* @return array of registers
* @throws NotImplementedException in cases where implementation is lacking for the type. This is thrown with 1-bit
* and 8-bit value types
*/
public static ModbusRegisterArray commandToRegisters(Command command, ModbusConstants.ValueType type) {
DecimalType numericCommand;
if (command instanceof OnOffType || command instanceof OpenClosedType) {
numericCommand = translateCommand2Boolean(command).get() ? new DecimalType(BigDecimal.ONE)
: DecimalType.ZERO;
} else if (command instanceof DecimalType) {
numericCommand = (DecimalType) command;
} else {
throw new NotImplementedException(String.format(
"Command '%s' of class '%s' cannot be converted to registers. Please use OnOffType, OpenClosedType, or DecimalType commands.",
command, command.getClass().getName()));
}
if (type.getBits() != 16 && type.getBits() != 32 && type.getBits() != 64) {
throw new IllegalArgumentException(String.format(
"Illegal type=%s (bits=%d). Only 16bit and 32bit types are supported", type, type.getBits()));
}
switch (type) {
case INT16:
case UINT16: {
short shortValue = numericCommand.shortValue();
// big endian byte ordering
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 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 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 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 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 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 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(
String.format("Illegal type=%s. Missing implementation for this type", type));
}
}
/**
* Converts command to a boolean
*
* true value is represented by {@link OnOffType.ON}, {@link OpenClosedType.OPEN}.
* false value is represented by {@link OnOffType.OFF}, {@link OpenClosedType.CLOSED}.
* Furthermore, {@link DecimalType} are converted to boolean true if they are unequal to zero.
*
* @param command to convert to boolean
* @return Boolean value matching the command. Empty if command cannot be converted
*/
public static Optional<Boolean> translateCommand2Boolean(Command command) {
if (command.equals(OnOffType.ON)) {
return Optional.of(Boolean.TRUE);
}
if (command.equals(OnOffType.OFF)) {
return Optional.of(Boolean.FALSE);
}
if (command.equals(OpenClosedType.OPEN)) {
return Optional.of(Boolean.TRUE);
}
if (command.equals(OpenClosedType.CLOSED)) {
return Optional.of(Boolean.FALSE);
}
if (command instanceof DecimalType) {
return Optional.of(!command.equals(DecimalType.ZERO));
}
return Optional.empty();
}
}

View File

@ -0,0 +1,109 @@
/**
* 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.core.io.transport.modbus;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* Interface for interacting with a particular modbus slave.
*
* When no further communication is expected with the slave, close the interface so that any underlying resources can be
* freed.
*
* Close unregisters all the regular polls registered with registerRegularPoll. When endpoint's last
* communication interface is closed, the connection is closed as well, no matter the what EndpointPoolConfiguration
* says.
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public interface ModbusCommunicationInterface extends AutoCloseable {
/**
* Get endpoint associated with this communication interface
*
* @return modbus slave endpoint
*/
public ModbusSlaveEndpoint getEndpoint();
/**
* Submit one-time poll task. The method returns immediately, and the execution of the poll task will happen in
* background.
*
* @param request request to send
* @param callback callback to call with data
* @param callback callback to call in case of failure
* @return future representing the polled task
* @throws IllegalStateException when this communication has been closed already
*/
public Future<?> submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusReadCallback resultCallback,
ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback);
/**
* Register regularly polled task. The method returns immediately, and the execution of the poll task will happen in
* the background.
*
* One can register only one regular poll task for triplet of (endpoint, request, callback).
*
* @param request request to send
* @param pollPeriodMillis poll interval, in milliseconds
* @param initialDelayMillis initial delay before starting polling, in milliseconds
* @param callback callback to call with data
* @param callback callback to call in case of failure
* @return poll task representing the regular poll
* @throws IllegalStateException when this communication has been closed already
*/
public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pollPeriodMillis,
long initialDelayMillis, ModbusReadCallback resultCallback,
ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback);
/**
* Unregister regularly polled task
*
* If this communication interface is closed already, the method returns immediately with false return value
*
* @param task poll task to unregister
* @return whether poll task was unregistered. Poll task is not unregistered in case of unexpected errors or
* in the case where the poll task is not registered in the first place
*/
public boolean unregisterRegularPoll(PollTask task);
/**
* Submit one-time write task. The method returns immediately, and the execution of the task will happen in
* background.
*
* @param request request to send
* @param callback callback to call with response
* @param callback callback to call in case of failure
* @return future representing the task
* @throws IllegalStateException when this communication has been closed already
*/
public Future<?> submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusWriteCallback resultCallback,
ModbusFailureCallback<ModbusWriteRequestBlueprint> failureCallback);
/**
* Close this communication interface and try to free all resources associated with it
*
* Upon close, all polling tasks registered by this instance are unregistered. In addition, connections are closed
* eagerly if this was the last connection interface pointing to the endpoint.
*
* After close, the communication interface cannot be used to communicate with the device.
*
*/
@Override
public void close() throws Exception;
}

View File

@ -0,0 +1,146 @@
/**
* 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.core.io.transport.modbus;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Constants for Modbus transport
*
* == Regarding maximum read and write limits ==
*
* Maximum number of registers that are allowed to be read.
*
* The Modbus protocol has many intepretation on maximum data size of messages. Good reference is here:
* https://wingpath.co.uk/manpage.php?product=modtest&page=message_limits.html
*
* We try to follow modern specification here (V1.1B3):
* https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf. See section 4.1 Protocol Specification in the
* specification.
*
* According to V1.1B3, maximum size for PDU is 253 bytes, making maximum ADU size 256 (RTU) or 260 (TCP).
*
* In the spec section 6, one can see maximum values for read and write counts.
*
* Note that this is not the only interpretation -- some sources limit the ADU to 256 also with TCP.
* In some cases, slaves cannot take in so much data.
*
*
* Reads are limited by response PDU size.
* Writes (FC15 & FC16) are limited by write request ADU size.
*
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusConstants {
/**
* Value types for different number types.
*
* @author Sami Salonen - Initial contribution
*
*/
public static enum ValueType {
BIT("bit", 1),
INT8("int8", 8),
UINT8("uint8", 8),
INT16("int16", 16),
UINT16("uint16", 16),
INT32("int32", 32),
UINT32("uint32", 32),
FLOAT32("float32", 32),
INT64("int64", 64),
UINT64("uint64", 64),
INT32_SWAP("int32_swap", 32),
UINT32_SWAP("uint32_swap", 32),
FLOAT32_SWAP("float32_swap", 32),
INT64_SWAP("int64_swap", 64),
UINT64_SWAP("uint64_swap", 64);
private final String configValue;
private final int bits;
ValueType(String configValue, int bits) {
this.configValue = configValue;
this.bits = bits;
}
/**
* Returns number of bits represented by this ValueType
*
* @return number of bits
*/
public int getBits() {
return bits;
}
/**
* Returns config value to refer to this value type
*
* @return config value as string
*/
public String getConfigValue() {
return configValue;
}
/**
* Returns config value
*/
@Override
public String toString() {
return getConfigValue();
}
/**
* Constructs ValueType given the config value string.
*
* @param configValueType config value that will be parsed to ValueType
* @return ValueType matching the config value
* @throws IllegalArgumentException with unknown value types
*/
@SuppressWarnings("null")
public static @NonNull ValueType fromConfigValue(@Nullable String configValueType)
throws IllegalArgumentException {
return Stream.of(ValueType.values()).filter(v -> v.getConfigValue().equals(configValueType)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid valueType " + configValueType));
}
}
/**
* Maximum number of coils or discrete inputs that are allowed to be read.
* Limitation by Modbus protocol V1.1B3, 6.1 definition of Read Holding registers.
*/
public static final int MAX_BITS_READ_COUNT = 2000;
/**
* Maximum number of registers that are allowed to be read.
* Limitation by Modbus protocol V1.1B3, 6.3 definition of Read Coils.
*/
public static final int MAX_REGISTERS_READ_COUNT = 125;
/**
* Maximum number of coils or discrete inputs that are allowed to be written.
* Limitation by Modbus protocol V1.1B3, 6.11 definition of Write Multiple coils.
*/
public static final int MAX_BITS_WRITE_COUNT = 1968;
/**
* Maximum number of registers that are allowed to be written.
* Limitation by Modbus protocol V1.1B3, 6.12 definition of Write Multiple registers.
*/
public static final int MAX_REGISTERS_WRITE_COUNT = 123;
}

View File

@ -0,0 +1,31 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Callback used to report failure in Modbus
*
* @author Nagy Attila Gabor - Initial contribution
*/
@FunctionalInterface
@NonNullByDefault
public interface ModbusFailureCallback<R> {
/**
* Callback handling response with error
*
* @param asyncModbusFailure details of the failure
*/
void handle(AsyncModbusFailure<R> failure);
}

View File

@ -0,0 +1,49 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.endpoint.EndpointPoolConfiguration;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* ModbusManager is the main interface for interacting with Modbus slaves
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusManager {
/**
* Open communication interface to endpoint
*
* @param endpoint endpoint pointing to modbus slave
* @param configuration configuration for the endpoint
* @return Communication interface for interacting with the slave
* @throws IllegalArgumentException if there is already open communication interface with same endpoint but
* differing configuration
*/
public ModbusCommunicationInterface newModbusCommunicationInterface(ModbusSlaveEndpoint endpoint,
@Nullable EndpointPoolConfiguration configuration) throws IllegalArgumentException;
/**
* Get general configuration settings applied to a given endpoint
*
* Note that default configuration settings are returned in case the endpoint has not been configured.
*
* @param endpoint endpoint to query
* @return general connection settings of the given endpoint
*/
public @Nullable EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint);
}

View File

@ -0,0 +1,32 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for read callbacks
*
* @author Sami Salonen - Initial contribution
*/
@FunctionalInterface
@NonNullByDefault
public interface ModbusReadCallback extends ModbusResultCallback {
/**
* Callback handling response data
*
* @param result result of the read operation
*/
void handle(AsyncModbusReadResult result);
}

View File

@ -0,0 +1,25 @@
/**
* 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.core.io.transport.modbus;
/**
* Modbus read function codes supported by this transport
*
* @author Sami Salonen - Initial contribution
*/
public enum ModbusReadFunctionCode {
READ_COILS,
READ_INPUT_DISCRETES,
READ_MULTIPLE_REGISTERS,
READ_INPUT_REGISTERS
}

View File

@ -0,0 +1,132 @@
/**
* 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.core.io.transport.modbus;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import net.wimpi.modbus.Modbus;
/**
* Implementation of immutable representation of modbus read request
*
* Equals and hashCode implemented keeping {@link PollTask} in mind: two instances of this class are considered the same
* if they have
* the equal parameters (same slave id, start, length, function code and maxTries).
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusReadRequestBlueprint {
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private final int slaveId;
private final ModbusReadFunctionCode functionCode;
private final int start;
private final int length;
private final int maxTries;
public ModbusReadRequestBlueprint(int slaveId, ModbusReadFunctionCode functionCode, int start, int length,
int maxTries) {
this.slaveId = slaveId;
this.functionCode = functionCode;
this.start = start;
this.length = length;
this.maxTries = maxTries;
}
/**
* Returns the unit identifier of this
* <tt>ModbusMessage</tt> as <tt>int</tt>.<br>
* The identifier is a 1-byte non negative
* integer value valid in the range of 0-255.
* <p>
*
* @return the unit identifier as <tt>int</tt>.
*/
public int getUnitID() {
return slaveId;
}
public int getReference() {
return start;
}
public ModbusReadFunctionCode getFunctionCode() {
return functionCode;
}
public int getDataLength() {
return length;
}
/**
* Maximum number of tries to execute the request, when request fails
*
* For example, number 1 means on try only with no re-tries.
*
* @return number of maximum tries
*/
public int getMaxTries() {
return maxTries;
}
/**
* Returns the protocol identifier of this
* <tt>ModbusMessage</tt> as <tt>int</tt>.<br>
* The identifier is a 2-byte (short) non negative
* integer value valid in the range of 0-65535.
* <p>
*
* @return the protocol identifier as <tt>int</tt>.
*/
public int getProtocolID() {
return Modbus.DEFAULT_PROTOCOL_ID;
}
@Override
public int hashCode() {
return new HashCodeBuilder(81, 3).append(slaveId).append(functionCode).append(start).append(length)
.append(maxTries).toHashCode();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("functionCode", functionCode)
.append("start", start).append("length", length).append("maxTries", maxTries).toString();
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
ModbusReadRequestBlueprint rhs = (ModbusReadRequestBlueprint) obj;
return new EqualsBuilder().append(slaveId, rhs.slaveId).append(functionCode, rhs.functionCode)
.append(start, rhs.start).append(length, rhs.length).isEquals();
}
}

View File

@ -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.core.io.transport.modbus;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.util.HexUtils;
/**
* Immutable {@link ModbusRegisterArray} implementation
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusRegisterArray {
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 <code>ModbusRegisterArray</code> array from register values
*
* @param registerValues register values, each <code>int</code> corresponding to one register
* @return
*/
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;
}
}
/**
* Get register index i as unsigned integer
*
* @param i register index
* @return register value interpreted as unsigned integer (big-endian byte ordering)
*/
public int getRegister(int i) {
int hi = bytes[i * 2] & 0xff;
int lo = bytes[i * 2 + 1] & 0xff;
return ((hi << 8) | lo) & 0xffff;
}
/**
* Return bytes representing the registers
*
*
* 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
* ...
*
* @return set of bytes
*/
public byte[] getBytes() {
return bytes;
}
/**
* Get number of registers stored in this instance
*
* @return
*/
public int size() {
return bytes.length / 2;
}
@Override
public String toString() {
if (bytes.length == 0) {
return "ModbusRegisterArray(<empty>)";
}
return new StringBuilder(bytes.length).append("ModbusRegisterArray(").append(toHexString()).append(')')
.toString();
}
/**
* Get register data as a hex string
*
* For example, 04 45 00 00
*
* @return string representing the bytes of the register array
*/
public String toHexString() {
if (size() == 0) {
return "";
}
return HexUtils.bytesToHex(getBytes());
}
}

View File

@ -0,0 +1,36 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Minimal representation of a modbus response.
*
* Only function code is exposed, which allows detecting MODBUS exception codes from normal codes.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusResponse {
/**
* Function code of the response.
*
* Note that in case of Slave responding with Modbus exception response, the response
* function code might differ from request function code
*
* @return function code of the response
*/
public int getFunctionCode();
}

View File

@ -0,0 +1,24 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Base interface for callbacks used in Modbus
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusResultCallback {
}

View File

@ -0,0 +1,32 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for write callbacks
*
* @author Sami Salonen - Initial contribution
*/
@FunctionalInterface
@NonNullByDefault
public interface ModbusWriteCallback extends ModbusResultCallback {
/**
* Callback handling response data
*
* @param asyncModbusWriteResult result of the write operation
*/
void handle(AsyncModbusWriteResult result);
}

View File

@ -0,0 +1,121 @@
/**
* 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.core.io.transport.modbus;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Implementation for writing coils
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusWriteCoilRequestBlueprint extends ModbusWriteRequestBlueprint {
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private final int slaveId;
private final int reference;
private final BitArray bits;
private final boolean writeMultiple;
private final int maxTries;
/**
* Construct coil write request with single bit of data
*
* @param slaveId slave id to write to
* @param reference reference address
* @param data bit to write
* @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over
* {@link ModbusWriteFunctionCode.WRITE_COIL}
* @param maxTries maximum number of tries in case of errors, should be at least 1
*/
public ModbusWriteCoilRequestBlueprint(int slaveId, int reference, boolean data, boolean writeMultiple,
int maxTries) {
this(slaveId, reference, new BitArray(data), writeMultiple, maxTries);
}
/**
* Construct coil write request with many bits of data
*
* @param slaveId slave id to write to
* @param reference reference address
* @param data bit(s) to write
* @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over
* {@link ModbusWriteFunctionCode.WRITE_COIL}. Useful with single bit of data.
* @param maxTries maximum number of tries in case of errors, should be at least 1
* @throws IllegalArgumentException in case <code>data</code> is empty, <code>writeMultiple</code> is
* <code>false</code> but there are many bits to write.
*/
public ModbusWriteCoilRequestBlueprint(int slaveId, int reference, BitArray data, boolean writeMultiple,
int maxTries) {
super();
this.slaveId = slaveId;
this.reference = reference;
this.bits = data;
this.writeMultiple = writeMultiple;
this.maxTries = maxTries;
if (!writeMultiple && bits.size() > 1) {
throw new IllegalArgumentException("With multiple coils, writeMultiple must be true");
}
if (bits.size() == 0) {
throw new IllegalArgumentException("Must have at least one bit");
}
if (maxTries <= 0) {
throw new IllegalArgumentException("maxTries should be positive, was " + maxTries);
}
}
@Override
public int getUnitID() {
return slaveId;
}
@Override
public int getReference() {
return reference;
}
@Override
public ModbusWriteFunctionCode getFunctionCode() {
return writeMultiple ? ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS : ModbusWriteFunctionCode.WRITE_COIL;
}
public BitArray getCoils() {
return bits;
}
@Override
public int getMaxTries() {
return maxTries;
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("reference", reference)
.append("functionCode", getFunctionCode()).append("bits", bits).append("maxTries", maxTries).toString();
}
@Override
public void accept(ModbusWriteRequestBlueprintVisitor visitor) {
visitor.visit(this);
}
}

View File

@ -0,0 +1,59 @@
/**
* 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.core.io.transport.modbus;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNull;
import net.wimpi.modbus.Modbus;
/**
* Modbus write function codes supported by this transport
*
* @author Sami Salonen - Initial contribution
*/
public enum ModbusWriteFunctionCode {
WRITE_COIL(Modbus.WRITE_COIL),
WRITE_MULTIPLE_COILS(Modbus.WRITE_MULTIPLE_COILS),
WRITE_SINGLE_REGISTER(Modbus.WRITE_SINGLE_REGISTER),
WRITE_MULTIPLE_REGISTERS(Modbus.WRITE_MULTIPLE_REGISTERS);
private final int functionCode;
ModbusWriteFunctionCode(int code) {
functionCode = code;
}
/**
* Get numeric function code represented by this instance
*
* @return
*/
public int getFunctionCode() {
return functionCode;
}
/**
* Construct {@link ModbusWriteFunctionCode} from the numeric function code
*
* @param functionCode numeric function code
* @return {@link ModbusWriteFunctionCode} matching the numeric function code
* @throws IllegalArgumentException with unsupported functions
*/
@SuppressWarnings("null")
public static @NonNull ModbusWriteFunctionCode fromFunctionCode(int functionCode) throws IllegalArgumentException {
return Stream.of(ModbusWriteFunctionCode.values()).filter(v -> v.getFunctionCode() == functionCode).findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid functionCode"));
}
}

View File

@ -0,0 +1,108 @@
/**
* 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.core.io.transport.modbus;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Implementation for writing registers
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusWriteRegisterRequestBlueprint extends ModbusWriteRequestBlueprint {
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private final int slaveId;
private final int reference;
private final ModbusRegisterArray registers;
private final boolean writeMultiple;
private final int maxTries;
/**
* Construct coil write request with many bits of data
*
* @param slaveId slave id to write to
* @param reference reference address
* @param registers register(s) to write
* @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over
* {@link ModbusWriteFunctionCode.WRITE_COIL}. Useful with single register of data.
* @param maxTries maximum number of tries in case of errors, should be at least 1
* @throws IllegalArgumentException in case <code>data</code> is empty, <code>writeMultiple</code> is
* <code>false</code> but there are many registers to write.
*/
public ModbusWriteRegisterRequestBlueprint(int slaveId, int reference, ModbusRegisterArray registers,
boolean writeMultiple, int maxTries) throws IllegalArgumentException {
super();
this.slaveId = slaveId;
this.reference = reference;
this.registers = registers;
this.writeMultiple = writeMultiple;
this.maxTries = maxTries;
if (!writeMultiple && registers.size() > 1) {
throw new IllegalArgumentException("With multiple registers, writeMultiple must be true");
}
if (registers.size() == 0) {
throw new IllegalArgumentException("Must have at least one register");
}
if (maxTries <= 0) {
throw new IllegalArgumentException("maxTries should be positive");
}
}
@Override
public int getReference() {
return reference;
}
@Override
public int getUnitID() {
return slaveId;
}
@Override
public ModbusWriteFunctionCode getFunctionCode() {
return writeMultiple ? ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS
: ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER;
}
public ModbusRegisterArray getRegisters() {
return registers;
}
@Override
public int getMaxTries() {
return maxTries;
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("reference", reference)
.append("functionCode", getFunctionCode()).append("registers", registers).append("maxTries", maxTries)
.toString();
}
@Override
public void accept(ModbusWriteRequestBlueprintVisitor visitor) {
visitor.visit(this);
}
}

View File

@ -0,0 +1,89 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
import net.wimpi.modbus.Modbus;
/**
* Base interface for Modbus write requests
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public abstract class ModbusWriteRequestBlueprint {
/**
* Returns the protocol identifier of this
* <tt>ModbusMessage</tt> as <tt>int</tt>.<br>
* The identifier is a 2-byte (short) non negative
* integer value valid in the range of 0-65535.
* <p>
*
* @return the protocol identifier as <tt>int</tt>.
*/
public int getProtocolID() {
return Modbus.DEFAULT_PROTOCOL_ID;
}
/**
* Returns the reference of the register/coil/discrete input to to start
* writing with this request
* <p>
*
* @return the reference of the register
* to start reading from as <tt>int</tt>.
*/
public abstract int getReference();
/**
* Returns the unit identifier of this
* <tt>ModbusMessage</tt> as <tt>int</tt>.<br>
* The identifier is a 1-byte non negative
* integer value valid in the range of 0-255.
* <p>
*
* @return the unit identifier as <tt>int</tt>.
*/
public abstract int getUnitID();
/**
* Returns the function code of this
* <tt>ModbusMessage</tt> as <tt>int</tt>.<br>
* The function code is a 1-byte non negative
* integer value valid in the range of 0-127.<br>
* Function codes are ordered in conformance
* classes their values are specified in
* <tt>net.wimpi.modbus.Modbus</tt>.
* <p>
*
* @return the function code as <tt>int</tt>.
*
* @see net.wimpi.modbus.Modbus
*/
public abstract ModbusWriteFunctionCode getFunctionCode();
/**
* Get maximum number of tries, in case errors occur. Should be at least 1.
*/
public abstract int getMaxTries();
/**
* Accept visitor
*
* @param visitor
*/
public abstract void accept(ModbusWriteRequestBlueprintVisitor visitor);
}

View File

@ -0,0 +1,40 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* <p>
* ModbusWriteRequestBlueprintVisitor interface.
* </p>
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusWriteRequestBlueprintVisitor {
/**
* Visit request writing coil data
*
* @param blueprint
*/
public void visit(ModbusWriteCoilRequestBlueprint blueprint);
/**
* Visit request writing register data
*
* @param blueprint
*/
public void visit(ModbusWriteRegisterRequestBlueprint blueprint);
}

View File

@ -0,0 +1,34 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Poll task represents Modbus read request
*
* Must be hashable. HashCode and equals should be defined such that no two poll tasks are registered that are
* equal.
*
* @author Sami Salonen - Initial contribution
*
* @see ModbusManager.registerRegularPoll
*/
@NonNullByDefault
public interface PollTask extends
TaskWithEndpoint<ModbusReadRequestBlueprint, ModbusReadCallback, ModbusFailureCallback<ModbusReadRequestBlueprint>> {
@Override
default int getMaxTries() {
return getRequest().getMaxTries();
}
}

View File

@ -0,0 +1,57 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* Common base interface for read and write tasks.
*
* @author Sami Salonen - Initial contribution
*
* @param <R> request type
* @param <C> callback type
*/
@NonNullByDefault
public interface TaskWithEndpoint<R, C extends ModbusResultCallback, F extends ModbusFailureCallback<R>> {
/**
* Gets endpoint associated with this task
*
* @return
*/
ModbusSlaveEndpoint getEndpoint();
/**
* Gets request associated with this task
*
* @return
*/
R getRequest();
/**
* Gets the result callback associated with this task, will be called with response
*
* @return
*/
C getResultCallback();
/**
* Gets the failure callback associated with this task, will be called in case of an error
*
* @return
*/
F getFailureCallback();
int getMaxTries();
}

View File

@ -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.core.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));
}
}

View File

@ -0,0 +1,32 @@
/**
* 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.core.io.transport.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Poll task represents Modbus write request
*
* Unlike {@link PollTask}, this does not have to be hashable.
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public interface WriteTask extends
TaskWithEndpoint<ModbusWriteRequestBlueprint, ModbusWriteCallback, ModbusFailureCallback<ModbusWriteRequestBlueprint>> {
@Override
default int getMaxTries() {
return getRequest().getMaxTries();
}
}

View File

@ -0,0 +1,142 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Class representing pooling related configuration of a single endpoint
*
* This class implements equals hashcode constract, and thus is suitable for use as keys in HashMaps, for example.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class EndpointPoolConfiguration {
/**
* How long should be the minimum duration between previous transaction end and the next transaction with the same
* endpoint.
*
* In milliseconds.
*/
private long interTransactionDelayMillis;
/**
* How long should be the minimum duration between connection-establishments from the pool (with same endpoint). In
* milliseconds.
*/
private long interConnectDelayMillis;
/**
* How many times we want to try connecting to the endpoint before giving up. One means that connection
* establishment is tried once.
*/
private int connectMaxTries = 1;
/**
* Re-connect connection every X milliseconds. Negative means that connection is not disconnected automatically.
* One can use 0ms to denote reconnection after every transaction (default).
*/
private int reconnectAfterMillis;
/**
* How long before we give up establishing the connection. In milliseconds. Default of 0 means that system/OS
* default is respected.
*/
private int connectTimeoutMillis;
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
public long getInterConnectDelayMillis() {
return interConnectDelayMillis;
}
public void setInterConnectDelayMillis(long interConnectDelayMillis) {
this.interConnectDelayMillis = interConnectDelayMillis;
}
public int getConnectMaxTries() {
return connectMaxTries;
}
public void setConnectMaxTries(int connectMaxTries) {
this.connectMaxTries = connectMaxTries;
}
public int getReconnectAfterMillis() {
return reconnectAfterMillis;
}
public void setReconnectAfterMillis(int reconnectAfterMillis) {
this.reconnectAfterMillis = reconnectAfterMillis;
}
public long getInterTransactionDelayMillis() {
return interTransactionDelayMillis;
}
public void setInterTransactionDelayMillis(long interTransactionDelayMillis) {
this.interTransactionDelayMillis = interTransactionDelayMillis;
}
public int getConnectTimeoutMillis() {
return connectTimeoutMillis;
}
public void setConnectTimeoutMillis(int connectTimeoutMillis) {
this.connectTimeoutMillis = connectTimeoutMillis;
}
@Override
public int hashCode() {
return new HashCodeBuilder(2149, 3117).append(interTransactionDelayMillis).append(interConnectDelayMillis)
.append(connectMaxTries).append(reconnectAfterMillis).append(connectTimeoutMillis).toHashCode();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle)
.append("interTransactionDelayMillis", interTransactionDelayMillis)
.append("interConnectDelayMillis", interConnectDelayMillis).append("connectMaxTries", connectMaxTries)
.append("reconnectAfterMillis", reconnectAfterMillis)
.append("connectTimeoutMillis", connectTimeoutMillis).toString();
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
EndpointPoolConfiguration rhs = (EndpointPoolConfiguration) obj;
return new EqualsBuilder().append(interTransactionDelayMillis, rhs.interTransactionDelayMillis)
.append(interConnectDelayMillis, rhs.interConnectDelayMillis)
.append(connectMaxTries, rhs.connectMaxTries).append(reconnectAfterMillis, rhs.reconnectAfterMillis)
.append(connectTimeoutMillis, rhs.connectTimeoutMillis).isEquals();
}
}

View File

@ -0,0 +1,83 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Common base class for ip based endpoints. Endpoint differentiates different modbus slaves only by the ip address
* (string) and port name.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public abstract class ModbusIPSlaveEndpoint implements ModbusSlaveEndpoint {
private String address;
private int port;
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
public ModbusIPSlaveEndpoint(String address, int port) {
this.address = address;
this.port = port;
}
public String getAddress() {
return address;
}
public int getPort() {
return port;
}
@Override
public int hashCode() {
// differentiate different protocols using the class name, and after that use address and port
int protocolHash = this.getClass().getName().hashCode();
if (protocolHash % 2 == 0) {
protocolHash += 1;
}
return new HashCodeBuilder(11, protocolHash).append(address).append(port).toHashCode();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("address", address).append("port", port).toString();
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
// different protocol -> not equal!
return false;
}
ModbusIPSlaveEndpoint rhs = (ModbusIPSlaveEndpoint) obj;
return new EqualsBuilder().append(address, rhs.address).append(port, rhs.port).isEquals();
}
}

View File

@ -0,0 +1,107 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import net.wimpi.modbus.util.SerialParameters;
/**
* Serial endpoint. Endpoint differentiates different modbus slaves only by the serial port.
* port.
*
* Endpoint contains SerialParameters which should be enough to establish the connection.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusSerialSlaveEndpoint implements ModbusSlaveEndpoint {
private SerialParameters serialParameters;
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
public ModbusSerialSlaveEndpoint(String portName, int baudRate, int flowControlIn, int flowControlOut, int databits,
int stopbits, int parity, String encoding, boolean echo, int receiveTimeoutMillis) {
this(new SerialParameters(portName, baudRate, flowControlIn, flowControlOut, databits, stopbits, parity,
encoding, echo, receiveTimeoutMillis));
}
public ModbusSerialSlaveEndpoint(String portName, int baudRate, String flowControlIn, String flowControlOut,
int databits, String stopbits, String parity, String encoding, boolean echo, int receiveTimeoutMillis) {
SerialParameters parameters = new SerialParameters();
parameters.setPortName(portName);
parameters.setBaudRate(baudRate);
parameters.setFlowControlIn(flowControlIn);
parameters.setFlowControlOut(flowControlOut);
parameters.setDatabits(databits);
parameters.setStopbits(stopbits);
parameters.setParity(parity);
parameters.setEncoding(encoding);
parameters.setEcho(echo);
parameters.setReceiveTimeoutMillis(receiveTimeoutMillis);
this.serialParameters = parameters;
}
private ModbusSerialSlaveEndpoint(SerialParameters serialParameters) {
this.serialParameters = serialParameters;
}
public SerialParameters getSerialParameters() {
return serialParameters;
}
@Override
public <R> R accept(ModbusSlaveEndpointVisitor<R> factory) {
return factory.visit(this);
}
public String getPortName() {
return serialParameters.getPortName();
}
@Override
public int hashCode() {
// hashcode & equal is determined purely by port name
return serialParameters.getPortName().hashCode();
}
@Override
public boolean equals(@Nullable Object obj) {
// equals is determined purely by port name
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
ModbusSerialSlaveEndpoint rhs = (ModbusSerialSlaveEndpoint) obj;
return new EqualsBuilder().append(serialParameters.getPortName(), rhs.serialParameters.getPortName())
.isEquals();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("portName", serialParameters.getPortName()).toString();
}
}

View File

@ -0,0 +1,31 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* ModbusSlaveEndpoint contains minimal connection information to establish connection to the slave. End point equals
* and hashCode methods should be implemented such that
* they can be used to differentiate different physical slaves. Read and write transactions are processed
* one at a time if they are associated with the same endpoint (in the sense of equals and hashCode).
*
* Note that, endpoint class might not include all configuration that might be necessary to actually
* communicate with the slave, just the data that is required to establish the connection.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusSlaveEndpoint {
public <R> R accept(ModbusSlaveEndpointVisitor<R> visitor);
}

View File

@ -0,0 +1,35 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Visitor for ModbusSlaveEndpoint
*
* @param <R> return type from visit
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusSlaveEndpointVisitor<R> {
@Nullable
R visit(ModbusTCPSlaveEndpoint endpoint);
@Nullable
R visit(ModbusSerialSlaveEndpoint endpoint);
@Nullable
R visit(ModbusUDPSlaveEndpoint endpoint);
}

View File

@ -0,0 +1,34 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Endpoint for TCP slaves
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusTCPSlaveEndpoint extends ModbusIPSlaveEndpoint {
public ModbusTCPSlaveEndpoint(String address, int port) {
super(address, port);
}
@Override
public <R> R accept(ModbusSlaveEndpointVisitor<R> factory) {
return factory.visit(this);
}
}

View File

@ -0,0 +1,34 @@
/**
* 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.core.io.transport.modbus.endpoint;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Endpoint for UDP slaves
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusUDPSlaveEndpoint extends ModbusIPSlaveEndpoint {
public ModbusUDPSlaveEndpoint(String address, int port) {
super(address, port);
}
@Override
public <R> R accept(ModbusSlaveEndpointVisitor<R> factory) {
return factory.visit(this);
}
}

View File

@ -0,0 +1,57 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* Exception for connection issues
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusConnectionException extends ModbusTransportException {
private static final long serialVersionUID = -6171226761518661925L;
private ModbusSlaveEndpoint endpoint;
/**
*
* @param endpoint endpoint associated with this exception
*/
public ModbusConnectionException(ModbusSlaveEndpoint endpoint) {
this.endpoint = endpoint;
}
/**
* Get endpoint associated with this connection error
*
* @return endpoint with the error
*/
public ModbusSlaveEndpoint getEndpoint() {
return endpoint;
}
@Override
public @Nullable String getMessage() {
return String.format("Error connecting to endpoint %s", endpoint);
}
@Override
public String toString() {
return String.format("ModbusConnectionException(Error connecting to endpoint=%s)", endpoint);
}
}

View File

@ -0,0 +1,106 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for explicit exception responses from Modbus slave
*
* @author Sami Salonen - Initial contribution
* @author Nagy Attila Gabor - added getter for error type
*
*/
@NonNullByDefault
public abstract class ModbusSlaveErrorResponseException extends ModbusTransportException {
/**
* The function code received in the query is not an allowable action for the slave. This may be because the
* function code is only applicable to newer devices, and was not implemented in the unit selected. It could also
* indicate that the slave is in the wrong state to process a request of this type, for example because it is
* unconfigured and is being asked to return register values. If a Poll Program Complete command was issued, this
* code indicates that no program function preceded it.
*/
public static final int ILLEGAL_FUNCTION = 1;
/**
* The data address received in the query is not an allowable address for the slave. More specifically, the
* combination of reference number and transfer length is invalid. For a controller with 100 registers, a request
* with offset 96 and length 4 would succeed, a request with offset 96 and length 5 will generate exception 02.
*/
public static final int ILLEGAL_DATA_ACCESS = 2;
/**
* A value contained in the query data field is not an allowable value for the slave. This indicates a fault in the
* structure of remainder of a complex request, such as that the implied length is incorrect. It specifically does
* NOT mean that a data item submitted for storage in a register has a value outside the expectation of the
* application program, since the Modbus protocol is unaware of the significance of any particular value of any
* particular register.
*/
public static final int ILLEGAL_DATA_VALUE = 3;
/**
* An unrecoverable error occurred while the slave was attempting to perform the requested action.
*/
public static final int SLAVE_DEVICE_FAILURE = 4;
/**
* Specialized use in conjunction with programming commands.
* The slave has accepted the request and is processing it, but a long duration of time will be required to do so.
* This response is returned to prevent a timeout error from occurring in the master. The master can next issue a
* Poll Program Complete message to determine if processing is completed.
*/
public static final int ACKNOWLEDGE = 5;
/**
* Specialized use in conjunction with programming commands.
* The slave is engaged in processing a long-duration program command. The master should retransmit the message
* later when the slave is free.
*/
public static final int SLAVE_DEVICE_BUSY = 6;
/**
* The slave cannot perform the program function received in the query. This code is returned for an unsuccessful
* programming request using function code 13 or 14 decimal. The master should request diagnostic or error
* information from the slave.
*/
public static final int NEGATIVE_ACKNOWLEDGE = 7;
/**
* Specialized use in conjunction with function codes 20 and 21 and reference type 6, to indicate that the extended
* file area failed to pass a consistency check.
* The slave attempted to read extended memory or record file, but detected a parity error in memory. The master can
* retry the request, but service may be required on the slave device.
*/
public static final int MEMORY_PARITY_ERROR = 8;
/**
* Specialized use in conjunction with gateways, indicates that the gateway was unable to allocate an internal
* communication path from the input port to the output port for processing the request. Usually means the gateway
* is misconfigured or overloaded.
*/
public static final int GATEWAY_PATH_UNVAVAILABLE = 10;
/**
* Specialized use in conjunction with gateways, indicates that no response was obtained from the target device.
* Usually means that the device is not present on the network.
*/
public static final int GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 11;
private static final long serialVersionUID = -1435199498550990487L;
/**
* @return the Modbus exception code that happened
*/
public abstract int getExceptionCode();
}

View File

@ -0,0 +1,27 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for all IO errors
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusSlaveIOException extends ModbusTransportException {
private static final long serialVersionUID = -8568199166837844463L;
}

View File

@ -0,0 +1,27 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Base exception for all exceptions in Modbus transport bundle
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusTransportException extends Exception {
private static final long serialVersionUID = 1684767401685843339L;
}

View File

@ -0,0 +1,48 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Exception representing situation where function code of the response does not match request
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusUnexpectedResponseFunctionCodeException extends ModbusTransportException {
private static final long serialVersionUID = 1109165449703638949L;
private int requestFunctionCode;
private int responseFunctionCode;
public ModbusUnexpectedResponseFunctionCodeException(int requestFunctionCode, int responseFunctionCode) {
this.requestFunctionCode = requestFunctionCode;
this.responseFunctionCode = responseFunctionCode;
}
@Override
public @Nullable String getMessage() {
return String.format("Function code of request (%d) does not equal response (%d)", requestFunctionCode,
responseFunctionCode);
}
@Override
public String toString() {
return String.format(
"ModbusUnexpectedResponseFunctionCodeException(requestFunctionCode=%d, responseFunctionCode=%d)",
requestFunctionCode, responseFunctionCode);
}
}

View File

@ -0,0 +1,47 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Exception representing situation where data length of the response does not match request
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusUnexpectedResponseSizeException extends ModbusTransportException {
private static final long serialVersionUID = 2460907938819984483L;
private int requestSize;
private int responseSize;
public ModbusUnexpectedResponseSizeException(int requestSize, int responseSize) {
this.requestSize = requestSize;
this.responseSize = responseSize;
}
@Override
public @Nullable String getMessage() {
return String.format("Data length of the request (%d) does not equal response (%d). Slave response is invalid.",
requestSize, responseSize);
}
@Override
public String toString() {
return String.format("ModbusUnexpectedResponseSizeException(requestFunctionCode=%d, responseFunctionCode=%d)",
requestSize, responseSize);
}
}

View File

@ -0,0 +1,56 @@
/**
* 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.core.io.transport.modbus.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Exception representing situation where transaction id of the response does not match request
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusUnexpectedTransactionIdException extends ModbusTransportException {
private static final long serialVersionUID = -2453232634024813933L;
private int requestId;
private int responseId;
public ModbusUnexpectedTransactionIdException(int requestId, int responseId) {
this.requestId = requestId;
this.responseId = responseId;
}
@Override
public @Nullable String getMessage() {
return String.format("Transaction id of request (%d) does not equal response (%d). Slave response is invalid.",
requestId, responseId);
}
@Override
public String toString() {
return String.format(
"ModbusUnexpectedTransactionIdException(requestTransactionId=%d, responseTransactionId=%d)", requestId,
responseId);
}
public int getRequestId() {
return requestId;
}
public int getResponseId() {
return responseId;
}
}

View File

@ -0,0 +1,72 @@
/**
* 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.core.io.transport.modbus.internal;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility for timing operations
*
* @author Sami Salonen - initial contribution
*
*/
@NonNullByDefault
public class AggregateStopWatch {
/**
* ID associated with this modbus operation
*/
final String operationId;
/**
* Total operation time
*/
final SimpleStopWatch total = new SimpleStopWatch();
/**
* Time for connection related actions
*/
final SimpleStopWatch connection = new SimpleStopWatch();
/**
* Time for actual the actual transaction (read/write to slave)
*/
final SimpleStopWatch transaction = new SimpleStopWatch();
/**
* Time for calling calling the callback
*/
final SimpleStopWatch callback = new SimpleStopWatch();
public AggregateStopWatch() {
this.operationId = UUID.randomUUID().toString();
}
/**
* Suspend all running stopwatches of this aggregate
*/
public void suspendAllRunning() {
for (SimpleStopWatch watch : new SimpleStopWatch[] { total, connection, transaction, callback }) {
if (watch.isRunning()) {
watch.suspend();
}
}
}
@Override
public String toString() {
return String.format("{total: %d ms, connection: %d, transaction=%d, callback=%d}", total.getTotalTimeMillis(),
connection.getTotalTimeMillis(), transaction.getTotalTimeMillis(), callback.getTotalTimeMillis());
}
}

View File

@ -0,0 +1,108 @@
/**
* 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.core.io.transport.modbus.internal;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.ModbusFailureCallback;
import org.openhab.core.io.transport.modbus.ModbusReadCallback;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.io.transport.modbus.PollTask;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* Implementation of {@link PollTask} that differentiates tasks using endpoint, request and callbacks.
*
* Note: Two differentiate poll tasks are considered unequal if their callbacks are unequal.
*
* HashCode and equals should be defined such that two poll tasks considered the same only if their request,
* maxTries, endpoint and callback are the same.
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class BasicPollTask implements PollTask {
static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private ModbusSlaveEndpoint endpoint;
private ModbusReadRequestBlueprint request;
private ModbusReadCallback resultCallback;
private ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback;
public BasicPollTask(ModbusSlaveEndpoint endpoint, ModbusReadRequestBlueprint request,
ModbusReadCallback resultCallback, ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback) {
this.endpoint = endpoint;
this.request = request;
this.resultCallback = resultCallback;
this.failureCallback = failureCallback;
}
@Override
public ModbusReadRequestBlueprint getRequest() {
return request;
}
@Override
public ModbusSlaveEndpoint getEndpoint() {
return endpoint;
}
@Override
public ModbusReadCallback getResultCallback() {
return resultCallback;
}
@Override
public ModbusFailureCallback<ModbusReadRequestBlueprint> getFailureCallback() {
return failureCallback;
}
@Override
public int hashCode() {
return new HashCodeBuilder(69, 5).append(request).append(getEndpoint()).append(getResultCallback())
.append(getFailureCallback()).toHashCode();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("request", request).append("endpoint", endpoint)
.append("resultCallback", getResultCallback()).append("failureCallback", getFailureCallback())
.toString();
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
BasicPollTask rhs = (BasicPollTask) obj;
return new EqualsBuilder().append(request, rhs.request).append(endpoint, rhs.endpoint)
.append(getResultCallback(), rhs.getResultCallback())
.append(getFailureCallback(), rhs.getFailureCallback()).isEquals();
}
}

View File

@ -0,0 +1,77 @@
/**
* 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.core.io.transport.modbus.internal;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.ModbusFailureCallback;
import org.openhab.core.io.transport.modbus.ModbusWriteCallback;
import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
import org.openhab.core.io.transport.modbus.WriteTask;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* Simple implementation for Modbus write requests
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class BasicWriteTask implements WriteTask {
private static final StandardToStringStyle TO_STRING_STYLE = new StandardToStringStyle();
static {
TO_STRING_STYLE.setUseShortClassName(true);
}
private ModbusSlaveEndpoint endpoint;
private ModbusWriteRequestBlueprint request;
private ModbusWriteCallback resultCallback;
private ModbusFailureCallback<ModbusWriteRequestBlueprint> failureCallback;
public BasicWriteTask(ModbusSlaveEndpoint endpoint, ModbusWriteRequestBlueprint request,
ModbusWriteCallback resultCallback, ModbusFailureCallback<ModbusWriteRequestBlueprint> failureCallback) {
super();
this.endpoint = endpoint;
this.request = request;
this.resultCallback = resultCallback;
this.failureCallback = failureCallback;
}
@Override
public ModbusSlaveEndpoint getEndpoint() {
return endpoint;
}
@Override
public ModbusWriteRequestBlueprint getRequest() {
return request;
}
@Override
public ModbusWriteCallback getResultCallback() {
return resultCallback;
}
@Override
public ModbusFailureCallback<ModbusWriteRequestBlueprint> getFailureCallback() {
return failureCallback;
}
@Override
public String toString() {
return new ToStringBuilder(this, TO_STRING_STYLE).append("request", request).append("endpoint", endpoint)
.append("resultCallback", resultCallback).append("failureCallback", failureCallback).toString();
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.core.io.transport.modbus.internal;
import org.apache.commons.pool2.KeyedPooledObjectFactory;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
import net.wimpi.modbus.net.ModbusSlaveConnection;
/**
* Pool for modbus connections.
*
* Only one connection is allowed to be active at a time.
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusConnectionPool extends GenericKeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> {
public ModbusConnectionPool(KeyedPooledObjectFactory<ModbusSlaveEndpoint, ModbusSlaveConnection> factory) {
super(factory, new ModbusPoolConfig());
}
@Override
public void setConfig(@Nullable GenericKeyedObjectPoolConfig<ModbusSlaveConnection> conf) {
if (conf == null) {
return;
} else if (!(conf instanceof ModbusPoolConfig)) {
throw new IllegalArgumentException("Only ModbusPoolConfig accepted!");
}
super.setConfig(conf);
}
}

View File

@ -0,0 +1,347 @@
/**
* 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.core.io.transport.modbus.internal;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
import org.openhab.core.io.transport.modbus.BitArray;
import org.openhab.core.io.transport.modbus.ModbusReadCallback;
import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprintVisitor;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSerialSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpointVisitor;
import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusUDPSlaveEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.wimpi.modbus.io.ModbusSerialTransaction;
import net.wimpi.modbus.io.ModbusTCPTransaction;
import net.wimpi.modbus.io.ModbusTransaction;
import net.wimpi.modbus.io.ModbusUDPTransaction;
import net.wimpi.modbus.msg.ModbusRequest;
import net.wimpi.modbus.msg.ModbusResponse;
import net.wimpi.modbus.msg.ReadCoilsRequest;
import net.wimpi.modbus.msg.ReadCoilsResponse;
import net.wimpi.modbus.msg.ReadInputDiscretesRequest;
import net.wimpi.modbus.msg.ReadInputDiscretesResponse;
import net.wimpi.modbus.msg.ReadInputRegistersRequest;
import net.wimpi.modbus.msg.ReadInputRegistersResponse;
import net.wimpi.modbus.msg.ReadMultipleRegistersRequest;
import net.wimpi.modbus.msg.ReadMultipleRegistersResponse;
import net.wimpi.modbus.msg.WriteCoilRequest;
import net.wimpi.modbus.msg.WriteMultipleCoilsRequest;
import net.wimpi.modbus.msg.WriteMultipleRegistersRequest;
import net.wimpi.modbus.msg.WriteSingleRegisterRequest;
import net.wimpi.modbus.net.ModbusSlaveConnection;
import net.wimpi.modbus.net.SerialConnection;
import net.wimpi.modbus.net.TCPMasterConnection;
import net.wimpi.modbus.net.UDPMasterConnection;
import net.wimpi.modbus.procimg.InputRegister;
import net.wimpi.modbus.procimg.Register;
import net.wimpi.modbus.procimg.SimpleInputRegister;
import net.wimpi.modbus.util.BitVector;
/**
* Conversion utilities between underlying Modbus library (net.wimpi.modbus) and this transport bundle
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusLibraryWrapper {
private static Logger getLogger() {
return LoggerFactory.getLogger(ModbusLibraryWrapper.class);
}
private static BitArray bitArrayFromBitVector(BitVector bitVector, int count) {
boolean[] bits = new boolean[count];
for (int i = 0; i < count; i++) {
bits[i] = bitVector.getBit(i);
}
return new BitArray(bits);
}
private static ModbusRegisterArray modbusRegisterArrayFromInputRegisters(InputRegister[] inputRegisters) {
int[] registers = new int[inputRegisters.length];
for (int i = 0; i < inputRegisters.length; i++) {
registers[i] = inputRegisters[i].getValue();
}
return new ModbusRegisterArray(registers);
}
/**
* Convert the general request to Modbus library request object
*
* @param message
* @throws IllegalArgumentException
* 1) in case function code implies coil data but we have registers
* 2) in case function code implies register data but we have coils
* 3) in case there is no data
* 4) in case there is too much data in case of WRITE_COIL or WRITE_SINGLE_REGISTER
* @throws IllegalStateException unexpected function code. Implementation is lacking and this can be considered a
* bug
* @return MODBUS library request matching the write request
*/
public static ModbusRequest createRequest(ModbusWriteRequestBlueprint message) {
// ModbusRequest[] request = new ModbusRequest[1];
AtomicReference<ModbusRequest> request = new AtomicReference<>();
AtomicBoolean writeSingle = new AtomicBoolean(false);
switch (message.getFunctionCode()) {
case WRITE_COIL:
writeSingle.set(true);
// fall-through on purpose
case WRITE_MULTIPLE_COILS:
message.accept(new ModbusWriteRequestBlueprintVisitor() {
@Override
public void visit(ModbusWriteRegisterRequestBlueprint blueprint) {
throw new IllegalArgumentException();
}
@Override
public void visit(ModbusWriteCoilRequestBlueprint blueprint) {
BitArray coils = blueprint.getCoils();
if (coils.size() == 0) {
throw new IllegalArgumentException("Must provide at least one coil");
}
if (writeSingle.get()) {
if (coils.size() != 1) {
throw new IllegalArgumentException("Must provide single coil with WRITE_COIL");
}
request.set(new WriteCoilRequest(message.getReference(), coils.getBit(0)));
} else {
request.set(new WriteMultipleCoilsRequest(message.getReference(),
ModbusLibraryWrapper.convertBits(coils)));
}
}
});
break;
case WRITE_SINGLE_REGISTER:
writeSingle.set(true);
// fall-through on purpose
case WRITE_MULTIPLE_REGISTERS:
message.accept(new ModbusWriteRequestBlueprintVisitor() {
@Override
public void visit(ModbusWriteRegisterRequestBlueprint blueprint) {
Register[] registers = ModbusLibraryWrapper.convertRegisters(blueprint.getRegisters());
if (registers.length == 0) {
throw new IllegalArgumentException("Must provide at least one register");
}
if (writeSingle.get()) {
if (blueprint.getRegisters().size() != 1) {
throw new IllegalArgumentException(
"Must provide single register with WRITE_SINGLE_REGISTER");
}
request.set(new WriteSingleRegisterRequest(message.getReference(), registers[0]));
} else {
request.set(new WriteMultipleRegistersRequest(message.getReference(), registers));
}
}
@Override
public void visit(ModbusWriteCoilRequestBlueprint blueprint) {
throw new IllegalArgumentException();
}
});
break;
default:
getLogger().error("Unexpected function code {}", message.getFunctionCode());
throw new IllegalStateException(
String.format("Unexpected function code %s", message.getFunctionCode()));
}
ModbusRequest modbusRequest = request.get();
modbusRequest.setUnitID(message.getUnitID());
modbusRequest.setProtocolID(message.getProtocolID());
return modbusRequest;
}
/**
* Create a fresh transaction for the given endpoint and connection
*
* The retries of the transaction will be disabled.
*
* @param endpoint
* @param connection
* @return
*/
public static ModbusTransaction createTransactionForEndpoint(ModbusSlaveEndpoint endpoint,
ModbusSlaveConnection connection) {
ModbusTransaction transaction = endpoint.accept(new ModbusSlaveEndpointVisitor<ModbusTransaction>() {
@Override
public @NonNull ModbusTransaction visit(ModbusTCPSlaveEndpoint modbusIPSlavePoolingKey) {
ModbusTCPTransaction transaction = new ModbusTCPTransaction();
transaction.setReconnecting(false);
return transaction;
}
@Override
public @NonNull ModbusTransaction visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) {
return new ModbusSerialTransaction();
}
@Override
public @NonNull ModbusTransaction visit(ModbusUDPSlaveEndpoint modbusUDPSlavePoolingKey) {
return new ModbusUDPTransaction();
}
});
// We disable modbus library retries and handle in the Manager implementation
transaction.setRetries(0);
transaction.setRetryDelayMillis(0);
if (transaction instanceof ModbusSerialTransaction) {
((ModbusSerialTransaction) transaction).setSerialConnection((SerialConnection) connection);
} else if (transaction instanceof ModbusUDPTransaction) {
((ModbusUDPTransaction) transaction).setTerminal(((UDPMasterConnection) connection).getTerminal());
} else if (transaction instanceof ModbusTCPTransaction) {
((ModbusTCPTransaction) transaction).setConnection((TCPMasterConnection) connection);
} else {
throw new IllegalStateException();
}
return transaction;
}
/**
* Create fresh request corresponding to {@link ModbusReadRequestBlueprint}
*
* @param message
* @return
*/
public static ModbusRequest createRequest(ModbusReadRequestBlueprint message) {
ModbusRequest request;
if (message.getFunctionCode() == ModbusReadFunctionCode.READ_COILS) {
request = new ReadCoilsRequest(message.getReference(), message.getDataLength());
} else if (message.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_DISCRETES) {
request = new ReadInputDiscretesRequest(message.getReference(), message.getDataLength());
} else if (message.getFunctionCode() == ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
request = new ReadMultipleRegistersRequest(message.getReference(), message.getDataLength());
} else if (message.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_REGISTERS) {
request = new ReadInputRegistersRequest(message.getReference(), message.getDataLength());
} else {
throw new IllegalArgumentException(String.format("Unexpected function code %s", message.getFunctionCode()));
}
request.setUnitID(message.getUnitID());
request.setProtocolID(message.getProtocolID());
return request;
}
/**
* Convert {@link BitArray} to {@link BitVector}
*
* @param bits
* @return
*/
public static BitVector convertBits(BitArray bits) {
BitVector bitVector = new BitVector(bits.size());
IntStream.range(0, bits.size()).forEach(i -> bitVector.setBit(i, bits.getBit(i)));
return bitVector;
}
/**
* Convert {@link ModbusRegisterArray} to array of {@link Register}
*
* @param bits
* @return
*/
public static Register[] convertRegisters(ModbusRegisterArray arr) {
return IntStream.range(0, arr.size()).mapToObj(i -> new SimpleInputRegister(arr.getRegister(i)))
.collect(Collectors.toList()).toArray(new Register[0]);
}
/**
* Get number of bits/registers/discrete inputs in the request.
*
*
* @param response
* @param request
* @return
*/
public static int getNumberOfItemsInResponse(ModbusResponse response, ModbusReadRequestBlueprint request) {
// jamod library seems to be a bit buggy when it comes number of coils/discrete inputs in the response. Some
// of the methods such as ReadCoilsResponse.getBitCount() are returning wrong values.
//
// This is the reason we use a bit more verbose way to get the number of items in the response.
final int responseCount;
if (request.getFunctionCode() == ModbusReadFunctionCode.READ_COILS) {
responseCount = ((ReadCoilsResponse) response).getCoils().size();
} else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_DISCRETES) {
responseCount = ((ReadInputDiscretesResponse) response).getDiscretes().size();
} else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
responseCount = ((ReadMultipleRegistersResponse) response).getRegisters().length;
} else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_REGISTERS) {
responseCount = ((ReadInputRegistersResponse) response).getRegisters().length;
} else {
throw new IllegalArgumentException(String.format("Unexpected function code %s", request.getFunctionCode()));
}
return responseCount;
}
/**
* Invoke callback with the data received
*
* @param message original request
* @param callback callback for read
* @param response Modbus library response object
*/
public static void invokeCallbackWithResponse(ModbusReadRequestBlueprint request, ModbusReadCallback callback,
ModbusResponse response) {
try {
getLogger().trace("Calling read response callback {} for request {}. Response was {}", callback, request,
response);
// The number of coils/discrete inputs received in response are always in the multiples of 8
// bits.
// So even if querying 5 bits, you will actually get 8 bits. Here we wrap the data in
// BitArrayWrappingBitVector
// with will validate that the consumer is not accessing the "invalid" bits of the response.
int dataItemsInResponse = getNumberOfItemsInResponse(response, request);
if (request.getFunctionCode() == ModbusReadFunctionCode.READ_COILS) {
BitVector bits = ((ReadCoilsResponse) response).getCoils();
BitArray payload = bitArrayFromBitVector(bits, Math.min(dataItemsInResponse, request.getDataLength()));
callback.handle(new AsyncModbusReadResult(request, payload));
} else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_DISCRETES) {
BitVector bits = ((ReadInputDiscretesResponse) response).getDiscretes();
BitArray payload = bitArrayFromBitVector(bits, Math.min(dataItemsInResponse, request.getDataLength()));
callback.handle(new AsyncModbusReadResult(request, payload));
} else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
ModbusRegisterArray payload = modbusRegisterArrayFromInputRegisters(
((ReadMultipleRegistersResponse) response).getRegisters());
callback.handle(new AsyncModbusReadResult(request, payload));
} else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_REGISTERS) {
ModbusRegisterArray payload = modbusRegisterArrayFromInputRegisters(
((ReadInputRegistersResponse) response).getRegisters());
callback.handle(new AsyncModbusReadResult(request, payload));
} else {
throw new IllegalArgumentException(
String.format("Unexpected function code %s", request.getFunctionCode()));
}
} finally {
getLogger().trace("Called read response callback {} for request {}. Response was {}", callback, request,
response);
}
}
}

View File

@ -0,0 +1,82 @@
/**
* 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.core.io.transport.modbus.internal;
import org.apache.commons.pool2.impl.DefaultEvictionPolicy;
import org.apache.commons.pool2.impl.EvictionPolicy;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.internal.pooling.ModbusSlaveConnectionEvictionPolicy;
import net.wimpi.modbus.net.ModbusSlaveConnection;
/**
* Configuration for Modbus connection pool
*
* Default is that
* - there is only one connection per endpoint
* - clients are served "fairly" (first-come-first-serve)
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusPoolConfig extends GenericKeyedObjectPoolConfig<ModbusSlaveConnection> {
@SuppressWarnings("unused")
private EvictionPolicy<ModbusSlaveConnection> evictionPolicy = new DefaultEvictionPolicy<>();
public ModbusPoolConfig() {
// When the pool is exhausted, multiple calling threads may be simultaneously blocked waiting for instances
// to
// become available. As of pool 1.5, a "fairness" algorithm has been implemented to ensure that threads
// receive
// available instances in request arrival order.
setFairness(true);
// Limit one connection per endpoint (i.e. same ip:port pair or same serial device).
// If there are multiple read/write requests to process at the same time, block until previous one finishes
setBlockWhenExhausted(true);
setMaxTotalPerKey(1);
// block infinitely when exhausted
setMaxWaitMillis(-1);
// Connections are "tested" on return. Effectively, disconnected connections are destroyed when returning on
// pool
// Note that we do not test on borrow -- that would mean blocking situation when connection cannot be
// established.
// Instead, borrowing connection from pool can return unconnected connection.
setTestOnReturn(true);
// disable JMX
setJmxEnabled(false);
// Evict idle connections every 10 seconds
setEvictionPolicy(new ModbusSlaveConnectionEvictionPolicy());
setTimeBetweenEvictionRunsMillis(10000);
// Let eviction re-create ready-to-use idle (=unconnected) connections
// This is to avoid occasional / rare deadlocks seen with pool 2.8.1 & 2.4.3 when
// borrow hangs (waiting indefinitely for idle object to appear in the pool)
// https://github.com/openhab/openhab-addons/issues/8460
setMinIdlePerKey(1);
}
@Override
public void setEvictionPolicyClassName(@Nullable String evictionPolicyClassName) {
// Protect against https://issues.apache.org/jira/browse/POOL-338
// Disallow re-setting eviction policy with class name. Only setEvictionPolicy allowed
throw new IllegalStateException("setEvictionPolicyClassName disallowed! Will fail in OSGI");
}
}

View File

@ -0,0 +1,44 @@
/**
* 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.core.io.transport.modbus.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.ModbusResponse;
import net.wimpi.modbus.msg.ModbusMessage;
/**
* Basic implementation of {@link ModbusResponse}
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusResponseImpl implements ModbusResponse {
private int responseFunctionCode;
public ModbusResponseImpl(ModbusMessage response) {
super();
this.responseFunctionCode = response.getFunctionCode();
}
@Override
public int getFunctionCode() {
return responseFunctionCode;
}
@Override
public String toString() {
return String.format("ModbusResponseImpl(responseFC=%d)", responseFunctionCode);
}
}

View File

@ -0,0 +1,55 @@
/**
* 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.core.io.transport.modbus.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.exception.ModbusSlaveErrorResponseException;
import net.wimpi.modbus.ModbusSlaveException;
/**
* Exception for explicit exception responses from Modbus slave
*
* @author Sami Salonen - Initial contribution
* @author Nagy Attila Gabor - added getter for error type
*
*/
@NonNullByDefault
public class ModbusSlaveErrorResponseExceptionImpl extends ModbusSlaveErrorResponseException {
private static final long serialVersionUID = 6334580162425192133L;
private int type;
public ModbusSlaveErrorResponseExceptionImpl(ModbusSlaveException e) {
type = e.getType();
}
/**
* @return the Modbus exception code that happened
*/
@Override
public int getExceptionCode() {
return type;
}
@Override
public @Nullable String getMessage() {
return String.format("Slave responsed with error=%d", type);
}
@Override
public String toString() {
return String.format("ModbusSlaveErrorResponseException(error=%d)", type);
}
}

View File

@ -0,0 +1,58 @@
/**
* 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.core.io.transport.modbus.internal;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.exception.ModbusSlaveIOException;
import net.wimpi.modbus.ModbusIOException;
/**
* Exception for all IO errors
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusSlaveIOExceptionImpl extends ModbusSlaveIOException {
private static final long serialVersionUID = -8910463902857643468L;
private Exception error;
public ModbusSlaveIOExceptionImpl(ModbusIOException e) {
this.error = e;
}
public ModbusSlaveIOExceptionImpl(IOException e) {
this.error = e;
}
@Override
public @Nullable String getMessage() {
return String.format("Modbus IO Error with cause=%s, EOF=%s, message='%s', cause2=%s",
error.getClass().getSimpleName(),
error instanceof ModbusIOException ? ((ModbusIOException) error).isEOF() : "?", error.getMessage(),
error.getCause());
}
@Override
public String toString() {
return String.format("ModbusSlaveIOException(cause=%s, EOF=%s, message='%s', cause2=%s)",
error.getClass().getSimpleName(),
error instanceof ModbusIOException ? ((ModbusIOException) error).isEOF() : "?", error.getMessage(),
error.getCause());
}
}

View File

@ -0,0 +1,174 @@
/**
* 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.core.io.transport.modbus.internal;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.internal.ModbusManagerImpl.PollTaskUnregistered;
import net.wimpi.modbus.ModbusException;
/**
* Implementation of simple stop watch.
*
* @author Sami Salonen - initial contribution
*
*/
@NonNullByDefault
public class SimpleStopWatch {
private volatile long totalMillis;
private volatile long resumed;
@FunctionalInterface
public abstract interface SupplierWithPollTaskUnregisteredException<T> {
public abstract T get() throws ModbusManagerImpl.PollTaskUnregistered;
}
@FunctionalInterface
public abstract interface RunnableWithModbusException {
public abstract void run() throws ModbusException;
}
/**
* Resume or start the stop watch
*
* @throws IllegalStateException if stop watch is running already
*/
public synchronized void resume() {
if (isRunning()) {
throw new IllegalStateException("Cannot suspend a running StopWatch");
}
resumed = System.currentTimeMillis();
}
/**
* Suspend the stop watch
*
* @throws IllegalStateException if stop watch has not been resumed
*/
public synchronized void suspend() {
if (!isRunning()) {
throw new IllegalStateException("Cannot suspend non-running StopWatch");
}
totalMillis += System.currentTimeMillis() - resumed;
resumed = 0;
}
/**
* Get total running time of this StopWatch in milliseconds
*
* @return total running time in milliseconds
*/
public synchronized long getTotalTimeMillis() {
return totalMillis;
}
/**
* Tells whether this StopWatch is now running
*
* @return boolean telling whether this StopWatch is running
*/
public synchronized boolean isRunning() {
return resumed > 0;
}
/**
* Time single action using this StopWatch
*
* First StopWatch is resumed, then action is applied. Finally the StopWatch is suspended.
*
* @param supplier action to time
* @return return value from supplier
* @throws PollTaskUnregistered when original supplier throws the exception
*/
public <R> R timeSupplierWithPollTaskUnregisteredException(SupplierWithPollTaskUnregisteredException<R> supplier)
throws PollTaskUnregistered {
try {
this.resume();
return supplier.get();
} finally {
this.suspend();
}
}
/**
* Time single action using this StopWatch
*
* First StopWatch is resumed, then action is applied. Finally the StopWatch is suspended.
*
* @param supplier action to time
* @return return value from supplier
*/
public <R> R timeSupplier(Supplier<R> supplier) {
try {
this.resume();
return supplier.get();
} finally {
this.suspend();
}
}
/**
* Time single action using this StopWatch
*
* First StopWatch is resumed, then action is applied. Finally the StopWatch is suspended.
*
* @param action action to time
* @throws ModbusException when original action throws the exception
*/
public void timeRunnableWithModbusException(RunnableWithModbusException action) throws ModbusException {
try {
this.resume();
action.run();
} finally {
this.suspend();
}
}
/**
* Time single action using this StopWatch
*
* First StopWatch is resumed, then action is applied. Finally the StopWatch is suspended.
*
* @param supplier action to time
* @return return value from supplier
*/
public void timeRunnable(Runnable runnable) {
try {
this.resume();
runnable.run();
} finally {
this.suspend();
}
}
/**
* Time single action using this StopWatch
*
* First StopWatch is resumed, then action is applied. Finally the StopWatch is suspended.
*
* @param consumer action to time
* @return return value from supplier
*/
public <T> void timeConsumer(Consumer<T> consumer, T parameter) {
try {
this.resume();
consumer.accept(parameter);
} finally {
this.suspend();
}
}
}

View File

@ -0,0 +1,35 @@
/**
* 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.core.io.transport.modbus.internal.pooling;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.EvictionConfig;
import org.apache.commons.pool2.impl.EvictionPolicy;
import org.openhab.core.io.transport.modbus.internal.pooling.ModbusSlaveConnectionFactoryImpl.PooledConnection;
import net.wimpi.modbus.net.ModbusSlaveConnection;
/**
* Eviction policy, i.e. policy for deciding when to close idle, unused connections.
*
* Connections are evicted according to {@link PooledConnection} maybeResetConnection method.
*
* @author Sami Salonen - Initial contribution
*/
public class ModbusSlaveConnectionEvictionPolicy implements EvictionPolicy<ModbusSlaveConnection> {
@Override
public boolean evict(EvictionConfig config, PooledObject<ModbusSlaveConnection> underTest, int idleCount) {
return ((PooledConnection) underTest).maybeResetConnection("evict");
}
}

View File

@ -0,0 +1,28 @@
/**
* 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.core.io.transport.modbus.internal.pooling;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpointVisitor;
import net.wimpi.modbus.net.ModbusSlaveConnection;
/**
* Factory for ModbusSlaveConnection objects using endpoint definition.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusSlaveConnectionFactory extends ModbusSlaveEndpointVisitor<ModbusSlaveConnection> {
}

View File

@ -0,0 +1,364 @@
/**
* 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.core.io.transport.modbus.internal.pooling;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.endpoint.EndpointPoolConfiguration;
import org.openhab.core.io.transport.modbus.endpoint.ModbusIPSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSerialSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpointVisitor;
import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusUDPSlaveEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.wimpi.modbus.net.ModbusSlaveConnection;
import net.wimpi.modbus.net.SerialConnection;
import net.wimpi.modbus.net.TCPMasterConnection;
import net.wimpi.modbus.net.UDPMasterConnection;
/**
* ModbusSlaveConnectionFactoryImpl responsible of the lifecycle of modbus slave connections
*
* The actual pool uses instance of this class to create and destroy connections as-needed.
*
* The overall functionality goes as follow
* - create: create connection object but do not connect it yet
* - destroyObject: close connection and free all resources. Called by the pool when the pool is being closed or the
* object is invalidated.
* - activateObject: prepare connection to be used. In practice, connect if disconnected
* - passivateObject: passivate connection before returning it back to the pool. Currently, passivateObject closes all
* IP-based connections every now and then (reconnectAfterMillis). Serial connections we keep open.
* - wrap: wrap created connection to pooled object wrapper class. It tracks usage statistics and last connection time.
*
* Note that the implementation must be thread safe.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusSlaveConnectionFactoryImpl
extends BaseKeyedPooledObjectFactory<ModbusSlaveEndpoint, ModbusSlaveConnection> {
class PooledConnection extends DefaultPooledObject<ModbusSlaveConnection> {
private volatile long lastConnected;
private volatile @Nullable ModbusSlaveEndpoint endpoint;
public PooledConnection(ModbusSlaveConnection object) {
super(object);
}
public long getLastConnected() {
return lastConnected;
}
public void setLastConnected(ModbusSlaveEndpoint endpoint, long lastConnected) {
this.endpoint = endpoint;
this.lastConnected = lastConnected;
}
/**
*
* Reset connection if it is too old or fulfills some of the other criteria
*
* @param activityName ongoing activity calling this method. For logging
* @return whether connection was reseted
*/
public boolean maybeResetConnection(String activityName) {
ModbusSlaveEndpoint localEndpoint = endpoint;
if (localEndpoint == null) {
// We have not connected yet, abort
// Without endpoint we have no age parameters available (endpointPoolConfigs &
// disconnectIfConnectedBefore)
return false;
}
long localLastConnected = lastConnected;
ModbusSlaveConnection connection = getObject();
@Nullable
EndpointPoolConfiguration configuration = endpointPoolConfigs.get(localEndpoint);
long reconnectAfterMillis = configuration == null ? 0 : configuration.getReconnectAfterMillis();
long connectionAgeMillis = System.currentTimeMillis() - localLastConnected;
long disconnectIfConnectedBeforeMillis = disconnectIfConnectedBefore.getOrDefault(localEndpoint, -1L);
boolean disconnectSinceTooOldConnection = disconnectIfConnectedBeforeMillis < 0L ? false
: localLastConnected <= disconnectIfConnectedBeforeMillis;
boolean shouldBeDisconnected = (reconnectAfterMillis == 0
|| (reconnectAfterMillis > 0 && connectionAgeMillis > reconnectAfterMillis)
|| disconnectSinceTooOldConnection);
if (shouldBeDisconnected) {
logger.trace(
"({}) Connection {} (endpoint {}) age {}ms is over the reconnectAfterMillis={}ms limit or has been connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={} -> disconnecting.",
activityName, connection, localEndpoint, connectionAgeMillis, reconnectAfterMillis,
localLastConnected, disconnectIfConnectedBeforeMillis);
connection.resetConnection();
return true;
} else {
logger.trace(
"({}) Connection {} (endpoint {}) age ({}ms) is below the reconnectAfterMillis ({}ms) limit and connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={}. Keep the connection open.",
activityName, connection, localEndpoint, connectionAgeMillis, reconnectAfterMillis,
localLastConnected, disconnectIfConnectedBeforeMillis);
return false;
}
}
}
private final Logger logger = LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class);
private volatile Map<ModbusSlaveEndpoint, @Nullable EndpointPoolConfiguration> endpointPoolConfigs = new ConcurrentHashMap<>();
private volatile Map<ModbusSlaveEndpoint, Long> lastPassivateMillis = new ConcurrentHashMap<>();
private volatile Map<ModbusSlaveEndpoint, Long> lastConnectMillis = new ConcurrentHashMap<>();
private volatile Map<ModbusSlaveEndpoint, Long> disconnectIfConnectedBefore = new ConcurrentHashMap<>();
private volatile Function<ModbusSlaveEndpoint, @Nullable EndpointPoolConfiguration> defaultPoolConfigurationFactory = endpoint -> null;
private @Nullable InetAddress getInetAddress(ModbusIPSlaveEndpoint key) {
try {
return InetAddress.getByName(key.getAddress());
} catch (UnknownHostException e) {
logger.error("KeyedPooledModbusSlaveConnectionFactory: Unknown host: {}. Connection creation failed.",
e.getMessage());
return null;
}
}
@Override
public ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Exception {
return endpoint.accept(new ModbusSlaveEndpointVisitor<ModbusSlaveConnection>() {
@Override
public @Nullable ModbusSlaveConnection visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) {
SerialConnection connection = new SerialConnection(modbusSerialSlavePoolingKey.getSerialParameters());
logger.trace("Created connection {} for endpoint {}", connection, modbusSerialSlavePoolingKey);
return connection;
}
@Override
public @Nullable ModbusSlaveConnection visit(ModbusTCPSlaveEndpoint key) {
InetAddress address = getInetAddress(key);
if (address == null) {
return null;
}
EndpointPoolConfiguration config = getEndpointPoolConfiguration(key);
int connectTimeoutMillis = 0;
if (config != null) {
connectTimeoutMillis = config.getConnectTimeoutMillis();
}
TCPMasterConnection connection = new TCPMasterConnection(address, key.getPort(), connectTimeoutMillis);
logger.trace("Created connection {} for endpoint {}", connection, key);
return connection;
}
@Override
public @Nullable ModbusSlaveConnection visit(ModbusUDPSlaveEndpoint key) {
InetAddress address = getInetAddress(key);
if (address == null) {
return null;
}
UDPMasterConnection connection = new UDPMasterConnection(address, key.getPort());
logger.trace("Created connection {} for endpoint {}", connection, key);
return connection;
}
});
}
@Override
public PooledObject<ModbusSlaveConnection> wrap(ModbusSlaveConnection connection) {
return new PooledConnection(connection);
}
@Override
public void destroyObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject<ModbusSlaveConnection> obj) {
if (obj == null) {
return;
}
logger.trace("destroyObject for connection {} and endpoint {} -> closing the connection", obj.getObject(),
endpoint);
obj.getObject().resetConnection();
}
@Override
public void activateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject<ModbusSlaveConnection> obj)
throws Exception {
if (obj == null) {
return;
}
ModbusSlaveConnection connection = obj.getObject();
try {
@Nullable
EndpointPoolConfiguration config = getEndpointPoolConfiguration(endpoint);
if (!connection.isConnected()) {
tryConnect(endpoint, obj, connection, config);
}
if (config != null) {
long waited = waitAtleast(lastPassivateMillis.get(endpoint), config.getInterTransactionDelayMillis());
logger.trace(
"Waited {}ms (interTransactionDelayMillis {}ms) before giving returning connection {} for endpoint {}, to ensure delay between transactions.",
waited, config.getInterTransactionDelayMillis(), obj.getObject(), endpoint);
}
} catch (InterruptedException e) {
// Someone wants to cancel us, reset the connection and abort
if (connection.isConnected()) {
connection.resetConnection();
}
} catch (Exception e) {
logger.error("Error connecting connection {} for endpoint {}: {}", obj.getObject(), endpoint,
e.getMessage());
}
}
@Override
public void passivateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject<ModbusSlaveConnection> obj) {
if (obj == null) {
return;
}
ModbusSlaveConnection connection = obj.getObject();
logger.trace("Passivating connection {} for endpoint {}...", connection, endpoint);
lastPassivateMillis.put(endpoint, System.currentTimeMillis());
((PooledConnection) obj).maybeResetConnection("passivate");
logger.trace("...Passivated connection {} for endpoint {}", obj.getObject(), endpoint);
}
@Override
public boolean validateObject(ModbusSlaveEndpoint key, @Nullable PooledObject<ModbusSlaveConnection> p) {
boolean valid = p != null && p.getObject().isConnected();
logger.trace("Validating endpoint {} connection {} -> {}", key, p.getObject(), valid);
return valid;
}
/**
* Configure general connection settings with a given endpoint
*
* @param endpoint endpoint to configure
* @param configuration configuration for the endpoint. Use null to reset the configuration to default settings.
*/
public void setEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint, @Nullable EndpointPoolConfiguration config) {
if (config == null) {
endpointPoolConfigs.remove(endpoint);
} else {
endpointPoolConfigs.put(endpoint, config);
}
}
/**
* Get general configuration settings applied to a given endpoint
*
* Note that default configuration settings are returned in case the endpoint has not been configured.
*
* @param endpoint endpoint to query
* @return general connection settings of the given endpoint
*/
@SuppressWarnings("null")
public @Nullable EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint) {
@Nullable
EndpointPoolConfiguration config = endpointPoolConfigs.computeIfAbsent(endpoint,
defaultPoolConfigurationFactory);
return config;
}
/**
* Set default factory for {@link EndpointPoolConfiguration}
*
* @param defaultPoolConfigurationFactory function providing defaults for a given endpoint
*/
public void setDefaultPoolConfigurationFactory(
Function<ModbusSlaveEndpoint, @Nullable EndpointPoolConfiguration> defaultPoolConfigurationFactory) {
this.defaultPoolConfigurationFactory = defaultPoolConfigurationFactory;
}
private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj,
ModbusSlaveConnection connection, @Nullable EndpointPoolConfiguration config) throws Exception {
if (connection.isConnected()) {
return;
}
int tryIndex = 0;
Long lastConnect = lastConnectMillis.get(endpoint);
int maxTries = config == null ? 1 : config.getConnectMaxTries();
do {
try {
if (config != null) {
long waited = waitAtleast(lastConnect,
Math.max(config.getInterConnectDelayMillis(), config.getInterTransactionDelayMillis()));
if (waited > 0) {
logger.trace(
"Waited {}ms (interConnectDelayMillis {}ms, interTransactionDelayMillis {}ms) before "
+ "connecting disconnected connection {} for endpoint {}, to allow delay "
+ "between connections re-connects",
waited, config.getInterConnectDelayMillis(), config.getInterTransactionDelayMillis(),
obj.getObject(), endpoint);
}
}
connection.connect();
long curTime = System.currentTimeMillis();
((PooledConnection) obj).setLastConnected(endpoint, curTime);
lastConnectMillis.put(endpoint, curTime);
break;
} catch (InterruptedException e) {
logger.error("connect try {}/{} error: {}. Aborting since interrupted. Connection {}. Endpoint {}.",
tryIndex, maxTries, e.getMessage(), connection, endpoint);
throw e;
} catch (Exception e) {
tryIndex++;
logger.error("connect try {}/{} error: {}. Connection {}. Endpoint {}", tryIndex, maxTries,
e.getMessage(), connection, endpoint);
if (tryIndex >= maxTries) {
logger.error("re-connect reached max tries {}, throwing last error: {}. Connection {}. Endpoint {}",
maxTries, e.getMessage(), connection, endpoint);
throw e;
}
lastConnect = System.currentTimeMillis();
}
} while (true);
}
/**
* Sleep until <code>waitMillis</code> has passed from <code>lastOperation</code>
*
* @param lastOperation last time operation was executed, or null if it has not been executed
* @param waitMillis
* @return milliseconds slept
* @throws InterruptedException
*/
public static long waitAtleast(@Nullable Long lastOperation, long waitMillis) throws InterruptedException {
if (lastOperation == null) {
return 0;
}
long millisSinceLast = System.currentTimeMillis() - lastOperation;
long millisToWaitStill = Math.min(waitMillis, Math.max(0, waitMillis - millisSinceLast));
try {
Thread.sleep(millisToWaitStill);
} catch (InterruptedException e) {
LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class).debug("wait interrupted", e);
throw e;
}
return millisToWaitStill;
}
/**
* Disconnect returning connections which have been connected before certain time
*
* @param disconnectBeforeConnectedMillis disconnected connections that have been connected before this time
*/
public void disconnectOnReturn(ModbusSlaveEndpoint endpoint, long disconnectBeforeConnectedMillis) {
disconnectIfConnectedBefore.put(endpoint, disconnectBeforeConnectedMillis);
}
}

View File

@ -0,0 +1,217 @@
/**
* 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.core.io.transport.modbus.json;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang.NotImplementedException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.modbus.BitArray;
import org.openhab.core.io.transport.modbus.ModbusConstants;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* Utilities for converting JSON to {@link ModbusWriteRequestBlueprint}
*
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public final class WriteRequestJsonUtilities {
/**
* Constant for the function code key in the JSON
*/
public static final String JSON_FUNCTION_CODE = "functionCode";
/**
* Constant for the write address key in the JSON
*/
public static final String JSON_ADDRESS = "address";
/**
* Constant for the value key in the JSON
*/
public static final String JSON_VALUE = "value";
/**
* Constant for the maxTries key in the JSON
*/
public static final String JSON_MAX_TRIES = "maxTries";
/**
* Default maxTries when it has not been specified
*/
public static final int DEFAULT_MAX_TRIES = 3;
private static final JsonParser PARSER = new JsonParser();
private WriteRequestJsonUtilities() {
throw new NotImplementedException();
}
/**
* Parse JSON string to collection of {@link ModbusWriteRequestBlueprint}
*
* JSON string should represent a JSON array, with JSON objects. Each JSON object represents a write request. The
* JSON object must have the following keys
* - functionCode: numeric function code
* - address: reference or start address of the write
* - value: array of data to be written. Use zero and one when writing coils. With registers, each number
* corresponds to register's 16 bit data.
* - maxTries: number of tries with the write in case of errors
*
*
* @param unitId unit id for the constructed {@link ModbusWriteRequestBlueprint}
* @param jsonString json to be parsed in string format
* @return collection of {@link ModbusWriteRequestBlueprint} representing the json
* @throws IllegalArgumentException in case of unexpected function codes, or too large payload exceeding modbus
* protocol specification
* @throws IllegalStateException in case of parsing errors and unexpected json structure
*
* @see WriteRequestJsonUtilities.JSON_FUNCTION_CODE
* @see WriteRequestJsonUtilities.JSON_ADDRESS
* @see WriteRequestJsonUtilities.JSON_VALUE
* @see WriteRequestJsonUtilities.JSON_MAX_TRIES
*/
public static Collection<ModbusWriteRequestBlueprint> fromJson(int unitId, String jsonString) {
JsonArray jsonArray = PARSER.parse(jsonString).getAsJsonArray();
if (jsonArray.size() == 0) {
return new LinkedList<>();
}
Deque<ModbusWriteRequestBlueprint> writes = new LinkedList<>();
jsonArray.forEach(writeElem -> {
writes.add(constructBluerint(unitId, writeElem));
});
return writes;
}
private static ModbusWriteRequestBlueprint constructBluerint(int unitId, JsonElement arrayElement) {
final JsonObject writeObject;
try {
writeObject = arrayElement.getAsJsonObject();
} catch (IllegalStateException e) {
throw new IllegalStateException("JSON array contained something else than a JSON object!", e);
}
@Nullable
JsonElement functionCode = writeObject.get(JSON_FUNCTION_CODE);
@Nullable
JsonElement address = writeObject.get(JSON_ADDRESS);
@Nullable
JsonElement maxTries = writeObject.get(JSON_MAX_TRIES);
@Nullable
JsonArray valuesElem;
try {
valuesElem = writeObject.get(JSON_VALUE).getAsJsonArray();
} catch (IllegalStateException e) {
throw new IllegalStateException(String.format("JSON object '%s' is not an JSON array!", JSON_VALUE), e);
}
return constructBluerint(unitId, functionCode, address, maxTries, valuesElem);
}
private static ModbusWriteRequestBlueprint constructBluerint(int unitId, @Nullable JsonElement functionCodeElem,
@Nullable JsonElement addressElem, @Nullable JsonElement maxTriesElem, @Nullable JsonArray valuesElem) {
int functionCodeNumeric;
if (functionCodeElem == null || functionCodeElem.isJsonNull()) {
throw new IllegalStateException(String.format("Value for '%s' is invalid", JSON_FUNCTION_CODE));
}
try {
functionCodeNumeric = functionCodeElem.getAsInt();
} catch (ClassCastException | IllegalStateException e) {
throw new IllegalStateException(String.format("Value for '%s' is invalid", JSON_FUNCTION_CODE), e);
}
ModbusWriteFunctionCode functionCode = ModbusWriteFunctionCode.fromFunctionCode(functionCodeNumeric);
int address;
if (addressElem == null || addressElem.isJsonNull()) {
throw new IllegalStateException(String.format("Value for '%s' is invalid", JSON_ADDRESS));
}
try {
address = addressElem.getAsInt();
} catch (ClassCastException | IllegalStateException e) {
throw new IllegalStateException(String.format("Value for '%s' is invalid", JSON_ADDRESS), e);
}
int maxTries;
if (maxTriesElem == null || maxTriesElem.isJsonNull()) {
// Go with default
maxTries = DEFAULT_MAX_TRIES;
} else {
try {
maxTries = maxTriesElem.getAsInt();
} catch (ClassCastException | IllegalStateException e) {
throw new IllegalStateException(String.format("Value for '%s' is invalid", JSON_MAX_TRIES), e);
}
}
if (valuesElem == null || valuesElem.isJsonNull()) {
throw new IllegalArgumentException(String.format("Expecting non-null value, got: %s", valuesElem));
}
AtomicBoolean writeSingle = new AtomicBoolean(false);
switch (functionCode) {
case WRITE_COIL:
writeSingle.set(true);
if (valuesElem.size() != 1) {
throw new IllegalArgumentException(String
.format("Expecting single value with functionCode=%s, got: %d", functionCode, valuesElem));
}
// fall-through to WRITE_MULTIPLE_COILS
case WRITE_MULTIPLE_COILS:
if (valuesElem.size() == 0) {
throw new IllegalArgumentException("Must provide at least one coil");
} else if (valuesElem.size() > ModbusConstants.MAX_BITS_WRITE_COUNT) {
throw new IllegalArgumentException(
String.format("Trying to write too many coils (%d). Maximum is %s", valuesElem.size(),
ModbusConstants.MAX_BITS_WRITE_COUNT));
}
BitArray bits = new BitArray(valuesElem.size());
for (int i = 0; i < valuesElem.size(); i++) {
bits.setBit(i, valuesElem.get(i).getAsInt() != 0);
}
return new ModbusWriteCoilRequestBlueprint(unitId, address, bits, !writeSingle.get(), maxTries);
case WRITE_SINGLE_REGISTER:
writeSingle.set(true);
if (valuesElem.size() != 1) {
throw new IllegalArgumentException(String
.format("Expecting single value with functionCode=%s, got: %d", functionCode, valuesElem));
}
// fall-through to WRITE_MULTIPLE_REGISTERS
case WRITE_MULTIPLE_REGISTERS: {
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) {
throw new IllegalArgumentException(
String.format("Trying to write too many registers (%d). Maximum is %s", valuesElem.size(),
ModbusConstants.MAX_REGISTERS_WRITE_COUNT));
}
for (int i = 0; i < valuesElem.size(); i++) {
registers[i] = valuesElem.get(i).getAsInt();
}
return new ModbusWriteRegisterRequestBlueprint(unitId, address, new ModbusRegisterArray(registers),
!writeSingle.get(), maxTries);
}
default:
throw new IllegalArgumentException("Unknown function code");
}
}
}

View File

@ -0,0 +1,70 @@
/**
* 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.core.io.transport.modbus.test;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
/**
* @author Sami Salonen - Initial contribution
*/
abstract class AbstractRequestComparer<T extends ModbusWriteRequestBlueprint> extends TypeSafeMatcher<T> {
private int expectedUnitId;
private int expectedAddress;
private ModbusWriteFunctionCode expectedFunctionCode;
private int expectedMaxTries;
public AbstractRequestComparer(int expectedUnitId, int expectedAddress,
ModbusWriteFunctionCode expectedFunctionCode, int expectedMaxTries) {
this.expectedUnitId = expectedUnitId;
this.expectedAddress = expectedAddress;
this.expectedFunctionCode = expectedFunctionCode;
this.expectedMaxTries = expectedMaxTries;
}
@Override
public void describeTo(Description description) {
description.appendText("should return request with");
description.appendText(" unitID=");
description.appendValue(expectedUnitId);
description.appendText(" address=");
description.appendValue(expectedAddress);
description.appendText(" functionCode=");
description.appendValue(expectedFunctionCode);
description.appendText(" maxTries=");
description.appendValue(expectedMaxTries);
}
@SuppressWarnings("null")
@Override
protected boolean matchesSafely(T item) {
if (item.getUnitID() != expectedUnitId) {
return false;
}
if (item.getReference() != expectedAddress) {
return false;
}
if (item.getFunctionCode() != expectedFunctionCode) {
return false;
}
if (item.getMaxTries() != expectedMaxTries) {
return false;
}
return doMatchData(item);
}
protected abstract boolean doMatchData(T item);
}

View File

@ -0,0 +1,87 @@
/**
* 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.core.io.transport.modbus.test;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import org.openhab.core.io.transport.modbus.BitArray;
/**
* @author Sami Salonen - Initial contribution
*/
public class BasicBitArrayTest {
@Test
public void testGetBitAndSetBit() {
BitArray data1 = new BitArray(true, false, true);
assertThat(data1.size(), is(equalTo(3)));
assertThat(data1.getBit(0), is(equalTo(true)));
assertThat(data1.getBit(1), is(equalTo(false)));
assertThat(data1.getBit(2), is(equalTo(true)));
data1.setBit(1, true);
data1.setBit(2, false);
assertThat(data1.size(), is(equalTo(3)));
assertThat(data1.getBit(0), is(equalTo(true)));
assertThat(data1.getBit(1), is(equalTo(true)));
assertThat(data1.getBit(2), is(equalTo(false)));
}
@Test
public void testGetBitAndSetBit2() {
BitArray data1 = new BitArray(3);
assertThat(data1.size(), is(equalTo(3)));
assertThat(data1.getBit(0), is(equalTo(false)));
assertThat(data1.getBit(1), is(equalTo(false)));
assertThat(data1.getBit(2), is(equalTo(false)));
data1.setBit(1, true);
assertThat(data1.size(), is(equalTo(3)));
assertThat(data1.getBit(0), is(equalTo(false)));
assertThat(data1.getBit(1), is(equalTo(true)));
assertThat(data1.getBit(2), is(equalTo(false)));
data1.setBit(1, false);
assertThat(data1.size(), is(equalTo(3)));
assertThat(data1.getBit(0), is(equalTo(false)));
assertThat(data1.getBit(1), is(equalTo(false)));
assertThat(data1.getBit(2), is(equalTo(false)));
}
@Test
public void testOutOfBounds() {
BitArray data1 = new BitArray(true, false, true);
assertThrows(IndexOutOfBoundsException.class, () -> data1.getBit(3));
}
@Test
public void testOutOfBounds2() {
BitArray data1 = new BitArray(true, false, true);
assertThrows(IndexOutOfBoundsException.class, () -> data1.getBit(-1));
}
@Test
public void testOutOfBounds3() {
BitArray data1 = new BitArray(3);
assertThrows(IndexOutOfBoundsException.class, () -> data1.getBit(3));
}
@Test
public void testOutOfBounds4() {
BitArray data1 = new BitArray(3);
assertThrows(IndexOutOfBoundsException.class, () -> data1.getBit(-1));
}
}

View File

@ -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.core.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.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.types.Command;
/**
* @author Sami Salonen - Initial contribution
*/
public class BitUtilitiesCommandToRegistersTest {
private static short[] shorts(int... ints) {
short[] shorts = new short[ints.length];
for (int i = 0; i < ints.length; i++) {
short s = (short) ints[i];
shorts[i] = s;
}
return shorts;
}
public static Collection<Object[]> data() {
return Collections.unmodifiableList(Stream
.of(new Object[] { new DecimalType("1.0"), ValueType.BIT, IllegalArgumentException.class },
new Object[] { new DecimalType("1.0"), ValueType.INT8, IllegalArgumentException.class },
//
// INT16
//
new Object[] { new DecimalType("1.0"), ValueType.INT16, shorts(1) },
new Object[] { new DecimalType("1.6"), ValueType.INT16, shorts(1) },
new Object[] { new DecimalType("2.6"), ValueType.INT16, shorts(2) },
new Object[] { new DecimalType("-1004.4"), ValueType.INT16, shorts(-1004), },
// within bounds for signed int16
new Object[] { new DecimalType("32000"), ValueType.INT16, shorts(32000), },
new Object[] { new DecimalType("-32000"), ValueType.INT16, shorts(-32000), },
// out bounds for signed int16, but not for uint16
new Object[] { new DecimalType("60000"), ValueType.INT16, shorts(60000), },
new Object[] { new DecimalType("64000"), ValueType.INT16, shorts(64000), }, //
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType("70004.4"),
// 70004 -> 0x00011174 (int) -> 0x1174 (short) = 4468
ValueType.INT16, shorts(4468), },
//
// UINT16 (same as INT16)
//
new Object[] { new DecimalType("1.0"), ValueType.UINT16, shorts(1) },
new Object[] { new DecimalType("1.6"), ValueType.UINT16, shorts(1) },
new Object[] { new DecimalType("2.6"), ValueType.UINT16, shorts(2) },
new Object[] { new DecimalType("-1004.4"), ValueType.UINT16, shorts(-1004), },
// within bounds for signed int16
new Object[] { new DecimalType("32000"), ValueType.UINT16, shorts(32000), },
new Object[] { new DecimalType("-32000"), ValueType.UINT16, shorts(-32000), },
// out bounds for signed int16, but not for uint16
new Object[] { new DecimalType("60000"), ValueType.UINT16, shorts(60000), },
new Object[] { new DecimalType("64000"), ValueType.UINT16, shorts(64000), }, //
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType("70004.4"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.UINT16, shorts(0x1174), },
//
// INT32
//
new Object[] { new DecimalType("1.0"), ValueType.INT32, shorts(0, 1) },
new Object[] { new DecimalType("1.6"), ValueType.INT32, shorts(0, 1) },
new Object[] { new DecimalType("2.6"), ValueType.INT32, shorts(0, 2) },
new Object[] { new DecimalType("-1004.4"), ValueType.INT32,
// -1004 = 0xFFFFFC14 (32bit) =
shorts(0xFFFF, 0xFC14), },
new Object[] { new DecimalType("64000"), ValueType.INT32, shorts(0, 64000), }, //
// within signed int32 range: +-2,000,000,00
new Object[] { new DecimalType("-2000000000"), ValueType.INT32, shorts(0x88CA, 0x6C00), },
new Object[] { new DecimalType("2000000000"), ValueType.INT32, shorts(0x7735, 0x9400), },
// out bounds for signed int32, but not for uint32
new Object[] { new DecimalType("3000000000"), ValueType.INT32, shorts(0xB2D0, 0x5E00), }, //
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"),
// 5000000000 -> 0x12a05f200 () -> 0x1174 (16bit)
ValueType.INT32, shorts(0x2a05, 0xf200), },
//
// UINT32 (same as INT32)
//
new Object[] { new DecimalType("1.0"), ValueType.UINT32, shorts(0, 1) },
new Object[] { new DecimalType("1.6"), ValueType.UINT32, shorts(0, 1) },
new Object[] { new DecimalType("2.6"), ValueType.UINT32, shorts(0, 2) },
new Object[] { new DecimalType("-1004.4"), ValueType.UINT32,
// -1004 = 0xFFFFFC14 (32bit) =
shorts(0xFFFF, 0xFC14), },
new Object[] { new DecimalType("64000"), ValueType.UINT32, shorts(0, 64000), }, //
// within signed int32 range: +-2,000,000,00
new Object[] { new DecimalType("-2000000000"), ValueType.UINT32, shorts(0x88CA, 0x6C00), },
new Object[] { new DecimalType("2000000000"), ValueType.UINT32, shorts(0x7735, 0x9400), },
// out bounds for signed int32, but not for uint32
new Object[] { new DecimalType("3000000000"), ValueType.UINT32, shorts(0xB2D0, 0x5E00), }, //
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"),
// 5000000000 -> 0x12a05f200 () -> 0x1174 (16bit)
ValueType.UINT32, shorts(0x2a05, 0xf200), },
//
// INT32_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.INT32_SWAP, shorts(1, 0) },
new Object[] { new DecimalType("1.6"), ValueType.INT32_SWAP, shorts(1, 0) },
new Object[] { new DecimalType("2.6"), ValueType.INT32_SWAP, shorts(2, 0) },
new Object[] { new DecimalType("-1004.4"), ValueType.INT32_SWAP,
// -1004 = 0xFFFFFC14 (32bit)
shorts(0xFC14, 0xFFFF), },
new Object[] { new DecimalType("64000"), ValueType.INT32_SWAP, shorts(64000, 0), },
// within signed int32 range: +-2,000,000,00
new Object[] { new DecimalType("-2000000000"), ValueType.INT32_SWAP, shorts(0x6C00, 0x88CA), },
new Object[] { new DecimalType("2000000000"), ValueType.INT32_SWAP, shorts(0x9400, 0x7735), },
// out bounds for signed int32, but not for uint32
new Object[] { new DecimalType("3000000000"), ValueType.INT32_SWAP, shorts(0x5E00, 0xB2D0), }, //
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"),
// 5000000000 -> 0x12a05f200
ValueType.INT32_SWAP, shorts(0xf200, 0x2a05), },
//
// UINT32_SWAP (same as INT32_SWAP)
//
new Object[] { new DecimalType("1.0"), ValueType.UINT32_SWAP, shorts(1, 0) },
new Object[] { new DecimalType("1.6"), ValueType.UINT32_SWAP, shorts(1, 0) },
new Object[] { new DecimalType("2.6"), ValueType.UINT32_SWAP, shorts(2, 0) },
new Object[] { new DecimalType("-1004.4"), ValueType.UINT32_SWAP,
// -1004 = 0xFFFFFC14 (32bit)
shorts(0xFC14, 0xFFFF), },
new Object[] { new DecimalType("64000"), ValueType.UINT32_SWAP, shorts(64000, 0), },
// within signed int32 range: +-2,000,000,00
new Object[] { new DecimalType("-2000000000"), ValueType.UINT32_SWAP, shorts(0x6C00, 0x88CA), },
new Object[] { new DecimalType("2000000000"), ValueType.UINT32_SWAP, shorts(0x9400, 0x7735), },
// out bounds for signed int32, but not for uint32
new Object[] { new DecimalType("3000000000"), ValueType.UINT32_SWAP, shorts(0x5E00, 0xB2D0), }, //
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"),
// 5000000000 -> 0x12a05f200
ValueType.UINT32_SWAP, shorts(0xf200, 0x2a05), },
//
// FLOAT32
//
new Object[] { new DecimalType("1.0"), ValueType.FLOAT32, shorts(0x3F80, 0x0000) },
new Object[] { new DecimalType("1.6"), ValueType.FLOAT32, shorts(0x3FCC, 0xCCCD) },
new Object[] { new DecimalType("2.6"), ValueType.FLOAT32, shorts(0x4026, 0x6666) },
new Object[] { new DecimalType("-1004.4"), ValueType.FLOAT32, shorts(0xC47B, 0x199A), },
new Object[] { new DecimalType("64000"), ValueType.FLOAT32, shorts(0x477A, 0x0000), },
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType("70004.4"), ValueType.FLOAT32, shorts(0x4788, 0xBA33), },
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"), ValueType.FLOAT32, shorts(0x4F95, 0x02F9), },
//
// FLOAT32_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.FLOAT32_SWAP, shorts(0x0000, 0x3F80) },
new Object[] { new DecimalType("1.6"), ValueType.FLOAT32_SWAP, shorts(0xCCCD, 0x3FCC) },
new Object[] { new DecimalType("2.6"), ValueType.FLOAT32_SWAP, shorts(0x6666, 0x4026) },
new Object[] { new DecimalType("-1004.4"), ValueType.FLOAT32_SWAP, shorts(0x199A, 0xC47B), },
new Object[] { new DecimalType("64000"), ValueType.FLOAT32_SWAP, shorts(0x0000, 0x477A), },
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType("70004.4"), ValueType.FLOAT32_SWAP, shorts(0xBA33, 0x4788), },
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"), ValueType.FLOAT32_SWAP, shorts(0x02F9, 0x4F95) },
// ON/OFF
new Object[] { OnOffType.ON, ValueType.FLOAT32_SWAP, shorts(0x0000, 0x3F80) },
new Object[] { OnOffType.OFF, ValueType.FLOAT32_SWAP, shorts(0x0000, 0x0000) },
// OPEN
new Object[] { OpenClosedType.OPEN, ValueType.FLOAT32_SWAP, shorts(0x0000, 0x3F80) },
new Object[] { OpenClosedType.OPEN, ValueType.INT16, shorts(1) },
// CLOSED
new Object[] { OpenClosedType.CLOSED, ValueType.FLOAT32_SWAP, shorts(0x0000, 0x0000) },
new Object[] { OpenClosedType.CLOSED, ValueType.INT16, shorts(0x0000) },
// Unsupported command
new Object[] { IncreaseDecreaseType.INCREASE, ValueType.FLOAT32_SWAP,
NotImplementedException.class },
//
// INT64
//
new Object[] { new DecimalType("1.0"), ValueType.INT64, shorts(0, 0, 0, 1) },
new Object[] { new DecimalType("1.6"), ValueType.INT64, shorts(0, 0, 0, 1) },
new Object[] { new DecimalType("2.6"), ValueType.INT64, shorts(0, 0, 0, 2) },
new Object[] { new DecimalType("-1004.4"), ValueType.INT64,
shorts(0xFFFF, 0xFFFF, 0xFFFF, 0xFC14), },
new Object[] { new DecimalType("64000"), ValueType.INT64, shorts(0, 0, 0, 64000), },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.INT64, shorts(0x0, 0x8, 0x0, 0x0), },
// within signed int64 range: +-9,200,000,000,000,000,000
new Object[] { new DecimalType("-9200000000000000000"), ValueType.INT64,
shorts(0x8053, 0x08BE, 0x6268, 0x0000), },
new Object[] { new DecimalType("9200000000000000000"), ValueType.INT64,
shorts(0x7FAC, 0xF741, 0x9D98, 0x0000), },
// within unsigned int64 range (but out of range for signed int64)
new Object[] { new DecimalType("18200000000000000000"), ValueType.INT64,
shorts(0xFC93, 0x6392, 0x801C, 0x0000), },
new Object[] {
// out of bounds of unsigned 64bit
new DecimalType("3498348904359085439088905"),
// should pick the low 64 bits
ValueType.INT64, shorts(0xDFC5, 0xBBB7, 0x772E, 0x7909), },
//
// UINT64 (same as INT64)
//
new Object[] { new DecimalType("1.0"), ValueType.UINT64, shorts(0, 0, 0, 1) },
new Object[] { new DecimalType("1.6"), ValueType.UINT64, shorts(0, 0, 0, 1) },
new Object[] { new DecimalType("2.6"), ValueType.UINT64, shorts(0, 0, 0, 2) },
new Object[] { new DecimalType("-1004.4"), ValueType.UINT64,
shorts(0xFFFF, 0xFFFF, 0xFFFF, 0xFC14), },
new Object[] { new DecimalType("64000"), ValueType.UINT64, shorts(0, 0, 0, 64000), },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.UINT64, shorts(0x0, 0x8, 0x0, 0x0), },
// within signed int64 range: +-9,200,000,000,000,000,000
new Object[] { new DecimalType("-9200000000000000000"), ValueType.UINT64,
shorts(0x8053, 0x08BE, 0x6268, 0x0000), },
new Object[] { new DecimalType("9200000000000000000"), ValueType.UINT64,
shorts(0x7FAC, 0xF741, 0x9D98, 0x0000), },
// within unsigned int64 range (but out of range for signed int64)
new Object[] { new DecimalType("18200000000000000000"), ValueType.UINT64,
shorts(0xFC93, 0x6392, 0x801C, 0x0000), },
new Object[] {
// out of bounds of unsigned 64bit
new DecimalType("3498348904359085439088905"),
// should pick the low 64 bits
ValueType.UINT64, shorts(0xDFC5, 0xBBB7, 0x772E, 0x7909), },
//
// INT64_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.INT64_SWAP, shorts(1, 0, 0, 0) },
new Object[] { new DecimalType("1.6"), ValueType.INT64_SWAP, shorts(1, 0, 0, 0) },
new Object[] { new DecimalType("2.6"), ValueType.INT64_SWAP, shorts(2, 0, 0, 0) },
new Object[] { new DecimalType("-1004.4"), ValueType.INT64_SWAP,
shorts(0xFC14, 0xFFFF, 0xFFFF, 0xFFFF), },
new Object[] { new DecimalType("64000"), ValueType.INT64_SWAP, shorts(64000, 0, 0, 0), },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.INT64_SWAP, shorts(0x0, 0x0, 0x8, 0x0), },
new Object[] {
// out of bounds of unsigned 64bit
new DecimalType("3498348904359085439088905"),
// should pick the low 64 bits
ValueType.INT64_SWAP, shorts(0x7909, 0x772E, 0xBBB7, 0xDFC5), },
//
// UINT64_SWAP (same as INT64_SWAP)
//
new Object[] { new DecimalType("1.0"), ValueType.UINT64_SWAP, shorts(1, 0, 0, 0) },
new Object[] { new DecimalType("1.6"), ValueType.UINT64_SWAP, shorts(1, 0, 0, 0) },
new Object[] { new DecimalType("2.6"), ValueType.UINT64_SWAP, shorts(2, 0, 0, 0) },
new Object[] { new DecimalType("-1004.4"), ValueType.UINT64_SWAP,
shorts(0xFC14, 0xFFFF, 0xFFFF, 0xFFFF), },
new Object[] { new DecimalType("64000"), ValueType.UINT64_SWAP, shorts(64000, 0, 0, 0), },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.UINT64_SWAP, shorts(0x0, 0x0, 0x8, 0x0), },
new Object[] {
// out of bounds of unsigned 64bit
new DecimalType("3498348904359085439088905"),
// should pick the low 64 bits
ValueType.UINT64_SWAP, shorts(0x7909, 0x772E, 0xBBB7, 0xDFC5), })
.collect(Collectors.toList()));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@ParameterizedTest
@MethodSource("data")
public void testCommandToRegisters(Command command, ValueType type, Object expectedResult) {
if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) {
assertThrows((Class) expectedResult, () -> ModbusBitUtilities.commandToRegisters(command, type));
return;
}
ModbusRegisterArray registers = ModbusBitUtilities.commandToRegisters(command, type);
short[] expectedRegisters = (short[]) expectedResult;
assertThat(String.format("register index command=%s, type=%s", command, type), registers.size(),
is(equalTo(expectedRegisters.length)));
for (int i = 0; i < expectedRegisters.length; i++) {
int expectedRegisterDataUnsigned = expectedRegisters[i] & 0xffff;
int actualUnsigned = registers.getRegister(i);
assertThat(String.format("register index i=%d, command=%s, type=%s", i, command, type), actualUnsigned,
is(equalTo(expectedRegisterDataUnsigned)));
}
}
}

View File

@ -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.core.io.transport.modbus.test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.openhab.core.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));
}
}

View File

@ -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.core.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.core.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);
}
}

View File

@ -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.core.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.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.library.types.DecimalType;
/**
* @author Sami Salonen - Initial contribution
*/
public class BitUtilitiesExtractIndividualMethodsTest {
public static Collection<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> registerVariations(Object expectedResult, ValueType type,
ModbusRegisterArray registers, int index) {
byte[] origBytes = registers.getBytes();
int origRegisterIndex = index;
int origByteIndex = origRegisterIndex * 2;
Builder<Object[]> 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<Number> methodUnderTest, Function<DecimalType, Number> expectedPrimitive) {
testIndividual(expectedResult, type, bytes, byteIndex, methodUnderTest, expectedPrimitive, null);
}
@SuppressWarnings("unchecked")
private void testIndividual(Object expectedResult, ValueType type, byte[] bytes, int byteIndex,
Supplier<Number> methodUnderTest, Function<DecimalType, Number> 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<? extends Throwable>) 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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);
}
}

View File

@ -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.core.io.transport.modbus.test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.openhab.core.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));
}
}

View File

@ -0,0 +1,376 @@
/**
* 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.core.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.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNull;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.library.types.DecimalType;
/**
* @author Sami Salonen - Initial contribution
*/
public class BitUtilitiesExtractStateFromRegistersTest {
private static ModbusRegisterArray shortArrayToRegisterArray(int... arr) {
return new ModbusRegisterArray(arr);
}
public static Collection<Object[]> data() {
return Collections.unmodifiableList(Stream.of(
//
// BIT
//
new Object[] { new DecimalType("1.0"), ValueType.BIT,
shortArrayToRegisterArray(1 << 5 | 1 << 4 | 1 << 15), 4 },
new Object[] { new DecimalType("1.0"), ValueType.BIT,
shortArrayToRegisterArray(1 << 5 | 1 << 4 | 1 << 15), 15 },
new Object[] { new DecimalType("0.0"), ValueType.BIT, shortArrayToRegisterArray(1 << 5), 7 },
new Object[] { new DecimalType("1.0"), ValueType.BIT, shortArrayToRegisterArray(1 << 5), 5 },
new Object[] { new DecimalType("0.0"), ValueType.BIT, shortArrayToRegisterArray(1 << 5), 4 },
new Object[] { new DecimalType("0.0"), ValueType.BIT, shortArrayToRegisterArray(1 << 5), 0 },
new Object[] { new DecimalType("0.0"), ValueType.BIT, shortArrayToRegisterArray(0, 0), 15 },
new Object[] { new DecimalType("1.0"), ValueType.BIT, shortArrayToRegisterArray(1 << 5, 1 << 4), 5 },
new Object[] { new DecimalType("1.0"), ValueType.BIT, shortArrayToRegisterArray(1 << 5, 1 << 4), 20 },
new Object[] { IllegalArgumentException.class, ValueType.BIT, shortArrayToRegisterArray(1 << 5), 16 },
new Object[] { IllegalArgumentException.class, ValueType.BIT, shortArrayToRegisterArray(1 << 5), 200 },
new Object[] { IllegalArgumentException.class, ValueType.BIT, shortArrayToRegisterArray(), 0 },
new Object[] { IllegalArgumentException.class, ValueType.BIT, shortArrayToRegisterArray(0, 0), 32 },
//
// INT8
//
new Object[] { new DecimalType("5.0"), ValueType.INT8, shortArrayToRegisterArray(5), 0 },
new Object[] { new DecimalType("-5.0"), ValueType.INT8, shortArrayToRegisterArray(-5), 0 },
new Object[] { new DecimalType("3.0"), ValueType.INT8,
shortArrayToRegisterArray(((byte) 6 << 8) | (byte) 3), 0 },
new Object[] { new DecimalType("6.0"), ValueType.INT8,
shortArrayToRegisterArray(((byte) 6 << 8) | (byte) 3), 1 },
new Object[] { new DecimalType("4.0"), ValueType.INT8,
shortArrayToRegisterArray(((byte) 6 << 8) | (byte) 3, 4), 2 },
new Object[] { new DecimalType("6.0"), ValueType.INT8,
shortArrayToRegisterArray(55, ((byte) 6 << 8) | (byte) 3), 3 },
new Object[] { IllegalArgumentException.class, ValueType.INT8, shortArrayToRegisterArray(1), 2 },
new Object[] { IllegalArgumentException.class, ValueType.INT8, shortArrayToRegisterArray(1, 2), 4 },
//
// UINT8
//
new Object[] { new DecimalType("5.0"), ValueType.UINT8, shortArrayToRegisterArray(5), 0 },
new Object[] { new DecimalType("251.0"), ValueType.UINT8, shortArrayToRegisterArray(-5), 0 },
new Object[] { new DecimalType("3.0"), ValueType.UINT8,
shortArrayToRegisterArray(((byte) 6 << 8) | (byte) 3), 0 },
new Object[] { new DecimalType("6.0"), ValueType.UINT8,
shortArrayToRegisterArray(((byte) 6 << 8) | (byte) 3), 1 },
new Object[] { new DecimalType("4.0"), ValueType.UINT8,
shortArrayToRegisterArray(((byte) 6 << 8) | (byte) 3, 4), 2 },
new Object[] { new DecimalType("6.0"), ValueType.UINT8,
shortArrayToRegisterArray(55, ((byte) 6 << 8) | (byte) 3), 3 },
new Object[] { IllegalArgumentException.class, ValueType.UINT8, shortArrayToRegisterArray(1), 2 },
new Object[] { IllegalArgumentException.class, ValueType.UINT8, shortArrayToRegisterArray(1, 2), 4 },
//
// INT16
//
new Object[] { new DecimalType("1.0"), ValueType.INT16, shortArrayToRegisterArray(1), 0 },
new Object[] { new DecimalType("2.0"), ValueType.INT16, shortArrayToRegisterArray(2), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT16, shortArrayToRegisterArray(-1004), 0 },
new Object[] { new DecimalType("-1536"), ValueType.INT16, shortArrayToRegisterArray(64000), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT16, shortArrayToRegisterArray(4, -1004), 1 },
new Object[] { new DecimalType("-1004"), ValueType.INT16, shortArrayToRegisterArray(-1004, 4), 0 },
new Object[] { IllegalArgumentException.class, ValueType.INT16, shortArrayToRegisterArray(4, -1004),
2 },
//
// UINT16
//
new Object[] { new DecimalType("1.0"), ValueType.UINT16, shortArrayToRegisterArray(1), 0 },
new Object[] { new DecimalType("2.0"), ValueType.UINT16, shortArrayToRegisterArray(2), 0 },
new Object[] { new DecimalType("64532"), ValueType.UINT16, shortArrayToRegisterArray(-1004), 0 },
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.UINT16, shortArrayToRegisterArray(4, -1004),
2 },
//
// INT32
//
new Object[] { new DecimalType("1.0"), ValueType.INT32, shortArrayToRegisterArray(0, 1), 0 },
new Object[] { new DecimalType("2.0"), ValueType.INT32, shortArrayToRegisterArray(0, 2), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT32,
// -1004 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFFFF, 0xFC14), 0 },
new Object[] { new DecimalType("64000"), ValueType.INT32, shortArrayToRegisterArray(0, 64000), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT32,
// -1004 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0x4, 0xFFFF, 0xFC14), 1 },
new Object[] { new DecimalType("-1004"), ValueType.INT32,
// -1004 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFFFF, 0xFC14, 0x4), 0 },
new Object[] { IllegalArgumentException.class, ValueType.INT32, shortArrayToRegisterArray(4, -1004),
1 },
new Object[] { IllegalArgumentException.class, ValueType.INT32, shortArrayToRegisterArray(4, -1004),
2 },
new Object[] { IllegalArgumentException.class, ValueType.INT32, shortArrayToRegisterArray(0, 0, 0), 2 },
//
// UINT32
//
new Object[] { new DecimalType("1.0"), ValueType.UINT32, shortArrayToRegisterArray(0, 1), 0 },
new Object[] { new DecimalType("2.0"), ValueType.UINT32, shortArrayToRegisterArray(0, 2), 0 },
new Object[] { new DecimalType("4294966292"), ValueType.UINT32,
// 4294966292 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFFFF, 0xFC14), 0 },
new Object[] { new DecimalType("64000"), ValueType.UINT32, shortArrayToRegisterArray(0, 64000), 0 },
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType("70004"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.UINT32, shortArrayToRegisterArray(1, 4468), 0 },
new Object[] { new DecimalType("4294966292"), ValueType.UINT32,
// 4294966292 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFFFF, 0xFC14, 0x5), 0 },
new Object[] { new DecimalType("4294966292"), ValueType.UINT32,
// 4294966292 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0x5, 0xFFFF, 0xFC14), 1 },
new Object[] { IllegalArgumentException.class, ValueType.UINT32, shortArrayToRegisterArray(4, -1004),
1 },
new Object[] { IllegalArgumentException.class, ValueType.UINT32, shortArrayToRegisterArray(4, -1004),
2 },
new Object[] { IllegalArgumentException.class, ValueType.UINT32, shortArrayToRegisterArray(0, 0, 0),
2 },
//
// INT32_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.INT32_SWAP, shortArrayToRegisterArray(1, 0), 0 },
new Object[] { new DecimalType("2.0"), ValueType.INT32_SWAP, shortArrayToRegisterArray(2, 0), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT32_SWAP,
// -1004 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFC14, 0xFFFF), 0 },
new Object[] { new DecimalType("64000"), ValueType.INT32_SWAP, shortArrayToRegisterArray(64000, 0), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT32_SWAP,
// -1004 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0x4, 0xFC14, 0xFFFF), 1 },
new Object[] { new DecimalType("-1004"), ValueType.INT32_SWAP,
// -1004 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFC14, 0xFFFF, 0x4), 0 },
new Object[] { IllegalArgumentException.class, ValueType.INT32_SWAP,
shortArrayToRegisterArray(4, -1004), 1 },
new Object[] { IllegalArgumentException.class, ValueType.INT32_SWAP,
shortArrayToRegisterArray(4, -1004), 2 },
new Object[] { IllegalArgumentException.class, ValueType.INT32_SWAP, shortArrayToRegisterArray(0, 0, 0),
2 },
//
// UINT32_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.UINT32_SWAP, shortArrayToRegisterArray(1, 0), 0 },
new Object[] { new DecimalType("2.0"), ValueType.UINT32_SWAP, shortArrayToRegisterArray(2, 0), 0 },
new Object[] { new DecimalType("4294966292"), ValueType.UINT32_SWAP,
// 4294966292 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFC14, 0xFFFF), 0 },
new Object[] { new DecimalType("64000"), ValueType.UINT32_SWAP, shortArrayToRegisterArray(64000, 0),
0 },
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType("70004"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.UINT32_SWAP, shortArrayToRegisterArray(4468, 1), 0 },
new Object[] { new DecimalType("4294966292"), ValueType.UINT32_SWAP,
// 4294966292 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0xFC14, 0xFFFF, 0x5), 0 },
new Object[] { new DecimalType("4294966292"), ValueType.UINT32_SWAP,
// 4294966292 = 0xFFFFFC14 (32bit) =
shortArrayToRegisterArray(0x5, 0xFC14, 0xFFFF), 1 },
new Object[] { IllegalArgumentException.class, ValueType.UINT32_SWAP,
shortArrayToRegisterArray(4, -1004), 1 },
new Object[] { IllegalArgumentException.class, ValueType.UINT32_SWAP,
shortArrayToRegisterArray(4, -1004), 2 },
new Object[] { IllegalArgumentException.class, ValueType.UINT32_SWAP,
shortArrayToRegisterArray(0, 0, 0), 2 },
//
// FLOAT32
//
new Object[] { new DecimalType("1.0"), ValueType.FLOAT32, shortArrayToRegisterArray(0x3F80, 0x0000),
0 },
new Object[] { new DecimalType(1.6f), ValueType.FLOAT32, shortArrayToRegisterArray(0x3FCC, 0xCCCD), 0 },
new Object[] { new DecimalType(2.6f), ValueType.FLOAT32, shortArrayToRegisterArray(0x4026, 0x6666), 0 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32, shortArrayToRegisterArray(0xC47B, 0x199A),
0 },
new Object[] { new DecimalType("64000"), ValueType.FLOAT32, shortArrayToRegisterArray(0x477A, 0x0000),
0 },
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType(70004.4f), ValueType.FLOAT32, shortArrayToRegisterArray(0x4788, 0xBA33), 0 },
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"), ValueType.FLOAT32, shortArrayToRegisterArray(0x4F95, 0x02F9),
0 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32,
shortArrayToRegisterArray(0x4, 0xC47B, 0x199A), 1 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32,
shortArrayToRegisterArray(0xC47B, 0x199A, 0x4), 0 },
new Object[] { // equivalent of NaN
Optional.empty(), ValueType.FLOAT32, shortArrayToRegisterArray(0x7fc0, 0x0000), 0 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32,
shortArrayToRegisterArray(0x4, 0x0, 0x0, 0x0, 0xC47B, 0x199A), 4 },
new Object[] { IllegalArgumentException.class, ValueType.FLOAT32, shortArrayToRegisterArray(4, -1004),
1 },
new Object[] { IllegalArgumentException.class, ValueType.FLOAT32, shortArrayToRegisterArray(4, -1004),
2 },
new Object[] { IllegalArgumentException.class, ValueType.FLOAT32, shortArrayToRegisterArray(0, 0, 0),
2 },
//
// FLOAT32_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0x0000, 0x3F80), 0 },
new Object[] { new DecimalType(1.6f), ValueType.FLOAT32_SWAP, shortArrayToRegisterArray(0xCCCD, 0x3FCC),
0 },
new Object[] { new DecimalType(2.6f), ValueType.FLOAT32_SWAP, shortArrayToRegisterArray(0x6666, 0x4026),
0 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0x199A, 0xC47B), 0 },
new Object[] { new DecimalType("64000"), ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0x0000, 0x477A), 0 },
new Object[] { // equivalent of NaN
Optional.empty(), ValueType.FLOAT32_SWAP, shortArrayToRegisterArray(0x0000, 0x7fc0), 0 },
new Object[] {
// out of bounds of unsigned 16bit (0 to 65,535)
new DecimalType(70004.4f), ValueType.FLOAT32_SWAP, shortArrayToRegisterArray(0xBA33, 0x4788),
0 },
new Object[] {
// out of bounds of unsigned 32bit (0 to 4,294,967,295)
new DecimalType("5000000000"), ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0x02F9, 0x4F95), 0 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0x4, 0x199A, 0xC47B), 1 },
new Object[] { new DecimalType(-1004.4f), ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0x199A, 0xC47B, 0x4), 0 },
new Object[] { IllegalArgumentException.class, ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(4, -1004), 1 },
new Object[] { IllegalArgumentException.class, ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(4, -1004), 2 },
new Object[] { IllegalArgumentException.class, ValueType.FLOAT32_SWAP,
shortArrayToRegisterArray(0, 0, 0), 2 },
//
// INT64
//
new Object[] { new DecimalType("1.0"), ValueType.INT64, shortArrayToRegisterArray(0, 0, 0, 1), 0 },
new Object[] { new DecimalType("2.0"), ValueType.INT64, shortArrayToRegisterArray(0, 0, 0, 2), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT64,
shortArrayToRegisterArray(0xFFFF, 0xFFFF, 0xFFFF, 0xFC14), 0 },
new Object[] { new DecimalType("64000"), ValueType.INT64, shortArrayToRegisterArray(0, 0, 0, 64000),
0 },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"), ValueType.INT64, shortArrayToRegisterArray(0x0, 0x8, 0x0, 0x0),
0 },
new Object[] { new DecimalType("-2322243636186679031"), ValueType.INT64,
shortArrayToRegisterArray(0xDFC5, 0xBBB7, 0x772E, 0x7909), 0 },
// would read over the registers
new Object[] { IllegalArgumentException.class, ValueType.INT64,
shortArrayToRegisterArray(0xDFC5, 0xBBB7, 0x772E, 0x7909), 1 },
// would read over the registers
new Object[] { IllegalArgumentException.class, ValueType.INT64,
shortArrayToRegisterArray(0xDFC5, 0xBBB7, 0x772E, 0x7909), 2 },
// 4 registers expected, only 3 available
new Object[] { IllegalArgumentException.class, ValueType.INT64,
shortArrayToRegisterArray(0xDFC5, 0xBBB7, 0x772E), 0 },
//
// UINT64
//
new Object[] { new DecimalType("1.0"), ValueType.UINT64, shortArrayToRegisterArray(0, 0, 0, 1), 0 },
new Object[] { new DecimalType("2.0"), ValueType.UINT64, shortArrayToRegisterArray(0, 0, 0, 2), 0 },
new Object[] { new DecimalType("18446744073709550612"), ValueType.UINT64,
shortArrayToRegisterArray(0xFFFF, 0xFFFF, 0xFFFF, 0xFC14), 0 },
new Object[] { new DecimalType("64000"), ValueType.UINT64, shortArrayToRegisterArray(0, 0, 0, 64000),
0 },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"), ValueType.UINT64, shortArrayToRegisterArray(0x0, 0x8, 0x0, 0x0),
0 },
new Object[] { new DecimalType("16124500437522872585"), ValueType.UINT64,
shortArrayToRegisterArray(0xDFC5, 0xBBB7, 0x772E, 0x7909), 0 },
//
// INT64_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.INT64_SWAP, shortArrayToRegisterArray(1, 0, 0, 0), 0 },
new Object[] { new DecimalType("2.0"), ValueType.INT64_SWAP, shortArrayToRegisterArray(2, 0, 0, 0), 0 },
new Object[] { new DecimalType("-1004"), ValueType.INT64_SWAP,
shortArrayToRegisterArray(0xFC14, 0xFFFF, 0xFFFF, 0xFFFF), 0 },
new Object[] { new DecimalType("64000"), ValueType.INT64_SWAP,
shortArrayToRegisterArray(64000, 0, 0, 0), 0 },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"),
// 70004 -> 0x00011174 (32bit) -> 0x1174 (16bit)
ValueType.INT64_SWAP, shortArrayToRegisterArray(0x0, 0x0, 0x8, 0x0), 0 },
new Object[] { new DecimalType("-2322243636186679031"), ValueType.INT64_SWAP,
shortArrayToRegisterArray(0x7909, 0x772E, 0xBBB7, 0xDFC5), 0 },
//
// UINT64_SWAP
//
new Object[] { new DecimalType("1.0"), ValueType.UINT64_SWAP, shortArrayToRegisterArray(1, 0, 0, 0),
0 },
new Object[] { new DecimalType("2.0"), ValueType.UINT64_SWAP, shortArrayToRegisterArray(2, 0, 0, 0),
0 },
new Object[] { new DecimalType("18446744073709550612"), ValueType.UINT64_SWAP,
shortArrayToRegisterArray(0xFC14, 0xFFFF, 0xFFFF, 0xFFFF), 0 },
new Object[] { new DecimalType("64000"), ValueType.UINT64_SWAP,
shortArrayToRegisterArray(64000, 0, 0, 0), 0 },
new Object[] {
// out of bounds of unsigned 32bit
new DecimalType("34359738368"), ValueType.UINT64_SWAP,
shortArrayToRegisterArray(0x0, 0x0, 0x8, 0x0), 0 },
new Object[] {
// out of bounds of unsigned 64bit
new DecimalType("16124500437522872585"), ValueType.UINT64_SWAP,
shortArrayToRegisterArray(0x7909, 0x772E, 0xBBB7, 0xDFC5), 0 })
.collect(Collectors.toList()));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@ParameterizedTest
@MethodSource("data")
public void testextractStateFromRegisters(Object expectedResult, ValueType type, ModbusRegisterArray registers,
int index) {
if (expectedResult instanceof Class && Exception.class.isAssignableFrom((Class) expectedResult)) {
assertThrows((Class) expectedResult,
() -> ModbusBitUtilities.extractStateFromRegisters(registers, index, type));
return;
}
Optional<@NonNull DecimalType> actualState = ModbusBitUtilities.extractStateFromRegisters(registers, index,
type);
// Wrap given expectedResult to Optional, if necessary
Optional<@NonNull DecimalType> expectedStateWrapped = expectedResult instanceof DecimalType
? Optional.of((DecimalType) expectedResult)
: (Optional<@NonNull DecimalType>) expectedResult;
assertThat(String.format("registers=%s, index=%d, type=%s", registers, index, type), actualState,
is(equalTo(expectedStateWrapped)));
}
}

View File

@ -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.core.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.core.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.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<Object[]> 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 <EFBFBD>
new Object[] { "<EFBFBD>", 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<Object[]> 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<Object[]> 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<Object[]> 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));
}
}
}

View File

@ -0,0 +1,79 @@
/**
* 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.core.io.transport.modbus.test;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
/**
* @author Sami Salonen - Initial contribution
*/
public class BitUtilitiesTranslateCommand2BooleanTest {
@Test
public void testZero() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(DecimalType.ZERO);
assertThat(actual, is(equalTo(Optional.of(false))));
}
@Test
public void testNegative() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(new DecimalType(-3.4));
assertThat(actual, is(equalTo(Optional.of(true))));
}
@Test
public void testPositive() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(new DecimalType(3.4));
assertThat(actual, is(equalTo(Optional.of(true))));
}
@Test
public void testOn() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(OnOffType.ON);
assertThat(actual, is(equalTo(Optional.of(true))));
}
@Test
public void testOpen() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(OpenClosedType.OPEN);
assertThat(actual, is(equalTo(Optional.of(true))));
}
@Test
public void testOff() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(OnOffType.OFF);
assertThat(actual, is(equalTo(Optional.of(false))));
}
@Test
public void testClosed() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(OpenClosedType.CLOSED);
assertThat(actual, is(equalTo(Optional.of(false))));
}
@Test
public void testUnknown() {
Optional<Boolean> actual = ModbusBitUtilities.translateCommand2Boolean(IncreaseDecreaseType.INCREASE);
assertThat(actual, is(equalTo(Optional.empty())));
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.core.io.transport.modbus.test;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.StreamSupport;
import org.hamcrest.Description;
import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
/**
* @author Sami Salonen - Initial contribution
*/
class CoilMatcher extends AbstractRequestComparer<ModbusWriteCoilRequestBlueprint> {
private Boolean[] expectedCoils;
public CoilMatcher(int expectedUnitId, int expectedAddress, int expectedMaxTries,
ModbusWriteFunctionCode expectedFunctionCode, Boolean... expectedCoils) {
super(expectedUnitId, expectedAddress, expectedFunctionCode, expectedMaxTries);
this.expectedCoils = expectedCoils;
}
@Override
public void describeTo(Description description) {
super.describeTo(description);
description.appendText(" coils=");
description.appendValue(Arrays.toString(expectedCoils));
}
@Override
protected boolean doMatchData(ModbusWriteCoilRequestBlueprint item) {
Object[] actual = StreamSupport.stream(item.getCoils().spliterator(), false).toArray();
return Objects.deepEquals(actual, expectedCoils);
}
}

View File

@ -0,0 +1,357 @@
/**
* 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.core.io.transport.modbus.test;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import java.io.File;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.function.LongSupplier;
import org.apache.commons.lang.NotImplementedException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
import org.openhab.core.io.transport.modbus.internal.ModbusManagerImpl;
import org.openhab.core.test.java.JavaTest;
import gnu.io.SerialPort;
import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.ModbusCoupler;
import net.wimpi.modbus.ModbusIOException;
import net.wimpi.modbus.io.ModbusTransport;
import net.wimpi.modbus.msg.ModbusRequest;
import net.wimpi.modbus.net.ModbusSerialListener;
import net.wimpi.modbus.net.ModbusTCPListener;
import net.wimpi.modbus.net.ModbusUDPListener;
import net.wimpi.modbus.net.SerialConnection;
import net.wimpi.modbus.net.SerialConnectionFactory;
import net.wimpi.modbus.net.TCPSlaveConnection;
import net.wimpi.modbus.net.TCPSlaveConnection.ModbusTCPTransportFactory;
import net.wimpi.modbus.net.TCPSlaveConnectionFactory;
import net.wimpi.modbus.net.UDPSlaveTerminal;
import net.wimpi.modbus.net.UDPSlaveTerminal.ModbusUDPTransportFactoryImpl;
import net.wimpi.modbus.net.UDPSlaveTerminalFactory;
import net.wimpi.modbus.net.UDPTerminal;
import net.wimpi.modbus.procimg.SimpleProcessImage;
import net.wimpi.modbus.util.AtomicCounter;
import net.wimpi.modbus.util.SerialParameters;
/**
* @author Sami Salonen - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.WARN)
public class IntegrationTestSupport extends JavaTest {
public enum ServerType {
TCP,
UDP,
SERIAL
}
/**
* Servers to test
* Serial is system dependent
*/
public static final ServerType[] TEST_SERVERS = new ServerType[] { ServerType.TCP
// ServerType.UDP,
// ServerType.SERIAL
};
// One can perhaps test SERIAL with https://github.com/freemed/tty0tty
// and using those virtual ports? Not the same thing as real serial device of course
private static String SERIAL_SERVER_PORT = "/dev/pts/7";
private static String SERIAL_CLIENT_PORT = "/dev/pts/8";
private static SerialParameters SERIAL_PARAMETERS_CLIENT = new SerialParameters(SERIAL_CLIENT_PORT, 115200,
SerialPort.FLOWCONTROL_NONE, SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE, Modbus.SERIAL_ENCODING_ASCII, false, 1000);
private static SerialParameters SERIAL_PARAMETERS_SERVER = new SerialParameters(SERIAL_SERVER_PORT,
SERIAL_PARAMETERS_CLIENT.getBaudRate(), SERIAL_PARAMETERS_CLIENT.getFlowControlIn(),
SERIAL_PARAMETERS_CLIENT.getFlowControlOut(), SERIAL_PARAMETERS_CLIENT.getDatabits(),
SERIAL_PARAMETERS_CLIENT.getStopbits(), SERIAL_PARAMETERS_CLIENT.getParity(),
SERIAL_PARAMETERS_CLIENT.getEncoding(), SERIAL_PARAMETERS_CLIENT.isEcho(), 1000);
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
System.setProperty("gnu.io.rxtx.SerialPorts", SERIAL_SERVER_PORT + File.pathSeparator + SERIAL_CLIENT_PORT);
}
/**
* Max time to wait for connections/requests from client
*/
protected int MAX_WAIT_REQUESTS_MILLIS = 1000;
/**
* The server runs in single thread, only one connection is accepted at a time.
* This makes the tests as strict as possible -- connection must be closed.
*/
private static final int SERVER_THREADS = 1;
protected static int SLAVE_UNIT_ID = 1;
private static AtomicCounter udpServerIndex = new AtomicCounter(0);
protected @Spy TCPSlaveConnectionFactory tcpConnectionFactory = new TCPSlaveConnectionFactoryImpl();
protected @Spy UDPSlaveTerminalFactory udpTerminalFactory = new UDPSlaveTerminalFactoryImpl();
protected @Spy SerialConnectionFactory serialConnectionFactory = new SerialConnectionFactoryImpl();
protected ResultCaptor<ModbusRequest> modbustRequestCaptor;
protected ModbusTCPListener tcpListener;
protected ModbusUDPListener udpListener;
protected ModbusSerialListener serialListener;
protected SimpleProcessImage spi;
protected int tcpModbusPort = -1;
protected int udpModbusPort = -1;
protected ServerType serverType = ServerType.TCP;
protected long artificialServerWait = 0;
protected NonOSGIModbusManager modbusManager;
private Thread serialServerThread = new Thread("ModbusTransportTestsSerialServer") {
@Override
public void run() {
serialListener = new ModbusSerialListener(SERIAL_PARAMETERS_SERVER);
}
};
protected static InetAddress localAddress() throws UnknownHostException {
return InetAddress.getByName("127.0.0.1");
}
@BeforeEach
public void setUp() throws Exception {
modbustRequestCaptor = new ResultCaptor<>(new LongSupplier() {
@Override
public long getAsLong() {
return artificialServerWait;
}
});
modbusManager = new NonOSGIModbusManager();
startServer();
}
@AfterEach
public void tearDown() {
stopServer();
modbusManager.close();
}
protected void waitForRequests(int expectedRequestCount) {
waitForAssert(
() -> assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(expectedRequestCount))),
MAX_WAIT_REQUESTS_MILLIS, 10);
}
protected void waitForConnectionsReceived(int expectedConnections) {
waitForAssert(() -> {
if (ServerType.TCP.equals(serverType)) {
verify(tcpConnectionFactory, times(expectedConnections)).create(any(Socket.class));
} else if (ServerType.UDP.equals(serverType)) {
// No-op
// verify(udpTerminalFactory, times(expectedConnections)).create(any(InetAddress.class),
// any(Integer.class));
} else if (ServerType.SERIAL.equals(serverType)) {
// No-op
} else {
throw new NotImplementedException();
}
}, MAX_WAIT_REQUESTS_MILLIS, 10);
}
private void startServer() throws UnknownHostException, InterruptedException {
spi = new SimpleProcessImage();
ModbusCoupler.getReference().setProcessImage(spi);
ModbusCoupler.getReference().setMaster(false);
ModbusCoupler.getReference().setUnitID(SLAVE_UNIT_ID);
if (ServerType.TCP.equals(serverType)) {
startTCPServer();
} else if (ServerType.UDP.equals(serverType)) {
startUDPServer();
} else if (ServerType.SERIAL.equals(serverType)) {
startSerialServer();
} else {
throw new NotImplementedException();
}
}
private void stopServer() {
if (ServerType.TCP.equals(serverType)) {
tcpListener.stop();
} else if (ServerType.UDP.equals(serverType)) {
udpListener.stop();
System.err.println(udpModbusPort);
} else if (ServerType.SERIAL.equals(serverType)) {
try {
serialServerThread.join(100);
} catch (InterruptedException e) {
System.err.println("Serial server thread .join() interrupted! Will interrupt it now.");
}
serialServerThread.interrupt();
} else {
throw new NotImplementedException();
}
}
private void startUDPServer() throws UnknownHostException, InterruptedException {
udpListener = new ModbusUDPListener(localAddress(), udpTerminalFactory);
for (int portCandidate = 10000 + udpServerIndex.increment(); portCandidate < 20000; portCandidate++) {
try {
DatagramSocket socket = new DatagramSocket(portCandidate);
socket.close();
udpListener.setPort(portCandidate);
break;
} catch (SocketException e) {
continue;
}
}
udpListener.start();
waitForUDPServerStartup();
assertNotSame(-1, udpModbusPort);
assertNotSame(0, udpModbusPort);
}
private void waitForUDPServerStartup() throws InterruptedException {
// Query server port. It seems to take time (probably due to thread starting)
waitFor(() -> udpListener.getLocalPort() > 0, 5, 10_000);
udpModbusPort = udpListener.getLocalPort();
}
private void startTCPServer() throws UnknownHostException, InterruptedException {
// Serve single user at a time
tcpListener = new ModbusTCPListener(SERVER_THREADS, localAddress(), tcpConnectionFactory);
// Use any open port
tcpListener.setPort(0);
tcpListener.start();
// Query server port. It seems to take time (probably due to thread starting)
waitForTCPServerStartup();
assertNotSame(-1, tcpModbusPort);
assertNotSame(0, tcpModbusPort);
}
private void waitForTCPServerStartup() throws InterruptedException {
waitFor(() -> tcpListener.getLocalPort() > 0, 10_000, 5);
tcpModbusPort = tcpListener.getLocalPort();
}
private void startSerialServer() throws UnknownHostException, InterruptedException {
serialServerThread.start();
Thread.sleep(1000);
}
public ModbusSlaveEndpoint getEndpoint() {
assert tcpModbusPort > 0;
return new ModbusTCPSlaveEndpoint("127.0.0.1", tcpModbusPort);
}
/**
* Transport factory that spies the created transport items
*/
public class SpyingModbusTCPTransportFactory extends ModbusTCPTransportFactory {
@Override
public ModbusTransport create(Socket socket) {
ModbusTransport transport = spy(super.create(socket));
// Capture requests produced by our server transport
try {
doAnswer(modbustRequestCaptor).when(transport).readRequest();
} catch (ModbusIOException e) {
throw new RuntimeException(e);
}
return transport;
}
}
public class SpyingModbusUDPTransportFactory extends ModbusUDPTransportFactoryImpl {
@Override
public ModbusTransport create(UDPTerminal terminal) {
ModbusTransport transport = spy(super.create(terminal));
// Capture requests produced by our server transport
try {
doAnswer(modbustRequestCaptor).when(transport).readRequest();
} catch (ModbusIOException e) {
throw new RuntimeException(e);
}
return transport;
}
}
public class TCPSlaveConnectionFactoryImpl implements TCPSlaveConnectionFactory {
@Override
public TCPSlaveConnection create(Socket socket) {
return new TCPSlaveConnection(socket, new SpyingModbusTCPTransportFactory());
}
}
public class UDPSlaveTerminalFactoryImpl implements UDPSlaveTerminalFactory {
@Override
public UDPSlaveTerminal create(InetAddress interfac, int port) {
UDPSlaveTerminal terminal = new UDPSlaveTerminal(interfac, new SpyingModbusUDPTransportFactory(), 1);
terminal.setLocalPort(port);
return terminal;
}
}
public class SerialConnectionFactoryImpl implements SerialConnectionFactory {
@Override
public SerialConnection create(SerialParameters parameters) {
SerialConnection serialConnection = new SerialConnection(parameters) {
@Override
public ModbusTransport getModbusTransport() {
ModbusTransport transport = spy(super.getModbusTransport());
try {
doAnswer(modbustRequestCaptor).when(transport).readRequest();
} catch (ModbusIOException e) {
throw new RuntimeException(e);
}
return transport;
}
};
return serialConnection;
}
}
public static class NonOSGIModbusManager extends ModbusManagerImpl implements AutoCloseable {
public NonOSGIModbusManager() {
activate(new HashMap<>());
}
@Override
public void close() {
deactivate();
}
}
}

View File

@ -0,0 +1,108 @@
/**
* 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.core.io.transport.modbus.test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.openhab.core.io.transport.modbus.endpoint.ModbusSerialSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
import org.openhab.core.io.transport.modbus.endpoint.ModbusUDPSlaveEndpoint;
import gnu.io.SerialPort;
import net.wimpi.modbus.Modbus;
/**
* @author Sami Salonen - Initial contribution
*/
public class ModbusSlaveEndpointTestCase {
@Test
public void testEqualsSameTcp() {
ModbusTCPSlaveEndpoint e1 = new ModbusTCPSlaveEndpoint("127.0.0.1", 500);
ModbusTCPSlaveEndpoint e2 = new ModbusTCPSlaveEndpoint("127.0.0.1", 500);
assertEquals(e1, e2);
}
@Test
public void testEqualsSameSerial2() {
ModbusSerialSlaveEndpoint e1 = new ModbusSerialSlaveEndpoint("port1", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, true, 500);
ModbusSerialSlaveEndpoint e2 = new ModbusSerialSlaveEndpoint("port1", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, true, 500);
assertEquals(e1, e2);
}
/**
* even though different echo parameter & baud rate, the endpoints are considered the same due to same port
*/
@Test
public void testEqualsSameSerial3() {
ModbusSerialSlaveEndpoint e1 = new ModbusSerialSlaveEndpoint("port1", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, true, 500);
ModbusSerialSlaveEndpoint e2 = new ModbusSerialSlaveEndpoint("port1", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, false, 500);
assertEquals(e1, e2);
assertEquals(e1.hashCode(), e2.hashCode());
}
@Test
public void testEqualsDifferentSerial() {
ModbusSerialSlaveEndpoint e1 = new ModbusSerialSlaveEndpoint("port1", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, true, 500);
ModbusSerialSlaveEndpoint e2 = new ModbusSerialSlaveEndpoint("port2", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, true, 500);
assertNotEquals(e1, e2);
assertNotEquals(e1.hashCode(), e2.hashCode());
}
@Test
public void testEqualsDifferentTCPPort() {
ModbusTCPSlaveEndpoint e1 = new ModbusTCPSlaveEndpoint("127.0.0.1", 500);
ModbusTCPSlaveEndpoint e2 = new ModbusTCPSlaveEndpoint("127.0.0.1", 501);
assertNotEquals(e1, e2);
assertNotEquals(e1.hashCode(), e2.hashCode());
}
@Test
public void testEqualsDifferentTCPHost() {
ModbusTCPSlaveEndpoint e1 = new ModbusTCPSlaveEndpoint("127.0.0.1", 500);
ModbusTCPSlaveEndpoint e2 = new ModbusTCPSlaveEndpoint("127.0.0.2", 501);
assertNotEquals(e1, e2);
assertNotEquals(e1.hashCode(), e2.hashCode());
}
@Test
public void testEqualsDifferentProtocol() {
ModbusTCPSlaveEndpoint e1 = new ModbusTCPSlaveEndpoint("127.0.0.1", 500);
ModbusUDPSlaveEndpoint e2 = new ModbusUDPSlaveEndpoint("127.0.0.1", 500);
assertNotEquals(e1, e2);
assertNotEquals(e1.hashCode(), e2.hashCode());
}
@Test
public void testEqualsDifferentProtocol2() {
ModbusTCPSlaveEndpoint e1 = new ModbusTCPSlaveEndpoint("127.0.0.1", 500);
ModbusSerialSlaveEndpoint e2 = new ModbusSerialSlaveEndpoint("port2", 9600, SerialPort.FLOWCONTROL_NONE,
SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE,
Modbus.DEFAULT_SERIAL_ENCODING, true, 500);
assertNotEquals(e1, e2);
assertNotEquals(e1.hashCode(), e2.hashCode());
}
}

View File

@ -0,0 +1,53 @@
/**
* 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.core.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.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
/**
* @author Sami Salonen - Initial contribution
*/
class RegisterMatcher extends AbstractRequestComparer<ModbusWriteRegisterRequestBlueprint> {
private Integer[] expectedRegisterValues;
public RegisterMatcher(int expectedUnitId, int expectedAddress, int expectedMaxTries,
ModbusWriteFunctionCode expectedFunctionCode, Integer... expectedRegisterValues) {
super(expectedUnitId, expectedAddress, expectedFunctionCode, expectedMaxTries);
this.expectedRegisterValues = expectedRegisterValues;
}
@Override
public void describeTo(Description description) {
super.describeTo(description);
description.appendText(" registers=");
description.appendValue(Arrays.toString(expectedRegisterValues));
}
@Override
protected boolean doMatchData(ModbusWriteRegisterRequestBlueprint item) {
ModbusRegisterArray registers = item.getRegisters();
Object[] actual = StreamSupport
.stream(IntStream.range(0, registers.size()).mapToObj(registers::getRegister).spliterator(), false)
.toArray();
return Objects.deepEquals(actual, expectedRegisterValues);
}
}

View File

@ -0,0 +1,52 @@
/**
* 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.core.io.transport.modbus.test;
import java.util.ArrayList;
import java.util.function.LongSupplier;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* @author Sami Salonen - Initial contribution
*
* @param <T>
*/
public class ResultCaptor<T> implements Answer<T> {
private ArrayList<T> results = new ArrayList<>();
private LongSupplier longSupplier;
public ResultCaptor(LongSupplier longSupplier) {
this.longSupplier = longSupplier;
}
public ArrayList<T> getAllReturnValues() {
return results;
}
@SuppressWarnings("unchecked")
@Override
public T answer(InvocationOnMock invocationOnMock) throws Throwable {
T result = (T) invocationOnMock.callRealMethod();
synchronized (this.results) {
results.add(result);
}
long wait = longSupplier.getAsLong();
if (wait > 0) {
Thread.sleep(wait);
}
return result;
}
}

View File

@ -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.core.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.core.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());
}
}

View File

@ -0,0 +1,228 @@
/**
* 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.core.io.transport.modbus.test;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.ArrayMatching.arrayContaining;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.openhab.core.io.transport.modbus.ModbusConstants.*;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.jdt.annotation.NonNull;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
import org.openhab.core.io.transport.modbus.json.WriteRequestJsonUtilities;
/**
* @author Sami Salonen - Initial contribution
*/
public class WriteRequestJsonUtilitiesTest {
private static List<String> MAX_REGISTERS = IntStream.range(0, MAX_REGISTERS_WRITE_COUNT).mapToObj(i -> "1")
.collect(Collectors.toList());
private static List<String> OVER_MAX_REGISTERS = IntStream.range(0, MAX_REGISTERS_WRITE_COUNT + 1)
.mapToObj(i -> "1").collect(Collectors.toList());
private static List<String> MAX_COILS = IntStream.range(0, MAX_BITS_WRITE_COUNT).mapToObj(i -> "1")
.collect(Collectors.toList());
private static List<String> OVER_MAX_COILS = IntStream.range(0, MAX_BITS_WRITE_COUNT + 1).mapToObj(i -> "1")
.collect(Collectors.toList());
@Test
public void testEmptyArray() {
assertThat(WriteRequestJsonUtilities.fromJson(3, "[]").size(), is(equalTo(0)));
}
@Test
public void testFC6NoRegister() {
assertThrows(IllegalArgumentException.class, () -> WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 6,"//
+ "\"address\": 5412,"//
+ "\"value\": []"//
+ "}]"));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC6SingleRegister() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 6,"//
+ "\"address\": 5412,"//
+ "\"value\": [3]"//
+ "}]").toArray(),
arrayContaining((Matcher) new RegisterMatcher(55, 5412, WriteRequestJsonUtilities.DEFAULT_MAX_TRIES,
ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, 3)));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC6SingleRegisterMaxTries99() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 6,"//
+ "\"address\": 5412,"//
+ "\"value\": [3],"//
+ "\"maxTries\": 99"//
+ "}]").toArray(),
arrayContaining(
(Matcher) new RegisterMatcher(55, 5412, 99, ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, 3)));
}
@Test
public void testFC6MultipleRegisters() {
assertThrows(IllegalArgumentException.class, () -> WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 6,"//
+ "\"address\": 5412,"//
+ "\"value\": [3, 4]"//
+ "}]"));
}
@Test
public void testFC16NoRegister() {
assertThrows(IllegalArgumentException.class, () -> WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 16,"//
+ "\"address\": 5412,"//
+ "\"value\": []"//
+ "}]"));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC16SingleRegister() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 16,"//
+ "\"address\": 5412,"//
+ "\"value\": [3]"//
+ "}]").toArray(),
arrayContaining((Matcher) new RegisterMatcher(55, 5412, WriteRequestJsonUtilities.DEFAULT_MAX_TRIES,
ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS, 3)));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC16MultipleRegisters() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 16,"//
+ "\"address\": 5412,"//
+ "\"value\": [3, 4, 2]"//
+ "}]").toArray(),
arrayContaining((Matcher) new RegisterMatcher(55, 5412, WriteRequestJsonUtilities.DEFAULT_MAX_TRIES,
ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS, 3, 4, 2)));
}
@Test
public void testFC16MultipleRegistersMaxRegisters() {
Collection<@NonNull ModbusWriteRequestBlueprint> writes = WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 16,"//
+ "\"address\": 5412,"//
+ "\"value\": [" + String.join(",", MAX_REGISTERS) + "]"//
+ "}]");
assertThat(writes.size(), is(equalTo(1)));
}
@Test
public void testFC16MultipleRegistersTooManyRegisters() {
assertThrows(IllegalArgumentException.class, () -> WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 16,"//
+ "\"address\": 5412,"//
+ "\"value\": [" + String.join(",", OVER_MAX_REGISTERS) + "]"//
+ "}]"));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC5SingeCoil() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 5,"//
+ "\"address\": 5412,"//
+ "\"value\": [3]" // value 3 (!= 0) is converted to boolean true
+ "}]").toArray(),
arrayContaining((Matcher) new CoilMatcher(55, 5412, WriteRequestJsonUtilities.DEFAULT_MAX_TRIES,
ModbusWriteFunctionCode.WRITE_COIL, true)));
}
@Test
public void testFC5MultipleCoils() {
assertThrows(IllegalArgumentException.class, () -> WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 5,"//
+ "\"address\": 5412,"//
+ "\"value\": [3, 4]"//
+ "}]"));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC15SingleCoil() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 15,"//
+ "\"address\": 5412,"//
+ "\"value\": [3]"//
+ "}]").toArray(),
arrayContaining((Matcher) new CoilMatcher(55, 5412, WriteRequestJsonUtilities.DEFAULT_MAX_TRIES,
ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS, true)));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testFC15MultipleCoils() {
assertThat(WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 15,"//
+ "\"address\": 5412,"//
+ "\"value\": [1, 0, 5]"//
+ "}]").toArray(),
arrayContaining((Matcher) new CoilMatcher(55, 5412, WriteRequestJsonUtilities.DEFAULT_MAX_TRIES,
ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS, true, false, true)));
}
@Test
public void testFC15MultipleCoilsMaxCoils() {
Collection<@NonNull ModbusWriteRequestBlueprint> writes = WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 15,"//
+ "\"address\": 5412,"//
+ "\"value\": [" + String.join(",", MAX_COILS) + "]"//
+ "}]");
assertThat(writes.size(), is(equalTo(1)));
}
@Test
public void testFC15MultipleCoilsTooManyCoils() {
assertThrows(IllegalArgumentException.class, () -> WriteRequestJsonUtilities.fromJson(55, "[{"//
+ "\"functionCode\": 15,"//
+ "\"address\": 5412,"//
+ "\"value\": [" + String.join(",", OVER_MAX_COILS) + "]"//
+ "}]"));
}
@Test
public void testEmptyObject() {
// we are expecting list, not object -> error
assertThrows(IllegalStateException.class, () -> WriteRequestJsonUtilities.fromJson(3, "{}"));
}
@Test
public void testNumber() {
// we are expecting list, not primitive (number) -> error
assertThrows(IllegalStateException.class, () -> WriteRequestJsonUtilities.fromJson(3, "3"));
}
@Test
public void testEmptyList() {
assertThat(WriteRequestJsonUtilities.fromJson(3, "[]").size(), is(equalTo(0)));
}
}

View File

@ -68,6 +68,7 @@
<module>org.openhab.core.io.rest.ui</module>
<module>org.openhab.core.io.rest.voice</module>
<module>org.openhab.core.io.transport.mdns</module>
<module>org.openhab.core.io.transport.modbus</module>
<module>org.openhab.core.io.transport.mqtt</module>
<module>org.openhab.core.io.transport.serial</module>
<module>org.openhab.core.io.transport.serial.javacomm</module>

View File

@ -192,6 +192,14 @@
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.mdns/${project.version}</bundle>
</feature>
<feature name="openhab-core-io-transport-modbus" version="${project.version}">
<feature>openhab-core-base</feature>
<feature>openhab-transport-serial</feature>
<bundle dependency="true">mvn:org.apache.commons/commons-pool2/2.8.1</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.transport.modbus/${project.version}</bundle>
</feature>
<feature name="openhab-core-io-transport-mqtt" version="${project.version}">
<feature>openhab-core-base</feature>
@ -436,6 +444,10 @@
<feature>openhab-core-io-transport-mdns</feature>
</feature>
<feature name="openhab-transport-modbus" description="Modbus Transport" version="${project.version}">
<feature>openhab-core-io-transport-modbus</feature>
</feature>
<feature name="openhab-transport-mqtt" description="MQTT Transport" version="${project.version}">
<feature>openhab-core-io-transport-mqtt</feature>
</feature>