Example #1
0
    def test_valid_strings(self):

        tests = [
            ("simple1", [Token(Token.STR, "simple1")]),
            (
                "simple1 simple2.txt",
                [Token(Token.STR, "simple1"),
                 Token(Token.STR, "simple2.txt")],
            ),
            ('"Quoted"', [Token(Token.QSTR, "Quoted")]),
            (
                '"Quoted with whitespace" non-quoted',
                [
                    Token(Token.QSTR, "Quoted with whitespace"),
                    Token(Token.STR, "non-quoted"),
                ],
            ),
            (
                '"$1+2  _3%2*1+2-2/#.py~"  $1+2_3%2*1+2-2/#.py~',
                [
                    Token(Token.QSTR, "$1+2  _3%2*1+2-2/#.py~"),
                    Token(Token.STR, "$1+2_3%2*1+2-2/#.py~"),
                ],
            ),
        ]

        t = Tokenizer()

        for string, exp_tokens in tests:
            tokens, rest = t.tokenize(string)
            assert rest == ""
            assert self.__cmp_tokens(exp_tokens, tokens)
Example #2
0
    def test_valid_strings(self):

        tests = [
            ("simple1", [Token(Token.STR, "simple1")]),
            (
                "simple1 simple2.txt",
                [Token(Token.STR, "simple1"), Token(Token.STR, "simple2.txt")],
            ),
            ('"Quoted"', [Token(Token.QSTR, "Quoted")]),
            (
                '"Quoted with whitespace" non-quoted',
                [
                    Token(Token.QSTR, "Quoted with whitespace"),
                    Token(Token.STR, "non-quoted"),
                ],
            ),
            (
                '"$1+2  _3%2*1+2-2/#.py~"  $1+2_3%2*1+2-2/#.py~',
                [
                    Token(Token.QSTR, "$1+2  _3%2*1+2-2/#.py~"),
                    Token(Token.STR, "$1+2_3%2*1+2-2/#.py~"),
                ],
            ),
        ]

        t = Tokenizer()

        for string, exp_tokens in tests:
            tokens, rest = t.tokenize(string)
            assert rest == ""
            assert self.__cmp_tokens(exp_tokens, tokens)
Example #3
0
    def test_invalid_strings(self):

        tests = [
            ('char ? is invalid', '? is invalid'),
            ('"char ? is invalid"', '"char ? is invalid"'),
            ('"unbalanced quotes', '"unbalanced quotes'),
            ('"valid quotes" valid "unbalanced quotes', '"unbalanced quotes'),
            ('unbalanced quotes"', '"'),
        ]

        t = Tokenizer()

        for string, exp_rest in tests:
            tokens, rest = t.tokenize(string)
            assert rest == exp_rest
Example #4
0
    def test_invalid_strings(self):

        tests = [
            ("char ? is invalid", "? is invalid"),
            ('"char ? is invalid"', '"char ? is invalid"'),
            ('"unbalanced quotes', '"unbalanced quotes'),
            ('"valid quotes" valid "unbalanced quotes', '"unbalanced quotes'),
            ('unbalanced quotes"', '"'),
        ]

        t = Tokenizer()

        for string, exp_rest in tests:
            tokens, rest = t.tokenize(string)
            assert rest == exp_rest
Example #5
0
    def test_valid_strings(self):

        tests = [
            ('simple1', [
                Token(Token.STR, 'simple1')]),
            ('simple1 simple2.txt', [
                Token(Token.STR, 'simple1'),
                Token(Token.STR, 'simple2.txt')]),
            ('"Quoted"', [
                Token(Token.QSTR, 'Quoted')]),
            ('"Quoted with whitespace" non-quoted', [
                Token(Token.QSTR, 'Quoted with whitespace'),
                Token(Token.STR, 'non-quoted')]),
            ('"$1+2  _3%2*1+2-2/#.py~"  $1+2_3%2*1+2-2/#.py~', [
                Token(Token.QSTR, '$1+2  _3%2*1+2-2/#.py~'),
                Token(Token.STR, '$1+2_3%2*1+2-2/#.py~')]),
        ]

        t = Tokenizer()

        for string, exp_tokens in tests:
            tokens, rest = t.tokenize(string)
            assert rest == ''
            assert self.__cmp_tokens(exp_tokens, tokens)
Example #6
0
class MpFileShell(cmd.Cmd):
    def __init__(self, color=False, caching=False, reset=False):
        if color:
            colorama.init()
            cmd.Cmd.__init__(self, stdout=colorama.initialise.wrapped_stdout)
        else:
            cmd.Cmd.__init__(self)

        if platform.system() == "Windows":
            self.use_rawinput = False

        self.color = color
        self.caching = caching
        self.reset = reset

        self.fe = None
        self.repl = None
        self.tokenizer = Tokenizer()

        self.__intro()
        self.__set_prompt_path()

    def __del__(self):
        self.__disconnect()

    def __intro(self):

        if self.color:
            self.intro = ("\n" + colorama.Fore.GREEN +
                          "** Micropython File Shell v%s, [email protected] ** " %
                          version.FULL + colorama.Fore.RESET + "\n")
        else:
            self.intro = (
                "\n** Micropython File Shell v%s, [email protected] **\n" %
                version.FULL)

        self.intro += "-- Running on Python %d.%d using PySerial %s --\n" % (
            sys.version_info[0],
            sys.version_info[1],
            serial.VERSION,
        )

    def __set_prompt_path(self):

        if self.fe is not None:
            pwd = self.fe.pwd()
        else:
            pwd = "/"

        if self.color:
            self.prompt = (colorama.Fore.BLUE + "mpfs [" +
                           colorama.Fore.YELLOW + pwd + colorama.Fore.BLUE +
                           "]> " + colorama.Fore.RESET)
        else:
            self.prompt = "mpfs [" + pwd + "]> "

    def __error(self, msg):

        if self.color:
            print("\n" + colorama.Fore.RED + msg + colorama.Fore.RESET + "\n")
        else:
            print("\n" + msg + "\n")

    def __connect(self, port):

        try:
            self.__disconnect()

            if self.reset:
                print("Hard resetting device ...")
            if self.caching:
                self.fe = MpFileExplorerCaching(port, self.reset)
            else:
                self.fe = MpFileExplorer(port, self.reset)
            print("Connected to %s" % self.fe.sysname)
            self.__set_prompt_path()
            return True
        except PyboardError as e:
            logging.error(e)
            self.__error(str(e))
        except ConError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        except AttributeError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        return False

    def __disconnect(self):

        if self.fe is not None:
            try:
                self.fe.close()
                self.fe = None
                self.__set_prompt_path()
            except RemoteIOError as e:
                self.__error(str(e))

    def __is_open(self):

        if self.fe is None:
            self.__error("Not connected to device. Use 'open' first.")
            return False

        return True

    def __parse_file_names(self, args):

        tokens, rest = self.tokenizer.tokenize(args)

        if rest != "":
            self.__error("Invalid filename given: %s" % rest)
        else:
            return [token.value for token in tokens]

        return None

    def do_exit(self, args):
        """exit
        Exit this shell.
        """
        self.__disconnect()

        return True

    do_EOF = do_exit

    def do_open(self, args):
        """open <TARGET>
        Open connection to device with given target. TARGET might be:

        - a serial port, e.g.       ttyUSB0, ser:/dev/ttyUSB0
        - a telnet host, e.g        tn:192.168.1.1 or tn:192.168.1.1,login,passwd
        - a websocket host, e.g.    ws:192.168.1.1 or ws:192.168.1.1,passwd
        """

        if not len(args):
            self.__error("Missing argument: <PORT>")
            return False

        if (not args.startswith("ser:/dev/") and not args.startswith("ser:COM")
                and not args.startswith("tn:") and not args.startswith("ws:")):

            if platform.system() == "Windows":
                args = "ser:" + args
            else:
                args = "ser:/dev/" + args

        return self.__connect(args)

    def complete_open(self, *args):
        ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
        return [i[5:] for i in ports if i[5:].startswith(args[0])]

    def do_close(self, args):
        """close
        Close connection to device.
        """

        self.__disconnect()

    def do_ls(self, args):
        """ls
        List remote files.
        """

        if self.__is_open():
            try:
                files = self.fe.ls(add_details=True)

                if self.fe.pwd() != "/":
                    files = [("..", "D")] + files

                print("\nRemote files in '%s':\n" % self.fe.pwd())

                for elem, type in files:
                    if type == "F":
                        if self.color:
                            print(colorama.Fore.CYAN + ("       %s" % elem) +
                                  colorama.Fore.RESET)
                        else:
                            print("       %s" % elem)
                    else:
                        if self.color:
                            print(colorama.Fore.MAGENTA +
                                  (" <dir> %s" % elem) + colorama.Fore.RESET)
                        else:
                            print(" <dir> %s" % elem)

                print("")

            except IOError as e:
                self.__error(str(e))

    def do_pwd(self, args):
        """pwd
        Print current remote directory.
        """
        if self.__is_open():
            print(self.fe.pwd())

    def do_cd(self, args):
        """cd <TARGET DIR>
        Change current remote directory to given target.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.cd(s_args[0])
                self.__set_prompt_path()
            except IOError as e:
                self.__error(str(e))

    def complete_cd(self, *args):

        try:
            files = self.fe.ls(add_files=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_md(self, args):
        """md <TARGET DIR>
        Create new remote directory.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.md(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def do_lls(self, args):
        """lls
        List files in current local directory.
        """

        files = os.listdir(".")

        print("\nLocal files:\n")

        for f in files:
            if os.path.isdir(f):
                if self.color:
                    print(colorama.Fore.MAGENTA + (" <dir> %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print(" <dir> %s" % f)
        for f in files:
            if os.path.isfile(f):
                if self.color:
                    print(colorama.Fore.CYAN + ("       %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print("       %s" % f)
        print("")

    def do_lcd(self, args):
        """lcd <TARGET DIR>
        Change current local directory to given target.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL DIR>")
        else:
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <LOCAL DIR>")
                    return

                os.chdir(s_args[0])
            except OSError as e:
                self.__error(str(e).split("] ")[-1])

    def complete_lcd(self, *args):
        dirs = [
            o for o in os.listdir(".") if os.path.isdir(os.path.join(".", o))
        ]
        return [i for i in dirs if i.startswith(args[0])]

    def do_lpwd(self, args):
        """lpwd
        Print current local directory.
        """

        print(os.getcwd())

    def do_put(self, args):
        """put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.
        """

        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = lfile_name

            try:
                self.fe.put(lfile_name, rfile_name)
            except IOError as e:
                self.__error(str(e))

    def complete_put(self, *args):
        files = [
            o for o in os.listdir(".") if os.path.isfile(os.path.join(".", o))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_mput(self, args):
        """mput <SELECTION REGEX>
        Upload all local files that match the given regular expression.
        The remote files will be named the same as the local files.

        "mput" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mput(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def do_get(self, args):
        """get <REMOTE FILE> [<LOCAL FILE>]
        Download remote file. If the second parameter is given,
        its value is used for the local file name. Otherwise the
        locale file will be named the same as the remote file.
        """

        if not len(args):
            self.__error("Missing arguments: <REMOTE FILE> [<LOCAL FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <REMOTE FILE> [<LOCAL FILE>]"
                )
                return

            rfile_name = s_args[0]

            if len(s_args) > 1:
                lfile_name = s_args[1]
            else:
                lfile_name = rfile_name

            try:
                self.fe.get(rfile_name, lfile_name)
            except IOError as e:
                self.__error(str(e))

    def do_mget(self, args):
        """mget <SELECTION REGEX>
        Download all remote files that match the given regular expression.
        The local files will be named the same as the remote files.

        "mget" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mget(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_get(self, *args):

        try:
            files = self.fe.ls(add_dirs=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_rm(self, args):
        """rm <REMOTE FILE or DIR>
        Delete a remote file or directory.

        Note: only empty directories could be removed.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                self.fe.rm(s_args[0])
            except IOError as e:
                self.__error(str(e))
            except PyboardError:
                self.__error("Unable to send request to %s" % self.fe.sysname)

    def do_mrm(self, args):
        """mrm <SELECTION REGEX>
        Delete all remote files that match the given regular expression.

        "mrm" does not delete directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mrm(args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_rm(self, *args):

        try:
            files = self.fe.ls()
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_cat(self, args):
        """cat <REMOTE FILE>
        Print the contents of a remote file.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                print(self.fe.gets(s_args[0]))
            except IOError as e:
                self.__error(str(e))

    complete_cat = complete_get

    def do_exec(self, args):
        """exec <STATEMENT>
        Execute a Python statement on remote.
        """
        def data_consumer(data):
            data = str(data.decode("utf-8"))
            sys.stdout.write(data.strip("\x04"))

        if not len(args):
            self.__error("Missing argument: <STATEMENT>")
        elif self.__is_open():

            try:
                self.fe.exec_raw_no_follow(args + "\n")
                ret = self.fe.follow(None, data_consumer)

                if len(ret[-1]):
                    self.__error(ret[-1].decode("utf-8"))

            except IOError as e:
                self.__error(str(e))
            except PyboardError as e:
                self.__error(str(e))

    def do_repl(self, args):
        """repl
        Enter Micropython REPL.
        """

        import serial

        ver = serial.VERSION.split(".")

        if int(ver[0]) < 2 or (int(ver[0]) == 2 and int(ver[1]) < 7):
            self.__error("REPL needs PySerial version >= 2.7, found %s" %
                         serial.VERSION)
            return

        if self.__is_open():

            if self.repl is None:

                from mp.term import Term

                self.repl = Term(self.fe.con)

                if platform.system() == "Windows":
                    self.repl.exit_character = chr(0x11)
                else:
                    self.repl.exit_character = chr(0x1D)

                self.repl.raw = True
                self.repl.set_rx_encoding("UTF-8")
                self.repl.set_tx_encoding("UTF-8")

            else:
                self.repl.serial = self.fe.con

            pwd = self.fe.pwd()
            self.fe.teardown()
            self.repl.start()

            if self.repl.exit_character == chr(0x11):
                print("\n*** Exit REPL with Ctrl+Q ***")
            else:
                print("\n*** Exit REPL with Ctrl+] ***")

            try:
                self.repl.join(True)
            except Exception:
                pass

            self.repl.console.cleanup()

            if self.caching:
                # Clear the file explorer cache so we can see any new files.
                self.fe.cache = {}

            self.fe.setup()
            try:
                self.fe.cd(pwd)
            except RemoteIOError as e:
                # Working directory does not exist anymore
                self.__error(str(e))
            finally:
                self.__set_prompt_path()
            print("")

    def do_mpyc(self, args):
        """mpyc <LOCAL PYTHON FILE>
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the path).
        The compiled file has the same name as the original file but with extension '.mpy'.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL FILE>")
        else:

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <LOCAL FILE>")
                return

            try:
                self.fe.mpy_cross(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def complete_mpyc(self, *args):
        files = [
            o for o in os.listdir(".")
            if (os.path.isfile(os.path.join(".", o)) and o.endswith(".py"))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_putc(self, args):
        """mputc <LOCAL PYTHON FILE> [<REMOTE FILE>]
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the
        path) and upload it. The compiled file has the same name as the original file but
        with extension '.mpy' by default.
        """
        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():
            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = (lfile_name[:lfile_name.rfind(".")]
                              if "." in lfile_name else lfile_name) + ".mpy"

            _, tmp = tempfile.mkstemp()

            try:
                self.fe.mpy_cross(src=lfile_name, dst=tmp)
                self.fe.put(tmp, rfile_name)
            except IOError as e:
                self.__error(str(e))

            os.unlink(tmp)

    complete_putc = complete_mpyc
Example #7
0
class MpFileShell(cmd.Cmd):
    def view_all_serial(self):
        import serial.tools.list_ports
        print(colorama.Fore.LIGHTCYAN_EX + "\nlooking for computer port...")
        plist = list(serial.tools.list_ports.comports())

        if len(plist) <= 0:
            print("serial not found!")
        else:
            for serial in plist:
                print("serial name :", serial[0].split('/')[-1])
            print("input ' open", plist[len(plist) - 1][0].split('/')[-1],
                  "' and enter connect your board.")

    def __init__(self, color=False, caching=False, reset=False):
        if color:
            colorama.init()
            cmd.Cmd.__init__(self, stdout=colorama.initialise.wrapped_stdout)
        else:
            cmd.Cmd.__init__(self)

        self.color = color
        self.caching = caching
        self.reset = reset
        self.open_args = None
        self.fe = None
        self.repl = None
        self.tokenizer = Tokenizer()

        if platform.system() == 'Windows':
            self.use_rawinput = False

        if platform.system() == 'Darwin':
            self.reset = True

        self.__intro()
        self.__set_prompt_path()

        self.do_help(None)
        print(
            colorama.Fore.YELLOW +
            "All support commands, can input help ls or other command if you don't know how to use it(ls)."
        )
        self.view_all_serial()

    def __del__(self):
        self.__disconnect()

    def __intro(self):

        if self.color:
            self.intro = '\n' + colorama.Fore.GREEN + \
                         '** Micropython File Shell v%s, [email protected] & [email protected] ** ' % version.FULL + \
                         colorama.Fore.RESET + '\n'
        else:
            self.intro = '\n** Micropython File Shell v%s, [email protected] & [email protected] **\n' % version.FULL

        self.intro += '-- Running on Python %d.%d using PySerial %s --\n' \
                       % (sys.version_info[0], sys.version_info[1], serial.VERSION)

    def __set_prompt_path(self):

        if self.fe is not None:
            pwd = self.fe.pwd()
        else:
            pwd = "/"

        if self.color:
            self.prompt = colorama.Fore.BLUE + "mpfs [" + \
                          colorama.Fore.YELLOW + pwd + \
                          colorama.Fore.BLUE + "]> " + colorama.Fore.RESET
        else:
            self.prompt = "mpfs [" + pwd + "]> "

    def __error(self, msg):

        if self.color:
            print('\n' + colorama.Fore.RED + msg + colorama.Fore.RESET + '\n')
        else:
            print('\n' + msg + '\n')

    def __connect(self, port):

        try:
            self.__disconnect()
            if (port is None):
                port = self.open_args
            if self.reset:
                print("Hard resetting device ...")
            if self.caching:
                self.fe = MpFileExplorerCaching(port, self.reset)
            else:
                self.fe = MpFileExplorer(port, self.reset)
            print("Connected to %s" % self.fe.sysname)
            self.__set_prompt_path()
        except PyboardError as e:
            logging.error(e)
            self.__error(str(e))
        except ConError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        except AttributeError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)

        if self.__is_open() == False:
            time.sleep(3)
            self.__connect(None)

    def __reconnect(self):
        import time
        for a in range(3):
            self.__connect(None)
            if self.__is_open():
                break
            print(colorama.Fore.GREEN + 'try reconnect... ' +
                  colorama.Fore.RESET)
            time.sleep(3)

    def __disconnect(self):

        if self.fe is not None:
            try:
                self.fe.close()
                self.fe = None
                self.__set_prompt_path()
            except RemoteIOError as e:
                self.__error(str(e))

    def __is_open(self):

        if self.fe is None:
            self.__error("Not connected to device. Use 'open' first.")
            return False

        return True

    def __parse_file_names(self, args):

        tokens, rest = self.tokenizer.tokenize(args)

        if rest != '':
            self.__error("Invalid filename given: %s" % rest)
        else:
            return [token.value for token in tokens]

        return None

    def do_q(self, args):
        return self.do_quit(args)

    def do_quit(self, args):
        """quit(q)
        Exit this shell.
        """
        self.__disconnect()

        return True

    do_EOF = do_quit

    def do_o(self, args):
        return self.do_open(args)

    def do_open(self, args):
        """open(o) <TARGET>
        Open connection to device with given target. TARGET might be:

        - a serial port, e.g.       ttyUSB0, ser:/dev/ttyUSB0
        - a telnet host, e.g        tn:192.168.1.1 or tn:192.168.1.1,login,passwd
        - a websocket host, e.g.    ws:192.168.1.1 or ws:192.168.1.1,passwd
        """

        if not len(args):
            self.__error("Missing argument: <PORT>")
        else:
            if not args.startswith("ser:/dev/") \
                    and not args.startswith("ser:COM") \
                    and not args.startswith("tn:") \
                    and not args.startswith("ws:"):

                if platform.system() == "Windows":
                    args = "ser:" + args
                elif '/dev' in args:
                    args = "ser:" + args
                else:
                    args = "ser:/dev/" + args

            self.open_args = args

            self.__connect(args)

    def complete_open(self, *args):
        ports = glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')
        return [i[5:] for i in ports if i[5:].startswith(args[0])]

    def do_close(self, args):
        """close
        Close connection to device.
        """

        self.__disconnect()

    def do_ls(self, args):
        """ls
        List remote files.
        """

        if self.__is_open():
            try:
                files = self.fe.ls(add_details=True)

                if self.fe.pwd() != "/":
                    files = [("..", "D")] + files

                print("\nRemote files in '%s':\n" % self.fe.pwd())

                for elem, type in files:
                    if type == 'F':
                        if self.color:
                            print(colorama.Fore.CYAN + ("       %s" % elem) +
                                  colorama.Fore.RESET)
                        else:
                            print("       %s" % elem)
                    else:
                        if self.color:
                            print(colorama.Fore.MAGENTA +
                                  (" <dir> %s" % elem) + colorama.Fore.RESET)
                        else:
                            print(" <dir> %s" % elem)

                print("")

            except IOError as e:
                self.__error(str(e))

    def do_pwd(self, args):
        """pwd
         Print current remote directory.
         """
        if self.__is_open():
            print(self.fe.pwd())

    def do_cd(self, args):
        """cd <TARGET DIR>
        Change current remote directory to given target.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.cd(s_args[0])
                self.__set_prompt_path()
            except IOError as e:
                self.__error(str(e))

    def complete_cd(self, *args):

        try:
            files = self.fe.ls(add_files=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_md(self, args):
        """md <TARGET DIR>
        Create new remote directory.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.md(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def do_lls(self, args):
        """lls
        List files in current local directory.
        """

        files = os.listdir(".")

        print("\nLocal files:\n")

        for f in files:
            if os.path.isdir(f):
                if self.color:
                    print(colorama.Fore.MAGENTA + (" <dir> %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print(" <dir> %s" % f)
        for f in files:
            if os.path.isfile(f):
                if self.color:
                    print(colorama.Fore.CYAN + ("       %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print("       %s" % f)
        print("")

    def do_lcd(self, args):
        """lcd <TARGET DIR>
        Change current local directory to given target.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL DIR>")
        else:
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <LOCAL DIR>")
                    return

                os.chdir(s_args[0])
            except OSError as e:
                self.__error(str(e).split("] ")[-1])

    def complete_lcd(self, *args):
        dirs = [
            o for o in os.listdir(".") if os.path.isdir(os.path.join(".", o))
        ]
        return [i for i in dirs if i.startswith(args[0])]

    def do_lpwd(self, args):
        """lpwd
        Print current local directory.
        """

        print(os.getcwd())

    def do_put(self, args):
        """put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.
        """

        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = lfile_name
            try:
                self.fe.put(lfile_name, rfile_name)
            except IOError as e:
                self.__error(str(e))

    def complete_put(self, *args):
        files = [
            o for o in os.listdir(".") if os.path.isfile(os.path.join(".", o))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_mput(self, args):
        """mput <SELECTION REGEX>
        Upload all local files that match the given regular expression.
        The remote files will be named the same as the local files.

        "mput" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mput(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def do_get(self, args):
        """get <REMOTE FILE> [<LOCAL FILE>]
        Download remote file. If the second parameter is given,
        its value is used for the local file name. Otherwise the
        locale file will be named the same as the remote file.
        """

        if not len(args):
            self.__error("Missing arguments: <REMOTE FILE> [<LOCAL FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <REMOTE FILE> [<LOCAL FILE>]"
                )
                return

            rfile_name = s_args[0]

            if len(s_args) > 1:
                lfile_name = s_args[1]
            else:
                lfile_name = rfile_name

            try:
                self.fe.get(rfile_name, lfile_name)
            except IOError as e:
                self.__error(str(e))

    def do_mget(self, args):
        """mget <SELECTION REGEX>
        Download all remote files that match the given regular expression.
        The local files will be named the same as the remote files.

        "mget" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mget(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_get(self, *args):

        try:
            files = self.fe.ls(add_dirs=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_rm(self, args):
        """rm <REMOTE FILE or DIR>
        Delete a remote file or directory.

        Note: only empty directories could be removed.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                self.fe.rm(s_args[0])
            except IOError as e:
                self.__error(str(e))
            except PyboardError:
                self.__error("Unable to send request to %s" % self.fe.sysname)

    def do_mrm(self, args):
        """mrm <SELECTION REGEX>
        Delete all remote files that match the given regular expression.

        "mrm" does not delete directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mrm(args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_rm(self, *args):

        try:
            files = self.fe.ls()
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_c(self, args):
        self.do_cat(args)

    def do_cat(self, args):
        """cat(c) <REMOTE FILE>
        Print the contents of a remote file.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                print(self.fe.gets(s_args[0]))
            except IOError as e:
                self.__error(str(e))

    complete_cat = complete_get

    def do_rf(self, args):
        self.do_runfile(args)

    def do_runfile(self, args):
        """runfile(rf) <LOCAL FILE>
        download and running local file in board.
        """

        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE>")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error(
                    "Only one ore one arguments allowed: <LOCAL FILE> ")
                return

            lfile_name = s_args[0]

            try:
                self.fe.put(lfile_name, lfile_name)
                self.do_ef(args)
            except IOError as e:
                self.__error(str(e))

    def do_ef(self, args):
        self.do_execfile(args)

    def do_execfile(self, args):
        """execfile(ef) <REMOTE FILE>
        Execute a Python filename on remote.
        """
        if self.__is_open():
            try:
                self.do_exec("execfile('%s')" % args)
                ret = self.fe.follow(2)
                if len(ret[-1]):
                    self.__error(str(ret[-1].decode('utf-8')))
            except KeyboardInterrupt as e:
                self.fe.keyboard_interrupt()
            except PyboardError as e:
                print(e)
            finally:
                if (self.open_args.startswith("ser:")):
                    self.__reconnect()
                if (self.__is_open()):
                    self.fe.enter_raw_repl()

    def do_lef(self, args):
        self.do_lexecfile(args)

    def do_lexecfile(self, args):
        """execfile(ef) <LOCAL FILE>
        Execute a Python filename on local.
        """
        if self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error(
                    "Only one ore one arguments allowed: <LOCAL FILE> ")
                return

            lfile_name = s_args[0]

            try:
                self.fe.put(lfile_name, lfile_name)

                self.do_repl('execfile("{0}")\r\n'.format(args))

            except IOError as e:
                self.__error(str(e))

    def do_e(self, args):
        self.do_exec(args)

    def do_exec(self, args):
        """exec(e) <Python CODE>
        Execute a Python CODE on remote.
        """
        def data_consumer(data):
            data = str(data.decode('utf-8'))
            sys.stdout.write(data.strip("\x04"))

        if not len(args):
            self.__error("Missing argument: <Python CODE>")
        elif self.__is_open():

            try:
                self.fe.exec_raw_no_follow(
                    "print('Enter remote execution and stop using Ctrl+C.')\n"
                    + args + "\n")
                ret = self.fe.follow(None, data_consumer)

                if len(ret[-1]):
                    self.__error(str(ret[-1].decode('utf-8')))

            except IOError as e:
                self.__error(str(e))
            except PyboardError as e:
                self.__error(str(e))
            except Exception as e:
                self.__error(str(e))

    def do_r(self, args):
        self.do_repl(args)

    def do_repl(self, args):
        """repl(r)
        Enter Micropython REPL.
        """

        import serial

        ver = serial.VERSION.split(".")

        if int(ver[0]) < 2 or (int(ver[0]) == 2 and int(ver[1]) < 7):
            self.__error("REPL needs PySerial version >= 2.7, found %s" %
                         serial.VERSION)
            return

        if self.__is_open():

            if self.repl is None:

                from mp.term import Term
                self.repl = Term(self.fe.con)

                if platform.system() == "Windows":
                    self.repl.exit_character = chr(0x11)
                else:
                    self.repl.exit_character = chr(0x1d)

                self.repl.raw = True
                self.repl.set_rx_encoding('UTF-8')
                self.repl.set_tx_encoding('UTF-8')

            else:
                self.repl.serial = self.fe.con

            self.fe.teardown()
            self.repl.start()

            if self.repl.exit_character == chr(0x11):
                print("\n*** Exit REPL with Ctrl+Q ***")
            else:
                print("\n*** Exit REPL with Ctrl+] ***")

            try:
                if args != None:
                    self.fe.con.write(bytes(args, encoding="utf8"))
                self.repl.join(True)
            except Exception as e:
                # print(e)
                pass

            self.repl.console.cleanup()

            self.fe.setup()
            print("")

    def do_mpyc(self, args):
        """mpyc <LOCAL PYTHON FILE>
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the path).
        The compiled file has the same name as the original file but with extension '.mpy'.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL FILE>")
        else:

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <LOCAL FILE>")
                return

            try:
                self.fe.mpy_cross(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def complete_mpyc(self, *args):
        files = [
            o for o in os.listdir(".")
            if (os.path.isfile(os.path.join(".", o)) and o.endswith(".py"))
        ]
        return [i for i in files if i.startswith(args[0])]
Example #8
0
class MpFileShell(cmd.Cmd):
    def __init__(self, color=False, caching=False, reset=False):
        if color:
            colorama.init()
            cmd.Cmd.__init__(self, stdout=colorama.initialise.wrapped_stdout)
        else:
            cmd.Cmd.__init__(self)
        self.use_rawinput = False

        self.color = color
        self.caching = caching
        self.reset = reset

        self.fe = None
        self.repl = None
        self.tokenizer = Tokenizer()

        self.__intro()
        self.__set_prompt_path()

    def __del__(self):
        self.__disconnect()

    def __intro(self):

        if self.color:
            self.intro = '\n' + colorama.Fore.GREEN + \
                         '** Micropython File Shell v%s, [email protected] ** ' % version.FULL + \
                         colorama.Fore.RESET + '\n'
        else:
            self.intro = '\n** Micropython File Shell v%s, [email protected] **\n' % version.FULL

        self.intro += '-- Running on Python %d.%d using PySerial %s --\n' \
                       % (sys.version_info[0], sys.version_info[1], serial.VERSION)

    def __set_prompt_path(self):

        if self.fe is not None:
            pwd = self.fe.pwd()
        else:
            pwd = "/"

        if self.color:
            self.prompt = colorama.Fore.BLUE + "mpfs [" + \
                          colorama.Fore.YELLOW + pwd + \
                          colorama.Fore.BLUE + "]> " + colorama.Fore.RESET
        else:
            self.prompt = "mpfs [" + pwd + "]> "

    def __error(self, msg):

        if self.color:
            print('\n' + colorama.Fore.RED + msg + colorama.Fore.RESET + '\n')
        else:
            print('\n' + msg + '\n')

    def __connect(self, port):

        try:
            self.__disconnect()

            if self.reset:
                print("Hard resetting device ...")
            if self.caching:
                self.fe = MpFileExplorerCaching(port, self.reset)
            else:
                self.fe = MpFileExplorer(port, self.reset)
            print("Connected to %s" % self.fe.sysname)
        except PyboardError as e:
            logging.error(e)
            self.__error(str(e))
        except ConError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        except AttributeError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)

    def __disconnect(self):

        if self.fe is not None:
            try:
                self.fe.close()
                self.fe = None
                self.__set_prompt_path()
            except RemoteIOError as e:
                self.__error(str(e))

    def __is_open(self):

        if self.fe is None:
            self.__error("Not connected to device. Use 'open' first.")
            return False

        return True

    def __parse_file_names(self, args):

        tokens, rest = self.tokenizer.tokenize(args)

        if rest != '':
            self.__error("Invalid filename given: %s" % rest)
        else:
            return [token.value for token in tokens]

        return None

    def do_exit(self, args):
        """exit
        Exit this shell.
        """
        self.__disconnect()

        return True

    do_EOF = do_exit

    def do_open(self, args):
        """open <TARGET>
        Open connection to device with given target. TARGET might be:

        - a serial port, e.g.       ttyUSB0, ser:/dev/ttyUSB0
        - a telnet host, e.g        tn:192.168.1.1 or tn:192.168.1.1,login,passwd
        - a websocket host, e.g.    ws:192.168.1.1 or ws:192.168.1.1,passwd
        """

        if not len(args):
            self.__error("Missing argument: <TARGET>")
        else:
            if not args.startswith("ser:/dev/") \
                    and not args.startswith("ser:COM") \
                    and not args.startswith("tn:") \
                    and not args.startswith("ws:"):

                if platform.system() == "Windows":
                    args = "ser:" + args
                else:
                    args = "ser:/dev/" + args

            self.__connect(args)

    def complete_open(self, *args):
        ports = glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')
        return [i[5:] for i in ports if i[5:].startswith(args[0])]

    def do_close(self, args):
        """close
        Close connection to device.
        """

        self.__disconnect()

    def do_ls(self, directory):
        """ls [directory]
        List remote files, optionally in directory
        """

        if self.__is_open():
            olddir = self.fe.pwd()
            try:
                if len(directory) != 0:
                    self.fe.cd(directory)

                files = self.fe.ls(add_details=True)

                if self.fe.pwd() != "/":
                    files = [("..", "D")] + files

                print("\nRemote files in '%s':\n" % self.fe.pwd())

                # Sort alphabetically, then sort folders over files
                files = sorted(sorted(files, key=lambda file: file[0]),
                               key=lambda file: file[1])

                for elem, type in files:
                    if type == 'F':
                        if self.color:
                            print(colorama.Fore.CYAN + ("       %s" % elem) +
                                  colorama.Fore.RESET)
                        else:
                            print("       %s" % elem)
                    else:
                        if self.color:
                            print(colorama.Fore.MAGENTA +
                                  (" <dir> %s" % elem) + colorama.Fore.RESET)
                        else:
                            print(" <dir> %s" % elem)

                print("")

            except IOError as e:
                self.__error(str(e))

            if len(directory) != 0:
                self.fe.cd(olddir)

    def do_tree(self, directory):
        """tree [directory]
        List remote files recursively, optionally in directory
        """

        if self.__is_open():
            olddir = self.fe.pwd()
            if len(directory) != 0:
                self.fe.cd(directory)
            else:
                directory = self.fe.pwd()

            self.rec_tree(directory)

            if len(directory) != 0:
                self.fe.cd(olddir)

    def rec_tree(self, directory, prefix=""):
        try:
            olddir = self.fe.pwd()
            self.fe.cd(directory)
            files = self.fe.ls(add_details=True)
            if self.color:
                print(colorama.Fore.MAGENTA +
                      ("%s%s" % (prefix, self.fe.pwd().split("/")[-1])) +
                      colorama.Fore.RESET)
            else:
                print("%s%s" % (prefix, self.fe.pwd().split("/")[-1]))

            if len(prefix) >= 4:
                if prefix[-4] == 'â””':
                    prefix = prefix[:-4] + ' ' * 4
            files = sorted(sorted(files, key=lambda file: file[0]),
                           key=lambda file: file[1])

            i = 0
            for elem, type in files:
                if type == 'F':
                    if len(prefix) >= 3:
                        fprefix = prefix[:-4] + (' ' if prefix[-4] == ' ' else
                                                 '│') + ' ' * 3
                    else:
                        fprefix = prefix
                    if len(files) == i + 1:
                        fprefix = fprefix + "└── "
                    else:
                        fprefix = fprefix + "├── "

                    if self.color:
                        print(colorama.Fore.CYAN + ("%s%s" % (fprefix, elem)) +
                              colorama.Fore.RESET)
                    else:
                        print("%s%s" % (fprefix, elem))
                else:
                    if len(files) == i + 1:
                        self.rec_tree(self.fe._fqn(elem), prefix + "└── ")
                    else:
                        self.rec_tree(self.fe._fqn(elem), prefix + "├── ")
                i += 1
            self.fe.cd(olddir)

        except IOError as e:
            self.__error(str(e))

    def do_pwd(self, args):
        """pwd
         Print current remote directory.
         """
        if self.__is_open():
            print(self.fe.pwd())

    def do_cd(self, args):
        """cd <TARGET DIR>
        Change current remote directory to given target.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.cd(s_args[0])
                self.__set_prompt_path()
            except IOError as e:
                self.__error(str(e))
            except RemoteIOError as e:
                self.__error(str(e))

    def complete_cd(self, *args):

        try:
            files = self.fe.ls(add_files=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_md(self, args):
        """md <TARGET DIR>
        Create new remote directory.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.md(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def do_lls(self, args):
        """lls
        List files in current local directory.
        """

        files = os.listdir(".")

        print("\nLocal files:\n")

        for f in files:
            if os.path.isdir(f):
                if self.color:
                    print(colorama.Fore.MAGENTA + (" <dir> %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print(" <dir> %s" % f)
        for f in files:
            if os.path.isfile(f):
                if self.color:
                    print(colorama.Fore.CYAN + ("       %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print("       %s" % f)
        print("")

    def do_lcd(self, args):
        """lcd <TARGET DIR>
        Change current local directory to given target.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL DIR>")
        else:
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <LOCAL DIR>")
                    return

                os.chdir(s_args[0])
            except OSError as e:
                self.__error(str(e).split("] ")[-1])

    def complete_lcd(self, *args):
        dirs = [
            o for o in os.listdir(".") if os.path.isdir(os.path.join(".", o))
        ]
        return [i for i in dirs if i.startswith(args[0])]

    def do_lpwd(self, args):
        """lpwd
        Print current local directory.
        """

        print(os.getcwd())

    def do_put(self, args):
        """put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.
        """

        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            try:
                self.fe.put(src=s_args[0],
                            dst=(s_args[1] if len(s_args) > 1 else None))
            except IOError as e:
                self.__error(str(e))

    def do_putr(self, args):
        """putr <LOCAL DIRECTORY> [<REMOTE DIRECTORY>]
        Upload local directory recursively. If the second parameter is given,
        its value is used for the remote directory name. Otherwise the
        remote directory will be named the same as the local directory.
        """

        if not len(args):
            self.__error(
                "Missing arguments: <LOCAL DIRECTORY> [<REMOTE DIRECTORY>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL DIRECTORY> [<REMOTE DIRECTORY>]"
                )
                return

            try:
                self.fe.putr(src=s_args[0],
                             dst=(s_args[1] if len(s_args) > 1 else None))
            except IOError as e:
                self.__error(str(e))

    def complete_put(self, *args):
        files = [
            o for o in os.listdir(".") if os.path.isfile(os.path.join(".", o))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_mput(self, args):
        """mput <SELECTION REGEX>
        Upload all local files that match the given regular expression.
        The remote files will be named the same as the local files.

        "mput" does not get directories, and it is note recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mput(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def do_get(self, args):
        """get <REMOTE FILE> [<LOCAL FILE>]
        Download remote file. If the second parameter is given,
        its value is used for the local file name. Otherwise the
        locale file will be named the same as the remote file.
        """

        if not len(args):
            self.__error("Missing arguments: <REMOTE FILE> [<LOCAL FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <REMOTE FILE> [<LOCAL FILE>]"
                )
                return

            try:
                self.fe.get(src=s_args[0],
                            dst=(s_args[1] if len(s_args) > 1 else None))
            except IOError as e:
                self.__error(str(e))

    def do_getr(self, args):
        """get <REMOTE DIRECTORY> [<LOCAL DIRECTORY>]
        Download remote directory recursively. If the second parameter is given,
        its value is used for the local directory name. Otherwise the
        locale directory will be named the same as the remote directory.
        """
        if not len(args):
            self.__error(
                "Missing arguments: <REMOTE DIRECTORY> [<LOCAL DIRECTORY>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <REMOTE DIRECTORY> [<LOCAL DIRECTORY>]"
                )
                return

            try:
                self.fe.getr(src=s_args[0],
                             dst=(s_args[1] if len(s_args) > 1 else None))
            except IOError as e:
                self.__error(str(e))

    def do_mget(self, args):
        """mget <SELECTION REGEX>
        Download all remote files that match the given regular expression.
        The local files will be named the same as the remote files.

        "mget" does not get directories, and it is note recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mget(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_get(self, *args):

        try:
            files = self.fe.ls(add_dirs=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_rm(self, args):
        """rm <REMOTE FILE>
        Delete a remote file or empty directory.

        Note: to delete directories recursively see rmr.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                self.fe.rm(s_args[0])
            except IOError as e:
                self.__error(str(e))
            except PyboardError:
                self.__error("Unable to send request to %s" % self.fe.sysname)

    def do_rmr(self, args):
        """rm <REMOTE DIR>
        Delete a remote directory recursively.

        Note: to delete single files and empty directories see rm.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE DIRECTORY>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE DIRECTORY>")
                return

            try:
                self.fe.rmr(s_args[0])
            except IOError as e:
                self.__error(str(e))
            except PyboardError:
                self.__error("Unable to send request to %s" % self.fe.sysname)

    def do_mrm(self, args):
        """mrm <SELECTION REGEX>
        Delete all remote files that match the given regular expression.

        "mrm" does not delete directories, and it is note recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mrm(args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_rm(self, *args):

        try:
            files = self.fe.ls()
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_cat(self, args):
        """cat <REMOTE FILE>
        Print the contents of a remote file.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                print(self.fe.gets(s_args[0]))
            except IOError as e:
                self.__error(str(e))

    complete_cat = complete_get

    def do_exec(self, args):
        """exec <STATEMENT>
        Execute a Python statement on remote.
        """
        def data_consumer(data):
            data = str(data.decode('utf-8'))
            sys.stdout.write(data.strip("\x04"))

        if not len(args):
            self.__error("Missing argument: <STATEMENT>")
        elif self.__is_open():

            try:
                self.fe.exec_raw_no_follow(args + "\n")
                ret = self.fe.follow(None, data_consumer)

                if len(ret[-1]):
                    self.__error(ret[-1])

            except IOError as e:
                self.__error(str(e))
            except PyboardError as e:
                self.__error(str(e))

    def do_repl(self, args):
        """repl
        Enter Micropython REPL.
        """

        import serial

        ver = serial.VERSION.split(".")

        if int(ver[0]) < 2 or (int(ver[0]) == 2 and int(ver[1]) < 7):
            self.__error("REPL needs PySerial version >= 2.7, found %s" %
                         serial.VERSION)
            return

        if self.__is_open():

            if self.repl is None:

                from mp.term import Term
                self.repl = Term(self.fe.con)

                if platform.system() == "Windows":
                    self.repl.exit_character = chr(0x11)
                else:
                    self.repl.exit_character = chr(0x1d)

                self.repl.raw = True
                self.repl.set_rx_encoding('UTF-8')
                self.repl.set_tx_encoding('UTF-8')

            else:
                self.repl.serial = self.fe.con

            self.fe.teardown()
            self.repl.start()

            if self.repl.exit_character == chr(0x11):
                print("\n*** Exit REPL with Ctrl+Q ***")
            else:
                print("\n*** Exit REPL with Ctrl+] ***")

            try:
                self.repl.join(True)
            except Exception:
                pass

            self.repl.console.cleanup()

            self.fe.setup()
            print("")

    def do_mpyc(self, args):
        """mpyc <LOCAL PYTHON FILE>
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the path).
        The compiled file has the same name as the original file but with extension '.mpy'.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL FILE>")
        else:

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <LOCAL FILE>")
                return

            try:
                self.fe.mpy_cross(s_args[0])
            except IOError as e:
                self.__error(str(e))

    #def do_crash(self, args):
    #    """crash
    #    Runs some test code which likely crashes
    #    """
    #    if self.__is_open():
    #        try:
    #            self.fe.crash()
    #        except IOError as e:
    #            self.__error(str(e))

    def complete_mpyc(self, *args):
        files = [
            o for o in os.listdir(".")
            if (os.path.isfile(os.path.join(".", o)) and o.endswith(".py"))
        ]
        return [i for i in files if i.startswith(args[0])]
Example #9
0
class MpFileShell(cmd.Cmd):
    def __init__(self, color=False, caching=False, reset=False):
        if color:
            colorama.init()
            cmd.Cmd.__init__(self, stdout=colorama.initialise.wrapped_stdout)
        else:
            cmd.Cmd.__init__(self)

        if platform.system() == "Windows":
            self.use_rawinput = False

        self.color = color
        self.caching = caching
        self.reset = reset

        self.fe = None
        self.repl = None
        self.tokenizer = Tokenizer()

        self.__intro()
        self.__set_prompt_path()

    def __del__(self):
        self.__disconnect()

    def __intro(self):

        if self.color:
            self.intro = (
                "\n"
                + colorama.Fore.GREEN
                + "** Micropython File Shell v%s, [email protected] ** " % version.FULL
                + colorama.Fore.RESET
                + "\n"
            )
        else:
            self.intro = (
                "\n** Micropython File Shell v%s, [email protected] **\n" % version.FULL
            )

        self.intro += "-- Running on Python %d.%d using PySerial %s --\n" % (
            sys.version_info[0],
            sys.version_info[1],
            serial.VERSION,
        )

    def __set_prompt_path(self):

        if self.fe is not None:
            pwd = self.fe.pwd()
        else:
            pwd = "/"

        if self.color:
            self.prompt = (
                colorama.Fore.BLUE
                + "mpfs ["
                + colorama.Fore.YELLOW
                + pwd
                + colorama.Fore.BLUE
                + "]> "
                + colorama.Fore.RESET
            )
        else:
            self.prompt = "mpfs [" + pwd + "]> "

    def __error(self, msg):

        if self.color:
            print("\n" + colorama.Fore.RED + msg + colorama.Fore.RESET + "\n")
        else:
            print("\n" + msg + "\n")

    def __connect(self, port):

        try:
            self.__disconnect()

            if self.reset:
                print("Hard resetting device ...")
            if self.caching:
                self.fe = MpFileExplorerCaching(port, self.reset)
            else:
                self.fe = MpFileExplorer(port, self.reset)
            print("Connected to %s" % self.fe.sysname)
            self.__set_prompt_path()
        except PyboardError as e:
            logging.error(e)
            self.__error(str(e))
        except ConError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        except AttributeError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)

    def __disconnect(self):

        if self.fe is not None:
            try:
                self.fe.close()
                self.fe = None
                self.__set_prompt_path()
            except RemoteIOError as e:
                self.__error(str(e))

    def __is_open(self):

        if self.fe is None:
            self.__error("Not connected to device. Use 'open' first.")
            return False

        return True

    def __parse_file_names(self, args):

        tokens, rest = self.tokenizer.tokenize(args)

        if rest != "":
            self.__error("Invalid filename given: %s" % rest)
        else:
            return [token.value for token in tokens]

        return None

    def do_exit(self, args):
        """exit
        Exit this shell.
        """
        self.__disconnect()

        return True

    do_EOF = do_exit

    def do_open(self, args):
        """open <TARGET>
        Open connection to device with given target. TARGET might be:

        - a serial port, e.g.       ttyUSB0, ser:/dev/ttyUSB0
        - a telnet host, e.g        tn:192.168.1.1 or tn:192.168.1.1,login,passwd
        - a websocket host, e.g.    ws:192.168.1.1 or ws:192.168.1.1,passwd
        """

        if not len(args):
            self.__error("Missing argument: <PORT>")
        else:
            if (
                not args.startswith("ser:/dev/")
                and not args.startswith("ser:COM")
                and not args.startswith("tn:")
                and not args.startswith("ws:")
            ):

                if platform.system() == "Windows":
                    args = "ser:" + args
                else:
                    args = "ser:/dev/" + args

            self.__connect(args)

    def complete_open(self, *args):
        ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
        return [i[5:] for i in ports if i[5:].startswith(args[0])]

    def do_close(self, args):
        """close
        Close connection to device.
        """

        self.__disconnect()

    def do_ls(self, args):
        """ls
        List remote files.
        """

        if self.__is_open():
            try:
                files = self.fe.ls(add_details=True)

                if self.fe.pwd() != "/":
                    files = [("..", "D")] + files

                print("\nRemote files in '%s':\n" % self.fe.pwd())

                for elem, type in files:
                    if type == "F":
                        if self.color:
                            print(
                                colorama.Fore.CYAN
                                + ("       %s" % elem)
                                + colorama.Fore.RESET
                            )
                        else:
                            print("       %s" % elem)
                    else:
                        if self.color:
                            print(
                                colorama.Fore.MAGENTA
                                + (" <dir> %s" % elem)
                                + colorama.Fore.RESET
                            )
                        else:
                            print(" <dir> %s" % elem)

                print("")

            except IOError as e:
                self.__error(str(e))

    def do_pwd(self, args):
        """pwd
         Print current remote directory.
         """
        if self.__is_open():
            print(self.fe.pwd())

    def do_cd(self, args):
        """cd <TARGET DIR>
        Change current remote directory to given target.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.cd(s_args[0])
                self.__set_prompt_path()
            except IOError as e:
                self.__error(str(e))

    def complete_cd(self, *args):

        try:
            files = self.fe.ls(add_files=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_md(self, args):
        """md <TARGET DIR>
        Create new remote directory.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.md(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def do_lls(self, args):
        """lls
        List files in current local directory.
        """

        files = os.listdir(".")

        print("\nLocal files:\n")

        for f in files:
            if os.path.isdir(f):
                if self.color:
                    print(
                        colorama.Fore.MAGENTA + (" <dir> %s" % f) + colorama.Fore.RESET
                    )
                else:
                    print(" <dir> %s" % f)
        for f in files:
            if os.path.isfile(f):
                if self.color:
                    print(colorama.Fore.CYAN + ("       %s" % f) + colorama.Fore.RESET)
                else:
                    print("       %s" % f)
        print("")

    def do_lcd(self, args):
        """lcd <TARGET DIR>
        Change current local directory to given target.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL DIR>")
        else:
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <LOCAL DIR>")
                    return

                os.chdir(s_args[0])
            except OSError as e:
                self.__error(str(e).split("] ")[-1])

    def complete_lcd(self, *args):
        dirs = [o for o in os.listdir(".") if os.path.isdir(os.path.join(".", o))]
        return [i for i in dirs if i.startswith(args[0])]

    def do_lpwd(self, args):
        """lpwd
        Print current local directory.
        """

        print(os.getcwd())

    def do_put(self, args):
        """put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.
        """

        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = lfile_name

            try:
                self.fe.put(lfile_name, rfile_name)
            except IOError as e:
                self.__error(str(e))

    def complete_put(self, *args):
        files = [o for o in os.listdir(".") if os.path.isfile(os.path.join(".", o))]
        return [i for i in files if i.startswith(args[0])]

    def do_mput(self, args):
        """mput <SELECTION REGEX>
        Upload all local files that match the given regular expression.
        The remote files will be named the same as the local files.

        "mput" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mput(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def do_get(self, args):
        """get <REMOTE FILE> [<LOCAL FILE>]
        Download remote file. If the second parameter is given,
        its value is used for the local file name. Otherwise the
        locale file will be named the same as the remote file.
        """

        if not len(args):
            self.__error("Missing arguments: <REMOTE FILE> [<LOCAL FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <REMOTE FILE> [<LOCAL FILE>]"
                )
                return

            rfile_name = s_args[0]

            if len(s_args) > 1:
                lfile_name = s_args[1]
            else:
                lfile_name = rfile_name

            try:
                self.fe.get(rfile_name, lfile_name)
            except IOError as e:
                self.__error(str(e))

    def do_mget(self, args):
        """mget <SELECTION REGEX>
        Download all remote files that match the given regular expression.
        The local files will be named the same as the remote files.

        "mget" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mget(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_get(self, *args):

        try:
            files = self.fe.ls(add_dirs=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_rm(self, args):
        """rm <REMOTE FILE or DIR>
        Delete a remote file or directory.

        Note: only empty directories could be removed.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                self.fe.rm(s_args[0])
            except IOError as e:
                self.__error(str(e))
            except PyboardError:
                self.__error("Unable to send request to %s" % self.fe.sysname)

    def do_mrm(self, args):
        """mrm <SELECTION REGEX>
        Delete all remote files that match the given regular expression.

        "mrm" does not delete directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mrm(args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_rm(self, *args):

        try:
            files = self.fe.ls()
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_cat(self, args):
        """cat <REMOTE FILE>
        Print the contents of a remote file.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                print(self.fe.gets(s_args[0]))
            except IOError as e:
                self.__error(str(e))

    complete_cat = complete_get

    def do_exec(self, args):
        """exec <STATEMENT>
        Execute a Python statement on remote.
        """

        def data_consumer(data):
            data = str(data.decode("utf-8"))
            sys.stdout.write(data.strip("\x04"))

        if not len(args):
            self.__error("Missing argument: <STATEMENT>")
        elif self.__is_open():

            try:
                self.fe.exec_raw_no_follow(args + "\n")
                ret = self.fe.follow(None, data_consumer)

                if len(ret[-1]):
                    self.__error(ret[-1])

            except IOError as e:
                self.__error(str(e))
            except PyboardError as e:
                self.__error(str(e))

    def do_repl(self, args):
        """repl
        Enter Micropython REPL.
        """

        import serial

        ver = serial.VERSION.split(".")

        if int(ver[0]) < 2 or (int(ver[0]) == 2 and int(ver[1]) < 7):
            self.__error(
                "REPL needs PySerial version >= 2.7, found %s" % serial.VERSION
            )
            return

        if self.__is_open():

            if self.repl is None:

                from mp.term import Term

                self.repl = Term(self.fe.con)

                if platform.system() == "Windows":
                    self.repl.exit_character = chr(0x11)
                else:
                    self.repl.exit_character = chr(0x1D)

                self.repl.raw = True
                self.repl.set_rx_encoding("UTF-8")
                self.repl.set_tx_encoding("UTF-8")

            else:
                self.repl.serial = self.fe.con

            self.fe.teardown()
            self.repl.start()

            if self.repl.exit_character == chr(0x11):
                print("\n*** Exit REPL with Ctrl+Q ***")
            else:
                print("\n*** Exit REPL with Ctrl+] ***")

            try:
                self.repl.join(True)
            except Exception:
                pass

            self.repl.console.cleanup()

            if self.caching:
                # Clear the file explorer cache so we can see any new files.
                self.fe.cache = {}

            self.fe.setup()
            print("")

    def do_mpyc(self, args):
        """mpyc <LOCAL PYTHON FILE>
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the path).
        The compiled file has the same name as the original file but with extension '.mpy'.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL FILE>")
        else:

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <LOCAL FILE>")
                return

            try:
                self.fe.mpy_cross(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def complete_mpyc(self, *args):
        files = [
            o
            for o in os.listdir(".")
            if (os.path.isfile(os.path.join(".", o)) and o.endswith(".py"))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_putc(self, args):
        """mputc <LOCAL PYTHON FILE> [<REMOTE FILE>]
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the
        path) and upload it. The compiled file has the same name as the original file but
        with extension '.mpy' by default.
        """
        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():
            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = (lfile_name[:lfile_name.rfind(".")] if "." in lfile_name else lfile_name) + ".mpy"

            _, tmp = tempfile.mkstemp()

            try:
                self.fe.mpy_cross(src=lfile_name, dst=tmp)
                self.fe.put(tmp, rfile_name)
            except IOError as e:
                self.__error(str(e))

            os.unlink(tmp)
    complete_putc = complete_mpyc
Example #10
0
class MpFileShell(cmd.Cmd):
    def __init__(self, color=False, caching=False, reset=False):
        if color:
            colorama.init()
            cmd.Cmd.__init__(self, stdout=colorama.initialise.wrapped_stdout)
        else:
            cmd.Cmd.__init__(self)

        self.emptyline = lambda: None

        if platform.system() == "Windows":
            self.use_rawinput = False

        self.color = color
        self.caching = caching
        self.reset = reset

        self.fe = None
        self.repl = None
        self.tokenizer = Tokenizer()

        self.__intro()
        self.__set_prompt_path()

        # Change prompts to be more descriptive
        self.prompts = {
            "gps_uart_tx": ("GPS UART TX pin#: ", int),
            "gps_uart_rx": ("GPS UART RX pin#: ", int),
            "i2c_servo_scl": ("Servo SCL pin#: ", int),
            "i2c_servo_sda": ("Servo SDA pin#: ", int),
            "i2c_bno_scl": ("BNO055 SCL pin#: ", int),
            "i2c_bno_sda": ("BNO055 SDA pin#: ", int),
            "i2c_screen_scl": ("Screen SCL pin#: ", int),
            "i2c_screen_sda": ("Screen SDA pin#: ", int),
            "elevation_servo_index":
            ("Servo default elevation index: ", float),
            "azimuth_servo_index": ("Servo default azimuth index: ", float),
            "elevation_max_rate": ("Servo elevation max rate: ", float),
            "azimuth_max_rate": ("Servo azimuth max rate: ", float)
        }

    def __del__(self):
        self.__disconnect()

    def __intro(self):

        if self.color:
            self.intro = ("\n" + colorama.Fore.GREEN +
                          "** Micropython File Shell v%s, [email protected] ** " %
                          version.FULL + colorama.Fore.RESET + "\n")
        else:
            self.intro = (
                "\n** Micropython File Shell v%s, [email protected] **\n" %
                version.FULL)

        self.intro += "-- Running on Python %d.%d using PySerial %s --\n" % (
            sys.version_info[0],
            sys.version_info[1],
            serial.VERSION,
        )

    def __set_prompt_path(self):

        if self.fe is not None:
            pwd = self.fe.pwd()
        else:
            pwd = "/"

        if self.color:
            self.prompt = (colorama.Fore.BLUE + "mpfs [" +
                           colorama.Fore.YELLOW + pwd + colorama.Fore.BLUE +
                           "]> " + colorama.Fore.RESET)
        else:
            self.prompt = "mpfs [" + pwd + "]> "

    def __error(self, msg):

        if self.color:
            print("\n" + colorama.Fore.RED + msg + colorama.Fore.RESET + "\n")
        else:
            print("\n" + msg + "\n")

    def __connect(self, port):

        try:
            self.__disconnect()

            if self.reset:
                print("Hard resetting device ...")
            if self.caching:
                self.fe = MpFileExplorerCaching(port, self.reset)
            else:
                self.fe = MpFileExplorer(port, self.reset)
            print("Connected to %s" % self.fe.sysname)
            self.__set_prompt_path()
            return True
        except PyboardError as e:
            logging.error(e)
            self.__error(str(e))
        except ConError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        except AttributeError as e:
            logging.error(e)
            self.__error("Failed to open: %s" % port)
        return False

    def __disconnect(self):

        if self.fe is not None:
            try:
                self.fe.close()
                self.fe = None
                self.__set_prompt_path()
            except RemoteIOError as e:
                self.__error(str(e))

    def __is_open(self):

        if self.fe is None:
            self.__error("Not connected to device. Use 'open' first.")
            return False

        return True

    def __parse_file_names(self, args):

        tokens, rest = self.tokenizer.tokenize(args)

        if rest != "":
            self.__error("Invalid filename given: %s" % rest)
        else:
            return [token.value for token in tokens]

        return None

    def __config_set(self, key, val):
        if isinstance(val, int) or isinstance(val, float):
            self.do_exec("config.set(\"%s\", %d)" % (key, val))
        elif isinstance(val, str):
            self.do_exec("config.set(\"%s\", %s)" % (key, val))

    def __config_get(self, key):
        command = "config.get(\"{}\")".format(key).encode('utf-8')
        return self.fe.eval_string_mode(command).decode()

    def do_exit(self, args):
        """exit
        Exit this shell.
        """
        self.__disconnect()

        return True

    do_EOF = do_exit

    def do_open(self, args):
        """open <TARGET>
        Open connection to device with given target. TARGET might be:

        - a serial port, e.g.       ttyUSB0, ser:/dev/ttyUSB0
        - a telnet host, e.g        tn:192.168.1.1 or tn:192.168.1.1,login,passwd
        - a websocket host, e.g.    ws:192.168.1.1 or ws:192.168.1.1,passwd
        """

        if not len(args):
            self.__error("Missing argument: <PORT>")
            return False

        if (not args.startswith("ser:/dev/") and not args.startswith("ser:COM")
                and not args.startswith("tn:") and not args.startswith("ws:")):

            if platform.system() == "Windows":
                args = "ser:" + args
            else:
                args = "ser:/dev/" + args

        return self.__connect(args)

    def complete_open(self, *args):
        ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
        return [i[5:] for i in ports if i[5:].startswith(args[0])]

    def do_close(self, args):
        """close
        Close connection to device.
        """

        self.__disconnect()

    def do_ls(self, args):
        """ls
        List remote files.
        """

        if self.__is_open():
            try:
                files = self.fe.ls(add_details=True)

                if self.fe.pwd() != "/":
                    files = [("..", "D")] + files

                print("\nRemote files in '%s':\n" % self.fe.pwd())

                for elem, type in files:
                    if type == "F":
                        if self.color:
                            print(colorama.Fore.CYAN + ("       %s" % elem) +
                                  colorama.Fore.RESET)
                        else:
                            print("       %s" % elem)
                    else:
                        if self.color:
                            print(colorama.Fore.MAGENTA +
                                  (" <dir> %s" % elem) + colorama.Fore.RESET)
                        else:
                            print(" <dir> %s" % elem)

                print("")

            except IOError as e:
                self.__error(str(e))

    def do_pwd(self, args):
        """pwd
         Print current remote directory.
         """
        if self.__is_open():
            print(self.fe.pwd())

    def do_cd(self, args):
        """cd <TARGET DIR>
        Change current remote directory to given target.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.cd(s_args[0])
                self.__set_prompt_path()
            except IOError as e:
                self.__error(str(e))

    def complete_cd(self, *args):

        try:
            files = self.fe.ls(add_files=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_md(self, args):
        """md <TARGET DIR>
        Create new remote directory.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE DIR>")
        elif self.__is_open():
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <REMOTE DIR>")
                    return

                self.fe.md(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def do_lls(self, args):
        """lls
        List files in current local directory.
        """

        files = os.listdir(".")

        print("\nLocal files:\n")

        for f in files:
            if os.path.isdir(f):
                if self.color:
                    print(colorama.Fore.MAGENTA + (" <dir> %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print(" <dir> %s" % f)
        for f in files:
            if os.path.isfile(f):
                if self.color:
                    print(colorama.Fore.CYAN + ("       %s" % f) +
                          colorama.Fore.RESET)
                else:
                    print("       %s" % f)
        print("")

    def do_lcd(self, args):
        """lcd <TARGET DIR>
        Change current local directory to given target.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL DIR>")
        else:
            try:
                s_args = self.__parse_file_names(args)
                if not s_args:
                    return
                elif len(s_args) > 1:
                    self.__error("Only one argument allowed: <LOCAL DIR>")
                    return

                os.chdir(s_args[0])
            except OSError as e:
                self.__error(str(e).split("] ")[-1])

    def complete_lcd(self, *args):
        dirs = [
            o for o in os.listdir(".") if os.path.isdir(os.path.join(".", o))
        ]
        return [i for i in dirs if i.startswith(args[0])]

    def do_lpwd(self, args):
        """lpwd
        Print current local directory.
        """

        print(os.getcwd())

    def do_put(self, args):
        """put <LOCAL FILE> [<REMOTE FILE>]
        Upload local file. If the second parameter is given,
        its value is used for the remote file name. Otherwise the
        remote file will be named the same as the local file.
        """

        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = lfile_name

            try:
                self.fe.put(lfile_name, rfile_name)
            except IOError as e:
                self.__error(str(e))

    def complete_put(self, *args):
        files = [
            o for o in os.listdir(".") if os.path.isfile(os.path.join(".", o))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_mput(self, args):
        """mput <SELECTION REGEX>
        Upload all local files that match the given regular expression.
        The remote files will be named the same as the local files.

        "mput" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mput(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def do_get(self, args):
        """get <REMOTE FILE> [<LOCAL FILE>]
        Download remote file. If the second parameter is given,
        its value is used for the local file name. Otherwise the
        locale file will be named the same as the remote file.
        """

        if not len(args):
            self.__error("Missing arguments: <REMOTE FILE> [<LOCAL FILE>]")

        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <REMOTE FILE> [<LOCAL FILE>]"
                )
                return

            rfile_name = s_args[0]

            if len(s_args) > 1:
                lfile_name = s_args[1]
            else:
                lfile_name = rfile_name

            try:
                self.fe.get(rfile_name, lfile_name)
            except IOError as e:
                self.__error(str(e))

    def do_mget(self, args):
        """mget <SELECTION REGEX>
        Download all remote files that match the given regular expression.
        The local files will be named the same as the remote files.

        "mget" does not get directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mget(os.getcwd(), args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_get(self, *args):

        try:
            files = self.fe.ls(add_dirs=False)
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_rm(self, args):
        """rm <REMOTE FILE or DIR>
        Delete a remote file or directory.

        Note: only empty directories could be removed.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                self.fe.rm(s_args[0])
            except IOError as e:
                self.__error(str(e))
            except PyboardError:
                self.__error("Unable to send request to %s" % self.fe.sysname)

    def do_mrm(self, args):
        """mrm <SELECTION REGEX>
        Delete all remote files that match the given regular expression.

        "mrm" does not delete directories, and it is not recursive.
        """

        if not len(args):
            self.__error("Missing argument: <SELECTION REGEX>")

        elif self.__is_open():

            try:
                self.fe.mrm(args, True)
            except IOError as e:
                self.__error(str(e))

    def complete_rm(self, *args):

        try:
            files = self.fe.ls()
        except Exception:
            files = []

        return [i for i in files if i.startswith(args[0])]

    def do_cat(self, args):
        """cat <REMOTE FILE>
        Print the contents of a remote file.
        """

        if not len(args):
            self.__error("Missing argument: <REMOTE FILE>")
        elif self.__is_open():

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <REMOTE FILE>")
                return

            try:
                print(self.fe.gets(s_args[0]))
            except IOError as e:
                self.__error(str(e))

    complete_cat = complete_get

    def do_exec(self, args):
        """exec <STATEMENT>
        Execute a Python statement on remote.
        """
        def data_consumer(data):
            data = str(data.decode("utf-8"))
            sys.stdout.write(data.strip("\x04"))

        if not len(args):
            self.__error("Missing argument: <STATEMENT>")
        elif self.__is_open():

            try:
                self.fe.exec_raw_no_follow(args + "\n")
                ret = self.fe.follow(None, data_consumer)
                if len(ret[-1]):
                    self.__error(ret[-1].decode("utf-8"))

            except IOError as e:
                self.__error(str(e))
            except PyboardError as e:
                self.__error(str(e))

    def do_repl(self, args):
        """repl
        Enter Micropython REPL.
        """

        import serial

        ver = serial.VERSION.split(".")

        if int(ver[0]) < 2 or (int(ver[0]) == 2 and int(ver[1]) < 7):
            self.__error("REPL needs PySerial version >= 2.7, found %s" %
                         serial.VERSION)
            return

        if self.__is_open():

            if self.repl is None:

                from mp.term import Term

                self.repl = Term(self.fe.con)

                if platform.system() == "Windows":
                    self.repl.exit_character = chr(0x11)
                else:
                    self.repl.exit_character = chr(0x1D)

                self.repl.raw = True
                self.repl.set_rx_encoding("UTF-8")
                self.repl.set_tx_encoding("UTF-8")

            else:
                self.repl.serial = self.fe.con

            pwd = self.fe.pwd()
            self.fe.teardown()
            self.repl.start()

            if self.repl.exit_character == chr(0x11):
                print("\n*** Exit REPL with Ctrl+Q ***")
            else:
                print("\n*** Exit REPL with Ctrl+] ***")

            try:
                self.repl.join(True)
            except Exception:
                pass

            self.repl.console.cleanup()

            if self.caching:
                # Clear the file explorer cache so we can see any new files.
                self.fe.cache = {}

            self.fe.setup()
            try:
                self.fe.cd(pwd)
            except RemoteIOError as e:
                # Working directory does not exist anymore
                self.__error(str(e))
            finally:
                self.__set_prompt_path()
            print("")

    def do_mpyc(self, args):
        """mpyc <LOCAL PYTHON FILE>
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the path).
        The compiled file has the same name as the original file but with extension '.mpy'.
        """

        if not len(args):
            self.__error("Missing argument: <LOCAL FILE>")
        else:

            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 1:
                self.__error("Only one argument allowed: <LOCAL FILE>")
                return

            try:
                self.fe.mpy_cross(s_args[0])
            except IOError as e:
                self.__error(str(e))

    def complete_mpyc(self, *args):
        files = [
            o for o in os.listdir(".")
            if (os.path.isfile(os.path.join(".", o)) and o.endswith(".py"))
        ]
        return [i for i in files if i.startswith(args[0])]

    def do_putc(self, args):
        """mputc <LOCAL PYTHON FILE> [<REMOTE FILE>]
        Compile a Python file into byte-code by using mpy-cross (which needs to be in the
        path) and upload it. The compiled file has the same name as the original file but
        with extension '.mpy' by default.
        """
        if not len(args):
            self.__error("Missing arguments: <LOCAL FILE> [<REMOTE FILE>]")

        elif self.__is_open():
            s_args = self.__parse_file_names(args)
            if not s_args:
                return
            elif len(s_args) > 2:
                self.__error(
                    "Only one ore two arguments allowed: <LOCAL FILE> [<REMOTE FILE>]"
                )
                return

            lfile_name = s_args[0]

            if len(s_args) > 1:
                rfile_name = s_args[1]
            else:
                rfile_name = (lfile_name[:lfile_name.rfind(".")]
                              if "." in lfile_name else lfile_name) + ".mpy"

            _, tmp = tempfile.mkstemp()

            try:
                self.fe.mpy_cross(src=lfile_name, dst=tmp)
                self.fe.put(tmp, rfile_name)
            except IOError as e:
                self.__error(str(e))

            os.unlink(tmp)

    complete_putc = complete_mpyc

    def do_edit(self, args):
        """edit <REMOTE FILE>
        Copies file over, opens it in your editor, copies back when done.
        """
        if not len(args):
            self.__error("Missing argument: <REMOTE_FILE>")

        elif self.__is_open():
            try:
                self.do_get(args)
            except IOError as e:
                if "No such file" in str(e):
                    # make new file locally, then copy
                    # Not implemented yet
                    self.__error(str(e))
                    pass

            rfile_name, = self.__parse_file_names(args)
            if platform.system() == 'Windows':
                EDITOR = os.environ.get('EDITOR', 'notepad')
                subprocess.call([EDITOR, rfile_name], shell=True)
            else:
                EDITOR = os.environ.get('EDITOR', 'vim')
                subprocess.call([EDITOR, rfile_name])
            self.do_put(rfile_name)

    complete_edit = complete_get

    def do_setup(self, args):
        """setup
        Interactive script to populate the initial config file.
        """
        if self.__is_open():
            print(colorama.Fore.GREEN + "Welcome to Antenny!" +
                  colorama.Fore.RESET)
            print(
                "Please enter the following information about your hardware\n")

            for k, info in self.prompts.items():
                prompt_text, typ = info
                try:
                    new_val = typ(input(prompt_text))
                except ValueError:
                    new_val = config._defaults[k]

                self.__config_set(k, new_val)

            print(colorama.Fore.GREEN +
                    "\nConfiguration set!\n" +
                    colorama.Fore.RESET +
                    "You can use \"set\" to change individual parameters\n" \
                    "or \"edit config.json\" to change the config file " \
                    "directly")

    def do_set(self, args):
        """set <CONFIG_PARAM> <NEW_VAL>
        Set a parameter in the configuration file to a new value."""
        if not len(args):
            self.__error("missing arguments: <config_param> <new_val>")

        elif self.__is_open():
            s_args = self.__parse_file_names(args)
            if len(s_args) < 2:
                self.__error("Missing argument: <new_val>")
                return

            key, new_val = s_args
            try:
                old_val = self.__config_get(key)
            except:
                self.__error("No such configuration parameter")
                return

            _, typ = self.prompts[key]
            try:
                new_val = typ(new_val)
            except ValueError:
                self.__error(str(e))
                return

            self.__config_set(key, new_val)
            print("Changed " + "\"" + key + "\" from " + str(old_val) +
                  " --> " + str(new_val))

    def complete_set(self, *args):
        if self.__is_open():
            return [
                key for key in self.prompts.keys() if key.startswith(args[0])
            ]
        else:
            return []

    def do_configs(self, args):
        """configs
        Print a list of all configuration parameters."""
        if self.__is_open():
            print(colorama.Fore.GREEN + "-Config parameters-\n" +
                  colorama.Fore.RESET)
            for key in self.prompts.keys():
                print(key + ": " + self.__config_get(key))

    def do_clear(self, args):
        """clear
        Clear the currently used config file."""
        ## FIX
        if self.__is_open():
            self.do_exec("config.clear()")

    def do_revert(self, args):
        """revert
        Switch to using the backup config file. Current config
        will be deleted."""
        if self.__is_open():
            self.do_exec("config.revert()")

    def do_switch(self, args):
        """switch <CONFIG_FILE>
        Switch to using a different config file."""
        if not len(args):
            self.__error("Missing arguments: <config_file>")

        elif self.__is_open():
            s_args = self.__parse_file_names(args)
            if len(s_args) > 1:
                self.__error("Usage: switch <CONFIG_FILE>")
                return
            name, = s_args
            self.do_exec("config.switch({})".format(name))