1#!/usr/bin/env python3
2#
3# This file is part of the MicroPython project, http://micropython.org/
4#
5# The MIT License (MIT)
6#
7# Copyright (c) 2014-2019 Damien P. George
8# Copyright (c) 2017 Paul Sokolovsky
9#
10# Permission is hereby granted, free of charge, to any person obtaining a copy
11# of this software and associated documentation files (the "Software"), to deal
12# in the Software without restriction, including without limitation the rights
13# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14# copies of the Software, and to permit persons to whom the Software is
15# furnished to do so, subject to the following conditions:
16#
17# The above copyright notice and this permission notice shall be included in
18# all copies or substantial portions of the Software.
19#
20# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26# THE SOFTWARE.
27
28"""
29haasboard interface
30
31This module provides the HaaSboard class, used to communicate with and
32control a MicroPython device over a communication channel. Both real
33boards and emulated devices (e.g. running in QEMU) are supported.
34Various communication channels are supported, including a serial
35connection, telnet-style network connection, external process
36connection.
37
38Example usage:
39
40    import haasboard
41    pyb = haasboard.HaaSboard('/dev/ttyACM0')
42
43Or:
44
45    pyb = haasboard.HaaSboard('192.168.1.1')
46
47Then:
48
49    pyb.enter_raw_repl()
50    pyb.exec('import pyb')
51    pyb.exec('pyb.LED(1).on()')
52    pyb.exit_raw_repl()
53
54Note: if using Python2 then pyb.exec must be written as pyb.exec_.
55To run a script from the local machine on the board and print out the results:
56
57    import haasboard
58    haasboard.execfile('test.py', device='/dev/ttyACM0')
59
60This script can also be run directly.  To execute a local script, use:
61
62    ./haasboard.py test.py
63
64Or:
65
66    python haasboard.py test.py
67
68"""
69
70import sys
71import time
72import os
73import ast
74
75try:
76    stdout = sys.stdout.buffer
77except AttributeError:
78    # Python2 doesn't have buffer attr
79    stdout = sys.stdout
80
81
82def stdout_write_bytes(b):
83    b = b.replace(b"\x04", b"")
84    stdout.write(b)
85    stdout.flush()
86
87
88class HaaSboardError(Exception):
89    pass
90
91
92class TelnetToSerial:
93    def __init__(self, ip, user, password, read_timeout=None):
94        self.tn = None
95        import telnetlib
96
97        self.tn = telnetlib.Telnet(ip, timeout=15)
98        self.read_timeout = read_timeout
99        if b"Login as:" in self.tn.read_until(b"Login as:", timeout=read_timeout):
100            self.tn.write(bytes(user, "ascii") + b"\r\n")
101
102            if b"Password:" in self.tn.read_until(b"Password:", timeout=read_timeout):
103                # needed because of internal implementation details of the telnet server
104                time.sleep(0.2)
105                self.tn.write(bytes(password, "ascii") + b"\r\n")
106
107                if b"for more information." in self.tn.read_until(
108                    b'Type "help()" for more information.', timeout=read_timeout
109                ):
110                    # login successful
111                    from collections import deque
112
113                    self.fifo = deque()
114                    return
115
116        raise HaaSboardError("Failed to establish a telnet connection with the board")
117
118    def __del__(self):
119        self.close()
120
121    def close(self):
122        if self.tn:
123            self.tn.close()
124
125    def read(self, size=1):
126        while len(self.fifo) < size:
127            timeout_count = 0
128            data = self.tn.read_eager()
129            if len(data):
130                self.fifo.extend(data)
131                timeout_count = 0
132            else:
133                time.sleep(0.25)
134                if self.read_timeout is not None and timeout_count > 4 * self.read_timeout:
135                    break
136                timeout_count += 1
137        data = b""
138        while len(data) < size and len(self.fifo) > 0:
139            data += bytes([self.fifo.popleft()])
140        return data
141
142    def write(self, data):
143        self.tn.write(data)
144        return len(data)
145
146    def inWaiting(self):
147        n_waiting = len(self.fifo)
148        if not n_waiting:
149            data = self.tn.read_eager()
150            self.fifo.extend(data)
151            return len(data)
152        else:
153            return n_waiting
154
155
156class ProcessToSerial:
157    "Execute a process and emulate serial connection using its stdin/stdout."
158
159    def __init__(self, cmd):
160        import subprocess
161
162        self.subp = subprocess.Popen(
163            cmd,
164            bufsize=0,
165            shell=True,
166            preexec_fn=os.setsid,
167            stdin=subprocess.PIPE,
168            stdout=subprocess.PIPE,
169        )
170
171        # Initially was implemented with selectors, but that adds Python3
172        # dependency. However, there can be race conditions communicating
173        # with a particular child process (like QEMU), and selectors may
174        # still work better in that case, so left inplace for now.
175        #
176        # import selectors
177        # self.sel = selectors.DefaultSelector()
178        # self.sel.register(self.subp.stdout, selectors.EVENT_READ)
179
180        import select
181
182        self.poll = select.poll()
183        self.poll.register(self.subp.stdout.fileno())
184
185    def close(self):
186        import signal
187
188        os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)
189
190    def read(self, size=1):
191        data = b""
192        while len(data) < size:
193            data += self.subp.stdout.read(size - len(data))
194        return data
195
196    def write(self, data):
197        self.subp.stdin.write(data)
198        return len(data)
199
200    def inWaiting(self):
201        # res = self.sel.select(0)
202        res = self.poll.poll(0)
203        if res:
204            return 1
205        return 0
206
207
208class ProcessPtyToTerminal:
209    """Execute a process which creates a PTY and prints slave PTY as
210    first line of its output, and emulate serial connection using
211    this PTY."""
212
213    def __init__(self, cmd):
214        import subprocess
215        import re
216        import serial
217
218        self.subp = subprocess.Popen(
219            cmd.split(),
220            bufsize=0,
221            shell=False,
222            preexec_fn=os.setsid,
223            stdin=subprocess.PIPE,
224            stdout=subprocess.PIPE,
225            stderr=subprocess.PIPE,
226        )
227        pty_line = self.subp.stderr.readline().decode("utf-8")
228        m = re.search(r"/dev/pts/[0-9]+", pty_line)
229        if not m:
230            print("Error: unable to find PTY device in startup line:", pty_line)
231            self.close()
232            sys.exit(1)
233        pty = m.group()
234        # rtscts, dsrdtr params are to workaround pyserial bug:
235        # http://stackoverflow.com/questions/34831131/pyserial-does-not-play-well-with-virtual-port
236        self.ser = serial.Serial(pty, interCharTimeout=1, rtscts=True, dsrdtr=True)
237
238    def close(self):
239        import signal
240
241        os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)
242
243    def read(self, size=1):
244        return self.ser.read(size)
245
246    def write(self, data):
247        return self.ser.write(data)
248
249    def inWaiting(self):
250        return self.ser.inWaiting()
251
252
253class HaaSboard:
254    def __init__(self, device, baudrate=1500000, user="micro", password="python", wait=0):
255        self.use_raw_paste = True
256        if device.startswith("exec:"):
257            self.serial = ProcessToSerial(device[len("exec:") :])
258        elif device.startswith("execpty:"):
259            self.serial = ProcessPtyToTerminal(device[len("qemupty:") :])
260        elif device and device[0].isdigit() and device[-1].isdigit() and device.count(".") == 3:
261            # device looks like an IP address
262            self.serial = TelnetToSerial(device, user, password, read_timeout=10)
263        else:
264            import serial
265
266            delayed = False
267            for attempt in range(wait + 1):
268
269                self.serial = serial.Serial()
270                self.serial.port = device
271                self.serial.baudrate = baudrate
272                self.serial.parity = "N"
273                self.serial.bytesize = 8
274                self.serial.stopbits = 1
275                self.serial.timeout = 0.05
276
277                try:
278                    self.serial.open()
279                except Exception as e:
280                    raise Exception("Failed to open serial port: %s!" % device)
281                break
282
283                time.sleep(1)
284                sys.stdout.write(".")
285                sys.stdout.flush()
286            else:
287                if delayed:
288                    print("")
289                raise HaaSboardError("failed to access " + device)
290            if delayed:
291                print("")
292
293
294    def close(self):
295        self.serial.close()
296
297
298    def run_cmd(self,cmd):
299        self.serial.write(cmd)
300
301    def run_cmd_follow(self,cmd,ending,timeout = 10):
302        self.serial.write(cmd)
303        return self.read_until(1,ending,timeout)
304
305
306
307    def read_until(self, min_num_bytes, ending, timeout=10, data_consumer=None):
308        # if data_consumer is used then data is not accumulated and the ending must be 1 byte long
309        assert data_consumer is None or len(ending) == 1
310
311        data = self.serial.read(min_num_bytes)
312        if data_consumer:
313            data_consumer(data)
314        timeout_count = 0
315        while True:
316            if data.endswith(ending):
317                break
318            elif self.serial.inWaiting() > 0:
319                new_data = self.serial.read(1)
320                if data_consumer:
321                    data_consumer(new_data)
322                    data = new_data
323                else:
324                    data = data + new_data
325                timeout_count = 0
326            else:
327                timeout_count += 1
328                if timeout is not None and timeout_count >= 100 * timeout:
329                    print("Error:Waiting for  %s  timeout" %(ending.decode('utf-8')))
330                    print('read data = ', data)
331                    break
332                time.sleep(0.01)
333        return data
334
335
336
337    def enter_raw_repl(self):
338        # interrupt running python
339        # print('step1 exit running thread')
340        self.serial.write(b"\r\x03")  # exit running code
341        time.sleep(0.05)
342        self.serial.write(b"\r\x02")
343        time.sleep(0.2)
344        self.serial.write(b"\r\n")
345        time.sleep(0.05)
346        # inter python cmd
347        # print('enter python mode')
348        self.serial.write(b"python\r\n")  # ctrl-C twice: interrupt any running program
349        time.sleep(0.5)
350        # flush input (without relying on serial.flushInput())
351        # print('python mode checked')
352        n = self.serial.inWaiting()
353        while n > 0:
354            self.serial.read(n)
355            n = self.serial.inWaiting()
356
357        # print('enter raw repl mode')
358        self.serial.write(b"\r\n")
359        time.sleep(0.2)
360        self.serial.write(b"\r\x01")  # ctrl-A: enter raw REPL
361        time.sleep(0.2)
362
363        data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\r\n", 2)
364        # print(data)
365        if not data.endswith(b"raw REPL; CTRL-B to exit\r\r\n"):
366            # recovery
367            self.serial.write(b"\r\x01")  # ctrl-A: enter raw REPL
368            data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\r\n", 2)
369
370            if not data.endswith(b"raw REPL; CTRL-B to exit\r\r\n"):
371                raise HaaSboardError("could not enter raw repl")
372
373        # self.serial.write(b"\x04")  # ctrl-D: soft reset
374        # data = self.read_until(1, b"soft reboot\r\n")
375        # if not data.endswith(b"soft reboot\r\n"):
376        #     print(data)
377        #     raise HaaSboardError("could not enter raw repl")
378        # By splitting this into 2 reads, it allows boot.py to print stuff,
379        # which will show up after the soft reboot and before the raw REPL.
380        # data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n")
381        # if not data.endswith(b"raw REPL; CTRL-B to exit\r\n"):
382        #     print(data)
383        #     raise HaaSboardError("could not enter raw repl")
384
385    def exit_raw_repl(self):
386        time.sleep(0.05)
387        self.serial.write(b"\r\x02")  # ctrl-B: enter friendly REPL
388
389    def exit_python_mode(self):
390        time.sleep(0.05)
391        self.serial.write(b"\r\x04")  # ctrl-D: enter friendly REPL
392
393    def follow(self, timeout, data_consumer=None):
394        # wait for normal output
395        data = self.read_until(1, b"\x04", timeout=timeout, data_consumer=data_consumer)
396        if not data.endswith(b"\x04"):
397            raise HaaSboardError("timeout waiting for first EOF reception")
398        data = data[:-1]
399
400        # wait for error output
401        data_err = self.read_until(1, b"\x04", timeout=timeout)
402        if not data_err.endswith(b"\x04"):
403            raise HaaSboardError("timeout waiting for second EOF reception")
404        data_err = data_err[:-1]
405
406        # return normal and error output
407        return data, data_err
408
409    def raw_paste_write(self, command_bytes):
410        # Read initial header, with window size.
411        data = self.serial.read(2)
412        window_size = data[0] | data[1] << 8
413        window_remain = window_size
414
415        # Write out the command_bytes data.
416        i = 0
417        while i < len(command_bytes):
418            while window_remain == 0 or self.serial.inWaiting():
419                data = self.serial.read(1)
420                if data == b"\x01":
421                    # Device indicated that a new window of data can be sent.
422                    window_remain += window_size
423                elif data == b"\x04":
424                    # Device indicated abrupt end.  Acknowledge it and finish.
425                    self.serial.write(b"\x04")
426                    return
427                else:
428                    # Unexpected data from device.
429                    raise HaaSboardError("unexpected read during raw paste: {}".format(data))
430            # Send out as much data as possible that fits within the allowed window.
431            b = command_bytes[i : min(i + window_remain, len(command_bytes))]
432            self.serial.write(b)
433            window_remain -= len(b)
434            i += len(b)
435
436        # Indicate end of data.
437        self.serial.write(b"\x04")
438
439        # Wait for device to acknowledge end of data.
440        data = self.read_until(1, b"\x04")
441        if not data.endswith(b"\x04"):
442            raise HaaSboardError("could not complete raw paste: {}".format(data))
443
444    def exec_raw_no_follow(self, command):
445        if isinstance(command, bytes):
446            command_bytes = command
447        else:
448            command_bytes = bytes(command, encoding="utf8")
449
450        # check we have a prompt
451        data = self.read_until(1, b">")
452        if not data.endswith(b">"):
453            raise HaaSboardError("could not enter raw repl")
454        if self.use_raw_paste:
455            # Try to enter raw-paste mode.
456            self.serial.write(b"\x05A\x01")
457            data = self.serial.read(2)
458            if data == b"R\x00":
459                # Device understood raw-paste command but doesn't support it.
460                pass
461            elif data == b"R\x01":
462                # Device supports raw-paste mode, write out the command using this mode.
463                return self.raw_paste_write(command_bytes)
464            else:
465                # Device doesn't support raw-paste, fall back to normal raw REPL.
466                data = self.read_until(1, b"w REPL; CTRL-B to exit\r\n>")
467                if not data.endswith(b"w REPL; CTRL-B to exit\r\n>"):
468                    raise HaaSboardError("could not enter raw repl")
469            # Don't try to use raw-paste mode again for this connection.
470            self.use_raw_paste = False
471
472        # Write command using standard raw REPL, 256 bytes every 10ms.
473        for i in range(0, len(command_bytes), 256):
474            self.serial.write(command_bytes[i : min(i + 256, len(command_bytes))])
475            time.sleep(0.01)
476        self.serial.write(b"\x04")
477
478        # check if we could exec command
479        data = self.serial.read(2)
480        if data != b"OK":
481            raise HaaSboardError("could not exec command (response: %r)" % data)
482
483    def exec_raw(self, command, timeout=10, data_consumer=None):
484        self.exec_raw_no_follow(command)
485        return self.follow(timeout, data_consumer)
486
487    def eval(self, expression):
488        ret = self.exec_("print({})".format(expression))
489        ret = ret.strip()
490        return ret
491
492    def exec_(self, command, data_consumer=None):
493        ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)
494        if ret_err:
495            raise HaaSboardError("exception", ret, ret_err)
496        return ret
497
498    def execfile(self, filename):
499        with open(filename, "rb") as f:
500            pyfile = f.read()
501
502        return self.exec_(pyfile)
503
504    def get_time(self):
505        t = str(self.eval("pyb.RTC().datetime()"), encoding="utf8")[1:-1].split(", ")
506        return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
507
508    def fs_ls(self, src):
509        cmd = (
510            "import uos\nfor f in uos.ilistdir(%s):\n"
511            " print('{:12} {}{}'.format(f[3]if len(f)>3 else 0,f[0],'/'if f[1]&0x4000 else ''))"
512            % (("'%s'" % src) if src else "")
513        )
514        self.exec_(cmd, data_consumer=stdout_write_bytes)
515
516    def fs_cat(self, src, chunk_size=256):
517        cmd = (
518            "with open('%s') as f:\n while 1:\n"
519            "  b=f.read(%u)\n  if not b:break\n  print(b,end='')" % (src, chunk_size)
520        )
521        self.exec_(cmd, data_consumer=stdout_write_bytes)
522
523    def fs_get(self, src, dest, chunk_size=256):
524        self.exec_("f=open('%s','rb')\nr=f.read" % src)
525        with open(dest, "wb") as f:
526            while True:
527                data = bytearray()
528                self.exec_("print(r(%u))" % chunk_size, data_consumer=lambda d: data.extend(d))
529                assert data.endswith(b"\r\n\x04")
530                try:
531                    data = ast.literal_eval(str(data[:-3], "ascii"))
532                    if not isinstance(data, bytes):
533                        raise ValueError("Not bytes")
534                except (UnicodeError, ValueError) as e:
535                    raise HaaSboardError("fs_get: Could not interpret received data: %s" % str(e))
536                if not data:
537                    break
538                f.write(data)
539        self.exec_("f.close()")
540
541    def fs_put(self, src, dest, chunk_size=256):
542        self.exec_("f=open('%s','wb')\nw=f.write" % dest)
543        with open(src, "rb") as f:
544            while True:
545                data = f.read(chunk_size)
546                if not data:
547                    break
548                if sys.version_info < (3,):
549                    self.exec_("w(b" + repr(data) + ")")
550                else:
551                    self.exec_("w(" + repr(data) + ")")
552        self.exec_("f.close()")
553
554    def fs_mkdir(self, dir):
555        self.exec_("import uos\nuos.mkdir('%s')" % dir)
556
557    def fs_rmdir(self, dir):
558        self.exec_("import uos\nuos.rmdir('%s')" % dir)
559
560    def fs_rm(self, src):
561        self.exec_("import uos\nuos.remove('%s')" % src)
562
563
564# in Python2 exec is a keyword so one must use "exec_"
565# but for Python3 we want to provide the nicer version "exec"
566setattr(HaaSboard, "exec", HaaSboard.exec_)
567
568
569def execfile(filename, device="/dev/ttyACM0", baudrate=1500000, user="micro", password="python"):
570    pyb = HaaSboard(device, baudrate, user, password)
571    pyb.enter_raw_repl()
572    output = pyb.execfile(filename)
573    stdout_write_bytes(output)
574    pyb.exit_raw_repl()
575    pyb.exit_python_mode()
576    pyb.close()
577
578
579def filesystem_command(pyb, args):
580    def fname_remote(src):
581        if src.startswith(":"):
582            src = src[1:]
583        return src
584
585    def fname_cp_dest(src, dest):
586        src = src.rsplit("/", 1)[-1]
587        if dest is None or dest == "":
588            dest = src
589        elif dest == ".":
590            dest = "./" + src
591        elif dest.endswith("/"):
592            dest += src
593        return dest
594
595    cmd = args[0]
596    args = args[1:]
597    try:
598        if cmd == "cp":
599            srcs = args[:-1]
600            dest = args[-1]
601            if srcs[0].startswith("./") or dest.startswith(":"):
602                op = pyb.fs_put
603                fmt = "cp %s :%s"
604                dest = fname_remote(dest)
605            else:
606                op = pyb.fs_get
607                fmt = "cp :%s %s"
608            for src in srcs:
609                src = fname_remote(src)
610                dest2 = fname_cp_dest(src, dest)
611                print(fmt % (src, dest2))
612                op(src, dest2)
613        else:
614            op = {
615                "ls": pyb.fs_ls,
616                "cat": pyb.fs_cat,
617                "mkdir": pyb.fs_mkdir,
618                "rmdir": pyb.fs_rmdir,
619                "rm": pyb.fs_rm,
620            }[cmd]
621            if cmd == "ls" and not args:
622                args = [""]
623            for src in args:
624                src = fname_remote(src)
625                print("%s :%s" % (cmd, src))
626                op(src)
627    except HaaSboardError as er:
628        print(str(er.args[2], "ascii"))
629        pyb.exit_raw_repl()
630        pyb.close()
631        sys.exit(1)
632
633
634_injected_import_hook_code = """\
635import uos, uio
636class _FS:
637  class File(uio.IOBase):
638    def __init__(self):
639      self.off = 0
640    def ioctl(self, request, arg):
641      return 0
642    def readinto(self, buf):
643      buf[:] = memoryview(_injected_buf)[self.off:self.off + len(buf)]
644      self.off += len(buf)
645      return len(buf)
646  mount = umount = chdir = lambda *args: None
647  def stat(self, path):
648    if path == '_injected.mpy':
649      return tuple(0 for _ in range(10))
650    else:
651      raise OSError(-2) # ENOENT
652  def open(self, path, mode):
653    return self.File()
654uos.mount(_FS(), '/_')
655uos.chdir('/_')
656from _injected import *
657uos.umount('/_')
658del _injected_buf, _FS
659"""
660
661
662def main():
663    import argparse
664
665    cmd_parser = argparse.ArgumentParser(description="Run scripts on the haasboard.")
666    cmd_parser.add_argument(
667        "-d",
668        "--device",
669        default=os.environ.get("HAASBOARD_DEVICE", "/dev/ttyACM0"),
670        help="the serial device or the IP address of the haasboard",
671    )
672    cmd_parser.add_argument(
673        "-b",
674        "--baudrate",
675        default=os.environ.get("HAASBOARD_BAUDRATE", "115200"),
676        help="the baud rate of the serial device",
677    )
678    cmd_parser.add_argument("-u", "--user", default="micro", help="the telnet login username")
679    cmd_parser.add_argument("-p", "--password", default="python", help="the telnet login password")
680    cmd_parser.add_argument("-c", "--command", help="program passed in as string")
681    cmd_parser.add_argument(
682        "-w",
683        "--wait",
684        default=0,
685        type=int,
686        help="seconds to wait for USB connected board to become available",
687    )
688    group = cmd_parser.add_mutually_exclusive_group()
689    group.add_argument(
690        "--follow",
691        action="store_true",
692        help="follow the output after running the scripts [default if no scripts given]",
693    )
694    group.add_argument(
695        "--no-follow",
696        action="store_true",
697        help="Do not follow the output after running the scripts.",
698    )
699    cmd_parser.add_argument(
700        "-f", "--filesystem", action="store_true", help="perform a filesystem action"
701    )
702    cmd_parser.add_argument("files", nargs="*", help="input files")
703    args = cmd_parser.parse_args()
704
705    # open the connection to the haasboard
706    try:
707        pyb = HaaSboard(args.device, args.baudrate, args.user, args.password, args.wait)
708    except HaaSboardError as er:
709        print(er)
710        sys.exit(1)
711
712    # run any command or file(s)
713    if args.command is not None or args.filesystem or len(args.files):
714        # we must enter raw-REPL mode to execute commands
715        # this will do a soft-reset of the board
716        try:
717            pyb.enter_raw_repl()
718        except HaaSboardError as er:
719            print(er)
720            pyb.close()
721            sys.exit(1)
722
723        def execbuffer(buf):
724            try:
725                if args.no_follow:
726                    pyb.exec_raw_no_follow(buf)
727                    ret_err = None
728                else:
729                    ret, ret_err = pyb.exec_raw(
730                        buf, timeout=None, data_consumer=stdout_write_bytes
731                    )
732            except HaaSboardError as er:
733                print(er)
734                pyb.close()
735                sys.exit(1)
736            except KeyboardInterrupt:
737                sys.exit(1)
738            if ret_err:
739                pyb.exit_raw_repl()
740                pyb.close()
741                stdout_write_bytes(ret_err)
742                sys.exit(1)
743
744        # do filesystem commands, if given
745        if args.filesystem:
746            filesystem_command(pyb, args.files)
747            del args.files[:]
748
749        # run the command, if given
750        if args.command is not None:
751            execbuffer(args.command.encode("utf-8"))
752
753        # run any files
754        for filename in args.files:
755            with open(filename, "rb") as f:
756                pyfile = f.read()
757                if filename.endswith(".mpy") and pyfile[0] == ord("M"):
758                    pyb.exec_("_injected_buf=" + repr(pyfile))
759                    pyfile = _injected_import_hook_code
760                execbuffer(pyfile)
761
762        # exiting raw-REPL just drops to friendly-REPL mode
763        pyb.exit_raw_repl()
764
765    # if asked explicitly, or no files given, then follow the output
766    if args.follow or (args.command is None and not args.filesystem and len(args.files) == 0):
767        try:
768            ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)
769        except HaaSboardError as er:
770            print(er)
771            sys.exit(1)
772        except KeyboardInterrupt:
773            sys.exit(1)
774        if ret_err:
775            pyb.close()
776            stdout_write_bytes(ret_err)
777            sys.exit(1)
778
779    # close the connection to the haasboard
780    pyb.close()
781
782
783if __name__ == "__main__":
784    main()
785