Merge
authorTomaz Orac
Tue, 02 Feb 2021 13:47:48 +0100
changeset 2724 3f3f9dce9140
parent 2719 745b64e7c695 (diff)
parent 2723 cde2e410b874 (current diff)
child 2725 2b00f90c6888
Merge
modbus/modbus.py
--- a/BeremizIDE.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/BeremizIDE.py	Tue Feb 02 13:47:48 2021 +0100
@@ -103,7 +103,7 @@
     }
 else:
     faces = {
-        'mono': 'Courier',
+        'mono': 'FreeMono',
         'size': 10,
     }
 
--- a/CodeFileTreeNode.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/CodeFileTreeNode.py	Tue Feb 02 13:47:48 2021 +0100
@@ -207,9 +207,6 @@
                 variable.gettype(),
                 variable.getinitial())
                for variable in variables]
-        ret.extend([("On"+variable.getname()+"Change", "python_poll", "")
-                    for variable in variables
-                    if variable.getonchange()])
         return ret
 
     def CTNSearch(self, criteria):
--- a/ConfigTreeNode.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/ConfigTreeNode.py	Tue Feb 02 13:47:48 2021 +0100
@@ -46,6 +46,7 @@
 from xmlclass import GenerateParserFromXSDstring
 from PLCControler import LOCATION_CONFNODE
 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
+from POULibrary import UserAddressedException
 
 _BaseParamsParser = GenerateParserFromXSDstring("""<?xml version="1.0" encoding="ISO-8859-1" ?>
         <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
@@ -469,21 +470,21 @@
     def GetContextualMenuItems(self):
         return None
 
-    def GetView(self):
-        if self._View is None and self.EditorType is not None:
+    def GetView(self, onlyopened=False):
+        if self._View is None and not onlyopened and self.EditorType is not None:
             app_frame = self.GetCTRoot().AppFrame
             self._View = self.EditorType(app_frame.TabsOpened, self, app_frame)
 
         return self._View
 
     def _OpenView(self, name=None, onlyopened=False):
-        view = self.GetView()
+        view = self.GetView(onlyopened)
 
         if view is not None:
             if name is None:
                 name = self.CTNFullName()
             app_frame = self.GetCTRoot().AppFrame
-            app_frame.EditProjectElement(view, name)
+            app_frame.EditProjectElement(view, name, onlyopened)
 
         return view
 
--- a/ProjectController.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/ProjectController.py	Tue Feb 02 13:47:48 2021 +0100
@@ -159,6 +159,7 @@
                                                          no_stdout=True,
                                                          no_stderr=True).spin()
         except Exception:
+            self.logger.write_error(_("Couldn't launch IEC compiler to determine compatible options.\n"))
             return buildopt
 
         for opt in options:
@@ -254,7 +255,7 @@
         # Setup debug information
         self.IECdebug_datas = {}
 
-        self.DebugTimer = None
+        self.DebugUpdatePending = False
         self.ResetIECProgramsAndVariables()
 
         # In both new or load scenario, no need to save
@@ -276,8 +277,6 @@
         self.debug_status = PlcStatus.Stopped
 
     def __del__(self):
-        if self.DebugTimer:
-            self.DebugTimer.cancel()
         self.KillDebugThread()
 
     def LoadLibraries(self):
@@ -1538,7 +1537,6 @@
         return debug_status, ticks, buffers
 
     def RegisterDebugVarToConnector(self):
-        self.DebugTimer = None
         Idxs = []
         self.TracedIECPath = []
         self.TracedIECTypes = []
@@ -1577,25 +1575,15 @@
                 self._connector.SetTraceVariablesList([])
                 self.DebugToken = None
             self.debug_status, _debug_ticks, _buffers = self.SnapshotAndResetDebugValuesBuffers()
+        self.DebugUpdatePending = False
 
     def IsPLCStarted(self):
         return self.previous_plcstate == PlcStatus.Started
 
-    def ReArmDebugRegisterTimer(self):
-        if self.DebugTimer is not None:
-            self.DebugTimer.cancel()
-
-        # Prevent to call RegisterDebugVarToConnector when PLC is not started
-        # If an output location var is forced it's leads to segmentation fault in runtime
-        # Links between PLC located variables and real variables are not ready
-        if self.IsPLCStarted():
-            # Timer to prevent rapid-fire when registering many variables
-            # use wx.CallAfter use keep using same thread. TODO : use wx.Timer
-            # instead
-            self.DebugTimer = Timer(
-                0.5, wx.CallAfter, args=[self.RegisterDebugVarToConnector])
-            # Rearm anti-rapid-fire timer
-            self.DebugTimer.start()
+    def AppendDebugUpdate(self):
+        if not self.DebugUpdatePending :
+            wx.CallAfter(self.RegisterDebugVarToConnector)
+            self.DebugUpdatePending = True
 
     def GetDebugIECVariableType(self, IECPath):
         _Idx, IEC_Type = self._IECPathToIdx.get(IECPath, (None, None))
@@ -1625,7 +1613,7 @@
 
         IECdebug_data[0][callableobj] = buffer_list
 
-        self.ReArmDebugRegisterTimer()
+        self.AppendDebugUpdate()
 
         return IECdebug_data[1]
 
@@ -1641,12 +1629,12 @@
                     IECdebug_data[0].itervalues(),
                     False)
 
-        self.ReArmDebugRegisterTimer()
+        self.AppendDebugUpdate()
 
     def UnsubscribeAllDebugIECVariable(self):
         self.IECdebug_datas = {}
 
-        self.ReArmDebugRegisterTimer()
+        self.AppendDebugUpdate()
 
     def ForceDebugIECVariable(self, IECPath, fvalue):
         if IECPath not in self.IECdebug_datas:
@@ -1657,7 +1645,7 @@
         IECdebug_data[2] = "Forced"
         IECdebug_data[3] = fvalue
 
-        self.ReArmDebugRegisterTimer()
+        self.AppendDebugUpdate()
 
     def ReleaseDebugIECVariable(self, IECPath):
         if IECPath not in self.IECdebug_datas:
@@ -1668,7 +1656,7 @@
         IECdebug_data[2] = "Registered"
         IECdebug_data[3] = None
 
-        self.ReArmDebugRegisterTimer()
+        self.AppendDebugUpdate()
 
     def CallWeakcallables(self, IECPath, function_name, *cargs):
         data_tuple = self.IECdebug_datas.get(IECPath, None)
--- a/XSLTransform.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/XSLTransform.py	Tue Feb 02 13:47:48 2021 +0100
@@ -22,4 +22,7 @@
         # print(self.xslt.error_log)
         return res
 
+    def get_error_log(self):
+        return self.xslt.error_log
 
+
--- a/bacnet/bacnet.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/bacnet/bacnet.py	Tue Feb 02 13:47:48 2021 +0100
@@ -790,6 +790,6 @@
         runtimefile.close()
 
         return ([(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True,
-                ("runtime_bacnet_websettings_%s.py" % location_str, open(runtimefile_path, "rb")),
+                ("runtime_%s_bacnet_websettings.py" % location_str, open(runtimefile_path, "rb")),
         )
         #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle)
--- a/bacnet/web_settings.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/bacnet/web_settings.py	Tue Feb 02 13:47:48 2021 +0100
@@ -295,7 +295,7 @@
 
 
 # location_str is replaced by extension's value in CTNGenerateC call
-def _runtime_bacnet_websettings_%(location_str)s_init():
+def _runtime_%(location_str)s_bacnet_websettings_init():
     """
     # Callback function, called (by PLCObject.py) when a new PLC program
     # (i.e. XXX.so file) is transfered to the PLC runtime
@@ -383,7 +383,7 @@
 
 
 # location_str is replaced by extension's value in CTNGenerateC call
-def _runtime_bacnet_websettings_%(location_str)s_cleanup():
+def _runtime_%(location_str)s_bacnet_websettings_cleanup():
     """
     # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
     """
--- a/controls/CustomStyledTextCtrl.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/controls/CustomStyledTextCtrl.py	Tue Feb 02 13:47:48 2021 +0100
@@ -40,7 +40,7 @@
 else:
     faces = {
         'times': 'Times',
-        'mono':  'Courier',
+        'mono':  'FreeMono',
         'helv':  'Helvetica',
         'other': 'new century schoolbook',
         'size':  12,
--- a/controls/LogViewer.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/controls/LogViewer.py	Tue Feb 02 13:47:48 2021 +0100
@@ -339,7 +339,7 @@
         if wx.Platform == '__WXMSW__':
             self.Font = wx.Font(8, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='Courier New')
         else:
-            self.Font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='Courier')
+            self.Font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='FreeMono')
         self.MessagePanel.Bind(wx.EVT_LEFT_UP, self.OnMessagePanelLeftUp)
         self.MessagePanel.Bind(wx.EVT_RIGHT_UP, self.OnMessagePanelRightUp)
         self.MessagePanel.Bind(wx.EVT_LEFT_DCLICK, self.OnMessagePanelLeftDCLick)
--- a/docutil/docsvg.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/docutil/docsvg.py	Tue Feb 02 13:47:48 2021 +0100
@@ -34,16 +34,23 @@
     if wx.Platform == '__WXMSW__':
         from six.moves import winreg
         try:
-            svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
-                                           'Software\\Classes\\svgfile\\shell\\Inkscape\\command')
+                svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
+                                               'Software\\Classes\\svgfile\\shell\\Inkscape\\command')
         except OSError:
-            svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
-                                           'Software\\Classes\\inkscape.svg\\shell\\open\\command')
+            try:
+                svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
+                                               'Software\\Classes\\inkscape.svg\\shell\\open\\command')
+            except Exception:
+                return None
+
         svgexepath = svgexepath.replace('"%1"', '').strip()
         return svgexepath.replace('"', '')
     else:
-        # TODO: search path
-        return os.path.join("/usr/bin", "inkscape")
+        # TODO: search for inkscape in $PATH
+        svgexepath = os.path.join("/usr/bin", "inkscape")
+        if os.path.exists(svgexepath):
+            return svgexepath
+        return None
 
 
 def open_win_svg(svgexepath, svgfile):
--- a/editors/ConfTreeNodeEditor.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/editors/ConfTreeNodeEditor.py	Tue Feb 02 13:47:48 2021 +0100
@@ -48,7 +48,7 @@
 else:
     faces = {
         'times': 'Times',
-        'mono':  'Courier',
+        'mono':  'FreeMono',
         'helv':  'Helvetica',
         'other': 'new century schoolbook',
         'size':  18,
--- a/editors/Viewer.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/editors/Viewer.py	Tue Feb 02 13:47:48 2021 +0100
@@ -82,7 +82,7 @@
 else:
     faces = {
         'times': 'Times',
-        'mono':  'Courier',
+        'mono':  'FreeMono',
         'helv':  'Helvetica',
         'other': 'new century schoolbook',
         'size':  10,
--- a/etherlab/EthercatMaster.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/etherlab/EthercatMaster.py	Tue Feb 02 13:47:48 2021 +0100
@@ -97,6 +97,8 @@
         return ((["etherlab_ext"], [(Gen_etherlabfile_path, IECCFLAGS)], True), "",
                 ("runtime_etherlab.py", open(GetLocalPath("runtime_etherlab.py"))))
 
+                # TODO : rename to match runtime_{location}_extname.py format
+
 # --------------------------------------------------
 #                 Ethercat MASTER
 # --------------------------------------------------
--- a/etherlab/runtime_etherlab.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/etherlab/runtime_etherlab.py	Tue Feb 02 13:47:48 2021 +0100
@@ -114,6 +114,7 @@
         time.sleep(0.5)
 
 
+# TODO : rename to match _runtime_{location}_extname_init() format
 def _runtime_etherlab_init():
     global KMSGPollThread, StopKMSGThread
     StopKMSGThread = False
@@ -121,6 +122,7 @@
     KMSGPollThread.start()
 
 
+# TODO : rename to match _runtime_{location}_extname_cleanup() format
 def _runtime_etherlab_cleanup():
     global KMSGPollThread, StopKMSGThread, SDOThread
     try:
--- a/modbus/modbus.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/modbus/modbus.py	Tue Feb 02 13:47:48 2021 +0100
@@ -1257,5 +1257,5 @@
         runtimefile.close()
 
         return ([(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True,
-                ("runtime_modbus_websettings_%s.py" % location_str, open(runtimefile_path, "rb")),
+                ("runtime_%s_modbus_websettings.py" % location_str, open(runtimefile_path, "rb")),
         )
--- a/modbus/web_settings.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/modbus/web_settings.py	Tue Feb 02 13:47:48 2021 +0100
@@ -183,7 +183,7 @@
     ("baud"             , _("Baud Rate")             , ctypes.c_int,       MB_Baud         ),
     ("parity"           , _("Parity")                , ctypes.c_int,       MB_Parity       ),
     ("stop_bits"        , _("Stop Bits")             , ctypes.c_int,       MB_StopBits     ),
-    ("slave_id"         , _("Slave ID")              , ctypes.c_ulonglong, annotate.Integer)
+    ("slave_id"         , _("Slave ID")              , ctypes.c_ubyte,     annotate.Integer)
     ]
 
 
@@ -228,7 +228,7 @@
         # This allows us to confirm the saved data contains the correct addr_type
         # when loading from file
         save_info = {}
-        save_info["addr_type"] = ["addr_type"]
+        save_info["addr_type"] = WebNode_entry["addr_type"]
         save_info["node_type"] = WebNode_entry["node_type"]
         save_info["config"   ] = newConfig
         
@@ -262,7 +262,7 @@
     filename = _WebNodeList[WebNode_id]["filename"]
     try:
         #if os.path.isfile(filename):
-        save_info = json.load(open(filename))
+        save_info = json.load(open(os.path.realpath(filename)))
     except Exception:    
         return None
 
@@ -526,7 +526,7 @@
 
 
 
-def _runtime_modbus_websettings_%(location_str)s_init():
+def _runtime_%(location_str)s_modbus_websettings_init():
     """
     Callback function, called (by PLCObject.py) when a new PLC program
     (i.e. XXX.so file) is transfered to the PLC runtime
@@ -609,7 +609,7 @@
 
 
 
-def _runtime_modbus_websettings_%(location_str)s_cleanup():
+def _runtime_%(location_str)s_modbus_websettings_cleanup():
     """
     Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
     """
--- a/plcopen/plcopen.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/plcopen/plcopen.py	Tue Feb 02 13:47:48 2021 +0100
@@ -331,12 +331,16 @@
 
 
 def SaveProject(project, filepath):
-    project_file = open(filepath, 'w')
-    project_file.write(etree.tostring(
+    content = etree.tostring(
         project,
         pretty_print=True,
         xml_declaration=True,
-        encoding='utf-8'))
+        encoding='utf-8')
+
+    assert len(content) != 0
+        
+    project_file = open(filepath, 'w')
+    project_file.write(content)
     project_file.close()
 
 
--- a/py_ext/PythonFileCTNMixin.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/py_ext/PythonFileCTNMixin.py	Tue Feb 02 13:47:48 2021 +0100
@@ -36,6 +36,8 @@
 from py_ext.PythonEditor import PythonEditor
 
 
+
+
 class PythonFileCTNMixin(CodeFile):
 
     CODEFILE_NAME = "PyFile"
@@ -95,19 +97,41 @@
                getattr(self.CodeFile, section).getanyText() + "\n" + \
                self.PostSectionsTexts.get(section, "")
 
+    def CTNGlobalInstances(self):
+        variables = self.CodeFileVariables(self.CodeFile)
+        ret = [(variable.getname(),
+                variable.gettype(),
+                variable.getinitial())
+               for variable in variables]
+        location_str = "_".join(map(str, self.GetCurrentLocation()))
+        ret.append(("On_"+location_str+"_Change", "python_poll", ""))
+        return ret
+
+    @staticmethod
+    def GetVarOnChangeContent(var):
+        """
+        returns given variable onchange field
+        function is meant to allow customization 
+        """
+        return var.getonchange()
+
     def CTNGenerate_C(self, buildpath, locations):
         # location string for that CTN
         location_str = "_".join(map(str, self.GetCurrentLocation()))
         configname = self.GetCTRoot().GetProjectConfigNames()[0]
 
         def _onchangecode(var):
-            return '"' + var.getonchange() + \
-                "('" + var.getname() + "')\"" \
-                if var.getonchange() else '""'
+            result = []
+            for onchangecall in self.GetVarOnChangeContent(var).split(','):
+                onchangecall = onchangecall.strip()
+                if onchangecall:
+                    result.append(onchangecall + "('" + var.getname() + "')")
+            return result
+
 
         def _onchange(var):
-            return repr(var.getonchange()) \
-                if var.getonchange() else None
+            content = self.GetVarOnChangeContent(var)
+            return repr(content) if content else None
 
         pyextname = self.CTNName()
         varinfos = map(
@@ -124,6 +148,9 @@
                 "pyextname": pyextname
             },
             self.CodeFile.variables.variable)
+
+        onchange_var_count = len([None for varinfo in varinfos if varinfo["onchange"]])
+
         # python side PLC global variables access stub
         globalstubs = "\n".join([
             """\
@@ -142,8 +169,21 @@
     %(desc)s,
     %(onchange)s,
     %(opts)s))
-""" % varinfo for varinfo in varinfos])
-
+""" % varinfo + ("""
+_PyOnChangeCount_%(name)s = ctypes.c_uint.in_dll(PLCBinary,"__%(name)s_onchange_count")
+_PyOnChangeFirst_%(name)s = _%(name)s_ctype.in_dll(PLCBinary,"__%(name)s_onchange_firstval")
+_PyOnChangeLast_%(name)s = _%(name)s_ctype.in_dll(PLCBinary,"__%(name)s_onchange_lastval")
+""" % varinfo if varinfo["onchange"] else "") for varinfo in varinfos])
+
+        on_change_func_body = "\n".join(["""
+    if _PyOnChangeCount_%(name)s.value > 0:
+        # %(name)s
+        try:""" % varinfo + """
+            """ + """
+            """.join(varinfo['onchangecode'])+"""
+        except Exception as e:
+            errors.append("%(name)s: "+str(e))
+""" % varinfo for varinfo in varinfos if varinfo["onchange"]])
         # Runtime calls (start, stop, init, and cleanup)
         rtcalls = ""
         for section in self.SECTIONS_NAMES:
@@ -163,6 +203,9 @@
             "globalstubs": globalstubs,
             "globalsection": globalsection,
             "rtcalls": rtcalls,
+            "location_str": location_str,
+            "on_change_func_body":on_change_func_body,
+            "onchange_var_count": onchange_var_count
         }
 
         PyFileContent = """\
@@ -174,6 +217,11 @@
 ## Code for PLC global variable access
 from runtime.typemapping import TypeTranslator
 import ctypes
+
+_PySafeGetChanges_%(pyextname)s = PLCBinary.PySafeGetChanges_%(location_str)s
+_PySafeGetChanges_%(pyextname)s.restype = None
+_PySafeGetChanges_%(pyextname)s.argtypes = None
+
 _%(pyextname)sGlobalsDesc = []
 __ext_name__ = "%(pyextname)s"
 PLCGlobalsDesc.append(( "%(pyextname)s" , _%(pyextname)sGlobalsDesc ))
@@ -185,6 +233,13 @@
 ## Beremiz python runtime calls
 %(rtcalls)s
 
+def On_%(pyextname)s_Change():
+    _PySafeGetChanges_%(pyextname)s()
+    errors = []
+%(on_change_func_body)s
+    if len(errors)>0 :
+        raise Exception("Exception in %(pyextname)s OnChange call:\\\\n" + "\\\\n".join(errors))
+
 del __ext_name__
 
 """ % loc_dict
@@ -220,7 +275,12 @@
 """
 
         vardeconchangefmt = """\
-PYTHON_POLL* __%(name)s_notifier;
+unsigned int __%(name)s_rbuffer_written = 0;
+IEC_%(IECtype)s __%(name)s_rbuffer_firstval;
+IEC_%(IECtype)s __%(name)s_rbuffer_lastval;
+unsigned int __%(name)s_onchange_count = 0;
+IEC_%(IECtype)s __%(name)s_onchange_firstval;
+IEC_%(IECtype)s __%(name)s_onchange_lastval;
 """
 
         varretfmt = """\
@@ -243,16 +303,27 @@
     if(!AtomicCompareExchange(&__%(name)s_rlock, 0, 1)){
         IEC_%(IECtype)s tmp = __GET_VAR(%(configname)s__%(uppername)s);
         if(NE_%(IECtype)s(1, NULL, __%(name)s_rbuffer, tmp)){
+            if(__%(name)s_rbuffer_written == 0);
+                __%(name)s_rbuffer_firstval = __%(name)s_rbuffer;
+            __%(name)s_rbuffer_lastval = tmp;
             __%(name)s_rbuffer = tmp;
-            PYTHON_POLL_body__(__%(name)s_notifier);
+            /* count one more change */
+            __%(name)s_rbuffer_written += 1;
+            some_change_found = 1;
         }
         AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
     }
 """
-        varinitonchangefmt = """\
-    __%(name)s_notifier = __GET_GLOBAL_ON%(uppername)sCHANGE();
-    __SET_VAR(__%(name)s_notifier->,TRIG,,__BOOL_LITERAL(TRUE));
-    __SET_VAR(__%(name)s_notifier->,CODE,,__STRING_LITERAL(%(onchangelen)d,%(onchangecode)s));
+
+        varcollectchangefmt = """\
+    while(AtomicCompareExchange(&__%(name)s_rlock, 0, 1));
+    __%(name)s_onchange_count = __%(name)s_rbuffer_written;
+    __%(name)s_onchange_firstval = __%(name)s_rbuffer_firstval;
+    __%(name)s_onchange_lastval = __%(name)s_rbuffer_lastval;
+    /* mark variable as unchanged */
+    __%(name)s_rbuffer_written = 0;
+    AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
+
 """
         vardec = "\n".join([(vardecfmt + vardeconchangefmt
                              if varinfo["onchange"] else vardecfmt) % varinfo
@@ -261,16 +332,20 @@
         varpub = "\n".join([(varpubonchangefmt if varinfo["onchange"] else
                              varpubfmt) % varinfo
                             for varinfo in varinfos])
-        varinit = "\n".join([varinitonchangefmt %
-                             dict(onchangelen=len(varinfo["onchangecode"]), **varinfo)
+        varcollectchange = "\n".join([varcollectchangefmt % varinfo
                              for varinfo in varinfos if varinfo["onchange"]])
 
+        pysafe_pypoll_code = "On_"+pyextname+"_Change()"
+
         loc_dict = {
             "vardec": vardec,
-            "varinit": varinit,
             "varret": varret,
             "varpub": varpub,
             "location_str": location_str,
+            "pysafe_pypoll_code": '"'+pysafe_pypoll_code+'"',
+            "pysafe_pypoll_code_len": len(pysafe_pypoll_code),
+            "varcollectchange": varcollectchange,
+            "onchange_var_count": onchange_var_count
         }
 
         # TODO : use config name obtained from model instead of default
@@ -286,12 +361,17 @@
 #include "config.h"
 #include "beremiz.h"
 
+PYTHON_POLL* __%(location_str)s_notifier;
+
 /* User variables reference */
 %(vardec)s
 
 /* Beremiz confnode functions */
 int __init_%(location_str)s(int argc,char **argv){
-%(varinit)s
+    __%(location_str)s_notifier = __GET_GLOBAL_ON_%(location_str)s_CHANGE();
+    __SET_VAR(__%(location_str)s_notifier->,TRIG,,__BOOL_LITERAL(TRUE));
+    __SET_VAR(__%(location_str)s_notifier->,CODE,,__STRING_LITERAL(%(pysafe_pypoll_code_len)d,%(pysafe_pypoll_code)s));
+
     return 0;
 }
 
@@ -302,9 +382,22 @@
 %(varret)s
 }
 
+static int passing_changes_to_python = 0;
 void __publish_%(location_str)s(void){
+    int some_change_found = 0;
 %(varpub)s
-}
+    passing_changes_to_python |= some_change_found;
+    // call python part if there was at least a change
+    if(passing_changes_to_python){
+        PYTHON_POLL_body__(__%(location_str)s_notifier);
+        passing_changes_to_python &= !(__GET_VAR(__%(location_str)s_notifier->ACK,));
+    }
+}
+
+void* PySafeGetChanges_%(location_str)s(void){
+%(varcollectchange)s
+}
+
 """ % loc_dict
 
         Gen_PyCfile_path = os.path.join(buildpath, "PyCFile_%s.c" % location_str)
--- a/runtime/NevowServer.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/runtime/NevowServer.py	Tue Feb 02 13:47:48 2021 +0100
@@ -39,6 +39,7 @@
 
 import util.paths as paths
 from runtime.loglevels import LogLevels, LogLevelsDict
+from runtime import MainWorker, GetPLCObjectSingleton
 
 PAGE_TITLE = 'Beremiz Runtime Web Interface'
 
@@ -217,6 +218,18 @@
                                                "Send a message to the log"),
                                            action=_("Send"))
 
+    # pylint: disable=no-self-argument
+    def restartOrRepairPLC(
+            ctx=annotate.Context(),
+            action=annotate.Choice(["Restart", "Repair"],
+                                  required=True,
+                                  label=_("Action"))):
+        pass
+
+    restartOrRepairPLC = annotate.autocallable(restartOrRepairPLC,
+                                           label=_(
+                                               "Restart or Repair"),
+                                           action=_("Do"))
 
 customSettingsURLs = {
 }
@@ -284,9 +297,16 @@
 
     def sendLogMessage(self, level, message, **kwargs):
         level = LogLevelsDict[level]
-        if _PySrv.plcobj is not None:
-            _PySrv.plcobj.LogMessage(
-                level, "Web form log message: " + message)
+        GetPLCObjectSingleton().LogMessage(
+            level, "Web form log message: " + message)
+
+    def restartOrRepairPLC(self, action, **kwargs):
+        if(action == "Repair"):
+            GetPLCObjectSingleton().RepairPLC()
+        else:
+            MainWorker.quit()
+            
+        
 
     def locateChild(self, ctx, segments):
         if segments[0] in customSettingsURLs:
--- a/runtime/PLCObject.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/runtime/PLCObject.py	Tue Feb 02 13:47:48 2021 +0100
@@ -360,8 +360,18 @@
                 v = parent.python_runtime_vars["_"+name+"_pack"](t, value)
                 parent.python_runtime_vars["_PySafeSetPLCGlob_"+name](ctypes.byref(v))
 
+        class OnChangeStateClass(object):
+            def __getattr__(self, name):
+                u = parent.python_runtime_vars["_"+name+"_unpack"]
+                return type("changedesc",(),dict(
+                    count = parent.python_runtime_vars["_PyOnChangeCount_"+name].value,
+                    first = u(parent.python_runtime_vars["_PyOnChangeFirst_"+name]),
+                    last = u(parent.python_runtime_vars["_PyOnChangeLast_"+name])))
+
+
         self.python_runtime_vars.update({
             "PLCGlobals":     PLCSafeGlobals(),
+            "OnChange":       OnChangeStateClass(),
             "WorkingDir":     self.workingdir,
             "PLCObject":      self,
             "PLCBinary":      self.PLClibraryHandle,
@@ -440,6 +450,7 @@
 
             if cmd == "Activate":
                 self.PythonRuntimeCall("start")
+                self.PreStartPLC()
                 self.PythonThreadLoop()
                 self.PythonRuntimeCall("stop", reverse_order=True)
             else:  # "Finish"
@@ -471,8 +482,6 @@
             if not self.LoadPLC():
                 self._fail(_("Problem starting PLC : can't load PLC"))
 
-        self.PreStartPLC()
-
         if self.CurrentPLCFilename is not None and self.PLCStatus == PlcStatus.Stopped:
             c_argv = ctypes.c_char_p * len(self.argv)
             res = self._startPLC(len(self.argv), c_argv(*self.argv))
@@ -546,12 +555,15 @@
             os.close(fd)
         self._init_blobs()
 
-    def _BlobAsFile(self, blobID, newpath):
+    def BlobAsFile(self, blobID, newpath):
         blob = self.blobs.pop(blobID, None)
 
         if blob is None:
             raise Exception(_("Missing data to create file: {}").format(newpath))
 
+        self._BlobAsFile(blob, newpath)
+
+    def _BlobAsFile(self, blob, newpath):
         fd, path, _md5sum = blob
         fobj = os.fdopen(fd)
         fobj.flush()
@@ -610,13 +622,13 @@
 
             try:
                 # Create new PLC file
-                self._BlobAsFile(plc_object, new_PLC_filename)
+                self.BlobAsFile(plc_object, new_PLC_filename)
 
                 # Then write the files
                 log = open(extra_files_log, "w")
                 for fname, blobID in extrafiles:
                     fpath = os.path.join(self.workingdir, fname)
-                    self._BlobAsFile(blobID, fpath)
+                    self.BlobAsFile(blobID, fpath)
                     log.write(fname+'\n')
 
                 # Store new PLC filename based on md5 key
--- a/targets/plc_debug.c	Thu Jan 28 14:51:16 2021 +0000
+++ b/targets/plc_debug.c	Tue Feb 02 13:47:48 2021 +0100
@@ -100,7 +100,7 @@
 void __init_debug(void)
 {
     /* init local static vars */
-#ifndef TARGET_ONLINE_DEBUG_DISABLE	
+#ifndef TARGET_ONLINE_DEBUG_DISABLE
     buffer_cursor = debug_buffer;
     buffer_state = BUFFER_FREE;
 #endif
@@ -109,9 +109,9 @@
     InitRetain();
     /* Iterate over all variables to fill debug buffer */
     if(CheckRetainBuffer()){
-    	__for_each_variable_do(RemindIterator);
+        __for_each_variable_do(RemindIterator);
     }else{
-    	char mstr[] = "RETAIN memory invalid - defaults used";
+        char mstr[] = "RETAIN memory invalid - defaults used";
         LogMessage(LOG_WARNING, mstr, sizeof(mstr));
     }
     retain_offset = 0;
@@ -124,7 +124,7 @@
 
 void __cleanup_debug(void)
 {
-#ifndef TARGET_ONLINE_DEBUG_DISABLE	
+#ifndef TARGET_ONLINE_DEBUG_DISABLE
     buffer_cursor = debug_buffer;
     InitiateDebugTransfer();
 #endif    
@@ -150,16 +150,14 @@
     if(flags & ( __IEC_DEBUG_FLAG | __IEC_RETAIN_FLAG)){
         USINT size = __get_type_enum_size(dsc->type);
 
-#ifndef TARGET_ONLINE_DEBUG_DISABLE	
+#ifndef TARGET_ONLINE_DEBUG_DISABLE
         if(flags & __IEC_DEBUG_FLAG){
             /* copy visible variable to buffer */;
             if(do_debug){
                 /* compute next cursor positon.
                    No need to check overflow, as BUFFER_SIZE
                    is computed large enough */
-		if((dsc->type == STRING_ENUM)   ||
-		   (dsc->type == STRING_P_ENUM) ||
-		   (dsc->type == STRING_O_ENUM)){
+                if(__Is_a_string(dsc)){
                     /* optimization for strings */
                     size = ((STRING*)visible_value_p)->len + 1;
                 }
@@ -174,7 +172,7 @@
                 memcpy(real_value_p, visible_value_p, size);
             }
         }
-#endif	
+#endif
 
         if(flags & __IEC_RETAIN_FLAG){
             /* compute next cursor positon*/
--- a/targets/var_access.c	Thu Jan 28 14:51:16 2021 +0000
+++ b/targets/var_access.c	Tue Feb 02 13:47:48 2021 +0100
@@ -14,6 +14,10 @@
             forced_value_p = &((__IEC_##TYPENAME##_p *)varp)->fvalue;\
             break;
 
+#define __Is_a_string(dsc) (dsc->type == STRING_ENUM)   ||\
+                           (dsc->type == STRING_P_ENUM) ||\
+                           (dsc->type == STRING_O_ENUM)
+
 static void* UnpackVar(__Unpack_desc_type *dsc, void **real_value_p, char *flags)
 {
     void *varp = dsc->ptr;
--- a/tests/python/plc.xml	Thu Jan 28 14:51:16 2021 +0000
+++ b/tests/python/plc.xml	Tue Feb 02 13:47:48 2021 +0100
@@ -1,7 +1,7 @@
 <?xml version='1.0' encoding='utf-8'?>
 <project xmlns="http://www.plcopen.org/xml/tc6_0201" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xhtml="http://www.w3.org/1999/xhtml" xsi:schemaLocation="http://www.plcopen.org/xml/tc6_0201">
   <fileHeader companyName="" productName="Beremiz" productVersion="0.0" creationDateTime="2008-12-14T16:21:19" contentDescription="This example shows many features in Beremiz:&#10;&#10;   1. How to implement python extensions.&#10;   2. How to implement basic C extension.&#10;   3. How to use C code in IEC POUs.&#10;   4. How to call C functions from python code.&#10;   5. How to avoid race conditions between IEC, C and python code.&#10;   6. How to convert betweet different IEC types.&#10;"/>
-  <contentHeader name="Beremiz Python Support Tests" modificationDateTime="2020-06-17T13:19:14">
+  <contentHeader name="Beremiz Python Support Tests" modificationDateTime="2020-10-19T23:53:08">
     <coordinateInfo>
       <pageSize x="1024" y="1024"/>
       <fbd>
--- a/tests/python/py_ext_0@py_ext/pyfile.xml	Thu Jan 28 14:51:16 2021 +0000
+++ b/tests/python/py_ext_0@py_ext/pyfile.xml	Tue Feb 02 13:47:48 2021 +0100
@@ -1,8 +1,8 @@
 <?xml version='1.0' encoding='utf-8'?>
 <PyFile xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <variables>
-    <variable name="SomeVarName" type="DINT" onchange="MyFunc"/>
-    <variable name="Grumpf" type="STRING" initial="'mhoo'" onchange="MyFunc"/>
+    <variable name="SomeVarName" type="DINT" onchange="MyFunc, SomeChange"/>
+    <variable name="Grumpf" type="STRING" initial="'mhoo'" onchange="MyFunc, MyOtherFunc"/>
   </variables>
   <globals>
     <xhtml:p><![CDATA[
@@ -12,6 +12,15 @@
 def MyFunc(*args):
     print args
 
+def MyOtherFunc(*args):
+    print "other", args
+
+def SomeChange(*args):
+    print "count",OnChange.SomeVarName.count
+    print "first",OnChange.SomeVarName.first
+    print "last",OnChange.SomeVarName.last
+
+
 ]]></xhtml:p>
   </globals>
   <init>
--- a/util/ProcessLogger.py	Thu Jan 28 14:51:16 2021 +0000
+++ b/util/ProcessLogger.py	Tue Feb 02 13:47:48 2021 +0100
@@ -78,7 +78,6 @@
                  timeout=None, outlimit=None, errlimit=None,
                  endlog=None, keyword=None, kill_it=False, cwd=None,
                  encoding=None, output_encoding=None):
-        assert(logger)
         self.logger = logger
         if not isinstance(Command, list):
             self.Command_str = Command
@@ -160,7 +159,7 @@
             v = v.decode(self.output_encoding)
         self.outdata.append(v)
         self.outlen += 1
-        if not self.no_stdout:
+        if self.logger and not self.no_stdout:
             self.logger.write(v)
         if (self.keyword and v.find(self.keyword) != -1) or (self.outlimit and self.outlen > self.outlimit):
             self.endlog()
@@ -170,7 +169,7 @@
             v = v.decode(self.output_encoding)
         self.errdata.append(v)
         self.errlen += 1
-        if not self.no_stderr:
+        if self.logger and not self.no_stderr:
             self.logger.write_warning(v)
         if self.errlimit and self.errlen > self.errlimit:
             self.endlog()