Last active
October 28, 2025 15:07
-
-
Save kdmukai/4b4032c5e84a89cb9dc393d5a9820575 to your computer and use it in GitHub Desktop.
Various utility functions to render calibration patterns to an ST7789 when running in a REPL on the Pi Zero
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Copy-paste of various mods to the ST7789 driver to facilitate testing. | |
| """ | |
| # Gamma curve constants for ST7789 display | |
| # Each tuple contains (positive_gamma_values, negative_gamma_values) | |
| GAMMA_CURVES = [ | |
| # Original SeedSigner gamma curve for the Waveshare display hat | |
| ( | |
| [0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23], | |
| [0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23] | |
| ), | |
| # # Brighter gamma curve | |
| # ( | |
| # [0xE0, 0x08, 0x10, 0x15, 0x17, 0x30, 0x45, 0x60, 0x50, 0x20, 0x10, 0x0E, 0x25, 0x28], | |
| # [0xE0, 0x08, 0x0F, 0x15, 0x17, 0x31, 0x45, 0x50, 0x58, 0x35, 0x25, 0x25, 0x26, 0x28] | |
| # ), | |
| ( | |
| [0xC0, 0x02, 0x08, 0x0D, 0x0F, 0x26, 0x39, 0x48, 0x40, 0x15, 0x08, 0x06, 0x18, 0x1E], | |
| [0xC0, 0x02, 0x07, 0x0D, 0x0F, 0x27, 0x39, 0x38, 0x45, 0x28, 0x18, 0x18, 0x19, 0x1E] | |
| ), | |
| # Modified: 20% darker shadows, 20% brighter highlights (first/last unchanged) | |
| ( | |
| [0xC0, 0x02, int(0x08*0.8), int(0x0D*0.8), int(0x0F*0.8), int(0x26*0.8), int(0x39*0.8), int(0x48*1.2), int(0x40*1.2), int(0x15*1.2), int(0x08*1.2), int(0x06*1.2), int(0x18*1.2), 0x1E], | |
| [0xC0, 0x02, int(0x07*0.8), int(0x0D*0.8), int(0x0F*0.8), int(0x27*0.8), int(0x39*0.8), int(0x38*1.2), int(0x45*1.2), int(0x28*1.2), int(0x18*1.2), int(0x18*1.2), int(0x19*1.2), 0x1E] | |
| ), | |
| # 20% darker | |
| ( | |
| [0xC0, 0x02, int(0x08*0.8*0.8), int(0x0D*0.8*0.8), int(0x0F*0.8*0.8), int(0x26*0.8*0.8), int(0x39*0.8*0.8), int(0x48*1.2*0.8), int(0x40*1.2*0.8), int(0x15*1.2*0.8), int(0x08*1.2*0.8), int(0x06*1.2*0.8), int(0x18*1.2*0.8), 0x1E], | |
| [0xC0, 0x02, int(0x07*0.8*0.8), int(0x0D*0.8*0.8), int(0x0F*0.8*0.8), int(0x27*0.8*0.8), int(0x39*0.8*0.8), int(0x38*1.2*0.8), int(0x45*1.2*0.8), int(0x28*1.2*0.8), int(0x18*1.2*0.8), int(0x18*1.2*0.8), int(0x19*1.2*0.8), 0x1E] | |
| ), | |
| # ST7789_mpy curve | |
| ( | |
| [0xD0, 0x00, 0x02, 0x07, 0x0A, 0x28, 0x32, 0x44, 0x42, 0x06, 0x0E, 0x12, 0x14, 0x17], | |
| [0xD0, 0x00, 0x02, 0x07, 0x0A, 0x28, 0x31, 0x54, 0x47, 0x0E, 0x1C, 0x17, 0x1B, 0x1E] | |
| ) | |
| ] | |
| # in the ST7789 class... | |
| def set_gamma_curve(self, curve_num: int): | |
| """Set gamma curve based on predefined options""" | |
| # Validate curve number and default to 0 if invalid | |
| if curve_num < 0 or curve_num >= len(GAMMA_CURVES): | |
| raise BaseDisplayDriver.InvalidGammaCurveNumber(f"{curve_num} > {len(GAMMA_CURVES)-1}") | |
| positive_gamma, negative_gamma = GAMMA_CURVES[curve_num] | |
| self._apply_gamma_values(positive_gamma, negative_gamma) | |
| @property | |
| def num_gamma_curves(self): | |
| """Return the number of available gamma curves""" | |
| return len(GAMMA_CURVES) | |
| def _apply_gamma_values(self, positive_gamma, negative_gamma): | |
| """Apply gamma correction values to the display""" | |
| # Apply positive gamma correction (0xE0) | |
| self.command(0xE0) | |
| for val in positive_gamma: | |
| self.data(val) | |
| # Apply negative gamma correction (0xE1) | |
| self.command(0xE1) | |
| for val in negative_gamma: | |
| self.data(val) | |
| def set_built_in_gamma(self, curve_num: int): | |
| # Change built-in gamma setting | |
| self.command(0x26) # GAMSET | |
| curves = [ | |
| 0x01, # Gamma curve 0 | |
| 0x02, # Gamma curve 1 | |
| 0x04, # Gamma curve 2 | |
| 0x08 # Gamma curve 3 | |
| ] | |
| self.data(curves[curve_num]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Recommend installing ipython on your Pi Zero dev hardware: `pip install ipython` | |
| These functions are meant to be pasted into the REPL and called to paint various | |
| calibration patterns on the ST7789 display. | |
| You can then make calls to change the gamma curve, pwm backlight, etc and see | |
| how those changes affect the onscreen calibration pattern. | |
| This code depends on: https://github.com/SeedSigner/seedsigner/pull/821 | |
| In my dev branch I've modified the ST7789 and ST7789_mpy drivers with functions | |
| to directly set different user-defined gamma curves (0xE0, 0xE1) as well as the | |
| built-in gamma curves (0x26 aka "GAMSET"). | |
| If you add those functions into the drivers, you can directly access them via: | |
| renderer = Renderer.get_instance() | |
| disp = renderer.disp | |
| disp.set_gamma_curve(1) | |
| disp.set_built_in_gamma(0) | |
| """ | |
| from seedsigner.gui.renderer import Renderer | |
| Renderer.configure_instance() | |
| renderer = Renderer.get_instance() | |
| BACKGROUND_COLOR = "#000000" | |
| INACTIVE_COLOR = "#414141" | |
| ACCENT_COLOR = "#FF9F0A" # Active Color | |
| WARNING_COLOR = "#FFD60A" | |
| DIRE_WARNING_COLOR = "#FF5700" | |
| ERROR_COLOR = "#FF1B0A" | |
| SUCCESS_COLOR = "#30D158" | |
| INFO_COLOR = "#409CFF" | |
| BITCOIN_ORANGE = "#FF9416" | |
| TESTNET_COLOR = "#00F100" | |
| REGTEST_COLOR = "#00CAF1" | |
| GREEN_INDICATOR_COLOR = "#00FF00" | |
| def display_blank_screen(): | |
| renderer = Renderer.get_instance() | |
| renderer.draw.rectangle((0, 0, renderer.canvas_width, renderer.canvas_height), outline=0, fill=0) | |
| renderer.show_image(renderer.canvas, 0, 0) | |
| def render_seedsigner_colors(): | |
| COLORS = [ | |
| ("BACKGROUND_COLOR", "#000000"), | |
| ("INACTIVE_COLOR", "#414141"), | |
| ("ACCENT_COLOR", "#FF9F0A"), # Active Color | |
| ("WARNING_COLOR", "#FFD60A"), | |
| ("DIRE_WARNING_COLOR", "#FF5700"), | |
| ("ERROR_COLOR", "#FF1B0A"), | |
| ("SUCCESS_COLOR", "#30D158"), | |
| ("INFO_COLOR", "#409CFF"), | |
| ("BITCOIN_ORANGE", "#FF9416"), | |
| ("TESTNET_COLOR", "#00F100"), | |
| ("REGTEST_COLOR", "#00CAF1"), | |
| ("GREEN_INDICATOR_COLOR", "#00FF00"), | |
| ] | |
| display_blank_screen() | |
| renderer = Renderer.get_instance() | |
| for i in range(0, len(COLORS), 2): | |
| swatch_height = 40 | |
| cur_y = i / 2 * 40 | |
| renderer.draw.rectangle((0, cur_y, renderer.canvas_width / 2, cur_y + swatch_height), outline=0, fill=COLORS[i][1]) | |
| renderer.draw.rectangle((renderer.canvas_width / 2, cur_y, renderer.canvas_width, cur_y + swatch_height), outline=0, fill=COLORS[i+1][1]) | |
| renderer.show_image(renderer.canvas, 0, 0) | |
| def render_grayscale(): | |
| display_blank_screen() | |
| renderer = Renderer.get_instance() | |
| num_swatches = 16 | |
| swatch_width = renderer.canvas_width // num_swatches | |
| swatch_height = renderer.canvas_height // 2 | |
| # First row: dark to light (left to right) | |
| for i in range(num_swatches): | |
| x0 = i * swatch_width | |
| x1 = (i + 1) * swatch_width if i < num_swatches - 1 else renderer.canvas_width | |
| gray = int(round(i * 255 / (num_swatches - 1))) if num_swatches > 1 else 0 | |
| renderer.draw.rectangle((x0, 0, x1, swatch_height), outline=0, fill=(gray, gray, gray)) | |
| # Second row: light to dark (left to right, reverse order) | |
| for i in range(num_swatches): | |
| x0 = i * swatch_width | |
| x1 = (i + 1) * swatch_width if i < num_swatches - 1 else renderer.canvas_width | |
| # Reverse the gray value calculation | |
| gray = int(round((num_swatches - 1 - i) * 255 / (num_swatches - 1))) if num_swatches > 1 else 255 | |
| renderer.draw.rectangle((x0, swatch_height, x1, renderer.canvas_height), outline=0, fill=(gray, gray, gray)) | |
| renderer.show_image(renderer.canvas, 0, 0) | |
| def render_rgb_six_rows(): | |
| display_blank_screen() | |
| renderer = Renderer.get_instance() | |
| num_swatches = 16 | |
| swatch_width = renderer.canvas_width // num_swatches | |
| row_height = renderer.canvas_height // 6 | |
| channels = ('red', 'green', 'blue') | |
| for ch_index, ch in enumerate(channels): | |
| # Two rows per channel: forward (dark->light) then reverse (light->dark) | |
| for dir_index in range(2): | |
| row_y = (ch_index * 2 + dir_index) * row_height | |
| for i in range(num_swatches): | |
| x0 = i * swatch_width | |
| x1 = (i + 1) * swatch_width if i < num_swatches - 1 else renderer.canvas_width | |
| if num_swatches > 1: | |
| if dir_index == 0: | |
| val = int(round(i * 255 / (num_swatches - 1))) | |
| else: | |
| val = int(round((num_swatches - 1 - i) * 255 / (num_swatches - 1))) | |
| else: | |
| val = 0 if dir_index == 0 else 255 | |
| if ch == 'red': | |
| color = (val, 0, 0) | |
| elif ch == 'green': | |
| color = (0, val, 0) | |
| else: # blue | |
| color = (0, 0, val) | |
| renderer.draw.rectangle((x0, row_y, x1, row_y + row_height), outline=0, fill=color) | |
| renderer.show_image(renderer.canvas, 0, 0) | |
| def fill_screen(color): | |
| """Fill the entire display with a single color. | |
| color may be a hex string like '#RRGGBB' or '#RGB', an (r,g,b) tuple/list, or an int grayscale value. | |
| """ | |
| renderer = Renderer.get_instance() | |
| # Normalize color | |
| if isinstance(color, str): | |
| s = color.lstrip('#') | |
| if len(s) == 3: | |
| s = ''.join(ch * 2 for ch in s) | |
| if len(s) == 6: | |
| try: | |
| rgb = tuple(int(s[i:i+2], 16) for i in (0, 2, 4)) | |
| except ValueError: | |
| raise ValueError("Invalid hex color string") | |
| else: | |
| # Let PIL handle named colors or other string forms | |
| rgb = color | |
| elif isinstance(color, (list, tuple)): | |
| if len(color) == 3: | |
| rgb = tuple(int(max(0, min(255, v))) for v in color) | |
| else: | |
| raise ValueError("Color tuple/list must have 3 elements") | |
| elif isinstance(color, int): | |
| v = int(max(0, min(255, color))) | |
| rgb = (v, v, v) | |
| else: | |
| raise ValueError("Unsupported color format") | |
| renderer.draw.rectangle((0, 0, renderer.canvas_width, renderer.canvas_height), outline=0, fill=rgb) | |
| renderer.show_image(renderer.canvas, 0, 0) | |
| def render_button_list_screen(): | |
| """ | |
| Invoking the ButtonListScreen directly works (surprisingly!) but might need a joystick input | |
| to repaint the screen. Also need to click a button to return control to the REPL. | |
| """ | |
| from seedsigner.gui.screens.screen import ButtonListScreen, ButtonOption | |
| from seedsigner.gui.components import FontAwesomeIconConstants | |
| IMAGE = ButtonOption("New seed", FontAwesomeIconConstants.CAMERA) | |
| DICE = ButtonOption("New seed", FontAwesomeIconConstants.DICE) | |
| KEYBOARD = ButtonOption("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) | |
| ADDRESS_EXPLORER = ButtonOption("Address explorer") | |
| VERIFY_ADDRESS = ButtonOption("Verify address") | |
| button_data = [IMAGE, DICE, KEYBOARD, ADDRESS_EXPLORER, VERIFY_ADDRESS] | |
| ButtonListScreen( | |
| title="Tools", | |
| is_button_text_centered=False, | |
| button_data=button_data | |
| ).display() | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| SEEDSIGNER_GAMMA_CURVE = ( | |
| b'\xD0\x04\x0D\x11\x13\x2B\x3F\x54\x4C\x18\x0D\x0B\x1F\x23', | |
| b'\xD0\x04\x0C\x11\x13\x2C\x3F\x44\x51\x2F\x1F\x1F\x20\x23', | |
| ) | |
| ORIGINAL_MPY_GAMMA_CURVE = ( | |
| b'\xD0\x00\x02\x07\x0A\x28\x32\x44\x42\x06\x0E\x12\x14\x17', | |
| b'\xD0\x00\x02\x07\x0A\x28\x31\x54\x47\x0E\x1C\x17\x1B\x1E', | |
| ) | |
| GAMMA_CURVES = [ | |
| SEEDSIGNER_GAMMA_CURVE, | |
| ORIGINAL_MPY_GAMMA_CURVE, | |
| ] | |
| def set_built_in_gamma(self, curve_num: int): | |
| # Change built-in gamma setting | |
| curves = [ | |
| b'\x01', # Gamma curve 0 | |
| b'\x02', # Gamma curve 1 | |
| b'\x04', # Gamma curve 2 | |
| b'\x08' # Gamma curve 3 | |
| ] | |
| self._write(b'\x26', curves[curve_num]) | |
| def set_gamma_curve(self, curve_num: int): | |
| """Set gamma curve based on predefined options""" | |
| # Validate curve number and default to 0 if invalid | |
| if curve_num < 0 or curve_num >= len(GAMMA_CURVES): | |
| raise BaseDisplayDriver.InvalidGammaCurveNumber(f"{curve_num} > {len(GAMMA_CURVES)-1}") | |
| positive_gamma, negative_gamma = GAMMA_CURVES[curve_num] | |
| self._write(b'\xe0', positive_gamma) | |
| self._write(b'\xe1', negative_gamma) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment