merged Denis change to apply input as we type in CTN configuration forms text fieds
authorEdouard Tisserant
Thu, 19 Apr 2018 15:03:23 +0200
changeset 1998 12b20698e4f3
parent 1997 d9e8fb47340f (diff)
parent 1991 34a9287b6c7d (current diff)
child 1999 36a624779f9f
merged Denis change to apply input as we type in CTN configuration forms text fieds
--- a/Beremiz.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/Beremiz.py	Thu Apr 19 15:03:23 2018 +0200
@@ -48,6 +48,7 @@
         self.splashPath = self.Bpath("images", "splash.png")
         self.modules = ["BeremizIDE"]
         self.debug = os.path.exists("BEREMIZ_DEBUG")
+        self.handle_exception = None
 
     def Bpath(self, *args):
         return os.path.join(self.app_dir, *args)
@@ -115,9 +116,13 @@
 
     def ShowSplashScreen(self):
         class Splash(AdvancedSplash):
+            Painted = False
+
             def OnPaint(_self, event):  # pylint: disable=no-self-argument
                 AdvancedSplash.OnPaint(_self, event)
-                wx.CallAfter(self.AppStart)
+                if not _self.Painted:  # trigger app start only once
+                    _self.Painted = True
+                    wx.CallAfter(self.AppStart)
         bmp = wx.Image(self.splashPath).ConvertToBitmap()
         self.splash = Splash(None,
                              bitmap=bmp,
@@ -198,10 +203,13 @@
             self.CreateUI()
             self.CloseSplash()
             self.ShowUI()
-        # except (KeyboardInterrupt, SystemExit):
-        #     raise
+        except (KeyboardInterrupt, SystemExit):
+            raise
         except Exception:
-            self.handle_exception(*sys.exc_info(), exit=True)
+            if self.handle_exception is not None:
+                self.handle_exception(*sys.exc_info(), exit=True)
+            else:
+                raise
 
     def MainLoop(self):
         self.app.MainLoop()
--- a/Beremiz_service.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/Beremiz_service.py	Thu Apr 19 15:03:23 2018 +0200
@@ -30,12 +30,13 @@
 import sys
 import getopt
 import threading
-from threading import Thread, currentThread, Semaphore
+from threading import Thread, currentThread, Semaphore, Lock
 import traceback
 import __builtin__
+import Pyro
 import Pyro.core as pyro
 
-from runtime import PLCObject, ServicePublisher
+from runtime import PLCObject, ServicePublisher, MainWorker
 import util.paths as paths
 
 
@@ -401,7 +402,7 @@
 
 class Server(object):
     def __init__(self, servicename, ip_addr, port,
-                 workdir, argv, autostart=False,
+                 workdir, argv,
                  statuschange=None, evaluator=default_evaluator,
                  pyruntimevars=None):
         self.continueloop = True
@@ -411,22 +412,48 @@
         self.port = port
         self.workdir = workdir
         self.argv = argv
-        self.plcobj = None
         self.servicepublisher = None
-        self.autostart = autostart
         self.statuschange = statuschange
         self.evaluator = evaluator
         self.pyruntimevars = pyruntimevars
-
-    def Loop(self):
+        self.plcobj = PLCObject(self)
+
+    def _to_be_published(self):
+        return self.servicename is not None and \
+               self.ip_addr is not None and \
+               self.ip_addr != "localhost" and \
+               self.ip_addr != "127.0.0.1"
+
+    def PrintServerInfo(self):
+        print(_("Pyro port :"), self.port)
+
+        # Beremiz IDE detects LOCAL:// runtime is ready by looking
+        # for self.workdir in the daemon's stdout.
+        print(_("Current working directory :"), self.workdir)
+
+        if self._to_be_published():
+            print(_("Publishing service on local network"))
+
+        sys.stdout.flush()
+
+    def PyroLoop(self, when_ready):
         while self.continueloop:
+            Pyro.config.PYRO_MULTITHREADED = 0
             pyro.initServer()
             self.daemon = pyro.Daemon(host=self.ip_addr, port=self.port)
+
             # pyro never frees memory after connection close if no timeout set
             # taking too small timeout value may cause
             # unwanted diconnection when IDE is kept busy for long periods
             self.daemon.setTimeout(60)
-            self.Start()
+
+            self.daemon.connect(self.plcobj, "PLCObject")
+
+            if self._to_be_published():
+                self.servicepublisher = ServicePublisher.ServicePublisher()
+                self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port)
+
+            when_ready()
             self.daemon.requestLoop()
             self.daemon.sock.close()
 
@@ -440,39 +467,6 @@
             self.plcobj.UnLoadPLC()
         self._stop()
 
-    def Start(self):
-        self.plcobj = PLCObject(self.workdir, self.daemon, self.argv,
-                                self.statuschange, self.evaluator,
-                                self.pyruntimevars)
-
-        uri = self.daemon.connect(self.plcobj, "PLCObject")
-
-        print(_("Pyro port :"), self.port)
-        print(_("Pyro object's uri :"), uri)
-
-        # Beremiz IDE detects daemon start by looking
-        # for self.workdir in the daemon's stdout.
-        # Therefore don't delete the following line
-        print(_("Current working directory :"), self.workdir)
-
-        # Configure and publish service
-        # Not publish service if localhost in address params
-        if self.servicename is not None and \
-           self.ip_addr is not None and \
-           self.ip_addr != "localhost" and \
-           self.ip_addr != "127.0.0.1":
-            print(_("Publishing service on local network"))
-            self.servicepublisher = ServicePublisher.ServicePublisher()
-            self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port)
-
-        self.plcobj.AutoLoad()
-        if self.plcobj.GetPLCstatus()[0] != "Empty":
-            if self.autostart:
-                self.plcobj.StartPLC()
-        self.plcobj.StatusChange()
-
-        sys.stdout.flush()
-
     def _stop(self):
         if self.plcobj is not None:
             self.plcobj.StopPLC()
@@ -481,6 +475,12 @@
             self.servicepublisher = None
         self.daemon.shutdown(True)
 
+    def AutoLoad(self):
+        self.plcobj.AutoLoad()
+        if self.plcobj.GetPLCstatus()[0] == "Stopped":
+            if autostart:
+                self.plcobj.StartPLC()
+        self.plcobj.StatusChange()
 
 if enabletwisted:
     import warnings
@@ -529,13 +529,13 @@
             return o.res
 
     pyroserver = Server(servicename, given_ip, port,
-                        WorkingDir, argv, autostart,
+                        WorkingDir, argv,
                         statuschange, evaluator, pyruntimevars)
 
     taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx)
 else:
     pyroserver = Server(servicename, given_ip, port,
-                        WorkingDir, argv, autostart,
+                        WorkingDir, argv,
                         statuschange, pyruntimevars=pyruntimevars)
 
 
@@ -631,19 +631,46 @@
         except Exception:
             LogMessageAndException(_("WAMP client startup failed. "))
 
+pyro_thread_started = Lock()
+pyro_thread_started.acquire()
+pyro_thread = Thread(target=pyroserver.PyroLoop,
+                     kwargs=dict(when_ready=pyro_thread_started.release))
+pyro_thread.start()
+
+# Wait for pyro thread to be effective
+pyro_thread_started.acquire()
+
+pyroserver.PrintServerInfo()
 
 if havetwisted or havewx:
-    pyro_thread = Thread(target=pyroserver.Loop)
-    pyro_thread.start()
-
+    ui_thread_started = Lock()
+    ui_thread_started.acquire()
     if havetwisted:
-        reactor.run()
-    elif havewx:
-        app.MainLoop()
-else:
-    try:
-        pyroserver.Loop()
-    except KeyboardInterrupt:
-        pass
+        # reactor._installSignalHandlersAgain()
+        def ui_thread_target():
+            # FIXME: had to disable SignaHandlers install because
+            # signal not working in non-main thread
+            reactor.run(installSignalHandlers=False)
+    else:
+        ui_thread_target = app.MainLoop
+
+    ui_thread = Thread(target=ui_thread_target)
+    ui_thread.start()
+
+    # This order ui loop to unblock main thread when ready.
+    if havetwisted:
+        reactor.callLater(0, ui_thread_started.release)
+    else:
+        wx.CallAfter(ui_thread_started.release)
+
+    # Wait for ui thread to be effective
+    ui_thread_started.acquire()
+    print("UI thread started successfully.")
+
+try:
+    MainWorker.runloop(pyroserver.AutoLoad)
+except KeyboardInterrupt:
+    pass
+
 pyroserver.Quit()
 sys.exit(0)
--- a/CodeFileTreeNode.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/CodeFileTreeNode.py	Thu Apr 19 15:03:23 2018 +0200
@@ -139,11 +139,11 @@
     def GetDataTypes(self, basetypes=False):
         return self.GetCTRoot().GetDataTypes(basetypes=basetypes)
 
-    def GenerateNewName(self, format, start_idx):
+    def GenerateNewName(self, name, format):
         return self.GetCTRoot().GenerateNewName(
-            None, None, format, start_idx,
-            dict([(var.getname().upper(), True)
-                  for var in self.CodeFile.variables.getvariable()]))
+            None, name, format,
+            exclude=dict([(var.getname().upper(), True)
+                          for var in self.CodeFile.variables.getvariable()]))
 
     def SetVariables(self, variables):
         self.CodeFile.variables.setvariable([])
--- a/IDEFrame.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/IDEFrame.py	Thu Apr 19 15:03:23 2018 +0200
@@ -1537,6 +1537,7 @@
         self.ProjectTree.SetItemText(root, item_name)
         self.ProjectTree.SetPyData(root, infos)
         highlight_colours = self.Highlights.get(infos.get("tagname", None), (wx.Colour(255, 255, 255, 0), wx.BLACK))
+        self.ProjectTree.SetItemBackgroundColour(root, highlight_colours[0])
         self.ProjectTree.SetItemTextColour(root, highlight_colours[1])
         self.ProjectTree.SetItemExtraImage(root, None)
         if infos["type"] == ITEM_POU:
--- a/PLCControler.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/PLCControler.py	Thu Apr 19 15:03:23 2018 +0200
@@ -35,7 +35,6 @@
 import util.paths as paths
 from plcopen import *
 from plcopen.types_enums import *
-from plcopen.XSLTModelQuery import _StringValue, _BoolValue, _translate_args
 from plcopen.InstancesPathCollector import InstancesPathCollector
 from plcopen.POUVariablesCollector import POUVariablesCollector
 from plcopen.InstanceTagnameCollector import InstanceTagnameCollector
@@ -45,11 +44,10 @@
 from PLCGenerator import *
 
 duration_model = re.compile("(?:([0-9]{1,2})h)?(?:([0-9]{1,2})m(?!s))?(?:([0-9]{1,2})s)?(?:([0-9]{1,3}(?:\.[0-9]*)?)ms)?")
+VARIABLE_NAME_SUFFIX_MODEL = re.compile('(\d+)$')
 
 ScriptDirectory = paths.AbsDir(__file__)
 
-
-
 # Length of the buffer
 UNDO_BUFFER_LENGTH = 20
 
@@ -1817,6 +1815,14 @@
         return text
 
     def GenerateNewName(self, tagname, name, format, start_idx=0, exclude=None, debug=False):
+        if name is not None:
+            result = re.search(VARIABLE_NAME_SUFFIX_MODEL, name)
+            if result is not None:
+                format = name[:result.start(1)] + '%d'
+                start_idx = int(result.group(1))
+            else:
+                format = name + '%d'
+
         names = {} if exclude is None else exclude.copy()
         if tagname is not None:
             names.update(dict([(varname.upper(), True)
@@ -1832,6 +1838,14 @@
                                  PLCOpenParser.GetElementClass("connector",    "commonObjects"),
                                  PLCOpenParser.GetElementClass("continuation", "commonObjects"))):
                             names[instance.getname().upper()] = True
+            elif words[0] == 'R':
+                element = self.GetEditedElement(tagname, debug)
+                for task in element.gettask():
+                    names[task.getname().upper()] = True
+                    for instance in task.getpouInstance():
+                        names[instance.getname().upper()] = True
+                for instance in element.getpouInstance():
+                    names[instance.getname().upper()] = True
         else:
             project = self.GetProject(debug)
             if project is not None:
--- a/ProjectController.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/ProjectController.py	Thu Apr 19 15:03:23 2018 +0200
@@ -37,7 +37,7 @@
 import re
 import tempfile
 from types import ListType
-from threading import Timer, Lock, Thread
+from threading import Timer
 from datetime import datetime
 from weakref import WeakKeyDictionary
 from itertools import izip
@@ -242,7 +242,6 @@
 
         # Setup debug information
         self.IECdebug_datas = {}
-        self.IECdebug_lock = Lock()
 
         self.DebugTimer = None
         self.ResetIECProgramsAndVariables()
@@ -258,7 +257,6 @@
         # After __init__ root confnode is not valid
         self.ProjectPath = None
         self._setBuildPath(None)
-        self.DebugThread = None
         self.debug_break = False
         self.previous_plcstate = None
         # copy ConfNodeMethods so that it can be later customized
@@ -1420,9 +1418,32 @@
         self.UpdateMethodsFromPLCStatus()
 
     def SnapshotAndResetDebugValuesBuffers(self):
+        if self._connector is not None:
+            plc_status, Traces = self._connector.GetTraceVariables()
+            # print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()]
+            if plc_status == "Started":
+                if len(Traces) > 0:
+                    for debug_tick, debug_buff in Traces:
+                        debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes)
+                        if debug_vars is not None and len(debug_vars) == len(self.TracedIECPath):
+                            for IECPath, values_buffer, value in izip(
+                                    self.TracedIECPath,
+                                    self.DebugValuesBuffers,
+                                    debug_vars):
+                                IECdebug_data = self.IECdebug_datas.get(IECPath, None)
+                                if IECdebug_data is not None and value is not None:
+                                    forced = IECdebug_data[2:4] == ["Forced", value]
+                                    if not IECdebug_data[4] and len(values_buffer) > 0:
+                                        values_buffer[-1] = (value, forced)
+                                    else:
+                                        values_buffer.append((value, forced))
+                            self.DebugTicks.append(debug_tick)
+
         buffers, self.DebugValuesBuffers = (self.DebugValuesBuffers,
                                             [list() for dummy in xrange(len(self.TracedIECPath))])
+
         ticks, self.DebugTicks = self.DebugTicks, []
+
         return ticks, buffers
 
     def RegisterDebugVarToConnector(self):
@@ -1431,7 +1452,6 @@
         self.TracedIECPath = []
         self.TracedIECTypes = []
         if self._connector is not None:
-            self.IECdebug_lock.acquire()
             IECPathsToPop = []
             for IECPath, data_tuple in self.IECdebug_datas.iteritems():
                 WeakCallableDict, _data_log, _status, fvalue, _buffer_list = data_tuple
@@ -1462,7 +1482,6 @@
                 self.TracedIECPath = []
                 self._connector.SetTraceVariablesList([])
             self.SnapshotAndResetDebugValuesBuffers()
-            self.IECdebug_lock.release()
 
     def IsPLCStarted(self):
         return self.previous_plcstate == "Started"
@@ -1494,7 +1513,6 @@
         if IECPath != "__tick__" and IECPath not in self._IECPathToIdx:
             return None
 
-        self.IECdebug_lock.acquire()
         # If no entry exist, create a new one with a fresh WeakKeyDictionary
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         if IECdebug_data is None:
@@ -1510,14 +1528,11 @@
 
         IECdebug_data[0][callableobj] = buffer_list
 
-        self.IECdebug_lock.release()
-
         self.ReArmDebugRegisterTimer()
 
         return IECdebug_data[1]
 
     def UnsubscribeDebugIECVariable(self, IECPath, callableobj):
-        self.IECdebug_lock.acquire()
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         if IECdebug_data is not None:
             IECdebug_data[0].pop(callableobj, None)
@@ -1528,14 +1543,11 @@
                     lambda x, y: x | y,
                     IECdebug_data[0].itervalues(),
                     False)
-        self.IECdebug_lock.release()
 
         self.ReArmDebugRegisterTimer()
 
     def UnsubscribeAllDebugIECVariable(self):
-        self.IECdebug_lock.acquire()
         self.IECdebug_datas = {}
-        self.IECdebug_lock.release()
 
         self.ReArmDebugRegisterTimer()
 
@@ -1543,30 +1555,22 @@
         if IECPath not in self.IECdebug_datas:
             return
 
-        self.IECdebug_lock.acquire()
-
         # If no entry exist, create a new one with a fresh WeakKeyDictionary
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         IECdebug_data[2] = "Forced"
         IECdebug_data[3] = fvalue
 
-        self.IECdebug_lock.release()
-
         self.ReArmDebugRegisterTimer()
 
     def ReleaseDebugIECVariable(self, IECPath):
         if IECPath not in self.IECdebug_datas:
             return
 
-        self.IECdebug_lock.acquire()
-
         # If no entry exist, create a new one with a fresh WeakKeyDictionary
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         IECdebug_data[2] = "Registered"
         IECdebug_data[3] = None
 
-        self.IECdebug_lock.release()
-
         self.ReArmDebugRegisterTimer()
 
     def CallWeakcallables(self, IECPath, function_name, *cargs):
@@ -1590,51 +1594,8 @@
             return -1, "No runtime connected!"
         return self._connector.RemoteExec(script, **kwargs)
 
-    def DebugThreadProc(self):
-        """
-        This thread waid PLC debug data, and dispatch them to subscribers
-        """
-        self.debug_break = False
-        debug_getvar_retry = 0
-        while (not self.debug_break) and (self._connector is not None):
-            plc_status, Traces = self._connector.GetTraceVariables()
-            debug_getvar_retry += 1
-            # print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()]
-            if plc_status == "Started":
-                if len(Traces) > 0:
-                    self.IECdebug_lock.acquire()
-                    for debug_tick, debug_buff in Traces:
-                        debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes)
-                        if debug_vars is not None and len(debug_vars) == len(self.TracedIECPath):
-                            for IECPath, values_buffer, value in izip(
-                                    self.TracedIECPath,
-                                    self.DebugValuesBuffers,
-                                    debug_vars):
-                                IECdebug_data = self.IECdebug_datas.get(IECPath, None)  # FIXME get
-                                if IECdebug_data is not None and value is not None:
-                                    forced = IECdebug_data[2:4] == ["Forced", value]
-                                    if not IECdebug_data[4] and len(values_buffer) > 0:
-                                        values_buffer[-1] = (value, forced)
-                                    else:
-                                        values_buffer.append((value, forced))
-                            self.DebugTicks.append(debug_tick)
-                            debug_getvar_retry = 0
-                    self.IECdebug_lock.release()
-
-                if debug_getvar_retry != 0:
-                    # Be patient, tollerate PLC to come with fresh samples
-                    time.sleep(0.1)
-            else:
-                self.debug_break = True
-        self.logger.write(_("Debugger disabled\n"))
-        self.DebugThread = None
-        if self.DispatchDebugValuesTimer is not None:
-            self.DispatchDebugValuesTimer.Stop()
-
     def DispatchDebugValuesProc(self, event):
-        self.IECdebug_lock.acquire()
         debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
-        self.IECdebug_lock.release()
         start_time = time.time()
         if len(self.TracedIECPath) == len(buffers):
             for IECPath, values in izip(self.TracedIECPath, buffers):
@@ -1645,22 +1606,12 @@
 
         delay = time.time() - start_time
         next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
-        if self.DispatchDebugValuesTimer is not None and self.DebugThread is not None:
+        if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Start(
                 int(next_refresh * 1000), oneShot=True)
         event.Skip()
 
     def KillDebugThread(self):
-        tmp_debugthread = self.DebugThread
-        self.debug_break = True
-        if tmp_debugthread is not None:
-            self.logger.writeyield(_("Stopping debugger...\n"))
-            tmp_debugthread.join(timeout=5)
-            if tmp_debugthread.isAlive() and self.logger:
-                self.logger.write_warning(_("Couldn't stop debugger.\n"))
-            else:
-                self.logger.write(_("Debugger stopped.\n"))
-        self.DebugThread = None
         if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Stop()
 
@@ -1672,9 +1623,6 @@
         if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Start(
                 int(REFRESH_PERIOD * 1000), oneShot=True)
-        if self.DebugThread is None:
-            self.DebugThread = Thread(target=self.DebugThreadProc)
-            self.DebugThread.start()
 
     def _Run(self):
         """
--- a/canfestival/canfestival.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/canfestival/canfestival.py	Thu Apr 19 15:03:23 2018 +0200
@@ -38,20 +38,16 @@
     LOCATION_CONFNODE, \
     LOCATION_VAR_MEMORY
 
-try:
-    from nodelist import NodeList
-except ImportError:
-    base_folder = paths.AbsParentDir(__file__, 2)
-    CanFestivalPath = os.path.join(base_folder, "CanFestival-3")
-    sys.path.append(os.path.join(CanFestivalPath, "objdictgen"))
-
-    from nodelist import NodeList
-
+base_folder = paths.AbsParentDir(__file__, 2)  # noqa
+CanFestivalPath = os.path.join(base_folder, "CanFestival-3")  # noqa
+sys.path.append(os.path.join(CanFestivalPath, "objdictgen"))  # noqa
+
+# pylint: disable=wrong-import-position
+from nodelist import NodeList
 from nodemanager import NodeManager
 import gen_cfile
 import eds_utils
 import canfestival_config as local_canfestival_config  # pylint: disable=import-error
-
 from commondialogs import CreateNodeDialog
 from subindextable import IECTypeConversion, SizeConversion
 from canfestival import config_utils
--- a/connectors/PYRO/__init__.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/connectors/PYRO/__init__.py	Thu Apr 19 15:03:23 2018 +0200
@@ -139,67 +139,24 @@
         confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n"))
         return None
 
+    _special_return_funcs = {
+        "StartPLC": False,
+        "GetTraceVariables": ("Broken", None),
+        "GetPLCstatus": ("Broken", None),
+        "RemoteExec": (-1, "RemoteExec script failed!")
+    }
+
     class PyroProxyProxy(object):
         """
         A proxy proxy class to handle Beremiz Pyro interface specific behavior.
         And to put Pyro exception catcher in between caller and Pyro proxy
         """
-        def __init__(self):
-            # for safe use in from debug thread, must create a copy
-            self.RemotePLCObjectProxyCopy = None
-
-        def GetPyroProxy(self):
-            """
-            This func returns the real Pyro Proxy.
-            Use this if you musn't keep reference to it.
-            """
-            return RemotePLCObjectProxy
-
-        def _PyroStartPLC(self, *args, **kwargs):
-            """
-            confnodesroot._connector.GetPyroProxy() is used
-            rather than RemotePLCObjectProxy because
-            object is recreated meanwhile,
-            so we must not keep ref to it here
-            """
-            current_status, _log_count = confnodesroot._connector.GetPyroProxy().GetPLCstatus()
-            if current_status == "Dirty":
-                # Some bad libs with static symbols may polute PLC
-                # ask runtime to suicide and come back again
-
-                confnodesroot.logger.write(_("Force runtime reload\n"))
-                confnodesroot._connector.GetPyroProxy().ForceReload()
-                confnodesroot._Disconnect()
-                # let remote PLC time to resurect.(freeze app)
-                sleep(0.5)
-                confnodesroot._Connect()
-            self.RemotePLCObjectProxyCopy = copy.copy(confnodesroot._connector.GetPyroProxy())
-            return confnodesroot._connector.GetPyroProxy().StartPLC(*args, **kwargs)
-        StartPLC = PyroCatcher(_PyroStartPLC, False)
-
-        def _PyroGetTraceVariables(self):
-            """
-            for safe use in from debug thread, must use the copy
-            """
-            if self.RemotePLCObjectProxyCopy is None:
-                self.RemotePLCObjectProxyCopy = copy.copy(confnodesroot._connector.GetPyroProxy())
-            return self.RemotePLCObjectProxyCopy.GetTraceVariables()
-        GetTraceVariables = PyroCatcher(_PyroGetTraceVariables, ("Broken", None))
-
-        def _PyroGetPLCstatus(self):
-            return RemotePLCObjectProxy.GetPLCstatus()
-        GetPLCstatus = PyroCatcher(_PyroGetPLCstatus, ("Broken", None))
-
-        def _PyroRemoteExec(self, script, **kwargs):
-            return RemotePLCObjectProxy.RemoteExec(script, **kwargs)
-        RemoteExec = PyroCatcher(_PyroRemoteExec, (-1, "RemoteExec script failed!"))
-
         def __getattr__(self, attrName):
             member = self.__dict__.get(attrName, None)
             if member is None:
                 def my_local_func(*args, **kwargs):
                     return RemotePLCObjectProxy.__getattr__(attrName)(*args, **kwargs)
-                member = PyroCatcher(my_local_func, None)
+                member = PyroCatcher(my_local_func, _special_return_funcs.get(attrName, None))
                 self.__dict__[attrName] = member
             return member
 
--- a/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Thu Apr 19 15:03:23 2018 +0200
@@ -48,7 +48,7 @@
 # Canvas height
 [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = [0, 100, 200]
 
-CANVAS_BORDER = (20., 10.)  # Border height on at bottom and top of graph
+CANVAS_BORDER = (30., 20.)  # Border height on at bottom and top of graph
 CANVAS_PADDING = 8.5        # Border inside graph where no label is drawn
 VALUE_LABEL_HEIGHT = 17.    # Height of variable label in graph
 AXES_LABEL_HEIGHT = 12.75   # Height of variable value in graph
--- a/controls/DebugVariablePanel/DebugVariableItem.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/controls/DebugVariablePanel/DebugVariableItem.py	Thu Apr 19 15:03:23 2018 +0200
@@ -24,6 +24,7 @@
 
 
 from __future__ import absolute_import
+from datetime import timedelta
 import binascii
 import numpy
 from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR
@@ -219,18 +220,20 @@
 
         else:
             self.Data = None
-
+            self.MinValue = None
+            self.MaxValue = None
         # Init variable value
         self.Value = ""
 
     def IsNumVariable(self):
         """
         Return if variable data type is numeric. String variables are
-        considered as numeric (string CRC)
+        considered as numeric (string CRC). Time variables are considered
+        as number of seconds
         @return: True if data type is numeric
         """
         return (self.Parent.IsNumType(self.VariableType) or
-                self.VariableType in ["STRING", "WSTRING"])
+                self.VariableType in ["STRING", "WSTRING", "TIME", "TOD", "DT", "DATE"])
 
     def NewValues(self, ticks, values):
         """
@@ -253,10 +256,15 @@
                 # Translate forced flag to float for storing in Data table
                 forced_value = float(forced)
 
-                # String data value is CRC
-                num_value = (binascii.crc32(value) & STRING_CRC_MASK
-                             if self.VariableType in ["STRING", "WSTRING"]
-                             else float(value))
+                if self.VariableType in ["STRING", "WSTRING"]:
+                    # String data value is CRC
+                    num_value = (binascii.crc32(value) & STRING_CRC_MASK)
+                elif self.VariableType in ["TIME", "TOD", "DT", "DATE"]:
+                    # Numeric value of time type variables
+                    # is represented in seconds
+                    num_value = float(value.total_seconds())
+                else:
+                    num_value = float(value)
 
                 # Update variable range values
                 self.MinValue = (min(self.MinValue, num_value)
@@ -341,6 +349,9 @@
                 if self.VariableType in ["STRING", "WSTRING"] \
                 else self.Data[idx, 1:3]
 
+            if self.VariableType in ["TIME", "TOD", "DT", "DATE"]:
+                value = timedelta(seconds=value)
+
             # Get raw value if asked
             if not raw:
                 value = TYPE_TRANSLATOR.get(
--- a/controls/DebugVariablePanel/DebugVariablePanel.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/controls/DebugVariablePanel/DebugVariablePanel.py	Thu Apr 19 15:03:23 2018 +0200
@@ -237,7 +237,7 @@
         default_range_idx = 0
         for idx, (text, _value) in enumerate(RANGE_VALUES):
             self.CanvasRange.Append(text)
-            if text == "1s":
+            if _value == 1000000000:
                 default_range_idx = idx
         self.CanvasRange.SetSelection(default_range_idx)
 
--- a/controls/LogViewer.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/controls/LogViewer.py	Thu Apr 19 15:03:23 2018 +0200
@@ -396,7 +396,7 @@
         self.HasNewData = False
 
     def SetLogSource(self, log_source):
-        self.LogSource = proxy(log_source) if log_source else None
+        self.LogSource = proxy(log_source) if log_source is not None else None
         self.CleanButton.Enable(self.LogSource is not None)
         if log_source is not None:
             self.ResetLogMessages()
--- a/controls/ProjectPropertiesPanel.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/controls/ProjectPropertiesPanel.py	Thu Apr 19 15:03:23 2018 +0200
@@ -27,6 +27,8 @@
 import wx
 from wx.lib.scrolledpanel import ScrolledPanel
 
+from xmlclass.xmlclass import URI_model
+
 # -------------------------------------------------------------------------------
 #                                 Helpers
 # -------------------------------------------------------------------------------
@@ -294,8 +296,16 @@
             if self.Controller is not None and self.Values is not None:
                 old_value = self.Values.get(name)
                 new_value = textctrl.GetValue()
-                if name not in REQUIRED_PARAMS and new_value == "":
+                if name in REQUIRED_PARAMS and new_value == "":
                     new_value = None
+                if name == 'companyURL':
+                    if not URI_model.match(new_value):
+                        new_value = None
+                        dialog = wx.MessageDialog(self, _('Invalid URL!\n'
+                                                          'Please enter correct URL address.'),
+                                                  _("Error"), wx.OK | wx.ICON_ERROR)
+                        dialog.ShowModal()
+                        dialog.Destroy()
                 if old_value != new_value:
                     self.Controller.SetProjectProperties(properties={name: new_value})
                     self.ParentWindow._Refresh(TITLE, FILEMENU, EDITMENU,
--- a/controls/VariablePanel.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/controls/VariablePanel.py	Thu Apr 19 15:03:23 2018 +0200
@@ -105,7 +105,6 @@
 }
 
 LOCATION_MODEL = re.compile("((?:%[IQM](?:\*|(?:[XBWLD]?[0-9]+(?:\.[0-9]+)*)))?)$")
-VARIABLE_NAME_SUFFIX_MODEL = re.compile("([0-9]*)$")
 
 
 # -------------------------------------------------------------------------------
@@ -514,7 +513,7 @@
         self.FilterChoices = []
         self.FilterChoiceTransfer = GetFilterChoiceTransfer()
 
-        self.DefaultValue = _VariableInfos("", "", "", "", "", True, "", DefaultType, ([], []), 0)
+        self.DefaultValue = _VariableInfos("LocalVar0", "", "", "", "", True, "", DefaultType, ([], []), 0)
 
         if element_type in ["config", "resource"]:
             self.DefaultTypes = {"All": "Global"}
@@ -590,36 +589,16 @@
         self.VariablesGrid.SetEditable(not self.Debug)
 
         def _AddVariable(new_row):
+            row_content = self.DefaultValue.copy()
             if new_row > 0:
-                row_content = self.Values[new_row - 1].copy()
-
-                result = VARIABLE_NAME_SUFFIX_MODEL.search(row_content.Name)
-                if result is not None:
-                    name = row_content.Name[:result.start(1)]
-                    suffix = result.group(1)
-                    if suffix != "":
-                        start_idx = int(suffix)
-                    else:
-                        start_idx = 0
-                else:
-                    name = row_content.Name
-                    start_idx = 0
-            else:
-                row_content = None
-                start_idx = 0
-                name = "LocalVar"
-
-            if row_content is not None and row_content.Edit:
-                row_content = self.Values[new_row - 1].copy()
-            else:
-                row_content = self.DefaultValue.copy()
-                if self.Filter in self.DefaultTypes:
-                    row_content.Class = self.DefaultTypes[self.Filter]
-                else:
-                    row_content.Class = self.Filter
-
-            row_content.Name = self.Controler.GenerateNewName(
-                self.TagName, None, name + "%d", start_idx)
+                # doesn't copy values of previous var if it's non-editable (like a FB)
+                if self.Values[new_row-1].Edit:
+                    row_content = self.Values[new_row-1].copy()
+                old_name = self.Values[new_row-1].Name
+                row_content.Name = self.Controler.GenerateNewName(
+                    self.TagName, old_name, old_name+'%d')
+            if not row_content.Class:
+                row_content.Class = self.DefaultTypes.get(self.Filter, self.Filter)
 
             if self.Filter == "All" and len(self.Values) > 0:
                 self.Values.insert(new_row, row_content)
--- a/editors/CodeFileEditor.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/editors/CodeFileEditor.py	Thu Apr 19 15:03:23 2018 +0200
@@ -35,7 +35,6 @@
 from plcopen.structures import TestIdentifier, IEC_KEYWORDS, DefaultType
 from controls import CustomGrid, CustomTable
 from controls.CustomStyledTextCtrl import CustomStyledTextCtrl, faces, GetCursorPos, NAVIGATION_KEYS
-from controls.VariablePanel import VARIABLE_NAME_SUFFIX_MODEL
 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
 from util.BitmapLibrary import GetBitmap
 from util.TranslationCatalogs import NoTranslate
@@ -674,7 +673,7 @@
         self.Controler = controler
 
         self.VariablesDefaultValue = {
-            "Name":        "",
+            "Name":        "LocalVar0",
             "Type":        DefaultType,
             "Initial":     "",
             "Description": "",
@@ -694,19 +693,9 @@
         def _AddVariable(new_row):
             if new_row > 0:
                 row_content = self.Table.data[new_row - 1].copy()
-                result = VARIABLE_NAME_SUFFIX_MODEL.search(row_content["Name"])
-                if result is not None:
-                    name = row_content["Name"][:result.start(1)]
-                    suffix = result.group(1)
-                    if suffix != "":
-                        start_idx = int(suffix)
-                    else:
-                        start_idx = 0
-                else:
-                    name = row_content["Name"]
-                    start_idx = 0
-                row_content["Name"] = self.Controler.GenerateNewName(
-                    name + "%d", start_idx)
+                old_name = row_content['Name']
+                row_content['Name'] =\
+                    self.Controler.GenerateNewName(old_name, old_name+'%d')
             else:
                 row_content = self.VariablesDefaultValue.copy()
             self.Table.InsertRow(new_row, row_content)
--- a/editors/ResourceEditor.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/editors/ResourceEditor.py	Thu Apr 19 15:03:23 2018 +0200
@@ -303,7 +303,8 @@
         self.RefreshHighlightsTimer = wx.Timer(self, -1)
         self.Bind(wx.EVT_TIMER, self.OnRefreshHighlightsTimer, self.RefreshHighlightsTimer)
 
-        self.TasksDefaultValue = {"Name": "", "Triggering": "", "Single": "", "Interval": "", "Priority": 0}
+        self.TasksDefaultValue = {"Name": "task0", "Triggering": "Cyclic",
+                                  "Single": "", "Interval": "T#20ms", "Priority": 0}
         self.TasksTable = ResourceTable(self, [], GetTasksTableColnames())
         self.TasksTable.SetColAlignements([wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, wx.ALIGN_RIGHT])
         self.TasksTable.SetColSizes([200, 100, 100, 150, 100])
@@ -314,7 +315,15 @@
                                    "Down": self.DownTaskButton})
 
         def _AddTask(new_row):
-            self.TasksTable.InsertRow(new_row, self.TasksDefaultValue.copy())
+            if new_row > 0:
+                row_content = self.TasksTable.data[new_row-1].copy()
+                old_name = row_content['Name']
+                row_content['Name'] =\
+                    self.Controler.GenerateNewName(self.TagName, old_name, old_name+'%d')
+            else:
+                row_content = self.TasksDefaultValue.copy()
+
+            self.TasksTable.InsertRow(new_row, row_content)
             self.RefreshModel()
             self.RefreshView()
             return new_row
@@ -338,7 +347,7 @@
         self.TasksTable.ResetView(self.TasksGrid)
         self.TasksGrid.RefreshButtons()
 
-        self.InstancesDefaultValue = {"Name": "", "Type": "", "Task": ""}
+        self.InstancesDefaultValue = {"Name": "instance0", "Type": "", "Task": ""}
         self.InstancesTable = ResourceTable(self, [], GetInstancesTableColnames())
         self.InstancesTable.SetColAlignements([wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT])
         self.InstancesTable.SetColSizes([200, 150, 150])
@@ -349,7 +358,15 @@
                                        "Down": self.DownInstanceButton})
 
         def _AddInstance(new_row):
-            self.InstancesTable.InsertRow(new_row, self.InstancesDefaultValue.copy())
+            if new_row > 0:
+                row_content = self.InstancesTable.data[new_row - 1].copy()
+                old_name = row_content['Name']
+                row_content['Name'] =\
+                    self.Controler.GenerateNewName(self.TagName, old_name, old_name+'%d')
+            else:
+                row_content = self.InstancesDefaultValue.copy()
+
+            self.InstancesTable.InsertRow(new_row, row_content)
             self.RefreshModel()
             self.RefreshView()
             return new_row
--- a/plcopen/BlockInstanceCollector.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/plcopen/BlockInstanceCollector.py	Thu Apr 19 15:03:23 2018 +0200
@@ -4,8 +4,8 @@
 # See COPYING file for copyrights details.
 
 from __future__ import absolute_import
+from collections import OrderedDict, namedtuple
 from plcopen.XSLTModelQuery import XSLTModelQuery, _StringValue, _BoolValue, _translate_args
-from collections import OrderedDict, namedtuple
 
 # -------------------------------------------------------------------------------
 #           Helpers object for generating pou block instances list
--- a/plcopen/plcopen.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/plcopen/plcopen.py	Thu Apr 19 15:03:23 2018 +0200
@@ -152,12 +152,12 @@
     test_result = []
     result = criteria["pattern"].search(text)
     while result is not None:
-        prev_pos = result.endpos
+        prev_pos = result.span()[1]
         start = TextLenInRowColumn(text[:result.start()])
         end = TextLenInRowColumn(text[:result.end() - 1])
         test_result.append((start, end, "\n".join(lines[start[0]:end[0] + 1])))
         result = criteria["pattern"].search(text, result.end())
-        if result is not None and prev_pos == result.endpos:
+        if result is not None and prev_pos == result.end():
             break
     return test_result
 
@@ -441,7 +441,7 @@
                     "authorName": contentheader_obj.setauthor,
                     "pageSize": lambda v: contentheader_obj.setpageSize(*v),
                     "scaling": contentheader_obj.setscaling}.get(attr)
-            if func is not None:
+            if func is not None and value is not None:
                 func(value)
             elif attr in ["modificationDateTime", "organization", "language"]:
                 setattr(contentheader_obj, attr, value)
--- a/runtime/PLCObject.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/runtime/PLCObject.py	Thu Apr 19 15:03:23 2018 +0200
@@ -23,7 +23,8 @@
 
 
 from __future__ import absolute_import
-from threading import Timer, Thread, Lock, Semaphore, Event
+import thread
+from threading import Thread, Lock, Semaphore, Event, Condition
 import ctypes
 import os
 import sys
@@ -60,31 +61,139 @@
     sys.stdout.flush()
 
 
+class job(object):
+    """
+    job to be executed by a worker
+    """
+    def __init__(self, call, *args, **kwargs):
+        self.job = (call, args, kwargs)
+        self.result = None
+        self.success = False
+        self.exc_info = None
+
+    def do(self):
+        """
+        do the job by executing the call, and deal with exceptions
+        """
+        try:
+            call, args, kwargs = self.job
+            self.result = call(*args, **kwargs)
+            self.success = True
+        except Exception:
+            self.success = False
+            self.exc_info = sys.exc_info()
+
+
+class worker(object):
+    """
+    serialize main thread load/unload of PLC shared objects
+    """
+    def __init__(self):
+        # Only one job at a time
+        self._finish = False
+        self._threadID = None
+        self.mutex = Lock()
+        self.todo = Condition(self.mutex)
+        self.done = Condition(self.mutex)
+        self.free = Condition(self.mutex)
+        self.job = None
+
+    def runloop(self, *args, **kwargs):
+        """
+        meant to be called by worker thread (blocking)
+        """
+        self._threadID = thread.get_ident()
+        if args or kwargs:
+            job(*args, **kwargs).do()
+            # result is ignored
+        self.mutex.acquire()
+        while not self._finish:
+            self.todo.wait()
+            if self.job is not None:
+                self.job.do()
+                self.done.notify()
+            else:
+                self.free.notify()
+        self.mutex.release()
+
+    def call(self, *args, **kwargs):
+        """
+        creates a job, execute it in worker thread, and deliver result.
+        if job execution raise exception, re-raise same exception
+        meant to be called by non-worker threads, but this is accepted.
+        blocking until job done
+        """
+
+        _job = job(*args, **kwargs)
+
+        if self._threadID == thread.get_ident() or self._threadID is None:
+            # if caller is worker thread execute immediately
+            _job.do()
+        else:
+            # otherwise notify and wait for completion
+            self.mutex.acquire()
+
+            while self.job is not None:
+                self.free.wait()
+
+            self.job = _job
+            self.todo.notify()
+            self.done.wait()
+            _job = self.job
+            self.job = None
+            self.mutex.release()
+
+        if _job.success:
+            return _job.result
+        else:
+            raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2]
+
+    def quit(self):
+        """
+        unblocks main thread, and terminate execution of runloop()
+        """
+        # mark queue
+        self._finish = True
+        self.mutex.acquire()
+        self.job = None
+        self.todo.notify()
+        self.mutex.release()
+
+
+MainWorker = worker()
+
+
+def RunInMain(func):
+    def func_wrapper(*args, **kwargs):
+        return MainWorker.call(func, *args, **kwargs)
+    return func_wrapper
+
+
 class PLCObject(pyro.ObjBase):
-    def __init__(self, workingdir, daemon, argv, statuschange, evaluator, pyruntimevars):
+    def __init__(self, server):
         pyro.ObjBase.__init__(self)
-        self.evaluator = evaluator
-        self.argv = [workingdir] + argv  # force argv[0] to be "path" to exec...
-        self.workingdir = workingdir
+        self.evaluator = server.evaluator
+        self.argv = [server.workdir] + server.argv  # force argv[0] to be "path" to exec...
+        self.workingdir = server.workdir
         self.PLCStatus = "Empty"
         self.PLClibraryHandle = None
         self.PLClibraryLock = Lock()
         self.DummyIteratorLock = None
         # Creates fake C funcs proxies
-        self._FreePLC()
-        self.daemon = daemon
-        self.statuschange = statuschange
+        self._InitPLCStubCalls()
+        self.daemon = server.daemon
+        self.statuschange = server.statuschange
         self.hmi_frame = None
-        self.pyruntimevars = pyruntimevars
+        self.pyruntimevars = server.pyruntimevars
         self._loading_error = None
         self.python_runtime_vars = None
         self.TraceThread = None
         self.TraceLock = Lock()
-        self.TraceWakeup = Event()
         self.Traces = []
 
+    # First task of worker -> no @RunInMain
     def AutoLoad(self):
-        # Get the last transfered PLC if connector must be restart
+        # Get the last transfered PLC
         try:
             self.CurrentPLCFilename = open(
                 self._GetMD5FileName(),
@@ -100,6 +209,7 @@
             for callee in self.statuschange:
                 callee(self.PLCStatus)
 
+    @RunInMain
     def LogMessage(self, *args):
         if len(args) == 2:
             level, msg = args
@@ -111,16 +221,19 @@
             return self._LogMessage(level, msg, len(msg))
         return None
 
+    @RunInMain
     def ResetLogCount(self):
         if self._ResetLogCount is not None:
             self._ResetLogCount()
 
+    # used internaly
     def GetLogCount(self, level):
         if self._GetLogCount is not None:
             return int(self._GetLogCount(level))
         elif self._loading_error is not None and level == 0:
             return 1
 
+    @RunInMain
     def GetLogMessage(self, level, msgid):
         tick = ctypes.c_uint32()
         tv_sec = ctypes.c_uint32()
@@ -145,12 +258,13 @@
     def _GetLibFileName(self):
         return os.path.join(self.workingdir, self.CurrentPLCFilename)
 
-    def LoadPLC(self):
+    def _LoadPLC(self):
         """
         Load PLC library
         Declare all functions, arguments and return values
         """
         md5 = open(self._GetMD5FileName(), "r").read()
+        self.PLClibraryLock.acquire()
         try:
             self._PLClibraryHandle = dlopen(self._GetLibFileName())
             self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle)
@@ -227,25 +341,34 @@
 
             self._loading_error = None
 
-            self.PythonRuntimeInit()
-
-            return True
         except Exception:
             self._loading_error = traceback.format_exc()
             PLCprint(self._loading_error)
             return False
-
+        finally:
+            self.PLClibraryLock.release()
+
+        return True
+
+    @RunInMain
+    def LoadPLC(self):
+        res = self._LoadPLC()
+        if res:
+            self.PythonRuntimeInit()
+        else:
+            self._FreePLC()
+
+        return res
+
+    @RunInMain
     def UnLoadPLC(self):
         self.PythonRuntimeCleanup()
         self._FreePLC()
 
-    def _FreePLC(self):
-        """
-        Unload PLC library.
-        This is also called by __init__ to create dummy C func proxies
-        """
-        self.PLClibraryLock.acquire()
-        # Forget all refs to library
+    def _InitPLCStubCalls(self):
+        """
+        create dummy C func proxies
+        """
         self._startPLC = lambda x, y: None
         self._stopPLC = lambda: None
         self._ResetDebugVariables = lambda: None
@@ -259,13 +382,26 @@
         self._GetLogCount = None
         self._LogMessage = None
         self._GetLogMessage = None
+        self._PLClibraryHandle = None
         self.PLClibraryHandle = None
-        # Unload library explicitely
-        if getattr(self, "_PLClibraryHandle", None) is not None:
-            dlclose(self._PLClibraryHandle)
-            self._PLClibraryHandle = None
-
-        self.PLClibraryLock.release()
+
+    def _FreePLC(self):
+        """
+        Unload PLC library.
+        This is also called by __init__ to create dummy C func proxies
+        """
+        self.PLClibraryLock.acquire()
+        try:
+            # Unload library explicitely
+            if getattr(self, "_PLClibraryHandle", None) is not None:
+                dlclose(self._PLClibraryHandle)
+
+            # Forget all refs to library
+            self._InitPLCStubCalls()
+
+        finally:
+            self.PLClibraryLock.release()
+
         return False
 
     def PythonRuntimeCall(self, methodname):
@@ -278,6 +414,7 @@
             if exp is not None:
                 self.LogMessage(0, '\n'.join(traceback.format_exception(*exp)))
 
+    # used internaly
     def PythonRuntimeInit(self):
         MethodNames = ["init", "start", "stop", "cleanup"]
         self.python_runtime_vars = globals().copy()
@@ -329,6 +466,7 @@
 
         self.PythonRuntimeCall("init")
 
+    # used internaly
     def PythonRuntimeCleanup(self):
         if self.python_runtime_vars is not None:
             self.PythonRuntimeCall("cleanup")
@@ -340,10 +478,8 @@
         res, cmd, blkid = "None", "None", ctypes.c_void_p()
         compile_cache = {}
         while True:
-            # print "_PythonIterator(", res, ")",
             cmd = self._PythonIterator(res, blkid)
             FBID = blkid.value
-            # print " -> ", cmd, blkid
             if cmd is None:
                 break
             try:
@@ -364,6 +500,7 @@
                 res = "#EXCEPTION : "+str(e)
                 self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (FBID, cmd, str(e)))
 
+    @RunInMain
     def StartPLC(self):
         if self.CurrentPLCFilename is not None and self.PLCStatus == "Stopped":
             c_argv = ctypes.c_char_p * len(self.argv)
@@ -382,6 +519,7 @@
                 self.PLCStatus = "Broken"
                 self.StatusChange()
 
+    @RunInMain
     def StopPLC(self):
         if self.PLCStatus == "Started":
             self.LogMessage("PLC stopped")
@@ -391,40 +529,38 @@
             self.StatusChange()
             self.PythonRuntimeCall("stop")
             if self.TraceThread is not None:
-                self.TraceWakeup.set()
                 self.TraceThread.join()
                 self.TraceThread = None
             return True
         return False
 
-    def _Reload(self):
-        self.daemon.shutdown(True)
-        self.daemon.sock.close()
-        os.execv(sys.executable, [sys.executable]+sys.argv[:])
-        # never reached
-        return 0
-
-    def ForceReload(self):
-        # respawn python interpreter
-        Timer(0.1, self._Reload).start()
-        return True
-
+    @RunInMain
     def GetPLCstatus(self):
         return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
 
+    @RunInMain
     def NewPLC(self, md5sum, data, extrafiles):
         if self.PLCStatus in ["Stopped", "Empty", "Broken"]:
             NewFileName = md5sum + lib_ext
             extra_files_log = os.path.join(self.workingdir, "extra_files.txt")
 
-            self.UnLoadPLC()
+            old_PLC_filename = os.path.join(self.workingdir, self.CurrentPLCFilename) \
+                if self.CurrentPLCFilename is not None \
+                else None
+            new_PLC_filename = os.path.join(self.workingdir, NewFileName)
+
+            # Some platform (i.e. Xenomai) don't like reloading same .so file
+            replace_PLC_shared_object = new_PLC_filename != old_PLC_filename
+
+            if replace_PLC_shared_object:
+                self.UnLoadPLC()
 
             self.LogMessage("NewPLC (%s)" % md5sum)
             self.PLCStatus = "Empty"
 
             try:
-                os.remove(os.path.join(self.workingdir,
-                                       self.CurrentPLCFilename))
+                if replace_PLC_shared_object:
+                    os.remove(old_PLC_filename)
                 for filename in file(extra_files_log, "r").readlines() + [extra_files_log]:
                     try:
                         os.remove(os.path.join(self.workingdir, filename.strip()))
@@ -435,8 +571,8 @@
 
             try:
                 # Create new PLC file
-                open(os.path.join(self.workingdir, NewFileName),
-                     'wb').write(data)
+                if replace_PLC_shared_object:
+                    open(new_PLC_filename, 'wb').write(data)
 
                 # Store new PLC filename based on md5 key
                 open(self._GetMD5FileName(), "w").write(md5sum)
@@ -456,11 +592,12 @@
                 PLCprint(traceback.format_exc())
                 return False
 
-            if self.LoadPLC():
+            if not replace_PLC_shared_object:
+                self.PLCStatus = "Stopped"
+            elif self.LoadPLC():
                 self.PLCStatus = "Stopped"
             else:
                 self.PLCStatus = "Broken"
-                self._FreePLC()
             self.StatusChange()
 
             return self.PLCStatus == "Stopped"
@@ -496,14 +633,6 @@
         else:
             self._suspendDebug(True)
 
-    def _TracesPush(self, trace):
-        self.TraceLock.acquire()
-        lT = len(self.Traces)
-        if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024:
-            self.Traces.pop(0)
-        self.Traces.append(trace)
-        self.TraceLock.release()
-
     def _TracesSwap(self):
         self.LastSwapTrace = time()
         if self.TraceThread is None and self.PLCStatus == "Started":
@@ -513,26 +642,9 @@
         Traces = self.Traces
         self.Traces = []
         self.TraceLock.release()
-        self.TraceWakeup.set()
         return Traces
 
-    def _TracesAutoSuspend(self):
-        # TraceProc stops here if Traces not polled for 3 seconds
-        traces_age = time() - self.LastSwapTrace
-        if traces_age > 3:
-            self.TraceLock.acquire()
-            self.Traces = []
-            self.TraceLock.release()
-            self._suspendDebug(True)  # Disable debugger
-            self.TraceWakeup.clear()
-            self.TraceWakeup.wait()
-            self._resumeDebug()  # Re-enable debugger
-
-    def _TracesFlush(self):
-        self.TraceLock.acquire()
-        self.Traces = []
-        self.TraceLock.release()
-
+    @RunInMain
     def GetTraceVariables(self):
         return self.PLCStatus, self._TracesSwap()
 
@@ -540,23 +652,47 @@
         """
         Return a list of traces, corresponding to the list of required idx
         """
+        self._resumeDebug()  # Re-enable debugger
         while self.PLCStatus == "Started":
             tick = ctypes.c_uint32()
             size = ctypes.c_uint32()
             buff = ctypes.c_void_p()
             TraceBuffer = None
-            if self.PLClibraryLock.acquire(False):
-                if self._GetDebugData(ctypes.byref(tick),
-                                      ctypes.byref(size),
-                                      ctypes.byref(buff)) == 0:
-                    if size.value:
-                        TraceBuffer = ctypes.string_at(buff.value, size.value)
-                    self._FreeDebugData()
-                self.PLClibraryLock.release()
+
+            self.PLClibraryLock.acquire()
+
+            res = self._GetDebugData(ctypes.byref(tick),
+                                     ctypes.byref(size),
+                                     ctypes.byref(buff))
+            if res == 0:
+                if size.value:
+                    TraceBuffer = ctypes.string_at(buff.value, size.value)
+                self._FreeDebugData()
+
+            self.PLClibraryLock.release()
+
+            # leave thread if GetDebugData isn't happy.
+            if res != 0:
+                break
+
             if TraceBuffer is not None:
-                self._TracesPush((tick.value, TraceBuffer))
-            self._TracesAutoSuspend()
-        self._TracesFlush()
+                self.TraceLock.acquire()
+                lT = len(self.Traces)
+                if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024:
+                    self.Traces.pop(0)
+                self.Traces.append((tick.value, TraceBuffer))
+                self.TraceLock.release()
+
+            # TraceProc stops here if Traces not polled for 3 seconds
+            traces_age = time() - self.LastSwapTrace
+            if traces_age > 3:
+                self.TraceLock.acquire()
+                self.Traces = []
+                self.TraceLock.release()
+                self._suspendDebug(True)  # Disable debugger
+                break
+
+        self.TraceThread = None
 
     def RemoteExec(self, script, *kwargs):
         try:
--- a/runtime/__init__.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/runtime/__init__.py	Thu Apr 19 15:03:23 2018 +0200
@@ -24,5 +24,5 @@
 from __future__ import absolute_import
 import os
 
-from runtime.PLCObject import PLCObject, PLCprint
+from runtime.PLCObject import PLCObject, PLCprint, MainWorker
 import runtime.ServicePublisher
--- a/runtime/typemapping.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/runtime/typemapping.py	Thu Apr 19 15:03:23 2018 +0200
@@ -35,7 +35,7 @@
 
 def _ttime():
     return (IEC_TIME,
-            lambda x: td(0, x.s, x.ns/1000),
+            lambda x: td(0, x.s, x.ns/1000.0),
             lambda t, x: t(x.days * 24 * 3600 + x.seconds, x.microseconds*1000))
 
 
--- a/targets/Xenomai/plc_Xenomai_main.c	Thu Apr 19 09:50:00 2018 +0200
+++ b/targets/Xenomai/plc_Xenomai_main.c	Thu Apr 19 15:03:23 2018 +0200
@@ -11,10 +11,10 @@
 #include <sys/mman.h>
 #include <sys/fcntl.h>
 
-#include <native/task.h>
-#include <native/timer.h>
-#include <native/sem.h>
-#include <native/pipe.h>
+#include <alchemy/task.h>
+#include <alchemy/timer.h>
+#include <alchemy/sem.h>
+#include <alchemy/pipe.h>
 
 unsigned int PLC_state = 0;
 #define PLC_STATE_TASK_CREATED                 1
@@ -37,6 +37,15 @@
 #define PYTHON_PIPE_MINOR            3
 #define PIPE_SIZE                    1 
 
+// rt-pipes commands
+
+#define PYTHON_PENDING_COMMAND 1
+#define PYTHON_FINISH 2
+
+#define DEBUG_FINISH 2
+
+#define DEBUG_PENDING_DATA 1
+#define DEBUG_UNLOCK 1
 
 long AtomicCompareExchange(long* atomicvar,long compared, long exchange)
 {
@@ -82,6 +91,18 @@
         if (PLC_shutdown) break;
         rt_task_wait_period(NULL);
     }
+    /* since xenomai 3 it is not enough to close() 
+       file descriptor to unblock read()... */
+    {
+        /* explicitely finish python thread */
+        char msg = PYTHON_FINISH;
+        rt_pipe_write(&WaitPython_pipe, &msg, sizeof(msg), P_NORMAL);
+    }
+    {
+        /* explicitely finish debug thread */
+        char msg = DEBUG_FINISH;
+        rt_pipe_write(&WaitDebug_pipe, &msg, sizeof(msg), P_NORMAL);
+    }
 }
 
 static unsigned long __debug_tick;
@@ -159,6 +180,15 @@
     exit(0);
 }
 
+#define _startPLCLog(text) \
+    {\
+    	char mstr[] = text;\
+        LogMessage(LOG_CRITICAL, mstr, sizeof(mstr));\
+        goto error;\
+    }
+
+#define FO "Failed opening "
+
 #define max_val(a,b) ((a>b)?a:b)
 int startPLC(int argc,char **argv)
 {
@@ -171,49 +201,55 @@
 
     /*** RT Pipes creation and opening ***/
     /* create Debug_pipe */
-    if(rt_pipe_create(&Debug_pipe, "Debug_pipe", DEBUG_PIPE_MINOR, PIPE_SIZE)) 
-        goto error;
+    if(rt_pipe_create(&Debug_pipe, "Debug_pipe", DEBUG_PIPE_MINOR, PIPE_SIZE) < 0) 
+        _startPLCLog(FO "Debug_pipe real-time end");
     PLC_state |= PLC_STATE_DEBUG_PIPE_CREATED;
 
     /* open Debug_pipe*/
-    if((Debug_pipe_fd = open(DEBUG_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((Debug_pipe_fd = open(DEBUG_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO DEBUG_PIPE_DEVICE);
     PLC_state |= PLC_STATE_DEBUG_FILE_OPENED;
 
     /* create Python_pipe */
-    if(rt_pipe_create(&Python_pipe, "Python_pipe", PYTHON_PIPE_MINOR, PIPE_SIZE)) 
-        goto error;
+    if(rt_pipe_create(&Python_pipe, "Python_pipe", PYTHON_PIPE_MINOR, PIPE_SIZE) < 0) 
+        _startPLCLog(FO "Python_pipe real-time end");
     PLC_state |= PLC_STATE_PYTHON_PIPE_CREATED;
 
     /* open Python_pipe*/
-    if((Python_pipe_fd = open(PYTHON_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((Python_pipe_fd = open(PYTHON_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO PYTHON_PIPE_DEVICE);
     PLC_state |= PLC_STATE_PYTHON_FILE_OPENED;
 
     /* create WaitDebug_pipe */
-    if(rt_pipe_create(&WaitDebug_pipe, "WaitDebug_pipe", WAITDEBUG_PIPE_MINOR, PIPE_SIZE))
-        goto error;
+    if(rt_pipe_create(&WaitDebug_pipe, "WaitDebug_pipe", WAITDEBUG_PIPE_MINOR, PIPE_SIZE) < 0)
+        _startPLCLog(FO "WaitDebug_pipe real-time end");
     PLC_state |= PLC_STATE_WAITDEBUG_PIPE_CREATED;
 
     /* open WaitDebug_pipe*/
-    if((WaitDebug_pipe_fd = open(WAITDEBUG_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((WaitDebug_pipe_fd = open(WAITDEBUG_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO WAITDEBUG_PIPE_DEVICE);
     PLC_state |= PLC_STATE_WAITDEBUG_FILE_OPENED;
 
     /* create WaitPython_pipe */
-    if(rt_pipe_create(&WaitPython_pipe, "WaitPython_pipe", WAITPYTHON_PIPE_MINOR, PIPE_SIZE))
-        goto error;
+    if(rt_pipe_create(&WaitPython_pipe, "WaitPython_pipe", WAITPYTHON_PIPE_MINOR, PIPE_SIZE) < 0)
+        _startPLCLog(FO "WaitPython_pipe real-time end");
     PLC_state |= PLC_STATE_WAITPYTHON_PIPE_CREATED;
 
     /* open WaitPython_pipe*/
-    if((WaitPython_pipe_fd = open(WAITPYTHON_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((WaitPython_pipe_fd = open(WAITPYTHON_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO WAITPYTHON_PIPE_DEVICE);
     PLC_state |= PLC_STATE_WAITPYTHON_FILE_OPENED;
 
     /*** create PLC task ***/
-    if(rt_task_create(&PLC_task, "PLC_task", 0, 50, T_JOINABLE)) goto error;
+    if(rt_task_create(&PLC_task, "PLC_task", 0, 50, T_JOINABLE))
+        _startPLCLog("Failed creating PLC task");
     PLC_state |= PLC_STATE_TASK_CREATED;
 
     if(__init(argc,argv)) goto error;
 
     /* start PLC task */
-    if(rt_task_start(&PLC_task, &PLC_task_proc, NULL)) goto error;
+    if(rt_task_start(&PLC_task, &PLC_task_proc, NULL))
+        _startPLCLog("Failed starting PLC task");
 
     return 0;
 
@@ -241,7 +277,6 @@
     return 0;
 }
 
-#define DEBUG_UNLOCK 1
 void LeaveDebugSection(void)
 {
     if(AtomicCompareExchange( &debug_state, 
@@ -254,7 +289,6 @@
 
 extern unsigned long __tick;
 
-#define DEBUG_PENDING_DATA 1
 int WaitDebugData(unsigned long *tick)
 {
     char cmd;
@@ -304,8 +338,6 @@
     AtomicCompareExchange( &debug_state, DEBUG_BUSY, DEBUG_FREE);
 }
 
-#define PYTHON_PENDING_COMMAND 1
-
 #define PYTHON_FREE 0
 #define PYTHON_BUSY 1
 static long python_state = PYTHON_FREE;
--- a/targets/plc_main_head.c	Thu Apr 19 09:50:00 2018 +0200
+++ b/targets/plc_main_head.c	Thu Apr 19 15:03:23 2018 +0200
@@ -35,6 +35,9 @@
 /* Help to quit cleanly when init fail at a certain level */
 static int init_level = 0;
 
+/* Prototype for Logging to help spotting errors at init */
+int LogMessage(uint8_t level, char* buf, uint32_t size);
+
 /*
  * Prototypes of functions exported by plugins
  **/
--- a/tests/tools/test_application.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/tests/tools/test_application.py	Thu Apr 19 15:03:23 2018 +0200
@@ -41,7 +41,7 @@
 
 class UserApplicationTest(unittest.TestCase):
     def InstallExceptionHandler(self):
-        def handle_exception(e_type, e_value, e_traceback):
+        def handle_exception(e_type, e_value, e_traceback, exit=False):
             # traceback.print_exception(e_type, e_value, e_traceback)
             self.exc_info = [e_type, e_value, e_traceback]
         self.exc_info = None
@@ -89,7 +89,9 @@
         # disable default exception handler in Beremiz
         self.app.InstallExceptionHandler = lambda: None
         self.InstallExceptionHandler()
+        self.app.handle_exception = sys.excepthook
         self.app.PreStart()
+        self.ProcessEvents()
         self.app.frame.Show()
         self.ProcessEvents()
         self.app.frame.ShowFullScreen(True)
--- a/xmlclass/xmlclass.py	Thu Apr 19 09:50:00 2018 +0200
+++ b/xmlclass/xmlclass.py	Thu Apr 19 15:03:23 2018 +0200
@@ -67,7 +67,7 @@
 QName_model = re.compile('((?:[a-zA-Z_][\w]*:)?[a-zA-Z_][\w]*)$')
 QNames_model = re.compile('((?:[a-zA-Z_][\w]*:)?[a-zA-Z_][\w]*(?: (?:[a-zA-Z_][\w]*:)?[a-zA-Z_][\w]*)*)$')
 NCName_model = re.compile('([a-zA-Z_][\w]*)$')
-URI_model = re.compile('((?:http://|/)?(?:[\w.-]*/?)*)$')
+URI_model = re.compile('((?:htt(p|ps)://|/)?(?:[\w.-]*/?)*)$')
 LANGUAGE_model = re.compile('([a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*)$')
 
 ONLY_ANNOTATION = re.compile("((?:annotation )?)")