Long due merge
authorEdouard Tisserant
Mon, 11 Mar 2019 13:51:07 +0100
changeset 2521 48ebcbe7f19b
parent 2493 0ad5e616d37f (diff)
parent 2520 3a1c0c161f80 (current diff)
child 2522 1378c18402c3
child 2523 aa7f138648f3
child 2530 02d09fc6eb90
Long due merge
PLCGenerator.py
ProjectController.py
connectors/PYRO_dialog.py
connectors/WAMP_dialog.py
controls/DiscoveryPanel.py
dialogs/UriEditor.py
modbus/mb_runtime.c
--- a/BeremizIDE.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/BeremizIDE.py	Mon Mar 11 13:51:07 2019 +0100
@@ -25,6 +25,7 @@
 
 
 from __future__ import absolute_import
+from __future__ import print_function
 import os
 import sys
 import tempfile
@@ -208,10 +209,6 @@
     def write_error(self, s):
         self.write(s, self.red_yellow)
 
-    def writeyield(self, s):
-        self.write(s)
-        wx.GetApp().Yield()
-
     def flush(self):
         # Temporary deactivate read only mode on StyledTextCtrl for clearing
         # text. It seems that text modifications, even programmatically, are
@@ -614,11 +611,7 @@
     def AddToDoBeforeQuit(self, Thing):
         self.ToDoBeforeQuit.append(Thing)
 
-    def OnCloseFrame(self, event):
-        for evt_type in [wx.EVT_SET_FOCUS,
-                         wx.EVT_KILL_FOCUS,
-                         wx.stc.EVT_STC_UPDATEUI]:
-            self.LogConsole.Unbind(evt_type)
+    def TryCloseFrame(self):
         if self.CTR is None or self.CheckSaveBeforeClosing(_("Close Application")):
             if self.CTR is not None:
                 self.CTR.KillDebugThread()
@@ -630,8 +623,14 @@
                 Thing()
             self.ToDoBeforeQuit = []
 
+            return True
+        return False
+
+    def OnCloseFrame(self, event):
+        if self.TryCloseFrame():
             event.Skip()
         else:
+            # prevent event to continue, i.e. cancel closing
             event.Veto()
 
     def RefreshFileMenu(self):
--- a/Beremiz_service.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/Beremiz_service.py	Mon Mar 11 13:51:07 2019 +0100
@@ -30,17 +30,18 @@
 import sys
 import getopt
 import threading
-from threading import Thread, Semaphore, Lock
-import traceback
+from threading import Thread, Semaphore, Lock, currentThread
+import __builtin__
 from builtins import str as text
 from past.builtins import execfile
 from six.moves import builtins
-import Pyro
-import Pyro.core as pyro
-
-from runtime import PLCObject, ServicePublisher, MainWorker
+
+import runtime
+from runtime.PyroServer import PyroServer
 from runtime.xenomai import TryPreloadXenomai
+from runtime import LogMessageAndException
 from runtime import PlcStatus
+from runtime.Stunnel import ensurePSK
 import util.paths as paths
 
 
@@ -54,7 +55,7 @@
     print("""
 Usage of Beremiz PLC execution service :\n
 %s {[-n servicename] [-i IP] [-p port] [-x enabletaskbar] [-a autostart]|-h|--help} working_dir
-  -n  zeroconf service name (default:disabled)
+  -n  service name (default:None, zeroconf discovery disabled)
   -i  IP address of interface to bind to (default:localhost)
   -p  port number default:3000
   -h  print this help text and quit
@@ -63,7 +64,7 @@
   -t  enable/disable Twisted web interface (0:disable 1:enable) (default:1)
   -w  web server port or "off" to disable web server (default:8009)
   -c  WAMP client config file (can be overriden by wampconf.json in project)
-  -s  WAMP client secret, given as a file (can be overriden by wamp.secret in project)
+  -s  PSK secret path (default:PSK disabled)
   -e  python extension (absolute path .py)
 
            working_dir - directory where are stored PLC files
@@ -79,10 +80,10 @@
     sys.exit(2)
 
 # default values
-given_ip = None
+interface = ''
 port = 3000
 webport = 8009
-wampsecret = None
+PSKpath = None
 wampconf = None
 servicename = None
 autostart = False
@@ -101,8 +102,10 @@
         version()
         sys.exit()
     elif o == "-i":
-        if len(a.split(".")) == 4 or a == "localhost":
-            given_ip = a
+        if len(a.split(".")) == 4:
+            interface = a
+        elif a == "localhost":
+            interface = '127.0.0.1'
         else:
             usage()
             sys.exit()
@@ -122,7 +125,7 @@
     elif o == "-c":
         wampconf = None if a == "off" else a
     elif o == "-s":
-        wampsecret = None if a == "off" else a
+        PSKpath = None if a == "off" else a
     elif o == "-e":
         fnameanddirname = list(os.path.split(os.path.realpath(a)))
         fnameanddirname.reverse()
@@ -144,11 +147,10 @@
     WorkingDir = os.getcwd()
     argv = [WorkingDir]
 
-if __name__ == '__main__':
-    builtins.__dict__['_'] = lambda x: x
-    # TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem
-    TryPreloadXenomai()
-    version()
+builtins.__dict__['_'] = lambda x: x
+# TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem
+TryPreloadXenomai()
+version()
 
 
 def Bpath(*args):
@@ -187,9 +189,8 @@
     def unicode_translation(message):
         return wx.GetTranslation(message).encode(default_locale)
 
-    if __name__ == '__main__':
-        builtins.__dict__['_'] = unicode_translation
-        # builtins.__dict__['_'] = wx.GetTranslation
+    builtins.__dict__['_'] = unicode_translation
+    # builtins.__dict__['_'] = wx.GetTranslation
 
 
 # Life is hard... have a candy.
@@ -256,12 +257,11 @@
             TBMENU_CHANGE_WD = wx.NewId()
             TBMENU_QUIT = wx.NewId()
 
-            def __init__(self, pyroserver, level):
+            def __init__(self, pyroserver):
                 wx.TaskBarIcon.__init__(self)
                 self.pyroserver = pyroserver
                 # Set the image
                 self.UpdateIcon(None)
-                self.level = level
 
                 # bind some events
                 self.Bind(wx.EVT_MENU, self.OnTaskBarStartPLC, id=self.TBMENU_START)
@@ -284,15 +284,14 @@
                 menu = wx.Menu()
                 menu.Append(self.TBMENU_START, _("Start PLC"))
                 menu.Append(self.TBMENU_STOP, _("Stop PLC"))
-                if self.level == 1:
-                    menu.AppendSeparator()
-                    menu.Append(self.TBMENU_CHANGE_NAME, _("Change Name"))
-                    menu.Append(self.TBMENU_CHANGE_INTERFACE, _("Change IP of interface to bind"))
-                    menu.Append(self.TBMENU_CHANGE_PORT, _("Change Port Number"))
-                    menu.Append(self.TBMENU_CHANGE_WD, _("Change working directory"))
-                    menu.AppendSeparator()
-                    menu.Append(self.TBMENU_LIVE_SHELL, _("Launch a live Python shell"))
-                    menu.Append(self.TBMENU_WXINSPECTOR, _("Launch WX GUI inspector"))
+                menu.AppendSeparator()
+                menu.Append(self.TBMENU_CHANGE_NAME, _("Change Name"))
+                menu.Append(self.TBMENU_CHANGE_INTERFACE, _("Change IP of interface to bind"))
+                menu.Append(self.TBMENU_CHANGE_PORT, _("Change Port Number"))
+                menu.Append(self.TBMENU_CHANGE_WD, _("Change working directory"))
+                menu.AppendSeparator()
+                menu.Append(self.TBMENU_LIVE_SHELL, _("Launch a live Python shell"))
+                menu.Append(self.TBMENU_WXINSPECTOR, _("Launch WX GUI inspector"))
                 menu.AppendSeparator()
                 menu.Append(self.TBMENU_QUIT, _("Quit"))
                 return menu
@@ -311,19 +310,10 @@
                 return icon
 
             def OnTaskBarStartPLC(self, evt):
-                if self.pyroserver.plcobj is not None:
-                    plcstatus = self.pyroserver.plcobj.GetPLCstatus()[0]
-                    if plcstatus is PlcStatus.Stopped:
-                        self.pyroserver.plcobj.StartPLC()
-                    else:
-                        print(_("PLC is empty or already started."))
+                runtime.GetPLCObjectSingleton().StartPLC()
 
             def OnTaskBarStopPLC(self, evt):
-                if self.pyroserver.plcobj is not None:
-                    if self.pyroserver.plcobj.GetPLCstatus()[0] == PlcStatus.Started:
-                        Thread(target=self.pyroserver.plcobj.StopPLC).start()
-                    else:
-                        print(_("PLC is not started."))
+                runtime.GetPLCObjectSingleton().StopPLC()
 
             def OnTaskBarChangeInterface(self, evt):
                 ip_addr = self.pyroserver.ip_addr
@@ -350,19 +340,16 @@
                     self.pyroserver.Restart()
 
             def OnTaskBarChangeName(self, evt):
-                servicename = self.pyroserver.servicename
-                servicename = '' if servicename is None else servicename
-                dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=servicename)
+                _servicename = self.pyroserver.servicename
+                _servicename = '' if _servicename is None else _servicename
+                dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=_servicename)
                 dlg.SetTests([(lambda name: len(name) is not 0, _("Name must not be null!"))])
                 if dlg.ShowModal() == wx.ID_OK:
                     self.pyroserver.servicename = dlg.GetValue()
                     self.pyroserver.Restart()
 
             def _LiveShellLocals(self):
-                if self.pyroserver.plcobj is not None:
-                    return {"locals": self.pyroserver.plcobj.python_runtime_vars}
-                else:
-                    return {}
+                return {"locals": runtime.GetPLCObjectSingleton().python_runtime_vars}
 
             def OnTaskBarLiveShell(self, evt):
                 from wx import py
@@ -406,89 +393,6 @@
     return res
 
 
-class Server(object):
-    def __init__(self, servicename, ip_addr, port,
-                 workdir, argv,
-                 statuschange=None, evaluator=default_evaluator,
-                 pyruntimevars=None):
-        self.continueloop = True
-        self.daemon = None
-        self.servicename = servicename
-        self.ip_addr = ip_addr
-        self.port = port
-        self.workdir = workdir
-        self.argv = argv
-        self.servicepublisher = None
-        self.statuschange = statuschange
-        self.evaluator = evaluator
-        self.pyruntimevars = pyruntimevars
-        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.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()
-
-    def Restart(self):
-        self._stop()
-
-    def Quit(self):
-        self.continueloop = False
-        if self.plcobj is not None:
-            self.plcobj.StopPLC()
-            self.plcobj.UnLoadPLC()
-        self._stop()
-
-    def _stop(self):
-        if self.plcobj is not None:
-            self.plcobj.StopPLC()
-        if self.servicepublisher is not None:
-            self.servicepublisher.UnRegisterService()
-            self.servicepublisher = None
-        self.daemon.shutdown(True)
-
-    def AutoLoad(self):
-        self.plcobj.AutoLoad()
-        if self.plcobj.GetPLCstatus()[0] == PlcStatus.Stopped:
-            if autostart:
-                self.plcobj.StartPLC()
-        self.plcobj.StatusChange()
-
-
 if enabletwisted:
     import warnings
     with warnings.catch_warnings():
@@ -511,10 +415,11 @@
     if havewx:
         reactor.registerWxApp(app)
 
+twisted_reactor_thread_id = None
+ui_thread = None
+
 if havewx:
     wx_eval_lock = Semaphore(0)
-    # FIXME : beware wx mainloop is _not_ running in main thread
-    # main_thread = currentThread()
 
     def statuschangeTskBar(status):
         wx.CallAfter(taskbar_instance.UpdateIcon, status)
@@ -527,40 +432,30 @@
         wx_eval_lock.release()
 
     def evaluator(tocall, *args, **kwargs):
-        # FIXME : should implement anti-deadlock
-        # if main_thread == currentThread():
-        #     # avoid dead lock if called from the wx mainloop
-        #     return default_evaluator(tocall, *args, **kwargs)
-        # else:
-        o = type('', (object,), dict(call=(tocall, args, kwargs), res=None))
-        wx.CallAfter(wx_evaluator, o)
-        wx_eval_lock.acquire()
-        return o.res
-
-    pyroserver = Server(servicename, given_ip, port,
-                        WorkingDir, argv,
-                        statuschange, evaluator, pyruntimevars)
-
-    taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx)
+        # To prevent deadlocks, check if current thread is not one of the UI
+        # UI threads can be either the one from WX main loop or
+        # worker thread from twisted "threadselect" reactor
+        current_id = currentThread().ident
+
+        if ui_thread is not None \
+            and ui_thread.ident != current_id \
+            and (not havetwisted or (
+                    twisted_reactor_thread_id is not None
+                    and twisted_reactor_thread_id != current_id)):
+
+            o = type('', (object,), dict(call=(tocall, args, kwargs), res=None))
+            wx.CallAfter(wx_evaluator, o)
+            wx_eval_lock.acquire()
+            return o.res
+        else:
+            # avoid dead lock if called from the wx mainloop
+            return default_evaluator(tocall, *args, **kwargs)
 else:
-    pyroserver = Server(servicename, given_ip, port,
-                        WorkingDir, argv,
-                        statuschange, pyruntimevars=pyruntimevars)
-
+    evaluator = default_evaluator
 
 # Exception hooks
 
 
-def LogMessageAndException(msg, exp=None):
-    if exp is None:
-        exp = sys.exc_info()
-    if pyroserver.plcobj is not None:
-        pyroserver.plcobj.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp)))
-    else:
-        print(msg)
-        traceback.print_exception(*exp)
-
-
 def LogException(*exp):
     LogMessageAndException("", exp)
 
@@ -610,10 +505,24 @@
     sys.path.append(extension_folder)
     execfile(os.path.join(extension_folder, extention_file), locals())
 
+# Service name is used as an ID for stunnel's PSK
+# Some extension may set 'servicename' to a computed ID or Serial Number
+# instead of using commandline '-n'
+if servicename is not None and PSKpath is not None:
+    ensurePSK(servicename, PSKpath)
+
+runtime.CreatePLCObjectSingleton(
+    WorkingDir, argv, statuschange, evaluator, pyruntimevars)
+
+pyroserver = PyroServer(servicename, interface, port)
+
+if havewx:
+    taskbar_instance = BeremizTaskBarIcon(pyroserver)
+
 if havetwisted:
     if webport is not None:
         try:
-            website = NS.RegisterWebsite(webport)
+            website = NS.RegisterWebsite(interface, webport)
             pyruntimevars["website"] = website
             NS.SetServer(pyroserver)
             statuschange.append(NS.website_statuslistener_factory(website))
@@ -623,7 +532,7 @@
     if havewamp:
         try:
             WC.SetServer(pyroserver)
-            WC.RegisterWampClient(wampconf, wampsecret)
+            WC.RegisterWampClient(wampconf, PSKpath)
             WC.RegisterWebSettings(NS)
         except Exception:
             LogMessageAndException(_("WAMP client startup failed. "))
@@ -639,6 +548,11 @@
 
 pyroserver.PrintServerInfo()
 
+# Beremiz IDE detects LOCAL:// runtime is ready by looking
+# for self.workdir in the daemon's stdout.
+sys.stdout.write(_("Current working directory :") + WorkingDir + "\n")
+sys.stdout.flush()
+
 if havetwisted or havewx:
     ui_thread_started = Lock()
     ui_thread_started.acquire()
@@ -656,7 +570,11 @@
 
     # This order ui loop to unblock main thread when ready.
     if havetwisted:
-        reactor.callLater(0, ui_thread_started.release)
+        def signal_uithread_started():
+            global twisted_reactor_thread_id
+            twisted_reactor_thread_id = currentThread().ident
+            ui_thread_started.release()
+        reactor.callLater(0, signal_uithread_started)
     else:
         wx.CallAfter(ui_thread_started.release)
 
@@ -665,9 +583,15 @@
     print("UI thread started successfully.")
 
 try:
-    MainWorker.runloop(pyroserver.AutoLoad)
+    runtime.MainWorker.runloop(
+        runtime.GetPLCObjectSingleton().AutoLoad, autostart)
 except KeyboardInterrupt:
     pass
 
 pyroserver.Quit()
+
+plcobj = runtime.GetPLCObjectSingleton()
+plcobj.StopPLC()
+plcobj.UnLoadPLC()
+
 sys.exit(0)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PSKManagement.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,160 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import os
+import time
+import json
+from zipfile import ZipFile
+
+# PSK Management Data model :
+# [[ID,Desc, LastKnownURI, LastConnect]]
+COL_ID, COL_URI, COL_DESC, COL_LAST = range(4)
+REPLACE, REPLACE_ALL, KEEP, KEEP_ALL, CANCEL = range(5)
+
+
+def _pskpath(project_path):
+    return os.path.join(project_path, 'psk')
+
+
+def _mgtpath(project_path):
+    return os.path.join(_pskpath(project_path), 'management.json')
+
+
+def _ensurePSKdir(project_path):
+    pskpath = _pskpath(project_path)
+    if not os.path.exists(pskpath):
+        os.mkdir(pskpath)
+    return pskpath
+
+
+def _default(ID):
+    return [ID,
+            '',  # default description
+            None,  # last known URI
+            None]  # last connection date
+
+
+def _dataByID(data):
+    return {row[COL_ID]: row for row in data}
+
+
+def _LoadData(project_path):
+    """ load known keys metadata """
+    if os.path.isdir(_pskpath(project_path)):
+        _path = _mgtpath(project_path)
+        if os.path.exists(_path):
+            return json.loads(open(_path).read())
+    return []
+
+
+def _filterData(psk_files, data_input):
+    input_by_ID = _dataByID(data_input)
+    output = []
+    # go through all secret files available an build data
+    # out of data recoverd from json and list of secret.
+    # this implicitly filters IDs out of metadata who's
+    # secret is missing
+    for filename in psk_files:
+        if filename.endswith('.secret'):
+            ID = filename[:-7]  # strip filename extension
+            output.append(input_by_ID.get(ID, _default(ID)))
+    return output
+
+
+def GetData(project_path):
+    loaded_data = _LoadData(project_path)
+    if loaded_data:
+        psk_files = os.listdir(_pskpath(project_path))
+        return _filterData(psk_files, loaded_data)
+    return []
+
+
+def DeleteID(project_path, ID):
+    secret_path = os.path.join(_pskpath(project_path), ID+'.secret')
+    os.remove(secret_path)
+
+
+def SaveData(project_path, data):
+    _ensurePSKdir(project_path)
+    with open(_mgtpath(project_path), 'w') as f:
+        f.write(json.dumps(data))
+
+
+def UpdateID(project_path, ID, secret, URI):
+    pskpath = _ensurePSKdir(project_path)
+    if not os.path.exists(pskpath):
+        os.mkdir(pskpath)
+
+    secpath = os.path.join(pskpath, ID+'.secret')
+    with open(secpath, 'w') as f:
+        f.write(ID+":"+secret)
+
+    # here we directly use _LoadData, avoiding filtering that could be long
+    data = _LoadData(project_path)
+    idata = _dataByID(data)
+    dataForID = idata.get(ID, None) if data else None
+
+    _is_new_ID = dataForID is None
+    if _is_new_ID:
+        dataForID = _default(ID)
+
+    dataForID[COL_URI] = URI
+    # FIXME : could store time instead os a string and use DVC model's cmp
+    # then date display could be smarter, etc - sortable sting hack for now
+    dataForID[COL_LAST] = time.strftime('%y/%M/%d-%H:%M:%S')
+
+    if _is_new_ID:
+        data.append(dataForID)
+
+    SaveData(project_path, data)
+
+
+def ExportIDs(project_path, export_zip):
+    with ZipFile(export_zip, 'w') as zf:
+        path = _pskpath(project_path)
+        for nm in os.listdir(path):
+            if nm.endswith('.secret') or nm == 'management.json':
+                zf.write(os.path.join(path, nm), nm)
+
+
+def ImportIDs(project_path, import_zip, should_I_replace_callback):
+    zf = ZipFile(import_zip, 'r')
+    data = GetData(project_path)
+
+    zip_loaded_data = json.loads(zf.open('management.json').read())
+    name_list = zf.namelist()
+    zip_filtered_data = _filterData(name_list, zip_loaded_data)
+
+    idata = _dataByID(data)
+
+    keys_to_import = []
+    result = None
+
+    for imported_row in zip_filtered_data:
+        ID = imported_row[COL_ID]
+        existing_row = idata.get(ID, None)
+        if existing_row is None:
+            data.append(imported_row)
+        else:
+            # callback returns the selected list for merge or none if canceled
+            if result not in [REPLACE_ALL, KEEP_ALL]:
+                result = should_I_replace_callback(existing_row, imported_row)
+
+            if result == CANCEL:
+                return
+
+            if result in [REPLACE_ALL, REPLACE]:
+                # replace with imported
+                existing_row[:] = imported_row
+                # copy the key of selected
+                keys_to_import.append(ID)
+
+    for ID in keys_to_import:
+        zf.extract(ID+".secret", _pskpath(project_path))
+
+    SaveData(project_path, data)
+
+    return data
--- a/ProjectController.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/ProjectController.py	Mon Mar 11 13:51:07 2019 +0100
@@ -56,7 +56,7 @@
 from editors.ProjectNodeEditor import ProjectNodeEditor
 from editors.IECCodeViewer import IECCodeViewer
 from editors.DebugViewer import DebugViewer, REFRESH_PERIOD
-from dialogs import DiscoveryDialog
+from dialogs import UriEditor, IDManager
 from PLCControler import PLCControler
 from plcopen.structures import IEC_KEYWORDS
 from plcopen.types_enums import ComputeConfigurationResourceName, ITEM_CONFNODE
@@ -269,8 +269,10 @@
         self._setBuildPath(None)
         self.debug_break = False
         self.previous_plcstate = None
-        # copy ConfNodeMethods so that it can be later customized
+        # copy StatusMethods so that it can be later customized
         self.StatusMethods = [dic.copy() for dic in self.StatusMethods]
+        self.DebugToken = None
+        self.debug_status = PlcStatus.Stopped
 
     def __del__(self):
         if self.DebugTimer:
@@ -280,10 +282,14 @@
     def LoadLibraries(self):
         self.Libraries = []
         TypeStack = []
-        for libname, clsname, _default in features.libraries:
-            if self.BeremizRoot.Libraries is not None and \
-               getattr(self.BeremizRoot.Libraries,
-                       "Enable_" + libname + "_Library"):
+        for libname, clsname, lib_enabled in features.libraries:
+            if self.BeremizRoot.Libraries is not None:
+                enable_attr = getattr(self.BeremizRoot.Libraries,
+                                      "Enable_" + libname + "_Library")
+                if enable_attr is not None:
+                    lib_enabled = enable_attr
+
+            if lib_enabled:
                 Lib = GetClassImporter(clsname)()(self, libname, TypeStack)
                 TypeStack.append(Lib.GetTypes())
                 self.Libraries.append(Lib)
@@ -897,19 +903,6 @@
             self._builder = targetclass(self)
         return self._builder
 
-    def ResetBuildMD5(self):
-        builder = self.GetBuilder()
-        if builder is not None:
-            builder.ResetBinaryCodeMD5()
-        self.EnableMethod("_Transfer", False)
-
-    def GetLastBuildMD5(self):
-        builder = self.GetBuilder()
-        if builder is not None:
-            return builder.GetBinaryCodeMD5()
-        else:
-            return None
-
     #
     #
     #                C CODE GENERATION METHODS
@@ -1137,7 +1130,6 @@
         # If IEC code gen fail, bail out.
         if not IECGenRes:
             self.logger.write_error(_("PLC code generation failed !\n"))
-            self.ResetBuildMD5()
             return False
 
         # Reset variable and program list that are parsed from
@@ -1153,7 +1145,6 @@
         builder = self.GetBuilder()
         if builder is None:
             self.logger.write_error(_("Fatal : cannot get builder.\n"))
-            self.ResetBuildMD5()
             return False
 
         # Build
@@ -1162,9 +1153,9 @@
                 self.logger.write_error(_("C Build failed.\n"))
                 return False
         except Exception:
+            builder.ResetBinaryMD5()
             self.logger.write_error(_("C Build crashed !\n"))
             self.logger.write_error(traceback.format_exc())
-            self.ResetBuildMD5()
             return False
 
         self.logger.write(_("Successfully built.\n"))
@@ -1184,7 +1175,6 @@
             self.logger.write_error(
                 _("Runtime IO extensions C code generation failed !\n"))
             self.logger.write_error(traceback.format_exc())
-            self.ResetBuildMD5()
             return False
 
         # Generate C code and compilation params from liraries
@@ -1195,7 +1185,6 @@
             self.logger.write_error(
                 _("Runtime library extensions C code generation failed !\n"))
             self.logger.write_error(traceback.format_exc())
-            self.ResetBuildMD5()
             return False
 
         self.LocationCFilesAndCFLAGS = LibCFilesAndCFLAGS + \
@@ -1245,7 +1234,6 @@
             except Exception:
                 self.logger.write_error(name + _(" generation failed !\n"))
                 self.logger.write_error(traceback.format_exc())
-                self.ResetBuildMD5()
                 return False
         self.logger.write(_("C code generated successfully.\n"))
         return True
@@ -1270,6 +1258,11 @@
 
     _IECCodeView = None
 
+    def _showIDManager(self):
+        dlg = IDManager(self.AppFrame, self)
+        dlg.ShowModal()
+        dlg.Destroy()
+
     def _showIECcode(self):
         self._OpenView("IEC code")
 
@@ -1498,11 +1491,12 @@
         self.UpdateMethodsFromPLCStatus()
 
     def SnapshotAndResetDebugValuesBuffers(self):
-        if self._connector is not None:
-            plc_status, Traces = self._connector.GetTraceVariables()
+        debug_status = PlcStatus.Disconnected
+        if self._connector is not None and self.DebugToken is not None:
+            debug_status, Traces = self._connector.GetTraceVariables(self.DebugToken)
             # print [dict.keys() for IECPath, (dict, log, status, fvalue) in
             # self.IECdebug_datas.items()]
-            if plc_status == PlcStatus.Started:
+            if debug_status == PlcStatus.Started:
                 if len(Traces) > 0:
                     for debug_tick, debug_buff in Traces:
                         debug_vars = UnpackDebugBuffer(
@@ -1530,14 +1524,14 @@
 
         ticks, self.DebugTicks = self.DebugTicks, []
 
-        return ticks, buffers
+        return debug_status, ticks, buffers
 
     def RegisterDebugVarToConnector(self):
         self.DebugTimer = None
         Idxs = []
         self.TracedIECPath = []
         self.TracedIECTypes = []
-        if self._connector is not None:
+        if self._connector is not None and self.debug_status != PlcStatus.Broken:
             IECPathsToPop = []
             for IECPath, data_tuple in self.IECdebug_datas.iteritems():
                 WeakCallableDict, _data_log, _status, fvalue, _buffer_list = data_tuple
@@ -1566,11 +1560,12 @@
                 IdxsT = zip(*Idxs)
                 self.TracedIECPath = IdxsT[3]
                 self.TracedIECTypes = IdxsT[1]
-                self._connector.SetTraceVariablesList(zip(*IdxsT[0:3]))
+                self.DebugToken = self._connector.SetTraceVariablesList(zip(*IdxsT[0:3]))
             else:
                 self.TracedIECPath = []
                 self._connector.SetTraceVariablesList([])
-            self.SnapshotAndResetDebugValuesBuffers()
+                self.DebugToken = None
+            self.debug_status, _debug_ticks, _buffers = self.SnapshotAndResetDebugValuesBuffers()
 
     def IsPLCStarted(self):
         return self.previous_plcstate == PlcStatus.Started
@@ -1686,7 +1681,7 @@
         return self._connector.RemoteExec(script, **kwargs)
 
     def DispatchDebugValuesProc(self, event):
-        debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
+        self.debug_status, debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
         start_time = time.time()
         if len(self.TracedIECPath) == len(buffers):
             for IECPath, values in zip(self.TracedIECPath, buffers):
@@ -1697,11 +1692,15 @@
                 self.CallWeakcallables(
                     "__tick__", "NewDataAvailable", debug_ticks)
 
-        delay = time.time() - start_time
-        next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
-        if self.DispatchDebugValuesTimer is not None:
-            self.DispatchDebugValuesTimer.Start(
-                int(next_refresh * 1000), oneShot=True)
+        if self.debug_status == PlcStatus.Broken:
+            self.logger.write_warning(
+                _("Debug: token rejected - other debug took over - reconnect to recover\n"))
+        else:
+            delay = time.time() - start_time
+            next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
+            if self.DispatchDebugValuesTimer is not None:
+                self.DispatchDebugValuesTimer.Start(
+                    int(next_refresh * 1000), oneShot=True)
         event.Skip()
 
     def KillDebugThread(self):
@@ -1712,6 +1711,9 @@
         self.previous_plcstate = None
         if self.AppFrame:
             self.AppFrame.ResetGraphicViewers()
+
+        self.debug_status = PlcStatus.Started
+
         self.RegisterDebugVarToConnector()
         if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Start(
@@ -1748,7 +1750,6 @@
         if connector is not None:
             if self.StatusTimer is not None:
                 # Start the status Timer
-                wx.Yield()
                 self.StatusTimer.Start(milliseconds=500, oneShot=False)
         else:
             if self.StatusTimer is not None:
@@ -1771,9 +1772,9 @@
         if uri == "":
             try:
                 # Launch Service Discovery dialog
-                dialog = DiscoveryDialog(self.AppFrame)
+                dialog = UriEditor(self.AppFrame, self)
                 answer = dialog.ShowModal()
-                uri = dialog.GetURI()
+                uri = str(dialog.GetURI())
                 dialog.Destroy()
             except Exception:
                 self.logger.write_error(_("Local service discovery failed!\n"))
@@ -1798,10 +1799,10 @@
         # Get connector from uri
         try:
             self._SetConnector(connectors.ConnectorFactory(uri, self))
-        except Exception:
+        except Exception as e:
             self.logger.write_error(
-                _("Exception while connecting %s!\n") % uri)
-            self.logger.write_error(traceback.format_exc())
+                _("Exception while connecting to '{uri}': {ex}\n").format(
+                    uri=uri, ex=e))
 
         # Did connection success ?
         if self._connector is None:
@@ -1823,26 +1824,20 @@
     def CompareLocalAndRemotePLC(self):
         if self._connector is None:
             return
-        # We are now connected. Update button status
-        MD5 = self.GetLastBuildMD5()
+        builder = self.GetBuilder()
+        if builder is None:
+            return
+        MD5 = builder.GetBinaryMD5()
+        if MD5 is None:
+            return
         # Check remote target PLC correspondance to that md5
-        if MD5 is not None:
-            if not self._connector.MatchMD5(MD5):
-                # self.logger.write_warning(
-                # _("Latest build does not match with target, please
-                # transfer.\n"))
-                self.EnableMethod("_Transfer", True)
-            else:
-                # self.logger.write(
-                #     _("Latest build matches target, no transfer needed.\n"))
-                self.EnableMethod("_Transfer", True)
-                # warns controller that program match
-                self.ProgramTransferred()
-                # self.EnableMethod("_Transfer", False)
+        if self._connector.MatchMD5(MD5):
+            self.logger.write(
+                _("Latest build matches with connected target.\n"))
+            self.ProgramTransferred()
         else:
-            # self.logger.write_warning(
-            #     _("Cannot compare latest build to target. Please build.\n"))
-            self.EnableMethod("_Transfer", False)
+            self.logger.write(
+                _("Latest build does not match with connected target.\n"))
 
     def _Disconnect(self):
         self._SetConnector(None)
@@ -1858,8 +1853,13 @@
             else:
                 return
 
-        # Get the last build PLC's
-        MD5 = self.GetLastBuildMD5()
+        builder = self.GetBuilder()
+        if builder is None:
+            self.logger.write_error(_("Fatal : cannot get builder.\n"))
+            return False
+
+        # recover md5 from last build
+        MD5 = builder.GetBinaryMD5()
 
         # Check if md5 file is empty : ask user to build PLC
         if MD5 is None:
@@ -1872,35 +1872,44 @@
             self.logger.write(
                 _("Latest build already matches current target. Transfering anyway...\n"))
 
-        # Get temprary directory path
+        # purge any non-finished transfer
+        # note: this would abord any runing transfer with error
+        self._connector.PurgeBlobs()
+
+        # transfer extra files
         extrafiles = []
         for extrafilespath in [self._getExtraFilesPath(),
                                self._getProjectFilesPath()]:
 
-            extrafiles.extend(
-                [(name, open(os.path.join(extrafilespath, name),
-                             'rb').read())
-                 for name in os.listdir(extrafilespath)])
+            for name in os.listdir(extrafilespath):
+                extrafiles.append((
+                    name,
+                    self._connector.BlobFromFile(
+                        # use file name as a seed to avoid collisions
+                        # with files having same content
+                        os.path.join(extrafilespath, name), name)))
 
         # Send PLC on target
-        builder = self.GetBuilder()
-        if builder is not None:
-            data = builder.GetBinaryCode()
-            if data is not None:
-                if self._connector.NewPLC(MD5, data, extrafiles) and self.GetIECProgramsAndVariables():
-                    self.UnsubscribeAllDebugIECVariable()
-                    self.ProgramTransferred()
-                    if self.AppFrame is not None:
-                        self.AppFrame.CloseObsoleteDebugTabs()
-                        self.AppFrame.RefreshPouInstanceVariablesPanel()
-                    self.logger.write(_("Transfer completed successfully.\n"))
-                    self.AppFrame.LogViewer.ResetLogCounters()
-                else:
-                    self.logger.write_error(_("Transfer failed\n"))
-                self.HidePLCProgress()
+        object_path = builder.GetBinaryPath()
+        # arbitrarily use MD5 as a seed, could be any string
+        object_blob = self._connector.BlobFromFile(object_path, MD5)
+
+        self.HidePLCProgress()
+
+        self.logger.write(_("PLC data transfered successfully.\n"))
+
+        if self._connector.NewPLC(MD5, object_blob, extrafiles):
+            if self.GetIECProgramsAndVariables():
+                self.UnsubscribeAllDebugIECVariable()
+                self.ProgramTransferred()
+                self.AppFrame.CloseObsoleteDebugTabs()
+                self.AppFrame.RefreshPouInstanceVariablesPanel()
+                self.AppFrame.LogViewer.ResetLogCounters()
+                self.logger.write(_("PLC installed successfully.\n"))
             else:
-                self.logger.write_error(
-                    _("No PLC to transfer (did build succeed ?)\n"))
+                self.logger.write_error(_("Missing debug data\n"))
+        else:
+            self.logger.write_error(_("PLC couldn't be installed\n"))
 
         wx.CallAfter(self.UpdateMethodsFromPLCStatus)
 
@@ -1953,6 +1962,12 @@
             "shown":      False,
         },
         {
+            "bitmap":    "IDManager",
+            "name":    _("ID Manager"),
+            "tooltip": _("Manage secure connection identities"),
+            "method":   "_showIDManager",
+        },
+        {
             "bitmap":    "ShowIECcode",
             "name":    _("Show code"),
             "tooltip": _("Show IEC code generated by PLCGenerator"),
--- a/bacnet/runtime/device.c	Tue Mar 05 11:43:20 2019 +0300
+++ b/bacnet/runtime/device.c	Mon Mar 11 13:51:07 2019 +0100
@@ -398,7 +398,7 @@
     PROP_OBJECT_TYPE,                      /* R  R ( 79) */
     PROP_SYSTEM_STATUS,                    /* R  R (112) */
     PROP_VENDOR_NAME,                      /* R  R (121) */
-    PROP_VENDOR_IDENTIFIER,                /* W  R (120) */
+    PROP_VENDOR_IDENTIFIER,                /* R  R (120) */
     PROP_MODEL_NAME,                       /* W  R ( 70) */
     PROP_FIRMWARE_REVISION,                /* R  R ( 44) */
     PROP_APPLICATION_SOFTWARE_VERSION,     /* R  R ( 12) */
@@ -1366,16 +1366,16 @@
                 apdu_timeout_set((uint16_t) value.type.Unsigned_Int);
             }
             break;
-        case PROP_VENDOR_IDENTIFIER:
-            status =
-                WPValidateArgType(&value, BACNET_APPLICATION_TAG_UNSIGNED_INT,
-                &wp_data->error_class, &wp_data->error_code);
-            if (status) {
-                /* FIXME: bounds check? */
-                Device_Set_Vendor_Identifier((uint16_t) value.
-                    type.Unsigned_Int);
-            }
-            break;
+//         case PROP_VENDOR_IDENTIFIER:
+//             status =
+//                 WPValidateArgType(&value, BACNET_APPLICATION_TAG_UNSIGNED_INT,
+//                 &wp_data->error_class, &wp_data->error_code);
+//             if (status) {
+//                 /* FIXME: bounds check? */
+//                 Device_Set_Vendor_Identifier((uint16_t) value.
+//                     type.Unsigned_Int);
+//             }
+//             break;
 //       case PROP_SYSTEM_STATUS:
 //           status =
 //               WPValidateArgType(&value, BACNET_APPLICATION_TAG_ENUMERATED,
@@ -1453,6 +1453,7 @@
         case PROP_OBJECT_TYPE:
         case PROP_SYSTEM_STATUS:
         case PROP_VENDOR_NAME:
+        case PROP_VENDOR_IDENTIFIER:
         case PROP_FIRMWARE_REVISION:
         case PROP_APPLICATION_SOFTWARE_VERSION:
         case PROP_LOCAL_TIME:
--- a/bacnet/runtime/server.c	Tue Mar 05 11:43:20 2019 +0300
+++ b/bacnet/runtime/server.c	Mon Mar 11 13:51:07 2019 +0100
@@ -517,7 +517,7 @@
         if (elapsed_seconds) {
             last_seconds = current_seconds;
             dcc_timer_seconds(elapsed_seconds);
-            bvlc_maintenance_timer(elapsed_seconds);
+            //bvlc_maintenance_timer(elapsed_seconds); // already called by dlenv_maintenance_timer() => do _not_ call here!
             dlenv_maintenance_timer(elapsed_seconds);
             elapsed_milliseconds = elapsed_seconds * 1000;
             tsm_timer_milliseconds(elapsed_milliseconds);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/ConnectorBase.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import md5
+
+
+class ConnectorBase(object):
+
+    chuncksize = 1024*1024
+
+    def BlobFromFile(self, filepath, seed):
+        s = md5.new()
+        s.update(seed)
+        blobID = self.SeedBlob(seed)
+        with open(filepath, "rb") as f:
+            while blobID == s.digest():
+                chunk = f.read(self.chuncksize)
+                if len(chunk) == 0:
+                    return blobID
+                blobID = self.AppendChunkToBlob(chunk, blobID)
+                s.update(chunk)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/PYRO/PSK_Adapter.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,94 @@
+from __future__ import absolute_import
+from __future__ import print_function
+
+import socket
+import re
+import sslpsk
+import ssl
+import Pyro
+from Pyro.core import PyroURI
+from Pyro.protocol import _connect_socket, TCPConnection, PYROAdapter
+from Pyro.errors import ConnectionDeniedError, ProtocolError
+from Pyro.util import Log
+
+
+# The TLS-PSK adapter that handles SSL connections instead of regular sockets,
+# but using Pre Shared Keys instead of Certificates
+#
+class PYROPSKAdapter(PYROAdapter):
+    # This is essentialy the same as in Pyro/protocol.py
+    # only raw_sock wrapping into sock through sslpsk.wrap_socket was added
+    # Pyro unfortunately doesn't allow cleaner customization
+    def bindToURI(self, URI):
+        with self.lock:   # only 1 thread at a time can bind the URI
+            try:
+                self.URI = URI
+
+                # This are the statements that differ from Pyro/protocol.py
+                raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                _connect_socket(raw_sock, URI.address, URI.port, self.timeout)
+                sock = sslpsk.wrap_socket(
+                    raw_sock, psk=Pyro.config.PYROPSK, server_side=False,
+                    ciphers="PSK-AES256-CBC-SHA",  # available in openssl 1.0.2
+                    ssl_version=ssl.PROTOCOL_TLSv1)
+                # all the rest is the same as in Pyro/protocol.py
+
+                conn = TCPConnection(sock, sock.getpeername())
+                # receive the authentication challenge string, and use that to build the actual identification string.
+                try:
+                    authChallenge = self.recvAuthChallenge(conn)
+                except ProtocolError, x:
+                    # check if we were denied
+                    if hasattr(x, "partialMsg") and x.partialMsg[:len(self.denyMSG)] == self.denyMSG:
+                        raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(x.partialMsg[-1])])
+                    else:
+                        raise
+                # reply with our ident token, generated from the ident passphrase and the challenge
+                msg = self._sendConnect(sock, self.newConnValidator.createAuthToken(self.ident, authChallenge, conn.addr, self.URI, None))
+                if msg == self.acceptMSG:
+                    self.conn = conn
+                    self.conn.connected = 1
+                    Log.msg('PYROAdapter', 'connected to', str(URI))
+                    if URI.protocol == 'PYROLOCPSK':
+                        self.resolvePYROLOC_URI("PYROPSK")  # updates self.URI
+                elif msg[:len(self.denyMSG)] == self.denyMSG:
+                    try:
+                        raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(msg[-1])])
+                    except (KeyError, ValueError):
+                        raise ConnectionDeniedError('invalid response')
+            except socket.error:
+                Log.msg('PYROAdapter', 'connection failed to URI', str(URI))
+                raise ProtocolError('connection failed')
+
+
+_getProtocolAdapter = Pyro.protocol.getProtocolAdapter
+
+
+def getProtocolAdapter(protocol):
+    if protocol in ('PYROPSK', 'PYROLOCPSK'):
+        return PYROPSKAdapter()
+    return _getProtocolAdapter(protocol)
+
+
+Pyro.protocol.getProtocolAdapter = getProtocolAdapter
+
+
+_processStringURI = Pyro.core.processStringURI
+
+
+def processStringURI(URI):
+    x = re.match(r'(?P<protocol>PYROLOCPSK)://(?P<hostname>[^\s:]+):?(?P<port>\d+)?/(?P<name>\S*)', URI)
+    if x:
+        protocol = x.group('protocol')
+        hostname = x.group('hostname')
+        port = x.group('port')
+        if port:
+            port = int(port)
+        else:
+            port = 0
+        name = x.group('name')
+        return PyroURI(hostname, name, port, protocol)
+    return _processStringURI(URI)
+
+
+Pyro.core.processStringURI = processStringURI
--- a/connectors/PYRO/__init__.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/connectors/PYRO/__init__.py	Mon Mar 11 13:51:07 2019 +0100
@@ -36,9 +36,9 @@
 import Pyro.util
 from Pyro.errors import PyroError
 
+import PSKManagement as PSK
 from runtime import PlcStatus
 
-service_type = '_PYRO._tcp.local.'
 # this module attribute contains a list of DNS-SD (Zeroconf) service types
 # supported by this connector confnode.
 #
@@ -52,66 +52,35 @@
     """
     confnodesroot.logger.write(_("PYRO connecting to URI : %s\n") % uri)
 
-    servicetype, location = uri.split("://")
-    if servicetype == "PYROS":
-        schemename = "PYROLOCSSL"
-        # Protect against name->IP substitution in Pyro3
-        Pyro.config.PYRO_DNS_URI = True
-        # Beware Pyro lib need str path, not unicode
-        # don't rely on PYRO_STORAGE ! see documentation
-        Pyro.config.PYROSSL_CERTDIR = os.path.abspath(str(confnodesroot.ProjectPath) + '/certs')
-        if not os.path.exists(Pyro.config.PYROSSL_CERTDIR):
+    scheme, location = uri.split("://")
+    if scheme == "PYROS":
+        import connectors.PYRO.PSK_Adapter # pylint: disable=wrong-import-order,unused-import,wrong-import-position
+        schemename = "PYROLOCPSK"
+        url, ID = location.split('#')  # TODO fix exception when # not found
+        # load PSK from project
+        secpath = os.path.join(str(confnodesroot.ProjectPath), 'psk', ID+'.secret')
+        if not os.path.exists(secpath):
             confnodesroot.logger.write_error(
-                'Error : the directory %s is missing for SSL certificates (certs_dir).'
-                'Please fix it in your project.\n' % Pyro.config.PYROSSL_CERTDIR)
+                'Error: Pre-Shared-Key Secret in %s is missing!\n' % secpath)
             return None
-        else:
-            confnodesroot.logger.write(_("PYRO using certificates in '%s' \n")
-                                       % (Pyro.config.PYROSSL_CERTDIR))
-        Pyro.config.PYROSSL_CERT = "client.crt"
-        Pyro.config.PYROSSL_KEY = "client.key"
-
-        # Ugly Monkey Patching
-        def _gettimeout(self):
-            return self.timeout
-
-        def _settimeout(self, timeout):
-            self.timeout = timeout
-        from M2Crypto.SSL import Connection  # pylint: disable=import-error
-        Connection.timeout = None
-        Connection.gettimeout = _gettimeout
-        Connection.settimeout = _settimeout
-        # M2Crypto.SSL.Checker.WrongHost: Peer certificate commonName does not
-        # match host, expected 127.0.0.1, got server
-        Connection.clientPostConnectionCheck = None
+        secret = open(secpath).read().partition(':')[2].rstrip('\n\r')
+        Pyro.config.PYROPSK = (secret, ID)
+        # strip ID from URL, so that pyro can understand it.
+        location = url
     else:
         schemename = "PYROLOC"
-    if location.find(service_type) != -1:
-        try:
-            from zeroconf import Zeroconf
-            r = Zeroconf()
-            i = r.get_service_info(service_type, location)
-            if i is None:
-                raise Exception("'%s' not found" % location)
-            ip = str(socket.inet_ntoa(i.address))
-            port = str(i.port)
-            newlocation = ip + ':' + port
-            confnodesroot.logger.write(_("'{a1}' is located at {a2}\n").format(a1=location, a2=newlocation))
-            location = newlocation
-            r.close()
-        except Exception:
-            confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n") % location)
-            confnodesroot.logger.write_error(traceback.format_exc())
-            return None
 
     # Try to get the proxy object
     try:
         RemotePLCObjectProxy = Pyro.core.getAttrProxyForURI(schemename + "://" + location + "/PLCObject")
-    except Exception:
-        confnodesroot.logger.write_error(_("Connection to '%s' failed.\n") % location)
-        confnodesroot.logger.write_error(traceback.format_exc())
+    except Exception, e:
+        confnodesroot.logger.write_error(
+            _("Connection to {loc} failed with exception {ex}\n").format(
+                loc=location, exo=str(e)))
         return None
 
+    RemotePLCObjectProxy.adapter.setTimeout(60)
+
     def PyroCatcher(func, default=None):
         """
         A function that catch a Pyro exceptions, write error to logger
@@ -136,13 +105,16 @@
 
     # Check connection is effective.
     # lambda is for getattr of GetPLCstatus to happen inside catcher
-    if PyroCatcher(RemotePLCObjectProxy.GetPLCstatus)() is None:
-        confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n"))
-        return None
+    IDPSK = PyroCatcher(RemotePLCObjectProxy.GetPLCID)()
+    if IDPSK is None:
+        confnodesroot.logger.write_warning(_("PLC did not provide identity and security infomation.\n"))
+    else:
+        ID, secret = IDPSK
+        PSK.UpdateID(confnodesroot.ProjectPath, ID, secret, uri)
 
     _special_return_funcs = {
         "StartPLC": False,
-        "GetTraceVariables": ("Broken", None),
+        "GetTraceVariables": (PlcStatus.Broken, None),
         "GetPLCstatus": (PlcStatus.Broken, None),
         "RemoteExec": (-1, "RemoteExec script failed!")
     }
@@ -161,4 +133,4 @@
                 self.__dict__[attrName] = member
             return member
 
-    return PyroProxyProxy()
+    return PyroProxyProxy
--- a/connectors/PYRO/dialog.py	Tue Mar 05 11:43:20 2019 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2018: Smarteh
-#
-# See COPYING file for copyrights details.
-
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import wx
-from zope.interface import implementer
-
-from controls.UriLocationEditor import IConnectorPanel
-
-URITypes = ["LOCAL", "PYRO", "PYROS"]
-
-
-def PYRO_connector_dialog(confnodesroot):
-    [ID_IPTEXT, ID_PORTTEXT] = [wx.NewId() for _init_ctrls in range(2)]
-
-    @implementer(IConnectorPanel)
-    class PYROConnectorPanel(wx.Panel):
-        def __init__(self, typeConnector, parrent, *args, **kwargs):
-            self.type = typeConnector
-            self.parrent = parrent
-            wx.Panel.__init__(self, parrent, *args, **kwargs)
-            self._init_ctrls()
-            self._init_sizers()
-            self.uri = None
-
-        def _init_ctrls(self):
-            self.IpText = wx.TextCtrl(parent=self, id=ID_IPTEXT, size=wx.Size(200, -1))
-            self.PortText = wx.TextCtrl(parent=self, id=ID_PORTTEXT, size=wx.Size(200, -1))
-
-        def _init_sizers(self):
-            self.mainSizer = wx.FlexGridSizer(cols=2, hgap=10, rows=5, vgap=10)
-            self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI host:")),
-                                     flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.IpText, flag=wx.GROW)
-
-            self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI port:")),
-                                     flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.PortText, flag=wx.GROW)
-            self.SetSizer(self.mainSizer)
-
-        def SetURI(self, uri):
-            self.uri = uri
-            uri_list = uri.strip().split(":")
-            length = len(uri_list)
-            if length == 3:
-                self.IpText.SetValue(uri_list[1].strip("/"))
-                self.PortText.SetValue(uri_list[2])
-            elif length == 2:
-                self.IpText.SetValue(uri_list[1].strip("/"))
-
-        def GetURI(self):
-            self.uri = self.type+"://"+self.IpText.GetValue()+":"+self.PortText.GetValue()
-            return self.uri
-
-    return PYROConnectorPanel("PYRO", confnodesroot)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/PYRO_dialog.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from itertools import repeat, islice, chain
+import wx
+
+from connectors.SchemeEditor import SchemeEditor
+
+
+model = [('host', _("Host:")),
+         ('port', _("Port:"))]
+
+# (scheme, model, secure)
+models = [("LOCAL", [], False), ("PYRO", model, False), ("PYROS", model, True)]
+
+Schemes = list(zip(*models)[0])
+
+_PerSchemeConf = {sch: (mod, sec) for sch, mod, sec in models}
+
+
+class PYRO_dialog(SchemeEditor):
+    def __init__(self, scheme, *args, **kwargs):
+        # ID selector is enabled only on PYROS (secure)
+        self.model, self.EnableIDSelector = _PerSchemeConf[scheme]
+
+        SchemeEditor.__init__(self, scheme, *args, **kwargs)
+
+    # pylint: disable=unused-variable
+    def SetLoc(self, loc):
+        hostport, ID = list(islice(chain(loc.split("#"), repeat("")), 2))
+        host, port = list(islice(chain(hostport.split(":"), repeat("")), 2))
+        self.SetFields(locals())
+
+    def GetLoc(self):
+        if self.model:
+            fields = self.GetFields()
+            template = "{host}"
+            if fields['port']:
+                template += ":{port}"
+            if fields['ID']:
+                template += "#{ID}"
+
+            return template.format(**fields)
+        return ''
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/SchemeEditor.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from functools import partial
+import wx
+
+from controls.IDBrowser import IDBrowser
+
+
+class SchemeEditor(wx.Panel):
+    def __init__(self, scheme, parent, *args, **kwargs):
+        self.txtctrls = {}
+        wx.Panel.__init__(self, parent, *args, **kwargs)
+
+        self.fieldsizer = wx.FlexGridSizer(cols=2, hgap=10, vgap=10)
+
+        if self.EnableIDSelector:
+            self.model = self.model + [("ID", _("ID:"))]
+
+        for tag, label in self.model:
+            txtctrl = wx.TextCtrl(parent=self, size=wx.Size(200, -1))
+            self.txtctrls[tag] = txtctrl
+            for win, flag in [
+                    (wx.StaticText(self, label=label),
+                    wx.ALIGN_CENTER_VERTICAL),
+                    (txtctrl, wx.GROW)]:
+                self.fieldsizer.AddWindow(win, flag=flag)
+
+        self.fieldsizer.AddSpacer(20)
+
+        if self.EnableIDSelector:
+            self.mainsizer = wx.FlexGridSizer(cols=2, hgap=10, vgap=10)
+            self.mainsizer.AddSizer(self.fieldsizer)
+            self.idselector = IDBrowser(
+                self, parent.ctr,
+                # use a callafter, as editor can be deleted by calling SetURI
+                partial(wx.CallAfter, parent.SetURI),
+                self.txtctrls[tag].SetValue)
+            self.mainsizer.AddWindow(self.idselector)
+            self.SetSizer(self.mainsizer)
+        else:
+            self.SetSizer(self.fieldsizer)
+
+    def SetFields(self, fields):
+        for tag, _label in self.model:
+            self.txtctrls[tag].SetValue(fields[tag])
+
+    def GetFields(self):
+        return {tag: self.txtctrls[tag].GetValue() for tag, _label in self.model}
--- a/connectors/WAMP/__init__.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/connectors/WAMP/__init__.py	Mon Mar 11 13:51:07 2019 +0100
@@ -27,8 +27,9 @@
 from __future__ import print_function
 import sys
 import traceback
+from functools import partial
 from threading import Thread, Event
-from builtins import str as text
+from six import text_type as text
 
 from twisted.internet import reactor, threads
 from autobahn.twisted import wamp
@@ -66,15 +67,15 @@
 }
 
 
-def WAMP_connector_factory(uri, confnodesroot):
+def _WAMP_connector_factory(cls, uri, confnodesroot):
     """
     WAMP://127.0.0.1:12345/path#realm#ID
     WAMPS://127.0.0.1:12345/path#realm#ID
     """
-    servicetype, location = uri.split("://")
+    scheme, location = uri.split("://")
     urlpath, realm, ID = location.split('#')
     urlprefix = {"WAMP":  "ws",
-                 "WAMPS": "wss"}[servicetype]
+                 "WAMPS": "wss"}[scheme]
     url = urlprefix+"://"+urlpath
 
     def RegisterWampClient():
@@ -88,7 +89,7 @@
             extra={"ID": ID})
         session_factory = wamp.ApplicationSessionFactory(
             config=component_config)
-        session_factory.session = WampSession
+        session_factory.session = cls
 
         # create a WAMP-over-WebSocket transport client factory
         transport_factory = WampWebSocketClientFactory(
@@ -153,10 +154,10 @@
                 self.__dict__[attrName] = member
             return member
 
-    # Try to get the proxy object
-    try:
-        return WampPLCObjectProxy()
-    except Exception:
-        confnodesroot.logger.write_error(_("WAMP connection to '%s' failed.\n") % location)
-        confnodesroot.logger.write_error(traceback.format_exc())
-        return None
+    # TODO : GetPLCID()
+    # TODO : PSK.UpdateID()
+
+    return WampPLCObjectProxy
+
+
+WAMP_connector_factory = partial(_WAMP_connector_factory, WampSession)
--- a/connectors/WAMP/dialog.py	Tue Mar 05 11:43:20 2019 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2018: Smarteh
-#
-# See COPYING file for copyrights details.
-
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import wx
-from zope.interface import implementer
-
-from controls.UriLocationEditor import IConnectorPanel
-
-URITypes = ["WAMP", "WAMPS"]
-
-
-def WAMP_connector_dialog(confnodesroot):
-    [ID_IPTEXT, ID_PORTTEXT, ID_REALMTEXT, ID_WAMPIDTEXT, ID_SECURECHECKBOX] = [wx.NewId() for _init_ctrls in range(5)]
-
-    @implementer(IConnectorPanel)
-    class WAMPConnectorPanel(wx.Panel):
-        def __init__(self, typeConnector, parrent, *args, **kwargs):
-            self.type = typeConnector
-            self.parrent = parrent
-            wx.Panel.__init__(self, parrent, *args, **kwargs)
-            self._init_ctrls()
-            self._init_sizers()
-            self.uri = None
-
-        def _init_ctrls(self):
-            self.IpText = wx.TextCtrl(parent=self, id=ID_IPTEXT, size=wx.Size(200, -1))
-            self.PortText = wx.TextCtrl(parent=self, id=ID_PORTTEXT, size=wx.Size(200, -1))
-            self.RealmText = wx.TextCtrl(parent=self, id=ID_REALMTEXT, size=wx.Size(200, -1))
-            self.WAMPIDText = wx.TextCtrl(parent=self, id=ID_WAMPIDTEXT, size=wx.Size(200, -1))
-            self.SecureCheckbox = wx.CheckBox(self, ID_SECURECHECKBOX, _("Is connection secure?"))
-
-        def _init_sizers(self):
-            self.mainSizer = wx.FlexGridSizer(cols=2, hgap=10, rows=5, vgap=10)
-            self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI host:")),
-                                     flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.IpText, flag=wx.GROW)
-
-            self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI port:")),
-                                     flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.PortText, flag=wx.GROW)
-
-            self.mainSizer.AddWindow(wx.StaticText(self, label=_("Realm:")),
-                                     flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.RealmText, flag=wx.GROW)
-
-            self.mainSizer.AddWindow(wx.StaticText(self, label=_("WAMP ID:")),
-                                     flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.WAMPIDText, flag=wx.GROW)
-
-            self.mainSizer.AddWindow(wx.StaticText(self, label=""), flag=wx.ALIGN_CENTER_VERTICAL)
-            self.mainSizer.AddWindow(self.SecureCheckbox, flag=wx.GROW)
-
-            self.SetSizer(self.mainSizer)
-
-        def SetURI(self, uri):
-            self.uri = uri
-            uri_list = uri.strip().split(":")
-            length = len(uri_list)
-
-            if length > 0:
-                if uri_list[0] == URITypes[1]:
-                    self.SecureCheckbox.SetValue(True)
-
-                if length > 2:
-                    self.IpText.SetValue(uri_list[1].strip("/"))
-                    wampSett = uri_list[2].split("#")
-                    length2 = len(wampSett)
-                    if length2 > 0:
-                        self.PortText.SetValue(wampSett[0])
-                        if length2 > 1:
-                            self.RealmText.SetValue(wampSett[1])
-                            if length2 > 2:
-                                self.WAMPIDText.SetValue(wampSett[2])
-
-        def GetURI(self):
-            if self.IpText.Validate():
-                typeForURI = self.type + "S" if self.SecureCheckbox.GetValue() else self.type
-                self.uri = typeForURI + "://" + self.IpText.GetValue() + ":" + self.PortText.GetValue() + "#" + self.RealmText.GetValue() + "#" + self.WAMPIDText.GetValue()
-                return self.uri
-            else:
-                return ""
-
-    return WAMPConnectorPanel("WAMP", confnodesroot)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/WAMP_dialog.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from itertools import repeat, islice, chain
+
+from connectors.SchemeEditor import SchemeEditor
+
+Schemes = ["WAMP", "WAMPS"]
+
+model = [('host', _("Host:")),
+         ('port', _("Port:")),
+         ('realm', _("Realm:"))]
+
+
+class WAMP_dialog(SchemeEditor):
+    def __init__(self, *args, **kwargs):
+        self.model = model
+        self.EnableIDSelector = True
+        SchemeEditor.__init__(self, *args, **kwargs)
+
+    # pylint: disable=unused-variable
+    def SetLoc(self, loc):
+        hostport, realm, ID = list(islice(chain(loc.split("#"), repeat("")), 3))
+        host, port = list(islice(chain(hostport.split(":"), repeat("")), 2))
+        self.SetFields(locals())
+
+    def GetLoc(self):
+        fields = self.GetFields()
+
+        # TODO : input validation test
+
+        template = "{host}" + \
+                   (":{port}" if fields['port'] else '') +\
+                   "#{realm}#{ID}"
+
+        return template.format(**fields)
--- a/connectors/__init__.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/connectors/__init__.py	Mon Mar 11 13:51:07 2019 +0100
@@ -29,33 +29,37 @@
 from __future__ import absolute_import
 from os import listdir, path
 import util.paths as paths
+from connectors.ConnectorBase import ConnectorBase
+from types import ClassType
 
-_base_path = paths.AbsDir(__file__)
+connectors_packages = ["PYRO", "WAMP"]
 
 
 def _GetLocalConnectorClassFactory(name):
     return lambda: getattr(__import__(name, globals(), locals()), name + "_connector_factory")
 
 
-def _GetLocalConnectorClassDialog(name):
-    return lambda: getattr(__import__(name + '.dialog', globals(), locals(), fromlist=['dialog']), name + "_connector_dialog")
+connectors = {name: _GetLocalConnectorClassFactory(name)
+              for name in connectors_packages}
+
+_dialogs_imported = False
+per_URI_connectors = None
+schemes = None
 
 
-def _GetLocalConnectorURITypes(name):
-    return lambda: getattr(__import__(name + '.dialog', globals(), locals(), fromlist=['dialog']), "URITypes", None)
+# lazy import of connectors dialogs, only if used
+def _Import_Dialogs():
+    global per_URI_connectors, schemes, _dialogs_imported
+    if not _dialogs_imported:
+        _dialogs_imported = True
+        per_URI_connectors = {}
+        schemes = []
+        for con_name in connectors_packages:
+            module = __import__(con_name + '_dialog', globals(), locals())
 
-
-connectors = {name:
-              _GetLocalConnectorClassFactory(name)
-              for name in listdir(_base_path)
-              if (path.isdir(path.join(_base_path, name)) and
-                  not name.startswith("__"))}
-
-connectors_dialog = {name:
-                     {"function": _GetLocalConnectorClassDialog(name), "URITypes": _GetLocalConnectorURITypes(name)}
-                     for name in listdir(_base_path)
-                     if (path.isdir(path.join(_base_path, name)) and
-                         not name.startswith("__"))}
+            for scheme in module.Schemes:
+                per_URI_connectors[scheme] = getattr(module, con_name + '_dialog')
+                schemes += [scheme]
 
 
 def ConnectorFactory(uri, confnodesroot):
@@ -63,41 +67,67 @@
     Return a connector corresponding to the URI
     or None if cannot connect to URI
     """
-    servicetype = uri.split("://")[0].upper()
-    if servicetype == "LOCAL":
+    _scheme = uri.split("://")[0].upper()
+
+    # commented code to enable for MDNS:// support
+    # _scheme, location = uri.split("://")
+    # _scheme = _scheme.upper()
+
+    if _scheme == "LOCAL":
         # Local is special case
         # pyro connection to local runtime
         # started on demand, listening on random port
-        servicetype = "PYRO"
+        scheme = "PYRO"
         runtime_port = confnodesroot.AppFrame.StartLocalRuntime(
             taskbaricon=True)
         uri = "PYROLOC://127.0.0.1:" + str(runtime_port)
-    elif servicetype in connectors:
-        pass
-    elif servicetype[-1] == 'S' and servicetype[:-1] in connectors:
-        servicetype = servicetype[:-1]
+
+    # commented code to enable for MDNS:// support
+    # elif _scheme == "MDNS":
+    #     try:
+    #         from zeroconf import Zeroconf
+    #         r = Zeroconf()
+    #         i = r.get_service_info(zeroconf_service_type, location)
+    #         if i is None:
+    #             raise Exception("'%s' not found" % location)
+    #         ip = str(socket.inet_ntoa(i.address))
+    #         port = str(i.port)
+    #         newlocation = ip + ':' + port
+    #         confnodesroot.logger.write(_("'{a1}' is located at {a2}\n").format(a1=location, a2=newlocation))
+    #         location = newlocation
+    #         # not a bug, but a workaround against obvious downgrade attack
+    #         scheme = "PYROS"
+    #         r.close()
+    #     except Exception:
+    #         confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n") % location)
+    #         confnodesroot.logger.write_error(traceback.format_exc())
+    #         return None
+
+    elif _scheme in connectors:
+        scheme = _scheme
+    elif _scheme[-1] == 'S' and _scheme[:-1] in connectors:
+        scheme = _scheme[:-1]
     else:
         return None
 
-    # import module according to uri type
-    connectorclass = connectors[servicetype]()
-    return connectorclass(uri, confnodesroot)
+    # import module according to uri type and get connector specific baseclass
+    # first call to import the module,
+    # then call with parameters to create the class
+    connector_specific_class = connectors[scheme]()(uri, confnodesroot)
+
+    if connector_specific_class is None:
+        return None
+
+    # new class inheriting from generic and specific connector base classes
+    return ClassType(_scheme + "_connector",
+                     (ConnectorBase, connector_specific_class), {})()
 
 
-def ConnectorDialog(conn_type, confnodesroot):
-    if conn_type not in connectors_dialog:
-        return None
-
-    connectorclass = connectors_dialog[conn_type]["function"]()
-    return connectorclass(confnodesroot)
+def EditorClassFromScheme(scheme):
+    _Import_Dialogs()
+    return per_URI_connectors.get(scheme, None)
 
 
-def GetConnectorFromURI(uri):
-    typeOfConnector = None
-    for conn_type in connectors_dialog:
-        connectorTypes = connectors_dialog[conn_type]["URITypes"]()
-        if connectorTypes and uri in connectorTypes:
-            typeOfConnector = conn_type
-            break
-
-    return typeOfConnector
+def ConnectorSchemes():
+    _Import_Dialogs()
+    return schemes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controls/DiscoveryPanel.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,261 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz, a Integrated Development Environment for
+# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
+#
+# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
+# Copyright (C) 2017: Andrey Skvortsov <andrej.skvortzov@gmail.com>
+#
+# See COPYING file for copyrights details.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+from __future__ import absolute_import
+import socket
+from six.moves import xrange
+import wx
+import wx.lib.mixins.listctrl as listmix
+from zeroconf import ServiceBrowser, Zeroconf, get_all_addresses
+
+service_type = '_Beremiz._tcp.local.'
+
+
+class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
+    def __init__(self, parent, id, name, pos=wx.DefaultPosition,
+                 size=wx.DefaultSize, style=0):
+        wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name)
+        listmix.ListCtrlAutoWidthMixin.__init__(self)
+
+
+class DiscoveryPanel(wx.Panel, listmix.ColumnSorterMixin):
+
+    def _init_coll_MainSizer_Items(self, parent):
+        parent.AddWindow(self.staticText1,    0, border=20, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW)
+        parent.AddWindow(self.ServicesList,   0, border=20, flag=wx.LEFT | wx.RIGHT | wx.GROW)
+        parent.AddSizer(self.ButtonGridSizer, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW)
+
+    def _init_coll_MainSizer_Growables(self, parent):
+        parent.AddGrowableCol(0)
+        parent.AddGrowableRow(1)
+
+    def _init_coll_ButtonGridSizer_Items(self, parent):
+        parent.AddWindow(self.RefreshButton, 0, border=0, flag=0)
+        # parent.AddWindow(self.ByIPCheck, 0, border=0, flag=0)
+
+    def _init_coll_ButtonGridSizer_Growables(self, parent):
+        parent.AddGrowableCol(0)
+        parent.AddGrowableRow(0)
+
+    def _init_sizers(self):
+        self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=10)
+        self.ButtonGridSizer = wx.FlexGridSizer(cols=2, hgap=5, rows=1, vgap=0)
+
+        self._init_coll_MainSizer_Items(self.MainSizer)
+        self._init_coll_MainSizer_Growables(self.MainSizer)
+        self._init_coll_ButtonGridSizer_Items(self.ButtonGridSizer)
+        self._init_coll_ButtonGridSizer_Growables(self.ButtonGridSizer)
+
+        self.SetSizer(self.MainSizer)
+
+    def _init_list_ctrl(self):
+        # Set up list control
+        listID = wx.NewId()
+        self.ServicesList = AutoWidthListCtrl(
+            id=listID,
+            name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0),
+            style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)
+        self.ServicesList.InsertColumn(0, _('NAME'))
+        self.ServicesList.InsertColumn(1, _('TYPE'))
+        self.ServicesList.InsertColumn(2, _('IP'))
+        self.ServicesList.InsertColumn(3, _('PORT'))
+        self.ServicesList.SetColumnWidth(0, 150)
+        self.ServicesList.SetColumnWidth(1, 150)
+        self.ServicesList.SetColumnWidth(2, 150)
+        self.ServicesList.SetColumnWidth(3, 150)
+        self.ServicesList.SetInitialSize(wx.Size(-1, 300))
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, id=listID)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, id=listID)
+
+    def _init_ctrls(self, prnt):
+        self.staticText1 = wx.StaticText(
+            label=_('Services available:'), name='staticText1', parent=self,
+            pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
+
+        self.RefreshButton = wx.Button(
+            label=_('Refresh'), name='RefreshButton', parent=self,
+            pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
+        self.RefreshButton.Bind(wx.EVT_BUTTON, self.OnRefreshButton)
+
+        # self.ByIPCheck = wx.CheckBox(self, label=_("Use IP instead of Service Name"))
+        # self.ByIPCheck.SetValue(True)
+
+        self._init_sizers()
+        self.Fit()
+
+    def __init__(self, parent):
+        wx.Panel.__init__(self, parent)
+
+        self.parent = parent
+
+        self._init_list_ctrl()
+        listmix.ColumnSorterMixin.__init__(self, 4)
+
+        self._init_ctrls(parent)
+
+        self.itemDataMap = {}
+        self.nextItemId = 0
+
+        self.URI = None
+        self.Browser = None
+        self.ZeroConfInstance = None
+
+        self.RefreshList()
+        self.LatestSelection = None
+
+        self.IfacesMonitorState = None
+        self.IfacesMonitorTimer = wx.Timer(self)
+        self.IfacesMonitorTimer.Start(2000)
+        self.Bind(wx.EVT_TIMER, self.IfacesMonitor, self.IfacesMonitorTimer)
+
+    def __del__(self):
+        self.IfacesMonitorTimer.Stop()
+        self.Browser.cancel()
+        self.ZeroConfInstance.close()
+
+    def IfacesMonitor(self, event):
+        NewState = get_all_addresses(socket.AF_INET)
+
+        if self.IfacesMonitorState != NewState:
+            if self.IfacesMonitorState is not None:
+                # refresh only if a new address appeared
+                for addr in NewState:
+                    if addr not in self.IfacesMonitorState:
+                        self.RefreshList()
+                        break
+            self.IfacesMonitorState = NewState
+        event.Skip()
+
+    def RefreshList(self):
+        self.ServicesList.DeleteAllItems()
+        if self.Browser is not None:
+            self.Browser.cancel()
+        if self.ZeroConfInstance is not None:
+            self.ZeroConfInstance.close()
+        self.ZeroConfInstance = Zeroconf()
+        self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
+
+    def OnRefreshButton(self, event):
+        self.RefreshList()
+
+    # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
+    def GetListCtrl(self):
+        return self.ServicesList
+
+    def getColumnText(self, index, col):
+        item = self.ServicesList.GetItem(index, col)
+        return item.GetText()
+
+    def OnItemSelected(self, event):
+        self.SetURI(event.m_itemIndex)
+        event.Skip()
+
+    def OnItemActivated(self, event):
+        self.SetURI(event.m_itemIndex)
+        self.parent.EndModal(wx.ID_OK)
+        event.Skip()
+
+#    def SetURI(self, idx):
+#        connect_type = self.getColumnText(idx, 1)
+#        connect_address = self.getColumnText(idx, 2)
+#        connect_port = self.getColumnText(idx, 3)
+#
+#        self.URI = "%s://%s:%s"%(connect_type, connect_address, connect_port)
+
+    def SetURI(self, idx):
+        self.LatestSelection = idx
+
+    def GetURI(self):
+        if self.LatestSelection is not None:
+            # if self.ByIPCheck.IsChecked():
+            svcname, scheme, host, port = \
+                map(lambda col: self.getColumnText(self.LatestSelection, col),
+                    range(4))
+            return ("%s://%s:%s#%s" % (scheme, host, port, svcname)) \
+                if scheme[-1] == "S" \
+                else ("%s://%s:%s" % (scheme, host, port))
+            # else:
+            #     svcname = self.getColumnText(self.LatestSelection, 0)
+            #     connect_type = self.getColumnText(self.LatestSelection, 1)
+            #     return str("MDNS://%s" % svcname)
+        return None
+
+    def remove_service(self, zeroconf, _type, name):
+        wx.CallAfter(self._removeService, name)
+
+    def _removeService(self, name):
+        '''
+        called when a service with the desired type goes offline.
+        '''
+
+        # loop through the list items looking for the service that went offline
+        for idx in xrange(self.ServicesList.GetItemCount()):
+            # this is the unique identifier assigned to the item
+            item_id = self.ServicesList.GetItemData(idx)
+
+            # this is the full typename that was received by addService
+            item_name = self.itemDataMap[item_id][4]
+
+            if item_name == name:
+                self.ServicesList.DeleteItem(idx)
+                break
+
+    def add_service(self, zeroconf, _type, name):
+        wx.CallAfter(self._addService, _type, name)
+
+    def _addService(self, _type, name):
+        '''
+        called when a service with the desired type is discovered.
+        '''
+        info = self.ZeroConfInstance.get_service_info(_type, name)
+        if info is None:
+            return
+        svcname = name.split(".")[0]
+        typename = info.properties.get("protocol", None)
+        ip = str(socket.inet_ntoa(info.address))
+        port = info.port
+
+        num_items = self.ServicesList.GetItemCount()
+
+        # display the new data in the list
+        new_item = self.ServicesList.InsertStringItem(num_items, svcname)
+        self.ServicesList.SetStringItem(new_item, 1, "%s" % typename)
+        self.ServicesList.SetStringItem(new_item, 2, "%s" % ip)
+        self.ServicesList.SetStringItem(new_item, 3, "%s" % port)
+
+        # record the new data for the ColumnSorterMixin
+        # we assign every list item a unique id (that won't change when items
+        # are added or removed)
+        self.ServicesList.SetItemData(new_item, self.nextItemId)
+
+        # the value of each column has to be stored in the itemDataMap
+        # so that ColumnSorterMixin knows how to sort the column.
+
+        # "name" is included at the end so that self.removeService
+        # can access it.
+        self.itemDataMap[self.nextItemId] = [svcname, typename, ip, port, name]
+
+        self.nextItemId += 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controls/IDBrowser.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,221 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import wx
+import wx.dataview as dv
+import PSKManagement as PSK
+from PSKManagement import *
+from dialogs.IDMergeDialog import IDMergeDialog
+
+
+class IDBrowserModel(dv.PyDataViewIndexListModel):
+    def __init__(self, project_path, columncount):
+        self.project_path = project_path
+        self.columncount = columncount
+        self.data = PSK.GetData(project_path)
+        dv.PyDataViewIndexListModel.__init__(self, len(self.data))
+
+    def _saveData(self):
+        PSK.SaveData(self.project_path, self.data)
+
+    def GetColumnType(self, col):
+        return "string"
+
+    def GetValueByRow(self, row, col):
+        return self.data[row][col]
+
+    def SetValueByRow(self, value, row, col):
+        self.data[row][col] = value
+        self._saveData()
+
+    def GetColumnCount(self):
+        return len(self.data[0]) if self.data else self.columncount
+
+    def GetCount(self):
+        return len(self.data)
+
+    def Compare(self, item1, item2, col, ascending):
+        if not ascending:  # swap sort order?
+            item2, item1 = item1, item2
+        row1 = self.GetRow(item1)
+        row2 = self.GetRow(item2)
+        if col == 0:
+            return cmp(int(self.data[row1][col]), int(self.data[row2][col]))
+        else:
+            return cmp(self.data[row1][col], self.data[row2][col])
+
+    def DeleteRows(self, rows):
+        rows = list(rows)
+        rows.sort(reverse=True)
+
+        for row in rows:
+            PSK.DeleteID(self.project_path, self.data[row][COL_ID])
+            del self.data[row]
+            self.RowDeleted(row)
+        self._saveData()
+
+    def AddRow(self, value):
+        self.data.append(value)
+        self.RowAppended()
+        self._saveData()
+
+    def Import(self, filepath, sircb):
+        data = PSK.ImportIDs(self.project_path, filepath, sircb)
+        if data is not None:
+            self.data = data
+            self.Reset(len(self.data))
+
+    def Export(self, filepath):
+        PSK.ExportIDs(self.project_path, filepath)
+
+
+colflags = dv.DATAVIEW_COL_RESIZABLE | dv.DATAVIEW_COL_SORTABLE
+
+
+class IDBrowser(wx.Panel):
+    def __init__(self, parent, ctr, SelectURICallBack=None, SelectIDCallBack=None, **kwargs):
+        big = self.isManager = SelectURICallBack is None and SelectIDCallBack is None
+        wx.Panel.__init__(self, parent, -1, size=(800 if big else 450,
+                                                  600 if big else 200))
+
+        self.SelectURICallBack = SelectURICallBack
+        self.SelectIDCallBack = SelectIDCallBack
+
+        dvStyle = wx.BORDER_THEME | dv.DV_ROW_LINES
+        if self.isManager:
+            # no multiple selection in selector mode
+            dvStyle |= dv.DV_MULTIPLE
+        self.dvc = dv.DataViewCtrl(self, style=dvStyle)
+
+        def args(*a, **k):
+            return (a, k)
+
+        ColumnsDesc = [
+            args(_("ID"), COL_ID, width=70),
+            args(_("Last URI"), COL_URI, width=300 if big else 80),
+            args(_("Description"), COL_DESC, width=300 if big else 200,
+                 mode=dv.DATAVIEW_CELL_EDITABLE
+                      if self.isManager
+                      else dv.DATAVIEW_CELL_INERT),
+            args(_("Last connection"), COL_LAST, width=120),
+        ]
+
+        self.model = IDBrowserModel(ctr.ProjectPath, len(ColumnsDesc))
+        self.dvc.AssociateModel(self.model)
+
+        col_list = []
+        for a, k in ColumnsDesc:
+            col_list.append(
+                self.dvc.AppendTextColumn(*a, **dict(k, flags=colflags)))
+        col_list[COL_LAST].SetSortOrder(False)
+
+        # TODO : sort by last bvisit by default
+
+        self.Sizer = wx.BoxSizer(wx.VERTICAL)
+        self.Sizer.Add(self.dvc, 1, wx.EXPAND)
+
+        btnbox = wx.BoxSizer(wx.HORIZONTAL)
+        if self.isManager:
+
+            # deletion of secret and metadata
+            deleteButton = wx.Button(self, label=_("Delete ID"))
+            self.Bind(wx.EVT_BUTTON, self.OnDeleteButton, deleteButton)
+            btnbox.Add(deleteButton, 0, wx.LEFT | wx.RIGHT, 5)
+
+            # export all
+            exportButton = wx.Button(self, label=_("Export all"))
+            self.Bind(wx.EVT_BUTTON, self.OnExportButton, exportButton)
+            btnbox.Add(exportButton, 0, wx.LEFT | wx.RIGHT, 5)
+
+            # import with a merge -> duplicates are asked for
+            importButton = wx.Button(self, label=_("Import"))
+            self.Bind(wx.EVT_BUTTON, self.OnImportButton, importButton)
+            btnbox.Add(importButton, 0, wx.LEFT | wx.RIGHT, 5)
+
+        else:
+            # selector mode
+            self.useURIButton = wx.Button(self, label=_("Use last URI"))
+            self.Bind(wx.EVT_BUTTON, self.OnUseURIButton, self.useURIButton)
+            self.useURIButton.Disable()
+            btnbox.Add(self.useURIButton, 0, wx.LEFT | wx.RIGHT, 5)
+
+        self.Sizer.Add(btnbox, 0, wx.TOP | wx.BOTTOM, 5)
+        self.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.OnSelectionChanged, self.dvc)
+
+    def OnDeleteButton(self, evt):
+        items = self.dvc.GetSelections()
+        rows = [self.model.GetRow(item) for item in items]
+
+        # Ask if user really wants to delete
+        if wx.MessageBox(_('Are you sure to delete selected IDs?'),
+                         _('Delete IDs'),
+                         wx.YES_NO | wx.CENTRE | wx.NO_DEFAULT) != wx.YES:
+            return
+
+        self.model.DeleteRows(rows)
+
+    def OnSelectionChanged(self, evt):
+        if not self.isManager:
+            items = self.dvc.GetSelections()
+            somethingSelected = len(items) > 0
+            self.useURIButton.Enable(somethingSelected)
+            if somethingSelected:
+                row = self.model.GetRow(items[0])
+                ID = self.model.GetValueByRow(row, COL_ID)
+                self.SelectIDCallBack(ID)
+
+    def OnUseURIButton(self, evt):
+        row = self.model.GetRow(self.dvc.GetSelections()[0])
+        URI = self.model.GetValueByRow(row, COL_URI)
+        if URI:
+            self.SelectURICallBack(URI)
+
+    def OnExportButton(self, evt):
+        dialog = wx.FileDialog(self, _("Choose a file"),
+                               wildcard=_("PSK ZIP files (*.zip)|*.zip"),
+                               style=wx.SAVE | wx.OVERWRITE_PROMPT)
+        if dialog.ShowModal() == wx.ID_OK:
+            self.model.Export(dialog.GetPath())
+
+    # pylint: disable=unused-variable
+    def ShouldIReplaceCallback(self, existing, replacement):
+        ID, URI, DESC, LAST = existing
+        _ID, _URI, _DESC, _LAST = replacement
+        dlg = IDMergeDialog(
+            self,
+            _("Import IDs"),
+            (_("Replace information for ID {ID} ?") + "\n\n" +
+             _("Existing:") + "\n    " +
+             _("Description:") + " {DESC}\n    " +
+             _("Last known URI:") + " {URI}\n    " +
+             _("Last connection:") + " {LAST}\n\n" +
+             _("Replacement:") + "\n    " +
+             _("Description:") + " {_DESC}\n    " +
+             _("Last known URI:") + " {_URI}\n    " +
+             _("Last connection:") + " {_LAST}\n").format(**locals()),
+            _("Do the same for following IDs"),
+            [_("Replace"), _("Keep"), _("Cancel")])
+
+        answer = dlg.ShowModal()  # return value ignored as we have "Ok" only anyhow
+        if answer == wx.ID_CANCEL:
+            return CANCEL
+
+        if dlg.OptionChecked():
+            if answer == wx.ID_YES:
+                return REPLACE_ALL
+            return KEEP_ALL
+        else:
+            if answer == wx.ID_YES:
+                return REPLACE
+            return KEEP
+
+    def OnImportButton(self, evt):
+        dialog = wx.FileDialog(self, _("Choose a file"),
+                               wildcard=_("PSK ZIP files (*.zip)|*.zip"),
+                               style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
+        if dialog.ShowModal() == wx.ID_OK:
+            self.model.Import(dialog.GetPath(),
+                              self.ShouldIReplaceCallback)
--- a/controls/UriLocationEditor.py	Tue Mar 05 11:43:20 2019 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-from __future__ import absolute_import
-
-import wx
-from zope.interface import Interface, Attribute
-from zope.interface.verify import verifyObject
-from connectors import connectors_dialog, ConnectorDialog, GetConnectorFromURI
-
-
-[ID_URIWIZARDDIALOG, ID_URITYPECHOICE] = [wx.NewId() for _init_ctrls in range(2)]
-
-
-class IConnectorPanel(Interface):
-    """This is interface for panel of seperate connector type"""
-    uri = Attribute("""uri of connections""")
-    type = Attribute("""type of connector""")
-
-    def SetURI(uri):     # pylint: disable=no-self-argument
-        """methode for set uri"""
-
-    def GetURI():        # pylint: disable=no-self-argument
-        """metohde for get uri"""
-
-
-class UriLocationEditor(wx.Dialog):
-    def _init_ctrls(self, parent):
-        self.UriTypeChoice = wx.Choice(parent=self, id=ID_URIWIZARDDIALOG, choices=self.URITYPES)
-        self.UriTypeChoice.SetSelection(0)
-        self.Bind(wx.EVT_CHOICE, self.OnTypeChoice, self.UriTypeChoice)
-        self.PanelSizer = wx.BoxSizer(wx.HORIZONTAL)
-        self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
-
-    def _init_sizers(self):
-        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
-        typeSizer = wx.BoxSizer(wx.HORIZONTAL)
-        typeSizer.Add(wx.StaticText(self, wx.ID_ANY, _("URI type:")), border=5, flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL)
-        typeSizer.Add(self.UriTypeChoice, border=5, flag=wx.ALL)
-        self.mainSizer.Add(typeSizer)
-
-        self.mainSizer.Add(self.PanelSizer, border=5, flag=wx.ALL)
-        self.mainSizer.Add(self.ButtonSizer, border=5, flag=wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
-        self.SetSizer(self.mainSizer)
-        self.Layout()
-        self.Fit()
-
-    def __init__(self, parent, uri):
-        wx.Dialog.__init__(self, id=ID_URIWIZARDDIALOG,
-                           name='UriLocationEditor', parent=parent,
-                           title='Uri location')
-        self.URITYPES = [_("- Select URI type -")]
-        for connector_type, connector_function in connectors_dialog.iteritems():
-            try:
-                connector_function['function']()
-                self.URITYPES.append(connector_type)
-            except Exception:
-                pass
-
-        self.selected = None
-        self.parrent = parent
-        self.logger = self.parrent.CTR.logger
-        self._init_ctrls(parent)
-        self._init_sizers()
-        self.SetURI(uri)
-        self.CenterOnParent()
-
-    def OnTypeChoice(self, event):
-        self._removePanelType()
-        index = event.GetSelection()
-        if index > 0:
-            self.selected = event.GetString()
-            self.panelType = self._getConnectorDialog(self.selected)
-            if self.panelType:
-                self.PanelSizer.Add(self.panelType)
-                self.mainSizer.Layout()
-                self.Fit()
-                self.panelType.Refresh()
-
-    def SetURI(self, uri):
-        self._removePanelType()
-        uri_list = uri.strip().split(":")
-        if uri_list:
-            uri_type = uri_list[0].upper()
-            type = GetConnectorFromURI(uri_type)
-            if type:
-                self.selected = type
-                self.UriTypeChoice.SetStringSelection(self.selected)
-                self.panelType = self._getConnectorDialog(self.selected)
-                if self.panelType:
-                    self.panelType.SetURI(uri)
-                    self.PanelSizer.Add(self.panelType)
-                    self.PanelSizer.Layout()
-                    self.mainSizer.Layout()
-                    self.Fit()
-                    self.panelType.Refresh()
-
-    def GetURI(self):
-        if not self.selected or not self.panelType:
-            return ""
-        else:
-            return self.panelType.GetURI()
-
-    def _removePanelType(self):
-        for i in range(self.PanelSizer.GetItemCount()):
-            item = self.PanelSizer.GetItem(i)
-            item.DeleteWindows()
-            self.PanelSizer.Remove(i)
-            self.Fit()
-        self.PanelSizer.Layout()
-
-    def _getConnectorDialog(self, connectorType):
-        connector = ConnectorDialog(connectorType, self)
-        if connector and IConnectorPanel.providedBy(connector):
-            if verifyObject(IConnectorPanel, connector):
-                return connector
-        else:
-            return None
--- a/dialogs/DiscoveryDialog.py	Tue Mar 05 11:43:20 2019 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,268 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# This file is part of Beremiz, a Integrated Development Environment for
-# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
-#
-# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
-# Copyright (C) 2017: Andrey Skvortsov <andrej.skvortzov@gmail.com>
-#
-# See COPYING file for copyrights details.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-
-
-from __future__ import absolute_import
-import socket
-from six.moves import xrange
-import wx
-import wx.lib.mixins.listctrl as listmix
-from zeroconf import ServiceBrowser, Zeroconf
-
-
-service_type = '_PYRO._tcp.local.'
-
-
-class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
-    def __init__(self, parent, id, name, pos=wx.DefaultPosition,
-                 size=wx.DefaultSize, style=0):
-        wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name)
-        listmix.ListCtrlAutoWidthMixin.__init__(self)
-
-
-[
-    ID_DISCOVERYDIALOG, ID_DISCOVERYDIALOGSTATICTEXT1,
-    ID_DISCOVERYDIALOGSERVICESLIST, ID_DISCOVERYDIALOGREFRESHBUTTON,
-    ID_DISCOVERYDIALOGLOCALBUTTON, ID_DISCOVERYDIALOGIPBUTTON,
-] = [wx.NewId() for _init_ctrls in range(6)]
-
-
-class DiscoveryDialog(wx.Dialog, listmix.ColumnSorterMixin):
-
-    def _init_coll_MainSizer_Items(self, parent):
-        parent.AddWindow(self.staticText1,    0, border=20, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW)
-        parent.AddWindow(self.ServicesList,   0, border=20, flag=wx.LEFT | wx.RIGHT | wx.GROW)
-        parent.AddSizer(self.ButtonGridSizer, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW)
-
-    def _init_coll_MainSizer_Growables(self, parent):
-        parent.AddGrowableCol(0)
-        parent.AddGrowableRow(1)
-
-    def _init_coll_ButtonGridSizer_Items(self, parent):
-        parent.AddWindow(self.RefreshButton, 0, border=0, flag=0)
-        parent.AddWindow(self.LocalButton, 0, border=0, flag=0)
-        parent.AddWindow(self.IpButton, 0, border=0, flag=0)
-        parent.AddSizer(self.ButtonSizer, 0, border=0, flag=0)
-
-    def _init_coll_ButtonGridSizer_Growables(self, parent):
-        parent.AddGrowableCol(0)
-        parent.AddGrowableCol(1)
-        parent.AddGrowableRow(0)
-
-    def _init_sizers(self):
-        self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=10)
-        self.ButtonGridSizer = wx.FlexGridSizer(cols=4, hgap=5, rows=1, vgap=0)
-
-        self._init_coll_MainSizer_Items(self.MainSizer)
-        self._init_coll_MainSizer_Growables(self.MainSizer)
-        self._init_coll_ButtonGridSizer_Items(self.ButtonGridSizer)
-        self._init_coll_ButtonGridSizer_Growables(self.ButtonGridSizer)
-
-        self.SetSizer(self.MainSizer)
-
-    def _init_list_ctrl(self):
-        # Set up list control
-        self.ServicesList = AutoWidthListCtrl(
-            id=ID_DISCOVERYDIALOGSERVICESLIST,
-            name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0),
-            style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)
-        self.ServicesList.InsertColumn(0, _('NAME'))
-        self.ServicesList.InsertColumn(1, _('TYPE'))
-        self.ServicesList.InsertColumn(2, _('IP'))
-        self.ServicesList.InsertColumn(3, _('PORT'))
-        self.ServicesList.SetColumnWidth(0, 150)
-        self.ServicesList.SetColumnWidth(1, 150)
-        self.ServicesList.SetColumnWidth(2, 150)
-        self.ServicesList.SetColumnWidth(3, 150)
-        self.ServicesList.SetInitialSize(wx.Size(-1, 300))
-        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, id=ID_DISCOVERYDIALOGSERVICESLIST)
-        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, id=ID_DISCOVERYDIALOGSERVICESLIST)
-
-    def _init_ctrls(self, prnt):
-        self.staticText1 = wx.StaticText(
-            id=ID_DISCOVERYDIALOGSTATICTEXT1,
-            label=_('Services available:'), name='staticText1', parent=self,
-            pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
-
-        self.RefreshButton = wx.Button(
-            id=ID_DISCOVERYDIALOGREFRESHBUTTON,
-            label=_('Refresh'), name='RefreshButton', parent=self,
-            pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
-        self.Bind(wx.EVT_BUTTON, self.OnRefreshButton, id=ID_DISCOVERYDIALOGREFRESHBUTTON)
-
-        self.LocalButton = wx.Button(
-            id=ID_DISCOVERYDIALOGLOCALBUTTON,
-            label=_('Local'), name='LocalButton', parent=self,
-            pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
-        self.Bind(wx.EVT_BUTTON, self.OnLocalButton, id=ID_DISCOVERYDIALOGLOCALBUTTON)
-
-        self.IpButton = wx.Button(
-            id=ID_DISCOVERYDIALOGIPBUTTON,
-            label=_('Add IP'), name='IpButton', parent=self,
-            pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
-        self.Bind(wx.EVT_BUTTON, self.OnIpButton, id=ID_DISCOVERYDIALOGIPBUTTON)
-
-        self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL | wx.CENTER)
-
-        self._init_sizers()
-        self.Fit()
-
-    def __init__(self, parent):
-        wx.Dialog.__init__(
-            self, id=ID_DISCOVERYDIALOG,
-            name='DiscoveryDialog', parent=parent,
-            style=wx.DEFAULT_DIALOG_STYLE,
-            title=_('Service Discovery'))
-
-        self._init_list_ctrl()
-        listmix.ColumnSorterMixin.__init__(self, 4)
-
-        self._init_ctrls(parent)
-
-        self.itemDataMap = {}
-        self.nextItemId = 0
-
-        self.URI = None
-        self.Browser = None
-
-        self.ZeroConfInstance = Zeroconf()
-        self.RefreshList()
-        self.LatestSelection = None
-
-    def __del__(self):
-        if self.Browser is not None:
-            self.Browser.cancel()
-        self.ZeroConfInstance.close()
-
-    def RefreshList(self):
-        if self.Browser is not None:
-            self.Browser.cancel()
-        self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
-
-    def OnRefreshButton(self, event):
-        self.ServicesList.DeleteAllItems()
-        self.RefreshList()
-
-    def OnLocalButton(self, event):
-        self.URI = "LOCAL://"
-        self.EndModal(wx.ID_OK)
-        event.Skip()
-
-    def OnIpButton(self, event):
-        def GetColText(col):
-            return self.getColumnText(self.LatestSelection, col)
-
-        if self.LatestSelection is not None:
-            self.URI = "%s://%s:%s" % tuple(map(GetColText, (1, 2, 3)))
-            self.EndModal(wx.ID_OK)
-        event.Skip()
-
-    # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
-    def GetListCtrl(self):
-        return self.ServicesList
-
-    def getColumnText(self, index, col):
-        item = self.ServicesList.GetItem(index, col)
-        return item.GetText()
-
-    def OnItemSelected(self, event):
-        self.SetURI(event.m_itemIndex)
-        event.Skip()
-
-    def OnItemActivated(self, event):
-        self.SetURI(event.m_itemIndex)
-        self.EndModal(wx.ID_OK)
-        event.Skip()
-
-#    def SetURI(self, idx):
-#        connect_type = self.getColumnText(idx, 1)
-#        connect_address = self.getColumnText(idx, 2)
-#        connect_port = self.getColumnText(idx, 3)
-#
-#        self.URI = "%s://%s:%s"%(connect_type, connect_address, connect_port)
-
-    def SetURI(self, idx):
-        self.LatestSelection = idx
-        svcname = self.getColumnText(idx, 0)
-        connect_type = self.getColumnText(idx, 1)
-        self.URI = "%s://%s" % (connect_type, svcname + '.' + service_type)
-
-    def GetURI(self):
-        return self.URI
-
-    def remove_service(self, zeroconf, _type, name):
-        wx.CallAfter(self._removeService, name)
-
-    def _removeService(self, name):
-        '''
-        called when a service with the desired type goes offline.
-        '''
-
-        # loop through the list items looking for the service that went offline
-        for idx in xrange(self.ServicesList.GetItemCount()):
-            # this is the unique identifier assigned to the item
-            item_id = self.ServicesList.GetItemData(idx)
-
-            # this is the full typename that was received by addService
-            item_name = self.itemDataMap[item_id][4]
-
-            if item_name == name:
-                self.ServicesList.DeleteItem(idx)
-                break
-
-    def add_service(self, zeroconf, _type, name):
-        wx.CallAfter(self._addService, _type, name)
-
-    def _addService(self, _type, name):
-        '''
-        called when a service with the desired type is discovered.
-        '''
-        info = self.ZeroConfInstance.get_service_info(_type, name)
-        svcname = name.split(".")[0]
-        typename = _type.split(".")[0][1:]
-        ip = str(socket.inet_ntoa(info.address))
-        port = info.port
-
-        num_items = self.ServicesList.GetItemCount()
-
-        # display the new data in the list
-        new_item = self.ServicesList.InsertStringItem(num_items, svcname)
-        self.ServicesList.SetStringItem(new_item, 1, "%s" % typename)
-        self.ServicesList.SetStringItem(new_item, 2, "%s" % ip)
-        self.ServicesList.SetStringItem(new_item, 3, "%s" % port)
-
-        # record the new data for the ColumnSorterMixin
-        # we assign every list item a unique id (that won't change when items
-        # are added or removed)
-        self.ServicesList.SetItemData(new_item, self.nextItemId)
-
-        # the value of each column has to be stored in the itemDataMap
-        # so that ColumnSorterMixin knows how to sort the column.
-
-        # "name" is included at the end so that self.removeService
-        # can access it.
-        self.itemDataMap[self.nextItemId] = [svcname, typename, ip, port, name]
-
-        self.nextItemId += 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dialogs/IDManager.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,24 @@
+from __future__ import absolute_import
+
+import wx
+from controls.IDBrowser import IDBrowser
+
+
+class IDManager(wx.Dialog):
+    def __init__(self, parent, ctr):
+        self.ctr = ctr
+        wx.Dialog.__init__(self,
+                           name='IDManager', parent=parent,
+                           title=_('URI Editor'),
+                           style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
+                           size=(800, 600))
+        # start IDBrowser in manager mode
+        self.browser = IDBrowser(self, ctr)
+        self.Bind(wx.EVT_CHAR_HOOK, self.OnEscapeKey)
+
+    def OnEscapeKey(self, event):
+        keycode = event.GetKeyCode()
+        if keycode == wx.WXK_ESCAPE:
+            self.EndModal(wx.ID_CANCEL)
+        else:
+            event.Skip()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dialogs/IDMergeDialog.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import wx
+
+
+# class RichMessageDialog is still not available in wxPython 3.0.2
+class IDMergeDialog(wx.Dialog):
+    def __init__(self, parent, title, question, optiontext, button_texts):
+        wx.Dialog.__init__(self, parent, title=title)
+
+        main_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        message = wx.StaticText(self, label=question)
+        main_sizer.AddWindow(message, border=20,
+                             flag=wx.ALIGN_CENTER_HORIZONTAL | wx.TOP | wx.LEFT | wx.RIGHT)
+
+        self.check = wx.CheckBox(self, label=optiontext)
+        main_sizer.AddWindow(self.check, border=20,
+                             flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
+
+        buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        for label, wxID in zip(button_texts, [wx.ID_YES, wx.ID_NO, wx.ID_CANCEL]):
+            Button = wx.Button(self, label=label)
+
+            def OnButtonFactory(_wxID):
+                return lambda event: self.EndModal(_wxID)
+
+            self.Bind(wx.EVT_BUTTON, OnButtonFactory(wxID), Button)
+            buttons_sizer.AddWindow(Button)
+
+        main_sizer.AddSizer(buttons_sizer, border=20,
+                            flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.ALIGN_RIGHT)
+
+        self.SetSizer(main_sizer)
+        self.Fit()
+
+        self.Bind(wx.EVT_CHAR_HOOK, self.OnEscapeKey)
+
+    def OnEscapeKey(self, event):
+        keycode = event.GetKeyCode()
+        if keycode == wx.WXK_ESCAPE:
+            self.EndModal(wx.ID_CANCEL)
+        else:
+            event.Skip()
+
+    def OptionChecked(self):
+        return self.check.GetValue()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dialogs/UriEditor.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,92 @@
+from __future__ import absolute_import
+
+import wx
+from connectors import ConnectorSchemes, EditorClassFromScheme
+from controls.DiscoveryPanel import DiscoveryPanel
+
+
+class UriEditor(wx.Dialog):
+    def _init_ctrls(self, parent):
+        self.UriTypeChoice = wx.Choice(parent=self, choices=self.choices)
+        self.UriTypeChoice.SetSelection(0)
+        self.Bind(wx.EVT_CHOICE, self.OnTypeChoice, self.UriTypeChoice)
+        self.editor_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
+
+    def _init_sizers(self):
+        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
+        typeSizer = wx.BoxSizer(wx.HORIZONTAL)
+        typeSizer.Add(wx.StaticText(self, wx.ID_ANY, _("Scheme :")), border=5,
+                      flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL)
+        typeSizer.Add(self.UriTypeChoice, border=5, flag=wx.ALL)
+        self.mainSizer.Add(typeSizer)
+
+        self.mainSizer.Add(self.editor_sizer, border=5, flag=wx.ALL)
+        self.mainSizer.Add(self.ButtonSizer, border=5,
+                           flag=wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
+        self.SetSizer(self.mainSizer)
+        self.Layout()
+        self.Fit()
+
+    def __init__(self, parent, ctr, uri=''):
+        self.ctr = ctr
+        wx.Dialog.__init__(self,
+                           name='UriEditor', parent=parent,
+                           title=_('URI Editor'))
+        self.choices = [_("- Search local network -")] + ConnectorSchemes()
+        self._init_ctrls(parent)
+        self._init_sizers()
+        self.scheme = None
+        self.scheme_editor = None
+        self.SetURI(uri)
+        self.CenterOnParent()
+
+    def OnTypeChoice(self, event):
+        index = event.GetSelection()
+        self._replaceSchemeEditor(event.GetString() if index > 0 else None)
+
+    def SetURI(self, uri):
+        try:
+            scheme, loc = uri.strip().split("://", 1)
+            scheme = scheme.upper()
+        except Exception:
+            scheme = None
+
+        if scheme in ConnectorSchemes():
+            self.UriTypeChoice.SetStringSelection(scheme)
+        else:
+            self.UriTypeChoice.SetSelection(0)
+            scheme = None
+
+        self._replaceSchemeEditor(scheme)
+
+        if scheme is not None:
+            self.scheme_editor.SetLoc(loc)
+
+    def GetURI(self):
+        if self.scheme is None:
+            return self.scheme_editor.GetURI()
+        else:
+            return self.scheme+"://"+self.scheme_editor.GetLoc()
+
+    def _replaceSchemeEditor(self, scheme):
+        self.scheme = scheme
+
+        if self.scheme_editor is not None:
+            self.editor_sizer.Detach(self.scheme_editor)
+            self.scheme_editor.Destroy()
+            self.scheme_editor = None
+
+        if scheme is not None:
+            EditorClass = EditorClassFromScheme(scheme)
+            self.scheme_editor = EditorClass(scheme, self)
+        else:
+            # None is for searching local network
+            self.scheme_editor = DiscoveryPanel(self)
+
+        self.editor_sizer.Add(self.scheme_editor)
+        self.scheme_editor.Refresh()
+
+        self.editor_sizer.Layout()
+        self.mainSizer.Layout()
+        self.Fit()
--- a/dialogs/__init__.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/dialogs/__init__.py	Mon Mar 11 13:51:07 2019 +0100
@@ -49,4 +49,5 @@
 from dialogs.PouActionDialog import PouActionDialog
 from dialogs.FindInPouDialog import FindInPouDialog
 from dialogs.BrowseValuesLibraryDialog import BrowseValuesLibraryDialog
-from dialogs.DiscoveryDialog import DiscoveryDialog
+from dialogs.UriEditor import UriEditor
+from dialogs.IDManager import IDManager
--- a/editors/ConfTreeNodeEditor.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/editors/ConfTreeNodeEditor.py	Mon Mar 11 13:51:07 2019 +0100
@@ -33,8 +33,8 @@
 
 from IDEFrame import TITLE, FILEMENU, PROJECTTREE, PAGETITLES
 
-from controls import TextCtrlAutoComplete, UriLocationEditor
-from dialogs import BrowseValuesLibraryDialog
+from controls import TextCtrlAutoComplete
+from dialogs import BrowseValuesLibraryDialog, UriEditor
 from util.BitmapLibrary import GetBitmap
 
 if wx.Platform == '__WXMSW__':
@@ -345,7 +345,7 @@
 
         # Get connector uri
         uri = CTR_BeremizRoot.getURI_location().strip()
-        dialog = UriLocationEditor.UriLocationEditor(CTR_AppFrame, uri)
+        dialog = UriEditor(CTR_AppFrame, CTR, uri)
 
         if dialog.ShowModal() == wx.ID_OK:
             CTR_BeremizRoot.setURI_location(dialog.GetURI())
--- a/etherlab/plc_cia402node.c	Tue Mar 05 11:43:20 2019 +0300
+++ b/etherlab/plc_cia402node.c	Mon Mar 11 13:51:07 2019 +0100
@@ -74,6 +74,31 @@
 
 static IEC_BOOL __FirstTick = 1;
 
+typedef enum {
+    mc_mode_none, // No motion mode
+    mc_mode_csp,  // Continuous Synchronous Positionning mode
+    mc_mode_csv,  // Continuous Synchronous Velocity mode
+    mc_mode_cst,  // Continuous Synchronous Torque mode
+} mc_axismotionmode_enum;
+
+typedef struct {
+   IEC_BOOL Power;
+   IEC_BOOL CommunicationReady;
+   IEC_UINT NetworkPosition;
+   IEC_BOOL ReadyForPowerOn;
+   IEC_BOOL PowerFeedback;
+   IEC_DINT ActualRawPosition;
+   IEC_DINT ActualRawVelocity;
+   IEC_DINT ActualRawTorque;
+   IEC_DINT RawPositionSetPoint;
+   IEC_DINT RawVelocitySetPoint;
+   IEC_DINT RawTorqueSetPoint;
+   mc_axismotionmode_enum AxisMotionMode;
+   IEC_LREAL ActualVelocity;
+   IEC_LREAL ActualPosition;
+   IEC_LREAL ActualTorque;
+}axis_s;
+
 typedef struct {
 %(entry_variables)s
     axis_s* axis;
Binary file images/IDManager.png has changed
--- a/images/icons.svg	Tue Mar 05 11:43:20 2019 +0300
+++ b/images/icons.svg	Mon Mar 11 13:51:07 2019 +0100
@@ -15,7 +15,7 @@
    height="1052.3622"
    id="svg2"
    sodipodi:version="0.32"
-   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   inkscape:version="0.91 r13725"
    sodipodi:docname="icons.svg"
    inkscape:output_extension="org.inkscape.output.svg.inkscape">
   <metadata
@@ -31,8 +31,8 @@
     </rdf:RDF>
   </metadata>
   <sodipodi:namedview
-     inkscape:window-height="1136"
-     inkscape:window-width="1920"
+     inkscape:window-height="874"
+     inkscape:window-width="1600"
      inkscape:pageshadow="2"
      inkscape:pageopacity="0"
      guidetolerance="10.0"
@@ -43,11 +43,11 @@
      pagecolor="#ffffff"
      id="base"
      showgrid="false"
-     inkscape:zoom="2.5873931"
-     inkscape:cx="986.14665"
-     inkscape:cy="698.07857"
+     inkscape:zoom="16"
+     inkscape:cx="754.13513"
+     inkscape:cy="907.03479"
      inkscape:window-x="0"
-     inkscape:window-y="27"
+     inkscape:window-y="24"
      inkscape:current-layer="svg2"
      showguides="true"
      inkscape:guide-bbox="true"
@@ -89942,12 +89942,13 @@
      y="121.52582"
      id="text16266"
      xml:space="preserve"
-     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"><tspan
+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     sodipodi:linespacing="0%"><tspan
        sodipodi:role="line"
        id="tspan16268"
        x="73.295929"
        y="121.52582"
-       style="font-size:12.76095104px;line-height:1.25">%% editIECrawcode editWXGLADE editPYTHONcode EditCfile Transfer Connect Disconnect Debug %%</tspan></text>
+       style="font-size:12.76095104px;line-height:1.25">%% editIECrawcode editWXGLADE editPYTHONcode EditCfile Transfer Connect Disconnect Debug IDManager %%</tspan></text>
   <rect
      width="24"
      height="24"
@@ -93315,4 +93316,37 @@
        id="tspan16195-3-6"
        sodipodi:role="line"
        style="font-size:12.76000023px;line-height:1.25">%% fullscreen %%</tspan></text>
+  <rect
+     width="24"
+     height="24"
+     x="730"
+     y="131.36218"
+     id="IDManager"
+     style="display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;enable-background:accumulate"
+     inkscape:label="#rect16270" />
+  <g
+     id="g19473"
+     transform="matrix(1.3198788,0,0,1.3198788,-237.35005,-42.92225)"
+     style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d993d;fill-opacity:1;fill-rule:nonzero;stroke:#3d993d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10.43299961;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate">
+    <rect
+       y="140.41789"
+       x="736.44702"
+       height="8.1489973"
+       width="11.106004"
+       id="rect18652"
+       style="fill:#3d993d;fill-opacity:1;fill-rule:nonzero;stroke:#3d993d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10.43299961;stroke-dasharray:none;stroke-opacity:1;color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;stroke-dashoffset:0;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d993d;fill-opacity:1;fill-rule:nonzero;stroke:#3d993d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10.43299961;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0"
+       d="m 741.99023,134.18555 c -1.03169,0.0327 -2.08009,0.38311 -2.90625,1.11328 -0.82615,0.73017 -1.38416,1.85745 -1.41796,3.20898 l -0.002,0.0117 0,2.03516 2,0 0,-1.99805 c 0.0216,-0.86174 0.3173,-1.38252 0.74414,-1.75976 0.42684,-0.37725 1.03088,-0.59378 1.64649,-0.61329 0.61561,-0.0195 1.21879,0.1637 1.62304,0.47461 0.40425,0.31092 0.64912,0.70425 0.65821,1.37305 l 0,2.66211 2,0 0,-2.68359 0,-0.006 c -0.0168,-1.23751 -0.60543,-2.28823 -1.43946,-2.92969 -0.83402,-0.64146 -1.87455,-0.92136 -2.90625,-0.88867 z"
+       id="path18654"
+       inkscape:connector-curvature="0" />
+  </g>
+  <rect
+     inkscape:label="#rect16270"
+     style="display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#6e6e6e;stroke-width:0.1;marker:none;enable-background:accumulate;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     id="rect19460"
+     y="131.36218"
+     x="730"
+     height="24"
+     width="24" />
 </svg>
--- a/modbus/mb_runtime.c	Tue Mar 05 11:43:20 2019 +0300
+++ b/modbus/mb_runtime.c	Mon Mar 11 13:51:07 2019 +0100
@@ -1,614 +1,614 @@
-/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */
-
-/*
- * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
- *
- * This file is part of the Modbus library for Beremiz and matiec.
- *
- * This Modbus library is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
- * General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this Modbus library.  If not, see <http://www.gnu.org/licenses/>.
- *
- * This code is made available on the understanding that it will not be
- * used in safety-critical situations without a full and competent review.
- */
-
-
-#include <stdio.h>
-#include <string.h>  /* required for memcpy() */
-#include "mb_slave_and_master.h"
-#include "MB_%(locstr)s.h"
-
-
-#define MAX_MODBUS_ERROR_CODE 11
-static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = {
-    /* 0 */ "",                             /* un-used -> no error! */
-    /* 1 */ "illegal/unsuported function",
-    /* 2 */ "illegal data address",
-    /* 3 */ "illegal data value",
-    /* 4 */ "slave device failure",
-    /* 5 */ "acknowledge -> slave intends to reply later",
-    /* 6 */ "slave device busy",
-    /* 7 */ "negative acknowledge",
-    /* 8 */ "memory parity error",
-    /* 9 */ "",                             /* undefined by Modbus */
-    /* 10*/ "gateway path unavalilable",
-    /* 11*/ "gateway target device failed to respond"
-};    
-
-
-/* Execute a modbus client transaction/request */
-static int __execute_mb_request(int request_id){
-	switch (client_requests[request_id].mb_function){
-	
-	case  1: /* read coils */
-		return read_output_bits(client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].count,
-					client_requests[request_id].coms_buffer,
-					(int) client_requests[request_id].count,
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-
-	case  2: /* read discrete inputs */
-		return read_input_bits( client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].count,
-					client_requests[request_id].coms_buffer,
-					(int) client_requests[request_id].count,
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-
-	case  3: /* read holding registers */
-		return read_output_words(client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].count,
-					client_requests[request_id].coms_buffer,
-					(int) client_requests[request_id].count,
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-	
-	case  4: /* read input registers */
-		return read_input_words(client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].count,
-					client_requests[request_id].coms_buffer,
-					(int) client_requests[request_id].count,
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-
-	case  5: /* write single coil */
-		return write_output_bit(client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].coms_buffer[0],
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-
-	case  6: /* write single register */
-		return write_output_word(client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].coms_buffer[0],
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-
-	case  7: break; /* function not yet supported */
-	case  8: break; /* function not yet supported */
-	case  9: break; /* function not yet supported */
-	case 10: break; /* function not yet supported */
-	case 11: break; /* function not yet supported */
-	case 12: break; /* function not yet supported */
-	case 13: break; /* function not yet supported */
-	case 14: break; /* function not yet supported */
-	
-	case 15: /* write multiple coils */
-		return write_output_bits(client_requests[request_id].slave_id,
-					 client_requests[request_id].address,
-					 client_requests[request_id].count,
-					 client_requests[request_id].coms_buffer,
-					 client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					 client_requests[request_id].retries,
-					 &(client_requests[request_id].error_code),
-					 &(client_requests[request_id].resp_timeout),
-					 &(client_requests[request_id].coms_buf_mutex));
-
-	case 16: /* write multiple registers */
-		return write_output_words(client_requests[request_id].slave_id,
-					client_requests[request_id].address,
-					client_requests[request_id].count,
-					client_requests[request_id].coms_buffer,
-					client_nodes[client_requests[request_id].client_node_id].mb_nd,
-					client_requests[request_id].retries,
-					&(client_requests[request_id].error_code),
-					&(client_requests[request_id].resp_timeout),
-					&(client_requests[request_id].coms_buf_mutex));
-	
-	default: break;  /* should never occur, if file generation is correct */
-	}
-
-	fprintf(stderr, "Modbus plugin: Modbus function %%d not supported\n", request_id); /* should never occur, if file generation is correct */
-	return -1;
-}
-
-
-
-/* pack bits from unpacked_data to packed_data */
-static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count,  u8  *packed_data) {
-  u8 bit;
-  u16 byte, coils_processed;
-
-  if ((0 == bit_count) || (65535-start_addr < bit_count-1))
-    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-  
-  for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
-    packed_data[byte] = 0;
-    for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) {
-      if(unpacked_data[start_addr + coils_processed])
-            packed_data[byte] |=  bit; /*   set bit */
-      else  packed_data[byte] &= ~bit; /* reset bit */
-    }
-  }
-  return 0;
-}
-
-
-/* unpack bits from packed_data to unpacked_data */
-static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count,  u8  *packed_data) {
-  u8  temp, bit;
-  u16 byte, coils_processed;
-
-  if ((0 == bit_count) || (65535-start_addr < bit_count-1))
-    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-  
-  for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
-    temp = packed_data[byte] ;
-    for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) {
-      unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0;
-    }
-  }
-  return 0;
-}
-
-
-static int __read_inbits   (void *mem_map, u16 start_addr, u16 bit_count, u8  *data_bytes)
-  {return   __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);}
-static int __read_outbits  (void *mem_map, u16 start_addr, u16 bit_count, u8  *data_bytes)
-  {return   __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);}
-static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8  *data_bytes)
-  {return __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes); }
-
-
-
-static int __read_inwords  (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
-
-  if ((start_addr + word_count) > MEM_AREA_SIZE)
-    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
-  /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
-  memcpy(/* dest */ (void *)data_words,
-         /* src  */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]),
-         /* size */ word_count * 2);
-  return 0;
-}
-
-
-
-static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
-
-  if ((start_addr + word_count) > MEM_AREA_SIZE)
-    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
-  /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
-  memcpy(/* dest */ (void *)data_words,
-         /* src  */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
-         /* size */ word_count * 2);
-  return 0;
-}
-
-
-
-
-static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
-
-  if ((start_addr + word_count) > MEM_AREA_SIZE)
-    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
-  /* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned.
-   *           It is not therefore safe to cast it to an u16 data type.
-   *           The following code cannot be used. memcpy() is used instead.
-   */
-  /*
-  for (count = 0; count < word_count ; count++)
-    ((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count];
-  */
-  memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
-         /* src  */ (void *)data_words,
-         /* size */ word_count * 2);
-  return 0;
-}
-
-
-
-
-#include <pthread.h>
-
-static void *__mb_server_thread(void *_server_node)  {
-	server_node_t *server_node = _server_node;
-	mb_slave_callback_t callbacks = { 
-			&__read_inbits,
-			&__read_outbits,
-			&__write_outbits,
-			&__read_inwords,
-			&__read_outwords,
-			&__write_outwords,
-			(void *)&(server_node->mem_area)
-			};  
-	
-	// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
-	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
-
-	// mb_slave_run() should never return!
-	mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id);
-	fprintf(stderr, "Modbus plugin: Modbus server for node %%s died unexpectedly!\n", server_node->location); /* should never occur */
-	return NULL;
-}
-
-
-#define timespec_add(ts, sec, nsec) {		\
-	ts.tv_sec  +=  sec;			\
-	ts.tv_nsec += nsec;			\
-	if (ts.tv_nsec >= 1000000000) {		\
-		ts.tv_sec  ++;			\
-		ts.tv_nsec -= 1000000000;	\
-	}					\
-}
-
-
-static void *__mb_client_thread(void *_index)  {
-	int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast)
-	struct timespec next_cycle;
-	int period_sec  =  client_nodes[client_node_id].comm_period / 1000;          /* comm_period is in ms */
-	int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */
-
-	// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
-	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
-	
-	// get the current time
-	clock_gettime(CLOCK_MONOTONIC, &next_cycle);
-
-	// loop the communication with the client
-	while (1) {
-		/*
-		struct timespec cur_time;
-		clock_gettime(CLOCK_MONOTONIC, &cur_time);
-		fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec);
-		*/
-		int req;
-		for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){
-			/*just do the requests belonging to the client */
-			if (client_requests[req].client_node_id != client_node_id)
-				continue;
-			int res_tmp = __execute_mb_request(req);
-			switch (res_tmp) {
-			  case PORT_FAILURE: {
-				if (res_tmp != client_nodes[client_node_id].prev_error)
-					fprintf(stderr, "Modbus plugin: Error connecting Modbus client %%s to remote server.\n", client_nodes[client_node_id].location);
-				client_nodes[client_node_id].prev_error = res_tmp;
-				break;
-			  }
-			  case INVALID_FRAME: {
-				if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
-					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location);
-				client_requests[req].prev_error = res_tmp;
-				break;
-			  }
-			  case TIMEOUT: {
-				if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
-					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s timed out waiting for reply from server.\n", client_requests[req].location);
-				client_requests[req].prev_error = res_tmp;
-				break;
-			  }
-			  case MODBUS_ERROR: {
-				if (client_requests[req].prev_error != client_requests[req].error_code) {
-					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned error code 0x%%2x", client_requests[req].location, client_requests[req].error_code);
-					if (client_requests[req].error_code <= MAX_MODBUS_ERROR_CODE ) {
-						fprintf(stderr, "(%%s)", modbus_error_messages[client_requests[req].error_code]);
-						fprintf(stderr, ".\n");
-					}
-				}
-				client_requests[req].prev_error = client_requests[req].error_code;
-				break;
-			  }
-			  default: {
-				if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) {
-					fprintf(stderr, "Modbus plugin: Modbus client %%s has reconnected to server/slave.\n", client_nodes[client_node_id].location);
-				}
-				if ((res_tmp >= 0) && (client_requests[req]        .prev_error != 0)) {
-					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s has succesfully resumed comunication.\n", client_requests[req].location);
-				}
-				client_nodes[client_node_id].prev_error = 0;
-				client_requests[req]        .prev_error = 0;
-				break;
-			  }
-			}
-		}
-		// Determine absolute time instant for starting the next cycle
-		struct timespec prev_cycle, now;
-		prev_cycle = next_cycle;
-		timespec_add(next_cycle, period_sec, period_nsec);
-		/* NOTE A:
-		 * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will
-		 * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously
-		 * for some time until we catch up.
-		 * This is undesirable, so we detect it by making sure the next_cycle will start in the future.
-		 * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval.
-		 * 
-		 * NOTE B:
-		 * It probably does not make sense to check for overflow of timer - so we don't do it for now!
-		 * Even in 32 bit systems this will take at least 68 years since the computer booted
-		 * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0
-		 * every time the system boots). On 64 bit systems, it will take over 
-		 * 10^11 years to overflow.
-		 */
-		clock_gettime(CLOCK_MONOTONIC, &now);
-		if (  ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec)))
-		   /* We are falling behind. See NOTE A above */
-		   || (next_cycle.tv_sec < prev_cycle.tv_sec)
-		   /* Timer overflow. See NOTE B above */
-		   ) {
-			next_cycle = now;
-			timespec_add(next_cycle, period_sec, period_nsec);
-		}
-
-		clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL);
-	}
-
-	// humour the compiler.
-	return NULL;
-}
-
-
-int __cleanup_%(locstr)s ();
-int __init_%(locstr)s (int argc, char **argv){
-	int index;
-
-	for (index=0; index < NUMBER_OF_CLIENT_NODES;index++)
-		client_nodes[index].mb_nd = -1;
-	for (index=0; index < NUMBER_OF_SERVER_NODES;index++)
-		// mb_nd with negative numbers indicate how far it has been initialised (or not)
-		//   -2  --> no modbus node created;  no thread  created
-		//   -1  -->    modbus node created!; no thread  created
-		//  >=0  -->    modbus node created!;    thread  created!
-		server_nodes[index].mb_nd = -2; 
-
-	/* modbus library init */
-	/* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus
-	 *  extension currently in the user's project. This file (MB_xx.c) is handling only one instance,
-	 *  but must initialize the library for all instances. Only the first call to mb_slave_and_master_init()
-	 *  will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored
-	 *  by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments.
-	 */
-	if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) {
-		fprintf(stderr, "Modbus plugin: Error starting modbus library\n");
-		// return imediately. Do NOT goto error_exit, as we did not get to
-		//  start the modbus library!
-		return -1;
-	}
-	
-	/* init the mutex for each client request */
-	/* Must be done _before_ launching the client threads!! */
-	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
-		if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) {
-			fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
-			goto error_exit;
-		}
-	}
-
-	/* init each client connection to remote modbus server, and launch thread */
-	/* NOTE: All client_nodes[].init_state are initialised to 0 in the code 
-	 *       generated by the modbus plugin 
-	 */
-	for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){
-		/* establish client connection */
-		client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address);
-		if (client_nodes[index].mb_nd < 0){
-			fprintf(stderr, "Modbus plugin: Error creating modbus client node %%s\n", client_nodes[index].location);
-			goto error_exit;
-		}
-		client_nodes[index].init_state = 1; // we have created the node 
-		
-		/* launch a thread to handle this client node */
-		{
-			int res = 0;
-			pthread_attr_t attr;
-			res |= pthread_attr_init(&attr);
-			res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
-			if (res !=  0) {
-				fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location);
-				goto error_exit;
-			}
-		}
-		client_nodes[index].init_state = 2; // we have created the node and a thread
-	}
-
-	/* init each local server */
-	/* NOTE: All server_nodes[].init_state are initialised to 0 in the code 
-	 *       generated by the modbus plugin 
-	 */
-	for (index=0; index < NUMBER_OF_SERVER_NODES;index++){
-		/* create the modbus server */
-		server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address);
-		if (server_nodes[index].mb_nd < 0){
-			fprintf(stderr, "Modbus plugin: Error creating modbus server node %%s\n", server_nodes[index].location);
-			goto error_exit;
-		}
-		server_nodes[index].init_state = 1; // we have created the node
-		
-		/* launch a thread to handle this server node */
-		{
-			int res = 0;
-			pthread_attr_t attr;
-			res |= pthread_attr_init(&attr);
-			res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index]));
-			if (res !=  0) {
-				fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location);
-				goto error_exit;
-			}
-		}
-		server_nodes[index].init_state = 2; // we have created the node and thread
-	}
-
-	return 0;
-	
-error_exit:
-	__cleanup_%(locstr)s ();
-	return -1;
-}
-
-
-
-
-
-void __publish_%(locstr)s (){
-	int index;
-
-	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
-		/*just do the output requests */
-		if (client_requests[index].req_type == req_output){
-			if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
-                // copy from plcv_buffer to coms_buffer
-                memcpy((void *)client_requests[index].coms_buffer /* destination */,
-                       (void *)client_requests[index].plcv_buffer /* source */,
-                       REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
-                pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
-            }
-		}
-	}
-}
-
-
-
-
-
-void __retrieve_%(locstr)s (){
-	int index;
-
-	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
-		/*just do the input requests */
-		if (client_requests[index].req_type == req_input){
-			if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
-                // copy from coms_buffer to plcv_buffer
-                memcpy((void *)client_requests[index].plcv_buffer /* destination */,
-                       (void *)client_requests[index].coms_buffer /* source */,
-                       REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
-                pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
-            }
-		}
-	}
-}
-
-
-
-
-
-int __cleanup_%(locstr)s (){
-	int index, close;
-	int res = 0;
-
-	/* kill thread and close connections of each modbus client node */
-	for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) {
-		close = 0;
-		if (client_nodes[index].init_state >= 2) {
-			// thread was launched, so we try to cancel it!
-			close  = pthread_cancel(client_nodes[index].thread_id);
-			close |= pthread_join  (client_nodes[index].thread_id, NULL);
-			if (close < 0)
-				fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location);
-		}
-		res |= close;
-
-		close = 0;
-		if (client_nodes[index].init_state >= 1) {
-			// modbus client node was created, so we try to close it!
-			close = mb_master_close (client_nodes[index].mb_nd);
-			if (close < 0){
-				fprintf(stderr, "Modbus plugin: Error closing modbus client node %%s\n", client_nodes[index].location);
-				// We try to shut down as much as possible, so we do not return noW!
-			}
-			client_nodes[index].mb_nd = -1;
-		}
-		res |= close;
-		client_nodes[index].init_state = 0;
-	}
-	
-	/* kill thread and close connections of each modbus server node */
-	for (index=0; index < NUMBER_OF_SERVER_NODES; index++) {
-		close = 0;
-		if (server_nodes[index].init_state >= 2) {
-			// thread was launched, so we try to cancel it!
-			close  = pthread_cancel(server_nodes[index].thread_id);
-			close |= pthread_join  (server_nodes[index].thread_id, NULL);
-			if (close < 0)
-				fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %%s\n", server_nodes[index].location);
-		}
-		res |= close;
-
-		close = 0;
-		if (server_nodes[index].init_state >= 1) {
-			// modbus server node was created, so we try to close it!
-			close = mb_slave_close (server_nodes[index].mb_nd);
-			if (close < 0) {
-				fprintf(stderr, "Modbus plugin: Error closing node for modbus server %%s (%%d)\n", server_nodes[index].location, server_nodes[index].mb_nd);
-				// We try to shut down as much as possible, so we do not return noW!
-			}
-			server_nodes[index].mb_nd = -1;
-		}
-		res |= close;
-		server_nodes[index].init_state = 0;
-	}
-
-	/* destroy the mutex of each client request */
-	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) {
-		if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) {
-			fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
-			// We try to shut down as much as possible, so we do not return noW!
-			res |= -1;
-		}
-	}
-
-	/* modbus library close */
-	//fprintf(stderr, "Shutting down modbus library...\n");
-	if (mb_slave_and_master_done()<0) {
-		fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n");
-		res |= -1;
-	}
-
-	return res;
-}
-
+/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */
+
+/*
+ * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
+ *
+ * This file is part of the Modbus library for Beremiz and matiec.
+ *
+ * This Modbus library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this Modbus library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * This code is made available on the understanding that it will not be
+ * used in safety-critical situations without a full and competent review.
+ */
+
+
+#include <stdio.h>
+#include <string.h>  /* required for memcpy() */
+#include "mb_slave_and_master.h"
+#include "MB_%(locstr)s.h"
+
+
+#define MAX_MODBUS_ERROR_CODE 11
+static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = {
+    /* 0 */ "",                             /* un-used -> no error! */
+    /* 1 */ "illegal/unsuported function",
+    /* 2 */ "illegal data address",
+    /* 3 */ "illegal data value",
+    /* 4 */ "slave device failure",
+    /* 5 */ "acknowledge -> slave intends to reply later",
+    /* 6 */ "slave device busy",
+    /* 7 */ "negative acknowledge",
+    /* 8 */ "memory parity error",
+    /* 9 */ "",                             /* undefined by Modbus */
+    /* 10*/ "gateway path unavalilable",
+    /* 11*/ "gateway target device failed to respond"
+};    
+
+
+/* Execute a modbus client transaction/request */
+static int __execute_mb_request(int request_id){
+	switch (client_requests[request_id].mb_function){
+	
+	case  1: /* read coils */
+		return read_output_bits(client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].count,
+					client_requests[request_id].coms_buffer,
+					(int) client_requests[request_id].count,
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+
+	case  2: /* read discrete inputs */
+		return read_input_bits( client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].count,
+					client_requests[request_id].coms_buffer,
+					(int) client_requests[request_id].count,
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+
+	case  3: /* read holding registers */
+		return read_output_words(client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].count,
+					client_requests[request_id].coms_buffer,
+					(int) client_requests[request_id].count,
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+	
+	case  4: /* read input registers */
+		return read_input_words(client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].count,
+					client_requests[request_id].coms_buffer,
+					(int) client_requests[request_id].count,
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+
+	case  5: /* write single coil */
+		return write_output_bit(client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].coms_buffer[0],
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+
+	case  6: /* write single register */
+		return write_output_word(client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].coms_buffer[0],
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+
+	case  7: break; /* function not yet supported */
+	case  8: break; /* function not yet supported */
+	case  9: break; /* function not yet supported */
+	case 10: break; /* function not yet supported */
+	case 11: break; /* function not yet supported */
+	case 12: break; /* function not yet supported */
+	case 13: break; /* function not yet supported */
+	case 14: break; /* function not yet supported */
+	
+	case 15: /* write multiple coils */
+		return write_output_bits(client_requests[request_id].slave_id,
+					 client_requests[request_id].address,
+					 client_requests[request_id].count,
+					 client_requests[request_id].coms_buffer,
+					 client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					 client_requests[request_id].retries,
+					 &(client_requests[request_id].error_code),
+					 &(client_requests[request_id].resp_timeout),
+					 &(client_requests[request_id].coms_buf_mutex));
+
+	case 16: /* write multiple registers */
+		return write_output_words(client_requests[request_id].slave_id,
+					client_requests[request_id].address,
+					client_requests[request_id].count,
+					client_requests[request_id].coms_buffer,
+					client_nodes[client_requests[request_id].client_node_id].mb_nd,
+					client_requests[request_id].retries,
+					&(client_requests[request_id].error_code),
+					&(client_requests[request_id].resp_timeout),
+					&(client_requests[request_id].coms_buf_mutex));
+	
+	default: break;  /* should never occur, if file generation is correct */
+	}
+
+	fprintf(stderr, "Modbus plugin: Modbus function %%d not supported\n", request_id); /* should never occur, if file generation is correct */
+	return -1;
+}
+
+
+
+/* pack bits from unpacked_data to packed_data */
+static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count,  u8  *packed_data) {
+  u8 bit;
+  u16 byte, coils_processed;
+
+  if ((0 == bit_count) || (65535-start_addr < bit_count-1))
+    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+  
+  for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
+    packed_data[byte] = 0;
+    for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) {
+      if(unpacked_data[start_addr + coils_processed])
+            packed_data[byte] |=  bit; /*   set bit */
+      else  packed_data[byte] &= ~bit; /* reset bit */
+    }
+  }
+  return 0;
+}
+
+
+/* unpack bits from packed_data to unpacked_data */
+static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count,  u8  *packed_data) {
+  u8  temp, bit;
+  u16 byte, coils_processed;
+
+  if ((0 == bit_count) || (65535-start_addr < bit_count-1))
+    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+  
+  for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
+    temp = packed_data[byte] ;
+    for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) {
+      unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0;
+    }
+  }
+  return 0;
+}
+
+
+static int __read_inbits   (void *mem_map, u16 start_addr, u16 bit_count, u8  *data_bytes)
+  {return   __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);}
+static int __read_outbits  (void *mem_map, u16 start_addr, u16 bit_count, u8  *data_bytes)
+  {return   __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);}
+static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8  *data_bytes)
+  {return __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes); }
+
+
+
+static int __read_inwords  (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
+
+  if ((start_addr + word_count) > MEM_AREA_SIZE)
+    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+  /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
+  memcpy(/* dest */ (void *)data_words,
+         /* src  */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]),
+         /* size */ word_count * 2);
+  return 0;
+}
+
+
+
+static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
+
+  if ((start_addr + word_count) > MEM_AREA_SIZE)
+    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+  /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
+  memcpy(/* dest */ (void *)data_words,
+         /* src  */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
+         /* size */ word_count * 2);
+  return 0;
+}
+
+
+
+
+static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
+
+  if ((start_addr + word_count) > MEM_AREA_SIZE)
+    return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+  /* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned.
+   *           It is not therefore safe to cast it to an u16 data type.
+   *           The following code cannot be used. memcpy() is used instead.
+   */
+  /*
+  for (count = 0; count < word_count ; count++)
+    ((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count];
+  */
+  memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
+         /* src  */ (void *)data_words,
+         /* size */ word_count * 2);
+  return 0;
+}
+
+
+
+
+#include <pthread.h>
+
+static void *__mb_server_thread(void *_server_node)  {
+	server_node_t *server_node = _server_node;
+	mb_slave_callback_t callbacks = { 
+			&__read_inbits,
+			&__read_outbits,
+			&__write_outbits,
+			&__read_inwords,
+			&__read_outwords,
+			&__write_outwords,
+			(void *)&(server_node->mem_area)
+			};  
+	
+	// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
+	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+	// mb_slave_run() should never return!
+	mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id);
+	fprintf(stderr, "Modbus plugin: Modbus server for node %%s died unexpectedly!\n", server_node->location); /* should never occur */
+	return NULL;
+}
+
+
+#define timespec_add(ts, sec, nsec) {		\
+	ts.tv_sec  +=  sec;			\
+	ts.tv_nsec += nsec;			\
+	if (ts.tv_nsec >= 1000000000) {		\
+		ts.tv_sec  ++;			\
+		ts.tv_nsec -= 1000000000;	\
+	}					\
+}
+
+
+static void *__mb_client_thread(void *_index)  {
+	int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast)
+	struct timespec next_cycle;
+	int period_sec  =  client_nodes[client_node_id].comm_period / 1000;          /* comm_period is in ms */
+	int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */
+
+	// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
+	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+	
+	// get the current time
+	clock_gettime(CLOCK_MONOTONIC, &next_cycle);
+
+	// loop the communication with the client
+	while (1) {
+		/*
+		struct timespec cur_time;
+		clock_gettime(CLOCK_MONOTONIC, &cur_time);
+		fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec);
+		*/
+		int req;
+		for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){
+			/*just do the requests belonging to the client */
+			if (client_requests[req].client_node_id != client_node_id)
+				continue;
+			int res_tmp = __execute_mb_request(req);
+			switch (res_tmp) {
+			  case PORT_FAILURE: {
+				if (res_tmp != client_nodes[client_node_id].prev_error)
+					fprintf(stderr, "Modbus plugin: Error connecting Modbus client %%s to remote server.\n", client_nodes[client_node_id].location);
+				client_nodes[client_node_id].prev_error = res_tmp;
+				break;
+			  }
+			  case INVALID_FRAME: {
+				if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
+					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location);
+				client_requests[req].prev_error = res_tmp;
+				break;
+			  }
+			  case TIMEOUT: {
+				if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
+					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s timed out waiting for reply from server.\n", client_requests[req].location);
+				client_requests[req].prev_error = res_tmp;
+				break;
+			  }
+			  case MODBUS_ERROR: {
+				if (client_requests[req].prev_error != client_requests[req].error_code) {
+					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned error code 0x%%2x", client_requests[req].location, client_requests[req].error_code);
+					if (client_requests[req].error_code <= MAX_MODBUS_ERROR_CODE ) {
+						fprintf(stderr, "(%%s)", modbus_error_messages[client_requests[req].error_code]);
+						fprintf(stderr, ".\n");
+					}
+				}
+				client_requests[req].prev_error = client_requests[req].error_code;
+				break;
+			  }
+			  default: {
+				if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) {
+					fprintf(stderr, "Modbus plugin: Modbus client %%s has reconnected to server/slave.\n", client_nodes[client_node_id].location);
+				}
+				if ((res_tmp >= 0) && (client_requests[req]        .prev_error != 0)) {
+					fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s has succesfully resumed comunication.\n", client_requests[req].location);
+				}
+				client_nodes[client_node_id].prev_error = 0;
+				client_requests[req]        .prev_error = 0;
+				break;
+			  }
+			}
+		}
+		// Determine absolute time instant for starting the next cycle
+		struct timespec prev_cycle, now;
+		prev_cycle = next_cycle;
+		timespec_add(next_cycle, period_sec, period_nsec);
+		/* NOTE A:
+		 * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will
+		 * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously
+		 * for some time until we catch up.
+		 * This is undesirable, so we detect it by making sure the next_cycle will start in the future.
+		 * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval.
+		 * 
+		 * NOTE B:
+		 * It probably does not make sense to check for overflow of timer - so we don't do it for now!
+		 * Even in 32 bit systems this will take at least 68 years since the computer booted
+		 * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0
+		 * every time the system boots). On 64 bit systems, it will take over 
+		 * 10^11 years to overflow.
+		 */
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		if (  ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec)))
+		   /* We are falling behind. See NOTE A above */
+		   || (next_cycle.tv_sec < prev_cycle.tv_sec)
+		   /* Timer overflow. See NOTE B above */
+		   ) {
+			next_cycle = now;
+			timespec_add(next_cycle, period_sec, period_nsec);
+		}
+
+		clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL);
+	}
+
+	// humour the compiler.
+	return NULL;
+}
+
+
+int __cleanup_%(locstr)s ();
+int __init_%(locstr)s (int argc, char **argv){
+	int index;
+
+	for (index=0; index < NUMBER_OF_CLIENT_NODES;index++)
+		client_nodes[index].mb_nd = -1;
+	for (index=0; index < NUMBER_OF_SERVER_NODES;index++)
+		// mb_nd with negative numbers indicate how far it has been initialised (or not)
+		//   -2  --> no modbus node created;  no thread  created
+		//   -1  -->    modbus node created!; no thread  created
+		//  >=0  -->    modbus node created!;    thread  created!
+		server_nodes[index].mb_nd = -2; 
+
+	/* modbus library init */
+	/* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus
+	 *  extension currently in the user's project. This file (MB_xx.c) is handling only one instance,
+	 *  but must initialize the library for all instances. Only the first call to mb_slave_and_master_init()
+	 *  will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored
+	 *  by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments.
+	 */
+	if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) {
+		fprintf(stderr, "Modbus plugin: Error starting modbus library\n");
+		// return imediately. Do NOT goto error_exit, as we did not get to
+		//  start the modbus library!
+		return -1;
+	}
+	
+	/* init the mutex for each client request */
+	/* Must be done _before_ launching the client threads!! */
+	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+		if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) {
+			fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
+			goto error_exit;
+		}
+	}
+
+	/* init each client connection to remote modbus server, and launch thread */
+	/* NOTE: All client_nodes[].init_state are initialised to 0 in the code 
+	 *       generated by the modbus plugin 
+	 */
+	for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){
+		/* establish client connection */
+		client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address);
+		if (client_nodes[index].mb_nd < 0){
+			fprintf(stderr, "Modbus plugin: Error creating modbus client node %%s\n", client_nodes[index].location);
+			goto error_exit;
+		}
+		client_nodes[index].init_state = 1; // we have created the node 
+		
+		/* launch a thread to handle this client node */
+		{
+			int res = 0;
+			pthread_attr_t attr;
+			res |= pthread_attr_init(&attr);
+			res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
+			if (res !=  0) {
+				fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location);
+				goto error_exit;
+			}
+		}
+		client_nodes[index].init_state = 2; // we have created the node and a thread
+	}
+
+	/* init each local server */
+	/* NOTE: All server_nodes[].init_state are initialised to 0 in the code 
+	 *       generated by the modbus plugin 
+	 */
+	for (index=0; index < NUMBER_OF_SERVER_NODES;index++){
+		/* create the modbus server */
+		server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address);
+		if (server_nodes[index].mb_nd < 0){
+			fprintf(stderr, "Modbus plugin: Error creating modbus server node %%s\n", server_nodes[index].location);
+			goto error_exit;
+		}
+		server_nodes[index].init_state = 1; // we have created the node
+		
+		/* launch a thread to handle this server node */
+		{
+			int res = 0;
+			pthread_attr_t attr;
+			res |= pthread_attr_init(&attr);
+			res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index]));
+			if (res !=  0) {
+				fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location);
+				goto error_exit;
+			}
+		}
+		server_nodes[index].init_state = 2; // we have created the node and thread
+	}
+
+	return 0;
+	
+error_exit:
+	__cleanup_%(locstr)s ();
+	return -1;
+}
+
+
+
+
+
+void __publish_%(locstr)s (){
+	int index;
+
+	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+		/*just do the output requests */
+		if (client_requests[index].req_type == req_output){
+			if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
+                // copy from plcv_buffer to coms_buffer
+                memcpy((void *)client_requests[index].coms_buffer /* destination */,
+                       (void *)client_requests[index].plcv_buffer /* source */,
+                       REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
+                pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
+            }
+		}
+	}
+}
+
+
+
+
+
+void __retrieve_%(locstr)s (){
+	int index;
+
+	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+		/*just do the input requests */
+		if (client_requests[index].req_type == req_input){
+			if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
+                // copy from coms_buffer to plcv_buffer
+                memcpy((void *)client_requests[index].plcv_buffer /* destination */,
+                       (void *)client_requests[index].coms_buffer /* source */,
+                       REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
+                pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
+            }
+		}
+	}
+}
+
+
+
+
+
+int __cleanup_%(locstr)s (){
+	int index, close;
+	int res = 0;
+
+	/* kill thread and close connections of each modbus client node */
+	for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) {
+		close = 0;
+		if (client_nodes[index].init_state >= 2) {
+			// thread was launched, so we try to cancel it!
+			close  = pthread_cancel(client_nodes[index].thread_id);
+			close |= pthread_join  (client_nodes[index].thread_id, NULL);
+			if (close < 0)
+				fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location);
+		}
+		res |= close;
+
+		close = 0;
+		if (client_nodes[index].init_state >= 1) {
+			// modbus client node was created, so we try to close it!
+			close = mb_master_close (client_nodes[index].mb_nd);
+			if (close < 0){
+				fprintf(stderr, "Modbus plugin: Error closing modbus client node %%s\n", client_nodes[index].location);
+				// We try to shut down as much as possible, so we do not return noW!
+			}
+			client_nodes[index].mb_nd = -1;
+		}
+		res |= close;
+		client_nodes[index].init_state = 0;
+	}
+	
+	/* kill thread and close connections of each modbus server node */
+	for (index=0; index < NUMBER_OF_SERVER_NODES; index++) {
+		close = 0;
+		if (server_nodes[index].init_state >= 2) {
+			// thread was launched, so we try to cancel it!
+			close  = pthread_cancel(server_nodes[index].thread_id);
+			close |= pthread_join  (server_nodes[index].thread_id, NULL);
+			if (close < 0)
+				fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %%s\n", server_nodes[index].location);
+		}
+		res |= close;
+
+		close = 0;
+		if (server_nodes[index].init_state >= 1) {
+			// modbus server node was created, so we try to close it!
+			close = mb_slave_close (server_nodes[index].mb_nd);
+			if (close < 0) {
+				fprintf(stderr, "Modbus plugin: Error closing node for modbus server %%s (%%d)\n", server_nodes[index].location, server_nodes[index].mb_nd);
+				// We try to shut down as much as possible, so we do not return noW!
+			}
+			server_nodes[index].mb_nd = -1;
+		}
+		res |= close;
+		server_nodes[index].init_state = 0;
+	}
+
+	/* destroy the mutex of each client request */
+	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) {
+		if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) {
+			fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
+			// We try to shut down as much as possible, so we do not return noW!
+			res |= -1;
+		}
+	}
+
+	/* modbus library close */
+	//fprintf(stderr, "Shutting down modbus library...\n");
+	if (mb_slave_and_master_done()<0) {
+		fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n");
+		res |= -1;
+	}
+
+	return res;
+}
+
--- a/modbus/modbus.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/modbus/modbus.py	Mon Mar 11 13:51:07 2019 +0100
@@ -789,7 +789,7 @@
 
         LDFLAGS = []
         LDFLAGS.append(" \"-L" + ModbusPath + "\"")
-        LDFLAGS.append(os.path.join(ModbusPath, "libmb.a"))
+        LDFLAGS.append(" \"" + os.path.join(ModbusPath, "libmb.a") + "\"")
         LDFLAGS.append(" \"-Wl,-rpath," + ModbusPath + "\"")
         # LDFLAGS.append("\"" + os.path.join(ModbusPath, "mb_slave_and_master.o") + "\"")
         # LDFLAGS.append("\"" + os.path.join(ModbusPath, "mb_slave.o") + "\"")
--- a/py_ext/plc_python.c	Tue Mar 05 11:43:20 2019 +0300
+++ b/py_ext/plc_python.c	Mon Mar 11 13:51:07 2019 +0100
@@ -89,6 +89,10 @@
  */
 void __PythonEvalFB(int poll, PYTHON_EVAL* data__)
 {
+    if(!__GET_VAR(data__->TRIG)){
+        /* ACK is False when TRIG is false, except a pulse when receiving result */
+        __SET_VAR(data__->, ACK,, 0);
+    }
 	/* detect rising edge on TRIG to trigger evaluation */
 	if(((__GET_VAR(data__->TRIG) && !__GET_VAR(data__->TRIGM1)) ||
 	   /* polling is equivalent to trig on value rather than on rising edge*/
@@ -109,7 +113,7 @@
 		if(__GET_VAR(data__->STATE) == PYTHON_FB_ANSWERED){
 			/* Copy buffer content into result*/
 			__SET_VAR(data__->, RESULT,, __GET_VAR(data__->BUFFER));
-			/* signal result presece to PLC*/
+			/* signal result presence to PLC*/
 			__SET_VAR(data__->, ACK,, 1);
 			/* Mark as free */
 			__SET_VAR(data__->, STATE,, PYTHON_FB_FREE);
--- a/runtime/NevowServer.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/NevowServer.py	Mon Mar 11 13:51:07 2019 +0100
@@ -30,6 +30,7 @@
 from zope.interface import implements
 from nevow import appserver, inevow, tags, loaders, athena, url, rend
 from nevow.page import renderer
+from nevow.static import File
 from formless import annotate
 from formless import webform
 from formless import configurable
@@ -77,10 +78,12 @@
 class MainPage(athena.LiveElement):
     jsClass = u"WebInterface.PLC"
     docFactory = loaders.stan(
-        tags.div(render=tags.directive('liveElement'))[
-            tags.div(id='content')[
-                tags.div(render=tags.directive('PLCElement'))]
-        ])
+        tags.invisible[
+            tags.div(render=tags.directive('liveElement'))[
+                tags.div(id='content')[
+                    tags.div(render=tags.directive('PLCElement'))]
+            ],
+            tags.a(href='settings')['Settings']])
 
     def __init__(self, *a, **kw):
         athena.LiveElement.__init__(self, *a, **kw)
@@ -135,11 +138,34 @@
     def __init__(self):
         configurable.Configurable.__init__(self, None)
         self.bindingsNames = []
+        self.infostringcount = 0
 
     def getBindingNames(self, ctx):
         return self.bindingsNames
 
-    def addExtension(self, name, desc, fields, btnlabel, callback):
+    def addInfoString(self, label, value, name=None):
+        if isinstance(value, str):
+            def default(*k):
+                return value
+        else:
+            def default(*k):
+                return value()
+
+        if name is None:
+            name = "_infostring_" + str(self.infostringcount)
+            self.infostringcount = self.infostringcount + 1
+
+        def _bind(ctx):
+            return annotate.Property(
+                name,
+                annotate.String(
+                    label=label,
+                    default=default,
+                    immutable=True))
+        setattr(self, 'bind_' + name, _bind)
+        self.bindingsNames.append(name)
+
+    def addSettings(self, name, desc, fields, btnlabel, callback):
         def _bind(ctx):
             return annotate.MethodBinding(
                 'action_' + name,
@@ -192,25 +218,27 @@
 
     # This makes webform_css url answer some default CSS
     child_webform_css = webform.defaultCSS
+    child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css')
 
     implements(ISettings)
 
-    docFactory = loaders.stan([
-        tags.html[
-            tags.head[
-                tags.title[_("Beremiz Runtime Settings")],
-                tags.link(rel='stylesheet',
-                          type='text/css',
-                          href=url.here.child("webform_css"))
-            ],
-            tags.body[
-                tags.h1["Runtime settings:"],
-                webform.renderForms('staticSettings'),
-                tags.h2["Extensions settings:"],
-                webform.renderForms('dynamicSettings'),
-            ]
-        ]
-    ])
+    docFactory = loaders.stan([tags.html[
+        tags.head[
+            tags.title[_("Beremiz Runtime Settings")],
+            tags.link(rel='stylesheet',
+                      type='text/css',
+                      href=url.here.child("webform_css")),
+            tags.link(rel='stylesheet',
+                      type='text/css',
+                      href=url.here.child("webinterface_css"))
+        ],
+        tags.body[
+            tags.a(href='/')['Back'],
+            tags.h1["Runtime settings:"],
+            webform.renderForms('staticSettings'),
+            tags.h1["Extensions settings:"],
+            webform.renderForms('dynamicSettings'),
+        ]]])
 
     def configurable_staticSettings(self, ctx):
         return configurable.TypedInterfaceConfigurable(self)
@@ -305,11 +333,11 @@
         # print "We will be called back when the client disconnects"
 
 
-def RegisterWebsite(port):
+def RegisterWebsite(iface, port):
     website = WebInterface()
     site = appserver.NevowSite(website)
 
-    reactor.listenTCP(port, site)
+    reactor.listenTCP(port, site, interface=iface)
     print(_('HTTP interface port :'), port)
     return website
 
--- a/runtime/PLCObject.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/PLCObject.py	Mon Mar 11 13:51:07 2019 +0100
@@ -23,21 +23,25 @@
 
 
 from __future__ import absolute_import
-from threading import Thread, Lock, Semaphore, Event, Condition
+from threading import Thread, Lock, Semaphore, Event
 import ctypes
 import os
 import sys
 import traceback
 from time import time
 import _ctypes  # pylint: disable=wrong-import-order
+from six.moves import xrange
 from past.builtins import execfile
-import Pyro.core as pyro
-import six
-from six.moves import _thread, xrange
+import md5
+from tempfile import mkstemp
+import shutil
+from functools import wraps, partial
 
 from runtime.typemapping import TypeTranslator
 from runtime.loglevels import LogLevelsDefault, LogLevelsCount
+from runtime.Stunnel import getPSKID
 from runtime import PlcStatus
+from runtime import MainWorker
 
 if os.name in ("nt", "ce"):
     dlopen = _ctypes.LoadLibrary
@@ -64,141 +68,41 @@
     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:
-            exc_type = _job.exc_info[0]
-            exc_value = _job.exc_info[1]
-            exc_traceback = _job.exc_info[2]
-            six.reraise(exc_type, exc_value, exc_traceback)
-
-    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):
+    @wraps(func)
     def func_wrapper(*args, **kwargs):
         return MainWorker.call(func, *args, **kwargs)
     return func_wrapper
 
 
-class PLCObject(pyro.ObjBase):
-    def __init__(self, server):
-        pyro.ObjBase.__init__(self)
-        self.evaluator = server.evaluator
-        self.argv = [server.workdir] + server.argv  # force argv[0] to be "path" to exec...
-        self.workingdir = server.workdir
+class PLCObject(object):
+    def __init__(self, WorkingDir, argv, statuschange, evaluator, pyruntimevars):
+        self.workingdir = WorkingDir  # must exits already
+        self.tmpdir = os.path.join(WorkingDir, 'tmp')
+        if os.path.exists(self.tmpdir):
+            shutil.rmtree(self.tmpdir)
+        os.mkdir(self.tmpdir)
+        # FIXME : is argv of any use nowadays ?
+        self.argv = [WorkingDir] + argv  # force argv[0] to be "path" to exec...
+        self.statuschange = statuschange
+        self.evaluator = evaluator
+        self.pyruntimevars = pyruntimevars
         self.PLCStatus = PlcStatus.Empty
         self.PLClibraryHandle = None
         self.PLClibraryLock = Lock()
-        self.DummyIteratorLock = None
         # Creates fake C funcs proxies
         self._InitPLCStubCalls()
-        self.daemon = server.daemon
-        self.statuschange = server.statuschange
-        self.hmi_frame = None
-        self.pyruntimevars = server.pyruntimevars
         self._loading_error = None
         self.python_runtime_vars = None
         self.TraceThread = None
         self.TraceLock = Lock()
         self.Traces = []
+        self.DebugToken = 0
+
+        self._init_blobs()
 
     # First task of worker -> no @RunInMain
-    def AutoLoad(self):
+    def AutoLoad(self, autostart):
         # Get the last transfered PLC
         try:
             self.CurrentPLCFilename = open(
@@ -206,10 +110,15 @@
                 "r").read().strip() + lib_ext
             if self.LoadPLC():
                 self.PLCStatus = PlcStatus.Stopped
+                if autostart:
+                    self.StartPLC()
+                    return
         except Exception:
             self.PLCStatus = PlcStatus.Empty
             self.CurrentPLCFilename = None
 
+        self.StatusChange()
+
     def StatusChange(self):
         if self.statuschange is not None:
             for callee in self.statuschange:
@@ -545,7 +454,56 @@
         return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
 
     @RunInMain
-    def NewPLC(self, md5sum, data, extrafiles):
+    def GetPLCID(self):
+        return getPSKID(partial(self.LogMessage, 0))
+
+    def _init_blobs(self):
+        self.blobs = {}
+        if os.path.exists(self.tmpdir):
+            shutil.rmtree(self.tmpdir)
+        os.mkdir(self.tmpdir)
+
+    @RunInMain
+    def SeedBlob(self, seed):
+        blob = (mkstemp(dir=self.tmpdir) + (md5.new(),))
+        _fobj, _path, md5sum = blob
+        md5sum.update(seed)
+        newBlobID = md5sum.digest()
+        self.blobs[newBlobID] = blob
+        return newBlobID
+
+    @RunInMain
+    def AppendChunkToBlob(self, data, blobID):
+        blob = self.blobs.pop(blobID, None)
+
+        if blob is None:
+            return None
+
+        fobj, _path, md5sum = blob
+        md5sum.update(data)
+        newBlobID = md5sum.digest()
+        os.write(fobj, data)
+        self.blobs[newBlobID] = blob
+        return newBlobID
+
+    @RunInMain
+    def PurgeBlobs(self):
+        for fobj, _path, _md5sum in self.blobs:
+            os.close(fobj)
+        self._init_blobs()
+
+    def _BlobAsFile(self, blobID, newpath):
+        blob = self.blobs.pop(blobID, None)
+
+        if blob is None:
+            raise Exception(_("Missing data to create file: {}").format(newpath))
+
+        fobj, path, _md5sum = blob
+        os.close(fobj)
+        shutil.move(path, newpath)
+
+    @RunInMain
+    def NewPLC(self, md5sum, plc_object, extrafiles):
         if self.PLCStatus in [PlcStatus.Stopped, PlcStatus.Empty, PlcStatus.Broken]:
             NewFileName = md5sum + lib_ext
             extra_files_log = os.path.join(self.workingdir, "extra_files.txt")
@@ -555,18 +513,13 @@
                 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.UnLoadPLC()
 
             self.LogMessage("NewPLC (%s)" % md5sum)
             self.PLCStatus = PlcStatus.Empty
 
             try:
-                if replace_PLC_shared_object:
-                    os.remove(old_PLC_filename)
+                os.remove(old_PLC_filename)
                 for filename in open(extra_files_log, "rt").readlines() + [extra_files_log]:
                     try:
                         os.remove(os.path.join(self.workingdir, filename.strip()))
@@ -577,17 +530,16 @@
 
             try:
                 # Create new PLC file
-                if replace_PLC_shared_object:
-                    open(new_PLC_filename, 'wb').write(data)
+                self._BlobAsFile(plc_object, new_PLC_filename)
 
                 # Store new PLC filename based on md5 key
                 open(self._GetMD5FileName(), "w").write(md5sum)
 
                 # Then write the files
                 log = open(extra_files_log, "w")
-                for fname, fdata in extrafiles:
+                for fname, blobID in extrafiles:
                     fpath = os.path.join(self.workingdir, fname)
-                    open(fpath, "wb").write(fdata)
+                    self._BlobAsFile(blobID, fpath)
                     log.write(fname+'\n')
 
                 # Store new PLC filename
@@ -598,9 +550,7 @@
                 PLCprint(traceback.format_exc())
                 return False
 
-            if not replace_PLC_shared_object:
-                self.PLCStatus = PlcStatus.Stopped
-            elif self.LoadPLC():
+            if self.LoadPLC():
                 self.PLCStatus = PlcStatus.Stopped
             else:
                 self.PLCStatus = PlcStatus.Broken
@@ -617,11 +567,13 @@
             pass
         return False
 
+    @RunInMain
     def SetTraceVariablesList(self, idxs):
         """
         Call ctype imported function to append
         these indexes to registred variables in PLC debugger
         """
+        self.DebugToken += 1
         if idxs:
             # suspend but dont disable
             if self._suspendDebug(False) == 0:
@@ -636,8 +588,10 @@
                     self._RegisterDebugVariable(idx, force)
                 self._TracesSwap()
                 self._resumeDebug()
+                return self.DebugToken
         else:
             self._suspendDebug(True)
+        return None
 
     def _TracesSwap(self):
         self.LastSwapTrace = time()
@@ -651,8 +605,10 @@
         return Traces
 
     @RunInMain
-    def GetTraceVariables(self):
-        return self.PLCStatus, self._TracesSwap()
+    def GetTraceVariables(self, DebugToken):
+        if (DebugToken is not None and DebugToken == self.DebugToken):
+            return self.PLCStatus, self._TracesSwap()
+        return PlcStatus.Broken, []
 
     def TraceThreadProc(self):
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/PyroServer.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz runtime.
+
+# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
+# Copyright (C) 2017: Andrey Skvortsov
+# Copyright (C) 2018: Edouard TISSERANT
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+from __future__ import print_function
+import sys
+
+import Pyro
+import Pyro.core as pyro
+import runtime
+from runtime.ServicePublisher import ServicePublisher
+
+
+class PyroServer(object):
+    def __init__(self, servicename, ip_addr, port):
+        self.continueloop = True
+        self.daemon = None
+        self.servicename = servicename
+        self.ip_addr = ip_addr
+        self.port = port
+        self.servicepublisher = None
+
+    def _to_be_published(self):
+        return self.servicename is not None and \
+               self.ip_addr not in ["", "localhost", "127.0.0.1"]
+
+    def PrintServerInfo(self):
+        print(_("Pyro port :"), self.port)
+
+        if self._to_be_published():
+            print(_("Publishing service on local network"))
+
+        sys.stdout.flush()
+
+    def PyroLoop(self, when_ready):
+        if self._to_be_published():
+            self.Publish()
+
+        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)
+
+            pyro_obj = Pyro.core.ObjBase()
+            pyro_obj.delegateTo(runtime.GetPLCObjectSingleton())
+
+            self.daemon.connect(pyro_obj, "PLCObject")
+
+            when_ready()
+            self.daemon.requestLoop()
+            self.daemon.sock.close()
+        self.Unpublish()
+
+    def Restart(self):
+        self.daemon.shutdown(True)
+
+    def Quit(self):
+        self.continueloop = False
+        self.daemon.shutdown(True)
+
+    def Publish(self):
+        self.servicepublisher = ServicePublisher("PYRO")
+        self.servicepublisher.RegisterService(self.servicename,
+                                              self.ip_addr, self.port)
+
+    def Unpublish(self):
+        if self.servicepublisher is not None:
+            self.servicepublisher.UnRegisterService()
+            self.servicepublisher = None
--- a/runtime/ServicePublisher.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/ServicePublisher.py	Mon Mar 11 13:51:07 2019 +0100
@@ -28,13 +28,17 @@
 import threading
 import zeroconf
 
-service_type = '_PYRO._tcp.local.'
+
+service_type = '_Beremiz._tcp.local.'
 
 
 class ServicePublisher(object):
-    def __init__(self):
+    def __init__(self, protocol):
         # type: fully qualified service type name
-        self.serviceproperties = {'description': 'Beremiz remote PLC'}
+        self.serviceproperties = {
+            'description': 'Beremiz remote PLC',
+            'protocol': protocol
+        }
 
         self.name = None
         self.ip_32b = None
@@ -52,15 +56,19 @@
 
     def _RegisterService(self, name, ip, port):
         # name: fully qualified service name
-        self.service_name = 'Beremiz_%s.%s' % (name, service_type)
+        self.service_name = '%s.%s' % (name, service_type)
         self.name = name
         self.port = port
 
-        self.server = zeroconf.Zeroconf()
-        print("MDNS brodcasting on :" + ip)
+        if ip == "0.0.0.0":
+            print("MDNS brodcasted on all interfaces")
+            interfaces = zeroconf.InterfaceChoice.All
+            ip = self.gethostaddr()
+        else:
+            interfaces = [ip]
 
-        if ip == "0.0.0.0":
-            ip = self.gethostaddr()
+        self.server = zeroconf.Zeroconf(interfaces=interfaces)
+
         print("MDNS brodcasted service address :" + ip)
         self.ip_32b = socket.inet_aton(ip)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/Stunnel.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,44 @@
+from __future__ import absolute_import
+import os
+from binascii import b2a_hqx
+try:
+    from runtime.spawn_subprocess import call
+except ImportError:
+    from subprocess import call
+
+restart_stunnel_cmdline = ["/etc/init.d/S50stunnel", "restart"]
+
+_PSKpath = None
+
+
+def PSKgen(ID, PSKpath):
+
+    # b2a_hqx output len is 4/3 input len
+    secret = os.urandom(192)  # int(256/1.3333)
+    secretstring = b2a_hqx(secret)
+
+    PSKstring = ID+":"+secretstring
+    with open(PSKpath, 'w') as f:
+        f.write(PSKstring)
+    call(restart_stunnel_cmdline)
+
+
+def ensurePSK(ID, PSKpath):
+    global _PSKpath
+    _PSKpath = PSKpath
+    # check if already there
+    if not os.path.exists(PSKpath):
+        # create if needed
+        PSKgen(ID, PSKpath)
+
+
+def getPSKID(errorlog):
+    if _PSKpath is not None:
+        if not os.path.exists(_PSKpath):
+            errorlog(
+                'Error: Pre-Shared-Key Secret in %s is missing!\n' % _PSKpath)
+            return None
+        ID, _sep, PSK = open(_PSKpath).read().partition(':')
+        PSK = PSK.rstrip('\n\r')
+        return (ID, PSK)
+    return None
--- a/runtime/WampClient.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/WampClient.py	Mon Mar 11 13:51:07 2019 +0100
@@ -33,13 +33,13 @@
 from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
 from autobahn.wamp import types, auth
 from autobahn.wamp.serializer import MsgPackSerializer
-from twisted.internet.defer import inlineCallbacks
 from twisted.internet.protocol import ReconnectingClientFactory
 from twisted.python.components import registerAdapter
 
 from formless import annotate, webform
 import formless
 from nevow import tags, url, static
+from runtime import GetPLCObjectSingleton
 
 mandatoryConfigItems = ["ID", "active", "realm", "url"]
 
@@ -56,6 +56,10 @@
     ("StartPLC", {}),
     ("StopPLC", {}),
     ("GetPLCstatus", {}),
+    ("GetPLCID", {}),
+    ("SeedBlob", {}),
+    ("AppendChunkToBlob", {}),
+    ("PurgeBlobs", {}),
     ("NewPLC", {}),
     ("MatchMD5", {}),
     ("SetTraceVariablesList", {}),
@@ -88,7 +92,7 @@
 def GetCallee(name):
     """ Get Callee or Subscriber corresponding to '.' spearated object path """
     names = name.split('.')
-    obj = _PySrv.plcobj
+    obj = GetPLCObjectSingleton()
     while names:
         obj = getattr(obj, names.pop(0))
     return obj
@@ -116,7 +120,6 @@
             raise Exception(
                 "don't know how to handle authmethod {}".format(challenge.method))
 
-    @inlineCallbacks
     def onJoin(self, details):
         global _WampSession
         _WampSession = self
@@ -129,13 +132,13 @@
                 registerOptions = None
                 print(_("TypeError register option: {}".format(e)))
 
-            yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
+            self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
 
         for name in SubscribedEvents:
-            yield self.subscribe(GetCallee(name), text(name))
+            self.subscribe(GetCallee(name), unicode(name))
 
         for func in DoOnJoin:
-            yield func(self)
+            func(self)
 
         print(_('WAMP session joined (%s) by:' % time.ctime()), ID)
 
@@ -146,6 +149,10 @@
         _transportFactory = None
         print(_('WAMP session left'))
 
+    def publishWithOwnID(self, eventID, value):
+        ID = self.config.extra["ID"]
+        self.publish(unicode(ID+'.'+eventID), value)
+
 
 class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory):
 
@@ -167,22 +174,16 @@
         return ReconnectingClientFactory.buildProtocol(self, addr)
 
     def clientConnectionFailed(self, connector, reason):
-        if self.continueTrying:
-            print(_("WAMP Client connection failed (%s) .. retrying ..") %
-                  time.ctime())
-            super(ReconnectingWampWebSocketClientFactory,
-                  self).clientConnectionFailed(connector, reason)
-        else:
-            del connector
+        print(_("WAMP Client connection failed (%s) .. retrying ..") %
+              time.ctime())
+        super(ReconnectingWampWebSocketClientFactory,
+              self).clientConnectionFailed(connector, reason)
 
     def clientConnectionLost(self, connector, reason):
-        if self.continueTrying:
-            print(_("WAMP Client connection lost (%s) .. retrying ..") %
-                  time.ctime())
-            super(ReconnectingWampWebSocketClientFactory,
-                  self).clientConnectionFailed(connector, reason)
-        else:
-            del connector
+        print(_("WAMP Client connection lost (%s) .. retrying ..") %
+              time.ctime())
+        super(ReconnectingWampWebSocketClientFactory,
+              self).clientConnectionFailed(connector, reason)
 
 
 def CheckConfiguration(WampClientConf):
@@ -226,12 +227,9 @@
 
     with open(os.path.realpath(_WampConf), 'w') as f:
         json.dump(WampClientConf, f, sort_keys=True, indent=4)
+    StopReconnectWampClient()
     if 'active' in WampClientConf and WampClientConf['active']:
-        if _transportFactory and _WampSession:
-            StopReconnectWampClient()
         StartReconnectWampClient()
-    else:
-        StopReconnectWampClient()
 
     return WampClientConf
 
@@ -343,6 +341,16 @@
     _PySrv = pysrv
 
 
+def PublishEvent(eventID, value):
+    if getWampStatus() == "Attached":
+        _WampSession.publish(unicode(eventID), value)
+
+
+def PublishEventWithOwnID(eventID, value):
+    if getWampStatus() == "Attached":
+        _WampSession.publishWithOwnID(unicode(eventID), value)
+
+
 # WEB CONFIGURATION INTERFACE
 WAMP_SECRET_URL = "secret"
 webExposedConfigItems = ['active', 'url', 'ID']
@@ -428,7 +436,8 @@
 
 
 def RegisterWebSettings(NS):
-    NS.ConfigurableSettings.addExtension(
+
+    NS.ConfigurableSettings.addSettings(
         "wamp",
         _("Wamp Settings"),
         webFormInterface,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/Worker.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz runtime.
+#
+# Copyright (C) 2018: Edouard TISSERANT
+#
+# See COPYING.Runtime file for copyrights details.
+
+from __future__ import absolute_import
+import sys
+import thread
+from threading import Lock, Condition
+import six
+
+
+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()
+        self.mutex.acquire()
+        if args or kwargs:
+            _job = job(*args, **kwargs)
+            _job.do()
+            if _job.success:
+                # result is ignored
+                pass
+            else:
+                raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2]
+
+        while not self._finish:
+            self.todo.wait()
+            if self.job is not None:
+                self.job.do()
+                self.done.notify()
+            else:
+                break
+
+        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():
+            # 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()
+            self.job = None
+            self.free.notify()
+            self.mutex.release()
+
+        if _job.success:
+            return _job.result
+        else:
+            exc_type = _job.exc_info[0]
+            exc_value = _job.exc_info[1]
+            exc_traceback = _job.exc_info[2]
+            six.reraise(exc_type, exc_value, exc_traceback)
+
+    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()
--- a/runtime/__init__.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/__init__.py	Mon Mar 11 13:51:07 2019 +0100
@@ -1,28 +1,32 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-# This file is part of Beremiz runtime.
-#
-# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
-#
-# See COPYING.Runtime file for copyrights details.
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
+from __future__ import absolute_import
+from __future__ import print_function
+import traceback
+import sys
 
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
+from runtime.Worker import worker
+MainWorker = worker()
 
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+_PLCObjectSingleton = None
 
-from __future__ import absolute_import
-import os
 
-from runtime.PLCObject import PLCObject, PLCprint, MainWorker
-import runtime.ServicePublisher
+def GetPLCObjectSingleton():
+    assert _PLCObjectSingleton is not None
+    return _PLCObjectSingleton
+
+
+def LogMessageAndException(msg, exp=None):
+    if exp is None:
+        exp = sys.exc_info()
+    if _PLCObjectSingleton is not None:
+        _PLCObjectSingleton.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp)))
+    print(msg)
+    traceback.print_exception(*exp)
+
+
+def CreatePLCObjectSingleton(*args, **kwargs):
+    global _PLCObjectSingleton
+    from runtime.PLCObject import PLCObject  # noqa # pylint: disable=wrong-import-position
+    _PLCObjectSingleton = PLCObject(*args, **kwargs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/spawn_subprocess.py	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# subset of subprocess built-in module using posix_spawn rather than fork.
+
+from __future__ import absolute_import
+import os
+import signal
+import posix_spawn
+
+PIPE = "42"
+
+
+class Popen(object):
+    def __init__(self, args, stdin=None, stdout=None):
+        self.returncode = None
+        self.stdout = None
+        self.stdin = None
+        # TODO: stderr
+        file_actions = posix_spawn.FileActions()
+        if stdout is not None:
+            # child's stdout, child 2 parent pipe
+            c2pread, c2pwrite = os.pipe()
+            # attach child's stdout to writing en of c2p pipe
+            file_actions.add_dup2(c2pwrite, 1)
+            # close other end
+            file_actions.add_close(c2pread)
+        if stdin is not None:
+            # child's stdin, parent to child pipe
+            p2cread, p2cwrite = os.pipe()
+            # attach child's stdin to reading en of p2c pipe
+            file_actions.add_dup2(p2cread, 0)
+            # close other end
+            file_actions.add_close(p2cwrite)
+        self.pid = posix_spawn.posix_spawnp(args[0], args, file_actions=file_actions)
+        if stdout is not None:
+            self.stdout = os.fdopen(c2pread)
+            os.close(c2pwrite)
+        if stdin is not None:
+            self.stdin = os.fdopen(p2cwrite, 'w')
+            os.close(p2cread)
+
+    def _wait(self):
+        if self.returncode is None:
+            self.returncode = os.waitpid(self.pid, 0)[1]
+
+    def communicate(self):
+        if self.stdin is not None:
+            self.stdin.close()
+            self.stdin = None
+        if self.stdout is not None:
+            stdoutdata = self.stdout.read()
+        else:
+            stdoutdata = ""
+
+        # TODO
+        stderrdata = ""
+
+        self._wait()
+        if self.stdout is not None:
+            self.stdout.close()
+            self.stdout = None
+
+        return (stdoutdata, stderrdata)
+
+    def wait(self):
+        if self.stdin is not None:
+            self.stdin.close()
+            self.stdin = None
+        self._wait()
+        if self.stdout is not None:
+            self.stdout.close()
+            self.stdout = None
+        return self.returncode
+
+    def poll(self):
+        if self.returncode is None:
+            pid, ret = os.waitpid(self.pid, os.WNOHANG)
+            if (pid, ret) != (0, 0):
+                self.returncode = ret
+
+                if self.stdin is not None:
+                    self.stdin.close()
+                    self.stdin = None
+                if self.stdout is not None:
+                    self.stdout.close()
+                    self.stdout = None
+
+        return self.returncode
+
+    def kill(self):
+        os.kill(self.pid, signal.SIGKILL)
+
+        if self.stdin is not None:
+            self.stdin.close()
+            self.stdin = None
+        if self.stdout is not None:
+            self.stdout.close()
+            self.stdout = None
+
+
+def call(*args):
+    cmd = []
+    if isinstance(args[0], str):
+        if len(args) == 1:
+            # oversimplified splitting of arguments,
+            # TODO: care about use of simple and double quotes
+            cmd = args[0].split()
+        else:
+            cmd = args
+    elif isinstance(args[0], list) and len(args) == 1:
+        cmd = args[0]
+    else:
+        raise Exception("Wrong arguments passed to subprocess.call")
+    pid = posix_spawn.posix_spawnp(cmd[0], cmd)
+    return os.waitpid(pid, 0)
+
+
+if __name__ == '__main__':
+    # unit test
+
+    p = Popen(["tr", "abc", "def"], stdin=PIPE, stdout=PIPE)
+    p.stdin.write("blah")
+    p.stdin.close()
+    print(p.stdout.read())
+    p.wait()
+
+    p = Popen(["tr", "abc", "def"], stdin=PIPE, stdout=PIPE)
+    p.stdin.write("blah")
+    print(p.communicate())
+
+    call("echo blah0")
+    call(["echo", "blah1"])
+    call("echo", "blah2")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/webinterface.css	Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,6 @@
+
+.freeform-label {
+    float: left;
+    width: 30%;
+}
+
--- a/runtime_files.list	Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime_files.list	Mon Mar 11 13:51:07 2019 +0100
@@ -10,6 +10,7 @@
 runtime/PLCObject.py
 runtime/NevowServer.py
 runtime/webinterface.js
+runtime/webinterface.css
 runtime/__init__.py
 runtime/ServicePublisher.py
 runtime/typemapping.py 
--- a/targets/__init__.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/targets/__init__.py	Mon Mar 11 13:51:07 2019 +0100
@@ -74,9 +74,10 @@
             DictXSD_toolchain["toolchain_"+toolchainname] = open(xsdfilename).read()
 
     # Get all xsd targets
-    for _targetname, nfo in targets.iteritems():
+    for target_name, nfo in targets.iteritems():
         xsd_string = open(nfo["xsd"]).read()
-        targetchoices += xsd_string % DictXSD_toolchain
+        targetchoices += xsd_string % dict(DictXSD_toolchain,
+                                           target_name=target_name)
 
     return targetchoices
 
--- a/targets/toolchain_gcc.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/targets/toolchain_gcc.py	Mon Mar 11 13:51:07 2019 +0100
@@ -72,23 +72,20 @@
         """
         return self.CTRInstance.GetTarget().getcontent().getLinker()
 
-    def GetBinaryCode(self):
-        try:
-            return open(self.exe_path, "rb").read()
-        except Exception:
-            return None
+    def GetBinaryPath(self):
+        return self.bin_path
 
     def _GetMD5FileName(self):
         return os.path.join(self.buildpath, "lastbuildPLC.md5")
 
-    def ResetBinaryCodeMD5(self):
+    def ResetBinaryMD5(self):
         self.md5key = None
         try:
             os.remove(self._GetMD5FileName())
         except Exception:
             pass
 
-    def GetBinaryCodeMD5(self):
+    def GetBinaryMD5(self):
         if self.md5key is not None:
             return self.md5key
         else:
@@ -100,8 +97,8 @@
     def SetBuildPath(self, buildpath):
         if self.buildpath != buildpath:
             self.buildpath = buildpath
-            self.exe = self.CTRInstance.GetProjectName() + self.extension
-            self.exe_path = os.path.join(self.buildpath, self.exe)
+            self.bin = self.CTRInstance.GetProjectName() + self.extension
+            self.bin_path = os.path.join(self.buildpath, self.bin)
             self.md5key = None
             self.srcmd5 = {}
 
@@ -152,9 +149,6 @@
                 wholesrcdata += self.concat_deps(CFileName)
         return hashlib.md5(wholesrcdata).hexdigest()
 
-    def calc_md5(self):
-        return hashlib.md5(self.GetBinaryCode()).hexdigest()
-
     def build(self):
         # Retrieve compiler and linker
         self.compiler = self.getCompiler()
@@ -165,7 +159,7 @@
         # ----------------- GENERATE OBJECT FILES ------------------------
         obns = []
         objs = []
-        relink = self.GetBinaryCode() is None
+        relink = not os.path.exists(self.bin_path)
         for Location, CFilesAndCFLAGS, _DoCalls in self.CTRInstance.LocationCFilesAndCFLAGS:
             if CFilesAndCFLAGS:
                 if Location:
@@ -213,14 +207,14 @@
 
             ALLldflags = ' '.join(self.getBuilderLDFLAGS())
 
-            self.CTRInstance.logger.write("   [CC]  " + ' '.join(obns)+" -> " + self.exe + "\n")
+            self.CTRInstance.logger.write("   [CC]  " + ' '.join(obns)+" -> " + self.bin + "\n")
 
             status, _result, _err_result = ProcessLogger(
                 self.CTRInstance.logger,
                 "\"%s\" %s -o \"%s\" %s" %
                 (self.linker,
                  listobjstring,
-                 self.exe_path,
+                 self.bin_path,
                  ALLldflags)
             ).spin()
 
@@ -228,10 +222,10 @@
                 return False
 
         else:
-            self.CTRInstance.logger.write("   [pass]  " + ' '.join(obns)+" -> " + self.exe + "\n")
+            self.CTRInstance.logger.write("   [pass]  " + ' '.join(obns)+" -> " + self.bin + "\n")
 
         # Calculate md5 key and get data for the new created PLC
-        self.md5key = self.calc_md5()
+        self.md5key = hashlib.md5(open(self.bin_path, "rb").read()).hexdigest()
 
         # Store new PLC filename based on md5 key
         f = open(self._GetMD5FileName(), "w")
--- a/targets/toolchain_makefile.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/targets/toolchain_makefile.py	Mon Mar 11 13:51:07 2019 +0100
@@ -47,20 +47,20 @@
             self.buildpath = buildpath
             self.md5key = None
 
-    def GetBinaryCode(self):
+    def GetBinaryPath(self):
         return None
 
     def _GetMD5FileName(self):
         return os.path.join(self.buildpath, "lastbuildPLC.md5")
 
-    def ResetBinaryCodeMD5(self):
+    def ResetBinaryMD5(self):
         self.md5key = None
         try:
             os.remove(self._GetMD5FileName())
         except Exception:
             pass
 
-    def GetBinaryCodeMD5(self):
+    def GetBinaryMD5(self):
         if self.md5key is not None:
             return self.md5key
         else:
--- a/wxglade_hmi/wxglade_hmi.py	Tue Mar 05 11:43:20 2019 +0300
+++ b/wxglade_hmi/wxglade_hmi.py	Mon Mar 11 13:51:07 2019 +0100
@@ -132,32 +132,39 @@
         else:
             define_hmi = ""
 
-        declare_hmi = "\n".join(["%(name)s = None\n" % x for x in main_frames])
-        declare_hmi += "\n".join(["\n".join(["%(class)s.%(h)s = %(h)s" %
-                                             dict(x, h=h) for h in x['handlers']])
-                                  for x in hmi_objects])
         global_hmi = ("global %s\n" % ",".join(
             [x["name"] for x in main_frames]) if len(main_frames) > 0 else "")
-        init_hmi = "\n".join(["""\
+
+        declare_hmi = \
+            "\n".join(["%(name)s = None\n" % x for x in main_frames]) + \
+            "\n".join(["\n".join(["%(class)s.%(h)s = %(h)s" % dict(x, h=h)
+                                  for h in x['handlers']])
+                       for x in hmi_objects]) + """\
+
 def OnCloseFrame(evt):
     wx.MessageBox(_("Please stop PLC to close"))
 
-%(name)s = %(class)s(None)
-%(name)s.Bind(wx.EVT_CLOSE, OnCloseFrame)
-%(name)s.Show()
+def InitHMI():
+    """ + global_hmi + "\n" + "\n".join(["""\
+    %(name)s = %(class)s(None)
+    %(name)s.Bind(wx.EVT_CLOSE, OnCloseFrame)
+    %(name)s.Show()
+
+""" % x for x in main_frames]) + """\
+def CleanupHMI():
+    """ + global_hmi + "\n" + "\n".join(["""\
+    if %(name)s is not None:
+        %(name)s.Destroy()
 """ % x for x in main_frames])
-        cleanup_hmi = "\n".join(
-            ["if %(name)s is not None: %(name)s.Destroy()" % x
-             for x in main_frames])
 
         self.PreSectionsTexts = {
             "globals": define_hmi,
             "start":   global_hmi,
-            "stop":    global_hmi + cleanup_hmi
+            "stop":    "CleanupHMI()\n"
         }
         self.PostSectionsTexts = {
             "globals": declare_hmi,
-            "start":   init_hmi,
+            "start":   "InitHMI()\n",
         }
 
         if len(main_frames) == 0 and \