/** * 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.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collection; import java.util.Comparator; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.collections.IteratorUtils; import org.apache.commons.collections.ResettableIterator; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.pool2.KeyedObjectPool; import org.apache.commons.pool2.SwallowedExceptionListener; import org.apache.commons.pool2.impl.GenericKeyedObjectPool; import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; import org.openhab.binding.modbus.ModbusBindingProvider; import org.openhab.binding.modbus.internal.pooling.EndpointPoolConfiguration; import org.openhab.binding.modbus.internal.pooling.ModbusSlaveConnectionFactoryImpl; import org.openhab.binding.modbus.internal.pooling.ModbusSlaveEndpoint; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.binding.BindingProvider; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.types.DecimalType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.wimpi.modbus.Modbus; import net.wimpi.modbus.net.ModbusSlaveConnection; import net.wimpi.modbus.procimg.InputRegister; import net.wimpi.modbus.util.BitVector; import net.wimpi.modbus.util.SerialParameters; /** * Modbus binding allows to connect to multiple Modbus slaves as TCP master. * * @author Dmitry Krasnov * @since 1.1.0 */ public class ModbusBinding extends AbstractActiveBinding<ModbusBindingProvider> implements ManagedService { private static final long DEFAULT_POLL_INTERVAL = 200; /** * Time to wait between connection passive+borrow, i.e. time to wait between * transactions * Default 60ms for TCP slaves, Siemens S7 1212 PLC couldn't handle faster * requests with default settings. */ private static final long DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS = 60; /** * Time to wait between connection passive+borrow, i.e. time to wait between * transactions * Default 35ms for Serial slaves, motivation discussed * here https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/111?u=ssalonen */ private static final long DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS = 35; private static final Logger logger = LoggerFactory.getLogger(ModbusBinding.class); private static final String UDP_PREFIX = "udp"; private static final String TCP_PREFIX = "tcp"; private static final String SERIAL_PREFIX = "serial"; private static final String VALID_CONFIG_KEYS = "connection|id|start|length|type|valuetype|rawdatamultiplier|writemultipleregisters|updateunchangeditems|postundefinedonreaderror"; private static final Pattern EXTRACT_MODBUS_CONFIG_PATTERN = Pattern.compile( "^(" + TCP_PREFIX + "|" + UDP_PREFIX + "|" + SERIAL_PREFIX + "|)\\.(.*?)\\.(" + VALID_CONFIG_KEYS + ")$"); /** Stores instances of all the slaves defined in cfg file */ private static Map<String, ModbusSlave> modbusSlaves = new ConcurrentHashMap<>(); private static GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig(); static { // 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. poolConfig.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 poolConfig.setBlockWhenExhausted(true); poolConfig.setMaxTotalPerKey(1); // block infinitely when exhausted poolConfig.setMaxWaitMillis(-1); // make sure we return connected connections from/to connection pool poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(true); // disable JMX poolConfig.setJmxEnabled(false); } /** * We use connection pool to ensure that only single transaction is ongoing per each endpoint. This is especially * important with serial slaves but practice has shown that even many tcp slaves have limited * capability to handle many connections at the same time * * Relevant discussion at the time of implementation: * - https://community.openhab.org/t/modbus-connection-problem/6108/ * - https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/ */ private static KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> connectionPool; private static ModbusSlaveConnectionFactoryImpl connectionFactory; private static void reconstructConnectionPool() { connectionFactory = new ModbusSlaveConnectionFactoryImpl(); GenericKeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> genericKeyedObjectPool = new GenericKeyedObjectPool<>( connectionFactory, poolConfig); genericKeyedObjectPool.setSwallowedExceptionListener(new SwallowedExceptionListener() { @Override public void onSwallowException(Exception e) { logger.error("Connection pool swallowed unexpected exception: {}", e.getMessage()); } }); connectionPool = genericKeyedObjectPool; } /** * For testing */ static KeyedObjectPool<ModbusSlaveEndpoint, ModbusSlaveConnection> getReconstructedConnectionPoolForTesting() { reconstructConnectionPool(); return connectionPool; } /** slaves update interval in milliseconds */ public static long pollInterval = DEFAULT_POLL_INTERVAL; @Override public void activate() { } @Override public void deactivate() { clearAndClose(); } @Override protected long getRefreshInterval() { return pollInterval; } @Override protected String getName() { return "Modbus Polling Service"; } /** * Parses configuration creating Modbus slave instances defined in cfg file * {@inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { for (ModbusBindingProvider provider : providers) { if (!provider.providesBindingFor(itemName)) { continue; } logger.trace("Received command '{}' for item '{}'", command, itemName); ModbusBindingConfig config = provider.getConfig(itemName); List<ItemIOConnection> writeConnections = config.getWriteConnectionsByCommand(command); Comparator<ItemIOConnection> byLastPolledTime = (ItemIOConnection a, ItemIOConnection b) -> Long .compareUnsigned(a.getPollNumber(), b.getPollNumber()); // find out the most recently polled state (from any slave as long as it is bound to this item!) for this // item (if available) Optional<State> previouslyPolledState = config.getReadConnections().stream().max(byLastPolledTime) .map(connection -> connection.getPreviouslyPolledState()); for (ItemIOConnection writeConnection : writeConnections) { ModbusSlave slave = modbusSlaves.get(writeConnection.getSlaveName()); if (writeConnection.supportsCommand(command)) { Transformation transformation = writeConnection.getTransformation(); Command transformedCommand = transformation == null ? command : transformation.transformCommand(config.getItemAcceptedCommandTypes(), command); logger.trace( "Executing command '{}' (transformed from '{}' using transformation {}) using item '{}' IO connection {} (writeIndex={}, previouslyPolledState={})", transformedCommand, command, transformation, itemName, writeConnection, previouslyPolledState); slave.executeCommand(itemName, transformedCommand, writeConnection.getIndex(), previouslyPolledState); } else { logger.trace( "Command '{}' using item '{}' IO connection {} not triggered/supported by the IO connection", command, itemName, writeConnection); } } } } /** * Posts update event to OpenHAB bus for "holding" and "input register" type slaves * * @param binding ModbusBinding to get item configuration from BindingProviding * @param registers data received from slave device in the last pollInterval * @param itemName item to update */ protected void internalUpdateItem(String slaveName, InputRegister[] registers, String itemName) { for (ModbusBindingProvider provider : providers) { if (!provider.providesBindingFor(itemName)) { continue; } ModbusBindingConfig config = provider.getConfig(itemName); List<ItemIOConnection> connections = config.getReadConnectionsBySlaveName(slaveName); for (ItemIOConnection connection : connections) { ModbusSlave slave = modbusSlaves.get(slaveName); String slaveValueType = slave.getValueType(); double rawDataMultiplier = slave.getRawDataMultiplier(); String valueType = connection.getEffectiveValueType(slaveValueType); /* receive data manipulation */ State newState = extractStateFromRegisters(registers, connection.getIndex(), valueType); // Convert newState (DecimalType) to on/off kind of state if we have "boolean item" (Switch, Contact // etc). In other cases (such as Number items) newStateBoolean will be UNDEF State newStateBoolean = provider.getConfig(itemName).translateBoolean2State( connection.getPreviouslyPolledState(), !newState.equals(DecimalType.ZERO)); // If we have boolean item (newStateBoolean is not UNDEF) if (!UnDefType.UNDEF.equals(newStateBoolean)) { newState = newStateBoolean; } else if ((rawDataMultiplier != 1) && (config.getItemClass().isAssignableFrom(NumberItem.class))) { double tmpValue = ((DecimalType) newState).doubleValue() * rawDataMultiplier; newState = new DecimalType(String.valueOf(tmpValue)); } boolean stateChanged = !newState.equals(connection.getPreviouslyPolledState()); if (connection.supportsState(newState, stateChanged, slave.isUpdateUnchangedItems())) { logger.trace( "internalUpdateItem(Register): Updating slave {} item {}, state {} (changed={}) matched ItemIOConnection {}.", slaveName, itemName, newState, stateChanged, connection); Transformation transformation = connection.getTransformation(); State transformedState = transformation == null ? newState : transformation.transformState(config.getItemAcceptedDataTypes(), newState); eventPublisher.postUpdate(itemName, transformedState); connection.setPreviouslyPolledState(newState); } else { logger.trace( "internalUpdateItem(Register): Not updating slave {} item {} since state {} (changed={}) not supported by ItemIOConnection {}.", slaveName, itemName, newState, stateChanged, connection); } } } } /** * Posts update event to OpenHAB bus for all types of slaves when there is a read error * * @param binding ModbusBinding to get item configuration from BindingProviding * @param error * @param itemName item to update */ protected void internalUpdateReadErrorItem(String slaveName, Exception error, String itemName) { ModbusSlave slave = modbusSlaves.get(slaveName); if (!slave.isPostUndefinedOnReadError()) { return; } State newState = UnDefType.UNDEF; for (ModbusBindingProvider provider : providers) { if (!provider.providesBindingFor(itemName)) { continue; } ModbusBindingConfig config = provider.getConfig(itemName); List<ItemIOConnection> connections = config.getReadConnectionsBySlaveName(slaveName); for (ItemIOConnection connection : connections) { boolean stateChanged = !newState.equals(connection.getPreviouslyPolledState()); if (connection.supportsState(newState, stateChanged, slave.isUpdateUnchangedItems())) { logger.trace( "internalUpdateReadErrorItem: Updating slave {} item {}, state {} (changed={}) matched ItemIOConnection {}.", slaveName, itemName, newState, stateChanged, connection); // Note: no transformation with errors, always emit the UNDEFINED eventPublisher.postUpdate(itemName, newState); connection.setPreviouslyPolledState(newState); } else { logger.trace( "internalUpdateReadErrorItem: Not updating slave {} item {} since state {} (changed={}) not supported by ItemIOConnection {}.", slaveName, itemName, newState, stateChanged, connection); } } } } /** * 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 values are interpreted as unsigned integers * 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 values are interpreted as unsigned integers * INT32: * - registers (2 * index) and ( 2 *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 * UINT32: * - same as UINT32 except values are interpreted as unsigned integers * FLOAT32: * - registers (2 * index) and ( 2 *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 * * @param registers * list of registers, each register represent 16bit of data * @param index * zero based item index. Interpretation of this depends on type * @param type * item type, e.g. unsigned 16bit integer (<tt>ModbusBindingProvider.VALUE_TYPE_UINT16</tt>) * @return number representation queried value * @throws IllegalArgumentException when <tt>type</tt> does not match a known type * @throws IndexOutOfBoundsException when <tt>index</tt> is out of bounds of registers * */ private DecimalType extractStateFromRegisters(InputRegister[] registers, int index, String type) { if (type.equals(ModbusBindingProvider.VALUE_TYPE_BIT)) { return new DecimalType((registers[index / 16].toUnsignedShort() >> (index % 16)) & 1); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT8)) { return new DecimalType(registers[index / 2].toBytes()[1 - (index % 2)]); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT8)) { return new DecimalType((registers[index / 2].toUnsignedShort() >> (8 * (index % 2))) & 0xff); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT16)) { ByteBuffer buff = ByteBuffer.allocate(2); buff.put(registers[index].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getShort(0)); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT16)) { return new DecimalType(registers[index].toUnsignedShort()); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT32)) { ByteBuffer buff = ByteBuffer.allocate(4); buff.put(registers[index * 2 + 0].toBytes()); buff.put(registers[index * 2 + 1].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getInt(0)); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT32)) { ByteBuffer buff = ByteBuffer.allocate(8); buff.position(4); buff.put(registers[index * 2 + 0].toBytes()); buff.put(registers[index * 2 + 1].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0)); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_FLOAT32)) { ByteBuffer buff = ByteBuffer.allocate(4); buff.put(registers[index * 2 + 0].toBytes()); buff.put(registers[index * 2 + 1].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getFloat(0)); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_INT32_SWAP)) { ByteBuffer buff = ByteBuffer.allocate(4); buff.put(registers[index * 2 + 1].toBytes()); buff.put(registers[index * 2 + 0].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getInt(0)); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_UINT32_SWAP)) { ByteBuffer buff = ByteBuffer.allocate(8); buff.position(4); buff.put(registers[index * 2 + 1].toBytes()); buff.put(registers[index * 2 + 0].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getLong(0)); } else if (type.equals(ModbusBindingProvider.VALUE_TYPE_FLOAT32_SWAP)) { ByteBuffer buff = ByteBuffer.allocate(4); buff.put(registers[index * 2 + 1].toBytes()); buff.put(registers[index * 2 + 0].toBytes()); return new DecimalType(buff.order(ByteOrder.BIG_ENDIAN).getFloat(0)); } else { throw new IllegalArgumentException(); } } /** * Posts update event to OpenHAB bus for "coil" and "discrete input" type slaves * * @param binding ModbusBinding to get item configuration from BindingProviding * @param registers data received from slave device in the last pollInterval * @param item item to update */ protected void internalUpdateItem(String slaveName, BitVector coils, String itemName) { for (ModbusBindingProvider provider : providers) { if (!provider.providesBindingFor(itemName)) { continue; } ModbusBindingConfig config = provider.getConfig(itemName); List<ItemIOConnection> connections = config.getReadConnectionsBySlaveName(slaveName); for (ItemIOConnection connection : connections) { ModbusSlave slave = modbusSlaves.get(slaveName); if (connection.getIndex() >= slave.getLength()) { logger.warn( "Item '{}' read index '{}' is out-of-bounds. Slave '{}' has been configured " + "to read only '{}' bits. Check your configuration!", itemName, connection.getIndex(), slaveName, slave.getLength()); continue; } boolean state = coils.getBit(connection.getIndex()); State newState = config.translateBoolean2State(connection.getPreviouslyPolledState(), state); // For types not taking in OpenClosedType or OnOffType (e.g. Number items) // We fall back to DecimalType if (newState.equals(UnDefType.UNDEF)) { newState = state ? new DecimalType(BigDecimal.ONE) : DecimalType.ZERO; } boolean stateChanged = !newState.equals(connection.getPreviouslyPolledState()); if (connection.supportsState(newState, stateChanged, slave.isUpdateUnchangedItems())) { Transformation transformation = connection.getTransformation(); State transformedState = transformation == null ? newState : transformation.transformState(config.getItemAcceptedDataTypes(), newState); logger.trace( "internalUpdateItem(BitVector): Updating slave {} item {}, state {} (changed={}) matched ItemIOConnection {}.", slaveName, itemName, newState, stateChanged, connection); eventPublisher.postUpdate(itemName, transformedState); connection.setPreviouslyPolledState(newState); } else { logger.trace( "internalUpdateItem(BitVector): Not updating slave {} item {} since state {} (changed={}) not supported by ItemIOConnection {}.", slaveName, itemName, newState, stateChanged, connection); } } } } /** * Returns names of all the items, registered with this binding * * @return list of item names */ public Collection<String> getItemNames() { Collection<String> items = null; for (BindingProvider provider : providers) { if (items == null) { items = provider.getItemNames(); } else { items.addAll(provider.getItemNames()); } } return items; } /** * updates all slaves from the modbusSlaves */ @Override protected void execute() { Collection<ModbusSlave> slaves = new HashSet<>(); synchronized (slaves) { slaves.addAll(modbusSlaves.values()); } for (ModbusSlave slave : slaves) { slave.update(this); } } /** * Clear all configuration and close all connections */ private void clearAndClose() { try { // Closes all connections by calling destroyObject method in the ObjectFactory implementation if (connectionPool != null) { connectionPool.close(); } } catch (Exception e) { // Should not happen logger.error("Error clearing connections", e); } modbusSlaves.clear(); } @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { try { // remove all known items if configuration changed clearAndClose(); reconstructConnectionPool(); if (config == null) { logger.debug("Got null config!"); return; } Enumeration<String> keys = config.keys(); Map<String, EndpointPoolConfiguration> slavePoolConfigs = new HashMap<>(); Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs = new HashMap<>(); while (keys.hasMoreElements()) { final String key = keys.nextElement(); final String value = (String) config.get(key); try { // the config-key enumeration contains additional keys that we // don't want to process here ... if ("service.pid".equals(key)) { continue; } Matcher matcher = EXTRACT_MODBUS_CONFIG_PATTERN.matcher(key); if (!matcher.matches()) { if ("poll".equals(key)) { if (StringUtils.isNotBlank((String) config.get(key))) { pollInterval = Integer.valueOf((String) config.get(key)); } } else if ("writemultipleregisters".equals(key)) { // XXX: ugly to touch base class but kept here for backwards compat // FIXME: should this be deprecated as introduced as slave specific parameter? ModbusSlave.setWriteMultipleRegisters(Boolean.valueOf(config.get(key).toString())); } else { logger.debug( "given modbus-slave-config-key '{}' does not follow the expected pattern or 'serial.<slaveId>.<{}>'", key, VALID_CONFIG_KEYS); } continue; } matcher.reset(); matcher.find(); String slave = matcher.group(2); ModbusSlave modbusSlave = modbusSlaves.get(slave); EndpointPoolConfiguration endpointPoolConfig = slavePoolConfigs.get(slave); if (modbusSlave == null) { if (matcher.group(1).equals(TCP_PREFIX)) { modbusSlave = new ModbusTcpSlave(slave, connectionPool); } else if (matcher.group(1).equals(UDP_PREFIX)) { modbusSlave = new ModbusUdpSlave(slave, connectionPool); } else if (matcher.group(1).equals(SERIAL_PREFIX)) { modbusSlave = new ModbusSerialSlave(slave, connectionPool); } else { throw new ConfigurationException(slave, "the given slave type '" + slave + "' is unknown"); } endpointPoolConfig = new EndpointPoolConfiguration(); // Do not give up if the connection attempt fails on the first time... endpointPoolConfig.setConnectMaxTries(Modbus.DEFAULT_RETRIES); logger.debug("modbusSlave '{}' instanciated", slave); modbusSlaves.put(slave, modbusSlave); } String configKey = matcher.group(3); if ("connection".equals(configKey)) { String[] chunks = value.split(":"); Iterator<String> settingIterator = stringArrayIterator(chunks); if (modbusSlave instanceof ModbusIPSlave) { ((ModbusIPSlave) modbusSlave).setHost(settingIterator.next()); // // Defaults for endpoint and slave // modbusSlave.setRetryDelayMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS); endpointPoolConfig.setPassivateBorrowMinMillis(DEFAULT_TCP_INTER_TRANSACTION_DELAY_MILLIS); // // Optional parameters // try { ((ModbusIPSlave) modbusSlave).setPort(Integer.valueOf(settingIterator.next())); long passivateBorrowMinMillis = Long.parseLong(settingIterator.next()); modbusSlave.setRetryDelayMillis(passivateBorrowMinMillis); endpointPoolConfig.setPassivateBorrowMinMillis(passivateBorrowMinMillis); endpointPoolConfig.setReconnectAfterMillis(Integer.parseInt(settingIterator.next())); // time to wait before trying connect closed connection. Note that // ModbusSlaveConnectionFactoryImpl makes sure that max{passivateBorrowMinMillis, this // parameter} is waited between connection attempts endpointPoolConfig.setInterConnectDelayMillis(Long.parseLong(settingIterator.next())); endpointPoolConfig.setConnectMaxTries(Integer.parseInt(settingIterator.next())); endpointPoolConfig.setConnectTimeoutMillis(Integer.parseInt(settingIterator.next())); } catch (NoSuchElementException e) { // Some of the optional parameters are missing -- it's ok! } if (settingIterator.hasNext()) { String errMsg = String .format("%s Has too many colon (:) separated connection settings for a tcp/udp modbus slave. " + "Expecting at most 6 parameters: hostname (mandatory) and " + "optionally (in this order) port number, " + "interTransactionDelayMillis, reconnectAfterMillis," + "interConnectDelayMillis, connectMaxTries, connectTimeout.", key); throw new ConfigurationException(key, errMsg); } } else if (modbusSlave instanceof ModbusSerialSlave) { SerialParameters serialParameters = new SerialParameters(); serialParameters.setPortName(settingIterator.next()); // // Defaults for endpoint and slave // endpointPoolConfig.setReconnectAfterMillis(-1); // never "disconnect" (close/open serial // port) // serial connection between borrows modbusSlave.setRetryDelayMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS); endpointPoolConfig .setPassivateBorrowMinMillis(DEFAULT_SERIAL_INTER_TRANSACTION_DELAY_MILLIS); // // Optional parameters // try { serialParameters.setBaudRate(settingIterator.next()); serialParameters.setDatabits(settingIterator.next()); serialParameters.setParity(settingIterator.next()); serialParameters.setStopbits(settingIterator.next()); serialParameters.setEncoding(settingIterator.next()); // time to wait between connection passive+borrow, i.e. time to wait between // transactions long passivateBorrowMinMillis = Long.parseLong(settingIterator.next()); modbusSlave.setRetryDelayMillis(passivateBorrowMinMillis); endpointPoolConfig.setPassivateBorrowMinMillis(passivateBorrowMinMillis); serialParameters.setReceiveTimeoutMillis(settingIterator.next()); serialParameters.setFlowControlIn(settingIterator.next()); serialParameters.setFlowControlOut(settingIterator.next()); } catch (NoSuchElementException e) { // Some of the optional parameters are missing -- it's ok! } if (settingIterator.hasNext()) { String errMsg = String.format( "%s Has too many colon (:) separated connection settings for a serial modbus slave. " + "Expecting at most 9 parameters (got %d): devicePort (mandatory), " + "and 0 or more optional parameters (in this order): " + "baudRate, dataBits, parity, stopBits, " + "encoding, interTransactionWaitMillis, " + "receiveTimeoutMillis, flowControlIn, flowControlOut", key, chunks.length); throw new ConfigurationException(key, errMsg); } ((ModbusSerialSlave) modbusSlave).setSerialParameters(serialParameters); } } else if ("start".equals(configKey)) { modbusSlave.setStart(Integer.valueOf(value)); } else if ("length".equals(configKey)) { modbusSlave.setLength(Integer.valueOf(value)); } else if ("id".equals(configKey)) { modbusSlave.setId(Integer.valueOf(value)); } else if ("type".equals(configKey)) { if (ArrayUtils.contains(ModbusBindingProvider.SLAVE_DATA_TYPES, value)) { modbusSlave.setType(value); } else { throw new ConfigurationException(configKey, "the given slave type '" + value + "' is invalid"); } } else if ("valuetype".equals(configKey)) { if (ArrayUtils.contains(ModbusBindingProvider.VALUE_TYPES, value)) { modbusSlave.setValueType(value); } else { throw new ConfigurationException(configKey, "the given value type '" + value + "' is invalid"); } } else if ("rawdatamultiplier".equals(configKey)) { modbusSlave.setRawDataMultiplier(Double.valueOf(value.toString())); } else if ("updateunchangeditems".equals(configKey)) { modbusSlave.setUpdateUnchangedItems(Boolean.valueOf(value.toString())); } else if ("postundefinedonreaderror".equals(configKey)) { modbusSlave.setPostUndefinedOnReadError(Boolean.valueOf(value.toString())); } else { throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown"); } modbusSlaves.put(slave, modbusSlave); slavePoolConfigs.put(slave, endpointPoolConfig); } catch (Exception e) { String errMsg = String.format("Exception when parsing configuration parameter %s = %s -- %s %s", key, value, e.getClass().getName(), e.getMessage()); logger.error(errMsg); throw new ConfigurationException(key, errMsg); } } // Finally, go through each slave definition, and combine the slave pool configurations for (Entry<String, EndpointPoolConfiguration> slaveEntry : slavePoolConfigs.entrySet()) { String slave = slaveEntry.getKey(); EndpointPoolConfiguration poolConfiguration = slaveEntry.getValue(); ModbusSlaveEndpoint endpoint = modbusSlaves.get(slave).getEndpoint(); EndpointPoolConfiguration existingPoolConfiguration = endpointPoolConfigs.get(endpoint); // Do we have two slaves with same endpoint, but different pool configuration parameters? Warn if we do. if (existingPoolConfiguration != null && !existingPoolConfiguration.equals(poolConfiguration)) { logger.warn( "Slave {} (endpoint {}) has different retry/connection delay " + "(EndpointPoolConfiguration) etc. settings. Replacing {} with {}", slave, endpoint, existingPoolConfiguration, poolConfiguration); } endpointPoolConfigs.put(endpoint, poolConfiguration); } connectionFactory.applyEndpointPoolConfigs(endpointPoolConfigs); logger.debug("Parsed the following slave->endpoint configurations: {}. If the endpoint is same, " + "connections are shared between the instances.", slavePoolConfigs); logger.debug("Parsed the following pool configurations: {}", endpointPoolConfigs); logger.debug("config looked good"); setProperlyConfigured(true); } catch (ConfigurationException ce) { setProperlyConfigured(false); throw ce; } } private static Iterator<String> stringArrayIterator(final String[] chunks) { Iterator<String> settingIterator = new Iterator<String>() { private ResettableIterator inner = IteratorUtils.arrayIterator(chunks); @Override public String next() { return (String) inner.next(); } @Override public boolean hasNext() { return inner.hasNext(); } @Override public void remove() { inner.remove(); } }; return settingIterator; } }