From 713290794e3e2c4f381d46a07c086ff7e29607a1 Mon Sep 17 00:00:00 2001 From: Anomalocaridid <29845794+Anomalocaridid@users.noreply.github.com> Date: Mon, 28 Aug 2023 08:51:36 +0000 Subject: [PATCH] Add SH1106 display support (#846) * add support for sh1106 oled displays * replace 'oled' with 'display' --------- Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com> --- docs/en/{OLED_Display.md => Display.md} | 104 ++++++++++----- kmk/extensions/{oled.py => display.py} | 168 +++++++++++++++++++----- 2 files changed, 212 insertions(+), 60 deletions(-) rename docs/en/{OLED_Display.md => Display.md} (64%) rename kmk/extensions/{oled.py => display.py} (69%) diff --git a/docs/en/OLED_Display.md b/docs/en/Display.md similarity index 64% rename from docs/en/OLED_Display.md rename to docs/en/Display.md index 266cc46fc..bd411b327 100644 --- a/docs/en/OLED_Display.md +++ b/docs/en/Display.md @@ -1,48 +1,90 @@ -# OLED Display +# Display Not enough screen space? Add a display to your keyboard! -This documentation concerns the recommended OLED extension. +This documentation concerns the recommended Display extension. *Note:* -Driving an OLED display can bind up a considerable amount of CPU time and RAM. +Driving a display can bind up a considerable amount of CPU time and RAM. Be aware of the performance degradation that can occur. ## Preparation -First of all you need to download a few libraries that will make it possible for your OLED to work. +First of all you need to download a few libraries that will make it possible for your display to work. You can get them with the [Adafruit CircuitPython Libraries bundle](https://circuitpython.org/libraries). Make sure you to choose the one that matches your version of CircuitPython. Create a `lib` directory under the CircuitPython drive and copy the following from the library bundle there: * `adafruit_display_text/` -* `adafruit_displayio_ssd1306.mpy` + +Depending on which kind of display your keyboard has, you may also need a display-specific library. See the below table: + +| Display Type | Library to use | +| ------------------------------------------------------------ | -------------------------------- | +| SSD1306 | `adafruit_displayio_ssd1306.mpy` | +| SH1106 | `adafruit_displayio_sh1106.mpy` | +| Already initialized (e.g. available through `board.DISPLAY`) | None | ## Configuration -Here's how you may initialize the extension: +Here's how you may initialize the extension. Note that this includes examples of all currently supported display types and you only need the one that corresponds to your display: ```python import board import busio -from kmk.extensions.oled import Oled, TextEntry, ImageEntry + +# For SSD1306 +from kmk.extensions.display import Display, SSD1306, TextEntry, ImageEntry # Replace SCL and SDA according to your hardware configuration. i2c_bus = busio.I2C(board.GP_SCL, board.GP_SDA) -oled = Oled( +driver = SSD1306( # Mandatory: i2c=i2c_bus, # Optional: device_address=0x3C, +) + +# For SH1106 +from kmk.extensions.display import Display, SH1106, TextEntry, ImageEntry + +# Replace SCK and MOSI according to your hardware configuration. +spi_bus = busio.SPI(board.GP_SCK, board.GP_MOSI) + +# Replace command, chip_select, and reset according to your hardware configuration. +driver = SH1106( + # Mandatory: + spi=spi_bus, + command=board.GP_DC, + chip_select=board.GP_CS, + reset=board.GP_RESET, +) + +# For displays initialized by CircuitPython by default +from kmk.extensions.display import Display, BuiltInDisplay, TextEntry, ImageEntry + +# Replace display, sleep_command, and wake_command according to your hardware configuration. +driver = BuiltInDisplay( + # Mandatory: + display=board.DISPLAY + sleep_command=0xAE + wake_command=0xAF +) + +# For all display types +display = Display( + # Mandatory: + display=driver, + # Optional: width=128, # screen size - height=64, # screen size + height=32, # screen size flip = False, # flips your display content flip_left = False, # flips your display content on left side split flip_right = False, # flips your display content on right side split - dim_time=10, # time in seconds to reduce screen brightness - dim_target=0.1, # set level for brightness decrease - off_time=0, # time in seconds to turn off screen - brightness=1, # initial screen brightness level + brightness=0.8, # initial screen brightness level brightness_step=0.1, # used for brightness increase/decrease keycodes + dim_time=20, # time in seconds to reduce screen brightness + dim_target=0.1, # set level for brightness decrease + off_time=60, # time in seconds to turn off screen powersave_dim_time=10, # time in seconds to reduce screen brightness powersave_dim_target=0.1, # set level for brightness decrease powersave_off_time=30, # time in seconds to turn off screen @@ -58,42 +100,42 @@ have to be placed in the root of the CircuitPython drive. **Placing it in separate a seperate directory may cause issues.** ```python -oled.entries = [ +display.entries = [ ImageEntry(image="1.bmp", x=0, y=0), ] -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` You can also make your images appear only on specific layers, ```python -oled.entries = [ +display.entries = [ ImageEntry(image="1.bmp", x=0, y=0, layer=0), ImageEntry(image="2.bmp", x=0, y=0, layer=1), ] -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` and/or side of your split keyboard. ```python -oled.entries = [ +display.entries = [ ImageEntry(image="L1.bmp", x=0, y=0, side="L"), ImageEntry(image="R1.bmp", x=0, y=0, side="R"), ] -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` ## Text You're able to freely positon your text to place it wherever you want just by changing x and y values. ```python -oled.entries = [ +display.entries = [ TextEntry(text="Layer = 1", x=0, y=0), TextEntry(text="Macros", x=0, y=12), TextEntry(text="Hey there!", x=0, y=24), ] -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` ### X and Y anchors @@ -106,31 +148,31 @@ For more infos about anchors check the [Adafruit docs](https://learn.adafruit.co Notable difference: KMK uses strings ("T", "M","B" and "L", "M", "R") instead of numbers. ```python -oled.entries = [ +display.entries = [ TextEntry(text="Layer = 1", x=128, y=0, x_anchor="R", y_anchor="T"), # text in Top Right corner TextEntry(text="Macros", x=128, y=64, x_anchor="R", y_anchor="B"), # text in Bottom Right corner TextEntry(text="Hey there!", x=64, y=32, x_anchor="M", y_anchor="M"), # text in the Middle of screen ] -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` ### Split Same as with images you can change displaying according to current layer or side of split keyboard. ```python -oled.entries = [ +display.entries = [ TextEntry(text="Longer text that", x=0, y=0, layer=0), TextEntry(text="has been divided", x=0, y=12, layer=0, side="L"), TextEntry(text="for an example", x=0, y=24, layer=0, side="R"), ] -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` ### Inverting Inverts colours of your text. Comes in handy, for example, as a good layer indicator. ```python -oled_ext = Oled( +display_ext = Display( entries=[ TextEntry(text='0 1 2 4', x=0, y=0), TextEntry(text='0', x=0, y=0, inverted=True, layer=0), @@ -148,14 +190,17 @@ from kmk.kmk_keyboard import KMKKeyboard from kmk.keys import KC from kmk.scanners import DiodeOrientation from kmk.modules.layers import Layers -from kmk.extensions.oled import Oled, TextEntry, ImageEntry +from kmk.extensions.display import Display, SSD1306, TextEntry, ImageEntry keyboard = KMKKeyboard() layers = Layers() keyboard.modules.append(layers) i2c_bus = busio.I2C(board.GP21, board.GP20) -oled = Oled( +display_driver = SSD1306(i2c=i2c_bus) + +display = Display( + display=display_driver entries=[ TextEntry(text='Layer: ', x=0, y=32, y_anchor='B'), TextEntry(text='BASE', x=40, y=32, y_anchor='B', layer=0), @@ -166,7 +211,6 @@ oled = Oled( TextEntry(text='1', x=12, y=4, inverted=True, layer=1), TextEntry(text='2', x=24, y=4, inverted=True, layer=2), ], - i2c=i2c_bus, device_address=0x3C, width=128, height=64, @@ -176,5 +220,5 @@ oled = Oled( brightness=1, ) -keyboard.extensions.append(oled) +keyboard.extensions.append(display) ``` diff --git a/kmk/extensions/oled.py b/kmk/extensions/display.py similarity index 69% rename from kmk/extensions/oled.py rename to kmk/extensions/display.py index eb672159e..46227a6d3 100644 --- a/kmk/extensions/oled.py +++ b/kmk/extensions/display.py @@ -1,7 +1,6 @@ import busio from supervisor import ticks_ms -import adafruit_displayio_ssd1306 import displayio import terminalio from adafruit_display_text import label @@ -13,8 +12,6 @@ from kmk.modules.split import Split, SplitSide from kmk.utils import clamp -displayio.release_displays() - class TextEntry: def __init__( @@ -77,13 +74,136 @@ def __init__(self, x=0, y=0, image='', layer=None, side=None): self.side = SplitSide.RIGHT -class Oled(Extension): +class DisplayBackend: + def __init__(self): + raise NotImplementedError + + def during_bootup(self, width, height, rotation): + raise NotImplementedError + + def deinit(self): + raise NotImplementedError + + def sleep(self): + self.display.sleep() + + def wake(self): + self.display.wake() + + @property + def brightness(self): + return self.display.brightness + + @brightness.setter + def brightness(self, new_brightness): + self.display.brightness = new_brightness + + # display.show() is deprecated, so use root_group instead + @property + def root_group(self): + return self.display.root_group + + @root_group.setter + def root_group(self, group): + self.display.root_group = group + + +# Intended for displays with drivers built into CircuitPython +# that can be used directly without manual initialization +class BuiltInDisplay(DisplayBackend): + def __init__(self, display=None, sleep_command=None, wake_command=None): + self.display = display + self.sleep_command = sleep_command + self.wake_command = wake_command + self.is_awake = True + + def during_bootup(self, width, height, rotation): + self.display.rotation = rotation + return self.display + + def deinit(self): + return + + def sleep(self): + self.display.bus.send(self.sleep_command, b'') + + def wake(self): + self.display.bus.send(self.wake_command, b'') + + +class SSD1306(DisplayBackend): + def __init__(self, i2c=None, sda=None, scl=None, device_address=0x3C): + displayio.release_displays() + self.device_address = device_address + # i2c initialization + self.i2c = i2c + if self.i2c is None: + self.i2c = busio.I2C(scl, sda) + + def during_bootup(self, width, height, rotation): + import adafruit_displayio_ssd1306 + + self.display = adafruit_displayio_ssd1306.SSD1306( + displayio.I2CDisplay(self.i2c, device_address=self.device_address), + width=width, + height=height, + rotation=rotation, + brightness=self.brightness, + ) + + return self.display + + def deinit(self): + self.i2c.deinit() + + +class SH1106(DisplayBackend): + def __init__( + self, + spi=None, + sck=None, + mosi=None, + command=None, + chip_select=None, + reset=None, + baudrate=1000000, + ): + displayio.release_displays() + self.command = command + self.chip_select = chip_select + self.reset = reset + self.baudrate = baudrate + # spi initialization + self.spi = spi + if self.spi is None: + self.spi = busio.SPI(sck, mosi) + + def during_bootup(self, width, height, rotation): + import adafruit_displayio_sh1106 + + self.display = adafruit_displayio_sh1106.SH1106( + displayio.FourWire( + self.spi, + command=self.command, + chip_select=self.chip_select, + reset=self.reset, + baudrate=self.baudrate, + ), + width=width, + height=height, + rotation=rotation, + ) + + return self.display + + def deinit(self): + self.spi.deinit() + + +class Display(Extension): def __init__( self, - i2c=None, - sda=None, - scl=None, - device_address=0x3C, + display=None, entries=[], width=128, height=32, @@ -99,7 +219,7 @@ def __init__( powersave_dim_target=0.1, powersave_off_time=30, ): - self.device_address = device_address + self.display = display self.flip = flip self.flip_left = flip_left self.flip_right = flip_right @@ -119,19 +239,15 @@ def __init__( self.powersave_off_time_ms = powersave_off_time * 1000 self.dim_period = PeriodicTimer(50) self.split_side = None - # i2c initialization - self.i2c = i2c - if self.i2c is None: - self.i2c = busio.I2C(scl, sda) make_key( - names=('OLED_BRI',), - on_press=self.oled_brightness_increase, + names=('DIS_BRI',), + on_press=self.display_brightness_increase, on_release=handler_passthrough, ) make_key( - names=('OLED_BRD',), - on_press=self.oled_brightness_decrease, + names=('DIS_BRD',), + on_press=self.display_brightness_decrease, on_release=handler_passthrough, ) @@ -164,7 +280,7 @@ def render(self, layer): y=entry.y, ) ) - self.display.show(splash) + self.display.root_group = splash def on_runtime_enable(self, sandbox): return @@ -173,7 +289,6 @@ def on_runtime_disable(self, sandbox): return def during_bootup(self, keyboard): - for module in keyboard.modules: if isinstance(module, Split): self.split_side = module.split_side @@ -187,13 +302,7 @@ def during_bootup(self, keyboard): if entry.side != self.split_side and entry.side is not None: del self.entries[idx] - self.display = adafruit_displayio_ssd1306.SSD1306( - displayio.I2CDisplay(self.i2c, device_address=self.device_address), - width=self.width, - height=self.height, - rotation=180 if self.flip else 0, - brightness=self.brightness, - ) + self.display.during_bootup(self.width, self.height, 180 if self.flip else 0) def before_matrix_scan(self, sandbox): if self.dim_period.tick(): @@ -220,15 +329,15 @@ def on_powersave_disable(self, sandbox): def deinit(self, sandbox): displayio.release_displays() - self.i2c.deinit() + self.display.deinit() - def oled_brightness_increase(self): + def display_brightness_increase(self, *args): self.display.brightness = clamp( self.display.brightness + self.brightness_step, 0, 1 ) self.brightness = self.display.brightness # Save current brightness - def oled_brightness_decrease(self): + def display_brightness_decrease(self, *args): self.display.brightness = clamp( self.display.brightness - self.brightness_step, 0, 1 ) @@ -236,7 +345,6 @@ def oled_brightness_decrease(self): def dim(self): if self.powersave: - if ( self.powersave_off_time_ms and ticks_diff(ticks_ms(), self.timer_start)