package tcg.scada.da.modbus;
import java.util.ArrayList;
import java.util.Calendar;
import org.apache.log4j.NDC;
import tcg.common.LoggerManager;
import tcg.common.util.BufferParser;
import tcg.common.util.DetachedThread;
import tcg.scada.cos.CosDpQualityEnum;
import tcg.scada.cos.CosDpValueStruct;
import tcg.scada.cos.CosDpValueUnion;
import tcg.scada.da.DataPoint;
import tcg.scada.da.ESourceDataType;
import tcg.scada.da.ESubsystemType;
import tcg.scada.da.IDataPoint;
import tcg.scada.da.Subsystem;
import tcg.scada.da.SubsystemConfig;
import tcg.scada.modbus.EModbusFunctionCode;
/**
* Implementation of Modbus Subsystem. It contains poller to poll for data and
* status from Modbus subsystem.
*
* <p>
* Note: It expects the first words (address = 0) to be status word for the
* subystem. The first bit can be used to define which host (among a pair of
* host) is the preferred host (1 = preferred, 0 = normal). The subsequent bits
* can be used to pass the state/quality of a managed subsystem under this
* modbus subsystem (1 = managed subsystem is OK, 0 = managed subsystem is NOT
* OK). When the state of a managed subsystem is NOT_OK, all datapoints in the
* managed subsystem will be set to QualityBad.
* </p>
*
* <p><b>
* Note: Subsystem status word is the first word inside input registers.
* </b></p>
*
* @author Yoga
*
*/
public class ModbusSubsystem extends Subsystem
{
// public static int OUTPUT_COIL = 1;
// public static int INPUT_CONTACT = 2;
// public static int HOLDING_REGISTER = 3;
// public static int INPUT_REGISTER = 4;
// private int primaryStatus_ = 0;
// private int standbyStatus_ = 0;
// private SubsystemConfig config_ = new SubsystemConfig();
/**
* Status polling segment
*/
private PollingSegment statusSegment_ = new PollingSegment();
/**
* List of polling segment
*/
private ArrayList<PollingSegment> segments_ = new ArrayList<PollingSegment>();
/**
* Primary modbus client, connected to primary host
*/
private ModbusClient primaryHost_ = new ModbusClient();
/**
* Standby modbus client, connected to standby host
*/
private ModbusClient standbyHost_ = new ModbusClient();
/**
* Active modbus client, ie. currently connected modbus client.
*/
private ModbusClient activeHost_ = null;
// control command (boolean). default: write-multiple-coils
private int boolCommand_ = EModbusFunctionCode.FORCE_MULTIPLE_COILS;
// control command (integer). default: write-multiple-registers
private int intCommand_ = EModbusFunctionCode.PRESET_MULTIPLE_REGISTERS;
// polling thread
private PollingThread thread_ = new PollingThread(this);
/**
* Default ctor
*/
public ModbusSubsystem()
{
// override the logger
logger = LoggerManager.getLogger(ModbusSubsystem.class.toString());
// default to bad quality
quality = CosDpQualityEnum.QualityBad;
}
/**
* Alternative ctor
*
* @param config
* - subsystem configuration
*/
public ModbusSubsystem(SubsystemConfig config)
{
// configure the modbus client to primary host
primaryHost_.setHost(config.primaryHost.deviceId,
config.primaryHost.hostname, config.primaryHost.startPort,
config.primaryHost.portRange);
primaryHost_.setTimeOut(config.timeout.sockTimeoutMillis,
config.timeout.connTimeoutMillis,
config.timeout.idleTimeoutMillis);
// configure the modbus client to standby host
standbyHost_.setHost(config.standbyHost.deviceId,
config.standbyHost.hostname, config.standbyHost.startPort,
config.standbyHost.portRange);
standbyHost_.setTimeOut(config.timeout.sockTimeoutMillis,
config.timeout.connTimeoutMillis,
config.timeout.idleTimeoutMillis);
// TODO: retrieve the polling segments
// TODO: implement properly. this might include retrieving the list of
// polling segments
// override the logger
logger = LoggerManager.getLogger(ModbusSubsystem.class.toString());
// default to bad quality
quality = CosDpQualityEnum.QualityBad;
}
@Override
public ESubsystemType getType()
{
return ESubsystemType.SUBSYSTEM_MODBUS;
}
@Override
public void setPrimaryHost(int inDeviceId, String inHost, int inPort,
short inPortRange)
{
primaryHost_.setHost((short) inDeviceId, inHost, inPort, inPortRange);
}
@Override
public int getPrimaryHostDeviceId()
{
return primaryHost_.getDeviceId();
}
@Override
public String getPrimaryHostName()
{
return primaryHost_.getHostName();
}
@Override
public int getPrimaryHostPort()
{
return primaryHost_.getPort();
}
@Override
public void setStandbyHost(int inDeviceId, String inHost, int inPort,
short inPortRange)
{
standbyHost_.setHost((short) inDeviceId, inHost, inPort, inPortRange);
}
@Override
public int getStandbyHostDeviceId()
{
return standbyHost_.getDeviceId();
}
@Override
public String getStandbyHostName()
{
return standbyHost_.getHostName();
}
@Override
public int getStandbyHostPort()
{
return standbyHost_.getPort();
}
@Override
public void setTimeOut(int inSocketTimeOutMillis,
int inConnectTimeOutMillis, int inIdleTimeOutMillis)
{
// set the modbus client's timeout setting
primaryHost_.setTimeOut(inSocketTimeOutMillis, inConnectTimeOutMillis,
inIdleTimeOutMillis);
standbyHost_.setTimeOut(inSocketTimeOutMillis, inConnectTimeOutMillis,
inIdleTimeOutMillis);
}
@Override
public int getSocketTimeOut()
{
return primaryHost_.getSocketTimeOut();
}
@Override
public int getConnectTimeOut()
{
return primaryHost_.getConnectTimeOut();
}
@Override
public int getIdleTimeOut()
{
return primaryHost_.getIdleTimeOut();
}
@Override
public void setControlCommand(int inBoolCommand, int inIntCommand)
{
// set boolean control command
if (inBoolCommand == EModbusFunctionCode.FORCE_SINGLE_COIL)
{
inBoolCommand = EModbusFunctionCode.FORCE_SINGLE_COIL;
}
else
{
inBoolCommand = EModbusFunctionCode.FORCE_MULTIPLE_COILS;
}
// set integer control command
if (inIntCommand == EModbusFunctionCode.PRESET_SINGLE_REGISTER)
{
inIntCommand = EModbusFunctionCode.PRESET_SINGLE_REGISTER;
}
else
{
inIntCommand = EModbusFunctionCode.PRESET_MULTIPLE_REGISTERS;
}
return;
}
@Override
public boolean setControl(IDataPoint inDataPoint, boolean inValue)
{
boolean status = false;
int retval = 0;
// validation : datapoint
if (inDataPoint == null)
{
return false;
}
// logging context
NDC.push(inDataPoint.getName());
// check if we use set-multiple-coils
boolean[] states = null;
if (boolCommand_ != EModbusFunctionCode.FORCE_SINGLE_COIL)
{
states = new boolean[1];
states[0] = inValue;
}
// send using active host first if it is available
if (activeHost_ != null)
{
logger.info("Sending boolean control to active host. Address: "
+ inDataPoint.getAddress() + ". Value: " + inValue);
try
{
if (boolCommand_ == EModbusFunctionCode.FORCE_SINGLE_COIL)
{
retval = activeHost_.writeOutputCoil(inDataPoint
.getAddress(), inValue);
if (retval == 1)
{
// successful
status = true;
}
}
else
{
retval = activeHost_.writeOutputCoils(inDataPoint
.getAddress(), 1, states);
if (retval == 1)
{
// successful
status = true;
}
}
}
catch (RuntimeException rte)
{
logger
.warn("Can not send boolean control to active host. Exception: "
+ rte.getMessage());
}
}
// send to primary host if it is not the current active host
if (primaryHost_ != null && primaryHost_.isEnabled()
&& primaryHost_ != activeHost_)
{
logger.info("Sending boolean control to primary host. Address: "
+ inDataPoint.getAddress() + ". Value: " + inValue);
try
{
if (boolCommand_ == EModbusFunctionCode.FORCE_SINGLE_COIL)
{
retval = primaryHost_.writeOutputCoil(inDataPoint
.getAddress(), inValue);
if (retval == 1)
{
// successful
status = true;
}
}
else
{
retval = primaryHost_.writeOutputCoils(inDataPoint
.getAddress(), 1, states);
if (retval == 1)
{
// successful
status = true;
}
}
}
catch (RuntimeException rte)
{
logger
.warn("Can not send boolean control to primary host. Exception: "
+ rte.getMessage());
}
}
// send to standby host if it is not the current active host
if (standbyHost_ != null && standbyHost_.isEnabled()
&& standbyHost_ != activeHost_)
{
logger.info("Sending boolean control to standby host. Address: "
+ inDataPoint.getAddress() + ". Value: " + inValue);
try
{
if (boolCommand_ == EModbusFunctionCode.FORCE_SINGLE_COIL)
{
retval = standbyHost_.writeOutputCoil(inDataPoint
.getAddress(), inValue);
if (retval == 1)
{
// successful
status = true;
}
}
else
{
retval = standbyHost_.writeOutputCoils(inDataPoint
.getAddress(), 1, states);
if (retval == 1)
{
// successful
status = true;
}
}
}
catch (RuntimeException rte)
{
logger
.warn("Can not send boolean control to standby host. Exception: "
+ rte.getMessage());
}
}
// logging context
NDC.pop();
return status;
}
@Override
public boolean setControl(IDataPoint inDataPoint, int inValue)
{
boolean status = false;
int retval = 0;
// validation : datapoint
if (inDataPoint == null)
{
return false;
}
// logging context
NDC.push(inDataPoint.getName());
// check if we use set-multiple-registers
short[] values = null;
if (intCommand_ != EModbusFunctionCode.PRESET_SINGLE_REGISTER)
{
values = new short[1];
values[0] = (short) inValue;
}
// send using active host first if it is available
if (activeHost_ != null)
{
logger.info("Sending integer control to active host. Address: "
+ inDataPoint.getAddress() + ". Value: " + inValue);
try
{
if (intCommand_ == EModbusFunctionCode.PRESET_SINGLE_REGISTER)
{
retval = activeHost_.writeHoldingRegister(inDataPoint
.getAddress(), (short) inValue);
if (retval == 1)
{
// successful
status = true;
}
}
else
{
retval = activeHost_.writeHoldingRegisters(inDataPoint
.getAddress(), 1, values);
if (retval == 1)
{
// successful
status = true;
}
}
}
catch (RuntimeException rte)
{
logger
.warn("Can not send integer control to active host. Exception: "
+ rte.getMessage());
}
}
// send to primary host if it is not the current active host
if (primaryHost_ != null && primaryHost_.isEnabled()
&& primaryHost_ != activeHost_)
{
logger.info("Sending integer control to primary host. Address: "
+ inDataPoint.getAddress() + ". Value: " + inValue);
try
{
if (intCommand_ == EModbusFunctionCode.PRESET_SINGLE_REGISTER)
{
retval = primaryHost_.writeHoldingRegister(inDataPoint
.getAddress(), (short) inValue);
if (retval == 1)
{
// successful
status = true;
}
}
else
{
retval = primaryHost_.writeHoldingRegisters(inDataPoint
.getAddress(), 1, values);
if (retval == 1)
{
// successful
status = true;
}
}
}
catch (RuntimeException rte)
{
logger
.warn("Can not send integer control to primary host. Exception: "
+ rte.getMessage());
}
}
// send to standby host if it is not the current active host
if (standbyHost_ != null && standbyHost_.isEnabled()
&& standbyHost_ != activeHost_)
{
logger.info("Sending integer control to standby host. Address: "
+ inDataPoint.getAddress() + ". Value: " + inValue);
try
{
if (intCommand_ == EModbusFunctionCode.PRESET_SINGLE_REGISTER)
{
retval = standbyHost_.writeHoldingRegister(inDataPoint
.getAddress(), (short) inValue);
if (retval == 1)
{
// successful
status = true;
}
}
else
{
retval = standbyHost_.writeHoldingRegisters(inDataPoint
.getAddress(), 1, values);
if (retval == 1)
{
// successful
status = true;
}
}
}
catch (RuntimeException rte)
{
logger
.warn("Can not send integer control to standby host. Exception: "
+ rte.getMessage());
}
}
// logging context
NDC.pop();
return status;
}
@Override
public boolean startControl()
{
// check if it is already running
if (thread_.isRunning())
{
return true;
}
// start the polling thread
thread_.start();
return true;
}
@Override
public boolean startMonitor()
{
// nothing to do in monitor mode
return true;
}
@Override
public boolean stopControl()
{
// check if it is never running
if (!thread_.isRunning())
{
return true;
}
// stop the polling thread
thread_.stop(100);
return true;
}
@Override
public boolean stopMonitor()
{
// nothing to do in monitor mode. so nothing to stop
return true;
}
/**
* Perform status and data polling
*
* @return the number of segment successfully polled
*/
protected int poll()
{
// TODO: check each polling segment if it is the time for polling it
long curtime = Calendar.getInstance().getTimeInMillis();
int counter = 0;
PollingSegment segment = null;
for (int i = 0; i < segments_.size(); i++)
{
segment = segments_.get(i);
if (segment == null)
{
continue;
}
// check if it is already time for polling this segment
if (segment.nextPollingMillis > curtime)
{
continue;
}
// try the current active host
if (activeHost_ == null)
{
activeHost_ = primaryHost_;
}
// logging context
NDC.push(segment.name);
// poll the segment
if (segment.commandType == EModbusFunctionCode.READ_COIL_STATUS)
{
boolean[] arr = null;
// try the current active host
try
{
arr = activeHost_.readOutputCoils(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("Can not poll the segment from primary host. "
+"Attempting to switch to standby host...");
}
else
{
logger.warn("Can not poll the segment from standby host. "
+"Attempting to switch to primary host...");
}
}
// if fails, switch the host and try again
if (arr == null)
{
// switch the host
if (activeHost_ == primaryHost_)
{
activeHost_ = standbyHost_;
}
else
{
activeHost_ = primaryHost_;
}
// try again
try
{
arr = activeHost_.readOutputCoils(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("STILL can not poll the segment from primary host. "
+"Forcing immediate status polling...");
}
else
{
logger.warn("STILL can not poll the segment from standby host. "
+"Forcing immediate status polling...");
}
} //try-catch
} //if (arr == null)
// if both fails, set the datapoint' source quality to bad
if (arr == null)
{
// set the datapoint' source quality to bad
// TODO
// force a status polling
// TODO
}
else
{
// update the value
// TODO
// successful polling
counter++;
}
}
else if (segment.commandType == EModbusFunctionCode.READ_INPUT_STATUS)
{
boolean[] arr = null;
// try the current active host
try
{
arr = activeHost_.readInputContacts(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("Can not poll the segment from primary host. "
+"Attempting to switch to standby host...");
}
else
{
logger.warn("Can not poll the segment from standby host. "
+"Attempting to switch to primary host...");
}
}
// if fails, switch the host and try again
if (arr == null)
{
// switch the host
if (activeHost_ == primaryHost_)
{
activeHost_ = standbyHost_;
}
else
{
activeHost_ = primaryHost_;
}
// try again
try
{
arr = activeHost_.readInputContacts(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("STILL can not poll the segment from primary host. "
+"Forcing immediate status polling...");
}
else
{
logger.warn("STILL can not poll the segment from standby host. "
+"Forcing immediate status polling...");
}
} //try-catch
} //if (arr == null)
// if both fails, set the datapoint' source quality to bad
if (arr == null)
{
// set the datapoint' source quality to bad
// TODO
// force a status polling
// TODO
}
else
{
// update the value
// TODO
// successful polling
counter++;
}
}
else if (segment.commandType == EModbusFunctionCode.READ_HOLDING_REGISTER)
{
short[] arr = null;
// try the current active host
try
{
arr = activeHost_.readHoldingRegisters(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("Can not poll the segment from primary host. "
+"Attempting to switch to standby host...");
}
else
{
logger.warn("Can not poll the segment from standby host. "
+"Attempting to switch to primary host...");
}
}
// if fails, switch the host and try again
if (arr == null)
{
// switch the host
if (activeHost_ == primaryHost_)
{
activeHost_ = standbyHost_;
}
else
{
activeHost_ = primaryHost_;
}
// try again
try
{
arr = activeHost_.readHoldingRegisters(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("STILL can not poll the segment from primary host. "
+"Forcing immediate status polling...");
}
else
{
logger.warn("STILL can not poll the segment from standby host. "
+"Forcing immediate status polling...");
}
} //try-catch
} //if (arr == null)
// if both fails, set the datapoint' source quality to bad
if (arr == null)
{
// set the datapoint' source quality to bad
// TODO
// force a status polling
// TODO
}
else
{
// update the value
// TODO
// successful polling
counter++;
}
}
else //default: read input registers
{
short[] arr = null;
// try the current active host
try
{
arr = activeHost_.readInputRegisters(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("Can not poll the segment from primary host. "
+"Attempting to switch to standby host...");
}
else
{
logger.warn("Can not poll the segment from standby host. "
+"Attempting to switch to primary host...");
}
}
// if fails, switch the host and try again
if (arr == null)
{
// switch the host
if (activeHost_ == primaryHost_)
{
activeHost_ = standbyHost_;
}
else
{
activeHost_ = primaryHost_;
}
// try again
try
{
arr = activeHost_.readInputRegisters(segment.start,
segment.size);
}
catch (RuntimeException re)
{
// verbose
if (activeHost_ == primaryHost_)
{
logger.warn("STILL can not poll the segment from primary host. "
+"Forcing immediate status polling...");
}
else
{
logger.warn("STILL can not poll the segment from standby host. "
+"Forcing immediate status polling...");
}
} //try-catch
} //if (arr == null)
// if both fails, set the datapoint' source quality to bad
if (arr == null)
{
// set the datapoint' source quality to bad
// TODO
// force a status polling
// TODO
}
else
{
// update the value
// TODO
// successful polling
counter++;
}
}
// logging context
NDC.pop();
}
return 0;
}
private void update_values(ArrayList<DataPoint> datapoints,
boolean[] states, int offset)
{
if (datapoints == null || states == null || offset < 0)
{
return;
}
CosDpValueStruct value = new CosDpValueStruct();
value.timestamp = 0;
value.quality = CosDpQualityEnum.QualityGood;
// update the value
IDataPoint dp = null;
boolean boolValue = false;
for (int i = 0; i < datapoints.size(); i++)
{
dp = datapoints.get(i);
if (dp == null || dp.getSourceType() != ESourceDataType.TYPE_DI)
{
continue;
}
// TODO: parse DDI and TDI as well
boolValue = states[dp.getAddress() - offset];
value.value.boolValue(boolValue);
// set the dp value
if (dp.setSourceValue(value) < 0)
{
logger.warn("Can not update source value of datapoint "
+ dp.getName());
}
}
}
private void update_values(ArrayList<DataPoint> datapoints, short[] values,
int offset)
{
if (datapoints == null || values == null || offset < 0)
{
return;
}
CosDpValueStruct value = new CosDpValueStruct();
value.timestamp = 0;
value.quality = CosDpQualityEnum.QualityGood;
// update the value
DataPoint dp = null;
// int dpWordAddress = 0;
boolean boolValue = false;
long intValue = 0;
String strValue = "";
double dblValue = 0;
for (int i = 0; i < datapoints.size(); i++)
{
dp = datapoints.get(i);
if (dp == null)
{
continue;
}
switch (dp.getSourceType())
{
case TYPE_DI:
boolValue = BufferParser.getBooleanValue(values, dp
.getAddress()
- offset, dp.getBitAddress());
value.value.boolValue(boolValue);
break;
case TYPE_INT16:
intValue = BufferParser.getLongValue(values, dp
.getAddress()
- offset, 2, false);
value.value.longValue((int) intValue);
break;
case TYPE_INT32:
intValue = BufferParser.getLongValue(values, dp
.getAddress()
- offset, 4, false);
value.value.longValue((int) intValue);
break;
case TYPE_UINT16:
intValue = BufferParser.getLongValue(values, dp
.getAddress()
- offset, 2, true);
value.value.unsignedValue((int) intValue);
break;
case TYPE_UINT32:
intValue = BufferParser.getLongValue(values, dp
.getAddress()
- offset, 4, true);
value.value.unsignedValue((int) intValue);
break;
case TYPE_BCD:
intValue = BufferParser.getBcdValue(values, dp.getAddress()
- offset, dp.getLength() / 2);
value.value.longValue((int) intValue);
break;
case TYPE_IEEE16:
dblValue = BufferParser.getDoubleValue(values, dp
.getAddress()
- offset, 2);
value.value.dblValue(dblValue);
break;
case TYPE_IEEE32:
dblValue = BufferParser.getDoubleValue(values, dp
.getAddress()
- offset, 4);
value.value.dblValue(dblValue);
break;
case TYPE_STRING:
strValue = BufferParser.getStringValue(values, dp
.getAddress()
- offset, dp.getLength() / 2);
value.value.charValue(strValue);
break;
}
// set the dp value
if (dp.setSourceValue(value) < 0)
{
logger.warn("Can not update source value of datapoint "
+ dp.name);
}
}
}
/**
* Worker polling thread
*
* @author Yoga
*/
class PollingThread extends DetachedThread
{
private ModbusSubsystem subsystem = null;
PollingThread(ModbusSubsystem inSwc)
{
subsystem = inSwc;
// set the precision explicitly
precisionMillis = DetachedThread.DEF_PRECISION_MILLIS;
}
@Override
protected void _doWork()
{
subsystem.poll();
}
@Override
protected void _initial()
{
NDC.push(subsystem.getName());
logger.info("Poller thread is started.");
}
@Override
protected void _final()
{
logger.info("Poller thread has stopped.");
NDC.pop();
}
@Override
protected void _onStart()
{
logger.info("Starting subsystem " + subsystem.getName()
+ " poller thread...");
}
@Override
protected void _onStop()
{
logger.info("Stopping subsystem " + subsystem.getName()
+ " poller thread...");
}
}
}