# HG changeset patch # User Edouard Tisserant # Date 1536785970 -7200 # Node ID 56f1d8aca88642b2cf62f6a101460e550a3f0faa # Parent a0efe3d9c853011fa148b5629ae825d319fa4489# Parent 68e6649039a55f75b0f86eecb91f86f3ae1a68d3 Merged in Andrey's branch diff -r 68e6649039a5 -r 56f1d8aca886 Beremiz_service.py --- a/Beremiz_service.py Fri Sep 07 20:58:13 2018 +0000 +++ b/Beremiz_service.py Wed Sep 12 22:59:30 2018 +0200 @@ -31,13 +31,12 @@ import getopt import threading from threading import Thread, Semaphore, Lock -import traceback import __builtin__ -import Pyro -import Pyro.core as pyro - -from runtime import PLCObject, ServicePublisher, MainWorker + +import runtime +from runtime.PyroServer import Server from runtime.xenomai import TryPreloadXenomai +from runtime import LogMessageAndException import util.paths as paths @@ -252,12 +251,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) @@ -280,15 +278,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 @@ -307,19 +304,10 @@ return icon def OnTaskBarStartPLC(self, evt): - if self.pyroserver.plcobj is not None: - plcstatus = self.pyroserver.plcobj.GetPLCstatus()[0] - if plcstatus is "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] == "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 @@ -355,10 +343,7 @@ 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 @@ -401,90 +386,6 @@ res = (None, sys.exc_info()) 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] == "Stopped": - if autostart: - self.plcobj.StartPLC() - self.plcobj.StatusChange() - - if enabletwisted: import warnings with warnings.catch_warnings(): @@ -532,31 +433,11 @@ 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) 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) @@ -606,6 +487,15 @@ sys.path.append(extension_folder) execfile(os.path.join(extension_folder, extention_file), locals()) + +runtime.CreatePLCObjectSingleton( + WorkingDir, argv, statuschange, evaluator, pyruntimevars) + +pyroserver = Server(servicename, given_ip, port) + +if havewx: + taskbar_instance = BeremizTaskBarIcon(pyroserver) + if havetwisted: if webport is not None: try: @@ -635,6 +525,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() @@ -661,9 +556,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) diff -r 68e6649039a5 -r 56f1d8aca886 ProjectController.py --- a/ProjectController.py Fri Sep 07 20:58:13 2018 +0000 +++ b/ProjectController.py Wed Sep 12 22:59:30 2018 +0200 @@ -267,7 +267,7 @@ 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] def __del__(self): diff -r 68e6649039a5 -r 56f1d8aca886 bacnet/runtime/device.c --- a/bacnet/runtime/device.c Fri Sep 07 20:58:13 2018 +0000 +++ b/bacnet/runtime/device.c Wed Sep 12 22:59:30 2018 +0200 @@ -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: diff -r 68e6649039a5 -r 56f1d8aca886 bacnet/runtime/server.c --- a/bacnet/runtime/server.c Fri Sep 07 20:58:13 2018 +0000 +++ b/bacnet/runtime/server.c Wed Sep 12 22:59:30 2018 +0200 @@ -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); diff -r 68e6649039a5 -r 56f1d8aca886 py_ext/plc_python.c --- a/py_ext/plc_python.c Fri Sep 07 20:58:13 2018 +0000 +++ b/py_ext/plc_python.c Wed Sep 12 22:59:30 2018 +0200 @@ -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); diff -r 68e6649039a5 -r 56f1d8aca886 runtime/NevowServer.py --- a/runtime/NevowServer.py Fri Sep 07 20:58:13 2018 +0000 +++ b/runtime/NevowServer.py Wed Sep 12 22:59:30 2018 +0200 @@ -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) diff -r 68e6649039a5 -r 56f1d8aca886 runtime/PLCObject.py --- a/runtime/PLCObject.py Fri Sep 07 20:58:13 2018 +0000 +++ b/runtime/PLCObject.py Wed Sep 12 22:59:30 2018 +0200 @@ -23,7 +23,6 @@ from __future__ import absolute_import -import thread from threading import Thread, Lock, Semaphore, Event, Condition import ctypes import os @@ -31,10 +30,10 @@ import traceback from time import time import _ctypes # pylint: disable=wrong-import-order -import Pyro.core as pyro from runtime.typemapping import TypeTranslator from runtime.loglevels import LogLevelsDefault, LogLevelsCount +from runtime import MainWorker if os.name in ("nt", "ce"): dlopen = _ctypes.LoadLibrary @@ -61,107 +60,6 @@ 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: - raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2] - - 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): def func_wrapper(*args, **kwargs): @@ -169,22 +67,19 @@ 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 + # 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 = "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 @@ -192,7 +87,7 @@ self.Traces = [] # First task of worker -> no @RunInMain - def AutoLoad(self): + def AutoLoad(self, autostart): # Get the last transfered PLC try: self.CurrentPLCFilename = open( @@ -200,10 +95,15 @@ "r").read().strip() + lib_ext if self.LoadPLC(): self.PLCStatus = "Stopped" + if autostart: + self.StartPLC() + return except Exception: self.PLCStatus = "Empty" self.CurrentPLCFilename = None + self.StatusChange() + def StatusChange(self): if self.statuschange is not None: for callee in self.statuschange: diff -r 68e6649039a5 -r 56f1d8aca886 runtime/PyroServer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/PyroServer.py Wed Sep 12 22:59:30 2018 +0200 @@ -0,0 +1,80 @@ +#!/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. + +import sys + +import Pyro +import Pyro.core as pyro +import runtime +from runtime.ServicePublisher import ServicePublisher + +class Server(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 is not None and \ + self.ip_addr != "localhost" and \ + self.ip_addr != "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): + 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") + + if self._to_be_published(): + self.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 + self._stop() + + def _stop(self): + if self.servicepublisher is not None: + self.servicepublisher.UnRegisterService() + self.servicepublisher = None + self.daemon.shutdown(True) + + + diff -r 68e6649039a5 -r 56f1d8aca886 runtime/WampClient.py --- a/runtime/WampClient.py Fri Sep 07 20:58:13 2018 +0000 +++ b/runtime/WampClient.py Wed Sep 12 22:59:30 2018 +0200 @@ -427,7 +427,8 @@ def RegisterWebSettings(NS): - NS.ConfigurableSettings.addExtension( + + NS.ConfigurableSettings.addSettings( "wamp", _("Wamp Settings"), webFormInterface, diff -r 68e6649039a5 -r 56f1d8aca886 runtime/Worker.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/Worker.py Wed Sep 12 22:59:30 2018 +0200 @@ -0,0 +1,110 @@ +#!/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 thread +from threading import Lock, Condition + +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(): + # 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: + raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2] + + 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() diff -r 68e6649039a5 -r 56f1d8aca886 runtime/__init__.py --- a/runtime/__init__.py Fri Sep 07 20:58:13 2018 +0000 +++ b/runtime/__init__.py Wed Sep 12 22:59:30 2018 +0200 @@ -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 +import traceback -# 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 +from runtime.PLCObject import PLCObject -from __future__ import absolute_import -import os +_PLCObjectSingleton = None -from runtime.PLCObject import PLCObject, PLCprint, MainWorker -import runtime.ServicePublisher +def GetPLCObjectSingleton(): + global _PLCObjectSingleton + assert(_PLCObjectSingleton is not None) + return _PLCObjectSingleton + + +def LogMessageAndException(msg, exp=None): + global _PLCObjectSingleton + if exp is None: + exp = sys.exc_info() + if _PLCObjectSingleton is not None: + _PLCObjectSingleton.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp))) + else: + print(msg) + traceback.print_exception(*exp) + +def CreatePLCObjectSingleton(*args): + global _PLCObjectSingleton + _PLCObjectSingleton = PLCObject(*args) diff -r 68e6649039a5 -r 56f1d8aca886 runtime/webinterface.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/webinterface.css Wed Sep 12 22:59:30 2018 +0200 @@ -0,0 +1,6 @@ + +.freeform-label { + float: left; + width: 30%; +} + diff -r 68e6649039a5 -r 56f1d8aca886 runtime_files.list --- a/runtime_files.list Fri Sep 07 20:58:13 2018 +0000 +++ b/runtime_files.list Wed Sep 12 22:59:30 2018 +0200 @@ -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