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@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 = ''' Edouard@1438: Edouard@1438: ''' Edouard@1438: Edouard@1453: WorkingDir = None Edouard@2210: _PySrv = 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@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@2700: edouard@2700: 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: Edouard@2210: def SetServer(pysrv): Edouard@2210: global _PySrv Edouard@2210: _PySrv = pysrv