Merged. Some changes that should already have been incuded during previous merge (mostly about PlcStatus) have been included this time.
--- a/BeremizIDE.py Fri Oct 12 13:24:47 2018 +0300
+++ b/BeremizIDE.py Fri Nov 23 12:13:24 2018 +0100
@@ -25,6 +25,7 @@
from __future__ import absolute_import
+from __future__ import print_function
import os
import sys
import tempfile
--- a/Beremiz_service.py Fri Oct 12 13:24:47 2018 +0300
+++ b/Beremiz_service.py Fri Nov 23 12:13:24 2018 +0100
@@ -31,15 +31,15 @@
import getopt
import threading
from threading import Thread, Semaphore, Lock
-import traceback
+import __builtin__
from builtins import str as text
from past.builtins import execfile
from six.moves import builtins
-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
from runtime import PlcStatus
import util.paths as paths
@@ -54,7 +54,7 @@
print("""
Usage of Beremiz PLC execution service :\n
%s {[-n servicename] [-i IP] [-p port] [-x enabletaskbar] [-a autostart]|-h|--help} working_dir
- -n zeroconf service name (default:disabled)
+ -n service name (default:None, zeroconf discovery disabled)
-i IP address of interface to bind to (default:localhost)
-p port number default:3000
-h print this help text and quit
@@ -63,7 +63,7 @@
-t enable/disable Twisted web interface (0:disable 1:enable) (default:1)
-w web server port or "off" to disable web server (default:8009)
-c WAMP client config file (can be overriden by wampconf.json in project)
- -s WAMP client secret, given as a file (can be overriden by wamp.secret in project)
+ -s PSK secret path (default:PSK disabled)
-e python extension (absolute path .py)
working_dir - directory where are stored PLC files
@@ -79,10 +79,10 @@
sys.exit(2)
# default values
-given_ip = None
+interface = ''
port = 3000
webport = 8009
-wampsecret = None
+PSKpath = None
wampconf = None
servicename = None
autostart = False
@@ -101,8 +101,10 @@
version()
sys.exit()
elif o == "-i":
- if len(a.split(".")) == 4 or a == "localhost":
- given_ip = a
+ if len(a.split(".")) == 4:
+ interface = a
+ elif a == "localhost":
+ interface = '127.0.0.1'
else:
usage()
sys.exit()
@@ -122,7 +124,7 @@
elif o == "-c":
wampconf = None if a == "off" else a
elif o == "-s":
- wampsecret = None if a == "off" else a
+ PSKpath = None if a == "off" else a
elif o == "-e":
fnameanddirname = list(os.path.split(os.path.realpath(a)))
fnameanddirname.reverse()
@@ -144,11 +146,10 @@
WorkingDir = os.getcwd()
argv = [WorkingDir]
-if __name__ == '__main__':
- builtins.__dict__['_'] = lambda x: x
- # TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem
- TryPreloadXenomai()
- version()
+builtins.__dict__['_'] = lambda x: x
+# TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem
+TryPreloadXenomai()
+version()
def Bpath(*args):
@@ -187,9 +188,8 @@
def unicode_translation(message):
return wx.GetTranslation(message).encode(default_locale)
- if __name__ == '__main__':
- builtins.__dict__['_'] = unicode_translation
- # builtins.__dict__['_'] = wx.GetTranslation
+ builtins.__dict__['_'] = unicode_translation
+ # builtins.__dict__['_'] = wx.GetTranslation
# Life is hard... have a candy.
@@ -256,12 +256,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)
@@ -284,15 +283,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
@@ -311,19 +309,10 @@
return icon
def OnTaskBarStartPLC(self, evt):
- if self.pyroserver.plcobj is not None:
- plcstatus = self.pyroserver.plcobj.GetPLCstatus()[0]
- if plcstatus is PlcStatus.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] == PlcStatus.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
@@ -350,19 +339,16 @@
self.pyroserver.Restart()
def OnTaskBarChangeName(self, evt):
- servicename = self.pyroserver.servicename
- servicename = '' if servicename is None else servicename
- dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=servicename)
+ _servicename = self.pyroserver.servicename
+ _servicename = '' if _servicename is None else _servicename
+ dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=_servicename)
dlg.SetTests([(lambda name: len(name) is not 0, _("Name must not be null!"))])
if dlg.ShowModal() == wx.ID_OK:
self.pyroserver.servicename = 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 {}
+ return {"locals": runtime.GetPLCObjectSingleton().python_runtime_vars}
def OnTaskBarLiveShell(self, evt):
from wx import py
@@ -406,89 +392,6 @@
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] == PlcStatus.Stopped:
- if autostart:
- self.plcobj.StartPLC()
- self.plcobj.StatusChange()
-
-
if enabletwisted:
import warnings
with warnings.catch_warnings():
@@ -536,31 +439,12 @@
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)
@@ -610,10 +494,25 @@
sys.path.append(extension_folder)
execfile(os.path.join(extension_folder, extention_file), locals())
+# Service name is used as an ID for stunnel's PSK
+# Some extension may set 'servicename' to a computed ID or Serial Number
+# instead of using commandline '-n'
+if servicename is not None and PSKpath is not None:
+ from runtime.Stunnel import ensurePSK
+ ensurePSK(servicename, PSKpath)
+
+runtime.CreatePLCObjectSingleton(
+ WorkingDir, argv, statuschange, evaluator, pyruntimevars)
+
+pyroserver = Server(servicename, interface, port)
+
+if havewx:
+ taskbar_instance = BeremizTaskBarIcon(pyroserver)
+
if havetwisted:
if webport is not None:
try:
- website = NS.RegisterWebsite(webport)
+ website = NS.RegisterWebsite(interface, webport)
pyruntimevars["website"] = website
NS.SetServer(pyroserver)
statuschange.append(NS.website_statuslistener_factory(website))
@@ -623,7 +522,7 @@
if havewamp:
try:
WC.SetServer(pyroserver)
- WC.RegisterWampClient(wampconf, wampsecret)
+ WC.RegisterWampClient(wampconf, PSKpath)
WC.RegisterWebSettings(NS)
except Exception:
LogMessageAndException(_("WAMP client startup failed. "))
@@ -639,6 +538,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()
@@ -665,9 +569,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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/PSKManagement.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import os
+import time
+import json
+from zipfile import ZipFile
+
+# PSK Management Data model :
+# [[ID,Desc, LastKnownURI, LastConnect]]
+COL_ID, COL_URI, COL_DESC, COL_LAST = range(4)
+REPLACE, REPLACE_ALL, KEEP, KEEP_ALL, CANCEL = range(5)
+
+def _pskpath(project_path):
+ return os.path.join(project_path, 'psk')
+
+def _mgtpath(project_path):
+ return os.path.join(_pskpath(project_path), 'management.json')
+
+def _default(ID):
+ return [ID,
+ '', # default description
+ None, # last known URI
+ None] # last connection date
+
+def _dataByID(data):
+ return {row[COL_ID]:row for row in data}
+
+def _LoadData(project_path):
+ """ load known keys metadata """
+ if os.path.isdir(_pskpath(project_path)):
+ _path = _mgtpath(project_path)
+ if os.path.exists(_path):
+ return json.loads(open(_path).read())
+ return []
+
+def _filterData(psk_files, data_input):
+ input_by_ID = _dataByID(data_input)
+ output = []
+ # go through all secret files available an build data
+ # out of data recoverd from json and list of secret.
+ # this implicitly filters IDs out of metadata who's
+ # secret is missing
+ for filename in psk_files:
+ if filename.endswith('.secret'):
+ ID = filename[:-7] # strip filename extension
+ output.append(input_by_ID.get(ID,_default(ID)))
+ return output
+
+def GetData(project_path):
+ loaded_data = _LoadData(project_path)
+ if loaded_data:
+ psk_files = os.listdir(_pskpath(project_path))
+ return _filterData(psk_files, loaded_data)
+ return []
+
+def DeleteID(project_path, ID):
+ secret_path = os.path.join(_pskpath(project_path), ID+'.secret')
+ os.remove(secret_path)
+
+def SaveData(project_path, data):
+ pskpath = _pskpath(project_path)
+ if not os.path.isdir(pskpath):
+ os.mkdir(pskpath)
+ with open(_mgtpath(project_path), 'w') as f:
+ f.write(json.dumps(data))
+
+
+def UpdateID(project_path, ID, secret, URI):
+ pskpath = _pskpath(project_path)
+ if not os.path.exists(pskpath):
+ os.mkdir(pskpath)
+
+ secpath = os.path.join(pskpath, ID+'.secret')
+ with open(secpath, 'w') as f:
+ f.write(ID+":"+secret)
+
+ # here we directly use _LoadData, avoiding filtering that could be long
+ data = _LoadData(project_path)
+ idata = _dataByID(data)
+ dataForID = idata.get(ID, _default(ID)) if data else _default(ID)
+ dataForID[COL_URI] = URI
+ # FIXME : could store time instead os a string and use DVC model's cmp
+ # then date display could be smarter, etc - sortable sting hack for now
+ dataForID[COL_LAST] = time.strftime('%y/%M/%d-%H:%M:%S')
+ SaveData(project_path, data)
+
+def ExportIDs(project_path, export_zip):
+ with ZipFile(export_zip, 'w') as zf:
+ path = _pskpath(project_path)
+ for nm in os.listdir(path):
+ if nm.endswith('.secret') or nm == 'management.json':
+ zf.write(os.path.join(path, nm), nm)
+
+def ImportIDs(project_path, import_zip, should_I_replace_callback):
+ zf = ZipFile(import_zip, 'r')
+ data = GetData(project_path)
+
+ zip_loaded_data = json.loads(zf.open('management.json').read())
+ name_list = zf.namelist()
+ zip_filtered_data = _filterData(name_list, zip_loaded_data)
+
+ idata = _dataByID(data)
+
+ keys_to_import = []
+ result = None
+
+ for imported_row in zip_filtered_data:
+ ID = imported_row[COL_ID]
+ existing_row = idata.get(ID, None)
+ if existing_row is None:
+ data.append(imported_row)
+ else:
+ # callback returns the selected list for merge or none if canceled
+ if result not in [REPLACE_ALL, KEEP_ALL]:
+ result = should_I_replace_callback(existing_row, imported_row)
+
+ if result == CANCEL:
+ return
+
+ if result in [REPLACE_ALL, REPLACE]:
+ # replace with imported
+ existing_row[:] = imported_row
+ # copy the key of selected
+ keys_to_import.append(ID)
+
+ for ID in keys_to_import:
+ zf.extract(ID+".secret", _pskpath(project_path))
+
+ SaveData(project_path, data)
+
+ return data
+
+
--- a/ProjectController.py Fri Oct 12 13:24:47 2018 +0300
+++ b/ProjectController.py Fri Nov 23 12:13:24 2018 +0100
@@ -55,7 +55,7 @@
from editors.ProjectNodeEditor import ProjectNodeEditor
from editors.IECCodeViewer import IECCodeViewer
from editors.DebugViewer import DebugViewer, REFRESH_PERIOD
-from dialogs import DiscoveryDialog
+from dialogs import UriEditor, IDManager
from PLCControler import PLCControler
from plcopen.structures import IEC_KEYWORDS
from plcopen.types_enums import ComputeConfigurationResourceName, ITEM_CONFNODE
@@ -268,7 +268,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):
@@ -1260,6 +1260,11 @@
_IECCodeView = None
+ def _showIDManager(self):
+ dlg = IDManager(self.AppFrame, self)
+ dlg.ShowModal()
+ dlg.Destroy()
+
def _showIECcode(self):
self._OpenView("IEC code")
@@ -1759,7 +1764,7 @@
if uri == "":
try:
# Launch Service Discovery dialog
- dialog = DiscoveryDialog(self.AppFrame)
+ dialog = UriEditor(self.AppFrame, self)
answer = dialog.ShowModal()
uri = dialog.GetURI()
dialog.Destroy()
@@ -1941,6 +1946,12 @@
"shown": False,
},
{
+ "bitmap": "IDManager",
+ "name": _("ID Manager"),
+ "tooltip": _("Manage secure connection identities"),
+ "method": "_showIDManager",
+ },
+ {
"bitmap": "ShowIECcode",
"name": _("Show code"),
"tooltip": _("Show IEC code generated by PLCGenerator"),
--- a/bacnet/runtime/device.c Fri Oct 12 13:24:47 2018 +0300
+++ b/bacnet/runtime/device.c Fri Nov 23 12:13:24 2018 +0100
@@ -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:
--- a/bacnet/runtime/server.c Fri Oct 12 13:24:47 2018 +0300
+++ b/bacnet/runtime/server.c Fri Nov 23 12:13:24 2018 +0100
@@ -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);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/PYRO/PSK_Adapter.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,85 @@
+from __future__ import absolute_import
+from __future__ import print_function
+
+import socket
+import re
+import sslpsk
+import ssl
+import Pyro
+from Pyro.core import PyroURI
+from Pyro.protocol import _connect_socket,TCPConnection,PYROAdapter
+from Pyro.errors import ConnectionDeniedError, ProtocolError
+from Pyro.util import Log
+
+#
+# The TLS-PSK adapter that handles SSL connections instead of regular sockets,
+# but using Pre Shared Keys instead of Certificates
+#
+class PYROPSKAdapter(PYROAdapter):
+ # This is essentialy the same as in Pyro/protocol.py
+ # only raw_sock wrapping into sock through sslpsk.wrap_socket was added
+ # Pyro unfortunately doesn't allow cleaner customization
+ def bindToURI(self,URI):
+ with self.lock: # only 1 thread at a time can bind the URI
+ try:
+ self.URI=URI
+
+ # This are the statements that differ from Pyro/protocol.py
+ raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ _connect_socket(raw_sock, URI.address, URI.port, self.timeout)
+ sock = sslpsk.wrap_socket(
+ raw_sock, psk=Pyro.config.PYROPSK, server_side=False,
+ ciphers="PSK-AES256-CBC-SHA", # available in openssl 1.0.2
+ ssl_version=ssl.PROTOCOL_TLSv1)
+ # all the rest is the same as in Pyro/protocol.py
+
+ conn=TCPConnection(sock, sock.getpeername())
+ # receive the authentication challenge string, and use that to build the actual identification string.
+ try:
+ authChallenge=self.recvAuthChallenge(conn)
+ except ProtocolError,x:
+ # check if we were denied
+ if hasattr(x,"partialMsg") and x.partialMsg[:len(self.denyMSG)]==self.denyMSG:
+ raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(x.partialMsg[-1])])
+ else:
+ raise
+ # reply with our ident token, generated from the ident passphrase and the challenge
+ msg = self._sendConnect(sock,self.newConnValidator.createAuthToken(self.ident, authChallenge, conn.addr, self.URI, None) )
+ if msg==self.acceptMSG:
+ self.conn=conn
+ self.conn.connected=1
+ Log.msg('PYROAdapter','connected to',str(URI))
+ if URI.protocol=='PYROLOCPSK':
+ self.resolvePYROLOC_URI("PYROPSK") # updates self.URI
+ elif msg[:len(self.denyMSG)]==self.denyMSG:
+ try:
+ raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(msg[-1])])
+ except (KeyError,ValueError):
+ raise ConnectionDeniedError('invalid response')
+ except socket.error:
+ Log.msg('PYROAdapter','connection failed to URI',str(URI))
+ raise ProtocolError('connection failed')
+
+_getProtocolAdapter = Pyro.protocol.getProtocolAdapter
+def getProtocolAdapter(protocol):
+ if protocol in ('PYROPSK', 'PYROLOCPSK'):
+ return PYROPSKAdapter()
+ return _getProtocolAdapter(protocol)
+
+Pyro.protocol.getProtocolAdapter = getProtocolAdapter
+
+_processStringURI = Pyro.core.processStringURI
+def processStringURI(URI):
+ x=re.match(r'(?P<protocol>PYROLOCPSK)://(?P<hostname>[^\s:]+):?(?P<port>\d+)?/(?P<name>\S*)',URI)
+ if x:
+ protocol=x.group('protocol')
+ hostname=x.group('hostname')
+ port=x.group('port')
+ if port:
+ port=int(port)
+ else:
+ port=0
+ name=x.group('name')
+ return PyroURI(hostname,name,port,protocol)
+ return _processStringURI(URI)
+Pyro.core.processStringURI = processStringURI
--- a/connectors/PYRO/__init__.py Fri Oct 12 13:24:47 2018 +0300
+++ b/connectors/PYRO/__init__.py Fri Nov 23 12:13:24 2018 +0100
@@ -36,9 +36,10 @@
import Pyro.util
from Pyro.errors import PyroError
+import PSKManagement as PSK
from runtime import PlcStatus
-service_type = '_PYRO._tcp.local.'
+zeroconf_service_type = '_PYRO._tcp.local.'
# this module attribute contains a list of DNS-SD (Zeroconf) service types
# supported by this connector confnode.
#
@@ -52,45 +53,29 @@
"""
confnodesroot.logger.write(_("PYRO connecting to URI : %s\n") % uri)
- servicetype, location = uri.split("://")
- if servicetype == "PYROS":
- schemename = "PYROLOCSSL"
- # Protect against name->IP substitution in Pyro3
- Pyro.config.PYRO_DNS_URI = True
- # Beware Pyro lib need str path, not unicode
- # don't rely on PYRO_STORAGE ! see documentation
- Pyro.config.PYROSSL_CERTDIR = os.path.abspath(str(confnodesroot.ProjectPath) + '/certs')
- if not os.path.exists(Pyro.config.PYROSSL_CERTDIR):
+ scheme, location = uri.split("://")
+ if scheme == "PYROS":
+ import connectors.PYRO.PSK_Adapter
+ schemename = "PYROLOCPSK"
+ url, ID = location.split('#') #TODO fix exception when # not found
+ # load PSK from project
+ secpath = os.path.join(str(confnodesroot.ProjectPath), 'psk', ID+'.secret')
+ if not os.path.exists(secpath):
confnodesroot.logger.write_error(
- 'Error : the directory %s is missing for SSL certificates (certs_dir).'
- 'Please fix it in your project.\n' % Pyro.config.PYROSSL_CERTDIR)
+ 'Error: Pre-Shared-Key Secret in %s is missing!\n' % secpath)
return None
- else:
- confnodesroot.logger.write(_("PYRO using certificates in '%s' \n")
- % (Pyro.config.PYROSSL_CERTDIR))
- Pyro.config.PYROSSL_CERT = "client.crt"
- Pyro.config.PYROSSL_KEY = "client.key"
-
- # Ugly Monkey Patching
- def _gettimeout(self):
- return self.timeout
-
- def _settimeout(self, timeout):
- self.timeout = timeout
- from M2Crypto.SSL import Connection # pylint: disable=import-error
- Connection.timeout = None
- Connection.gettimeout = _gettimeout
- Connection.settimeout = _settimeout
- # M2Crypto.SSL.Checker.WrongHost: Peer certificate commonName does not
- # match host, expected 127.0.0.1, got server
- Connection.clientPostConnectionCheck = None
+ secret = open(secpath).read().partition(':')[2].rstrip('\n\r')
+ Pyro.config.PYROPSK = (secret, ID)
+ # strip ID from URL, so that pyro can understand it.
+ location = url
else:
schemename = "PYROLOC"
- if location.find(service_type) != -1:
+
+ if location.find(zeroconf_service_type) != -1:
try:
from zeroconf import Zeroconf
r = Zeroconf()
- i = r.get_service_info(service_type, location)
+ i = r.get_service_info(zeroconf_service_type, location)
if i is None:
raise Exception("'%s' not found" % location)
ip = str(socket.inet_ntoa(i.address))
@@ -108,8 +93,8 @@
try:
RemotePLCObjectProxy = Pyro.core.getAttrProxyForURI(schemename + "://" + location + "/PLCObject")
except Exception:
- confnodesroot.logger.write_error(_("Connection to '%s' failed.\n") % location)
- confnodesroot.logger.write_error(traceback.format_exc())
+ confnodesroot.logger.write_error(_("Connection to '%s' failed with exception '%s'\n") % (location, str(e)))
+ #confnodesroot.logger.write_error(traceback.format_exc())
return None
def PyroCatcher(func, default=None):
@@ -136,13 +121,17 @@
# Check connection is effective.
# lambda is for getattr of GetPLCstatus to happen inside catcher
- if PyroCatcher(RemotePLCObjectProxy.GetPLCstatus)() is None:
- confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n"))
- return None
+ IDPSK = PyroCatcher(RemotePLCObjectProxy.GetPLCID)()
+ if IDPSK is None:
+ confnodesroot.logger.write_warning(_("PLC did not provide identity and security infomation.\n"))
+ else:
+ ID,secret = IDPSK
+ PSK.UpdateID(confnodesroot.ProjectPath, ID, secret, uri)
+
_special_return_funcs = {
"StartPLC": False,
- "GetTraceVariables": ("Broken", None),
+ "GetTraceVariables": (PlcStatus.Broken, None),
"GetPLCstatus": (PlcStatus.Broken, None),
"RemoteExec": (-1, "RemoteExec script failed!")
}
--- a/connectors/PYRO/dialog.py Fri Oct 12 13:24:47 2018 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2018: Smarteh
-#
-# See COPYING file for copyrights details.
-
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import wx
-from zope.interface import implementer
-
-from controls.UriLocationEditor import IConnectorPanel
-
-URITypes = ["LOCAL", "PYRO", "PYROS"]
-
-
-def PYRO_connector_dialog(confnodesroot):
- [ID_IPTEXT, ID_PORTTEXT] = [wx.NewId() for _init_ctrls in range(2)]
-
- @implementer(IConnectorPanel)
- class PYROConnectorPanel(wx.Panel):
- def __init__(self, typeConnector, parrent, *args, **kwargs):
- self.type = typeConnector
- self.parrent = parrent
- wx.Panel.__init__(self, parrent, *args, **kwargs)
- self._init_ctrls()
- self._init_sizers()
- self.uri = None
-
- def _init_ctrls(self):
- self.IpText = wx.TextCtrl(parent=self, id=ID_IPTEXT, size=wx.Size(200, -1))
- self.PortText = wx.TextCtrl(parent=self, id=ID_PORTTEXT, size=wx.Size(200, -1))
-
- def _init_sizers(self):
- self.mainSizer = wx.FlexGridSizer(cols=2, hgap=10, rows=5, vgap=10)
- self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI host:")),
- flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.IpText, flag=wx.GROW)
-
- self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI port:")),
- flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.PortText, flag=wx.GROW)
- self.SetSizer(self.mainSizer)
-
- def SetURI(self, uri):
- self.uri = uri
- uri_list = uri.strip().split(":")
- length = len(uri_list)
- if length == 3:
- self.IpText.SetValue(uri_list[1].strip("/"))
- self.PortText.SetValue(uri_list[2])
- elif length == 2:
- self.IpText.SetValue(uri_list[1].strip("/"))
-
- def GetURI(self):
- self.uri = self.type+"://"+self.IpText.GetValue()+":"+self.PortText.GetValue()
- return self.uri
-
- return PYROConnectorPanel("PYRO", confnodesroot)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/PYRO_dialog.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from itertools import repeat, islice, chain
+import wx
+
+from connectors.SchemeEditor import SchemeEditor
+
+
+model = [('host',_("Host:")),
+ ('port',_("Port:"))]
+
+# (scheme, model, secure)
+models = [("LOCAL", [], False), ("PYRO", model, False), ("PYROS", model, True)]
+
+Schemes = list(zip(*models)[0])
+
+_PerSchemeConf = {sch : (mod,sec) for sch,mod,sec in models}
+
+class PYRO_dialog(SchemeEditor):
+ def __init__(self, scheme, *args, **kwargs):
+
+ # ID selector is enabled only on PYROS (secure)
+ self.model, self.EnableIDSelector = _PerSchemeConf[scheme]
+
+ SchemeEditor.__init__(self, scheme, *args, **kwargs)
+
+ def SetLoc(self, loc):
+ hostport, ID = list(islice(chain(loc.split("#"), repeat("")),2))
+ host, port = list(islice(chain(hostport.split(":"), repeat("")),2))
+ self.SetFields(locals())
+
+ def GetLoc(self):
+ if self.model:
+ fields = self.GetFields()
+ template = "{host}"
+ if fields['port']:
+ template += ":{port}"
+ if fields['ID']:
+ template += "#{ID}"
+
+ return template.format(**fields)
+ return ''
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/SchemeEditor.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from itertools import repeat, izip_longest
+from functools import partial
+import wx
+
+from controls.IDBrowser import IDBrowser
+
+class SchemeEditor(wx.Panel):
+ def __init__(self, scheme, parent, *args, **kwargs):
+ self.txtctrls = {}
+ wx.Panel.__init__(self, parent, *args, **kwargs)
+
+ self.fieldsizer = wx.FlexGridSizer(cols=2, hgap=10, vgap=10)
+
+ if self.EnableIDSelector:
+ self.model = self.model + [("ID", _("ID:"))]
+
+ for tag, label in self.model:
+ txtctrl = wx.TextCtrl(parent=self, size=wx.Size(200, -1))
+ self.txtctrls[tag] = txtctrl
+ for win, flag in [
+ (wx.StaticText(self, label=label), wx.ALIGN_CENTER_VERTICAL),
+ (txtctrl, wx.GROW)]:
+ self.fieldsizer.AddWindow(win, flag=flag)
+
+ self.fieldsizer.AddSpacer(20)
+
+ if self.EnableIDSelector:
+ self.mainsizer = wx.FlexGridSizer(cols=2, hgap=10, vgap=10)
+ self.mainsizer.AddSizer(self.fieldsizer)
+ self.idselector = IDBrowser(
+ self, parent.ctr,
+ # use a callafter, as editor can be deleted by calling SetURI
+ partial(wx.CallAfter, parent.SetURI),
+ self.txtctrls[tag].SetValue)
+ self.mainsizer.AddWindow(self.idselector)
+ self.SetSizer(self.mainsizer)
+ else:
+ self.SetSizer(self.fieldsizer)
+
+ def SetFields(self, fields):
+ for tag, label in self.model:
+ self.txtctrls[tag].SetValue(fields[tag])
+
+ def GetFields(self):
+ return {tag: self.txtctrls[tag].GetValue() for tag,label in self.model}
+
--- a/connectors/WAMP/__init__.py Fri Oct 12 13:24:47 2018 +0300
+++ b/connectors/WAMP/__init__.py Fri Nov 23 12:13:24 2018 +0100
@@ -71,10 +71,10 @@
WAMP://127.0.0.1:12345/path#realm#ID
WAMPS://127.0.0.1:12345/path#realm#ID
"""
- servicetype, location = uri.split("://")
+ scheme, location = uri.split("://")
urlpath, realm, ID = location.split('#')
urlprefix = {"WAMP": "ws",
- "WAMPS": "wss"}[servicetype]
+ "WAMPS": "wss"}[scheme]
url = urlprefix+"://"+urlpath
def RegisterWampClient():
@@ -153,6 +153,9 @@
self.__dict__[attrName] = member
return member
+ # TODO : GetPLCID()
+ # TODO : PSK.UpdateID()
+
# Try to get the proxy object
try:
return WampPLCObjectProxy()
--- a/connectors/WAMP/dialog.py Fri Oct 12 13:24:47 2018 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2018: Smarteh
-#
-# See COPYING file for copyrights details.
-
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import wx
-from zope.interface import implementer
-
-from controls.UriLocationEditor import IConnectorPanel
-
-URITypes = ["WAMP", "WAMPS"]
-
-
-def WAMP_connector_dialog(confnodesroot):
- [ID_IPTEXT, ID_PORTTEXT, ID_REALMTEXT, ID_WAMPIDTEXT, ID_SECURECHECKBOX] = [wx.NewId() for _init_ctrls in range(5)]
-
- @implementer(IConnectorPanel)
- class WAMPConnectorPanel(wx.Panel):
- def __init__(self, typeConnector, parrent, *args, **kwargs):
- self.type = typeConnector
- self.parrent = parrent
- wx.Panel.__init__(self, parrent, *args, **kwargs)
- self._init_ctrls()
- self._init_sizers()
- self.uri = None
-
- def _init_ctrls(self):
- self.IpText = wx.TextCtrl(parent=self, id=ID_IPTEXT, size=wx.Size(200, -1))
- self.PortText = wx.TextCtrl(parent=self, id=ID_PORTTEXT, size=wx.Size(200, -1))
- self.RealmText = wx.TextCtrl(parent=self, id=ID_REALMTEXT, size=wx.Size(200, -1))
- self.WAMPIDText = wx.TextCtrl(parent=self, id=ID_WAMPIDTEXT, size=wx.Size(200, -1))
- self.SecureCheckbox = wx.CheckBox(self, ID_SECURECHECKBOX, _("Is connection secure?"))
-
- def _init_sizers(self):
- self.mainSizer = wx.FlexGridSizer(cols=2, hgap=10, rows=5, vgap=10)
- self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI host:")),
- flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.IpText, flag=wx.GROW)
-
- self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI port:")),
- flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.PortText, flag=wx.GROW)
-
- self.mainSizer.AddWindow(wx.StaticText(self, label=_("Realm:")),
- flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.RealmText, flag=wx.GROW)
-
- self.mainSizer.AddWindow(wx.StaticText(self, label=_("WAMP ID:")),
- flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.WAMPIDText, flag=wx.GROW)
-
- self.mainSizer.AddWindow(wx.StaticText(self, label=""), flag=wx.ALIGN_CENTER_VERTICAL)
- self.mainSizer.AddWindow(self.SecureCheckbox, flag=wx.GROW)
-
- self.SetSizer(self.mainSizer)
-
- def SetURI(self, uri):
- self.uri = uri
- uri_list = uri.strip().split(":")
- length = len(uri_list)
-
- if length > 0:
- if uri_list[0] == URITypes[1]:
- self.SecureCheckbox.SetValue(True)
-
- if length > 2:
- self.IpText.SetValue(uri_list[1].strip("/"))
- wampSett = uri_list[2].split("#")
- length2 = len(wampSett)
- if length2 > 0:
- self.PortText.SetValue(wampSett[0])
- if length2 > 1:
- self.RealmText.SetValue(wampSett[1])
- if length2 > 2:
- self.WAMPIDText.SetValue(wampSett[2])
-
- def GetURI(self):
- if self.IpText.Validate():
- typeForURI = self.type + "S" if self.SecureCheckbox.GetValue() else self.type
- self.uri = typeForURI + "://" + self.IpText.GetValue() + ":" + self.PortText.GetValue() + "#" + self.RealmText.GetValue() + "#" + self.WAMPIDText.GetValue()
- return self.uri
- else:
- return ""
-
- return WAMPConnectorPanel("WAMP", confnodesroot)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/WAMP_dialog.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from itertools import repeat, islice, chain
+import wx
+
+from connectors.SchemeEditor import SchemeEditor
+
+Schemes = ["WAMP", "WAMPS"]
+
+model = [('host',_("Host:")),
+ ('port',_("Port:")),
+ ('realm',_("Realm:"))]
+
+class WAMP_dialog(SchemeEditor):
+ def __init__(self, *args, **kwargs):
+ self.model = model
+ self.EnableIDSelector = True
+ SchemeEditor.__init__(self, *args, **kwargs)
+
+ def SetLoc(self, loc):
+ hostport, realm, ID = list(islice(chain(loc.split("#"), repeat("")),3))
+ host, port = list(islice(chain(hostport.split(":"), repeat("")),2))
+ self.SetFields(locals())
+
+ def GetLoc(self):
+ fields = self.GetFields()
+
+ #TODO : input validation test
+
+ template = "{host}" + \
+ (":{port}" if fields['port'] else '') +\
+ "#{realm}#{ID}"
+
+ return template.format(**fields)
+
--- a/connectors/__init__.py Fri Oct 12 13:24:47 2018 +0300
+++ b/connectors/__init__.py Fri Nov 23 12:13:24 2018 +0100
@@ -30,32 +30,33 @@
from os import listdir, path
import util.paths as paths
-_base_path = paths.AbsDir(__file__)
+connectors_packages = ["PYRO","WAMP"]
def _GetLocalConnectorClassFactory(name):
return lambda: getattr(__import__(name, globals(), locals()), name + "_connector_factory")
-def _GetLocalConnectorClassDialog(name):
- return lambda: getattr(__import__(name + '.dialog', globals(), locals(), fromlist=['dialog']), name + "_connector_dialog")
+connectors = {name: _GetLocalConnectorClassFactory(name)
+ for name in connectors_packages}
+_dialogs_imported = False
+per_URI_connectors = None
+schemes = None
-def _GetLocalConnectorURITypes(name):
- return lambda: getattr(__import__(name + '.dialog', globals(), locals(), fromlist=['dialog']), "URITypes", None)
+# lazy import of connectors dialogs, only if used
+def _Import_Dialogs():
+ global per_URI_connectors, schemes, _dialogs_imported
+ if not _dialogs_imported:
+ _dialogs_imported = True
+ per_URI_connectors = {}
+ schemes = []
+ for con_name in connectors_packages:
+ module = __import__(con_name + '_dialog', globals(), locals())
-
-connectors = {name:
- _GetLocalConnectorClassFactory(name)
- for name in listdir(_base_path)
- if (path.isdir(path.join(_base_path, name)) and
- not name.startswith("__"))}
-
-connectors_dialog = {name:
- {"function": _GetLocalConnectorClassDialog(name), "URITypes": _GetLocalConnectorURITypes(name)}
- for name in listdir(_base_path)
- if (path.isdir(path.join(_base_path, name)) and
- not name.startswith("__"))}
+ for scheme in module.Schemes:
+ per_URI_connectors[scheme] = getattr(module, con_name + '_dialog')
+ schemes += [scheme]
def ConnectorFactory(uri, confnodesroot):
@@ -63,41 +64,31 @@
Return a connector corresponding to the URI
or None if cannot connect to URI
"""
- servicetype = uri.split("://")[0].upper()
- if servicetype == "LOCAL":
+ scheme = uri.split("://")[0].upper()
+ if scheme == "LOCAL":
# Local is special case
# pyro connection to local runtime
# started on demand, listening on random port
- servicetype = "PYRO"
+ scheme = "PYRO"
runtime_port = confnodesroot.AppFrame.StartLocalRuntime(
taskbaricon=True)
uri = "PYROLOC://127.0.0.1:" + str(runtime_port)
- elif servicetype in connectors:
+ elif scheme in connectors:
pass
- elif servicetype[-1] == 'S' and servicetype[:-1] in connectors:
- servicetype = servicetype[:-1]
+ elif scheme[-1] == 'S' and scheme[:-1] in connectors:
+ scheme = scheme[:-1]
else:
return None
# import module according to uri type
- connectorclass = connectors[servicetype]()
+ connectorclass = connectors[scheme]()
return connectorclass(uri, confnodesroot)
-def ConnectorDialog(conn_type, confnodesroot):
- if conn_type not in connectors_dialog:
- return None
+def EditorClassFromScheme(scheme):
+ _Import_Dialogs()
+ return per_URI_connectors.get(scheme, None)
- connectorclass = connectors_dialog[conn_type]["function"]()
- return connectorclass(confnodesroot)
-
-
-def GetConnectorFromURI(uri):
- typeOfConnector = None
- for conn_type in connectors_dialog:
- connectorTypes = connectors_dialog[conn_type]["URITypes"]()
- if connectorTypes and uri in connectorTypes:
- typeOfConnector = conn_type
- break
-
- return typeOfConnector
+def ConnectorSchemes():
+ _Import_Dialogs()
+ return schemes
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/controls/DiscoveryPanel.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,235 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz, a Integrated Development Environment for
+# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
+#
+# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
+# Copyright (C) 2017: Andrey Skvortsov <andrej.skvortzov@gmail.com>
+#
+# See COPYING file for copyrights details.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+from __future__ import absolute_import
+import socket
+from six.moves import xrange
+import wx
+import wx.lib.mixins.listctrl as listmix
+from zeroconf import ServiceBrowser, Zeroconf
+
+
+service_type = '_PYRO._tcp.local.'
+
+
+class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
+ def __init__(self, parent, id, name, pos=wx.DefaultPosition,
+ size=wx.DefaultSize, style=0):
+ wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name)
+ listmix.ListCtrlAutoWidthMixin.__init__(self)
+
+class DiscoveryPanel(wx.Panel, listmix.ColumnSorterMixin):
+
+ def _init_coll_MainSizer_Items(self, parent):
+ parent.AddWindow(self.staticText1, 0, border=20, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW)
+ parent.AddWindow(self.ServicesList, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.GROW)
+ parent.AddSizer(self.ButtonGridSizer, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW)
+
+ def _init_coll_MainSizer_Growables(self, parent):
+ parent.AddGrowableCol(0)
+ parent.AddGrowableRow(1)
+
+ def _init_coll_ButtonGridSizer_Items(self, parent):
+ parent.AddWindow(self.RefreshButton, 0, border=0, flag=0)
+ parent.AddWindow(self.ByIPCheck, 0, border=0, flag=0)
+
+ def _init_coll_ButtonGridSizer_Growables(self, parent):
+ parent.AddGrowableCol(0)
+ parent.AddGrowableRow(0)
+
+ def _init_sizers(self):
+ self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=10)
+ self.ButtonGridSizer = wx.FlexGridSizer(cols=2, hgap=5, rows=1, vgap=0)
+
+ self._init_coll_MainSizer_Items(self.MainSizer)
+ self._init_coll_MainSizer_Growables(self.MainSizer)
+ self._init_coll_ButtonGridSizer_Items(self.ButtonGridSizer)
+ self._init_coll_ButtonGridSizer_Growables(self.ButtonGridSizer)
+
+ self.SetSizer(self.MainSizer)
+
+ def _init_list_ctrl(self):
+ # Set up list control
+ listID = wx.NewId()
+ self.ServicesList = AutoWidthListCtrl(
+ id=listID,
+ name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0),
+ style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)
+ self.ServicesList.InsertColumn(0, _('NAME'))
+ self.ServicesList.InsertColumn(1, _('TYPE'))
+ self.ServicesList.InsertColumn(2, _('IP'))
+ self.ServicesList.InsertColumn(3, _('PORT'))
+ self.ServicesList.SetColumnWidth(0, 150)
+ self.ServicesList.SetColumnWidth(1, 150)
+ self.ServicesList.SetColumnWidth(2, 150)
+ self.ServicesList.SetColumnWidth(3, 150)
+ self.ServicesList.SetInitialSize(wx.Size(-1, 300))
+ self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, id=listID)
+ self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, id=listID)
+
+ def _init_ctrls(self, prnt):
+ self.staticText1 = wx.StaticText(
+ label=_('Services available:'), name='staticText1', parent=self,
+ pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
+
+ refreshID = wx.NewId()
+ self.RefreshButton = wx.Button(
+ id=refreshID,
+ label=_('Refresh'), name='RefreshButton', parent=self,
+ pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
+ self.Bind(wx.EVT_BUTTON, self.OnRefreshButton, id=refreshID)
+
+ self.ByIPCheck = wx.CheckBox(self, label=_("Use IP instead of Service Name"))
+
+ self._init_sizers()
+ self.Fit()
+
+ def __init__(self, parent):
+ wx.Panel.__init__(self, parent)
+
+ self._init_list_ctrl()
+ listmix.ColumnSorterMixin.__init__(self, 4)
+
+ self._init_ctrls(parent)
+
+ self.itemDataMap = {}
+ self.nextItemId = 0
+
+ self.URI = None
+ self.Browser = None
+
+ self.ZeroConfInstance = Zeroconf()
+ self.RefreshList()
+ self.LatestSelection = None
+
+ def __del__(self):
+ if self.Browser is not None:
+ self.Browser.cancel()
+ self.ZeroConfInstance.close()
+
+ def RefreshList(self):
+ if self.Browser is not None:
+ self.Browser.cancel()
+ self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
+
+ def OnRefreshButton(self, event):
+ self.ServicesList.DeleteAllItems()
+ self.RefreshList()
+
+ # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
+ def GetListCtrl(self):
+ return self.ServicesList
+
+ def getColumnText(self, index, col):
+ item = self.ServicesList.GetItem(index, col)
+ return item.GetText()
+
+ def OnItemSelected(self, event):
+ self.SetURI(event.m_itemIndex)
+ event.Skip()
+
+ def OnItemActivated(self, event):
+ self.SetURI(event.m_itemIndex)
+ self.EndModal(wx.ID_OK)
+ event.Skip()
+
+# def SetURI(self, idx):
+# connect_type = self.getColumnText(idx, 1)
+# connect_address = self.getColumnText(idx, 2)
+# connect_port = self.getColumnText(idx, 3)
+#
+# self.URI = "%s://%s:%s"%(connect_type, connect_address, connect_port)
+
+ def SetURI(self, idx):
+ self.LatestSelection = idx
+ svcname = self.getColumnText(idx, 0)
+ connect_type = self.getColumnText(idx, 1)
+ self.URI = "%s://%s" % (connect_type, svcname + '.' + service_type)
+
+ def GetURI(self):
+ if self.LatestSelection is not None:
+ if self.ByIPCheck.IsChecked():
+ self.URI = "%s://%s:%s" % tuple(
+ map(lambda col:self.getColumnText(self.LatestSelection, col),
+ (1, 2, 3)))
+
+ return self.URI
+ return None
+
+ def remove_service(self, zeroconf, _type, name):
+ wx.CallAfter(self._removeService, name)
+
+ def _removeService(self, name):
+ '''
+ called when a service with the desired type goes offline.
+ '''
+
+ # loop through the list items looking for the service that went offline
+ for idx in xrange(self.ServicesList.GetItemCount()):
+ # this is the unique identifier assigned to the item
+ item_id = self.ServicesList.GetItemData(idx)
+
+ # this is the full typename that was received by addService
+ item_name = self.itemDataMap[item_id][4]
+
+ if item_name == name:
+ self.ServicesList.DeleteItem(idx)
+ break
+
+ def add_service(self, zeroconf, _type, name):
+ wx.CallAfter(self._addService, _type, name)
+
+ def _addService(self, _type, name):
+ '''
+ called when a service with the desired type is discovered.
+ '''
+ info = self.ZeroConfInstance.get_service_info(_type, name)
+ svcname = name.split(".")[0]
+ typename = _type.split(".")[0][1:]
+ ip = str(socket.inet_ntoa(info.address))
+ port = info.port
+
+ num_items = self.ServicesList.GetItemCount()
+
+ # display the new data in the list
+ new_item = self.ServicesList.InsertStringItem(num_items, svcname)
+ self.ServicesList.SetStringItem(new_item, 1, "%s" % typename)
+ self.ServicesList.SetStringItem(new_item, 2, "%s" % ip)
+ self.ServicesList.SetStringItem(new_item, 3, "%s" % port)
+
+ # record the new data for the ColumnSorterMixin
+ # we assign every list item a unique id (that won't change when items
+ # are added or removed)
+ self.ServicesList.SetItemData(new_item, self.nextItemId)
+
+ # the value of each column has to be stored in the itemDataMap
+ # so that ColumnSorterMixin knows how to sort the column.
+
+ # "name" is included at the end so that self.removeService
+ # can access it.
+ self.itemDataMap[self.nextItemId] = [svcname, typename, ip, port, name]
+
+ self.nextItemId += 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/controls/IDBrowser.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import os
+import wx
+import wx.dataview as dv
+import PSKManagement as PSK
+from PSKManagement import *
+from dialogs.IDMergeDialog import IDMergeDialog
+
+class IDBrowserModel(dv.PyDataViewIndexListModel):
+ def __init__(self, project_path, columncount):
+ self.project_path = project_path
+ self.columncount = columncount
+ self.data = PSK.GetData(project_path)
+ dv.PyDataViewIndexListModel.__init__(self, len(self.data))
+
+ def _saveData(self):
+ PSK.SaveData(self.project_path, self.data)
+
+ def GetColumnType(self, col):
+ return "string"
+
+ def GetValueByRow(self, row, col):
+ return self.data[row][col]
+
+ def SetValueByRow(self, value, row, col):
+ self.data[row][col] = value
+ self._saveData()
+
+ def GetColumnCount(self):
+ return len(self.data[0]) if self.data else self.columncount
+
+ def GetCount(self):
+ return len(self.data)
+
+ def Compare(self, item1, item2, col, ascending):
+ if not ascending: # swap sort order?
+ item2, item1 = item1, item2
+ row1 = self.GetRow(item1)
+ row2 = self.GetRow(item2)
+ if col == 0:
+ return cmp(int(self.data[row1][col]), int(self.data[row2][col]))
+ else:
+ return cmp(self.data[row1][col], self.data[row2][col])
+
+ def DeleteRows(self, rows):
+ rows = list(rows)
+ rows.sort(reverse=True)
+
+ for row in rows:
+ PSK.DeleteID(self.project_path, self.data[row][COL_ID])
+ del self.data[row]
+ self.RowDeleted(row)
+ self._saveData()
+
+ def AddRow(self, value):
+ self.data.append(value)
+ self.RowAppended()
+ self._saveData()
+
+ def Import(self, filepath, sircb):
+ data = PSK.ImportIDs(self.project_path, filepath, sircb)
+ if data is not None:
+ self.data = data
+ self.Reset(len(self.data))
+
+ def Export(self, filepath):
+ PSK.ExportIDs(self.project_path, filepath)
+
+colflags = dv.DATAVIEW_COL_RESIZABLE|dv.DATAVIEW_COL_SORTABLE
+
+class IDBrowser(wx.Panel):
+ def __init__(self, parent, ctr, SelectURICallBack=None, SelectIDCallBack=None, **kwargs):
+ big = self.isManager = SelectURICallBack is None and SelectIDCallBack is None
+ wx.Panel.__init__(self, parent, -1, size=(800 if big else 450,
+ 600 if big else 200))
+
+ self.SelectURICallBack = SelectURICallBack
+ self.SelectIDCallBack = SelectIDCallBack
+
+ dvStyle = wx.BORDER_THEME | dv.DV_ROW_LINES
+ if self.isManager :
+ # no multiple selection in selector mode
+ dvStyle |= dv.DV_MULTIPLE
+ self.dvc = dv.DataViewCtrl(self, style = dvStyle)
+
+ args = lambda *a,**k:(a,k)
+
+ ColumnsDesc = [
+ args(_("ID"), COL_ID, width = 70),
+ args(_("Last URI"), COL_URI, width = 300 if big else 80),
+ args(_("Description"), COL_DESC, width = 300 if big else 200,
+ mode = dv.DATAVIEW_CELL_EDITABLE
+ if self.isManager
+ else dv.DATAVIEW_CELL_INERT),
+ args(_("Last connection"), COL_LAST, width = 120),
+ ]
+
+ self.model = IDBrowserModel(ctr.ProjectPath, len(ColumnsDesc))
+ self.dvc.AssociateModel(self.model)
+
+ col_list = []
+ for a,k in ColumnsDesc:
+ col_list.append(
+ self.dvc.AppendTextColumn(*a,**dict(k, flags = colflags)))
+ col_list[COL_LAST].SetSortOrder(False)
+
+ # TODO : sort by last bvisit by default
+
+ self.Sizer = wx.BoxSizer(wx.VERTICAL)
+ self.Sizer.Add(self.dvc, 1, wx.EXPAND)
+
+ btnbox = wx.BoxSizer(wx.HORIZONTAL)
+ if self.isManager :
+
+ # deletion of secret and metadata
+ deleteButton = wx.Button(self, label=_("Delete ID"))
+ self.Bind(wx.EVT_BUTTON, self.OnDeleteButton, deleteButton)
+ btnbox.Add(deleteButton, 0, wx.LEFT|wx.RIGHT, 5)
+
+ # export all
+ exportButton = wx.Button(self, label=_("Export all"))
+ self.Bind(wx.EVT_BUTTON, self.OnExportButton, exportButton)
+ btnbox.Add(exportButton, 0, wx.LEFT|wx.RIGHT, 5)
+
+ # import with a merge -> duplicates are asked for
+ importButton = wx.Button(self, label=_("Import"))
+ self.Bind(wx.EVT_BUTTON, self.OnImportButton, importButton)
+ btnbox.Add(importButton, 0, wx.LEFT|wx.RIGHT, 5)
+
+ else :
+ # selector mode
+ self.useURIButton = wx.Button(self, label=_("Use last URI"))
+ self.Bind(wx.EVT_BUTTON, self.OnUseURIButton, self.useURIButton)
+ self.useURIButton.Disable()
+ btnbox.Add(self.useURIButton, 0, wx.LEFT|wx.RIGHT, 5)
+
+ self.Sizer.Add(btnbox, 0, wx.TOP|wx.BOTTOM, 5)
+ self.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.OnSelectionChanged, self.dvc)
+
+
+ def OnDeleteButton(self, evt):
+ items = self.dvc.GetSelections()
+ rows = [self.model.GetRow(item) for item in items]
+
+ # Ask if user really wants to delete
+ if wx.MessageBox(_('Are you sure to delete selected IDs?'),
+ _('Delete IDs'),
+ wx.YES_NO | wx.CENTRE | wx.NO_DEFAULT) != wx.YES:
+ return
+
+ self.model.DeleteRows(rows)
+
+ def OnSelectionChanged(self, evt):
+ if not self.isManager :
+ items = self.dvc.GetSelections()
+ somethingSelected = len(items) > 0
+ self.useURIButton.Enable(somethingSelected)
+ if somethingSelected:
+ row = self.model.GetRow(items[0])
+ ID = self.model.GetValueByRow(row, COL_ID)
+ self.SelectIDCallBack(ID)
+
+
+ def OnUseURIButton(self, evt):
+ row = self.model.GetRow(self.dvc.GetSelections()[0])
+ URI = self.model.GetValueByRow(row, COL_URI)
+ if URI:
+ self.SelectURICallBack(URI)
+
+ def OnExportButton(self, evt):
+ dialog = wx.FileDialog(self, _("Choose a file"),
+ wildcard = _("PSK ZIP files (*.zip)|*.zip"),
+ style = wx.SAVE | wx.OVERWRITE_PROMPT)
+ if dialog.ShowModal() == wx.ID_OK:
+ self.model.Export(dialog.GetPath())
+
+ def ShouldIReplaceCallback(self,existing,replacement):
+ ID,URI,DESC,LAST = existing
+ _ID,_URI,_DESC,_LAST = replacement
+ dlg = IDMergeDialog(self,
+ _("Import IDs"),
+ (_("Replace information for ID {ID} ?") + "\n\n" +
+ _("Existing:") + "\n " +
+ _("Description:") + " {DESC}\n " +
+ _("Last known URI:") + " {URI}\n " +
+ _("Last connection:") + " {LAST}\n\n" +
+ _("Replacement:") + "\n " +
+ _("Description:") + " {_DESC}\n " +
+ _("Last known URI:") + " {_URI}\n " +
+ _("Last connection:") + " {_LAST}\n").format(**locals()),
+ _("Do the same for following IDs"),
+ [_("Replace"), _("Keep"),_("Cancel")])
+
+ answer = dlg.ShowModal() # return value ignored as we have "Ok" only anyhow
+ if answer == wx.ID_CANCEL:
+ return CANCEL
+
+ if dlg.OptionChecked():
+ if answer == wx.ID_YES:
+ return REPLACE_ALL
+ return KEEP_ALL
+ else:
+ if answer == wx.ID_YES:
+ return REPLACE
+ return KEEP
+
+ def OnImportButton(self, evt):
+ dialog = wx.FileDialog(self, _("Choose a file"),
+ wildcard = _("PSK ZIP files (*.zip)|*.zip"),
+ style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
+ if dialog.ShowModal() == wx.ID_OK:
+ self.model.Import(dialog.GetPath(),
+ self.ShouldIReplaceCallback)
+
--- a/controls/UriLocationEditor.py Fri Oct 12 13:24:47 2018 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-from __future__ import absolute_import
-
-import wx
-from zope.interface import Interface, Attribute
-from zope.interface.verify import verifyObject
-from connectors import connectors_dialog, ConnectorDialog, GetConnectorFromURI
-
-
-[ID_URIWIZARDDIALOG, ID_URITYPECHOICE] = [wx.NewId() for _init_ctrls in range(2)]
-
-
-class IConnectorPanel(Interface):
- """This is interface for panel of seperate connector type"""
- uri = Attribute("""uri of connections""")
- type = Attribute("""type of connector""")
-
- def SetURI(uri): # pylint: disable=no-self-argument
- """methode for set uri"""
-
- def GetURI(): # pylint: disable=no-self-argument
- """metohde for get uri"""
-
-
-class UriLocationEditor(wx.Dialog):
- def _init_ctrls(self, parent):
- self.UriTypeChoice = wx.Choice(parent=self, id=ID_URIWIZARDDIALOG, choices=self.URITYPES)
- self.UriTypeChoice.SetSelection(0)
- self.Bind(wx.EVT_CHOICE, self.OnTypeChoice, self.UriTypeChoice)
- self.PanelSizer = wx.BoxSizer(wx.HORIZONTAL)
- self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
-
- def _init_sizers(self):
- self.mainSizer = wx.BoxSizer(wx.VERTICAL)
- typeSizer = wx.BoxSizer(wx.HORIZONTAL)
- typeSizer.Add(wx.StaticText(self, wx.ID_ANY, _("URI type:")), border=5, flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL)
- typeSizer.Add(self.UriTypeChoice, border=5, flag=wx.ALL)
- self.mainSizer.Add(typeSizer)
-
- self.mainSizer.Add(self.PanelSizer, border=5, flag=wx.ALL)
- self.mainSizer.Add(self.ButtonSizer, border=5, flag=wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
- self.SetSizer(self.mainSizer)
- self.Layout()
- self.Fit()
-
- def __init__(self, parent, uri):
- wx.Dialog.__init__(self, id=ID_URIWIZARDDIALOG,
- name='UriLocationEditor', parent=parent,
- title='Uri location')
- self.URITYPES = [_("- Select URI type -")]
- for connector_type, connector_function in connectors_dialog.iteritems():
- try:
- connector_function['function']()
- self.URITYPES.append(connector_type)
- except Exception:
- pass
-
- self.selected = None
- self.parrent = parent
- self.logger = self.parrent.CTR.logger
- self._init_ctrls(parent)
- self._init_sizers()
- self.SetURI(uri)
- self.CenterOnParent()
-
- def OnTypeChoice(self, event):
- self._removePanelType()
- index = event.GetSelection()
- if index > 0:
- self.selected = event.GetString()
- self.panelType = self._getConnectorDialog(self.selected)
- if self.panelType:
- self.PanelSizer.Add(self.panelType)
- self.mainSizer.Layout()
- self.Fit()
- self.panelType.Refresh()
-
- def SetURI(self, uri):
- self._removePanelType()
- uri_list = uri.strip().split(":")
- if uri_list:
- uri_type = uri_list[0].upper()
- type = GetConnectorFromURI(uri_type)
- if type:
- self.selected = type
- self.UriTypeChoice.SetStringSelection(self.selected)
- self.panelType = self._getConnectorDialog(self.selected)
- if self.panelType:
- self.panelType.SetURI(uri)
- self.PanelSizer.Add(self.panelType)
- self.PanelSizer.Layout()
- self.mainSizer.Layout()
- self.Fit()
- self.panelType.Refresh()
-
- def GetURI(self):
- if not self.selected or not self.panelType:
- return ""
- else:
- return self.panelType.GetURI()
-
- def _removePanelType(self):
- for i in range(self.PanelSizer.GetItemCount()):
- item = self.PanelSizer.GetItem(i)
- item.DeleteWindows()
- self.PanelSizer.Remove(i)
- self.Fit()
- self.PanelSizer.Layout()
-
- def _getConnectorDialog(self, connectorType):
- connector = ConnectorDialog(connectorType, self)
- if connector and IConnectorPanel.providedBy(connector):
- if verifyObject(IConnectorPanel, connector):
- return connector
- else:
- return None
--- a/controls/__init__.py Fri Oct 12 13:24:47 2018 +0300
+++ b/controls/__init__.py Fri Nov 23 12:13:24 2018 +0100
@@ -44,3 +44,4 @@
from controls.LogViewer import LogViewer
from controls.CustomStyledTextCtrl import CustomStyledTextCtrl
from controls.CustomToolTip import CustomToolTip
+
--- a/dialogs/DiscoveryDialog.py Fri Oct 12 13:24:47 2018 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,268 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# This file is part of Beremiz, a Integrated Development Environment for
-# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
-#
-# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
-# Copyright (C) 2017: Andrey Skvortsov <andrej.skvortzov@gmail.com>
-#
-# See COPYING file for copyrights details.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
-from __future__ import absolute_import
-import socket
-from six.moves import xrange
-import wx
-import wx.lib.mixins.listctrl as listmix
-from zeroconf import ServiceBrowser, Zeroconf
-
-
-service_type = '_PYRO._tcp.local.'
-
-
-class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
- def __init__(self, parent, id, name, pos=wx.DefaultPosition,
- size=wx.DefaultSize, style=0):
- wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name)
- listmix.ListCtrlAutoWidthMixin.__init__(self)
-
-
-[
- ID_DISCOVERYDIALOG, ID_DISCOVERYDIALOGSTATICTEXT1,
- ID_DISCOVERYDIALOGSERVICESLIST, ID_DISCOVERYDIALOGREFRESHBUTTON,
- ID_DISCOVERYDIALOGLOCALBUTTON, ID_DISCOVERYDIALOGIPBUTTON,
-] = [wx.NewId() for _init_ctrls in range(6)]
-
-
-class DiscoveryDialog(wx.Dialog, listmix.ColumnSorterMixin):
-
- def _init_coll_MainSizer_Items(self, parent):
- parent.AddWindow(self.staticText1, 0, border=20, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW)
- parent.AddWindow(self.ServicesList, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.GROW)
- parent.AddSizer(self.ButtonGridSizer, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW)
-
- def _init_coll_MainSizer_Growables(self, parent):
- parent.AddGrowableCol(0)
- parent.AddGrowableRow(1)
-
- def _init_coll_ButtonGridSizer_Items(self, parent):
- parent.AddWindow(self.RefreshButton, 0, border=0, flag=0)
- parent.AddWindow(self.LocalButton, 0, border=0, flag=0)
- parent.AddWindow(self.IpButton, 0, border=0, flag=0)
- parent.AddSizer(self.ButtonSizer, 0, border=0, flag=0)
-
- def _init_coll_ButtonGridSizer_Growables(self, parent):
- parent.AddGrowableCol(0)
- parent.AddGrowableCol(1)
- parent.AddGrowableRow(0)
-
- def _init_sizers(self):
- self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=10)
- self.ButtonGridSizer = wx.FlexGridSizer(cols=4, hgap=5, rows=1, vgap=0)
-
- self._init_coll_MainSizer_Items(self.MainSizer)
- self._init_coll_MainSizer_Growables(self.MainSizer)
- self._init_coll_ButtonGridSizer_Items(self.ButtonGridSizer)
- self._init_coll_ButtonGridSizer_Growables(self.ButtonGridSizer)
-
- self.SetSizer(self.MainSizer)
-
- def _init_list_ctrl(self):
- # Set up list control
- self.ServicesList = AutoWidthListCtrl(
- id=ID_DISCOVERYDIALOGSERVICESLIST,
- name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0),
- style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)
- self.ServicesList.InsertColumn(0, _('NAME'))
- self.ServicesList.InsertColumn(1, _('TYPE'))
- self.ServicesList.InsertColumn(2, _('IP'))
- self.ServicesList.InsertColumn(3, _('PORT'))
- self.ServicesList.SetColumnWidth(0, 150)
- self.ServicesList.SetColumnWidth(1, 150)
- self.ServicesList.SetColumnWidth(2, 150)
- self.ServicesList.SetColumnWidth(3, 150)
- self.ServicesList.SetInitialSize(wx.Size(-1, 300))
- self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, id=ID_DISCOVERYDIALOGSERVICESLIST)
- self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, id=ID_DISCOVERYDIALOGSERVICESLIST)
-
- def _init_ctrls(self, prnt):
- self.staticText1 = wx.StaticText(
- id=ID_DISCOVERYDIALOGSTATICTEXT1,
- label=_('Services available:'), name='staticText1', parent=self,
- pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
-
- self.RefreshButton = wx.Button(
- id=ID_DISCOVERYDIALOGREFRESHBUTTON,
- label=_('Refresh'), name='RefreshButton', parent=self,
- pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
- self.Bind(wx.EVT_BUTTON, self.OnRefreshButton, id=ID_DISCOVERYDIALOGREFRESHBUTTON)
-
- self.LocalButton = wx.Button(
- id=ID_DISCOVERYDIALOGLOCALBUTTON,
- label=_('Local'), name='LocalButton', parent=self,
- pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
- self.Bind(wx.EVT_BUTTON, self.OnLocalButton, id=ID_DISCOVERYDIALOGLOCALBUTTON)
-
- self.IpButton = wx.Button(
- id=ID_DISCOVERYDIALOGIPBUTTON,
- label=_('Add IP'), name='IpButton', parent=self,
- pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
- self.Bind(wx.EVT_BUTTON, self.OnIpButton, id=ID_DISCOVERYDIALOGIPBUTTON)
-
- self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL | wx.CENTER)
-
- self._init_sizers()
- self.Fit()
-
- def __init__(self, parent):
- wx.Dialog.__init__(
- self, id=ID_DISCOVERYDIALOG,
- name='DiscoveryDialog', parent=parent,
- style=wx.DEFAULT_DIALOG_STYLE,
- title=_('Service Discovery'))
-
- self._init_list_ctrl()
- listmix.ColumnSorterMixin.__init__(self, 4)
-
- self._init_ctrls(parent)
-
- self.itemDataMap = {}
- self.nextItemId = 0
-
- self.URI = None
- self.Browser = None
-
- self.ZeroConfInstance = Zeroconf()
- self.RefreshList()
- self.LatestSelection = None
-
- def __del__(self):
- if self.Browser is not None:
- self.Browser.cancel()
- self.ZeroConfInstance.close()
-
- def RefreshList(self):
- if self.Browser is not None:
- self.Browser.cancel()
- self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
-
- def OnRefreshButton(self, event):
- self.ServicesList.DeleteAllItems()
- self.RefreshList()
-
- def OnLocalButton(self, event):
- self.URI = "LOCAL://"
- self.EndModal(wx.ID_OK)
- event.Skip()
-
- def OnIpButton(self, event):
- def GetColText(col):
- return self.getColumnText(self.LatestSelection, col)
-
- if self.LatestSelection is not None:
- self.URI = "%s://%s:%s" % tuple(map(GetColText, (1, 2, 3)))
- self.EndModal(wx.ID_OK)
- event.Skip()
-
- # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
- def GetListCtrl(self):
- return self.ServicesList
-
- def getColumnText(self, index, col):
- item = self.ServicesList.GetItem(index, col)
- return item.GetText()
-
- def OnItemSelected(self, event):
- self.SetURI(event.m_itemIndex)
- event.Skip()
-
- def OnItemActivated(self, event):
- self.SetURI(event.m_itemIndex)
- self.EndModal(wx.ID_OK)
- event.Skip()
-
-# def SetURI(self, idx):
-# connect_type = self.getColumnText(idx, 1)
-# connect_address = self.getColumnText(idx, 2)
-# connect_port = self.getColumnText(idx, 3)
-#
-# self.URI = "%s://%s:%s"%(connect_type, connect_address, connect_port)
-
- def SetURI(self, idx):
- self.LatestSelection = idx
- svcname = self.getColumnText(idx, 0)
- connect_type = self.getColumnText(idx, 1)
- self.URI = "%s://%s" % (connect_type, svcname + '.' + service_type)
-
- def GetURI(self):
- return self.URI
-
- def remove_service(self, zeroconf, _type, name):
- wx.CallAfter(self._removeService, name)
-
- def _removeService(self, name):
- '''
- called when a service with the desired type goes offline.
- '''
-
- # loop through the list items looking for the service that went offline
- for idx in xrange(self.ServicesList.GetItemCount()):
- # this is the unique identifier assigned to the item
- item_id = self.ServicesList.GetItemData(idx)
-
- # this is the full typename that was received by addService
- item_name = self.itemDataMap[item_id][4]
-
- if item_name == name:
- self.ServicesList.DeleteItem(idx)
- break
-
- def add_service(self, zeroconf, _type, name):
- wx.CallAfter(self._addService, _type, name)
-
- def _addService(self, _type, name):
- '''
- called when a service with the desired type is discovered.
- '''
- info = self.ZeroConfInstance.get_service_info(_type, name)
- svcname = name.split(".")[0]
- typename = _type.split(".")[0][1:]
- ip = str(socket.inet_ntoa(info.address))
- port = info.port
-
- num_items = self.ServicesList.GetItemCount()
-
- # display the new data in the list
- new_item = self.ServicesList.InsertStringItem(num_items, svcname)
- self.ServicesList.SetStringItem(new_item, 1, "%s" % typename)
- self.ServicesList.SetStringItem(new_item, 2, "%s" % ip)
- self.ServicesList.SetStringItem(new_item, 3, "%s" % port)
-
- # record the new data for the ColumnSorterMixin
- # we assign every list item a unique id (that won't change when items
- # are added or removed)
- self.ServicesList.SetItemData(new_item, self.nextItemId)
-
- # the value of each column has to be stored in the itemDataMap
- # so that ColumnSorterMixin knows how to sort the column.
-
- # "name" is included at the end so that self.removeService
- # can access it.
- self.itemDataMap[self.nextItemId] = [svcname, typename, ip, port, name]
-
- self.nextItemId += 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dialogs/IDManager.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,27 @@
+from __future__ import absolute_import
+
+import wx
+from connectors import ConnectorSchemes, EditorClassFromScheme
+from controls.DiscoveryPanel import DiscoveryPanel
+from controls.IDBrowser import IDBrowser
+
+class IDManager(wx.Dialog):
+ def __init__(self, parent, ctr):
+ self.ctr = ctr
+ wx.Dialog.__init__(self,
+ name='IDManager', parent=parent,
+ title=_('URI Editor'),
+ style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
+ size=(800,600))
+ # start IDBrowser in manager mode
+ self.browser = IDBrowser(self, ctr)
+ self.Bind(wx.EVT_CHAR_HOOK, self.OnEscapeKey)
+
+ def OnEscapeKey(self, event):
+ keycode = event.GetKeyCode()
+ if keycode == wx.WXK_ESCAPE:
+ self.EndModal(wx.ID_CANCEL)
+ else:
+ event.Skip()
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dialogs/IDMergeDialog.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import wx
+
+# class RichMessageDialog is still not available in wxPython 3.0.2
+class IDMergeDialog(wx.Dialog):
+ def __init__(self, parent, title, question, optiontext, button_texts):
+ wx.Dialog.__init__(self, parent, title=title)
+
+ main_sizer = wx.BoxSizer(wx.VERTICAL)
+
+ message = wx.StaticText(self, label=question)
+ main_sizer.AddWindow(message, border=20,
+ flag = wx.ALIGN_CENTER_HORIZONTAL | wx.TOP | wx.LEFT | wx.RIGHT)
+
+ self.check = wx.CheckBox(self, label=optiontext)
+ main_sizer.AddWindow(self.check, border=20,
+ flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
+
+ buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ for label,wxID in zip(button_texts, [wx.ID_YES, wx.ID_NO, wx.ID_CANCEL]):
+ Button = wx.Button(self, label=label)
+ def OnButtonFactory(_wxID):
+ return lambda event: self.EndModal(_wxID)
+ self.Bind(wx.EVT_BUTTON, OnButtonFactory(wxID), Button)
+ buttons_sizer.AddWindow(Button)
+
+ main_sizer.AddSizer(buttons_sizer, border=20,
+ flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.ALIGN_RIGHT)
+
+ self.SetSizer(main_sizer)
+ self.Fit()
+
+ self.Bind(wx.EVT_CHAR_HOOK, self.OnEscapeKey)
+
+ def OnEscapeKey(self, event):
+ keycode = event.GetKeyCode()
+ if keycode == wx.WXK_ESCAPE:
+ self.EndModal(wx.ID_CANCEL)
+ else:
+ event.Skip()
+
+ def OptionChecked(self):
+ return self.check.GetValue()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dialogs/UriEditor.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,92 @@
+from __future__ import absolute_import
+
+import wx
+from connectors import ConnectorSchemes, EditorClassFromScheme
+from controls.DiscoveryPanel import DiscoveryPanel
+
+class UriEditor(wx.Dialog):
+ def _init_ctrls(self, parent):
+ self.UriTypeChoice = wx.Choice(parent=self, choices=self.choices)
+ self.UriTypeChoice.SetSelection(0)
+ self.Bind(wx.EVT_CHOICE, self.OnTypeChoice, self.UriTypeChoice)
+ self.editor_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
+
+ def _init_sizers(self):
+ self.mainSizer = wx.BoxSizer(wx.VERTICAL)
+ typeSizer = wx.BoxSizer(wx.HORIZONTAL)
+ typeSizer.Add(wx.StaticText(self, wx.ID_ANY, _("Scheme :")), border=5,
+ flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL)
+ typeSizer.Add(self.UriTypeChoice, border=5, flag=wx.ALL)
+ self.mainSizer.Add(typeSizer)
+
+ self.mainSizer.Add(self.editor_sizer, border=5, flag=wx.ALL)
+ self.mainSizer.Add(self.ButtonSizer, border=5,
+ flag=wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
+ self.SetSizer(self.mainSizer)
+ self.Layout()
+ self.Fit()
+
+ def __init__(self, parent, ctr, uri=''):
+ self.ctr = ctr
+ wx.Dialog.__init__(self,
+ name='UriEditor', parent=parent,
+ title=_('URI Editor'))
+ self.choices = [_("- Search local network -")] + ConnectorSchemes()
+ self._init_ctrls(parent)
+ self._init_sizers()
+ self.scheme = None
+ self.scheme_editor = None
+ self.SetURI(uri)
+ self.CenterOnParent()
+
+ def OnTypeChoice(self, event):
+ index = event.GetSelection()
+ self._replaceSchemeEditor(event.GetString() if index > 0 else None)
+
+ def SetURI(self, uri):
+ try:
+ scheme, loc = uri.strip().split("://",1)
+ scheme = scheme.upper()
+ except:
+ scheme = None
+
+ if scheme in ConnectorSchemes():
+ self.UriTypeChoice.SetStringSelection(scheme)
+ else:
+ self.UriTypeChoice.SetSelection(0)
+
+ self._replaceSchemeEditor(scheme)
+
+ if scheme is not None:
+ self.scheme_editor.SetLoc(loc)
+
+
+ def GetURI(self):
+ if self.scheme is None:
+ return self.scheme_editor.GetURI()
+ else:
+ return self.scheme+"://"+self.scheme_editor.GetLoc()
+
+ def _replaceSchemeEditor(self, scheme):
+ self.scheme = scheme
+
+ if self.scheme_editor is not None:
+ self.editor_sizer.Detach(self.scheme_editor)
+ self.scheme_editor.Destroy()
+ self.scheme_editor = None
+
+ if scheme is not None :
+ EditorClass = EditorClassFromScheme(scheme)
+ self.scheme_editor = EditorClass(scheme,self)
+ else :
+ # None is for searching local network
+ self.scheme_editor = DiscoveryPanel(self)
+
+ self.editor_sizer.Add(self.scheme_editor)
+ self.scheme_editor.Refresh()
+
+ self.editor_sizer.Layout()
+ self.mainSizer.Layout()
+ self.Fit()
+
--- a/dialogs/__init__.py Fri Oct 12 13:24:47 2018 +0300
+++ b/dialogs/__init__.py Fri Nov 23 12:13:24 2018 +0100
@@ -49,4 +49,6 @@
from dialogs.PouActionDialog import PouActionDialog
from dialogs.FindInPouDialog import FindInPouDialog
from dialogs.BrowseValuesLibraryDialog import BrowseValuesLibraryDialog
-from dialogs.DiscoveryDialog import DiscoveryDialog
+from dialogs.UriEditor import UriEditor
+from dialogs.IDManager import IDManager
+
--- a/editors/ConfTreeNodeEditor.py Fri Oct 12 13:24:47 2018 +0300
+++ b/editors/ConfTreeNodeEditor.py Fri Nov 23 12:13:24 2018 +0100
@@ -33,8 +33,8 @@
from IDEFrame import TITLE, FILEMENU, PROJECTTREE, PAGETITLES
-from controls import TextCtrlAutoComplete, UriLocationEditor
-from dialogs import BrowseValuesLibraryDialog
+from controls import TextCtrlAutoComplete
+from dialogs import BrowseValuesLibraryDialog, UriEditor
from util.BitmapLibrary import GetBitmap
if wx.Platform == '__WXMSW__':
@@ -345,7 +345,7 @@
# Get connector uri
uri = CTR_BeremizRoot.getURI_location().strip()
- dialog = UriLocationEditor.UriLocationEditor(CTR_AppFrame, uri)
+ dialog = UriEditor(CTR_AppFrame, CTR, uri)
if dialog.ShowModal() == wx.ID_OK:
CTR_BeremizRoot.setURI_location(dialog.GetURI())
Binary file images/IDManager.png has changed
--- a/images/icons.svg Fri Oct 12 13:24:47 2018 +0300
+++ b/images/icons.svg Fri Nov 23 12:13:24 2018 +0100
@@ -15,7 +15,7 @@
height="1052.3622"
id="svg2"
sodipodi:version="0.32"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ inkscape:version="0.91 r13725"
sodipodi:docname="icons.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<metadata
@@ -31,8 +31,8 @@
</rdf:RDF>
</metadata>
<sodipodi:namedview
- inkscape:window-height="1136"
- inkscape:window-width="1920"
+ inkscape:window-height="874"
+ inkscape:window-width="1600"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10.0"
@@ -43,11 +43,11 @@
pagecolor="#ffffff"
id="base"
showgrid="false"
- inkscape:zoom="2.5873931"
- inkscape:cx="986.14665"
- inkscape:cy="698.07857"
+ inkscape:zoom="16"
+ inkscape:cx="754.13513"
+ inkscape:cy="907.03479"
inkscape:window-x="0"
- inkscape:window-y="27"
+ inkscape:window-y="24"
inkscape:current-layer="svg2"
showguides="true"
inkscape:guide-bbox="true"
@@ -89942,12 +89942,13 @@
y="121.52582"
id="text16266"
xml:space="preserve"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"><tspan
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ sodipodi:linespacing="0%"><tspan
sodipodi:role="line"
id="tspan16268"
x="73.295929"
y="121.52582"
- style="font-size:12.76095104px;line-height:1.25">%% editIECrawcode editWXGLADE editPYTHONcode EditCfile Transfer Connect Disconnect Debug %%</tspan></text>
+ style="font-size:12.76095104px;line-height:1.25">%% editIECrawcode editWXGLADE editPYTHONcode EditCfile Transfer Connect Disconnect Debug IDManager %%</tspan></text>
<rect
width="24"
height="24"
@@ -93315,4 +93316,37 @@
id="tspan16195-3-6"
sodipodi:role="line"
style="font-size:12.76000023px;line-height:1.25">%% fullscreen %%</tspan></text>
+ <rect
+ width="24"
+ height="24"
+ x="730"
+ y="131.36218"
+ id="IDManager"
+ style="display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;enable-background:accumulate"
+ inkscape:label="#rect16270" />
+ <g
+ id="g19473"
+ transform="matrix(1.3198788,0,0,1.3198788,-237.35005,-42.92225)"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d993d;fill-opacity:1;fill-rule:nonzero;stroke:#3d993d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10.43299961;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate">
+ <rect
+ y="140.41789"
+ x="736.44702"
+ height="8.1489973"
+ width="11.106004"
+ id="rect18652"
+ style="fill:#3d993d;fill-opacity:1;fill-rule:nonzero;stroke:#3d993d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10.43299961;stroke-dasharray:none;stroke-opacity:1;color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;stroke-dashoffset:0;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d993d;fill-opacity:1;fill-rule:nonzero;stroke:#3d993d;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10.43299961;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0"
+ d="m 741.99023,134.18555 c -1.03169,0.0327 -2.08009,0.38311 -2.90625,1.11328 -0.82615,0.73017 -1.38416,1.85745 -1.41796,3.20898 l -0.002,0.0117 0,2.03516 2,0 0,-1.99805 c 0.0216,-0.86174 0.3173,-1.38252 0.74414,-1.75976 0.42684,-0.37725 1.03088,-0.59378 1.64649,-0.61329 0.61561,-0.0195 1.21879,0.1637 1.62304,0.47461 0.40425,0.31092 0.64912,0.70425 0.65821,1.37305 l 0,2.66211 2,0 0,-2.68359 0,-0.006 c -0.0168,-1.23751 -0.60543,-2.28823 -1.43946,-2.92969 -0.83402,-0.64146 -1.87455,-0.92136 -2.90625,-0.88867 z"
+ id="path18654"
+ inkscape:connector-curvature="0" />
+ </g>
+ <rect
+ inkscape:label="#rect16270"
+ style="display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#6e6e6e;stroke-width:0.1;marker:none;enable-background:accumulate;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect19460"
+ y="131.36218"
+ x="730"
+ height="24"
+ width="24" />
</svg>
--- a/modbus/modbus.py Fri Oct 12 13:24:47 2018 +0300
+++ b/modbus/modbus.py Fri Nov 23 12:13:24 2018 +0100
@@ -789,7 +789,7 @@
LDFLAGS = []
LDFLAGS.append(" \"-L" + ModbusPath + "\"")
- LDFLAGS.append(os.path.join(ModbusPath, "libmb.a"))
+ LDFLAGS.append(" \"" + os.path.join(ModbusPath, "libmb.a") + "\"")
LDFLAGS.append(" \"-Wl,-rpath," + ModbusPath + "\"")
# LDFLAGS.append("\"" + os.path.join(ModbusPath, "mb_slave_and_master.o") + "\"")
# LDFLAGS.append("\"" + os.path.join(ModbusPath, "mb_slave.o") + "\"")
--- a/py_ext/plc_python.c Fri Oct 12 13:24:47 2018 +0300
+++ b/py_ext/plc_python.c Fri Nov 23 12:13:24 2018 +0100
@@ -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);
--- a/runtime/NevowServer.py Fri Oct 12 13:24:47 2018 +0300
+++ b/runtime/NevowServer.py Fri Nov 23 12:13:24 2018 +0100
@@ -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)
@@ -305,11 +333,11 @@
# print "We will be called back when the client disconnects"
-def RegisterWebsite(port):
+def RegisterWebsite(iface, port):
website = WebInterface()
site = appserver.NevowSite(website)
- reactor.listenTCP(port, site)
+ reactor.listenTCP(port, site, interface=iface)
print(_('HTTP interface port :'), port)
return website
--- a/runtime/PLCObject.py Fri Oct 12 13:24:47 2018 +0300
+++ b/runtime/PLCObject.py Fri Nov 23 12:13:24 2018 +0100
@@ -23,7 +23,7 @@
from __future__ import absolute_import
-from threading import Thread, Lock, Semaphore, Event, Condition
+from threading import Thread, Lock, Semaphore, Event
import ctypes
import os
import sys
@@ -37,7 +37,9 @@
from runtime.typemapping import TypeTranslator
from runtime.loglevels import LogLevelsDefault, LogLevelsCount
+from runtime.Stunnel import getPSKID
from runtime import PlcStatus
+from runtime import MainWorker
if os.name in ("nt", "ce"):
dlopen = _ctypes.LoadLibrary
@@ -64,133 +66,25 @@
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:
- exc_type = _job.exc_info[0]
- exc_value = _job.exc_info[1]
- exc_traceback = _job.exc_info[2]
- six.reraise(exc_type, exc_value, exc_traceback)
-
- 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):
return MainWorker.call(func, *args, **kwargs)
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 = 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
@@ -198,7 +92,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(
@@ -206,10 +100,15 @@
"r").read().strip() + lib_ext
if self.LoadPLC():
self.PLCStatus = PlcStatus.Stopped
+ if autostart:
+ self.StartPLC()
+ return
except Exception:
self.PLCStatus = PlcStatus.Empty
self.CurrentPLCFilename = None
+ self.StatusChange()
+
def StatusChange(self):
if self.statuschange is not None:
for callee in self.statuschange:
@@ -545,6 +444,10 @@
return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
@RunInMain
+ def GetPLCID(self):
+ return getPSKID()
+
+ @RunInMain
def NewPLC(self, md5sum, data, extrafiles):
if self.PLCStatus in [PlcStatus.Stopped, PlcStatus.Empty, PlcStatus.Broken]:
NewFileName = md5sum + lib_ext
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/PyroServer.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,78 @@
+#!/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.
+
+from __future__ import absolute_import
+from __future__ import print_function
+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 not in ["", "localhost", "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)
--- a/runtime/ServicePublisher.py Fri Oct 12 13:24:47 2018 +0300
+++ b/runtime/ServicePublisher.py Fri Nov 23 12:13:24 2018 +0100
@@ -56,11 +56,15 @@
self.name = name
self.port = port
- self.server = zeroconf.Zeroconf()
- print("MDNS brodcasting on :" + ip)
+ if ip == "0.0.0.0":
+ print("MDNS brodcasted on all interfaces")
+ interfaces=zeroconf.InterfaceChoice.All
+ ip = self.gethostaddr()
+ else:
+ interfaces=[ip]
- if ip == "0.0.0.0":
- ip = self.gethostaddr()
+ self.server = zeroconf.Zeroconf(interfaces=interfaces)
+
print("MDNS brodcasted service address :" + ip)
self.ip_32b = socket.inet_aton(ip)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/Stunnel.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,41 @@
+import os
+from binascii import b2a_hqx
+try:
+ from runtime.spawn_subprocess import call
+except ImportError:
+ from subprocess import call
+
+restart_stunnel_cmdline = ["/etc/init.d/S50stunnel","restart"]
+
+_PSKpath = None
+
+def PSKgen(ID, PSKpath):
+
+ # b2a_hqx output len is 4/3 input len
+ secret = os.urandom(192) # int(256/1.3333)
+ secretstring = b2a_hqx(secret)
+
+ PSKstring = ID+":"+secretstring
+ with open(PSKpath, 'w') as f:
+ f.write(PSKstring)
+ call(restart_stunnel_cmdline)
+
+def ensurePSK(ID, PSKpath):
+ global _PSKpath
+ _PSKpath = PSKpath
+ # check if already there
+ if not os.path.exists(PSKpath):
+ # create if needed
+ PSKgen(ID, PSKpath)
+
+def getPSKID():
+ if _PSKpath is not None :
+ if not os.path.exists(_PSKpath):
+ confnodesroot.logger.write_error(
+ 'Error: Pre-Shared-Key Secret in %s is missing!\n' % _PSKpath)
+ return None
+ ID,_sep,PSK = open(_PSKpath).read().partition(':')
+ PSK = PSK.rstrip('\n\r')
+ return (ID,PSK)
+ return None
+
--- a/runtime/WampClient.py Fri Oct 12 13:24:47 2018 +0300
+++ b/runtime/WampClient.py Fri Nov 23 12:13:24 2018 +0100
@@ -33,13 +33,13 @@
from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
from autobahn.wamp import types, auth
from autobahn.wamp.serializer import MsgPackSerializer
-from twisted.internet.defer import inlineCallbacks
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.python.components import registerAdapter
from formless import annotate, webform
import formless
from nevow import tags, url, static
+from runtime import GetPLCObjectSingleton
mandatoryConfigItems = ["ID", "active", "realm", "url"]
@@ -88,7 +88,7 @@
def GetCallee(name):
""" Get Callee or Subscriber corresponding to '.' spearated object path """
names = name.split('.')
- obj = _PySrv.plcobj
+ obj = GetPLCObjectSingleton()
while names:
obj = getattr(obj, names.pop(0))
return obj
@@ -116,7 +116,6 @@
raise Exception(
"don't know how to handle authmethod {}".format(challenge.method))
- @inlineCallbacks
def onJoin(self, details):
global _WampSession
_WampSession = self
@@ -129,13 +128,13 @@
registerOptions = None
print(_("TypeError register option: {}".format(e)))
- yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
+ self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
for name in SubscribedEvents:
- yield self.subscribe(GetCallee(name), text(name))
+ self.subscribe(GetCallee(name), unicode(name))
for func in DoOnJoin:
- yield func(self)
+ func(self)
print(_('WAMP session joined (%s) by:' % time.ctime()), ID)
@@ -146,6 +145,10 @@
_transportFactory = None
print(_('WAMP session left'))
+ def publishWithOwnID(self, eventID, value):
+ ID = self.config.extra["ID"]
+ self.publish(unicode(ID+'.'+eventID), value)
+
class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory):
@@ -343,6 +346,16 @@
_PySrv = pysrv
+def PublishEvent(eventID, value):
+ if getWampStatus() == "Attached":
+ _WampSession.publish(unicode(eventID), value)
+
+
+def PublishEventWithOwnID(eventID, value):
+ if getWampStatus() == "Attached":
+ _WampSession.publishWithOwnID(unicode(eventID), value)
+
+
# WEB CONFIGURATION INTERFACE
WAMP_SECRET_URL = "secret"
webExposedConfigItems = ['active', 'url', 'ID']
@@ -428,7 +441,8 @@
def RegisterWebSettings(NS):
- NS.ConfigurableSettings.addExtension(
+
+ NS.ConfigurableSettings.addSettings(
"wamp",
_("Wamp Settings"),
webFormInterface,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/Worker.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,120 @@
+#!/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 sys
+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 = job(*args, **kwargs)
+ _job.do()
+ if _job.success:
+ # result is ignored
+ pass
+ else:
+ raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2]
+ 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:
+ exc_type = _job.exc_info[0]
+ exc_value = _job.exc_info[1]
+ exc_traceback = _job.exc_info[2]
+ six.reraise(exc_type, exc_value, exc_traceback)
+
+ 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()
--- a/runtime/__init__.py Fri Oct 12 13:24:47 2018 +0300
+++ b/runtime/__init__.py Fri Nov 23 12:13:24 2018 +0100
@@ -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
+from __future__ import print_function
+import traceback
+import sys
-# 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
+_PLCObjectSingleton = None
-from __future__ import absolute_import
-import os
-from runtime.PLCObject import PLCObject, PLCprint, MainWorker
-import runtime.ServicePublisher
+def GetPLCObjectSingleton():
+ assert _PLCObjectSingleton is not None
+ return _PLCObjectSingleton
+
+
+def LogMessageAndException(msg, exp=None):
+ if exp is None:
+ exp = sys.exc_info()
+ if _PLCObjectSingleton is not None:
+ _PLCObjectSingleton.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp)))
+ print(msg)
+ traceback.print_exception(*exp)
+
+
+def CreatePLCObjectSingleton(*args, **kwargs):
+ global _PLCObjectSingleton
+ from runtime.PLCObject import PLCObject # noqa # pylint: disable=wrong-import-position
+ _PLCObjectSingleton = PLCObject(*args, **kwargs)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/spawn_subprocess.py Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# subset of subprocess built-in module using posix_spawn rather than fork.
+
+import posix_spawn
+import os
+import signal
+
+PIPE = "42"
+
+class Popen(object):
+ def __init__(self, args,stdin=None, stdout=None):
+ self.returncode = None
+ self.stdout = None
+ self.stdin = None
+ # TODO: stderr
+ file_actions = posix_spawn.FileActions()
+ if stdout is not None:
+ # child's stdout, child 2 parent pipe
+ c2pread, c2pwrite = os.pipe()
+ # attach child's stdout to writing en of c2p pipe
+ file_actions.add_dup2(c2pwrite, 1)
+ # close other end
+ file_actions.add_close(c2pread)
+ if stdin is not None:
+ # child's stdin, parent to child pipe
+ p2cread, p2cwrite = os.pipe()
+ # attach child's stdin to reading en of p2c pipe
+ file_actions.add_dup2(p2cread, 0)
+ # close other end
+ file_actions.add_close(p2cwrite)
+ self.pid = posix_spawn.posix_spawnp(args[0], args, file_actions=file_actions)
+ if stdout is not None:
+ self.stdout = os.fdopen(c2pread)
+ os.close(c2pwrite)
+ if stdin is not None:
+ self.stdin = os.fdopen(p2cwrite, 'w')
+ os.close(p2cread)
+
+ def _wait(self):
+ if self.returncode is None:
+ self.returncode = os.waitpid(self.pid,0)[1]
+
+ def communicate(self):
+ 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 = ""
+
+ self._wait()
+ if self.stdout is not None:
+ self.stdout.close()
+ self.stdout = 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
+ return self.returncode
+
+ def poll(self):
+ if self.returncode is None:
+ pid, ret = os.waitpid(self.pid, os.WNOHANG)
+ if (pid,ret) != (0,0):
+ self.returncode = ret
+
+ if self.stdin is not None:
+ self.stdin.close()
+ self.stdin = None
+ if self.stdout is not None:
+ self.stdout.close()
+ self.stdout = None
+
+ return self.returncode
+
+ def kill(self):
+ os.kill(self.pid, signal.SIGKILL)
+
+ if self.stdin is not None:
+ self.stdin.close()
+ self.stdin = None
+ if self.stdout is not None:
+ self.stdout.close()
+ self.stdout = None
+
+
+
+def call(*args):
+ cmd = []
+ if isinstance(args[0], str):
+ if len(args)==1:
+ # oversimplified splitting of arguments,
+ # TODO: care about use of simple and double quotes
+ cmd = args[0].split()
+ else:
+ cmd = args
+ elif isinstance(args[0], list) and len(args)==1:
+ cmd = args[0]
+ else:
+ raise Exception("Wrong arguments passed to subprocess.call")
+ pid = posix_spawn.posix_spawnp(cmd[0], cmd)
+ return os.waitpid(pid,0)
+
+if __name__ == '__main__':
+ # unit test
+
+ p = Popen(["tr", "abc", "def"], stdin=PIPE, stdout=PIPE)
+ p.stdin.write("blah")
+ p.stdin.close()
+ print p.stdout.read()
+ p.wait()
+
+ p = Popen(["tr", "abc", "def"], stdin=PIPE, stdout=PIPE)
+ p.stdin.write("blah")
+ print p.communicate()
+
+ call("echo blah0")
+ call(["echo", "blah1"])
+ call("echo", "blah2")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/webinterface.css Fri Nov 23 12:13:24 2018 +0100
@@ -0,0 +1,6 @@
+
+.freeform-label {
+ float: left;
+ width: 30%;
+}
+
--- a/runtime_files.list Fri Oct 12 13:24:47 2018 +0300
+++ b/runtime_files.list Fri Nov 23 12:13:24 2018 +0100
@@ -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