Examples of using CPython in Ignition using subprocess, flask, etc

I'm in need of Using CPython libraries like numpy, pandas, etc. I'm having a very difficult time trying to utilize any of the several known methods of talking from Ignition to external python modules as described here: Python in Ignition - (Flask, subprocess call)

I watched the video by Ivana Šenk, [Machine Learning using Ignition]
(How to Develop a Low-Cost, Open Source Machine Learning Solution Using Ignition | Inductive Automation)
I was able to install PyCharm and the Flask library on separate server and get "hello world" to work, but would like to call calculations from Ignition and retrieve results.

Is there any other more clear step by step examples instead of trying to sift through many pages of documentation?

In the first link, Approach 2 describes a more built-in method of calling an external Python script. However, in the script console, I get this:

OSError: [Errno 2] No such file or directory

Is this maybe because I'm trying this in the script console? I installed a python interpreter on both my local computer and the Ignition Server as well as dropping the a py file with the appropriate name in the same location local to the machine. What am I doing wrong?

Jython does not use CPython.

You should be looking for Java solutions.

If you just need to communicate between Flask and Ignition you can have Ignition run GET/POST requests to your flask server.

Or have them both point to the same database, or file system.

To allow Flask to run GET/POST requests to Ignition you would need a WebDev module.

If you have a question on a script always show it. My first guess though is it is a scope issue. You put some file on the gateway, but you're running the script in script console on a designer which is not running on your gateway machine.

2 Likes

You should be able to call Pyhon3 scripts with subprocess calls from igition, but ignition will not be running those scripts, and like others have posted you would have to find ways like with APIs/fileshares/DB Sharing to have ignition use those python3 C library results.

With ignition being a jython, a lot of really useful libraries (Cpython) are not available, but there are some java methods that are exposed which you can use, but that isn't going to help with your CPython.

1 Like

Hopefully my first paragraph made sense that I understand that Ignition uses Jython and we need to use some external python interpreter for CPython, I was hoping for some help in going the right direction. I also didn't want to write a novel in my first post which might make it more confusing.

To start more simple, how can I get the "approach 2" method to work as linked here: Python in Ignition.

With the below code, I would like to retrieve the result. They reference "test.py", what would the code look like to return something in subprocess (even a simple example)?

import subprocess

pythonPath = "C:/Program Files/Python310/python.exe"
scriptPath = "C:/test.py"
param1 = "Motor"
param2 = "243"

result = subprocess.check_output([pythonPath, scriptPath, param1, param2])

subprocess.check_output returns the captured stdout of the child process:
https://docs.python.org/2.7/library/subprocess.html#subprocess.check_output

So in the program you're launching, if you use e.g. print("abc"), that's printing to standard output in the child process. That output will be captured and available in result from the launching process.

2 Likes

Ok, now I'm getting somewhere.

Using the basic example in the link I provided here is what I have.

  1. My py file called test.py:
from sys import argv

def foo(args):
    print(args)
    #>> ['#arg1', 'arg2']  # (based on the example below)

foo(argv[1:])

**Note that this py file and interpreter must be located on the local machine where the Script Console is used. If used in the Client or Session, you then could put that on the gateway's server.

  1. In Ignition
import subprocess, sys
pythonPath = "C:/Program Files/Python311/python.exe"
scriptPath = "C:/test.py"
param1 = "Motor"
param2 = "243"

result = subprocess.check_output([pythonPath, scriptPath, param1, param2], shell=True)
print result #returns as a string only  >>'['Motor', '243']'

result = system.util.jsonDecode(result) #turns string into python object
for r in result:
	print r
#>> 'Motor'
#>> '243'

Yep, that's basically it.

If you want to pass more complex data back and forth, ensure you've decided on an encoding scheme (e.g. JSON) and that both sides explicitly communicate with it. It should also be possible to send/retrieve arbitrary byte sequences over a subprocess link like this, but a text-based encoding will likely be easier to debug down the road.

1 Like

Note that starting CPython separately for each request will utterly crush performance. Which is there's advice to run a separate CPython script as a web service alongside Ignition, and then use httpClient to make web requests to localhost.

4 Likes

I read it as the other way: ignition would be requesting flask for data, therefore no Web dev needed

1 Like

You have a solution using subprocess but for posterity, I thought I'd post an example using a web service.

"""An example Ignition SCADA Gateway tag change event script that sends a simple JSON message to a flask
server when the production line starts and stops. The server runs alongside a tkinter window that
will shrink to a small semi-transparent button when the line starts running. The computer this runs
on was there for a reason, so obscuring the existing software was not an option. If there is an
unsaved defect record active in the window, it will pop back up when they stop so that they can update
it when finished.

This is quite quick. The line start has a ~5 second delay while a line-starting signal goes off. So, the
fraction of a second delay is nothing.

Ignition SCADA version == 8.1.17
"""

if not initialChange:
    src_path = event.getTagPath().toString()  # tag path that triggered this event

    lam_num = Shared.L.get_num_from_path(src_path)  # get the production line number from the path

# ip address for the industrial PC with the flask server depends on the line number
flask_server_url = {1: '10.155.0.102', 2: '10.155.0.201'}[lam_num]  # static IP addresses!
defects_url = 'http://{}:5000/popup'.format(flask_server_url)  # the address for for the flask endpoint

hc = system.net.httpClient()  # this class can be used for POST and GET requests

# if the lam has started running, hide the popup
if newValue.getValue():  # boolean tags, so if the line is running, this is True
    param_dict = {"action": "shrink"}
else:
    param_dict = {"action": "show"}

result = hc.post(defects_url, params=param_dict).json

gist

You can see the Flask server and all the related mess here: https://github.com/HelloMorrisMoss/mahlo_popup

When this was set up we hadn't gotten the WebDev module yet, so when the operator creates a new defect record, by pushing the button, the available tag data is read directly from the Postgres/Timescale
database with the tag history using the psycopg2 library. (Timescale simplifies this since it "looks" like a single table.)

An instance of this runs on two production lines and one on a server where it provides the defect record data for a Dash defect lookup web interface and an automated report converter that includes the records in the final reports.

But, it's not a good place to start. So here's a much-pared down all-in-one file version using Flask and Flask-RESTful. Probably only use Python 3.8 if you're stuck on Windows 7 Compact Embedded edition, too.

"""Example flask endpoint for receiving http requests from Ignition (or whatever).

Based on https://github.com/HelloMorrisMoss/mahlo_popup
Python version == 3.8.4
Flask==2.0.2
Flask-RESTful==0.3.9
"""

from flask import Flask
from flask_restful import reqparse, Resource, Api


action_dict = {  # shortened actions dictionary
    'shrink':
        {'debug_message': 'shrinking popup',  # this is used in logging
         'action_params': {'action': 'shrink'},  # this gets passed to the window
         'return_result': ({'popup_result': 'Shrinking popup window.'}, 200),  # sent back to post request
         'description': 'Shrink the window to button size, no change if already button.',  # just documentation atm
         },
    'show':
        {'debug_message': 'showing popup',
         'action_params': {'action': 'show'},
         'return_result': ({'popup_result': 'Showing popup window.'}, 200),
         'description': 'Show the full window if there are defects active, no change if already full.',
         }
    }


def action_function_dummy(action_data: dict):
    """The actual thing done here is to add the json->dict to a collections.deque for the tkinter
    window to check and act on."""

    print(f"I don't do anything! Here's my action data: {action_data=}")


class Popup(Resource):
    """This is used by flask-restful to provide end-point functionality."""
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('action', type=str, required=True, help='You must provide a command action.')

        data = parser.parse_args()
        action_to_take = action_dict.get(data['action'])  # get the action info from the dictionary above

        if action_to_take is not None:  # if the action data matches an action in the dict, do it
            action_function_dummy(action_to_take['action_params'])
            return action_to_take['return_result']
        else:
            return {'popup_result': 'No valid request.'}, 400  # or the action requested doesn't match, return that

    def get(self):
        """Can also have other methods on the same endpoint."""
        raise NotImplementedError('Build something here!')


# instantiate the flask app
app = Flask(__name__)

# add the restful endpoints
api = Api(app)
api.add_resource(Popup, '/popup')

if __name__ == '__main__':
    app.run(debug=True)

gist

3 Likes