Merged
authorEdouard Tisserant
Fri, 20 Jul 2018 11:05:17 +0200
changeset 2225 2a9549e4380e
parent 2188 7f59aa398669 (current diff)
parent 2224 ebd83ec387f9 (diff)
child 2226 addb6eff8d94
Merged
tests/wamp/.crossbar/config.json
tests/wamp/README
tests/wamp/py_ext_0@py_ext/baseconfnode.xml
tests/wamp/py_ext_0@py_ext/pyfile.xml
--- a/Beremiz_service.py	Fri Jun 15 09:48:05 2018 +0200
+++ b/Beremiz_service.py	Fri Jul 20 11:05:17 2018 +0200
@@ -45,17 +45,17 @@
     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)
-           -i        - IP address of interface to bind to (default:localhost)
-           -p        - port number default:3000
-           -h        - print this help text and quit
-           -a        - autostart PLC (0:disable 1:enable) (default:0)
-           -x        - enable/disable wxTaskbarIcon (0:disable 1:enable) (default:1)
-           -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 default config file (default:wampconf.json)
-           -s        - WAMP client secret, given as a file
-           -e        - python extension (absolute path .py)
+  -n  zeroconf service name (default:disabled)
+  -i  IP address of interface to bind to (default:localhost)
+  -p  port number default:3000
+  -h  print this help text and quit
+  -a  autostart PLC (0:disable 1:enable) (default:0)
+  -x  enable/disable wxTaskbarIcon (0:disable 1:enable) (default:1)
+  -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)
+  -e  python extension (absolute path .py)
 
            working_dir - directory where are stored PLC files
 """ % sys.argv[0])
@@ -582,29 +582,23 @@
 
 
 installThreadExcepthook()
+havewamp = False
 
 if havetwisted:
     if webport is not None:
         try:
             import runtime.NevowServer as NS  # pylint: disable=ungrouped-imports
-        except Exception, e:
-            print(_("Nevow/Athena import failed :"), e)
+        except Exception:
+            LogMessageAndException(_("Nevow/Athena import failed :"))
             webport = None
         NS.WorkingDir = WorkingDir
 
-    # Find pre-existing project WAMP config file
-    _wampconf = os.path.join(WorkingDir, "wampconf.json")
-
-    # If project's WAMP config file exits, override default (-c)
-    if os.path.exists(_wampconf):
-        wampconf = _wampconf
-
-    if wampconf is not None:
-        try:
-            import runtime.WampClient as WC  # pylint: disable=ungrouped-imports
-        except Exception, e:
-            print(_("WAMP import failed :"), e)
-            wampconf = None
+    try:
+        import runtime.WampClient as WC  # pylint: disable=ungrouped-imports
+        WC.WorkingDir = WorkingDir
+        havewamp = True
+    except Exception:
+        LogMessageAndException(_("WAMP import failed :"))
 
 # Load extensions
 for extention_file, extension_folder in extensions:
@@ -616,22 +610,16 @@
         try:
             website = NS.RegisterWebsite(webport)
             pyruntimevars["website"] = website
+            NS.SetServer(pyroserver)
             statuschange.append(NS.website_statuslistener_factory(website))
         except Exception:
             LogMessageAndException(_("Nevow Web service failed. "))
 
-    if wampconf is not None:
+    if havewamp:
         try:
-            _wampconf = WC.LoadWampClientConf(wampconf)
-            if _wampconf:
-                if _wampconf["url"]:  # TODO : test more ?
-                    WC.RegisterWampClient(wampconf, wampsecret)
-                    pyruntimevars["wampsession"] = WC.GetSession
-                    WC.SetServer(pyroserver)
-                else:
-                    raise Exception(_("WAMP config is incomplete."))
-            else:
-                raise Exception(_("WAMP config is missing."))
+            WC.SetServer(pyroserver)
+            WC.RegisterWampClient(wampconf, wampsecret)
+            WC.RegisterWebSettings(NS)
         except Exception:
             LogMessageAndException(_("WAMP client startup failed. "))
 
--- a/ProjectController.py	Fri Jun 15 09:48:05 2018 +0200
+++ b/ProjectController.py	Fri Jul 20 11:05:17 2018 +0200
@@ -1352,6 +1352,31 @@
             if self.AppFrame is not None:
                 self.AppFrame.LogViewer.SetLogCounters(log_count)
 
+    DefaultMethods = {
+        "_Run": False,
+        "_Stop": False,
+        "_Transfer": False,
+        "_Connect": True,
+        "_Disconnect": False
+    }
+
+    MethodsFromStatus = {
+        "Started":      {"_Stop": True,
+                         "_Transfer": True,
+                         "_Connect": False,
+                         "_Disconnect": True},
+        "Stopped":      {"_Run": True,
+                         "_Transfer": True,
+                         "_Connect": False,
+                         "_Disconnect": True},
+        "Empty":        {"_Transfer": True,
+                         "_Connect": False,
+                         "_Disconnect": True},
+        "Broken":       {"_Connect": False,
+                         "_Disconnect": True},
+        "Disconnected": {},
+    }
+
     def UpdateMethodsFromPLCStatus(self):
         updated = False
         status = None
@@ -1364,21 +1389,11 @@
             self._SetConnector(None, False)
             status = "Disconnected"
         if self.previous_plcstate != status:
-            for args in {
-                    "Started":      [("_Run", False),
-                                     ("_Stop", True)],
-                    "Stopped":      [("_Run", True),
-                                     ("_Stop", False)],
-                    "Empty":        [("_Run", False),
-                                     ("_Stop", False)],
-                    "Broken":       [],
-                    "Disconnected": [("_Run", False),
-                                     ("_Stop", False),
-                                     ("_Transfer", False),
-                                     ("_Connect", True),
-                                     ("_Disconnect", False)],
-            }.get(status, []):
-                self.ShowMethod(*args)
+            allmethods = self.DefaultMethods.copy()
+            allmethods.update(
+                self.MethodsFromStatus.get(status, {}))
+            for method, active in allmethods.items():
+                self.ShowMethod(method,active)
             self.previous_plcstate = status
             if self.AppFrame is not None:
                 updated = True
@@ -1713,10 +1728,6 @@
             # Oups.
             self.logger.write_error(_("Connection failed to %s!\n") % uri)
         else:
-            self.ShowMethod("_Connect", False)
-            self.ShowMethod("_Disconnect", True)
-            self.ShowMethod("_Transfer", True)
-
             self.CompareLocalAndRemotePLC()
 
             # Init with actual PLC status and print it
--- a/runtime/NevowServer.py	Fri Jun 15 09:48:05 2018 +0200
+++ b/runtime/NevowServer.py	Fri Jul 20 11:05:17 2018 +0200
@@ -26,10 +26,19 @@
 from __future__ import absolute_import
 from __future__ import print_function
 import os
-from nevow import appserver, inevow, tags, loaders, athena
+import platform
+from zope.interface import implements
+from nevow import appserver, inevow, tags, loaders, athena, url, rend
 from nevow.page import renderer
+from formless import annotate
+from formless import webform
+from formless import configurable
 from twisted.internet import reactor
+
 import util.paths as paths
+from runtime.loglevels import LogLevels, LogLevelsDict
+
+PAGE_TITLE = 'Beremiz Runtime Web Interface'
 
 xhtml_header = '''<?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
@@ -37,6 +46,7 @@
 '''
 
 WorkingDir = None
+_PySrv = None
 
 
 class PLCHMI(athena.LiveElement):
@@ -49,7 +59,6 @@
     def HMIinitialisation(self):
         self.HMIinitialised(None)
 
-
 class DefaultPLCStartedHMI(PLCHMI):
     docFactory = loaders.stan(
         tags.div(render=tags.directive('liveElement'))[
@@ -119,19 +128,113 @@
         for child in self.liveFragmentChildren[:]:
             child.detach()
 
+class ConfigurableBindings(configurable.Configurable):
+
+    def __init__(self):
+        configurable.Configurable.__init__(self, None)
+        self.bindingsNames = []
+
+    def getBindingNames(self, ctx):
+        return self.bindingsNames
+
+    def addExtension(self, name, desc, fields, btnlabel, callback):
+        def _bind(ctx):
+            return annotate.MethodBinding(
+                'action_'+name,
+                annotate.Method(arguments=[
+                    annotate.Argument(*field)
+                    for field in fields],
+                    label = desc),
+                action = btnlabel)
+        setattr(self, 'bind_'+name, _bind)
+            
+        setattr(self, 'action_'+name, callback)
+
+        self.bindingsNames.append(name)
+
+ConfigurableSettings = ConfigurableBindings()
+
+class ISettings(annotate.TypedInterface):
+    platform = annotate.String(label = _("Platform"),
+                           default = platform.system() + " " + platform.release(),
+                           immutable = True)
+    # TODO version ?
+
+    def sendLogMessage(
+        ctx = annotate.Context(),
+        level = annotate.Choice(LogLevels,
+                                required=True, 
+                                label=_("Log message level")),
+        message = annotate.String(label=_("Message text"))):
+            pass
+    sendLogMessage = annotate.autocallable(sendLogMessage, 
+                                           label=_("Send a message to the log"),
+                                           action=_("Send"))
+
+customSettingsURLs = {
+}
+
+class SettingsPage(rend.Page):
+    # We deserve a slash
+    addSlash = True
+    
+    # This makes webform_css url answer some default CSS
+    child_webform_css = webform.defaultCSS
+
+    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'),
+                                   ]]])
+
+    def configurable_staticSettings(self, ctx):
+        return configurable.TypedInterfaceConfigurable(self)
+
+    def configurable_dynamicSettings(self, ctx):
+        return ConfigurableSettings
+    
+    def sendLogMessage(self, level, message, **kwargs):
+        level = LogLevelsDict[level]
+        if _PySrv.plcobj is not None:
+            _PySrv.plcobj.LogMessage(level, "Web form log message: " + message )
+
+    def locateChild(self, ctx, segments):
+        if segments[0] in customSettingsURLs :
+            return customSettingsURLs[segments[0]](ctx, segments)
+        return super(SettingsPage, self).locateChild(ctx, segments)
+
 
 class WebInterface(athena.LivePage):
 
     docFactory = loaders.stan([tags.raw(xhtml_header),
                                tags.html(xmlns="http://www.w3.org/1999/xhtml")[
-                                   tags.head(render=tags.directive('liveglue')),
+                                   tags.head(render=tags.directive('liveglue'))[
+                                       tags.title[PAGE_TITLE],
+                                       tags.link(rel='stylesheet',
+                                                 type='text/css', 
+                                                 href=url.here.child("webform_css"))
+                                   ],
                                    tags.body[
                                        tags.div[
-                                           tags.div(render=tags.directive("MainPage"))
+                                           tags.div(render=tags.directive("MainPage")),
                                        ]]]])
     MainPage = MainPage()
     PLCHMI = PLCHMI
 
+    def child_settings(self, context):
+        return SettingsPage()
+
     def __init__(self, plcState=False, *a, **kw):
         super(WebInterface, self).__init__(*a, **kw)
         self.jsModules.mapping[u'WebInterface'] = paths.AbsNeighbourFile(__file__, 'webinterface.js')
@@ -184,6 +287,7 @@
         # print "We will be called back when the client disconnects"
 
 
+
 def RegisterWebsite(port):
     website = WebInterface()
     site = appserver.NevowSite(website)
@@ -209,3 +313,9 @@
 
 def website_statuslistener_factory(site):
     return statuslistener(site).listen
+
+
+def SetServer(pysrv):
+    global _PySrv
+    _PySrv = pysrv
+
--- a/runtime/WampClient.py	Fri Jun 15 09:48:05 2018 +0200
+++ b/runtime/WampClient.py	Fri Jul 20 11:05:17 2018 +0200
@@ -26,31 +26,53 @@
 from __future__ import print_function
 import time
 import json
+import os
+import re
 from autobahn.twisted import wamp
 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
+
+mandatoryConfigItems = ["ID", "active", "realm", "url"]
+
+_transportFactory = None
 _WampSession = None
 _PySrv = None
+WorkingDir = None
+
+# Find pre-existing project WAMP config file
+_WampConf = None
+_WampSecret = None
 
 ExposedCalls = [
-    "StartPLC",
-    "StopPLC",
-    "ForceReload",
-    "GetPLCstatus",
-    "NewPLC",
-    "MatchMD5",
-    "SetTraceVariablesList",
-    "GetTraceVariables",
-    "RemoteExec",
-    "GetLogMessage",
-    "ResetLogCount",
+    ("StartPLC", {}),
+    ("StopPLC", {}),
+    ("ForceReload", {}),
+    ("GetPLCstatus", {}),
+    ("NewPLC", {}),
+    ("MatchMD5", {}),
+    ("SetTraceVariablesList", {}),
+    ("GetTraceVariables", {}),
+    ("RemoteExec", {}),
+    ("GetLogMessage", {}),
+    ("ResetLogCount", {})
 ]
 
+# de-activated dumb wamp config
+defaultWampConfig = {
+    "ID": "wamptest", 
+    "active": False, 
+    "realm": "Automation", 
+    "url": "ws://127.0.0.1:8888"
+}
+
 # Those two lists are meant to be filled by customized runtime
 # or User python code.
 
@@ -60,6 +82,7 @@
 """ things to do on join (callables) """
 DoOnJoin = []
 
+lastKnownConfig = None
 
 def GetCallee(name):
     """ Get Callee or Subscriber corresponding to '.' spearated object path """
@@ -73,16 +96,19 @@
 class WampSession(wamp.ApplicationSession):
     def onConnect(self):
         if "secret" in self.config.extra:
-            user = self.config.extra["ID"].encode('utf8')
+            user = self.config.extra["ID"]
             self.join(u"Automation", [u"wampcra"], user)
         else:
             self.join(u"Automation")
 
     def onChallenge(self, challenge):
         if challenge.method == u"wampcra":
-            secret = self.config.extra["secret"].encode('utf8')
-            signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8'))
-            return signature.decode("ascii")
+            if "secret" in self.config.extra:
+                secret = self.config.extra["secret"].encode('utf8')
+                signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8'))
+                return signature.decode("ascii")
+            else:
+                raise Exception("no secret given for authentication")
         else:
             raise Exception("don't know how to handle authmethod {}".format(challenge.method))
 
@@ -91,10 +117,15 @@
         global _WampSession
         _WampSession = self
         ID = self.config.extra["ID"]
-        print('WAMP session joined by :', ID)
-        for name in ExposedCalls:
-            regoption = types.RegisterOptions(u'exact', u'last')
-            yield self.register(GetCallee(name), u'.'.join((ID, name)), regoption)
+
+        for name, kwargs in ExposedCalls:
+            try:
+                registerOptions = types.RegisterOptions(**kwargs)
+            except TypeError as e:
+                registerOptions = None
+                print(_("TypeError register option: {}".format(e)))
+
+            yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
 
         for name in SubscribedEvents:
             yield self.subscribe(GetCallee(name), unicode(name))
@@ -102,81 +133,283 @@
         for func in DoOnJoin:
             yield func(self)
 
+        print(_('WAMP session joined (%s) by:' % time.ctime()), ID)
+
     def onLeave(self, details):
-        global _WampSession
+        global _WampSession, _transportFactory
+        super(WampSession, self).onLeave(details)
         _WampSession = None
+        _transportFactory = None
         print(_('WAMP session left'))
 
 
 class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory):
+    def __init__(self, config, *args, **kwargs):
+        global _transportFactory
+        WampWebSocketClientFactory.__init__(self, *args, **kwargs)
+
+        try:
+            protocolOptions = config.extra.get('protocolOptions', None)
+            if protocolOptions:
+                self.setProtocolOptions(**protocolOptions)
+            _transportFactory = self
+        except Exception, e:
+            print(_("Custom protocol options failed :"), e)
+            _transportFactory = None
+
+    def buildProtocol(self, addr):
+        self.resetDelay()
+        return ReconnectingClientFactory.buildProtocol(self, addr)
+
     def clientConnectionFailed(self, connector, reason):
-        print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime()))
-        ReconnectingClientFactory.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
 
     def clientConnectionLost(self, connector, reason):
-        print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime()))
-        ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
-
-
-def LoadWampClientConf(wampconf):
-    try:
-        WSClientConf = json.load(open(wampconf))
-        return WSClientConf
-    except ValueError, ve:
-        print(_("WAMP load error: "), ve)
-        return None
-    except Exception:
-        return None
+        if self.continueTrying:
+            print(_("WAMP Client connection lost (%s) .. retrying ..") % time.ctime())
+            super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason)
+        else:
+            del connector
+
+
+def CheckConfiguration(WampClientConf):
+    url = WampClientConf["url"]
+    if not IsCorrectUri(url):
+        raise annotate.ValidateError(
+            {"url":"Invalid URL: {}".format(url)},
+            _("WAMP configuration error:"))
+
+def GetConfiguration():
+    global lastKnownConfig
+
+    if os.path.exists(_WampConf):
+        WampClientConf = json.load(open(_WampConf))
+    else: 
+        WampClientConf = defaultWampConfig.copy()
+
+    for itemName in mandatoryConfigItems:
+        if WampClientConf.get(itemName, None) is None :
+            raise Exception(_("WAMP configuration error : missing '{}' parameter.").format(itemName))
+
+    CheckConfiguration(WampClientConf)
+
+    lastKnownConfig = WampClientConf.copy()
+    return WampClientConf
+
+
+def SetWampSecret(wampSecret):
+    with open(os.path.realpath(_WampSecret), 'w') as f:
+        f.write(wampSecret)
+
+def SetConfiguration(WampClientConf):
+    global lastKnownConfig
+
+    CheckConfiguration(WampClientConf)
+
+    lastKnownConfig = WampClientConf.copy()
+    
+    with open(os.path.realpath(_WampConf), 'w') as f:
+        json.dump(WampClientConf, f, sort_keys=True, indent=4)
+    if 'active' in WampClientConf and WampClientConf['active']:
+        if _transportFactory and _WampSession:
+            StopReconnectWampClient()
+        StartReconnectWampClient()
+    else:
+        StopReconnectWampClient()
+
+    return WampClientConf
 
 
 def LoadWampSecret(secretfname):
-    try:
-        WSClientWampSecret = open(secretfname, 'rb').read()
-        return WSClientWampSecret
-    except ValueError, ve:
-        print(_("Wamp secret load error:"), ve)
-        return None
-    except Exception:
-        return None
-
-
-def RegisterWampClient(wampconf, secretfname):
-
-    WSClientConf = LoadWampClientConf(wampconf)
-
-    if not WSClientConf:
-        print(_("WAMP client connection not established!"))
+    WSClientWampSecret = open(secretfname, 'rb').read()
+    if len(WSClientWampSecret) == 0 :
+        raise Exception(_("WAMP secret empty"))
+    return WSClientWampSecret
+
+
+def IsCorrectUri(uri):
+    return re.match(r'wss?://[^\s?:#-]+(:[0-9]+)?(/[^\s]*)?$', uri) is not None
+
+
+def RegisterWampClient(wampconf=None, wampsecret=None):
+    global _WampConf, _WampSecret
+    _WampConfDefault = os.path.join(WorkingDir, "wampconf.json")
+    _WampSecretDefault = os.path.join(WorkingDir, "wamp.secret")
+
+    # set config file path only if not already set
+    if _WampConf is None:
+        # default project's wampconf has precedance over commandline given
+        if os.path.exists(_WampConfDefault) or wampconf is None:
+            _WampConf = _WampConfDefault
+        else:
+            _WampConf = wampconf
+
+    WampClientConf = GetConfiguration()
+
+    # set secret file path only if not already set
+    if _WampSecret is None:
+        # default project's wamp secret also has precedance over commandline given
+        if os.path.exists(_WampSecretDefault):
+            _WampSecret = _WampSecretDefault
+        else:
+            _WampSecret = wampsecret
+
+    if _WampSecret is not None:
+        WampClientConf["secret"] = LoadWampSecret(_WampSecret)
+    else :
+        print(_("WAMP authentication has no secret configured"))
+        _WampSecret = _WampSecretDefault
+
+    if not WampClientConf["active"]:
+        print(_("WAMP deactivated in configuration"))
         return
 
-    WampSecret = LoadWampSecret(secretfname)
-
-    if WampSecret is not None:
-        WSClientConf["secret"] = WampSecret
-
     # create a WAMP application session factory
     component_config = types.ComponentConfig(
-        realm=WSClientConf["realm"],
-        extra=WSClientConf)
+        realm=WampClientConf["realm"],
+        extra=WampClientConf)
     session_factory = wamp.ApplicationSessionFactory(
         config=component_config)
     session_factory.session = WampSession
 
     # create a WAMP-over-WebSocket transport client factory
-    transport_factory = ReconnectingWampWebSocketClientFactory(
+    ReconnectingWampWebSocketClientFactory(
+        component_config,
         session_factory,
-        url=WSClientConf["url"],
+        url=WampClientConf["url"],
         serializers=[MsgPackSerializer()])
 
     # start the client from a Twisted endpoint
-    conn = connectWS(transport_factory)
-    print(_("WAMP client connecting to :"), WSClientConf["url"])
-    return conn
+    if _transportFactory:
+        conn = connectWS(_transportFactory)
+        print(_("WAMP client connecting to :"), WampClientConf["url"])
+        return True
+    else:
+        print(_("WAMP client can not connect to :"), WampClientConf["url"])
+        return False
+
+
+def StopReconnectWampClient():
+    if _transportFactory is not None :
+        _transportFactory.stopTrying()
+    if _WampSession is not None :
+        _WampSession.leave()
+
+
+def StartReconnectWampClient():
+    if _WampSession:
+        # do reconnect
+        _WampSession.disconnect()
+        return True
+    else:
+        # do connect
+        RegisterWampClient()
+        return True
 
 
 def GetSession():
     return _WampSession
 
+def getWampStatus():
+    if _transportFactory is not None :
+        if _WampSession is not None :
+            if _WampSession.is_attached() :
+                return "Attached"
+            return "Established"
+        return "Connecting"
+    return "Disconnected"
+
 
 def SetServer(pysrv):
-    global _PySrv
     _PySrv = pysrv
+
+
+#### WEB CONFIGURATION INTERFACE ####
+WAMP_SECRET_URL = "secret"
+webExposedConfigItems = ['active', 'url', 'ID']
+
+def wampConfigDefault(ctx,argument):
+    if lastKnownConfig is not None :
+        return lastKnownConfig.get(argument.name, None)
+
+def wampConfig(**kwargs):
+    secretfile_field = kwargs["secretfile"]
+    if secretfile_field is not None:
+        secretfile = getattr(secretfile_field, "file", None)
+        if secretfile is not None:
+            secret = secretfile_field.file.read()
+            SetWampSecret(secret)
+
+    newConfig = lastKnownConfig.copy()
+    for argname in webExposedConfigItems:
+        arg = kwargs.get(argname, None)
+        if arg is not None :
+            newConfig[argname] = arg
+
+    SetConfiguration(newConfig)
+
+class FileUploadDownload(annotate.FileUpload):
+    pass
+
+
+class FileUploadDownloadRenderer(webform.FileUploadRenderer):
+    def input(self, context, slot, data, name, value):
+        slot[_("Upload:")]
+        slot = webform.FileUploadRenderer.input(self, context, slot, data, name, value)
+        download_url = data.typedValue.getAttribute('download_url')
+        return slot[tags.a(href=download_url)[_("Download")]]
+
+registerAdapter(FileUploadDownloadRenderer, FileUploadDownload, formless.iformless.ITypedRenderer)
+           
+def getDownloadUrl(ctx, argument):
+    if lastKnownConfig is not None :
+        return url.URL.fromContext(ctx).\
+            child(WAMP_SECRET_URL).\
+            child(lastKnownConfig["ID"]+".secret")
+
+webFormInterface = [
+    ("status",
+       annotate.String(label=_("Current status"),
+                       immutable = True,
+                       default = lambda *k:getWampStatus())),
+    ("ID",
+       annotate.String(label=_("ID"),
+                       default = wampConfigDefault)),
+    ("secretfile",
+       FileUploadDownload(
+           label = _("File containing secret for that ID"),
+           download_url = getDownloadUrl,
+       )),
+    ("active",
+       annotate.Boolean(label=_("Enable WAMP connection"),
+                        default=wampConfigDefault)),
+    ("url",
+       annotate.String(label=_("WAMP Server URL"),
+                       default=wampConfigDefault))]
+
+
+def deliverWampSecret(ctx, segments):
+    filename = segments[1].decode('utf-8')
+    # FIXME: compare filename to ID+".secret"
+    # for now all url under /secret returns the secret
+
+    # TODO: make beutifull message in case of exception 
+    # while loading secret (if empty or dont exist)
+    secret = LoadWampSecret(_WampSecret)
+    return static.Data(secret, 'application/octet-stream'),()
+
+def RegisterWebSettings(NS):
+    NS.ConfigurableSettings.addExtension(
+        "wamp", 
+        _("Wamp Settings"),
+        webFormInterface,
+        _("Set"),
+        wampConfig)
+
+
+    NS.customSettingsURLs[WAMP_SECRET_URL] = deliverWampSecret
+
--- a/tests/wamp/.crossbar/config.json	Fri Jun 15 09:48:05 2018 +0200
+++ b/tests/wamp/.crossbar/config.json	Fri Jul 20 11:05:17 2018 +0200
@@ -39,13 +39,15 @@
             "transports": [
                 {
                     "type": "websocket",
+                    "debug": true,
                     "endpoint": {
                         "type": "tcp",
                         "port": 8888
                     },
                     "url": "ws://127.0.0.1:8888/",
                     "serializers": [
-                        "msgpack"
+                        "msgpack",
+                        "json"
                     ]
                 }
             ]
--- a/tests/wamp/README	Fri Jun 15 09:48:05 2018 +0200
+++ b/tests/wamp/README	Fri Jul 20 11:05:17 2018 +0200
@@ -1,25 +1,26 @@
-Crossbar test router configuration is available in .crossbar directory.
-
-Starting command:
-crossbar start
-
-This project contains wamp client config to be loaded at runtime startup.
-
-project_files/wampconf.json
+/* This project contains wamp client config to be loaded at runtime startup. */
+./project_files/wampconf.json
 
 wampconf.json is in "Project Files", so it is copied to runtime's working directory, and then loaded after program transfer + runtime restart.
 
 Otherwise, wamp config file path can be forced :
 ./Beremiz_service.py -c /path/to/my/wampconf.json /working/dir
 
-Otherwise, path for CRA secret can be forced :
-./Beremiz_service.py -s /path/to/my/secret /working/dir
+/* Crossbar install */
+#sudo apt-get update
+#sudo apt-get -y dist-upgrade
+sudo apt-get -y install build-essential libssl-dev libffi-dev libreadline-dev libbz2-dev libsqlite3-dev libncurses5-dev
+sudo python -m pip install -U pip
+sudo pip install crossbar
+crossbar version
 
+/* Start Crossbar command: */
+crossbar start
+
+/* Crossbar test router configuration is available in .crossbar directory. */
 Tested on version:
- Crossbar.io        : 17.12.1 (Crossbar.io COMMUNITY)
-   Autobahn         : 17.10.1 (with JSON, MessagePack, CBOR, UBJSON)
+ Crossbar.io        : 18.3.1 (Crossbar.io COMMUNITY)
+   Autobahn         : 18.3.1 (with JSON, MessagePack, CBOR, UBJSON)
    Twisted          : 17.9.0-EPollReactor
    LMDB             : 0.93/lmdb-0.9.18
-   Python           : 2.7.12/CPython
-
-
+   Python           : 2.7.12/CPython
\ No newline at end of file
--- a/tests/wamp/beremiz.xml	Fri Jun 15 09:48:05 2018 +0200
+++ b/tests/wamp/beremiz.xml	Fri Jul 20 11:05:17 2018 +0200
@@ -1,4 +1,4 @@
 <?xml version='1.0' encoding='utf-8'?>
-<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="WAMP://127.0.0.1:8888#Automation#wamptest">
+<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="WAMP://127.0.0.1:8888#Automation#WampID">
   <TargetType/>
 </BeremizRoot>
--- a/tests/wamp/project_files/wampconf.json	Fri Jun 15 09:48:05 2018 +0200
+++ b/tests/wamp/project_files/wampconf.json	Fri Jul 20 11:05:17 2018 +0200
@@ -1,7 +1,10 @@
 {
-    "url":"ws://127.0.0.1:8888",
-    "realm":"Automation",
-    "ID":"wamptest",
-    "password":"1234567890",
-    "key":"ABCDEFGHIJ"
+    "ID": "wamptest", 
+    "active": true, 
+    "protocolOptions": {
+        "autoPingInterval": 60, 
+        "autoPingTimeout": 20
+    }, 
+    "realm": "Automation", 
+    "url": "ws://127.0.0.1:8888"
 }