/* ==================================================================
* ModbusHelper.java - Jul 15, 2013 7:54:17 AM
*
* Copyright 2007-2013 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.io.modbus;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import java.util.LinkedHashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.solarnetwork.util.OptionalService;
import net.wimpi.modbus.ModbusException;
import net.wimpi.modbus.io.ModbusSerialTransaction;
import net.wimpi.modbus.msg.ReadCoilsRequest;
import net.wimpi.modbus.msg.ReadCoilsResponse;
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.WriteCoilResponse;
import net.wimpi.modbus.net.SerialConnection;
import net.wimpi.modbus.procimg.InputRegister;
/**
* Helper methods for working with Modbus.
*
* @author matt
* @version 1.4
*/
public final class ModbusHelper {
private static final String UTF8_CHARSET = "UTF-8";
private static final String ASCII_CHARSET = "US-ASCII";
private static final Logger LOG = LoggerFactory.getLogger(ModbusHelper.class);
/**
* Perform some work with a Modbus {@link SerialConnection}.
*
* <p>
* This method attempts to obtain a {@link SerialConnection} from the
* supplied {@link ModbusSerialConnectionFactory}. If the connection is
* obtained, it will call
* {@link ModbusConnectionCallback#doInConnection(SerialConnection)}, and
* then close the connection when finished.
* </p>
*
* <p>
* <b>Note</b> that if either the connection factory is unavailable, or it
* fails to return a connection, the callback method will never be called.
* </p>
*
* @param connectionFactory
* the connection factory to use, via an {@link OptionalService}
* @param action
* the connection callback
* @return the result of the callback, or <em>null</em> if the callback is
* never invoked
*/
public static <T> T execute(OptionalService<ModbusSerialConnectionFactory> connectionFactory,
ModbusConnectionCallback<T> action) {
T result = null;
ModbusSerialConnectionFactory factory = (connectionFactory == null ? null
: connectionFactory.service());
if ( factory != null ) {
result = factory.execute(action);
}
return result;
}
/**
* Get the values of a set of "coil" type registers, as a BitSet. This uses
* a Modbus function code {@code 1} request.
*
* @param conn
* the Modbus connection to use
* @param addresses
* the Modbus register addresses to read
* @param count
* the count of registers to read with each address
* @param unitId
* the Modbus unit ID to use in the read request
* @return BitSet, with each index corresponding to an index in the
* <code>addresses</code> parameter
*/
public static BitSet readDiscreetValues(SerialConnection conn, final Integer[] addresses,
final int count, final int unitId) {
BitSet result = new BitSet(addresses.length);
for ( int i = 0; i < addresses.length; i++ ) {
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
ReadCoilsRequest req = new ReadCoilsRequest(addresses[i], 1);
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
ReadCoilsResponse res = (ReadCoilsResponse) trans.getResponse();
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got Modbus read coil {} response [{}]", addresses[i], res.getCoils());
}
result.set(i, res.getCoilStatus(0));
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus coil {} values: {}", addresses, result);
}
return result;
}
/**
* Set the value of a set of "coil" type registers. This uses a Modbus
* function code {@code 5} request.
*
* @param conn
* the Modbus connection to use
* @param addresses
* the Modbus register addresses to read
* @param bits
* a BitSet representing the value to set for each corresponding
* {@code addresses} value
* @param unitId
* the Modbus unit ID to use in the read request
* @return BitSet, with each index corresponding to an index in the
* <code>addresses</code> parameter
*/
public static Boolean writeDiscreetValues(SerialConnection conn, final Integer[] addresses,
final BitSet bits, final int unitId) {
for ( int i = 0; i < addresses.length; i++ ) {
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
WriteCoilRequest req = new WriteCoilRequest(addresses[i], bits.get(i));
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
WriteCoilResponse res = (WriteCoilResponse) trans.getResponse();
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got write {} response [{}]", addresses[i], res.getCoil());
}
}
return Boolean.TRUE;
}
/**
* Get the values of a set of "coil" type registers, as a BitSet.This uses a
* Modbus function code {@code 1} request.
*
* @param connectionFactory
* the connection factory to obtain a connection with
* @param addresses
* the Modbus register addresses to read
* @param count
* the count of registers to read with each address
* @param unitId
* the Modbus unit ID to use in the read request
* @return BitSet, with each index corresponding to an index in the
* <code>addresses</code> parameter
* @see readDiscreetValues(SerialConnection, Integer[], int, int)
*/
public static BitSet readDiscreetValues(
OptionalService<ModbusSerialConnectionFactory> connectionFactory, final Integer[] addresses,
final int count, final int unitId) {
return execute(connectionFactory, new ModbusConnectionCallback<BitSet>() {
@Override
public BitSet doInConnection(SerialConnection conn) throws IOException {
return readDiscreetValues(conn, addresses, count, unitId);
}
});
}
/**
* Get the values of specific "input" type registers. This uses a Modbus
* function code {@code 4} request.
*
* @param conn
* the Modbus connection to use
* @param addresses
* the Modbus register addresses to read
* @param count
* the number of Modbus "words" to read from each address
* @param unitId
* the Modbus unit ID to use in the read request
* @return map of integer addresses to corresponding integer values, there
* should be {@code count} values for each {@code address} read
*/
public static Map<Integer, Integer> readInputValues(SerialConnection conn, final Integer[] addresses,
final int count, final int unitId) {
Map<Integer, Integer> result = new LinkedHashMap<Integer, Integer>(
(addresses == null ? 0 : addresses.length) * count);
for ( int i = 0; i < addresses.length; i++ ) {
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
ReadInputRegistersRequest req = new ReadInputRegistersRequest(addresses[i], count);
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
ReadInputRegistersResponse res = (ReadInputRegistersResponse) trans.getResponse();
for ( int w = 0; w < res.getWordCount(); w++ ) {
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got Modbus read input {} response {}", addresses[i] + w,
res.getRegisterValue(w));
}
result.put(addresses[i] + w, res.getRegisterValue(w));
}
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus input registers {} values: {}", addresses, result);
}
return result;
}
/**
* Get the values of specific "input" type registers. This uses a Modbus
* function code {@code 4} request.
*
* @param connectionFactory
* the connection factory to obtain a connection with
* @param addresses
* the Modbus register addresses to read
* @param count
* the number of Modbus "words" to read from each address
* @param unitId
* the Modbus unit ID to use in the read request
* @param unitId
* @return list of integer values, there should be {@code count} values for
* each {@code address} read
*/
public static Map<Integer, Integer> readInputValues(
OptionalService<ModbusSerialConnectionFactory> connectionFactory, final Integer[] addresses,
final int count, final int unitId) {
return execute(connectionFactory, new ModbusConnectionCallback<Map<Integer, Integer>>() {
@Override
public Map<Integer, Integer> doInConnection(SerialConnection conn) throws IOException {
return readInputValues(conn, addresses, count, unitId);
}
});
}
/**
* Get the values of specific registers as an array. This uses a Modbus
* function code {@code 3} request.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @return array of register values; the result will have a length equal to
* {@code count}
*/
public static Integer[] readValues(SerialConnection conn, final Integer address, final int count,
final int unitId) {
Integer[] result = new Integer[count];
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
ReadMultipleRegistersRequest req = new ReadMultipleRegistersRequest(address, count);
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
ReadMultipleRegistersResponse res = (ReadMultipleRegistersResponse) trans.getResponse();
for ( int w = 0; w < res.getWordCount(); w++ ) {
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got Modbus read {} response {}", address + w, res.getRegisterValue(w));
}
result[w] = res.getRegisterValue(w);
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus register {} count {} values: {}",
new Object[] { address, count, result });
}
return result;
}
/**
* Get the values of specific registers as an array of unsigned integers.
* This uses a Modbus function code {@code 3} request.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @return array of register values; the result will have a length equal to
* {@code count}
*/
public static int[] readInts(SerialConnection conn, final Integer address, final int count,
final int unitId) {
int[] result = new int[count];
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
ReadMultipleRegistersRequest req = new ReadMultipleRegistersRequest(address, count);
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
ReadMultipleRegistersResponse res = (ReadMultipleRegistersResponse) trans.getResponse();
for ( int w = 0; w < res.getWordCount(); w++ ) {
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got Modbus read {} response {}", address + w, res.getRegisterValue(w));
}
result[w] = res.getRegisterValue(w);
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus register {} count {} values: {}",
new Object[] { address, count, result });
}
return result;
}
/**
* Get the values of specific registers as an array of signed shorts. This
* uses a Modbus function code {@code 3} request.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @return array of register values; the result will have a length equal to
* {@code count}
*/
public static short[] readSignedShorts(SerialConnection conn, final Integer address, final int count,
final int unitId) {
short[] result = new short[count];
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
ReadMultipleRegistersRequest req = new ReadMultipleRegistersRequest(address, count);
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
ReadMultipleRegistersResponse res = (ReadMultipleRegistersResponse) trans.getResponse();
for ( int w = 0; w < res.getWordCount(); w++ ) {
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got Modbus read {} response {}", address + w, res.getRegisterValue(w));
}
result[w] = res.getRegister(w).toShort();
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus register {} count {} shorts: {}",
new Object[] { address, count, result });
}
return result;
}
/**
* Get the raw bytes of specific registers as an array. This uses a Modbus
* function code {@code 3} request.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus 2-byte "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @return array of register bytes; the result will have a length equal to
* {@code count * 2}
*/
public static byte[] readBytes(final SerialConnection conn, final Integer address, final int count,
final int unitId) {
byte[] result = new byte[count * 2];
ModbusSerialTransaction trans = new ModbusSerialTransaction(conn);
ReadMultipleRegistersRequest req = new ReadMultipleRegistersRequest(address, count);
req.setUnitID(unitId);
req.setHeadless();
trans.setRequest(req);
try {
trans.execute();
} catch ( ModbusException e ) {
throw new RuntimeException(e);
}
ReadMultipleRegistersResponse res = (ReadMultipleRegistersResponse) trans.getResponse();
InputRegister[] registers = res.getRegisters();
if ( registers != null ) {
for ( int i = 0; i < registers.length; i++ ) {
if ( LOG.isTraceEnabled() ) {
LOG.trace("Got Modbus read {} response {}", address + i, res.getRegisterValue(i));
}
System.arraycopy(registers[i].toBytes(), 0, result, i * 2, 2);
}
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus register {} count {} bytes: {}",
new Object[] { address, count, result });
}
return result;
}
/**
* Read a set of "input" type registers and interpret as a UTF-8 encoded
* string.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @param trim
* if <em>true</em> then remove leading/trailing whitespace from the
* resulting string
* @return String from interpreting raw bytes as a UTF-8 encoded string
* @see #readString(SerialConnection, Integer, int, int, boolean, String)
*/
public static String readUTF8String(final SerialConnection conn, final Integer address,
final int count, final int unitId, final boolean trim) {
return readString(conn, address, count, unitId, trim, UTF8_CHARSET);
}
/**
* Read a set of "input" type registers and interpret as a US-ASCII encoded
* string.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @param trim
* if <em>true</em> then remove leading/trailing whitespace from the
* resulting string
* @return String from interpreting raw bytes as a US-ASCII encoded string
* @see #readString(SerialConnection, Integer, int, int, boolean, String)
*/
public static String readASCIIString(final SerialConnection conn, final Integer address,
final int count, final int unitId, final boolean trim) {
return readString(conn, address, count, unitId, trim, ASCII_CHARSET);
}
/**
* Read a set of "input" type registers and interpret as a string. This uses
* a Modbus function code {@code 3} request.
*
* @param conn
* the Modbus connection to use
* @param address
* the Modbus register address to start reading from
* @param count
* the number of Modbus "words" to read
* @param unitId
* the Modbus unit ID to use in the read request
* @param trim
* if <em>true</em> then remove leading/trailing whitespace from the
* resulting string
* @param charsetName
* the character set to interpret the bytes as
* @return String from interpreting raw bytes as a string
* @see #readBytes(SerialConnection, Integer, int, int)
*/
public static String readString(final SerialConnection conn, final Integer address, final int count,
final int unitId, final boolean trim, final String charsetName) {
final byte[] bytes = readBytes(conn, address, count, unitId);
String result = null;
if ( bytes != null ) {
try {
result = new String(bytes, charsetName);
if ( trim ) {
result = result.trim();
}
} catch ( UnsupportedEncodingException e ) {
throw new RuntimeException(e);
}
}
if ( LOG.isDebugEnabled() ) {
LOG.debug("Read Modbus input register {} count {} string: {}",
new Object[] { address, count, result });
}
return result;
}
/**
* Get a 32-bit Modbus long word from a 16-bit high word and a 16-bit low
* word.
*
* @param hiWord
* the high word
* @param loWord
* the low word
* @return a 32-bit long word value
*/
public static int getLongWord(int hiWord, int loWord) {
return (((hiWord & 0xFFFF) << 16) | (loWord & 0xFFFF));
}
/**
* Parse a 32-bit float value from raw Modbus register values. The
* {@code data} array is expected to have a length of at least
* {@code offset} + {@code 1}, and be arranged in big-endian order.
*
* @param data
* the data array
* @return the parsed float, or <em>null</em> if not available or parsed
* float is {@code NaN}
*/
public static Float parseFloat32(final int[] data, int offset) {
Float result = null;
if ( data != null && (offset + 1) < data.length ) {
result = parseFloat32(data[0], data[1]);
}
return result;
}
/**
* Parse a 32-bit float value from raw Modbus register values.
*
* @param high
* the high 16 bits
* @param low
* the low 16 bits
* @return the parsed float, or <em>null</em> if not available or parsed
* float is {@code NaN}
*/
public static Float parseFloat32(final int high, final int low) {
Float result = Float.intBitsToFloat(((high & 0xFFFF) << 16) | (low & 0xFFFF));
if ( result.isNaN() ) {
LOG.trace("Data results in NaN: {} {}", high, low);
result = null;
}
return result;
}
/**
* Parse a 32-bit float value from raw Modbus register values. The
* {@code data} array is expected to have a length of {@code 2}, and be
* arranged in big-endian order.
*
* @param data
* the data array
* @return the parsed float, or <em>null</em> if not available or parsed
* float is {@code NaN}
*/
public static Float parseFloat32(final Integer[] data) {
Float result = null;
if ( data != null && data.length == 2 ) {
result = Float.intBitsToFloat(
((data[0].intValue() & 0xFFFF) << 16) | (data[1].intValue() & 0xFFFF));
if ( result.isNaN() ) {
LOG.trace("Data results in NaN: {}", (Object) data);
result = null;
}
}
return result;
}
/**
* Parse a 64-bit long value from raw Modbus register values. The
* {@code data} array is expected to have a length of {@code 4}, and be
* arranged in big-endian order.
*
* @param data
* the data array
* @return the parsed long
*/
public static Long parseInt64(final Integer[] data) {
Long result = null;
if ( data != null && data.length == 4 ) {
result = parseInt64(data[0], data[1], data[2], data[3]);
}
return result;
}
/**
* Parse a 64-bit long value from raw Modbus register values.
*
* @param h1
* bits 63-48
* @param h2
* bits 47-32
* @param l1
* bits 31-16
* @param l2
* bits 15-0
* @return the parsed long
*/
public static Long parseInt64(final int h1, final int h2, final int l1, final int l2) {
return ((((long) h1 & 0xFFFF) << 48) | (((long) h2 & 0xFFFF) << 32)
| (((long) l1 & 0xFFFF) << 16) | ((long) l2 & 0xFFFF));
}
/**
* Parse a 32-bit long value from raw Modbus register values. The
* {@code data} array is expected to have a length of at least
* {@code offset} + {@code 1}, and be arranged in big-endian order.
* <b>Note</b> a {@code Long} is returned to support unsigned 32-bit values.
*
* @param data
* the data array
* @param offset
* the offset in the array to parse the 32-bit value
* @return the parsed long
*/
public static Long parseInt32(final int[] data, int offset) {
Long result = null;
if ( data != null && (offset + 1) < data.length ) {
result = ((long) ((data[offset] & 0xFFFF) << 16) | (long) (data[offset + 1] & 0xFFFF));
}
return result;
}
}