Beremiz_service.py
changeset 1475 de4ee16f7c6c
parent 1458 5c87045af670
child 1571 486f94a8032c
--- a/Beremiz_service.py	Sat Dec 06 19:31:51 2014 +0000
+++ b/Beremiz_service.py	Wed Oct 21 15:00:32 2015 +0100
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 #This file is part of Beremiz, a Integrated Development Environment for
-#programming IEC 61131-3 automates supporting plcopen standard and CanFestival. 
+#programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
 #
 #Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
 #
@@ -36,12 +36,15 @@
            -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" (default:8009)
+           -c        - WAMP client config file or "off" (default:wampconf.json)
+           -e        - python extension (absolute path .py)
+
            working_dir - directory where are stored PLC files
 """%sys.argv[0]
 
 try:
-    opts, argv = getopt.getopt(sys.argv[1:], "i:p:n:x:t:a:h")
+    opts, argv = getopt.getopt(sys.argv[1:], "i:p:n:x:t:a:w:c:e:h")
 except getopt.GetoptError, err:
     # print help information and exit:
     print str(err) # will print something like "option -a not recognized"
@@ -51,6 +54,8 @@
 # default values
 given_ip = None
 port = 3000
+webport = 8009
+wampconf = "wampconf.json"
 servicename = None
 autostart = False
 enablewx = True
@@ -58,6 +63,8 @@
 enabletwisted = True
 havetwisted = False
 
+extensions=[]
+
 for o, a in opts:
     if o == "-h":
         usage()
@@ -79,10 +86,18 @@
         enabletwisted = int(a)
     elif o == "-a":
         autostart = int(a)
+    elif o == "-w":
+        webport = None if a == "off" else int(a)
+    elif o == "-c":
+        wampconf = None if a == "off" else a
+    elif o == "-e":
+        extensions.append(a)
     else:
         usage()
         sys.exit()
 
+beremiz_dir = os.path.dirname(os.path.realpath(__file__))
+
 if len(argv) > 1:
     usage()
     sys.exit()
@@ -99,26 +114,28 @@
 
 if enablewx:
     try:
-        import wx, re
-        from threading import Thread, currentThread
-        from types import *
+        import wxversion
+        wxversion.select('2.8')
+        import wx
         havewx = True
     except:
         print "Wx unavailable !"
         havewx = False
 
     if havewx:
+        import re
+        from threading import Thread, currentThread
+        from types import *
         app=wx.App(redirect=False)
-        
+
         # Import module for internationalization
         import gettext
-        
-        CWD = os.path.split(os.path.realpath(__file__))[0]
+
         def Bpath(*args):
-            return os.path.join(CWD,*args)
-        
+            return os.path.join(beremiz_dir,*args)
+
         # Get folder containing translation files
-        localedir = os.path.join(CWD,"locale")
+        localedir = os.path.join(beremiz_dir,"locale")
         # Get the default language
         langid = wx.LANGUAGE_DEFAULT
         # Define translation domain (name of translation files)
@@ -139,11 +156,11 @@
 
         if __name__ == '__main__':
             __builtin__.__dict__['_'] = wx.GetTranslation#unicode_translation
-        
+
         defaulticon = wx.Image(Bpath("images", "brz.png"))
         starticon = wx.Image(Bpath("images", "icoplay24.png"))
         stopicon = wx.Image(Bpath("images", "icostop24.png"))
-        
+
         class ParamsEntryDialog(wx.TextEntryDialog):
             if wx.VERSION < (2, 6, 0):
                 def Bind(self, event, function, id = None):
@@ -151,12 +168,12 @@
                         event(self, id, function)
                     else:
                         event(self, function)
-            
-            
-            def __init__(self, parent, message, caption = "Please enter text", defaultValue = "", 
+
+
+            def __init__(self, parent, message, caption = "Please enter text", defaultValue = "",
                                style = wx.OK|wx.CANCEL|wx.CENTRE, pos = wx.DefaultPosition):
                 wx.TextEntryDialog.__init__(self, parent, message, caption, defaultValue, style, pos)
-                
+
                 self.Tests = []
                 if wx.VERSION >= (2, 8, 0):
                     self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetAffirmativeId())
@@ -164,7 +181,7 @@
                     self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetSizer().GetItem(3).GetSizer().GetAffirmativeButton().GetId())
                 else:
                     self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetSizer().GetItem(3).GetSizer().GetChildren()[0].GetSizer().GetChildren()[0].GetWindow().GetId())
-            
+
             def OnOK(self, event):
                 value = self.GetValue()
                 texts = {"value" : value}
@@ -176,13 +193,13 @@
                         return
                 self.EndModal(wx.ID_OK)
                 event.Skip()
-            
+
             def GetValue(self):
                 return self.GetSizer().GetItem(1).GetWindow().GetValue()
-            
+
             def SetTests(self, tests):
                 self.Tests = tests
-        
+
         class BeremizTaskBarIcon(wx.TaskBarIcon):
             TBMENU_START = wx.NewId()
             TBMENU_STOP = wx.NewId()
@@ -193,14 +210,14 @@
             TBMENU_WXINSPECTOR = wx.NewId()
             TBMENU_CHANGE_WD = wx.NewId()
             TBMENU_QUIT = wx.NewId()
-            
+
             def __init__(self, pyroserver, level):
                 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)
                 self.Bind(wx.EVT_MENU, self.OnTaskBarStopPLC, id=self.TBMENU_STOP)
@@ -211,7 +228,7 @@
                 self.Bind(wx.EVT_MENU, self.OnTaskBarChangePort, id=self.TBMENU_CHANGE_PORT)
                 self.Bind(wx.EVT_MENU, self.OnTaskBarChangeWorkingDir, id=self.TBMENU_CHANGE_WD)
                 self.Bind(wx.EVT_MENU, self.OnTaskBarQuit, id=self.TBMENU_QUIT)
-            
+
             def CreatePopupMenu(self):
                 """
                 This method is called by the base class when it needs to popup
@@ -234,7 +251,7 @@
                 menu.AppendSeparator()
                 menu.Append(self.TBMENU_QUIT, _("Quit"))
                 return menu
-            
+
             def MakeIcon(self, img):
                 """
                 The various platforms have different requirements for the
@@ -247,15 +264,15 @@
                 # wxMac can be any size upto 128x128, so leave the source img alone....
                 icon = wx.IconFromBitmap(img.ConvertToBitmap() )
                 return icon
-            
+
             def OnTaskBarStartPLC(self, evt):
-                if self.pyroserver.plcobj is not None: 
+                if self.pyroserver.plcobj is not None:
                     self.pyroserver.plcobj.StartPLC()
-            
+
             def OnTaskBarStopPLC(self, evt):
                 if self.pyroserver.plcobj is not None:
                     Thread(target=self.pyroserver.plcobj.StopPLC).start()
-            
+
             def OnTaskBarChangeInterface(self, evt):
                 dlg = ParamsEntryDialog(None, _("Enter the IP of the interface to bind"), defaultValue=self.pyroserver.ip_addr)
                 dlg.SetTests([(re.compile('\d{1,3}(?:\.\d{1,3}){3}$').match, _("IP is not valid!")),
@@ -264,38 +281,38 @@
                 if dlg.ShowModal() == wx.ID_OK:
                     self.pyroserver.ip_addr = dlg.GetValue()
                     self.pyroserver.Stop()
-            
+
             def OnTaskBarChangePort(self, evt):
                 dlg = ParamsEntryDialog(None, _("Enter a port number "), defaultValue=str(self.pyroserver.port))
                 dlg.SetTests([(UnicodeType.isdigit, _("Port number must be an integer!")), (lambda port : 0 <= int(port) <= 65535 , _("Port number must be 0 <= port <= 65535!"))])
                 if dlg.ShowModal() == wx.ID_OK:
                     self.pyroserver.port = int(dlg.GetValue())
                     self.pyroserver.Stop()
-            
+
             def OnTaskBarChangeWorkingDir(self, evt):
                 dlg = wx.DirDialog(None, _("Choose a working directory "), self.pyroserver.workdir, wx.DD_NEW_DIR_BUTTON)
                 if dlg.ShowModal() == wx.ID_OK:
                     self.pyroserver.workdir = dlg.GetPath()
                     self.pyroserver.Stop()
-            
+
             def OnTaskBarChangeName(self, evt):
                 dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=self.pyroserver.name)
                 dlg.SetTests([(lambda name : len(name) is not 0 , _("Name must not be null!"))])
                 if dlg.ShowModal() == wx.ID_OK:
                     self.pyroserver.name = 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 {}
-            
+
             def OnTaskBarLiveShell(self, evt):
                 from wx import py
                 frame = py.crust.CrustFrame(**self._LiveShellLocals())
                 frame.Show()
-            
+
             def OnTaskBarWXInspector(self, evt):
                 # Activate the widget inspection tool
                 from wx.lib.inspection import InspectionTool
@@ -304,13 +321,13 @@
 
                 wnd = wx.GetApp()
                 InspectionTool().Show(wnd, True)
-            
+
             def OnTaskBarQuit(self, evt):
                 if wx.Platform == '__WXMSW__':
                     Thread(target=self.pyroserver.Quit).start()
                 self.RemoveIcon()
                 wx.CallAfter(wx.GetApp().ExitMainLoop)
-            
+
             def UpdateIcon(self, plcstatus):
                 if plcstatus is "Started" :
                     currenticon = self.MakeIcon(starticon)
@@ -334,7 +351,10 @@
     return res
 
 class Server():
-    def __init__(self, servicename, ip_addr, port, workdir, argv, autostart=False, statuschange=None, evaluator=default_evaluator, website=None):
+    def __init__(self, servicename, ip_addr, port,
+                 workdir, argv, autostart=False,
+                 statuschange=None, evaluator=default_evaluator,
+                 pyruntimevars=None):
         self.continueloop = True
         self.daemon = None
         self.servicename = servicename
@@ -347,12 +367,12 @@
         self.autostart = autostart
         self.statuschange = statuschange
         self.evaluator = evaluator
-        self.website = website
-    
+        self.pyruntimevars = pyruntimevars
+
     def Loop(self):
         while self.continueloop:
             self.Start()
-        
+
     def Restart(self):
         self.Stop()
 
@@ -365,30 +385,34 @@
     def Start(self):
         pyro.initServer()
         self.daemon=pyro.Daemon(host=self.ip_addr, port=self.port)
-        self.plcobj = PLCObject(self.workdir, self.daemon, self.argv, self.statuschange, self.evaluator, self.website)
+        self.plcobj = PLCObject(self.workdir, self.daemon, self.argv,
+                                self.statuschange, self.evaluator,
+                                self.pyruntimevars)
         uri = self.daemon.connect(self.plcobj,"PLCObject")
-    
+
         print "Pyro port :",self.port
         print "Pyro object's uri :",uri
         print "Current working directory :",self.workdir
-        
+
         # Configure and publish service
         # Not publish service if localhost in address params
-        if (self.servicename is not None and 
-            self.ip_addr is not None and 
-            self.ip_addr != "localhost" and 
+        if (self.servicename is not None and
+            self.ip_addr is not None and
+            self.ip_addr != "localhost" and
             self.ip_addr != "127.0.0.1"):
             print "Publishing service on local network"
             self.servicepublisher = ServicePublisher.ServicePublisher()
             self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port)
-        
-        if self.autostart and self.plcobj.GetPLCstatus()[0] != "Empty":
-            self.plcobj.StartPLC()
-        
+
+        if self.autostart :
+            self.plcobj.AutoLoad()
+            if self.plcobj.GetPLCstatus()[0] != "Empty":
+                self.plcobj.StartPLC()
+
         sys.stdout.flush()
-        
+
         self.daemon.requestLoop()
-    
+
     def Stop(self):
         if self.plcobj is not None:
             self.plcobj.StopPLC()
@@ -406,205 +430,57 @@
             if havewx:
                 from twisted.internet import wxreactor
                 wxreactor.install()
-            from twisted.internet import reactor, task
-            from twisted.python import log, util
-            from nevow import rend, appserver, inevow, tags, loaders, athena
-            from nevow.page import renderer
-            
+            from twisted.internet import reactor
+
             havetwisted = True
         except:
-            print "Twisted unavailable !"
+            print "Twisted unavailable."
             havetwisted = False
 
+pyruntimevars = {}
+statuschange = []
+
 if havetwisted:
-    
-    xhtml_header = '''<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
-"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
-'''
-
-    class PLCHMI(athena.LiveElement):
-    
-        initialised = False
-    
-        def HMIinitialised(self, result):
-            self.initialised = True
-        
-        def HMIinitialisation(self):
-            self.HMIinitialised(None)
-    
-    class DefaultPLCStartedHMI(PLCHMI):
-        docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))[                                    
-                                             tags.h1["PLC IS NOW STARTED"],
-                                             ])
-        
-    class PLCStoppedHMI(PLCHMI):
-        docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))[
-                                             tags.h1["PLC IS STOPPED"],
-                                             ])
-    
-    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')),
-                                                        ]])
-        
-        def __init__(self, *a, **kw):
-            athena.LiveElement.__init__(self, *a, **kw)
-            self.pcl_state = False
-            self.HMI = None
-            self.resetPLCStartedHMI()
-        
-        def setPLCState(self, state):
-            self.pcl_state = state
-            if self.HMI is not None:
-                self.callRemote('updateHMI')
-        
-        def setPLCStartedHMI(self, hmi):
-            self.PLCStartedHMIClass = hmi
-        
-        def resetPLCStartedHMI(self):
-            self.PLCStartedHMIClass = DefaultPLCStartedHMI
-        
-        def getHMI(self):
-            return self.HMI
-        
-        def HMIexec(self, function, *args, **kwargs):
-            if self.HMI is not None:
-                getattr(self.HMI, function, lambda:None)(*args, **kwargs)
-        athena.expose(HMIexec)
-        
-        def resetHMI(self):
-            self.HMI = None
-        
-        def PLCElement(self, ctx, data):
-            return self.getPLCElement()
-        renderer(PLCElement)
-        
-        def getPLCElement(self):
-            self.detachFragmentChildren()
-            if self.pcl_state:
-                f = self.PLCStartedHMIClass()
-            else:
-                f = PLCStoppedHMI()
-            f.setFragmentParent(self)
-            self.HMI = f
-            return f
-        athena.expose(getPLCElement)
-
-        def detachFragmentChildren(self):
-            for child in self.liveFragmentChildren[:]:
-                child.detach()
-    
-    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.body[
-                                           tags.div[
-                                                   tags.div( render = tags.directive( "MainPage" ))
-                                                   ]]]])
-        MainPage = MainPage()
-        PLCHMI = PLCHMI
-        
-        def __init__(self, plcState=False, *a, **kw):
-            super(WebInterface, self).__init__(*a, **kw)
-            self.jsModules.mapping[u'WebInterface'] = util.sibpath(__file__, os.path.join('runtime', 'webinterface.js'))
-            self.plcState = plcState
-            self.MainPage.setPLCState(plcState)
-
-        def getHMI(self):
-            return self.MainPage.getHMI()
-        
-        def LoadHMI(self, hmi, jsmodules):
-            for name, path in jsmodules.iteritems():
-                self.jsModules.mapping[name] = os.path.join(WorkingDir, path)
-            self.MainPage.setPLCStartedHMI(hmi)
-        
-        def UnLoadHMI(self):
-            self.MainPage.resetPLCStartedHMI()
-        
-        def PLCStarted(self):
-            self.plcState = True
-            self.MainPage.setPLCState(True)
-        
-        def PLCStopped(self):
-            self.plcState = False
-            self.MainPage.setPLCState(False)
-            
-        def renderHTTP(self, ctx):
-            """
-            Force content type to fit with SVG
-            """
-            req = inevow.IRequest(ctx)
-            req.setHeader('Content-type', 'application/xhtml+xml')
-            return super(WebInterface, self).renderHTTP(ctx)
-
-        def render_MainPage(self, ctx, data):
-            f = self.MainPage
-            f.setFragmentParent(self)
-            return ctx.tag[f]
-
-        def child_(self, ctx):
-            self.MainPage.detachFragmentChildren()
-            return WebInterface(plcState=self.plcState)
-            
-        def beforeRender(self, ctx):
-            d = self.notifyOnDisconnect()
-            d.addErrback(self.disconnected)
-        
-        def disconnected(self, reason):
-            self.MainPage.resetHMI()
-            #print reason
-            #print "We will be called back when the client disconnects"
-        
+
     if havewx:
         reactor.registerWxApp(app)
-    website = WebInterface()
-    site = appserver.NevowSite(website)
-    
-    website_port = 8009
-    listening = False
-    while not listening:
-        try:
-            reactor.listenTCP(website_port, site)
-            listening = True
-        except:
-            website_port += 1
-    print "Http interface port :",website_port
-else:
-    website = None
 
 if havewx:
     from threading import Semaphore
     wx_eval_lock = Semaphore(0)
     main_thread = currentThread()
 
-    def statuschange(status):
+    def statuschangeTskBar(status):
         wx.CallAfter(taskbar_instance.UpdateIcon,status)
-        
+
+    statuschange.append(statuschangeTskBar)
+
     def wx_evaluator(obj, *args, **kwargs):
         tocall,args,kwargs = obj.call
         obj.res = default_evaluator(tocall, *args, **kwargs)
         wx_eval_lock.release()
-        
+
     def evaluator(tocall, *args, **kwargs):
         global main_thread
         if(main_thread == currentThread()):
-            # avoid dead lock if called from the wx mainloop 
+            # 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, autostart, statuschange, evaluator, website)
+
+    pyroserver = Server(servicename, given_ip, port,
+                        WorkingDir, argv, autostart,
+                        statuschange, evaluator, pyruntimevars)
+
     taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx)
 else:
-    pyroserver = Server(servicename, given_ip, port, WorkingDir, argv, autostart, website=website)
+    pyroserver = Server(servicename, given_ip, port,
+                        WorkingDir, argv, autostart,
+                        statuschange, pyruntimevars=pyruntimevars)
+
 
 # Exception hooks s
 import threading, traceback
@@ -631,6 +507,46 @@
     threading.Thread.__init__ = init
 installThreadExcepthook()
 
+if havetwisted:
+    if webport is not None :
+        try:
+            import runtime.NevowServer as NS
+        except Exception, e:
+            print "Nevow/Athena import failed :", e
+            webport = None
+        NS.WorkingDir = WorkingDir
+
+    if wampconf is not None :
+        try:
+            import runtime.WampClient as WC
+        except Exception, e:
+            print "WAMP import failed :", e
+            wampconf = None
+
+# Load extensions
+for extfilename in extensions:
+    extension_folder = os.path.split(os.path.realpath(extfilename))[0]
+    sys.path.append(extension_folder)
+    execfile(extfilename, locals())
+
+if havetwisted:
+    if webport is not None :
+        try:
+            website = NS.RegisterWebsite(webport)
+            pyruntimevars["website"] = website
+            statuschange.append(NS.website_statuslistener_factory(website))
+        except Exception, e:
+            print "Nevow Web service failed.", e
+
+    if wampconf is not None :
+        try:
+            WC.RegisterWampClient(wampconf)
+            pyruntimevars["wampsession"] = WC.GetSession
+            WC.SetServer(pyroserver)
+        except Exception, e:
+            print "WAMP client startup failed.", e
+
+
 if havetwisted or havewx:
     pyro_thread=Thread(target=pyroserver.Loop)
     pyro_thread.start()