diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rwxr-xr-x | mp-tool | 88 | ||||
-rw-r--r-- | mp_tool/__init__.py | 112 | ||||
-rw-r--r-- | setup.py | 21 |
5 files changed, 229 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b35463c --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# MP-Tool + +Python commandline tool for [micropython](www.micropython.org) on the ESP 8266. + +``` +./mp-tool --help +```
\ No newline at end of file @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +import mp_tool + +import argparse +import sys + + +class HelpAction(argparse._HelpAction): + def __call__(self, parser, namespace, values, option_string=None): + formatter = parser._get_formatter() + formatter.add_usage(parser.usage, + parser._actions, + parser._mutually_exclusive_groups) + + formatter.start_section(parser._optionals.title) + formatter.add_text(parser._optionals.description) + formatter.add_arguments(parser._optionals._group_actions) + formatter.end_section() + + subparsers_actions = [ + action for action in parser._actions + if isinstance(action, argparse._SubParsersAction)] + + for subparsers_action in subparsers_actions: + # get all subparsers and print help + subparsers = subparsers_action.choices + for subaction in subparsers_action._get_subactions(): + subparser = subparsers[subaction.dest] + usage = formatter._format_actions_usage(subparser._actions, []) + usage_parent = formatter._format_actions_usage(filter( + lambda a: not (isinstance(a, HelpAction) or isinstance(a, argparse._SubParsersAction)), + parser._actions), []) + formatter.start_section("{} {} {} {}".format(formatter._prog, + usage_parent, + subaction.dest, + usage)) + formatter.add_text(subaction.help) + formatter.add_arguments(subparser._positionals._group_actions) + formatter.add_arguments(filter(lambda a: not isinstance(a, argparse._HelpAction), + subparser._optionals._group_actions)) + formatter.end_section() + + print(formatter.format_help()) + parser.exit(0) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--help", action=HelpAction, help="Display full help") + parser.add_argument("--password", action='store', nargs='?') + parser.add_argument("WEBSOCKET", action='store', nargs=1, + help="Websocket address (e.g. ws://ESP_E1278E:8266)") + + subparsers = parser.add_subparsers() + + parser_eval = subparsers.add_parser("eval", help="Eval python code remotely") + parser_eval.set_defaults(func=mp_tool.do_eval) + parser_eval.add_argument("CODE", action='store', nargs=1, help="Code to execute") + + parser_repl = subparsers.add_parser("repl", help="Start interactive REPL") + parser_repl.set_defaults(func=mp_tool.do_repl) + + parser_put = subparsers.add_parser("put", help="Send file to remote") + parser_put.set_defaults(func=mp_tool.do_put) + + parser_get = subparsers.add_parser("get", help="Load file from remote") + parser_put.set_defaults(func=mp_tool.do_get) + + args = parser.parse_args(sys.argv[1:]) + if 'func' in args: + args.func(args) + else: + subparsers_actions = [ + action for action in parser._actions + if isinstance(action, argparse._SubParsersAction)] + + formatter = parser._get_formatter() + formatter.add_usage(parser.usage, + parser._actions, + parser._mutually_exclusive_groups) + + formatter.start_section("Choose subcommand") + for subparsers_action in subparsers_actions: + formatter.add_argument(subparsers_action) + formatter.end_section() + + print(formatter.format_help()) + parser.exit(0) diff --git a/mp_tool/__init__.py b/mp_tool/__init__.py new file mode 100644 index 0000000..481ba39 --- /dev/null +++ b/mp_tool/__init__.py @@ -0,0 +1,112 @@ +import websocket + +import tty +import termios +from threading import Thread +from sys import stdout, stdin +from copy import copy + + +def connect_and_auth(url, password) -> websocket.WebSocket: + ws = websocket.create_connection(url, timeout=0.5) + frame = ws.recv_frame() + + if frame.data != b"Password: ": + raise Exception("Unexpected response: {}".format(frame.data)) + stdout.write(frame.data.decode('utf-8')) + ws.send(password + "\n") + + frame = ws.recv_frame() + if frame.data.strip() != b"WebREPL connected\r\n>>>": + raise Exception("Unexpected response: {}".format(frame.data)) + return ws + + +def do_eval(args): + ws = connect_and_auth(args.WEBSOCKET[0], args.password) + ws.send("\x02") + stdout.write(read_until_eval_or_timeout(ws)) + ws.send(args.CODE[0] + "\r\n") + + result = read_until_eval_or_timeout(ws) + stdout.write(result[:-6]) + print("") + ws.close() + + +def read_until_eval_or_timeout(ws: websocket.WebSocket): + buf = "" + while not buf.endswith("\r\n>>> "): + buf += ws.recv() + return buf + + +class Reader(Thread): + def __init__(self, ws): + Thread.__init__(self) + self.ws = ws + self.stop = False + + def run(self): + while True: + try: + frame = self.ws.recv_frame() + stdout.write(frame.data.decode('utf-8')) + stdout.flush() + except Exception as e: + if self.stop: + break + + +def set_tty_raw_mode(fd): + saved_mode = termios.tcgetattr(fd) + + new_mode = copy(saved_mode) + new_mode[tty.LFLAG] = new_mode[tty.LFLAG] & ~termios.ECHO + new_mode[tty.CC][tty.VMIN] = 1 + new_mode[tty.CC][tty.VTIME] = 0 + set_tty_mode(fd, new_mode) + + return saved_mode + + +def set_tty_mode(fd, mode): + termios.tcsetattr(fd, termios.TCSAFLUSH, mode) + + +def do_repl(args): + print("Type ^[ CTRL-] or CTRL-D to quit") + ws = connect_and_auth(args.WEBSOCKET[0], args.password) + ws.send("\x02") + + reader = Reader(ws) + reader.start() + + saved_tty_mode = set_tty_raw_mode(stdin.fileno()) + try: + tty.setraw(stdin.fileno()) + while True: + try: + in_char = stdin.read(1) + if in_char == "\x1d" or in_char == "\x04": # escape char 'Ctrl-]' or CTRL-C + break + else: + ws.send(in_char) + except KeyboardInterrupt: + break + except Exception as _: + pass + + reader.stop = True + ws.close() + + set_tty_mode(stdin.fileno(), saved_tty_mode) + print("") + + +def do_put(args): + raise NotImplementedError() + + +def do_get(args): + raise NotImplementedError() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..facc0a5 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +from setuptools import setup + +setup(name='mp-tool', + version='0.1', + description='CLI tool to interact with micropython webrepl', + author='Yves Fischer', + author_email='yvesf+git@xapek.org', + license="MIT", + packages=['mp_tool'], + scripts=['mp-tool'], + url='https://example.com/', + install_requires=['websocket_client==0.40.0'], + tests_require=[], + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + ]) |