Last active
August 13, 2021 17:02
-
-
Save rpavlik/73d1de102038f85de051fc1b8cdd3009 to your computer and use it in GitHub Desktop.
Adafruit MacroPad with my mods
This file contains 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
# 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 |
This file contains 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
# 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 |
This file contains 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
""" | |
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