Source code for pico_synth_sandbox.display

# pico_synth_sandbox/display.py
# 2023 Cooper Dalrymple - me@dcdalrymple.com
# GPL v3 License

import asyncio
from pico_synth_sandbox.tasks import Task
from pico_synth_sandbox import clamp, truncate_str, unmap_value
import math
from digitalio import DigitalInOut
from adafruit_character_lcd.character_lcd import Character_LCD_Mono

[docs]class Display(Task): """Control the connected 16x2 character display (aka *1602*). Hardware connections are abstracted and text writing and cursor management is simplified. """ def __init__(self, board): self._lcd = board.get_lcd() self._lcd.cursor = False self._lcd.text_direction = self._lcd.LEFT_TO_RIGHT self._cursor_enabled = None self._cursor_blink = None self._cursor_position = (-1,-1) self._buffer = [ [['\0' for x in range(16)] for y in range(2)], # Input Buffer [[' ' for x in range(16)] for y in range(2)] # Output Buffer ] self._needs_update = False Task.__init__(self, update_frequency=4)
[docs] def clear(self): """Remove all text from display and hide and reset cursor position """ self._lcd.clear() self.set_cursor_enabled(False) self.set_cursor_position(0, 0, True) for y in range(2): for x in range(16): self._buffer[0][y][x] = '\0' self._buffer[1][y][x] = ' ' self._needs_update = False
[docs] def write(self, value, position=(0,0), length=None, right_aligned=False): """Display a string or number on the display at the designated position. Can be truncated to a specified length and right-aligned. :param value: The message or number you would like to display. Any part of the string beyond the first 16 characters or the defined length will not be displayed. :type value: string, float :param position: Use a tuple of two 0-based integers for the column and row of that you would like to write the value to. Ie: (x,y). The column (x) should be between 0 and 15 and the row (y) should be between 0 and 1. :type position: tuple :param length: The length of the message you would like to write. By default, the length will be 16 or the length to the last column of the row from the designated x-position. :type length: int :param right_aligned: Whether or not you would like to align the data to the right padded by spaces as determined by the designated length. :type right_align: bool """ position = self._sanitize_position(position) if not length: length = 16 length = clamp(length,1,16-position[0]) if type(value) is float: value = "{:.2f}".format(value) value = truncate_str(str(value), length, right_aligned) for x in range(length): self._buffer[0][position[1]][position[0]+x] = value[x] self._needs_update = True
[docs] async def update(self, reset_cursor=True): """Write buffer to display. Must be called after any changes are made to the display for those changes to be visible. :param reset_cursor: It is required to manipulate the cursor position in order to make writes to the display. By default, the cursor is reset to the previous position if needed for other applications. If you would like to keep the cursor at it's newly written location, set this value as False. :type reset_cursor: bool """ # Exit early if no buffer updates recorded if not self._needs_update: return # Locate the end of front buffer data end = -1 for i in range(2*16-1, -1, -1): x = i % 16 y = i // 16 if self._buffer[0][y][x] != '\0' and self._buffer[0][y][x] != self._buffer[1][y][x]: end = i break if end < 0: return # No changes found # Locate the start of front buffer data and start building data start = -1 data = [] for i in range(2*16): x = i % 16 y = i // 16 if start < 0: if self._buffer[0][y][x] != '\0' and self._buffer[0][y][x] != self._buffer[1][y][x]: start = i if start-end+1 == 2*16: # Needs full buffer refresh break else: continue if self._buffer[0][y][x] != '\0': self._buffer[1][y][x] = self._buffer[0][y][x] data.append(self._buffer[1][y][x]) if i == end: # We've reached the end of new buffer data break elif x == 15: # We're at the end of a line data.append('\n') if not data: # If no data appended, needs full buffer refresh data = "\n".join(["".join(self._buffer[1][y]) for y in range(2)]) else: data = "".join(data) # Write new data to display self._lcd.cursor_position(start%16, start//16) self._lcd.message = data if reset_cursor: self._lcd.cursor_position(self._cursor_position[0], self._cursor_position[1]) # Reset input buffer for y in range(2): for x in range(16): self._buffer[0][y][x] = '\0'
def force_update(self, reset_cursor=True): asyncio.run(self.update(reset_cursor)) def _sanitize_position(self, column, row=0): if type(column) is tuple: if len(column) != 2: return (0,0) row = column[1] column = column[0] return (clamp(column, 0, 15), clamp(row, 0, 1))
[docs] def set_cursor_enabled(self, value): """Set whether or not the cursor should be displayed. :param value: The visibility of the cursor. :type value: bool """ if self._cursor_enabled != value: self._cursor_enabled = value self._lcd.cursor = value
[docs] def set_cursor_position(self, column=0, row=0, force=False): """Set the position of the cursor. :param column: The x-position or column of the cursor which should be between 0 and 15. Can use a tuple of (x,y) to set both column and row. :type column: int|tuple :param row: The y-position or row of the cursor which should be between 0 and 1. :type row: int :param force: Force the display to update the cursor position even if it hasn't changed. :type force: bool """ column, row = self._sanitize_position(column, row) if force or self._cursor_position[0] != column or self._cursor_position[1] != row: self._cursor_position = (column, row) self._lcd.cursor_position(column, row)
[docs] def show_cursor(self, column=0, row=0): """A quick method to ensure that the cursor is being displayed and set the position. Will not cause unnecessary display writes if called multiple times. :param column: The x-position or column of the cursor which should be between 0 and 15. :type column: int :param row: The y-position or row of the cursor which should be between 0 and 1. :type row: int """ self.set_cursor_enabled(True) self.set_cursor_position(column, row)
[docs] def hide_cursor(self): """A quick method to hide the cursor. """ self.set_cursor_enabled(False)
def enable_vertical_graph(self): data = [] for i in range(1, 8): data.append([0x00 for i in range(8-i)] + [0x1f for i in range(i)]) self.load_character_data(data) def enable_horizontal_graph(self): data = [] # Left to Right for i in range(1, 5): val = 0x00 for j in range(i): val |= (1<<(4-j)) data.append([val for j in range(8)]) # Right to Left for i in range(1, 5): val = 0x00 for j in range(i): val |= (1<<j) data.append([val for j in range(8)]) self.load_character_data(data) def load_character_data(self, data): for i in range(min(len(data), 8)): self._lcd.create_char(i, data[i]) def _write_graph(self, value=0.0, minimum=0.0, maximum=1.0, position=(0,0), length=1, vertical=False, centered=False): position = self._sanitize_position(position) length = clamp(length, 1, (2 if vertical else 16) - position[1 if vertical else 0]) value = unmap_value(value, minimum, maximum) segment = 1.0 / length bar = segment / (9.0 if vertical else 6.0) data = [] start = 0 if not vertical and centered: start = length//2 for i in range(0, start): if value <= segment*i+bar: char = 0xff elif value >= segment*(i+1)-bar: char = 0xfe else: char = 0x07 - int(math.floor((value - (segment*i+bar)) / bar)) data.append(chr(char)) for i in range(start, length): if value >= segment*(i+1)-bar: char = 0xff elif value <= segment*i+bar: char = 0xfe else: char = 0x00 + int(math.floor((value - (segment*i+bar)) / bar)) data.append(chr(char)) # Convert \0 to spaces for i in range(len(data)): if data[i] == '\0': data[i] = " " for i in range(length): if vertical: self._buffer[0][position[1]+(length-i-1)][position[0]] = data[i] else: self._buffer[0][position[1]][position[0]+i] = data[i] self._needs_update = True def write_vertical_graph(self, value=0.0, minimum=0.0, maximum=1.0, position=(0,0), height=1): self._write_graph(value, minimum, maximum, position, height, True, False) def write_horizontal_graph(self, value=0.0, minimum=0.0, maximum=1.0, position=(0,0), width=1, centered=False): # NOTE: If horizontally centered, length must be divisible by 2. self._write_graph(value, minimum, maximum, position, width, False, centered)