Stephen Posted December 10, 2020 Share Posted December 10, 2020 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. Quote Link to comment Share on other sites More sharing options...
AzeoTech Posted December 10, 2020 Share Posted December 10, 2020 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. Quote Link to comment Share on other sites More sharing options...
Stephen Posted December 10, 2020 Author Share Posted December 10, 2020 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. Quote Link to comment Share on other sites More sharing options...
AzeoTech Posted December 11, 2020 Share Posted December 11, 2020 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. Quote Link to comment Share on other sites More sharing options...
bms Posted June 29, 2021 Share Posted June 29, 2021 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 Quote Link to comment Share on other sites More sharing options...
AzeoTech Posted July 6, 2021 Share Posted July 6, 2021 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. Quote Link to comment Share on other sites More sharing options...
bms Posted July 8, 2021 Share Posted July 8, 2021 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 Quote Link to comment Share on other sites More sharing options...
AzeoTech Posted July 16, 2021 Share Posted July 16, 2021 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. Quote Link to comment Share on other sites More sharing options...
bms Posted December 1, 2021 Share Posted December 1, 2021 When using these comms objects, I can't seem to poll Input or Holding Registers in the 30000 to 50000 range. EG when asking for Input Register 30233: ModbusDevice.ReadInputU16(1,30233,2) The command that actually goes out is for Input Register 232 (00 00 00 00 00 06 02 04 00 e8 00 02). It's as if it is assuming I am using Modicon 30xxx 40xxx convention. Asking for 60232 correctly sends the right command: 00 00 00 00 00 06 01 04 eb 48 00 02. Is there a way to force it to evaluate my data addressing literally? Quote Link to comment Share on other sites More sharing options...
AzeoTech Posted December 1, 2021 Share Posted December 1, 2021 If you read a tag > 50,000 it should force the driver to stick with 0 indexed. So I am guessing after you read 60232 that going back and reading 30233 should have worked. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.