# HG changeset patch # User Edouard Tisserant # Date 1655136312 -7200 # Node ID fa291393aac72d2d8b6c5f9c101c2399ee82c565 # Parent 88c4b18453d565a4420791c832e25e8300bcc72e Added preliminary CLI. Supports building a project, transfering and running on PLC. diff -r 88c4b18453d5 -r fa291393aac7 BeremizIDE.py --- 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 diff -r 88c4b18453d5 -r fa291393aac7 Beremiz_cli.py --- /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() + diff -r 88c4b18453d5 -r fa291393aac7 CLIController.py --- /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() + + diff -r 88c4b18453d5 -r fa291393aac7 LocalRuntimeMixin.py --- /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 + diff -r 88c4b18453d5 -r fa291393aac7 ProjectController.py --- 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( diff -r 88c4b18453d5 -r fa291393aac7 connectors/__init__.py --- 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 diff -r 88c4b18453d5 -r fa291393aac7 fake_wx.py --- /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 + diff -r 88c4b18453d5 -r fa291393aac7 util/paths.py --- 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)