/**
* Copyright (c) 2010-2017 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.modbus.internal;
import java.util.Collection;
import java.util.Optional;
import org.apache.commons.pool2.KeyedObjectPool;
import org.openhab.binding.modbus.ModbusBindingProvider;
import org.openhab.binding.modbus.internal.pooling.ModbusSlaveEndpoint;
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.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.wimpi.modbus.ModbusException;
import net.wimpi.modbus.io.ModbusTransaction;
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.WriteMultipleRegistersRequest;
import net.wimpi.modbus.msg.WriteSingleRegisterRequest;
import net.wimpi.modbus.net.ModbusSlaveConnection;
import net.wimpi.modbus.procimg.InputRegister;
import net.wimpi.modbus.procimg.Register;
import net.wimpi.modbus.procimg.SimpleRegister;
import net.wimpi.modbus.util.BitVector;
/**
* ModbusSlave class is an abstract class that server as a base class for
* MobvusTCPSlave and ModbusSerialSlave instantiates physical Modbus slave.
* It is responsible for polling data from physical device using appropriate connection.
* It is also responsible for updating physical devices according to OpenHAB commands
*
* @author Dmitry Krasnov
* @since 1.1.0
*/
public abstract class ModbusSlave {
private static final Logger logger = LoggerFactory.getLogger(ModbusSlave.class);
/** name - slave name from cfg file, used for items binding */
protected String name = null;
protected ModbusSlaveEndpoint endpoint;
private static boolean writeMultipleRegisters = false;
public static void setWriteMultipleRegisters(boolean setwmr) {
writeMultipleRegisters = setwmr;
}
/**
* Type of data provided by the physical device
* "coil" and "discrete" use boolean (bit) values
* "input" and "holding" use byte values
*/
private String type;
private KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> connectionPool;
/** Modbus slave id */
private int id = 1;
/** starting reference and number of item to fetch from the device */
private int start = 0;
private int length = 0;
/**
* How to interpret Modbus register values.
* Examples:
* uint16 - one register - one unsigned integer value (default)
* int32 - every two registers will be interpreted as single 32-bit integer value
* bit - every register will be interpreted as 16 independent 1-bit values
*/
private String valueType = ModbusBindingProvider.VALUE_TYPE_UINT16;
/**
* A multiplier for the raw incoming data
*
* @note rawMultiplier can also be used for divisions, by simply
* setting the value smaller than zero.
*
* E.g.:
* - data/100 ... rawDataMultiplier=0.01
*/
private double rawDataMultiplier = 1.0;
private Object storage;
private Exception readError;
protected ModbusTransaction transaction = null;
/**
* Does the binding post updates even when the item did not change it's state?
*
* default is "false"
*/
private boolean updateUnchangedItems = false;
/**
* Does the binding post UNDEF update event when read fails
*
* default is "false"
*/
private boolean postUndefinedOnReadError = false;
/**
* @param slave slave name from cfg file used for item binding
* @connectionPool pool to create connections
*/
public ModbusSlave(String slave, KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> connectionPool) {
this.name = slave;
this.connectionPool = connectionPool;
}
/**
* writes data to Modbus device corresponding to OpenHAB command
* works only with types "coil" and "holding"
*
* @param command OpenHAB command received
* @param writeIndex
* @param readIndex read index to use for commands that require previously polled value
*
*/
public void executeCommand(String itemName, Command command, int writeIndex,
Optional<State> previouslyPolledState) {
try {
if (ModbusBindingProvider.TYPE_COIL.equals(getType())) {
setCoil(command, writeIndex);
} else if (ModbusBindingProvider.TYPE_HOLDING.equals(getType())) {
setRegister(itemName, command, writeIndex, previouslyPolledState);
}
} catch (Exception e) {
// Error already logged, just continue as normal
}
}
/**
* Calculates boolean value that will be written to the device as a result of OpenHAB command
* Used with item bound to "coil" type slaves
*
* @param command OpenHAB command received by the item. OnOffType, OpenClosedType and DecimalType are handled.
* @return new boolean value to be written to the device
*/
protected static boolean translateCommand2Boolean(Command command) {
if (command.equals(OnOffType.ON)) {
return true;
}
if (command.equals(OnOffType.OFF)) {
return false;
}
if (command.equals(OpenClosedType.OPEN)) {
return true;
}
if (command.equals(OpenClosedType.CLOSED)) {
return false;
}
if (command instanceof DecimalType) {
// Transformation might return DecimalType commands even for coil items
return !command.equals(DecimalType.ZERO);
}
throw new IllegalArgumentException("command not supported");
}
/**
* Performs physical write to device when slave type is "coil"
*
* @param command command received from OpenHAB
* @param writeIndex
* @throws ModbusConnectionException when connection cannot be established
* @throws ModbusException ModbusIOException on IO errors, ModbusSlaveException with protocol level exceptions
* @throws ModbusUnexpectedTransactionIdException when response transaction id does not match the request
*/
private void setCoil(Command command, int writeIndex)
throws ModbusConnectionException, ModbusException, ModbusUnexpectedTransactionIdException {
int coilOffset = writeIndex;
boolean b = translateCommand2Boolean(command);
doSetCoil(getStart() + coilOffset, b);
}
/**
* Performs physical write to device when slave type is "holding" using Modbus FC06 function
*
* @param command command received from OpenHAB
* @param config
* @throws ModbusConnectionException when connection cannot be established
* @throws ModbusException ModbusIOException on IO errors, ModbusSlaveException with protocol level exceptions
* @throws ModbusUnexpectedTransactionIdException when response transaction id does not match the request
*/
protected void setRegister(String itemName, Command command, int writeIndex, Optional<State> previouslyPolledState)
throws ModbusConnectionException, ModbusException, ModbusUnexpectedTransactionIdException {
int writeRegister = getStart() + writeIndex;
Register newValue;
if (command instanceof IncreaseDecreaseType || command instanceof UpDownType) {
if (!previouslyPolledState.isPresent()) {
logger.warn("Not polled value for item {}. Cannot process command {}", itemName, command);
return;
}
State prevState = previouslyPolledState.get();
if (!(prevState instanceof Number)) {
logger.warn("Previously polled value ({}) is not number, cannot process command {}",
previouslyPolledState, command);
return;
}
int prevValue = ((Number) prevState).intValue();
newValue = new SimpleRegister();
if (command.equals(IncreaseDecreaseType.INCREASE) || command.equals(UpDownType.UP)) {
newValue.setValue(prevValue + 1);
} else if (command.equals(IncreaseDecreaseType.DECREASE) || command.equals(UpDownType.DOWN)) {
newValue.setValue(prevValue - 1);
}
} else if (command instanceof DecimalType) {
newValue = new SimpleRegister();
newValue.setValue(((DecimalType) command).intValue());
} else if (command instanceof OnOffType || command instanceof OpenClosedType) {
newValue = new SimpleRegister();
if (command.equals(OnOffType.ON) || command.equals(OpenClosedType.OPEN)) {
newValue.setValue(1);
} else if (command.equals(OnOffType.OFF) || command.equals(OpenClosedType.CLOSED)) {
newValue.setValue(0);
}
} else {
logger.warn("Item {} received unsupported command: {}. Not setting register.", itemName, command);
return;
}
ModbusRequest request = null;
if (writeMultipleRegisters) {
Register[] regs = new Register[1];
regs[0] = newValue;
request = new WriteMultipleRegistersRequest(writeRegister, regs);
} else {
request = new WriteSingleRegisterRequest(writeRegister, newValue);
}
request.setUnitID(getId());
logger.debug("ModbusSlave ({}): FC{} ref={} value={}", name, request.getFunctionCode(), writeRegister,
newValue.getValue());
executeWriteRequest(request);
}
/**
* @return slave name from cfg file
*/
public String getName() {
return name;
}
/**
* Sends boolean (bit) data to the device using Modbus FC05 function
*
* @param writeRegister
* @param b
* @throws ModbusUnexpectedTransactionIdException
* @throws ModbusException
* @throws ModbusConnectionException
*/
public void doSetCoil(int writeRegister, boolean b)
throws ModbusConnectionException, ModbusException, ModbusUnexpectedTransactionIdException {
ModbusRequest request = new WriteCoilRequest(writeRegister, b);
request.setUnitID(getId());
logger.debug("ModbusSlave ({}): FC05 ref={} value={}", name, writeRegister, b);
executeWriteRequest(request);
}
/**
*
* @param request
* @throws ModbusConnectionException when connection cannot be established
* @throws ModbusException ModbusIOException on IO errors, ModbusSlaveException with protocol level exceptions
* @throws ModbusUnexpectedTransactionIdException when response transaction id does not match the request
*/
private void executeWriteRequest(ModbusRequest request)
throws ModbusConnectionException, ModbusException, ModbusUnexpectedTransactionIdException {
ModbusSlaveEndpoint endpoint = getEndpoint();
ModbusSlaveConnection connection = null;
ModbusResponse response;
int requestTransactionID;
try {
connection = getConnection(endpoint);
if (connection == null) {
logger.warn("ModbusSlave ({}): not connected -- aborting request {}", name, request);
throw new ModbusConnectionException(endpoint);
}
synchronized (transaction) {
transaction.setRequest(request);
try {
logger.trace(
"Executing modbus request {} using transaction {} (global transaction id before increment {}) to write data",
transaction.getRequest(), transaction, transaction.getTransactionID());
transaction.execute();
} catch (Exception e) {
// Note, one could catch ModbusIOException and ModbusSlaveException if more detailed
// exception handling is required. For now, all exceptions are handled the same way with writes.
logger.error("ModbusSlave ({}): error when executing write request ({}): {}", name, request,
e.getMessage());
invalidate(endpoint, connection);
// set connection to null such that it is not returned to pool
connection = null;
throw e;
}
response = transaction.getResponse();
logger.trace("ModbusSlave ({}): response for write (FC={}) {}", name, response.getFunctionCode(),
response.getHexMessage());
requestTransactionID = transaction.getRequest().getTransactionID();
if ((response.getTransactionID() != requestTransactionID) && !response.isHeadless()) {
logger.warn(
"ModbusSlave ({}): Transaction id of the response ({}) does not match request {} id {}. Endpoint {}. Connection: {}. Ignoring response.",
name, response.getTransactionID(), request, requestTransactionID, endpoint, connection);
throw new ModbusUnexpectedTransactionIdException();
}
}
} finally {
returnConnection(endpoint, connection);
}
}
protected ModbusSlaveConnection getConnection(ModbusSlaveEndpoint endpoint) {
ModbusSlaveConnection connection = borrowConnection(endpoint);
return connection;
}
private ModbusSlaveConnection borrowConnection(ModbusSlaveEndpoint endpoint) {
ModbusSlaveConnection connection = null;
long start = System.currentTimeMillis();
try {
connection = connectionPool.borrowObject(endpoint);
} catch (Exception e) {
invalidate(endpoint, connection);
logger.warn("ModbusSlave ({}): Error getting a new connection for endpoint {}. Error was: {}", name,
endpoint, e.getMessage());
}
logger.trace("ModbusSlave ({}): borrowing connection (got {}) for endpoint {} took {} ms", name, connection,
endpoint, System.currentTimeMillis() - start);
return connection;
}
private void invalidate(ModbusSlaveEndpoint endpoint, ModbusSlaveConnection connection) {
if (connection == null) {
return;
}
try {
connectionPool.invalidateObject(endpoint, connection);
} catch (Exception e) {
logger.warn("ModbusSlave ({}): Error invalidating connection in pool for endpoint {}. Error was: {}", name,
endpoint, e.getMessage());
}
}
private void returnConnection(ModbusSlaveEndpoint endpoint, ModbusSlaveConnection connection) {
if (connection == null) {
return;
}
try {
connectionPool.returnObject(endpoint, connection);
} catch (Exception e) {
logger.warn("ModbusSlave ({}): Error returning connection to pool for endpoint {}. Error was: {}", name,
endpoint, e.getMessage());
}
logger.trace("ModbusSlave ({}): returned connection for endpoint {}", name, endpoint);
}
/**
* Reads data from the connected device and updates items with the new data
*
* @param binding ModbusBindig that stores providers information
*/
public void update(ModbusBinding binding) {
try {
Object local = null;
Exception localReadError = null;
try {
if (ModbusBindingProvider.TYPE_COIL.equals(getType())) {
ModbusRequest request = new ReadCoilsRequest(getStart(), getLength());
if (this instanceof ModbusSerialSlave) {
request.setHeadless();
}
ReadCoilsResponse response = (ReadCoilsResponse) getModbusData(request);
local = response.getCoils();
} else if (ModbusBindingProvider.TYPE_DISCRETE.equals(getType())) {
ModbusRequest request = new ReadInputDiscretesRequest(getStart(), getLength());
ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) getModbusData(request);
local = response.getDiscretes();
} else if (ModbusBindingProvider.TYPE_HOLDING.equals(getType())) {
ModbusRequest request = new ReadMultipleRegistersRequest(getStart(), getLength());
ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) getModbusData(request);
local = response.getRegisters();
} else if (ModbusBindingProvider.TYPE_INPUT.equals(getType())) {
ModbusRequest request = new ReadInputRegistersRequest(getStart(), getLength());
ReadInputRegistersResponse response = (ReadInputRegistersResponse) getModbusData(request);
local = response.getRegisters();
}
} catch (ModbusException e) {
// Logging already done in getModbusData
localReadError = e;
} catch (ModbusConnectionException e) {
// Logging already done in getModbusData
localReadError = e;
} catch (ModbusUnexpectedTransactionIdException e) {
// Logging already done in getModbusData
localReadError = e;
}
if (storage == null) {
storage = local;
readError = localReadError;
} else {
synchronized (storage) {
storage = local;
readError = localReadError;
}
}
Collection<String> items = binding.getItemNames();
for (String item : items) {
updateItem(binding, item);
}
} catch (Exception e) {
logger.error("ModbusSlave ({}) error getting response from slave", name, e);
}
}
/**
* Updates OpenHAB item with data read from slave device
* works only for type "coil" and "holding"
*
* @param binding ModbusBinding
* @param item item to update
*/
private void updateItem(ModbusBinding binding, String item) {
if (readError == null) {
if (ModbusBindingProvider.TYPE_COIL.equals(getType())
|| ModbusBindingProvider.TYPE_DISCRETE.equals(getType())) {
binding.internalUpdateItem(name, (BitVector) storage, item);
} else if (ModbusBindingProvider.TYPE_HOLDING.equals(getType())
|| ModbusBindingProvider.TYPE_INPUT.equals(getType())) {
binding.internalUpdateItem(name, (InputRegister[]) storage, item);
}
} else {
binding.internalUpdateReadErrorItem(name, readError, item);
}
}
public boolean isUpdateUnchangedItems() {
return updateUnchangedItems;
}
public void setUpdateUnchangedItems(boolean updateUnchangedItems) {
this.updateUnchangedItems = updateUnchangedItems;
}
/**
* Executes Modbus transaction that reads data from the device and returns response data
*
* @param request describes what data are requested from the device
* @return response data
* @throws ModbusConnectionException when connection cannot be established
* @throws ModbusException ModbusIOException on IO errors, ModbusSlaveException with protocol level exceptions
* @throws ModbusUnexpectedTransactionIdException when response transaction id does not match the request
*/
private ModbusResponse getModbusData(ModbusRequest request)
throws ModbusConnectionException, ModbusException, ModbusUnexpectedTransactionIdException {
ModbusSlaveEndpoint endpoint = getEndpoint();
ModbusSlaveConnection connection = null;
ModbusResponse response = null;
int requestTransactionID;
try {
connection = getConnection(endpoint);
if (connection == null) {
logger.warn("ModbusSlave ({}) not connected -- aborting read request {}. Endpoint {}", name, request,
endpoint);
throw new ModbusConnectionException(endpoint);
}
request.setUnitID(getId());
synchronized (transaction) {
transaction.setRequest(request);
try {
logger.trace(
"Executing modbus request {} using transaction {} (global transaction id before increment {}) to read data",
transaction.getRequest(), transaction, transaction.getTransactionID());
transaction.execute();
} catch (ModbusException e) {
logger.error(
"ModbusSlave ({}): Error getting modbus data for request {}. Error: {}. Endpoint {}. Connection: {}",
name, request, e.getMessage(), endpoint, connection);
invalidate(endpoint, connection);
// Invalidated connections should not be returned
connection = null;
throw e;
}
requestTransactionID = transaction.getRequest().getTransactionID();
response = transaction.getResponse();
if ((response.getTransactionID() != requestTransactionID) && !response.isHeadless()) {
logger.warn(
"ModbusSlave ({}): Transaction id of the response ({}) does not match request {} id {}. Endpoint {}. Connection: {}. Ignoring response.",
name, response.getTransactionID(), request, requestTransactionID, endpoint, connection);
throw new ModbusUnexpectedTransactionIdException();
}
}
logger.trace("ModbusSlave ({}): response for read (FC={}) {}", name, response.getFunctionCode(),
response.getHexMessage());
} finally {
returnConnection(endpoint, connection);
}
return response;
}
public ModbusSlaveEndpoint getEndpoint() {
return endpoint;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getValueType() {
return valueType;
}
public void setValueType(String valueType) {
this.valueType = valueType;
}
public void setRawDataMultiplier(double value) {
this.rawDataMultiplier = value;
}
public double getRawDataMultiplier() {
return rawDataMultiplier;
}
public long getRetryDelayMillis() {
if (transaction == null) {
throw new IllegalStateException("transaction not initialized!");
}
return transaction.getRetryDelayMillis();
}
public void setRetryDelayMillis(long retryDelayMillis) {
if (transaction == null) {
throw new IllegalStateException("transaction not initialized!");
}
transaction.setRetryDelayMillis(retryDelayMillis);
}
public boolean isPostUndefinedOnReadError() {
return postUndefinedOnReadError;
}
public void setPostUndefinedOnReadError(boolean postUndefinedOnReadError) {
this.postUndefinedOnReadError = postUndefinedOnReadError;
}
}