Skip to content

Instantly share code, notes, and snippets.

@rpavlik
Last active August 13, 2021 17:02
Show Gist options
  • Save rpavlik/73d1de102038f85de051fc1b8cdd3009 to your computer and use it in GitHub Desktop.
Save rpavlik/73d1de102038f85de051fc1b8cdd3009 to your computer and use it in GitHub Desktop.
Adafruit MacroPad with my mods
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2020 Mark Roberts for Adafruit Industries
# SPDX-FileCopyrightText: 2021 James Carr
# SPDX-FileCopyrightText: 2021 Ryan Pavlik
#
# SPDX-License-Identifier: MIT
"""
`adafruit_displayio_sh1107_wrapper`
================================================================================
Add features to a built-in DisplayIO driver for SH1107 monochrome displays.
Based on the `adafruit_displayio_sh1107` library with all the stuff built-in to
board libraries removed.
* Author(s): Scott Shawcroft, Mark Roberts (mdroberts1243), James Carr, Ryan Pavlik
Implementation Notes
--------------------
**Hardware:**
* `Adafruit MacroPad RP2040`_
**Software and Dependencies:**
* Adafruit CircuitPython (version 7+) firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
class SH1107_Wrapper:
"""
Wrapper for built-in DisplayIO SSD1107 driver
"""
def __init__(
self,
display
):
self.display = display
self.bus = display.bus
self._is_awake = True # Display starts in active state (_INIT_SEQUENCE)
@property
def is_awake(self):
"""
The power state of the display. (read-only)
True if the display is active, False if in sleep mode.
"""
return self._is_awake
def sleep(self):
"""
Put display into sleep mode
The display uses < 5uA in sleep mode
Sleep mode does the following:
1) Stops the oscillator and DC-DC circuits
2) Stops the OLED drive
3) Remembers display data and operation mode active prior to sleeping
4) The MP can access (update) the built-in display RAM
"""
if self._is_awake:
self.bus.send(int(0xAE), "") # 0xAE = display off, sleep mode
self._is_awake = False
def wake(self):
"""
Wake display from sleep mode
"""
if not self._is_awake:
self.bus.send(int(0xAF), "") # 0xAF = display on
self._is_awake = True
# SPDX-FileCopyrightText: 2020-2021, Ryan Pavlik <ryan.pavlik@gmail.com>
# SPDX-License-Identifier: MIT OR Unlicense
import time
try:
from typing import Optional
except ImportError:
pass
class AutoOffScreen:
def __init__(self, duration=60 * 15, initial_duration=10) -> None:
self.turn_off = None # type: Optional[int]
self.duration = duration
self.set_turn_off(time.monotonic() + initial_duration)
def set_turn_off(self, off_time):
self.on = True
if self.turn_off:
self.turn_off = max(self.turn_off, off_time)
else:
self.turn_off = off_time
def update_active(self):
"""turn on/push out turn-off time"""
self.set_turn_off(time.monotonic() + self.duration)
def poll(self) -> bool:
now = time.monotonic()
if self.on and self.turn_off is not None and now >= self.turn_off:
self.on = False
self.turn_off = None
return self.on
"""
A fairly straightforward macro/hotkey program for Adafruit MACROPAD.
Macro key setups are stored in the /macros folder (configurable below),
load up just the ones you're likely to use. Plug into computer's USB port,
use dial to select an application macro set, press MACROPAD keys to send
key sequences.
"""
# pylint: disable=import-error, unused-import, too-few-public-methods
import os
import displayio
import terminalio
import board
from adafruit_display_shapes.rect import Rect
from adafruit_display_text import label
from adafruit_macropad import MacroPad
from adafruit_bitmap_font import bitmap_font
from autoscreen import AutoOffScreen
from adafruit_displayio_sh1107_wrapper import SH1107_Wrapper
# CONFIGURABLES ------------------------
MACRO_FOLDER = '/macros'
# FONT = terminalio.FONT
# FONT = bitmap_font.load_font("ter-u12n.pcf") # bigger but maybe faster to load?
# FONT = bitmap_font.load_font("ter-u12n.bdf")
# FONT = bitmap_font.load_font("TerminusPlusMedium-12.bdf")
FONT = bitmap_font.load_font("ter-u12n-more.bdf")
# CLASSES AND FUNCTIONS ----------------
class App:
""" Class representing a host-side application, for which we have a set
of macro sequences. Project code was originally more complex and
this was helpful, but maybe it's excessive now?"""
def __init__(self, appdata):
self.name = appdata['name']
self.macros = appdata['macros']
def set_pixels(self):
for i in range(12):
if i < len(self.macros): # Key in use, set label + LED color
macropad.pixels[i] = self.macros[i][0]
else: # Key not in use, no label or LED
macropad.pixels[i] = 0
def switch(self):
""" Activate application settings; update OLED labels and LED
colors. """
macropad.red_led = True
self.set_pixels()
group[13].text = self.name # Application name
for i in range(12):
if i < len(self.macros): # Key in use, set label + LED color
group[i].text = self.macros[i][1]
else: # Key not in use, no label or LED
group[i].text = ''
macropad.keyboard.release_all()
macropad.pixels.show()
macropad.display.refresh()
macropad.red_led = False
# INITIALIZATION -----------------------
macropad = MacroPad()
macropad.display.auto_refresh = False
macropad.pixels.auto_write = False
# Set up timeout
autoscreen = AutoOffScreen(15 * 60)
# Use a mangled copy of the SH1107 python driver to add sleep ability to display.
display_sleeper = SH1107_Wrapper(macropad.display)
# Set up displayio group with all the labels
group = displayio.Group()
for key_index in range(12):
x = key_index % 3
y = key_index // 3
group.append(label.Label(FONT, text='', color=0xFFFFFF,
anchored_position=((macropad.display.width - 1) * x / 2,
macropad.display.height - 1 -
(3 - y) * 12),
anchor_point=(x / 2, 1.0)))
group.append(Rect(0, 0, macropad.display.width, 12, fill=0xFFFFFF))
group.append(label.Label(terminalio.FONT, text='', color=0x000000,
anchored_position=(macropad.display.width//2, -2),
anchor_point=(0.5, 0.0)))
macropad.display.show(group)
# Load all the macro key setups from .py files in MACRO_FOLDER
apps = []
files = os.listdir(MACRO_FOLDER)
files.sort()
for filename in files:
if filename.endswith('.py'):
try:
module = __import__(MACRO_FOLDER + '/' + filename[:-3])
apps.append(App(module.app))
except (SyntaxError, ImportError, AttributeError, KeyError, NameError,
IndexError, TypeError) as err:
pass
if not apps:
group[13].text = 'NO MACRO FILES FOUND'
macropad.display.refresh()
while True:
pass
last_position = None
last_encoder_switch = macropad.encoder_switch_debounced.pressed
app_index = 0
apps[app_index].switch()
# This actually just turns down contrast, does not reduce brightness to zero
macropad.display.brightness = 0
# MAIN LOOP ----------------------------
last_light_status = True
while True:
lights_on = autoscreen.poll()
if lights_on != last_light_status:
if lights_on:
print("lights on!")
display_sleeper.wake()
# triggers re-setting the neopixels
macropad.pixels.brightness = 1
apps[app_index].set_pixels()
else:
print("lights out")
display_sleeper.sleep()
macropad.pixels.show()
macropad.pixels.brightness = 0
last_light_status = lights_on
# Read encoder position. If it's changed, switch apps.
position = macropad.encoder
if position != last_position:
autoscreen.update_active()
app_index = position % len(apps)
apps[app_index].switch()
last_position = position
# Handle encoder button. If state has changed, and if there's a
# corresponding macro, set up variables to act on this just like
# the keypad keys, as if it were a 13th key/macro.
macropad.encoder_switch_debounced.update()
encoder_switch = macropad.encoder_switch_debounced.pressed
if encoder_switch != last_encoder_switch:
autoscreen.update_active()
last_encoder_switch = encoder_switch
if len(apps[app_index].macros) < 13:
continue # No 13th macro, just resume main loop
key_number = 12 # else process below as 13th macro
pressed = encoder_switch
else:
event = macropad.keys.events.get()
if event:
autoscreen.update_active()
if not event or event.key_number >= len(apps[app_index].macros):
continue # No key events, or no corresponding macro, resume loop
key_number = event.key_number
pressed = event.pressed
# If code reaches here, a key or the encoder button WAS pressed/released
# and there IS a corresponding macro available for it...other situations
# are avoided by 'continue' statements above which resume the loop.
sequence = apps[app_index].macros[key_number][2]
if pressed:
if key_number < 12: # No pixel for encoder button
macropad.pixels[key_number] = 0xFFFFFF
macropad.pixels.show()
for item in sequence:
if isinstance(item, int):
if item >= 0:
macropad.keyboard.press(item)
else:
macropad.keyboard.release(-item)
else:
macropad.keyboard_layout.write(item)
else:
# Release any still-pressed modifier keys
for item in sequence:
if isinstance(item, int) and item >= 0:
macropad.keyboard.release(item)
if key_number < 12: # No pixel for encoder button
macropad.pixels[key_number] = apps[app_index].macros[key_number][0]
macropad.pixels.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment