andrej@1511: #!/usr/bin/env python
andrej@1511: # -*- coding: utf-8 -*-
andrej@1511: 
andrej@1667: # This file is part of Beremiz runtime.
andrej@1511: #
andrej@1511: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
andrej@1680: # Copyright (C) 2017: Andrey Skvortsov
andrej@1511: #
andrej@1667: # See COPYING.Runtime file for copyrights details.
andrej@1511: #
andrej@1667: # This library is free software; you can redistribute it and/or
andrej@1667: # modify it under the terms of the GNU Lesser General Public
andrej@1667: # License as published by the Free Software Foundation; either
andrej@1667: # version 2.1 of the License, or (at your option) any later version.
andrej@1667: 
andrej@1667: # This library is distributed in the hope that it will be useful,
andrej@1511: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1667: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
andrej@1667: # Lesser General Public License for more details.
andrej@1667: 
andrej@1667: # You should have received a copy of the GNU Lesser General Public
andrej@1667: # License along with this library; if not, write to the Free Software
andrej@1667: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
andrej@1511: 
andrej@1826: 
andrej@1881: from __future__ import absolute_import
andrej@1826: from __future__ import print_function
Edouard@1438: import os
Edouard@2670: import collections
Edouard@3703: import shutil
Edouard@2247: import platform as platform_module
Edouard@2208: from zope.interface import implements
Edouard@2208: from nevow import appserver, inevow, tags, loaders, athena, url, rend
Edouard@1438: from nevow.page import renderer
denis@2266: from nevow.static import File
Edouard@2208: from formless import annotate
Edouard@2208: from formless import webform
Edouard@2209: from formless import configurable
Edouard@1439: from twisted.internet import reactor
Edouard@2210: 
Edouard@1919: import util.paths as paths
Edouard@2210: from runtime.loglevels import LogLevels, LogLevelsDict
edouard@2700: from runtime import MainWorker, GetPLCObjectSingleton
Edouard@1438: 
Edouard@2208: PAGE_TITLE = 'Beremiz Runtime Web Interface'
Edouard@2208: 
Edouard@1438: xhtml_header = '''<?xml version="1.0" encoding="utf-8"?>
Edouard@1438: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
Edouard@1438: "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
Edouard@1438: '''
Edouard@1438: 
Edouard@1453: WorkingDir = None
Edouard@1453: 
andrej@1736: 
Edouard@1438: class PLCHMI(athena.LiveElement):
Edouard@1438: 
Edouard@1438:     initialised = False
Edouard@1438: 
Edouard@1438:     def HMIinitialised(self, result):
Edouard@1438:         self.initialised = True
Edouard@1438: 
Edouard@1438:     def HMIinitialisation(self):
Edouard@1438:         self.HMIinitialised(None)
Edouard@1438: 
Edouard@2246: 
Edouard@1438: class DefaultPLCStartedHMI(PLCHMI):
andrej@1878:     docFactory = loaders.stan(
andrej@1878:         tags.div(render=tags.directive('liveElement'))[
andrej@1878:             tags.h1["PLC IS NOW STARTED"],
andrej@1878:         ])
Edouard@1438: 
andrej@1736: 
Edouard@1438: class PLCStoppedHMI(PLCHMI):
andrej@1878:     docFactory = loaders.stan(
andrej@1878:         tags.div(render=tags.directive('liveElement'))[
andrej@1878:             tags.h1["PLC IS STOPPED"],
andrej@1878:         ])
Edouard@1438: 
andrej@1736: 
Edouard@1438: class MainPage(athena.LiveElement):
Edouard@1438:     jsClass = u"WebInterface.PLC"
andrej@1878:     docFactory = loaders.stan(
denis@2266:         tags.invisible[
denis@2266:             tags.div(render=tags.directive('liveElement'))[
denis@2266:                 tags.div(id='content')[
denis@2266:                     tags.div(render=tags.directive('PLCElement'))]
denis@2266:             ],
denis@2266:             tags.a(href='settings')['Settings']])
Edouard@1438: 
Edouard@1438:     def __init__(self, *a, **kw):
Edouard@1438:         athena.LiveElement.__init__(self, *a, **kw)
Edouard@1438:         self.pcl_state = False
Edouard@1438:         self.HMI = None
Edouard@1438:         self.resetPLCStartedHMI()
Edouard@1438: 
Edouard@1438:     def setPLCState(self, state):
Edouard@1438:         self.pcl_state = state
Edouard@1438:         if self.HMI is not None:
Edouard@1438:             self.callRemote('updateHMI')
Edouard@1438: 
Edouard@1438:     def setPLCStartedHMI(self, hmi):
Edouard@1438:         self.PLCStartedHMIClass = hmi
Edouard@1438: 
Edouard@1438:     def resetPLCStartedHMI(self):
Edouard@1438:         self.PLCStartedHMIClass = DefaultPLCStartedHMI
Edouard@1438: 
Edouard@1438:     def getHMI(self):
Edouard@1438:         return self.HMI
Edouard@1438: 
Edouard@1438:     def HMIexec(self, function, *args, **kwargs):
Edouard@1438:         if self.HMI is not None:
andrej@1740:             getattr(self.HMI, function, lambda: None)(*args, **kwargs)
Edouard@1438:     athena.expose(HMIexec)
Edouard@1438: 
Edouard@1438:     def resetHMI(self):
Edouard@1438:         self.HMI = None
Edouard@1438: 
Edouard@1438:     def PLCElement(self, ctx, data):
Edouard@1438:         return self.getPLCElement()
Edouard@1438:     renderer(PLCElement)
Edouard@1438: 
Edouard@1438:     def getPLCElement(self):
Edouard@1438:         self.detachFragmentChildren()
Edouard@1438:         if self.pcl_state:
Edouard@1438:             f = self.PLCStartedHMIClass()
Edouard@1438:         else:
Edouard@1438:             f = PLCStoppedHMI()
Edouard@1438:         f.setFragmentParent(self)
Edouard@1438:         self.HMI = f
Edouard@1438:         return f
Edouard@1438:     athena.expose(getPLCElement)
Edouard@1438: 
Edouard@1438:     def detachFragmentChildren(self):
Edouard@1438:         for child in self.liveFragmentChildren[:]:
Edouard@1438:             child.detach()
Edouard@1438: 
Edouard@2246: 
Edouard@2209: class ConfigurableBindings(configurable.Configurable):
Edouard@2209: 
Edouard@2209:     def __init__(self):
Edouard@2209:         configurable.Configurable.__init__(self, None)
Edouard@2209:         self.bindingsNames = []
Edouard@2262:         self.infostringcount = 0
Edouard@2209: 
Edouard@2209:     def getBindingNames(self, ctx):
Edouard@2209:         return self.bindingsNames
Edouard@2209: 
Edouard@2262:     def addInfoString(self, label, value, name=None):
Edouard@2267:         if isinstance(value, str):
Edouard@2262:             def default(*k):
Edouard@2262:                 return value
Edouard@2262:         else:
Edouard@2262:             def default(*k):
Edouard@2262:                 return value()
Edouard@2262: 
Edouard@2262:         if name is None:
Edouard@2262:             name = "_infostring_" + str(self.infostringcount)
Edouard@2262:             self.infostringcount = self.infostringcount + 1
Edouard@2262: 
Edouard@2262:         def _bind(ctx):
Edouard@2262:             return annotate.Property(
Edouard@2262:                 name,
Edouard@2262:                 annotate.String(
Edouard@2262:                     label=label,
Edouard@2262:                     default=default,
Edouard@2262:                     immutable=True))
Edouard@2262:         setattr(self, 'bind_' + name, _bind)
Edouard@2262:         self.bindingsNames.append(name)
Edouard@2262: 
Edouard@2670:     def addSettings(self, name, desc, fields, btnlabel, callback):
Edouard@2209:         def _bind(ctx):
Edouard@2209:             return annotate.MethodBinding(
Edouard@2246:                 'action_' + name,
Edouard@2247:                 annotate.Method(
Edouard@2247:                     arguments=[
Edouard@2247:                         annotate.Argument(*field)
Edouard@2247:                         for field in fields],
Edouard@2246:                     label=desc),
Edouard@2246:                 action=btnlabel)
Edouard@2246:         setattr(self, 'bind_' + name, _bind)
Edouard@2246: 
Edouard@2246:         setattr(self, 'action_' + name, callback)
Edouard@2209: 
Edouard@2670:         self.bindingsNames.append(name)
msousa@2654:             
Edouard@2260: 
Edouard@2209: ConfigurableSettings = ConfigurableBindings()
Edouard@2209: 
Edouard@2672: def newExtensionSetting(display, token):
Edouard@2670:     global extensions_settings_od
Edouard@2670:     settings = ConfigurableBindings()
Edouard@2672:     extensions_settings_od[token] = (settings, display)
Edouard@2670:     return settings
Edouard@2670: 
Edouard@2672: def removeExtensionSetting(token):
Edouard@2670:     global extensions_settings_od
Edouard@2672:     extensions_settings_od.pop(token)
Edouard@2246: 
Edouard@2208: class ISettings(annotate.TypedInterface):
Edouard@2246:     platform = annotate.String(label=_("Platform"),
Edouard@2247:                                default=platform_module.system() +
Edouard@2247:                                " " + platform_module.release(),
Edouard@2247:                                immutable=True)
Edouard@2247: 
Edouard@2217:     # TODO version ?
Edouard@2217: 
Edouard@2247:     # pylint: disable=no-self-argument
Edouard@2210:     def sendLogMessage(
Edouard@2247:             ctx=annotate.Context(),
Edouard@2247:             level=annotate.Choice(LogLevels,
Edouard@2247:                                   required=True,
Edouard@2247:                                   label=_("Log message level")),
Edouard@2246:             message=annotate.String(label=_("Message text"))):
Edouard@2247:         pass
Edouard@2247: 
Edouard@2246:     sendLogMessage = annotate.autocallable(sendLogMessage,
Edouard@2246:                                            label=_(
Edouard@2246:                                                "Send a message to the log"),
Edouard@2210:                                            action=_("Send"))
Edouard@2208: 
edouard@2700:     # pylint: disable=no-self-argument
Edouard@2701:     def restartOrRepairPLC(
edouard@2700:             ctx=annotate.Context(),
Edouard@2701:             action=annotate.Choice(["Restart", "Repair"],
edouard@2700:                                   required=True,
edouard@2700:                                   label=_("Action"))):
edouard@2700:         pass
edouard@2700: 
Edouard@2701:     restartOrRepairPLC = annotate.autocallable(restartOrRepairPLC,
edouard@2700:                                            label=_(
Edouard@2701:                                                "Restart or Repair"),
edouard@2700:                                            action=_("Do"))
Edouard@2260: 
Edouard@3703:     # pylint: disable=no-self-argument
Edouard@3703:     def uploadFile(
Edouard@3703:             ctx=annotate.Context(),
Edouard@3703:             uploadedfile=annotate.FileUpload(required=True,
Edouard@3703:                                   label=_("File to upload"))):
Edouard@3703:         pass
Edouard@3703: 
Edouard@3703:     uploadFile = annotate.autocallable(uploadFile,
Edouard@3703:                                            label=_(
Edouard@3703:                                                "Upload a file to PLC working directory"),
Edouard@3703:                                            action=_("Upload"))
Edouard@3703: 
Edouard@2219: customSettingsURLs = {
Edouard@2219: }
Edouard@2208: 
Edouard@2670: extensions_settings_od = collections.OrderedDict()
Edouard@2246: 
Edouard@2208: class SettingsPage(rend.Page):
Edouard@2208:     # We deserve a slash
Edouard@2208:     addSlash = True
Edouard@2246: 
Edouard@2208:     # This makes webform_css url answer some default CSS
Edouard@2208:     child_webform_css = webform.defaultCSS
denis@2266:     child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css')
Edouard@2208: 
Edouard@2208:     implements(ISettings)
Edouard@2670:    
Edouard@2670:     def __getattr__(self, name):
Edouard@2670:         global extensions_settings_od
Edouard@2670:         if name.startswith('configurable_'):
Edouard@2672:             token = name[13:]
Edouard@2670:             def configurable_something(ctx):
Edouard@2672:                 settings, _display = extensions_settings_od[token]
Edouard@2672:                 return settings
Edouard@2670:             return configurable_something
Edouard@2670:         raise AttributeError
Edouard@2670:     
Edouard@2670:     def extensions_settings(self, context, data):
Edouard@2670:         """ Project extensions settings
Edouard@2670:         Extensions added to Configuration Tree in IDE have their setting rendered here
Edouard@2670:         """
Edouard@2670:         global extensions_settings_od
Edouard@2670:         res = []
Edouard@2672:         for token in extensions_settings_od:
Edouard@2672:             _settings, display = extensions_settings_od[token]
Edouard@2672:             res += [tags.h2[display], webform.renderForms(token)] 
Edouard@2670:         return res
Edouard@2208: 
Edouard@2208:     docFactory = loaders.stan([tags.html[
Edouard@2267:         tags.head[
Edouard@2267:             tags.title[_("Beremiz Runtime Settings")],
Edouard@2267:             tags.link(rel='stylesheet',
Edouard@2267:                       type='text/css',
Edouard@2267:                       href=url.here.child("webform_css")),
Edouard@2267:             tags.link(rel='stylesheet',
Edouard@2267:                       type='text/css',
Edouard@2267:                       href=url.here.child("webinterface_css"))
Edouard@2267:         ],
Edouard@2267:         tags.body[
Edouard@2267:             tags.a(href='/')['Back'],
Edouard@2267:             tags.h1["Runtime settings:"],
Edouard@2267:             webform.renderForms('staticSettings'),
Edouard@2267:             tags.h1["Extensions settings:"],
Edouard@2267:             webform.renderForms('dynamicSettings'),
Edouard@2670:             extensions_settings
Edouard@2267:         ]]])
Edouard@2208: 
Edouard@2209:     def configurable_staticSettings(self, ctx):
Edouard@2209:         return configurable.TypedInterfaceConfigurable(self)
Edouard@2209: 
Edouard@2209:     def configurable_dynamicSettings(self, ctx):
Edouard@2670:         """ Runtime Extensions settings
Edouard@2670:         Extensions loaded through Beremiz_service -e or optional runtime features render setting forms here
Edouard@2670:         """
Edouard@2209:         return ConfigurableSettings
Edouard@2246: 
Edouard@2210:     def sendLogMessage(self, level, message, **kwargs):
Edouard@2210:         level = LogLevelsDict[level]
edouard@2700:         GetPLCObjectSingleton().LogMessage(
edouard@2700:             level, "Web form log message: " + message)
edouard@2700: 
Edouard@2701:     def restartOrRepairPLC(self, action, **kwargs):
Edouard@2701:         if(action == "Repair"):
edouard@2700:             GetPLCObjectSingleton().RepairPLC()
edouard@2700:         else:
edouard@2700:             MainWorker.quit()
Edouard@3703: 
Edouard@3703:     def uploadFile(self, uploadedfile, **kwargs):
Edouard@3703:         if uploadedfile is not None:
Edouard@3703:             fobj = getattr(uploadedfile, "file", None)
Edouard@3703:         if fobj is not None:
Edouard@3703:             with open(uploadedfile.filename, 'w') as destfd:
Edouard@3703:                 fobj.seek(0)
Edouard@3703:                 shutil.copyfileobj(fobj,destfd)
Edouard@2208: 
Edouard@2219:     def locateChild(self, ctx, segments):
Edouard@2246:         if segments[0] in customSettingsURLs:
Edouard@2219:             return customSettingsURLs[segments[0]](ctx, segments)
Edouard@2219:         return super(SettingsPage, self).locateChild(ctx, segments)
Edouard@2219: 
andrej@1736: 
Edouard@1438: class WebInterface(athena.LivePage):
Edouard@1438: 
Edouard@1438:     docFactory = loaders.stan([tags.raw(xhtml_header),
andrej@1767:                                tags.html(xmlns="http://www.w3.org/1999/xhtml")[
Edouard@2208:                                    tags.head(render=tags.directive('liveglue'))[
Edouard@2208:                                        tags.title[PAGE_TITLE],
Edouard@2208:                                        tags.link(rel='stylesheet',
Edouard@2246:                                                  type='text/css',
Edouard@2208:                                                  href=url.here.child("webform_css"))
Edouard@2208:                                    ],
andrej@1767:                                    tags.body[
andrej@1767:                                        tags.div[
Edouard@2246:                                            tags.div(
Edouard@2246:                                                render=tags.directive(
Edouard@2246:                                                    "MainPage")),
andrej@1767:                                        ]]]])
Edouard@1438:     MainPage = MainPage()
Edouard@1438:     PLCHMI = PLCHMI
Edouard@1438: 
Edouard@2208:     def child_settings(self, context):
Edouard@2208:         return SettingsPage()
Edouard@2208: 
Edouard@1438:     def __init__(self, plcState=False, *a, **kw):
Edouard@1438:         super(WebInterface, self).__init__(*a, **kw)
Edouard@2246:         self.jsModules.mapping[u'WebInterface'] = paths.AbsNeighbourFile(
Edouard@2246:             __file__, 'webinterface.js')
Edouard@1438:         self.plcState = plcState
Edouard@1438:         self.MainPage.setPLCState(plcState)
Edouard@1438: 
Edouard@1438:     def getHMI(self):
Edouard@1438:         return self.MainPage.getHMI()
Edouard@1438: 
Edouard@1438:     def LoadHMI(self, hmi, jsmodules):
Edouard@1438:         for name, path in jsmodules.iteritems():
Edouard@1438:             self.jsModules.mapping[name] = os.path.join(WorkingDir, path)
Edouard@1438:         self.MainPage.setPLCStartedHMI(hmi)
Edouard@1438: 
Edouard@1438:     def UnLoadHMI(self):
Edouard@1438:         self.MainPage.resetPLCStartedHMI()
Edouard@1438: 
Edouard@1438:     def PLCStarted(self):
Edouard@1438:         self.plcState = True
Edouard@1438:         self.MainPage.setPLCState(True)
Edouard@1438: 
Edouard@1438:     def PLCStopped(self):
Edouard@1438:         self.plcState = False
Edouard@1438:         self.MainPage.setPLCState(False)
Edouard@1438: 
Edouard@1438:     def renderHTTP(self, ctx):
Edouard@1438:         """
Edouard@1438:         Force content type to fit with SVG
Edouard@1438:         """
andrej@1870:         req = ctx.locate(inevow.IRequest)
Edouard@1438:         req.setHeader('Content-type', 'application/xhtml+xml')
Edouard@1438:         return super(WebInterface, self).renderHTTP(ctx)
Edouard@1438: 
Edouard@1438:     def render_MainPage(self, ctx, data):
Edouard@1438:         f = self.MainPage
Edouard@1438:         f.setFragmentParent(self)
Edouard@1438:         return ctx.tag[f]
Edouard@1438: 
Edouard@1438:     def child_(self, ctx):
Edouard@1438:         self.MainPage.detachFragmentChildren()
Edouard@1438:         return WebInterface(plcState=self.plcState)
Edouard@1438: 
Edouard@1438:     def beforeRender(self, ctx):
Edouard@1438:         d = self.notifyOnDisconnect()
Edouard@1438:         d.addErrback(self.disconnected)
Edouard@1438: 
Edouard@1438:     def disconnected(self, reason):
Edouard@1438:         self.MainPage.resetHMI()
andrej@1782:         # print reason
andrej@1782:         # print "We will be called back when the client disconnects"
Edouard@1438: 
andrej@1736: 
Edouard@2311: def RegisterWebsite(iface, port):
Edouard@1438:     website = WebInterface()
Edouard@1438:     site = appserver.NevowSite(website)
Edouard@1438: 
Edouard@2311:     reactor.listenTCP(port, site, interface=iface)
andrej@1826:     print(_('HTTP interface port :'), port)
Edouard@1439:     return website
Edouard@1438: 
Edouard@2210: 
andrej@1831: class statuslistener(object):
Edouard@2246: 
Edouard@1438:     def __init__(self, site):
Edouard@1438:         self.oldstate = None
Edouard@1438:         self.site = site
Edouard@1438: 
Edouard@1438:     def listen(self, state):
Edouard@1438:         if state != self.oldstate:
Edouard@1453:             action = {'Started': self.site.PLCStarted,
Edouard@1453:                       'Stopped': self.site.PLCStopped}.get(state, None)
andrej@1756:             if action is not None:
andrej@1756:                 action()
Edouard@1438:             self.oldstate = state
Edouard@1438: 
andrej@1736: 
Edouard@1438: def website_statuslistener_factory(site):
Edouard@1438:     return statuslistener(site).listen
Edouard@2208: 
Edouard@2208: