diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..22d151a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,71 @@
+# Contributing to [Project Name]
+
+This document provides guidelines and instructions for contributing to the project.
+
+## Getting Started
+
+1. Fork the repository
+2. Clone your fork:
+
+   ```bash
+   git clone https://github.com/your-username/project-name.git
+   cd project-name
+   ```
+
+3. Create a new branch, be descriptive:
+
+   ```bash
+   git checkout -b feature/your-feature-name
+
+   # OR
+
+   git checkout -b bugfix/bug-this-pr-fixes
+
+   # OR
+
+   git checkout -b docs/documenting-something
+   ```
+
+## Development Workflow
+
+### Testing
+
+- Implement your feature/changes
+- Test any code changes thoroughly
+- Make sure your PR contains all the information for any developer to checkout your branch and validate it
+
+### Commit Guidelines (not enforced but nice)
+
+- Your commit message should be under 72 characters
+- Your commit message should start with a topic (e.g.: "Docs: ")
+- Sign-off your commits with `git commit -sm "Feature: Commit subject under 72 characters."`
+
+## Submitting Changes
+
+1. Push your changes to your fork
+2. Submit a pull request towards the `development` branch of this repository
+3. Ensure the PR description clearly describes the problem and solution
+4. Include the relevant issue number if applicable
+
+### Pull Request Process
+
+1. Update documentation if needed: anyone should be able to checkout your branch and validate the changes
+2. Add tests for new features
+3. Wait for review
+4. Upon approval, if you have write access you can merge your PR
+
+### New version/release (maintainers only)
+
+Once all PRs are merged into `development` and it's considered ready for a new version, create a new branch from it and name it as the new version (e.g.: `v1.5.5`)
+
+1. Update the `"version"` field in `package.json`
+1. Run `npm install` to update `package-lock.json`
+1. Validate the above
+1. Commit and push these changes
+1. Open a PR from this new branch to `main` to align version numbers
+
+The branch `main` is always assumed to be stable, and contains the latest version.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the project's license.
diff --git a/TESTS.md b/TESTS.md
new file mode 100644
index 0000000..4656a26
--- /dev/null
+++ b/TESTS.md
@@ -0,0 +1,36 @@
+# Test cases:
+
+Get prompt
+Write to repl and get data as you type / real time repl
+Enter raw repl from prompt
+
+Get prompt when you are on raw repl
+Re-enter raw repl
+
+Exit raw repl
+
+Execute raw and get final output
+Execute raw and get data as it executes and the leave raw repl
+
+List files on root
+Make directory
+Put small file on root
+Put big file on root
+Get root file content
+
+Put small file on directory
+Put big file on directory
+Get directory file content
+
+Check for a file that exists on root folder
+Check for a file that exists on folder
+
+Remove file
+Remove folder
+
+Check for a file that doesn't exist
+
+Creates a file from string
+Overrides a file content with string
+Rename file on root
+Rename file on folder
diff --git a/cli.js b/cli.js
index 124d37a..7a13be2 100755
--- a/cli.js
+++ b/cli.js
@@ -82,7 +82,7 @@ const executeString = (args, port, dataConsumer) => {
   const code = args[0] || ''
   return board.open(port)
   .then(() => board.enter_raw_repl())
-  .then(() => board.exec_raw({ command: code, data_consumer: dataConsumer }))
+  .then(() => board.exec_raw(code, dataConsumer))
   .then(async (out) => {
     await board.exit_raw_repl()
     await board.close()
@@ -140,7 +140,8 @@ const getFile = (args, port, dataConsumer) => {
   return board.open(port)
   .then(async () => {
     try {
-      let output = await board.fs_cat(boardFilename, consumer)
+      let output = await board.fs_cat_binary(boardFilename, consumer)
+      output = Buffer.from(output);
       fs.writeFileSync(diskFilename, output)
       log('output')
       log(output)
diff --git a/examples/07_save_file.js b/examples/07_save_file.js
index c0f806e..5729b0a 100644
--- a/examples/07_save_file.js
+++ b/examples/07_save_file.js
@@ -4,6 +4,7 @@ let content = `
 """
 Test
 """
+# Emoji TEST 💩🤯🫶🏼
 
 from time import sleep
 from machine import Pin
diff --git a/examples/big_file.py b/examples/big_file.py
index 9df9021..8994a6c 100644
--- a/examples/big_file.py
+++ b/examples/big_file.py
@@ -1,1043 +1,95 @@
-"""ILI9341 LCD/Touch module."""
-from time import sleep
-from math import cos, sin, pi, radians
-from sys import implementation
-from framebuf import FrameBuffer, RGB565  # type: ignore
-import ustruct  # type: ignore
-
-
-def color565(r, g, b):
-    """Return RGB565 color value.
-
-    Args:
-        r (int): Red value.
-        g (int): Green value.
-        b (int): Blue value.
-    """
-    return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
-
-
-class Display(object):
-    """Serial interface for 16-bit color (5-6-5 RGB) IL9341 display.
-
-    Note:  All coordinates are zero based.
-    """
-
-    # Command constants from ILI9341 datasheet
-    NOP = const(0x00)  # No-op
-    SWRESET = const(0x01)  # Software reset
-    RDDID = const(0x04)  # Read display ID info
-    RDDST = const(0x09)  # Read display status
-    SLPIN = const(0x10)  # Enter sleep mode
-    SLPOUT = const(0x11)  # Exit sleep mode
-    PTLON = const(0x12)  # Partial mode on
-    NORON = const(0x13)  # Normal display mode on
-    RDMODE = const(0x0A)  # Read display power mode
-    RDMADCTL = const(0x0B)  # Read display MADCTL
-    RDPIXFMT = const(0x0C)  # Read display pixel format
-    RDIMGFMT = const(0x0D)  # Read display image format
-    RDSELFDIAG = const(0x0F)  # Read display self-diagnostic
-    INVOFF = const(0x20)  # Display inversion off
-    INVON = const(0x21)  # Display inversion on
-    GAMMASET = const(0x26)  # Gamma set
-    DISPLAY_OFF = const(0x28)  # Display off
-    DISPLAY_ON = const(0x29)  # Display on
-    SET_COLUMN = const(0x2A)  # Column address set
-    SET_PAGE = const(0x2B)  # Page address set
-    WRITE_RAM = const(0x2C)  # Memory write
-    READ_RAM = const(0x2E)  # Memory read
-    PTLAR = const(0x30)  # Partial area
-    VSCRDEF = const(0x33)  # Vertical scrolling definition
-    MADCTL = const(0x36)  # Memory access control
-    VSCRSADD = const(0x37)  # Vertical scrolling start address
-    PIXFMT = const(0x3A)  # COLMOD: Pixel format set
-    WRITE_DISPLAY_BRIGHTNESS = const(0x51)  # Brightness hardware dependent!
-    READ_DISPLAY_BRIGHTNESS = const(0x52)
-    WRITE_CTRL_DISPLAY = const(0x53)
-    READ_CTRL_DISPLAY = const(0x54)
-    WRITE_CABC = const(0x55)  # Write Content Adaptive Brightness Control
-    READ_CABC = const(0x56)  # Read Content Adaptive Brightness Control
-    WRITE_CABC_MINIMUM = const(0x5E)  # Write CABC Minimum Brightness
-    READ_CABC_MINIMUM = const(0x5F)  # Read CABC Minimum Brightness
-    FRMCTR1 = const(0xB1)  # Frame rate control (In normal mode/full colors)
-    FRMCTR2 = const(0xB2)  # Frame rate control (In idle mode/8 colors)
-    FRMCTR3 = const(0xB3)  # Frame rate control (In partial mode/full colors)
-    INVCTR = const(0xB4)  # Display inversion control
-    DFUNCTR = const(0xB6)  # Display function control
-    PWCTR1 = const(0xC0)  # Power control 1
-    PWCTR2 = const(0xC1)  # Power control 2
-    PWCTRA = const(0xCB)  # Power control A
-    PWCTRB = const(0xCF)  # Power control B
-    VMCTR1 = const(0xC5)  # VCOM control 1
-    VMCTR2 = const(0xC7)  # VCOM control 2
-    RDID1 = const(0xDA)  # Read ID 1
-    RDID2 = const(0xDB)  # Read ID 2
-    RDID3 = const(0xDC)  # Read ID 3
-    RDID4 = const(0xDD)  # Read ID 4
-    GMCTRP1 = const(0xE0)  # Positive gamma correction
-    GMCTRN1 = const(0xE1)  # Negative gamma correction
-    DTCA = const(0xE8)  # Driver timing control A
-    DTCB = const(0xEA)  # Driver timing control B
-    POSC = const(0xED)  # Power on sequence control
-    ENABLE3G = const(0xF2)  # Enable 3 gamma control
-    PUMPRC = const(0xF7)  # Pump ratio control
-
-    ROTATE = {
-        0: 0x88,
-        90: 0xE8,
-        180: 0x48,
-        270: 0x28
-    }
-
-    def __init__(self, spi, cs, dc, rst,
-                 width=240, height=320, rotation=0):
-        """Initialize OLED.
-
-        Args:
-            spi (Class Spi):  SPI interface for OLED
-            cs (Class Pin):  Chip select pin
-            dc (Class Pin):  Data/Command pin
-            rst (Class Pin):  Reset pin
-            width (Optional int): Screen width (default 240)
-            height (Optional int): Screen height (default 320)
-            rotation (Optional int): Rotation must be 0 default, 90. 180 or 270
-        """
-        self.spi = spi
-        self.cs = cs
-        self.dc = dc
-        self.rst = rst
-        self.width = width
-        self.height = height
-        if rotation not in self.ROTATE.keys():
-            raise RuntimeError('Rotation must be 0, 90, 180 or 270.')
-        else:
-            self.rotation = self.ROTATE[rotation]
-
-        # Initialize GPIO pins and set implementation specific methods
-        if implementation.name == 'circuitpython':
-            self.cs.switch_to_output(value=True)
-            self.dc.switch_to_output(value=False)
-            self.rst.switch_to_output(value=True)
-            self.reset = self.reset_cpy
-            self.write_cmd = self.write_cmd_cpy
-            self.write_data = self.write_data_cpy
-        else:
-            self.cs.init(self.cs.OUT, value=1)
-            self.dc.init(self.dc.OUT, value=0)
-            self.rst.init(self.rst.OUT, value=1)
-            self.reset = self.reset_mpy
-            self.write_cmd = self.write_cmd_mpy
-            self.write_data = self.write_data_mpy
-        self.reset()
-        # Send initialization commands
-        self.write_cmd(self.SWRESET)  # Software reset
-        sleep(.1)
-        self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30)  # Pwr ctrl B
-        self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81)  # Pwr on seq. ctrl
-        self.write_cmd(self.DTCA, 0x85, 0x00, 0x78)  # Driver timing ctrl A
-        self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02)  # Pwr ctrl A
-        self.write_cmd(self.PUMPRC, 0x20)  # Pump ratio control
-        self.write_cmd(self.DTCB, 0x00, 0x00)  # Driver timing ctrl B
-        self.write_cmd(self.PWCTR1, 0x23)  # Pwr ctrl 1
-        self.write_cmd(self.PWCTR2, 0x10)  # Pwr ctrl 2
-        self.write_cmd(self.VMCTR1, 0x3E, 0x28)  # VCOM ctrl 1
-        self.write_cmd(self.VMCTR2, 0x86)  # VCOM ctrl 2
-        self.write_cmd(self.MADCTL, self.rotation)  # Memory access ctrl
-        self.write_cmd(self.VSCRSADD, 0x00)  # Vertical scrolling start address
-        self.write_cmd(self.PIXFMT, 0x55)  # COLMOD: Pixel format
-        self.write_cmd(self.FRMCTR1, 0x00, 0x18)  # Frame rate ctrl
-        self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27)
-        self.write_cmd(self.ENABLE3G, 0x00)  # Enable 3 gamma ctrl
-        self.write_cmd(self.GAMMASET, 0x01)  # Gamma curve selected
-        self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E,
-                       0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00)
-        self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31,
-                       0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F)
-        self.write_cmd(self.SLPOUT)  # Exit sleep
-        sleep(.1)
-        self.write_cmd(self.DISPLAY_ON)  # Display on
-        sleep(.1)
-        self.clear()
-
-    def block(self, x0, y0, x1, y1, data):
-        """Write a block of data to display.
-
-        Args:
-            x0 (int):  Starting X position.
-            y0 (int):  Starting Y position.
-            x1 (int):  Ending X position.
-            y1 (int):  Ending Y position.
-            data (bytes): Data buffer to write.
-        """
-        self.write_cmd(self.SET_COLUMN, *ustruct.pack(">HH", x0, x1))
-        self.write_cmd(self.SET_PAGE, *ustruct.pack(">HH", y0, y1))
-
-        self.write_cmd(self.WRITE_RAM)
-        self.write_data(data)
-
-    def cleanup(self):
-        """Clean up resources."""
-        self.clear()
-        self.display_off()
-        self.spi.deinit()
-        print('display off')
-
-    def clear(self, color=0, hlines=8):
-        """Clear display.
-
-        Args:
-            color (Optional int): RGB565 color value (Default: 0 = Black).
-            hlines (Optional int): # of horizontal lines per chunk (Default: 8)
-        Note:
-            hlines was introduced to deal with memory allocation on some
-            boards.  Smaller values allocate less memory but take longer
-            to execute.  hlines must be a factor of the display height.
-            For example, for a 240 pixel height, valid values for hline
-            would be 1, 2, 4, 5, 8, 10, 16, 20, 32, 40, 64, 80, 160.
-            Higher values may result in memory allocation errors.
-        """
-        w = self.width
-        h = self.height
-        assert hlines > 0 and h % hlines == 0, (
-            "hlines must be a non-zero factor of height.")
-        # Clear display
-        if color:
-            line = color.to_bytes(2, 'big') * (w * hlines)
-        else:
-            line = bytearray(w * 2 * hlines)
-        for y in range(0, h, hlines):
-            self.block(0, y, w - 1, y + hlines - 1, line)
-
-    def display_off(self):
-        """Turn display off."""
-        self.write_cmd(self.DISPLAY_OFF)
-
-    def display_on(self):
-        """Turn display on."""
-        self.write_cmd(self.DISPLAY_ON)
-
-    def draw_circle(self, x0, y0, r, color):
-        """Draw a circle.
-
-        Args:
-            x0 (int): X coordinate of center point.
-            y0 (int): Y coordinate of center point.
-            r (int): Radius.
-            color (int): RGB565 color value.
-        """
-        f = 1 - r
-        dx = 1
-        dy = -r - r
-        x = 0
-        y = r
-        self.draw_pixel(x0, y0 + r, color)
-        self.draw_pixel(x0, y0 - r, color)
-        self.draw_pixel(x0 + r, y0, color)
-        self.draw_pixel(x0 - r, y0, color)
-        while x < y:
-            if f >= 0:
-                y -= 1
-                dy += 2
-                f += dy
-            x += 1
-            dx += 2
-            f += dx
-            self.draw_pixel(x0 + x, y0 + y, color)
-            self.draw_pixel(x0 - x, y0 + y, color)
-            self.draw_pixel(x0 + x, y0 - y, color)
-            self.draw_pixel(x0 - x, y0 - y, color)
-            self.draw_pixel(x0 + y, y0 + x, color)
-            self.draw_pixel(x0 - y, y0 + x, color)
-            self.draw_pixel(x0 + y, y0 - x, color)
-            self.draw_pixel(x0 - y, y0 - x, color)
-
-    def draw_ellipse(self, x0, y0, a, b, color):
-        """Draw an ellipse.
-
-        Args:
-            x0, y0 (int): Coordinates of center point.
-            a (int): Semi axis horizontal.
-            b (int): Semi axis vertical.
-            color (int): RGB565 color value.
-        Note:
-            The center point is the center of the x0,y0 pixel.
-            Since pixels are not divisible, the axes are integer rounded
-            up to complete on a full pixel.  Therefore the major and
-            minor axes are increased by 1.
-        """
-        a2 = a * a
-        b2 = b * b
-        twoa2 = a2 + a2
-        twob2 = b2 + b2
-        x = 0
-        y = b
-        px = 0
-        py = twoa2 * y
-        # Plot initial points
-        self.draw_pixel(x0 + x, y0 + y, color)
-        self.draw_pixel(x0 - x, y0 + y, color)
-        self.draw_pixel(x0 + x, y0 - y, color)
-        self.draw_pixel(x0 - x, y0 - y, color)
-        # Region 1
-        p = round(b2 - (a2 * b) + (0.25 * a2))
-        while px < py:
-            x += 1
-            px += twob2
-            if p < 0:
-                p += b2 + px
-            else:
-                y -= 1
-                py -= twoa2
-                p += b2 + px - py
-            self.draw_pixel(x0 + x, y0 + y, color)
-            self.draw_pixel(x0 - x, y0 + y, color)
-            self.draw_pixel(x0 + x, y0 - y, color)
-            self.draw_pixel(x0 - x, y0 - y, color)
-        # Region 2
-        p = round(b2 * (x + 0.5) * (x + 0.5) +
-                  a2 * (y - 1) * (y - 1) - a2 * b2)
-        while y > 0:
-            y -= 1
-            py -= twoa2
-            if p > 0:
-                p += a2 - py
-            else:
-                x += 1
-                px += twob2
-                p += a2 - py + px
-            self.draw_pixel(x0 + x, y0 + y, color)
-            self.draw_pixel(x0 - x, y0 + y, color)
-            self.draw_pixel(x0 + x, y0 - y, color)
-            self.draw_pixel(x0 - x, y0 - y, color)
-
-    def draw_hline(self, x, y, w, color):
-        """Draw a horizontal line.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            w (int): Width of line.
-            color (int): RGB565 color value.
-        """
-        if self.is_off_grid(x, y, x + w - 1, y):
-            return
-        line = color.to_bytes(2, 'big') * w
-        self.block(x, y, x + w - 1, y, line)
-
-    def draw_image(self, path, x=0, y=0, w=320, h=240):
-        """Draw image from flash.
-
-        Args:
-            path (string): Image file path.
-            x (int): X coordinate of image left.  Default is 0.
-            y (int): Y coordinate of image top.  Default is 0.
-            w (int): Width of image.  Default is 320.
-            h (int): Height of image.  Default is 240.
-        """
-        x2 = x + w - 1
-        y2 = y + h - 1
-        if self.is_off_grid(x, y, x2, y2):
-            return
-        with open(path, "rb") as f:
-            chunk_height = 1024 // w
-            chunk_count, remainder = divmod(h, chunk_height)
-            chunk_size = chunk_height * w * 2
-            chunk_y = y
-            if chunk_count:
-                for c in range(0, chunk_count):
-                    buf = f.read(chunk_size)
-                    self.block(x, chunk_y,
-                               x2, chunk_y + chunk_height - 1,
-                               buf)
-                    chunk_y += chunk_height
-            if remainder:
-                buf = f.read(remainder * w * 2)
-                self.block(x, chunk_y,
-                           x2, chunk_y + remainder - 1,
-                           buf)
-
-    def draw_letter(self, x, y, letter, font, color, background=0,
-                    landscape=False):
-        """Draw a letter.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            letter (string): Letter to draw.
-            font (XglcdFont object): Font.
-            color (int): RGB565 color value.
-            background (int): RGB565 background color (default: black).
-            landscape (bool): Orientation (default: False = portrait)
-        """
-        buf, w, h = font.get_letter(letter, color, background, landscape)
-        # Check for errors (Font could be missing specified letter)
-        if w == 0:
-            return w, h
-
-        if landscape:
-            y -= w
-            if self.is_off_grid(x, y, x + h - 1, y + w - 1):
-                return 0, 0
-            self.block(x, y,
-                       x + h - 1, y + w - 1,
-                       buf)
-        else:
-            if self.is_off_grid(x, y, x + w - 1, y + h - 1):
-                return 0, 0
-            self.block(x, y,
-                       x + w - 1, y + h - 1,
-                       buf)
-        return w, h
-
-    def draw_line(self, x1, y1, x2, y2, color):
-        """Draw a line using Bresenham's algorithm.
-
-        Args:
-            x1, y1 (int): Starting coordinates of the line
-            x2, y2 (int): Ending coordinates of the line
-            color (int): RGB565 color value.
-        """
-        # Check for horizontal line
-        if y1 == y2:
-            if x1 > x2:
-                x1, x2 = x2, x1
-            self.draw_hline(x1, y1, x2 - x1 + 1, color)
-            return
-        # Check for vertical line
-        if x1 == x2:
-            if y1 > y2:
-                y1, y2 = y2, y1
-            self.draw_vline(x1, y1, y2 - y1 + 1, color)
-            return
-        # Confirm coordinates in boundary
-        if self.is_off_grid(min(x1, x2), min(y1, y2),
-                            max(x1, x2), max(y1, y2)):
-            return
-        # Changes in x, y
-        dx = x2 - x1
-        dy = y2 - y1
-        # Determine how steep the line is
-        is_steep = abs(dy) > abs(dx)
-        # Rotate line
-        if is_steep:
-            x1, y1 = y1, x1
-            x2, y2 = y2, x2
-        # Swap start and end points if necessary
-        if x1 > x2:
-            x1, x2 = x2, x1
-            y1, y2 = y2, y1
-        # Recalculate differentials
-        dx = x2 - x1
-        dy = y2 - y1
-        # Calculate error
-        error = dx >> 1
-        ystep = 1 if y1 < y2 else -1
-        y = y1
-        for x in range(x1, x2 + 1):
-            # Had to reverse HW ????
-            if not is_steep:
-                self.draw_pixel(x, y, color)
-            else:
-                self.draw_pixel(y, x, color)
-            error -= abs(dy)
-            if error < 0:
-                y += ystep
-                error += dx
-
-    def draw_lines(self, coords, color):
-        """Draw multiple lines.
-
-        Args:
-            coords ([[int, int],...]): Line coordinate X, Y pairs
-            color (int): RGB565 color value.
-        """
-        # Starting point
-        x1, y1 = coords[0]
-        # Iterate through coordinates
-        for i in range(1, len(coords)):
-            x2, y2 = coords[i]
-            self.draw_line(x1, y1, x2, y2, color)
-            x1, y1 = x2, y2
-
-    def draw_pixel(self, x, y, color):
-        """Draw a single pixel.
-
-        Args:
-            x (int): X position.
-            y (int): Y position.
-            color (int): RGB565 color value.
-        """
-        if self.is_off_grid(x, y, x, y):
-            return
-        self.block(x, y, x, y, color.to_bytes(2, 'big'))
-
-    def draw_polygon(self, sides, x0, y0, r, color, rotate=0):
-        """Draw an n-sided regular polygon.
-
-        Args:
-            sides (int): Number of polygon sides.
-            x0, y0 (int): Coordinates of center point.
-            r (int): Radius.
-            color (int): RGB565 color value.
-            rotate (Optional float): Rotation in degrees relative to origin.
-        Note:
-            The center point is the center of the x0,y0 pixel.
-            Since pixels are not divisible, the radius is integer rounded
-            up to complete on a full pixel.  Therefore diameter = 2 x r + 1.
-        """
-        coords = []
-        theta = radians(rotate)
-        n = sides + 1
-        for s in range(n):
-            t = 2.0 * pi * s / sides + theta
-            coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)])
-
-        # Cast to python float first to fix rounding errors
-        self.draw_lines(coords, color=color)
-
-    def draw_rectangle(self, x, y, w, h, color):
-        """Draw a rectangle.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            w (int): Width of rectangle.
-            h (int): Height of rectangle.
-            color (int): RGB565 color value.
-        """
-        x2 = x + w - 1
-        y2 = y + h - 1
-        self.draw_hline(x, y, w, color)
-        self.draw_hline(x, y2, w, color)
-        self.draw_vline(x, y, h, color)
-        self.draw_vline(x2, y, h, color)
-
-    def draw_sprite(self, buf, x, y, w, h):
-        """Draw a sprite (optimized for horizontal drawing).
-
-        Args:
-            buf (bytearray): Buffer to draw.
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            w (int): Width of drawing.
-            h (int): Height of drawing.
-        """
-        x2 = x + w - 1
-        y2 = y + h - 1
-        if self.is_off_grid(x, y, x2, y2):
-            return
-        self.block(x, y, x2, y2, buf)
-
-    def draw_text(self, x, y, text, font, color,  background=0,
-                  landscape=False, spacing=1):
-        """Draw text.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            text (string): Text to draw.
-            font (XglcdFont object): Font.
-            color (int): RGB565 color value.
-            background (int): RGB565 background color (default: black).
-            landscape (bool): Orientation (default: False = portrait)
-            spacing (int): Pixels between letters (default: 1)
-        """
-        for letter in text:
-            # Get letter array and letter dimensions
-            w, h = self.draw_letter(x, y, letter, font, color, background,
-                                    landscape)
-            # Stop on error
-            if w == 0 or h == 0:
-                print('Invalid width {0} or height {1}'.format(w, h))
-                return
-
-            if landscape:
-                # Fill in spacing
-                if spacing:
-                    self.fill_hrect(x, y - w - spacing, h, spacing, background)
-                # Position y for next letter
-                y -= (w + spacing)
-            else:
-                # Fill in spacing
-                if spacing:
-                    self.fill_hrect(x + w, y, spacing, h, background)
-                # Position x for next letter
-                x += (w + spacing)
-
-                # # Fill in spacing
-                # if spacing:
-                #     self.fill_vrect(x + w, y, spacing, h, background)
-                # # Position x for next letter
-                # x += w + spacing
-
-    def draw_text8x8(self, x, y, text, color,  background=0,
-                     rotate=0):
-        """Draw text using built-in MicroPython 8x8 bit font.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            text (string): Text to draw.
-            color (int): RGB565 color value.
-            background (int): RGB565 background color (default: black).
-            rotate(int): 0, 90, 180, 270
-        """
-        w = len(text) * 8
-        h = 8
-        # Confirm coordinates in boundary
-        if self.is_off_grid(x, y, x + 7, y + 7):
-            return
-        # Rearrange color
-        r = (color & 0xF800) >> 8
-        g = (color & 0x07E0) >> 3
-        b = (color & 0x1F) << 3
-        buf = bytearray(w * 16)
-        fbuf = FrameBuffer(buf, w, h, RGB565)
-        if background != 0:
-            bg_r = (background & 0xF800) >> 8
-            bg_g = (background & 0x07E0) >> 3
-            bg_b = (background & 0x1F) << 3
-            fbuf.fill(color565(bg_b, bg_r, bg_g))
-        fbuf.text(text, 0, 0, color565(b, r, g))
-        if rotate == 0:
-            self.block(x, y, x + w - 1, y + (h - 1), buf)
-        elif rotate == 90:
-            buf2 = bytearray(w * 16)
-            fbuf2 = FrameBuffer(buf2, h, w, RGB565)
-            for y1 in range(h):
-                for x1 in range(w):
-                    fbuf2.pixel(y1, x1,
-                                fbuf.pixel(x1, (h - 1) - y1))
-            self.block(x, y, x + (h - 1), y + w - 1, buf2)
-        elif rotate == 180:
-            buf2 = bytearray(w * 16)
-            fbuf2 = FrameBuffer(buf2, w, h, RGB565)
-            for y1 in range(h):
-                for x1 in range(w):
-                    fbuf2.pixel(x1, y1,
-                                fbuf.pixel((w - 1) - x1, (h - 1) - y1))
-            self.block(x, y, x + w - 1, y + (h - 1), buf2)
-        elif rotate == 270:
-            buf2 = bytearray(w * 16)
-            fbuf2 = FrameBuffer(buf2, h, w, RGB565)
-            for y1 in range(h):
-                for x1 in range(w):
-                    fbuf2.pixel(y1, x1,
-                                fbuf.pixel((w - 1) - x1, y1))
-            self.block(x, y, x + (h - 1), y + w - 1, buf2)
-
-    def draw_vline(self, x, y, h, color):
-        """Draw a vertical line.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            h (int): Height of line.
-            color (int): RGB565 color value.
-        """
-        # Confirm coordinates in boundary
-        if self.is_off_grid(x, y, x, y + h - 1):
-            return
-        line = color.to_bytes(2, 'big') * h
-        self.block(x, y, x, y + h - 1, line)
-
-    def fill_circle(self, x0, y0, r, color):
-        """Draw a filled circle.
-
-        Args:
-            x0 (int): X coordinate of center point.
-            y0 (int): Y coordinate of center point.
-            r (int): Radius.
-            color (int): RGB565 color value.
-        """
-        f = 1 - r
-        dx = 1
-        dy = -r - r
-        x = 0
-        y = r
-        self.draw_vline(x0, y0 - r, 2 * r + 1, color)
-        while x < y:
-            if f >= 0:
-                y -= 1
-                dy += 2
-                f += dy
-            x += 1
-            dx += 2
-            f += dx
-            self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color)
-            self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color)
-            self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color)
-            self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color)
-
-    def fill_ellipse(self, x0, y0, a, b, color):
-        """Draw a filled ellipse.
-
-        Args:
-            x0, y0 (int): Coordinates of center point.
-            a (int): Semi axis horizontal.
-            b (int): Semi axis vertical.
-            color (int): RGB565 color value.
-        Note:
-            The center point is the center of the x0,y0 pixel.
-            Since pixels are not divisible, the axes are integer rounded
-            up to complete on a full pixel.  Therefore the major and
-            minor axes are increased by 1.
-        """
-        a2 = a * a
-        b2 = b * b
-        twoa2 = a2 + a2
-        twob2 = b2 + b2
-        x = 0
-        y = b
-        px = 0
-        py = twoa2 * y
-        # Plot initial points
-        self.draw_line(x0, y0 - y, x0, y0 + y, color)
-        # Region 1
-        p = round(b2 - (a2 * b) + (0.25 * a2))
-        while px < py:
-            x += 1
-            px += twob2
-            if p < 0:
-                p += b2 + px
-            else:
-                y -= 1
-                py -= twoa2
-                p += b2 + px - py
-            self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color)
-            self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color)
-        # Region 2
-        p = round(b2 * (x + 0.5) * (x + 0.5) +
-                  a2 * (y - 1) * (y - 1) - a2 * b2)
-        while y > 0:
-            y -= 1
-            py -= twoa2
-            if p > 0:
-                p += a2 - py
-            else:
-                x += 1
-                px += twob2
-                p += a2 - py + px
-            self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color)
-            self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color)
-
-    def fill_hrect(self, x, y, w, h, color):
-        """Draw a filled rectangle (optimized for horizontal drawing).
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            w (int): Width of rectangle.
-            h (int): Height of rectangle.
-            color (int): RGB565 color value.
-        """
-        if self.is_off_grid(x, y, x + w - 1, y + h - 1):
-            return
-        chunk_height = 1024 // w
-        chunk_count, remainder = divmod(h, chunk_height)
-        chunk_size = chunk_height * w
-        chunk_y = y
-        if chunk_count:
-            buf = color.to_bytes(2, 'big') * chunk_size
-            for c in range(0, chunk_count):
-                self.block(x, chunk_y,
-                           x + w - 1, chunk_y + chunk_height - 1,
-                           buf)
-                chunk_y += chunk_height
-
-        if remainder:
-            buf = color.to_bytes(2, 'big') * remainder * w
-            self.block(x, chunk_y,
-                       x + w - 1, chunk_y + remainder - 1,
-                       buf)
-
-    def fill_rectangle(self, x, y, w, h, color):
-        """Draw a filled rectangle.
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            w (int): Width of rectangle.
-            h (int): Height of rectangle.
-            color (int): RGB565 color value.
-        """
-        if self.is_off_grid(x, y, x + w - 1, y + h - 1):
-            return
-        if w > h:
-            self.fill_hrect(x, y, w, h, color)
+# stepper.py
+
+# A micropython driver for 4-phase, unipolar stepper motors such as
+# the 28BYJ-48
+
+# Relesed to the Public Domain by Nicko van Someren, 2020
+
+# The constructor for the Stepper class takes as arguments the four
+# pins for driving the motor phases, in phase order, and optionally a
+# timer. The pins can be passed as pin numbers or machine.Pin objects
+# and the timer can be a machine.Timer object or a timer index. Note
+# that if two stepper motors use the same timer then they will not be
+# able to run at the same time.
+#
+# The run() method takes a number of steps and an optional delay (in
+# seconds) between driving the steps (the default is 1ms). A negative
+# step count will drive the motor in the oposite direction to a
+# positive count. The count represents "half steps" since the driver
+# alternates driving single coils and driving pairs of adjacent coils.
+# Calls to run() return immediately; the motor runs on a timer in the
+# background. Calling run() again before the previous command has
+# finished adds the new count to the old count, so the destination
+# position is the sum of the requests; the delay is set to the new
+# value if stepper is not already at its final location.
+#
+# The stop() method will stop the rotation of the motor. It returns
+# the number of un-taken steps that would be needed to perform the
+# outstanding requests from previous calls to run().
+#
+# The is_running property returns true if the motor is running,
+# i.e. stop() would return a non-zero value, and false otherwise.
+
+import machine
+import time
+
+# When the following number is sampled at four consecutive
+# even-numbered bits it will have two bits set, but sampling at four
+# consecutive odd-numbered bits will only yield one bit set.
+# 😌🥲🙏🏼💩
+_WAVE_MAGIC = 0b0000011100000111
+
+class Stepper:
+    def __init__(self, A, B, C, D, T=1):
+        if not isinstance(T, machine.Timer):
+            T = machine.Timer(T)
+        self._timer = T
+        l = []
+        for p in (A, B, C, D):
+            if not isinstance(p, machine.Pin):
+                p = machine.Pin(p, machine.Pin.OUT)
+            l.append(p)
+        self._pins = l
+        self._phase = 0
+        self._stop()
+        self._run_remaining = 0
+
+    def _stop(self):
+        [p.off() for p in self._pins]
+
+    # Note: This is called on an interrupt on some platforms, so it must not use the heap
+    def _callback(self, t):
+        if self._run_remaining != 0:
+            direction = 1 if self._run_remaining > 0 else -1
+            self._phase = (self._phase + direction) % 8
+            wave = _WAVE_MAGIC >> self._phase
+            for i in range(4):
+                self._pins[i].value((wave >> (i*2)) & 1)
+            self._run_remaining -= direction
         else:
-            self.fill_vrect(x, y, w, h, color)
-
-    def fill_polygon(self, sides, x0, y0, r, color, rotate=0):
-        """Draw a filled n-sided regular polygon.
-
-        Args:
-            sides (int): Number of polygon sides.
-            x0, y0 (int): Coordinates of center point.
-            r (int): Radius.
-            color (int): RGB565 color value.
-            rotate (Optional float): Rotation in degrees relative to origin.
-        Note:
-            The center point is the center of the x0,y0 pixel.
-            Since pixels are not divisible, the radius is integer rounded
-            up to complete on a full pixel.  Therefore diameter = 2 x r + 1.
-        """
-        # Determine side coordinates
-        coords = []
-        theta = radians(rotate)
-        n = sides + 1
-        for s in range(n):
-            t = 2.0 * pi * s / sides + theta
-            coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)])
-        # Starting point
-        x1, y1 = coords[0]
-        # Minimum Maximum X dict
-        xdict = {y1: [x1, x1]}
-        # Iterate through coordinates
-        for row in coords[1:]:
-            x2, y2 = row
-            xprev, yprev = x2, y2
-            # Calculate perimeter
-            # Check for horizontal side
-            if y1 == y2:
-                if x1 > x2:
-                    x1, x2 = x2, x1
-                if y1 in xdict:
-                    xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])]
-                else:
-                    xdict[y1] = [x1, x2]
-                x1, y1 = xprev, yprev
-                continue
-            # Non horizontal side
-            # Changes in x, y
-            dx = x2 - x1
-            dy = y2 - y1
-            # Determine how steep the line is
-            is_steep = abs(dy) > abs(dx)
-            # Rotate line
-            if is_steep:
-                x1, y1 = y1, x1
-                x2, y2 = y2, x2
-            # Swap start and end points if necessary
-            if x1 > x2:
-                x1, x2 = x2, x1
-                y1, y2 = y2, y1
-            # Recalculate differentials
-            dx = x2 - x1
-            dy = y2 - y1
-            # Calculate error
-            error = dx >> 1
-            ystep = 1 if y1 < y2 else -1
-            y = y1
-            # Calcualte minimum and maximum x values
-            for x in range(x1, x2 + 1):
-                if is_steep:
-                    if x in xdict:
-                        xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])]
-                    else:
-                        xdict[x] = [y, y]
-                else:
-                    if y in xdict:
-                        xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])]
-                    else:
-                        xdict[y] = [x, x]
-                error -= abs(dy)
-                if error < 0:
-                    y += ystep
-                    error += dx
-            x1, y1 = xprev, yprev
-        # Fill polygon
-        for y, x in xdict.items():
-            self.draw_hline(x[0], y, x[1] - x[0] + 2, color)
-
-    def fill_vrect(self, x, y, w, h, color):
-        """Draw a filled rectangle (optimized for vertical drawing).
-
-        Args:
-            x (int): Starting X position.
-            y (int): Starting Y position.
-            w (int): Width of rectangle.
-            h (int): Height of rectangle.
-            color (int): RGB565 color value.
-        """
-        if self.is_off_grid(x, y, x + w - 1, y + h - 1):
-            return
-        chunk_width = 1024 // h
-        chunk_count, remainder = divmod(w, chunk_width)
-        chunk_size = chunk_width * h
-        chunk_x = x
-        if chunk_count:
-            buf = color.to_bytes(2, 'big') * chunk_size
-            for c in range(0, chunk_count):
-                self.block(chunk_x, y,
-                           chunk_x + chunk_width - 1, y + h - 1,
-                           buf)
-                chunk_x += chunk_width
-
-        if remainder:
-            buf = color.to_bytes(2, 'big') * remainder * h
-            self.block(chunk_x, y,
-                       chunk_x + remainder - 1, y + h - 1,
-                       buf)
-
-    def is_off_grid(self, xmin, ymin, xmax, ymax):
-        """Check if coordinates extend past display boundaries.
-
-        Args:
-            xmin (int): Minimum horizontal pixel.
-            ymin (int): Minimum vertical pixel.
-            xmax (int): Maximum horizontal pixel.
-            ymax (int): Maximum vertical pixel.
-        Returns:
-            boolean: False = Coordinates OK, True = Error.
-        """
-        if xmin < 0:
-            print('x-coordinate: {0} below minimum of 0.'.format(xmin))
-            return True
-        if ymin < 0:
-            print('y-coordinate: {0} below minimum of 0.'.format(ymin))
-            return True
-        if xmax >= self.width:
-            print('x-coordinate: {0} above maximum of {1}.'.format(
-                xmax, self.width - 1))
-            return True
-        if ymax >= self.height:
-            print('y-coordinate: {0} above maximum of {1}.'.format(
-                ymax, self.height - 1))
-            return True
-        return False
-
-    def load_sprite(self, path, w, h):
-        """Load sprite image.
-
-        Args:
-            path (string): Image file path.
-            w (int): Width of image.
-            h (int): Height of image.
-        Notes:
-            w x h cannot exceed 2048
-        """
-        buf_size = w * h * 2
-        with open(path, "rb") as f:
-            return f.read(buf_size)
-
-    def reset_cpy(self):
-        """Perform reset: Low=initialization, High=normal operation.
-
-        Notes: CircuitPython implemntation
-        """
-        self.rst.value = False
-        sleep(.05)
-        self.rst.value = True
-        sleep(.05)
-
-    def reset_mpy(self):
-        """Perform reset: Low=initialization, High=normal operation.
-
-        Notes: MicroPython implemntation
-        """
-        self.rst(0)
-        sleep(.05)
-        self.rst(1)
-        sleep(.05)
-
-    def scroll(self, y):
-        """Scroll display vertically.
-
-        Args:
-            y (int): Number of pixels to scroll display.
-        """
-        self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF)
-
-    def set_scroll(self, top, bottom):
-        """Set the height of the top and bottom scroll margins.
-
-        Args:
-            top (int): Height of top scroll margin
-            bottom (int): Height of bottom scroll margin
-        """
-        if top + bottom <= self.height:
-            middle = self.height - (top + bottom)
-            print(top, middle, bottom)
-            self.write_cmd(self.VSCRDEF,
-                           top >> 8,
-                           top & 0xFF,
-                           middle >> 8,
-                           middle & 0xFF,
-                           bottom >> 8,
-                           bottom & 0xFF)
-
-    def sleep(self, enable=True):
-        """Enters or exits sleep mode.
-
-        Args:
-            enable (bool): True (default)=Enter sleep mode, False=Exit sleep
-        """
-        if enable:
-            self.write_cmd(self.SLPIN)
+            self._timer.deinit()
+            self._stop()
+
+    def run(self, count, delay=0.001):
+        tick_hz=1000000
+        period = int(delay*tick_hz)
+        if period < 500:
+            period = 500
+        self._run_remaining += count
+        if self._run_remaining != 0:
+            self._timer.init(period=period, tick_hz=tick_hz,
+                             mode=machine.Timer.PERIODIC, callback=self._callback)
         else:
-            self.write_cmd(self.SLPOUT)
-
-
-    def write_cmd_mpy(self, command, *args):
-        """Write command to OLED (MicroPython).
-
-        Args:
-            command (byte): ILI9341 command code.
-            *args (optional bytes): Data to transmit.
-        """
-        self.dc(0)
-        self.cs(0)
-        self.spi.write(bytearray([command]))
-        self.cs(1)
-        # Handle any passed data
-        if len(args) > 0:
-            self.write_data(bytearray(args))
-
-    def write_cmd_cpy(self, command, *args):
-        """Write command to OLED (CircuitPython).
-
-        Args:
-            command (byte): ILI9341 command code.
-            *args (optional bytes): Data to transmit.
-        """
-        self.dc.value = False
-        self.cs.value = False
-        # Confirm SPI locked before writing
-        while not self.spi.try_lock():
-            pass
-        self.spi.write(bytearray([command]))
-        self.spi.unlock()
-        self.cs.value = True
-        # Handle any passed data
-        if len(args) > 0:
-            self.write_data(bytearray(args))
-
-    def write_data_mpy(self, data):
-        """Write data to OLED (MicroPython).
-
-        Args:
-            data (bytes): Data to transmit.
-        """
-        self.dc(1)
-        self.cs(0)
-        self.spi.write(data)
-        self.cs(1)
-
-    def write_data_cpy(self, data):
-        """Write data to OLED (CircuitPython).
-
-        Args:
-            data (bytes): Data to transmit.
-        """
-        self.dc.value = True
-        self.cs.value = False
-        # Confirm SPI locked before writing
-        while not self.spi.try_lock():
-            pass
-        self.spi.write(data)
-        self.spi.unlock()
-        self.cs.value = True
-
-
-#xxx
\ No newline at end of file
+            self._timer.deinit()
+            self._stop()
+
+    def stop(self):
+        remaining = self._run_remaining
+        self._run_remaining = 0
+        self._timer.deinit()
+        self._stop()
+        return remaining
+
+    @property
+    def is_running(self):
+        return self._run_remaining != 0
diff --git a/examples/test.py b/examples/test.py
index d130bc9..a110c7e 100644
--- a/examples/test.py
+++ b/examples/test.py
@@ -1,14 +1,10 @@
 """
 Test
 """
-
+# 🙏🏼
 from time import sleep
-from machine import Pin
-pin = Pin(2, Pin.OUT)
+
 print("start OK \r\n")
 for i in range(0, 10):
-  print('duh')
-  pin.on()
-  sleep(0.1)
-  pin.off()
+  print('...')
   sleep(0.1)
diff --git a/examples/test_out b/examples/test_out
new file mode 100644
index 0000000..2ccd7e4
--- /dev/null
+++ b/examples/test_out
@@ -0,0 +1,12 @@
+start OK 
+
+...
+...
+...
+...
+...
+...
+...
+...
+...
+...
diff --git a/micropython.js b/micropython.js
index 5916aec..5bec72b 100644
--- a/micropython.js
+++ b/micropython.js
@@ -25,6 +25,12 @@ function extract(out) {
   return out.slice(2, -3)
 }
 
+function extractBytes(out, cut_before = 2, cut_after = 3) {
+  bytes = out.slice(cut_before, -cut_after)
+  bytes = bytes.split(',').map(Number)
+  return bytes
+}
+
 class MicroPythonBoard {
   constructor() {
     this.port = null
@@ -69,7 +75,7 @@ class MicroPythonBoard {
   }
 
   close() {
-    if (this.serial.isOpen) {
+    if (this.serial && this.serial.isOpen) {
       return this.serial.close()
     } else {
       return Promise.resolve()
@@ -204,6 +210,7 @@ class MicroPythonBoard {
         command += `  print(1)\n`
         command += `except OSError:\n`
         command += `  print(0)\n`
+        command += `del f\n`
     await this.enter_raw_repl()
     let output = await this.exec_raw(command)
     await this.exit_raw_repl()
@@ -234,11 +241,13 @@ class MicroPythonBoard {
     let command = `import uos\n`
         command += `try:\n`
         command += `  l=[]\n`
-        command += `  for file in uos.ilistdir("${folderPath}"):\n`
-        command += `    l.append(list(file))\n`
+        command += `  for f in uos.ilistdir("${folderPath}"):\n`
+        command += `    l.append(list(f))\n`
         command += `  print(l)\n`
         command += `except OSError:\n`
         command += `  print([])\n`
+        command += `del l\n`
+        command += `del f\n`
     await this.enter_raw_repl()
     let output = await this.exec_raw(command)
     await this.exit_raw_repl()
@@ -250,11 +259,31 @@ class MicroPythonBoard {
     return Promise.resolve(files)
   }
 
+  async fs_cat_binary(filePath) {
+    if (filePath) {
+      await this.enter_raw_repl()
+      const chunkSize = 256
+      let command =  `with open('${filePath}','rb') as f:\n`
+          command += `  while 1:\n`
+          command += `    b=f.read(${chunkSize})\n`
+          command += `    if not b:break\n`
+          command += `    print(",".join(str(e) for e in b),end=',')\n`
+          command += `del f\n`
+          command += `del b\n`
+      let output = await this.exec_raw(command)
+      
+      await this.exit_raw_repl()
+      output = extractBytes(output, 2, 4)
+      return Promise.resolve((output))
+    }
+    return Promise.reject(new Error(`Path to file was not specified`))
+  }
+
   async fs_cat(filePath) {
     if (filePath) {
       await this.enter_raw_repl()
       let output = await this.exec_raw(
-        `with open('${filePath}','r') as f:\n while 1:\n  b=f.read(256)\n  if not b:break\n  print(b,end='')`
+        `with open('${filePath}','r') as f:\n while 1:\n  b=f.read(256)\n  if not b:break\n  print(b,end='')\ndel f\ndel b\n`
       )
       await this.exit_raw_repl()
       output = extract(output)
@@ -266,24 +295,19 @@ class MicroPythonBoard {
   async fs_put(src, dest, data_consumer) {
     data_consumer = data_consumer || function() {}
     if (src && dest) {
-      const contentBuffer = fs.readFileSync(path.resolve(src))
-      let contentString = contentBuffer.toString()
-      contentString = fixLineBreak(contentString)
-      const hexArray = contentString.split('').map(
-        c => c.charCodeAt(0).toString(16).padStart(2, '0')
-      )
+      const fileContent = fs.readFileSync(path.resolve(src), 'binary')
+      const contentBuffer =  Buffer.from(fileContent, 'binary')
       let out = ''
       out += await this.enter_raw_repl()
-      out += await this.exec_raw(`f=open('${dest}','w')\nw=f.write`)
+      out += await this.exec_raw(`f=open('${dest}','wb')\nw=f.write`)
       const chunkSize = 48
-      for (let i = 0; i < hexArray.length; i+= chunkSize) {
-        let slice = hexArray.slice(i, i+chunkSize)
-        let bytes = slice.map(h => `0x${h}`)
-        let line = `w(bytes([${bytes.join(',')}]))`
+      for (let i = 0; i < contentBuffer.length; i+= chunkSize) {
+        let slice = Uint8Array.from(contentBuffer.subarray(i, i+chunkSize))
+        let line = `w(bytes([${slice}]))`
         out += await this.exec_raw(line)
-        data_consumer( parseInt((i / hexArray.length) * 100) + '%')
+        data_consumer( parseInt((i / contentBuffer.length) * 100) + '%')
       }
-      out += await this.exec_raw(`f.close()`)
+      out += await this.exec_raw(`f.close()\ndel f\ndel w\n`)
       out += await this.exit_raw_repl()
       return Promise.resolve(out)
     }
@@ -293,22 +317,18 @@ class MicroPythonBoard {
   async fs_save(content, dest, data_consumer) {
     data_consumer = data_consumer || function() {}
     if (content && dest) {
-      let contentString = fixLineBreak(content)
-      const hexArray = contentString.split('').map(
-        c => c.charCodeAt(0).toString(16).padStart(2, '0')
-      )
+      const contentBuffer = Buffer.from(content, 'utf-8')
       let out = ''
       out += await this.enter_raw_repl()
-      out += await this.exec_raw(`f=open('${dest}','w')\nw=f.write`)
+      out += await this.exec_raw(`f=open('${dest}','wb')\nw=f.write`)
       const chunkSize = 48
-      for (let i = 0; i < hexArray.length; i+= chunkSize) {
-        let slice = hexArray.slice(i, i+chunkSize)
-        let bytes = slice.map(h => `0x${h}`)
-        let line = `w(bytes([${bytes.join(',')}]))`
+      for (let i = 0; i < contentBuffer.length; i+= chunkSize) {
+        let slice = Uint8Array.from(contentBuffer.subarray(i, i+chunkSize))
+        let line = `w(bytes([${slice}]))`
         out += await this.exec_raw(line)
-        data_consumer( parseInt((i / hexArray.length) * 100) + '%')
+        data_consumer( parseInt((i / contentBuffer.length) * 100) + '%')
       }
-      out += await this.exec_raw(`f.close()`)
+      out += await this.exec_raw(`f.close()\ndel f\ndel w\n`)
       out += await this.exit_raw_repl()
       return Promise.resolve(out)
     } else {
diff --git a/package-lock.json b/package-lock.json
index 08f69b3..b5256b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "micropython.js",
-  "version": "1.4.2",
+  "version": "1.5.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "micropython.js",
-      "version": "1.4.2",
+      "version": "1.5.1",
       "dependencies": {
         "serialport": "^10.4.0"
       },
diff --git a/package.json b/package.json
index adcbe36..62cae7b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "micropython.js",
-  "version": "1.4.2",
+  "version": "1.5.1",
   "description": "Interpretation of pyboard.py in javascript",
   "main": "micropython.js",
   "scripts": {
diff --git a/test.js b/test.js
index dc6f76e..7cb694c 100644
--- a/test.js
+++ b/test.js
@@ -1,6 +1,7 @@
 const Board = require('./micropython.js')
-
-let out
+const assert = require('assert')
+const fs = require('fs')
+const path = require('path')
 
 function sleep(millis) {
   return new Promise((resolve, reject) => {
@@ -10,88 +11,251 @@ function sleep(millis) {
   })
 }
 
-
-async function main() {
+async function before(port) {
   const board = new Board()
-  await board.open(process.env.PORT || '/dev/ttyACM0')
-  console.log('connected')
-
-  await board.get_prompt()
-  console.log('has prompt')
-
-  const fn = async () =>  {
-    let o = await board.serial.read()
-    console.log('DATA', o.toString())
-  }
-  board.serial.on('readable', fn)
-  await board.eval('pri')
-  await sleep(10)
-  await board.eval('nt(1')
-  await sleep(10)
-  await board.eval('23)')
-  await board.eval('\r')
-  await sleep(10)
-  board.serial.removeListener('readable', fn)
-
-  await board.enter_raw_repl()
-  console.log('in raw repl')
-
-  const code = `from time import sleep\nfor i in range(0, 10):\n  print('.')\n  sleep(0.1)\n`
-  let i = 0
-  out = await board.exec_raw(code, async (d) => {
-    console.log('->', d)
-    // i += 1; if (i > 3) await board.stop()
-  })
-  console.log('executed', out)
-
-  await board.exit_raw_repl()
-  console.log('out raw repl')
-
-  out = await board.fs_exists('boot.py')
-  console.log('boot.py exists', out)
-  out = await board.fs_exists('this_is_not_a_file.py')
-  console.log('nope.py exists', out)
+  await board.open(port)
+  return Promise.resolve(board)
+}
 
-  out = await board.fs_ls('./')
-  console.log('root files', out)
-  out = await board.fs_ls('./lib')
-  console.log('lib files', out)
+async function after(board) {
+  await board.close()
+  return Promise.resolve()
+}
 
-  out = await board.fs_ils('./')
-  console.log('root files', out)
-  out = await board.fs_ils('./lib')
-  console.log('lib files', out)
+const testCases = {
+  "get prompt": async (board) => {
+    // From unknown state
+    let output = await board.get_prompt()
+    assert.notEqual(output.indexOf('>>>'), -1)
+    // From raw repl
+    await board.enter_raw_repl()
+    output = await board.get_prompt()
+    assert.notEqual(output.indexOf('>>>'), -1)
+    // Running code
+    board.run(`from time import sleep\nwhile True:\n  print('.')\nsleep(1)\n`)
+      .catch(() => null)
+    output = await board.get_prompt()
+    assert.notEqual(output.indexOf('>>>'), -1)
 
-  out = await board.fs_put(
-    './examples/test.py', 'test.py', (d) => console.log('progress', d)
-  )
-  console.log('send file to board', out)
+    return Promise.resolve()
+  },
+  "real time repl": async (board) => {
+    let output = ''
+    const fn = async () =>  {
+      let o = await board.serial.read()
+      output += o.toString()
+    }
+    board.serial.on('readable', fn)
+    await board.eval('pri')
+    await sleep(10)
+    await board.eval('nt(1')
+    await sleep(10)
+    await board.eval('23)')
+    await board.eval('\r')
+    await sleep(10)
+    board.serial.removeListener('readable', fn)
+    assert(output, 'print(123)\r\n123\r\n>>> ')
+    return Promise.resolve()
+  },
+  "enter raw repl": async (board) => {
+    let output = await board.enter_raw_repl()
+    assert.notEqual(output.indexOf('raw REPL; CTRL-B to exit'), -1)
+    // reenter raw repl
+    output = await board.enter_raw_repl()
+    assert.notEqual(output.indexOf('raw REPL; CTRL-B to exit'), -1)
 
-  out = await board.fs_cat('test.py')
-  console.log('get test.py content', out)
+    return Promise.resolve()
+  },
+  "exit raw repl": async (board) => {
+    let output = await board.exit_raw_repl()
+    assert.notEqual(output.indexOf('>>>'), -1)
 
-  out = await board.fs_save(
-    '# overrides test file', 'test.py', (d) => console.log('progress', d)
-  )
-  console.log('save test.py content', out)
+    await board.enter_raw_repl()
+    output = await board.exit_raw_repl()
+    assert.notEqual(output.indexOf('>>>'), -1)
 
-  out = await board.fs_cat('test.py')
-  console.log('get test.py content', out)
+    return Promise.resolve()
+  },
+  "execute raw small": async (board) => {
+    await board.enter_raw_repl()
+    const output = await board.exec_raw('print(123)')
+    await board.exit_raw_repl()
+    assert.equal(output, 'OK123\r\n\x04\x04>')
+    return Promise.resolve()
+  },
+  "execute raw big": async (board) => {
+    let bigFile = fs.readFileSync('./examples/big_file.py')
+    await board.enter_raw_repl()
+    const output = await board.exec_raw(bigFile.toString())
+    assert.equal(output, 'OK\x04\x04>')
+    await board.exit_raw_repl()
+    return Promise.resolve()
+  },
+  "run small code": async (board) => {
+    const output = await board.run('print(123)')
+    assert.equal(output, 'OK123\r\n\x04\x04>')
+    return Promise.resolve()
+  },
+  "run big code": async (board) => {
+    let bigFile = fs.readFileSync('./examples/big_file.py')
+    const output = await board.run(bigFile.toString())
+    assert.equal(output, 'OK\x04\x04>')
+    return Promise.resolve()
+  },
+  "run code after stop": async (board) => {
+    debugger
+    board.run(
+      `from time import sleep\nfor i in range(0, 10):\n  print('.')\n  sleep(1)\n`
+    ).catch(e => {
+      // console.log('stopped')
+    })
+    await (new Promise((r) => setTimeout(r, 100)))
+    await board.stop()
+    await (new Promise((r) => setTimeout(r, 100)))
+    await board.get_prompt()
+    const output = await board.run('print(123)')
+    assert.equal(output, 'OK123\r\n\x04\x04>')
 
-  out = await board.fs_rm('test.py')
-  console.log('removing test.py', out)
+  },
+  "run code after run": async (board) => {
+    board.run(
+      `from time import sleep\nfor i in range(0, 10):\n  print('.')\n  sleep(1)\n`,
+      // (o) => console.log('board run outputs', o)
+    ).catch(e => {
+      // console.log('stopped')
+    })
+    await board.get_prompt()
+    const output = await board.run('print(123)')
+    assert.equal(output, 'OK123\r\n\x04\x04>')
 
-  out = await board.fs_mkdir('test_dir')
-  console.log('created test_dir', out)
-  out = await board.fs_ils()
-  console.log('files at ./', out)
+  },
+  "upload file": async (board) => {
+    const diskFilePath = path.resolve('./examples/test.py')
+    const serialFilePath = '/test.py'
+    await board.fs_put(diskFilePath, serialFilePath)
+    const diskFileContent = fs.readFileSync(diskFilePath)
+    const boardFileContent = await board.fs_cat(serialFilePath)
+    assert.equal(diskFileContent.toString(), boardFileContent)
+    await board.fs_rm(serialFilePath)
+  },
+  "upload big file": async (board) => {
+    const diskFilePath = path.resolve('./examples/big_file.py')
+    const serialFilePath = '/big_file.py'
+    await board.fs_put(
+      diskFilePath, serialFilePath,
+      (e) => console.log('uploading big file', e)
+    )
+    const diskFileContent = fs.readFileSync(diskFilePath)
+    const boardFileContent = await board.fs_cat(serialFilePath)
+    assert.equal(diskFileContent.toString(), boardFileContent)
+    await board.fs_rm(serialFilePath)
+  },
+  "create folder": async (board) => {
+    const folderPath = '/test_folder'
+    await board.fs_mkdir(folderPath)
+    const ls = await board.fs_ils('/')
+    const folder = ls.find(f => f[0] === 'test_folder' && f[1] === 16384)
+    assert.ok(folder)
+    await board.fs_rmdir(folderPath)
+  },
+  "list files and folders": async (board) => {
+    const file = [ 'test.py', 32768 ]
+    const test_folder = 'test_folder'
+    await board.fs_put(path.resolve('examples', file[0]), '/'+file[0])
+    await board.fs_mkdir('/'+test_folder)
+    const ls = await board.fs_ils('/')
+    const createdFile = ls.find(f => f[0] === file[0] && f[1] === file[1])
+    const createdFolder = ls.find(f => f[0] === test_folder && f[1] === 16384)
+    assert.ok(createdFile)
+    assert.ok(createdFolder)
+  },
+  "check if file exists": async (board) => {
+    const filePath = '/test_exist'+parseInt(Math.random()*99999)
+    await board.fs_save('.', filePath)
+    const fileExists = await board.fs_exists(filePath)
+    assert(fileExists)
+    const fileDoesNotExist = await board.fs_exists('/xxx'+parseInt(Math.random()*99999))
+    assert.ok(!fileDoesNotExist)
+    await board.fs_rm(filePath)
+  },
+  "save file content": async (board) => {
+      const filePath = '/test.py'
+      const content = `.`
+      await board.fs_save(content, filePath)
+      const boardContent = await board.fs_cat(filePath)
+      assert.equal(content, boardContent)
+  },
+  "save big file content": async (board) => {
+    const filePath = '/test.py'
+    const content = fs.readFileSync(path.resolve('./examples/big_file.py'))
+    await board.fs_save(content.toString(), filePath)
+    const boardContent = await board.fs_cat(filePath)
+    assert.equal(content, boardContent)
+  },
+  "get file": async (board) => {
+    const filePath = '/test.py'
+    const content = `.`
+    await board.fs_save(content, filePath)
+    const boardContent = await board.fs_cat(filePath)
+    assert.equal(content, boardContent)
+  },
+  "remove file": async (board) => {
+    const filePath = '/test_remove'+parseInt(Math.random()*99999)
+    await board.fs_save('.', filePath)
+    const fileExists = await board.fs_exists(filePath)
+    assert(fileExists)
+    await board.fs_rm(filePath)
+    const fileDoesNotExist = await board.fs_exists(filePath)
+    assert.ok(!fileDoesNotExist)
+  },
+  "remove folder": async (board) => {
+    const folderPath = '/test_remove'+parseInt(Math.random()*99999)
+    await board.fs_mkdir(folderPath)
+    let ls = await board.fs_ils('/')
+    const foundFolder = ls.find(f => f[0] === folderPath.slice(1) && f[1] === 16384)
+    assert.ok(foundFolder)
+    await board.fs_rmdir(folderPath)
+    ls = await board.fs_ils('/')
+    const notFoundFolder = ls.find(f => f[0] === folderPath.slice(1) && f[1] === 16384)
+    assert.ok(!notFoundFolder)
+  },
+  // "foo": async (board) => Promise.reject()
+}
 
-  out = await board.fs_rmdir('test_dir')
-  console.log('removed test_dir', out)
-  out = await board.fs_ils()
-  console.log('files at ./', out)
+// SKIP LONG RUNNERS
+delete testCases['execute raw big']
+delete testCases['run big code']
+delete testCases['upload big file']
+delete testCases['save big file content']
 
-  board.close()
+async function main() {
+  let errors = []
+  const board = new Board()
+  let ports = await board.list_ports()
+  ports = ports.filter(p => p.vendorId && p.productId).map(p => p.path)
+  for (port of ports) {
+    console.log('')
+    console.log('🔊 Running test for', port)
+    for (const [name, test] of Object.entries(testCases)) {
+      console.log('>', name)
+      const board = await before(port)
+      try {
+        const result = await test(board)
+        console.log("🟩 success")
+      } catch(e) {
+        console.log('💔 error', e)
+        errors.push([port, name])
+      }
+      await after(board)
+    }
+  }
+  console.log('')
+  console.log('Errors:', errors.length)
+  if (errors.length > 0) {
+    for (const [port, name] of errors) {
+      console.log(name, 'failed on', port)
+    }
+  }
 }
 
 main()
