Persisting an Instance of an Object that Talks to Devices

QUESTION: Is the following approach to object persistence in Ignition optimal?

(I added a lot of extra documentation, because I think this is probably a common-ish problem in manufacturing.)

Problem:

I have a VB.NET program that sits on a test station and talks to a few devices. This was legacy code that was reworked a little to work with Ignition system.
We talk to the VB program and get data back from it through a PLC middleman.
I want to move this VB.NET driver into Ignition, converting to Python 2.5.

Why I want to make the change:

  • One language for future developers - Python
  • Reduce pain of adding / rearranging stations, by not relying on PLC unnecessarily
  • More maintainable
  • Far richer controls from user’s perspective ( a lot of features have not been added, because of limited PLC space and pain of deployment ).
  • Cheaper -> No PLCs for at least this part of our system

Characteristics of driver:

  • receives async command from user
  • maintains state & periodically polls the device to keep connection alive
  • periodically does other things like evaluate bus to check for new devices
  • only one command allowed at time to be sent to device, to avoid weird overlapping commands from separate chains of commands that result in funky operation.

The Approach I am Thinking of:

  • create a class, Device() -> see below for example. Use Python 3…just for print statements

  • use a few kill events - Event() thread objects - to stop long threaded processes, like testing and polling

  • set these kill events when I need to abruptly end something in a safe manner ( the real-world version will involve data collection with a Queue() )
    - use putClientProperty/getClientProperty to store this class between asynchronous calls to action from the user to test / abort a device

    import threading, time

    class Device():

      def __init__(self):
      	self._poll_kill_event = threading.Event()
      	self._kill_event = threading.Event()
      	self._lock = threading.Lock()
      	self.__poll()
      	self._stopped = False
      
      def __send(self,cmd):
      	print( 'sending  %s to device...' % cmd.upper(), end='' )
      	time.sleep(1)
      	print( 'done!' )
      
      def __poll(self):
      	def poll():
      		while True:
      			try:
      				self._lock.acquire()
      				self.__send('polling...')
      			finally:
      				self._lock.release()
      			time.sleep(2)
      			if self._poll_kill_event.isSet():
      				print('polling kill event received!')
      				self._poll_kill_event.clear()
      				break
      	t=threading.Thread(target=poll)
      	t.daemon=True
      	t.start()
    
      def stop(self):
      	print('stopping device...',end='')
      	if self._stopped == False:
      		self._poll_kill_event.set()
      		while self._poll_kill_event.isSet():
      			time.sleep(1)
      		self._stopped = True
      	print('done!')
    
      def _test(self):
      	try:
      		self._lock.acquire()
      		start = time.process_time()
      		while time.process_time() - start < 10:
      			self.__send( 'testing...' )
      			if self._kill_event.isSet():
      				self._kill_event.clear()
      				print('kill event received. stop sending TEST cmd.')
      				break
      	finally:
      		self._lock.release()
      
      def test(self):
      	t=threading.Thread(target=self._test)
      	t.daemon=True
      	t.start()
    
      def abort(self):
      	if self._lock.locked():
      		self._kill_event.set()
      	print('waiting for lock...',end='')
      	while self._lock.locked():
      		time.sleep(1)
      	print('done!')
      	try:
      		self._lock.acquire()
      		self.__send( 'abort' )
      	finally:
      		self._lock.release()
    

    device = Device()
    device.test()
    time.sleep(4)
    device.abort()
    time.sleep(2)
    device.stop()

Question (again):

So, again my question is whether this is the best approach to this problem.
I haven’t seen anything quite like this question here. Most questions about persistence revolved around global variables or using the database and tags, but that is painful while an object-oriented approach feels much cleaner and understandable.

I am open to hearing other possible implementation schemes…whatever anyone has to offer.

1 Like

Ive read this 3 times. And Id like to get some clarity.
We talk to the VB program and get data back from it through a PLC middleman. <----- Confused [ is this an Ingear thing?]

I want to move this VB.NET driver into Ignition, converting to Python 2.5. <---- By driver do you mean leveraging Ignitions built in device drivers and writing your code around them?

It’s not a terrible approach, but consider not using python threads or events. Jython is really a java environment, not python, and using invokeAsynchronous is really the best way to launch independent threads in Ignition. Also, make sure that a scripting restart in the gateway triggers replacement of your background threads – this is not automatic, and will be a source of grief (memory leaks and blocked sockets, at least). So too with all jython classes – make sure instances are tracked in system.util.getGlobals() so they can be replaced. Consider writing state changes to one or more memory tags and letting Ignition distribute naturally. (You can then initialize from them when scripts reload, too…)

  1. I plan on using sockets lib in Python to handle the SCPI commands. I may need to write it in Python 2.7/3, so I can use PyVisa though.

  2. By driver, I just mean a program that sits on the host computer, takes commands through the PLC and then executes those by sending commands to the device itself. This currently uses Ingear drivers, but PyVisa has a pure python implementation that doesn’t require the drivers to be installed.

Thanks for your suggestions.

I am not sure why I need to replace my threads when there is a scripting restart in the gateway. If you are saying that I will lose my object instance when an update comes into the client, then a simple if statement to create another and re-evaluate the bus would solve this. As for storing state changes, I am really trying to make something completely independent of Ignition. It sounds like running this in Ignition will create too many weird problems when the client is updated.

I am thinking of just writing in Python 3, storing a self-contained (all libs & python 3 included) copy of the program on the server, having Ignition copy this to the host computer when a client opens if the program is not already there and the version has not been updated since the last copy, and then start that program from Ignition and use a server/client model with sockets lib to send commands back and forth. I don’t like this as much…thoughts?

Because your threads will continue running old code in the old modules' contexts, but all new calls from the rest of Ignition into your code will call the new function and class instances in the new modules' contexts. Even the unedited modules.
I think running such a "driver" in a separate process, with code in Ignition to pass information back and forth, is dramatically more complicated than just writing the driver for jython 2.5 (or 2.7 in v8).

Wait...

If I create an instance of a class, which has internal references to threads it has created, and then I update the client and, for example, I added a new function to the class in the update, then all instances of that class should get updated automatically - right?

Shouldn't the instance of the class already sitting there in the client from before the update still be available after the update? It should, I think, so the new method should appear and I should be able to use the new code immediately.

If this last assumption is wrong and an instance of the class doesn't get updated, then I guess the thing I would need to do is check the edit count and then kill all threads connected to the class instance and then create a new instance that references the new class code.

...

I have hit a wall with converting from VB.NET to jython/python. It looks like I absolutely need the PyVisa library. I tried importing to Ignition, but it needed to use:

from future import unicode_literals

and this wasn't available in current future.py file. I tried getting a copy of this file from the latest jython version available and pasting that in, but I get the same error after deleting the cache on my computer and restarting the designer. I am not sure if there is maybe another way to handle this? If I can't get this library into Ignition, I have no choice but to create an external python program to act as my "driver."

I just designed a part of a project where an instance of a class could survive an update and hot-load any script changes made. This is no trivial task. It requires an understanding of async threads and if you’re communicating with the event data thread back and forth it gets even deeper. Maybe consider your design where any state is off loaded to a db or tag. @pturmel is giving you good advice. And I would highly recommend any socket/io comms to be run through a java module of your on devising. Making comms truly non-blocking in jython is tricky especially if its running in the gateway scope.We had a need for unsolicited messages so I wrote an ENIP/Cip AB driver in jython that runs in the gateway scope and it took a long while with much heartache to get it stable. Also any experimentation with (infinite loops) that you run in the gateway scope could crater it… backup frequently.

1 Like

Holy smokes! I'm guessing this was before I introduced my Ethernet/IP module?

Alrighty then…what I am hearing is that object persistence / thread persistence is a pain in the butt and not worth the trouble.

New Design:

  • create a Device() class in pure Python
  • replace all of the threading calls with system.util.invokeAsynchrous()
  • use tags to store data, kill event bools. When class created, use @property in python to do lazy reloading of this data from the tags as needed.

So I am still using a class, but I am not going to maintain an instance and instead create it on-the-fly when I need it. This should avoid all of the problems you have described.

I am not going with writing a java module, because a big reason for doing this in Ignition is reducing the number of programming languages so it is easier to maintain and for new developers to understand.

Thanks for the help!

No it had been out a while. I just wanted the experience and knowledge. I see 0x4c in my sleep now. I also got it running on Python 3.7 using async/io for comms full logix data structure browsing, local emulation, and reading and writing. The other side of the 3.7 version is that it can be used as a api for web services. For example think of a web api where you post a query for a tag(s) and get a json object in return. In my spare time im integrating a websocket framework. I got inspired by a crypto market API for pushing crypto currency market data to the client and i thought i could make this work for my driver. Its a side project for fun though nothing commercial.

No. For a client, I believe the client restarts deep enough that background threads will be killed. But if not, and certainly in gateway scopes, the old instance and threads will still be there running the old code. All event routines will call into the new code, with new instances. Even the module global scope (where module functions and class names exist) is recreated from scratch upon script reload. Old threads see the old modules. The dictionary you get from system.util.getGlobals() is the only object that remains untouched. Put your state variables, thread objects, and sockets here so that your module code can find and kill off old versions. You don't need to watch the edit count. Just do it unconditionally at the top level of your script module(s).

Ewww. I don't see any way to use VISA drivers from a java environment, a requirement for jython access.

They are not killed on Update Push i know that for certain. Restart absolutely.

There's a SourceForge project that wraps the VISA API for java:

PyVisa has its own internal implementation of VISA, so that you don’t need any external driver. PyVisa is completely self-contained and has a very simple interface, which is why I want to use it.

Moved to new thread for PyVisa / VISA communication.