diff --git a/stmhal/pyexec.c b/stmhal/pyexec.c
index 08a5866a04d7ad0d8292cc61fad6b8835eafe4e0..3fd797c0d3c605b0692f4ea6632a6cf3544192f7 100644
--- a/stmhal/pyexec.c
+++ b/stmhal/pyexec.c
@@ -54,31 +54,47 @@
 pyexec_mode_kind_t pyexec_mode_kind = PYEXEC_MODE_FRIENDLY_REPL;
 STATIC bool repl_display_debugging_info = 0;
 
+#define EXEC_FLAG_PRINT_EOF (1)
+#define EXEC_FLAG_ALLOW_DEBUGGING (2)
+#define EXEC_FLAG_IS_REPL (4)
+
 // parses, compiles and executes the code in the lexer
 // frees the lexer before returning
-int parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t input_kind, bool is_repl) {
+// EXEC_FLAG_PRINT_EOF prints 2 EOF chars: 1 after normal output, 1 after exception output
+// EXEC_FLAG_ALLOW_DEBUGGING allows debugging info to be printed after executing the code
+// EXEC_FLAG_IS_REPL is used for REPL inputs (flag passed on to mp_compile)
+STATIC int parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t input_kind, int exec_flags) {
+    int ret = 0;
+
     mp_parse_error_kind_t parse_error_kind;
     mp_parse_node_t pn = mp_parse(lex, input_kind, &parse_error_kind);
     qstr source_name = mp_lexer_source_name(lex);
 
+    // check for parse error
     if (pn == MP_PARSE_NODE_NULL) {
-        // parse error
+        if (exec_flags & EXEC_FLAG_PRINT_EOF) {
+            stdout_tx_strn("\x04", 1);
+        }
         mp_parse_show_exception(lex, parse_error_kind);
         mp_lexer_free(lex);
-        return 0;
+        goto finish;
     }
 
     mp_lexer_free(lex);
 
-    mp_obj_t module_fun = mp_compile(pn, source_name, MP_EMIT_OPT_NONE, is_repl);
+    mp_obj_t module_fun = mp_compile(pn, source_name, MP_EMIT_OPT_NONE, exec_flags & EXEC_FLAG_IS_REPL);
 
+    // check for compile error
     if (mp_obj_is_exception_instance(module_fun)) {
+        if (exec_flags & EXEC_FLAG_PRINT_EOF) {
+            stdout_tx_strn("\x04", 1);
+        }
         mp_obj_print_exception(module_fun);
-        return 0;
+        goto finish;
     }
 
+    // execute code
     nlr_buf_t nlr;
-    int ret;
     uint32_t start = HAL_GetTick();
     if (nlr_push(&nlr) == 0) {
         usb_vcp_set_interrupt_char(VCP_CHAR_CTRL_C); // allow ctrl-C to interrupt us
@@ -86,10 +102,17 @@ int parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t input_kind, boo
         usb_vcp_set_interrupt_char(VCP_CHAR_NONE); // disable interrupt
         nlr_pop();
         ret = 1;
+        if (exec_flags & EXEC_FLAG_PRINT_EOF) {
+            stdout_tx_strn("\x04", 1);
+        }
     } else {
         // uncaught exception
         // FIXME it could be that an interrupt happens just before we disable it here
         usb_vcp_set_interrupt_char(VCP_CHAR_NONE); // disable interrupt
+        // print EOF after normal output
+        if (exec_flags & EXEC_FLAG_PRINT_EOF) {
+            stdout_tx_strn("\x04", 1);
+        }
         // check for SystemExit
         if (mp_obj_is_subclass_fast(mp_obj_get_type((mp_obj_t)nlr.ret_val), &mp_type_SystemExit)) {
             // at the moment, the value of SystemExit is unused
@@ -101,7 +124,7 @@ int parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t input_kind, boo
     }
 
     // display debugging info if wanted
-    if (is_repl && repl_display_debugging_info) {
+    if ((exec_flags & EXEC_FLAG_ALLOW_DEBUGGING) && repl_display_debugging_info) {
         uint32_t ticks = HAL_GetTick() - start; // TODO implement a function that does this properly
         printf("took %lu ms\n", ticks);
         gc_collect();
@@ -123,6 +146,11 @@ int parse_compile_execute(mp_lexer_t *lex, mp_parse_input_kind_t input_kind, boo
         }
     }
 
+finish:
+    if (exec_flags & EXEC_FLAG_PRINT_EOF) {
+        stdout_tx_strn("\x04", 1);
+    }
+
     return ret;
 }
 
@@ -171,16 +199,13 @@ raw_repl_reset:
 
         mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, line.buf, line.len, 0);
         if (lex == NULL) {
-            printf("MemoryError\n");
+            printf("\x04MemoryError\n\x04");
         } else {
-            int ret = parse_compile_execute(lex, MP_PARSE_FILE_INPUT, false);
+            int ret = parse_compile_execute(lex, MP_PARSE_FILE_INPUT, EXEC_FLAG_PRINT_EOF);
             if (ret & PYEXEC_FORCED_EXIT) {
                 return ret;
             }
         }
-
-        // indicate end of output with EOF character
-        stdout_tx_str("\004");
     }
 }
 
@@ -217,6 +242,7 @@ friendly_repl_reset:
     */
 
     for (;;) {
+    input_restart:
         vstr_reset(&line);
         int ret = readline(&line, ">>> ");
 
@@ -246,7 +272,11 @@ friendly_repl_reset:
         while (mp_repl_continue_with_input(vstr_str(&line))) {
             vstr_add_char(&line, '\n');
             int ret = readline(&line, "... ");
-            if (ret == VCP_CHAR_CTRL_D) {
+            if (ret == VCP_CHAR_CTRL_C) {
+                // cancel everything
+                stdout_tx_str("\r\n");
+                goto input_restart;
+            } else if (ret == VCP_CHAR_CTRL_D) {
                 // stop entering compound statement
                 break;
             }
@@ -256,7 +286,7 @@ friendly_repl_reset:
         if (lex == NULL) {
             printf("MemoryError\n");
         } else {
-            int ret = parse_compile_execute(lex, MP_PARSE_SINGLE_INPUT, true);
+            int ret = parse_compile_execute(lex, MP_PARSE_SINGLE_INPUT, EXEC_FLAG_ALLOW_DEBUGGING | EXEC_FLAG_IS_REPL);
             if (ret & PYEXEC_FORCED_EXIT) {
                 return ret;
             }
@@ -272,7 +302,7 @@ int pyexec_file(const char *filename) {
         return false;
     }
 
-    return parse_compile_execute(lex, MP_PARSE_FILE_INPUT, false);
+    return parse_compile_execute(lex, MP_PARSE_FILE_INPUT, 0);
 }
 
 mp_obj_t pyb_set_repl_info(mp_obj_t o_value) {
diff --git a/stmhal/readline.c b/stmhal/readline.c
index 5ef86c44fa859d46247f254a8f28b21feb8e92a0..4b1f48baae12f7b70a0a9f07bce7f339093bc3e9 100644
--- a/stmhal/readline.c
+++ b/stmhal/readline.c
@@ -86,6 +86,9 @@ int readline(vstr_t *line, const char *prompt) {
             } else if (c == VCP_CHAR_CTRL_A) {
                 // CTRL-A with non-empty line is go-to-start-of-line
                 goto home_key;
+            } else if (c == VCP_CHAR_CTRL_C) {
+                // CTRL-C with non-empty line is cancel
+                return c;
             } else if (c == VCP_CHAR_CTRL_E) {
                 // CTRL-E is go-to-end-of-line
                 goto end_key;
diff --git a/stmhal/usbd_cdc_interface.c b/stmhal/usbd_cdc_interface.c
index 28f06067bbfc36db671b5601fdfba83e27d681fe..e9b4863f6b07314a4257240bc3d50d8e37cfcf26 100644
--- a/stmhal/usbd_cdc_interface.c
+++ b/stmhal/usbd_cdc_interface.c
@@ -364,6 +364,8 @@ static int8_t CDC_Itf_Receive(uint8_t* Buf, uint32_t *Len) {
         for (; src < buf_top; src++) {
             if (*src == user_interrupt_char) {
                 char_found = true;
+                // raise exception when interrupts are finished
+                pendsv_nlr_jump(user_interrupt_data);
             } else {
                 if (char_found) {
                     *dest = *src;
@@ -372,11 +374,6 @@ static int8_t CDC_Itf_Receive(uint8_t* Buf, uint32_t *Len) {
             }
         }
 
-        if (char_found) {
-            // raise exception when interrupts are finished
-            pendsv_nlr_jump(user_interrupt_data);
-        }
-
         // length of remaining characters
         delta_len = dest - Buf;
     }
diff --git a/tools/pyboard.py b/tools/pyboard.py
old mode 100644
new mode 100755
index 40f2ac934c1f3eba8173b6563d07c1aff29fba5f..d5a01720b76687e4316d59fd4eb8cd7c408453d4
--- a/tools/pyboard.py
+++ b/tools/pyboard.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python
+
 """
 pyboard interface
 
@@ -19,10 +21,15 @@ To run a script from the local machine on the board and print out the results:
 
 This script can also be run directly.  To execute a local script, use:
 
+    ./pyboard.py test.py
+
+Or:
+
     python pyboard.py test.py
 
 """
 
+import sys
 import time
 import serial
 
@@ -31,21 +38,26 @@ class PyboardError(BaseException):
 
 class Pyboard:
     def __init__(self, serial_device):
-        self.serial = serial.Serial(serial_device)
+        self.serial = serial.Serial(serial_device, baudrate=115200, interCharTimeout=1)
 
     def close(self):
         self.serial.close()
 
-    def read_until(self, min_num_bytes, ending, timeout=10):
+    def read_until(self, min_num_bytes, ending, timeout=10, data_consumer=None):
         data = self.serial.read(min_num_bytes)
+        if data_consumer:
+            data_consumer(data)
         timeout_count = 0
         while True:
-            if self.serial.inWaiting() > 0:
-                data = data + self.serial.read(self.serial.inWaiting())
-                time.sleep(0.01)
-                timeout_count = 0
-            elif data.endswith(ending):
+            if data.endswith(ending):
                 break
+            elif self.serial.inWaiting() > 0:
+                new_data = self.serial.read(1)
+                data = data + new_data
+                if data_consumer:
+                    data_consumer(new_data)
+                #time.sleep(0.01)
+                timeout_count = 0
             else:
                 timeout_count += 1
                 if timeout_count >= 10 * timeout:
@@ -54,8 +66,12 @@ class Pyboard:
         return data
 
     def enter_raw_repl(self):
-        self.serial.write(b'\r\x03') # ctrl-C: interrupt any running program
+        self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program
         self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL
+        data = self.read_until(1, b'to exit\r\n>')
+        if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
+            print(data)
+            raise PyboardError('could not enter raw repl')
         self.serial.write(b'\x04') # ctrl-D: soft reset
         data = self.read_until(1, b'to exit\r\n>')
         if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
@@ -65,31 +81,51 @@ class Pyboard:
     def exit_raw_repl(self):
         self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
 
-    def eval(self, expression):
-        ret = self.exec('print({})'.format(expression))
-        ret = ret.strip()
-        return ret
+    def follow(self, data_consumer=False):
+        # wait for normal output
+        data = self.read_until(1, b'\x04', data_consumer=data_consumer)
+        if not data.endswith(b'\x04'):
+            raise PyboardError('timeout waiting for first EOF reception')
+        data = data[:-1]
 
-    def exec(self, command):
+        # wait for error output
+        data_err = self.read_until(2, b'\x04>')
+        if not data_err.endswith(b'\x04>'):
+            raise PyboardError('timeout waiting for second EOF reception')
+        data_err = data_err[:-2]
+
+        # return normal and error output
+        return data, data_err
+
+    def exec_raw(self, command, data_consumer=False):
         if isinstance(command, bytes):
             command_bytes = command
         else:
             command_bytes = bytes(command, encoding='ascii')
+
+        # write command
         for i in range(0, len(command_bytes), 32):
             self.serial.write(command_bytes[i:min(i+32, len(command_bytes))])
             time.sleep(0.01)
         self.serial.write(b'\x04')
+
+        # check if we could exec command
         data = self.serial.read(2)
         if data != b'OK':
             raise PyboardError('could not exec command')
-        data = self.read_until(2, b'\x04>')
-        if not data.endswith(b'\x04>'):
-            print(data)
-            raise PyboardError('timeout waiting for EOF reception')
-        if data.startswith(b'Traceback') or data.startswith(b'  File '):
-            print(data)
-            raise PyboardError('command failed')
-        return data[:-2]
+
+        return self.follow(data_consumer)
+
+    def eval(self, expression):
+        ret = self.exec('print({})'.format(expression))
+        ret = ret.strip()
+        return ret
+
+    def exec(self, command):
+        ret, ret_err = self.exec_raw(command)
+        if ret_err:
+            raise PyboardError('exception', ret, ret_err)
+        return ret
 
     def execfile(self, filename):
         with open(filename) as f:
@@ -175,8 +211,37 @@ def main():
     if args.test:
         run_test(device=args.device)
 
-    for file in args.files:
-        execfile(file, device=args.device)
+    if len(args.files) == 0:
+        try:
+            pyb = Pyboard(args.device)
+            ret, ret_err = pyb.follow(data_consumer=lambda d:print(str(d, encoding='ascii'), end=''))
+            pyb.close()
+        except PyboardError as er:
+            print(er)
+            sys.exit(1)
+        except KeyboardInterrupt:
+            sys.exit(1)
+        if ret_err:
+            print(str(ret_err, encoding='ascii'), end='')
+            sys.exit(1)
+
+    for filename in args.files:
+        try:
+            pyb = Pyboard(args.device)
+            pyb.enter_raw_repl()
+            with open(filename) as f:
+                pyfile = f.read()
+            ret, ret_err = pyb.exec_raw(pyfile, data_consumer=lambda d:print(str(d, encoding='ascii'), end=''))
+            pyb.exit_raw_repl()
+            pyb.close()
+        except PyboardError as er:
+            print(er)
+            sys.exit(1)
+        except KeyboardInterrupt:
+            sys.exit(1)
+        if ret_err:
+            print(str(ret_err, encoding='ascii'), end='')
+            sys.exit(1)
 
 if __name__ == "__main__":
     main()