summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md7
-rwxr-xr-xmp-tool88
-rw-r--r--mp_tool/__init__.py112
-rw-r--r--setup.py21
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
diff --git a/mp-tool b/mp-tool
new file mode 100755
index 0000000..b990e27
--- /dev/null
+++ b/mp-tool
@@ -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",
+ ])