CompactLogix User Data

jthornton

Member
Join Date
Jul 2002
Location
Poplar Bluff, MO
Posts
295
I'm trying to read a tag from a user defined data that is an array. This is the data type.



part-records.jpg



This is the tag I'm trying to read.

fifo-rcrd.jpg







fifo-rcrd.jpg


This is the result of the read using pylogix
Code:
>>> with PLC() as comm:
...   comm.IPAddress = '192.168.117.130'
...   ret = comm.Read('FIFO_RCRD[0]')
...   print(ret.TagName, ret.Value, ret.Status)
... 
FIFO_RCRD[0] [b'\xc4\x85\xd2\x07\x00\x00\n\x00\x00\x00\x13\x00\x00\x00\x15\x00\x00\x00;\x00\x00\x00\x19\x00\x00\x00\x1f\x94\x08\x00\x04\x00\x00\x00W-26\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x8b\x8aA`=vIp\xeb\x16A\x8aD\x16A\x00\xe6&=\x00\x00\x00\x00\x00\x00\x80?\x00\x00lB\x10X\tA\x00\x00\x00\x00\x00\x00\x00\x00\xb6\xb6+BN\xf7>C\x0cSbA\xca\xfebA\x00\x00\x00\x00\x00\x00\x00\x00', b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x8b\x8aA`=vIp\xeb\x16A\x8aD\x16A\x00\xe6&=\x00\x00\x00\x00\x00\x00\x80?\x00\x00lB\x10X\tA\x00\x00\x00\x00\x00\x00\x00\x00\xb6\xb6+BN\xf7>C\x0cSbA\xca\xfebA\x00\x00\x00\x00\x00\x00\x00\x00', b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x8b\x8aA`=vIp\xeb\x16A\x8aD\x16A\x00\xe6&=\x00\x00\x00\x00\x00\x00\x80?\x00\x00lB\x10X\tA\x00\x00\x00\x00\x00\x00\x00\x00\xb6\xb6+BN\xf7>C\x0cSbA\xca\xfebA\x00\x00\x00\x00\x00\x00\x00\x00'] Success


Anyone have a suggestion?


JT
 
Last edited:
Pylogix will not automatically parse UDT's. It only really supports the atomic data types (BOOL, SINT, INT, DINT, LINT, REAL, STRING). Once a STRUCT/UDT is detected, it just returns the raw bytes, which you would have to parse.

My position on this has been met with some criticism but it is what it is. There are strategies that some users have taken to automate this, like exporting their UDT to L5X, then writing something to parse it to understand the members of the UDT, then read each member.

You'll have to read each member of your UDT, or understand how the data is packed in your UDT and parse it. The more complex the UDT, the more difficult this is.

The way I would do it is pass Read() a list of tags, this will speed things up compared to reading each member individually:

Code:
# individual reads
ret = comm.Read('FIFO_RORD[0].Tool_Name')
ret = comm.Read('FIFO_RORD[0].Current_User')
ret = comm.Read('FIFO_RORD[0].Auto_Cycle_Time')

# combined read, uses multi service request
tags = ['FIFO_RORD[0].Tool_Name', 'FIFO_RORD[0].Current_User', 'FIFO_RORD[0].Auto_Cycle_Time']
ret = comm.Read(tags)

Reading a list of tags will return a list of the values. It will pack as many reads into one packet as will fit.
 
Wow pylogix reads UDTs? That is sweet!


> Anyone have a suggestion?

Anyway, I see two choices:

1) read the tags individually, as noted by the author, certainly the simplest programming approach, but possibly less efficient for the pc/plc interface, and possibly not, depending how pylogix is implemented.

2) parse the raw bytes using the Python [struct] module's .unpack method.

Roughly the same number of steps, but the latter requires a lot of address (string index) fiddling to be done up front in the programming, while the latter hides all that behind the equivalent of "tags."

I would go with the latter, but then I am "drBITboy." ;)
 
I would go with the latter, but then I am "drBITboy." ;)

That's because you like to party!

Simple UDT's are pretty straightforward to parse. Where things get weird is when BOOL's are peppered through your UDT. They end up packed funny and you have to figure it out. Timer data types, for example, return 4 bytes that contain the .DN, .EN and .TT, they are packed in the last 3 bits.

I have a few fixes I was waiting to hear back regarding the raw UDT bytes with large UDT's that require more than one packet.
 
I had a good time today sorting out some code. I ended up using the list index to get individual bits of data out. Not sure if that't the best way.



Code:
#!/usr/bin/env python3
from pylogix import PLC


with PLC() as comm:
  comm.IPAddress = '192.168.117.130'
  tags = ['DISPLAY_RCRD[0].Date_Time.month',
  'DISPLAY_RCRD[0].Date_Time.day',
  'DISPLAY_RCRD[0].Date_Time.year',
  'DISPLAY_RCRD[0].Date_Time.hour',
  'DISPLAY_RCRD[0].Date_Time.minute',
  'DISPLAY_RCRD[0].Date_Time.second',
  'DISPLAY_RCRD[0].Tool_Name',
  'DISPLAY_RCRD[0].Auto_Cycle_Time',
  'DISPLAY_RCRD[0].Machine_Cycle_Number',
  'DISPLAY_RCRD[0].Total_Melt_Travel',
  'DISPLAY_RCRD[0].Total_Weld_Time',
  'DISPLAY_RCRD[0].Start_Weld_Hydraulic_PSI',
  'DISPLAY_RCRD[0].Hold_Weld_Hydraulic_PSI',
  'DISPLAY_RCRD[0].Amplitude_Feedback_During_Weld',
  'DISPLAY_RCRD[0].Frequency_Feedback_During_Weld']
  ret = comm.Read(tags)

  #for item in ret:
  #  print(item.TagName, item.Value, item.Status)

print(f"Date {ret[0].Value}-{ret[1].Value}-{ret[2].Value}")
print(f"Time {ret[3].Value}:{ret[4].Value}:{ret[5].Value}")
print(f"Tool {ret[6].Value}")
print(f"Cycle Time {ret[7].Value:.2f}")
print(f"Machine Cycle {ret[8].Value:,.0f}")
print(f"Melt Travel {ret[9].Value:.3f}")
print(f"Weld Time {ret[10].Value:.2f}")
print(f"Amplitude {ret[13].Value:.2f}")
print(f"Frequency {ret[14].Value:.2f}")
print('Date, Time, Tool, Cycle Time, Machine Cycle, Melt Travel, Weld Time, Amplitude, Frequency')

part_date = f"{ret[0].Value}-{ret[1].Value}-{ret[2].Value}, "
part_time = f"{ret[3].Value}:{ret[4].Value}:{ret[5].Value}, "
tool  = f"{ret[6].Value}, "
cycle_time = f"{ret[7].Value:.2f}, "
cycle  = f"{ret[8].Value:,.0f}, "
melt_travel = f"{ret[9].Value:.3f}, "
weld_time = f"{ret[10].Value:.2f}, "
amplitude = f"{ret[13].Value:.2f}, "
frequency = f"{ret[14].Value:.2f}"
print(part_date,
      part_time,
      tool,
      cycle_time,
      cycle,
      melt_travel,
      weld_time,
      amplitude,
      frequency)


And the output is



Code:
john@compaclogix:~ $ tags4
Date 10-20-2002
Time 0:36:36
Tool W-26
Cycle Time 17.21
Machine Cycle 1,008,693
Melt Travel 0.041
Weld Time 8.50
Amplitude 42.96
Frequency 193.10
Date, Time, Tool, Cycle Time, Machine Cycle, Melt Travel, Weld Time, Amplitude, Frequency
10-20-2002,  0:36:36,  W-26,  17.21,  1,008,693,  0.041,  8.50,  42.96,  193.10


Interestingly I printed ret to see what I got:


Code:
print(ret)
[Response(TagName=FIFO_RCRD[0].Date_Time.year, Value=2002, Status=Success), Response(TagName=DISPLAY_RCRD[0].Date_Time.month, Value=10, Status=Success), Response(TagName=DISPLAY_RCRD[0].Date_Time.day, Value=20, Status=Success), Response(TagName=DISPLAY_RCRD[0].Date_Time.hour, Value=0, Status=Success), Response(TagName=DISPLAY_RCRD[0].Date_Time.minute, Value=36, Status=Success), Response(TagName=DISPLAY_RCRD[0].Date_Time.second, Value=36, Status=Success), Response(TagName=DISPLAY_RCRD[0].Tool_Name, Value=W-26, Status=Success), Response(TagName=DISPLAY_RCRD[0].Auto_Cycle_Time, Value=17.211000442504883, Status=Success), Response(TagName=DISPLAY_RCRD[0].Machine_Cycle_Number, Value=1008693.0, Status=Success), Response(TagName=DISPLAY_RCRD[0].Total_Melt_Travel, Value=0.04133796691894531, Status=Success), Response(TagName=DISPLAY_RCRD[0].Total_Weld_Time, Value=8.503000259399414, Status=Success), Response(TagName=DISPLAY_RCRD[0].Start_Weld_Hydraulic_PSI, Value=0.0, Status=Success), Response(TagName=DISPLAY_RCRD[0].Hold_Weld_Hydraulic_PSI, Value=0.0, Status=Success), Response(TagName=DISPLAY_RCRD[0].Amplitude_Feedback_During_Weld, Value=42.9574089050293, Status=Success), Response(TagName=DISPLAY_RCRD[0].Frequency_Feedback_During_Weld, Value=193.09718322753906, Status=Success)]


Monday I'll plug my Rpi into a running machine to see if I can capture the DISPLAY_RCRD[0].Machine_Cycle_Number tag value increasing.


Thanks
JT
 
You got the result you want and you understand it, you did it the best way! :)

The way I prefer to format strings is like this:

Code:
print("Melt Travel {:.3f}".format(ret[9].Value))

If you wanted to make it easier to change the base tag name you could do something like this:

Code:
index = 0
tag = "DISPLAY_RCRD[{}]".format(index)
tag_list = ["{}.Date_Time.day".format(tag),
            "{}.Date_Time.year".format(tag),
            "{}.Date_Time.hour".format(tag)]
 
Sweet, nicely done!



Alternate style suggestion for your consideration below, I have an unhealthy predilection toward one-liners and usually try to eliminate unnecessary duplication ("DISPLAY_RCD[0]." 14 times?) and commas and ticks when making lists of strings. In your favor, mine is less easy to read.


Brian Carcich


[update: haha dmr did the same thing; "opinions are like noses, everybody has one, but you may not want to look up mine!"]




Code:
#!/usr/bin/env python3
from pylogix import PLC

with PLC() as comm:
  comm.IPAddress = '192.168.117.130'
  retVals = [retN.Value for retN in comm.Read(
["DISPLAY_RCRD[0].{0}".format(s.strip()) for s in """
Date_Time.month
Date_Time.day
Date_Time.year
Date_Time.hour
Date_Time.minute
Date_Time.second
Tool_Name
Auto_Cycle_Time
Machine_Cycle_Number
Total_Melt_Travel
Total_Weld_Time
Start_Weld_Hydraulic_PSI
Hold_Weld_Hydraulic_PSI
Amplitude_Feedback_During_Weld
Frequency_Feedback_During_Weld
""".strip().split('\n')]
)]


  #for item in ret:
  #  print(item.TagName, item.Value, item.Status)

print("""Date {0}-{1}-{2}
Time {3}:{4}:{5}
Tool {6}
Cycle Time {7:.2f}
Machine Cycle {8:,.0f}
Melt Travel {9:.3f}
Weld Time {10:.2f}
Amplitude {13:.2f}
Frequency {14:.2f}
Date, Time, Tool, Cycle Time, Machine Cycle, Melt Travel, Weld Time, Amplitude, Frequency
{0}-{1}-{2}, {3}:{4}:{5}, {6}, {7:.2f}, {8:,.0f}, {9:.3f}, {10:.2f}, {13:.2f}, {14:.2f}""".format(*retVals))
 
Last edited:
Sweet, nicely done!



Alternate style suggestion for your consideration below, I have an unhealthy predilection toward one-liners and usually try to eliminate unnecessary duplication ("DISPLAY_RCD[0]." 14 times?) and commas and ticks when making lists of strings. In your favor, mine is less easy to read.


Brian Carcich


[update: haha dmr did the same thing; "opinions are like noses, everybody has one, but you may not want to look up mine!"]




Code:
#!/usr/bin/env python3
from pylogix import PLC

with PLC() as comm:
  comm.IPAddress = '192.168.117.130'
  retVals = [retN.Value for retN in comm.Read(
["DISPLAY_RCRD[0].{0}".format(s.strip()) for s in """
Date_Time.month
Date_Time.day
Date_Time.year
Date_Time.hour
Date_Time.minute
Date_Time.second
Tool_Name
Auto_Cycle_Time
Machine_Cycle_Number
Total_Melt_Travel
Total_Weld_Time
Start_Weld_Hydraulic_PSI
Hold_Weld_Hydraulic_PSI
Amplitude_Feedback_During_Weld
Frequency_Feedback_During_Weld
""".strip().split('\n')]
)]


  #for item in ret:
  #  print(item.TagName, item.Value, item.Status)

print("""Date {0}-{1}-{2}
Time {3}:{4}:{5}
Tool {6}
Cycle Time {7:.2f}
Machine Cycle {8:,.0f}
Melt Travel {9:.3f}
Weld Time {10:.2f}
Amplitude {13:.2f}
Frequency {14:.2f}
Date, Time, Tool, Cycle Time, Machine Cycle, Melt Travel, Weld Time, Amplitude, Frequency
{0}-{1}-{2}, {3}:{4}:{5}, {6}, {7:.2f}, {8:,.0f}, {9:.3f}, {10:.2f}, {13:.2f}, {14:.2f}""".format(*retVals))

"I like your style dude"
-The Stranger, Big Lebowski
 
Actually I just copied the tag names from Logix5000 and pasted them in my program.


But for the sake of completeness I use f strings myself. Record 0 is the current record so that's the only one I copy to my csv file.

Code:
>>> tag = "DISPLAY_RCRD[0]"
>>> tag
'DISPLAY_RCRD[0]'
>>> tag_list = [f"{tag}Date_Time.day",
...     f"{tag}Date_Time.year",
...     f"{tag}Date_Time.day"]
>>> for tag in tag_list:
...     print(tag)
... 
DISPLAY_RCRD[0]Date_Time.day
DISPLAY_RCRD[0]Date_Time.year
DISPLAY_RCRD[0]Date_Time.day
>>>
JT
 
Last edited:
And the almost complete program, I have a few tweaks to do yet like wait for the connection on power up if the CompacLogix is not booted up before the Rpi etc.

Code:
#!/usr/bin/env python3

import time, os
from pylogix import PLC


monitor_tag = 'DISPLAY_RCRD[0].Machine_Cycle_Number'
tags = ['DISPLAY_RCRD[0].Date_Time.month',
'DISPLAY_RCRD[0].Date_Time.day',
'DISPLAY_RCRD[0].Date_Time.year',
'DISPLAY_RCRD[0].Date_Time.hour',
'DISPLAY_RCRD[0].Date_Time.minute',
'DISPLAY_RCRD[0].Date_Time.second',
'DISPLAY_RCRD[0].Tool_Name',
'DISPLAY_RCRD[0].Auto_Cycle_Time',
'DISPLAY_RCRD[0].Machine_Cycle_Number',
'DISPLAY_RCRD[0].Total_Melt_Travel',
'DISPLAY_RCRD[0].Total_Weld_Time',
'DISPLAY_RCRD[0].Amplitude_Feedback_During_Weld',
'DISPLAY_RCRD[0].Frequency_Feedback_During_Weld']

header = 'Date, Time, Tool, Cycle Time, Cycle, Melt, Weld, Amplitude, Frequency'

def pull_data(c):
	"""
	Pull some data from the PLC.  Add more reads/writes as
	necessary.
	"""
	stuff = c.Read(tags)
	#print_data(stuff)
	save_data(stuff)
	#print(stuff)
	t = c.GetPLCTime()
	#print(t)

def print_data(ret):
	print(f"Date {ret[0].Value}-{ret[1].Value}-{ret[2].Value}")
	print(f"Time {ret[3].Value}:{ret[4].Value}:{ret[5].Value}")
	print(f"Tool {ret[6].Value}")
	print(f"Cycle Time {ret[7].Value:.2f}")
	print(f"Machine Cycle {ret[8].Value:.0f}")
	print(f"Melt Travel {ret[9].Value:.3f}")
	print(f"Weld Time {ret[10].Value:.2f}")
	print(f"Amplitude {ret[13].Value:.2f}")
	print(f"Frequency {ret[14].Value:.2f}")

def save_data(ret):
	data_str = f"{ret[0].Value}-{ret[1].Value}-{ret[2].Value}, "
	data_str += f"{ret[3].Value}:{ret[4].Value}:{ret[5].Value}, "
	data_str += f"{ret[6].Value}, "
	data_str += f"{ret[7].Value:.2f}, "
	data_str += f"{ret[8].Value:.0f}, "
	data_str += f"{ret[9].Value:.3f}, "
	data_str += f"{ret[10].Value:.2f}, "
	data_str += f"{ret[11].Value:.2f}, "
	data_str += f"{ret[12].Value:.2f}"

	cfile = f'VW03_{ret[2].Value}_{ret[0].Value}.csv'
	if not os.path.isfile(cfile):
		print(f'{cfile} does not exist')
		with open(cfile, 'a') as f:
			print(header, file=f)
			print(data_str, file=f)
	else:
		with open(cfile, 'a') as f:
			print(data_str, file=f)
	print(data_str)


with PLC('192.168.117.130') as comm:
	run = False
	# read our value at start
	init = comm.Read(monitor_tag)
	year = comm.Read('DISPLAY_RCRD[0].Date_Time.year')
	month = comm.Read('DISPLAY_RCRD[0].Date_Time.month')


	# if the read was successful, save the value and
	# allow us to monitor the tag
	if init.Status == 'Success':
		# make last value = to current value so we don't pull
		# our data the first time we run
		last_value = init.Value
		print(f'Current Cycle {init.Value:.0f}')
		run = True
	else:
		print('Failed')

	# run a loop to monitor our tag
	while run:
		try:
			value = comm.Read(monitor_tag).Value

			# if the value changed, gather our data
			if value != last_value:
				# our tag changed, call pull_cata
				pull_data(comm)
				last_value = value
				#print(f'Cycle {value:.0f}')
			else:
				# take a 0.5 second nap if the value didn't change
				time.sleep(0.5)
		except KeyboardInterrupt:
			run = False
		except:
			run = False

JT
 
Very nice! I tried a different approach (below), but I think yours is cleaner.


TL;DR



The
[exception: run = False] => [while run:]
implicit combination to leave the loop would be simpler (clearer*?) as
[exception: break]
As an alternative, equivalent logic could be done with exceptions:


Code:
with PLC(...) as comm:
        try:
                init = comm.Read(...)
                assert init
                ...

                while True:
                        ...
        except AssertionError as e   : print('Failed')
        except KeyboardInterrupt as e: pass
        except                       : pass
Not much of an improvement really e.g. no reduction of indents; the only thing it really does is remove the variable [run], but memory and CPU cycles are cheap, so who cares?



* clarity is in the eye of the beholder.
 
Interesting, I've not messed with assert before..n interesting.


Didn't actually put up the last rendition of my code... this is where I test for a connection in case the PLC is slower to boot than the Rpi for some reason.



Code:
with PLC('192.168.117.130') as comm:
    run = False
    while not run: # wait for a sucessful read
        init = comm.Read(monitor_tag)
        if init.Status == 'Success':
            # make last value = to current value so we don't pull
            # our data the first time we run
            last_value = init.Value
            print(f'Current Cycle {init.Value:.0f}')
            run = True
        else:
            time.sleep(5)


JT
 
Excellent; you really know what you are doing.


My OCD makes me post this, which feels a bit cleaner (to me)


Code:
 with PLC(...) as comm:

   while True:              ### Loop until successful comm.Read
    init = comm.Read(...)
    if init.Status == 'Success': break
    time.sleep(5)          ### - assume failure is booting PLC

  last_value = ...
  print(...)

  while True:    ### Exception or Interrupt exits loop via break
    try:
      ...
    except KeyboardInterrupt:
      break
    except:
      import traceback
      traceback.print_exc()    ### Show exception for debugging
      break
 

Similar Topics

Hi everyone, i have a compact logic 1769-L18 PLC and I'm using FTalk View ME for the display. I wanted to do some visualization on Grafana. At...
Replies
1
Views
95
Does anyone know what the data transfer rate for this series of CompactLogix PLC's? 1769-L24ER-QB1B to be exact. Cheers.
Replies
1
Views
93
Does this instruction calculate values during a single scan, or does it require number of scans based on element count in the array? For Example...
Replies
3
Views
112
Hello all, and thank you in advance for any assistance you may be able to provide! This is my first post, so if I need to reformat or change...
Replies
8
Views
451
We are trying to poll data coming from a PLC for remote monitoring we have the IP address of the PLC and the default port number and the path is...
Replies
25
Views
564
Back
Top Bottom