Stephen

Alarm on comm timeout/cable unplug

Recommended Posts

I'm working on a facilities monitoring installation... I need to alarm if LAN cables (Modbus TCP) are disconnected.

I have one set of channels (the Click PLC) being polled in a sequence.  I can try-catch a timeout and flag this error... but the sequence bombs and has to be restarted after the cable is re-plugged.  At least I know there's a problem.

The other set of channels (the H2 generator) is polled by the normal channel-device loop.  I've tried comparing GetTime(mychan[0]) == GetTime(mychan[2]) and it seems that the channel is not updated when a timeout occurs... which I fully support as normal.

Is there a graceful way to set an alarm on a channel timeout?  Is there a way to catch a timeout and spin until the read is made OK, without bombing the sequence?  TIA.

Share this post


Link to post
Share on other sites

The sequence shouldn't bomb out if you use a proper try/catch.  If it is then you might have it setup wrong.  Post your script.

If you are using channel timing, then you'll need to use the other method that you used in H2 generator, but slightly different.  Comparing mychan[0] to time of mychan[2] doesn't really help unless your code is updating mychan with the same timestamp.  I prefer to calculate data age using systime(), for example:

systime() - getTime(myChan[0]) > 10

which will return true if myChan hasn't been updated in 10 seconds.

Share this post


Link to post
Share on other sites
21 hours ago, AzeoTech said:

> The sequence shouldn't bomb out if

> you use a proper try/catch.  If it is then

> you might have it setup > wrong.  Post your script.

<>

while(1)
   
lamp_update()

delay(0.1)

private In0

try
   In0 = Device.ClickDevice.ReadCoilStatus(0,0xF066,1)
catch()
   Click_PLC_Comm_Alm = 1
endcatch

env_CommErr.AddValue(In0[0][0])


delay(0.1)

private In = Device.ClickDevice.ReadHoldingS32(0,10,4)

etc etc...

</>

So ideally I'd like to spin on that first read until the device comes back... there is a device Write() in LampUpdate(), but it doesn't seem to cause an alarm.  I tried putting a 'contine' inside the catch but that didn't help.

 

Thanks for your help, the other solution is elegant (for the H2) and I understand how to implement it.

Share this post


Link to post
Share on other sites

The problem is that you only have that one query inside a try/catch.

What I often do is create stub functions for comms.  So instead of calling device.ClickDevice.ReadCoilStatus() status directly, you'd create a function called, say "ClickReadCoilStatus()" that would take the required parameters, probably just address and num values, and call device.ClickDevice.ReadCoilStatus() in there inside a try / catch:

function clickReadCoilStatus(address, num)

   try
      private ret = device.clickDevice.ReadCoilStatus(0, address, num)
      return(ret)
   catch()
      ? strLastError
      Click_PLC_Comm_Alm = 1
   endcatch
   return(-1)

The only problem is you still have to check the result in your core program, as it returns -1 to indicate failure, but at least it won't crash out of the calling sequence. 

The other option is to put a try/catch around the entire contents of the while() except the delay:
 

while(1)
   try
      lamp_update()
      delay(0.1)
      // .... etc....
   catch()
       ? strLastError
      Click_PLC_Comm_Alm = 1      
   endcatch
   delay(0.1)
endwhile

You have to have a delay outside the loop, or in the catch() so that if you get constant errors from programming fault instead of comms you don't end up with an infinite loop without a delay(), which will tie up the CPU.
       
 Also note that using 0 for the Modbus ID is invalid, even in ModbusTCP.  An ID of 0 indicates "broadcast" in Modbus, which means that any device on the chain may respond.  With ModbusTCP there usually isn't a chain, and often TCP devices just ignore the ID, but still, you should use a valid ID so that you don't screw yourself if you change hardware, or they do a firmware update that changes the behavior of the device to only accept valid IDs.  It is also just good form.

 

 

Share this post


Link to post
Share on other sites

Is it possible to call a Comm Device by referencing its underlying object? For example, switching between two different Modbus devices using a single wrapper:

device.DataManager.Address       		= "10.0.0.19"
device.SafetyEquipment.Address       	= "10.0.0.20"

class ModbusStatsClass
	local Interface
	local Address

	function ReadModbus(start, size)
		return Interface.ReadHoldingU16(Address, start, size)       
	endfunction
endclass
  
class DataManagerClass     
    local WindSpeed  	
    local ModbusStats
    
	function Init()
		ModbusStats = new (ModbusStatsClass)
      ModbusStats.Address = 1
      ModbusStats.Interface = device.DataManager
	endfunction
endclass

class SafetyEquipmentClass     
    local LocalRemote  	
    local ModbusStats
    
	function Init()
		ModbusStats = new (ModbusStatsClass)
      ModbusStats.Address = 2
      ModbusStats.Interface = device.SafetyEquipment
	endfunction
endclass

Global DM = new (DataManagerClass)
Global SEIO = new (SafetyEquipmentClass)

DM.Init()
SEIO.Init()

Private HoldingRegisters = DM.ModbusStats.ReadModbus(0, 125)
// Do some stuff

HoldingRegisters = SEIO.ModbusStats.ReadModbus(0, 60)
//Do some other stuff

 

Share this post


Link to post
Share on other sites

No, DAQFactory doesn't generally support pointers except in some situations (like by reference storage of objects).  However, you can use the execute() and evaluate() commands to do what you want.  So, you store the name of the device in a local string variable, then instead of calling directly, you use execute() to call:

execute("device." + myDeviceName + ".ReadHoldingS16(...")

I'll typically wrap this up to include error handling and debugging (if needed).

 That all said, there is a way to dynamically create comm devices as objects.  There are three:

CCommEthernet
CCommSerial
CCommDevice

So:

myPort = new(CCommEthernet)

would create a new Ethernet port which you can then configure with the usual members (Address, Port, InitComm(), etc)

likewise with CCommSerial.

Then you can create a device (or use an existing):

myDevice = new(CCommDevice)

then assign the port and protocol.  The protocol you have to do by name using a string, but there is a separate member variable called PortObject of a device which takes a port object, CCommEthernet or CCommDevice.  So:

myDevice.PortObject = myPort
myDevice.ProtocolName = "ModbusRTU"

Of course DAQFactory handles all the garbage collection and will close the ports when they go out of scope.  Note that PortObject is a by reference variable just like any other object, so if you ran the above code and then let myPort go out of scope or assigned it to something else, myDevice would still retain that original port object.  You would then need to update portObject with a new port to have that one go away.

Share this post


Link to post
Share on other sites

That's great, thank you. Do these objects also support ModbusTCP Slave?

Also, I'm confused with variable scope in a specific situation:

class ModbusClass
      local string IPAddress  = "192.168.1.1"
      local ModbusAddress     = 1     
      local EthernetPort
      local ModbusDevice     
      
      function Init()           
         EthernetPort               = new (CCommEthernet)         
         EthernetPort.Address       = IPAddress
         EthernetPort.Port          = 502          
         EthernetPort.InitComm()
         EthernetPort.Purge() 
         ModbusDevice               = new (CCommDevice)         
         ModbusDevice.PortObject    = EthernetPort
         ModbusDevice.ProtocolName  = "ModbusTCP"        
      endfunction     
      
      function Read(start, length, string type)   
			? format("IP Address: %s", ModbusDevice.IPAddress)
            return evaluate("ModbusDevice." + type + "(" + ModbusAddress + "," + start + "," + length + ")")           
      endfunction
endclass

class DataManagerClass
	local Modbus
	
	function Init()
		Modbus = new (ModbusClass)
	endfunction
endclass

global DM = new (DataManagerClass)
DM.Init()
DM.Modbus.IPAddress     = "10.0.0.19"
DM.Modbus.Init() 

DM.Modbus.Read(0,125,"ReadInputU16")

The output is: 192.168.1.1. The local variable IPAddress holds "10.0.0.19" as I would expect, but why does ModbusDevice.IPAddress print 192.168.1.1? Curiously, I do correctly poll the slave at 10.0.0.19

 

Share this post


Link to post
Share on other sites

Maybe.  I can't say for sure.  It wasn't really designed for that and the mechanism for getting data to the slave is different than other protocols.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now