Python runtime: call "OnIdle" tasks when py_eval FBs execution queue is empty.
authorEdouard Tisserant <edouard@beremiz.fr>
Fri, 07 Feb 2025 10:52:09 +0100 (5 weeks ago)
changeset 4104 9e59bb5ad9e1
parent 4095 5d86ede7384a
child 4105 79aa1772f491
Python runtime: call "OnIdle" tasks when py_eval FBs execution queue is empty.

This is usefull to execute slow operations that can be deffered from functions called by py_eval FBs.
User python code add a callable to "OnIdle" list made available in global scope.
py_ext/plc_python.c
runtime/PLCObject.py
--- a/py_ext/plc_python.c	Fri Jan 24 15:53:11 2025 +0100
+++ b/py_ext/plc_python.c	Fri Feb 07 10:52:09 2025 +0100
@@ -156,7 +156,7 @@
 	}
 }
 
-char* PythonIterator(char* result, void** id)
+char* PythonIterator(char* result, void** id, int* is_last)
 {
 	char* next_command;
 	PYTHON_EVAL* data__;
@@ -213,6 +213,8 @@
 	/* next command is BUFFER */
 	next_command = (char*)__GET_VAR(data__->BUFFER, .body);
 	*id=data__;
+    /*check if last command in the queue */
+	*is_last = EvalFBs[(Current_Python_EvalFB + 1) %% %(python_eval_fb_count)d] == NULL;
 	/* free python mutex */
 	UnLockPython();
 	/* return the next command to eval */
--- a/runtime/PLCObject.py	Fri Jan 24 15:53:11 2025 +0100
+++ b/runtime/PLCObject.py	Fri Feb 07 10:52:09 2025 +0100
@@ -207,7 +207,7 @@
             self._PythonIterator = getattr(self.PLClibraryHandle, "PythonIterator", None)
             if self._PythonIterator is not None:
                 self._PythonIterator.restype = ctypes.c_char_p
-                self._PythonIterator.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)]
+                self._PythonIterator.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int)]
 
                 self._stopPLC = self._stopPLC_real
             else:
@@ -215,7 +215,7 @@
                 # as a call that block pythonthread until StopPLC
                 self.PlcStopping = Event()
 
-                def PythonIterator(res, blkid):
+                def PythonIterator(res, blkid, is_last):
                     self.PlcStopping.clear()
                     self.PlcStopping.wait()
                     return None
@@ -307,7 +307,7 @@
         self._GetDebugData = lambda: -1
         self._suspendDebug = lambda x: -1
         self._resumeDebug = lambda: None
-        self._PythonIterator = lambda: ""
+        self._PythonIterator = lambda *a: ""
         self._GetLogCount = None
         self._LogMessage = None
         self._GetLogMessage = None
@@ -389,7 +389,8 @@
             "WorkingDir":     self.workingdir,
             "PLCObject":      self,
             "PLCBinary":      self.PLClibraryHandle,
-            "PLCGlobalsDesc": []})
+            "PLCGlobalsDesc": [],
+            "OnIdle":         []})
 
         for methodname in MethodNames:
             self.python_runtime_vars["_runtime_%s" % methodname] = []
@@ -429,11 +430,12 @@
         self.python_runtime_vars = None
 
     def PythonThreadLoop(self):
-        res, cmd, blkid = "None", "None", ctypes.c_void_p()
+        res, cmd, blkid, is_last = "None", "None", ctypes.c_void_p(), ctypes.c_int()
         compile_cache = {}
         while True:
-            cmd = self._PythonIterator(res.encode(), blkid)
+            cmd = self._PythonIterator(res.encode(), blkid, ctypes.byref(is_last))
             FBID = blkid.value
+            GOING_IDLE = is_last.value != 0
             if cmd is None:
                 break
             cmd = cmd.decode()
@@ -455,6 +457,11 @@
                 res = "#EXCEPTION : "+str(e)
                 self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (FBID, cmd, str(e)))
 
+            if GOING_IDLE:
+                todo = self.python_runtime_vars["OnIdle"]
+                while todo:
+                    todo.pop(0)()
+
     def PythonThreadProc(self):
         while True:
             self.PythonThreadCondLock.acquire()