--- a/connectors/ConnectorBase.py Sat Nov 25 00:18:05 2023 +0100
+++ b/connectors/ConnectorBase.py Thu Dec 07 22:41:32 2023 +0100
@@ -5,12 +5,21 @@
import hashlib
+from runtime import PlcStatus
class ConnectorBase(object):
chuncksize = 1024*1024
+ PLCObjDefaults = {
+ "StartPLC": False,
+ "GetTraceVariables": (PlcStatus.Broken, None),
+ "GetPLCstatus": (PlcStatus.Broken, None),
+ "RemoteExec": (-1, "RemoteExec script failed!"),
+ "GetVersions": "*** Unknown ***"
+ }
+
def BlobFromFile(self, filepath, seed):
s = hashlib.new('md5')
s.update(seed.encode())
--- a/connectors/PYRO/__init__.py Sat Nov 25 00:18:05 2023 +0100
+++ b/connectors/PYRO/__init__.py Thu Dec 07 22:41:32 2023 +0100
@@ -34,7 +34,6 @@
# TODO: PSK
-from runtime import PlcStatus
import importlib
@@ -64,6 +63,9 @@
RemotePLCObjectProxy._pyroTimeout = 60
+ class MissingCallException(Exception):
+ pass
+
def PyroCatcher(func, default=None):
"""
A function that catch a Pyro exceptions, write error to logger
@@ -77,6 +79,8 @@
confnodesroot.logger.write_error(_("Connection lost!\n"))
except Pyro5.errors.ProtocolError as e:
confnodesroot.logger.write_error(_("Pyro exception: %s\n") % e)
+ except MissingCallException as e:
+ confnodesroot.logger.write_warning(_("Remote call not supported: %s\n") % e.message)
except Exception as e:
errmess = ''.join(Pyro5.errors.get_pyro_traceback())
confnodesroot.logger.write_error(errmess + "\n")
@@ -94,13 +98,6 @@
ID, secret = IDPSK
PSK.UpdateID(confnodesroot.ProjectPath, ID, secret, uri)
- _special_return_funcs = {
- "StartPLC": False,
- "GetTraceVariables": (PlcStatus.Broken, None),
- "GetPLCstatus": (PlcStatus.Broken, None),
- "RemoteExec": (-1, "RemoteExec script failed!")
- }
-
class PyroProxyProxy(object):
"""
A proxy proxy class to handle Beremiz Pyro interface specific behavior.
@@ -110,8 +107,12 @@
member = self.__dict__.get(attrName, None)
if member is None:
def my_local_func(*args, **kwargs):
- return RemotePLCObjectProxy.__getattr__(attrName)(*args, **kwargs)
- member = PyroCatcher(my_local_func, _special_return_funcs.get(attrName, None))
+ call = RemotePLCObjectProxy.__getattr__(attrName)
+ if call is None:
+ raise MissingCallException(attrName)
+ else:
+ return call(*args, **kwargs)
+ member = PyroCatcher(my_local_func, self.PLCObjDefaults.get(attrName, None))
self.__dict__[attrName] = member
return member
--- a/connectors/WAMP/__init__.py Sat Nov 25 00:18:05 2023 +0100
+++ b/connectors/WAMP/__init__.py Thu Dec 07 22:41:32 2023 +0100
@@ -35,7 +35,6 @@
from autobahn.wamp.exception import TransportLost
from autobahn.wamp.serializer import MsgPackSerializer
-from runtime import PlcStatus
_WampSession = None
_WampConnection = None
@@ -56,14 +55,6 @@
print('WAMP session left')
-PLCObjDefaults = {
- "StartPLC": False,
- "GetTraceVariables": ("Broken", None),
- "GetPLCstatus": (PlcStatus.Broken, None),
- "RemoteExec": (-1, "RemoteExec script failed!")
-}
-
-
def _WAMP_connector_factory(cls, uri, confnodesroot):
"""
WAMP://127.0.0.1:12345/path#realm#ID
@@ -107,26 +98,6 @@
AddToDoBeforeQuit(reactor.stop)
reactor.run(installSignalHandlers=False)
- def WampSessionProcMapper(funcname):
- wampfuncname = str('.'.join((ID, funcname)))
-
- def catcher_func(*args, **kwargs):
- if _WampSession is not None:
- try:
- return threads.blockingCallFromThread(
- reactor, _WampSession.call, wampfuncname,
- *args, **kwargs)
- except TransportLost:
- confnodesroot.logger.write_error(_("Connection lost!\n"))
- confnodesroot._SetConnector(None)
- except Exception:
- errmess = traceback.format_exc()
- confnodesroot.logger.write_error(errmess+"\n")
- print(errmess)
- # confnodesroot._SetConnector(None)
- return PLCObjDefaults.get(funcname)
- return catcher_func
-
class WampPLCObjectProxy(object):
def __init__(self):
global _WampConnection
@@ -144,10 +115,30 @@
#
# reactor.stop()
+ def WampSessionProcMapper(self, funcname):
+ wampfuncname = str('.'.join((ID, funcname)))
+
+ def catcher_func(*args, **kwargs):
+ if _WampSession is not None:
+ try:
+ return threads.blockingCallFromThread(
+ reactor, _WampSession.call, wampfuncname,
+ *args, **kwargs)
+ except TransportLost:
+ confnodesroot.logger.write_error(_("Connection lost!\n"))
+ confnodesroot._SetConnector(None)
+ except Exception:
+ errmess = traceback.format_exc()
+ confnodesroot.logger.write_error(errmess+"\n")
+ print(errmess)
+ # confnodesroot._SetConnector(None)
+ return self.PLCObjDefaults.get(funcname)
+ return catcher_func
+
def __getattr__(self, attrName):
member = self.__dict__.get(attrName, None)
if member is None:
- member = WampSessionProcMapper(attrName)
+ member = self.WampSessionProcMapper(attrName)
self.__dict__[attrName] = member
return member
--- a/modbus/mb_runtime.c Sat Nov 25 00:18:05 2023 +0100
+++ b/modbus/mb_runtime.c Thu Dec 07 22:41:32 2023 +0100
@@ -690,7 +690,7 @@
res |= pthread_attr_init(&attr);
res |= pthread_create(&(client_nodes[index].timer_thread_id), &attr, &__mb_client_timer_thread, (void *)((char *)NULL + index));
if (res != 0) {
- fprintf(stderr, "Modbus plugin: Error starting timer thread for modbus client node %%s\n", client_nodes[index].location);
+ fprintf(stderr, "Modbus plugin: Error (%%d) starting timer thread for modbus client node %%s\n", res, client_nodes[index].location);
goto error_exit;
}
}
@@ -703,7 +703,7 @@
res |= pthread_attr_init(&attr);
res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
if (res != 0) {
- fprintf(stderr, "Modbus plugin: Error starting thread for modbus client node %%s\n", client_nodes[index].location);
+ fprintf(stderr, "Modbus plugin: Error (%%d) starting thread for modbus client node %%s\n", res, client_nodes[index].location);
goto error_exit;
}
}
@@ -730,7 +730,7 @@
res |= pthread_attr_init(&attr);
res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index]));
if (res != 0) {
- fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location);
+ fprintf(stderr, "Modbus plugin: Error (%%d) starting modbus server thread for node %%s\n", res, server_nodes[index].location);
goto error_exit;
}
}
--- a/runtime/NevowServer.py Sat Nov 25 00:18:05 2023 +0100
+++ b/runtime/NevowServer.py Thu Dec 07 22:41:32 2023 +0100
@@ -28,7 +28,6 @@
import os
import collections
import shutil
-import platform as platform_module
from zope.interface import implementer
from nevow import appserver, inevow, tags, loaders, athena, url, rend
from nevow.page import renderer
@@ -99,6 +98,17 @@
self.bindingsNames.append(name)
+ customSettingsURLs = {}
+ def addCustomURL(self, segment, func):
+ self.customSettingsURLs[segment] = func
+
+ def removeCustomURL(self, segment):
+ del self.customSettingsURLs[segment]
+
+ def customLocateChild(self, ctx, segments):
+ segment = segments[0]
+ if segment in self.customSettingsURLs:
+ return self.customSettingsURLs[segment](ctx, segments)
ConfigurableSettings = ConfigurableBindings()
@@ -112,13 +122,12 @@
global extensions_settings_od
extensions_settings_od.pop(token)
+
class ISettings(annotate.TypedInterface):
platform = annotate.String(label=_("Platform"),
- default=platform_module.system() +
- " " + platform_module.release(),
+ default=lambda *a,**k:GetPLCObjectSingleton().GetVersions(),
immutable=True)
- # TODO version ?
# pylint: disable=no-self-argument
def sendLogMessage(
@@ -159,30 +168,26 @@
"Upload a file to PLC working directory"),
action=_("Upload"))
-customSettingsURLs = {
-}
-
extensions_settings_od = collections.OrderedDict()
+
+CSS_tags = [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"))]
+
@implementer(ISettings)
-class SettingsPage(rend.Page):
- # We deserve a slash
+class StyledSettingsPage(rend.Page):
addSlash = True
# 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')
+
+class SettingsPage(StyledSettingsPage):
- def __getattr__(self, name):
- global extensions_settings_od
- if name.startswith('configurable_'):
- token = name[13:]
- def configurable_something(ctx):
- settings, _display = extensions_settings_od[token]
- return settings
- return configurable_something
- raise AttributeError
-
def extensions_settings(self, context, data):
""" Project extensions settings
Extensions added to Configuration Tree in IDE have their setting rendered here
@@ -191,25 +196,22 @@
res = []
for token in extensions_settings_od:
_settings, display = extensions_settings_od[token]
- res += [tags.h2[display], webform.renderForms(token)]
+ res += [tags.p[tags.a(href=token)[display]]]
return res
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"))
+ CSS_tags
],
tags.body[
+ tags.h1["Settings"],
tags.a(href='/')['Back'],
- tags.h1["Runtime settings:"],
+ tags.h2["Runtime service"],
webform.renderForms('staticSettings'),
- tags.h1["Extensions settings:"],
+ tags.h2["Target specific"],
webform.renderForms('dynamicSettings'),
+ tags.h2["Extensions"],
extensions_settings
]]])
@@ -242,10 +244,47 @@
shutil.copyfileobj(fobj,destfd)
def locateChild(self, ctx, segments):
- if segments[0] in customSettingsURLs:
- return customSettingsURLs[segments[0]](ctx, segments)
+ segment = segments[0]
+ if segment in extensions_settings_od:
+ settings, display = extensions_settings_od[segment]
+ return ExtensionSettingsPage(settings, display), segments[1:]
+ else:
+ res = ConfigurableSettings.customLocateChild(ctx, segments)
+ if res:
+ return res
return super(SettingsPage, self).locateChild(ctx, segments)
+class ExtensionSettingsPage(StyledSettingsPage):
+
+ docFactory = loaders.stan([
+ tags.html[
+ tags.head()[
+ tags.title[tags.directive("title")],
+ CSS_tags
+ ],
+ tags.body[
+ tags.h1[tags.directive("title")],
+ tags.a(href='/settings')['Back'],
+ webform.renderForms('settings')
+ ]]])
+
+ def render_title(self, ctx, data):
+ return self._display_name
+
+ def configurable_settings(self, ctx):
+ return self._settings
+
+ def __init__(self, settings, display):
+ self._settings = settings
+ self._display_name = display
+
+ def locateChild(self, ctx, segments):
+ res = self._settings.customLocateChild(ctx, segments)
+ if res:
+ return res
+ return super(ExtensionSettingsPage, self).locateChild(ctx, segments)
+
+
def RegisterWebsite(iface, port):
website = SettingsPage()
site = appserver.NevowSite(website)
--- a/runtime/PLCObject.py Sat Nov 25 00:18:05 2023 +0100
+++ b/runtime/PLCObject.py Thu Dec 07 22:41:32 2023 +0100
@@ -28,6 +28,7 @@
import sys
import traceback
import shutil
+import platform as platform_module
from time import time
import hashlib
from tempfile import mkstemp
@@ -811,3 +812,7 @@
return (-1, "RemoteExec script failed!\n\nLine %d: %s\n\t%s" %
(line_no, e_value, script.splitlines()[line_no - 1]))
return (0, kwargs.get("returnVal", None))
+
+ def GetVersions(self):
+ return platform_module.system() + " " + platform_module.release()
+
--- a/runtime/WampClient.py Sat Nov 25 00:18:05 2023 +0100
+++ b/runtime/WampClient.py Thu Dec 07 22:41:32 2023 +0100
@@ -490,11 +490,13 @@
def RegisterWebSettings(NS):
- NS.ConfigurableSettings.addSettings(
+ WebSettings = NS.newExtensionSetting("Wamp Extension Settings", "wamp_settings")
+ WebSettings.addSettings(
"wamp",
_("Wamp Settings"),
webFormInterface,
_("Set"),
wampConfig)
- NS.customSettingsURLs[WAMP_SECRET_URL] = deliverWampSecret
+ WebSettings.addCustomURL(WAMP_SECRET_URL, deliverWampSecret)
+
--- a/runtime/spawn_subprocess.py Sat Nov 25 00:18:05 2023 +0100
+++ b/runtime/spawn_subprocess.py Thu Dec 07 22:41:32 2023 +0100
@@ -15,17 +15,24 @@
fsencoding = sys.getfilesystemencoding()
class Popen(object):
- def __init__(self, args, stdin=None, stdout=None):
+ def __init__(self, args, stdin=None, stdout=None, stderr=None):
self.returncode = None
self.stdout = None
+ self.stderr = None
self.stdin = None
- # TODO: stderr
file_actions = posix_spawn.FileActions()
if stdout is not None:
# child's stdout, child 2 parent pipe
+ c1pread, c1pwrite = os.pipe()
+ # attach child's stdout to writing en of c1p pipe
+ file_actions.add_dup2(c1pwrite, 1)
+ # close other end
+ file_actions.add_close(c1pread)
+ if stderr is not None:
+ # child's stderr, child 2 parent pipe
c2pread, c2pwrite = os.pipe()
- # attach child's stdout to writing en of c2p pipe
- file_actions.add_dup2(c2pwrite, 1)
+ # attach child's stderr to writing en of c2p pipe
+ file_actions.add_dup2(c2pwrite, 2)
# close other end
file_actions.add_close(c2pread)
if stdin is not None:
@@ -38,7 +45,10 @@
args = [s.encode(fsencoding) for s in args if type(s)==str]
self.pid = posix_spawn.posix_spawnp(args[0], args, file_actions=file_actions)
if stdout is not None:
- self.stdout = os.fdopen(c2pread)
+ self.stdout = os.fdopen(c1pread)
+ os.close(c1pwrite)
+ if stderr is not None:
+ self.stderr = os.fdopen(c2pread)
os.close(c2pwrite)
if stdin is not None:
self.stdin = os.fdopen(p2cwrite, 'w')
@@ -52,29 +62,44 @@
if self.stdin is not None:
self.stdin.close()
self.stdin = None
+
if self.stdout is not None:
stdoutdata = self.stdout.read()
else:
stdoutdata = ""
- # TODO
- stderrdata = ""
+ if self.stderr is not None:
+ stderrdata = self.stderr.read()
+ else:
+ stderrdata = ""
self._wait()
+
if self.stdout is not None:
self.stdout.close()
self.stdout = None
+ if self.stderr is not None:
+ self.stderr.close()
+ self.stderr = None
+
return (stdoutdata, stderrdata)
def wait(self):
if self.stdin is not None:
self.stdin.close()
self.stdin = None
+
self._wait()
+
if self.stdout is not None:
self.stdout.close()
self.stdout = None
+
+ if self.stderr is not None:
+ self.stderr.close()
+ self.stderr = None
+
return self.returncode
def poll(self):
@@ -86,10 +111,15 @@
if self.stdin is not None:
self.stdin.close()
self.stdin = None
+
if self.stdout is not None:
self.stdout.close()
self.stdout = None
+ if self.stderr is not None:
+ self.stderr.close()
+ self.stderr = None
+
return self.returncode
def kill(self):
@@ -98,10 +128,15 @@
if self.stdin is not None:
self.stdin.close()
self.stdin = None
+
if self.stdout is not None:
self.stdout.close()
self.stdout = None
+ if self.stderr is not None:
+ self.stderr.close()
+ self.stderr = None
+
def call(*args):
cmd = []
--- a/runtime/webinterface.css Sat Nov 25 00:18:05 2023 +0100
+++ b/runtime/webinterface.css Thu Dec 07 22:41:32 2023 +0100
@@ -1,6 +1,52 @@
+body {
+ background-color: #f5faff;
+}
+
+* {
+ font-family: Tahoma, Verdana, sans-serif;
+}
+
+h1, h2 {
+ color: darkslateblue;
+}
+
+fieldset {
+ color: #424242;
+ border: 1px solid gray;
+ border-radius: 4px;
+}
+
+input, select, button, a {
+ font-size: 14px;
+}
+
+button,
+input,
+select,
+textarea {
+ color: inherit;
+}
+
+legend, .freeform-form-label {
+ color: darkslateblue;
+}
.freeform-label {
float: left;
width: 30%;
}
+a {
+ background-color: #e9e9ed;
+ color: #424242;
+ padding: 2px 6px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ border: 1px solid gray;
+ border-radius: 4px;
+}
+
+a:hover, a:active {
+ background-color: silver;
+}