Creating a Visual Feedback Dashboard that Executes a Ping Script Continuously

Hello all,

Relatively new to designing in Ignition, so I want to apologize in advance if I come off as vague when discussing my project. I will be periodically checking this thread to answer any questions or provide feedback on the suggestions.

The Details of the Project

Here is the dashboard being displayed, the text is present showing the device name, I am only crossing it out for privacy reasons:

ui

As the title suggests, I'm creating a view in perspective that will show all of our connected devices' current state (connected or not). This script that pings the device is set to run every 10 seconds via a previously configured gateway script that counts seconds, bound to a custom property called "Seconds" that modifies the value of another property called "PingEvery10":

dashcustomprops

here is the change script to determine every 10 second increment:

if self.custom.Seconds % 10 == 0:
	self.custom.PingEvery10 = self.custom.PingEvery10 + 1

the change script of "PingEvery10" to reset to 0 after 1 day's worth of time has passed (6 time per minute * 60 * 24 = 8640 times), the custom property that every device component is referencing:

if self.custom.PingEvery10 > 8640:
	self.custom.PingEvery10 = 0

Lastly, every device I currently have configured (only 7 devices in comparison to thousands that will be needed) is using a button component with two custom properties: "ip_Address" and "pinger". Every instance of "pinger" has a binding to the "PingEvery10" custom property on the view. Here is the change script that executes when "pinger"'s value changes, assigning a specified style class based on the result:

def ping(address):
	import subprocess
	command = 'ping', '%s' % address
	process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
	process_out = process.communicate()[0].strip().split('\r\n')
	print process_out[1:5]
	if any("TTL" in word for word in process_out):
		self.props.style.classes = "Connected"
	elif any("Pinging" in word for word in process_out):
		self.props.style.classes = "NoComms"
	else:
		self.props.text = "Ping Execution Error"
		self.props.style.classes = ""
	
address = self.custom.ip_Address
ping("%s" % address)

The Problem

Now for the reasoning for the thread. When I first created this script I had one device to trial the script for success. The typical response time for changes to show up in the session view was about 30 - 40 seconds, which is satisfactory in terms of what I was expecting; however, as more devices are added, that time gets much longer. With 7 devices, the changes take about 3 minutes to show up. This makes me theorize that it is pinging every device before showing updates to any single one.

I've read some similar scenarios, most of which point to using a gateway script with a system.util.invokeasynchronous command. I have not yet tried this due to my lack of knowledge on gateway scripts.

Related Questions

  • If the above-mentioned system utility is the way to resolve this, then how do I "pass" the "ip_Address" of each device button to the gateway script to be executed.
  • If the response time is able to be faster, what ways could I improve it?
  • If/When this is working, how would I "future proof " it for adding new devices? I'm assuming there would be a way in which to make an "Add New Device/Remove Device" tab that could collect information from the user and generate a button in the next available spot based on the details applied or conversely remove the device.

Thank you all in advance for your time! I am open to any other recommendations you might have to help me make this great.

I have done a similar job and I'll see if I can dig out the details.

In the meantime, have a look on the Ignition Exchange and search for "ping". There are a couple of projects there which may suit or give you some ideas.

'Network Device Status' looks like what you want.

1 Like

Also instead of shelling out check out this classic Ignition answer using java built-ins to ping -

2 Likes

system.device.listDevices() returns a dataset which has a State column, why isn't that sufficient for this?

I assume that State is equivalent to the Status from the Device Connections status page on the Gateway.

1 Like

One reason might be that the OP is testing connection to devices that are not OPC connections or Ignition devices. I have an Ignition application that tracks our IP address asignments for PLCs, HMIs, robots, vision systems, and various other devices. Having a ping status for each of these is very useful but most won't appear in Ignition's device list.

2 Likes

Yeah, I guess, that since the OP didn't specify why it didn't work, I wanted to insure that they weren't overcomplicating something for which there is already a built in system function.

If a ping is needed, then great, if not then don't make it harder than it should be.

1 Like

Do note that this requires raw packet permissions. In linux, if not root, your user needs CAP_NET_RAW, which can be easily added (with AmbientCapabilities=) to your SystemD service with an override file.

1 Like

Also, worth calling out:
You don't want to run this script directly from Perspective, otherwise each Perspective session you open is going to increase comm load unnecessarily.

3 Likes

That is correct,

This would be pinging devices that are not all OPC or I/O type. Examples of this would be desktop computers, switches, scanners, etc.

Thank you, this option looks to be a cleaner version of the code I had to open up the command shell. Looking at it, I'm curious about what you implemented with the project you referenced to allow this to work for many devices. I though maybe converting the parameters on the root container from objects to an array of objects (one for each ip) was the answer, and it may be, but I am struggling to get it to function with more than one device.

Here you go, for what it's worth.
I run this on a gateway timer event set to 30,000 ms. This timer value depends on the number of pings to be executed so may need to be extended as each ping is done sequentially.

The list of devices to be pinged is obtained from a database query.
The results are written to a memory tag. A couple of other tags keep track of the script status.

# Gateway Timer Script - Ping test
# Author:	Transistor
# Date:		2020-11-28
# Desc:		A series of ping tests is carried out on the devices included in resultsTag.
#			The ping tests are carried out from the gateway - not the client.
#			The ping tests are carried out by a gateway timer.
#			The ping test timer MUST BE RUN AS A DEDICATED THREAD.
import datetime
import os			# Required for ping operation
import re			# Required to strip leading zeros from IP address octets.

# Run a query on the routers table every 30s or so. Be awware that enough time should be allowed to run the maximum number of pings.
# Generate a dataset of all the form
#[
#['10.166.28.200', 'Area 1', 'Router', 'Machine 1'],
#['10.166.28.201', 'Area 1', 'PLC',    'Machine 1'],
#... ]
# from the router table. Initialise the dataset with the last seen and status columns.
pingList = system.db.runQuery("SELECT wan_ip, location, device, system, 0 AS 'last ping', 0 AS status FROM routers WHERE ping > 0", 'dbLAN')

# Read in the resultsTag. We'll need this to get the previous ping time
prevScanResults = system.dataset.toPyDataSet(system.tag.read('[default]BAPL_ping/Results').value)

# Prepare the dataset for output.
headers = ['wan_ip', 'location', 'device', 'system', 'last ping', 'status']
data = []

# Record the script start time.
startTime = datetime.datetime.now()
system.tag.write('[default]BAPL_Ping/LastPingStartTime', startTime)
system.tag.write('[default]BAPL_Ping/PingBusy', True)

for row in pingList:
	ip = row['wan_ip']
	shortIp = re.sub('(^|\.)0+(?=[^.])', r'\1', ip)			# Strip the leading zeros from each octet or Windows ping won't find it!	
	# For Windows: -n 1 = ping once. -w 200 = timeout value.
	# For Linux:   -c 1 = ping once. -w 1 = 1 s timeout.
	r = os.system("ping -n 1 -w 150 " + shortIp) == 0
	if r:										# Good result.
		lastPing = 'OK at ' + str(datetime.datetime.now())[10:19]
		status = 1
	else:										# No response.
		lastPing = 'Unknown'					# Default.
		status = -1								# Default.
		for row_p in prevScanResults:
			if row['wan_ip'] == row_p['wan_ip']:# This device was on the previous scan list.
				lastPing = row_p['last ping']
				status = 0
				break

	data.append([row['wan_ip'], row['location'], row['device'], row['system'], lastPing, status])

# Prepare the dataset for output.
dataOut = system.dataset.toDataSet(headers, data)
# Update the resultsTag for availability for all clients.
system.tag.write('[default]BAPL_Ping/Results', dataOut)

system.tag.write('[default]BAPL_Ping/PingBusy', False)
# Record the completion timestamp.
system.tag.write('[default]BAPL_Ping/LastPingEndTime', datetime.datetime.now())
timeDelta = datetime.datetime.now() - startTime
system.tag.write('[default]BAPL_Ping/TotalPingTime', float(timeDelta.seconds))		# Convert to seconds.
1 Like

Looks good my only pet peeve is importing and using datetime instead of using system.date.* functions.

I was young then. The code was put together from various bits of information from the web. I've learned all my Python on Ignition. I've since learned to use system.date.* but never updated this. Thanks for the alert!

1 Like

Haha fair enough I too have old scripts using datetime that are in production somewhere :upside_down_face:

Might want to scrub the comments to remove your private IP Addresses?

Should probably also, convert all of those system.tag.write() into system.tag.writeAsync() or combine them all into a single system.tag.writeBlocking(). And the system.tag.read() to the non-deprecated system.tag.readBlocking()