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/examples/big_file.py b/examples/big_file.py index 9df9021..f9affdf 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..62f1fe9 100644 --- a/examples/test.py +++ b/examples/test.py @@ -3,12 +3,8 @@ """ 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 e11b534..a0bb3a8 100644 --- a/micropython.js +++ b/micropython.js @@ -114,9 +114,9 @@ class MicroPythonBoard { } async get_prompt() { - await sleep(100) + await sleep(150) await this.stop() - await sleep(100) + await sleep(150) const out = await this.write_and_read_until(`\r\x03\x02`, '\r\n>>>') return Promise.resolve(out) } 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()