Added preliminary CLI. Supports building a project, transfering and running on PLC.
--- a/BeremizIDE.py Mon May 30 15:30:51 2022 +0200
+++ b/BeremizIDE.py Mon Jun 13 18:05:12 2022 +0200
@@ -28,9 +28,7 @@
from __future__ import print_function
import os
import sys
-import tempfile
import shutil
-import random
import time
from time import time as gettime
from threading import Lock, Timer, currentThread
@@ -47,9 +45,8 @@
from editors.TextViewer import TextViewer
from editors.ResourceEditor import ConfigurationEditor, ResourceEditor
from editors.DataTypeEditor import DataTypeEditor
-from util import paths as paths
+from util.paths import Bpath
from util.MiniTextControler import MiniTextControler
-from util.ProcessLogger import ProcessLogger
from util.BitmapLibrary import GetBitmap
from controls.LogViewer import LogViewer
from controls.CustomStyledTextCtrl import CustomStyledTextCtrl
@@ -84,12 +81,8 @@
EncodeFileSystemPath, \
DecodeFileSystemPath
-
-beremiz_dir = paths.AbsDir(__file__)
-
-
-def Bpath(*args):
- return os.path.join(beremiz_dir, *args)
+from LocalRuntimeMixin import LocalRuntimeMixin
+
def AppendMenu(parent, help, id, kind, text):
return parent.Append(wx.MenuItem(helpString=help, id=id, kind=kind, text=text))
@@ -240,7 +233,7 @@
ID_FILEMENURECENTPROJECTS = wx.NewId()
-class Beremiz(IDEFrame):
+class Beremiz(IDEFrame, LocalRuntimeMixin):
def _init_utils(self):
self.ConfNodeMenu = wx.Menu(title='')
@@ -451,6 +444,8 @@
os.environ["PATH"] = os.getcwd()+';'+os.environ["PATH"]
def __init__(self, parent, projectOpen=None, buildpath=None, ctr=None, debug=True, logf=None):
+ LocalRuntimeMixin.__init__(self)
+
# Add beremiz's icon in top left corner of the frame
self.icon = wx.Icon(Bpath("images", "brz.ico"), wx.BITMAP_TYPE_ICO)
self.__init_execute_path()
@@ -458,10 +453,6 @@
IDEFrame.__init__(self, parent, debug)
self.Log = LogPseudoFile(self.LogConsole, self.SelectTab, logf)
- self.local_runtime = None
- self.runtime_port = None
- self.local_runtime_tmpdir = None
-
self.LastPanelSelected = None
# Define Tree item icon list
@@ -525,37 +516,6 @@
else:
self.SetTitle(name)
- def StartLocalRuntime(self, taskbaricon=True):
- if (self.local_runtime is None) or (self.local_runtime.exitcode is not None):
- # create temporary directory for runtime working directory
- self.local_runtime_tmpdir = tempfile.mkdtemp()
- # choose an arbitrary random port for runtime
- self.runtime_port = int(random.random() * 1000) + 61131
- self.Log.write(_("Starting local runtime...\n"))
- # launch local runtime
- self.local_runtime = ProcessLogger(
- self.Log,
- "\"%s\" \"%s\" -p %s -i localhost %s %s" % (
- sys.executable,
- Bpath("Beremiz_service.py"),
- self.runtime_port,
- {False: "-x 0", True: "-x 1"}[taskbaricon],
- self.local_runtime_tmpdir),
- no_gui=False,
- timeout=500, keyword=self.local_runtime_tmpdir,
- cwd=self.local_runtime_tmpdir)
- self.local_runtime.spin()
- return self.runtime_port
-
- def KillLocalRuntime(self):
- if self.local_runtime is not None:
- # shutdown local runtime
- self.local_runtime.kill(gently=False)
- # clear temp dir
- shutil.rmtree(self.local_runtime_tmpdir)
-
- self.local_runtime = None
-
def OnOpenWidgetInspector(self, evt):
# Activate the widget inspection tool
from wx.lib.inspection import InspectionTool
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Beremiz_cli.py Mon Jun 13 18:05:12 2022 +0200
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import posixpath
+import sys
+from functools import wraps
+
+import click
+from importlib import import_module
+
+
+class CLISession(object):
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+ self.controller = None
+
+pass_session = click.make_pass_decorator(CLISession)
+
+
+@click.group(chain=True)
+@click.option(
+ "--project-home",
+ envvar="PROJECT_HOME",
+ default=".",
+ metavar="PATH",
+ help="Changes the project folder location.",
+)
+@click.option(
+ "--config",
+ nargs=2,
+ multiple=True,
+ metavar="KEY VALUE",
+ help="Overrides a config key/value pair.",
+)
+@click.option(
+ "--keep", "-k", is_flag=True,
+ help="Keep local runtime, do not kill it after executing commands.",
+)
+@click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode.")
+@click.option(
+ "--buildpath", "-b", help="Where to store files created during build."
+)
+@click.option(
+ "--uri", "-u", help="URI to reach remote PLC."
+)
+@click.version_option("0.1")
+@click.pass_context
+def cli(ctx, **kwargs):
+ """Beremiz CLI manipulates beremiz projects and runtimes. """
+
+ ctx.obj = CLISession(**kwargs)
+
+def ensure_controller(func):
+ @wraps(func)
+ def func_wrapper(session, *args, **kwargs):
+ if session.controller is None:
+ session.controller = import_module("CLIController").CLIController(session)
+ ret = func(session, *args, **kwargs)
+ return ret
+
+ return func_wrapper
+
+@cli.command()
+@click.option(
+ "--target", "-t", help="Target system triplet."
+)
+@pass_session
+@ensure_controller
+def build(session, target):
+ """Builds project. """
+ def processor():
+ return session.controller.build_project(target)
+ return processor
+
+@cli.command()
+@pass_session
+@ensure_controller
+def transfer(session):
+ """Transfer program to PLC runtim."""
+ def processor():
+ return session.controller.transfer_project()
+ return processor
+
+@cli.command()
+@pass_session
+@ensure_controller
+def run(session):
+ """Run program already present in PLC. """
+ def processor():
+ return session.controller.run_project()
+ return processor
+
+
+@cli.resultcallback()
+@pass_session
+def process_pipeline(session, processors, **kwargs):
+ ret = 0
+ for processor in processors:
+ ret = processor()
+ if ret != 0:
+ if len(processors) > 1 :
+ click.echo("Command sequence aborted")
+ break
+
+ session.controller.finish()
+
+ return ret
+
+if __name__ == '__main__':
+ cli()
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/CLIController.py Mon Jun 13 18:05:12 2022 +0200
@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+from functools import wraps
+
+import click
+
+import fake_wx
+
+from ProjectController import ProjectController
+from LocalRuntimeMixin import LocalRuntimeMixin
+
+class Log:
+
+ def __init__(self):
+ self.crlfpending = False
+
+ def write(self, s):
+ if s:
+ if self.crlfpending:
+ sys.stdout.write("\n")
+ sys.stdout.write(s)
+ self.crlfpending = 0
+
+ def write_error(self, s):
+ if s:
+ self.write("Error: "+s)
+
+ def write_warning(self, s):
+ if s:
+ self.write("Warning: "+s)
+
+ def flush(self):
+ sys.stdout.flush()
+
+ def isatty(self):
+ return False
+
+ def progress(self, s):
+ if s:
+ sys.stdout.write(s+"\r")
+ self.crlfpending = True
+
+
+def with_project_loaded(func):
+ @wraps(func)
+ def func_wrapper(self, *args, **kwargs):
+ if not self.HasOpenedProject():
+ if self.check_and_load_project():
+ return 1
+ self.apply_config()
+ return func(self, *args, **kwargs)
+
+ return func_wrapper
+
+def connected(func):
+ @wraps(func)
+ def func_wrapper(self, *args, **kwargs):
+ if self._connector is None:
+ if self.session.uri:
+ self.BeremizRoot.setURI_location(self.session.uri)
+ if not self._Connect():
+ return 1
+ return func(self, *args, **kwargs)
+
+ return func_wrapper
+
+class CLIController(LocalRuntimeMixin, ProjectController):
+ def __init__(self, session):
+ self.session = session
+ log = Log()
+ LocalRuntimeMixin.__init__(self, log)
+ ProjectController.__init__(self, None, log)
+
+ def check_and_load_project(self):
+ if not os.path.isdir(self.session.project_home):
+ self.logger.write_error(
+ _("\"%s\" is not a valid Beremiz project\n") % self.session.project_home)
+ return True
+
+ errmsg, error = self.LoadProject(self.session.project_home, self.session.buildpath)
+ if error:
+ self.logger.write_error(errmsg)
+ return True
+
+ def apply_config(self):
+ for k,v in self.session.config:
+ self.SetParamsAttribute("BeremizRoot."+k, v)
+
+ @with_project_loaded
+ def build_project(self, target):
+
+ if target:
+ self.SetParamsAttribute("BeremizRoot.TargetType", target)
+
+ return 0 if self._Build() else 1
+
+ @with_project_loaded
+ @connected
+ def transfer_project(self):
+
+ return 0 if self._Transfer() else 1
+
+ @with_project_loaded
+ @connected
+ def run_project(self):
+
+ return 0 if self._Run() else 1
+
+
+ def finish(self):
+
+ self._Disconnect()
+
+ if not self.session.keep:
+ self.KillLocalRuntime()
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LocalRuntimeMixin.py Mon Jun 13 18:05:12 2022 +0200
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import sys
+import tempfile
+import random
+import shutil
+from util.ProcessLogger import ProcessLogger
+from util.paths import Bpath
+
+class LocalRuntimeMixin():
+
+ def __init__(self, log):
+ self.local_runtime_log = log
+ self.local_runtime = None
+ self.runtime_port = None
+ self.local_runtime_tmpdir = None
+
+ def StartLocalRuntime(self, taskbaricon=True):
+ if (self.local_runtime is None) or (self.local_runtime.exitcode is not None):
+ # create temporary directory for runtime working directory
+ self.local_runtime_tmpdir = tempfile.mkdtemp()
+ # choose an arbitrary random port for runtime
+ self.runtime_port = int(random.random() * 1000) + 61131
+ self.local_runtime_log.write(_("Starting local runtime...\n"))
+ # launch local runtime
+ self.local_runtime = ProcessLogger(
+ self.local_runtime_log,
+ "\"%s\" \"%s\" -p %s -i localhost %s %s" % (
+ sys.executable,
+ Bpath("Beremiz_service.py"),
+ self.runtime_port,
+ {False: "-x 0", True: "-x 1"}[taskbaricon],
+ self.local_runtime_tmpdir),
+ no_gui=False,
+ timeout=500, keyword=self.local_runtime_tmpdir,
+ cwd=self.local_runtime_tmpdir)
+ self.local_runtime.spin()
+ return self.runtime_port
+
+ def KillLocalRuntime(self):
+ if self.local_runtime is not None:
+ # shutdown local runtime
+ self.local_runtime.kill(gently=False)
+ # clear temp dir
+ shutil.rmtree(self.local_runtime_tmpdir)
+
+ self.local_runtime = None
+
--- a/ProjectController.py Mon May 30 15:30:51 2022 +0200
+++ b/ProjectController.py Mon Jun 13 18:05:12 2022 +0200
@@ -1526,7 +1526,8 @@
# clear previous_plcstate to restore status
# in UpdateMethodsFromPLCStatus()
self.previous_plcstate = ""
- self.AppFrame.ProgressStatusBar.Hide()
+ if self.AppFrame is not None:
+ self.AppFrame.ProgressStatusBar.Hide()
self.UpdateMethodsFromPLCStatus()
def PullPLCStatusProc(self, event):
@@ -1785,13 +1786,16 @@
"""
Start PLC
"""
+ success = False
if self.GetIECProgramsAndVariables():
self._connector.StartPLC()
self.logger.write(_("Starting PLC\n"))
self._connect_debug()
+ success = True
else:
self.logger.write_error(_("Couldn't start PLC !\n"))
wx.CallAfter(self.UpdateMethodsFromPLCStatus)
+ return success
def _Stop(self):
"""
@@ -1805,6 +1809,10 @@
wx.CallAfter(self.UpdateMethodsFromPLCStatus)
+ def StartLocalRuntime(self):
+ if self.AppFrame:
+ return self.AppFrame.StartLocalRuntime()
+
def _SetConnector(self, connector, update_status=True):
self._connector = connector
if self.AppFrame is not None:
@@ -1821,6 +1829,7 @@
wx.CallAfter(self.UpdateMethodsFromPLCStatus)
def _Connect(self):
+ success = False
# don't accept re-connetion if already connected
if self._connector is not None:
self.logger.write_error(
@@ -1882,6 +1891,8 @@
else:
self.logger.write_warning(
_("Debug does not match PLC - stop/transfert/start to re-enable\n"))
+ success = True
+ return success
def CompareLocalAndRemotePLC(self):
if self._connector is None:
@@ -1905,6 +1916,7 @@
self._SetConnector(None)
def _Transfer(self):
+ success = False
if self.IsPLCStarted():
dialog = wx.MessageDialog(
self.AppFrame,
@@ -1967,16 +1979,19 @@
if self.GetIECProgramsAndVariables():
self.UnsubscribeAllDebugIECVariable()
self.ProgramTransferred()
- self.AppFrame.CloseObsoleteDebugTabs()
- self.AppFrame.RefreshPouInstanceVariablesPanel()
- self.AppFrame.LogViewer.ResetLogCounters()
+ if self.AppFrame is not None:
+ self.AppFrame.CloseObsoleteDebugTabs()
+ self.AppFrame.RefreshPouInstanceVariablesPanel()
+ self.AppFrame.LogViewer.ResetLogCounters()
self.logger.write(_("PLC installed successfully.\n"))
+ success = True
else:
self.logger.write_error(_("Missing debug data\n"))
else:
self.logger.write_error(_("PLC couldn't be installed\n"))
wx.CallAfter(self.UpdateMethodsFromPLCStatus)
+ return success
def _Repair(self):
dialog = wx.MessageDialog(
--- a/connectors/__init__.py Mon May 30 15:30:51 2022 +0200
+++ b/connectors/__init__.py Mon Jun 13 18:05:12 2022 +0200
@@ -76,8 +76,7 @@
# pyro connection to local runtime
# started on demand, listening on random port
scheme = "PYRO"
- runtime_port = confnodesroot.AppFrame.StartLocalRuntime(
- taskbaricon=True)
+ runtime_port = confnodesroot.StartLocalRuntime()
uri = "PYROLOC://127.0.0.1:" + str(runtime_port)
# commented code to enable for MDNS:// support
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fake_wx.py Mon Jun 13 18:05:12 2022 +0200
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import new
+from types import ModuleType
+
+# TODO use gettext instead
+def get_translation(txt):
+ return txt
+
+
+class FakeObject:
+ def __init__(self, *args, **kwargs):
+ self.__classname__ = kwargs["__classname__"]
+
+ def __getattr__(self,name):
+ if name.startswith('__'):
+ raise AttributeError, name
+ return FakeObject(__classname__=self.__classname__+"."+name)
+
+ def __call__(self, *args, **kwargs):
+ return FakeObject(__classname__=self.__classname__+"()")
+
+ def __getitem__(self, key):
+ raise IndexError, key
+
+ def __str__(self):
+ return self.__classname__
+
+ def __or__(self, other):
+ return FakeObject(__classname__=self.__classname__+"|"+other.__classname__)
+
+
+class FakeClass:
+ def __init__(self, *args, **kwargs):
+ print("DUMMY Class __init__ !",self.__name__,args,kwargs)
+
+
+class FakeModule(ModuleType):
+ def __init__(self, name, classes):
+ self.__modname__ = name
+ self.__objects__ = dict(map(lambda desc:
+ (desc, new.classobj(desc, (FakeClass,), {}))
+ if type(desc)==str else desc, classes))
+ ModuleType(name)
+
+ def __getattr__(self,name):
+ if name.startswith('__'):
+ raise AttributeError, name
+
+ if self.__objects__.has_key(name):
+ return self.__objects__[name]
+
+ obj = FakeObject(__classname__=self.__modname__+"."+name)
+ self.__objects__[name] = obj
+ return obj
+
+
+# Keep track of already faked modules to catch those
+# that are already present in sys.modules from start
+# (i.e. mpl_toolkits for exemple)
+already_patched = {}
+
+for name, classes in [
+ # list given for each module name contains name string for a FakeClass,
+ # otherwise a tuple (name, object) for arbitrary object/function/class
+ ('wx',[
+ 'Panel', 'PyCommandEvent', 'Dialog', 'PopupWindow', 'TextEntryDialog',
+ 'Notebook', 'ListCtrl', 'TextDropTarget', 'PyControl', 'TextCtrl',
+ 'SplitterWindow', 'Frame', 'Printout', 'StaticBitmap',
+ ('GetTranslation', get_translation)]),
+ ('wx.lib.agw.advancedsplash',[]),
+ ('wx.lib.buttons',['GenBitmapTextButton']),
+ ('wx.adv',['EditableListBox']),
+ ('wx.grid',[
+ 'Grid', 'PyGridTableBase', 'GridCellEditor', 'GridCellTextEditor',
+ 'GridCellChoiceEditor']),
+ ('wx.lib.agw.customtreectrl',['CustomTreeCtrl']),
+ ('wx.lib.intctrl',['IntCtrl']),
+ ('matplotlib.pyplot',[]),
+ ('matplotlib.backends.backend_wxagg',['FigureCanvasWxAgg']),
+ ('wx.stc',['StyledTextCtrl']),
+ ('wx.lib.scrolledpanel',[]),
+ ('wx.lib.mixins.listctrl',['ColumnSorterMixin', 'ListCtrlAutoWidthMixin']),
+ ('wx.dataview',['PyDataViewIndexListModel']),
+ ('matplotlib.backends.backend_agg',[]),
+ ('wx.aui',[]),
+ ('mpl_toolkits.mplot3d',[])]:
+ modpath = None
+ parentmod = None
+ for identifier in name.split("."):
+ modpath = (modpath + "." + identifier) if modpath else identifier
+ mod = sys.modules.get(modpath, None)
+
+ if mod is None or modpath not in already_patched:
+ mod = FakeModule(modpath, classes)
+ sys.modules[modpath] = mod
+ already_patched[modpath] = True
+
+ if parentmod is not None:
+ parentmod.__objects__[identifier] = mod
+
+ parentmod = mod
+
+from six.moves import builtins
+
+builtins.__dict__['_'] = get_translation
+
--- a/util/paths.py Mon May 30 15:30:51 2022 +0200
+++ b/util/paths.py Mon Jun 13 18:05:12 2022 +0200
@@ -55,3 +55,8 @@
"""
return os.path.join(AbsParentDir(__file__, 2), name)
+def Bpath(name):
+ """
+ Return path of files in Beremiz project
+ """
+ return os.path.join(AbsParentDir(__file__, 1), name)