import kivy from kivy.app import App from kivy.logger import Logger from kivy.uix.stacklayout import StackLayout from kivy.config import ConfigParser from kivy.clock import Clock, mainthread from kivy.factory import Factory from multi_input_box import MultiInputBox import configparser from functools import partial import subprocess import threading import select import re import os ''' user defined macros are configurable and stored in a configuration file called macros.ini or macros-cnc.ini if in cnc mode format is:- button name = command to send ''' class MacrosWidget(StackLayout): """adds macro buttons""" def __init__(self, **kwargs): super(MacrosWidget, self).__init__(**kwargs) self.app = App.get_running_app() # we do this so the kv defined buttons are loaded first Clock.schedule_once(self._load_user_buttons) self.toggle_buttons = {} def _handle_toggle(self, name, v): t = self.toggle_buttons.get(name, None) if t is None: Logger.error("MacrosWidget: no toggle button named: {}".format(name)) return if t[4].state == 'normal': t[4].text = t[0] self.send(t[3]) # NOTE the button already toggled so the states are reversed else: t[4].text = t[1] self.send(t[2]) def reload(self): self.toggle_buttons = {} for mb in self.walk(restrict=True): if hasattr(mb, 'ud') and mb.ud: self.remove_widget(mb) self._load_user_buttons() def _load_user_buttons(self, *args): # load user defined macros fn = None if self.app.is_cnc: # check to see if we have a macros-cnc.ini if so use it if os.path.isfile('macros-cnc.ini') and os.access('macros-cnc.ini', os.R_OK): fn = 'macros-cnc.ini' if fn is None: fn = 'macros.ini' if not (os.path.isfile(fn) and os.access(fn, os.R_OK)): Logger.info("MacrosWidget: no user defined macros file to load") return self.macro_file = fn try: config = configparser.ConfigParser() config.read(fn) # add toggle button handling switch states for section in config.sections(): if section.startswith('toggle button '): name = config.get(section, 'name', fallback=None) poll = config.getboolean(section, 'poll', fallback=False) lon = config.get(section, 'label on', fallback=None) loff = config.get(section, 'label off', fallback=None) cmd_on = config.get(section, 'command on', fallback=None) cmd_off = config.get(section, 'command off', fallback=None) if name is None or lon is None or loff is None or cmd_on is None or cmd_off is None: Logger.error("MacrosWidget: config error - {} is invalid".format(section)) continue tbtn = Factory.MacroToggleButton() tbtn.text = lon self.toggle_buttons[name] = (lon, loff, cmd_on, cmd_off, tbtn, poll) tbtn.bind(on_press=partial(self._handle_toggle, name)) tbtn.ud = True self.add_widget(tbtn) elif section.startswith('script '): name = config.get(section, 'name', fallback=None) script = config.get(section, 'exec', fallback=None) args = config.get(section, 'args', fallback=None) io = config.getboolean(section, 'io', fallback=False) btn = Factory.MacroButton() btn.text = name btn.background_color = (1, 1, 0, 1) btn.bind(on_press=partial(self.exec_script, script, io, args)) btn.ud = True self.add_widget(btn) # add simple macro buttons (with optional prompts) for (key, v) in config.items('macro buttons'): btn = Factory.MacroButton() btn.text = key btn.bind(on_press=partial(self.send, v)) btn.ud = True self.add_widget(btn) except Exception as err: Logger.warning('MacrosWidget: WARNING - exception parsing config file: {}'.format(err)) def new_macro(self): o = MultiInputBox(title='Add Macro') o.setOptions(['Name', 'Command'], self._new_macro) o.open() def _new_macro(self, opts): if opts and opts['Name'] and opts['Command']: btn = Factory.MacroButton() btn.text = opts['Name'] btn.bind(on_press=partial(self.send, opts['Command'])) btn.ud = True self.add_widget(btn) # write it to macros.ini try: config = configparser.ConfigParser() config.read(self.macro_file) if not config.has_section("macro buttons"): config.add_section("macro buttons") config.set("macro buttons", opts['Name'], opts['Command']) with open(self.macro_file, 'w') as configfile: config.write(configfile) Logger.info('MacrosWidget: added macro button {}'.format(opts['Name'])) except Exception as err: Logger.error('MacrosWidget: ERROR - exception writing config file: {}'.format(err)) def update_buttons(self): # check the state of the toggle macro buttons that have poll set, called when we switch to the macro window # we send the new $S command so it gets processed immediately despite being busy cmd = "$S" for name in self.toggle_buttons: if self.toggle_buttons[name][5]: # if poll is set cmd += " " cmd += name if len(cmd) <= 3: cmd = "" else: cmd += "\n" return cmd @mainthread def switch_response(self, name, value): # check response and compare state with current state and toggle to match state if necessary t = self.toggle_buttons.get(name, None) if t is None: Logger.error("MacrosWidget: switch_response no toggle button named: {}".format(name)) return if value == '0' and t[4].state != 'normal': t[4].state = 'normal' t[4].text = t[0] elif value == '1' and t[4].state == 'normal': t[4].state = 'down' t[4].text = t[1] def _substitute_args(self, m, arg): """ substitute {?prompt}) with prompted value """ # get arguments to prompt for v = [x[2:-1] for x in m] mb = MultiInputBox(title="Arguments for {}".format(arg)) mb.setOptions(v, partial(self._do_substitute_exec, m, arg)) mb.open() def _do_substitute_exec(self, m, arg, opts): for i in m: v = opts[i[2:-1]] if v: arg = arg.replace(i, v) else: self.app.main_window.async_display("ERROR: argument missing for {}".format(i)) return self.app.comms.write('{}\n'.format(arg)) def _send_file(self, fn): try: with open(fn) as f: for line in f: # FIXME on V2 this may well go too fast self.app.comms.write('{}'.format(line)) except Exception: self.app.main_window.async_display("ERROR: File not found: {}".format(fn)) def send(self, cmd, *args): # if first character is @ then execute contents of the following file name if cmd.startswith('@'): self._send_file(cmd[1:]) return # look for {?prompt}) and substitute entered value if found m = re.findall(r'\{\?[^}]+\}', cmd) if m: self._substitute_args(m, cmd) else: # plain command just send it self.app.comms.write('{}\n'.format(cmd)) def exec_script(self, cmd, io, params, *args): if params is not None: ll = params.split(',') mb = MultiInputBox(title='Arguments') mb.setOptions(ll, partial(self._exec_script_params, cmd, io)) mb.open() else: self._exec_script(cmd, io) def _exec_script_params(self, cmd, io, opts): for x in opts: cmd += " " + x + " " + opts[x] self._exec_script(cmd, io) def _exec_script(self, cmd, io): # needs to be run in a thread t = threading.Thread(target=self._script_thread, daemon=True, args=(cmd, io,)) t.start() def _send_it(self, p, x): p.stdin.write("{}\n".format(x)) # print("{}\n".format(x)) def _script_thread(self, cmd, io): try: if io: if not self.app.is_connected: Logger.error('MacrosWidget: Not connected') self.app.main_window.async_display('> not connected') io = False return repeating = False if cmd.startswith('-'): # repeating output on same line repeating = True cmd = cmd[1:] # I/O is piped to/from smoothie self.app.main_window.async_display("> running script: {}".format(cmd)) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True, bufsize=1) self.app.comms.redirect_incoming(lambda x: self._send_it(p, x)) # so we can see which has output poll_obj = select.poll() poll_obj.register(p.stdout, select.POLLIN) poll_obj.register(p.stderr, select.POLLIN) while p.returncode is None: poll_result = poll_obj.poll(0) for pr in poll_result: if pr[0] == p.stdout.name: s = p.stdout.readline() if s: if not repeating: self.app.main_window.async_display("<<< script: {}".format(s.rstrip())) self.app.comms.write('{}'.format(s)) elif pr[0] == p.stderr.name: e = p.stderr.readline() if e: if repeating: self.app.main_window.async_display('{}\r'.format(e.rstrip())) else: self.app.main_window.async_display('>>> script: {}'.format(e.rstrip())) p.poll() self.app.main_window.async_display('> script complete') else: # just display results self.app.main_window.async_display("> {}".format(cmd)) p = subprocess.Popen(cmd, shell=True, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result, err = p.communicate() for l in result.splitlines(): self.app.main_window.async_display(l) for l in err.splitlines(): self.app.main_window.async_display(l) if p.returncode != 0: self.app.main_window.async_display("return code: {}".format(p.returncode)) except Exception as err: Logger.error('MacrosWidget: script exception: {}'.format(err)) self.app.main_window.async_display('>>> script exception, see log') finally: if io and self.app.is_connected: self.app.comms.redirect_incoming(None)