--- a/BeremizIDE.py Tue Mar 05 11:43:20 2019 +0300
+++ b/BeremizIDE.py Mon Mar 11 13:51:07 2019 +0100
@@ -25,6 +25,7 @@
from __future__ import absolute_import
+from __future__ import print_function
import os
import sys
import tempfile
@@ -208,10 +209,6 @@
def write_error(self, s):
self.write(s, self.red_yellow)
- def writeyield(self, s):
- self.write(s)
- wx.GetApp().Yield()
-
def flush(self):
# Temporary deactivate read only mode on StyledTextCtrl for clearing
# text. It seems that text modifications, even programmatically, are
@@ -614,11 +611,7 @@
def AddToDoBeforeQuit(self, Thing):
self.ToDoBeforeQuit.append(Thing)
- def OnCloseFrame(self, event):
- for evt_type in [wx.EVT_SET_FOCUS,
- wx.EVT_KILL_FOCUS,
- wx.stc.EVT_STC_UPDATEUI]:
- self.LogConsole.Unbind(evt_type)
+ def TryCloseFrame(self):
if self.CTR is None or self.CheckSaveBeforeClosing(_("Close Application")):
if self.CTR is not None:
self.CTR.KillDebugThread()
@@ -630,8 +623,14 @@
Thing()
self.ToDoBeforeQuit = []
+ return True
+ return False
+
+ def OnCloseFrame(self, event):
+ if self.TryCloseFrame():
event.Skip()
else:
+ # prevent event to continue, i.e. cancel closing
event.Veto()
def RefreshFileMenu(self):
--- a/Beremiz_service.py Tue Mar 05 11:43:20 2019 +0300
+++ b/Beremiz_service.py Mon Mar 11 13:51:07 2019 +0100
@@ -30,17 +30,18 @@
import sys
import getopt
import threading
-from threading import Thread, Semaphore, Lock
-import traceback
+from threading import Thread, Semaphore, Lock, currentThread
+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 PyroServer
from runtime.xenomai import TryPreloadXenomai
+from runtime import LogMessageAndException
from runtime import PlcStatus
+from runtime.Stunnel import ensurePSK
import util.paths as paths
@@ -54,7 +55,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 +64,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 +80,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 +102,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 +125,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 +147,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 +189,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 +257,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 +284,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 +310,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 +340,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 +393,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():
@@ -511,10 +415,11 @@
if havewx:
reactor.registerWxApp(app)
+twisted_reactor_thread_id = None
+ui_thread = None
+
if havewx:
wx_eval_lock = Semaphore(0)
- # FIXME : beware wx mainloop is _not_ running in main thread
- # main_thread = currentThread()
def statuschangeTskBar(status):
wx.CallAfter(taskbar_instance.UpdateIcon, status)
@@ -527,40 +432,30 @@
wx_eval_lock.release()
def evaluator(tocall, *args, **kwargs):
- # FIXME : should implement anti-deadlock
- # if main_thread == currentThread():
- # # avoid dead lock if called from the wx mainloop
- # return default_evaluator(tocall, *args, **kwargs)
- # else:
- o = type('', (object,), dict(call=(tocall, args, kwargs), res=None))
- 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)
+ # To prevent deadlocks, check if current thread is not one of the UI
+ # UI threads can be either the one from WX main loop or
+ # worker thread from twisted "threadselect" reactor
+ current_id = currentThread().ident
+
+ if ui_thread is not None \
+ and ui_thread.ident != current_id \
+ and (not havetwisted or (
+ twisted_reactor_thread_id is not None
+ and twisted_reactor_thread_id != current_id)):
+
+ o = type('', (object,), dict(call=(tocall, args, kwargs), res=None))
+ wx.CallAfter(wx_evaluator, o)
+ wx_eval_lock.acquire()
+ return o.res
+ else:
+ # avoid dead lock if called from the wx mainloop
+ return default_evaluator(tocall, *args, **kwargs)
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 +505,24 @@
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:
+ ensurePSK(servicename, PSKpath)
+
+runtime.CreatePLCObjectSingleton(
+ WorkingDir, argv, statuschange, evaluator, pyruntimevars)
+
+pyroserver = PyroServer(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 +532,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 +548,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()
@@ -656,7 +570,11 @@
# This order ui loop to unblock main thread when ready.
if havetwisted:
- reactor.callLater(0, ui_thread_started.release)
+ def signal_uithread_started():
+ global twisted_reactor_thread_id
+ twisted_reactor_thread_id = currentThread().ident
+ ui_thread_started.release()
+ reactor.callLater(0, signal_uithread_started)
else:
wx.CallAfter(ui_thread_started.release)
@@ -665,9 +583,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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,160 @@
+#!/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 _ensurePSKdir(project_path):
+ pskpath = _pskpath(project_path)
+ if not os.path.exists(pskpath):
+ os.mkdir(pskpath)
+ return pskpath
+
+
+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):
+ _ensurePSKdir(project_path)
+ with open(_mgtpath(project_path), 'w') as f:
+ f.write(json.dumps(data))
+
+
+def UpdateID(project_path, ID, secret, URI):
+ pskpath = _ensurePSKdir(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, None) if data else None
+
+ _is_new_ID = dataForID is None
+ if _is_new_ID:
+ dataForID = _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')
+
+ if _is_new_ID:
+ data.append(dataForID)
+
+ 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 Tue Mar 05 11:43:20 2019 +0300
+++ b/ProjectController.py Mon Mar 11 13:51:07 2019 +0100
@@ -56,7 +56,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
@@ -269,8 +269,10 @@
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]
+ self.DebugToken = None
+ self.debug_status = PlcStatus.Stopped
def __del__(self):
if self.DebugTimer:
@@ -280,10 +282,14 @@
def LoadLibraries(self):
self.Libraries = []
TypeStack = []
- for libname, clsname, _default in features.libraries:
- if self.BeremizRoot.Libraries is not None and \
- getattr(self.BeremizRoot.Libraries,
- "Enable_" + libname + "_Library"):
+ for libname, clsname, lib_enabled in features.libraries:
+ if self.BeremizRoot.Libraries is not None:
+ enable_attr = getattr(self.BeremizRoot.Libraries,
+ "Enable_" + libname + "_Library")
+ if enable_attr is not None:
+ lib_enabled = enable_attr
+
+ if lib_enabled:
Lib = GetClassImporter(clsname)()(self, libname, TypeStack)
TypeStack.append(Lib.GetTypes())
self.Libraries.append(Lib)
@@ -897,19 +903,6 @@
self._builder = targetclass(self)
return self._builder
- def ResetBuildMD5(self):
- builder = self.GetBuilder()
- if builder is not None:
- builder.ResetBinaryCodeMD5()
- self.EnableMethod("_Transfer", False)
-
- def GetLastBuildMD5(self):
- builder = self.GetBuilder()
- if builder is not None:
- return builder.GetBinaryCodeMD5()
- else:
- return None
-
#
#
# C CODE GENERATION METHODS
@@ -1137,7 +1130,6 @@
# If IEC code gen fail, bail out.
if not IECGenRes:
self.logger.write_error(_("PLC code generation failed !\n"))
- self.ResetBuildMD5()
return False
# Reset variable and program list that are parsed from
@@ -1153,7 +1145,6 @@
builder = self.GetBuilder()
if builder is None:
self.logger.write_error(_("Fatal : cannot get builder.\n"))
- self.ResetBuildMD5()
return False
# Build
@@ -1162,9 +1153,9 @@
self.logger.write_error(_("C Build failed.\n"))
return False
except Exception:
+ builder.ResetBinaryMD5()
self.logger.write_error(_("C Build crashed !\n"))
self.logger.write_error(traceback.format_exc())
- self.ResetBuildMD5()
return False
self.logger.write(_("Successfully built.\n"))
@@ -1184,7 +1175,6 @@
self.logger.write_error(
_("Runtime IO extensions C code generation failed !\n"))
self.logger.write_error(traceback.format_exc())
- self.ResetBuildMD5()
return False
# Generate C code and compilation params from liraries
@@ -1195,7 +1185,6 @@
self.logger.write_error(
_("Runtime library extensions C code generation failed !\n"))
self.logger.write_error(traceback.format_exc())
- self.ResetBuildMD5()
return False
self.LocationCFilesAndCFLAGS = LibCFilesAndCFLAGS + \
@@ -1245,7 +1234,6 @@
except Exception:
self.logger.write_error(name + _(" generation failed !\n"))
self.logger.write_error(traceback.format_exc())
- self.ResetBuildMD5()
return False
self.logger.write(_("C code generated successfully.\n"))
return True
@@ -1270,6 +1258,11 @@
_IECCodeView = None
+ def _showIDManager(self):
+ dlg = IDManager(self.AppFrame, self)
+ dlg.ShowModal()
+ dlg.Destroy()
+
def _showIECcode(self):
self._OpenView("IEC code")
@@ -1498,11 +1491,12 @@
self.UpdateMethodsFromPLCStatus()
def SnapshotAndResetDebugValuesBuffers(self):
- if self._connector is not None:
- plc_status, Traces = self._connector.GetTraceVariables()
+ debug_status = PlcStatus.Disconnected
+ if self._connector is not None and self.DebugToken is not None:
+ debug_status, Traces = self._connector.GetTraceVariables(self.DebugToken)
# print [dict.keys() for IECPath, (dict, log, status, fvalue) in
# self.IECdebug_datas.items()]
- if plc_status == PlcStatus.Started:
+ if debug_status == PlcStatus.Started:
if len(Traces) > 0:
for debug_tick, debug_buff in Traces:
debug_vars = UnpackDebugBuffer(
@@ -1530,14 +1524,14 @@
ticks, self.DebugTicks = self.DebugTicks, []
- return ticks, buffers
+ return debug_status, ticks, buffers
def RegisterDebugVarToConnector(self):
self.DebugTimer = None
Idxs = []
self.TracedIECPath = []
self.TracedIECTypes = []
- if self._connector is not None:
+ if self._connector is not None and self.debug_status != PlcStatus.Broken:
IECPathsToPop = []
for IECPath, data_tuple in self.IECdebug_datas.iteritems():
WeakCallableDict, _data_log, _status, fvalue, _buffer_list = data_tuple
@@ -1566,11 +1560,12 @@
IdxsT = zip(*Idxs)
self.TracedIECPath = IdxsT[3]
self.TracedIECTypes = IdxsT[1]
- self._connector.SetTraceVariablesList(zip(*IdxsT[0:3]))
+ self.DebugToken = self._connector.SetTraceVariablesList(zip(*IdxsT[0:3]))
else:
self.TracedIECPath = []
self._connector.SetTraceVariablesList([])
- self.SnapshotAndResetDebugValuesBuffers()
+ self.DebugToken = None
+ self.debug_status, _debug_ticks, _buffers = self.SnapshotAndResetDebugValuesBuffers()
def IsPLCStarted(self):
return self.previous_plcstate == PlcStatus.Started
@@ -1686,7 +1681,7 @@
return self._connector.RemoteExec(script, **kwargs)
def DispatchDebugValuesProc(self, event):
- debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
+ self.debug_status, debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
start_time = time.time()
if len(self.TracedIECPath) == len(buffers):
for IECPath, values in zip(self.TracedIECPath, buffers):
@@ -1697,11 +1692,15 @@
self.CallWeakcallables(
"__tick__", "NewDataAvailable", debug_ticks)
- delay = time.time() - start_time
- next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
- if self.DispatchDebugValuesTimer is not None:
- self.DispatchDebugValuesTimer.Start(
- int(next_refresh * 1000), oneShot=True)
+ if self.debug_status == PlcStatus.Broken:
+ self.logger.write_warning(
+ _("Debug: token rejected - other debug took over - reconnect to recover\n"))
+ else:
+ delay = time.time() - start_time
+ next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
+ if self.DispatchDebugValuesTimer is not None:
+ self.DispatchDebugValuesTimer.Start(
+ int(next_refresh * 1000), oneShot=True)
event.Skip()
def KillDebugThread(self):
@@ -1712,6 +1711,9 @@
self.previous_plcstate = None
if self.AppFrame:
self.AppFrame.ResetGraphicViewers()
+
+ self.debug_status = PlcStatus.Started
+
self.RegisterDebugVarToConnector()
if self.DispatchDebugValuesTimer is not None:
self.DispatchDebugValuesTimer.Start(
@@ -1748,7 +1750,6 @@
if connector is not None:
if self.StatusTimer is not None:
# Start the status Timer
- wx.Yield()
self.StatusTimer.Start(milliseconds=500, oneShot=False)
else:
if self.StatusTimer is not None:
@@ -1771,9 +1772,9 @@
if uri == "":
try:
# Launch Service Discovery dialog
- dialog = DiscoveryDialog(self.AppFrame)
+ dialog = UriEditor(self.AppFrame, self)
answer = dialog.ShowModal()
- uri = dialog.GetURI()
+ uri = str(dialog.GetURI())
dialog.Destroy()
except Exception:
self.logger.write_error(_("Local service discovery failed!\n"))
@@ -1798,10 +1799,10 @@
# Get connector from uri
try:
self._SetConnector(connectors.ConnectorFactory(uri, self))
- except Exception:
+ except Exception as e:
self.logger.write_error(
- _("Exception while connecting %s!\n") % uri)
- self.logger.write_error(traceback.format_exc())
+ _("Exception while connecting to '{uri}': {ex}\n").format(
+ uri=uri, ex=e))
# Did connection success ?
if self._connector is None:
@@ -1823,26 +1824,20 @@
def CompareLocalAndRemotePLC(self):
if self._connector is None:
return
- # We are now connected. Update button status
- MD5 = self.GetLastBuildMD5()
+ builder = self.GetBuilder()
+ if builder is None:
+ return
+ MD5 = builder.GetBinaryMD5()
+ if MD5 is None:
+ return
# Check remote target PLC correspondance to that md5
- if MD5 is not None:
- if not self._connector.MatchMD5(MD5):
- # self.logger.write_warning(
- # _("Latest build does not match with target, please
- # transfer.\n"))
- self.EnableMethod("_Transfer", True)
- else:
- # self.logger.write(
- # _("Latest build matches target, no transfer needed.\n"))
- self.EnableMethod("_Transfer", True)
- # warns controller that program match
- self.ProgramTransferred()
- # self.EnableMethod("_Transfer", False)
+ if self._connector.MatchMD5(MD5):
+ self.logger.write(
+ _("Latest build matches with connected target.\n"))
+ self.ProgramTransferred()
else:
- # self.logger.write_warning(
- # _("Cannot compare latest build to target. Please build.\n"))
- self.EnableMethod("_Transfer", False)
+ self.logger.write(
+ _("Latest build does not match with connected target.\n"))
def _Disconnect(self):
self._SetConnector(None)
@@ -1858,8 +1853,13 @@
else:
return
- # Get the last build PLC's
- MD5 = self.GetLastBuildMD5()
+ builder = self.GetBuilder()
+ if builder is None:
+ self.logger.write_error(_("Fatal : cannot get builder.\n"))
+ return False
+
+ # recover md5 from last build
+ MD5 = builder.GetBinaryMD5()
# Check if md5 file is empty : ask user to build PLC
if MD5 is None:
@@ -1872,35 +1872,44 @@
self.logger.write(
_("Latest build already matches current target. Transfering anyway...\n"))
- # Get temprary directory path
+ # purge any non-finished transfer
+ # note: this would abord any runing transfer with error
+ self._connector.PurgeBlobs()
+
+ # transfer extra files
extrafiles = []
for extrafilespath in [self._getExtraFilesPath(),
self._getProjectFilesPath()]:
- extrafiles.extend(
- [(name, open(os.path.join(extrafilespath, name),
- 'rb').read())
- for name in os.listdir(extrafilespath)])
+ for name in os.listdir(extrafilespath):
+ extrafiles.append((
+ name,
+ self._connector.BlobFromFile(
+ # use file name as a seed to avoid collisions
+ # with files having same content
+ os.path.join(extrafilespath, name), name)))
# Send PLC on target
- builder = self.GetBuilder()
- if builder is not None:
- data = builder.GetBinaryCode()
- if data is not None:
- if self._connector.NewPLC(MD5, data, extrafiles) and self.GetIECProgramsAndVariables():
- self.UnsubscribeAllDebugIECVariable()
- self.ProgramTransferred()
- if self.AppFrame is not None:
- self.AppFrame.CloseObsoleteDebugTabs()
- self.AppFrame.RefreshPouInstanceVariablesPanel()
- self.logger.write(_("Transfer completed successfully.\n"))
- self.AppFrame.LogViewer.ResetLogCounters()
- else:
- self.logger.write_error(_("Transfer failed\n"))
- self.HidePLCProgress()
+ object_path = builder.GetBinaryPath()
+ # arbitrarily use MD5 as a seed, could be any string
+ object_blob = self._connector.BlobFromFile(object_path, MD5)
+
+ self.HidePLCProgress()
+
+ self.logger.write(_("PLC data transfered successfully.\n"))
+
+ if self._connector.NewPLC(MD5, object_blob, extrafiles):
+ if self.GetIECProgramsAndVariables():
+ self.UnsubscribeAllDebugIECVariable()
+ self.ProgramTransferred()
+ self.AppFrame.CloseObsoleteDebugTabs()
+ self.AppFrame.RefreshPouInstanceVariablesPanel()
+ self.AppFrame.LogViewer.ResetLogCounters()
+ self.logger.write(_("PLC installed successfully.\n"))
else:
- self.logger.write_error(
- _("No PLC to transfer (did build succeed ?)\n"))
+ self.logger.write_error(_("Missing debug data\n"))
+ else:
+ self.logger.write_error(_("PLC couldn't be installed\n"))
wx.CallAfter(self.UpdateMethodsFromPLCStatus)
@@ -1953,6 +1962,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 Tue Mar 05 11:43:20 2019 +0300
+++ b/bacnet/runtime/device.c Mon Mar 11 13:51:07 2019 +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 Tue Mar 05 11:43:20 2019 +0300
+++ b/bacnet/runtime/server.c Mon Mar 11 13:51:07 2019 +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/ConnectorBase.py Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import md5
+
+
+class ConnectorBase(object):
+
+ chuncksize = 1024*1024
+
+ def BlobFromFile(self, filepath, seed):
+ s = md5.new()
+ s.update(seed)
+ blobID = self.SeedBlob(seed)
+ with open(filepath, "rb") as f:
+ while blobID == s.digest():
+ chunk = f.read(self.chuncksize)
+ if len(chunk) == 0:
+ return blobID
+ blobID = self.AppendChunkToBlob(chunk, blobID)
+ s.update(chunk)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/PYRO/PSK_Adapter.py Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,94 @@
+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 Tue Mar 05 11:43:20 2019 +0300
+++ b/connectors/PYRO/__init__.py Mon Mar 11 13:51:07 2019 +0100
@@ -36,9 +36,9 @@
import Pyro.util
from Pyro.errors import PyroError
+import PSKManagement as PSK
from runtime import PlcStatus
-service_type = '_PYRO._tcp.local.'
# this module attribute contains a list of DNS-SD (Zeroconf) service types
# supported by this connector confnode.
#
@@ -52,66 +52,35 @@
"""
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 # pylint: disable=wrong-import-order,unused-import,wrong-import-position
+ 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:
- try:
- from zeroconf import Zeroconf
- r = Zeroconf()
- i = r.get_service_info(service_type, location)
- if i is None:
- raise Exception("'%s' not found" % location)
- ip = str(socket.inet_ntoa(i.address))
- port = str(i.port)
- newlocation = ip + ':' + port
- confnodesroot.logger.write(_("'{a1}' is located at {a2}\n").format(a1=location, a2=newlocation))
- location = newlocation
- r.close()
- except Exception:
- confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n") % location)
- confnodesroot.logger.write_error(traceback.format_exc())
- return None
# Try to get the proxy object
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())
+ except Exception, e:
+ confnodesroot.logger.write_error(
+ _("Connection to {loc} failed with exception {ex}\n").format(
+ loc=location, exo=str(e)))
return None
+ RemotePLCObjectProxy.adapter.setTimeout(60)
+
def PyroCatcher(func, default=None):
"""
A function that catch a Pyro exceptions, write error to logger
@@ -136,13 +105,16 @@
# 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!")
}
@@ -161,4 +133,4 @@
self.__dict__[attrName] = member
return member
- return PyroProxyProxy()
+ return PyroProxyProxy
--- a/connectors/PYRO/dialog.py Tue Mar 05 11:43:20 2019 +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 Mon Mar 11 13:51:07 2019 +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)
+
+ # pylint: disable=unused-variable
+ 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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+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 Tue Mar 05 11:43:20 2019 +0300
+++ b/connectors/WAMP/__init__.py Mon Mar 11 13:51:07 2019 +0100
@@ -27,8 +27,9 @@
from __future__ import print_function
import sys
import traceback
+from functools import partial
from threading import Thread, Event
-from builtins import str as text
+from six import text_type as text
from twisted.internet import reactor, threads
from autobahn.twisted import wamp
@@ -66,15 +67,15 @@
}
-def WAMP_connector_factory(uri, confnodesroot):
+def _WAMP_connector_factory(cls, uri, confnodesroot):
"""
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():
@@ -88,7 +89,7 @@
extra={"ID": ID})
session_factory = wamp.ApplicationSessionFactory(
config=component_config)
- session_factory.session = WampSession
+ session_factory.session = cls
# create a WAMP-over-WebSocket transport client factory
transport_factory = WampWebSocketClientFactory(
@@ -153,10 +154,10 @@
self.__dict__[attrName] = member
return member
- # Try to get the proxy object
- try:
- return WampPLCObjectProxy()
- except Exception:
- confnodesroot.logger.write_error(_("WAMP connection to '%s' failed.\n") % location)
- confnodesroot.logger.write_error(traceback.format_exc())
- return None
+ # TODO : GetPLCID()
+ # TODO : PSK.UpdateID()
+
+ return WampPLCObjectProxy
+
+
+WAMP_connector_factory = partial(_WAMP_connector_factory, WampSession)
--- a/connectors/WAMP/dialog.py Tue Mar 05 11:43:20 2019 +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 Mon Mar 11 13:51:07 2019 +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
+
+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)
+
+ # pylint: disable=unused-variable
+ 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 Tue Mar 05 11:43:20 2019 +0300
+++ b/connectors/__init__.py Mon Mar 11 13:51:07 2019 +0100
@@ -29,33 +29,37 @@
from __future__ import absolute_import
from os import listdir, path
import util.paths as paths
+from connectors.ConnectorBase import ConnectorBase
+from types import ClassType
-_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 +67,67 @@
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()
+
+ # commented code to enable for MDNS:// support
+ # _scheme, location = uri.split("://")
+ # _scheme = _scheme.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:
- pass
- elif servicetype[-1] == 'S' and servicetype[:-1] in connectors:
- servicetype = servicetype[:-1]
+
+ # commented code to enable for MDNS:// support
+ # elif _scheme == "MDNS":
+ # try:
+ # from zeroconf import Zeroconf
+ # r = Zeroconf()
+ # 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))
+ # port = str(i.port)
+ # newlocation = ip + ':' + port
+ # confnodesroot.logger.write(_("'{a1}' is located at {a2}\n").format(a1=location, a2=newlocation))
+ # location = newlocation
+ # # not a bug, but a workaround against obvious downgrade attack
+ # scheme = "PYROS"
+ # r.close()
+ # except Exception:
+ # confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n") % location)
+ # confnodesroot.logger.write_error(traceback.format_exc())
+ # return None
+
+ elif _scheme in connectors:
+ scheme = _scheme
+ elif _scheme[-1] == 'S' and _scheme[:-1] in connectors:
+ scheme = _scheme[:-1]
else:
return None
- # import module according to uri type
- connectorclass = connectors[servicetype]()
- return connectorclass(uri, confnodesroot)
+ # import module according to uri type and get connector specific baseclass
+ # first call to import the module,
+ # then call with parameters to create the class
+ connector_specific_class = connectors[scheme]()(uri, confnodesroot)
+
+ if connector_specific_class is None:
+ return None
+
+ # new class inheriting from generic and specific connector base classes
+ return ClassType(_scheme + "_connector",
+ (ConnectorBase, connector_specific_class), {})()
-def ConnectorDialog(conn_type, confnodesroot):
- if conn_type not in connectors_dialog:
- return None
-
- connectorclass = connectors_dialog[conn_type]["function"]()
- return connectorclass(confnodesroot)
+def EditorClassFromScheme(scheme):
+ _Import_Dialogs()
+ return per_URI_connectors.get(scheme, None)
-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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,261 @@
+#!/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, get_all_addresses
+
+service_type = '_Beremiz._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)
+
+ self.RefreshButton = wx.Button(
+ label=_('Refresh'), name='RefreshButton', parent=self,
+ pos=wx.Point(0, 0), size=wx.DefaultSize, style=0)
+ self.RefreshButton.Bind(wx.EVT_BUTTON, self.OnRefreshButton)
+
+ # self.ByIPCheck = wx.CheckBox(self, label=_("Use IP instead of Service Name"))
+ # self.ByIPCheck.SetValue(True)
+
+ self._init_sizers()
+ self.Fit()
+
+ def __init__(self, parent):
+ wx.Panel.__init__(self, parent)
+
+ self.parent = 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 = None
+
+ self.RefreshList()
+ self.LatestSelection = None
+
+ self.IfacesMonitorState = None
+ self.IfacesMonitorTimer = wx.Timer(self)
+ self.IfacesMonitorTimer.Start(2000)
+ self.Bind(wx.EVT_TIMER, self.IfacesMonitor, self.IfacesMonitorTimer)
+
+ def __del__(self):
+ self.IfacesMonitorTimer.Stop()
+ self.Browser.cancel()
+ self.ZeroConfInstance.close()
+
+ def IfacesMonitor(self, event):
+ NewState = get_all_addresses(socket.AF_INET)
+
+ if self.IfacesMonitorState != NewState:
+ if self.IfacesMonitorState is not None:
+ # refresh only if a new address appeared
+ for addr in NewState:
+ if addr not in self.IfacesMonitorState:
+ self.RefreshList()
+ break
+ self.IfacesMonitorState = NewState
+ event.Skip()
+
+ def RefreshList(self):
+ self.ServicesList.DeleteAllItems()
+ if self.Browser is not None:
+ self.Browser.cancel()
+ if self.ZeroConfInstance is not None:
+ self.ZeroConfInstance.close()
+ self.ZeroConfInstance = Zeroconf()
+ self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
+
+ def OnRefreshButton(self, event):
+ 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.parent.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
+
+ def GetURI(self):
+ if self.LatestSelection is not None:
+ # if self.ByIPCheck.IsChecked():
+ svcname, scheme, host, port = \
+ map(lambda col: self.getColumnText(self.LatestSelection, col),
+ range(4))
+ return ("%s://%s:%s#%s" % (scheme, host, port, svcname)) \
+ if scheme[-1] == "S" \
+ else ("%s://%s:%s" % (scheme, host, port))
+ # else:
+ # svcname = self.getColumnText(self.LatestSelection, 0)
+ # connect_type = self.getColumnText(self.LatestSelection, 1)
+ # return str("MDNS://%s" % svcname)
+ 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)
+ if info is None:
+ return
+ svcname = name.split(".")[0]
+ typename = info.properties.get("protocol", None)
+ 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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,221 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+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)
+
+ def args(*a, **k):
+ return (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())
+
+ # pylint: disable=unused-variable
+ 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 Tue Mar 05 11:43:20 2019 +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/dialogs/DiscoveryDialog.py Tue Mar 05 11:43:20 2019 +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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,24 @@
+from __future__ import absolute_import
+
+import wx
+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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,51 @@
+#!/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 Mon Mar 11 13:51:07 2019 +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 Exception:
+ scheme = None
+
+ if scheme in ConnectorSchemes():
+ self.UriTypeChoice.SetStringSelection(scheme)
+ else:
+ self.UriTypeChoice.SetSelection(0)
+ scheme = None
+
+ 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 Tue Mar 05 11:43:20 2019 +0300
+++ b/dialogs/__init__.py Mon Mar 11 13:51:07 2019 +0100
@@ -49,4 +49,5 @@
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 Tue Mar 05 11:43:20 2019 +0300
+++ b/editors/ConfTreeNodeEditor.py Mon Mar 11 13:51:07 2019 +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())
--- a/etherlab/plc_cia402node.c Tue Mar 05 11:43:20 2019 +0300
+++ b/etherlab/plc_cia402node.c Mon Mar 11 13:51:07 2019 +0100
@@ -74,6 +74,31 @@
static IEC_BOOL __FirstTick = 1;
+typedef enum {
+ mc_mode_none, // No motion mode
+ mc_mode_csp, // Continuous Synchronous Positionning mode
+ mc_mode_csv, // Continuous Synchronous Velocity mode
+ mc_mode_cst, // Continuous Synchronous Torque mode
+} mc_axismotionmode_enum;
+
+typedef struct {
+ IEC_BOOL Power;
+ IEC_BOOL CommunicationReady;
+ IEC_UINT NetworkPosition;
+ IEC_BOOL ReadyForPowerOn;
+ IEC_BOOL PowerFeedback;
+ IEC_DINT ActualRawPosition;
+ IEC_DINT ActualRawVelocity;
+ IEC_DINT ActualRawTorque;
+ IEC_DINT RawPositionSetPoint;
+ IEC_DINT RawVelocitySetPoint;
+ IEC_DINT RawTorqueSetPoint;
+ mc_axismotionmode_enum AxisMotionMode;
+ IEC_LREAL ActualVelocity;
+ IEC_LREAL ActualPosition;
+ IEC_LREAL ActualTorque;
+}axis_s;
+
typedef struct {
%(entry_variables)s
axis_s* axis;
Binary file images/IDManager.png has changed
--- a/images/icons.svg Tue Mar 05 11:43:20 2019 +0300
+++ b/images/icons.svg Mon Mar 11 13:51:07 2019 +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/mb_runtime.c Tue Mar 05 11:43:20 2019 +0300
+++ b/modbus/mb_runtime.c Mon Mar 11 13:51:07 2019 +0100
@@ -1,614 +1,614 @@
-/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */
-
-/*
- * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
- *
- * This file is part of the Modbus library for Beremiz and matiec.
- *
- * This Modbus 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 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 Lesser
- * General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this Modbus library. If not, see <http://www.gnu.org/licenses/>.
- *
- * This code is made available on the understanding that it will not be
- * used in safety-critical situations without a full and competent review.
- */
-
-
-#include <stdio.h>
-#include <string.h> /* required for memcpy() */
-#include "mb_slave_and_master.h"
-#include "MB_%(locstr)s.h"
-
-
-#define MAX_MODBUS_ERROR_CODE 11
-static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = {
- /* 0 */ "", /* un-used -> no error! */
- /* 1 */ "illegal/unsuported function",
- /* 2 */ "illegal data address",
- /* 3 */ "illegal data value",
- /* 4 */ "slave device failure",
- /* 5 */ "acknowledge -> slave intends to reply later",
- /* 6 */ "slave device busy",
- /* 7 */ "negative acknowledge",
- /* 8 */ "memory parity error",
- /* 9 */ "", /* undefined by Modbus */
- /* 10*/ "gateway path unavalilable",
- /* 11*/ "gateway target device failed to respond"
-};
-
-
-/* Execute a modbus client transaction/request */
-static int __execute_mb_request(int request_id){
- switch (client_requests[request_id].mb_function){
-
- case 1: /* read coils */
- return read_output_bits(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].count,
- client_requests[request_id].coms_buffer,
- (int) client_requests[request_id].count,
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 2: /* read discrete inputs */
- return read_input_bits( client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].count,
- client_requests[request_id].coms_buffer,
- (int) client_requests[request_id].count,
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 3: /* read holding registers */
- return read_output_words(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].count,
- client_requests[request_id].coms_buffer,
- (int) client_requests[request_id].count,
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 4: /* read input registers */
- return read_input_words(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].count,
- client_requests[request_id].coms_buffer,
- (int) client_requests[request_id].count,
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 5: /* write single coil */
- return write_output_bit(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].coms_buffer[0],
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 6: /* write single register */
- return write_output_word(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].coms_buffer[0],
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 7: break; /* function not yet supported */
- case 8: break; /* function not yet supported */
- case 9: break; /* function not yet supported */
- case 10: break; /* function not yet supported */
- case 11: break; /* function not yet supported */
- case 12: break; /* function not yet supported */
- case 13: break; /* function not yet supported */
- case 14: break; /* function not yet supported */
-
- case 15: /* write multiple coils */
- return write_output_bits(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].count,
- client_requests[request_id].coms_buffer,
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- case 16: /* write multiple registers */
- return write_output_words(client_requests[request_id].slave_id,
- client_requests[request_id].address,
- client_requests[request_id].count,
- client_requests[request_id].coms_buffer,
- client_nodes[client_requests[request_id].client_node_id].mb_nd,
- client_requests[request_id].retries,
- &(client_requests[request_id].error_code),
- &(client_requests[request_id].resp_timeout),
- &(client_requests[request_id].coms_buf_mutex));
-
- default: break; /* should never occur, if file generation is correct */
- }
-
- fprintf(stderr, "Modbus plugin: Modbus function %%d not supported\n", request_id); /* should never occur, if file generation is correct */
- return -1;
-}
-
-
-
-/* pack bits from unpacked_data to packed_data */
-static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) {
- u8 bit;
- u16 byte, coils_processed;
-
- if ((0 == bit_count) || (65535-start_addr < bit_count-1))
- return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
- for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
- packed_data[byte] = 0;
- for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) {
- if(unpacked_data[start_addr + coils_processed])
- packed_data[byte] |= bit; /* set bit */
- else packed_data[byte] &= ~bit; /* reset bit */
- }
- }
- return 0;
-}
-
-
-/* unpack bits from packed_data to unpacked_data */
-static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) {
- u8 temp, bit;
- u16 byte, coils_processed;
-
- if ((0 == bit_count) || (65535-start_addr < bit_count-1))
- return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
- for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
- temp = packed_data[byte] ;
- for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) {
- unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0;
- }
- }
- return 0;
-}
-
-
-static int __read_inbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes)
- {return __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);}
-static int __read_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes)
- {return __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);}
-static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes)
- {return __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes); }
-
-
-
-static int __read_inwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
-
- if ((start_addr + word_count) > MEM_AREA_SIZE)
- return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
- /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
- memcpy(/* dest */ (void *)data_words,
- /* src */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]),
- /* size */ word_count * 2);
- return 0;
-}
-
-
-
-static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
-
- if ((start_addr + word_count) > MEM_AREA_SIZE)
- return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
- /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
- memcpy(/* dest */ (void *)data_words,
- /* src */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
- /* size */ word_count * 2);
- return 0;
-}
-
-
-
-
-static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
-
- if ((start_addr + word_count) > MEM_AREA_SIZE)
- return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
-
- /* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned.
- * It is not therefore safe to cast it to an u16 data type.
- * The following code cannot be used. memcpy() is used instead.
- */
- /*
- for (count = 0; count < word_count ; count++)
- ((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count];
- */
- memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
- /* src */ (void *)data_words,
- /* size */ word_count * 2);
- return 0;
-}
-
-
-
-
-#include <pthread.h>
-
-static void *__mb_server_thread(void *_server_node) {
- server_node_t *server_node = _server_node;
- mb_slave_callback_t callbacks = {
- &__read_inbits,
- &__read_outbits,
- &__write_outbits,
- &__read_inwords,
- &__read_outwords,
- &__write_outwords,
- (void *)&(server_node->mem_area)
- };
-
- // Enable thread cancelation. Enabled is default, but set it anyway to be safe.
- pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
-
- // mb_slave_run() should never return!
- mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id);
- fprintf(stderr, "Modbus plugin: Modbus server for node %%s died unexpectedly!\n", server_node->location); /* should never occur */
- return NULL;
-}
-
-
-#define timespec_add(ts, sec, nsec) { \
- ts.tv_sec += sec; \
- ts.tv_nsec += nsec; \
- if (ts.tv_nsec >= 1000000000) { \
- ts.tv_sec ++; \
- ts.tv_nsec -= 1000000000; \
- } \
-}
-
-
-static void *__mb_client_thread(void *_index) {
- int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast)
- struct timespec next_cycle;
- int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */
- int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */
-
- // Enable thread cancelation. Enabled is default, but set it anyway to be safe.
- pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
-
- // get the current time
- clock_gettime(CLOCK_MONOTONIC, &next_cycle);
-
- // loop the communication with the client
- while (1) {
- /*
- struct timespec cur_time;
- clock_gettime(CLOCK_MONOTONIC, &cur_time);
- fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec);
- */
- int req;
- for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){
- /*just do the requests belonging to the client */
- if (client_requests[req].client_node_id != client_node_id)
- continue;
- int res_tmp = __execute_mb_request(req);
- switch (res_tmp) {
- case PORT_FAILURE: {
- if (res_tmp != client_nodes[client_node_id].prev_error)
- fprintf(stderr, "Modbus plugin: Error connecting Modbus client %%s to remote server.\n", client_nodes[client_node_id].location);
- client_nodes[client_node_id].prev_error = res_tmp;
- break;
- }
- case INVALID_FRAME: {
- if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
- fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location);
- client_requests[req].prev_error = res_tmp;
- break;
- }
- case TIMEOUT: {
- if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
- fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s timed out waiting for reply from server.\n", client_requests[req].location);
- client_requests[req].prev_error = res_tmp;
- break;
- }
- case MODBUS_ERROR: {
- if (client_requests[req].prev_error != client_requests[req].error_code) {
- fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned error code 0x%%2x", client_requests[req].location, client_requests[req].error_code);
- if (client_requests[req].error_code <= MAX_MODBUS_ERROR_CODE ) {
- fprintf(stderr, "(%%s)", modbus_error_messages[client_requests[req].error_code]);
- fprintf(stderr, ".\n");
- }
- }
- client_requests[req].prev_error = client_requests[req].error_code;
- break;
- }
- default: {
- if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) {
- fprintf(stderr, "Modbus plugin: Modbus client %%s has reconnected to server/slave.\n", client_nodes[client_node_id].location);
- }
- if ((res_tmp >= 0) && (client_requests[req] .prev_error != 0)) {
- fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s has succesfully resumed comunication.\n", client_requests[req].location);
- }
- client_nodes[client_node_id].prev_error = 0;
- client_requests[req] .prev_error = 0;
- break;
- }
- }
- }
- // Determine absolute time instant for starting the next cycle
- struct timespec prev_cycle, now;
- prev_cycle = next_cycle;
- timespec_add(next_cycle, period_sec, period_nsec);
- /* NOTE A:
- * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will
- * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously
- * for some time until we catch up.
- * This is undesirable, so we detect it by making sure the next_cycle will start in the future.
- * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval.
- *
- * NOTE B:
- * It probably does not make sense to check for overflow of timer - so we don't do it for now!
- * Even in 32 bit systems this will take at least 68 years since the computer booted
- * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0
- * every time the system boots). On 64 bit systems, it will take over
- * 10^11 years to overflow.
- */
- clock_gettime(CLOCK_MONOTONIC, &now);
- if ( ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec)))
- /* We are falling behind. See NOTE A above */
- || (next_cycle.tv_sec < prev_cycle.tv_sec)
- /* Timer overflow. See NOTE B above */
- ) {
- next_cycle = now;
- timespec_add(next_cycle, period_sec, period_nsec);
- }
-
- clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL);
- }
-
- // humour the compiler.
- return NULL;
-}
-
-
-int __cleanup_%(locstr)s ();
-int __init_%(locstr)s (int argc, char **argv){
- int index;
-
- for (index=0; index < NUMBER_OF_CLIENT_NODES;index++)
- client_nodes[index].mb_nd = -1;
- for (index=0; index < NUMBER_OF_SERVER_NODES;index++)
- // mb_nd with negative numbers indicate how far it has been initialised (or not)
- // -2 --> no modbus node created; no thread created
- // -1 --> modbus node created!; no thread created
- // >=0 --> modbus node created!; thread created!
- server_nodes[index].mb_nd = -2;
-
- /* modbus library init */
- /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus
- * extension currently in the user's project. This file (MB_xx.c) is handling only one instance,
- * but must initialize the library for all instances. Only the first call to mb_slave_and_master_init()
- * will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored
- * by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments.
- */
- if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) {
- fprintf(stderr, "Modbus plugin: Error starting modbus library\n");
- // return imediately. Do NOT goto error_exit, as we did not get to
- // start the modbus library!
- return -1;
- }
-
- /* init the mutex for each client request */
- /* Must be done _before_ launching the client threads!! */
- for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
- if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) {
- fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
- goto error_exit;
- }
- }
-
- /* init each client connection to remote modbus server, and launch thread */
- /* NOTE: All client_nodes[].init_state are initialised to 0 in the code
- * generated by the modbus plugin
- */
- for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){
- /* establish client connection */
- client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address);
- if (client_nodes[index].mb_nd < 0){
- fprintf(stderr, "Modbus plugin: Error creating modbus client node %%s\n", client_nodes[index].location);
- goto error_exit;
- }
- client_nodes[index].init_state = 1; // we have created the node
-
- /* launch a thread to handle this client node */
- {
- int res = 0;
- pthread_attr_t attr;
- res |= pthread_attr_init(&attr);
- res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
- if (res != 0) {
- fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location);
- goto error_exit;
- }
- }
- client_nodes[index].init_state = 2; // we have created the node and a thread
- }
-
- /* init each local server */
- /* NOTE: All server_nodes[].init_state are initialised to 0 in the code
- * generated by the modbus plugin
- */
- for (index=0; index < NUMBER_OF_SERVER_NODES;index++){
- /* create the modbus server */
- server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address);
- if (server_nodes[index].mb_nd < 0){
- fprintf(stderr, "Modbus plugin: Error creating modbus server node %%s\n", server_nodes[index].location);
- goto error_exit;
- }
- server_nodes[index].init_state = 1; // we have created the node
-
- /* launch a thread to handle this server node */
- {
- int res = 0;
- pthread_attr_t attr;
- res |= pthread_attr_init(&attr);
- res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index]));
- if (res != 0) {
- fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location);
- goto error_exit;
- }
- }
- server_nodes[index].init_state = 2; // we have created the node and thread
- }
-
- return 0;
-
-error_exit:
- __cleanup_%(locstr)s ();
- return -1;
-}
-
-
-
-
-
-void __publish_%(locstr)s (){
- int index;
-
- for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
- /*just do the output requests */
- if (client_requests[index].req_type == req_output){
- if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
- // copy from plcv_buffer to coms_buffer
- memcpy((void *)client_requests[index].coms_buffer /* destination */,
- (void *)client_requests[index].plcv_buffer /* source */,
- REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
- pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
- }
- }
- }
-}
-
-
-
-
-
-void __retrieve_%(locstr)s (){
- int index;
-
- for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
- /*just do the input requests */
- if (client_requests[index].req_type == req_input){
- if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
- // copy from coms_buffer to plcv_buffer
- memcpy((void *)client_requests[index].plcv_buffer /* destination */,
- (void *)client_requests[index].coms_buffer /* source */,
- REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
- pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
- }
- }
- }
-}
-
-
-
-
-
-int __cleanup_%(locstr)s (){
- int index, close;
- int res = 0;
-
- /* kill thread and close connections of each modbus client node */
- for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) {
- close = 0;
- if (client_nodes[index].init_state >= 2) {
- // thread was launched, so we try to cancel it!
- close = pthread_cancel(client_nodes[index].thread_id);
- close |= pthread_join (client_nodes[index].thread_id, NULL);
- if (close < 0)
- fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location);
- }
- res |= close;
-
- close = 0;
- if (client_nodes[index].init_state >= 1) {
- // modbus client node was created, so we try to close it!
- close = mb_master_close (client_nodes[index].mb_nd);
- if (close < 0){
- fprintf(stderr, "Modbus plugin: Error closing modbus client node %%s\n", client_nodes[index].location);
- // We try to shut down as much as possible, so we do not return noW!
- }
- client_nodes[index].mb_nd = -1;
- }
- res |= close;
- client_nodes[index].init_state = 0;
- }
-
- /* kill thread and close connections of each modbus server node */
- for (index=0; index < NUMBER_OF_SERVER_NODES; index++) {
- close = 0;
- if (server_nodes[index].init_state >= 2) {
- // thread was launched, so we try to cancel it!
- close = pthread_cancel(server_nodes[index].thread_id);
- close |= pthread_join (server_nodes[index].thread_id, NULL);
- if (close < 0)
- fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %%s\n", server_nodes[index].location);
- }
- res |= close;
-
- close = 0;
- if (server_nodes[index].init_state >= 1) {
- // modbus server node was created, so we try to close it!
- close = mb_slave_close (server_nodes[index].mb_nd);
- if (close < 0) {
- fprintf(stderr, "Modbus plugin: Error closing node for modbus server %%s (%%d)\n", server_nodes[index].location, server_nodes[index].mb_nd);
- // We try to shut down as much as possible, so we do not return noW!
- }
- server_nodes[index].mb_nd = -1;
- }
- res |= close;
- server_nodes[index].init_state = 0;
- }
-
- /* destroy the mutex of each client request */
- for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) {
- if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) {
- fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
- // We try to shut down as much as possible, so we do not return noW!
- res |= -1;
- }
- }
-
- /* modbus library close */
- //fprintf(stderr, "Shutting down modbus library...\n");
- if (mb_slave_and_master_done()<0) {
- fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n");
- res |= -1;
- }
-
- return res;
-}
-
+/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */
+
+/*
+ * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
+ *
+ * This file is part of the Modbus library for Beremiz and matiec.
+ *
+ * This Modbus 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 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 Lesser
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this Modbus library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * This code is made available on the understanding that it will not be
+ * used in safety-critical situations without a full and competent review.
+ */
+
+
+#include <stdio.h>
+#include <string.h> /* required for memcpy() */
+#include "mb_slave_and_master.h"
+#include "MB_%(locstr)s.h"
+
+
+#define MAX_MODBUS_ERROR_CODE 11
+static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = {
+ /* 0 */ "", /* un-used -> no error! */
+ /* 1 */ "illegal/unsuported function",
+ /* 2 */ "illegal data address",
+ /* 3 */ "illegal data value",
+ /* 4 */ "slave device failure",
+ /* 5 */ "acknowledge -> slave intends to reply later",
+ /* 6 */ "slave device busy",
+ /* 7 */ "negative acknowledge",
+ /* 8 */ "memory parity error",
+ /* 9 */ "", /* undefined by Modbus */
+ /* 10*/ "gateway path unavalilable",
+ /* 11*/ "gateway target device failed to respond"
+};
+
+
+/* Execute a modbus client transaction/request */
+static int __execute_mb_request(int request_id){
+ switch (client_requests[request_id].mb_function){
+
+ case 1: /* read coils */
+ return read_output_bits(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].count,
+ client_requests[request_id].coms_buffer,
+ (int) client_requests[request_id].count,
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 2: /* read discrete inputs */
+ return read_input_bits( client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].count,
+ client_requests[request_id].coms_buffer,
+ (int) client_requests[request_id].count,
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 3: /* read holding registers */
+ return read_output_words(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].count,
+ client_requests[request_id].coms_buffer,
+ (int) client_requests[request_id].count,
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 4: /* read input registers */
+ return read_input_words(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].count,
+ client_requests[request_id].coms_buffer,
+ (int) client_requests[request_id].count,
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 5: /* write single coil */
+ return write_output_bit(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].coms_buffer[0],
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 6: /* write single register */
+ return write_output_word(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].coms_buffer[0],
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 7: break; /* function not yet supported */
+ case 8: break; /* function not yet supported */
+ case 9: break; /* function not yet supported */
+ case 10: break; /* function not yet supported */
+ case 11: break; /* function not yet supported */
+ case 12: break; /* function not yet supported */
+ case 13: break; /* function not yet supported */
+ case 14: break; /* function not yet supported */
+
+ case 15: /* write multiple coils */
+ return write_output_bits(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].count,
+ client_requests[request_id].coms_buffer,
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ case 16: /* write multiple registers */
+ return write_output_words(client_requests[request_id].slave_id,
+ client_requests[request_id].address,
+ client_requests[request_id].count,
+ client_requests[request_id].coms_buffer,
+ client_nodes[client_requests[request_id].client_node_id].mb_nd,
+ client_requests[request_id].retries,
+ &(client_requests[request_id].error_code),
+ &(client_requests[request_id].resp_timeout),
+ &(client_requests[request_id].coms_buf_mutex));
+
+ default: break; /* should never occur, if file generation is correct */
+ }
+
+ fprintf(stderr, "Modbus plugin: Modbus function %%d not supported\n", request_id); /* should never occur, if file generation is correct */
+ return -1;
+}
+
+
+
+/* pack bits from unpacked_data to packed_data */
+static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) {
+ u8 bit;
+ u16 byte, coils_processed;
+
+ if ((0 == bit_count) || (65535-start_addr < bit_count-1))
+ return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+ for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
+ packed_data[byte] = 0;
+ for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) {
+ if(unpacked_data[start_addr + coils_processed])
+ packed_data[byte] |= bit; /* set bit */
+ else packed_data[byte] &= ~bit; /* reset bit */
+ }
+ }
+ return 0;
+}
+
+
+/* unpack bits from packed_data to unpacked_data */
+static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) {
+ u8 temp, bit;
+ u16 byte, coils_processed;
+
+ if ((0 == bit_count) || (65535-start_addr < bit_count-1))
+ return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+ for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) {
+ temp = packed_data[byte] ;
+ for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) {
+ unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0;
+ }
+ }
+ return 0;
+}
+
+
+static int __read_inbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes)
+ {return __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);}
+static int __read_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes)
+ {return __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);}
+static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes)
+ {return __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes); }
+
+
+
+static int __read_inwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
+
+ if ((start_addr + word_count) > MEM_AREA_SIZE)
+ return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+ /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
+ memcpy(/* dest */ (void *)data_words,
+ /* src */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]),
+ /* size */ word_count * 2);
+ return 0;
+}
+
+
+
+static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
+
+ if ((start_addr + word_count) > MEM_AREA_SIZE)
+ return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+ /* use memcpy() because loop with pointers (u16 *) caused alignment problems */
+ memcpy(/* dest */ (void *)data_words,
+ /* src */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
+ /* size */ word_count * 2);
+ return 0;
+}
+
+
+
+
+static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) {
+
+ if ((start_addr + word_count) > MEM_AREA_SIZE)
+ return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */
+
+ /* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned.
+ * It is not therefore safe to cast it to an u16 data type.
+ * The following code cannot be used. memcpy() is used instead.
+ */
+ /*
+ for (count = 0; count < word_count ; count++)
+ ((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count];
+ */
+ memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]),
+ /* src */ (void *)data_words,
+ /* size */ word_count * 2);
+ return 0;
+}
+
+
+
+
+#include <pthread.h>
+
+static void *__mb_server_thread(void *_server_node) {
+ server_node_t *server_node = _server_node;
+ mb_slave_callback_t callbacks = {
+ &__read_inbits,
+ &__read_outbits,
+ &__write_outbits,
+ &__read_inwords,
+ &__read_outwords,
+ &__write_outwords,
+ (void *)&(server_node->mem_area)
+ };
+
+ // Enable thread cancelation. Enabled is default, but set it anyway to be safe.
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+ // mb_slave_run() should never return!
+ mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id);
+ fprintf(stderr, "Modbus plugin: Modbus server for node %%s died unexpectedly!\n", server_node->location); /* should never occur */
+ return NULL;
+}
+
+
+#define timespec_add(ts, sec, nsec) { \
+ ts.tv_sec += sec; \
+ ts.tv_nsec += nsec; \
+ if (ts.tv_nsec >= 1000000000) { \
+ ts.tv_sec ++; \
+ ts.tv_nsec -= 1000000000; \
+ } \
+}
+
+
+static void *__mb_client_thread(void *_index) {
+ int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast)
+ struct timespec next_cycle;
+ int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */
+ int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */
+
+ // Enable thread cancelation. Enabled is default, but set it anyway to be safe.
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+ // get the current time
+ clock_gettime(CLOCK_MONOTONIC, &next_cycle);
+
+ // loop the communication with the client
+ while (1) {
+ /*
+ struct timespec cur_time;
+ clock_gettime(CLOCK_MONOTONIC, &cur_time);
+ fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec);
+ */
+ int req;
+ for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){
+ /*just do the requests belonging to the client */
+ if (client_requests[req].client_node_id != client_node_id)
+ continue;
+ int res_tmp = __execute_mb_request(req);
+ switch (res_tmp) {
+ case PORT_FAILURE: {
+ if (res_tmp != client_nodes[client_node_id].prev_error)
+ fprintf(stderr, "Modbus plugin: Error connecting Modbus client %%s to remote server.\n", client_nodes[client_node_id].location);
+ client_nodes[client_node_id].prev_error = res_tmp;
+ break;
+ }
+ case INVALID_FRAME: {
+ if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
+ fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location);
+ client_requests[req].prev_error = res_tmp;
+ break;
+ }
+ case TIMEOUT: {
+ if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error))
+ fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s timed out waiting for reply from server.\n", client_requests[req].location);
+ client_requests[req].prev_error = res_tmp;
+ break;
+ }
+ case MODBUS_ERROR: {
+ if (client_requests[req].prev_error != client_requests[req].error_code) {
+ fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned error code 0x%%2x", client_requests[req].location, client_requests[req].error_code);
+ if (client_requests[req].error_code <= MAX_MODBUS_ERROR_CODE ) {
+ fprintf(stderr, "(%%s)", modbus_error_messages[client_requests[req].error_code]);
+ fprintf(stderr, ".\n");
+ }
+ }
+ client_requests[req].prev_error = client_requests[req].error_code;
+ break;
+ }
+ default: {
+ if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) {
+ fprintf(stderr, "Modbus plugin: Modbus client %%s has reconnected to server/slave.\n", client_nodes[client_node_id].location);
+ }
+ if ((res_tmp >= 0) && (client_requests[req] .prev_error != 0)) {
+ fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s has succesfully resumed comunication.\n", client_requests[req].location);
+ }
+ client_nodes[client_node_id].prev_error = 0;
+ client_requests[req] .prev_error = 0;
+ break;
+ }
+ }
+ }
+ // Determine absolute time instant for starting the next cycle
+ struct timespec prev_cycle, now;
+ prev_cycle = next_cycle;
+ timespec_add(next_cycle, period_sec, period_nsec);
+ /* NOTE A:
+ * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will
+ * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously
+ * for some time until we catch up.
+ * This is undesirable, so we detect it by making sure the next_cycle will start in the future.
+ * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval.
+ *
+ * NOTE B:
+ * It probably does not make sense to check for overflow of timer - so we don't do it for now!
+ * Even in 32 bit systems this will take at least 68 years since the computer booted
+ * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0
+ * every time the system boots). On 64 bit systems, it will take over
+ * 10^11 years to overflow.
+ */
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if ( ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec)))
+ /* We are falling behind. See NOTE A above */
+ || (next_cycle.tv_sec < prev_cycle.tv_sec)
+ /* Timer overflow. See NOTE B above */
+ ) {
+ next_cycle = now;
+ timespec_add(next_cycle, period_sec, period_nsec);
+ }
+
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL);
+ }
+
+ // humour the compiler.
+ return NULL;
+}
+
+
+int __cleanup_%(locstr)s ();
+int __init_%(locstr)s (int argc, char **argv){
+ int index;
+
+ for (index=0; index < NUMBER_OF_CLIENT_NODES;index++)
+ client_nodes[index].mb_nd = -1;
+ for (index=0; index < NUMBER_OF_SERVER_NODES;index++)
+ // mb_nd with negative numbers indicate how far it has been initialised (or not)
+ // -2 --> no modbus node created; no thread created
+ // -1 --> modbus node created!; no thread created
+ // >=0 --> modbus node created!; thread created!
+ server_nodes[index].mb_nd = -2;
+
+ /* modbus library init */
+ /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus
+ * extension currently in the user's project. This file (MB_xx.c) is handling only one instance,
+ * but must initialize the library for all instances. Only the first call to mb_slave_and_master_init()
+ * will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored
+ * by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments.
+ */
+ if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) {
+ fprintf(stderr, "Modbus plugin: Error starting modbus library\n");
+ // return imediately. Do NOT goto error_exit, as we did not get to
+ // start the modbus library!
+ return -1;
+ }
+
+ /* init the mutex for each client request */
+ /* Must be done _before_ launching the client threads!! */
+ for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+ if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) {
+ fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
+ goto error_exit;
+ }
+ }
+
+ /* init each client connection to remote modbus server, and launch thread */
+ /* NOTE: All client_nodes[].init_state are initialised to 0 in the code
+ * generated by the modbus plugin
+ */
+ for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){
+ /* establish client connection */
+ client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address);
+ if (client_nodes[index].mb_nd < 0){
+ fprintf(stderr, "Modbus plugin: Error creating modbus client node %%s\n", client_nodes[index].location);
+ goto error_exit;
+ }
+ client_nodes[index].init_state = 1; // we have created the node
+
+ /* launch a thread to handle this client node */
+ {
+ int res = 0;
+ pthread_attr_t attr;
+ res |= pthread_attr_init(&attr);
+ res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
+ if (res != 0) {
+ fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location);
+ goto error_exit;
+ }
+ }
+ client_nodes[index].init_state = 2; // we have created the node and a thread
+ }
+
+ /* init each local server */
+ /* NOTE: All server_nodes[].init_state are initialised to 0 in the code
+ * generated by the modbus plugin
+ */
+ for (index=0; index < NUMBER_OF_SERVER_NODES;index++){
+ /* create the modbus server */
+ server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address);
+ if (server_nodes[index].mb_nd < 0){
+ fprintf(stderr, "Modbus plugin: Error creating modbus server node %%s\n", server_nodes[index].location);
+ goto error_exit;
+ }
+ server_nodes[index].init_state = 1; // we have created the node
+
+ /* launch a thread to handle this server node */
+ {
+ int res = 0;
+ pthread_attr_t attr;
+ res |= pthread_attr_init(&attr);
+ res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index]));
+ if (res != 0) {
+ fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location);
+ goto error_exit;
+ }
+ }
+ server_nodes[index].init_state = 2; // we have created the node and thread
+ }
+
+ return 0;
+
+error_exit:
+ __cleanup_%(locstr)s ();
+ return -1;
+}
+
+
+
+
+
+void __publish_%(locstr)s (){
+ int index;
+
+ for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+ /*just do the output requests */
+ if (client_requests[index].req_type == req_output){
+ if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
+ // copy from plcv_buffer to coms_buffer
+ memcpy((void *)client_requests[index].coms_buffer /* destination */,
+ (void *)client_requests[index].plcv_buffer /* source */,
+ REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
+ pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
+ }
+ }
+ }
+}
+
+
+
+
+
+void __retrieve_%(locstr)s (){
+ int index;
+
+ for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+ /*just do the input requests */
+ if (client_requests[index].req_type == req_input){
+ if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
+ // copy from coms_buffer to plcv_buffer
+ memcpy((void *)client_requests[index].plcv_buffer /* destination */,
+ (void *)client_requests[index].coms_buffer /* source */,
+ REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
+ pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
+ }
+ }
+ }
+}
+
+
+
+
+
+int __cleanup_%(locstr)s (){
+ int index, close;
+ int res = 0;
+
+ /* kill thread and close connections of each modbus client node */
+ for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) {
+ close = 0;
+ if (client_nodes[index].init_state >= 2) {
+ // thread was launched, so we try to cancel it!
+ close = pthread_cancel(client_nodes[index].thread_id);
+ close |= pthread_join (client_nodes[index].thread_id, NULL);
+ if (close < 0)
+ fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location);
+ }
+ res |= close;
+
+ close = 0;
+ if (client_nodes[index].init_state >= 1) {
+ // modbus client node was created, so we try to close it!
+ close = mb_master_close (client_nodes[index].mb_nd);
+ if (close < 0){
+ fprintf(stderr, "Modbus plugin: Error closing modbus client node %%s\n", client_nodes[index].location);
+ // We try to shut down as much as possible, so we do not return noW!
+ }
+ client_nodes[index].mb_nd = -1;
+ }
+ res |= close;
+ client_nodes[index].init_state = 0;
+ }
+
+ /* kill thread and close connections of each modbus server node */
+ for (index=0; index < NUMBER_OF_SERVER_NODES; index++) {
+ close = 0;
+ if (server_nodes[index].init_state >= 2) {
+ // thread was launched, so we try to cancel it!
+ close = pthread_cancel(server_nodes[index].thread_id);
+ close |= pthread_join (server_nodes[index].thread_id, NULL);
+ if (close < 0)
+ fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %%s\n", server_nodes[index].location);
+ }
+ res |= close;
+
+ close = 0;
+ if (server_nodes[index].init_state >= 1) {
+ // modbus server node was created, so we try to close it!
+ close = mb_slave_close (server_nodes[index].mb_nd);
+ if (close < 0) {
+ fprintf(stderr, "Modbus plugin: Error closing node for modbus server %%s (%%d)\n", server_nodes[index].location, server_nodes[index].mb_nd);
+ // We try to shut down as much as possible, so we do not return noW!
+ }
+ server_nodes[index].mb_nd = -1;
+ }
+ res |= close;
+ server_nodes[index].init_state = 0;
+ }
+
+ /* destroy the mutex of each client request */
+ for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) {
+ if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) {
+ fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
+ // We try to shut down as much as possible, so we do not return noW!
+ res |= -1;
+ }
+ }
+
+ /* modbus library close */
+ //fprintf(stderr, "Shutting down modbus library...\n");
+ if (mb_slave_and_master_done()<0) {
+ fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n");
+ res |= -1;
+ }
+
+ return res;
+}
+
--- a/modbus/modbus.py Tue Mar 05 11:43:20 2019 +0300
+++ b/modbus/modbus.py Mon Mar 11 13:51:07 2019 +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 Tue Mar 05 11:43:20 2019 +0300
+++ b/py_ext/plc_python.c Mon Mar 11 13:51:07 2019 +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 Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/NevowServer.py Mon Mar 11 13:51:07 2019 +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 Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/PLCObject.py Mon Mar 11 13:51:07 2019 +0100
@@ -23,21 +23,25 @@
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
import traceback
from time import time
import _ctypes # pylint: disable=wrong-import-order
+from six.moves import xrange
from past.builtins import execfile
-import Pyro.core as pyro
-import six
-from six.moves import _thread, xrange
+import md5
+from tempfile import mkstemp
+import shutil
+from functools import wraps, partial
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,141 +68,41 @@
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):
+ @wraps(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 # must exits already
+ self.tmpdir = os.path.join(WorkingDir, 'tmp')
+ if os.path.exists(self.tmpdir):
+ shutil.rmtree(self.tmpdir)
+ os.mkdir(self.tmpdir)
+ # 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
self.TraceLock = Lock()
self.Traces = []
+ self.DebugToken = 0
+
+ self._init_blobs()
# First task of worker -> no @RunInMain
- def AutoLoad(self):
+ def AutoLoad(self, autostart):
# Get the last transfered PLC
try:
self.CurrentPLCFilename = open(
@@ -206,10 +110,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,7 +454,56 @@
return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
@RunInMain
- def NewPLC(self, md5sum, data, extrafiles):
+ def GetPLCID(self):
+ return getPSKID(partial(self.LogMessage, 0))
+
+ def _init_blobs(self):
+ self.blobs = {}
+ if os.path.exists(self.tmpdir):
+ shutil.rmtree(self.tmpdir)
+ os.mkdir(self.tmpdir)
+
+ @RunInMain
+ def SeedBlob(self, seed):
+ blob = (mkstemp(dir=self.tmpdir) + (md5.new(),))
+ _fobj, _path, md5sum = blob
+ md5sum.update(seed)
+ newBlobID = md5sum.digest()
+ self.blobs[newBlobID] = blob
+ return newBlobID
+
+ @RunInMain
+ def AppendChunkToBlob(self, data, blobID):
+ blob = self.blobs.pop(blobID, None)
+
+ if blob is None:
+ return None
+
+ fobj, _path, md5sum = blob
+ md5sum.update(data)
+ newBlobID = md5sum.digest()
+ os.write(fobj, data)
+ self.blobs[newBlobID] = blob
+ return newBlobID
+
+ @RunInMain
+ def PurgeBlobs(self):
+ for fobj, _path, _md5sum in self.blobs:
+ os.close(fobj)
+ self._init_blobs()
+
+ def _BlobAsFile(self, blobID, newpath):
+ blob = self.blobs.pop(blobID, None)
+
+ if blob is None:
+ raise Exception(_("Missing data to create file: {}").format(newpath))
+
+ fobj, path, _md5sum = blob
+ os.close(fobj)
+ shutil.move(path, newpath)
+
+ @RunInMain
+ def NewPLC(self, md5sum, plc_object, extrafiles):
if self.PLCStatus in [PlcStatus.Stopped, PlcStatus.Empty, PlcStatus.Broken]:
NewFileName = md5sum + lib_ext
extra_files_log = os.path.join(self.workingdir, "extra_files.txt")
@@ -555,18 +513,13 @@
else None
new_PLC_filename = os.path.join(self.workingdir, NewFileName)
- # Some platform (i.e. Xenomai) don't like reloading same .so file
- replace_PLC_shared_object = new_PLC_filename != old_PLC_filename
-
- if replace_PLC_shared_object:
- self.UnLoadPLC()
+ self.UnLoadPLC()
self.LogMessage("NewPLC (%s)" % md5sum)
self.PLCStatus = PlcStatus.Empty
try:
- if replace_PLC_shared_object:
- os.remove(old_PLC_filename)
+ os.remove(old_PLC_filename)
for filename in open(extra_files_log, "rt").readlines() + [extra_files_log]:
try:
os.remove(os.path.join(self.workingdir, filename.strip()))
@@ -577,17 +530,16 @@
try:
# Create new PLC file
- if replace_PLC_shared_object:
- open(new_PLC_filename, 'wb').write(data)
+ self._BlobAsFile(plc_object, new_PLC_filename)
# Store new PLC filename based on md5 key
open(self._GetMD5FileName(), "w").write(md5sum)
# Then write the files
log = open(extra_files_log, "w")
- for fname, fdata in extrafiles:
+ for fname, blobID in extrafiles:
fpath = os.path.join(self.workingdir, fname)
- open(fpath, "wb").write(fdata)
+ self._BlobAsFile(blobID, fpath)
log.write(fname+'\n')
# Store new PLC filename
@@ -598,9 +550,7 @@
PLCprint(traceback.format_exc())
return False
- if not replace_PLC_shared_object:
- self.PLCStatus = PlcStatus.Stopped
- elif self.LoadPLC():
+ if self.LoadPLC():
self.PLCStatus = PlcStatus.Stopped
else:
self.PLCStatus = PlcStatus.Broken
@@ -617,11 +567,13 @@
pass
return False
+ @RunInMain
def SetTraceVariablesList(self, idxs):
"""
Call ctype imported function to append
these indexes to registred variables in PLC debugger
"""
+ self.DebugToken += 1
if idxs:
# suspend but dont disable
if self._suspendDebug(False) == 0:
@@ -636,8 +588,10 @@
self._RegisterDebugVariable(idx, force)
self._TracesSwap()
self._resumeDebug()
+ return self.DebugToken
else:
self._suspendDebug(True)
+ return None
def _TracesSwap(self):
self.LastSwapTrace = time()
@@ -651,8 +605,10 @@
return Traces
@RunInMain
- def GetTraceVariables(self):
- return self.PLCStatus, self._TracesSwap()
+ def GetTraceVariables(self, DebugToken):
+ if (DebugToken is not None and DebugToken == self.DebugToken):
+ return self.PLCStatus, self._TracesSwap()
+ return PlcStatus.Broken, []
def TraceThreadProc(self):
"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/PyroServer.py Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,82 @@
+#!/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 PyroServer(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):
+ if self._to_be_published():
+ self.Publish()
+
+ 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")
+
+ when_ready()
+ self.daemon.requestLoop()
+ self.daemon.sock.close()
+ self.Unpublish()
+
+ def Restart(self):
+ self.daemon.shutdown(True)
+
+ def Quit(self):
+ self.continueloop = False
+ self.daemon.shutdown(True)
+
+ def Publish(self):
+ self.servicepublisher = ServicePublisher("PYRO")
+ self.servicepublisher.RegisterService(self.servicename,
+ self.ip_addr, self.port)
+
+ def Unpublish(self):
+ if self.servicepublisher is not None:
+ self.servicepublisher.UnRegisterService()
+ self.servicepublisher = None
--- a/runtime/ServicePublisher.py Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/ServicePublisher.py Mon Mar 11 13:51:07 2019 +0100
@@ -28,13 +28,17 @@
import threading
import zeroconf
-service_type = '_PYRO._tcp.local.'
+
+service_type = '_Beremiz._tcp.local.'
class ServicePublisher(object):
- def __init__(self):
+ def __init__(self, protocol):
# type: fully qualified service type name
- self.serviceproperties = {'description': 'Beremiz remote PLC'}
+ self.serviceproperties = {
+ 'description': 'Beremiz remote PLC',
+ 'protocol': protocol
+ }
self.name = None
self.ip_32b = None
@@ -52,15 +56,19 @@
def _RegisterService(self, name, ip, port):
# name: fully qualified service name
- self.service_name = 'Beremiz_%s.%s' % (name, service_type)
+ self.service_name = '%s.%s' % (name, service_type)
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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,44 @@
+from __future__ import absolute_import
+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(errorlog):
+ if _PSKpath is not None:
+ if not os.path.exists(_PSKpath):
+ errorlog(
+ '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 Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/WampClient.py Mon Mar 11 13:51:07 2019 +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"]
@@ -56,6 +56,10 @@
("StartPLC", {}),
("StopPLC", {}),
("GetPLCstatus", {}),
+ ("GetPLCID", {}),
+ ("SeedBlob", {}),
+ ("AppendChunkToBlob", {}),
+ ("PurgeBlobs", {}),
("NewPLC", {}),
("MatchMD5", {}),
("SetTraceVariablesList", {}),
@@ -88,7 +92,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 +120,6 @@
raise Exception(
"don't know how to handle authmethod {}".format(challenge.method))
- @inlineCallbacks
def onJoin(self, details):
global _WampSession
_WampSession = self
@@ -129,13 +132,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 +149,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):
@@ -167,22 +174,16 @@
return ReconnectingClientFactory.buildProtocol(self, addr)
def clientConnectionFailed(self, connector, reason):
- if self.continueTrying:
- print(_("WAMP Client connection failed (%s) .. retrying ..") %
- time.ctime())
- super(ReconnectingWampWebSocketClientFactory,
- self).clientConnectionFailed(connector, reason)
- else:
- del connector
+ print(_("WAMP Client connection failed (%s) .. retrying ..") %
+ time.ctime())
+ super(ReconnectingWampWebSocketClientFactory,
+ self).clientConnectionFailed(connector, reason)
def clientConnectionLost(self, connector, reason):
- if self.continueTrying:
- print(_("WAMP Client connection lost (%s) .. retrying ..") %
- time.ctime())
- super(ReconnectingWampWebSocketClientFactory,
- self).clientConnectionFailed(connector, reason)
- else:
- del connector
+ print(_("WAMP Client connection lost (%s) .. retrying ..") %
+ time.ctime())
+ super(ReconnectingWampWebSocketClientFactory,
+ self).clientConnectionFailed(connector, reason)
def CheckConfiguration(WampClientConf):
@@ -226,12 +227,9 @@
with open(os.path.realpath(_WampConf), 'w') as f:
json.dump(WampClientConf, f, sort_keys=True, indent=4)
+ StopReconnectWampClient()
if 'active' in WampClientConf and WampClientConf['active']:
- if _transportFactory and _WampSession:
- StopReconnectWampClient()
StartReconnectWampClient()
- else:
- StopReconnectWampClient()
return WampClientConf
@@ -343,6 +341,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 +436,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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,123 @@
+#!/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
+import six
+
+
+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()
+ self.mutex.acquire()
+ 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]
+
+ while not self._finish:
+ self.todo.wait()
+ if self.job is not None:
+ self.job.do()
+ self.done.notify()
+ else:
+ break
+
+ 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()
+ self.job = None
+ self.free.notify()
+ 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 Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime/__init__.py Mon Mar 11 13:51:07 2019 +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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# subset of subprocess built-in module using posix_spawn rather than fork.
+
+from __future__ import absolute_import
+import os
+import signal
+import posix_spawn
+
+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 Mon Mar 11 13:51:07 2019 +0100
@@ -0,0 +1,6 @@
+
+.freeform-label {
+ float: left;
+ width: 30%;
+}
+
--- a/runtime_files.list Tue Mar 05 11:43:20 2019 +0300
+++ b/runtime_files.list Mon Mar 11 13:51:07 2019 +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
--- a/targets/__init__.py Tue Mar 05 11:43:20 2019 +0300
+++ b/targets/__init__.py Mon Mar 11 13:51:07 2019 +0100
@@ -74,9 +74,10 @@
DictXSD_toolchain["toolchain_"+toolchainname] = open(xsdfilename).read()
# Get all xsd targets
- for _targetname, nfo in targets.iteritems():
+ for target_name, nfo in targets.iteritems():
xsd_string = open(nfo["xsd"]).read()
- targetchoices += xsd_string % DictXSD_toolchain
+ targetchoices += xsd_string % dict(DictXSD_toolchain,
+ target_name=target_name)
return targetchoices
--- a/targets/toolchain_gcc.py Tue Mar 05 11:43:20 2019 +0300
+++ b/targets/toolchain_gcc.py Mon Mar 11 13:51:07 2019 +0100
@@ -72,23 +72,20 @@
"""
return self.CTRInstance.GetTarget().getcontent().getLinker()
- def GetBinaryCode(self):
- try:
- return open(self.exe_path, "rb").read()
- except Exception:
- return None
+ def GetBinaryPath(self):
+ return self.bin_path
def _GetMD5FileName(self):
return os.path.join(self.buildpath, "lastbuildPLC.md5")
- def ResetBinaryCodeMD5(self):
+ def ResetBinaryMD5(self):
self.md5key = None
try:
os.remove(self._GetMD5FileName())
except Exception:
pass
- def GetBinaryCodeMD5(self):
+ def GetBinaryMD5(self):
if self.md5key is not None:
return self.md5key
else:
@@ -100,8 +97,8 @@
def SetBuildPath(self, buildpath):
if self.buildpath != buildpath:
self.buildpath = buildpath
- self.exe = self.CTRInstance.GetProjectName() + self.extension
- self.exe_path = os.path.join(self.buildpath, self.exe)
+ self.bin = self.CTRInstance.GetProjectName() + self.extension
+ self.bin_path = os.path.join(self.buildpath, self.bin)
self.md5key = None
self.srcmd5 = {}
@@ -152,9 +149,6 @@
wholesrcdata += self.concat_deps(CFileName)
return hashlib.md5(wholesrcdata).hexdigest()
- def calc_md5(self):
- return hashlib.md5(self.GetBinaryCode()).hexdigest()
-
def build(self):
# Retrieve compiler and linker
self.compiler = self.getCompiler()
@@ -165,7 +159,7 @@
# ----------------- GENERATE OBJECT FILES ------------------------
obns = []
objs = []
- relink = self.GetBinaryCode() is None
+ relink = not os.path.exists(self.bin_path)
for Location, CFilesAndCFLAGS, _DoCalls in self.CTRInstance.LocationCFilesAndCFLAGS:
if CFilesAndCFLAGS:
if Location:
@@ -213,14 +207,14 @@
ALLldflags = ' '.join(self.getBuilderLDFLAGS())
- self.CTRInstance.logger.write(" [CC] " + ' '.join(obns)+" -> " + self.exe + "\n")
+ self.CTRInstance.logger.write(" [CC] " + ' '.join(obns)+" -> " + self.bin + "\n")
status, _result, _err_result = ProcessLogger(
self.CTRInstance.logger,
"\"%s\" %s -o \"%s\" %s" %
(self.linker,
listobjstring,
- self.exe_path,
+ self.bin_path,
ALLldflags)
).spin()
@@ -228,10 +222,10 @@
return False
else:
- self.CTRInstance.logger.write(" [pass] " + ' '.join(obns)+" -> " + self.exe + "\n")
+ self.CTRInstance.logger.write(" [pass] " + ' '.join(obns)+" -> " + self.bin + "\n")
# Calculate md5 key and get data for the new created PLC
- self.md5key = self.calc_md5()
+ self.md5key = hashlib.md5(open(self.bin_path, "rb").read()).hexdigest()
# Store new PLC filename based on md5 key
f = open(self._GetMD5FileName(), "w")
--- a/targets/toolchain_makefile.py Tue Mar 05 11:43:20 2019 +0300
+++ b/targets/toolchain_makefile.py Mon Mar 11 13:51:07 2019 +0100
@@ -47,20 +47,20 @@
self.buildpath = buildpath
self.md5key = None
- def GetBinaryCode(self):
+ def GetBinaryPath(self):
return None
def _GetMD5FileName(self):
return os.path.join(self.buildpath, "lastbuildPLC.md5")
- def ResetBinaryCodeMD5(self):
+ def ResetBinaryMD5(self):
self.md5key = None
try:
os.remove(self._GetMD5FileName())
except Exception:
pass
- def GetBinaryCodeMD5(self):
+ def GetBinaryMD5(self):
if self.md5key is not None:
return self.md5key
else:
--- a/wxglade_hmi/wxglade_hmi.py Tue Mar 05 11:43:20 2019 +0300
+++ b/wxglade_hmi/wxglade_hmi.py Mon Mar 11 13:51:07 2019 +0100
@@ -132,32 +132,39 @@
else:
define_hmi = ""
- declare_hmi = "\n".join(["%(name)s = None\n" % x for x in main_frames])
- declare_hmi += "\n".join(["\n".join(["%(class)s.%(h)s = %(h)s" %
- dict(x, h=h) for h in x['handlers']])
- for x in hmi_objects])
global_hmi = ("global %s\n" % ",".join(
[x["name"] for x in main_frames]) if len(main_frames) > 0 else "")
- init_hmi = "\n".join(["""\
+
+ declare_hmi = \
+ "\n".join(["%(name)s = None\n" % x for x in main_frames]) + \
+ "\n".join(["\n".join(["%(class)s.%(h)s = %(h)s" % dict(x, h=h)
+ for h in x['handlers']])
+ for x in hmi_objects]) + """\
+
def OnCloseFrame(evt):
wx.MessageBox(_("Please stop PLC to close"))
-%(name)s = %(class)s(None)
-%(name)s.Bind(wx.EVT_CLOSE, OnCloseFrame)
-%(name)s.Show()
+def InitHMI():
+ """ + global_hmi + "\n" + "\n".join(["""\
+ %(name)s = %(class)s(None)
+ %(name)s.Bind(wx.EVT_CLOSE, OnCloseFrame)
+ %(name)s.Show()
+
+""" % x for x in main_frames]) + """\
+def CleanupHMI():
+ """ + global_hmi + "\n" + "\n".join(["""\
+ if %(name)s is not None:
+ %(name)s.Destroy()
""" % x for x in main_frames])
- cleanup_hmi = "\n".join(
- ["if %(name)s is not None: %(name)s.Destroy()" % x
- for x in main_frames])
self.PreSectionsTexts = {
"globals": define_hmi,
"start": global_hmi,
- "stop": global_hmi + cleanup_hmi
+ "stop": "CleanupHMI()\n"
}
self.PostSectionsTexts = {
"globals": declare_hmi,
- "start": init_hmi,
+ "start": "InitHMI()\n",
}
if len(main_frames) == 0 and \